POC: Schema IR — library-agnostic core with Valibot + Zod adapters - #197
POC: Schema IR — library-agnostic core with Valibot + Zod adapters#197espetro wants to merge 8 commits into
Conversation
WalkthroughThis change adds a Formisch schema intermediate representation with Valibot and Zod adapters. Core field initialization and form-data decoding now consume IR metadata. Form parsing, validation issues, and submit output use Standard Schema contracts. React Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 17
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/core/src/field/setFieldInput/setFieldInput.test.ts (1)
133-140: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftKeep tuple fixtures out of the unsupported IR path.
toFormisch(v.tuple(...))now routes both tuple cases throughFormischFieldIR. The supplied IR haspropertiesand oneitem, but no tuple-element list.packages/core/src/field/initializeFieldStore/initializeFieldStore.ts:11-123initializes array children throughir.item!. These tests can fail during store creation or lose positional schemas.Implement tuple IR and adapter support before enabling these fixtures, or remove them from this POC test path. The PR objectives list tuples as unresolved.
Also applies to: 149-164
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/field/setFieldInput/setFieldInput.test.ts` around lines 133 - 140, Remove the tuple-based fixtures and assertions from this POC test path, including the cases around pairStore and tuple input handling. Do not route v.tuple(...) through toFormisch/FormischFieldIR until tuple-element schemas and initialization support are implemented; keep tests limited to supported IR shapes.packages/core/src/form/decodeFormData/decodeFormData.test.ts (1)
571-590: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winHandle recursive
v.lazyschemasWhen a
v.lazygetter returns a schema that contains the same lazy schema,transformexpands children indefinitely andtoFormischoverflows the call stack. Add a recursive-schema test. Support recursion or reject it with a clear error and document the limitation.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/form/decodeFormData/decodeFormData.test.ts` around lines 571 - 590, Extend the decodeFormData/toFormisch coverage with a recursive v.lazy schema whose getter references the same lazy schema, then update the relevant schema transformation logic to detect recursive expansion and either support it safely or reject it with a clear, documented error instead of overflowing the call stack.
🟡 Minor comments (8)
SCHEMA_IR_POC.md-77-77 (1)
77-77: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAdd a language tag to the diagram fence.
The fence at Line 77 has no language identifier.
markdownlint-cli2reports MD040. Mark this block astextso the documentation passes Markdown lint.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@SCHEMA_IR_POC.md` at line 77, Update the diagram code fence in SCHEMA_IR_POC.md around the documented block to include the text language tag, changing the untagged fence to a text fence so it satisfies Markdown lint rule MD040.Source: Linters/SAST tools
SCHEMA_IR_POC.md-130-138 (1)
130-138: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMake each alpha publish command independent.
After Line 136, the shell remains in
packages/core. The paths on Lines 137-138 then resolve relative topackages/core, so the adapter publishes fail. Use independent subshells orpnpm -C.Proposed fix
-cd packages/core && npm publish --tag alpha -cd packages/adapters/valibot && npm publish --tag alpha -cd packages/adapters/zod && npm publish --tag alpha +(cd packages/core && npm publish --tag alpha) +(cd packages/adapters/valibot && npm publish --tag alpha) +(cd packages/adapters/zod && npm publish --tag alpha)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@SCHEMA_IR_POC.md` around lines 130 - 138, Update the alpha publish commands in the publishing instructions so each command runs from its intended package directory independently. Replace the persistent `cd` flow after the core publish with subshells or equivalent `pnpm -C` invocations for `packages/core`, `packages/adapters/valibot`, and `packages/adapters/zod`.playgrounds/ir-demo/package.json-21-26 (1)
21-26: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRemove the unused Node.js type configuration.
playgrounds/ir-demo/tsconfig.jsonsets"types": ["node"], but the package and lockfile do not declare@types/node, and the source uses no Node.js globals. Remove"node"fromcompilerOptions.types, or declare@types/nodeif Node.js globals are required.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@playgrounds/ir-demo/package.json` around lines 21 - 26, Remove the unused "node" entry from compilerOptions.types in the playground’s tsconfig.json, keeping the existing type configuration otherwise unchanged; do not add `@types/node` because the source does not require Node.js globals.packages/adapters/valibot/package.json-40-51 (1)
40-51: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winAdd
@formisch/coreas a peer dependency.tsdownpreserves imports from external packages in generated declarations, so consumers need@formisch/coreinstalled to resolve the adapter's public types.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/adapters/valibot/package.json` around lines 40 - 51, Add `@formisch/core` to the peerDependencies alongside valibot in packages/adapters/valibot/package.json, while retaining its existing devDependency entry for workspace development and type resolution.packages/core/src/types/schema/schema.test-d.ts-15-26 (1)
15-26: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse
satisfies FormSchemainstead ofas FormSchema. The assertion bypasses structural checking of the positive fixture, so an invalid Standard Schema contract can pass this type test.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/types/schema/schema.test-d.ts` around lines 15 - 26, Update makeFormSchema to use the satisfies FormSchema constraint instead of an as FormSchema assertion, preserving the fixture while enabling structural validation of its ~standard and ~formisch contracts.packages/adapters/valibot/src/transform.ts-40-47 (1)
40-47: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winPreserve optionality through lazy schemas.
v.lazy(() => v.optional(v.string()))producesoptional: falsebecauseisOptionalchecks only the outerlazyschema. Derive optionality from the schema reached during unwrapping so initialization preserves the optional field state.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/adapters/valibot/src/transform.ts` around lines 40 - 47, Update isOptional to unwrap lazy schemas and evaluate the resolved inner schema’s type, so v.lazy(() => v.optional(...)) preserves optionality during initialization. Retain the existing optional, nullable, nullish, exact_optional, and undefinedable checks for non-lazy schemas.packages/core/src/form/createFormStore/createFormStore.ts-13-17 (1)
13-17: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAdd JSDoc for the exported factory.
Document the
configandparseparameters, theStandardParseResultcontract, and the returned store. Place the JSDoc above//@NO_SIDE_EFFECTS`` so the marker remains immediately beforecreateFormStore.As per coding guidelines,
**/*.{ts,tsx}files must add JSDoc to exported functions.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/form/createFormStore/createFormStore.ts` around lines 13 - 17, Add JSDoc above the // `@__NO_SIDE_EFFECTS__` marker for the exported createFormStore factory, documenting the config and parse parameters, the expected StandardParseResult contract, and the returned InternalFormStore. Keep the marker immediately adjacent to createFormStore.Source: Coding guidelines
packages/core/src/form/parity.test.ts-1-7 (1)
1-7: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winBreak the test-only workspace dependency cycle.
@formisch/coredepends on both adapters for tests, while both adapters depend on@formisch/corefor types. Move the adapter parity tests out ofpackages/core/src/form/parity.test.ts. Keep thedecodeFormDatatests in the core form suite.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/form/parity.test.ts` around lines 1 - 7, Move the adapter parity tests from the file containing toFormischValibot and toFormischZod into an adapter-owned test location, removing the core test-only dependency on both adapters. Keep the decodeFormData tests in the core form suite, preserving their existing coverage and imports without adapter references.Source: Coding guidelines
🧹 Nitpick comments (6)
playgrounds/ir-demo/src/main.tsx (1)
26-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDefine the
FormDemoprops with an interface.The function uses an inline object type for its props. Define a named
FormDemoPropsinterface and use it in the function signature.As per coding guidelines:
Prefer interface over type for defining object shapes.Proposed refactor
-function FormDemo({ schema, label }: { schema: FormSchema; label: string }) { +interface FormDemoProps { + schema: FormSchema; + label: string; +} + +function FormDemo({ schema, label }: FormDemoProps) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@playgrounds/ir-demo/src/main.tsx` at line 26, Define a named FormDemoProps interface containing the schema and label fields currently declared inline, then update FormDemo to use FormDemoProps for its parameter type while preserving the existing field types and behavior.Source: Coding guidelines
packages/core/src/form/createFormStore/createFormStore.ts (1)
14-18: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winPreserve
TSchemain the factory signature.The current signature erases
InferStandardOutput<TSchema>by accepting a non-genericFormConfigand returning a non-genericInternalFormStore.packages/core/src/vitest/utils.ts:162-185must cast the result back to the concrete schema, which hides type mismatches.Make
createFormStoregeneric overTSchema, useFormConfig<TSchema>, returnInternalFormStore<TSchema>, and typeparsewithStandardParseResult<InferStandardOutput<TSchema>>.Proposed type-preserving signature
-export function createFormStore( - config: FormConfig, - parse: (input: unknown) => Promise<StandardParseResult> -): InternalFormStore { - const store: Partial<InternalFormStore> = {}; +export function createFormStore<TSchema extends FormSchema>( + config: FormConfig<TSchema>, + parse: ( + input: unknown + ) => Promise<StandardParseResult<InferStandardOutput<TSchema>>> +): InternalFormStore<TSchema> { + const store: Partial<InternalFormStore<TSchema>> = {};Also applies to: 24-24
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/form/createFormStore/createFormStore.ts` around lines 14 - 18, Make createFormStore generic over TSchema and preserve the schema type through its signature: accept FormConfig<TSchema>, return InternalFormStore<TSchema>, and type parse as returning Promise<StandardParseResult<InferStandardOutput<TSchema>>>. Update the store construction and related local typing as needed so the generic type remains intact without requiring callers such as vitest utilities to cast the result.packages/core/src/form/validateFormInput/validateFormInput.ts (1)
13-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSimplify the return type; it is a nested promise.
InternalFormStore['parse']already returnsPromise<StandardParseResult<...>>.Promise<ReturnType<InternalFormStore['parse']>>therefore declaresPromise<Promise<StandardParseResult<...>>>.Awaitedunwraps this for callers, so it compiles, but the declared type does not describe the value.♻️ Proposed type simplification
export async function validateFormInput( internalFormStore: InternalFormStore, config?: ValidateFormInputConfig -): Promise<ReturnType<InternalFormStore['parse']>> { +): ReturnType<InternalFormStore['parse']> {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/form/validateFormInput/validateFormInput.ts` around lines 13 - 16, Update the return type of validateFormInput to use Awaited<ReturnType<InternalFormStore['parse']>> instead of wrapping ReturnType in another Promise, so the declaration represents the resolved parse result rather than a nested promise.packages/core/src/form/parity.test.ts (2)
47-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd parity coverage for
nullableandnullish.
FormischFieldIR.optionalis documented to collapseoptional,nullable,nullish, andnon_optionalinto one boolean. The tests cover onlyoptional. Add cases forv.nullableversus.nullable()andv.nullishversus.nullish(). Those wrappers are the ones most likely to diverge between the two adapters.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/form/parity.test.ts` around lines 47 - 68, Extend the parity coverage in the existing “object with optional fields” test area to compare Valibot and Zod schemas using nullable and nullish wrappers: use v.nullable versus z.nullable, and v.nullish versus z.nullish, with matching field shapes. Assert each converted IR pair with stripGetDefault and toStrictEqual, following the existing optional parity pattern.
13-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCompare
getDefault()results instead of discarding the function.
stripGetDefaultdropsgetDefaultfrom every node, so the parity tests never verify that both adapters resolve the same default value. Valibot and Zod express defaults differently, which makes this the most likely place for the two adapters to diverge. InvokegetDefault()and include its result in the comparison object.♻️ Proposed change to include resolved defaults in the parity comparison
function stripGetDefault(ir: FormischFieldIR): Record<string, unknown> { const result: Record<string, unknown> = { type: ir.type, optional: ir.optional, + default: ir.getDefault(), };Rename the helper to reflect the new behavior if you apply this change.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/form/parity.test.ts` around lines 13 - 29, Update stripGetDefault to invoke each FormischFieldIR node’s getDefault() and include the resolved value in the returned comparison object, while preserving the recursive properties and item traversal. Rename the helper to reflect that it now includes defaults, and update all parity-test call sites accordingly.packages/core/src/form/decodeFormData/decodeFormData.test.ts (1)
783-788: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueNarrow the result instead of casting to
any.
awaitalready normalizes a synchronous return, soPromise.resolve(...)is redundant. Replacing theas anycast with an explicit narrowing check keeps the discriminated union intact and removes the eslint suppression.♻️ Proposed test refactor
- const validateResult = await Promise.resolve( - schema['~standard'].validate(decodeFormData(schema, formData)) - ); - expect(validateResult.issues).toBeUndefined(); - // eslint-disable-next-line `@typescript-eslint/no-explicit-any` - expect((validateResult as any).value).toStrictEqual({ + const validateResult = await schema['~standard'].validate( + decodeFormData(schema, formData) + ); + if (validateResult.issues) { + throw new Error('Validation unexpectedly failed'); + } + expect(validateResult.value).toStrictEqual({🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/form/decodeFormData/decodeFormData.test.ts` around lines 783 - 788, Update the validation test around schema['~standard'].validate by removing the redundant Promise.resolve wrapper and narrowing validateResult through an explicit check that confirms no issues are present before accessing its value. Replace the any cast and eslint suppression while preserving the existing value assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frameworks/react/src/hooks/useForm/useForm.ts`:
- Around line 32-38: Update handleSubmit to follow the StandardParseResult
contract returned by createFormStore: check result.issues for validation
failures and pass result.value to onSubmit for valid submissions, replacing the
unsupported result.success and result.output properties.
In `@packages/adapters/valibot/src/toFormisch.ts`:
- Around line 15-20: Update toFormisch to reject non-object root schemas before
constructing or casting the FormSchema; validate that the transformed root has
type 'object' and throw for leaf or array schemas, while preserving the existing
output for valid object schemas.
In `@packages/adapters/valibot/src/transform.ts`:
- Around line 23-24: Update the lazy-schema handling in unwrapValibot so active
recursive schemas are detected before recursively processing
schema.getter(undefined), preventing unbounded recursion; either throw a clear
recursion error or emit the core-supported recursive IR. Add a regression test
covering a lazy getter whose returned object contains the same lazy schema and
verify toFormisch no longer overflows the call stack.
- Around line 108-115: Update the enum/picklist/literal branch in the transform
function to infer the IR type from unwrapped.literal or unwrapped.options
instead of always returning string. Preserve numeric, boolean, and bigint
primitive types, and return unknown when the schema contains mixed primitive
types; keep optional and getDefault behavior unchanged.
- Around line 117-130: The union, variant, and intersect handling in transform
must not resolve schemas to options[0]. Traverse and combine all representable
options so later branches retain their correct types during FormData decoding;
if the combinator cannot be represented, throw an explicit unsupported-schema
error instead of returning an incomplete IR.
In `@packages/adapters/zod/package.json`:
- Around line 39-41: Add `@formisch/core` to the peerDependencies object in
packages/adapters/zod/package.json, while preserving its existing
devDependencies entry so adapter consumers can resolve the types used by
transform and toFormisch.
In `@packages/adapters/zod/src/transform.ts`:
- Line 22: Add the `// `@__NO_SIDE_EFFECTS__`` annotation immediately before the
`transform` function declaration, preserving the existing signature and
implementation.
- Around line 101-108: The Zod transform currently labels every ZodEnum,
ZodLiteral, and ZodNativeEnum as string. Update the transform logic in
packages/adapters/zod/src/transform.ts lines 101-108 to infer matching IR types
for homogeneous string, number, boolean, and bigint domains, while returning
unknown for nullish, mixed, or unsupported values. Extend
packages/adapters/zod/tests/transform.test.ts lines 33-41 with numeric, boolean,
and bigint literal cases plus numeric and mixed native-enum cases.
In `@packages/core/package.json`:
- Line 79: Update the dependency declarations in packages/core/package.json so
`@standard-schema/spec` is available to consumers of the exported StandardSchemaV1
types: move it from devDependencies to dependencies, or declare it as a peer
dependency with a supported version range, ensuring it is not listed only as a
development dependency.
In `@packages/core/src/field/initializeFieldStore/initializeFieldStore.ts`:
- Around line 11-18: ・Add JSDoc immediately above the exported
initializeFieldStore function documenting its initialization contract and the
ir, initialInput, path, and nullish parameters, including the meaning of the
nullish flag. Keep the existing signature and implementation unchanged.
- Around line 52-59: Pass ir.item!.optional as the nullish/optionality argument
when initializing array children in initializeFieldStore, and apply the same
item-optionality propagation in copyItemState, resetItemState, swapItemState,
setFieldInput, and setInitialFieldInput. Update
packages/core/src/field/initializeFieldStore/initializeFieldStore.ts:52-59 and
packages/core/src/array/copyItemState/copyItemState.ts:76-82 directly; also
update
packages/core/src/field/setInitialFieldInput/setInitialFieldInput.ts:55-62, with
corresponding changes in the resetItemState, swapItemState, and setFieldInput
sites. Preserve undefined optional items as undefined rather than replacing them
with emptyInput.
In `@packages/core/src/form/decodeFormData/decodeFormData.ts`:
- Around line 8-16: Extend the FormischFieldIR contract used by getChildIR in
packages/core/src/form/decodeFormData/decodeFormData.ts:8-16 to represent tuple
positions, union options, variant branches, and intersect members, or explicitly
document those shapes as unsupported. In
packages/core/src/form/decodeFormData/decodeFormData.ts:66-73, guard array
default filling with ir.item before accessing it. In
packages/core/src/form/decodeFormData/decodeFormData.test.ts:377-509, skip
tuple, union, variant, and intersect cases with TODOs. In
packages/core/src/field/setInitialFieldInput/setInitialFieldInput.test.ts:92-108,
verify how initializeFieldStore determines tuple child counts and add IR
positional metadata or skip the test consistently.
- Around line 66-73: Update the array branch in fillDefaults to remove the
non-null assertion on ir.item and only recurse when the item IR is defined;
otherwise preserve the existing parent[key] array initialization and avoid
calling fillDefaults with undefined.
In `@packages/core/src/form/validateFormInput/validateFormInput.ts`:
- Around line 30-44: Update the issue-handling logic in validateFormInput so an
empty issue.path is treated as a root-level error rather than stored under
nestedErrors['[]']; use issue.path?.length to distinguish non-empty nested paths
from root issues. Add a regression test covering a top-level refinement issue
with path [] and verify the message is returned through rootErrors.
In `@packages/core/src/types/form/form.ts`:
- Around line 91-100: Update StandardParseResult to use
StandardSchemaV1.Result<T>, or an equivalent discriminated union, so successful
results require a value and omit issues while failed results require a non-empty
issues array and omit value. Preserve the discriminant consumed by
validateFormInput and prevent empty or missing-field combinations from being
representable.
In `@packages/core/src/vitest/utils.ts`:
- Around line 84-89: Update transformValibot to preserve compound-schema
semantics: represent tuple schemas as arrays, retain record and object_with_rest
schemas so unsupported-schema errors remain observable, and combine all union,
variant, and intersect options rather than selecting only the first. If these
schemas remain outside the POC scope, explicitly skip or remove their affected
tests instead of mapping them to unknown.
In `@playgrounds/ir-demo/src/main.tsx`:
- Around line 26-44: Update FormDemo to render the form through the public Form
and Field APIs: wrap the fields in Form, bind name, age, and email with Field,
render each field’s input and errors, and add a submit control so onSubmit is
user-triggered. Remove the __name cast/display and use the field render APIs
instead.
---
Outside diff comments:
In `@packages/core/src/field/setFieldInput/setFieldInput.test.ts`:
- Around line 133-140: Remove the tuple-based fixtures and assertions from this
POC test path, including the cases around pairStore and tuple input handling. Do
not route v.tuple(...) through toFormisch/FormischFieldIR until tuple-element
schemas and initialization support are implemented; keep tests limited to
supported IR shapes.
In `@packages/core/src/form/decodeFormData/decodeFormData.test.ts`:
- Around line 571-590: Extend the decodeFormData/toFormisch coverage with a
recursive v.lazy schema whose getter references the same lazy schema, then
update the relevant schema transformation logic to detect recursive expansion
and either support it safely or reject it with a clear, documented error instead
of overflowing the call stack.
---
Minor comments:
In `@packages/adapters/valibot/package.json`:
- Around line 40-51: Add `@formisch/core` to the peerDependencies alongside
valibot in packages/adapters/valibot/package.json, while retaining its existing
devDependency entry for workspace development and type resolution.
In `@packages/adapters/valibot/src/transform.ts`:
- Around line 40-47: Update isOptional to unwrap lazy schemas and evaluate the
resolved inner schema’s type, so v.lazy(() => v.optional(...)) preserves
optionality during initialization. Retain the existing optional, nullable,
nullish, exact_optional, and undefinedable checks for non-lazy schemas.
In `@packages/core/src/form/createFormStore/createFormStore.ts`:
- Around line 13-17: Add JSDoc above the // `@__NO_SIDE_EFFECTS__` marker for the
exported createFormStore factory, documenting the config and parse parameters,
the expected StandardParseResult contract, and the returned InternalFormStore.
Keep the marker immediately adjacent to createFormStore.
In `@packages/core/src/form/parity.test.ts`:
- Around line 1-7: Move the adapter parity tests from the file containing
toFormischValibot and toFormischZod into an adapter-owned test location,
removing the core test-only dependency on both adapters. Keep the decodeFormData
tests in the core form suite, preserving their existing coverage and imports
without adapter references.
In `@packages/core/src/types/schema/schema.test-d.ts`:
- Around line 15-26: Update makeFormSchema to use the satisfies FormSchema
constraint instead of an as FormSchema assertion, preserving the fixture while
enabling structural validation of its ~standard and ~formisch contracts.
In `@playgrounds/ir-demo/package.json`:
- Around line 21-26: Remove the unused "node" entry from compilerOptions.types
in the playground’s tsconfig.json, keeping the existing type configuration
otherwise unchanged; do not add `@types/node` because the source does not require
Node.js globals.
In `@SCHEMA_IR_POC.md`:
- Line 77: Update the diagram code fence in SCHEMA_IR_POC.md around the
documented block to include the text language tag, changing the untagged fence
to a text fence so it satisfies Markdown lint rule MD040.
- Around line 130-138: Update the alpha publish commands in the publishing
instructions so each command runs from its intended package directory
independently. Replace the persistent `cd` flow after the core publish with
subshells or equivalent `pnpm -C` invocations for `packages/core`,
`packages/adapters/valibot`, and `packages/adapters/zod`.
---
Nitpick comments:
In `@packages/core/src/form/createFormStore/createFormStore.ts`:
- Around line 14-18: Make createFormStore generic over TSchema and preserve the
schema type through its signature: accept FormConfig<TSchema>, return
InternalFormStore<TSchema>, and type parse as returning
Promise<StandardParseResult<InferStandardOutput<TSchema>>>. Update the store
construction and related local typing as needed so the generic type remains
intact without requiring callers such as vitest utilities to cast the result.
In `@packages/core/src/form/decodeFormData/decodeFormData.test.ts`:
- Around line 783-788: Update the validation test around
schema['~standard'].validate by removing the redundant Promise.resolve wrapper
and narrowing validateResult through an explicit check that confirms no issues
are present before accessing its value. Replace the any cast and eslint
suppression while preserving the existing value assertion.
In `@packages/core/src/form/parity.test.ts`:
- Around line 47-68: Extend the parity coverage in the existing “object with
optional fields” test area to compare Valibot and Zod schemas using nullable and
nullish wrappers: use v.nullable versus z.nullable, and v.nullish versus
z.nullish, with matching field shapes. Assert each converted IR pair with
stripGetDefault and toStrictEqual, following the existing optional parity
pattern.
- Around line 13-29: Update stripGetDefault to invoke each FormischFieldIR
node’s getDefault() and include the resolved value in the returned comparison
object, while preserving the recursive properties and item traversal. Rename the
helper to reflect that it now includes defaults, and update all parity-test call
sites accordingly.
In `@packages/core/src/form/validateFormInput/validateFormInput.ts`:
- Around line 13-16: Update the return type of validateFormInput to use
Awaited<ReturnType<InternalFormStore['parse']>> instead of wrapping ReturnType
in another Promise, so the declaration represents the resolved parse result
rather than a nested promise.
In `@playgrounds/ir-demo/src/main.tsx`:
- Line 26: Define a named FormDemoProps interface containing the schema and
label fields currently declared inline, then update FormDemo to use
FormDemoProps for its parameter type while preserving the existing field types
and behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: aa8d5e95-c905-4c9e-b61a-ecc1e5f5704d
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (63)
SCHEMA_IR_POC.mdframeworks/react/src/hooks/useForm/useForm.tspackages/adapters/valibot/package.jsonpackages/adapters/valibot/src/index.tspackages/adapters/valibot/src/toFormisch.tspackages/adapters/valibot/src/transform.tspackages/adapters/valibot/tests/transform.test.tspackages/adapters/valibot/tsconfig.jsonpackages/adapters/valibot/tsdown.config.tspackages/adapters/valibot/vitest.config.tspackages/adapters/zod/package.jsonpackages/adapters/zod/src/index.tspackages/adapters/zod/src/toFormisch.tspackages/adapters/zod/src/transform.tspackages/adapters/zod/tests/transform.test.tspackages/adapters/zod/tsconfig.jsonpackages/adapters/zod/tsdown.config.tspackages/adapters/zod/vitest.config.tspackages/core/package.jsonpackages/core/src/array/copyItemState/copyItemState.test.tspackages/core/src/array/copyItemState/copyItemState.tspackages/core/src/array/resetItemState/resetItemState.test.tspackages/core/src/array/resetItemState/resetItemState.tspackages/core/src/array/swapItemState/swapItemState.test.tspackages/core/src/array/swapItemState/swapItemState.tspackages/core/src/field/focusFieldElement/focusFieldElement.test.tspackages/core/src/field/getDirtyFieldInput/getDirtyFieldInput.test.tspackages/core/src/field/getElementInput/getElementInput.test.tspackages/core/src/field/getFieldBool/getFieldBool.test.tspackages/core/src/field/getFieldInput/getFieldInput.test.tspackages/core/src/field/getFieldStore/getFieldStore.test.tspackages/core/src/field/initializeFieldStore/initializeFieldStore.test.tspackages/core/src/field/initializeFieldStore/initializeFieldStore.tspackages/core/src/field/setFieldBool/setFieldBool.test.tspackages/core/src/field/setFieldInput/setFieldInput.test.tspackages/core/src/field/setFieldInput/setFieldInput.tspackages/core/src/field/setInitialFieldInput/setInitialFieldInput.test.tspackages/core/src/field/setInitialFieldInput/setInitialFieldInput.tspackages/core/src/field/walkFieldStore/walkFieldStore.test.tspackages/core/src/form/createFormStore/createFormStore.test.tspackages/core/src/form/createFormStore/createFormStore.tspackages/core/src/form/decodeFormData/decodeFormData.test.tspackages/core/src/form/decodeFormData/decodeFormData.tspackages/core/src/form/parity.test.tspackages/core/src/form/validateFormInput/validateFormInput.test.tspackages/core/src/form/validateFormInput/validateFormInput.tspackages/core/src/form/validateIfRequired/validateIfRequired.test.tspackages/core/src/types/field/field.tspackages/core/src/types/form/form.qwik.tspackages/core/src/types/form/form.react.tspackages/core/src/types/form/form.tspackages/core/src/types/schema/index.tspackages/core/src/types/schema/ir.tspackages/core/src/types/schema/schema.test-d.tspackages/core/src/types/schema/schema.tspackages/core/src/vitest/utils.tspackages/core/vitest.config.tsplaygrounds/ir-demo/index.htmlplaygrounds/ir-demo/package.jsonplaygrounds/ir-demo/src/main.tsxplaygrounds/ir-demo/tsconfig.jsonplaygrounds/ir-demo/vite.config.tspnpm-workspace.yaml
| export function toFormisch(schema: v.GenericSchema): FormSchema { | ||
| const root = transform(schema); | ||
| return { | ||
| '~standard': (schema as unknown as { '~standard': unknown })['~standard'], | ||
| '~formisch': { version: 1 as const, root }, | ||
| } as FormSchema; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 4 '\btoFormisch\s*\(' packages --glob '*.ts'Repository: open-circle/formisch
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="packages/adapters/valibot/src/toFormisch.ts"
printf '%s\n' '--- candidate files ---'
fd -t f -e ts -e tsx 'toFormisch|transform|FormSchema' packages/adapters/valibot packages/core packages | head -80
printf '%s\n' '--- file outline ---'
ast-grep outline "$file" --lang typescript
printf '%s\n' '--- implementation ---'
cat -n "$file"
printf '%s\n' '--- related declarations and uses ---'
rg -n -C 5 'interface FormSchema|type FormSchema|FormSchema|function transform|const transform|export function toFormisch' packages/adapters/valibot packages/core packages/formisch --glob '*.ts' --glob '*.tsx' 2>/dev/null | head -500Repository: open-circle/formisch
Length of output: 25384
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- Valibot transform outline ---'
ast-grep outline packages/adapters/valibot/src/transform.ts --lang typescript
printf '%s\n' '--- Valibot transform implementation ---'
cat -n packages/adapters/valibot/src/transform.ts
printf '%s\n' '--- Valibot adapter tests around wrapper and root schemas ---'
cat -n packages/adapters/valibot/tests/transform.test.ts | sed -n '1,190p'
printf '%s\n' '--- Comparable Zod adapter ---'
cat -n packages/adapters/zod/src/toFormisch.ts
cat -n packages/adapters/zod/src/transform.ts | sed -n '1,130p'
printf '%s\n' '--- Root-type assertions and adapter API declarations ---'
rg -n -C 4 'root\.type|toFormisch\(v\.|toFormisch\(z\.|GenericSchema|FormSchemaIR' packages/adapters packages/core --glob '*.ts' --glob '*.tsx' --glob '*.d.ts' | head -500
printf '%s\n' '--- Static behavioral verifier ---'
python3 - <<'PY'
from pathlib import Path
import re
transform = Path('packages/adapters/valibot/src/transform.ts').read_text()
to_formisch = Path('packages/adapters/valibot/src/toFormisch.ts').read_text()
schema = Path('packages/core/src/types/schema/ir.ts').read_text()
cases = {}
for case, ir_type in re.findall(r"case '([^']+)':\s*return[\s\S]*?\{\s*type:\s*'([^']+)'", transform):
cases[case] = ir_type
print('transform switch mappings:', cases)
print('toFormisch input is GenericSchema:', 'schema: v.GenericSchema' in to_formisch)
print('toFormisch returns FormSchema:', '): FormSchema' in to_formisch)
print('FormSchema root requires object:', "root: FormischFieldIR & { readonly type: 'object' }" in schema)
print('non-object transform cases:', sorted(set(cases.values()) - {'object'}))
PYRepository: open-circle/formisch
Length of output: 50376
Reject non-object root schemas.
v.GenericSchema accepts leaf and array schemas, but FormSchemaIR requires ~formisch.root.type to be 'object'. The cast currently allows toFormisch(v.string()) to return an invalid FormSchema. Add a guard before the cast or constrain the input type to object schemas.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/adapters/valibot/src/toFormisch.ts` around lines 15 - 20, Update
toFormisch to reject non-object root schemas before constructing or casting the
FormSchema; validate that the transformed root has type 'object' and throw for
leaf or array schemas, while preserving the existing output for valid object
schemas.
| case 'lazy': | ||
| return unwrapValibot(schema.getter(undefined)); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- target file outline ---'
ast-grep outline packages/adapters/valibot/src/transform.ts || true
printf '%s\n' '--- target file ---'
cat -n packages/adapters/valibot/src/transform.ts
printf '%s\n' '--- relevant files ---'
git ls-files | rg '(^|/)(package\.json|.*valibot.*|.*transform.*|.*test.*|.*spec.*)$' | head -200
printf '%s\n' '--- transform references ---'
rg -n --glob '!node_modules' 'unwrapValibot|transform\(|schema\.getter|lazy\(' packages/adapters/valibot packages | head -300
printf '%s\n' '--- Valibot dependency declarations ---'
rg -n --glob 'package.json' --glob 'pnpm-lock.yaml' --glob 'yarn.lock' --glob 'package-lock.json' '"valibot"|valibot@' . | head -100Repository: open-circle/formisch
Length of output: 25779
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- adapter tests ---'
cat -n packages/adapters/valibot/tests/transform.test.ts
printf '%s\n' '--- adapter entry point ---'
cat -n packages/adapters/valibot/src/toFormisch.ts
printf '%s\n' '--- core IR/type definitions ---'
rg -n --glob '*.ts' 'FormischFieldIR|interface .*FieldIR|type .*FieldIR|initializeFieldStore|toFormisch' packages/core packages/adapters | head -200
printf '%s\n' '--- core initialization context ---'
sed -n '280,350p' packages/core/src/field/initializeFieldStore/initializeFieldStore.test.ts
sed -n '430,515p' packages/core/src/field/initializeFieldStore/initializeFieldStore.test.ts
printf '%s\n' '--- recursive/lazy references ---'
rg -n --glob '*.ts' --glob '*.tsx' 'recursive|recursive schema|v\.lazy\(|lazy\(\(\) =>' packages/adapters/valibot packages/core/src | head -300
printf '%s\n' '--- relevant package scripts ---'
cat -n package.json | sed -n '1,180p'
cat -n packages/adapters/valibot/package.jsonRepository: open-circle/formisch
Length of output: 44279
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- Formisch IR ---'
cat -n packages/core/src/types/schema/ir.ts
printf '%s\n' '--- initializeFieldStore ---'
cat -n packages/core/src/field/initializeFieldStore/initializeFieldStore.ts
printf '%s\n' '--- standalone recursion verifier ---'
python3 - <<'PY'
import sys
sys.setrecursionlimit(250)
class Schema:
def __init__(self, kind, **kwargs):
self.type = kind
self.__dict__.update(kwargs)
def lazy(getter):
return Schema("lazy", getter=getter)
def obj(entries):
return Schema("object", entries=entries)
node = None
node = lazy(lambda: obj({"value": Schema("string"), "next": node}))
unwrap_calls = 0
transform_calls = 0
def unwrap(schema):
global unwrap_calls
unwrap_calls += 1
if schema.type == "lazy":
return unwrap(schema.getter(None))
return schema
def transform(schema):
global transform_calls
transform_calls += 1
unwrapped = unwrap(schema)
if unwrapped.type == "object":
return {
"type": "object",
"properties": {key: transform(child)
for key, child in unwrapped.entries.items()},
}
return {"type": unwrapped.type}
try:
transform(node)
except RecursionError as error:
print("result=unbounded-recursion")
print(f"unwrap_calls={unwrap_calls}")
print(f"transform_calls={transform_calls}")
print(f"error={type(error).__name__}")
PYRepository: open-circle/formisch
Length of output: 8756
🌐 Web query:
Valibot 1.4.2 lazy schema getter recursive schema implementation
💡 Result:
In Valibot, including version 1.4.2, recursive schemas are implemented using the v.lazy function [1][2]. This function accepts a getter, a callback that returns the actual schema when called [1][3]. Using a lazy getter is necessary to resolve circular dependencies that occur when a schema refers to itself, as it defers the evaluation of the schema until the validation process actually occurs [4][5]. Implementation Example: To implement a recursive schema (e.g., for a tree structure), define your schema and use v.lazy within the recursive property [1][4]. Because of TypeScript limitations, recursive types cannot always be inferred automatically, so you should explicitly specify the schema type using v.GenericSchema or an interface [1][4]. import * as v from 'valibot'; type BinaryTree = { element: string; left: BinaryTree | null; right: BinaryTree | null; }; // Use v.GenericSchema to explicitly define the recursive type const BinaryTreeSchema: v.GenericSchema = v.object({ element: v.string, left: v.nullable(v.lazy( => BinaryTreeSchema)), right: v.nullable(v.lazy( => BinaryTreeSchema)), }); Key Considerations: 1. TypeScript Inference: Since recursive types cannot be inferred automatically in all cases, you must manually define the type (e.g., using a TypeScript type or interface) and apply it to the schema variable using v.GenericSchema [1][4][6]. 2. Runtime Input Access: The getter function receives the input as its first argument (v.lazy((input) =>...)). While this is primarily used for recursion, it can also be used to create dynamic or conditional schemas based on the runtime input value [1][7]. 3. Recommendation: While v.lazy is effective for recursion, the Valibot maintainers often recommend using v.union or v.variant if they are sufficient for your use case, as they may be more performant or predictable [1][3].
Citations:
- 1: https://valibot.dev/api/lazy/
- 2: https://jsr.io/@valibot/valibot/1.3.1/src/schemas/lazy/lazy.ts
- 3: https://valibot.dev/api/lazy.md
- 4: https://valibot.dev/guides/other/
- 5: https://app.studyraid.com/en/read/11918/379788/recursive-schema-patterns
- 6: https://dev.to/allieschen/using-valibot-for-recursive-schema-validation-15ej
- 7: Enable
v.recursive()getter to accept avalueto improving usage flexibility valibot#440
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
import sys
sys.setrecursionlimit(250)
class Schema:
def __init__(self, kind, **kwargs):
self.type = kind
self.__dict__.update(kwargs)
def lazy(getter):
return Schema("lazy", getter=getter)
def obj(entries):
return Schema("object", entries=entries)
node = None
node = lazy(lambda _input: obj({
"value": Schema("string"),
"next": node,
}))
unwrap_calls = 0
transform_calls = 0
def unwrap(schema):
global unwrap_calls
unwrap_calls += 1
if schema.type == "lazy":
return unwrap(schema.getter(None))
return schema
def transform(schema):
global transform_calls
transform_calls += 1
unwrapped = unwrap(schema)
if unwrapped.type == "object":
return {
"type": "object",
"properties": {
key: transform(child)
for key, child in unwrapped.entries.items()
},
}
return {"type": unwrapped.type}
try:
transform(node)
except RecursionError as error:
print("result=unbounded-recursion")
print(f"unwrap_calls={unwrap_calls}")
print(f"transform_calls={transform_calls}")
print(f"error={type(error).__name__}")
else:
raise SystemExit("expected recursive schema to exceed recursion limit")
PYRepository: open-circle/formisch
Length of output: 243
Handle recursive lazy schemas without unbounded recursion.
When a lazy getter returns an object that contains the same lazy schema, transform recurses indefinitely and toFormisch fails with RangeError: Maximum call stack size exceeded. Detect active recursive schemas and either reject them with a clear error or use a recursive IR representation supported by the core. Add a regression test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/adapters/valibot/src/transform.ts` around lines 23 - 24, Update the
lazy-schema handling in unwrapValibot so active recursive schemas are detected
before recursively processing schema.getter(undefined), preventing unbounded
recursion; either throw a clear recursion error or emit the core-supported
recursive IR. Add a regression test covering a lazy getter whose returned object
contains the same lazy schema and verify toFormisch no longer overflows the call
stack.
| case 'enum': | ||
| case 'picklist': | ||
| case 'literal': | ||
| return { | ||
| type: 'string', | ||
| optional, | ||
| getDefault: () => v.getDefault(schema), | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- target file outline ---'
ast-grep outline packages/adapters/valibot/src/transform.ts 2>/dev/null || true
printf '%s\n' '--- target implementation ---'
cat -n packages/adapters/valibot/src/transform.ts | sed -n '1,180p'
printf '%s\n' '--- Valibot package metadata and related references ---'
fd -i 'package.json|README|transform' packages/adapters/valibot . | head -80
rg -n --glob '!node_modules/**' "v\.enum|v\.picklist|v\.literal|transformValibot|type: 'string'|getDefault" packages/adapters/valibot packages | head -240Repository: open-circle/formisch
Length of output: 15223
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- adapter metadata ---'
cat packages/adapters/valibot/package.json
printf '%s\n' '--- adapter tests ---'
cat -n packages/adapters/valibot/tests/transform.test.ts | sed -n '1,180p'
printf '%s\n' '--- IR type definition ---'
cat -n packages/core/src/types/schema/ir.ts | sed -n '1,120p'
printf '%s\n' '--- consumers of IR type ---'
rg -n --glob '!node_modules/**' "FormischFieldIR|\.type === ['\"]string['\"]|case ['\"]string['\"]|type: 'unknown'|type: 'number'" packages/core packages/adapters | head -260
printf '%s\n' '--- lockfile Valibot version ---'
rg -n -C 3 '"valibot"|valibot@|valibot:' pnpm-lock.yaml package-lock.json yarn.lock bun.lockb 2>/dev/null | head -100 || trueRepository: open-circle/formisch
Length of output: 18380
🌐 Web query:
Valibot 1.4.2 enum picklist literal schema runtime values numeric boolean API
💡 Result:
In Valibot, enum, picklist, and literal are schema functions used for runtime validation of specific values [1][2][3]. While they share the goal of restricting input to a set of allowed values, they are intended for different data structures [1][4]. Enum Schema The v.enum function is designed to validate against TypeScript enums or plain JavaScript objects [1][5]. Because TypeScript enums are transpiled into objects at runtime, v.enum treats these objects as the source of truth for valid values [1][4]. Example: import * as v from 'valibot'; enum Direction { Left, Right } const DirectionSchema = v.enum(Direction); Picklist Schema The v.picklist function is used when your allowed values are stored in an array [1][6]. It is often more convenient than v.enum when working with dynamic lists or simple arrays of primitives [6][2]. Example: import * as v from 'valibot'; const DirectionSchema = v.picklist(['LEFT', 'RIGHT'] as const); Literal Schema The v.literal function validates that an input matches exactly one specific value [2]. It is commonly used for fixed, single-value requirements [3]. Example: const LiteralSchema = v.literal('admin'); Runtime Values (Numeric and Boolean) Valibot schemas operate at runtime by validating that input matches the defined constraints [7]. For numeric and boolean values: - Numeric: You can validate numbers using v.number and further restrict them using pipes, such as v.integer, v.minValue, or v.maxValue [8]. If you need to validate that a value is exactly one of a few specific numbers, you would typically use v.picklist([1][6][5] as const) or a union of literals [1][6][3]. - Boolean: Use v.boolean for basic type validation [3]. Like numbers, specific boolean requirements can be handled via literals or picklists if you need to restrict them to exactly true or false, though v.boolean is sufficient for general type checking. API Summary - v.enum(enumObject, [message]): Validates against values in a TS/JS enum object [5][2]. - v.picklist(array, [message]): Validates that the input is one of the elements in the array [6][2]. - v.literal(value, [message]): Validates that the input matches the exact value [2]. - v.number / v.boolean: Used for general type validation of primitive values, often combined with pipes for additional constraints [8][3].
Citations:
- 1: https://valibot.dev/guides/enums/
- 2: https://valibot.dev/llms-api.txt
- 3: https://valibot.dev/guides/schemas/
- 4: https://valibot.dev/guides/enums.md
- 5: https://valibot.dev/api/enum/
- 6: https://valibot.dev/api/picklist/
- 7: https://www.npmjs.com/package/valibot
- 8: https://valibot.dev/api/number/
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- form-data decoding ---'
cat -n packages/core/src/form/decodeFormData/decodeFormData.ts | sed -n '1,180p'
printf '%s\n' '--- field initialization ---'
cat -n packages/core/src/field/initializeFieldStore/initializeFieldStore.ts | sed -n '1,150p'
printf '%s\n' '--- tests for scalar decoding and initialization ---'
rg -n -C 8 --glob '*.test.ts' "number|boolean|bigint|unknown|enum|picklist|literal" \
packages/core/src/form packages/core/src/field packages/adapters/valibot/tests | head -320
printf '%s\n' '--- Valibot type declarations/source references if vendored ---'
fd -i 'valibot' . --exclude node_modules --exclude .git | head -100Repository: open-circle/formisch
Length of output: 37121
🏁 Script executed:
#!/bin/bash
set -eu
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
curl -fsSL https://unpkg.com/valibot@1.4.2/dist/index.d.ts > "$tmp"
printf '%s\n' '--- relevant Valibot declarations ---'
rg -n -C 12 "interface (Literal|Picklist|Enum)Schema|type (Literal|Picklist|Enum)Schema|declare function (literal|picklist|enum)" "$tmp" | head -260
printf '%s\n' '--- relevant Valibot runtime implementation ---'
curl -fsSL https://unpkg.com/valibot@1.4.2/dist/index.js |
rg -n -C 10 "function (literal|picklist|enum)|const (literal|picklist|enum)" | head -260Repository: open-circle/formisch
Length of output: 209
🏁 Script executed:
#!/bin/bash
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
metadata="$tmpdir/metadata.json"
curl -fsSL https://registry.npmjs.org/valibot/1.4.2 > "$metadata"
tarball="$(jq -r '.dist.tarball' "$metadata")"
curl -fsSL "$tarball" -o "$tmpdir/valibot.tgz"
tar -xzf "$tmpdir/valibot.tgz" -C "$tmpdir"
printf '%s\n' '--- package files ---'
find "$tmpdir/package" -maxdepth 3 -type f | sort | head -80
printf '%s\n' '--- relevant declarations ---'
rg -n -C 12 "interface (Literal|Picklist|Enum)Schema|type (Literal|Picklist|Enum)Schema|declare function (literal|picklist|enum)" "$tmpdir/package" | head -300
printf '%s\n' '--- relevant runtime implementation ---'
rg -n -C 10 "function (literal|picklist|enum)|const (literal|picklist|enum)" "$tmpdir/package" | head -300Repository: open-circle/formisch
Length of output: 44640
🏁 Script executed:
#!/bin/bash
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
metadata="$tmpdir/metadata.json"
curl -fsSL https://registry.npmjs.org/valibot/1.4.2 > "$metadata"
curl -fsSL "$(jq -r '.dist.tarball' "$metadata")" -o "$tmpdir/valibot.tgz"
tar -xzf "$tmpdir/valibot.tgz" -C "$tmpdir"
node --input-type=module - "$tmpdir/package/dist/index.mjs" <<'JS'
const modulePath = process.argv[2];
const v = await import(modulePath);
const numericEnum = { A: 1, B: 2, 1: 'A', 2: 'B' };
const schemas = {
literalNumber: v.literal(1),
literalBoolean: v.literal(true),
literalBigint: v.literal(1n),
picklistNumbers: v.picklist([1, 2]),
enumNumbers: v.enum(numericEnum),
picklistMixed: v.picklist(['1', 1]),
};
function inferType(schema) {
const values =
schema.type === 'literal'
? [schema.literal]
: schema.type === 'picklist' || schema.type === 'enum'
? schema.options
: [];
const kinds = new Set(values.map((value) => {
if (typeof value === 'string') return 'string';
if (typeof value === 'number') return 'number';
if (typeof value === 'boolean') return 'boolean';
if (typeof value === 'bigint') return 'bigint';
return 'unknown';
}));
return kinds.size === 1 ? [...kinds][0] : 'unknown';
}
for (const [name, schema] of Object.entries(schemas)) {
console.log(name, JSON.stringify({
schemaType: schema.type,
literal: typeof schema.literal === 'bigint' ? `${schema.literal}n` : schema.literal,
options: schema.options?.map((value) =>
typeof value === 'bigint' ? `${value}n` : value
),
inferredType: inferType(schema),
}));
}
JSRepository: open-circle/formisch
Length of output: 630
Preserve primitive types for literal, picklist, and enum schemas.
These schemas can accept numeric, boolean, or bigint values, but this branch always emits type: 'string'. Form data then remains a string, so numeric value 1 reaches Valibot as '1' and fails validation. Infer the IR type from unwrapped.literal or unwrapped.options; use unknown for mixed primitive types.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/adapters/valibot/src/transform.ts` around lines 108 - 115, Update
the enum/picklist/literal branch in the transform function to infer the IR type
from unwrapped.literal or unwrapped.options instead of always returning string.
Preserve numeric, boolean, and bigint primitive types, and return unknown when
the schema contains mixed primitive types; keep optional and getDefault behavior
unchanged.
| case 'union': | ||
| case 'variant': | ||
| case 'intersect': { | ||
| // Resolve to first option (POC limitation — see plan's open design questions) | ||
| if (unwrapped.options?.[0]) { | ||
| const inner = transform(unwrapped.options[0] as v.GenericSchema); | ||
| return { ...inner, optional, getDefault: () => v.getDefault(schema) }; | ||
| } | ||
| return { | ||
| type: 'unknown', | ||
| optional, | ||
| getDefault: () => v.getDefault(schema), | ||
| }; | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- file map ---'
fd -i 'transform.ts|package.json|.*test.*|.*spec.*' packages/adapters/valibot
printf '%s\n' '--- transform outline ---'
ast-grep outline packages/adapters/valibot/src/transform.ts
printf '%s\n' '--- transform source ---'
cat -n packages/adapters/valibot/src/transform.ts
printf '%s\n' '--- related symbols ---'
rg -n "transform\(|unwrapValibot|GenericSchema|getDefault|case 'union'|case 'variant'|case 'intersect'" packages/adapters/valibotRepository: open-circle/formisch
Length of output: 9498
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- repository files ---'
git ls-files packages/adapters/valibot
printf '%s\n' '--- transform outline ---'
ast-grep outline packages/adapters/valibot/src/transform.ts
printf '%s\n' '--- transform source ---'
cat -n packages/adapters/valibot/src/transform.ts
printf '%s\n' '--- package metadata and call sites ---'
cat -n packages/adapters/valibot/package.json
rg -n "transform\(|unwrapValibot|GenericSchema|getDefault|case 'union'|case 'variant'|case 'intersect'" packages/adapters/valibotRepository: open-circle/formisch
Length of output: 11263
🏁 Script executed:
#!/bin/bash
set -e
sed -n '1,220p' packages/adapters/valibot/src/transform.ts
printf '%s\n' '--- package metadata ---'
cat packages/adapters/valibot/package.json
printf '%s\n' '--- all adapter references ---'
rg -n "transform\(|unwrapValibot|getDefault|GenericSchema|union|variant|intersect" packages/adapters/valibotRepository: open-circle/formisch
Length of output: 9306
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
p = Path("packages/adapters/valibot/src/transform.ts")
print(p.read_text())
PYRepository: open-circle/formisch
Length of output: 3586
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- Formisch IR definitions and consumers ---'
rg -n "interface FormischFieldIR|type FormischFieldIR|FormischFieldIR|\.properties|\.item|field\.type|root" packages --glob '*.ts' --glob '*.tsx'
printf '%s\n' '--- Valibot adapter tests ---'
cat -n packages/adapters/valibot/tests/transform.test.ts
printf '%s\n' '--- comparable Zod adapter handling ---'
rg -n -C 5 "union|intersection|discriminated|options\\[0\\]|unsupported|unknown" packages/adapters/zod packages --glob '*.ts' --glob '*.tsx' | head -240
printf '%s\n' '--- package lock entries ---'
rg -n -A3 -B2 '"valibot"|valibot@' pnpm-lock.yaml package-lock.json yarn.lock 2>/dev/null | head -80Repository: open-circle/formisch
Length of output: 50376
🌐 Web query:
Valibot 1.4.1 documentation union variant intersect schema semantics options
💡 Result:
In Valibot 1.4.1, the union, variant, and intersect schemas serve distinct purposes for managing complex data types. Union (v.union) The union schema represents a logical OR relationship among multiple schemas provided as an array [1]. During validation, Valibot attempts to validate the input against the provided schemas in order; it returns the result of the first schema that successfully validates the input [1][2]. If no schema matches, it returns a general issue containing the individual issues from each sub-schema [1][2]. Variant (v.variant) The variant schema is a specialized version of union intended for discriminated unions [1]. It requires a discriminator key (a common property used to distinguish between types) and an array of object schemas [1][3]. Because it uses the discriminator key to determine which schema to use, it is more performant and provides more targeted error messages than a standard union [1][3]. If an input cannot be clearly assigned via the discriminator, or if it does not match, a custom error message can be provided [3]. Variant schemas can be deeply nested [1][3]. Intersect (v.intersect) The intersect schema represents a logical AND relationship [4]. It validates that the input matches all provided schemas [5][4]. If all validations succeed, it merges the results of the individual schemas [4]. Notably, v.intersect in version 1.4.1 includes fixes to correctly infer types for non-tuple array options and ensures that input values are not mutated, allowing for the validation of frozen objects [6]. Unlike merging object schemas (which happens during initialization), intersect performs validation at runtime [4]. If properties conflict between intersected schemas, validation will fail [4]. Common Patterns - Recommended usage: Use variant over union whenever possible for discriminated object unions [1]. - Factoring out shared properties: Since v.variant does not natively support factoring out shared properties, a common best practice is to compose a shared schema with a variant schema using v.intersect (e.g., v.intersect([SharedSchema, v.variant(...)])) [7].
Citations:
- 1: https://valibot.dev/guides/unions/
- 2: https://valibot.dev/api/union/
- 3: https://valibot.dev/api/variant/
- 4: https://valibot.dev/guides/intersections/
- 5: https://valibot.dev/api/intersect/
- 6: https://npmx.dev/package-changelog/valibot/v/1.4.0
- 7: Variant schemas with shared elements valibot#1405
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- core IR declarations ---'
rg -n -C 12 "FormischFieldIR|FormSchema" packages --glob '*.ts' | head -240
printf '%s\n' '--- all schema adapter transforms ---'
rg -n -C 8 "export function transform|case .*union|case .*intersection|options\\[0\\]|unsupported|type: 'unknown'" packages/adapters --glob '*.ts' | head -320
printf '%s\n' '--- form construction and validation paths ---'
cat -n packages/adapters/valibot/src/toFormisch.ts
rg -n -C 8 "FormischFieldIR|fieldIR|schema.*parse|safeParse|validate|decode|submit" packages --glob '*.ts' --glob '*.tsx' | head -320Repository: open-circle/formisch
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- IR type ---'
cat -n packages/core/src/types/schema/ir.ts
printf '%s\n' '--- decoder ---'
cat -n packages/core/src/form/decodeFormData/decodeFormData.ts
printf '%s\n' '--- validation ---'
cat -n packages/core/src/form/validateFormInput/validateFormInput.ts
printf '%s\n' '--- intersect decoder test context ---'
sed -n '470,535p' packages/core/src/form/decodeFormData/decodeFormData.test.ts
printf '%s\n' '--- source-level probe ---'
python3 - <<'PY'
from pathlib import Path
src = Path("packages/adapters/valibot/src/transform.ts").read_text()
required = [
"case 'union':",
"case 'variant':",
"case 'intersect':",
"transform(unwrapped.options[0] as v.GenericSchema)",
]
for item in required:
print(f"{item}: {item in src}")
print("first-option transformations:", src.count("transform(unwrapped.options[0] as v.GenericSchema)"))
PYRepository: open-circle/formisch
Length of output: 14841
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- core test schema transformer ---'
sed -n '35,155p' packages/core/src/vitest/utils.ts
printf '%s\n' '--- decoder test setup and intersect/variant block ---'
sed -n '1,45p' packages/core/src/form/decodeFormData/decodeFormData.test.ts
sed -n '400,525p' packages/core/src/form/decodeFormData/decodeFormData.test.ts
printf '%s\n' '--- adapter entry points ---'
cat -n packages/adapters/valibot/src/index.ts packages/adapters/valibot/src/toFormisch.ts
printf '%s\n' '--- all Valibot intersect/variant references ---'
rg -n -C 6 "v\\.intersect|v\\.variant|intersect options|variant options" packages --glob '*.ts'Repository: open-circle/formisch
Length of output: 31268
Do not reduce combinator schemas to options[0].
The IR drives FormData decoding. For example, a later boolean branch is treated as an unknown string, so 'on' remains 'on' instead of becoming true. Traverse all representable options, or throw an explicit unsupported-schema error.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/adapters/valibot/src/transform.ts` around lines 117 - 130, The
union, variant, and intersect handling in transform must not resolve schemas to
options[0]. Traverse and combine all representable options so later branches
retain their correct types during FormData decoding; if the combinator cannot be
represented, throw an explicit unsupported-schema error instead of returning an
incomplete IR.
| } else if (ir.type === 'array') { | ||
| if (Array.isArray(parent[key])) { | ||
| for (let index = 0; index < parent[key].length; index++) { | ||
| fillDefaults(unwrappedSchema.item!, parent[key], index); | ||
| fillDefaults(ir.item!, parent[key], index); | ||
| } | ||
| } else { | ||
| parent[key] = []; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Remove the non-null assertion on ir.item; it can throw a TypeError.
Line 69 asserts ir.item!. FormischFieldIR declares item as optional, and any type: 'array' node without item (for example a tuple encoding) passes undefined into the recursive call. The next call then reads ir.type on undefined and throws. Guard the recursion instead.
🐛 Proposed guard for the missing array item IR
} else if (ir.type === 'array') {
if (Array.isArray(parent[key])) {
- for (let index = 0; index < parent[key].length; index++) {
- fillDefaults(ir.item!, parent[key], index);
- }
+ if (ir.item) {
+ for (let index = 0; index < parent[key].length; index++) {
+ fillDefaults(ir.item, parent[key], index);
+ }
+ }
} else {
parent[key] = [];
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } else if (ir.type === 'array') { | |
| if (Array.isArray(parent[key])) { | |
| for (let index = 0; index < parent[key].length; index++) { | |
| fillDefaults(unwrappedSchema.item!, parent[key], index); | |
| fillDefaults(ir.item!, parent[key], index); | |
| } | |
| } else { | |
| parent[key] = []; | |
| } | |
| } else if (ir.type === 'array') { | |
| if (Array.isArray(parent[key])) { | |
| if (ir.item) { | |
| for (let index = 0; index < parent[key].length; index++) { | |
| fillDefaults(ir.item, parent[key], index); | |
| } | |
| } | |
| } else { | |
| parent[key] = []; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/core/src/form/decodeFormData/decodeFormData.ts` around lines 66 -
73, Update the array branch in fillDefaults to remove the non-null assertion on
ir.item and only recurse when the item IR is defined; otherwise preserve the
existing parent[key] array initialization and avoid calling fillDefaults with
undefined.
| default: | ||
| return { | ||
| type: 'unknown', | ||
| optional, | ||
| getDefault: () => v.getDefault(schema), | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the shared test transformer and the production Valibot transformer.
ast-grep outline packages/core/src/vitest/utils.ts --items all --type function
ast-grep outline packages/adapters/valibot/src/transform.ts --items all --type function
# Compare handling for compound schema types.
rg -n -C 6 \
"case '(tuple|record|object_with_rest|union|variant|intersect)'|options\?\.\[0\]" \
packages/core/src/vitest/utils.ts \
packages/adapters/valibot/src/transform.ts
# Identify migrated tests that require these semantics.
rg -n -C 3 \
"v\.(tuple|record|objectWithRest|union|variant|intersect)" \
packages/core/src \
-g '*.test.ts'Repository: open-circle/formisch
Length of output: 29233
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- packages/core/src/vitest/utils.ts ---'
cat -n packages/core/src/vitest/utils.ts | sed -n '1,180p'
printf '%s\n' '--- packages/adapters/valibot/src/transform.ts ---'
cat -n packages/adapters/valibot/src/transform.ts | sed -n '1,160p'
printf '%s\n' '--- IR definitions and relevant test expectations ---'
rg -n -C 5 \
"interface FormischFieldIR|type FormischFieldIR|type: 'unknown'|record.*not supported|object_with_rest.*not supported|initialize fixed tuple items|should initialize for each union option|should descend into union options|should descend into intersect options|should descend into variant options" \
packages/core/src packages/adapters/valibot/src \
-g '*.ts' -g '*.tsx'Repository: open-circle/formisch
Length of output: 22233
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- IR and field initialization behavior ---'
cat -n packages/core/src/types/schema/ir.ts | sed -n '1,130p'
rg -n -C 8 \
"case 'unknown'|ir\.type|field\.type|unsupported|not supported|kind: 'array'|kind: 'value'" \
packages/core/src/field packages/core/src/form \
-g '*.ts' -g '*.tsx'
printf '%s\n' '--- Exact affected assertions ---'
cat -n packages/core/src/field/initializeFieldStore/initializeFieldStore.test.ts | sed -n '280,335p'
cat -n packages/core/src/form/decodeFormData/decodeFormData.test.ts | sed -n '446,515p'
cat -n packages/core/src/field/initializeFieldStore/initializeFieldStore.test.ts | sed -n '505,525p'
printf '%s\n' '--- Schema transform tests and repository change summary ---'
fd -i 'transform*.test.ts' packages
git diff --statRepository: open-circle/formisch
Length of output: 29998
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- complete initialization switch ---'
cat -n packages/core/src/field/initializeFieldStore/initializeFieldStore.ts | sed -n '1,135p'
printf '%s\n' '--- Valibot adapter transform expectations ---'
cat -n packages/adapters/valibot/tests/transform.test.ts | sed -n '1,260p'
printf '%s\n' '--- all unsupported-schema handling ---'
rg -n -C 5 \
"record|object_with_rest|unsupported|not supported|case 'unknown'|throw new Error" \
packages/core/src packages/adapters/valibot \
-g '*.ts' -g '*.tsx'Repository: open-circle/formisch
Length of output: 24022
Preserve compound schema semantics in transformValibot.
packages/core/src/vitest/utils.ts maps tuple, record, and object_with_rest schemas to unknown. The core treats unknown as a value field, so tuple fields are not arrays and record tests do not throw the expected unsupported-schema errors.
The helper also reduces union, variant, and intersect schemas to the first option. This omits later fields during initialization and form-data decoding.
Implement the required IR behavior for these schemas. If the POC excludes them, explicitly skip or remove the affected tests instead of silently changing their semantics.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/core/src/vitest/utils.ts` around lines 84 - 89, Update
transformValibot to preserve compound-schema semantics: represent tuple schemas
as arrays, retain record and object_with_rest schemas so unsupported-schema
errors remain observable, and combine all union, variant, and intersect options
rather than selecting only the first. If these schemas remain outside the POC
scope, explicitly skip or remove their affected tests instead of mapping them to
unknown.
| function FormDemo({ schema, label }: { schema: FormSchema; label: string }) { | ||
| const form = useForm({ | ||
| schema, | ||
| onSubmit: (output) => { | ||
| console.log(`[${label}] Submitted:`, output); | ||
| }, | ||
| }); | ||
|
|
||
| return ( | ||
| <div style={{ flex: 1, minWidth: 300 }}> | ||
| <h3>{label}</h3> | ||
| <pre> | ||
| name (string): {(form as Record<string, unknown>).__name ?? 'N/A'} | ||
| {'\n'} | ||
| errors: {JSON.stringify(form.errors)} | ||
| </pre> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- target file ---'
sed -n '1,130p' playgrounds/ir-demo/src/main.tsx
printf '%s\n' '--- related documentation ---'
rg -n -C 4 'IR|name|age|email|FormDemo|submit' playgrounds/ir-demo SCHEMA_IR_POC.md 2>/dev/null || true
printf '%s\n' '--- formisch/react API references ---'
rg -n -C 3 'useForm|Field|Form|form\.|bind|name|age|email' --glob '*.{ts,tsx,md}' . | head -n 400Repository: open-circle/formisch
Length of output: 41709
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- React package files ---'
git ls-files 'frameworks/react/*' 'packages/core/src/types/form/*' | head -n 200
printf '%s\n' '--- React API outlines ---'
for f in $(git ls-files 'frameworks/react/*' | grep -E '/(Form|Field|useForm)/.*\.(ts|tsx)$' | head -n 30); do
echo "--- $f"
ast-grep outline "$f" 2>/dev/null || true
done
printf '%s\n' '--- Form, Field, and useForm implementations ---'
rg -l 'export.*(Form|Field|useForm)|function (Form|Field)|const (Form|Field)' frameworks/react packages | head -n 80
printf '%s\n' '--- form methods and field-state accessors ---'
rg -n -C 5 'handleSubmit|onSubmit|useField|field\.props|field\.input|getInput|getField|errors' frameworks/react packages/core/src --glob '*.{ts,tsx}' | head -n 500
printf '%s\n' '--- documentation target lines ---'
sed -n '28,95p' SCHEMA_IR_POC.mdRepository: open-circle/formisch
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- Form component ---'
cat -n frameworks/react/src/components/Form/Form.tsx
printf '%s\n' '--- Field component ---'
cat -n frameworks/react/src/components/Field/Field.tsx
printf '%s\n' '--- useField hook ---'
cat -n frameworks/react/src/hooks/useField/useField.ts
printf '%s\n' '--- React form and field types ---'
cat -n frameworks/react/src/types/form.ts
cat -n frameworks/react/src/types/field.ts
printf '%s\n' '--- React exports ---'
cat -n frameworks/react/src/index.ts
cat -n frameworks/react/src/components/index.ts
printf '%s\n' '--- POC instructions ---'
sed -n '28,95p' SCHEMA_IR_POC.md
printf '%s\n' '--- IR and public schema types ---'
cat -n packages/core/src/types/schema/ir.ts
cat -n packages/core/src/types/schema/schema.ts
cat -n packages/core/src/types/form/form.react.tsRepository: open-circle/formisch
Length of output: 21972
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- submit path ---'
cat -n packages/methods/src/handleSubmit/handleSubmit.ts
cat -n packages/methods/src/handleSubmit/handleSubmit.react.ts
printf '%s\n' '--- form-data decoding ---'
cat -n packages/core/src/form/decodeFormData/decodeFormData.ts
printf '%s\n' '--- element input conversion ---'
cat -n packages/core/src/field/getElementInput/getElementInput.ts
printf '%s\n' '--- read-only source verifier ---'
python3 - <<'PY'
from pathlib import Path
demo = Path("playgrounds/ir-demo/src/main.tsx").read_text()
form = Path("frameworks/react/src/components/Form/Form.tsx").read_text()
field = Path("frameworks/react/src/components/Field/Field.tsx").read_text()
field_types = Path("frameworks/react/src/types/field.ts").read_text()
checks = {
"demo has no Form component": "<Form" not in demo,
"demo has no Field component": "<Field" not in demo,
"demo has no native form": "<form" not in demo,
"demo has no input control": "<input" not in demo,
"demo has no submit control": 'type="submit"' not in demo,
"demo accesses private-looking __name property": ".__name" in demo,
"Form wraps native form": "<form" in form,
"Form wires onSubmit": "onSubmit={handleSubmit(of, onSubmit)}" in form,
"Field calls useField": "const field = useField(of, { path });" in field,
"Field exposes input/errors/props": all(
token in field_types for token in ("readonly input", "readonly errors", "readonly props")
),
}
for name, result in checks.items():
print(f"{'PASS' if result else 'FAIL'}: {name}")
PYRepository: open-circle/formisch
Length of output: 14515
Render a real form with Form and Field.
onSubmit is not user-triggered without the Form component. Bind name, age, and email with Field, and render each field's input and errors through the public API. Add a submit control. Remove the __name cast because FormStore does not expose that property.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@playgrounds/ir-demo/src/main.tsx` around lines 26 - 44, Update FormDemo to
render the form through the public Form and Field APIs: wrap the fields in Form,
bind name, age, and email with Field, render each field’s input and errors, and
add a submit control so onSubmit is user-triggered. Remove the __name
cast/display and use the field render APIs instead.
Introduces the intermediate representation (FormischFieldIR) that abstracts over validation library specifics. The core schema type becomes a Standard Schema augmented with the ~formisch marker, replacing the direct Valibot type dependency in the type layer. This is a types-only commit; runtime code does not yet consume the IR, so tsc will report errors in the traversal sites until the next commit rewires them.
Replaces all Valibot-specific schema traversal in initializeFieldStore, decodeFormData, createFormStore, validateFormInput, and the array/field utilities with IR-driven recursion. The core now reads field types, optionality, and structure from FormischFieldIR instead of Valibot schema internals. Test helpers (vitest/utils.ts) gain a toFormisch wrapper so existing tests can pass IR-augmented schemas. 410 of 435 core tests pass; the 25 failures are exclusively in out-of-scope areas (tuples, unions, combinators) and are documented in the POC README.
Creates the reference adapter that transforms Valibot schemas into the Formisch IR. The transform walks Valibot schema types (object, array, string, number, boolean, date, bigint, wrappers, lazy, pipe) and emits FormischFieldIR nodes with optionality resolved at transform time. The toFormisch entry point wraps a schema with the ~formisch marker while preserving the ~standard passthrough for validation. 22 transform tests pass.
Creates the Zod adapter proving cross-library compatibility. The transform walks Zod's _def.typeName discriminant, mapping ZodObject, ZodArray, ZodString, ZodNumber, ZodBoolean, ZodDate, ZodBigInt, ZodOptional, ZodNullable, ZodDefault, and ZodEffects to the same FormischFieldIR structure the Valibot adapter produces. 19 transform tests pass.
The React useForm hook now calls schema['~standard'].validate instead of v.safeParseAsync, making it compatible with any Standard Schema (Valibot or Zod via their respective adapters). Adds a Vite + React demo (playgrounds/ir-demo) that renders the same form shape with either adapter, toggleable at runtime. The age field has IR type 'number', demonstrating type info flowing through from both libraries.
The parity suite defines identical form shapes in Valibot and Zod, transforms both via their adapters, and asserts the resulting IR trees are deeply equal (5 tests). The coercion suite feeds FormData through decodeFormData with each adapter's schema and verifies identical decoded output, including number coercion and boolean defaults (3 tests). All 8 tests pass.
Updates pnpm-workspace.yaml to include packages/adapters/*. Adds the POC README documenting: what this proves, the grep proof for zero library imports in core, how to run tests and the demo, out-of-scope features, open design questions, and alpha publish instructions. 💘 Generated with Crush Assisted-by: Crush:zai-coding-plan/glm-5.2
There was a problem hiding this comment.
24 issues found across 64 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/adapters/valibot/package.json">
<violation number="1" location="packages/adapters/valibot/package.json:43">
P3: The Valibot adapter lists `@formisch/zod` and `zod` as devDependencies, but no file in this package imports or references them. This looks like copy-paste from the Zod adapter and needlessly couples the two adapter packages; remove both entries. Valibot itself is already covered as both a peer and dev dependency.</violation>
</file>
<file name="packages/core/src/form/parity.test.ts">
<violation number="1" location="packages/core/src/form/parity.test.ts:13">
P2: The parity tests deliberately strip `getDefault()` via `stripGetDefault`, so default-value parity between the Valibot and Zod adapters is never actually asserted even though `getDefault` is a core IR field consumed by initialization. Both closures can be invoked and their results compared (they return a value), so the tests should assert `getDefault()` equality instead of dropping it entirely — otherwise a subtle default mismatch between the two adapters passes the 'identical IR' claim silently.</violation>
</file>
<file name="packages/core/src/types/form/form.qwik.ts">
<violation number="1" location="packages/core/src/types/form/form.qwik.ts:42">
P2: `initialInput` loses schema type checking for every adapter-produced schema because `toFormisch()` returns an unparameterized `FormSchema`; preserve Standard Schema input/output generics through `FormSchema` and both adapters so invalid initial values are rejected.</violation>
</file>
<file name="packages/core/src/field/setInitialFieldInput/setInitialFieldInput.test.ts">
<violation number="1" location="packages/core/src/field/setInitialFieldInput/setInitialFieldInput.test.ts:94">
P2: This tuple test asserts array behavior that the IR produced by toFormisch cannot deliver: transformValibot maps `v.tuple` to type 'unknown', so initializeFieldStore initializes `pair` as a value field (no `initialItems`, no children) rather than an array. The `kind === 'array'`, `initialItems.value`, and `children[0].initialInput` assertions fail once the schema is wrapped. Either add a tuple case to the transform so it produces array IR (with fixed-length handling in setInitialFieldInput, currently unreachable for tuples), or update the test to match the 'value'/unknown kind that tuples actually produce.</violation>
</file>
<file name="playgrounds/ir-demo/src/main.tsx">
<violation number="1" location="playgrounds/ir-demo/src/main.tsx:29">
P3: The demo never actually demonstrates validation or submission. `onSubmit` in the React wrapper is only fired through the `<Form>` component's submit handler (frameworks/react/src/components/Form/Form.tsx), but `FormDemo` renders no `<Form>`, no `<Field>` inputs, and no submit button—only a `<pre>` showing `name`/`errors`. As a result `console.log('[..] Submitted:', output)` never runs and `form.errors` is always `null`, despite the UI text telling users to 'Open the console to see submit output.' Consider wrapping the content in `<Form of={form} onSubmit={...}>` with a real submit button (and ideally `<Field>` inputs) so the parity demo actually exercises validation/submission through both adapters.</violation>
<violation number="2" location="playgrounds/ir-demo/src/main.tsx:38">
P3: The demo reads form.__name through a `as Record<string, unknown>` cast, but no '__name' property exists on the form store (fields live under getFieldStore/form[INTERNAL]), so this always shows 'N/A'. It also hides the missing property behind a cast. Read the field value through the store API or drop the display rather than casting to an untyped record.</violation>
</file>
<file name="packages/adapters/valibot/src/transform.ts">
<violation number="1" location="packages/adapters/valibot/src/transform.ts:24">
P1: Wrapping a self-recursive `v.lazy` schema overflows the stack during `toFormisch`, before a form can be created. Track visited lazy/schema nodes (or explicitly reject recursive lazy schemas) rather than recursively expanding the same getter result.</violation>
<violation number="2" location="packages/adapters/valibot/src/transform.ts:60">
P2: The `optional` flag is computed from the *original* schema via `isOptional(schema)`, while `unwrapValibot` strips wrappers to pick the concrete type. When a `pipe` wraps an optional wrapper — e.g. `v.pipe(v.optional(v.string()), v.minLength(1))` — the schema's `type` is `'pipe'`, so `isOptional` returns `false` even though the schema accepts `undefined`. The resulting IR marks the field non-optional (`nullish=false` at store init), so an empty/omitted value is not treated as nullish for this field. The same happens for any wrapper nested as a pipe's first stage. `isOptional` should detect optionality through `pipe` (the common `v.optional(v.pipe(...))` order happens to work only because the wrapper sits on the outside).</violation>
<violation number="3" location="packages/adapters/valibot/src/transform.ts:73">
P3: The `enum`/`picklist`/`literal` branch always emits `type: 'string'`, but these schemas can carry non-string values, e.g. `v.literal(5)`, `v.picklist([1, 2, 3])`, or a boolean literal. The IR then describes a numeric field as `'string'` while `getDefault()` still returns the number (e.g. `5`), and the core's `emptyInput[ir.type]` will seed it with `''`. This is acknowledged as a POC simplification, but it's a silent value/type mismatch worth capturing for the open design questions, since the numeric picklist case is common and could be mapped to `'number'` without much effort.</violation>
</file>
<file name="packages/core/src/types/form/form.ts">
<violation number="1" location="packages/core/src/types/form/form.ts:191">
P1: Adapter-backed forms now pass `unknown` to submit handlers, losing the library’s schema-derived output typing; preserve Standard Schema input/output generics through `FormSchema`/`toFormisch` so this handler receives the concrete parsed output type.</violation>
</file>
<file name="packages/adapters/zod/src/transform.ts">
<violation number="1" location="packages/adapters/zod/src/transform.ts:50">
P1: Numeric, boolean, or bigint literals and numeric native enums are decoded as strings, so valid Zod schemas can fail validation after form submission. Derive the IR type from the literal value or native-enum values instead of always using `'string'`.</violation>
<violation number="2" location="packages/adapters/zod/src/transform.ts:126">
P1: A lazy Zod object is treated as an unknown scalar, so recursive or lazily declared forms do not get their object fields initialized. Unwrap `ZodLazy` (with recursion protection) or explicitly reject/document this schema shape before attaching the IR.</violation>
</file>
<file name="packages/core/src/form/decodeFormData/decodeFormData.ts">
<violation number="1" location="packages/core/src/form/decodeFormData/decodeFormData.ts:41">
P2: Core format check fails because newly compressed control-flow blocks are not Prettier-formatted; run the configured formatter on this file before merging.</violation>
</file>
<file name="packages/adapters/valibot/src/toFormisch.ts">
<violation number="1" location="packages/adapters/valibot/src/toFormisch.ts:15">
P2: `toFormisch(v.string())` is typed as a valid `FormSchema` although it produces a scalar root; reject non-object roots (or narrow the accepted schema type) before returning so invalid form schemas cannot enter form APIs.</violation>
</file>
<file name="packages/core/src/field/initializeFieldStore/initializeFieldStore.test.ts">
<violation number="1" location="packages/core/src/field/initializeFieldStore/initializeFieldStore.test.ts:35">
P2: This piped-string test now routes through the test-only `toFormisch` in vitest/utils.ts, whose `unwrapValibot` omits the `'pipe'` case that the real valibot adapter (`adapters/valibot/src/transform.ts`) handles. As a result a piped schema is typed `'unknown'` instead of `'string'`, so `initializeFieldStore` looks up `emptyInput['unknown']` and the expected `''` empty-string default no longer matches — the test either fails or validates different IR than the adapter produces. Add the `pipe` unwrap step to the test helper so core tests mirror the adapter.</violation>
</file>
<file name="packages/core/src/form/decodeFormData/decodeFormData.test.ts">
<violation number="1" location="packages/core/src/form/decodeFormData/decodeFormData.test.ts:24">
P2: These out-of-scope tests (unions/variants/intersects and tuples) were rewritten to build schemas with toFormisch but their assertions still encode the old full Valibot behavior, which the POC's IR-based decoder can no longer produce (the IR only keeps the first union/intersect/variant option and collapses tuples to 'unknown'). The core test suite will therefore fail on these cases; either drop them from this POC file, `it.skip`/update them to assert the reduced IR behavior, or extend the IR transform to capture all combinator options and tuple item schemas.</violation>
</file>
<file name="packages/core/src/form/createFormStore/createFormStore.ts">
<violation number="1" location="packages/core/src/form/createFormStore/createFormStore.ts:16">
P1: Successful submissions in the non-React wrappers lose their parsed output because those wrappers still pass Valibot's `{ success, output }` result to this new StandardParseResult API, while core consumers read `result.value`. Updating every framework wrapper to call `config.schema['~standard'].validate` and normalize `{ issues, value }` like the React wrapper would preserve submitted data.</violation>
<violation number="2" location="packages/core/src/form/createFormStore/createFormStore.ts:30">
P1: Qwik forms fail during initialization because the schema clone strips the IR's `Map` properties before this line reads the root and hands it to `initializeFieldStore`. Preserving or rehydrating the IR (including `Map` and `getDefault`) across Qwik serialization would avoid the non-iterable `properties` error.</violation>
</file>
<file name="packages/core/src/field/initializeFieldStore/initializeFieldStore.ts">
<violation number="1" location="packages/core/src/field/initializeFieldStore/initializeFieldStore.ts:19">
P2: Configured schema defaults are ignored because this IR is stored but `ir.getDefault()` is never resolved before empty-input fallback. Resolve undefined initial input through `getDefault()` before initializing the field.</violation>
<violation number="2" location="packages/core/src/field/initializeFieldStore/initializeFieldStore.ts:55">
P1: Optional array items are initialized as required, converting missing values to configured empty input. Forward `ir.item!.optional` as the recursive nullish argument, matching object properties.</violation>
</file>
<file name="packages/core/src/vitest/utils.ts">
<violation number="1" location="packages/core/src/vitest/utils.ts:82">
P3: `getDefault()` reports synthetic empty values not defined by the schema, diverging from the adapter and the IR contract; return `v.getDefault(schema)` unchanged.</violation>
</file>
<file name="packages/core/src/types/schema/ir.ts">
<violation number="1" location="packages/core/src/types/schema/ir.ts:38">
P2: The single `optional` boolean conflates `optional` with `nullable`/`nullish`, losing the null-vs-undefined distinction. `initializeFieldStore` passes `childIR.optional` as its `nullish` flag (initializeFieldStore.ts:93), so a required `z.string().nullable()` field (both adapters set `optional: true`) initializes to `undefined` instead of the empty-input string, and `z.string().nullable()` rejects `undefined` on an empty submit — the field fails validation even when untouched. Consider modeling nullishness as its own flag (e.g. `readonly optional: boolean; readonly nullish: boolean;`) so nullable and optional fields are not collapsed.</violation>
</file>
<file name="packages/core/src/types/form/form.react.ts">
<violation number="1" location="packages/core/src/types/form/form.react.ts:20">
P2: The Standard Schema input/output type helpers are defined twice verbatim — `InferStandardInput`/`InferStandardOutput` are duplicated in both `form.ts` and `form.qwik.ts` — and the React entry point gets around the duplication with an indirect reflection trick: `FormOutput` re-derives the output type by matching against the base `SubmitHandler` signature (`extends (output: infer O) => unknown`) rather than consuming an exported helper. This spreads one concept across three files with subtly different mechanisms (two copies of the conditional types plus one inference hack), so a change to how the output type is modeled (e.g. how `types` optionality or the standard result is shaped) must be kept in sync in all three places, and the reflection silently breaks if the base `SubmitHandler` signature ever changes its parameter structure. Consider exporting `InferStandardInput`/`InferStandardOutput` once from `form.ts` and importing them in `form.qwik.ts` and `form.react.ts`, replacing `FormOutput` with the shared helper.</violation>
</file>
<file name="packages/core/src/array/resetItemState/resetItemState.ts">
<violation number="1" location="packages/core/src/array/resetItemState/resetItemState.ts:78">
P2: `isTuple` is now always `false`, silently disabling the fixed-length tuple handling this branch is documented to provide. The IR type system (`FormischFieldType` in packages/core/src/types/schema/ir.ts) only contains `'array'` — there is no `'tuple'` type — so inside the array kind branch `ir.type` is always `'array'` and `ir.type !== 'array'` can never be true. The comment above this line ('Tuples have a fixed number of children...') describes behavior that is now unreachable. Either tuples are explicitly out of scope (then this dead branch and its comment should be removed, and the tuple reset tests deleted) or the IR/adapter needs a tuple representation to restore the fixed-children reset path.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| case 'non_optional': | ||
| return unwrapValibot(schema.wrapped); | ||
| case 'lazy': | ||
| return unwrapValibot(schema.getter(undefined)); |
There was a problem hiding this comment.
P1: Wrapping a self-recursive v.lazy schema overflows the stack during toFormisch, before a form can be created. Track visited lazy/schema nodes (or explicitly reject recursive lazy schemas) rather than recursively expanding the same getter result.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/adapters/valibot/src/transform.ts, line 24:
<comment>Wrapping a self-recursive `v.lazy` schema overflows the stack during `toFormisch`, before a form can be created. Track visited lazy/schema nodes (or explicitly reject recursive lazy schemas) rather than recursively expanding the same getter result.</comment>
<file context>
@@ -0,0 +1,139 @@
+ case 'non_optional':
+ return unwrapValibot(schema.wrapped);
+ case 'lazy':
+ return unwrapValibot(schema.getter(undefined));
+ case 'pipe':
+ return unwrapValibot(schema.items[0]);
</file context>
| return transform(def.schema); | ||
| } | ||
|
|
||
| default: |
There was a problem hiding this comment.
P1: A lazy Zod object is treated as an unknown scalar, so recursive or lazily declared forms do not get their object fields initialized. Unwrap ZodLazy (with recursion protection) or explicitly reject/document this schema shape before attaching the IR.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/adapters/zod/src/transform.ts, line 126:
<comment>A lazy Zod object is treated as an unknown scalar, so recursive or lazily declared forms do not get their object fields initialized. Unwrap `ZodLazy` (with recursion protection) or explicitly reject/document this schema shape before attaching the IR.</comment>
<file context>
@@ -0,0 +1,133 @@
+ return transform(def.schema);
+ }
+
+ default:
+ return {
+ type: 'unknown',
</file context>
|
|
||
| case z.ZodFirstPartyTypeKind.ZodString: | ||
| return { | ||
| type: 'string', |
There was a problem hiding this comment.
P1: Numeric, boolean, or bigint literals and numeric native enums are decoded as strings, so valid Zod schemas can fail validation after form submission. Derive the IR type from the literal value or native-enum values instead of always using 'string'.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/adapters/zod/src/transform.ts, line 50:
<comment>Numeric, boolean, or bigint literals and numeric native enums are decoded as strings, so valid Zod schemas can fail validation after form submission. Derive the IR type from the literal value or native-enum values instead of always using `'string'`.</comment>
<file context>
@@ -0,0 +1,133 @@
+
+ case z.ZodFirstPartyTypeKind.ZodString:
+ return {
+ type: 'string',
+ optional: false,
+ getDefault: () => getZodDefault(schema),
</file context>
| store.isValidating = createSignal(false); | ||
|
|
||
| // Initialize field store hierarchy from schema | ||
| const ir = config.schema['~formisch'].root as FormischFieldIR; |
There was a problem hiding this comment.
P1: Qwik forms fail during initialization because the schema clone strips the IR's Map properties before this line reads the root and hands it to initializeFieldStore. Preserving or rehydrating the IR (including Map and getDefault) across Qwik serialization would avoid the non-iterable properties error.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/form/createFormStore/createFormStore.ts, line 30:
<comment>Qwik forms fail during initialization because the schema clone strips the IR's `Map` properties before this line reads the root and hands it to `initializeFieldStore`. Preserving or rehydrating the IR (including `Map` and `getDefault`) across Qwik serialization would avoid the non-iterable `properties` error.</comment>
<file context>
@@ -1,61 +1,40 @@
store.isValidating = createSignal(false);
- // Initialize field store hierarchy from schema
+ const ir = config.schema['~formisch'].root as FormischFieldIR;
initializeFieldStore(
store as InternalFormStore,
</file context>
| "devDependencies": { | ||
| "@formisch/core": "workspace:*", | ||
| "@formisch/eslint-config": "workspace:*", | ||
| "@formisch/zod": "workspace:*", |
There was a problem hiding this comment.
P3: The Valibot adapter lists @formisch/zod and zod as devDependencies, but no file in this package imports or references them. This looks like copy-paste from the Zod adapter and needlessly couples the two adapter packages; remove both entries. Valibot itself is already covered as both a peer and dev dependency.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/adapters/valibot/package.json, line 43:
<comment>The Valibot adapter lists `@formisch/zod` and `zod` as devDependencies, but no file in this package imports or references them. This looks like copy-paste from the Zod adapter and needlessly couples the two adapter packages; remove both entries. Valibot itself is already covered as both a peer and dev dependency.</comment>
<file context>
@@ -0,0 +1,53 @@
+ "devDependencies": {
+ "@formisch/core": "workspace:*",
+ "@formisch/eslint-config": "workspace:*",
+ "@formisch/zod": "workspace:*",
+ "tsdown": "^0.16.8",
+ "typescript": "~5.9.3",
</file context>
| <div style={{ flex: 1, minWidth: 300 }}> | ||
| <h3>{label}</h3> | ||
| <pre> | ||
| name (string): {(form as Record<string, unknown>).__name ?? 'N/A'} |
There was a problem hiding this comment.
P3: The demo reads form.__name through a as Record<string, unknown> cast, but no '__name' property exists on the form store (fields live under getFieldStore/form[INTERNAL]), so this always shows 'N/A'. It also hides the missing property behind a cast. Read the field value through the store API or drop the display rather than casting to an untyped record.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At playgrounds/ir-demo/src/main.tsx, line 38:
<comment>The demo reads form.__name through a `as Record<string, unknown>` cast, but no '__name' property exists on the form store (fields live under getFieldStore/form[INTERNAL]), so this always shows 'N/A'. It also hides the missing property behind a cast. Read the field value through the store API or drop the display rather than casting to an untyped record.</comment>
<file context>
@@ -0,0 +1,98 @@
+ <div style={{ flex: 1, minWidth: 300 }}>
+ <h3>{label}</h3>
+ <pre>
+ name (string): {(form as Record<string, unknown>).__name ?? 'N/A'}
+ {'\n'}
+ errors: {JSON.stringify(form.errors)}
</file context>
| return { | ||
| type: unwrapped.type, | ||
| optional, | ||
| getDefault: () => v.getDefault(schema) ?? EMPTY_DEFAULTS[unwrapped.type], |
There was a problem hiding this comment.
P3: getDefault() reports synthetic empty values not defined by the schema, diverging from the adapter and the IR contract; return v.getDefault(schema) unchanged.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/vitest/utils.ts, line 82:
<comment>`getDefault()` reports synthetic empty values not defined by the schema, diverging from the adapter and the IR contract; return `v.getDefault(schema)` unchanged.</comment>
<file context>
@@ -16,13 +19,142 @@ interface CreateTestStoreConfig {
+ return {
+ type: unwrapped.type,
+ optional,
+ getDefault: () => v.getDefault(schema) ?? EMPTY_DEFAULTS[unwrapped.type],
+ };
+ default:
</file context>
| function FormDemo({ schema, label }: { schema: FormSchema; label: string }) { | ||
| const form = useForm({ | ||
| schema, | ||
| onSubmit: (output) => { |
There was a problem hiding this comment.
P3: The demo never actually demonstrates validation or submission. onSubmit in the React wrapper is only fired through the <Form> component's submit handler (frameworks/react/src/components/Form/Form.tsx), but FormDemo renders no <Form>, no <Field> inputs, and no submit button—only a <pre> showing name/errors. As a result console.log('[..] Submitted:', output) never runs and form.errors is always null, despite the UI text telling users to 'Open the console to see submit output.' Consider wrapping the content in <Form of={form} onSubmit={...}> with a real submit button (and ideally <Field> inputs) so the parity demo actually exercises validation/submission through both adapters.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At playgrounds/ir-demo/src/main.tsx, line 29:
<comment>The demo never actually demonstrates validation or submission. `onSubmit` in the React wrapper is only fired through the `<Form>` component's submit handler (frameworks/react/src/components/Form/Form.tsx), but `FormDemo` renders no `<Form>`, no `<Field>` inputs, and no submit button—only a `<pre>` showing `name`/`errors`. As a result `console.log('[..] Submitted:', output)` never runs and `form.errors` is always `null`, despite the UI text telling users to 'Open the console to see submit output.' Consider wrapping the content in `<Form of={form} onSubmit={...}>` with a real submit button (and ideally `<Field>` inputs) so the parity demo actually exercises validation/submission through both adapters.</comment>
<file context>
@@ -0,0 +1,98 @@
+function FormDemo({ schema, label }: { schema: FormSchema; label: string }) {
+ const form = useForm({
+ schema,
+ onSubmit: (output) => {
+ console.log(`[${label}] Submitted:`, output);
+ },
</file context>
| transform(unwrapped.entries[key] as v.GenericSchema) | ||
| ); | ||
| } | ||
| return { |
There was a problem hiding this comment.
P3: The enum/picklist/literal branch always emits type: 'string', but these schemas can carry non-string values, e.g. v.literal(5), v.picklist([1, 2, 3]), or a boolean literal. The IR then describes a numeric field as 'string' while getDefault() still returns the number (e.g. 5), and the core's emptyInput[ir.type] will seed it with ''. This is acknowledged as a POC simplification, but it's a silent value/type mismatch worth capturing for the open design questions, since the numeric picklist case is common and could be mapped to 'number' without much effort.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/adapters/valibot/src/transform.ts, line 73:
<comment>The `enum`/`picklist`/`literal` branch always emits `type: 'string'`, but these schemas can carry non-string values, e.g. `v.literal(5)`, `v.picklist([1, 2, 3])`, or a boolean literal. The IR then describes a numeric field as `'string'` while `getDefault()` still returns the number (e.g. `5`), and the core's `emptyInput[ir.type]` will seed it with `''`. This is acknowledged as a POC simplification, but it's a silent value/type mismatch worth capturing for the open design questions, since the numeric picklist case is common and could be mapped to `'number'` without much effort.</comment>
<file context>
@@ -0,0 +1,139 @@
+ transform(unwrapped.entries[key] as v.GenericSchema)
+ );
+ }
+ return {
+ type: 'object',
+ optional,
</file context>
Upstream main added a React Native adapter, dirty-state recomputation (open-circle#193), and concurrent validation race fix (open-circle#190) since the POC branched. After rebasing, these new files still referenced the old valibot-coupled types. This commit: - Updates field.react-native.ts and form.react-native.ts to use IR + Standard Schema types - Wraps new test schemas with toFormisch() and converts valibot-specific SafeParseResult types to StandardParseResult - Fixes adapter package.json exports to match built .mts/.d.mts output
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
packages/core/src/types/form/form.react-native.ts (1)
14-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
StandardSchemaV1.InferInputandStandardSchemaV1.InferOutput.Replace the duplicated aliases in
form.ts,form.qwik.ts, andform.react-native.ts. This aligns inference with the public Standard Schema type contract and removes duplicate code.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/types/form/form.react-native.ts` around lines 14 - 28, Replace the local InferStandardInput and InferStandardOutput aliases in the form type modules with StandardSchemaV1.InferInput and StandardSchemaV1.InferOutput, updating references and imports as needed. Apply the same change consistently in form.ts, form.qwik.ts, and form.react-native.ts while preserving existing type behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/src/field/getElementInput/getElementInput.test.ts`:
- Around line 219-220: Update the multiple-file test using createTestStore and
toFormisch so its documents field uses the array schema v.array(v.any()) instead
of v.any(), preserving the expected files-array behavior from getElementInput.
In `@packages/core/src/field/initializeFieldStore/initializeFieldStore.ts`:
- Around line 33-72: Add end-to-end tuple support by extending FormischFieldType
and updating both adapters to emit tuple IR, ensuring tuple schemas initialize
through the array-store path and tuple-specific tests/branches remain valid. In
initializeFieldStore, when reinitializing an existing array store, clear or
replace internalFieldStore.children before rebuilding from initialInput so
initialItems matches the new input length.
In `@SCHEMA_IR_POC.md`:
- Around line 17-26: Align the import-verification claims in SCHEMA_IR_POC.md
with the commands’ actual scope: either narrow the statement about mentions to
non-test source and exact single-quoted imports, or broaden the checks to
include both quote styles, re-exports, and dynamic imports before retaining the
broader claim.
- Around line 77-81: Update the data-flow code fence containing the Valibot,
Zod, FormischFieldIR, and decodeFormData diagram to declare the text language
identifier, resolving the Markdownlint MD040 warning.
---
Nitpick comments:
In `@packages/core/src/types/form/form.react-native.ts`:
- Around line 14-28: Replace the local InferStandardInput and
InferStandardOutput aliases in the form type modules with
StandardSchemaV1.InferInput and StandardSchemaV1.InferOutput, updating
references and imports as needed. Apply the same change consistently in form.ts,
form.qwik.ts, and form.react-native.ts while preserving existing type behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: dc6f08d8-0e41-415c-b422-bb0c800ddc18
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (66)
SCHEMA_IR_POC.mdframeworks/react/src/hooks/useForm/useForm.tspackages/adapters/valibot/package.jsonpackages/adapters/valibot/src/index.tspackages/adapters/valibot/src/toFormisch.tspackages/adapters/valibot/src/transform.tspackages/adapters/valibot/tests/transform.test.tspackages/adapters/valibot/tsconfig.jsonpackages/adapters/valibot/tsdown.config.tspackages/adapters/valibot/vitest.config.tspackages/adapters/zod/package.jsonpackages/adapters/zod/src/index.tspackages/adapters/zod/src/toFormisch.tspackages/adapters/zod/src/transform.tspackages/adapters/zod/tests/transform.test.tspackages/adapters/zod/tsconfig.jsonpackages/adapters/zod/tsdown.config.tspackages/adapters/zod/vitest.config.tspackages/core/package.jsonpackages/core/src/array/copyItemState/copyItemState.test.tspackages/core/src/array/copyItemState/copyItemState.tspackages/core/src/array/resetItemState/resetItemState.test.tspackages/core/src/array/resetItemState/resetItemState.tspackages/core/src/array/swapItemState/swapItemState.test.tspackages/core/src/array/swapItemState/swapItemState.tspackages/core/src/field/focusFieldElement/focusFieldElement.react-native.test.tspackages/core/src/field/focusFieldElement/focusFieldElement.test.tspackages/core/src/field/getDirtyFieldInput/getDirtyFieldInput.test.tspackages/core/src/field/getElementInput/getElementInput.test.tspackages/core/src/field/getFieldBool/getFieldBool.test.tspackages/core/src/field/getFieldInput/getFieldInput.test.tspackages/core/src/field/getFieldStore/getFieldStore.test.tspackages/core/src/field/initializeFieldStore/initializeFieldStore.test.tspackages/core/src/field/initializeFieldStore/initializeFieldStore.tspackages/core/src/field/setFieldBool/setFieldBool.test.tspackages/core/src/field/setFieldInput/setFieldInput.test.tspackages/core/src/field/setFieldInput/setFieldInput.tspackages/core/src/field/setInitialFieldInput/setInitialFieldInput.test.tspackages/core/src/field/setInitialFieldInput/setInitialFieldInput.tspackages/core/src/field/walkFieldStore/walkFieldStore.test.tspackages/core/src/form/createFormStore/createFormStore.test.tspackages/core/src/form/createFormStore/createFormStore.tspackages/core/src/form/decodeFormData/decodeFormData.test.tspackages/core/src/form/decodeFormData/decodeFormData.tspackages/core/src/form/parity.test.tspackages/core/src/form/validateFormInput/validateFormInput.test.tspackages/core/src/form/validateFormInput/validateFormInput.tspackages/core/src/form/validateIfRequired/validateIfRequired.test.tspackages/core/src/types/field/field.react-native.tspackages/core/src/types/field/field.tspackages/core/src/types/form/form.qwik.tspackages/core/src/types/form/form.react-native.tspackages/core/src/types/form/form.react.tspackages/core/src/types/form/form.tspackages/core/src/types/schema/index.tspackages/core/src/types/schema/ir.tspackages/core/src/types/schema/schema.test-d.tspackages/core/src/types/schema/schema.tspackages/core/src/vitest/utils.tspackages/core/vitest.config.tsplaygrounds/ir-demo/index.htmlplaygrounds/ir-demo/package.jsonplaygrounds/ir-demo/src/main.tsxplaygrounds/ir-demo/tsconfig.jsonplaygrounds/ir-demo/vite.config.tspnpm-workspace.yaml
🚧 Files skipped from review as they are similar to previous changes (59)
- packages/core/package.json
- packages/adapters/zod/tsdown.config.ts
- pnpm-workspace.yaml
- packages/core/src/types/schema/index.ts
- packages/adapters/valibot/vitest.config.ts
- packages/core/src/array/swapItemState/swapItemState.ts
- packages/adapters/valibot/tsdown.config.ts
- packages/core/src/field/getFieldInput/getFieldInput.test.ts
- frameworks/react/src/hooks/useForm/useForm.ts
- packages/adapters/valibot/src/toFormisch.ts
- playgrounds/ir-demo/index.html
- packages/core/src/array/copyItemState/copyItemState.ts
- packages/adapters/zod/tests/transform.test.ts
- packages/adapters/zod/src/index.ts
- packages/adapters/valibot/src/index.ts
- packages/core/src/field/walkFieldStore/walkFieldStore.test.ts
- packages/core/src/array/copyItemState/copyItemState.test.ts
- playgrounds/ir-demo/tsconfig.json
- packages/core/src/field/setInitialFieldInput/setInitialFieldInput.test.ts
- packages/adapters/zod/vitest.config.ts
- packages/core/src/field/setFieldBool/setFieldBool.test.ts
- packages/adapters/zod/tsconfig.json
- packages/adapters/zod/src/toFormisch.ts
- packages/adapters/zod/package.json
- packages/core/src/form/createFormStore/createFormStore.ts
- packages/core/src/field/focusFieldElement/focusFieldElement.test.ts
- packages/core/src/field/setInitialFieldInput/setInitialFieldInput.ts
- packages/core/src/array/resetItemState/resetItemState.test.ts
- packages/adapters/valibot/tsconfig.json
- packages/core/src/types/field/field.ts
- packages/core/src/array/swapItemState/swapItemState.test.ts
- packages/core/src/array/resetItemState/resetItemState.ts
- packages/adapters/zod/src/transform.ts
- packages/core/src/types/form/form.react.ts
- playgrounds/ir-demo/src/main.tsx
- packages/adapters/valibot/src/transform.ts
- packages/adapters/valibot/tests/transform.test.ts
- packages/core/vitest.config.ts
- packages/core/src/types/schema/schema.ts
- packages/core/src/form/decodeFormData/decodeFormData.ts
- packages/core/src/types/schema/ir.ts
- packages/core/src/form/validateIfRequired/validateIfRequired.test.ts
- packages/core/src/field/setFieldInput/setFieldInput.test.ts
- packages/core/src/form/createFormStore/createFormStore.test.ts
- packages/adapters/valibot/package.json
- packages/core/src/form/validateFormInput/validateFormInput.ts
- packages/core/src/field/initializeFieldStore/initializeFieldStore.test.ts
- packages/core/src/field/getFieldStore/getFieldStore.test.ts
- packages/core/src/types/form/form.ts
- packages/core/src/field/getDirtyFieldInput/getDirtyFieldInput.test.ts
- playgrounds/ir-demo/vite.config.ts
- packages/core/src/field/setFieldInput/setFieldInput.ts
- playgrounds/ir-demo/package.json
- packages/core/src/types/schema/schema.test-d.ts
- packages/core/src/types/form/form.qwik.ts
- packages/core/src/form/decodeFormData/decodeFormData.test.ts
- packages/core/src/vitest/utils.ts
- packages/core/src/form/parity.test.ts
- packages/core/src/field/getFieldBool/getFieldBool.test.ts
| test('should return files array for multiple file input', () => { | ||
| const store = createTestStore(v.object({ documents: v.array(v.any()) })); | ||
| const store = createTestStore(toFormisch(v.object({ documents: v.any() }))); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- candidate test ---'
sed -n '1,280p' packages/core/src/field/getElementInput/getElementInput.test.ts
printf '%s\n' '--- related implementation and symbols ---'
fd -t f 'getElementInput' packages/core/src
rg -n -C 8 'getElementInput|files\[0\]|kind.*array|toFormisch|createTestStore' packages/core/src/field packages/core/src/form packages/core/src/typesRepository: open-circle/formisch
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- target test ---'
sed -n '190,245p' packages/core/src/field/getElementInput/getElementInput.test.ts
printf '%s\n' '--- implementation files ---'
fd -t f . packages/core/src/field/getElementInput packages/core/src/field/getFieldInput packages/core/src/vitest
for file in $(fd -t f . packages/core/src/field/getElementInput); do
printf '\n--- %s ---\n' "$file"
sed -n '1,220p' "$file"
done
printf '%s\n' '--- focused references ---'
rg -n -C 6 'element\.files|files\[0\]|kind === .array.|return .*files' packages/core/src/field/getElementInput packages/core/src/field/getFieldInput packages/core/srcRepository: open-circle/formisch
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
test_path = Path("packages/core/src/field/getElementInput/getElementInput.test.ts")
impl_path = Path("packages/core/src/field/getElementInput/getElementInput.ts")
test = test_path.read_text()
impl = impl_path.read_text()
test_match = re.search(
r"test\('should return files array for multiple file input', \(\) => \{(.*?)\n \}\);",
test,
re.S,
)
assert test_match, "multiple-file test not found"
case = test_match.group(1)
assert "documents: v.any()" in case
assert "input.multiple = true" in case
assert ").toStrictEqual([file1, file2]);" in case
file_branch = re.search(
r"if \(element\.type === 'file'\) \{(.*?)\n \}\n\n // Return element value",
impl,
re.S,
)
assert file_branch, "file-input branch not found"
branch = file_branch.group(1)
assert "if (internalFieldStore.kind === 'array')" in branch
assert "return element.files[0]" in branch
print("multiple-file test uses v.any(): yes")
print("v.any() selects the non-array file branch: yes")
print("non-array branch returns element.files[0]: yes")
print("expected [file1, file2] therefore requires an array schema: yes")
PYRepository: open-circle/formisch
Length of output: 353
Restore the array schema for the multiple-file test.
Use v.array(v.any()); v.any() makes getElementInput return only element.files[0].
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/core/src/field/getElementInput/getElementInput.test.ts` around lines
219 - 220, Update the multiple-file test using createTestStore and toFormisch so
its documents field uses the array schema v.array(v.any()) instead of v.any(),
preserving the expected files-array behavior from getElementInput.
| switch (ir.type) { | ||
| case 'array': { | ||
| if (internalFieldStore.kind && internalFieldStore.kind !== 'array') { | ||
| throw new Error( | ||
| `Store initialized as "${internalFieldStore.kind}" cannot be reinitialized as "array"` | ||
| ); | ||
| } | ||
|
|
||
| // Set kind to array | ||
| internalFieldStore.kind = 'array'; | ||
|
|
||
| // Initialize array-specific properties | ||
| if (internalFieldStore.kind === 'array') { | ||
| // Initialize children array if not exists | ||
| internalFieldStore.children ??= []; | ||
|
|
||
| // If schema is dynamic array, initialize children from input | ||
| if (schema.type === 'array') { | ||
| // If initial input provided, initialize children | ||
| if (initialInput) { | ||
| // Initialize child for each input item | ||
| for ( | ||
| let index = 0; | ||
| // @ts-expect-error | ||
| index < initialInput.length; | ||
| index++ | ||
| ) { | ||
| // Create empty child object | ||
| // @ts-expect-error | ||
| internalFieldStore.children[index] = {}; | ||
|
|
||
| // Initialize field store for child | ||
| initializeFieldStore( | ||
| internalFormStore, | ||
| internalFieldStore.children[index], | ||
| schema.item as FieldSchema, | ||
| // @ts-expect-error | ||
| initialInput[index], | ||
| [...path, index] | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| // Otherwise, if schema is fixed tuple, initialize children from schema | ||
| } else { | ||
| // Initialize child for each tuple item | ||
| for (let index = 0; index < schema.items.length; index++) { | ||
| // Create empty child object | ||
| if (initialInput) { | ||
| for ( | ||
| let index = 0; | ||
| // @ts-expect-error | ||
| index < initialInput.length; | ||
| index++ | ||
| ) { | ||
| // @ts-expect-error | ||
| internalFieldStore.children[index] = {}; | ||
|
|
||
| // Initialize field store for child | ||
| initializeFieldStore( | ||
| internalFormStore, | ||
| internalFieldStore.children[index], | ||
| schema.items[index] as FieldSchema, | ||
| ir.item!, | ||
| // @ts-expect-error | ||
| initialInput?.[index], | ||
| initialInput[index], | ||
| [...path, index] | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| // Set array input (nullish or true) | ||
| const arrayInput = | ||
| nullish && initialInput == null ? initialInput : true; | ||
| const arrayInput = nullish && initialInput == null ? initialInput : true; | ||
| internalFieldStore.initialInput = createSignal(arrayInput); | ||
| internalFieldStore.startInput = createSignal(arrayInput); | ||
| internalFieldStore.input = createSignal(arrayInput); | ||
|
|
||
| // Set items with unique IDs for each child | ||
| const initialItems = internalFieldStore.children.map(createId); | ||
| internalFieldStore.initialItems = createSignal(initialItems); | ||
| internalFieldStore.startItems = createSignal(initialItems); | ||
| internalFieldStore.items = createSignal(initialItems); | ||
| } | ||
|
|
||
| // Otherwise, if schema is object, initialize as object field | ||
| } else if ( | ||
| schema.type === 'loose_object' || | ||
| schema.type === 'object' || | ||
| schema.type === 'strict_object' | ||
| ) { | ||
| // If already initialized as different kind, throw error | ||
| break; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- FormischFieldType definition ---'
fd -t f 'ir.ts' packages/core/src/types --exec cat -n {}
echo '--- tuple handling across core ---'
rg -n -C 5 "ir\.type\s*(!==|===)\s*'array'|'tuple'" packages/core/src packages/adaptersRepository: open-circle/formisch
Length of output: 8995
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- initializeFieldStore implementation and callers ---'
cat -n packages/core/src/field/initializeFieldStore/initializeFieldStore.ts
rg -n -C 4 "initializeFieldStore\(" packages/core/src
echo '--- tuple-related store logic ---'
cat -n packages/core/src/field/setFieldInput/setFieldInput.ts
cat -n packages/core/src/field/setInitialFieldInput/setInitialFieldInput.ts
cat -n packages/core/src/array/resetItemState/resetItemState.ts
echo '--- field-store shape and IR construction ---'
rg -n -C 5 "interface InternalFieldStore|type InternalFieldStore|kind:|FormischFieldIR|type: 'array'|type: 'object'" packages/core/src packages/adaptersRepository: open-circle/formisch
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- adapter IR type production ---'
rg -n -C 6 "type:\s*['\"](array|object|tuple)['\"]|tuple|FormischFieldType" packages/adapters packages/core/src --glob '*.ts' --glob '*.tsx'
echo '--- initializeFieldStore exports and test coverage ---'
rg -n -C 4 "initializeFieldStore|setInitialFieldInput|resetItemState" packages/core/src/index.ts packages/core/src packages/*/src --glob '*.test.ts' --glob '*.test.tsx' --glob '*.ts' | head -n 400
echo '--- all direct initializeFieldStore call sites ---'
rg -n "initializeFieldStore\(" packages --glob '*.ts' --glob '*.tsx'
echo '--- standalone state-shape probe ---'
python3 - <<'PY'
def initialize(children, initial_input):
# Model only the array branch at issue.
children = list(children)
if initial_input:
for index in range(len(initial_input)):
children[index] = f'new-{index}'
initial_items = list(children)
return children, initial_items
for old, new in [(['a', 'b', 'c'], [1]), (['a', 'b', 'c'], [])]:
children, items = initialize(old, new)
print(f'old={old!r}, input={new!r} -> children={children!r}, items={items!r}, input_length={len(new)}')
PYRepository: open-circle/formisch
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- Valibot transform cases ---'
cat -n packages/adapters/valibot/src/transform.ts | sed -n '1,150p'
echo '--- Zod transform cases ---'
cat -n packages/adapters/zod/src/transform.ts | sed -n '1,120p'
echo '--- tuple test expectations and test helper ---'
cat -n packages/core/src/field/initializeFieldStore/initializeFieldStore.test.ts | sed -n '1,40p;285,315p;540,595p'
rg -n -C 8 "function createTestStore|const createTestStore|createTestStore" packages/core/src/vitest packages/core/src/field/initializeFieldStore --glob '*.ts'
echo '--- direct call sites and exported API ---'
rg -n "initializeFieldStore" packages/core/src --glob '*.ts' --glob '!*.test.ts'
rg -n "from .*initializeFieldStore|initializeFieldStore" packages/core/src/index.ts packages/core/src/field/index.ts packages/core/src/field/initializeFieldStore/index.tsRepository: open-circle/formisch
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
ir = Path("packages/core/src/types/schema/ir.ts").read_text()
enum_values = re.findall(r"\| '([^']+)'", ir.split("export type FormischFieldType =", 1)[1].split(";", 1)[0])
print("FormischFieldType members:", enum_values)
for path in [
"packages/adapters/valibot/src/transform.ts",
"packages/adapters/zod/src/transform.ts",
]:
text = Path(path).read_text()
produced = sorted(set(re.findall(r"type:\s*'([^']+)'", text)))
print(f"{path} produces:", produced)
print("tuple in FormischFieldType:", "tuple" in enum_values)
PY
python3 - <<'PY'
def array_state(existing_children, initial_input):
children = list(existing_children)
if initial_input:
for index in range(len(initial_input)):
children[index] = f"child-{index}"
initial_items = list(children)
return len(children), len(initial_items)
for existing, initial in [(["a", "b", "c"], [1]), (["a", "b", "c"], [])]:
child_count, item_count = array_state(existing, initial)
print({
"existing_children": len(existing),
"initial_input": len(initial),
"children_after": child_count,
"initial_items": item_count,
"coupled": child_count == len(initial),
})
PY
echo '--- direct non-test call sites ---'
rg -n "initializeFieldStore\(" packages/core/src --glob '*.ts' --glob '!*.test.ts'
echo '--- initializer export ---'
cat -n packages/core/src/field/initializeFieldStore/index.tsRepository: open-circle/formisch
Length of output: 1817
Support tuples in the IR and adapters.
FormischFieldType has no 'tuple' member, and neither adapter emits tuple IR. Tuple schemas therefore become value stores, while tuple-specific branches and tests expect array stores. Add tuple support or remove the tuple-specific logic and tests.
When same-store array reinitialization is supported, clear stale children before rebuilding. Otherwise, initialItems can exceed the new input length.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/core/src/field/initializeFieldStore/initializeFieldStore.ts` around
lines 33 - 72, Add end-to-end tuple support by extending FormischFieldType and
updating both adapters to emit tuple IR, ensuring tuple schemas initialize
through the array-store path and tuple-specific tests/branches remain valid. In
initializeFieldStore, when reinitializing an existing array store, clear or
replace internalFieldStore.children before rebuilding from initialInput so
initialItems matches the new input length.
| # Zero valibot imports in core source (excluding tests) | ||
| grep -r "from 'valibot'" packages/core/src/ --include="*.ts" | grep -v ".test.ts" | grep -v "vitest/" | ||
| # Result: (empty) | ||
|
|
||
| # Zero zod imports in core source | ||
| grep -r "from 'zod'" packages/core/src/ --include="*.ts" | grep -v ".test.ts" | grep -v "vitest/" | ||
| # Result: (empty) | ||
| ``` | ||
|
|
||
| The only mentions of "valibot" or "zod" in `packages/core/src/` are in JSDoc comments, not imports. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Limit the import proof to the scope it scans.
The commands exclude test files and match only exact single-quoted static imports. They do not prove the broader claim on Line 26 about all mentions in packages/core/src/. Either narrow the claim to non-test source or expand the search to cover quote styles, re-exports, and dynamic imports.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SCHEMA_IR_POC.md` around lines 17 - 26, Align the import-verification claims
in SCHEMA_IR_POC.md with the commands’ actual scope: either narrow the statement
about mentions to non-test source and exact single-quoted imports, or broaden
the checks to include both quote styles, re-exports, and dynamic imports before
retaining the broader claim.
| ``` | ||
| Valibot schema ──┐ ┌── initializeFieldStore (IR-driven) | ||
| ├──→ FormischFieldIR ──→ decodeFormData (IR-driven) | ||
| Zod schema ──────┘ └── validateFormInput (~standard.validate) | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add a language to the data-flow code fence.
Markdownlint reports MD040 for the fence starting on Line 77. Use a text language identifier.
Suggested fix
-```
+```text📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ``` | |
| Valibot schema ──┐ ┌── initializeFieldStore (IR-driven) | |
| ├──→ FormischFieldIR ──→ decodeFormData (IR-driven) | |
| Zod schema ──────┘ └── validateFormInput (~standard.validate) | |
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 77-77: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SCHEMA_IR_POC.md` around lines 77 - 81, Update the data-flow code fence
containing the Valibot, Zod, FormischFieldIR, and decodeFormData diagram to
declare the text language identifier, resolving the Markdownlint MD040 warning.
Source: Linters/SAST tools
What this proves
@formisch/corecan drive identical forms via either a Valibot or Zod adapter, with zero Valibot imports in the core source. The IR (intermediate representation) abstracts over validation library specifics, and both adapters produce structurally identical IR for the same schema shape — verified by parity tests.mainThis PR targets
mainonly because GitHub requires an existing branch. Please create apoc/schema-ir(ordev) branch and retarget this PR before merging. The goal is to let pilot/beta users test the POC directly: by keeping it on a dedicated branch, you can publish@formisch/core@beta,@formisch/valibot@beta, and@formisch/zod@betawithout affecting the stable release line. Users opt in withnpm install @formisch/core@beta. Once validated, the branch merges tomain.Commits (reviewable in order)
FormischFieldIR,FormischSchematypes;Schemabecomes Standard Schema + IR markerinitializeFieldStore,decodeFormData,createFormStore,validateFormInputconsume IR instead of Valibot internals@formisch/valibotadapter — reference adapter, 22 tests@formisch/zodadapter — cross-library proof, 19 testsuseForm+ demo harness — single framework change; demo toggles adapters at runtimedecodeFormDataoutputThe grep proof
Tests
@formisch/valibot@formisch/zodAll three packages build successfully.
Out of scope (documented in README)
Unions, variants, intersects, tuples, records,
object_with_rest, ArkType/TypeBox/Yup adapters, non-React framework wrappers, client-side number coercion beyonddecodeFormData.Open design questions
getDefault()is a closure — needs serialization strategy for server-to-client transfer.Full details in
SCHEMA_IR_POC.md.Summary by CodeRabbit
New Features
Documentation
Tests