@vielzeug/codex 3.1.1 → 26.10.1

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.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/data/catalog.json +166 -84
  3. package/data/llms-full.txt +3492 -1583
  4. package/data/llms.txt +9 -7
  5. package/data/manifest.json +1 -1
  6. package/data/packages/arsenal.json +40 -16
  7. package/data/packages/assay.json +12 -20
  8. package/data/packages/clockwork.json +31 -28
  9. package/data/packages/codex.json +5 -5
  10. package/data/packages/coins.json +4 -4
  11. package/data/packages/conduit.json +33 -34
  12. package/data/packages/courier.json +18 -17
  13. package/data/packages/dnd.json +11 -11
  14. package/data/packages/familiar.json +27 -6
  15. package/data/packages/flux.json +11 -8
  16. package/data/packages/focus.json +17 -7
  17. package/data/packages/forge.json +4 -4
  18. package/data/packages/gesture.json +11 -5
  19. package/data/packages/herald.json +16 -17
  20. package/data/packages/illusionist.json +5 -5
  21. package/data/packages/keymap.json +16 -12
  22. package/data/packages/ledger.json +26 -26
  23. package/data/packages/lingua.json +4 -4
  24. package/data/packages/mesh.json +30 -32
  25. package/data/packages/necromancer.json +7 -6
  26. package/data/packages/orbit.json +11 -10
  27. package/data/packages/ore.json +6 -5
  28. package/data/packages/postmaster.json +26 -26
  29. package/data/packages/prism.json +50 -42
  30. package/data/packages/pulse.json +34 -34
  31. package/data/packages/refine.json +7 -5
  32. package/data/packages/ripple.json +4 -4
  33. package/data/packages/rune.json +9 -9
  34. package/data/packages/sandbox.json +4 -4
  35. package/data/packages/scout.json +17 -11
  36. package/data/packages/scroll.json +14 -14
  37. package/data/packages/sentinel.json +26 -15
  38. package/data/packages/sigil.json +6 -6
  39. package/data/packages/sourcerer.json +4 -4
  40. package/data/packages/spell.json +31 -36
  41. package/data/packages/tandem.json +29 -0
  42. package/data/packages/tavern.json +23 -0
  43. package/data/packages/tempo.json +45 -43
  44. package/data/packages/vault.json +15 -25
  45. package/data/packages/ward.json +27 -30
  46. package/data/packages/wayfinder.json +34 -27
  47. package/data/refine.json +987 -134
  48. package/data/search.json +341 -306
  49. package/dist/http.js +2 -2
  50. package/dist/http.js.map +1 -1
  51. package/dist/index.d.ts +3 -4
  52. package/dist/index.js +3 -4
  53. package/dist/index.js.map +1 -1
  54. package/dist/refine.d.ts +1 -1
  55. package/dist/refine.js +1 -1
  56. package/dist/server.d.ts +1 -1
  57. package/dist/server.js +1 -1
  58. package/dist/tools/refine.js +1 -1
  59. package/package.json +5 -12
  60. package/skills/vielzeug/SKILL.md +57 -55
  61. package/dist/advanced.d.ts +0 -12
  62. package/dist/advanced.js +0 -13
  63. package/dist/advanced.js.map +0 -1
@@ -1,10 +1,10 @@
1
1
  {
2
- "apiSource": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';\nexport { ErrorCode, joinIssuePath, Schema, SpellDefinitionError, SpellError, SpellValidationError } from './core';\nexport { createParseContext, type DeepPartial } from './messages';\nexport { s } from './s';\n",
2
+ "apiSource": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';\nexport { ErrorCode, joinIssuePath, Schema, SpellDefinitionError, SpellError, SpellValidationError } from './core';\nexport { createParseContext, type DeepPartial } from './messages';\nexport { s } from './s';\nexport { tolerate } from './tolerate';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Spell — Schema validation for TypeScript\ndescription: Zero-dependency schema validation with explicit async checks, Standard Schema interoperability, and JSON Schema export.\npackage: spell\ncategory: validation\nkeywords: [schema, validation, parsing, standard-schema, json-schema, locale, typescript, descriptors]\nrelated: [forge, courier, vault]\nexports:\n [s, createParseContext, joinIssuePath, SpellValidationError, SpellDefinitionError, StandardSchemaV1, ErrorCode, './json', './predicates']\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"spell\" />\n\n## Why Spell?\n\nSpell keeps runtime validation, static inference, and portable definitions in one API. Use `s` for schema construction; import JSON conversion and predicates from dedicated subpaths.\n\nThis example shows the difference between manual branching and a single reusable schema.\n\n```ts\n// Before\nfunction parseUserBefore(value: unknown) {\n if (typeof value !== 'object' || value === null) throw new Error('Expected object');\n\n const candidate = value as Record<string, unknown>;\n\n if (typeof candidate.email !== 'string' || !candidate.email.includes('@')) {\n throw new Error('Expected valid email');\n }\n\n if (typeof candidate.role !== 'string' || !['admin', 'editor', 'viewer'].includes(candidate.role)) {\n throw new Error('Expected valid role');\n }\n\n return {\n email: candidate.email,\n role: candidate.role,\n };\n}\n\n// After\nimport { s } from '@vielzeug/spell';\n\nconst User = s.object({\n email: s.string().email(),\n role: s.enum(['admin', 'editor', 'viewer'] as const),\n});\n\nconst user = User.parse({ email: 'ada@example.com', role: 'admin' });\n```\n\n| Feature | Spell | Zod | Yup |\n| ----------------- | --------------------------------------------------------------------------- | -------------------------------------------- | -------------------------------------------- |\n| Bundle size | <PackageInfo package=\"spell\" type=\"size\" /> | ~62 kB | ~14 kB |\n| Type inference | <ore-icon name=\"check\" size=\"16\"></ore-icon> `Infer<T>` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial |\n| Coercion API | <ore-icon name=\"check\" size=\"16\"></ore-icon> `s.coerce.*` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Async validation | <ore-icon name=\"check\" size=\"16\"></ore-icon> `.checkAsync()` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Error flattening | <ore-icon name=\"check\" size=\"16\"></ore-icon> `flatten()` + `flattenFirst()` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Spell when** you want a fluent schema API with strong TypeScript inference, structured errors, and no third-party runtime dependencies.\n\n**Consider alternatives when** you are already standardized on another validator ecosystem and migration cost outweighs the API benefits.\n\n</div>\n\n## Installation\n\nUse your workspace package manager to add Spell.\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/spell\n```\n\n```sh [npm]\nnpm install @vielzeug/spell\n```\n\n```sh [yarn]\nyarn add @vielzeug/spell\n```\n\n:::\n\n## Quick Start\n\nStart with a schema, then parse unknown input and use the inferred output type everywhere else.\n\n```ts\nimport { s, type Infer } from '@vielzeug/spell';\n\nconst User = s\n .object({\n email: s.string().email(),\n name: s.string().min(1),\n role: s.enum(['admin', 'editor', 'viewer'] as const),\n })\n .relaxed(); // allow extra keys — omit for strict-mode (default)\n\ntype User = Infer<typeof User>;\n\nconst payload: unknown = {\n email: 'ada@example.com',\n name: 'Ada',\n role: 'admin',\n team: 'platform',\n};\n\nconst result = User.safeParse(payload);\n\nif (!result.success) throw result.error;\nconst user = result.data;\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- Namespace and tree-shakeable schema builders.\n- Sync and async parsing with `parse()`, `safeParse()`, `parseAsync()`, and `safeParseAsync()`.\n- Dependency-free Standard Schema v1 interoperability through `schema['~standard']`.\n- Explicit `check()` and `checkAsync()` rules; sync parsing never skips an async check.\n- Wrapper modes for `optional`, `nullable`, `nullish`, `default`, `catch`, and `required`.\n- Frozen declarative definitions through `definition()` and JSON Schema export via `fromDefinition()` from `@vielzeug/spell/json`.\n- Request-local message customization through `createParseContext()`.\n- Ordered union parsing produces the same selected branch in sync and async modes.\n- Structured errors with direct path lookup and flattened views.\n- Object parsing is hardened against prototype-pollution-style keys.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Forge](/forge/) — typed form state that uses Spell schemas as its validation layer\n- [Courier](/courier/) — HTTP client for validating request and response payloads at service boundaries\n- [Vault](/vault/) — unified storage API that accepts Spell schemas to type-gate persisted data\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: Spell — API Reference\ndescription: Reference for Spell schema builders, parsing, errors, context, and tooling exports.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ------------------------ | ------------------------------- | ---------------------------------- | ------------------------------------------ |\n| `s` | Creates schemas | Sync or async, depending on checks | `checkAsync()` requires async parsing |\n| `createParseContext()` | Creates request-local messages | Sync | Pass the context to each parse call |\n| `joinIssuePath()` | Flattens issue paths to UI keys | Sync | Empty or undefined paths join to `''` |\n| `SpellValidationError` | Provides validation issues | Sync/async parse failures | Use `safeParse()` for expected invalid data |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ---------------------------- | ----------------------------------------------- |\n| `@vielzeug/spell` | Schema builders, errors, types, and parse contexts |\n| `@vielzeug/spell/json` | Convert portable definitions to JSON Schema |\n| `@vielzeug/spell/predicates` | Standalone format and type predicates |\n\n```ts\nimport { createParseContext, s, type Infer } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\nimport { isEmail } from '@vielzeug/spell/predicates';\n```\n\n## `s`\n\nAll builders live under `s`.\n\n| Builder | Purpose |\n| ----------------------------------------------------------------- | -------------------------- |\n| `string`, `number`, `boolean`, `bigint`, `date` | Primitive values |\n| `literal`, `enum`, `null`, `undefined`, `unknown`, `never` | Exact and universal values |\n| `array`, `tuple`, `set`, `map`, `record`, `object` | Collections |\n| `union`, `intersect`, `discriminatedUnion`, `lazy` | Composition |\n| `coerce.*` | Coercing primitive schemas |\n\n```ts\nconst User = s.object({\n email: s.string().email(),\n id: s.string().uuid(),\n role: s.enum(['admin', 'member'] as const),\n});\n\ntype User = Infer<typeof User>;\n```\n\nObject schemas reject unknown keys. Use `.relaxed()` to retain extras.\n\n## Standard Schema interoperability\n\nEvery schema implements the dependency-free Standard Schema v1 structural protocol through `schema['~standard']`. Consumers that accept Standard Schema validators can use Spell schemas directly without an adapter.\n\n```ts\nconst User = s.object({ name: s.string() });\nconst result = await User['~standard'].validate({ name: 'Ada' });\n```\n\nSuccessful results contain `value`; failures contain portable `{ message, path }` issues. Failed unions remain one stable union issue instead of selecting a branch heuristically.\n\nThe exported `StandardSchemaV1` interface mirrors the full v1 protocol, including `validate(value, options?)` and the `InferInput` and `InferOutput` namespace helpers.\n\n## Parsing\n\nEvery schema provides:\n\n```ts\nschema.parse(value, context?); // Output or SpellValidationError\nschema.safeParse(value, context?); // ParseResult<Output>\nschema.parseAsync(value, context?); // Promise<Output>\nschema.safeParseAsync(value, context?); // Promise<ParseResult<Output>>\nschema.is(value); // value is Output\nschema.assert(value, label?); // assertion\n```\n\n`parse()`, `safeParse()`, `is()`, and `assert()` are available on synchronous schemas. Calling `checkAsync()` returns an async-only schema, where TypeScript exposes only `parseAsync()` and `safeParseAsync()`. That async-only mode propagates through compositional schemas when a child is asynchronous.\n\nUI adapters that accept `AnySchema` cannot call `safeParse()` on it — the structural surface type omits it. Type adapter parameters against `SyncParsable` instead:\n\n```ts\nimport type { SyncParsable } from '@vielzeug/spell';\n\nfunction spellValidator(schema: SyncParsable<unknown>) {\n const result = schema.safeParse(value); // types cleanly, no shim or cast\n}\n```\n\nThe shape is structural, so an async-only schema is assignable to `SyncParsable`; calling `safeParse()` on one returns a failed result (\"parse() cannot evaluate async checks\"). When the mode is not statically known, narrow with `InferSchemaMode` and use `safeParseAsync()` for async members.\n\n## Custom Checks\n\n`check()` is synchronous. `checkAsync()` is asynchronous. Do not return a Promise from `check()`.\n\n```ts\nconst Signup = s.object({ confirm: s.string(), password: s.string() }).check((value, context) => {\n if (value.password !== value.confirm) {\n context.addIssue({ code: 'custom', message: 'Passwords must match', path: ['confirm'] });\n }\n});\n\nconst AvailableEmail = s\n .string()\n .email()\n .checkAsync(async (value) => {\n return (await emailAvailable(value)) || 'Email is already registered';\n });\n```\n\n`CheckContext.addIssue()` takes `{ code, message, params?, path? }`. Paths are relative to current schema.\n\n## Modifiers and Transforms\n\n```ts\ns.string().optional();\ns.string().nullable();\ns.string().nullish();\ns.string().required();\ns.string().default('guest');\ns.string().catch('guest');\ns.string()\n .trim()\n .transform((value) => value.toLowerCase());\ns.string().pipe(s.string().slug());\ns.string().label('User name');\n```\n\n`default()`, `catch()`, preprocessors, transforms, and checks are runtime behavior. They cannot become portable definitions.\n\n## Definitions and JSON Schema\n\n`definition()` is only for schemas containing declarative structure. It returns frozen data and throws `SpellDefinitionError` when runtime behavior is present.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\n\nconst Product = s.object({\n id: s.string().uuid(),\n name: s.string().min(1),\n});\n\nconst definition = Product.definition();\nconst jsonSchema = fromDefinition(definition);\n```\n\nNo implicit schema-to-JSON conversion exists. Make definition boundary explicit.\n\n## Parse Contexts\n\n`createParseContext()` creates immutable request-local validation messages.\n\n```ts\nfunction createParseContext(messages?: DeepPartial<Messages>): ParseContext;\n```\n\n**Returns:** A parse context with built-in messages merged with the supplied overrides.\n\n```ts\nimport { createParseContext, s } from '@vielzeug/spell';\n\nconst context = createParseContext({\n object: { invalidKeys: () => 'Unsupported field' },\n});\n\nconst result = s.object({ email: s.string().email() }).safeParse({ email: 'ada@example.com', extra: true }, context);\n\nif (!result.success) {\n const messages = result.error.messagesAt('email');\n console.log(messages);\n}\n```\n\n## Types\n\n### Inference types\n\n| Type | Resolves to |\n| --- | --- |\n| `Infer<TSchema>` | Parsed output; alias of `InferOutput<TSchema>` |\n| `InferInput<TSchema>` | Input accepted before parsing or transformation |\n| `InferOutput<TSchema>` | Value returned after parsing and transformation |\n\nComposite schemas preserve child input types. Object input types make fields optional when their field schema accepts `undefined`.\n\n### Standard Schema\n\n```ts\ninterface StandardSchemaV1<Input = unknown, Output = Input> {\n readonly '~standard': StandardSchemaV1.Props<Input, Output>;\n}\n\nnamespace StandardSchemaV1 {\n interface Props<Input = unknown, Output = Input> {\n readonly version: 1;\n readonly vendor: string;\n readonly validate: (value: unknown, options?: Options) => Result<Output> | Promise<Result<Output>>;\n readonly types?: Types<Input, Output>;\n }\n\n type Result<Output> = { readonly value: Output; readonly issues?: undefined } | { readonly issues: readonly Issue[] };\n interface Options { readonly libraryOptions?: Record<string, unknown> }\n interface Issue { readonly message: string; readonly path?: readonly (PropertyKey | PathSegment)[] }\n interface PathSegment { readonly key: PropertyKey }\n interface Types<Input = unknown, Output = Input> { readonly input: Input; readonly output: Output }\n type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];\n type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];\n}\n```\n\n### Parse result and issues\n\n```ts\ntype ParseResult<T> = { data: T; success: true } | { error: SpellValidationError; success: false };\n\ntype Issue =\n | { code: 'custom'; message: string; params?: Record<string, unknown>; path: (string | number)[] }\n | { code: 'invalid_base64'; message: string; params: { format: string }; path: (string | number)[] }\n | { code: 'invalid_date'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_duration'; message: string; params: { format: string }; path: (string | number)[] }\n | { code: 'invalid_enum'; message: string; params: { values: readonly unknown[] }; path: (string | number)[] }\n | { code: 'invalid_finite'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_integer'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_keys'; message: string; params: { keys: string[] }; path: (string | number)[] }\n | { code: 'invalid_length'; message: string; params: { exact: number }; path: (string | number)[] }\n | { code: 'invalid_literal'; message: string; params: { expected: unknown }; path: (string | number)[] }\n | { code: 'invalid_multiple_of'; message: string; params: { step: number | bigint }; path: (string | number)[] }\n | { code: 'invalid_safe'; message: string; params?: undefined; path: (string | number)[] }\n | {\n code: 'invalid_string';\n message: string;\n params: { format?: string; includes?: string; pattern?: string; prefix?: string; suffix?: string };\n path: (string | number)[];\n }\n | { code: 'invalid_type'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_union'; message: string; params: { errors: Issue[][] }; path: (string | number)[] }\n | { code: 'invalid_unique'; message: string; params: { unique: true }; path: (string | number)[] }\n | { code: 'invalid_url'; message: string; params: { format: string }; path: (string | number)[] }\n | {\n code: 'invalid_variant';\n message: string;\n params: { discriminator: string; expected: string[] };\n path: (string | number)[];\n }\n | {\n code: 'too_big';\n message: string;\n params: { exclusive?: boolean; max: number | bigint | Date };\n path: (string | number)[];\n }\n | {\n code: 'too_small';\n message: string;\n params: { exclusive?: boolean; min: number | bigint | Date };\n path: (string | number)[];\n }\n | { code: string & {}; message: string; params?: Record<string, unknown>; path: (string | number)[] };\n```\n\n`ErrorCode` is a const object mapping each issue code to its string literal.\n\n### Validation contracts\n\n```ts\ntype ParseContext = { messages: Messages };\n\ntype CheckContext = {\n addIssue: (issue: {\n code: string;\n message: string;\n params?: Record<string, unknown>;\n path?: (string | number)[];\n }) => void;\n};\n\ntype ValidateResult = boolean | null | undefined | string;\n```\n\n### Messages\n\n```ts\ntype MessageFn<Ctx extends Record<string, unknown> = Record<string, unknown>> = string | ((ctx: Ctx) => string);\n\ntype Messages = {\n array: { length: (ctx: { exact: number; value: unknown[] }) => string; max: (ctx: { max: number; value: unknown[] }) => string; min: (ctx: { min: number; value: unknown[] }) => string; nonEmpty: () => string; type: () => string; unique: () => string };\n bigint: { max: (ctx: { max: bigint; value: bigint }) => string; min: (ctx: { min: bigint; value: bigint }) => string; multipleOf: (ctx: { step: bigint; value: bigint }) => string; negative: () => string; nonNegative: () => string; nonPositive: () => string; positive: () => string; type: () => string };\n boolean: { type: () => string };\n check: { default: () => string };\n date: { max: (ctx: { max: Date; value: Date }) => string; min: (ctx: { min: Date; value: Date }) => string; type: () => string };\n enum: { invalid: (ctx: { values: readonly unknown[] }) => string };\n instanceof: { type: (ctx: { className: string }) => string };\n literal: { expected: (ctx: { expected: unknown }) => string };\n map: { max: (ctx: { max: number; value: Map<unknown, unknown> }) => string; min: (ctx: { min: number; value: Map<unknown, unknown> }) => string; nonEmpty: () => string; size: (ctx: { exact: number; value: Map<unknown, unknown> }) => string; type: () => string };\n never: { invalid: () => string };\n number: { finite: () => string; int: () => string; max: (ctx: { max: number; value: number }) => string; min: (ctx: { min: number; value: number }) => string; multipleOf: (ctx: { step: number; value: number }) => string; negative: () => string; nonNegative: () => string; nonPositive: () => string; positive: () => string; safe: () => string; type: () => string };\n object: { invalidKeys: (ctx: { keys: string[] }) => string; type: () => string };\n set: { max: (ctx: { max: number; value: Set<unknown> }) => string; min: (ctx: { min: number; value: Set<unknown> }) => string; nonEmpty: () => string; size: (ctx: { exact: number; value: Set<unknown> }) => string; type: () => string };\n string: { base64: () => string; base64url: () => string; cuid: () => string; cuid2: () => string; date: () => string; dateTime: () => string; duration: () => string; email: () => string; emoji: () => string; endsWith: (ctx: { suffix: string; value: string }) => string; hex: () => string; hexColor: () => string; includes: (ctx: { substr: string; value: string }) => string; ip: () => string; jwt: () => string; length: (ctx: { exact: number; value: string }) => string; max: (ctx: { max: number; value: string }) => string; min: (ctx: { min: number; value: string }) => string; nanoid: () => string; nonEmpty: () => string; numeric: () => string; regex: (ctx: { value: string }) => string; semver: () => string; slug: () => string; startsWith: (ctx: { prefix: string; value: string }) => string; time: () => string; type: () => string; ulid: () => string; url: () => string; uuid: () => string };\n tuple: { length: (ctx: { exact: number }) => string; min: (ctx: { min: number }) => string; type: () => string };\n union: { invalid: () => string };\n variant: { invalidDiscriminator: (ctx: { discriminator: string; expected: string[] }) => string; type: () => string };\n};\n\ntype DeepPartial<T> = {\n [K in keyof T]?: T[K] extends Record<string, unknown> ? DeepPartial<T[K]> : T[K];\n};\n```\n\n### Descriptor and JSON Schema\n\n```ts\ntype SchemaDescriptor = BaseDescriptor &\n (\n | { kind: 'unknown' | 'never' | 'boolean' | 'bigint' | 'date' | 'lazy' }\n | { className: string; kind: 'instanceof' }\n | { contentEncoding?: string; format?: string; kind: 'string'; maxLength?: number; minLength?: number; pattern?: string | null }\n | { exclusiveMaximum?: number; exclusiveMinimum?: number; kind: 'number'; maximum?: number; minimum?: number; multipleOf?: number; typeHint?: 'integer' }\n | { kind: 'literal'; value: string | number | boolean | null | undefined }\n | { kind: 'enum'; values: readonly (string | number)[] }\n | { items: SchemaDescriptor; kind: 'array'; maxItems?: number; minItems?: number }\n | { items: SchemaDescriptor[]; kind: 'tuple'; rest: SchemaDescriptor | null }\n | { fields: Record<string, SchemaDescriptor>; kind: 'object'; strict: boolean }\n | { key: SchemaDescriptor; kind: 'record'; value: SchemaDescriptor }\n | { items: SchemaDescriptor; kind: 'set' }\n | { key: SchemaDescriptor; kind: 'map'; value: SchemaDescriptor }\n | { branches: SchemaDescriptor[]; kind: 'union' | 'intersect' }\n | { branches: Record<string, SchemaDescriptor>; discriminator: string; kind: 'variant' }\n | { from: SchemaDescriptor; kind: 'pipe'; to: SchemaDescriptor }\n );\n\ntype JsonSchema = Record<string, unknown>;\n```\n\n### Schema traversal\n\n`SchemaWalker<R>` is the visitor type accepted by `schema.walk()`. Handlers are optional and correspond to schema kinds; composite handlers also receive their walked children. Use `unknown` as the fallback handler.\n\n### Error helpers\n\n```ts\ntype FlatError = { messages: string[]; path: (string | number)[] };\ntype FlatErrorFirst = { message: string; path: (string | number)[] };\n\n/** An issue path in either shape: spell-native segments or Standard Schema `{ key }` segments. */\ntype IssuePathSegment = PropertyKey | { readonly key: PropertyKey };\ntype IssuePath = readonly IssuePathSegment[];\n\n/** The synchronous parse surface; `AnySchema` omits `safeParse`, adapter parameters use this. */\ntype SyncParsable<T = unknown> = { safeParse(value: unknown): ParseResult<T> };\n\nfunction joinIssuePath(path: IssuePath | undefined, separator?: string): string;\n```\n\n`joinIssuePath()` flattens an issue path into a UI key such as `'permissions.0.slug'`. It accepts spell-native `(string | number)[]` paths and Standard Schema paths with `{ key }` segments, and returns `''` for empty or undefined paths.\n\n## Errors\n\n- `SpellError` — base class. Use `instanceof SpellError` for cross-boundary narrowing.\n- `SpellValidationError` — validation failure with `issues`, `messagesAt()`, `flatten()`, and `flattenFirst()`.\n- `SpellDefinitionError` — schema cannot create portable definition.\n\n```ts\nconst result = s.object({ email: s.string().email() }).safeParse({ email: 'invalid' });\n\nif (!result.success) {\n const { fieldErrors, formErrors } = result.error.flatten();\n console.log(fieldErrors, formErrors);\n}\n```\n",
6
- "usage": "---\ntitle: Spell — Usage Guide\ndescription: Learn how to build schemas, compose wrappers, customize locales, and integrate spell with other Vielzeug packages.\n---\n\n[[toc]]\n\n## Basic Usage\n\nStart with `safeParse()` when you want explicit success and failure branches.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Signup = s.object({\n email: s.string().email(),\n password: s.string().min(12),\n referralCode: s.string().optional(),\n});\n\nconst result = Signup.safeParse({\n email: 'ada@example.com',\n password: 'horse-battery-staple',\n});\n\nif (!result.success) {\n console.error(result.error.issues);\n} else {\n console.log(result.data.email);\n}\n```\n\nUse `parse()` when invalid input should throw immediately. Use `safeParse()` when invalid input is part of normal control flow.\n\n## Building Schemas\n\nUse the namespace form when readability matters more than bundle trimming.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Article = s.object({\n id: s.string().uuid(),\n title: s.string().trim().min(1).max(120),\n slug: s.string().slug(),\n tags: s.array(s.string().min(1)).default(() => []),\n meta: s\n .object({\n published: s.boolean(),\n publishedAt: s.date().nullable(),\n })\n .relaxed(),\n});\n```\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Todo = s.object({\n done: s.boolean(),\n tags: s.array(s.string().min(1)).default(() => []),\n title: s.string().min(1),\n});\n```\n\nObject schemas reject unknown keys by default. Call `.relaxed()` when you need to preserve extra properties.\n\nCall `.defaults()` to get a fully default-filled object without providing any input. Every required field must have a `.default()` set, or a `SpellValidationError` is thrown. Call `.partialDefaults()` when only some fields have defaults — fields without a default are silently omitted instead of throwing.\n\n```ts\nconst Config = s.object({\n host: s.string().default('localhost'),\n port: s.number().default(3000),\n});\n\nConfig.defaults(); // { host: 'localhost', port: 3000 }\n\nconst Form = s.object({ name: s.string(), role: s.string().default('viewer') });\nForm.partialDefaults(); // { role: 'viewer' }\n```\n\n## Wrapper Modes, Defaults, and Fallbacks\n\nChain wrappers to describe missing values and recovery rules without losing schema metadata.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst DisplayName = s.string().trim().min(2).label('Display name').optional().default('Guest').nullable();\n\nDisplayName.parse(undefined); // 'Guest'\nDisplayName.parse(null); // null\nDisplayName.description; // 'Display name'\n```\n\nCall `.required()` to remove `undefined` without removing `null`.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst NullableButRequired = s.string().optional().nullable().required();\n\nNullableButRequired.parse('Ada');\nNullableButRequired.parse(null);\n// NullableButRequired.parse(undefined); // throws\n```\n\nUse `.catch()` when you want a fallback output after validation fails.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Port = s.number().int().min(1).max(65535).catch(3000);\n\nPort.parse('not-a-number'); // 3000\n```\n\n## Custom Validation\n\nUse `check()` for synchronous domain rules and `checkAsync()` for asynchronous rules. Sync parsing rejects schemas with asynchronous checks.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\n// Boolean shorthand: return false to fail with default message\nconst EvenNumber = s.number().check((n) => n % 2 === 0);\n\n// String shorthand: return the message as a string\nconst Username = s\n .string()\n .min(3)\n .check((v) => !v.startsWith('_') || 'Cannot start with underscore');\n\n// Multiple issues via ctx.addIssue()\nconst Signup = s.object({ confirm: s.string(), password: s.string() }).check((v, ctx) => {\n if (v.password !== v.confirm) {\n ctx.addIssue({ code: 'custom', message: 'Passwords must match', path: ['confirm'] });\n }\n});\n```\n\n`checkAsync()` returns an async-only schema: TypeScript exposes `parseAsync()` and `safeParseAsync()` but not `parse()` or `safeParse()`. This mode survives fluent modifiers and propagates through nested arrays, objects, unions, intersections, tuples, maps, records, sets, lazy schemas, pipelines, and `s.discriminatedUnion(...)` branches. Sync parsing also fails at runtime instead of accepting an unchecked value.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst takenEmails = new Set(['ada@example.com']);\n\nconst AccountEmail = s\n .string()\n .email()\n .checkAsync(async (value, ctx) => {\n if (takenEmails.has(value)) {\n ctx.addIssue({ code: 'custom', message: 'Email is already taken', path: [] });\n }\n });\n\n// Async checks require parseAsync\nawait AccountEmail.parseAsync('grace@example.com');\n```\n\nUse `check()` for predicate-only rules too. Return `true` on success or message on failure.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst PositivePrice = s.number().check((value) => value > 0 || 'Must be positive');\nPositivePrice.parse(9.99);\n```\n\n## Strings, Numbers, and Safe Regex Usage\n\nUse schema helpers for common string and number constraints instead of hand-written predicates.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Password = s.string().min(12).regex(/[A-Z]/).regex(/[0-9]/);\nconst Price = s.number().nonNegative().multipleOf(0.01);\nconst LaunchWindow = s.date().min(new Date('2025-01-01T00:00:00.000Z'));\n```\n\nSpell strips stateful `/g` and `/y` flags from `regex()` patterns before validation. Repeated parses stay deterministic even when the original regular expression is reused.\n\n## Coercion and Transforms\n\nUse coercion when input arrives as strings, query parameters, or form values.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Query = s.object({\n draft: s.coerce.boolean().default(false),\n limit: s.coerce.number().int().positive().default(20),\n publishedAt: s.coerce.date().nullable(),\n search: s.coerce.string().trim().min(1).optional(),\n});\n\nconst parsed = Query.parse({\n draft: 'true',\n limit: '50',\n publishedAt: '2025-04-01T12:00:00.000Z',\n search: ' vielzeug ',\n});\n```\n\nUse `transform()` or `pipe()` after validation when downstream code needs a different output shape.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst TrimmedTags = s.array(s.string().trim().min(1)).transform((tags) => tags.map((tag) => tag.toLowerCase()));\nconst Slug = s.string().trim().min(1).pipe(s.string().slug());\n```\n\n## Introspection, Round-Trips, and JSON Schema\n\nUse declarative definitions when schemas need to cross process boundaries or feed tooling.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\n\nconst Product = s\n .object({\n id: s.string().uuid(),\n name: s.string().min(1),\n price: s.number().positive().multipleOf(0.01),\n })\n .label('Product');\n\nconst definition = Product.definition();\nconst jsonSchema = fromDefinition(definition);\n\nProduct.parse({ id: '550e8400-e29b-41d4-a716-446655440000', name: 'Keyboard', price: 129.99 });\nconsole.log(jsonSchema.title);\n```\n\nDefinitions are frozen serializable snapshots of declarative schema structure. Use `definition()` and `fromDefinition()` for external tooling. Schemas with runtime checks, transforms, defaults, catches, or preprocessors intentionally have no definition.\n\n## Messages\n\nSpell has no mutable process-wide configuration. Build one parse context per request, locale, or form, then pass it explicitly.\n\n```ts\nimport { createParseContext, s } from '@vielzeug/spell';\n\nconst User = s.object({ email: s.string().email() });\nconst german = createParseContext({\n object: { invalidKeys: () => 'Keine unbekannten Felder erlaubt' },\n});\n\nUser.safeParse({ email: 'ada@example.com', extra: true }, german);\n```\n\nInternal development warnings always use `console.warn` in development builds. Route application diagnostics in application code instead of mutating library-wide logger state.\n\n## Working with Validation Errors\n\nUse `SpellValidationError` helpers when you need UI-ready error structures.\n\n```ts\nimport { s, SpellValidationError } from '@vielzeug/spell';\n\nconst User = s.object({\n email: s.string().email(),\n profile: s.object({\n name: s.string().min(2),\n }),\n});\n\nconst result = User.safeParse({ email: 'nope', profile: { name: '' } });\n\nif (!result.success && result.error instanceof SpellValidationError) {\n const profileErrors = result.error.messagesAt('profile', 'name');\n console.log(profileErrors);\n}\n```\n\nKey inline errors by dot-path with `joinIssuePath()`, which also unwraps Standard Schema `{ key }` segments:\n\n```ts\nimport { joinIssuePath } from '@vielzeug/spell';\n\nfor (const issue of result.error.issues) {\n console.log(joinIssuePath(issue.path), issue.message); // 'profile.name: ...'\n}\n```\n\nUnion failures produce one stable `invalid_union` issue. Use `s.discriminatedUnion()` when a discriminator can identify the intended object branch and provide field-specific errors.\n\n## Validating synchronously in React\n\nAdapters that validate inside a render or `validate` callback need the sync parse surface. `AnySchema` does not expose `safeParse`; type the adapter parameter against `SyncParsable` instead:\n\n```ts\nimport { type SyncParsable } from '@vielzeug/spell';\n\nfunction spellValidator(schema: SyncParsable<unknown>) {\n return (values: unknown) => {\n const result = schema.safeParse(values);\n if (result.success) return undefined;\n\n return result.error.issues.map((issue) => ({\n message: issue.message,\n path: issue.path,\n }));\n };\n}\n\nspellValidator(s.object({ email: s.string() })); // no shim or cast\n```\n\n## Schema Traversal with walk()\n\nUse `walk()` to inspect or transform a schema tree without importing internal implementation classes.\n\n```ts\nimport { s, type SchemaWalker } from '@vielzeug/spell';\n\nconst fields: string[] = [];\n\nconst collectFields: SchemaWalker<void> = {\n object(schema) {\n for (const [key, child] of Object.entries(schema.shape)) {\n fields.push(key);\n child.walk(collectFields);\n }\n },\n unknown() {},\n};\n\nconst User = s.object({\n email: s.string().email(),\n profile: s.object({ name: s.string() }),\n});\n\nUser.walk(collectFields);\nconsole.log(fields); // ['email', 'profile', 'name']\n```\n\n`walk()` dispatches by `schema.kind`. If no handler matches and no `unknown` fallback is provided, `walk()` returns `null`. Add an `unknown` handler to capture any kind not explicitly listed in your visitor.\n\n## Framework Integration\n\nSpell works anywhere you can call a function before state enters your app.\n\n::: code-group\n\n```tsx [React]\nimport { s } from '@vielzeug/spell';\n\nconst SearchParams = s\n .object({\n page: s.coerce.number().int().positive().default(1),\n q: s.string().trim().optional(),\n })\n .relaxed();\n\nexport function SearchPage({ rawParams }: { rawParams: unknown }) {\n const params = SearchParams.parse(rawParams);\n\n return (\n <div>\n {params.q ?? 'All results'} — page {params.page}\n </div>\n );\n}\n```\n\n```ts [Vue]\nimport { computed, ref } from 'vue';\nimport { s } from '@vielzeug/spell';\n\nconst Settings = s.object({\n locale: s.string().min(2),\n compact: s.coerce.boolean().default(false),\n});\n\nconst raw = ref<unknown>({ locale: 'en', compact: 'true' });\nconst settings = computed(() => Settings.parse(raw.value));\n```\n\n:::\n\nUse `safeParse()` at event boundaries and `parse()` inside trusted data flows.\n\n## Working with Other Vielzeug Libraries\n\nUse Spell as the validation layer and let other packages focus on transport, forms, or storage.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\nimport { schemaValidator } from '@vielzeug/forge/schema';\nimport { createCourier } from '@vielzeug/courier';\nimport { s } from '@vielzeug/spell';\n\nconst Profile = s.object({\n displayName: s.string().min(2),\n newsletter: s.boolean(),\n});\n\nconst form = createForm({\n initialValues: {\n displayName: '',\n newsletter: false,\n },\n validate: schemaValidator(Profile),\n});\n\nconst courier = createCourier({ baseUrl: '/api' });\nconst profile = Profile.parse(await courier.get('/profile'));\n```\n\nUse Spell definitions with `@vielzeug/codex` or other tooling when you need generated docs or external schema consumers.\n\n## Best Practices\n\n- Keep schemas close to the boundary where unknown data enters your app.\n- Use `s` consistently for construction; use explicit `/json` and `/predicates` subpaths for tooling.\n- Use `.default(() => value)` for mutable defaults such as arrays, objects, `Map`, and `Set`.\n- Call `.required()` when you want to remove `undefined` but keep `null` semantics intact.\n- Use `check()` with a `ctx` argument when you need `ctx.addIssue()`; return a message for simple predicate failures.\n- Use `checkAsync()` and `parseAsync()` for every asynchronous domain rule.\n- Build a parse context per request or test; never rely on mutable process-wide configuration.\n- Use `definition()` with `fromDefinition()` from `@vielzeug/spell/json` for external tooling.\n",
7
- "examples": "---\ntitle: Spell — Examples\ndescription: Practical examples and recipes for spell.\n---\n\n## Examples\n\n- [Validating API Payloads](./examples/api.md)\n- [Form-Safe Parsing](./examples/forms.md)\n- [Async Business Rules](./examples/async.md)\n- [Schema Introspection and Round-Trips](./examples/introspection.md)\n- [Unions, Intersections, and Variants](./examples/unions.md)\n- [Schema Traversal with walk()](./examples/walk.md)\n"
4
+ "index": "---\ntitle: 'Spell: Schema validation for TypeScript'\ndescription: Zero-dependency schema validation with explicit async checks, Standard Schema interoperability, and JSON Schema export.\npackage: spell\ncategory: validation\nkeywords: [schema, validation, parsing, standard-schema, json-schema, locale, typescript, descriptors]\nrelated: [forge, courier, vault]\nexports:\n [s, createParseContext, joinIssuePath, SpellValidationError, SpellDefinitionError, StandardSchemaV1, ErrorCode, './json', './predicates']\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"spell\" />\n\n## Why Spell?\n\nSpell keeps runtime validation, static inference, and portable definitions in one API. Use `s` for schema construction; import JSON conversion and predicates from dedicated subpaths.\n\nThis example shows the difference between manual branching and a single reusable schema.\n\n```ts\n// Before\nfunction parseUserBefore(value: unknown) {\n if (typeof value !== 'object' || value === null) throw new Error('Expected object');\n\n const candidate = value as Record<string, unknown>;\n\n if (typeof candidate.email !== 'string' || !candidate.email.includes('@')) {\n throw new Error('Expected valid email');\n }\n\n if (typeof candidate.role !== 'string' || !['admin', 'editor', 'viewer'].includes(candidate.role)) {\n throw new Error('Expected valid role');\n }\n\n return {\n email: candidate.email,\n role: candidate.role,\n };\n}\n\n// After\nimport { s } from '@vielzeug/spell';\n\nconst User = s.object({\n email: s.string().email(),\n role: s.enum(['admin', 'editor', 'viewer'] as const),\n});\n\nconst user = User.parse({ email: 'ada@example.com', role: 'admin' });\n```\n\n| Feature | Spell | Zod | Yup |\n| ----------------- | --------------------------------------------------------------------------- | -------------------------------------------- | -------------------------------------------- |\n| Bundle size | <PackageInfo package=\"spell\" type=\"size\" /> | ~62 kB | ~14 kB |\n| Type inference | <ore-icon name=\"check\" size=\"16\"></ore-icon> `Infer<T>` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial |\n| Coercion API | <ore-icon name=\"check\" size=\"16\"></ore-icon> `s.coerce.*` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Async validation | <ore-icon name=\"check\" size=\"16\"></ore-icon> `.checkAsync()` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Error flattening | <ore-icon name=\"check\" size=\"16\"></ore-icon> `flatten()` + `flattenFirst()` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Spell when** you want a fluent schema API with strong TypeScript inference, structured errors, and no third-party runtime dependencies.\n\n**Consider alternatives when** you are already standardized on another validator ecosystem and migration cost outweighs the API benefits.\n\n</div>\n\n## Installation\n\nUse your workspace package manager to add Spell.\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/spell\n```\n\n```sh [npm]\nnpm install @vielzeug/spell\n```\n\n```sh [yarn]\nyarn add @vielzeug/spell\n```\n\n:::\n\n## Quick Start\n\nStart with a schema, then parse unknown input and use the inferred output type everywhere else.\n\n```ts\nimport { s, type Infer } from '@vielzeug/spell';\n\nconst User = s\n .object({\n email: s.string().email(),\n name: s.string().min(1),\n role: s.enum(['admin', 'editor', 'viewer'] as const),\n })\n .relaxed(); // allow extra keys: omit for strict-mode (default)\n\ntype User = Infer<typeof User>;\n\nconst payload: unknown = {\n email: 'ada@example.com',\n name: 'Ada',\n role: 'admin',\n team: 'platform',\n};\n\nconst result = User.safeParse(payload);\n\nif (!result.success) throw result.error;\nconst user = result.data;\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- Namespace and tree-shakeable schema builders.\n- Sync and async parsing with `parse()`, `safeParse()`, `parseAsync()`, and `safeParseAsync()`.\n- Dependency-free Standard Schema v1 interoperability through `schema['~standard']`.\n- Explicit `check()` and `checkAsync()` rules; sync parsing never skips an async check.\n- Wrapper modes for `optional`, `nullable`, `nullish`, `default`, `catch`, and `required`.\n- Frozen declarative definitions through `definition()` and JSON Schema export via `fromDefinition()` from `@vielzeug/spell/json`.\n- Request-local message customization through `createParseContext()`.\n- Ordered union parsing produces the same selected branch in sync and async modes.\n- Structured errors with direct path lookup and flattened views.\n- Object parsing is hardened against prototype-pollution-style keys.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Forge](/forge/): typed form state that uses Spell schemas as its validation layer\n- [Courier](/courier/): HTTP client for validating request and response payloads at service boundaries\n- [Vault](/vault/): unified storage API that accepts Spell schemas to type-gate persisted data\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: 'Spell: API Reference'\ndescription: Reference for Spell schema builders, parsing, errors, context, and tooling exports.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ------------------------ | ------------------------------- | ---------------------------------- | ------------------------------------------ |\n| `s` | Creates schemas | Sync or async, depending on checks | `checkAsync()` requires async parsing |\n| `createParseContext()` | Creates request-local messages | Sync | Pass the context to each parse call |\n| `joinIssuePath()` | Flattens issue paths to UI keys | Sync | Empty or undefined paths join to `''` |\n| `SpellValidationError` | Provides validation issues | Sync/async parse failures | Use `safeParse()` for expected invalid data |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ---------------------------- | ----------------------------------------------- |\n| `@vielzeug/spell` | Schema builders, errors, types, and parse contexts |\n| `@vielzeug/spell/json` | Convert portable definitions to JSON Schema |\n| `@vielzeug/spell/predicates` | Standalone format and type predicates |\n\n```ts\nimport { createParseContext, s, type Infer } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\nimport { isEmail } from '@vielzeug/spell/predicates';\n```\n\n## `s`\n\nAll builders live under `s`.\n\n| Builder | Purpose |\n| ----------------------------------------------------------------- | -------------------------- |\n| `string`, `number`, `boolean`, `bigint`, `date` | Primitive values |\n| `literal`, `enum`, `null`, `undefined`, `unknown`, `never` | Exact and universal values |\n| `array`, `tuple`, `set`, `map`, `record`, `object` | Collections |\n| `union`, `intersect`, `discriminatedUnion`, `lazy` | Composition |\n| `coerce.*` | Coercing primitive schemas |\n\n```ts\nconst User = s.object({\n email: s.string().email(),\n id: s.string().uuid(),\n role: s.enum(['admin', 'member'] as const),\n});\n\ntype User = Infer<typeof User>;\n```\n\nObject schemas reject unknown keys. Use `.relaxed()` to retain extras.\n\n## Standard Schema interoperability\n\nEvery schema implements the dependency-free Standard Schema v1 structural protocol through `schema['~standard']`. Consumers that accept Standard Schema validators can use Spell schemas directly without an adapter.\n\n```ts\nconst User = s.object({ name: s.string() });\nconst result = await User['~standard'].validate({ name: 'Ada' });\n```\n\nSuccessful results contain `value`; failures contain portable `{ message, path }` issues. Failed unions remain one stable union issue instead of selecting a branch heuristically.\n\nThe exported `StandardSchemaV1` interface mirrors the full v1 protocol, including `validate(value, options?)` and the `InferInput` and `InferOutput` namespace helpers.\n\n## Parsing\n\nEvery schema provides:\n\n```ts\nschema.parse(value, context?); // Output or SpellValidationError\nschema.safeParse(value, context?); // ParseResult<Output>\nschema.parseAsync(value, context?); // Promise<Output>\nschema.safeParseAsync(value, context?); // Promise<ParseResult<Output>>\nschema.is(value); // value is Output\nschema.assert(value, label?); // assertion\n```\n\n`parse()`, `safeParse()`, `is()`, and `assert()` run synchronous parsing. When a schema, or any schema nested inside it: carries an async check added with `checkAsync()`, the synchronous methods throw at runtime with \"cannot evaluate async checks\"; use `parseAsync()` or `safeParseAsync()` instead. Async checks compose at any nesting depth, so `s.object({ tags: s.array(s.string().checkAsync(fn)) })` parses correctly through `parseAsync()`.\n\nUI adapters that accept `AnySchema` cannot call `safeParse()` on it: the structural surface type omits it. Type adapter parameters against `SyncParsable` instead:\n\n```ts\nimport type { SyncParsable } from '@vielzeug/spell';\n\nfunction spellValidator(schema: SyncParsable<unknown>) {\n const result = schema.safeParse(value); // types cleanly, no shim or cast\n}\n```\n\nThe shape is structural, so a schema with async checks is assignable to `SyncParsable`; calling `safeParse()` on one returns a failed result (\"parse() cannot evaluate async checks\"). When async checks may be present, use `safeParseAsync()` instead.\n\n## Custom Checks\n\n`check()` is synchronous. `checkAsync()` is asynchronous. Do not return a Promise from `check()`.\n\n```ts\nconst Signup = s.object({ confirm: s.string(), password: s.string() }).check((value, context) => {\n if (value.password !== value.confirm) {\n context.addIssue({ code: 'custom', message: 'Passwords must match', path: ['confirm'] });\n }\n});\n\nconst AvailableEmail = s\n .string()\n .email()\n .checkAsync(async (value) => {\n return (await emailAvailable(value)) || 'Email is already registered';\n });\n```\n\n`CheckContext.addIssue()` takes `{ code, message, params?, path? }`. Paths are relative to current schema.\n\n## Modifiers and Transforms\n\n```ts\ns.string().optional();\ns.string().nullable();\ns.string().nullish();\ns.string().required();\ns.string().default('guest');\ns.string().catch('guest');\ns.string()\n .trim()\n .transform((value) => value.toLowerCase());\ns.string().pipe(s.string().slug());\ns.string().label('User name');\n```\n\n`default()`, `catch()`, preprocessors, transforms, and checks are runtime behavior. They cannot become portable definitions.\n\n## `tolerate()`\n\nWraps an object schema so records saved by older schema versions still parse: the named keys are accepted and stripped instead of failing `invalid_keys`. The parsed output never contains the tolerated keys.\n\n```ts\nconst Person = tolerate(s.object({ name: s.string() }), 'address');\n```\n\nThe implementation is a preprocessor on the wrapped schema: non-object input reaches the wrapped schema untouched, declared fields keep their validation, and unknown keys outside the tolerated set still fail.\n\n## Definitions and JSON Schema\n\n`definition()` is only for schemas containing declarative structure. It returns frozen data and throws `SpellDefinitionError` when runtime behavior is present.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\n\nconst Product = s.object({\n id: s.string().uuid(),\n name: s.string().min(1),\n});\n\nconst definition = Product.definition();\nconst jsonSchema = fromDefinition(definition);\n```\n\nNo implicit schema-to-JSON conversion exists. Make definition boundary explicit.\n\n## Parse Contexts\n\n`createParseContext()` creates immutable request-local validation messages.\n\n```ts\nfunction createParseContext(messages?: DeepPartial<Messages>): ParseContext;\n```\n\n**Returns:** A parse context with built-in messages merged with the supplied overrides.\n\n```ts\nimport { createParseContext, s } from '@vielzeug/spell';\n\nconst context = createParseContext({\n object: { invalidKeys: () => 'Unsupported field' },\n});\n\nconst result = s.object({ email: s.string().email() }).safeParse({ email: 'ada@example.com', extra: true }, context);\n\nif (!result.success) {\n const messages = result.error.messagesAt('email');\n console.log(messages);\n}\n```\n\n## Types\n\n### Inference types\n\n| Type | Resolves to |\n| --- | --- |\n| `Infer<TSchema>` | Parsed output; alias of `InferOutput<TSchema>` |\n| `InferInput<TSchema>` | Input accepted before parsing or transformation |\n| `InferOutput<TSchema>` | Value returned after parsing and transformation |\n\nComposite schemas preserve child input types. Object input types make fields optional when their field schema accepts `undefined`.\n\n### Standard Schema\n\n```ts\ninterface StandardSchemaV1<Input = unknown, Output = Input> {\n readonly '~standard': StandardSchemaV1.Props<Input, Output>;\n}\n\nnamespace StandardSchemaV1 {\n interface Props<Input = unknown, Output = Input> {\n readonly version: 1;\n readonly vendor: string;\n readonly validate: (value: unknown, options?: Options) => Result<Output> | Promise<Result<Output>>;\n readonly types?: Types<Input, Output>;\n }\n\n type Result<Output> = { readonly value: Output; readonly issues?: undefined } | { readonly issues: readonly Issue[] };\n interface Options { readonly libraryOptions?: Record<string, unknown> }\n interface Issue { readonly message: string; readonly path?: readonly (PropertyKey | PathSegment)[] }\n interface PathSegment { readonly key: PropertyKey }\n interface Types<Input = unknown, Output = Input> { readonly input: Input; readonly output: Output }\n type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];\n type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];\n}\n```\n\n### Parse result and issues\n\n```ts\ntype ParseResult<T> = { data: T; success: true } | { error: SpellValidationError; success: false };\n\ntype Issue =\n | { code: 'custom'; message: string; params?: Record<string, unknown>; path: (string | number)[] }\n | { code: 'invalid_base64'; message: string; params: { format: string }; path: (string | number)[] }\n | { code: 'invalid_date'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_duration'; message: string; params: { format: string }; path: (string | number)[] }\n | { code: 'invalid_enum'; message: string; params: { values: readonly unknown[] }; path: (string | number)[] }\n | { code: 'invalid_finite'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_integer'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_keys'; message: string; params: { keys: string[] }; path: (string | number)[] }\n | { code: 'invalid_length'; message: string; params: { exact: number }; path: (string | number)[] }\n | { code: 'invalid_literal'; message: string; params: { expected: unknown }; path: (string | number)[] }\n | { code: 'invalid_multiple_of'; message: string; params: { step: number | bigint }; path: (string | number)[] }\n | { code: 'invalid_safe'; message: string; params?: undefined; path: (string | number)[] }\n | {\n code: 'invalid_string';\n message: string;\n params: { format?: string; includes?: string; pattern?: string; prefix?: string; suffix?: string };\n path: (string | number)[];\n }\n | { code: 'invalid_type'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_union'; message: string; params: { errors: Issue[][] }; path: (string | number)[] }\n | { code: 'invalid_unique'; message: string; params: { unique: true }; path: (string | number)[] }\n | { code: 'invalid_url'; message: string; params: { format: string }; path: (string | number)[] }\n | {\n code: 'invalid_variant';\n message: string;\n params: { discriminator: string; expected: string[] };\n path: (string | number)[];\n }\n | {\n code: 'too_big';\n message: string;\n params: { exclusive?: boolean; max: number | bigint | Date };\n path: (string | number)[];\n }\n | {\n code: 'too_small';\n message: string;\n params: { exclusive?: boolean; min: number | bigint | Date };\n path: (string | number)[];\n }\n | { code: string & {}; message: string; params?: Record<string, unknown>; path: (string | number)[] };\n```\n\n`ErrorCode` is a const object mapping each issue code to its string literal.\n\n### Validation contracts\n\n```ts\ntype ParseContext = { messages: Messages };\n\ntype CheckContext = {\n addIssue: (issue: {\n code: string;\n message: string;\n params?: Record<string, unknown>;\n path?: (string | number)[];\n }) => void;\n};\n\ntype ValidateResult = boolean | null | undefined | string;\n```\n\n### Messages\n\n```ts\ntype MessageFn<Ctx extends Record<string, unknown> = Record<string, unknown>> = string | ((ctx: Ctx) => string);\n\ntype Messages = {\n array: { length: (ctx: { exact: number; value: unknown[] }) => string; max: (ctx: { max: number; value: unknown[] }) => string; min: (ctx: { min: number; value: unknown[] }) => string; nonEmpty: () => string; type: () => string; unique: () => string };\n bigint: { max: (ctx: { max: bigint; value: bigint }) => string; min: (ctx: { min: bigint; value: bigint }) => string; multipleOf: (ctx: { step: bigint; value: bigint }) => string; negative: () => string; nonNegative: () => string; nonPositive: () => string; positive: () => string; type: () => string };\n boolean: { type: () => string };\n check: { default: () => string };\n date: { max: (ctx: { max: Date; value: Date }) => string; min: (ctx: { min: Date; value: Date }) => string; type: () => string };\n enum: { invalid: (ctx: { values: readonly unknown[] }) => string };\n instanceof: { type: (ctx: { className: string }) => string };\n literal: { expected: (ctx: { expected: unknown }) => string };\n map: { max: (ctx: { max: number; value: Map<unknown, unknown> }) => string; min: (ctx: { min: number; value: Map<unknown, unknown> }) => string; nonEmpty: () => string; size: (ctx: { exact: number; value: Map<unknown, unknown> }) => string; type: () => string };\n never: { invalid: () => string };\n number: { finite: () => string; int: () => string; max: (ctx: { max: number; value: number }) => string; min: (ctx: { min: number; value: number }) => string; multipleOf: (ctx: { step: number; value: number }) => string; negative: () => string; nonNegative: () => string; nonPositive: () => string; positive: () => string; safe: () => string; type: () => string };\n object: { invalidKeys: (ctx: { keys: string[] }) => string; type: () => string };\n set: { max: (ctx: { max: number; value: Set<unknown> }) => string; min: (ctx: { min: number; value: Set<unknown> }) => string; nonEmpty: () => string; size: (ctx: { exact: number; value: Set<unknown> }) => string; type: () => string };\n string: { base64: () => string; base64url: () => string; cuid: () => string; cuid2: () => string; date: () => string; dateTime: () => string; duration: () => string; email: () => string; emoji: () => string; endsWith: (ctx: { suffix: string; value: string }) => string; hex: () => string; hexColor: () => string; includes: (ctx: { substr: string; value: string }) => string; ip: () => string; jwt: () => string; length: (ctx: { exact: number; value: string }) => string; max: (ctx: { max: number; value: string }) => string; min: (ctx: { min: number; value: string }) => string; nanoid: () => string; nonEmpty: () => string; numeric: () => string; regex: (ctx: { value: string }) => string; semver: () => string; slug: () => string; startsWith: (ctx: { prefix: string; value: string }) => string; time: () => string; type: () => string; ulid: () => string; url: () => string; uuid: () => string };\n tuple: { length: (ctx: { exact: number }) => string; min: (ctx: { min: number }) => string; type: () => string };\n union: { invalid: () => string };\n variant: { invalidDiscriminator: (ctx: { discriminator: string; expected: string[] }) => string; type: () => string };\n};\n\ntype DeepPartial<T> = {\n [K in keyof T]?: T[K] extends Record<string, unknown> ? DeepPartial<T[K]> : T[K];\n};\n```\n\n### Descriptor and JSON Schema\n\n```ts\ntype SchemaDescriptor = BaseDescriptor &\n (\n | { kind: 'unknown' | 'never' | 'boolean' | 'bigint' | 'date' | 'lazy' }\n | { className: string; kind: 'instanceof' }\n | { contentEncoding?: string; format?: string; kind: 'string'; maxLength?: number; minLength?: number; pattern?: string | null }\n | { exclusiveMaximum?: number; exclusiveMinimum?: number; kind: 'number'; maximum?: number; minimum?: number; multipleOf?: number; typeHint?: 'integer' }\n | { kind: 'literal'; value: string | number | boolean | null | undefined }\n | { kind: 'enum'; values: readonly (string | number)[] }\n | { items: SchemaDescriptor; kind: 'array'; maxItems?: number; minItems?: number }\n | { items: SchemaDescriptor[]; kind: 'tuple'; rest: SchemaDescriptor | null }\n | { fields: Record<string, SchemaDescriptor>; kind: 'object'; strict: boolean }\n | { key: SchemaDescriptor; kind: 'record'; value: SchemaDescriptor }\n | { items: SchemaDescriptor; kind: 'set' }\n | { key: SchemaDescriptor; kind: 'map'; value: SchemaDescriptor }\n | { branches: SchemaDescriptor[]; kind: 'union' | 'intersect' }\n | { branches: Record<string, SchemaDescriptor>; discriminator: string; kind: 'variant' }\n | { from: SchemaDescriptor; kind: 'pipe'; to: SchemaDescriptor }\n );\n\ntype JsonSchema = Record<string, unknown>;\n```\n\n### Error helpers\n\n```ts\ntype FlatError = { messages: string[]; path: (string | number)[] };\ntype FlatErrorFirst = { message: string; path: (string | number)[] };\n\n/** An issue path in either shape: spell-native segments or Standard Schema `{ key }` segments. */\ntype IssuePathSegment = PropertyKey | { readonly key: PropertyKey };\ntype IssuePath = readonly IssuePathSegment[];\n\n/** The synchronous parse surface; `AnySchema` omits `safeParse`, adapter parameters use this. */\ntype SyncParsable<T = unknown> = { safeParse(value: unknown): ParseResult<T> };\n\nfunction joinIssuePath(path: IssuePath | undefined, separator?: string): string;\n```\n\n`joinIssuePath()` flattens an issue path into a UI key such as `'permissions.0.slug'`. It accepts spell-native `(string | number)[]` paths and Standard Schema paths with `{ key }` segments, and returns `''` for empty or undefined paths.\n\n## Errors\n\n- `SpellError`: base class. Use `instanceof SpellError` for cross-boundary narrowing.\n- `SpellValidationError`: validation failure with `issues`, `messagesAt()`, `flatten()`, and `flattenFirst()`.\n- `SpellDefinitionError`: schema cannot create portable definition.\n\n```ts\nconst result = s.object({ email: s.string().email() }).safeParse({ email: 'invalid' });\n\nif (!result.success) {\n const { fieldErrors, formErrors } = result.error.flatten();\n console.log(fieldErrors, formErrors);\n}\n```\n",
6
+ "usage": "---\ntitle: 'Spell: Usage Guide'\ndescription: Learn how to build schemas, compose wrappers, customize locales, and integrate spell with other Vielzeug packages.\n---\n\n[[toc]]\n\n## Basic Usage\n\nStart with `safeParse()` when you want explicit success and failure branches.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Signup = s.object({\n email: s.string().email(),\n password: s.string().min(12),\n referralCode: s.string().optional(),\n});\n\nconst result = Signup.safeParse({\n email: 'ada@example.com',\n password: 'horse-battery-staple',\n});\n\nif (!result.success) {\n console.error(result.error.issues);\n} else {\n console.log(result.data.email);\n}\n```\n\nUse `parse()` when invalid input should throw immediately. Use `safeParse()` when invalid input is part of normal control flow.\n\n## Building Schemas\n\nUse the namespace form when readability matters more than bundle trimming.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Article = s.object({\n id: s.string().uuid(),\n title: s.string().trim().min(1).max(120),\n slug: s.string().slug(),\n tags: s.array(s.string().min(1)).default(() => []),\n meta: s\n .object({\n published: s.boolean(),\n publishedAt: s.date().nullable(),\n })\n .relaxed(),\n});\n```\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Todo = s.object({\n done: s.boolean(),\n tags: s.array(s.string().min(1)).default(() => []),\n title: s.string().min(1),\n});\n```\n\nObject schemas reject unknown keys by default. Call `.relaxed()` when you need to preserve extra properties.\n\nCall `.defaults()` to get a fully default-filled object without providing any input. Every required field must have a `.default()` set, or a `SpellValidationError` is thrown. Call `.partialDefaults()` when only some fields have defaults: fields without a default are silently omitted instead of throwing.\n\n```ts\nconst Config = s.object({\n host: s.string().default('localhost'),\n port: s.number().default(3000),\n});\n\nConfig.defaults(); // { host: 'localhost', port: 3000 }\n\nconst Form = s.object({ name: s.string(), role: s.string().default('viewer') });\nForm.partialDefaults(); // { role: 'viewer' }\n```\n\n## Wrapper Modes, Defaults, and Fallbacks\n\nChain wrappers to describe missing values and recovery rules without losing schema metadata.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst DisplayName = s.string().trim().min(2).label('Display name').optional().default('Guest').nullable();\n\nDisplayName.parse(undefined); // 'Guest'\nDisplayName.parse(null); // null\nDisplayName.description; // 'Display name'\n```\n\nCall `.required()` to remove `undefined` without removing `null`.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst NullableButRequired = s.string().optional().nullable().required();\n\nNullableButRequired.parse('Ada');\nNullableButRequired.parse(null);\n// NullableButRequired.parse(undefined); // throws\n```\n\nUse `.catch()` when you want a fallback output after validation fails.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Port = s.number().int().min(1).max(65535).catch(3000);\n\nPort.parse('not-a-number'); // 3000\n```\n\n## Tolerating Removed Fields\n\nWhen a persisted record shape evolves, records saved by older versions carry keys the current schema no longer declares. `tolerate` accepts and drops exactly the named keys, so one call documents one removed field: no optional placeholder in the schema and no strip step after parsing.\n\n```ts\nimport { s, tolerate } from '@vielzeug/spell';\n\nconst Person = tolerate(\n s.object({ name: s.string(), email: s.number() }),\n 'address',\n);\n\nconst parsed = Person.parse({ name: 'John Doe', address: 'p sherman 42 wallaby way sydney', email: 'johndoe@mail.com' });\n// { name: 'John Doe', email: 'johndoe@mail.com' }: the tolerated key never reaches the output\n```\n\nFields added to later schema versions do not need `tolerate`: give them `default()` and older records repair on parse.\n\n## Custom Validation\n\nUse `check()` for synchronous domain rules and `checkAsync()` for asynchronous rules. Sync parsing rejects schemas with asynchronous checks.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\n// Boolean shorthand: return false to fail with default message\nconst EvenNumber = s.number().check((n) => n % 2 === 0);\n\n// String shorthand: return the message as a string\nconst Username = s\n .string()\n .min(3)\n .check((v) => !v.startsWith('_') || 'Cannot start with underscore');\n\n// Multiple issues via ctx.addIssue()\nconst Signup = s.object({ confirm: s.string(), password: s.string() }).check((v, ctx) => {\n if (v.password !== v.confirm) {\n ctx.addIssue({ code: 'custom', message: 'Passwords must match', path: ['confirm'] });\n }\n});\n```\n\n`checkAsync()` marks a schema as asynchronous: synchronous parsing throws at runtime instead of accepting an unchecked value, so async checks require `parseAsync()` or `safeParseAsync()`. Async checks compose at any nesting depth: arrays, objects, unions, intersections, tuples, maps, records, sets, lazy schemas, pipelines, and `s.discriminatedUnion(...)` branches all evaluate nested async checks correctly through `parseAsync()`.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst takenEmails = new Set(['ada@example.com']);\n\nconst AccountEmail = s\n .string()\n .email()\n .checkAsync(async (value, ctx) => {\n if (takenEmails.has(value)) {\n ctx.addIssue({ code: 'custom', message: 'Email is already taken', path: [] });\n }\n });\n\n// Async checks require parseAsync\nawait AccountEmail.parseAsync('grace@example.com');\n```\n\nUse `check()` for predicate-only rules too. Return `true` on success or message on failure.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst PositivePrice = s.number().check((value) => value > 0 || 'Must be positive');\nPositivePrice.parse(9.99);\n```\n\n## Strings, Numbers, and Safe Regex Usage\n\nUse schema helpers for common string and number constraints instead of hand-written predicates.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Password = s.string().min(12).regex(/[A-Z]/).regex(/[0-9]/);\nconst Price = s.number().nonNegative().multipleOf(0.01);\nconst LaunchWindow = s.date().min(new Date('2025-01-01T00:00:00.000Z'));\n```\n\nSpell strips stateful `/g` and `/y` flags from `regex()` patterns before validation. Repeated parses stay deterministic even when the original regular expression is reused.\n\n## Coercion and Transforms\n\nUse coercion when input arrives as strings, query parameters, or form values.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Query = s.object({\n draft: s.coerce.boolean().default(false),\n limit: s.coerce.number().int().positive().default(20),\n publishedAt: s.coerce.date().nullable(),\n search: s.coerce.string().trim().min(1).optional(),\n});\n\nconst parsed = Query.parse({\n draft: 'true',\n limit: '50',\n publishedAt: '2025-04-01T12:00:00.000Z',\n search: ' vielzeug ',\n});\n```\n\nUse `transform()` or `pipe()` after validation when downstream code needs a different output shape.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst TrimmedTags = s.array(s.string().trim().min(1)).transform((tags) => tags.map((tag) => tag.toLowerCase()));\nconst Slug = s.string().trim().min(1).pipe(s.string().slug());\n```\n\n## Introspection, Round-Trips, and JSON Schema\n\nUse declarative definitions when schemas need to cross process boundaries or feed tooling.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\n\nconst Product = s\n .object({\n id: s.string().uuid(),\n name: s.string().min(1),\n price: s.number().positive().multipleOf(0.01),\n })\n .label('Product');\n\nconst definition = Product.definition();\nconst jsonSchema = fromDefinition(definition);\n\nProduct.parse({ id: '550e8400-e29b-41d4-a716-446655440000', name: 'Keyboard', price: 129.99 });\nconsole.log(jsonSchema.title);\n```\n\nDefinitions are frozen serializable snapshots of declarative schema structure. Use `definition()` and `fromDefinition()` for external tooling. Schemas with runtime checks, transforms, defaults, catches, or preprocessors intentionally have no definition.\n\n## Messages\n\nSpell has no mutable process-wide configuration. Build one parse context per request, locale, or form, then pass it explicitly.\n\n```ts\nimport { createParseContext, s } from '@vielzeug/spell';\n\nconst User = s.object({ email: s.string().email() });\nconst german = createParseContext({\n object: { invalidKeys: () => 'Keine unbekannten Felder erlaubt' },\n});\n\nUser.safeParse({ email: 'ada@example.com', extra: true }, german);\n```\n\nInternal development warnings always use `console.warn` in development builds. Route application diagnostics in application code instead of mutating library-wide logger state.\n\n## Working with Validation Errors\n\nUse `SpellValidationError` helpers when you need UI-ready error structures.\n\n```ts\nimport { s, SpellValidationError } from '@vielzeug/spell';\n\nconst User = s.object({\n email: s.string().email(),\n profile: s.object({\n name: s.string().min(2),\n }),\n});\n\nconst result = User.safeParse({ email: 'nope', profile: { name: '' } });\n\nif (!result.success && result.error instanceof SpellValidationError) {\n const profileErrors = result.error.messagesAt('profile', 'name');\n console.log(profileErrors);\n}\n```\n\nKey inline errors by dot-path with `joinIssuePath()`, which also unwraps Standard Schema `{ key }` segments:\n\n```ts\nimport { joinIssuePath } from '@vielzeug/spell';\n\nfor (const issue of result.error.issues) {\n console.log(joinIssuePath(issue.path), issue.message); // 'profile.name: ...'\n}\n```\n\nUnion failures produce one stable `invalid_union` issue. Use `s.discriminatedUnion()` when a discriminator can identify the intended object branch and provide field-specific errors.\n\n## Validating synchronously in React\n\nAdapters that validate inside a render or `validate` callback need the sync parse surface. `AnySchema` does not expose `safeParse`; type the adapter parameter against `SyncParsable` instead:\n\n```ts\nimport { type SyncParsable } from '@vielzeug/spell';\n\nfunction spellValidator(schema: SyncParsable<unknown>) {\n return (values: unknown) => {\n const result = schema.safeParse(values);\n if (result.success) return undefined;\n\n return result.error.issues.map((issue) => ({\n message: issue.message,\n path: issue.path,\n }));\n };\n}\n\nspellValidator(s.object({ email: s.string() })); // no shim or cast\n```\n\n## Framework Integration\n\nSpell works anywhere you can call a function before state enters your app.\n\n::: code-group\n\n```tsx [React]\nimport { s } from '@vielzeug/spell';\n\nconst SearchParams = s\n .object({\n page: s.coerce.number().int().positive().default(1),\n q: s.string().trim().optional(),\n })\n .relaxed();\n\nexport function SearchPage({ rawParams }: { rawParams: unknown }) {\n const params = SearchParams.parse(rawParams);\n\n return (\n <div>\n {params.q ?? 'All results'}: page {params.page}\n </div>\n );\n}\n```\n\n```ts [Vue]\nimport { computed, ref } from 'vue';\nimport { s } from '@vielzeug/spell';\n\nconst Settings = s.object({\n locale: s.string().min(2),\n compact: s.coerce.boolean().default(false),\n});\n\nconst raw = ref<unknown>({ locale: 'en', compact: 'true' });\nconst settings = computed(() => Settings.parse(raw.value));\n```\n\n:::\n\nUse `safeParse()` at event boundaries and `parse()` inside trusted data flows.\n\n## Working with Other Vielzeug Libraries\n\nUse Spell as the validation layer and let other packages focus on transport, forms, or storage.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\nimport { schemaValidator } from '@vielzeug/forge/schema';\nimport { createCourier } from '@vielzeug/courier';\nimport { s } from '@vielzeug/spell';\n\nconst Profile = s.object({\n displayName: s.string().min(2),\n newsletter: s.boolean(),\n});\n\nconst form = createForm({\n initialValues: {\n displayName: '',\n newsletter: false,\n },\n validate: schemaValidator(Profile),\n});\n\nconst courier = createCourier({ baseUrl: '/api' });\nconst profile = Profile.parse(await courier.get('/profile'));\n```\n\nUse Spell definitions with `@vielzeug/codex` or other tooling when you need generated docs or external schema consumers.\n\n## Best Practices\n\n- Keep schemas close to the boundary where unknown data enters your app.\n- Use `s` consistently for construction; use explicit `/json` and `/predicates` subpaths for tooling.\n- Use `.default(() => value)` for mutable defaults such as arrays, objects, `Map`, and `Set`.\n- Call `.required()` when you want to remove `undefined` but keep `null` semantics intact.\n- Use `check()` with a `ctx` argument when you need `ctx.addIssue()`; return a message for simple predicate failures.\n- Use `checkAsync()` and `parseAsync()` for every asynchronous domain rule.\n- Build a parse context per request or test; never rely on mutable process-wide configuration.\n- Use `definition()` with `fromDefinition()` from `@vielzeug/spell/json` for external tooling.\n",
7
+ "examples": "---\ntitle: 'Spell: Examples'\ndescription: Practical examples and recipes for spell.\n---\n\n## Examples\n\n- [Validating API Payloads](./examples/api.md)\n- [Form-Safe Parsing](./examples/forms.md)\n- [Async Business Rules](./examples/async.md)\n- [Schema Introspection and Round-Trips](./examples/introspection.md)\n- [Unions, Intersections, and Variants](./examples/unions.md)\n"
8
8
  },
9
9
  "examples": [
10
10
  {
@@ -19,7 +19,7 @@
19
19
  },
20
20
  {
21
21
  "id": "basic-parsing",
22
- "code": "// Schema definition, type inference, and safe parsing\nimport { s } from '@vielzeug/spell'\n\nconst Product = s.object({\n id: s.string().uuid(),\n name: s.string().min(1).max(120),\n price: s.number().positive().multipleOf(0.01),\n tags: s.array(s.string().min(1)).default(() => []),\n})\n\n// Infer the TypeScript type directly from the schema\n// type Product = { id: string; name: string; price: number; tags: string[] }\n\n// parse() throws on failure — use when invalid input is a programmer error\nconst product = Product.parse({\n id: '550e8400-e29b-41d4-a716-446655440000',\n name: 'Mechanical Keyboard',\n price: 129.99,\n})\nconsole.log('Parsed:', product.name, '— tags:', product.tags)\n\n// safeParse() returns a tagged result union — use at untrusted boundaries\nconst bad = Product.safeParse({ id: 'not-a-uuid', name: '', price: -5 })\nif (!bad.success) {\n const paths = bad.error.issues.map(i => i.path.join('.') || 'root')\n console.log('Validation failed at:', paths.join(', '))\n}",
22
+ "code": "// Schema definition, type inference, and safe parsing\nimport { s } from '@vielzeug/spell'\n\nconst Product = s.object({\n id: s.string().uuid(),\n name: s.string().min(1).max(120),\n price: s.number().positive().multipleOf(0.01),\n tags: s.array(s.string().min(1)).default(() => []),\n})\n\n// Infer the TypeScript type directly from the schema\n// type Product = { id: string; name: string; price: number; tags: string[] }\n\n// parse() throws on failure: use when invalid input is a programmer error\nconst product = Product.parse({\n id: '550e8400-e29b-41d4-a716-446655440000',\n name: 'Mechanical Keyboard',\n price: 129.99,\n})\nconsole.log('Parsed:', product.name, ': tags:', product.tags)\n\n// safeParse() returns a tagged result union: use at untrusted boundaries\nconst bad = Product.safeParse({ id: 'not-a-uuid', name: '', price: -5 })\nif (!bad.success) {\n const paths = bad.error.issues.map(i => i.path.join('.') || 'root')\n console.log('Validation failed at:', paths.join(', '))\n}",
23
23
  "name": "Basic Parsing"
24
24
  },
25
25
  {
@@ -39,7 +39,7 @@
39
39
  },
40
40
  {
41
41
  "id": "discriminated-union",
42
- "code": "// s.discriminatedUnion() validates a discriminated union — objects sharing a common tag field.\n// Spell automatically injects the discriminator literal into each branch.\nimport { s } from '@vielzeug/spell'\n\nconst Event = s.discriminatedUnion('type', {\n click: s.object({ x: s.number(), y: s.number() }),\n keydown: s.object({ key: s.string(), repeat: s.boolean() }),\n resize: s.object({ width: s.number(), height: s.number() }),\n})\n\nconst click = Event.parse({ type: 'click', x: 100, y: 200 })\nconsole.log('click:', click)\n\nconst key = Event.parse({ type: 'keydown', key: 'Enter', repeat: false })\nconsole.log('keydown:', key)\n\n// Wrong discriminator value\nconst bad = Event.safeParse({ type: 'unknown', x: 0 })\nconsole.log('unknown type:', bad.success ? 'ok' : bad.error.issues[0].message)\n\n// Missing required field in matched branch\nconst missingField = Event.safeParse({ type: 'resize', width: 800 })\nconsole.log('missing height:', missingField.success ? 'ok' : missingField.error.issues[0].message)",
42
+ "code": "// s.discriminatedUnion() validates a discriminated union: objects sharing a common tag field.\n// Spell automatically injects the discriminator literal into each branch.\nimport { s } from '@vielzeug/spell'\n\nconst Event = s.discriminatedUnion('type', {\n click: s.object({ x: s.number(), y: s.number() }),\n keydown: s.object({ key: s.string(), repeat: s.boolean() }),\n resize: s.object({ width: s.number(), height: s.number() }),\n})\n\nconst click = Event.parse({ type: 'click', x: 100, y: 200 })\nconsole.log('click:', click)\n\nconst key = Event.parse({ type: 'keydown', key: 'Enter', repeat: false })\nconsole.log('keydown:', key)\n\n// Wrong discriminator value\nconst bad = Event.safeParse({ type: 'unknown', x: 0 })\nconsole.log('unknown type:', bad.success ? 'ok' : bad.error.issues[0].message)\n\n// Missing required field in matched branch\nconst missingField = Event.safeParse({ type: 'resize', width: 800 })\nconsole.log('missing height:', missingField.success ? 'ok' : missingField.error.issues[0].message)",
43
43
  "name": "Discriminated Union"
44
44
  },
45
45
  {
@@ -64,7 +64,7 @@
64
64
  },
65
65
  {
66
66
  "id": "object-defaults",
67
- "code": "import { s } from '@vielzeug/spell';\n\n// Schema where all fields have defaults\nconst ServerConfig = s.object({\n host: s.string().default('localhost'),\n port: s.number().int().positive().default(3000),\n tls: s.boolean().default(false),\n});\n\n// Get a fully filled config without providing any input\nconst config = ServerConfig.defaults();\nconsole.log(config);\n// { host: 'localhost', port: 3000, tls: false }\n\n// Works with nested schemas too\nconst AppConfig = s.object({\n server: ServerConfig,\n debug: s.boolean().default(false),\n});\n\n// Parse with partial input — missing fields use their defaults\nconst parsed = AppConfig.parse({ server: { host: 'prod.example.com', port: 443, tls: true }, debug: true });\nconsole.log(parsed.server.host); // 'prod.example.com'\n\n// Schema with required field (no default) — throws if .defaults() called\nconst Strict = s.object({ name: s.string() });\nconst result = Strict.safeParse({});\nconsole.log(result.success); // false — name is required\n",
67
+ "code": "import { s } from '@vielzeug/spell';\n\n// Schema where all fields have defaults\nconst ServerConfig = s.object({\n host: s.string().default('localhost'),\n port: s.number().int().positive().default(3000),\n tls: s.boolean().default(false),\n});\n\n// Get a fully filled config without providing any input\nconst config = ServerConfig.defaults();\nconsole.log(config);\n// { host: 'localhost', port: 3000, tls: false }\n\n// Works with nested schemas too\nconst AppConfig = s.object({\n server: ServerConfig,\n debug: s.boolean().default(false),\n});\n\n// Parse with partial input: missing fields use their defaults\nconst parsed = AppConfig.parse({ server: { host: 'prod.example.com', port: 443, tls: true }, debug: true });\nconsole.log(parsed.server.host); // 'prod.example.com'\n\n// Schema with required field (no default): throws if .defaults() called\nconst Strict = s.object({ name: s.string() });\nconst result = Strict.safeParse({});\nconsole.log(result.success); // false: name is required\n",
68
68
  "name": "Object Defaults"
69
69
  },
70
70
  {
@@ -79,14 +79,9 @@
79
79
  },
80
80
  {
81
81
  "id": "refinements",
82
- "code": "// check() and checkAsync() — explicit custom domain rules\nimport { s } from '@vielzeug/spell'\n\nconst reserved = new Set(['admin', 'root'])\n\n// check() is synchronous; return a string to fail with that message\nconst Username = s.string().min(3).check((value) =>\n !reserved.has(value) || value + ' is reserved'\n)\n\n// check() receives context for multiple issues or custom error codes\nconst Signup = s.object({ password: s.string().min(8), confirm: s.string() })\n .check((v, ctx) => {\n if (v.password !== v.confirm)\n ctx.addIssue({ code: 'custom', message: 'Passwords must match', path: ['confirm'] })\n })\n\n// check() also covers predicate-only domain rules\nconst EvenPort = s.number().int().min(1).max(65535)\n .check((n) => n % 2 === 0 || 'Port must be even')\n\nfor (const name of ['ad', 'admin', 'grace']) {\n const r = Username.safeParse(name)\n console.log(name, '->', r.success ? 'ok' : r.error.issues[0].message)\n}\n\nconst signupResult = Signup.safeParse({ password: 'secure123', confirm: 'different' })\nconsole.log('signup:', signupResult.success ? 'ok' : signupResult.error.issues[0].message)\n\nfor (const port of [8080, 3001, 443]) {\n const r = EvenPort.safeParse(port)\n console.log('port', port, '->', r.success ? 'ok' : r.error.issues[0].message)\n}",
82
+ "code": "// check() and checkAsync(): explicit custom domain rules\nimport { s } from '@vielzeug/spell'\n\nconst reserved = new Set(['admin', 'root'])\n\n// check() is synchronous; return a string to fail with that message\nconst Username = s.string().min(3).check((value) =>\n !reserved.has(value) || value + ' is reserved'\n)\n\n// check() receives context for multiple issues or custom error codes\nconst Signup = s.object({ password: s.string().min(8), confirm: s.string() })\n .check((v, ctx) => {\n if (v.password !== v.confirm)\n ctx.addIssue({ code: 'custom', message: 'Passwords must match', path: ['confirm'] })\n })\n\n// check() also covers predicate-only domain rules\nconst EvenPort = s.number().int().min(1).max(65535)\n .check((n) => n % 2 === 0 || 'Port must be even')\n\nfor (const name of ['ad', 'admin', 'grace']) {\n const r = Username.safeParse(name)\n console.log(name, '->', r.success ? 'ok' : r.error.issues[0].message)\n}\n\nconst signupResult = Signup.safeParse({ password: 'secure123', confirm: 'different' })\nconsole.log('signup:', signupResult.success ? 'ok' : signupResult.error.issues[0].message)\n\nfor (const port of [8080, 3001, 443]) {\n const r = EvenPort.safeParse(port)\n console.log('port', port, '->', r.success ? 'ok' : r.error.issues[0].message)\n}",
83
83
  "name": "Custom Validation"
84
84
  },
85
- {
86
- "id": "schema-walk",
87
- "code": "// Traverse a schema tree with walk() to extract field metadata.\nimport { s } from '@vielzeug/spell'\n\nconst Order = s.object({\n id: s.string().uuid(),\n amount: s.number().positive(),\n customer: s.object({\n email: s.string().email(),\n name: s.string().min(1),\n }),\n tags: s.array(s.string()).optional(),\n})\n\n// Collect every field name and whether it is optional.\nconst fields: { name: string; required: boolean }[] = []\n\nOrder.walk({\n object(node) {\n for (const [key, child] of Object.entries(node.shape)) {\n fields.push({ name: key, required: !child.isOptional })\n child.walk(this)\n }\n },\n // unknown() catches any kind without a handler; omitting it returns null instead of throwing\n unknown() {},\n})\n\nconsole.log('Fields:')\nfields.forEach(f => console.log(' ', f.name, f.required ? '(required)' : '(optional)'))\nconsole.log('Total:', fields.length)",
88
- "name": "Schema Traversal"
89
- },
90
85
  {
91
86
  "id": "string-validation",
92
87
  "code": "// Reuse one stateful regex safely across repeated parses in the browser REPL.\nimport { s } from '@vielzeug/spell'\n\nconst HexColor = s.string().regex(/#[0-9a-f]{6}/gy)\n\nfor (const value of ['#ff8800', '#ff8800', 'oops']) {\n const result = HexColor.safeParse(value)\n console.log(value, '=>', result.success)\n}",
@@ -94,31 +89,30 @@
94
89
  },
95
90
  {
96
91
  "id": "wrappers-and-defaults",
97
- "code": "// optional(), nullable(), default(), catch() — missing-value semantics\nimport { s } from '@vielzeug/spell'\n\n// optional: accepts undefined, passes through validation otherwise\nconst Nickname = s.string().min(2).optional().default('Guest')\n\nconsole.log(Nickname.parse(undefined)) // 'Guest'\nconsole.log(Nickname.parse('Ada')) // 'Ada'\n\n// nullable: accepts null explicitly\nconst Bio = s.string().max(200).nullable()\n\nconsole.log(Bio.parse(null)) // null\nconsole.log(Bio.parse('Loves types')) // 'Loves types'\n\n// nullish: accepts both null and undefined\nconst Avatar = s.string().url().nullish()\n\nconsole.log(Avatar.parse(null)) // null\nconsole.log(Avatar.parse(undefined)) // undefined\n\n// required(): strips undefined without removing null\nconst NullableButRequired = s.string().optional().nullable().required()\nconsole.log(NullableButRequired.parse(null)) // null\nconsole.log(NullableButRequired.safeParse(undefined).success) // false\n\n// catch(): returns a fallback when validation fails — never throws\nconst Port = s.number().int().min(1).max(65535).catch(3000)\nconsole.log(Port.parse(8080)) // 8080\nconsole.log(Port.parse('not-a-port')) // 3000",
92
+ "code": "// optional(), nullable(), default(), catch(): missing-value semantics\nimport { s } from '@vielzeug/spell'\n\n// optional: accepts undefined, passes through validation otherwise\nconst Nickname = s.string().min(2).optional().default('Guest')\n\nconsole.log(Nickname.parse(undefined)) // 'Guest'\nconsole.log(Nickname.parse('Ada')) // 'Ada'\n\n// nullable: accepts null explicitly\nconst Bio = s.string().max(200).nullable()\n\nconsole.log(Bio.parse(null)) // null\nconsole.log(Bio.parse('Loves types')) // 'Loves types'\n\n// nullish: accepts both null and undefined\nconst Avatar = s.string().url().nullish()\n\nconsole.log(Avatar.parse(null)) // null\nconsole.log(Avatar.parse(undefined)) // undefined\n\n// required(): strips undefined without removing null\nconst NullableButRequired = s.string().optional().nullable().required()\nconsole.log(NullableButRequired.parse(null)) // null\nconsole.log(NullableButRequired.safeParse(undefined).success) // false\n\n// catch(): returns a fallback when validation fails: never throws\nconst Port = s.number().int().min(1).max(65535).catch(3000)\nconsole.log(Port.parse(8080)) // 8080\nconsole.log(Port.parse('not-a-port')) // 3000",
98
93
  "name": "Wrappers & Defaults"
99
94
  }
100
95
  ],
101
96
  "typeSignatures": {
102
- "AnySchema": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
103
- "CheckContext": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
104
- "FlatError": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
105
- "FlatErrorFirst": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
106
- "Infer": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
107
- "InferInput": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
108
- "InferOutput": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
109
- "Issue": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
110
- "IssuePath": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
111
- "IssuePathSegment": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
112
- "JsonSchema": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
113
- "MessageFn": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
114
- "Messages": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
115
- "ParseContext": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
116
- "ParseResult": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
117
- "SchemaDescriptor": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
118
- "SchemaWalker": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
119
- "StandardSchemaV1": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
120
- "SyncParsable": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
121
- "ValidateResult": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaWalker,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
97
+ "AnySchema": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
98
+ "CheckContext": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
99
+ "FlatError": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
100
+ "FlatErrorFirst": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
101
+ "Infer": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
102
+ "InferInput": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
103
+ "InferOutput": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
104
+ "Issue": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
105
+ "IssuePath": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
106
+ "IssuePathSegment": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
107
+ "JsonSchema": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
108
+ "MessageFn": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
109
+ "Messages": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
110
+ "ParseContext": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
111
+ "ParseResult": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
112
+ "SchemaDescriptor": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
113
+ "StandardSchemaV1": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
114
+ "SyncParsable": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
115
+ "ValidateResult": "export type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n Issue,\n IssuePath,\n IssuePathSegment,\n JsonSchema,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n StandardSchemaV1,\n SyncParsable,\n ValidateResult,\n} from './core';",
122
116
  "ErrorCode": "export { ErrorCode, joinIssuePath, Schema, SpellDefinitionError, SpellError, SpellValidationError } from './core';",
123
117
  "joinIssuePath": "export { ErrorCode, joinIssuePath, Schema, SpellDefinitionError, SpellError, SpellValidationError } from './core';",
124
118
  "Schema": "export { ErrorCode, joinIssuePath, Schema, SpellDefinitionError, SpellError, SpellValidationError } from './core';",
@@ -127,6 +121,7 @@
127
121
  "SpellValidationError": "export { ErrorCode, joinIssuePath, Schema, SpellDefinitionError, SpellError, SpellValidationError } from './core';",
128
122
  "createParseContext": "export { createParseContext, type DeepPartial } from './messages';",
129
123
  "DeepPartial": "export { createParseContext, type DeepPartial } from './messages';",
130
- "s": "export { s } from './s';"
124
+ "s": "export { s } from './s';",
125
+ "tolerate": "export { tolerate } from './tolerate';"
131
126
  }
132
127
  }
@@ -0,0 +1,29 @@
1
+ {
2
+ "apiSource": "// Public root surface of @vielzeug/tandem. Every public export goes through this file.\nexport { createSync } from './tandem';\nexport type {\n SyncDeletion,\n SyncEnvelope,\n SyncGateway,\n SyncHandle,\n SyncOptions,\n SyncPort,\n SyncPullResult,\n SyncRecordBase,\n SyncState,\n TandemEvent,\n} from './types';\n",
3
+ "docs": {
4
+ "index": "---\ntitle: 'Tandem: Offline-first sync engine'\ndescription: Offline-first sync engine with rev baselines, tombstoned deletions, and idle-batched pushes for browser applications.\npackage: tandem\ncategory: sync\nkeywords: [sync, offline, conflict, tombstone, rev, replication, last-write-wins]\nrelated: [vault, postmaster, sentinel, ripple]\nexports: [createSync, SyncPort, SyncGateway, SyncHandle, TandemEvent]\nenvironments: [browser, node]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"tandem\" />\n\n## Why Tandem?\n\nA local-first app keeps its records in IndexedDB but must still reach a server: batch bursts of edits into one upload, propagate deletions that a plain pull would resurrect, and reconcile two devices that edited the same record while offline. Tandem drives that round trip against any backend, tracking a per-record write counter so a push carries only what actually changed.\n\n```ts\n// Before\n// Hand-rolled: debounce each save, POST the whole table, hope deletions survive.\nlet timer;\nfunction onSave(record) {\n clearTimeout(timer);\n timer = setTimeout(() => fetch('/sync', { method: 'POST', body: JSON.stringify(allRecords()) }), 3000);\n}\n\n// After\nimport { createSync } from '@vielzeug/tandem';\n\nconst sync = createSync({ gateway, port, idleDelayMs: 3000 });\nsync.changed(); // a local edit happened: the engine batches and pushes dirty records\n```\n\n| Feature | Tandem | RxSync | PowerSync |\n| ------------------------------------ | ------------------------------------------ | ------------------- | -------------------- |\n| Bundle size | <PackageInfo package=\"tandem\" type=\"size\" /> | <ore-icon name=\"x\" size=\"16\" /> | <ore-icon name=\"x\" size=\"16\" /> |\n| Zero runtime dependencies | <ore-icon name=\"check\" size=\"16\" /> | <ore-icon name=\"x\" size=\"16\" /> | <ore-icon name=\"x\" size=\"16\" /> |\n| Backend-agnostic (bring your `port`) | <ore-icon name=\"check\" size=\"16\" /> | <ore-icon name=\"x\" size=\"16\" /> | <ore-icon name=\"x\" size=\"16\" /> |\n| Storage-agnostic (bring your `gateway`) | <ore-icon name=\"check\" size=\"16\" /> | <ore-icon name=\"x\" size=\"16\" /> | <ore-icon name=\"check\" size=\"16\" /> |\n| Tombstoned deletions + rev conflicts | <ore-icon name=\"check\" size=\"16\" /> | <ore-icon name=\"check\" size=\"16\" /> | <ore-icon name=\"check\" size=\"16\" /> |\n\n<div class=\"decision-callout\">\n\n**Use `tandem` when** you already own a local store and a backend and want the sync scheduler: dirty tracking, batching, hide-flows, and conflict reconcile: without adopting a database or a server.\n\n**Consider `PowerSync` when** you want a managed sync service with its own SQLite client and server component rather than a scheduler over your existing storage.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/tandem\n```\n\n```sh [npm]\nnpm install @vielzeug/tandem\n```\n\n```sh [yarn]\nyarn add @vielzeug/tandem\n```\n\n:::\n\n## Quick Start\n\nImplement the two seams: a `SyncPort` for the server and a `SyncGateway` for local storage: then start the scheduler. Records are opaque to Tandem; it reads only `id` and `rev`, and every rev comparison is the engine's: the gateway upserts, validates, and tombstones.\n\n```ts\nimport { createSync, type SyncGateway, type SyncPort } from '@vielzeug/tandem';\n\nconst port: SyncPort = {\n pull: (since) => fetch(`/sync?since=${since ?? ''}`).then((r) => r.json()),\n push: async (records, deletions) => {\n const response = await fetch('/sync', { method: 'POST', body: JSON.stringify({ records, deletions }) });\n if (!response.ok) throw new Error(await response.text()); // divergence → the engine pulls to reconcile\n },\n};\n\nconst gateway: SyncGateway = {\n records: () => localRecords.map((record) => ({ entity: 'docs', record })),\n pendingDeletions: () => readTombstones(),\n applyRecords: (pulled) => upsert(pulled), // the engine pre-filters; you validate untrusted records\n applyDeletions: (deletions) => removeLocal(deletions),\n clearDeletions: (deletions) => dropTombstones(deletions),\n loadState: () => readSyncState(),\n saveState: (state) => writeSyncState(state),\n};\n\nconst sync = createSync({ gateway, port });\nsync.tap((event) => console.debug('sync', event)); // observe pushes, pulls, warnings\nmyStore.subscribe(() => sync.changed()); // a local write happened\n\n// On account switch or teardown: after flushing this account's work:\nawait sync.flush();\nsync.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`createSync()`**: drives a port against a gateway with a serialized push queue.\n- **`SyncPort`**: the server seam: `pull(since)` and `push(records, deletions)`.\n- **`SyncGateway`**: the storage seam: upserts, tombstones, and the persisted baseline: the engine owns every rev comparison.\n- **`rev` baselines**: a record is dirty exactly when its rev is above the last-synced one, across reloads.\n- **`changed()`**: wire it to your write path; the engine debounces a burst into one push.\n- **`flush()`**: a full cycle on demand: pull remote changes, then push dirty records; rejects on failure.\n- **Hide-flows**: flushes with `keepalive` on `pagehide`/`hidden`, runs a full cycle on return to the foreground.\n- **`tap()`**: typed `TandemEvent`s for pushes, pulls, invalid records, and warnings.\n- **Lifecycle-owned**: `dispose()`, `disposed`, `disposalSignal`, and `[Symbol.dispose]`.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Vault](/vault/): the typed storage core most gateways wrap; its IndexedDB adapter is a natural `records()`/`applyRecords()` backend.\n- [Postmaster](/postmaster/): the durable outbox half of offline sync; pair it with Tandem for at-least-once mutations alongside state sync.\n- [Sentinel](/sentinel/): subscribable browser state; its network signal can gate when you call `flush()`.\n- [Ripple](/ripple/): signals and effects; `store.subscribe(() => sync.changed())` is the usual wiring.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: 'Tandem: API Reference'\ndescription: Factory, port and gateway contracts, events, and types for Tandem.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createSync()` | Scheduler driving a port against a gateway | Sync (queues async work) | Boot runs pull-then-push; dispose before switching accounts |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/tandem` | `createSync`, port/gateway contracts, events, and types |\n\n## Factories\n\n### `createSync()`\n\n```ts\nfunction createSync<TRecord extends SyncRecordBase = SyncRecordBase>(\n options: SyncOptions<TRecord>,\n): SyncHandle;\n```\n\nStarts a sync scheduler: it loads the persisted baseline, pulls remote changes, pushes anything dirty, then flushes on `changed()`, on tab hide, and on foreground return.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.gateway` | `SyncGateway<TRecord>` | The device's storage seam |\n| `options.idleDelayMs` | `number \\| undefined` | Quiet period before a flush (default `3000`) |\n| `options.port` | `SyncPort<TRecord>` | The server seam |\n\n**Returns** a `SyncHandle`. Ambient flushes never reject: they surface failures as `warning` events through `tap()`. An explicit `flush()` rejects when its cycle fails.\n\n**Example**\n\n```ts\nimport { createSync } from '@vielzeug/tandem';\n\nconst sync = createSync({ gateway, idleDelayMs: 1500, port });\nsync.changed();\nawait sync.flush();\nsync.dispose();\n```\n\n#### `SyncHandle` methods\n\n| Member | Description |\n| --- | --- |\n| `changed()` | Notes a local change and (re)starts the idle flush timer |\n| `dispose()` | Stops the scheduler, clears the timer, removes document/window listeners |\n| `disposed` | `true` once disposed |\n| `disposalSignal` | `AbortSignal` aborted on dispose |\n| `flush()` | Runs a full cycle: pull, then push; resolves when it completes, rejects when it fails, and is a no-op after `dispose` |\n| `tap(handler, options?)` | Observes `TandemEvent`s; returns an unsubscribe function |\n| `[Symbol.dispose]()` | Calls `dispose()` for `using` scopes |\n\n## Contracts\n\n### `SyncPort`\n\nThe server seam. `pull(since)` returns remote records, remote deletions, and the next cursor. `push(records, deletions, options)` sends local changes; reject it when the server diverged so the engine pulls to reconcile. `options.keepalive` marks a flush that must survive the tab closing.\n\n### `SyncGateway`\n\nThe storage seam. The engine owns every rev comparison: it calls your gateway with the decisions already made.\n\n| Member | Contract |\n| --- | --- |\n| `records()` | This device's own records, remote mirrors excluded |\n| `pendingDeletions()` | Tombstones the server has not acknowledged |\n| `applyRecords(records)` | Upsert the given records: already filtered to what the server is ahead on; return the ids that failed validation |\n| `applyDeletions(deletions)` | Remove records deleted remotely |\n| `clearDeletions(deletions)` | Drop the given tombstones a successful push carried |\n| `loadState()` | The persisted `SyncState`, or `null` on a fresh device |\n| `saveState(state)` | Persist the baseline after a completed pull or push |\n\n## Events\n\n### `TandemEvent`\n\n```ts\ntype TandemEvent =\n | { readonly records: number; readonly type: 'pull' }\n | { readonly records: number; readonly type: 'push' }\n | { readonly message: string; readonly skipped: readonly string[]; readonly type: 'invalid' }\n | { readonly message: string; readonly type: 'warning' }\n | { readonly type: 'dispose' };\n```\n\n`invalid` reports record ids `applyRecords` refused; `warning` reports a rejected push or transport failure; `pull` and `push` report the record counts applied and pushed. `dispose` fires once, before tappers are cleared.\n\n## Types\n\n### `SyncRecordBase`\n\n```ts\ninterface SyncRecordBase {\n readonly id: string;\n readonly rev: number;\n}\n```\n\nThe only shape Tandem reads from a record: identity and a write counter. `rev` is required: the dirty model is a rev comparison, so every synced record carries one from birth.\n\n### `SyncEnvelope`\n\n```ts\ninterface SyncEnvelope<TRecord extends SyncRecordBase = SyncRecordBase> {\n readonly entity: string;\n readonly record: TRecord;\n}\n```\n\nA record paired with the entity (table or kind) it belongs to.\n\n### `SyncDeletion`\n\n```ts\ninterface SyncDeletion {\n readonly deletedAt: string;\n readonly entity: string;\n readonly id: string;\n}\n```\n\nA tombstone: a deletion that travels to the server on the next push.\n\n### `SyncState`\n\n```ts\ninterface SyncState {\n cursor: string | null;\n revs: Record<string, number>;\n}\n```\n\nThe persisted baseline. `revs` maps `\"${entity}:${id}\"` to the newest rev the server has seen.\n\n### `SyncPullResult`\n\n```ts\ninterface SyncPullResult<TRecord extends SyncRecordBase = SyncRecordBase> {\n readonly cursor: string | null;\n readonly deletions: readonly SyncDeletion[];\n readonly records: readonly SyncEnvelope<TRecord>[];\n}\n```\n\n### `SyncOptions`\n\n```ts\ninterface SyncOptions<TRecord extends SyncRecordBase = SyncRecordBase> {\n gateway: SyncGateway<TRecord>;\n idleDelayMs?: number;\n port: SyncPort<TRecord>;\n}\n```\n\n## Errors\n\nTandem defines no error classes. A rejected `push` is expected input, not a failure: the engine pulls to reconcile and reports it as a `warning` event. `flush()` additionally rejects with the underlying port or gateway error; dirty records survive the failure and retry on the next cycle.\n",
6
+ "usage": "---\ntitle: 'Tandem: Usage Guide'\ndescription: 'How to drive offline sync with @vielzeug/tandem: ports, gateways, rev baselines, tombstones, and reconciliation.'\n---\n\n[[toc]]\n\n## Basic Usage\n\nTandem sits between your storage and your server. You implement two interfaces and call `createSync`; the engine handles batching, dirty tracking, hide-flows, and conflict reconcile.\n\n```ts\nimport { createSync, type SyncGateway, type SyncPort } from '@vielzeug/tandem';\n\ninterface Doc {\n id: string;\n rev: number;\n title: string;\n}\n\nconst port: SyncPort<Doc> = {\n async pull(since) {\n return fetch(`/sync/pull?since=${since ?? ''}`).then((response) => response.json());\n },\n async push(records, deletions, options) {\n const response = await fetch('/sync/push', {\n body: JSON.stringify({ records, deletions }),\n headers: { 'Content-Type': 'application/json' },\n keepalive: options?.keepalive,\n method: 'POST',\n });\n if (!response.ok) throw new Error(`server diverged: ${await response.text()}`);\n },\n};\n\nconst gateway: SyncGateway<Doc> = {\n records: () => docs.map((record) => ({ entity: 'docs', record })),\n pendingDeletions: () => readTombstones(),\n async applyRecords(pulled) {\n // The engine already filtered these to records the server is ahead on;\n // validate the untrusted remotes and upsert the rest.\n const skipped: string[] = [];\n for (const { record } of pulled) {\n if (!isValid(record)) skipped.push(record.id);\n else upsert(record);\n }\n return skipped;\n },\n applyDeletions: (deletions) => Promise.all(deletions.map((d) => remove(d.id))),\n clearDeletions: (deletions) => Promise.all(deletions.map((d) => dropTombstone(d.id))),\n loadState: () => readRow('syncState'),\n saveState: (state) => writeRow('syncState', state),\n};\n\nconst sync = createSync<Doc>({ gateway, port });\n```\n\n## Track Dirty Records with Revs\n\nTandem never diffs record contents. Each record carries a `rev` counting its committed writes, and the gateway persists a baseline of the newest rev the server has seen. A record is dirty exactly when its current rev is above that baseline.\n\n```ts\n// Your write path bumps the rev on every committed change.\nfunction saveDoc(id: string, patch: Partial<Doc>): void {\n const current = docs.find((doc) => doc.id === id)!;\n upsert({ ...current, ...patch, rev: current.rev + 1 });\n sync.changed(); // schedule a flush\n}\n```\n\nBecause the baseline is persisted, dirtiness survives a reload: a record edited while offline is still above the baseline when the app restarts, and the boot sequence pushes it. The engine never asks your gateway to compare revs: ahead, dirty, and deletion precedence are all its decisions.\n\n## Propagate Deletions with Tombstones\n\nA plain pull cannot carry a deletion: the record is gone. Record a tombstone when you delete locally; Tandem pushes it and clears it only once the server acknowledges.\n\n```ts\nfunction deleteDoc(id: string): void {\n remove(id);\n writeTombstone({ deletedAt: new Date().toISOString(), entity: 'docs', id });\n sync.changed();\n}\n```\n\nA remote deletion loses to a local record with unpushed changes: if your local rev is above the baseline, Tandem keeps your edit and pushes it instead of applying the deletion. The guard runs both ways: a pulled record whose id is locally tombstoned, or deleted in the same pull, never applies, so a backend without an incremental cursor cannot resurrect your deletions.\n\n## Batch Changes and Flush\n\nCall `changed()` on every local write. The engine debounces a burst into one push after `idleDelayMs` (default 3000), and serializes pushes so two never overlap.\n\n```ts\nconst sync = createSync({ gateway, idleDelayMs: 1500, port });\n\n// Force a full cycle now: on a save button, or before navigating away.\ntry {\n await sync.flush();\n} catch {\n // the cycle failed: edits stay local and dirty; the next cycle retries them\n}\n```\n\n`flush()` pulls remote changes and then pushes dirty records, so it is also the wiring point for a returning network connection. It rejects when either half fails; the records stay dirty and the next cycle retries them. A `flush()` after `dispose()` resolves as a no-op.\n\nTandem also flushes on its own: it pushes with `keepalive: true` when the tab hides or closes, and runs a full cycle when the tab returns to the foreground. Serve the keepalive push with `navigator.sendBeacon` or a `keepalive` fetch so it survives the tab closing.\n\n## Reconcile a Rejected Push\n\nWhen the server is ahead of the lineage you pushed, reject the push. Tandem immediately pulls to reconcile, then the next flush retries with the merged state. Ambient flushes (debounce, hide) report the failure as a `warning` event; `flush()` additionally rejects with the port's error.\n\n```ts\nasync push(records, deletions) {\n const response = await fetch('/sync/push', { body: JSON.stringify({ records, deletions }), method: 'POST' });\n if (response.status === 409) throw new Error('server ahead'); // triggers a reconcile pull\n}\n```\n\n## Observe with tap()\n\n`tap()` is the observation seam: pushes, pulls, refused records, and warnings. Handler errors are swallowed, so an observer never breaks sync.\n\n```ts\nconst unsubscribe = sync.tap((event) => {\n switch (event.type) {\n case 'push':\n case 'pull':\n console.debug(`sync ${event.type}: ${event.records} records`);\n break;\n case 'invalid':\n console.warn(event.message, event.skipped);\n break;\n case 'warning':\n console.warn(event.message); // push rejected, network failure, etc.\n break;\n }\n});\n```\n\n## Lifecycle and Disposal\n\nDispose before switching accounts: the baseline is per-account state and a running scheduler would push into the wrong port.\n\n```ts\nconst sync = createSync({ gateway, port });\n// ...\nsync.dispose(); // stops the scheduler, clears the timer, removes document/window listeners\n```\n\n`disposed` and `disposalSignal` report the state; `[Symbol.dispose]` lets you scope it with `using`.\n\n```ts\n{\n using sync = createSync({ gateway, port });\n sync.changed();\n} // disposed here\n```\n\n## Testing\n\nDrive the engine against in-memory doubles. A `SyncPort` double records pushes and returns scripted pulls; a `SyncGateway` double is a record array plus a tombstone list plus a state row.\n\n```ts\nimport { createSync } from '@vielzeug/tandem';\n\nconst pushes: unknown[][] = [];\nconst port: SyncPort = {\n pull: async () => ({ cursor: null, deletions: [], records: [] }),\n push: async (records, deletions) => void pushes.push([records, deletions]),\n};\n\nconst sync = createSync({ gateway: fakeGateway, idleDelayMs: 10, port });\nawait vi.waitFor(() => expect(pushes.length).toBe(1)); // startup push\nsync.dispose();\n```\n\n## Framework Integration\n\nTandem is framework-neutral. Wire `changed()` to your reactive layer and `dispose()` to the component or scope lifecycle.\n\n::: code-group\n\n```ts [Vue 3]\nimport { onScopeDispose, watch } from 'vue';\n\nconst sync = createSync({ gateway, port });\nwatch(store.records, () => sync.changed(), { deep: true });\nonScopeDispose(() => sync.dispose());\n```\n\n```tsx [React]\nimport { useEffect, useRef } from 'react';\n\nconst syncRef = useRef(createSync({ gateway, port }));\nuseEffect(() => {\n const sync = syncRef.current;\n return () => sync.dispose();\n}, []);\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n const sync = createSync({ gateway, port });\n $effect(() => { $records; sync.changed(); });\n onDestroy(() => sync.dispose());\n</script>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n- **Vault**: the usual `gateway` backend. Its IndexedDB adapter stores records and the sync-state row; `records()` reads the in-memory mirror, `applyRecords()` writes through the codec.\n- **Postmaster**: complementary, not overlapping. Postmaster guarantees at-least-once delivery of discrete mutations; Tandem keeps whole-record state in step. Apps that need both run them side by side.\n- **Sentinel**: subscribe to `createNetwork()` and call `sync.flush()` when the connection returns; the cycle pulls and pushes, so remote changes land without waiting for the next foreground return.\n- **Ripple**: `store.subscribe(() => sync.changed())` is the idiomatic wiring for a signal-backed store.\n\n## Best Practices\n\n- Bump `rev` on every committed write: the whole dirty model depends on it.\n- Validate remote records in `applyRecords()` and return the ids you refuse; never trust a pulled record.\n- Keep `records()` free of remote session mirrors: it must return only this device's own records.\n- Dispose on account switch; a stale scheduler pushes into the wrong port.\n- Serve `keepalive` pushes with `sendBeacon` or a `keepalive` fetch so they survive tab close.\n- Reject a push on divergence instead of silently overwriting; let the reconcile pull merge.\n- Persist the sync-state row per account, alongside the records it describes.\n- Call `flush()` before actions that assume both sides are current, such as a share or export, and handle its rejection.\n",
7
+ "examples": "---\ntitle: 'Tandem: Examples'\ndescription: Offline sync recipes for two-device editing, deletions, and account switching.\n---\n\n## Examples\n\n- [Sync a Vault Store](./examples/sync-a-vault-store.md)\n- [Keep Deletions from Resurrecting](./examples/keep-deletions-from-resurrecting.md)\n- [Switch Accounts Safely](./examples/switch-accounts-safely.md)\n"
8
+ },
9
+ "examples": [
10
+ {
11
+ "id": "create-sync",
12
+ "code": "import { createSync } from '@vielzeug/tandem'\n\n// Two in-memory doubles stand in for the device store and the server.\nlet docs = [{ id: 'a', rev: 0, title: 'First doc' }]\nlet tombstones = []\nlet storedState = null\nconst server = []\n\nconst gateway = {\n records: () => docs.map((record) => ({ entity: 'docs', record })),\n pendingDeletions: async () => [...tombstones],\n async applyRecords(pulled) {\n // The engine already filtered these to records the server is ahead on.\n for (const { record } of pulled) {\n docs = [...docs.filter((d) => d.id !== record.id), record]\n }\n return []\n },\n async applyDeletions(deletions) {\n for (const { id } of deletions) docs = docs.filter((d) => d.id !== id)\n },\n async clearDeletions(deletions) {\n tombstones = tombstones.filter((t) => !deletions.some((d) => d.id === t.id))\n },\n loadState: async () => storedState,\n saveState: async (state) => { storedState = structuredClone(state) },\n}\n\nconst port = {\n pull: async () => ({ cursor: null, deletions: [], records: [] }),\n push: async (records, deletions) => {\n server.push({ records: [...records], deletions: [...deletions] })\n },\n}\n\nconst sync = createSync({ gateway, idleDelayMs: 20, port })\nsync.tap((event) => console.log('event:', event.type))\n\nawait new Promise((r) => setTimeout(r, 50)) // boot: load baseline, pull, push the seed\nconsole.log('after boot:', server.length) // 1\n\n// A burst of edits becomes ONE push: bump the rev, then announce the change.\nfor (let i = 1; i <= 3; i++) {\n docs = docs.map((d) => (d.id === 'a' ? { ...d, rev: d.rev + 1, title: `v${i}` } : d))\n sync.changed()\n}\n\nawait new Promise((r) => setTimeout(r, 120))\nconsole.log('pushes:', server.length) // 2: one flush for three edits\nconsole.log('pushed rev:', server.at(-1).records[0].record.rev) // 3: one upload carries all of them\nsync.dispose()",
13
+ "name": "createSync - Batched Pushes"
14
+ }
15
+ ],
16
+ "typeSignatures": {
17
+ "createSync": "export { createSync } from './tandem';",
18
+ "SyncDeletion": "export type {\n SyncDeletion,\n SyncEnvelope,\n SyncGateway,\n SyncHandle,\n SyncOptions,\n SyncPort,\n SyncPullResult,\n SyncRecordBase,\n SyncState,\n TandemEvent,\n} from './types';",
19
+ "SyncEnvelope": "export type {\n SyncDeletion,\n SyncEnvelope,\n SyncGateway,\n SyncHandle,\n SyncOptions,\n SyncPort,\n SyncPullResult,\n SyncRecordBase,\n SyncState,\n TandemEvent,\n} from './types';",
20
+ "SyncGateway": "export type {\n SyncDeletion,\n SyncEnvelope,\n SyncGateway,\n SyncHandle,\n SyncOptions,\n SyncPort,\n SyncPullResult,\n SyncRecordBase,\n SyncState,\n TandemEvent,\n} from './types';",
21
+ "SyncHandle": "export type {\n SyncDeletion,\n SyncEnvelope,\n SyncGateway,\n SyncHandle,\n SyncOptions,\n SyncPort,\n SyncPullResult,\n SyncRecordBase,\n SyncState,\n TandemEvent,\n} from './types';",
22
+ "SyncOptions": "export type {\n SyncDeletion,\n SyncEnvelope,\n SyncGateway,\n SyncHandle,\n SyncOptions,\n SyncPort,\n SyncPullResult,\n SyncRecordBase,\n SyncState,\n TandemEvent,\n} from './types';",
23
+ "SyncPort": "export type {\n SyncDeletion,\n SyncEnvelope,\n SyncGateway,\n SyncHandle,\n SyncOptions,\n SyncPort,\n SyncPullResult,\n SyncRecordBase,\n SyncState,\n TandemEvent,\n} from './types';",
24
+ "SyncPullResult": "export type {\n SyncDeletion,\n SyncEnvelope,\n SyncGateway,\n SyncHandle,\n SyncOptions,\n SyncPort,\n SyncPullResult,\n SyncRecordBase,\n SyncState,\n TandemEvent,\n} from './types';",
25
+ "SyncRecordBase": "export type {\n SyncDeletion,\n SyncEnvelope,\n SyncGateway,\n SyncHandle,\n SyncOptions,\n SyncPort,\n SyncPullResult,\n SyncRecordBase,\n SyncState,\n TandemEvent,\n} from './types';",
26
+ "SyncState": "export type {\n SyncDeletion,\n SyncEnvelope,\n SyncGateway,\n SyncHandle,\n SyncOptions,\n SyncPort,\n SyncPullResult,\n SyncRecordBase,\n SyncState,\n TandemEvent,\n} from './types';",
27
+ "TandemEvent": "export type {\n SyncDeletion,\n SyncEnvelope,\n SyncGateway,\n SyncHandle,\n SyncOptions,\n SyncPort,\n SyncPullResult,\n SyncRecordBase,\n SyncState,\n TandemEvent,\n} from './types';"
28
+ }
29
+ }