@maestro-js/agent-skills 1.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,231 @@
1
+ ---
2
+ name: exceptions
3
+ description: "Centralized exception reporting with automatic severity mapping and structured context, backed by @maestro-js/log. Use when working with @maestro-js/exceptions — reporting errors, silencing expected exceptions, attaching tags/extra metadata to error reports, or integrating exception handling into application services. Key capabilities: report exceptions with auto-detected severity (Error maps to 'error', non-Error maps to 'warn'), override log level via context, ignore/unignore specific exception instances, attach structured tags and extra data to reports. Follows the Provider pattern from @maestro-js/service-registry."
4
+ ---
5
+
6
+ # @maestro-js/exceptions
7
+
8
+ Centralized exception reporting that forwards exceptions as structured log entries through `@maestro-js/log`. Severity is auto-detected (`Error` instances log at `'error'`, everything else at `'warn'`) but can be overridden per report.
9
+
10
+ ## Quick Setup
11
+
12
+ ```ts
13
+ import { Log } from '@maestro-js/log'
14
+ import { Exceptions } from '@maestro-js/exceptions'
15
+
16
+ // 1. Create a logger
17
+ const logger = Log.create<[Exceptions.LogMessage]>({
18
+ transports: [{ write(message) { console.log(message) } }]
19
+ })
20
+
21
+ // 2. Create and register the exceptions service
22
+ const service = Exceptions.Provider.create({ logger })
23
+ Exceptions.Provider.register('default', service)
24
+
25
+ // 3. Use the facade directly
26
+ Exceptions.report(new Error('disk full'))
27
+ ```
28
+
29
+ ## Provider Pattern
30
+
31
+ Follows the standard Maestro service-registry pattern.
32
+
33
+ | Export | Purpose |
34
+ |---|---|
35
+ | `Exceptions.Provider.create(config)` | Factory — accepts `{ logger }` and returns an `ExceptionsService` |
36
+ | `Exceptions.Provider.register(name, service)` | Register a named instance in the registry |
37
+ | `Exceptions.provider(key)` | Resolve a named instance |
38
+ | `Exceptions.report(...)` / `Exceptions.ignore(...)` / etc. | Facade — delegates to the `'default'` provider |
39
+
40
+ ### Config
41
+
42
+ ```ts
43
+ interface ExceptionsServiceConfig {
44
+ logger: Log.LogFunctions<[Exceptions.LogMessage]>
45
+ }
46
+ ```
47
+
48
+ The `logger` must be a typed `Log` instance parameterized with `[Exceptions.LogMessage]`. Typically created via `Log.create<[Exceptions.LogMessage]>({ transports: [...] })` or by calling `.channel()` on an existing logger.
49
+
50
+ ## API Reference
51
+
52
+ ### `report(exception: unknown, context?: Exceptions.Context): void`
53
+
54
+ Log an exception through the configured logger. Skips exceptions marked by `ignore()`.
55
+
56
+ - `Error` instances default to `'error'` level.
57
+ - Non-Error values (strings, objects, etc.) default to `'warn'` level.
58
+ - Override level, attach `tags`, or add `extra` metadata via the context parameter.
59
+
60
+ ```ts
61
+ // Basic error report
62
+ Exceptions.report(new Error('disk full'))
63
+
64
+ // Non-Error value — logged at 'warn'
65
+ Exceptions.report('something broke')
66
+
67
+ // Override level and attach metadata
68
+ Exceptions.report(err, {
69
+ level: 'emergency',
70
+ tags: { userId: '42', region: 'us-east' },
71
+ extra: { requestBody: body }
72
+ })
73
+ ```
74
+
75
+ ### `ignore<E>(exception: E): E`
76
+
77
+ Mark an exception so future `report()` calls silently skip it. Returns the same reference.
78
+
79
+ Only object-type values (Error instances, plain objects, arrays) are tracked via an internal `WeakMap`. Primitives (string, number, null, undefined) are returned unchanged but not tracked.
80
+
81
+ ```ts
82
+ const err = new Error('expected')
83
+ Exceptions.ignore(err)
84
+ Exceptions.report(err) // no-op — silently skipped
85
+ ```
86
+
87
+ ### `unignore<E>(exception: E): E`
88
+
89
+ Reverse a previous `ignore()` call, allowing the exception to be reported again. Returns the same reference. No-op if the exception was never ignored or is a primitive.
90
+
91
+ ```ts
92
+ Exceptions.unignore(err)
93
+ Exceptions.report(err) // now logged again
94
+ ```
95
+
96
+ ### `isIgnored(exception: unknown): boolean`
97
+
98
+ Check whether an exception has been previously marked by `ignore()`. Returns `false` for primitives.
99
+
100
+ ```ts
101
+ Exceptions.isIgnored(err) // true if ignore() was called on this reference
102
+ ```
103
+
104
+ ## Types
105
+
106
+ ### `Exceptions.LogMessage`
107
+
108
+ Structured payload written to the logger on each `report()` call.
109
+
110
+ ```ts
111
+ interface LogMessage {
112
+ exception: any
113
+ tags?: { [key: string]: Primitive }
114
+ extra?: Record<string, unknown>
115
+ }
116
+ ```
117
+
118
+ `Primitive` is `number | string | boolean | bigint | symbol | null | undefined`.
119
+
120
+ ### `Exceptions.Context`
121
+
122
+ Optional metadata passed to `report()`.
123
+
124
+ ```ts
125
+ type Context = {
126
+ tags?: { [key: string]: Primitive }
127
+ extra?: Record<string, unknown>
128
+ level?: Log.Level
129
+ }
130
+ ```
131
+
132
+ When `level` is omitted, severity is auto-detected from the exception type.
133
+
134
+ ## Common Patterns
135
+
136
+ ### Error Reporting with Context
137
+
138
+ Attach structured tags for filtering and extra data for debugging.
139
+
140
+ ```ts
141
+ try {
142
+ await processOrder(orderId)
143
+ } catch (err) {
144
+ Exceptions.report(err, {
145
+ tags: { orderId, userId: ctx.userId },
146
+ extra: { payload: ctx.body }
147
+ })
148
+ }
149
+ ```
150
+
151
+ ### Silencing Expected Exceptions
152
+
153
+ Mark known/expected exceptions to prevent noise in logs.
154
+
155
+ ```ts
156
+ const err = new Error('expected timeout')
157
+ const ignored = Exceptions.ignore(err)
158
+ // ... pass ignored around; any report() call with this reference is a no-op
159
+ Exceptions.report(ignored) // silent
160
+
161
+ // Later, re-enable reporting
162
+ Exceptions.unignore(ignored)
163
+ Exceptions.report(ignored) // now logged
164
+ ```
165
+
166
+ ### Multiple Named Providers
167
+
168
+ Register separate exception services for different subsystems.
169
+
170
+ ```ts
171
+ const apiService = Exceptions.Provider.create({ logger: apiLogger })
172
+ Exceptions.Provider.register('api', apiService)
173
+
174
+ const workerService = Exceptions.Provider.create({ logger: workerLogger })
175
+ Exceptions.Provider.register('worker', workerService)
176
+
177
+ // Use a specific provider
178
+ Exceptions.provider('api').report(err)
179
+ Exceptions.provider('worker').report(err)
180
+ ```
181
+
182
+ ### Isolation Between Services
183
+
184
+ Each service instance maintains its own ignore state. Ignoring an exception in one service does not affect other services.
185
+
186
+ ```ts
187
+ const a = Exceptions.Provider.create({ logger: loggerA })
188
+ const b = Exceptions.Provider.create({ logger: loggerB })
189
+
190
+ a.ignore(err)
191
+ a.isIgnored(err) // true
192
+ b.isIgnored(err) // false — independent state
193
+ ```
194
+
195
+ ## Cross-Package Integration
196
+
197
+ ### Dependencies
198
+
199
+ - **`@maestro-js/service-registry`** — provides the Provider registry pattern
200
+ - **`@maestro-js/log`** — the logger that receives exception reports; the `logger` config must be a `Log.LogFunctions<[Exceptions.LogMessage]>` instance
201
+
202
+ ### Works With
203
+
204
+ - **`@maestro-js/log`** — create typed loggers with transports (console, file, external services) that receive structured `LogMessage` entries
205
+ - **`@maestro-js/context`** — use `AsyncLocalStorage`-based context to pull request-scoped data (user ID, request ID) into exception report tags/extra
206
+
207
+ ## Testing
208
+
209
+ Tests are at `packages/exceptions/tests/exceptions.test.ts`.
210
+
211
+ ```bash
212
+ pnpm --filter @maestro-js/exceptions test
213
+ ```
214
+
215
+ Test setup creates an in-memory logger that captures messages for assertion:
216
+
217
+ ```ts
218
+ function setup() {
219
+ const messages: Log.Message<[Exceptions.LogMessage]>[] = []
220
+ const logger = Log.create<[Exceptions.LogMessage]>({
221
+ transports: [{ write(message) { messages.push(message) } }]
222
+ })
223
+ const service = Exceptions.Provider.create({ logger })
224
+ return { service, messages }
225
+ }
226
+ ```
227
+
228
+ ## Source Files
229
+
230
+ - `packages/exceptions/src/index.ts` — single source file containing types, factory, registry, and facade
231
+ - `packages/exceptions/package.json` — package manifest
@@ -0,0 +1,391 @@
1
+ ---
2
+ name: form
3
+ description: "Headless React form primitives with client and server validation, hidden inputs for custom components, controlled/uncontrolled state management, and Standard Schema (Zod) FormData parsing with automatic type coercion. Use when building custom form components that need native constraint validation, server error integration, file upload handling, or type-safe FormData parsing. Provides HeadlessForm root component, useValidation/useWarningState/useControlledState/useReset hooks, HiddenInput/HiddenFileInput/HiddenSelect components, parseFormData/parseFormDataClientSide utilities, and composeValidators."
4
+ ---
5
+
6
+ # @maestro-js/form
7
+
8
+ Headless React form primitives with client and server validation, hidden inputs for custom components, controlled/uncontrolled state management, and Standard Schema (Zod) FormData parsing with automatic type coercion.
9
+
10
+ ## Quick Setup
11
+
12
+ ```tsx
13
+ import { HeadlessForm } from '@maestro-js/form'
14
+
15
+ // 1. Wrap your app with GlobalProvider to enable server error integration
16
+ function App() {
17
+ return (
18
+ <HeadlessForm.GlobalProvider useServerErrors={useMyServerErrors}>
19
+ <MyPage />
20
+ </HeadlessForm.GlobalProvider>
21
+ )
22
+ }
23
+
24
+ // 2. Use HeadlessForm as your form element
25
+ function LoginForm() {
26
+ return (
27
+ <HeadlessForm as="form" method="post" action="/login">
28
+ <input name="email" type="email" required />
29
+ <button type="submit">Log in</button>
30
+ </HeadlessForm>
31
+ )
32
+ }
33
+ ```
34
+
35
+ ## Core Concepts
36
+
37
+ ### Validation Flow
38
+
39
+ Custom validation via `validate` functions runs in realtime, setting `setCustomValidity()` on the referenced native element. Errors are committed (shown to the user) on input `change` or form `submit`. Server errors from `GlobalProvider` are shown immediately and cleared when the user edits the field. The first invalid input auto-focuses on submit.
40
+
41
+ ### Hidden Inputs for Custom Components
42
+
43
+ Custom UI components (dropdowns, file pickers, date pickers) can't natively participate in HTML forms. `HiddenInput`, `HiddenFileInput`, and `HiddenSelect` bridge this gap with visually-hidden native elements that carry the value and participate in constraint validation.
44
+
45
+ ### FormData Parsing
46
+
47
+ `parseFormData` and `parseFormDataClientSide` convert native FormData into typed objects using a Standard Schema (Zod). They handle type coercion (strings to numbers/booleans), nested objects via dot notation (`address.city`), arrays, and file uploads.
48
+
49
+ ## API Reference
50
+
51
+ ### HeadlessForm (Component)
52
+
53
+ ```tsx
54
+ <HeadlessForm as="form" method="post" id="profile-form">
55
+ {children}
56
+ </HeadlessForm>
57
+ ```
58
+
59
+ Root form component. Wraps a native `<form>` with validation and server error support.
60
+
61
+ - `as` -- Component to render (must resolve to `HTMLFormElement`)
62
+ - `method` -- `'get'` | `'post'` | `'put'` | `'patch'` | `'delete'`
63
+ - `id` -- Optional (auto-generated via `useId()` if omitted)
64
+ - Plus all standard form element props
65
+
66
+ GET forms validate on submit and prevent submission if invalid. Non-GET forms inject a hidden `__formId` input for server-side form identification.
67
+
68
+ ### GlobalProvider
69
+
70
+ ```tsx
71
+ <HeadlessForm.GlobalProvider useServerErrors={useMyServerErrors}>
72
+ {children}
73
+ </HeadlessForm.GlobalProvider>
74
+ ```
75
+
76
+ Wraps your app to provide server error resolution to all forms. The `useServerErrors` prop is a hook with signature:
77
+
78
+ ```ts
79
+ (formId: string) => { path: string; message: string }[] | null
80
+ ```
81
+
82
+ ### useValidation
83
+
84
+ ```ts
85
+ HeadlessForm.useValidation<T>(
86
+ props: {
87
+ name: string | string[]
88
+ value: T
89
+ form: string
90
+ validate?: ValidationFunction<T>
91
+ isInvalid?: boolean
92
+ isRequired?: boolean
93
+ focus?: () => void
94
+ },
95
+ ref: RefObject<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
96
+ ): {
97
+ isInvalid: boolean
98
+ validationErrors: string[]
99
+ validationDetails: ValidityState
100
+ validationMessage: string
101
+ commitValidation(): void
102
+ }
103
+ ```
104
+
105
+ Connects custom validation to the native Constraint Validation API. Use inside custom input components.
106
+
107
+ - Runs `validate(value)` in realtime, sets `setCustomValidity()` on the referenced element
108
+ - Commits errors on `change` event or form submit (`invalid` event)
109
+ - Auto-focuses the first invalid input on submission
110
+ - Integrates server errors from `GlobalProvider` -- clears when user edits
111
+ - Resets on form `reset` event
112
+
113
+ ### useWarningState
114
+
115
+ ```ts
116
+ HeadlessForm.useWarningState<T>(
117
+ props: { value: T; validate?: ValidationFunction<T>; isInvalid?: boolean }
118
+ ): {
119
+ realtimeValidation: ValidationResult
120
+ displayValidation: ValidationResult
121
+ warningMessage: string
122
+ commitValidation(): void
123
+ resetValidation(): void
124
+ updateValidation(result: ValidationResult): void
125
+ }
126
+ ```
127
+
128
+ Like `useValidation` but for non-blocking warnings. Does not integrate with native constraint validation or server errors. Use for soft validation hints that don't prevent submission.
129
+
130
+ ### useControlledState
131
+
132
+ ```ts
133
+ HeadlessForm.useControlledState<T>(
134
+ value: T | undefined,
135
+ defaultValue: T,
136
+ onChange?: (v: T) => void
137
+ ): [T, (value: T) => void]
138
+ ```
139
+
140
+ Manages controlled/uncontrolled state for custom form components. When `value` is defined, the component is controlled; when `undefined`, it uses `defaultValue` and manages its own state.
141
+
142
+ ### useReset
143
+
144
+ ```ts
145
+ HeadlessForm.useReset(
146
+ ref: RefObject<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
147
+ initialValue: T,
148
+ onReset: (value: T) => void
149
+ )
150
+ ```
151
+
152
+ Calls `onReset` with the initial value when the parent form fires a `reset` event.
153
+
154
+ ### useServerErrors
155
+
156
+ ```ts
157
+ HeadlessForm.useServerErrors(formId?: string): { path: string; message: string }[]
158
+ ```
159
+
160
+ Returns server errors for the given form ID (or the current form's ID from context if omitted). Requires `GlobalProvider` in the tree.
161
+
162
+ ### useTopLevelFormValidation
163
+
164
+ ```ts
165
+ HeadlessForm.useTopLevelFormValidation(
166
+ formElement: HTMLFormElement | string
167
+ ): { revalidate(field: string | null): void }
168
+ ```
169
+
170
+ Programmatically re-triggers validation on form fields. Pass `null` to revalidate all fields, or a field name prefix to revalidate only matching fields.
171
+
172
+ ### composeValidators
173
+
174
+ ```ts
175
+ HeadlessForm.composeValidators<T>(
176
+ ...fns: (ValidationFunction<T> | undefined | null)[]
177
+ ): ValidationFunction<T>
178
+ ```
179
+
180
+ Combines multiple validation functions. Each is called with the value; all returned errors are collected into a single array.
181
+
182
+ ```ts
183
+ const required = (v: string) => (!v ? 'Required' : null)
184
+ const minLen = (min: number) => (v: string) => (v.length < min ? `At least ${min} chars` : null)
185
+
186
+ const validate = HeadlessForm.composeValidators(required, minLen(5))
187
+ validate('') // ['Required', 'At least 5 chars']
188
+ ```
189
+
190
+ ### Hidden Input Components
191
+
192
+ #### HiddenInput
193
+
194
+ Visually hidden `<input>` that participates in form submission. Use inside custom components.
195
+
196
+ ```tsx
197
+ <HeadlessForm.HiddenInput ref={inputRef} name="country" value={selectedCountry} form={formId} isRequired />
198
+ ```
199
+
200
+ Props: `value`, `name`, `form`, `isRequired`, `type`, `min`, `max`
201
+
202
+ #### HiddenFileInput
203
+
204
+ Visually hidden `<input type="file">` with programmatic file assignment via `DataTransfer`.
205
+
206
+ ```tsx
207
+ <HeadlessForm.HiddenFileInput ref={fileRef} name="avatar" value={selectedFile} form={formId} isRequired />
208
+ ```
209
+
210
+ Props: `value` (`File | File[] | null`), `name`, `form`, `isRequired`
211
+
212
+ #### HiddenSelect
213
+
214
+ Visually hidden `<select>` for custom dropdown components.
215
+
216
+ ```tsx
217
+ <HeadlessForm.HiddenSelect ref={selectRef} name="role" value={selectedRole} form={formId} isRequired>
218
+ <option value="admin">Admin</option>
219
+ <option value="viewer">Viewer</option>
220
+ </HeadlessForm.HiddenSelect>
221
+ ```
222
+
223
+ Props: `value`, `name`, `form`, `isRequired`, `children`, `autoComplete`, `onChange`, `label`
224
+
225
+ ### FormData Parsing
226
+
227
+ #### parseFormData
228
+
229
+ ```ts
230
+ HeadlessForm.parseFormData<T>(
231
+ schema: StandardSchema<T>,
232
+ formData: FormData | Promise<FormData>,
233
+ options?: {
234
+ handleFile?(file: File, path: string, data: T): Promise<string>
235
+ libraryOptions?: Record<string, unknown>
236
+ }
237
+ ): Promise<ParseFormDataResult<T>>
238
+ ```
239
+
240
+ Server-side FormData parser. Converts raw FormData into a typed object validated against a Standard Schema (Zod).
241
+
242
+ ```ts
243
+ const schema = z.object({ name: z.string(), age: z.number(), avatar: z.string() })
244
+
245
+ const result = await HeadlessForm.parseFormData(schema, request.formData(), {
246
+ handleFile: async (file) => await uploadToS3(file)
247
+ })
248
+
249
+ if (result.success) {
250
+ result.data // { name: 'Alice', age: 30, avatar: 'https://cdn.example.com/avatar.png' }
251
+ }
252
+ ```
253
+
254
+ #### parseFormDataClientSide
255
+
256
+ ```ts
257
+ HeadlessForm.parseFormDataClientSide<T>(
258
+ schema: StandardSchema<T>,
259
+ formData: FormData,
260
+ options?: ParseFormDataOptions<T>
261
+ ): ParseFormDataResult<T> & { fileEntries: FoundFile[] }
262
+ ```
263
+
264
+ Synchronous client-side variant. Files are replaced with placeholder URLs for validation; actual file entries are returned separately in `fileEntries`.
265
+
266
+ ### Coercion Rules
267
+
268
+ The parser uses the schema's JSON Schema representation to coerce FormData string values:
269
+
270
+ | Schema Type | FormData Value | Result |
271
+ | --- | --- | --- |
272
+ | `string` | `'Alice'` | `'Alice'` |
273
+ | `string` (nullable) | `''` | `null` |
274
+ | `number` / `integer` | `'42'` | `42` |
275
+ | `number` (empty) | `''` | `null` |
276
+ | `boolean` | `'true'` / `'false'` | `true` / `false` |
277
+ | `boolean` (absent) | *(missing)* | `false` |
278
+ | `array` | multiple values | `['a', 'b']` |
279
+ | `array` | JSON string `'[1,2]'` | `[1, 2]` |
280
+ | `array` (empty) | `''` | `[]` |
281
+ | nested object | `'addr.city'` key | `{ addr: { city: ... } }` |
282
+ | array of objects | `'items.0.name'` key | `[{ name: ... }]` |
283
+
284
+ ### Types
285
+
286
+ ```ts
287
+ type ValidationFunction<T> = (value: T) => string | string[] | null | undefined
288
+
289
+ interface ValidationResult {
290
+ isInvalid: boolean
291
+ validationDetails: ValidityState
292
+ validationErrors: string[]
293
+ }
294
+
295
+ type ParseFormDataResult<T> =
296
+ | { success: true; data: T; formData: FormData; formId: string | null; issues: undefined }
297
+ | { success: false; data: any; formData: FormData; formId: string | null; issues: readonly StandardSchemaV1.Issue[] }
298
+ ```
299
+
300
+ ## Common Patterns
301
+
302
+ ### Custom Input with Validation
303
+
304
+ ```tsx
305
+ function PhoneInput({ name, form }: { name: string; form: string }) {
306
+ const inputRef = useRef<HTMLInputElement>(null)
307
+ const [value, setValue] = HeadlessForm.useControlledState<string>(undefined, '')
308
+
309
+ const validation = HeadlessForm.useValidation(
310
+ {
311
+ name,
312
+ form,
313
+ value,
314
+ validate: (v) => (v && !/^\d{10}$/.test(v) ? 'Enter a 10-digit phone number' : null)
315
+ },
316
+ inputRef
317
+ )
318
+
319
+ HeadlessForm.useReset(inputRef, '', setValue)
320
+
321
+ return (
322
+ <div>
323
+ <input value={value} onChange={(e) => setValue(e.target.value)} />
324
+ <HeadlessForm.HiddenInput ref={inputRef} name={name} value={value} form={form} />
325
+ {validation.isInvalid && <span>{validation.validationMessage}</span>}
326
+ </div>
327
+ )
328
+ }
329
+ ```
330
+
331
+ ### Server-Side Form Processing
332
+
333
+ ```ts
334
+ async function handleProfileUpdate(formData: FormData) {
335
+ const schema = z.object({
336
+ name: z.string().min(1),
337
+ bio: z.string().nullable(),
338
+ age: z.number().min(13),
339
+ avatar: z.string(),
340
+ tags: z.array(z.string())
341
+ })
342
+
343
+ const result = await HeadlessForm.parseFormData(schema, formData, {
344
+ handleFile: async (file) => uploadToStorage(file)
345
+ })
346
+
347
+ if (!result.success) {
348
+ return { errors: result.issues.map((i) => ({ path: i.path?.join('.') ?? '', message: i.message })) }
349
+ }
350
+
351
+ await db.updateUser(result.data)
352
+ }
353
+ ```
354
+
355
+ ### Custom Select with Hidden Native Element
356
+
357
+ ```tsx
358
+ function RoleSelect({ name, form }: { name: string; form: string }) {
359
+ const selectRef = useRef<HTMLSelectElement>(null)
360
+ const [value, setValue] = HeadlessForm.useControlledState<string>(undefined, '')
361
+
362
+ const validation = HeadlessForm.useValidation({ name, form, value, isRequired: true }, selectRef)
363
+ HeadlessForm.useReset(selectRef, '', setValue)
364
+
365
+ return (
366
+ <div>
367
+ <CustomDropdown value={value} onChange={setValue} options={['admin', 'editor', 'viewer']} />
368
+ <HeadlessForm.HiddenSelect ref={selectRef} name={name} value={value} form={form} isRequired>
369
+ <option value="admin">Admin</option>
370
+ <option value="editor">Editor</option>
371
+ <option value="viewer">Viewer</option>
372
+ </HeadlessForm.HiddenSelect>
373
+ {validation.isInvalid && <span>{validation.validationMessage}</span>}
374
+ </div>
375
+ )
376
+ }
377
+ ```
378
+
379
+ ## Warnings and Gotchas
380
+
381
+ > **Warning:** `GlobalProvider` must wrap your app before any `HeadlessForm` is rendered. Without it, `useServerErrors` throws: `'GlobalContext not found. Please wrap your site with GlobalProvider'`.
382
+
383
+ > **Warning:** Do not use async validation with `parseFormData` / `parseFormDataClientSide` -- they throw `'Async validation not supported'`. All schema validation must be synchronous.
384
+
385
+ > **Note:** Non-GET forms inject a hidden `__formId` field. `parseFormData` strips this automatically. If you parse FormData manually, filter out `__formId`.
386
+
387
+ > **Note:** `parseFormData` requires a `handleFile` option when the form contains file uploads. Without it, it throws. The client-side variant replaces files with placeholder URLs instead.
388
+
389
+ > **Warning:** Unchecked boolean checkboxes are absent from FormData. The parser defaults these to `false` based on the schema. If the boolean field isn't declared in the schema, the absent value won't be coerced.
390
+
391
+ > **Note:** `useControlledState` warns in the console if a component switches between controlled and uncontrolled mode. Always consistently pass either a value or `undefined`.