jsonisch 0.1.0 → 0.1.2

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.2](https://github.com/andrew310/jsonisch/compare/v0.1.1...v0.1.2) (2026-09-08)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * derived keys absent from input projection; dist augmentations target public specifier ([#30](https://github.com/andrew310/jsonisch/issues/30)) ([7188b24](https://github.com/andrew310/jsonisch/commit/7188b248e2a0b717224d0c029cc88b2c71beba51)), closes [#28](https://github.com/andrew310/jsonisch/issues/28)
9
+
10
+ ## [0.1.1](https://github.com/andrew310/jsonisch/compare/v0.1.0...v0.1.1) (2026-09-08)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **ci:** match plain vX.Y.Z tags — the changelog scanned all history ([#25](https://github.com/andrew310/jsonisch/issues/25)) ([207b2ef](https://github.com/andrew310/jsonisch/commit/207b2efd238c23cf9223ea72ae46de995d565d9e))
16
+
17
+
18
+ ### Documentation
19
+
20
+ * best-of-three names three real lineages ([#18](https://github.com/andrew310/jsonisch/issues/18)) ([fc6894e](https://github.com/andrew310/jsonisch/commit/fc6894e9149ee0e8fd02869a3c8bc67dfddc7d07))
21
+ * lineage section states what we actually studied ([#19](https://github.com/andrew310/jsonisch/issues/19)) ([3e70c2c](https://github.com/andrew310/jsonisch/commit/3e70c2c6c249fc599bc7ab61f09e79f5537e5a82))
22
+ * logo in the README ([#17](https://github.com/andrew310/jsonisch/issues/17)) ([0191401](https://github.com/andrew310/jsonisch/commit/0191401ca22c7410f9ec08061955540f63d0a406))
23
+ * the agent angle — a schema an LLM just wrote is a form ([#22](https://github.com/andrew310/jsonisch/issues/22)) ([dd7c1d4](https://github.com/andrew310/jsonisch/commit/dd7c1d450500f8493f110493038519ea4a6a7dbb))
24
+
3
25
  ## 0.1.0
4
26
 
5
27
  First public release. Extracted from the RWA platform with history intact.
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/andrew310/jsonisch/main/assets/logo.jpg" width="220" alt="jsonisch — a database record between braces" />
3
+ </p>
4
+
1
5
  # jsonisch
2
6
 
3
7
  > Schemas as values. A form library for apps where JSON-Schemas are **runtime
@@ -28,9 +32,10 @@ Most form libraries assume the shape of your form is known at build time —
28
32
  a Zod schema in a module, types inferred from it, a hand-written component
29
33
  per field. That assumption breaks the moment your app lets users customize
30
34
  their forms: now the schema is a **value**, fetched from a database at
31
- request time, different per tenant, edited without a deploy. There is no
32
- compile-time type to infer against, and nobody is hand-writing a component
33
- per field for a form that didn't exist yesterday.
35
+ request time, different per tenant, edited without a deploy or written
36
+ by an LLM a second ago. There is no compile-time type to infer against,
37
+ and nobody is hand-writing a component per field for a form that didn't
38
+ exist yesterday.
34
39
 
35
40
  `jsonisch` starts from that world. It takes a JSON-Schema value, walks it
36
41
  once, and gives you a fully reactive, validated form with the fields already
@@ -51,6 +56,13 @@ wired:
51
56
 
52
57
  You bring the components; it brings everything else.
53
58
 
59
+ That last part matters more in the age of agents. The cheapest thing a
60
+ model can produce is a JSON value — and a JSON-Schema value is a complete
61
+ form definition. An agent that needs structured input from a human can
62
+ emit a schema and have a validated, fully wired form on screen in the
63
+ same request: no codegen, no deploy, and the schema it wrote is the same
64
+ contract that validates what the human sends back.
65
+
54
66
  ---
55
67
 
56
68
  ## Quickstart
@@ -156,22 +168,29 @@ formulas use to address the root record) and wires the dependency graph.
156
168
 
157
169
  ---
158
170
 
159
- ## Where jsonisch sits — a deliberate "best of three"
171
+ ## Where jsonisch sits
160
172
 
161
- `jsonisch` is a synthesis of three lineages, picking one idea from each:
173
+ Two libraries are real lineage we read their source and took ideas from
174
+ each deliberately:
162
175
 
163
- - **From TanStack Form** — the **framework-agnostic core + thin adapters**
176
+ - **TanStack Form** — the **framework-agnostic core + thin adapters**
164
177
  layout, and the **`createFormHook({ widgets })` composition/registry**
165
178
  pattern: register your design-system widgets once, get a typed
166
179
  `useAppForm` with them baked in.
167
- - **From Formisch** — **signals** as the reactivity engine (its own, no
168
- external signal lib), so a keystroke re-renders only the fields that
169
- depend on it, and derived fields recompute automatically.
170
- - **New here** — the schema kind is **JSON-Schema**, not Zod/Valibot/Yup.
171
- Validation runs through an injected AJV-shaped validator. Derived values
172
- are computed signals over the formulas already declared in the schema, so
173
- they're excluded from dirty-tracking and the submit payload *by
174
- construction*.
180
+ - **Formisch** — the **signal** reactivity model jsonisch's store is built
181
+ on (its own tiny signal implementation, no external lib): a keystroke
182
+ re-renders only the fields that depend on it.
183
+
184
+ One more deserves naming: **react-jsonschema-form** has been rendering
185
+ forms from JSON-Schema since long before us. jsonisch is not derived from
186
+ it — candidly, we didn't study it the resemblance comes from sharing a
187
+ premise, not code. What's visibly different from here: jsonisch is
188
+ signal-reactive, ships no components of its own (your design system,
189
+ dispatched by control kind), injects validation instead of bundling it,
190
+ and evaluates schema-declared formulas as computed signals — **derived
191
+ values**, excluded from dirty-tracking and the submit payload *by
192
+ construction*. A careful comparison would make a good doc; we haven't
193
+ written it.
175
194
 
176
195
  The re-render question — "when I type one character into a 60-field form,
177
196
  what re-renders?" — has a two-decade history that signals largely closed,
@@ -679,6 +679,14 @@ interface JsonischPlugin<TState = unknown> {
679
679
  * the subtree dirty walks that decide payload emission.
680
680
  */
681
681
  fieldIsDirty?(ctx: PluginCtx<TState>, store: InternalValueStore): boolean;
682
+ /**
683
+ * 8b — whether THIS value field is a derived OUTPUT: the input
684
+ * projections (`getInput`, the validated submit output, the dirty
685
+ * whole-array emission) assemble around it, so its key is ABSENT from
686
+ * payloads instead of riding as own-key `undefined`. Only object
687
+ * ASSEMBLY consults this — a path-targeted leaf read stays raw.
688
+ */
689
+ fieldIsOutput?(ctx: PluginCtx<TState>, store: InternalValueStore): boolean;
682
690
  /**
683
691
  * 9 — wraps a value field's own payload entry (the LOS-573 envelope):
684
692
  * called for a dirty leaf with its outgoing value (`undefined` when only
@@ -718,7 +726,7 @@ type PluginsInput = ReadonlyArray<JsonischPlugin<unknown> | false | null | undef
718
726
  * The hooks the driver precomputes implementer lists for (the per-field
719
727
  * ones run in loops; scanning all plugins per field would be quadratic).
720
728
  */
721
- declare const HOOK_NAMES: readonly ["buildScope", "reseedScope", "resetField", "syncInput", "syncInitial", "rebase", "transferField", "swapField", "fieldIsDirty", "encodeValue", "isDirty", "fieldSnapshot"];
729
+ declare const HOOK_NAMES: readonly ["buildScope", "reseedScope", "resetField", "syncInput", "syncInitial", "rebase", "transferField", "swapField", "fieldIsDirty", "fieldIsOutput", "encodeValue", "isDirty", "fieldSnapshot"];
722
730
  type HookName = (typeof HOOK_NAMES)[number];
723
731
  /**
724
732
  * The resolved plugin runtime on a form store: the flat plugin list,
@@ -379,6 +379,7 @@ const KNOWN_MEMBERS = new Set([
379
379
  "transferField",
380
380
  "swapField",
381
381
  "fieldIsDirty",
382
+ "fieldIsOutput",
382
383
  "encodeValue",
383
384
  "isDirty",
384
385
  "fieldSnapshot"
@@ -397,6 +398,7 @@ const HOOK_NAMES = [
397
398
  "transferField",
398
399
  "swapField",
399
400
  "fieldIsDirty",
401
+ "fieldIsOutput",
400
402
  "encodeValue",
401
403
  "isDirty",
402
404
  "fieldSnapshot"
@@ -549,6 +551,17 @@ function fieldPluginDirty(form, store) {
549
551
  return dirty;
550
552
  }
551
553
  /**
554
+ * Whether THIS value leaf is a derived OUTPUT (its key is absent from the
555
+ * input projections by construction). Reads every implementer for the same
556
+ * no-short-circuit reason as `fieldPluginDirty` — callers sit inside
557
+ * computeds and tracked snapshot reads.
558
+ */
559
+ function fieldIsOutput(form, store) {
560
+ let output = false;
561
+ for (const plugin of form.pluginDriver?.hooks.fieldIsOutput ?? []) if (attributed(plugin, "fieldIsOutput", () => plugin.fieldIsOutput(ctxOf(form, plugin), store))) output = true;
562
+ return output;
563
+ }
564
+ /**
552
565
  * Whether any value leaf in the subtree has dirty plugin state — the
553
566
  * plugin half of the dirty walks that decide payload emission (a mode flip
554
567
  * with an unchanged value must still produce a payload). Reads array
@@ -992,15 +1005,24 @@ function focusFieldElement(internalFieldStore) {
992
1005
  * `undefined` for nullish container inputs, or the leaf value for value
993
1006
  * fields.
994
1007
  *
1008
+ * With a form store, object assembly additionally skips derived OUTPUT
1009
+ * leaves (`fieldIsOutput`) — the key is ABSENT from the projection, never
1010
+ * own-key `undefined` (issue #29: `JSON.stringify` hides the leak, but
1011
+ * `Object.keys`/`in`/spread consumers see it). Leaf reads stay raw either
1012
+ * way: asking for the output field itself returns its stored input.
1013
+ *
995
1014
  * @param internalFieldStore The field store to get input from.
1015
+ * @param internalFormStore The form store — pass it to project OUTPUT
1016
+ * leaves out of object assembly; omit for raw tree reads (plugin scope
1017
+ * resolution, per-field snapshots).
996
1018
  *
997
1019
  * @returns The field input.
998
1020
  */
999
- function getFieldInput(internalFieldStore) {
1021
+ function getFieldInput(internalFieldStore, internalFormStore) {
1000
1022
  if (internalFieldStore.kind === "array") {
1001
1023
  if (internalFieldStore.input.value) {
1002
1024
  const value = [];
1003
- for (let index = 0; index < internalFieldStore.items.value.length; index++) value[index] = getFieldInput(internalFieldStore.children[index]);
1025
+ for (let index = 0; index < internalFieldStore.items.value.length; index++) value[index] = getFieldInput(internalFieldStore.children[index], internalFormStore);
1004
1026
  return value;
1005
1027
  }
1006
1028
  return internalFieldStore.input.value;
@@ -1008,7 +1030,11 @@ function getFieldInput(internalFieldStore) {
1008
1030
  if (internalFieldStore.kind === "object") {
1009
1031
  if (internalFieldStore.input.value) {
1010
1032
  const value = {};
1011
- for (const key in internalFieldStore.children) value[key] = getFieldInput(internalFieldStore.children[key]);
1033
+ for (const key in internalFieldStore.children) {
1034
+ const child = internalFieldStore.children[key];
1035
+ if (internalFormStore && child.kind === "value" && fieldIsOutput(internalFormStore, child)) continue;
1036
+ value[key] = getFieldInput(child, internalFormStore);
1037
+ }
1012
1038
  return value;
1013
1039
  }
1014
1040
  return internalFieldStore.input.value;
@@ -1070,7 +1096,7 @@ function validateFormInput(internalFormStore, config) {
1070
1096
  internalFormStore.validators++;
1071
1097
  internalFormStore.isValidating.value = true;
1072
1098
  try {
1073
- const output = untrack(() => getFieldInput(internalFormStore));
1099
+ const output = untrack(() => getFieldInput(internalFormStore, internalFormStore));
1074
1100
  const issues = internalFormStore.validator?.(output);
1075
1101
  let fieldErrors;
1076
1102
  if (issues && issues.length > 0) {
@@ -1,4 +1,4 @@
1
- import { t as FormRef } from "./form-ref-D_xHSaqL.js";
1
+ import { t as FormRef } from "./form-ref-FEumWYOE.js";
2
2
 
3
3
  //#region src/methods/handle-submit.d.ts
4
4
 
@@ -1,4 +1,4 @@
1
- import { A as unwrapLeafInput, B as getFieldBool, L as untrack, M as batch, V as walkFieldStore, _ as resolveValueInput, b as dispatchReseedScope, c as isPresenceEqual, g as readOwn, j as createId, l as isSemanticEqual, m as containerPresence, n as validateFormInput, o as getFieldStoreChain, p as initializeFieldStore, t as internalOf, w as dispatchSyncInput } from "./form-ref-BEri3JKv.js";
1
+ import { A as unwrapLeafInput, B as getFieldBool, L as untrack, M as batch, V as walkFieldStore, _ as resolveValueInput, b as dispatchReseedScope, c as isPresenceEqual, g as readOwn, j as createId, l as isSemanticEqual, m as containerPresence, n as validateFormInput, o as getFieldStoreChain, p as initializeFieldStore, t as internalOf, w as dispatchSyncInput } from "./form-ref-WEtlz2iK.js";
2
2
 
3
3
  //#region src/core/field/reset-item-state.ts
4
4
  /**
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { C as flattenSourceRow, D as derivation, F as AmountOrPercentEnvelope, G as CalcParseResult, H as EstimateMeta, K as DerivationMode, L as EntryMeta, M as ValidateFormInputConfig, N as ValidationResult, R as EntryMode, S as computeBag, V as EstimateEnvelope, W as CalcEngine, _ as bagger, b as ComputeBagOptions, c as CheckDefinition, d as ChecksConfig, f as Finding, h as SeverityConfig, i as formulaCheck, j as envelopes, l as CheckInstanceConfig, m as Severity, n as replaceCheckInstances, p as FormulaOptions, q as DerivedState, r as UNEVALUABLE_MESSAGE_ID, s as CheckContext, t as checks, u as CheckScope, w as visibility, x as collectionKeys, y as BaggerOptions, z as Envelope } from "./plugin-BCsAE8RK.js";
2
- import { A as VisibleWhen, C as FieldErrors, G as JsonSchema, K as Path, M as ControlKind, N as inferControl, _ as WireContract, a as InternalFormStore, g as PluginsInput, i as FormValidator, j as VisibleWhenOp, o as ValidationIssue, q as PathSegment, r as FormConfig, s as ValidationMode, t as FormRef, w as FieldKind, x as ContainerInput } from "./form-ref-D_xHSaqL.js";
3
- import { n as handleSubmit, t as SubmitHandler } from "./handle-submit-Dk7QW3_q.js";
1
+ import { C as flattenSourceRow, D as derivation, F as AmountOrPercentEnvelope, G as CalcParseResult, H as EstimateMeta, K as DerivationMode, L as EntryMeta, M as ValidateFormInputConfig, N as ValidationResult, R as EntryMode, S as computeBag, V as EstimateEnvelope, W as CalcEngine, _ as bagger, b as ComputeBagOptions, c as CheckDefinition, d as ChecksConfig, f as Finding, h as SeverityConfig, i as formulaCheck, j as envelopes, l as CheckInstanceConfig, m as Severity, n as replaceCheckInstances, p as FormulaOptions, q as DerivedState, r as UNEVALUABLE_MESSAGE_ID, s as CheckContext, t as checks, u as CheckScope, w as visibility, x as collectionKeys, y as BaggerOptions, z as Envelope } from "./plugin-CDzZKTHb.js";
2
+ import { A as VisibleWhen, C as FieldErrors, G as JsonSchema, K as Path, M as ControlKind, N as inferControl, _ as WireContract, a as InternalFormStore, g as PluginsInput, i as FormValidator, j as VisibleWhenOp, o as ValidationIssue, q as PathSegment, r as FormConfig, s as ValidationMode, t as FormRef, w as FieldKind, x as ContainerInput } from "./form-ref-FEumWYOE.js";
3
+ import { n as handleSubmit, t as SubmitHandler } from "./handle-submit-AUrq5T7Y.js";
4
4
 
5
5
  //#region src/core/form/create-form-store.d.ts
6
6
  /**
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { A as unwrapLeafInput, B as getFieldBool, C as dispatchSyncInitial, D as fieldPluginDirty, E as encodeFieldValue, L as untrack, M as batch, N as computed, O as hasPluginDirtyField, P as createSignal, S as dispatchSwapField, T as dispatchTransferField, V as walkFieldStore, _ as resolveValueInput, a as getFieldStore, c as isPresenceEqual, d as DEFAULT_ROOT_RECORD_ALIAS, f as createFormStore, g as readOwn, h as isSafeKey, i as focusFieldElement, j as createId, l as isSemanticEqual, m as containerPresence, n as validateFormInput, o as getFieldStoreChain, p as initializeFieldStore, r as getFieldInput, s as isEmptyish, t as internalOf, u as DEFAULT_EMPTY_INPUT, x as dispatchResetField, y as dispatchRebase, z as inferControl } from "./form-ref-BEri3JKv.js";
2
- import { a as setFieldInput, i as computeContainerDirty, o as resetItemState, r as validateIfRequired, t as handleSubmit } from "./handle-submit-CD3WY9gW.js";
3
- import { A as bindAdopted, B as wrapEstimate, C as withRelationRowIdentity, E as envelopesKey, F as syncEstimateInput, H as getDirtyFieldInput, I as writeEnvelope, L as envelopesWire, M as decodeEstimateEnvelope, N as resolveEstimateMode, P as syncAmountOrPercentInput, R as isEnvelope, S as targetToKind, T as derivationKey, V as encodeScopeValues, _ as resolveRowFallback, b as relationParentFieldName, c as collectionKeys, d as visibility, i as formulaCheck, j as decodeAmountOrPercentEnvelope, k as adoptEnvelope, l as computeBag, m as resolveScopeValueAt, n as replaceCheckInstances, o as bagger, p as resolveScopeValue, r as UNEVALUABLE_MESSAGE_ID, t as checks, u as flattenSourceRow, w as mergeCollectionRows, x as relationRowIdentityProps, y as readRelationConfig, z as wrapAmountOrPercent } from "./plugin-Qka-vaO1.js";
1
+ import { A as unwrapLeafInput, B as getFieldBool, C as dispatchSyncInitial, D as fieldPluginDirty, E as encodeFieldValue, L as untrack, M as batch, N as computed, O as hasPluginDirtyField, P as createSignal, S as dispatchSwapField, T as dispatchTransferField, V as walkFieldStore, _ as resolveValueInput, a as getFieldStore, c as isPresenceEqual, d as DEFAULT_ROOT_RECORD_ALIAS, f as createFormStore, g as readOwn, h as isSafeKey, i as focusFieldElement, j as createId, l as isSemanticEqual, m as containerPresence, n as validateFormInput, o as getFieldStoreChain, p as initializeFieldStore, r as getFieldInput, s as isEmptyish, t as internalOf, u as DEFAULT_EMPTY_INPUT, x as dispatchResetField, y as dispatchRebase, z as inferControl } from "./form-ref-WEtlz2iK.js";
2
+ import { a as setFieldInput, i as computeContainerDirty, o as resetItemState, r as validateIfRequired, t as handleSubmit } from "./handle-submit-De-yR66a.js";
3
+ import { A as bindAdopted, B as wrapEstimate, C as withRelationRowIdentity, E as envelopesKey, F as syncEstimateInput, H as getDirtyFieldInput, I as writeEnvelope, L as envelopesWire, M as decodeEstimateEnvelope, N as resolveEstimateMode, P as syncAmountOrPercentInput, R as isEnvelope, S as targetToKind, T as derivationKey, V as encodeScopeValues, _ as resolveRowFallback, b as relationParentFieldName, c as collectionKeys, d as visibility, i as formulaCheck, j as decodeAmountOrPercentEnvelope, k as adoptEnvelope, l as computeBag, m as resolveScopeValueAt, n as replaceCheckInstances, o as bagger, p as resolveScopeValue, r as UNEVALUABLE_MESSAGE_ID, t as checks, u as flattenSourceRow, w as mergeCollectionRows, x as relationRowIdentityProps, y as readRelationConfig, z as wrapAmountOrPercent } from "./plugin-Bdw1RuiF.js";
4
4
 
5
5
  //#region src/core/codec/decode-record.ts
6
6
  /**
@@ -743,7 +743,7 @@ function collectDirtyPaths(internalFieldStore, paths) {
743
743
  */
744
744
  function getInput(form, path) {
745
745
  const internal = internalOf(form);
746
- return getFieldInput(path ? getFieldStore(internal, path) : internal);
746
+ return getFieldInput(path ? getFieldStore(internal, path) : internal, internal);
747
747
  }
748
748
 
749
749
  //#endregion
@@ -1388,6 +1388,9 @@ function derivation(engine) {
1388
1388
  }
1389
1389
  wireFormulaGraph(form, ctx.state, engine, scope.children, (dep, formValue) => resolveRowFallback(form, scope, dep, formValue));
1390
1390
  },
1391
+ fieldIsOutput(ctx, store) {
1392
+ return store.control === "formula" && ctx.state.has(store);
1393
+ },
1391
1394
  fieldSnapshot(ctx, store) {
1392
1395
  const slot = ctx.state.get(store);
1393
1396
  if (!slot) return {};
@@ -1,5 +1,5 @@
1
- import { A as EnvelopeState, B as EnvelopeSlot, E as DerivationState, I as AmountOrPercentSlot, O as DerivationSlot, P as validateFormInput, T as visibilityKey, U as EstimateSlot, a as ChecksState, g as BaggerState, k as derivationKey, o as checksKey, v as baggerKey } from "../plugin-BCsAE8RK.js";
2
- import { B as createSignal, D as InternalFieldStore, E as InternalBaseStore, F as ReadonlySignal, H as getListener, I as Signal, K as Path, L as Tracker, O as InternalObjectStore, P as Listener, R as batch, S as FieldElement, T as InternalArrayStore, U as untrack, V as createTracker, W as withListener, _ as WireContract, a as InternalFormStore, b as PluginKey, c as PluginDriver, d as hasPluginDirtyField, f as pluginsDirty, h as PluginCtx, k as InternalValueStore, l as encodeFieldValue, m as JsonischPlugin, n as internalOf, p as unwrapLeafInput, u as fieldPluginDirty, v as WireEnvelope, x as ContainerInput, y as FieldSlotKey, z as computed } from "../form-ref-D_xHSaqL.js";
1
+ import { A as EnvelopeState, B as EnvelopeSlot, E as DerivationState, I as AmountOrPercentSlot, O as DerivationSlot, P as validateFormInput, T as visibilityKey, U as EstimateSlot, a as ChecksState, g as BaggerState, k as derivationKey, o as checksKey, v as baggerKey } from "../plugin-CDzZKTHb.js";
2
+ import { B as createSignal, D as InternalFieldStore, E as InternalBaseStore, F as ReadonlySignal, H as getListener, I as Signal, K as Path, L as Tracker, O as InternalObjectStore, P as Listener, R as batch, S as FieldElement, T as InternalArrayStore, U as untrack, V as createTracker, W as withListener, _ as WireContract, a as InternalFormStore, b as PluginKey, c as PluginDriver, d as hasPluginDirtyField, f as pluginsDirty, h as PluginCtx, k as InternalValueStore, l as encodeFieldValue, m as JsonischPlugin, n as internalOf, p as unwrapLeafInput, u as fieldPluginDirty, v as WireEnvelope, x as ContainerInput, y as FieldSlotKey, z as computed } from "../form-ref-FEumWYOE.js";
3
3
 
4
4
  //#region src/core/field/focus-field-element.d.ts
5
5
 
@@ -40,11 +40,20 @@ declare function getFieldBool(internalFieldStore: InternalFieldStore, type: "err
40
40
  * `undefined` for nullish container inputs, or the leaf value for value
41
41
  * fields.
42
42
  *
43
+ * With a form store, object assembly additionally skips derived OUTPUT
44
+ * leaves (`fieldIsOutput`) — the key is ABSENT from the projection, never
45
+ * own-key `undefined` (issue #29: `JSON.stringify` hides the leak, but
46
+ * `Object.keys`/`in`/spread consumers see it). Leaf reads stay raw either
47
+ * way: asking for the output field itself returns its stored input.
48
+ *
43
49
  * @param internalFieldStore The field store to get input from.
50
+ * @param internalFormStore The form store — pass it to project OUTPUT
51
+ * leaves out of object assembly; omit for raw tree reads (plugin scope
52
+ * resolution, per-field snapshots).
44
53
  *
45
54
  * @returns The field input.
46
55
  */
47
- declare function getFieldInput(internalFieldStore: InternalFieldStore): unknown;
56
+ declare function getFieldInput(internalFieldStore: InternalFieldStore, internalFormStore?: InternalFormStore): unknown;
48
57
  //#endregion
49
58
  //#region src/core/field/get-field-store.d.ts
50
59
  /**
@@ -1,4 +1,4 @@
1
- import { A as unwrapLeafInput, B as getFieldBool, D as fieldPluginDirty, E as encodeFieldValue, F as createTracker, I as getListener, L as untrack, M as batch, N as computed, O as hasPluginDirtyField, P as createSignal, R as withListener, V as walkFieldStore, a as getFieldStore, i as focusFieldElement, k as pluginsDirty, n as validateFormInput, r as getFieldInput, t as internalOf } from "../form-ref-BEri3JKv.js";
2
- import { D as FieldSlotKey, E as envelopesKey, H as getDirtyFieldInput, O as PluginKey, T as derivationKey, V as encodeScopeValues, _ as resolveRowFallback, a as checksKey, f as visibilityKey, g as findRowStore, h as canonicalRowOf, s as baggerKey, v as resolveRowScopeValue } from "../plugin-Qka-vaO1.js";
1
+ import { A as unwrapLeafInput, B as getFieldBool, D as fieldPluginDirty, E as encodeFieldValue, F as createTracker, I as getListener, L as untrack, M as batch, N as computed, O as hasPluginDirtyField, P as createSignal, R as withListener, V as walkFieldStore, a as getFieldStore, i as focusFieldElement, k as pluginsDirty, n as validateFormInput, r as getFieldInput, t as internalOf } from "../form-ref-WEtlz2iK.js";
2
+ import { D as FieldSlotKey, E as envelopesKey, H as getDirtyFieldInput, O as PluginKey, T as derivationKey, V as encodeScopeValues, _ as resolveRowFallback, a as checksKey, f as visibilityKey, g as findRowStore, h as canonicalRowOf, s as baggerKey, v as resolveRowScopeValue } from "../plugin-Bdw1RuiF.js";
3
3
 
4
4
  export { FieldSlotKey, PluginKey, baggerKey, batch, canonicalRowOf, checksKey, computed, createSignal, createTracker, derivationKey, encodeFieldValue, encodeScopeValues, envelopesKey, fieldPluginDirty, findRowStore, focusFieldElement, getDirtyFieldInput, getFieldBool, getFieldInput, getFieldStore, getListener, hasPluginDirtyField, internalOf, pluginsDirty, resolveRowFallback, resolveRowScopeValue, untrack, unwrapLeafInput, validateFormInput, visibilityKey, walkFieldStore, withListener };
@@ -1,4 +1,4 @@
1
- import { B as getFieldBool, D as fieldPluginDirty, E as encodeFieldValue, N as computed, O as hasPluginDirtyField, P as createSignal, _ as resolveValueInput, d as DEFAULT_ROOT_RECORD_ALIAS, g as readOwn, h as isSafeKey, l as isSemanticEqual, r as getFieldInput, s as isEmptyish, t as internalOf } from "./form-ref-BEri3JKv.js";
1
+ import { B as getFieldBool, D as fieldPluginDirty, E as encodeFieldValue, N as computed, O as hasPluginDirtyField, P as createSignal, _ as resolveValueInput, d as DEFAULT_ROOT_RECORD_ALIAS, g as readOwn, h as isSafeKey, l as isSemanticEqual, r as getFieldInput, s as isEmptyish, t as internalOf } from "./form-ref-WEtlz2iK.js";
2
2
 
3
3
  //#region src/core/field/get-dirty-field-input.ts
4
4
  /**
@@ -22,7 +22,7 @@ import { B as getFieldBool, D as fieldPluginDirty, E as encodeFieldValue, N as c
22
22
  */
23
23
  function getDirtyFieldInput(internalFormStore, internalFieldStore) {
24
24
  if (!getFieldBool(internalFieldStore, "isDirty") && !hasPluginDirtyField(internalFormStore, internalFieldStore)) return;
25
- if (internalFieldStore.kind === "array") return encodeScopeValues(internalFormStore, internalFieldStore, getFieldInput(internalFieldStore));
25
+ if (internalFieldStore.kind === "array") return encodeScopeValues(internalFormStore, internalFieldStore, getFieldInput(internalFieldStore, internalFormStore));
26
26
  if (internalFieldStore.kind === "object") {
27
27
  if (internalFieldStore.input.value) {
28
28
  const value = {};
@@ -1,4 +1,4 @@
1
- import { D as InternalFieldStore, F as ReadonlySignal, G as JsonSchema, I as Signal, K as Path, a as InternalFormStore, b as PluginKey, m as JsonischPlugin, t as FormRef, y as FieldSlotKey } from "./form-ref-D_xHSaqL.js";
1
+ import { D as InternalFieldStore, F as ReadonlySignal, G as JsonSchema, I as Signal, K as Path, a as InternalFormStore, b as PluginKey, m as JsonischPlugin, t as FormRef, y as FieldSlotKey } from "./form-ref-FEumWYOE.js";
2
2
 
3
3
  //#region src/core/types/derivation.d.ts
4
4
 
@@ -263,7 +263,7 @@ type EnvelopeState = Map<InternalFieldStore, EnvelopeSlot>;
263
263
  * side-channel to decode).
264
264
  */
265
265
  declare function envelopes(): JsonischPlugin<EnvelopeState>;
266
- declare module "../../react/types" {
266
+ declare module "jsonisch/react" {
267
267
  interface FieldStoreSlots {
268
268
  /**
269
269
  * The estimate/formula mode of an estimate field, `undefined`
@@ -357,7 +357,7 @@ type DerivationState = Map<InternalFieldStore, DerivationSlot>;
357
357
  * plugin is a startup error instead.
358
358
  */
359
359
  declare function derivation(engine: CalcEngine): JsonischPlugin<DerivationState>;
360
- declare module "../../react/types" {
360
+ declare module "jsonisch/react" {
361
361
  interface FieldStoreSlots {
362
362
  /**
363
363
  * The mode-aware derived output of a formula/estimate field (what the
@@ -1,5 +1,5 @@
1
- import { C as FieldErrors, G as JsonSchema, K as Path, M as ControlKind, S as FieldElement, a as InternalFormStore, i as FormValidator, r as FormConfig } from "../form-ref-D_xHSaqL.js";
2
- import { t as SubmitHandler } from "../handle-submit-Dk7QW3_q.js";
1
+ import { C as FieldErrors, G as JsonSchema, K as Path, M as ControlKind, S as FieldElement, a as InternalFormStore, i as FormValidator, r as FormConfig } from "../form-ref-FEumWYOE.js";
2
+ import { t as SubmitHandler } from "../handle-submit-AUrq5T7Y.js";
3
3
  import { ComponentType, FormHTMLAttributes, ReactElement, ReactNode } from "react";
4
4
 
5
5
  //#region src/react/types.d.ts
@@ -1,5 +1,5 @@
1
- import { B as getFieldBool, F as createTracker, a as getFieldStore, f as createFormStore, n as validateFormInput, r as getFieldInput, v as dispatchFieldSnapshot } from "../form-ref-BEri3JKv.js";
2
- import { a as setFieldInput, n as setFieldBool, r as validateIfRequired, t as handleSubmit } from "../handle-submit-CD3WY9gW.js";
1
+ import { B as getFieldBool, F as createTracker, a as getFieldStore, f as createFormStore, n as validateFormInput, r as getFieldInput, v as dispatchFieldSnapshot } from "../form-ref-WEtlz2iK.js";
2
+ import { a as setFieldInput, n as setFieldBool, r as validateIfRequired, t as handleSubmit } from "../handle-submit-De-yR66a.js";
3
3
  import { useEffect, useLayoutEffect, useMemo, useSyncExternalStore } from "react";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsonisch",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Schema in, reactive form out. Derive forms from JSON Schema.",
5
5
  "license": "MIT",
6
6
  "author": "Andrew Brown <andrewbrown600@gmail.com>",