# @pyreon/form

## 0.44.0

### Patch Changes

- [#2149](https://github.com/pyreon/pyreon/pull/2149) [`c79a5f4`](https://github.com/pyreon/pyreon/commit/c79a5f492836645e41db6467b97c61ae2694c903) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Unknown-field-name handling is now CONSISTENT across every lookup API (release-audit finding: the same user mistake — an undeclared field name — produced five different behaviors). The contract:

  - **Binding/subscription APIs throw actionable guidance**: `register("missing")` (the flagship binding API) previously crashed with a bare `TypeError: … reading 'value'`; it now throws the same `[@pyreon/form]` message as `useField`/`setFieldValue`, including the `registerField()` escape hatch for dynamic fields. `useWatch(form, "missing")` (single + array forms) likewise now throws the guidance instead of a bare TypeError.
  - **Validity means INVALID**: `trigger("missing")` / `trigger(["title","missing"])` previously resolved `true` — silently reporting an undeclared field VALID. Unknown names now count as invalid (resolve `false`) + dev-warn naming them ("matched no field must mean INVALID", the same principle as schema-error routing).
  - **Probes stay probes, honestly typed**: `getFieldState()` returns `FieldState | undefined` — it always returned `undefined` at runtime for unknown names; the non-optional type just deferred the crash to your first property read. It now doubles as a documented existence probe for dynamic fields. (Type-level breaking: add `?.`/`!` at call sites that assume existence.)
  - **Baseline writes skip + dev-warn unknown keys**: `setInitialValues()`/`reset(values)` silently merged typo keys into the internal baseline; extra keys (e.g. server payloads carrying `id`/`createdAt`) are now skipped with a dev warning instead of polluting state — deliberately NOT a throw, so reset-to-server-data keeps working.
  - **Fields-array overload accepts `focusOnError`**: the overload's inline options literal had drifted from the exported `UseFormFieldsOptions` (TS2769) — fields-overload users couldn't opt out of a default-ON behavior. The overload now uses the exported type.

  All bisect-verified (guards reverted → 5 specs fail with the pre-fix behaviors; restored → green), plus the previously-unasserted `useField` guidance message is now test-locked.

- [#2152](https://github.com/pyreon/pyreon/pull/2152) [`7021725`](https://github.com/pyreon/pyreon/commit/702172586ab3ede234cddc19ee691f82b30c4770) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Value-integrity fixes across the form core — four release-audit defects, each with a proven failure scenario:

  - **Unrecognized `schema` now THROWS instead of silently disabling validation.** `useForm({ schema: { safeParse: (x) => x } as never })` (a zod<3.24 schema without `~standard` support, or a typo'd object) previously resolved to NO validator — `validate()` returned `true`, `errors()` stayed `{}`, and `onSubmit` fired with unvalidated data. `resolveSchemaValidator` now throws the same `[Pyreon] \`schema\` must be a SchemaValidateFn, a TypedSchemaAdapter…`guidance`@pyreon/store`gives for the identical input. Plain functions,`@pyreon/validation` adapters, raw Standard Schemas, and absent/`undefined` schemas are unaffected.

  - **`reset(values)` re-basing is now durable (react-hook-form `defaultValues`-replacement parity).** The per-field dirty compare and `field.reset()` captured the ORIGINAL initial at setup, ignoring the baseline moved by `reset(values)` / `setInitialValues`: after `reset({ name: 'saved' })`, typing `'x'` then back to `'saved'` reported `isDirty() === true` forever (unsaved-changes guards misfired on the exact reset-to-saved-server-data flow), and `resetField('name')` / a later plain `reset()` reverted to the ORIGINAL initial. All baseline reads now go through the live `currentInitials` record (one source of truth); `getValues()`'s fallback was reconciled onto it too (it read the stale `initialValues` while `getSubmitValues` read `currentInitials`). `_dirtyCount` stays consistent across baseline moves.

  - **Explicit `null`/`undefined` field values are no longer swallowed by the initial.** `values()` and the submit payload used `fields[name]?.value.peek() ?? initial`, so `setFieldValue('age', null)` silently reported the original initial — with file inputs (`FileList | null`), a server-prefilled file field cleared to `null` submitted the stale initial. Both reads now branch on FIELD EXISTENCE, never on value nullishness.

  - **Dynamic-field re-registration can no longer resurrect a stale async validation.** `registerField` seeded the per-field validation version at 0, so an unregister + re-register of the same name reused the old field's version space — a still-in-flight OLD async validator whose captured version collided wrote its stale error onto the fresh field. Every version (seed and per-run bump, for field validators AND per-field schema runs) now comes from one monotonic form-level counter, so a version value is never reused.

- Updated dependencies [[`38deec0`](https://github.com/pyreon/pyreon/commit/38deec0695ae616960966766e530e1b42d138ed1), [`d859370`](https://github.com/pyreon/pyreon/commit/d8593704b0941ef0e51a427147ebce2a385ecae3)]:
  - @pyreon/validation@0.44.0
  - @pyreon/reactivity@0.44.0
  - @pyreon/core@0.44.0

## 0.43.1

### Patch Changes

- Updated dependencies []:
  - @pyreon/validation@0.43.1

## 0.43.0

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.43.0
  - @pyreon/reactivity@0.43.0
  - @pyreon/validation@0.43.0

## 0.42.0

### Minor Changes

- [#2127](https://github.com/pyreon/pyreon/pull/2127) [`6c03a11`](https://github.com/pyreon/pyreon/commit/6c03a118d2c2ee35e1ac76b9962e11f98f52077d) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Add explicit runtime field registration for dynamic / data-driven forms: `form.registerField(name, initialValue?, validator?)` and `form.unregisterField(name)`. A registered field is fully first-class — it reaches `values()` / `onSubmit` and participates in validity — and unregistering cleanly removes its contribution to the invalid/dirty counts. This is the explicit escape hatch from the static-by-default model (there is still no _silent_ auto-registration, which would drop data). Dynamic fields are runtime-typed (not in the static `TValues`); read them via `getValues()[name]` / `fields[name]`. Internally the per-field setup was extracted into a reusable `createFieldState` so registered fields get identical wiring.

- [#2125](https://github.com/pyreon/pyreon/pull/2125) [`b1479a5`](https://github.com/pyreon/pyreon/commit/b1479a57a83d860fc1c738d2fcfb6850c9304c88) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Add file input support: `register(field, { type: 'file' })` returns a value-less props bag (a file input can't be value-controlled) whose `onInput` writes the input's `FileList` (`target.files`) to the field — so `field.value()` is a `FileList | null` (read `files?.[0]` for a single file). The file value flows into `values()` / `onSubmit` like any other field.

- [#2124](https://github.com/pyreon/pyreon/pull/2124) [`6376915`](https://github.com/pyreon/pyreon/commit/63769159fb169209278845b0e6e607879faf54ba) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Accessible error recovery: on a failed `handleSubmit`, focus now moves to the first errored + `register()`-bound field (react-hook-form's `shouldFocusError`, defaulted on). Opt out with `useForm({ focusOnError: false })`. Also exposes `form.focusFirstError()` for custom submit flows. SSR-safe; skips fields not bound via `register()`.

- [#2123](https://github.com/pyreon/pyreon/pull/2123) [`707e1be`](https://github.com/pyreon/pyreon/commit/707e1bee8455d0347dc13dd0f6845dd60971588e) Thanks [@vitbokisch](https://github.com/vitbokisch)! - `useForm({ schema })` now accepts a **raw Standard Schema** (zod ≥3.24 / valibot ≥1 / arktype ≥2 / `@pyreon/validate`'s `s`) directly — no `zodSchema()` adapter and no `as never` cast. The validation contract and Standard Schema bridge moved to `@pyreon/validation` (the universal validation gate); `@pyreon/form` depends on it and re-exports `ValidationError` / `ValidateFn` / `SchemaValidateFn` for back-compat, so existing `import { ValidationError } from '@pyreon/form'` keeps working.

- [#2126](https://github.com/pyreon/pyreon/pull/2126) [`fda03d2`](https://github.com/pyreon/pyreon/commit/fda03d2c023d5aebbcb5abc1ae5908b051e418df) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Reset ergonomics (react-hook-form parity): `reset(values?, options?)` now accepts new `values` to reset TO (the named fields become the new baseline; the rest revert to their original initial — the idiomatic "reset to freshly-saved server data" flow), plus `options` to preserve state across the reset — `keepErrors` / `keepTouched` / `keepDirty` / `keepSubmitCount`. `resetField(field, options?)` gains `keepError` / `keepTouched`.

### Patch Changes

- [#2120](https://github.com/pyreon/pyreon/pull/2120) [`538c92a`](https://github.com/pyreon/pyreon/commit/538c92a651bcf55f2b97dbdeab45c4099fd8c2dc) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Fix a silent validation bypass: a whole-form schema error keyed by a path that didn't match a top-level field was dropped, so `validate()` reported the form valid and `onSubmit` fired with invalid data. The zod/valibot/arktype adapters flatten a nested issue to a dot-path key (`address.city`) and path-less errors land under `""` — neither matched a top-level field. Now a nested error routes to its ancestor object field (`address.city` → `address`), and any key matching no field marks the form invalid + sets `submitError` + dev-warns. Both the submit and blur validation paths are fixed.

- Updated dependencies [[`f2a5a26`](https://github.com/pyreon/pyreon/commit/f2a5a262b5b497e735c825678c2b7a86d55ec87a), [`1a29fc3`](https://github.com/pyreon/pyreon/commit/1a29fc3d761b4facfe5e77d1503ffc3fd4f036e3), [`707e1be`](https://github.com/pyreon/pyreon/commit/707e1bee8455d0347dc13dd0f6845dd60971588e)]:
  - @pyreon/validation@0.42.0
  - @pyreon/core@0.42.0
  - @pyreon/reactivity@0.42.0

## 0.41.2

### Patch Changes

- [#2114](https://github.com/pyreon/pyreon/pull/2114) [`3ebf924`](https://github.com/pyreon/pyreon/commit/3ebf924cff00ed5bfeb0a099f66f578409fe4c18) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Fundamental completions of the migration-finding fixes:

  - **@pyreon/styler**: `insertGlobal`'s `@layer`-unsupported fallback now flattens `@layer` blocks RECURSIVELY — handling MULTIPLE sibling blocks and NESTED blocks, not just a single outer block. Previously only a lone `@layer name{…}` spanning the whole string was unwrapped; sibling/nested layers still failed to insert in happy-dom.
  - **@pyreon/form**: documented the STATIC-FIELD-MODEL contract in `use-form.ts` (why `useField`/`register` throw for undeclared fields instead of auto-registering — auto-registering would silently drop the field from the epoch-cached `values()`), and regression-locked the actionable "declare it in useForm({ initialValues })" guidance in the field-not-found errors.

## 0.41.1

### Patch Changes

- [#2108](https://github.com/pyreon/pyreon/pull/2108) [`12ce8e7`](https://github.com/pyreon/pyreon/commit/12ce8e72ffeff8b692db698301431674f7f87c40) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Two migration-surfaced fixes:

  - **@pyreon/styler**: `insertGlobal` now unwraps a single outer `@layer name{…}` block when `@layer` is unsupported (happy-dom, older engines), so global rules (e.g. a CSS reset) still land in the DOM via source order instead of being silently dropped with a per-insert DOMException warning. The scoped `insert()` path already gated `@layer` on `supportsLayer`; `insertGlobal` was the one path that didn't.
  - **@pyreon/form**: the "field not found" errors (`useField`, `setFieldValue`, `setFieldError`) now tell you the fix — declare the field in `useForm({ initialValues })` — and state that `@pyreon/form` does not auto-register fields on first use (unlike react-hook-form). Turns a "component threw during setup → blank UI" mystery into a self-explaining error.

## 0.41.0

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.41.0
  - @pyreon/reactivity@0.41.0

## 0.40.0

### Patch Changes

- Updated dependencies [[`c184330`](https://github.com/pyreon/pyreon/commit/c184330594a7726c4f1f1095cc3a785cfe9ef3f7), [`ed364d2`](https://github.com/pyreon/pyreon/commit/ed364d2a34f4b74df94c02f3c2e630b96a4f2e7f)]:
  - @pyreon/reactivity@0.40.0
  - @pyreon/core@0.40.0

## 0.39.0

### Patch Changes

- Updated dependencies [[`fa95aba`](https://github.com/pyreon/pyreon/commit/fa95aba3aebc24d0178093cd89870b8807beca72), [`794fb27`](https://github.com/pyreon/pyreon/commit/794fb27e6fa67e71608b603cd627cf4eff61a102), [`f7083e5`](https://github.com/pyreon/pyreon/commit/f7083e5a56768fb67e097ec9bc6ee6d1bc6e0d09), [`c82687c`](https://github.com/pyreon/pyreon/commit/c82687c07a2b2ba976787dea74bc891f72a1165a)]:
  - @pyreon/reactivity@0.39.0
  - @pyreon/core@0.39.0

## 0.38.0

### Patch Changes

- [#1896](https://github.com/pyreon/pyreon/pull/1896) [`5a39b0a`](https://github.com/pyreon/pyreon/commit/5a39b0ac0042dfa2ff8d120aa3679dbe98742014) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Lazy-allocate the four off-hot-path per-field signals (`error` / `touched` /
  `disabled` / `readOnly`), keeping `value` + `dirty` eager (both are written by
  `setValue` on the keystroke path, so they stay captured directly in the closures
  — no getter on the hot path, no V8 deopt). A freshly created N-field form now
  allocates **2N signals at setup instead of 6N**, materializing the rest on first
  access with stable signal identity. The `error` signal's `_invalidCount`
  subscriber attaches on materialization (until then the field has no error and
  contributes 0, so `isValid` stays correct).

  Measured (Tier-A headless bench, Apple M3 / Node 24, TanStack Form as the stable
  in-bench control): `setup-12-fields` **~4.8µs → ~3.65µs (−24%)** — now ties/beats
  TanStack Form, closing the one op where Pyreon was behind; the keystroke hot path
  is unchanged (40–42ns); `reset` is ~15% faster (it skips resetting unmaterialized
  signals). All existing behavior is preserved (228 form tests pass; the lazy
  contract is bisect-verified by `form-additional.test.tsx`).

- Updated dependencies [[`cfa422f`](https://github.com/pyreon/pyreon/commit/cfa422fdb6985e50c74e06cf0f4c1318213d6303), [`0376a3d`](https://github.com/pyreon/pyreon/commit/0376a3ddc75dd1fbee582e7cabe98beb01d60073), [`6ee46e7`](https://github.com/pyreon/pyreon/commit/6ee46e7dca1cb01aacaa7c61ef5dbbcf12b30668)]:
  - @pyreon/reactivity@0.38.0
  - @pyreon/core@0.38.0

## 0.37.1

## 0.37.0

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.37.0
  - @pyreon/reactivity@0.37.0

## 0.36.0

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.36.0
  - @pyreon/reactivity@0.36.0

## 0.35.0

### Minor Changes

- [#1736](https://github.com/pyreon/pyreon/pull/1736) [`1f29c4b`](https://github.com/pyreon/pyreon/commit/1f29c4b9791e6ad96901ca0e2b90e5335b803895) Thanks [@vitbokisch](https://github.com/vitbokisch)! - feat(form): auto-wire field accessibility (zero-config ARIA)

  `register(field)` now returns, in addition to value/onInput/onBlur, an
  auto-generated stable `id` plus reactive `aria-invalid` (`'true'` while the
  field has an error, attribute removed when valid) and `aria-describedby`
  (points at the field's error element while errored). New `errorProps(field)`
  and `labelProps(field)` helpers (on both `useForm` and `useField`) return
  ids that AGREE with the input, so:

  ```tsx
  <label {...form.labelProps('email')}>Email</label>
  <input {...form.register('email')} />
  {() => form.fields.email.error()
    ? <span {...form.errorProps('email')}>{form.fields.email.error()}</span>
    : null}
  ```

  gives full label↔control and input↔error association + an announced error
  (`role="alert"`) with no hand-threaded ids — all derived from existing field
  state, reactive in place (0 re-renders). This makes the most common form a11y
  mistake the zero-effort path, and resolves the label↔control association an
  AST lint rule structurally can't (the deferred `control-needs-label` cliff) at
  the runtime layer where the ids are knowable.

  `@pyreon/core` (patch): the JSX `aria-invalid` and `aria-describedby`
  attributes now also accept the reactive accessor form (`() => …`), matching
  `aria-required`/`aria-readonly`. Additive — existing static usage is
  unchanged.

- [#1692](https://github.com/pyreon/pyreon/pull/1692) [`bf6865c`](https://github.com/pyreon/pyreon/commit/bf6865c815e2ee4499995f9aba91591fa26a86f3) Thanks [@vitbokisch](https://github.com/vitbokisch)! - react-hook-form-parity additions + a competitor benchmark suite.

  New, strictly-typed accessors on `FormState` (all additive — no breaking
  changes):

  - **`trigger(field?)`** — validate a single field, a subset (array), or — with
    no argument — the whole form, on demand. Runs validators immediately
    (bypassing `debounceMs`) and returns whether the validated set is valid.
    Reuses the exact same per-field validation path as auto-validation, so a
    schema-only field gets its schema error applied too.
  - **`getValues(field?)`** — read one field's value (`getValues('email')`) or
    all values (`getValues()`, same as `values()`).
  - **`dirtyFields()` / `touchedFields()`** — the changed / visited fields as a
    `Partial<Record<keyof TValues, boolean>>` record. Reactive.
  - **`getFieldState(field)`** — a field's live `FieldState` signals (the same
    object as `form.fields[field]`), as a typed method.
  - **`isSubmitted`** (`Accessor<boolean>`, `submitCount > 0`) and
    **`isSubmitSuccessful`** (`Signal<boolean>`, true only after the most recent
    submit's `onSubmit` ran without a validation failure or throw; cleared by
    `reset()`). Both also surfaced on `useFormState`'s summary.

  Benchmarks (`packages/fundamentals/form/bench/`, real installed competitor
  deps) prove the perf position vs the most popular libraries:

  - Headless core vs TanStack Form (`form-bench.ts`): Pyreon's per-field signal
    write beats TanStack's store clone+notify on the keystroke hot path by
    **~17.7×** (`update-field` 193ns vs 3.48µs) and `reset` by ~4×. TanStack
    edges Pyreon on once-per-form `setup` (1.4×) and full-values `read` (15.8×) —
    the inverse face of the same plain-object-store trade-off that makes its
    writes 17.7× slower.
  - Re-renders vs Formik + react-hook-form (`form-rerender-bench.ts`): typing 10
    keystrokes into 1 field of a 20-field form triggers **0** Pyreon component
    re-renders (signals patch the bound node, values stay reactive) = RHF's 0
    (uncontrolled refs, but values not reactively bound) < Formik's 10
    (controlled — re-renders the form every keystroke).

  Known gap (deliberate, tracked): nested / deep typed field paths
  (`user.address.city`) — `TValues` keys are flat; the composable `field()` +
  flat model is the current design.

### Patch Changes

- [#1700](https://github.com/pyreon/pyreon/pull/1700) [`ce49268`](https://github.com/pyreon/pyreon/commit/ce49268f21615478fe5544ce5ab385b74704c75d) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Eliminate the per-field auto-revalidation `effect()` — the keystroke hot path
  is now ~5.3× faster, with no behavior change.

  `useForm` previously created one `effect()` per field that tracked `valueSig`

  - `submitCount` to drive auto-validation. That cost a per-field `EffectScope`
    at setup (the dominant per-field allocation) AND re-ran on EVERY keystroke
    (tracking `valueSig`) — even in the default `blur` mode where the body is a
    no-op. The auto-validation logic is now driven INLINE from `setValue` (the
    canonical value-mutation path), reading `submitCount.peek()` instead of
    tracking it:

  * **`update-field` (keystroke hot path): 242ns → 46ns (~5.3× faster)** — vs
    TanStack Form the lead widens from ~15× to **~79×** (46ns vs 3.60µs).
  * **`reset`: ~35% faster** (no per-keystroke/per-reset effect flush).
  * **`setup`: ~17% faster** (no per-field `EffectScope` allocation).

  Behavior is identical and bisect-verified: the two preserved triggers —
  change-mode validation and post-failed-submit live error-correction — are
  exactly covered by existing specs (dropping the `submitCount.peek()` trigger
  fails 3 post-submit-revalidation tests; dropping the change-mode setup
  validation fails the change-mode test). change-mode forms still validate their
  initial values once at setup (a one-time `validateField` call in the field
  loop, replacing the effect's setup-run). All 206 tests pass.

  The `form.fieldEffectCreate` dev perf-counter is removed (the effect it
  measured no longer exists).

  **Also: an epoch-cached `values()` / `getValues()` snapshot.** Rebuilding the
  values object on every read was O(N) (~109ns/12 fields vs a plain store's
  ~7ns). It's now cached and rebuilt only when a value-mutation method
  (`setValue` / `reset` / `setInitialValues`) advances an internal epoch — a
  clean read is an integer compare + cached-object return (**read-all-values:
  109ns → ~6ns, now FASTER than TanStack's ~7ns**), and the write path pays only
  one integer increment (no signal/subscriber → the 43ns keystroke path is
  untouched). Contract: the snapshot reflects mutations through the form's
  methods; a direct `form.fields.x.value.set(...)` bypasses the epoch (same as it
  already bypasses dirty-tracking + auto-validation — use `setFieldValue`). 6 new
  bisect-verified specs lock the invalidation contract.

  Net result vs TanStack Form (headless `form-bench.ts`): Pyreon now wins
  **update ~83×**, **read ~1.3×**, **reset ~6×**, and **0 vs 10 re-renders** —
  losing only once-per-form `setup` (~1.3×), the deliberate price of per-field
  fine-grained signals (which buy the 83× write win). (A dirty-count-inline
  attempt to close `setup` was measured + reverted — it regressed the keystroke
  path ~2× via V8 deopt of a larger `setValue`; the dirty subscriber was already
  free on the hot path since it fires only on dirty transitions, not per
  keystroke.)

- [#1710](https://github.com/pyreon/pyreon/pull/1710) [`ac75935`](https://github.com/pyreon/pyreon/commit/ac7593520f4467cd7ba362178ee00ca7029794da) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Fix `useFormState(form)` (no selector) `isValid` / `isDirty` not updating the
  rendered DOM.

  The no-selector summary's `isValid` / `isDirty` came from the count-based
  `form.isValid()` / `form.isDirty()` (backed by `_invalidCount` / `_dirtyCount`),
  but those counts are updated by a DEFERRED field-signal subscriber. The
  no-selector summary ALSO depends on the field signals (via the `errors` /
  `dirtyFields` atoms it materializes), so it re-derived on the field-signal
  change BEFORE the count caught up — rendering a STALE value. Symptom: a UI bound
  to `useFormState(form)().isDirty` / `.isValid` (e.g. a "Dirty" / "Invalid"
  badge) never flipped when a field changed.

  Fix: in the no-selector path, derive `isValid` / `isDirty` from the `errors` /
  `dirtyFields` atoms it already materializes (`Object.keys(errors).length === 0`
  / `Object.keys(dirtyFields).length > 0`) — the SAME field-signal dependency that
  triggers the recompute, so the value is always consistent. No added cost (the
  atoms were already read). The **selector** path is unchanged and keeps the O(1)
  count-based getters (it doesn't read the atoms, so it was never subject to the
  race — and remains the recommended pattern for hot UI).

  Pre-existing bug (not introduced by the perf changes in this release). Locked by
  mount-based regression specs + a real-Chromium form-interaction e2e
  (`e2e/fundamentals/form-interaction.spec.ts`).

- Updated dependencies [[`1f29c4b`](https://github.com/pyreon/pyreon/commit/1f29c4b9791e6ad96901ca0e2b90e5335b803895), [`02b77ae`](https://github.com/pyreon/pyreon/commit/02b77aed6b4383554b3458e408b462098fc3e708), [`35d440a`](https://github.com/pyreon/pyreon/commit/35d440a44d92ac913cf19f3f8e21b4603458a165)]:
  - @pyreon/core@0.35.0
  - @pyreon/reactivity@0.35.0

## 0.34.0

### Patch Changes

- [#1611](https://github.com/pyreon/pyreon/pull/1611) [`038a58c`](https://github.com/pyreon/pyreon/commit/038a58c0f39a35ad4338f6d2596c33c47e4e30cc) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Internal coverage hardening — documented `v8 ignore`s for genuinely-unreachable
  defensive guards (deepMerge's non-plain-input safety net, the plain-mode
  `config.state ?? {}` fallback that `model()` rejects upstream, the
  `snapshotValue` meta-guard already gated by `isModelInstance`, the nested-walk
  `applyPatch` non-instance guard) + a test for the `onValidationError`-suppressed
  patch path. No behavior change. Branches → 98.85%, S/F/L → 100%.
- Updated dependencies [[`66d44c5`](https://github.com/pyreon/pyreon/commit/66d44c58920bf81848e9ba858c413a88727a3c65)]:
  - @pyreon/reactivity@0.34.0
  - @pyreon/core@0.34.0

## 0.33.0

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.33.0
  - @pyreon/reactivity@0.33.0

## 0.32.0

### Patch Changes

- Updated dependencies [[`0e38332`](https://github.com/pyreon/pyreon/commit/0e3833212e93ec90994edfccb5f2966f9eb0e926), [`0c1ea1e`](https://github.com/pyreon/pyreon/commit/0c1ea1e89e4228e84367efd5d2cb334808955a25), [`e36bbe5`](https://github.com/pyreon/pyreon/commit/e36bbe52e7f1417a703b4e6ce23281c448d9132f), [`65ccdf2`](https://github.com/pyreon/pyreon/commit/65ccdf2ad95a16b676b58948acea51f957e5cf62), [`7f89196`](https://github.com/pyreon/pyreon/commit/7f89196dd3d99f61b0bba032481b9d389fdd8264)]:
  - @pyreon/core@0.33.0
  - @pyreon/reactivity@0.33.0

## 0.31.0

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.33.0
  - @pyreon/reactivity@0.33.0

## 0.30.0

### Patch Changes

- Updated dependencies [[`6feb9d4`](https://github.com/pyreon/pyreon/commit/6feb9d4bc8cc873191bfe97fac0afb88d5135388), [`883e69b`](https://github.com/pyreon/pyreon/commit/883e69baed47d77eb79f4dd09b87da96a0b52894), [`4efa71b`](https://github.com/pyreon/pyreon/commit/4efa71b83af84b9310681ed213a331842248bb65), [`960bb0f`](https://github.com/pyreon/pyreon/commit/960bb0f139839de49508d836878b98556b1c7d07), [`b720267`](https://github.com/pyreon/pyreon/commit/b720267f0d9fbe260398c56d49834dc1dd2b09fb)]:
  - @pyreon/reactivity@0.33.0
  - @pyreon/core@0.33.0

## 0.29.0

### Patch Changes

- [#1321](https://github.com/pyreon/pyreon/pull/1321) [`c2874df`](https://github.com/pyreon/pyreon/commit/c2874df8f2b07b19aaa7a64c2f9ff2ab6b11d2f0) Thanks [@vitbokisch](https://github.com/vitbokisch)! - fix: derive the singleton-sentinel version from package.json (was a stale hardcoded `0.24.6`)

  Every `@pyreon/*` package called `registerSingleton('@pyreon/X', '0.24.6', import.meta.url)`
  with a hardcoded version literal that the release process never bumped — so the
  duplicate-instance sentinel reported `0.24.6` for packages actually shipping
  `0.28.x`. The version is diagnostic-only (detection keys on module location, not
  version), but its diagnostic VALUE is exactly to surface a version skew between
  two installed copies — which a frozen literal silently defeats.

  Name + version are now derived from each package's own `package.json`
  (`import { name, version } from '../package.json' with { type: 'json' }`), so the
  diagnostic is always accurate and can never drift on release. The build inlines
  the strings (no `package.json` bloat); dev reads the live file. No new tooling
  needed — drift is structurally impossible.

- Updated dependencies [[`c54ce0f`](https://github.com/pyreon/pyreon/commit/c54ce0f284dab0335d9b597488ba75c6dea92b43), [`6d3e085`](https://github.com/pyreon/pyreon/commit/6d3e085183ec42883a842967afe22f806f0ea21d), [`c2874df`](https://github.com/pyreon/pyreon/commit/c2874df8f2b07b19aaa7a64c2f9ff2ab6b11d2f0), [`e1139cc`](https://github.com/pyreon/pyreon/commit/e1139cc20447860a2c0e547e6fc0ed67f359e1fe)]:
  - @pyreon/reactivity@0.33.0
  - @pyreon/core@0.33.0

## 0.28.1

### Patch Changes

- [#1210](https://github.com/pyreon/pyreon/pull/1210) [`9be0265`](https://github.com/pyreon/pyreon/commit/9be0265553ff756383b21f9c0ab556949d7cadb0) Thanks [@vitbokisch](https://github.com/vitbokisch)! - test(coverage): bulk-bump 31 packages' `statements` threshold 94 → 95 (already passing)

  PR 1 of the "whole-repo coverage ≥ 95%" initiative (user-approved sequence:
  by-gap-size, start with quick wins).

  Every package in this bump is **already reporting ≥ 95% actual** per
  `bun scripts/check-coverage.ts`. Locking the configured threshold in
  match prevents regressions and lets the `Coverage (Full)` CI gate enforce
  the new floor.

  **No runtime changes, no test additions** — pure config update.
  Drift-detection in `BELOW_FLOOR_EXEMPTIONS` was triggered for two
  exemption entries (`@pyreon/code`, `@pyreon/kinetic`) which had been
  listed with `currentStatements: 94`; updated to 95 with the new reason
  documenting the lift.

  Packages bumped (current actual in parens):

  - @pyreon/attrs (100), @pyreon/coolgrid (100), @pyreon/table (100), @pyreon/toast (100)
  - @pyreon/rocketstyle (99.41), @pyreon/primitives (99.26), @pyreon/i18n (99.21), @pyreon/validation (99.12)
  - @pyreon/rx (98.45), @pyreon/kinetic (98.24), @pyreon/feature (98.11), @pyreon/head (97.97), @pyreon/flow (97.94), @pyreon/form (97.94), @pyreon/document-primitives (97.82), @pyreon/preact-compat (97.68), @pyreon/server (97.54), @pyreon/svelte-compat (97.42), @pyreon/validate (98.69), @pyreon/dnd (97.33)
  - @pyreon/query (96.79), @pyreon/mcp (96.52), @pyreon/unistyle (96.36) [already 95], @pyreon/reactivity (96.13), @pyreon/connector-document (96.05), @pyreon/react-compat (96.03) [already 95]
  - @pyreon/storage (95.6), @pyreon/permissions (95.38), @pyreon/url-state (95.13), @pyreon/runtime-dom (95.02), @pyreon/code (95.02), @pyreon/core (95.68), @pyreon/vite-plugin (95.32)

  Pre-existing CI failures NOT addressed in this PR (separate follow-ups):

  - @pyreon/sized-map: 0% reported by check-coverage.ts (test detection bug — Tier 5)
  - @pyreon/styler: 93.16% < 94% threshold (Tier 3)
  - @pyreon/ui-core: 90.94% < 94% threshold (Tier 4)
  - @pyreon/zero: 91.65% < 94% threshold (Tier 4)
  - @pyreon/runtime-dom: branches 85.78% < 88% threshold (Tier 6)

  Next PR (Tier 2): close the < 1pt gaps on charts, elements, hooks,
  hotkeys, lint, router, state-tree with focused test additions.

## 0.28.0

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.33.0
  - @pyreon/reactivity@0.33.0

## 0.27.1

## 0.27.0

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.33.0
  - @pyreon/reactivity@0.33.0

## 0.26.3

## 0.26.2

## 0.26.1

## 0.26.0

### Patch Changes

- [#1117](https://github.com/pyreon/pyreon/pull/1117) [`fd3422c`](https://github.com/pyreon/pyreon/commit/fd3422cfec1d48c8b382f8512ed44f8256887931) Thanks [@vitbokisch](https://github.com/vitbokisch)! - perf(form,dnd): batch() 6 multi-signal write sites surfaced by `pyreon/no-unbatched-updates`

  Lint follow-up to PR [#1110](https://github.com/pyreon/pyreon/issues/1110) (fundamentals R2/R3/R4 batch audit) — six sites in `use-form.ts` + `use-sortable.ts` were missed by the manual audit but caught by the `pyreon/no-unbatched-updates` lint rule.

  Each `batch()` wrap collapses N notify cycles to 1 per event:

  - **`@pyreon/form` `use-form.ts`** (4 sites):
    - `field.reset()` — 4 signal writes per call (value / error / touched / dirty).
    - `validate()` clear-all-errors loop — N writes per validate.
    - `validate()` schema-error apply loop — N writes per validate.
    - `setInitialValues()` per-field loop — 4×N writes per call (typical use is async-prefill landing post-mount).
  - **`@pyreon/dnd` `use-sortable.ts`** (3 sites):
    - Container `onDrop` reset (activeId/overId/overEdge) — 3 writes per drop.
    - Per-item `onDrop` reset in `queueMicrotask` — 3 writes per drop.
    - `onCleanup` final reset — 3 writes per sortable dispose.

  Bisect-proven via a real `@pyreon/reactivity` synthetic harness: 4 un-batched signal writes fire 4 effect re-runs; 4 batched writes fire 1. The optimization is observable subscriber-side; consumers reading 2+ form fields together (typical for validation hints) see one re-render per `field.reset()` instead of four.

  Also documents one lint false positive (`runValidation` — 3 `errorSig.set()` calls in 3 mutually exclusive branches) with an inline `pyreon-lint-disable-next-line` + rationale comment. The lint rule counts function-scope total writes, not per-branch fan-out.

  Lint count: `@pyreon/form` 4→0 (+1 documented exemption), `@pyreon/dnd` 3→0.

- [#945](https://github.com/pyreon/pyreon/pull/945) [`745fd63`](https://github.com/pyreon/pyreon/commit/745fd63c3ce97d0eb7bab37fa85ae40ed8c1c9bd) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Fix two DX walls surfaced by [#942](https://github.com/pyreon/pyreon/issues/942)'s HN-clone audit:

  **W10 — `useForm({ schema, validateOn: 'blur' })` now actually validates on blur.**
  Previously `setTouched()` only ran the per-field `validators[name]` function;
  the form-level `schema` only fired in `validate()` on submit. So a schema-only
  form (the canonical zod / valibot / arktype shape) with `validateOn: 'blur'`
  silently behaved like `'submit'` — the option name lied. Fix: when a schema
  is configured and the field has no per-field validator, `setTouched()` now
  runs the schema and applies ONLY this field's resulting error (other fields
  are left untouched so users aren't surprised with errors on fields they
  haven't visited). Versioned to discard stale results from interleaved
  blurs. 5 new specs in `tests/schema-blur.test.tsx`; bisect-verified.

  **W12 — `@pyreon/charts` now fails LOUD when the tslib alias is missing.**
  ECharts imports `tslib` for TypeScript helpers (`__extends`, `__assign`, …);
  tslib's CJS factory shape causes the named-helper destructure to read
  `undefined` unless the consumer's vite.config has the `chartsViteAlias()`
  alias. Without it, charts silently rendered as empty divs — the error
  was buried in a signal nobody read, taking ~25 minutes to diagnose.
  Now: (a) `getCore()` detects the tslib helper name in the error message
  and rewraps with a prescriptive "Add `chartsViteAlias()` to your
  vite.config" hint with the actual code snippet, (b) `<Chart>` surfaces
  the error to `console.error` AND renders an inline error display in dev.
  5 new specs in `tests/tslib-alias-detection.test.ts`; bisect-verified.

- Updated dependencies [[`885d6d9`](https://github.com/pyreon/pyreon/commit/885d6d95f02b9dd1b462c1ba1114ecf94350671a), [`cc8e6ac`](https://github.com/pyreon/pyreon/commit/cc8e6ac08faaea4e486cbb09d1ea22404421e8b6), [`ba09525`](https://github.com/pyreon/pyreon/commit/ba09525e947ebff5573222332bd0f1548fcfae77), [`a31f7dd`](https://github.com/pyreon/pyreon/commit/a31f7dd8f8ddba6864c69bbf53117d36ddd477a3), [`71901d4`](https://github.com/pyreon/pyreon/commit/71901d4366e993542a0a8252647b7a4b0e8ec3d2), [`1921168`](https://github.com/pyreon/pyreon/commit/192116843a0547c777e884f0254ffc51a69bfae1), [`749c2f4`](https://github.com/pyreon/pyreon/commit/749c2f435909740ea43d528ebfc00a2155e64f74)]:
  - @pyreon/reactivity@0.33.0
  - @pyreon/core@0.33.0

## 0.25.1

### Patch Changes

- [#902](https://github.com/pyreon/pyreon/pull/902) [`b87fbac`](https://github.com/pyreon/pyreon/commit/b87fbaced0cbeb7304bdc1d358040818e4b1491e) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Ship source maps in published tarballs.

  Every `@pyreon/*` package now ships its `.js.map` and `.d.ts.map` files. The previous `!lib/**/*.map` exclusion in each package's `files` array left every emitted JS file pointing at a `//# sourceMappingURL=*.map` that wasn't actually published — causing Vite (and other bundlers) to log a "Failed to load source map" warning per file on every cold dev start. Real bug in shipped tarballs, not just dev-noise theory.

  The fix is shipping the maps. They make framework stack traces readable: `at mountChild (node_modules/@pyreon/runtime-dom/src/nodes.ts:147)` instead of `at e (node_modules/@pyreon/runtime-dom/lib/index.js:1:42857)`. This matters most when a user hits a framework bug, opens devtools, or sees an unreadable production error from a server-side render. Sentry / Bugsnag / Rollbar can also translate framework frames using the shipped maps; without them, the framework's part of every captured stack stays opaque.

  Cost: ~350KB-1MB per package in `node_modules`. Bundlers (Vite, Webpack, Rollup, esbuild) strip source maps from production builds automatically; they never reach end users. Every comparable library (React, Vue, Solid, Preact, Svelte, TanStack) does this.

  No API changes. The `check-distribution` CI gate inverts to enforce the new contract (maps must be present, not absent).

- Updated dependencies [[`c862965`](https://github.com/pyreon/pyreon/commit/c8629652a94ca7d1e8622cd2de5b4ac009874dbf), [`b87fbac`](https://github.com/pyreon/pyreon/commit/b87fbaced0cbeb7304bdc1d358040818e4b1491e)]:
  - @pyreon/reactivity@0.25.1
  - @pyreon/core@0.25.1

## 0.25.0

### Patch Changes

- [#883](https://github.com/pyreon/pyreon/pull/883) [`6075127`](https://github.com/pyreon/pyreon/commit/60751278894a6ff843c0f6f6c4894c76bcb6a720) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Singleton sentinel default-on across every `@pyreon/*` package with module-level state (PR A of the bullet-proof cross-module-instance plan, `.claude/plans/jaunty-herding-kazoo.md`).

  Each package's `src/index.ts` now calls `registerSingleton('@pyreon/<name>', <version>, import.meta.url)` at module load. The first registration records a marker on `globalThis`; a second registration with a DIFFERENT normalized location triggers detection. Default mode throws an actionable Error naming both file paths and three concrete fixes (Vite `resolve.dedupe`, `npm ls`, `bun ls`). `PYREON_SINGLE_INSTANCE=warn` demotes to `console.error`; `PYREON_SINGLE_INSTANCE=silent` opts out entirely (browser extensions, micro-frontends, nested SSR via `rocketstyle-collapse`).

  **HMR-aware.** Vite re-evaluates modules with the SAME path but possibly different query params (`?v=12345`, `?t=12345`, `?import`). The sentinel normalizes the location (strips query string) before comparing — same normalized location → HMR re-eval → silently allowed; different location → genuine dual-instance → throws.

  **Per-package detection.** The earlier prototype put the sentinel only in `@pyreon/reactivity` — insufficient because `@pyreon/core` (and every other package) has its own module-level state that can be silently corrupted under dual-load. The full plan requires per-package registration, which this PR ships.

  **Zero behavior change in correct setups.** Apps that already have a single instance of each `@pyreon/*` package (the overwhelmingly common case) see no runtime change. Apps with silently-tolerated duplicates today (sub-dep version mismatch, custom bundler config) will see their app throw at startup after upgrading with an error message naming the fix. `PYREON_SINGLE_INSTANCE=warn` is the immediate mitigation for any consumer surprised by the change.

  **Test coverage.** Contract tests at `packages/core/reactivity/src/tests/singleton-sentinel.test.ts` (57 specs) exercise the sentinel directly with synthetic `file://` URLs: default-mode throw + actionable error message, HMR re-eval allowance, `PYREON_SINGLE_INSTANCE=warn` / `=silent` escape hatches, per-package coverage across all 24 registered packages, and cross-package isolation. Bisect-verified — neutralizing the throw branch fails 49 positive-case tests; restored passes all 57. The synthetic-URL approach replaces the heavier filesystem dual-load reproducer (it's the sentinel's normalized-string comparison that matters, not Node's ESM loader behaviour).

- Updated dependencies [[`7da5b2b`](https://github.com/pyreon/pyreon/commit/7da5b2bcbc2aebd9600cb8fdefb763ace7f78c1a), [`bc145f3`](https://github.com/pyreon/pyreon/commit/bc145f3dd6ff8414ab3d36f7723d7f1217d19835), [`cddc592`](https://github.com/pyreon/pyreon/commit/cddc5926f2f23d1b600d01f60fa4e72513d2b6fe), [`6075127`](https://github.com/pyreon/pyreon/commit/60751278894a6ff843c0f6f6c4894c76bcb6a720), [`f71fb4c`](https://github.com/pyreon/pyreon/commit/f71fb4c1b219e19189a58afeadcd6a7c9f5957fb)]:
  - @pyreon/reactivity@0.25.0
  - @pyreon/core@0.25.0

## 0.24.6

### Patch Changes

- Updated dependencies [[`378efde`](https://github.com/pyreon/pyreon/commit/378efdeeba7236f7a07aadcd778d527002446777)]:
  - @pyreon/core@0.24.6
  - @pyreon/reactivity@0.24.6

## 0.24.5

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.24.5
  - @pyreon/reactivity@0.24.5

## 0.24.4

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.24.4
  - @pyreon/reactivity@0.24.4

## 0.24.3

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.24.3
  - @pyreon/reactivity@0.24.3

## 0.24.2

### Patch Changes

- Updated dependencies [[`1c1b135`](https://github.com/pyreon/pyreon/commit/1c1b135f3a5b5be626ff92149a4f5059024210e3)]:
  - @pyreon/core@0.24.2
  - @pyreon/reactivity@0.24.2

## 0.24.1

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.24.1
  - @pyreon/reactivity@0.24.1

## 0.24.0

### Patch Changes

- Updated dependencies [[`dfaefb8`](https://github.com/pyreon/pyreon/commit/dfaefb8e9e06eaff9039c001ad7731476b6b5732), [`67e1f37`](https://github.com/pyreon/pyreon/commit/67e1f371a20219481ee9564d2d7421ec2a0b5ddf), [`b8fb31c`](https://github.com/pyreon/pyreon/commit/b8fb31cf1a59578fc33f27d539695d2bc164b2f1), [`f400e85`](https://github.com/pyreon/pyreon/commit/f400e85282a370276d5ae0266ba501c41dce4f3e), [`891ca43`](https://github.com/pyreon/pyreon/commit/891ca4300727119dafd66ceaacd7cb39e68f3b4e), [`d4ec777`](https://github.com/pyreon/pyreon/commit/d4ec777643446ed2c51dedb1e74fbd8dce70bdfd), [`2abb672`](https://github.com/pyreon/pyreon/commit/2abb672d8a8bf7f4940af422bf8bf802aa129cdd)]:
  - @pyreon/core@0.24.0
  - @pyreon/reactivity@0.24.0

## 0.23.0

### Patch Changes

- Updated dependencies [[`6571df8`](https://github.com/pyreon/pyreon/commit/6571df8209c5dc72619194ffe19359765b1d2d7f), [`af4d5d8`](https://github.com/pyreon/pyreon/commit/af4d5d83fc087d738dbe5084950476566d488d77), [`441b5df`](https://github.com/pyreon/pyreon/commit/441b5dfa64ae52002d3e6612ec68566344ae999d)]:
  - @pyreon/core@0.23.0
  - @pyreon/reactivity@0.23.0

## 0.22.0

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.22.0
  - @pyreon/reactivity@0.22.0

## 0.21.0

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.21.0
  - @pyreon/reactivity@0.21.0

## 0.20.0

### Patch Changes

- Updated dependencies [[`3499594`](https://github.com/pyreon/pyreon/commit/3499594585b7fcb650ac0f80be4bc355f741491b)]:
  - @pyreon/reactivity@0.20.0
  - @pyreon/core@0.20.0

## 0.19.0

### Patch Changes

- Updated dependencies [[`c3d0a70`](https://github.com/pyreon/pyreon/commit/c3d0a7017ed2ef4468ec3fb4e4c09ec869d2917a), [`ecd8e52`](https://github.com/pyreon/pyreon/commit/ecd8e526943a1e6b07957ff96f4410fa482baa0d), [`ac1d375`](https://github.com/pyreon/pyreon/commit/ac1d37542b11cd95451a2f0b0a51cc43603d001a), [`21e465c`](https://github.com/pyreon/pyreon/commit/21e465c7957c3e57c838af58ffa995682908c5f8), [`c4b6e9a`](https://github.com/pyreon/pyreon/commit/c4b6e9a5850196171c2197fc918163f736708aa8), [`fb40906`](https://github.com/pyreon/pyreon/commit/fb409066e49e44c42f77084a92a68103a4e6c5ef), [`9f03747`](https://github.com/pyreon/pyreon/commit/9f037478763d9f8cd2365feb63dc87fda2545e5d), [`3374150`](https://github.com/pyreon/pyreon/commit/33741500499dfb487d031bbffe77723d74b8f261), [`fa4e37f`](https://github.com/pyreon/pyreon/commit/fa4e37fa620cf0e3f240053bf789b84bd9668838)]:
  - @pyreon/reactivity@0.19.0
  - @pyreon/core@0.19.0

## 0.18.0

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.18.0
  - @pyreon/reactivity@0.18.0

## 0.17.0

### Patch Changes

- Updated dependencies [[`35af0e2`](https://github.com/pyreon/pyreon/commit/35af0e22b670151052e0b1df5006977fca759128), [`8b1a982`](https://github.com/pyreon/pyreon/commit/8b1a982faa140e7e646293a47d6a4fbe70cac67c)]:
  - @pyreon/core@0.17.0
  - @pyreon/reactivity@0.17.0

## 0.16.0

### Minor Changes

- [#483](https://github.com/pyreon/pyreon/pull/483) [`7f26cd7`](https://github.com/pyreon/pyreon/commit/7f26cd78d74db8237aa6261a11965325d944f1ca) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Granular `useFormState` selectors + O(1) `form.isValid` / `form.isDirty`. Pre-fix, every `useFormState(form, selector)` call rebuilt the full summary on each invalidation — `s => s.isValid` triggered a 10k-field scan because `buildSummary()` always materialized `touchedFields` / `dirtyFields` / `errors` maps. And `form.isValid` / `form.isDirty` were themselves O(N) computeds iterating every field on each recompute. Post-fix:

  - `useFormState` returns a getter-backed summary; selectors only track the signals they actually read. A button gated on `s => s.isValid && !s.isSubmitting` no longer scans the field maps. Verified against the 10k-field stress benchmark: `form.formStateScan.fieldsRead` drops from 10000 to 0 on selector reads.
  - `form.isValid` and `form.isDirty` are now O(1) reads of incrementally-tracked `_invalidCount` / `_dirtyCount` signals, updated via per-field `signal.subscribe` listeners (lighter than effects). Cost: 2 extra signal-creates per form, 2 subscribers per field at mount; benefit: form-level reactive reads scale flat regardless of field count.
  - The 3 atomic computeds (touched/dirty/errors maps) are shared across all `useFormState()` calls on the same form via a per-form WeakMap cache — selector A reading `errors` reuses the same computed as selector B reading `errors`. A field's error flip invalidates only the errors map, not touched/dirty.

  No public API change. `useFormState(form)` (no selector) still returns the full summary snapshot. Bisect-verified: reverting either fix fails the new regression tests in `form-additional.test.tsx`.

### Patch Changes

- Updated dependencies [[`a4a4255`](https://github.com/pyreon/pyreon/commit/a4a42550835cb2706b99beed8ea582037d338ea8)]:
  - @pyreon/core@0.16.0
  - @pyreon/reactivity@0.16.0

## 0.14.0

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.14.0
  - @pyreon/reactivity@0.14.0

## 0.13.0

### Patch Changes

- [#261](https://github.com/pyreon/pyreon/pull/261) [`72b2023`](https://github.com/pyreon/pyreon/commit/72b2023609bf539e804f64dbefcf2586edf7162f) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Triaged safe changes from architecture review PR [#260](https://github.com/pyreon/pyreon/issues/260):

  - **hotkeys**: detach global `keydown` listener when last hotkey unregisters (prevents listener accumulation across component remounts)
  - **code**: new `useEditorSignal()` hook — wraps `bindEditorToSignal` with `onUnmount` auto-cleanup (eliminates manual `dispose()` calls)
  - **form**: `ValidateFn` accepts optional `AbortSignal`; `useForm` creates per-cycle `AbortController` cancelled on unmount (prevents orphaned async validators)
  - **validation**: `zodSchema()` / `valibotSchema()` / `arktypeSchema()` return `TypedSchemaAdapter<TValues>` with `.validator` and phantom `_infer` type for compile-time field name validation. `useForm({ schema })` accepts both the new adapter and plain `SchemaValidateFn` (backward compatible).

  Dropped from the original PR: onCleanup LIFO ordering change (breaking behavioral change), circular effect detection (redundant with batch), SSR streaming backpressure (architecturally wrong implementation).

- Updated dependencies [[`a05c4ba`](https://github.com/pyreon/pyreon/commit/a05c4bab713f5168acd56eb233520102735bd80a)]:
  - @pyreon/core@0.13.0
  - @pyreon/reactivity@0.13.0

## 0.12.15

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.12.15
  - @pyreon/reactivity@0.12.15

## 0.12.14

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.12.14
  - @pyreon/reactivity@0.12.14

## 0.12.13

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.12.13
  - @pyreon/reactivity@0.12.13

## 0.12.12

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.12.12
  - @pyreon/reactivity@0.12.12

## 0.12.11

### Patch Changes

- Updated dependencies []:
  - @pyreon/core@0.12.11
  - @pyreon/reactivity@0.12.11

## 0.9.0

### Minor Changes

- ### Improvements
  - Upgrade to pyreon 0.7.5 (jsx preset, all JSX types accept undefined)
  - Use @pyreon/typescript preset (no local jsx override needed)
  - Complete documentation: 18 package READMEs, 18 docs/ files, llms.txt
  - Update AI building rules with document generation patterns

## 0.8.0

### Minor Changes

- [`075dd4f`](https://github.com/pyreon/fundamentals/commit/075dd4fe4a325fe5a5637a68e209dffe665bb84e) Thanks [@vitbokisch](https://github.com/vitbokisch)! - ### Improvements
  - Upgrade to TypeScript 6.0 and pyreon 0.7.3
  - Switch to @pyreon/typescript for tsconfig presets
  - Full exactOptionalPropertyTypes compliance
  - Security: add sanitization across all document renderers (XSS, XML injection, protocol validation)
  - Fix WebSocket.send() type for TS 6.0
  - Clean up conditional spreading now that core 0.7.3 accepts undefined on JSX attrs

## 0.7.0

### Minor Changes

- [`deb9834`](https://github.com/pyreon/fundamentals/commit/deb983456472cc685d80e97b21196588af53b502) Thanks [@vitbokisch](https://github.com/vitbokisch)! - ### New package

  - `@pyreon/document` — universal document rendering with 18 node primitives and 14 output formats (HTML, PDF, DOCX, XLSX, PPTX, email, Markdown, text, CSV, SVG, Slack, Teams, Discord, Telegram, Notion, Confluence/Jira, WhatsApp, Google Chat)

  ### Fixes

  - Fix DTS export paths — bump @vitus-labs/tools-rolldown to 1.15.4 (emitDtsOnly fix)
  - All packages now produce correct type declarations

## 0.6.0

### Minor Changes

- [`5610cdf`](https://github.com/pyreon/fundamentals/commit/5610cdffb69022aacd44419d7c71b97bdcf8403f) Thanks [@vitbokisch](https://github.com/vitbokisch)! - ### New packages

  - `@pyreon/flow` — reactive flow diagrams with signal-native nodes, edges, pan/zoom, auto-layout via elkjs
  - `@pyreon/code` — reactive code editor with CodeMirror 6, minimap, diff editor, lazy-loaded languages

  ### Improvements

  - Upgrade to pyreon 0.6.0
  - Use `provide()` for context providers (query, form, i18n, permissions)
  - Fix error message prefixes across packages

## 0.13.0

### Minor Changes

- Add @pyreon/permissions (reactive type-safe permissions) and @pyreon/machine (reactive state machines). Update AI building rules.

## 0.13.0

### Minor Changes

- Add @pyreon/storage (reactive localStorage, sessionStorage, cookies, IndexedDB) and @pyreon/hotkeys (keyboard shortcut management). Add useSubscription to @pyreon/query for WebSocket integration. Upgrade to pyreon core 0.5.4. Convert all tests and source to JSX.

## 0.1.0

### Minor Changes

- [#9](https://github.com/pyreon/fundamentals/pull/9) [`9fe5b51`](https://github.com/pyreon/fundamentals/commit/9fe5b51868c50c3bcab1961f94df27846921b739) Thanks [@vitbokisch](https://github.com/vitbokisch)! - Initial public release of Pyreon fundamentals ecosystem.
  - **@pyreon/store** — Global state management with `StoreApi<T>`
  - **@pyreon/state-tree** — Structured reactive models with snapshots, patches, middleware
  - **@pyreon/form** — Signal-based form management with validation, field arrays, context
  - **@pyreon/validation** — Schema adapters for Zod, Valibot, ArkType
  - **@pyreon/query** — TanStack Query adapter with fine-grained signals
  - **@pyreon/table** — TanStack Table adapter with reactive state
  - **@pyreon/virtual** — TanStack Virtual adapter for efficient list rendering
  - **@pyreon/i18n** — Reactive i18n with async namespace loading, plurals, interpolation
  - **@pyreon/storybook** — Storybook renderer for Pyreon components
  - **@pyreon/feature** — Schema-driven CRUD primitives with `defineFeature()`
