jsonisch 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1521 @@
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";
4
+
5
+ //#region src/core/codec/decode-record.ts
6
+ /**
7
+ * How a server record becomes form state — one decode, then a fork:
8
+ *
9
+ * Postgres row { …columns, data: { myField: { kind, value?, mode, … } } }
10
+ * │
11
+ * │ ① decodeRecord (this file): schema-declared keys only,
12
+ * │ envelopes pass through WHOLE
13
+ * ▼
14
+ * initialInput { myField: { kind, value?, mode, … } }
15
+ * │
16
+ * │ ② createFormStore visits each declared field; an
17
+ * │ estimate/amount-or-percent field's envelope is read TWICE,
18
+ * │ once per half — both readers take the SAME raw object:
19
+ * ├──────────────────────────┬─────────────────────────────┐
20
+ * ▼ ▼ │
21
+ * value half → field input meta half → envelope slot │
22
+ * (`unwrapLeafInput`, (`envelopes()` buildScope, │
23
+ * core/plugin/driver.ts) plugins/envelopes/plugin.ts) │
24
+ *
25
+ * It is a FORK, not a chain: the meta reader consumes the original raw,
26
+ * never the value reader's output, so the value a user sees and the mode
27
+ * state next to it can never derive from different data. The same fork
28
+ * re-runs on reset, `applyBaseline`, and array-row reuse — every path
29
+ * funnels through the same two readers.
30
+ */
31
+ /**
32
+ * Decodes a nested server record into the form's `initialInput` shape,
33
+ * routing each declared root property by its `x-column` geometry:
34
+ * `x-column: true` fields read from a real record column (a top-level
35
+ * record key), everything else reads from the record's `data` JSONB bag.
36
+ *
37
+ * The schema is the allow-list — only declared property keys are read, so
38
+ * undeclared record keys (including prototype-pollution keys) never enter
39
+ * the result. Nested values pass through as-is; the store walk applies the
40
+ * allow-list recursively when the result becomes `initialInput`, and
41
+ * unwraps envelope leaves (`{ kind, value?, mode, … }`) through the
42
+ * registered wire contracts.
43
+ *
44
+ * For flat-JSONB surfaces without column routing (e.g. workflow form
45
+ * tasks), pass the bag as `{ data: bag }`.
46
+ *
47
+ * @param schema The form's JSON-Schema (object schema with properties).
48
+ * @param record The server record (`{ …columns, data? }`).
49
+ * @param options Decode options (envelope wire contracts).
50
+ *
51
+ * @returns The decoded initial input, or `undefined` for a nullish record.
52
+ */
53
+ function decodeRecord(schema, record, options = {}) {
54
+ if (record == null) return void 0;
55
+ const properties = schema.properties ?? {};
56
+ const data = record.data && typeof record.data === "object" ? record.data : void 0;
57
+ const input = {};
58
+ for (const key of Object.keys(properties)) {
59
+ if (!isSafeKey(key)) continue;
60
+ const isColumn = properties[key]["x-column"] === true;
61
+ let value;
62
+ if (isColumn && options.envelopes?.has(inferControl(properties[key]))) {
63
+ const bagValue = readOwn(data, key);
64
+ value = bagValue !== void 0 ? bagValue : readOwn(record, key);
65
+ } else value = isColumn ? readOwn(record, key) : readOwn(data, key);
66
+ if (value !== void 0) input[key] = value;
67
+ }
68
+ return input;
69
+ }
70
+
71
+ //#endregion
72
+ //#region src/core/field/align-rows.ts
73
+ /**
74
+ * Non-empty identity: `null`, `undefined`, and `""` do not join.
75
+ */
76
+ function isUsableId(id) {
77
+ return id != null && id !== "";
78
+ }
79
+ /**
80
+ * For each server index, the local row that supplies live state. Server
81
+ * order is the result order. A local index is used at most once — two
82
+ * server rows with the same id must not share one live store. Match only
83
+ * when both sides have a usable id.
84
+ *
85
+ * The caller must not invoke this when the item schema has no `id`; those
86
+ * arrays stay positional.
87
+ */
88
+ function alignRows(localIds, serverRows) {
89
+ const used = /* @__PURE__ */ new Set();
90
+ return serverRows.map((row) => {
91
+ const serverId = readOwn(row, "id");
92
+ if (!isUsableId(serverId)) return { fromLocalIndex: null };
93
+ for (let index = 0; index < localIds.length; index++) {
94
+ if (used.has(index) || localIds[index] !== serverId) continue;
95
+ used.add(index);
96
+ return { fromLocalIndex: index };
97
+ }
98
+ return { fromLocalIndex: null };
99
+ });
100
+ }
101
+
102
+ //#endregion
103
+ //#region src/core/field/copy-item-state.ts
104
+ /**
105
+ * Copies the deeply nested state (signal values) from one field store to
106
+ * another: `elements`, `errors`, `startInput`, `input`, `isTouched`,
107
+ * `isEdited`, `isDirty`, and for arrays `startItems` and `items`. This is
108
+ * the state-transfer engine under `insert`, `remove` and `move` — errors
109
+ * and dirty/touched flags travel with their row. `initialInput`,
110
+ * `initialItems` and `initialElements` stay put: the reset target belongs
111
+ * to the position, not the moving item.
112
+ *
113
+ * @param internalFormStore The form store providing the empty input config.
114
+ * @param fromInternalFieldStore The source field store to copy from.
115
+ * @param toInternalFieldStore The destination field store to copy to.
116
+ */
117
+ function copyItemState(internalFormStore, fromInternalFieldStore, toInternalFieldStore) {
118
+ batch(() => {
119
+ untrack(() => {
120
+ toInternalFieldStore.elements = fromInternalFieldStore.elements;
121
+ toInternalFieldStore.validationErrors.value = fromInternalFieldStore.validationErrors.value;
122
+ toInternalFieldStore.startInput.value = fromInternalFieldStore.startInput.value;
123
+ toInternalFieldStore.input.value = fromInternalFieldStore.input.value;
124
+ toInternalFieldStore.isTouched.value = fromInternalFieldStore.isTouched.value;
125
+ toInternalFieldStore.isEdited.value = fromInternalFieldStore.isEdited.value;
126
+ toInternalFieldStore.isDirty.value = fromInternalFieldStore.isDirty.value;
127
+ if (fromInternalFieldStore.kind === "value" && toInternalFieldStore.kind === "value") dispatchTransferField(internalFormStore, fromInternalFieldStore, toInternalFieldStore);
128
+ if (fromInternalFieldStore.kind === "array" && toInternalFieldStore.kind === "array") {
129
+ const fromItems = fromInternalFieldStore.items.value;
130
+ toInternalFieldStore.startItems.value = fromInternalFieldStore.startItems.value;
131
+ toInternalFieldStore.items.value = fromItems;
132
+ for (let index = 0; index < fromItems.length; index++) {
133
+ if (!toInternalFieldStore.children[index]) {
134
+ toInternalFieldStore.children[index] = {};
135
+ initializeFieldStore(internalFormStore, toInternalFieldStore.children[index], toInternalFieldStore.itemSchema, void 0, [...toInternalFieldStore.path, index]);
136
+ }
137
+ copyItemState(internalFormStore, fromInternalFieldStore.children[index], toInternalFieldStore.children[index]);
138
+ }
139
+ } else if (fromInternalFieldStore.kind === "object" && toInternalFieldStore.kind === "object") for (const key in fromInternalFieldStore.children) copyItemState(internalFormStore, fromInternalFieldStore.children[key], toInternalFieldStore.children[key]);
140
+ });
141
+ });
142
+ }
143
+
144
+ //#endregion
145
+ //#region src/core/field/park-item-state.ts
146
+ /**
147
+ * Path must be the item path or envelope slots are not built (LOS-596).
148
+ */
149
+ function parkItemState(form, array, source, sourceIndex) {
150
+ const parked = {};
151
+ initializeFieldStore(form, parked, array.itemSchema, void 0, [...array.path, sourceIndex]);
152
+ copyItemState(form, source, parked);
153
+ return parked;
154
+ }
155
+
156
+ //#endregion
157
+ //#region src/core/field/rebase-field-baseline.ts
158
+ /**
159
+ * Rebases the live state of a field store and all its children onto a new
160
+ * baseline value — the live half of `applyBaseline` (`setInitialFieldInput`
161
+ * is the reset-target half and must run first): the dirty baseline
162
+ * (`startInput`/`startItems`) always moves to the new value; the current
163
+ * input moves with it only when the field was clean, so an in-flight edit
164
+ * survives and is re-diffed against the new baseline (an edit equal to the
165
+ * fresh server value becomes clean — semantic dirty rules apply).
166
+ *
167
+ * Callers must wrap in `batch` + `untrack` (like the reset internals).
168
+ *
169
+ * @param internalFormStore The form store providing the empty input config.
170
+ * @param internalFieldStore The field store to rebase.
171
+ * @param input The new baseline value.
172
+ */
173
+ function rebaseFieldBaseline(internalFormStore, internalFieldStore, input) {
174
+ if (internalFieldStore.kind === "array") rebaseArrayBaseline(internalFormStore, internalFieldStore, input);
175
+ else if (internalFieldStore.kind === "object") {
176
+ const presence = containerPresence(internalFieldStore.isNullish, input);
177
+ const presenceClean = isPresenceEqual(internalFieldStore.startInput.value, internalFieldStore.input.value);
178
+ internalFieldStore.startInput.value = presence;
179
+ if (presenceClean) internalFieldStore.input.value = presence;
180
+ for (const key in internalFieldStore.children) rebaseFieldBaseline(internalFormStore, internalFieldStore.children[key], readOwn(input, key));
181
+ if (typeof internalFieldStore.path[internalFieldStore.path.length - 1] === "number") dispatchRebase(internalFormStore, internalFieldStore, input);
182
+ internalFieldStore.isDirty.value = computeContainerDirty(internalFieldStore);
183
+ } else {
184
+ const newValue = resolveValueInput(internalFormStore.emptyInput, internalFieldStore.schema, internalFieldStore.isNullish, unwrapLeafInput(internalFormStore, internalFieldStore.control, input));
185
+ const wasClean = isSemanticEqual(internalFieldStore.input.value, internalFieldStore.startInput.value);
186
+ internalFieldStore.startInput.value = newValue;
187
+ if (wasClean) internalFieldStore.input.value = newValue;
188
+ internalFieldStore.isDirty.value = !isSemanticEqual(internalFieldStore.input.value, newValue);
189
+ }
190
+ }
191
+ /**
192
+ * The array half of the rebase. Membership (item identity) follows the same
193
+ * clean-vs-dirty rule as values: unchanged membership adopts the server
194
+ * rows — by `id` when the item schema has usable ids, otherwise
195
+ * positionally — KEEPING surviving item IDs so mounted rows preserve their
196
+ * identity (react keys). Unmatched clean locals drop; unmatched dirty
197
+ * locals append after the server prefix. Locally changed membership
198
+ * (insert, remove, reorder, presence flip) wins wholesale — the array
199
+ * stays dirty and only rows matchable by a server `id` still rebase their
200
+ * content.
201
+ */
202
+ function rebaseArrayBaseline(internalFormStore, internalArrayStore, input) {
203
+ const presence = containerPresence(internalArrayStore.isNullish, input);
204
+ const presenceClean = isPresenceEqual(internalArrayStore.startInput.value, internalArrayStore.input.value);
205
+ internalArrayStore.startInput.value = presence;
206
+ if (presenceClean) internalArrayStore.input.value = presence;
207
+ const serverRows = Array.isArray(input) ? input : [];
208
+ const items = internalArrayStore.items.value;
209
+ const membershipClean = presenceClean && internalArrayStore.startItems.value.join() === items.join();
210
+ const serverRowsById = indexServerRowsById(internalArrayStore.itemSchema, serverRows);
211
+ if (membershipClean && serverRowsById) rebaseCleanMembershipById(internalFormStore, internalArrayStore, items, serverRows);
212
+ else if (membershipClean) rebaseCleanMembershipPositional(internalFormStore, internalArrayStore, items, serverRows);
213
+ else if (serverRowsById) for (let index = 0; index < items.length; index++) {
214
+ const serverRow = serverRowsById.get(readItemId(internalArrayStore.children[index]));
215
+ if (serverRow === void 0) continue;
216
+ rebaseFieldBaseline(internalFormStore, internalArrayStore.children[index], serverRow);
217
+ }
218
+ internalArrayStore.isDirty.value = !isPresenceEqual(internalArrayStore.startInput.value, internalArrayStore.input.value) || internalArrayStore.startItems.value.join() !== internalArrayStore.items.value.join();
219
+ }
220
+ /**
221
+ * Clean membership, no usable ids: adopt server rows by index. Grown
222
+ * slots are fresh baseline rows; surviving item IDs stay on their index.
223
+ */
224
+ function rebaseCleanMembershipPositional(internalFormStore, internalArrayStore, items, serverRows) {
225
+ for (let index = items.length; index < serverRows.length; index++) if (internalArrayStore.children[index]) resetItemState(internalFormStore, internalArrayStore.children[index], serverRows[index]);
226
+ else {
227
+ internalArrayStore.children[index] = {};
228
+ initializeFieldStore(internalFormStore, internalArrayStore.children[index], internalArrayStore.itemSchema, serverRows[index], [...internalArrayStore.path, index]);
229
+ }
230
+ const shared = Math.min(items.length, serverRows.length);
231
+ for (let index = 0; index < shared; index++) rebaseFieldBaseline(internalFormStore, internalArrayStore.children[index], serverRows[index]);
232
+ const newItems = [...items.slice(0, serverRows.length), ...Array.from({ length: Math.max(0, serverRows.length - items.length) }, () => createId())];
233
+ internalArrayStore.items.value = newItems;
234
+ internalArrayStore.startItems.value = newItems;
235
+ }
236
+ /**
237
+ * Clean membership with usable ids: grow/shrink to the server list, move
238
+ * live row state onto the aligned indices, then rebase each child onto
239
+ * that server row. Unmatched dirty locals append after that prefix;
240
+ * unmatched clean locals drop. Stores are position-fixed (`path` stays on
241
+ * the store) — never `children[i] = oldChildren[j]`.
242
+ */
243
+ function rebaseCleanMembershipById(internalFormStore, internalArrayStore, items, serverRows) {
244
+ const alignment = alignRows(items.map((_, index) => readItemId(internalArrayStore.children[index])), serverRows);
245
+ const usedLocal = /* @__PURE__ */ new Set();
246
+ for (const { fromLocalIndex } of alignment) if (fromLocalIndex != null) usedLocal.add(fromLocalIndex);
247
+ const leftoverDirty = [];
248
+ for (let index = 0; index < items.length; index++) {
249
+ if (usedLocal.has(index)) continue;
250
+ const child = internalArrayStore.children[index];
251
+ if (child && (getFieldBool(child, "isDirty") || hasPluginDirtyField(internalFormStore, child))) leftoverDirty.push(index);
252
+ }
253
+ const parked = /* @__PURE__ */ new Map();
254
+ for (const fromLocalIndex of [...usedLocal, ...leftoverDirty]) {
255
+ if (parked.has(fromLocalIndex)) continue;
256
+ parked.set(fromLocalIndex, parkItemState(internalFormStore, internalArrayStore, internalArrayStore.children[fromLocalIndex], fromLocalIndex));
257
+ }
258
+ for (let index = 0; index < serverRows.length; index++) {
259
+ ensureItemStore(internalFormStore, internalArrayStore, index, serverRows[index]);
260
+ const fromLocalIndex = alignment[index].fromLocalIndex;
261
+ if (fromLocalIndex == null) resetItemState(internalFormStore, internalArrayStore.children[index], serverRows[index]);
262
+ else copyItemState(internalFormStore, parked.get(fromLocalIndex), internalArrayStore.children[index]);
263
+ rebaseFieldBaseline(internalFormStore, internalArrayStore.children[index], serverRows[index]);
264
+ }
265
+ const leftoverIds = [];
266
+ for (let n = 0; n < leftoverDirty.length; n++) {
267
+ const fromLocalIndex = leftoverDirty[n];
268
+ const dest = serverRows.length + n;
269
+ ensureItemStore(internalFormStore, internalArrayStore, dest, void 0);
270
+ copyItemState(internalFormStore, parked.get(fromLocalIndex), internalArrayStore.children[dest]);
271
+ leftoverIds.push(items[fromLocalIndex]);
272
+ }
273
+ const newItems = [...alignment.map(({ fromLocalIndex }) => fromLocalIndex != null ? items[fromLocalIndex] : createId()), ...leftoverIds];
274
+ internalArrayStore.items.value = newItems;
275
+ internalArrayStore.startItems.value = newItems;
276
+ }
277
+ /**
278
+ * Reads the live `id` of an object row, or `undefined` when the store has
279
+ * no value-leaf `id` child.
280
+ */
281
+ function readItemId(store) {
282
+ if (store?.kind !== "object") return void 0;
283
+ const idStore = store.children.id;
284
+ return idStore?.kind === "value" ? idStore.input.value : void 0;
285
+ }
286
+ /**
287
+ * Ensures a child store exists at `index` so a dest write has a home.
288
+ * Existing stores are left as-is (live state is transferred or reset next).
289
+ */
290
+ function ensureItemStore(internalFormStore, internalArrayStore, index, input) {
291
+ if (internalArrayStore.children[index]) return;
292
+ internalArrayStore.children[index] = {};
293
+ initializeFieldStore(internalFormStore, internalArrayStore.children[index], internalArrayStore.itemSchema, input, [...internalArrayStore.path, index]);
294
+ }
295
+ /**
296
+ * Indexes server rows by their `id` value when the item schema declares an
297
+ * `id` property. Returns `undefined` when rows have no usable identity
298
+ * (no declared `id`, or no row carries one) — clean membership stays
299
+ * positional; dirty membership skips the id join.
300
+ */
301
+ function indexServerRowsById(itemSchema, serverRows) {
302
+ if (!itemSchema.properties || itemSchema.properties.id === void 0) return;
303
+ const index = /* @__PURE__ */ new Map();
304
+ for (const row of serverRows) {
305
+ const id = readOwn(row, "id");
306
+ if (id != null && id !== "" && !index.has(id)) index.set(id, row);
307
+ }
308
+ return index.size > 0 ? index : void 0;
309
+ }
310
+
311
+ //#endregion
312
+ //#region src/core/field/set-initial-field-input.ts
313
+ /**
314
+ * Sets the initial input (the reset target) for a field store and all its
315
+ * children recursively, initializing missing array children as needed.
316
+ * Updates only `initialInput` and `initialItems` — `reset` moves them into
317
+ * the live state. This is half of `applyBaseline`.
318
+ *
319
+ * @param internalFormStore The form store providing the empty input config.
320
+ * @param internalFieldStore The field store to update.
321
+ * @param initialInput The new initial input value.
322
+ */
323
+ function setInitialFieldInput(internalFormStore, internalFieldStore, initialInput, options) {
324
+ batch(() => {
325
+ if (internalFieldStore.kind === "array") {
326
+ internalFieldStore.initialInput.value = containerPresence(internalFieldStore.isNullish, initialInput);
327
+ const initialArrayInput = Array.isArray(initialInput) ? initialInput : [];
328
+ for (let index = internalFieldStore.children.length; index < initialArrayInput.length; index++) {
329
+ internalFieldStore.children[index] = {};
330
+ initializeFieldStore(internalFormStore, internalFieldStore.children[index], internalFieldStore.itemSchema, initialArrayInput[index], [...internalFieldStore.path, index]);
331
+ }
332
+ internalFieldStore.initialItems.value = Array.from({ length: initialArrayInput.length }, () => createId());
333
+ for (let index = 0; index < internalFieldStore.children.length; index++) setInitialFieldInput(internalFormStore, internalFieldStore.children[index], initialArrayInput[index], options);
334
+ } else if (internalFieldStore.kind === "object") {
335
+ internalFieldStore.initialInput.value = containerPresence(internalFieldStore.isNullish, initialInput);
336
+ for (const key in internalFieldStore.children) setInitialFieldInput(internalFormStore, internalFieldStore.children[key], readOwn(initialInput, key), options);
337
+ } else {
338
+ internalFieldStore.initialInput.value = resolveValueInput(internalFormStore.emptyInput, internalFieldStore.schema, internalFieldStore.isNullish, unwrapLeafInput(internalFormStore, internalFieldStore.control, initialInput));
339
+ if (options?.syncInitial) dispatchSyncInitial(internalFormStore, internalFieldStore, initialInput);
340
+ }
341
+ });
342
+ }
343
+
344
+ //#endregion
345
+ //#region src/methods/apply-baseline.ts
346
+ /**
347
+ * Rebases a live form on a fresh server-loaded record — after a save or a
348
+ * revalidate, the store adopts the record as its new baseline instead of
349
+ * being torn down and rebuilt: the record decodes through the `x-column`
350
+ * codec (envelope fields keep their kind-discriminated shape for
351
+ * the plugin rebase), clean fields
352
+ * take the new server value, dirty fields keep the user's in-flight edit
353
+ * re-diffed against the new baseline (an edit equal to the fresh server
354
+ * value becomes clean), and a later `reset()` returns to the NEW baseline.
355
+ * Array membership follows the same rule: unchanged membership adopts the
356
+ * server rows — by `id` when the item schema has usable ids, otherwise
357
+ * positionally — and surviving rows keep their identity. Unmatched clean
358
+ * locals drop; unmatched dirty locals append after the server prefix
359
+ * (field-level dirty still enables Save). Locally changed membership wins,
360
+ * with rows still rebasing content by server `id` where ids exist.
361
+ *
362
+ * It is NOT conflict resolution — two people editing the same field stays
363
+ * last-write-wins. A nullish record is a no-op (nothing to rebase on).
364
+ *
365
+ * For flat-JSONB surfaces without column routing, pass the bag as
366
+ * `{ data: bag }` (the `decodeRecord` convention).
367
+ *
368
+ * @param form The form store to rebase.
369
+ * @param record The fresh server record (`{ …columns, data? }`).
370
+ * @param config Rebase options (e.g. fresh off-form values).
371
+ */
372
+ function applyBaseline(form, record, config) {
373
+ const internalFormStore = internalOf(form);
374
+ const decoded = decodeRecord(internalFormStore.schema, record, { envelopes: internalFormStore.pluginDriver.envelopes });
375
+ if (decoded === void 0) return;
376
+ batch(() => {
377
+ untrack(() => {
378
+ setInitialFieldInput(internalFormStore, internalFormStore, decoded);
379
+ rebaseFieldBaseline(internalFormStore, internalFormStore, decoded);
380
+ dispatchRebase(internalFormStore, internalFormStore, decoded);
381
+ if (config?.offFormValues) internalFormStore.offFormValues.value = config.offFormValues;
382
+ if (internalFormStore.validate === "initial") validateFormInput(internalFormStore);
383
+ });
384
+ });
385
+ }
386
+
387
+ //#endregion
388
+ //#region src/core/field/swap-item-state.ts
389
+ /**
390
+ * Swaps the deeply nested state (signal values) between two field stores:
391
+ * `elements`, `errors`, `startInput`, `input`, `isTouched`, `isEdited`,
392
+ * `isDirty`, and for arrays `startItems` and `items`. The state-transfer
393
+ * engine under `swap` — errors and dirty/touched flags travel with their
394
+ * row. `initialInput`, `initialItems` and `initialElements` stay put: the
395
+ * reset target belongs to the position, not the moving item.
396
+ *
397
+ * @param internalFormStore The form store providing the empty input config.
398
+ * @param firstInternalFieldStore The first field store to swap.
399
+ * @param secondInternalFieldStore The second field store to swap.
400
+ */
401
+ function swapItemState(internalFormStore, firstInternalFieldStore, secondInternalFieldStore) {
402
+ batch(() => {
403
+ untrack(() => {
404
+ const tempElements = firstInternalFieldStore.elements;
405
+ firstInternalFieldStore.elements = secondInternalFieldStore.elements;
406
+ secondInternalFieldStore.elements = tempElements;
407
+ const tempErrors = firstInternalFieldStore.validationErrors.value;
408
+ firstInternalFieldStore.validationErrors.value = secondInternalFieldStore.validationErrors.value;
409
+ secondInternalFieldStore.validationErrors.value = tempErrors;
410
+ const tempStartInput = firstInternalFieldStore.startInput.value;
411
+ firstInternalFieldStore.startInput.value = secondInternalFieldStore.startInput.value;
412
+ secondInternalFieldStore.startInput.value = tempStartInput;
413
+ const tempInput = firstInternalFieldStore.input.value;
414
+ firstInternalFieldStore.input.value = secondInternalFieldStore.input.value;
415
+ secondInternalFieldStore.input.value = tempInput;
416
+ const tempIsTouched = firstInternalFieldStore.isTouched.value;
417
+ firstInternalFieldStore.isTouched.value = secondInternalFieldStore.isTouched.value;
418
+ secondInternalFieldStore.isTouched.value = tempIsTouched;
419
+ const tempIsEdited = firstInternalFieldStore.isEdited.value;
420
+ firstInternalFieldStore.isEdited.value = secondInternalFieldStore.isEdited.value;
421
+ secondInternalFieldStore.isEdited.value = tempIsEdited;
422
+ const tempIsDirty = firstInternalFieldStore.isDirty.value;
423
+ firstInternalFieldStore.isDirty.value = secondInternalFieldStore.isDirty.value;
424
+ secondInternalFieldStore.isDirty.value = tempIsDirty;
425
+ if (firstInternalFieldStore.kind === "value" && secondInternalFieldStore.kind === "value") dispatchSwapField(internalFormStore, firstInternalFieldStore, secondInternalFieldStore);
426
+ if (firstInternalFieldStore.kind === "array" && secondInternalFieldStore.kind === "array") {
427
+ const firstItems = firstInternalFieldStore.items.value;
428
+ const secondItems = secondInternalFieldStore.items.value;
429
+ const tempStartItems = firstInternalFieldStore.startItems.value;
430
+ firstInternalFieldStore.startItems.value = secondInternalFieldStore.startItems.value;
431
+ secondInternalFieldStore.startItems.value = tempStartItems;
432
+ firstInternalFieldStore.items.value = secondItems;
433
+ secondInternalFieldStore.items.value = firstItems;
434
+ const maxLength = Math.max(firstItems.length, secondItems.length);
435
+ for (let index = 0; index < maxLength; index++) {
436
+ if (!firstInternalFieldStore.children[index]) {
437
+ firstInternalFieldStore.children[index] = {};
438
+ initializeFieldStore(internalFormStore, firstInternalFieldStore.children[index], firstInternalFieldStore.itemSchema, void 0, [...firstInternalFieldStore.path, index]);
439
+ }
440
+ if (!secondInternalFieldStore.children[index]) {
441
+ secondInternalFieldStore.children[index] = {};
442
+ initializeFieldStore(internalFormStore, secondInternalFieldStore.children[index], secondInternalFieldStore.itemSchema, void 0, [...secondInternalFieldStore.path, index]);
443
+ }
444
+ swapItemState(internalFormStore, firstInternalFieldStore.children[index], secondInternalFieldStore.children[index]);
445
+ }
446
+ } else if (firstInternalFieldStore.kind === "object" && secondInternalFieldStore.kind === "object") for (const key in firstInternalFieldStore.children) swapItemState(internalFormStore, firstInternalFieldStore.children[key], secondInternalFieldStore.children[key]);
447
+ });
448
+ });
449
+ }
450
+
451
+ //#endregion
452
+ //#region src/methods/array-ops.ts
453
+ /**
454
+ * Resolves the array store at a path, marking every container between the
455
+ * root and the array as present (an insert into a nullish ancestor is a
456
+ * real change), and throws when the path does not resolve to an array
457
+ * field.
458
+ */
459
+ function resolveArrayStore(internalFormStore, path) {
460
+ const chain = getFieldStoreChain(internalFormStore, path);
461
+ const target = chain[chain.length - 1];
462
+ if (target.kind !== "array") throw new Error(`Expected an array field at path ${JSON.stringify(path)}, got "${target.kind}"`);
463
+ for (let index = 1; index < chain.length - 1; index++) {
464
+ const ancestor = chain[index];
465
+ if (ancestor.kind === "value") continue;
466
+ ancestor.input.value = true;
467
+ ancestor.isDirty.value = computeContainerDirty(ancestor);
468
+ }
469
+ return target;
470
+ }
471
+ /**
472
+ * Inserts a new item into the array field at the given path. All items at
473
+ * or after the insertion point shift up by one index, and their full state
474
+ * (values, errors, touched/dirty flags, elements) shifts with them.
475
+ *
476
+ * @param form The form store containing the array field.
477
+ * @param path The path to the array field.
478
+ * @param config The insert configuration.
479
+ */
480
+ function insert(form, path, config) {
481
+ batch(() => {
482
+ untrack(() => {
483
+ const internalFormStore = internalOf(form);
484
+ const internalArrayStore = resolveArrayStore(internalFormStore, path);
485
+ const items = internalArrayStore.items.value;
486
+ const insertIndex = config?.at ?? items.length;
487
+ if (insertIndex < 0 || insertIndex > items.length) return;
488
+ const newItems = [...items];
489
+ newItems.splice(insertIndex, 0, createId());
490
+ internalArrayStore.items.value = newItems;
491
+ for (let index = items.length; index > insertIndex; index--) {
492
+ if (!internalArrayStore.children[index]) {
493
+ internalArrayStore.children[index] = {};
494
+ initializeFieldStore(internalFormStore, internalArrayStore.children[index], internalArrayStore.itemSchema, void 0, [...internalArrayStore.path, index]);
495
+ }
496
+ copyItemState(internalFormStore, internalArrayStore.children[index - 1], internalArrayStore.children[index]);
497
+ }
498
+ if (!internalArrayStore.children[insertIndex]) {
499
+ internalArrayStore.children[insertIndex] = {};
500
+ initializeFieldStore(internalFormStore, internalArrayStore.children[insertIndex], internalArrayStore.itemSchema, config?.initialInput, [...internalArrayStore.path, insertIndex]);
501
+ } else resetItemState(internalFormStore, internalArrayStore.children[insertIndex], config?.initialInput);
502
+ internalArrayStore.input.value = true;
503
+ internalArrayStore.isTouched.value = true;
504
+ internalArrayStore.isEdited.value = true;
505
+ internalArrayStore.isDirty.value = true;
506
+ validateIfRequired(internalFormStore, internalArrayStore, "input");
507
+ });
508
+ });
509
+ }
510
+ /**
511
+ * Removes the item at the given index from the array field at the given
512
+ * path. All items after it shift down by one index with their full state.
513
+ *
514
+ * @param form The form store containing the array field.
515
+ * @param path The path to the array field.
516
+ * @param at The index of the item to remove.
517
+ */
518
+ function remove(form, path, at) {
519
+ batch(() => {
520
+ untrack(() => {
521
+ const internalFormStore = internalOf(form);
522
+ const internalArrayStore = resolveArrayStore(internalFormStore, path);
523
+ const items = internalArrayStore.items.value;
524
+ if (at < 0 || at > items.length - 1) return;
525
+ const newItems = [...items];
526
+ newItems.splice(at, 1);
527
+ internalArrayStore.items.value = newItems;
528
+ for (let index = at; index < items.length - 1; index++) copyItemState(internalFormStore, internalArrayStore.children[index + 1], internalArrayStore.children[index]);
529
+ internalArrayStore.isTouched.value = true;
530
+ internalArrayStore.isEdited.value = true;
531
+ internalArrayStore.isDirty.value = internalArrayStore.startItems.value.join() !== newItems.join();
532
+ validateIfRequired(internalFormStore, internalArrayStore, "input");
533
+ });
534
+ });
535
+ }
536
+ /**
537
+ * Moves the item at one index to another within the array field at the
538
+ * given path. All items between the two indices shift accordingly with
539
+ * their full state.
540
+ *
541
+ * @param form The form store containing the array field.
542
+ * @param path The path to the array field.
543
+ * @param from The index of the item to move.
544
+ * @param to The index to move the item to.
545
+ */
546
+ function move(form, path, from, to) {
547
+ batch(() => {
548
+ untrack(() => {
549
+ const internalFormStore = internalOf(form);
550
+ const internalArrayStore = resolveArrayStore(internalFormStore, path);
551
+ const items = internalArrayStore.items.value;
552
+ if (from < 0 || from > items.length - 1 || to < 0 || to > items.length - 1 || from === to) return;
553
+ const newItems = [...items];
554
+ newItems.splice(to, 0, newItems.splice(from, 1)[0]);
555
+ internalArrayStore.items.value = newItems;
556
+ const tempStore = parkItemState(internalFormStore, internalArrayStore, internalArrayStore.children[from], from);
557
+ if (from < to) for (let index = from; index < to; index++) copyItemState(internalFormStore, internalArrayStore.children[index + 1], internalArrayStore.children[index]);
558
+ else for (let index = from; index > to; index--) copyItemState(internalFormStore, internalArrayStore.children[index - 1], internalArrayStore.children[index]);
559
+ copyItemState(internalFormStore, tempStore, internalArrayStore.children[to]);
560
+ internalArrayStore.isTouched.value = true;
561
+ internalArrayStore.isEdited.value = true;
562
+ internalArrayStore.isDirty.value = internalArrayStore.startItems.value.join() !== newItems.join();
563
+ validateIfRequired(internalFormStore, internalArrayStore, "input");
564
+ });
565
+ });
566
+ }
567
+ /**
568
+ * Swaps two items in the array field at the given path by exchanging their
569
+ * positions and their full state.
570
+ *
571
+ * @param form The form store containing the array field.
572
+ * @param path The path to the array field.
573
+ * @param at The index of the first item.
574
+ * @param and The index of the second item.
575
+ */
576
+ function swap(form, path, at, and) {
577
+ batch(() => {
578
+ untrack(() => {
579
+ const internalFormStore = internalOf(form);
580
+ const internalArrayStore = resolveArrayStore(internalFormStore, path);
581
+ const items = internalArrayStore.items.value;
582
+ if (at < 0 || at > items.length - 1 || and < 0 || and > items.length - 1 || at === and) return;
583
+ const newItems = [...items];
584
+ const tempItemId = newItems[at];
585
+ newItems[at] = newItems[and];
586
+ newItems[and] = tempItemId;
587
+ internalArrayStore.items.value = newItems;
588
+ swapItemState(internalFormStore, internalArrayStore.children[at], internalArrayStore.children[and]);
589
+ internalArrayStore.isTouched.value = true;
590
+ internalArrayStore.isEdited.value = true;
591
+ internalArrayStore.isDirty.value = internalArrayStore.startItems.value.join() !== newItems.join();
592
+ validateIfRequired(internalFormStore, internalArrayStore, "input");
593
+ });
594
+ });
595
+ }
596
+
597
+ //#endregion
598
+ //#region src/methods/errors.ts
599
+ /**
600
+ * Sets or clears the error messages of the field at the given path, or the
601
+ * form-level (root) errors when no path is given. Useful for custom errors
602
+ * that do not come from schema validation (e.g. a failed server action).
603
+ *
604
+ * @param form The form store to set errors on.
605
+ * @param errors The error messages, or `null` to clear.
606
+ * @param path The path to the field (omit for form-level errors).
607
+ */
608
+ function setErrors(form, errors, path) {
609
+ const internal = internalOf(form);
610
+ (path ? getFieldStore(internal, path) : internal).validationErrors.value = errors;
611
+ }
612
+ /**
613
+ * Retrieves the error messages of the field at the given path, or the
614
+ * form-level (root) errors when no path is given. Does NOT include
615
+ * descendants — use `getDeepErrors` for a subtree.
616
+ *
617
+ * @param form The form store to read errors from.
618
+ * @param path The path to the field (omit for form-level errors).
619
+ *
620
+ * @returns The error messages, or `null`.
621
+ */
622
+ function getErrors(form, path) {
623
+ const internal = internalOf(form);
624
+ return (path ? getFieldStore(internal, path) : internal).errors.value;
625
+ }
626
+ /**
627
+ * Retrieves every error message of the field at the given path and all its
628
+ * descendants (the entire form when no path is given), in depth-first
629
+ * order. Form-level errors are included. This is what a checks panel sits
630
+ * on — it surfaces errors of fields that are not currently rendered.
631
+ *
632
+ * @param form The form store to read errors from.
633
+ * @param path The path to scope to (omit for the whole form).
634
+ *
635
+ * @returns The error messages, or `null` if none exist.
636
+ */
637
+ function getDeepErrors(form, path) {
638
+ const internal = internalOf(form);
639
+ let deepErrors = null;
640
+ walkFieldStore(path ? getFieldStore(internal, path) : internal, (internalFieldStore) => {
641
+ const errors = internalFieldStore.errors.value;
642
+ if (errors) if (deepErrors) deepErrors.push(...errors);
643
+ else deepErrors = [...errors];
644
+ });
645
+ return deepErrors;
646
+ }
647
+ /**
648
+ * Retrieves every erroring field of the subtree at the given path (the
649
+ * entire form when no path is given) as `{ path, errors }` entries in
650
+ * depth-first order.
651
+ *
652
+ * @param form The form store to read errors from.
653
+ * @param path The path to scope to (omit for the whole form).
654
+ *
655
+ * @returns The deep error entries (empty when none exist).
656
+ */
657
+ function getDeepErrorEntries(form, path) {
658
+ const internal = internalOf(form);
659
+ const entries = [];
660
+ walkFieldStore(path ? getFieldStore(internal, path) : internal, (internalFieldStore) => {
661
+ const errors = internalFieldStore.errors.value;
662
+ if (errors) entries.push({
663
+ path: [...internalFieldStore.path],
664
+ errors
665
+ });
666
+ });
667
+ return entries;
668
+ }
669
+
670
+ //#endregion
671
+ //#region src/methods/focus.ts
672
+ /**
673
+ * Focuses the first focusable element of the field at the given path.
674
+ * Detached, disabled or hidden elements are skipped.
675
+ *
676
+ * @param form The form store containing the field.
677
+ * @param path The path to the field to focus.
678
+ */
679
+ function focus(form, path) {
680
+ focusFieldElement(getFieldStore(internalOf(form), path));
681
+ }
682
+
683
+ //#endregion
684
+ //#region src/methods/get-dirty-input.ts
685
+ /**
686
+ * Retrieves only the dirty input values of the field at the given path, or
687
+ * the entire form when no path is given. Arrays are treated as atomic and
688
+ * returned in full if any item is dirty, while object keys without a dirty
689
+ * descendant are omitted. Returns `undefined` if nothing in the inspected
690
+ * subtree is dirty.
691
+ *
692
+ * @param form The form store to retrieve dirty input from.
693
+ * @param path The path to the field (omit for the whole form).
694
+ *
695
+ * @returns The dirty input, or `undefined`.
696
+ */
697
+ function getDirtyInput(form, path) {
698
+ const internal = internalOf(form);
699
+ return getDirtyFieldInput(internal, path ? getFieldStore(internal, path) : internal);
700
+ }
701
+
702
+ //#endregion
703
+ //#region src/methods/get-dirty-paths.ts
704
+ /**
705
+ * Returns the paths to the dirty fields of the form (or of the subtree at
706
+ * the given path). Arrays are treated as atomic and contribute only their
707
+ * own path if any item is dirty; object branches are recursed into, and a
708
+ * container that flipped dirty itself (e.g. nullish → present) emits its
709
+ * own path only when no descendant already covers it.
710
+ *
711
+ * @param form The form store to inspect.
712
+ * @param path The path to scope to (omit for the whole form).
713
+ *
714
+ * @returns The list of paths to the dirty fields.
715
+ */
716
+ function getDirtyPaths(form, path) {
717
+ const internal = internalOf(form);
718
+ const paths = [];
719
+ collectDirtyPaths(path ? getFieldStore(internal, path) : internal, paths);
720
+ return paths;
721
+ }
722
+ function collectDirtyPaths(internalFieldStore, paths) {
723
+ if (internalFieldStore.kind === "object" && internalFieldStore.input.value) {
724
+ const lengthBefore = paths.length;
725
+ for (const key in internalFieldStore.children) collectDirtyPaths(internalFieldStore.children[key], paths);
726
+ if (paths.length === lengthBefore && internalFieldStore.isDirty.value && internalFieldStore.path.length > 0) paths.push([...internalFieldStore.path]);
727
+ } else if (internalFieldStore.kind === "value") {
728
+ if (internalFieldStore.isDirty.value && internalFieldStore.path.length > 0) paths.push([...internalFieldStore.path]);
729
+ } else if (getFieldBool(internalFieldStore, "isDirty") && internalFieldStore.path.length > 0) paths.push([...internalFieldStore.path]);
730
+ }
731
+
732
+ //#endregion
733
+ //#region src/methods/get-input.ts
734
+ /**
735
+ * Retrieves the current input value of the field at the given path, or the
736
+ * entire form when no path is given. The tree is the allow-list, so the
737
+ * result only ever contains declared fields.
738
+ *
739
+ * @param form The form store to retrieve input from.
740
+ * @param path The path to the field (omit for the whole form).
741
+ *
742
+ * @returns The input value.
743
+ */
744
+ function getInput(form, path) {
745
+ const internal = internalOf(form);
746
+ return getFieldInput(path ? getFieldStore(internal, path) : internal);
747
+ }
748
+
749
+ //#endregion
750
+ //#region src/methods/pick-dirty.ts
751
+ /**
752
+ * Picks only the dirty parts of the given value, using the form's dirty
753
+ * fields as a structural mask while reading from the SUPPLIED value (e.g. a
754
+ * validated output), not the form's own input. Arrays are treated as atomic
755
+ * and object keys without a dirty descendant are omitted. Returns
756
+ * `undefined` if no field is dirty or no dirty key is present in the value.
757
+ *
758
+ * Envelope leaves (estimate/amount-or-percent) emit their COMPLETE
759
+ * kind envelope — the plugin wraps the supplied
760
+ * value with its meta half, and a leaf whose only change is plugin state
761
+ * (a mode flip) still emits.
762
+ *
763
+ * @param form The form store providing the dirty mask.
764
+ * @param from The value to filter down to its dirty parts.
765
+ *
766
+ * @returns The dirty parts of the value, or `undefined`.
767
+ */
768
+ function pickDirty(form, from) {
769
+ const internal = internalOf(form);
770
+ if (!getFieldBool(internal, "isDirty") && !hasPluginDirtyField(internal, internal)) return;
771
+ const result = pickFieldValue(internal, internal, from);
772
+ return result && typeof result === "object" && Object.keys(result).length ? result : void 0;
773
+ }
774
+ /**
775
+ * Recursively picks the dirty parts of a value using the field store as a
776
+ * structural mask. Objects with present input recurse into their dirty
777
+ * children that exist in the value; arrays, leaves, nullish-cleared
778
+ * containers and shape-diverging values are returned as-is (arrays with
779
+ * their envelope leaves wrapped).
780
+ */
781
+ function pickFieldValue(internalFormStore, internalFieldStore, value) {
782
+ if (internalFieldStore.kind === "object" && internalFieldStore.input.value && value && typeof value === "object" && !Array.isArray(value)) {
783
+ const result = {};
784
+ for (const key in internalFieldStore.children) {
785
+ const child = internalFieldStore.children[key];
786
+ const present = Object.prototype.hasOwnProperty.call(value, key);
787
+ if (child.kind === "value") {
788
+ const valueDirty = getFieldBool(child, "isDirty");
789
+ const pluginDirty = fieldPluginDirty(internalFormStore, child);
790
+ if (!(valueDirty && present) && !pluginDirty) continue;
791
+ const encoded = encodeFieldValue(internalFormStore, child, valueDirty && present ? value[key] : void 0);
792
+ if (valueDirty && present || encoded !== void 0) result[key] = encoded;
793
+ } else if ((getFieldBool(child, "isDirty") || hasPluginDirtyField(internalFormStore, child)) && present) result[key] = pickFieldValue(internalFormStore, child, value[key]);
794
+ }
795
+ return result;
796
+ }
797
+ if (internalFieldStore.kind === "array") return encodeScopeValues(internalFormStore, internalFieldStore, value);
798
+ return value;
799
+ }
800
+
801
+ //#endregion
802
+ //#region src/methods/reset.ts
803
+ /**
804
+ * Resets a specific field or the entire form to its initial state, with
805
+ * fine-grained control over which state to preserve via the `keep*` flags.
806
+ * When `initialInput` is provided it replaces the reset baseline first.
807
+ *
808
+ * @param form The form store to reset.
809
+ * @param config The reset configuration.
810
+ */
811
+ function reset(form, config) {
812
+ batch(() => {
813
+ untrack(() => {
814
+ const internalFormStore = internalOf(form);
815
+ const internalFieldStore = config?.path ? getFieldStore(internalFormStore, config.path) : internalFormStore;
816
+ if (config && "initialInput" in config) setInitialFieldInput(internalFormStore, internalFieldStore, config.initialInput, { syncInitial: true });
817
+ walkFieldStore(internalFieldStore, (fieldStore) => {
818
+ fieldStore.elements = fieldStore.initialElements;
819
+ if (!config?.keepErrors) fieldStore.validationErrors.value = null;
820
+ if (!config?.keepTouched) fieldStore.isTouched.value = false;
821
+ if (!config?.keepEdited) fieldStore.isEdited.value = false;
822
+ fieldStore.startInput.value = fieldStore.initialInput.value;
823
+ if (!config?.keepInput) fieldStore.input.value = fieldStore.initialInput.value;
824
+ if (fieldStore.kind === "array") {
825
+ fieldStore.startItems.value = fieldStore.initialItems.value;
826
+ if (!config?.keepInput || fieldStore.startItems.value.length === fieldStore.items.value.length) fieldStore.items.value = fieldStore.initialItems.value;
827
+ fieldStore.isDirty.value = computeContainerDirty(fieldStore);
828
+ } else if (fieldStore.kind === "object") fieldStore.isDirty.value = computeContainerDirty(fieldStore);
829
+ else {
830
+ fieldStore.isDirty.value = !isSemanticEqual(fieldStore.input.value, fieldStore.startInput.value);
831
+ dispatchResetField(internalFormStore, fieldStore);
832
+ for (const element of fieldStore.elements) if (element instanceof HTMLInputElement && element.type === "file") element.value = "";
833
+ }
834
+ });
835
+ if (!config?.path) {
836
+ if (!config?.keepSubmitted) internalFormStore.isSubmitted.value = false;
837
+ if (internalFormStore.validate === "initial") validateFormInput(internalFormStore);
838
+ }
839
+ });
840
+ });
841
+ }
842
+
843
+ //#endregion
844
+ //#region src/methods/set-entry.ts
845
+ function amountOrPercentOf(form, path) {
846
+ const internalFormStore = internalOf(form);
847
+ const store = getFieldStore(internalFormStore, path);
848
+ const slot = store.kind === "value" ? envelopesKey.get(internalFormStore, store) : void 0;
849
+ if (store.kind !== "value" || slot?.family !== "amount-or-percent") throw new Error(`Not an amount-or-percent field (at ${JSON.stringify(path)}) — needs a field with an amount-or-percent envelope slot`);
850
+ return {
851
+ form: internalFormStore,
852
+ store,
853
+ slot
854
+ };
855
+ }
856
+ /**
857
+ * Sets the entry mode of an amount-or-percent field (enter a dollar
858
+ * amount, or a percent of the percent basis). Dirties the meta half; the
859
+ * field's own value — always the resolved dollar amount — is the widget's
860
+ * to convert and write.
861
+ *
862
+ * @param form The form store containing the field.
863
+ * @param path The path to the amount-or-percent field.
864
+ * @param mode The entry mode.
865
+ */
866
+ function setEntryMode(form, path, mode) {
867
+ const target = amountOrPercentOf(form, path);
868
+ batch(() => {
869
+ untrack(() => {
870
+ writeEnvelope(target.form, target.store, target.slot, {
871
+ ...target.slot.envelope.value,
872
+ mode
873
+ });
874
+ });
875
+ });
876
+ }
877
+ /**
878
+ * Sets the percent basis of an amount-or-percent field (the root-level field key
879
+ * the percent is taken of). Dirties the meta half; keeping the resolved
880
+ * dollar amount constant against the new basis is the widget's job.
881
+ *
882
+ * @param form The form store containing the field.
883
+ * @param path The path to the amount-or-percent field.
884
+ * @param percentBasis The basis field key.
885
+ */
886
+ function setPercentBasis(form, path, percentBasis) {
887
+ const target = amountOrPercentOf(form, path);
888
+ batch(() => {
889
+ untrack(() => {
890
+ writeEnvelope(target.form, target.store, target.slot, {
891
+ ...target.slot.envelope.value,
892
+ basis: percentBasis
893
+ });
894
+ });
895
+ });
896
+ }
897
+
898
+ //#endregion
899
+ //#region src/methods/set-input.ts
900
+ /**
901
+ * Sets the input value of the field at the given path (or the entire form
902
+ * for an empty path), updating touched, edited and dirty state, and
903
+ * triggers validation when the form's validation mode requires it. Throws
904
+ * on a path not declared in the schema.
905
+ *
906
+ * @param form The form store to set input on.
907
+ * @param path The path to the field (`[]` for the whole form).
908
+ * @param input The new input value.
909
+ */
910
+ function setInput(form, path, input) {
911
+ batch(() => {
912
+ const internalFormStore = internalOf(form);
913
+ setFieldInput(internalFormStore, path, input);
914
+ validateIfRequired(internalFormStore, path.length ? getFieldStore(internalFormStore, path) : internalFormStore, "input");
915
+ });
916
+ }
917
+
918
+ //#endregion
919
+ //#region src/methods/set-mode.ts
920
+ /**
921
+ * Flips an estimate field between its two modes — the ONLY supported mode
922
+ * writer (a raw `mode` signal write skips value seeding and the flip
923
+ * timestamp):
924
+ *
925
+ * - `estimate` → `formula`: the current input is preserved as the meta
926
+ * half's `manualValue` and the field computes again. The derived value
927
+ * is NOT written into the input — derived values are outputs; the
928
+ * server recompute pass is their author.
929
+ * - `formula` → `estimate`: the manual value is seeded from the last
930
+ * formula result (estimate-first chronology: when you stop trusting the
931
+ * formula you start from its current value and adjust), which marks the
932
+ * value dirty like any user edit.
933
+ *
934
+ * Either flip dirties the meta half (`lastFlippedAt` stamped), so a flip
935
+ * with no other edit still produces a payload.
936
+ *
937
+ * Works at any depth: an estimate field inside an array row has its own
938
+ * envelope slot, built from the row's envelope. The throw is reserved
939
+ * for a field that genuinely has none — a non-estimate control, or a form
940
+ * without the envelopes plugin.
941
+ *
942
+ * @param form The form store containing the field.
943
+ * @param path The path to the estimate field.
944
+ * @param mode The target mode (a no-op when already current).
945
+ * @param options Flip options (e.g. an injected timestamp).
946
+ */
947
+ function setMode(form, path, mode, options = {}) {
948
+ const internalFormStore = internalOf(form);
949
+ const store = getFieldStore(internalFormStore, path);
950
+ const slot = store.kind === "value" ? envelopesKey.get(internalFormStore, store) : void 0;
951
+ if (store.kind !== "value" || slot?.family !== "estimate") throw new Error(`Not an estimate field (at ${JSON.stringify(path)}) — setMode needs a field with an estimate envelope slot`);
952
+ batch(() => {
953
+ untrack(() => {
954
+ const live = slot.envelope.value;
955
+ if (resolveEstimateMode(live, store.schema) === mode) return;
956
+ const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
957
+ if (mode === "formula") {
958
+ const manualValue = store.isDirty.value ? isEmptyish(store.input.value) ? null : store.input.value : live.manualValue ?? null;
959
+ writeEnvelope(internalFormStore, store, slot, {
960
+ ...live,
961
+ mode: "formula",
962
+ manualValue,
963
+ lastFlippedAt: now
964
+ });
965
+ return;
966
+ }
967
+ const candidate = derivationKey.get(internalFormStore, store)?.formulaValue.value;
968
+ const seeded = candidate && candidate.error === null && candidate.value !== void 0 ? candidate.value : live.value;
969
+ writeEnvelope(internalFormStore, store, slot, {
970
+ ...live,
971
+ mode: "estimate",
972
+ value: seeded,
973
+ lastFlippedAt: now
974
+ });
975
+ if (candidate && candidate.error === null && candidate.value !== void 0) {
976
+ store.isTouched.value = true;
977
+ store.isEdited.value = true;
978
+ }
979
+ });
980
+ });
981
+ }
982
+
983
+ //#endregion
984
+ //#region src/methods/set-off-form-values.ts
985
+ /**
986
+ * Replaces the form's off-form values (the read-only eval scope formula
987
+ * resolution falls back to). One signal write: every formula field reading
988
+ * off-form values re-resolves once — the LOS-470 `BpsBaseSync` behavior as
989
+ * a store property. `applyBaseline` performs the same
990
+ * write on reconcile, batched with the value rebase.
991
+ *
992
+ * @param form The form store.
993
+ * @param values The new off-form values.
994
+ */
995
+ function setOffFormValues(form, values) {
996
+ internalOf(form).offFormValues.value = values;
997
+ }
998
+
999
+ //#endregion
1000
+ //#region src/methods/validate.ts
1001
+ /**
1002
+ * Validates the entire form input with the injected validator, routing each
1003
+ * issue to its field's `errors` signal. Optionally focuses the first field
1004
+ * with an error.
1005
+ *
1006
+ * @param form The form store to validate.
1007
+ * @param config The validation configuration.
1008
+ *
1009
+ * @returns The validation result.
1010
+ */
1011
+ function validate(form, config) {
1012
+ return validateFormInput(internalOf(form), config);
1013
+ }
1014
+
1015
+ //#endregion
1016
+ //#region src/core/codec/encode-dirty.ts
1017
+ /**
1018
+ * Builds the envelope-control lookup from a wire list.
1019
+ */
1020
+ function envelopeContracts(wire) {
1021
+ const map = /* @__PURE__ */ new Map();
1022
+ for (const contract of wire ?? []) for (const control of contract.envelopeControls ?? []) map.set(control, contract);
1023
+ return map;
1024
+ }
1025
+ /**
1026
+ * Encodes a dirty-values object (the `pickDirty`/`getDirtyInput` result)
1027
+ * into the save payload, partitioning each root key by its `x-column`
1028
+ * geometry: `x-column: true` fields become column updates, everything else
1029
+ * lands in the `data` bag. Undeclared keys — including prototype-pollution
1030
+ * keys — are dropped: the schema is the allow-list at the write boundary
1031
+ * too.
1032
+ *
1033
+ * Wire contracts carry each plugin's persistence policy (behavior
1034
+ * relocated verbatim from LOS-461, not redesigned): `skipValue` drops a
1035
+ * formula value (the server recompute is its only author), and an
1036
+ * envelope contract's `encode` normalizes the outgoing envelope (an
1037
+ * estimate value persists exactly when its meta pins `mode: "estimate"`).
1038
+ *
1039
+ * @param schema The form's JSON-Schema (object schema with properties).
1040
+ * @param dirty The dirty values, or `undefined` when nothing is dirty.
1041
+ * @param options Encoding options (column set, wire contracts).
1042
+ *
1043
+ * @returns The partitioned payload, or `undefined` when nothing survives.
1044
+ */
1045
+ function encodeDirty(schema, dirty, options = {}) {
1046
+ if (dirty == null) return void 0;
1047
+ const properties = schema.properties ?? {};
1048
+ const envelopes$1 = envelopeContracts(options.wire);
1049
+ const columns = {};
1050
+ const data = {};
1051
+ let hasEntries = false;
1052
+ for (const key of Object.keys(dirty)) {
1053
+ if (!isSafeKey(key)) continue;
1054
+ if (!Object.prototype.hasOwnProperty.call(properties, key)) continue;
1055
+ const property = properties[key];
1056
+ const control = inferControl(property);
1057
+ if (options.wire?.some((w) => w.skipValue?.(control, dirty, key))) continue;
1058
+ const isColumn = property["x-column"] === true;
1059
+ const contract = envelopes$1.get(control);
1060
+ if (contract) {
1061
+ const encoded = contract.encode ? contract.encode(control, dirty[key]) : dirty[key];
1062
+ if (encoded === void 0) continue;
1063
+ data[key] = encoded;
1064
+ if (isColumn && (!options.knownColumns || options.knownColumns.has(key))) {
1065
+ const value = contract.unwrap ? contract.unwrap(encoded).value : void 0;
1066
+ if (value !== void 0) columns[key] = value;
1067
+ }
1068
+ hasEntries = true;
1069
+ continue;
1070
+ }
1071
+ if (isColumn) {
1072
+ if (options.knownColumns && !options.knownColumns.has(key)) continue;
1073
+ columns[key] = dirty[key];
1074
+ } else data[key] = dirty[key];
1075
+ hasEntries = true;
1076
+ }
1077
+ return hasEntries ? {
1078
+ columns,
1079
+ data
1080
+ } : void 0;
1081
+ }
1082
+
1083
+ //#endregion
1084
+ //#region src/plugins/envelopes/plugin.ts
1085
+ /**
1086
+ * Builds (or re-seeds) the slots of ONE object scope — the document root
1087
+ * or a single array row. Idempotent: called again on a REUSED store (an
1088
+ * array shrink-then-regrow, a rebuilt row) it re-seeds the existing
1089
+ * signals in place instead of replacing them, so computeds already wired
1090
+ * to the mode signal keep tracking it.
1091
+ */
1092
+ function buildScopeSlots(form, state, scope, raw) {
1093
+ for (const key of Object.keys(scope.children)) {
1094
+ const child = scope.children[key];
1095
+ if (child.kind !== "value") continue;
1096
+ if (child.control === "estimate") buildEstimateSlot(form, state, child, readOwn(raw, key));
1097
+ else if (child.control === "amount-or-percent") buildAmountOrPercentSlot(form, state, child, readOwn(raw, key));
1098
+ }
1099
+ }
1100
+ function estimateDirty(live, start, schema) {
1101
+ return resolveEstimateMode(live, schema) !== resolveEstimateMode(start, schema) || resolveEstimateMode(live, schema) === "estimate" && !isSemanticEqual(live.value, start.value);
1102
+ }
1103
+ function buildEstimateSlot(form, state, store, raw) {
1104
+ const decoded = decodeEstimateEnvelope(form, store, raw);
1105
+ const existing = state.get(store);
1106
+ if (existing?.family === "estimate") {
1107
+ existing.startEnvelope.value = decoded;
1108
+ writeEnvelope(form, store, existing, decoded);
1109
+ return;
1110
+ }
1111
+ const envelope = createSignal(decoded);
1112
+ const startEnvelope = createSignal(decoded);
1113
+ const slot = {
1114
+ family: "estimate",
1115
+ envelope,
1116
+ startEnvelope,
1117
+ mode: computed(() => resolveEstimateMode(envelope.value, store.schema)),
1118
+ manualValue: computed(() => envelope.value.manualValue ?? null),
1119
+ lastFlippedAt: computed(() => envelope.value.lastFlippedAt),
1120
+ isDirty: computed(() => estimateDirty(envelope.value, startEnvelope.value, store.schema))
1121
+ };
1122
+ state.set(store, slot);
1123
+ }
1124
+ function buildAmountOrPercentSlot(form, state, store, raw) {
1125
+ const decoded = decodeAmountOrPercentEnvelope(form, store, raw);
1126
+ const existing = state.get(store);
1127
+ if (existing?.family === "amount-or-percent") {
1128
+ existing.startEnvelope.value = decoded;
1129
+ writeEnvelope(form, store, existing, decoded);
1130
+ return;
1131
+ }
1132
+ const envelope = createSignal(decoded);
1133
+ const startEnvelope = createSignal(decoded);
1134
+ const slot = {
1135
+ family: "amount-or-percent",
1136
+ envelope,
1137
+ startEnvelope,
1138
+ entryMode: computed(() => envelope.value.mode),
1139
+ percentBasis: computed(() => envelope.value.basis),
1140
+ isDirty: computed(() => envelope.value.mode !== startEnvelope.value.mode || envelope.value.basis !== startEnvelope.value.basis)
1141
+ };
1142
+ state.set(store, slot);
1143
+ }
1144
+ /**
1145
+ * Rebases one scope's slots onto fresh raw data — the meta half of
1146
+ * `applyBaseline`. `adoptEnvelope` keeps per-channel clean-vs-dirty, then
1147
+ * `writeEnvelope` runs once.
1148
+ */
1149
+ function rebaseScopeSlots(form, state, scope, raw) {
1150
+ for (const key of Object.keys(scope.children)) {
1151
+ const child = scope.children[key];
1152
+ if (child.kind !== "value") continue;
1153
+ const slot = state.get(child);
1154
+ if (!slot) continue;
1155
+ if (slot.family === "estimate") {
1156
+ const incoming = decodeEstimateEnvelope(form, child, readOwn(raw, key));
1157
+ bindAdopted(form, child, slot, adoptEnvelope(slot.envelope.value, slot.startEnvelope.value, incoming, child.schema));
1158
+ } else {
1159
+ const incoming = decodeAmountOrPercentEnvelope(form, child, readOwn(raw, key));
1160
+ bindAdopted(form, child, slot, adoptEnvelope(slot.envelope.value, slot.startEnvelope.value, incoming, child.schema));
1161
+ }
1162
+ }
1163
+ }
1164
+ /**
1165
+ * Serializes an estimate slot's meta half. In estimate mode an EDITED input
1166
+ * is the manual value (keystrokes mirror into the meta — never the loaded
1167
+ * column value); an unedited one carries the decoded `manualValue`
1168
+ * forward. In formula mode the value preserved at flip time carries
1169
+ * forward.
1170
+ */
1171
+ function encodeEstimateMeta(store, slot) {
1172
+ const live = slot.envelope.value;
1173
+ const start = slot.startEnvelope.value;
1174
+ const mode = resolveEstimateMode(live, store.schema);
1175
+ const meta = {
1176
+ mode,
1177
+ manualValue: mode === "estimate" ? store.isDirty.value ? isEmptyish(store.input.value) ? null : store.input.value : start.manualValue ?? null : live.manualValue ?? null
1178
+ };
1179
+ const flippedAt = live.lastFlippedAt ?? start.lastFlippedAt;
1180
+ if (flippedAt !== void 0) meta.lastFlippedAt = flippedAt;
1181
+ return meta;
1182
+ }
1183
+ /**
1184
+ * Whether any LIVE value leaf in the subtree carries a dirty slot. Walks
1185
+ * the tree (not the slot map): stale child stores past an array shrink
1186
+ * keep their slots, and those must never phantom-dirty the form. Reads
1187
+ * array `items`, so a reactive caller subscribes to structural changes.
1188
+ * Never short-circuits — this runs inside the `isDirty` aggregate
1189
+ * computed, and an unread branch would deafen the projection.
1190
+ */
1191
+ function hasDirtySlot(state, store) {
1192
+ if (store.kind === "value") return state.get(store)?.isDirty.value ?? false;
1193
+ let dirty = false;
1194
+ if (store.kind === "array") {
1195
+ const length = store.items.value.length;
1196
+ for (let index = 0; index < length; index++) {
1197
+ const child = store.children[index];
1198
+ if (child && hasDirtySlot(state, child)) dirty = true;
1199
+ }
1200
+ return dirty;
1201
+ }
1202
+ for (const key in store.children) if (hasDirtySlot(state, store.children[key])) dirty = true;
1203
+ return dirty;
1204
+ }
1205
+ /**
1206
+ * The envelopes plugin: owns the meta half of estimate and
1207
+ * amount-or-percent fields — mode/entry state decoded from the kind
1208
+ * envelope, dirty-tracked, and serialized by wrapping the field's own
1209
+ * payload entry. No factory arguments: the envelope rides the field key,
1210
+ * so every scope's raw value already carries the meta half (no envelope
1211
+ * side-channel to decode).
1212
+ */
1213
+ function envelopes() {
1214
+ return {
1215
+ name: "envelopes",
1216
+ key: envelopesKey,
1217
+ wire: envelopesWire,
1218
+ build: () => /* @__PURE__ */ new Map(),
1219
+ buildScope(ctx, scope, raw) {
1220
+ buildScopeSlots(ctx.form, ctx.state, scope, raw);
1221
+ },
1222
+ reseedScope(ctx, scope, raw) {
1223
+ buildScopeSlots(ctx.form, ctx.state, scope, raw);
1224
+ },
1225
+ resetField(ctx, store) {
1226
+ if (store.kind !== "value") return;
1227
+ const slot = ctx.state.get(store);
1228
+ if (!slot) return;
1229
+ if (slot.family === "estimate") writeEnvelope(ctx.form, store, slot, {
1230
+ ...slot.startEnvelope.value,
1231
+ value: store.input.value
1232
+ });
1233
+ else writeEnvelope(ctx.form, store, slot, {
1234
+ ...slot.startEnvelope.value,
1235
+ value: store.input.value
1236
+ });
1237
+ },
1238
+ rebase(ctx, scope, raw) {
1239
+ rebaseScopeSlots(ctx.form, ctx.state, scope, raw);
1240
+ },
1241
+ syncInput(ctx, store, input) {
1242
+ if (store.kind !== "value") return false;
1243
+ const slot = ctx.state.get(store);
1244
+ if (!slot) return false;
1245
+ if (slot.family === "estimate") syncEstimateInput(ctx.form, store, slot, input);
1246
+ else syncAmountOrPercentInput(ctx.form, store, slot, input);
1247
+ return true;
1248
+ },
1249
+ syncInitial(ctx, store, raw) {
1250
+ if (store.kind !== "value") return;
1251
+ const slot = ctx.state.get(store);
1252
+ if (!slot) return;
1253
+ if (slot.family === "estimate") slot.startEnvelope.value = decodeEstimateEnvelope(ctx.form, store, raw);
1254
+ else slot.startEnvelope.value = decodeAmountOrPercentEnvelope(ctx.form, store, raw);
1255
+ },
1256
+ transferField(ctx, from, to) {
1257
+ const source = ctx.state.get(from);
1258
+ const target = ctx.state.get(to);
1259
+ if (!source || !target || source.family !== target.family) return;
1260
+ if (source.family === "estimate" && target.family === "estimate") {
1261
+ target.startEnvelope.value = source.startEnvelope.value;
1262
+ target.envelope.value = source.envelope.value;
1263
+ } else if (source.family === "amount-or-percent" && target.family === "amount-or-percent") {
1264
+ target.startEnvelope.value = source.startEnvelope.value;
1265
+ target.envelope.value = source.envelope.value;
1266
+ }
1267
+ },
1268
+ swapField(ctx, first, second) {
1269
+ const a = ctx.state.get(first);
1270
+ const b = ctx.state.get(second);
1271
+ if (!a || !b || a.family !== b.family) return;
1272
+ if (a.family === "estimate" && b.family === "estimate") {
1273
+ swapSignals(a.startEnvelope, b.startEnvelope);
1274
+ swapSignals(a.envelope, b.envelope);
1275
+ } else if (a.family === "amount-or-percent" && b.family === "amount-or-percent") {
1276
+ swapSignals(a.startEnvelope, b.startEnvelope);
1277
+ swapSignals(a.envelope, b.envelope);
1278
+ }
1279
+ },
1280
+ fieldIsDirty(ctx, store) {
1281
+ return ctx.state.get(store)?.isDirty.value ?? false;
1282
+ },
1283
+ encodeValue(ctx, store, valueOut) {
1284
+ const slot = ctx.state.get(store);
1285
+ if (!slot) return void 0;
1286
+ if (!slot.isDirty.value && valueOut === void 0) return void 0;
1287
+ if (slot.family === "estimate") {
1288
+ if (!slot.isDirty.value) {
1289
+ const persisted = slot.startEnvelope.value.mode;
1290
+ if (persisted === void 0) return void 0;
1291
+ const start = slot.startEnvelope.value;
1292
+ const meta$1 = {
1293
+ mode: persisted,
1294
+ manualValue: start.manualValue ?? null
1295
+ };
1296
+ if (start.lastFlippedAt !== void 0) meta$1.lastFlippedAt = start.lastFlippedAt;
1297
+ return persisted === "estimate" ? wrapEstimate(valueOut, meta$1) : wrapEstimate(void 0, meta$1);
1298
+ }
1299
+ const meta = encodeEstimateMeta(store, slot);
1300
+ return resolveEstimateMode(slot.envelope.value, store.schema) === "estimate" ? wrapEstimate(valueOut !== void 0 ? valueOut : store.input.value, meta) : wrapEstimate(void 0, meta);
1301
+ }
1302
+ return wrapAmountOrPercent(valueOut !== void 0 ? valueOut : store.input.value, {
1303
+ mode: slot.entryMode.value,
1304
+ basis: slot.percentBasis.value ?? ""
1305
+ });
1306
+ },
1307
+ isDirty(ctx) {
1308
+ return hasDirtySlot(ctx.state, ctx.form);
1309
+ },
1310
+ fieldSnapshot(ctx, store, path) {
1311
+ const slot = ctx.state.get(store);
1312
+ if (!slot) return {};
1313
+ if (slot.family === "estimate") {
1314
+ slot.callbacks ??= { setMode: (mode) => setMode(ctx.form, path, mode) };
1315
+ return {
1316
+ mode: slot.mode.value,
1317
+ setMode: slot.callbacks.setMode
1318
+ };
1319
+ }
1320
+ slot.callbacks ??= {
1321
+ setEntryMode: (mode) => setEntryMode(ctx.form, path, mode),
1322
+ setPercentBasis: (percentBasis) => setPercentBasis(ctx.form, path, percentBasis)
1323
+ };
1324
+ return {
1325
+ entryMode: slot.entryMode.value,
1326
+ percentBasis: slot.percentBasis.value,
1327
+ setEntryMode: slot.callbacks.setEntryMode,
1328
+ setPercentBasis: slot.callbacks.setPercentBasis
1329
+ };
1330
+ }
1331
+ };
1332
+ }
1333
+ function swapSignals(first, second) {
1334
+ const value = first.value;
1335
+ first.value = second.value;
1336
+ second.value = value;
1337
+ }
1338
+
1339
+ //#endregion
1340
+ //#region src/plugins/derivation/plugin.ts
1341
+ /**
1342
+ * Key for a directed dep-graph edge (`from` reads `to`).
1343
+ */
1344
+ function edgeKey(from, to) {
1345
+ return `${from}\u0000${to}`;
1346
+ }
1347
+ /**
1348
+ * Extracts a human-readable message from a thrown evaluation error.
1349
+ */
1350
+ function messageOf(error) {
1351
+ return error instanceof Error && error.message ? error.message : String(error);
1352
+ }
1353
+ /**
1354
+ * The derivation plugin: parses every `x-formula` once per scope (the
1355
+ * document root and each array row — it is scope, not machinery, that
1356
+ * differs; LOS-596), breaks dependency cycles deterministically, and gives
1357
+ * each formula field its `formulaValue`/`derived` slot plus the composed
1358
+ * `errors` channel.
1359
+ *
1360
+ * Scope resolution lives HERE and only here (the LOS-514 closure — no
1361
+ * other read path exists): per dependency, the form value wins (a formula
1362
+ * dep resolves through its own derived signal, chaining fresh values),
1363
+ * `offFormValues` fills only `undefined`, and when both sides are arrays
1364
+ * the canonical rows merge with the live form rows (the collection
1365
+ * overlay for aggregate refs).
1366
+ *
1367
+ * Declares `dependsOn: [envelopesKey]` — the estimate pin reads the mode
1368
+ * signal the envelopes plugin creates; without it the pin would silently
1369
+ * never engage and a manually pinned value would be overwritten by the
1370
+ * next recompute (the D2 data-loss scenario), so a missing envelopes
1371
+ * plugin is a startup error instead.
1372
+ */
1373
+ function derivation(engine) {
1374
+ return {
1375
+ name: "derivation",
1376
+ key: derivationKey,
1377
+ dependsOn: [envelopesKey],
1378
+ build: () => /* @__PURE__ */ new Map(),
1379
+ buildScope(ctx, scope) {
1380
+ const form = ctx.form;
1381
+ if (scope === form) {
1382
+ wireFormulaGraph(form, ctx.state, engine, scope.children, (dep, formValue) => {
1383
+ const offValue = readOwn(form.offFormValues.value, dep);
1384
+ if (Array.isArray(offValue) && Array.isArray(formValue)) return mergeCollectionRows(offValue, formValue);
1385
+ return formValue === void 0 ? offValue : formValue;
1386
+ });
1387
+ return;
1388
+ }
1389
+ wireFormulaGraph(form, ctx.state, engine, scope.children, (dep, formValue) => resolveRowFallback(form, scope, dep, formValue));
1390
+ },
1391
+ fieldSnapshot(ctx, store) {
1392
+ const slot = ctx.state.get(store);
1393
+ if (!slot) return {};
1394
+ return {
1395
+ derived: slot.derived.value,
1396
+ formulaValue: slot.formulaValue.value
1397
+ };
1398
+ }
1399
+ };
1400
+ }
1401
+ /**
1402
+ * Wires the formula fields of ONE scope (the document root or a single
1403
+ * array row): collects the scope's parseable `x-formula` fields, breaks
1404
+ * cycles among them, and gives each one its slot. Everything
1405
+ * scope-specific enters through `resolveOutside`.
1406
+ */
1407
+ function wireFormulaGraph(form, state, engine, children, resolveOutside) {
1408
+ const entries = /* @__PURE__ */ new Map();
1409
+ for (const key of Object.keys(children)) {
1410
+ const child = children[key];
1411
+ if (child.kind !== "value") continue;
1412
+ if (child.control !== "formula" && child.control !== "estimate") continue;
1413
+ if (child.schema["x-server-maintained"] === true) {
1414
+ const passthrough = computed(() => ({
1415
+ value: getFieldInput(child),
1416
+ error: null
1417
+ }));
1418
+ state.set(child, {
1419
+ derived: passthrough,
1420
+ formulaValue: passthrough,
1421
+ isRollup: false
1422
+ });
1423
+ continue;
1424
+ }
1425
+ const formula = child.schema["x-formula"];
1426
+ if (typeof formula !== "string" || formula.trim() === "") continue;
1427
+ const parsed = engine.parse(formula);
1428
+ entries.set(key, {
1429
+ store: child,
1430
+ node: parsed.ok ? parsed.node : void 0,
1431
+ deps: parsed.ok ? engine.extractDependencies(parsed.node) : [],
1432
+ parseError: parsed.ok ? null : parsed.error
1433
+ });
1434
+ }
1435
+ if (entries.size === 0) return;
1436
+ const brokenEdges = /* @__PURE__ */ new Set();
1437
+ const cycleErrors = /* @__PURE__ */ new Map();
1438
+ const dfsState = /* @__PURE__ */ new Map();
1439
+ const visit = (key) => {
1440
+ dfsState.set(key, "visiting");
1441
+ for (const dep of entries.get(key).deps) {
1442
+ if (!entries.has(dep)) continue;
1443
+ if (dep === key || dfsState.get(dep) === "visiting") {
1444
+ brokenEdges.add(edgeKey(key, dep));
1445
+ cycleErrors.set(key, `Circular reference: "${key}" reads "${dep}"`);
1446
+ continue;
1447
+ }
1448
+ if (dfsState.get(dep) !== "done") visit(dep);
1449
+ }
1450
+ dfsState.set(key, "done");
1451
+ };
1452
+ for (const key of entries.keys()) if (!dfsState.has(key)) visit(key);
1453
+ const resolveDep = (fromKey, dep) => {
1454
+ const child = children[dep];
1455
+ let formValue;
1456
+ if (child) {
1457
+ const depEntry = child.kind === "value" ? entries.get(dep) : void 0;
1458
+ if (depEntry && depEntry.store === child && !brokenEdges.has(edgeKey(fromKey, dep))) {
1459
+ const depState = state.get(child).derived.value;
1460
+ if (depState.error !== null) throw new Error(`Upstream formula error: "${dep}"`);
1461
+ formValue = depState.value;
1462
+ } else formValue = getFieldInput(child);
1463
+ }
1464
+ return resolveOutside(dep, formValue);
1465
+ };
1466
+ for (const [key, entry] of entries) {
1467
+ const { store } = entry;
1468
+ const isRollup = entry.parseError === null && (engine.extractPathRefs?.(entry.node)?.length ?? 0) > 0;
1469
+ const cycleError = cycleErrors.get(key) ?? null;
1470
+ const formulaValue = computed(() => {
1471
+ if (entry.parseError !== null) return {
1472
+ value: void 0,
1473
+ error: entry.parseError
1474
+ };
1475
+ try {
1476
+ const scope = Object.create(null);
1477
+ for (const dep of entry.deps) scope[dep] = resolveDep(key, dep);
1478
+ return {
1479
+ value: engine.evaluate(entry.node, scope),
1480
+ error: cycleError
1481
+ };
1482
+ } catch (error) {
1483
+ return {
1484
+ value: void 0,
1485
+ error: cycleError ?? messageOf(error)
1486
+ };
1487
+ }
1488
+ });
1489
+ const derived = store.control === "estimate" ? computed(() => {
1490
+ const envelope = envelopesKey.get(form, store);
1491
+ return envelope?.family === "estimate" && envelope.mode.value === "estimate" && !isEmptyish(store.input.value) ? {
1492
+ value: store.input.value,
1493
+ error: null
1494
+ } : formulaValue.value;
1495
+ }) : formulaValue;
1496
+ state.set(store, {
1497
+ derived,
1498
+ formulaValue,
1499
+ isRollup
1500
+ });
1501
+ store.errors = computed(() => {
1502
+ const validation = store.validationErrors.value;
1503
+ const calc = derived.value.error;
1504
+ if (calc === null) return validation;
1505
+ return validation ? [...validation, calc] : [calc];
1506
+ });
1507
+ }
1508
+ }
1509
+
1510
+ //#endregion
1511
+ //#region src/plugins/derivation/wire.ts
1512
+ /**
1513
+ * The derivation plugin's STATIC wire contract: a `formula` value is
1514
+ * ALWAYS server-recomputed — a client payload only carries a stale echo of
1515
+ * the last-rendered result, so `encodeDirty` drops it. (The estimate-pin
1516
+ * policy lives on `envelopesWire.encode` — the envelope owns it.)
1517
+ */
1518
+ const derivationWire = { skipValue: (control) => control === "formula" };
1519
+
1520
+ //#endregion
1521
+ export { DEFAULT_EMPTY_INPUT, DEFAULT_ROOT_RECORD_ALIAS, UNEVALUABLE_MESSAGE_ID, applyBaseline, bagger, checks, collectionKeys, computeBag, createFormStore, decodeRecord, derivation, derivationWire, encodeDirty, envelopeContracts, envelopes, envelopesWire, flattenSourceRow, focus, formulaCheck, getDeepErrorEntries, getDeepErrors, getDirtyInput, getDirtyPaths, getErrors, getInput, handleSubmit, inferControl, insert, isEmptyish, isEnvelope, isPresenceEqual, isSemanticEqual, mergeCollectionRows, move, pickDirty, readRelationConfig, relationParentFieldName, relationRowIdentityProps, remove, replaceCheckInstances, reset, resolveScopeValue, resolveScopeValueAt, setEntryMode, setErrors, setInput, setMode, setOffFormValues, setPercentBasis, swap, targetToKind, validate, visibility, withRelationRowIdentity, wrapAmountOrPercent, wrapEstimate };