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.
@@ -0,0 +1,1545 @@
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";
2
+
3
+ //#region src/core/field/get-dirty-field-input.ts
4
+ /**
5
+ * Returns only the dirty input of the field store. Arrays are treated as
6
+ * atomic and returned in full if any item is dirty, while object keys
7
+ * without a dirty descendant are omitted. Returns `undefined` if no
8
+ * descendant is dirty.
9
+ *
10
+ * Plugin state serializes THROUGH the field's own entry (the LOS-573
11
+ * envelope): a dirty estimate/amount-or-percent leaf emits
12
+ * kind envelope in place of its bare value — a mode flip
13
+ * with an unchanged value still produces a payload. Inside an emitted
14
+ * array every envelope leaf is wrapped COMPLETE, dirty or not: rows
15
+ * persist wholesale, and a bare value would clobber the persisted meta
16
+ * half of its envelope.
17
+ *
18
+ * @param internalFormStore The form store (plugin state lives here).
19
+ * @param internalFieldStore The field store to get dirty input from.
20
+ *
21
+ * @returns The dirty input, or `undefined` if no descendant is dirty.
22
+ */
23
+ function getDirtyFieldInput(internalFormStore, internalFieldStore) {
24
+ if (!getFieldBool(internalFieldStore, "isDirty") && !hasPluginDirtyField(internalFormStore, internalFieldStore)) return;
25
+ if (internalFieldStore.kind === "array") return encodeScopeValues(internalFormStore, internalFieldStore, getFieldInput(internalFieldStore));
26
+ if (internalFieldStore.kind === "object") {
27
+ if (internalFieldStore.input.value) {
28
+ const value = {};
29
+ for (const key in internalFieldStore.children) {
30
+ const child = internalFieldStore.children[key];
31
+ if (child.kind === "value") {
32
+ const valueDirty = getFieldBool(child, "isDirty");
33
+ const pluginDirty = fieldPluginDirty(internalFormStore, child);
34
+ if (!valueDirty && !pluginDirty) continue;
35
+ const encoded = encodeFieldValue(internalFormStore, child, valueDirty ? child.input.value : void 0);
36
+ if (valueDirty || encoded !== void 0) value[key] = encoded;
37
+ } else if (getFieldBool(child, "isDirty") || hasPluginDirtyField(internalFormStore, child)) value[key] = getDirtyFieldInput(internalFormStore, child);
38
+ }
39
+ return value;
40
+ }
41
+ return internalFieldStore.input.value;
42
+ }
43
+ return encodeFieldValue(internalFormStore, internalFieldStore, internalFieldStore.input.value);
44
+ }
45
+ /**
46
+ * Wraps the envelope leaves of an emitted subtree value: rows and nested
47
+ * objects keep their shape, but every value leaf claimed by a plugin's
48
+ * `encodeValue` (estimate/amount-or-percent) is replaced with its COMPLETE
49
+ * envelope — the whole-array emission convention. The supplied value is
50
+ * never mutated.
51
+ */
52
+ function encodeScopeValues(internalFormStore, internalFieldStore, value) {
53
+ if (!internalFieldStore) return value;
54
+ if (internalFieldStore.kind === "array" && Array.isArray(value)) return value.map((item, index) => encodeScopeValues(internalFormStore, internalFieldStore.children[index], item));
55
+ if (internalFieldStore.kind === "object" && value !== null && typeof value === "object" && !Array.isArray(value)) {
56
+ const row = { ...value };
57
+ for (const key in internalFieldStore.children) {
58
+ const child = internalFieldStore.children[key];
59
+ if (child.kind === "value") {
60
+ if (Object.prototype.hasOwnProperty.call(row, key) || fieldPluginDirty(internalFormStore, child)) {
61
+ const encoded = encodeFieldValue(internalFormStore, child, readOwn(row, key));
62
+ if (encoded !== void 0) row[key] = encoded;
63
+ }
64
+ } else if (Object.prototype.hasOwnProperty.call(row, key)) row[key] = encodeScopeValues(internalFormStore, child, row[key]);
65
+ }
66
+ return row;
67
+ }
68
+ return value;
69
+ }
70
+
71
+ //#endregion
72
+ //#region src/plugins/envelopes/wire.ts
73
+ /**
74
+ * Returns whether a raw persisted entry is an envelope: an object whose
75
+ * `kind` is `estimate` or `amount-or-percent`. Anything else — including
76
+ * the bare scalars every non-envelope field persists — is a bare value.
77
+ */
78
+ function isEnvelope(raw) {
79
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
80
+ const kind = raw.kind;
81
+ return kind === "estimate" || kind === "amount-or-percent";
82
+ }
83
+ /** Meta keys only — `kind` and `value` are the envelope's own halves. */
84
+ function metaOf(raw) {
85
+ const meta = {};
86
+ for (const key of Object.keys(raw)) {
87
+ if (key === "kind" || key === "value") continue;
88
+ meta[key] = raw[key];
89
+ }
90
+ return meta;
91
+ }
92
+ /**
93
+ * Wraps an estimate field's halves into its envelope. The value key is
94
+ * OMITTED when `value` is `undefined` (formula mode ships
95
+ * `{ kind: "estimate", mode: "formula" }` and lets the server recompute
96
+ * author the value half).
97
+ */
98
+ function wrapEstimate(value, meta) {
99
+ const envelope = {
100
+ kind: "estimate",
101
+ ...meta
102
+ };
103
+ if (value !== void 0) envelope.value = value;
104
+ return envelope;
105
+ }
106
+ /**
107
+ * Wraps an amount-or-percent field's halves into its envelope. Always
108
+ * complete — an envelope is one bag key, so a partial write would clobber
109
+ * the persisted other half.
110
+ */
111
+ function wrapAmountOrPercent(value, meta) {
112
+ return {
113
+ kind: "amount-or-percent",
114
+ value,
115
+ ...meta
116
+ };
117
+ }
118
+ /**
119
+ * The envelopes plugin's STATIC wire contract — isomorphic by
120
+ * construction: the server imports this same object for save routing
121
+ * (`encodeDirty`), the recompute pass, and engine-less readers
122
+ * (changelog, list pages), with no form store anywhere (D7).
123
+ *
124
+ * Envelope shape: estimate fields persist
125
+ * `{ kind: "estimate", value?, mode, manualValue?, lastFlippedAt? }`,
126
+ * amount-or-percent fields
127
+ * `{ kind: "amount-or-percent", value, mode, basis? }`.
128
+ * Wire `mode` uses settled names (`estimate`/`formula`, `amount`/`percent`).
129
+ * Only these two controls grow the envelope; scalars stay bare.
130
+ */
131
+ const envelopesWire = {
132
+ envelopeControls: ["estimate", "amount-or-percent"],
133
+ unwrap(raw) {
134
+ if (!isEnvelope(raw)) return {
135
+ value: raw,
136
+ meta: {}
137
+ };
138
+ return {
139
+ value: readOwn(raw, "value"),
140
+ meta: metaOf(raw)
141
+ };
142
+ },
143
+ encode(control, raw) {
144
+ if (control !== "estimate") return raw;
145
+ if (!isEnvelope(raw)) return void 0;
146
+ const meta = metaOf(raw);
147
+ if (meta.mode === "estimate") return raw;
148
+ return wrapEstimate(void 0, meta);
149
+ }
150
+ };
151
+
152
+ //#endregion
153
+ //#region src/plugins/envelopes/envelope.ts
154
+ /**
155
+ * Schema-declared opening mode for an unpinned estimate field.
156
+ * `"formula"` → formula-first; `"estimate"` / missing → manual-first.
157
+ */
158
+ function schemaDefaultEstimateMode(schema) {
159
+ return schema?.["x-estimate-default-mode"] === "formula" ? "formula" : "estimate";
160
+ }
161
+ /**
162
+ * Resolves an estimate field's live mode:
163
+ * - an explicit `formula` / `estimate` pin always wins
164
+ * - no pin + empty value + schema default `formula` → `formula` (LOS-823)
165
+ * - otherwise → `estimate` (manual-first, LOS-461: a stored unpinned
166
+ * value stays typeable so a schema default cannot clobber it)
167
+ */
168
+ function resolveEstimateMode(meta, schema) {
169
+ if (meta.mode === "formula" || meta.mode === "estimate") return meta.mode;
170
+ if (schemaDefaultEstimateMode(schema) === "formula" && isEmptyish(meta.value)) return "formula";
171
+ return "estimate";
172
+ }
173
+ /**
174
+ * Schema-declared opening unit for an unpinned amount-or-percent field.
175
+ * Missing / anything other than `"percent"` → `amount` (amount-first).
176
+ */
177
+ function schemaDefaultEntryMode(schema) {
178
+ return schema?.["x-hybrid-default-mode"] === "percent" ? "percent" : "amount";
179
+ }
180
+ /**
181
+ * Resolves persisted amount-or-percent entry state:
182
+ * - an explicit `percent` / `amount` pin always wins
183
+ * - no pin + empty value + schema default `percent` → `percent` (LOS-824)
184
+ * - otherwise → `amount` (amount-first: a stored unpinned value stays
185
+ * dollars so imported/legacy amounts are not treated as percent-owned)
186
+ * Percent basis falls back to the schema's declared default denominator.
187
+ */
188
+ function resolveAmountOrPercentEntry(meta, schema, value) {
189
+ const schemaDefault = schema["x-hybrid-default-denominator"];
190
+ return {
191
+ entryMode: meta.mode === "percent" || meta.mode === "amount" ? meta.mode : schemaDefaultEntryMode(schema) === "percent" && isEmptyish(value) ? "percent" : "amount",
192
+ percentBasis: typeof meta.basis === "string" ? meta.basis : typeof schemaDefault === "string" && schemaDefault !== "" ? schemaDefault : void 0
193
+ };
194
+ }
195
+ function resolveEnvelopeValue(form, store, value) {
196
+ return resolveValueInput(form.emptyInput, store.schema, store.isNullish, value);
197
+ }
198
+ /**
199
+ * Decodes one estimate field's raw (kind envelope or bare scalar) into
200
+ * the in-memory envelope. `mode` is omitted when the wire never pinned
201
+ * one — encode must not fabricate `{ mode: "estimate" }` for a virgin
202
+ * field.
203
+ */
204
+ function decodeEstimateEnvelope(form, store, raw) {
205
+ const { value, meta } = envelopesWire.unwrap(raw);
206
+ const estimateMeta = meta;
207
+ const envelope = {
208
+ kind: "estimate",
209
+ value: resolveEnvelopeValue(form, store, value)
210
+ };
211
+ if (estimateMeta.mode === "formula" || estimateMeta.mode === "estimate") return {
212
+ ...envelope,
213
+ mode: estimateMeta.mode,
214
+ manualValue: estimateMeta.manualValue ?? null,
215
+ ...estimateMeta.lastFlippedAt !== void 0 ? { lastFlippedAt: estimateMeta.lastFlippedAt } : {}
216
+ };
217
+ return {
218
+ ...envelope,
219
+ manualValue: estimateMeta.manualValue ?? null,
220
+ ...estimateMeta.lastFlippedAt !== void 0 ? { lastFlippedAt: estimateMeta.lastFlippedAt } : {}
221
+ };
222
+ }
223
+ /**
224
+ * Decodes one amount-or-percent field's raw into the in-memory envelope
225
+ * with entry state already resolved against the schema default.
226
+ */
227
+ function decodeAmountOrPercentEnvelope(form, store, raw) {
228
+ const { value, meta } = envelopesWire.unwrap(raw);
229
+ const { entryMode, percentBasis } = resolveAmountOrPercentEntry(meta, store.schema, value);
230
+ return {
231
+ kind: "amount-or-percent",
232
+ value: resolveEnvelopeValue(form, store, value),
233
+ mode: entryMode,
234
+ ...percentBasis !== void 0 ? { basis: percentBasis } : {}
235
+ };
236
+ }
237
+ function writeEnvelope(form, store, slot, next) {
238
+ const resolved = resolveEnvelopeValue(form, store, next.value);
239
+ if (slot.family === "estimate") slot.envelope.value = {
240
+ ...next,
241
+ value: resolved
242
+ };
243
+ else slot.envelope.value = {
244
+ ...next,
245
+ value: resolved
246
+ };
247
+ store.input.value = resolved;
248
+ store.isDirty.value = !isSemanticEqual(resolved, store.startInput.value);
249
+ }
250
+ function adoptEnvelope(live, start, incoming, schema) {
251
+ if (incoming.kind === "estimate") return adoptEstimate(live, start, incoming, schema);
252
+ return adoptAmountOrPercent(live, start, incoming);
253
+ }
254
+ function adoptEstimate(live, start, incoming, schema) {
255
+ const modeDirty = resolveEstimateMode(live, schema) !== resolveEstimateMode(start, schema);
256
+ const valueDirty = !isSemanticEqual(live.value, start.value);
257
+ return {
258
+ start: incoming,
259
+ live: {
260
+ ...incoming,
261
+ ...modeDirty ? {
262
+ mode: live.mode,
263
+ lastFlippedAt: live.lastFlippedAt,
264
+ manualValue: live.manualValue
265
+ } : {},
266
+ ...valueDirty ? {
267
+ value: live.value,
268
+ ...resolveEstimateMode(live, schema) === "estimate" ? { manualValue: live.manualValue } : {}
269
+ } : {}
270
+ }
271
+ };
272
+ }
273
+ function adoptAmountOrPercent(live, start, incoming) {
274
+ return {
275
+ start: incoming,
276
+ live: {
277
+ ...incoming,
278
+ ...live.mode !== start.mode ? { mode: live.mode } : {},
279
+ ...live.basis !== start.basis ? { basis: live.basis } : {},
280
+ ...!isSemanticEqual(live.value, start.value) ? { value: live.value } : {}
281
+ }
282
+ };
283
+ }
284
+ function bindAdopted(form, store, slot, adopted) {
285
+ if (slot.family === "estimate") {
286
+ slot.startEnvelope.value = adopted.start;
287
+ store.startInput.value = resolveEnvelopeValue(form, store, adopted.start.value);
288
+ writeEnvelope(form, store, slot, adopted.live);
289
+ return;
290
+ }
291
+ slot.startEnvelope.value = adopted.start;
292
+ store.startInput.value = resolveEnvelopeValue(form, store, adopted.start.value);
293
+ writeEnvelope(form, store, slot, adopted.live);
294
+ }
295
+ /**
296
+ * Estimate keystroke: value (and, in estimate mode, `manualValue`) land
297
+ * on the same envelope as the number.
298
+ */
299
+ function syncEstimateInput(form, store, slot, input) {
300
+ const live = slot.envelope.value;
301
+ writeEnvelope(form, store, slot, {
302
+ ...live,
303
+ value: input,
304
+ ...resolveEstimateMode(live, store.schema) === "estimate" ? { manualValue: isEmptyish(input) ? null : input } : {}
305
+ });
306
+ }
307
+ /**
308
+ * Amount-or-percent keystroke: value half only — entry state is untouched.
309
+ */
310
+ function syncAmountOrPercentInput(form, store, slot, input) {
311
+ writeEnvelope(form, store, slot, {
312
+ ...slot.envelope.value,
313
+ value: input
314
+ });
315
+ }
316
+
317
+ //#endregion
318
+ //#region src/core/plugin/key.ts
319
+ /**
320
+ * The identity a plugin's state is stored under in the form store's
321
+ * `pluginState` map (the ProseMirror pattern, minus its string-name
322
+ * registry). The generic is a phantom — it exists only so `getState`
323
+ * returns the right type; nothing is stored on the key itself.
324
+ *
325
+ * Keys are module-level singletons exported next to their plugin factory.
326
+ * Cross-plugin reads go through the exported key (derivation imports
327
+ * `envelopesKey`, never the envelopes implementation), and stay
328
+ * `T | undefined` — a form without the owning plugin is a legal config.
329
+ */
330
+ var PluginKey = class {
331
+ constructor(name) {
332
+ this.name = name;
333
+ }
334
+ /**
335
+ * Reads the owning plugin's state off a form store, or `undefined` when
336
+ * the plugin is not registered on this form.
337
+ */
338
+ getState(form) {
339
+ return form.pluginState.get(this);
340
+ }
341
+ };
342
+ /**
343
+ * A plugin key whose state is a per-field map keyed by field-store
344
+ * IDENTITY. Field stores are position-fixed — array operations move signal
345
+ * VALUES between stores, not the stores themselves — so per-field slots
346
+ * must transfer with item state via the `transferField`/`swapField` hooks,
347
+ * exactly like the input signals they sit next to.
348
+ */
349
+ var FieldSlotKey = class extends PluginKey {
350
+ /**
351
+ * Reads one field's slot, or `undefined` when the plugin is absent or the
352
+ * field carries no slot (a bare scalar to this plugin).
353
+ */
354
+ get(form, store) {
355
+ return this.getState(form)?.get(store);
356
+ }
357
+ };
358
+
359
+ //#endregion
360
+ //#region src/plugins/envelopes/key.ts
361
+ /**
362
+ * The envelopes plugin's slot key. Other plugins read envelope state
363
+ * through this identity (the derivation plugin pins on `mode`), never
364
+ * through the envelopes implementation.
365
+ */
366
+ const envelopesKey = new FieldSlotKey("envelopes");
367
+
368
+ //#endregion
369
+ //#region src/plugins/derivation/key.ts
370
+ /**
371
+ * The derivation plugin's slot key. The scope resolvers and the react
372
+ * surface read derived state through this identity.
373
+ */
374
+ const derivationKey = new FieldSlotKey("derivation");
375
+
376
+ //#endregion
377
+ //#region src/core/derivation/merge-collection-rows.ts
378
+ /**
379
+ * Merge a collection's canonical rows (the read model — full server rows
380
+ * with calc fields pre-evaluated, delivered via `offFormValues`) with the
381
+ * live form rows (the write model the user edits) for formula evaluation.
382
+ *
383
+ * Membership comes from the LIVE rows, so rows added or removed during the
384
+ * form session are respected. Each live row is enriched from the canonical
385
+ * row with the same `id`: live keys win (an explicit `null` means the user
386
+ * cleared the field), canonical fills everything the live row doesn't
387
+ * carry — core columns the write model never holds.
388
+ *
389
+ * One home for collection overlay (app re-exports this). The merged result
390
+ * is for evaluation only; it is never written back into form state.
391
+ */
392
+ function mergeCollectionRows(canonicalRows, liveRows) {
393
+ if (!Array.isArray(liveRows)) return [...canonicalRows];
394
+ const canonicalById = /* @__PURE__ */ new Map();
395
+ for (const row of canonicalRows) if (row && typeof row === "object" && "id" in row) canonicalById.set(row.id, row);
396
+ return liveRows.map((live) => {
397
+ if (!live || typeof live !== "object") return live;
398
+ const liveRow = live;
399
+ const canonical = canonicalById.get(liveRow.id);
400
+ if (!canonical) return liveRow;
401
+ const merged = { ...canonical };
402
+ for (const [key, value] of Object.entries(liveRow)) if (value !== void 0) merged[key] = value;
403
+ for (const key of Object.keys(merged)) if (merged[key] === void 0) delete merged[key];
404
+ return merged;
405
+ });
406
+ }
407
+
408
+ //#endregion
409
+ //#region src/core/relation/relation-config.ts
410
+ const SCHEMA_REF_PREFIX = "schema://";
411
+ function refTarget(value) {
412
+ return typeof value === "string" && value.startsWith(SCHEMA_REF_PREFIX) ? value.slice(9) : void 0;
413
+ }
414
+ function str(value) {
415
+ return typeof value === "string" ? value : void 0;
416
+ }
417
+ function bool(value) {
418
+ return typeof value === "boolean" ? value : void 0;
419
+ }
420
+ function strArray(value) {
421
+ return Array.isArray(value) && value.every((s) => typeof s === "string") ? value : void 0;
422
+ }
423
+ function hasType(schema, type) {
424
+ return Array.isArray(schema.type) ? schema.type.includes(type) : schema.type === type;
425
+ }
426
+ /**
427
+ * Reads the widget extras (role, traits filter, …) that ride as sibling
428
+ * keys on both relation forms — `x-relation` namespace first, flat vendor
429
+ * keys as fallback.
430
+ */
431
+ function readExtras(record) {
432
+ const ns = record["x-relation"];
433
+ if (ns && typeof ns === "object") {
434
+ const obj = ns;
435
+ return {
436
+ role: str(obj.role),
437
+ parent: str(obj.parent),
438
+ multiple: bool(obj.multiple),
439
+ traits: strArray(obj.traits),
440
+ useMockData: bool(obj.useMockData)
441
+ };
442
+ }
443
+ return {
444
+ role: str(record["x-relation-role"]),
445
+ parent: str(record["x-relation-parent"]),
446
+ multiple: bool(record["x-relation-multiple"]),
447
+ traits: strArray(record["x-relation-traits"]),
448
+ useMockData: bool(record["x-use-mock-data"])
449
+ };
450
+ }
451
+ /**
452
+ * Reads a node's relation config, or `undefined` for a plain enum
453
+ * select/multiselect. Two schema forms:
454
+ * 1. Canonical — `$ref: schema://<target>` (one) or array of that ref
455
+ * (many); widget extras ride as sibling keys.
456
+ * 2. Vendor — `x-relation` namespace or flat `x-relation-target`;
457
+ * cardinality follows the node's `type` unless `multiple` overrides.
458
+ */
459
+ function readRelationConfig(schema) {
460
+ const record = schema;
461
+ const { multiple, ...extras } = readExtras(record);
462
+ const items = Array.isArray(schema.items) ? void 0 : schema.items;
463
+ const nestedItems = items && typeof items.properties === "object" ? { items } : {};
464
+ const singleTarget = refTarget(schema.$ref);
465
+ if (singleTarget) return {
466
+ target: singleTarget,
467
+ many: false,
468
+ ...extras
469
+ };
470
+ const itemTarget = refTarget(items?.$ref);
471
+ if (hasType(schema, "array") && itemTarget) return {
472
+ target: itemTarget,
473
+ many: true,
474
+ ...extras
475
+ };
476
+ const ns = record["x-relation"];
477
+ const target = ns && typeof ns === "object" ? str(ns.target) : str(record["x-relation-target"]);
478
+ if (!target) return void 0;
479
+ return {
480
+ target,
481
+ many: multiple ?? hasType(schema, "array"),
482
+ ...extras,
483
+ ...nestedItems
484
+ };
485
+ }
486
+ /**
487
+ * Maps a relation target to the party autocomplete's kind hint. The widget
488
+ * only knows `entity` / `contact` / undefined (= search both); join-table
489
+ * targets (`participant`, `party`) stay open.
490
+ */
491
+ function targetToKind(target) {
492
+ return target === "entity" || target === "contact" ? target : void 0;
493
+ }
494
+ /**
495
+ * The sibling field a contact picker is scoped to (`memberOfEntityId`).
496
+ *
497
+ * Explicit `x-relation-parent` always wins. When it is missing — the live
498
+ * artifact schema never carried the key — a contact relation whose traits
499
+ * include exactly one sibling entity relation's role is scoped to that
500
+ * sibling (LOS-860: titleOfficer traits `["title-company"]` → titleCompany).
501
+ * Two matches is ambiguous, so no parent is invented.
502
+ */
503
+ function relationParentFieldName(schema, path, relation) {
504
+ if (relation.parent) return relation.parent;
505
+ if (targetToKind(relation.target) !== "contact") return void 0;
506
+ const traits = relation.traits ?? [];
507
+ if (traits.length === 0) return void 0;
508
+ const siblings = propertiesAtParent(schema, path);
509
+ if (!siblings) return void 0;
510
+ const fieldName = path[path.length - 1];
511
+ if (typeof fieldName !== "string") return void 0;
512
+ const matches = [];
513
+ for (const [key, node] of Object.entries(siblings)) {
514
+ if (key === fieldName) continue;
515
+ const sibling = readRelationConfig(node);
516
+ if (!sibling || targetToKind(sibling.target) !== "entity") continue;
517
+ if (sibling.role && traits.includes(sibling.role)) matches.push(key);
518
+ }
519
+ return matches.length === 1 ? matches[0] : void 0;
520
+ }
521
+ function propertiesAtParent(schema, path) {
522
+ let node = schema;
523
+ for (const segment of path.slice(0, -1)) if (typeof segment === "number") {
524
+ const items = Array.isArray(node.items) ? void 0 : node.items;
525
+ if (!items) return void 0;
526
+ node = items;
527
+ } else {
528
+ const next = node.properties?.[segment];
529
+ if (!next) return void 0;
530
+ node = next;
531
+ }
532
+ return node.properties;
533
+ }
534
+ /**
535
+ * The row-identity keys a relation row carries beyond the stage-configured
536
+ * nested fields — the autocomplete result contracts (`PartySearchResult` /
537
+ * `AssetSearchResult`), which the ref readers/mergers in relation-widgets
538
+ * enumerate. The store walk applies the schema as a recursive allow-list,
539
+ * and a stage-injected item schema declares ONLY the nested tray keys — so
540
+ * these must be declared too or decoded rows lose their identity (empty
541
+ * autocompletes, no trays, and no `id` for the baseline's id-match array
542
+ * rebase). Declared as `hidden` controls: they walk and encode, never
543
+ * render.
544
+ */
545
+ const HIDDEN_STRING = {
546
+ type: "string",
547
+ "x-ui": { control: "hidden" }
548
+ };
549
+ const PARTY_IDENTITY_PROPS = {
550
+ id: HIDDEN_STRING,
551
+ pubId: HIDDEN_STRING,
552
+ name: HIDDEN_STRING,
553
+ kind: HIDDEN_STRING,
554
+ members: {
555
+ type: "array",
556
+ "x-ui": { control: "hidden" }
557
+ }
558
+ };
559
+ const ASSET_IDENTITY_PROPS = {
560
+ id: HIDDEN_STRING,
561
+ label: HIDDEN_STRING,
562
+ propertyType: HIDDEN_STRING,
563
+ subtitle: HIDDEN_STRING,
564
+ address1: HIDDEN_STRING,
565
+ address2: HIDDEN_STRING,
566
+ city: HIDDEN_STRING,
567
+ region: HIDDEN_STRING,
568
+ postalCode: HIDDEN_STRING,
569
+ country: HIDDEN_STRING,
570
+ formattedAddress: HIDDEN_STRING
571
+ };
572
+ /**
573
+ * The row-identity contract for a relation target. A relation whose item
574
+ * schema declares NO properties never reaches `withRelationRowIdentity` —
575
+ * its rows are a value leaf holding the whole object, with no allow-list to
576
+ * thin them — so a host feeding such a relation from a canonical record
577
+ * must project the rows against this same set itself.
578
+ */
579
+ function relationRowIdentityProps(target) {
580
+ return target === "asset" ? ASSET_IDENTITY_PROPS : PARTY_IDENTITY_PROPS;
581
+ }
582
+ /**
583
+ * Returns a schema whose multi-row relation item schemas additionally
584
+ * declare the row-identity keys (stage-configured keys win on collision).
585
+ * Apply to the STORE schema only — rendering iterates the configured keys
586
+ * and skips hidden controls.
587
+ */
588
+ function withRelationRowIdentity(schema) {
589
+ const properties = schema.properties ?? {};
590
+ let changed = false;
591
+ const nextProps = { ...properties };
592
+ for (const [key, property] of Object.entries(properties)) {
593
+ const relation = readRelationConfig(property);
594
+ if (!relation?.many || !relation.items?.properties) continue;
595
+ const identity = relationRowIdentityProps(relation.target);
596
+ nextProps[key] = {
597
+ ...property,
598
+ items: {
599
+ ...relation.items,
600
+ properties: {
601
+ ...identity,
602
+ ...relation.items.properties
603
+ }
604
+ }
605
+ };
606
+ changed = true;
607
+ }
608
+ return changed ? {
609
+ ...schema,
610
+ properties: nextProps
611
+ } : schema;
612
+ }
613
+
614
+ //#endregion
615
+ //#region src/plugins/derivation/row-scope.ts
616
+ /**
617
+ * Returns the INNERMOST array-item object store containing the field at the
618
+ * given path, or `undefined` when the path is not inside an array row (a
619
+ * root-level field, or a field under a plain nested object).
620
+ *
621
+ * Tolerant on purpose: an unresolvable segment stops the walk and returns
622
+ * whatever row was found so far, so a stale path (a row removed mid-render)
623
+ * degrades to a scope lookup instead of throwing.
624
+ *
625
+ * @param internalFormStore The form store.
626
+ * @param path The path to the field.
627
+ *
628
+ * @returns The row store, or `undefined`.
629
+ */
630
+ function findRowStore(internalFormStore, path) {
631
+ let current = internalFormStore;
632
+ let row;
633
+ for (const segment of path) if (current.kind === "object" && typeof segment === "string") {
634
+ const child = readOwn(current.children, segment);
635
+ if (!child) return row;
636
+ current = child;
637
+ } else if (current.kind === "array" && typeof segment === "number") {
638
+ const child = current.children[segment];
639
+ if (!child) return row;
640
+ current = child;
641
+ if (current.kind === "object") row = current;
642
+ } else return row;
643
+ return row;
644
+ }
645
+ /**
646
+ * Resolves a row's CANONICAL record: the server row from `offFormValues`
647
+ * that carries the full column set (core columns the write model never
648
+ * holds, plus the server's pre-evaluated derived values). The row's
649
+ * collection is read by walking the row's own path into `offFormValues`
650
+ * (`["assets", 0]` → `offFormValues.assets`), and the row is matched by
651
+ * `id` — index is not identity, rows reorder.
652
+ *
653
+ * Reads signals (`offFormValues`, the row's `id` input), so callers get
654
+ * re-resolution for free inside a computed.
655
+ *
656
+ * @param internalFormStore The form store.
657
+ * @param rowStore The array-item object store.
658
+ *
659
+ * @returns The canonical row, or `undefined`.
660
+ */
661
+ function canonicalRowOf(internalFormStore, rowStore) {
662
+ const path = rowStore.path;
663
+ if (path.length < 2) return void 0;
664
+ let collection = internalFormStore.offFormValues.value;
665
+ for (let index = 0; index < path.length - 1; index++) {
666
+ collection = readOwn(collection, path[index]);
667
+ if (collection == null) return void 0;
668
+ }
669
+ if (!Array.isArray(collection)) return void 0;
670
+ const idStore = readOwn(rowStore.children, "id");
671
+ const rowId = idStore ? getFieldInput(idStore) : void 0;
672
+ return collection.find((candidate) => candidate && typeof candidate === "object" && candidate.id === rowId);
673
+ }
674
+ /**
675
+ * Resolves a row-scope dependency once the LIVE row value is already in
676
+ * hand — the precedence a per-row formula evaluates in:
677
+ *
678
+ * 1. the root-record alias (`form.rootRecordAlias`) from `offFormValues` (it
679
+ * wins outright: a row column named like the alias is never the
680
+ * root record),
681
+ * 2. the live sibling value in the SAME row (an explicit `null` counts —
682
+ * only `undefined` means "the row does not hold this"),
683
+ * 3. the canonical row's column.
684
+ *
685
+ * A row scope deliberately does NOT see root-level form fields or other
686
+ * `offFormValues` keys: a row's formula is evaluated against its own record
687
+ * plus the root record, exactly as the server evaluates it.
688
+ *
689
+ * @param internalFormStore The form store.
690
+ * @param rowStore The array-item object store the formula lives in.
691
+ * @param key The dependency identifier.
692
+ * @param formValue The live value the row holds for `key`, or `undefined`.
693
+ *
694
+ * @returns The resolved value, or `undefined`.
695
+ */
696
+ function resolveRowFallback(internalFormStore, rowStore, key, formValue) {
697
+ if (key === internalFormStore.rootRecordAlias) {
698
+ const handle = readOwn(internalFormStore.offFormValues.value, key);
699
+ if (handle && typeof handle === "object") return handle;
700
+ }
701
+ if (formValue !== void 0) return formValue;
702
+ return readOwn(canonicalRowOf(internalFormStore, rowStore), key);
703
+ }
704
+ /**
705
+ * Resolves a single identifier in the scope of a row: the same precedence
706
+ * `buildDerivation` evaluates a row formula's dependencies in, with the
707
+ * live sibling read derived-aware (a sibling formula resolves through its
708
+ * own derived signal, so the read is always fresh; an erroring sibling
709
+ * resolves `undefined`, never a stale number).
710
+ *
711
+ * @param internalFormStore The form store.
712
+ * @param rowStore The array-item object store.
713
+ * @param key The identifier to resolve.
714
+ *
715
+ * @returns The resolved value, or `undefined`.
716
+ */
717
+ function resolveRowScopeValue(internalFormStore, rowStore, key) {
718
+ const child = readOwn(rowStore.children, key);
719
+ let formValue;
720
+ if (child) {
721
+ const slot = child.kind === "value" ? derivationKey.get(internalFormStore, child) : void 0;
722
+ if (slot) {
723
+ const state = slot.derived.value;
724
+ formValue = state.error === null ? state.value : void 0;
725
+ } else formValue = getFieldInput(child);
726
+ }
727
+ return resolveRowFallback(internalFormStore, rowStore, key, formValue);
728
+ }
729
+
730
+ //#endregion
731
+ //#region src/plugins/derivation/resolve-scope-value.ts
732
+ /**
733
+ * Resolves a single scalar key through the canonical scope precedence —
734
+ * the same order the derivation plugin evaluates formula dependencies in:
735
+ * the form value wins (a formula field resolves through its derived
736
+ * signal, so the read is always fresh; an erroring formula resolves
737
+ * `undefined`, never a stale number), `offFormValues` fills what the form
738
+ * does not hold (an off-stage field still resolves from the canonical record).
739
+ *
740
+ * For widget-side scalar reads (e.g. an amount-or-percent field resolving
741
+ * its percent basis). Collection overlay semantics stay inside the
742
+ * derivation plugin — this helper is for scalars.
743
+ *
744
+ * @param internalFormStore The form store.
745
+ * @param key The root-level field key to resolve.
746
+ *
747
+ * @returns The resolved value, or `undefined`.
748
+ */
749
+ function resolveScopeValue(internalFormStore, key) {
750
+ const child = readOwn(internalFormStore.children, key);
751
+ let formValue;
752
+ if (child) {
753
+ const slot = child.kind === "value" ? derivationKey.get(internalFormStore, child) : void 0;
754
+ if (slot) {
755
+ const state = slot.derived.value;
756
+ formValue = state.error === null ? state.value : void 0;
757
+ } else formValue = getFieldInput(child);
758
+ }
759
+ return formValue === void 0 ? readOwn(internalFormStore.offFormValues.value, key) : formValue;
760
+ }
761
+ /**
762
+ * Resolves a single scalar key in the scope of the field at `path` — the
763
+ * path-aware sibling of `resolveScopeValue`, and the read a widget owned by
764
+ * a field should use:
765
+ *
766
+ * - inside an array row, the ROW's scope (live siblings in the same row →
767
+ * the canonical row from `offFormValues` matched by `id` → the parent
768
+ * root record under the form's `rootRecordAlias`), so a per-row formula's inputs are ITS
769
+ * row's values;
770
+ * - anywhere else, the document scope (`resolveScopeValue`).
771
+ *
772
+ * The same precedence the derivation graph evaluates the field's own
773
+ * formula in, so a widget can never display an input the value was not
774
+ * computed from.
775
+ *
776
+ * @param internalFormStore The form store.
777
+ * @param path The path of the field whose scope to resolve in.
778
+ * @param key The identifier to resolve.
779
+ *
780
+ * @returns The resolved value, or `undefined`.
781
+ */
782
+ function resolveScopeValueAt(internalFormStore, path, key) {
783
+ const rowStore = findRowStore(internalFormStore, path);
784
+ return rowStore ? resolveRowScopeValue(internalFormStore, rowStore, key) : resolveScopeValue(internalFormStore, key);
785
+ }
786
+
787
+ //#endregion
788
+ //#region src/core/visibility/resolve-conditionals.ts
789
+ /**
790
+ * Resolves the root schema's `allOf` `if/then/else` blocks into one
791
+ * `VisibleWhen` per gated field key (first-match-wins per key).
792
+ *
793
+ * Supported condition shapes (the only ones in seed + prod schemas):
794
+ * - `{ const: x }` → equals x
795
+ * - `{ enum: [x, y] }` → one-of [x, y] (LOS-822 multi-value)
796
+ * - `{ contains: { const: x } }` → contains x (array-valued watchers)
797
+ *
798
+ * A `then` branch keeps the positive op; an `else` branch flips it
799
+ * (`equals` → `not-equals`, `contains` → `not-contains`). Compound
800
+ * conditions (AND across watched fields) are out of scope — the first
801
+ * watched property of an `if` block decides.
802
+ *
803
+ * @param schema The root form schema.
804
+ *
805
+ * @returns The gated field keys mapped to their visibility rule.
806
+ */
807
+ function resolveConditionals(schema) {
808
+ const out = {};
809
+ const allOf = schema.allOf;
810
+ if (!Array.isArray(allOf)) return out;
811
+ for (const blockRaw of allOf) {
812
+ if (!blockRaw || typeof blockRaw !== "object") continue;
813
+ const block = blockRaw;
814
+ const condition = parseIfCondition(block.if);
815
+ if (!condition) continue;
816
+ collectBranch(out, block.then, condition.positive);
817
+ collectBranch(out, block.else, condition.negative);
818
+ }
819
+ return out;
820
+ }
821
+ /**
822
+ * Parses an `if` block's first watched property into the branch rules.
823
+ */
824
+ function parseIfCondition(ifBlock) {
825
+ if (!ifBlock || typeof ifBlock !== "object") return null;
826
+ const props = ifBlock.properties;
827
+ if (!props || typeof props !== "object") return null;
828
+ const entries = Object.entries(props);
829
+ if (entries.length === 0) return null;
830
+ const [field, condRaw] = entries[0];
831
+ if (!condRaw || typeof condRaw !== "object") return null;
832
+ const cond = condRaw;
833
+ if ("const" in cond) return {
834
+ positive: {
835
+ field,
836
+ op: "equals",
837
+ value: cond.const
838
+ },
839
+ negative: {
840
+ field,
841
+ op: "not-equals",
842
+ value: cond.const
843
+ }
844
+ };
845
+ if (Array.isArray(cond.enum)) return {
846
+ positive: {
847
+ field,
848
+ op: "one-of",
849
+ value: cond.enum
850
+ },
851
+ negative: {
852
+ field,
853
+ op: "not-one-of",
854
+ value: cond.enum
855
+ }
856
+ };
857
+ if ("contains" in cond && cond.contains && typeof cond.contains === "object") {
858
+ const containsConst = cond.contains.const;
859
+ if (containsConst !== void 0) return {
860
+ positive: {
861
+ field,
862
+ op: "contains",
863
+ value: containsConst
864
+ },
865
+ negative: {
866
+ field,
867
+ op: "not-contains",
868
+ value: containsConst
869
+ }
870
+ };
871
+ }
872
+ return null;
873
+ }
874
+ /**
875
+ * Records the branch's gated property keys under the given rule.
876
+ */
877
+ function collectBranch(out, branch, visibleWhen) {
878
+ if (!branch || typeof branch !== "object") return;
879
+ const props = branch.properties;
880
+ if (!props || typeof props !== "object") return;
881
+ for (const key of Object.keys(props)) {
882
+ if (!isSafeKey(key) || readOwn(out, key) !== void 0) continue;
883
+ out[key] = visibleWhen;
884
+ }
885
+ }
886
+
887
+ //#endregion
888
+ //#region src/plugins/visibility/plugin.ts
889
+ /**
890
+ * The visibility plugin's key. It keeps no state (rules and computeds live
891
+ * on the field stores' base `visibleWhen`/`visible` members).
892
+ */
893
+ const visibilityKey = new PluginKey("visibility");
894
+ /**
895
+ * The visibility plugin: every field gated by an `allOf` `if/then/else`
896
+ * block on its OWN scope's schema gets a `visible` computed signal over the
897
+ * watched field's resolved value. Two scope kinds (LOS-722 extended
898
+ * visibility into rows — the spec's open question 3):
899
+ *
900
+ * - the root scope, whose rules come from the form schema's `allOf`;
901
+ * - an array-row scope, whose rules come from the ITEMS schema's `allOf`
902
+ * (`buildNestedFieldItems` carries the related dataset's conditionals),
903
+ * evaluated against THAT row's values — row A's trigger never gates
904
+ * row B's field.
905
+ *
906
+ * Visibility gates RENDERING only: a hidden field keeps its state, stays
907
+ * in the dirty diff, and rides the payload. Fields without a rule get no
908
+ * signal — the public store reads that as always visible.
909
+ *
910
+ * Register AFTER derivation (array order): a WHEN watching a formula field
911
+ * resolves through its derived slot. No hard `dependsOn` — a form without
912
+ * a calc engine legitimately omits derivation, and the watched-value read
913
+ * falls back to the field's input.
914
+ */
915
+ function visibility() {
916
+ return {
917
+ name: "visibility",
918
+ key: visibilityKey,
919
+ build: () => null,
920
+ buildScope(ctx, scope) {
921
+ const form = ctx.form;
922
+ const isRoot = scope === form;
923
+ const rules = resolveConditionals(scope.schema);
924
+ for (const [key, visibleWhen] of Object.entries(rules)) {
925
+ const child = readOwn(scope.children, key);
926
+ if (!child || typeof child !== "object") continue;
927
+ const store = scope.children[key];
928
+ store.visibleWhen = visibleWhen;
929
+ store.visible = isRoot ? computed(() => evaluateVisibleWhen(form, visibleWhen)) : computed(() => evaluateRowVisibleWhen(form, scope, visibleWhen));
930
+ }
931
+ }
932
+ };
933
+ }
934
+ /**
935
+ * Evaluates a visibility rule against the canonical scope: the form value
936
+ * wins, `offFormValues` fills what the form does not hold — so a WHEN can
937
+ * watch an off-stage record field, and (via the `record[key]` bracket form) a
938
+ * root-record alias field from an asset schema (LOS-471).
939
+ */
940
+ function evaluateVisibleWhen(internalFormStore, visibleWhen) {
941
+ return compareVisibleWhen(resolveWhenRef(internalFormStore, visibleWhen.field), visibleWhen);
942
+ }
943
+ /**
944
+ * Evaluates a ROW field's visibility rule in its row's scope: the watched
945
+ * value resolves through `resolveRowScopeValue` (live row sibling,
946
+ * derived-aware → canonical row from `offFormValues`), so toggling row A's
947
+ * trigger flips row A's gated field and no other row's. The bracket form
948
+ * resolves its base through the same row precedence — `record[key]` reads off
949
+ * the root-record alias exactly as it does at root (LOS-819).
950
+ */
951
+ function evaluateRowVisibleWhen(internalFormStore, rowScope, visibleWhen) {
952
+ return compareVisibleWhen(resolveWhenRefWith((key) => resolveRowScopeValue(internalFormStore, rowScope, key), visibleWhen.field), visibleWhen);
953
+ }
954
+ function compareVisibleWhen(actual, visibleWhen) {
955
+ switch (visibleWhen.op) {
956
+ case "equals": return actual === visibleWhen.value;
957
+ case "not-equals": return actual !== visibleWhen.value;
958
+ case "one-of": return Array.isArray(visibleWhen.value) && visibleWhen.value.includes(actual);
959
+ case "not-one-of": return !Array.isArray(visibleWhen.value) || !visibleWhen.value.includes(actual);
960
+ case "contains": return Array.isArray(actual) && actual.includes(visibleWhen.value);
961
+ case "not-contains": return !Array.isArray(actual) || !actual.includes(visibleWhen.value);
962
+ }
963
+ }
964
+ /**
965
+ * Resolves a watched-field reference at ROOT scope via the shared parser.
966
+ */
967
+ function resolveWhenRef(internalFormStore, ref) {
968
+ return resolveWhenRefWith((key) => resolveScopeValue(internalFormStore, key), ref);
969
+ }
970
+ /**
971
+ * Parses a watched-field reference over a scope's own key resolver. Two
972
+ * shapes, mirroring the formula grammar: a plain key resolves through the
973
+ * scope precedence; the bracket form `record[key]` reads `key` off the OBJECT
974
+ * the scope resolves under the form's `rootRecordAlias`. An absent or
975
+ * non-object handle resolves `undefined`, so an equals-gated field simply
976
+ * stays hidden — e.g. in a host with no parent record in scope.
977
+ */
978
+ function resolveWhenRefWith(resolve, ref) {
979
+ const open = ref.indexOf("[");
980
+ if (open > 0 && ref.endsWith("]")) {
981
+ const base = resolve(ref.slice(0, open));
982
+ if (base && typeof base === "object" && !Array.isArray(base)) return readOwn(base, ref.slice(open + 1, -1));
983
+ return;
984
+ }
985
+ return resolve(ref);
986
+ }
987
+
988
+ //#endregion
989
+ //#region src/plugins/bagger/compute-scope.ts
990
+ /**
991
+ * canon is the record the domain's canonical loader returns; bagger
992
+ * distributes it: schema-declared keys become form state through the
993
+ * store's allow-list, the whole record becomes the off-form value bag.
994
+ *
995
+ * This module computes that off-form bag ("the shelf") as a pure function
996
+ * of a schema and a canonical record — no store, no signals, so a later
997
+ * task can call it identically from a plugin's `build` and from any
998
+ * engine-less reader.
999
+ */
1000
+ const DEFAULT_BAG = "data";
1001
+ function isPlainObject(value) {
1002
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1003
+ }
1004
+ /**
1005
+ * The record keys that hold relation ROWS, from both classification
1006
+ * sources. The union is load-bearing in both directions:
1007
+ *
1008
+ * - `collectionsSchema` (the dataset schema) alone decides collections the
1009
+ * walked schema does not configure. A stage that renders no assets tray
1010
+ * still needs `assets` on the shelf as FLATTENED rows — left raw, every
1011
+ * bag-resident key (`assignmentFee`) reads undefined per row and
1012
+ * `SUM(assets[assignmentFee])` or a check silently computes 0.
1013
+ * - The walked schema alone declares relations the dataset schema lacks:
1014
+ * registry-role slugs and fallback defs are synthesized per stage
1015
+ * (`buildStageSchemaFromConfig`), so replacing rather than union-ing
1016
+ * would drop those collections off the shelf.
1017
+ */
1018
+ function collectionKeys(schema, collectionsSchema) {
1019
+ const keys = /* @__PURE__ */ new Set();
1020
+ for (const properties of [schema.properties, collectionsSchema?.properties]) for (const [key, node] of Object.entries(properties ?? {})) if (readRelationConfig(node)?.many) keys.add(key);
1021
+ return keys;
1022
+ }
1023
+ /**
1024
+ * Merges a source row's `x-column` fields with its bag column — bag wins,
1025
+ * per the `x-column` save-routing authority (AI.md: read = row spread over
1026
+ * its `data`). INTACT merge only, no envelope unwrapping: a later task
1027
+ * seeds form rows from this output, and unwrapping here would hand the
1028
+ * store already-flattened envelope values, reproducing the LOS-461
1029
+ * regression.
1030
+ */
1031
+ function flattenSourceRow(row, bag) {
1032
+ const bagValue = row[bag];
1033
+ return isPlainObject(bagValue) ? {
1034
+ ...row,
1035
+ ...bagValue
1036
+ } : { ...row };
1037
+ }
1038
+ /**
1039
+ * Unwraps every kind-envelope entry (estimate / amount-or-percent) in a
1040
+ * flattened row to its value half, in place on a shallow copy. Shelf-only:
1041
+ * the off-form bag is read-only display data, so it carries resolved
1042
+ * values, never the envelope's mode/meta half.
1043
+ */
1044
+ function unwrapEnvelopes(flat) {
1045
+ const unwrapped = { ...flat };
1046
+ for (const key of Object.keys(unwrapped)) {
1047
+ const raw = unwrapped[key];
1048
+ if (isEnvelope(raw)) unwrapped[key] = envelopesWire.unwrap(raw).value;
1049
+ }
1050
+ return unwrapped;
1051
+ }
1052
+ /**
1053
+ * Computes the off-form value bag for a canonical record: every declared
1054
+ * scalar and relation collection, bag-merged and envelope-unwrapped, plus
1055
+ * an alias entry holding the whole computed scope (so a formula or
1056
+ * widget can address `record.termMonths` as readily as bare `termMonths`).
1057
+ */
1058
+ function computeBag(schema, record, opts) {
1059
+ const bag = opts?.bag ?? DEFAULT_BAG;
1060
+ const alias = opts?.rootRecordAlias ?? DEFAULT_ROOT_RECORD_ALIAS;
1061
+ const scope = unwrapEnvelopes(flattenSourceRow(record, bag));
1062
+ for (const key of collectionKeys(schema, opts?.collectionsSchema)) {
1063
+ const source = record[key];
1064
+ if (isPlainObject(source)) continue;
1065
+ scope[key] = Array.isArray(source) ? source.map((row) => unwrapEnvelopes(flattenSourceRow(row, bag))) : [];
1066
+ }
1067
+ scope[alias] = { ...scope };
1068
+ return scope;
1069
+ }
1070
+
1071
+ //#endregion
1072
+ //#region src/plugins/bagger/plugin.ts
1073
+ /**
1074
+ * canon is the record the domain's canonical loader returns; bagger
1075
+ * distributes it: schema-declared keys become form state through the
1076
+ * store's allow-list, the whole record becomes the off-form value bag.
1077
+ *
1078
+ * Two constraints:
1079
+ * - The shelf (`offFormValues`) unwraps kind envelopes (`computeBag`); the
1080
+ * form side never does — row seeding reads a SECOND, intact row index
1081
+ * built here from `flattenSourceRow`, so a seeded estimate keeps its
1082
+ * `{ kind, value, mode }` envelope instead of collapsing to a bare
1083
+ * number (LOS-461).
1084
+ * - `build` runs BEFORE the schema walk, so it may read only
1085
+ * `config.schema` — `form.schema` (set by `initializeFieldStore`) is
1086
+ * still undefined there. The shelf is written from the ROOT `buildScope`
1087
+ * call instead, the one write point after the walk (dispatched in plugin
1088
+ * array order — `create-form-store.ts`).
1089
+ *
1090
+ * A seeded envelope leaf (estimate / amount-or-percent) must never fork
1091
+ * its two halves: the envelopes plugin already built this leaf's slot
1092
+ * (empty) from the row's OWN raw before bagger runs in the same
1093
+ * `buildScope` dispatch, so seeding writes through the SAME decode
1094
+ * (`decodeEstimateEnvelope`/`decodeAmountOrPercentEnvelope`) and the SAME sole
1095
+ * writer (`writeEnvelope`) the walk itself uses — never a hand-built
1096
+ * envelope object landing straight in `input`.
1097
+ */
1098
+ const baggerKey = new PluginKey("bagger");
1099
+ /**
1100
+ * Builds the id-keyed row index for one many:true relation field, from the
1101
+ * INTACT (non-unwrapped) flatten — the seeding source, never `computeBag`'s
1102
+ * unwrapped rows.
1103
+ */
1104
+ function indexRows(rows, bag) {
1105
+ const index = /* @__PURE__ */ new Map();
1106
+ if (!Array.isArray(rows)) return index;
1107
+ for (const row of rows) {
1108
+ if (typeof row !== "object" || row === null) continue;
1109
+ const flat = flattenSourceRow(row, bag);
1110
+ const id = flat.id;
1111
+ if (typeof id === "string") index.set(id, flat);
1112
+ }
1113
+ return index;
1114
+ }
1115
+ /**
1116
+ * Seeds one declared leaf with its canonical value as BASELINE: `input`,
1117
+ * `startInput`, AND `initialInput` — `reset()` restores a field from
1118
+ * `initialInput` (`methods/reset.ts`), so a seed that skipped it would
1119
+ * survive until the first reset and then blank out, the legacy-hydration
1120
+ * parity `reset` otherwise breaks (a legacy record merged canonical
1121
+ * values into the row BEFORE decode, so every channel already carried
1122
+ * them; seeding after the walk must land on the same three channels by
1123
+ * hand).
1124
+ *
1125
+ * An envelope-control leaf (estimate / amount-or-percent) does NOT get
1126
+ * its raw envelope object written into `input` — that would fork the
1127
+ * value half (a wrapped object landing in a number field) from the
1128
+ * envelopes plugin's own slot, which the walk already built empty from
1129
+ * this row's OWN raw earlier in the same `buildScope` dispatch. Instead
1130
+ * this decodes through the plugin's own functions
1131
+ * (`decodeEstimateEnvelope`/`decodeAmountOrPercentEnvelope`) and writes through its
1132
+ * sole writer (`writeEnvelope`), plus reassigns `slot.startEnvelope` —
1133
+ * the same field `rebase`'s `bindAdopted` reassigns when adopting a new
1134
+ * baseline (`envelope.ts`) — so the meta half (mode, manualValue) seeds
1135
+ * alongside the value half instead of staying at its empty default.
1136
+ */
1137
+ function seedLeaf(ctx, child, rawValue) {
1138
+ const slot = envelopesKey.get(ctx.form, child);
1139
+ if (!slot) {
1140
+ child.input.value = rawValue;
1141
+ child.startInput.value = rawValue;
1142
+ child.initialInput.value = rawValue;
1143
+ return;
1144
+ }
1145
+ if (slot.family === "estimate") {
1146
+ const decoded = decodeEstimateEnvelope(ctx.form, child, rawValue);
1147
+ slot.startEnvelope.value = decoded;
1148
+ child.startInput.value = decoded.value;
1149
+ child.initialInput.value = decoded.value;
1150
+ writeEnvelope(ctx.form, child, slot, decoded);
1151
+ } else {
1152
+ const decoded = decodeAmountOrPercentEnvelope(ctx.form, child, rawValue);
1153
+ slot.startEnvelope.value = decoded;
1154
+ child.startInput.value = decoded.value;
1155
+ child.initialInput.value = decoded.value;
1156
+ writeEnvelope(ctx.form, child, slot, decoded);
1157
+ }
1158
+ }
1159
+ /**
1160
+ * Seeds one row scope's declared value leaves from its canonical source
1161
+ * row: a leaf with no live input yet (`input.value === undefined`) takes
1162
+ * the source row's value as baseline (`seedLeaf`). A row already holding
1163
+ * a value, or with no canonical match, is left untouched.
1164
+ */
1165
+ function seedRow(ctx, scope, raw) {
1166
+ const fieldKey = String(scope.path[0]);
1167
+ const rowId = raw?.id ?? "";
1168
+ const sourceRow = ctx.state.rowsByField.get(fieldKey)?.get(rowId);
1169
+ if (!sourceRow) return;
1170
+ for (const [key, child] of Object.entries(scope.children)) {
1171
+ if (child.kind !== "value") continue;
1172
+ if (child.input.value !== void 0) continue;
1173
+ const rawValue = sourceRow[key];
1174
+ if (rawValue === void 0) continue;
1175
+ seedLeaf(ctx, child, rawValue);
1176
+ }
1177
+ }
1178
+ /**
1179
+ * The bagger plugin: computes the off-form shelf once (`computeBag`) and
1180
+ * owns `offFormValues`, then seeds every relation row's declared keys from
1181
+ * its canonical row as an unedited baseline. Registration order:
1182
+ * `[envelopes(), bagger(record), derivation?, visibility()]` — envelopes
1183
+ * first so its slot already exists on every estimate/amount-or-percent leaf by the
1184
+ * time seeding runs (`dependsOn: [envelopesKey]` enforces this at
1185
+ * `createFormStore`).
1186
+ */
1187
+ function bagger(record, opts) {
1188
+ const bag = opts?.bag ?? "data";
1189
+ return {
1190
+ name: "bagger",
1191
+ key: baggerKey,
1192
+ dependsOn: [envelopesKey],
1193
+ build(form, config) {
1194
+ const schema = config.schema;
1195
+ const scope = computeBag(schema, record, {
1196
+ ...opts,
1197
+ rootRecordAlias: form.rootRecordAlias
1198
+ });
1199
+ const rowsByField = /* @__PURE__ */ new Map();
1200
+ for (const key of collectionKeys(schema, opts?.collectionsSchema)) rowsByField.set(key, indexRows(record[key], bag));
1201
+ return {
1202
+ scope,
1203
+ rowsByField
1204
+ };
1205
+ },
1206
+ buildScope(ctx, scope, raw) {
1207
+ if (scope === ctx.form) {
1208
+ ctx.form.offFormValues.value = ctx.state.scope;
1209
+ return;
1210
+ }
1211
+ seedRow(ctx, scope, raw);
1212
+ },
1213
+ reseedScope(ctx, scope, raw) {
1214
+ seedRow(ctx, scope, raw);
1215
+ }
1216
+ };
1217
+ }
1218
+
1219
+ //#endregion
1220
+ //#region src/plugins/checks/interpolate.ts
1221
+ /**
1222
+ * Eslint-style `{{ placeholder }}` interpolation (ANALYSIS-eslint.md §1.5).
1223
+ * An unsupplied placeholder is left literally in place rather than blanked.
1224
+ */
1225
+ function interpolate(text, data) {
1226
+ return text.replace(/\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g, (full, key) => {
1227
+ if (!data || !Object.prototype.hasOwnProperty.call(data, key)) return full;
1228
+ return String(data[key]);
1229
+ });
1230
+ }
1231
+
1232
+ //#endregion
1233
+ //#region src/plugins/checks/key.ts
1234
+ const checksKey = new PluginKey("checks");
1235
+
1236
+ //#endregion
1237
+ //#region src/plugins/checks/formula.ts
1238
+ /** Shared messageId for parse/eval failures (plugin catch + host mapping). */
1239
+ const UNEVALUABLE_MESSAGE_ID = "unevaluable";
1240
+ /**
1241
+ * The one built-in check definition: a `bespoke.check` row is an *instance*
1242
+ * of this, not a definition of its own (ANALYSIS-eslint.md §2.6).
1243
+ */
1244
+ function formulaCheck(engine) {
1245
+ return {
1246
+ meta: {
1247
+ name: "formula",
1248
+ messages: {
1249
+ failed: "{{message}}",
1250
+ [UNEVALUABLE_MESSAGE_ID]: "{{error}}"
1251
+ },
1252
+ defaultOptions: {
1253
+ formula: "",
1254
+ message: "Failed.",
1255
+ targetFieldKeys: []
1256
+ },
1257
+ optionsSchema: {
1258
+ type: "object",
1259
+ required: ["formula"],
1260
+ properties: {
1261
+ formula: { type: "string" },
1262
+ message: { type: "string" },
1263
+ name: { type: "string" },
1264
+ targetFieldKeys: {
1265
+ type: "array",
1266
+ items: { type: "string" }
1267
+ }
1268
+ }
1269
+ }
1270
+ },
1271
+ create(context) {
1272
+ const formula = context.options.formula;
1273
+ const targetPaths = (context.options.targetFieldKeys ?? []).map((key) => [key]);
1274
+ const parsed = engine.parse(formula);
1275
+ const deps = parsed.ok ? engine.extractDependencies(parsed.node) : [];
1276
+ const pathRefs = parsed.ok ? engine.extractPathRefs?.(parsed.node) ?? [] : [];
1277
+ const collections = new Set(pathRefs.map((ref) => ref.collection));
1278
+ return { evaluate() {
1279
+ if (!parsed.ok) {
1280
+ context.report({
1281
+ messageId: UNEVALUABLE_MESSAGE_ID,
1282
+ data: { error: parsed.error },
1283
+ paths: targetPaths
1284
+ });
1285
+ return;
1286
+ }
1287
+ const scope = buildEvalScope(context.scope, deps, collections);
1288
+ let out;
1289
+ try {
1290
+ out = engine.evaluate(parsed.node, scope);
1291
+ } catch (err) {
1292
+ context.report({
1293
+ messageId: UNEVALUABLE_MESSAGE_ID,
1294
+ data: { error: err instanceof Error ? err.message : String(err) },
1295
+ paths: targetPaths
1296
+ });
1297
+ return;
1298
+ }
1299
+ if (out === null || out === void 0) return;
1300
+ if (out) return;
1301
+ context.report({
1302
+ messageId: "failed",
1303
+ data: { message: context.options.message ?? "Failed." },
1304
+ paths: targetPaths
1305
+ });
1306
+ } };
1307
+ }
1308
+ };
1309
+ }
1310
+ /**
1311
+ * Eval bag for one formula run. Every dep goes through `scope.get` — pathRef
1312
+ * names are NOT forced through `rows()` (a `record[…]` root-record ref is a plain
1313
+ * object; rows would collapse it to `[]`). Collections absent from deps
1314
+ * still land via rows() for aggregate-only refs.
1315
+ */
1316
+ function buildEvalScope(scope, deps, collections) {
1317
+ const bag = {};
1318
+ for (const dep of deps) bag[dep] = scope.get(dep);
1319
+ for (const collection of collections) if (!(collection in bag)) bag[collection] = scope.rows(collection);
1320
+ return bag;
1321
+ }
1322
+
1323
+ //#endregion
1324
+ //#region src/plugins/checks/plugin.ts
1325
+ const SEVERITY_ORDER = {
1326
+ error: 0,
1327
+ warning: 1,
1328
+ info: 2
1329
+ };
1330
+ /**
1331
+ * Checks plugin (LOS-605 / spec D8): eslint's contract on a form store.
1332
+ * Findings are a parallel channel — they never merge into `errors`.
1333
+ *
1334
+ * Register after derivation so a check that reads a formula field resolves
1335
+ * through its derived slot. Array order, not a hard `dependsOn` — a form
1336
+ * without a calc engine legitimately omits derivation.
1337
+ *
1338
+ * Always register (even with zero instances). Live instance edits go through
1339
+ * `replaceCheckInstances` so the host does not remount the form.
1340
+ */
1341
+ function checks(config) {
1342
+ return {
1343
+ name: "checks",
1344
+ key: checksKey,
1345
+ build(form) {
1346
+ const collections = createSignal(config.collections ?? Object.freeze({}));
1347
+ const instances = createSignal(instanceComputeds(config, freezeScope(form, collections)));
1348
+ const findings = computed(() => {
1349
+ const all = [];
1350
+ for (const slot of instances.value) all.push(...slot.value);
1351
+ return [...all].sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity] || a.id.localeCompare(b.id));
1352
+ });
1353
+ return {
1354
+ findings,
1355
+ hasBlockingFinding: computed(() => findings.value.some((finding) => finding.severity === "error")),
1356
+ instances,
1357
+ collections
1358
+ };
1359
+ }
1360
+ };
1361
+ }
1362
+ /**
1363
+ * Re-registers check instances on a live form store (admin edits, late
1364
+ * prop arrival) without remounting. Swaps the per-instance computeds the
1365
+ * `findings` signal already reads.
1366
+ */
1367
+ function replaceCheckInstances(form, config) {
1368
+ const internal = internalOf(form);
1369
+ const state = checksKey.getState(internal);
1370
+ if (!state) throw new Error("replaceCheckInstances: checks plugin is not registered");
1371
+ state.collections.value = config.collections ?? Object.freeze({});
1372
+ const scope = freezeScope(internal, state.collections);
1373
+ state.instances.value = instanceComputeds(config, scope);
1374
+ }
1375
+ function instanceComputeds(config, scope) {
1376
+ return registerInstances(config, scope).map((runtime) => computed(() => evaluateInstance(runtime)));
1377
+ }
1378
+ function registerInstances(config, scope) {
1379
+ const runtimes = [];
1380
+ for (const instance of config.instances) {
1381
+ if (instance.severity === "off") continue;
1382
+ const definition = config.definitions[instance.check];
1383
+ if (!definition) throw new Error(`Check instance "${instance.id}" (${instance.check}): unknown definition`);
1384
+ const options = mergeAndValidateOptions(instance, definition);
1385
+ const reports = [];
1386
+ const context = {
1387
+ id: instance.id,
1388
+ checkId: instance.check,
1389
+ options,
1390
+ scope,
1391
+ report(finding) {
1392
+ reports.push(finding);
1393
+ }
1394
+ };
1395
+ const handlers = definition.create(context);
1396
+ runtimes.push({
1397
+ id: instance.id,
1398
+ checkId: instance.check,
1399
+ severity: instance.severity,
1400
+ definition,
1401
+ options,
1402
+ evaluate: () => handlers.evaluate(),
1403
+ takeReports: () => {
1404
+ const batch = reports.slice();
1405
+ reports.length = 0;
1406
+ return batch;
1407
+ }
1408
+ });
1409
+ }
1410
+ return runtimes;
1411
+ }
1412
+ function mergeAndValidateOptions(instance, definition) {
1413
+ const defaults = definition.meta.defaultOptions && typeof definition.meta.defaultOptions === "object" ? definition.meta.defaultOptions : {};
1414
+ const provided = instance.options && typeof instance.options === "object" ? instance.options : {};
1415
+ const merged = {
1416
+ ...defaults,
1417
+ ...provided
1418
+ };
1419
+ const schema = definition.meta.optionsSchema;
1420
+ if (schema === false) return Object.freeze(merged);
1421
+ if (schema === void 0) {
1422
+ if (Object.keys(provided).length > 0) throw new Error(`Check instance "${instance.id}" (${instance.check}): this check takes no options`);
1423
+ return Object.freeze(merged);
1424
+ }
1425
+ const required = Array.isArray(schema.required) ? schema.required : [];
1426
+ for (const key of required) {
1427
+ const value = merged[key];
1428
+ if (value === void 0 || value === null) throw new Error(`Check instance "${instance.id}" (${instance.check}): missing option "${key}"`);
1429
+ }
1430
+ const properties = schema.properties && typeof schema.properties === "object" ? schema.properties : {};
1431
+ for (const [key, propSchema] of Object.entries(properties)) {
1432
+ if (!Object.prototype.hasOwnProperty.call(merged, key)) continue;
1433
+ const value = merged[key];
1434
+ if (value === void 0) continue;
1435
+ validateOptionType(instance, key, value, propSchema);
1436
+ }
1437
+ return Object.freeze(merged);
1438
+ }
1439
+ function validateOptionType(instance, key, value, propSchema) {
1440
+ const named = `Check instance "${instance.id}" (${instance.check})`;
1441
+ if (propSchema.type === "string") {
1442
+ if (typeof value !== "string") throw new Error(`${named}: option "${key}" must be a string`);
1443
+ return;
1444
+ }
1445
+ if (propSchema.type === "array") {
1446
+ if (!Array.isArray(value)) throw new Error(`${named}: option "${key}" must be an array`);
1447
+ if (propSchema.items?.type === "string") {
1448
+ for (const entry of value) if (typeof entry !== "string") throw new Error(`${named}: option "${key}" must be an array of strings`);
1449
+ }
1450
+ }
1451
+ }
1452
+ /**
1453
+ * Live form / derived value only — offForm fill belongs to the derivation
1454
+ * closure in `get`, not here.
1455
+ */
1456
+ function resolveLiveFormValue(form, key) {
1457
+ const child = readOwn(form.children, key);
1458
+ if (!child) return void 0;
1459
+ const slot = child.kind === "value" ? derivationKey.get(form, child) : void 0;
1460
+ if (slot) {
1461
+ const state = slot.derived.value;
1462
+ return state.error === null ? state.value : void 0;
1463
+ }
1464
+ return getFieldInput(child);
1465
+ }
1466
+ function freezeScope(form, collections) {
1467
+ const get = (key) => {
1468
+ const formValue = resolveLiveFormValue(form, key);
1469
+ const offValue = readOwn(form.offFormValues.value, key);
1470
+ const fromCollections = readOwn(collections.value, key);
1471
+ const live = formValue === void 0 ? offValue : formValue;
1472
+ if (Array.isArray(fromCollections) && Array.isArray(live)) return mergeCollectionRows(fromCollections, live);
1473
+ if (Array.isArray(offValue) && Array.isArray(formValue)) return mergeCollectionRows(offValue, formValue);
1474
+ return live;
1475
+ };
1476
+ const scope = {
1477
+ get,
1478
+ rows(collection) {
1479
+ const value = get(collection);
1480
+ return Array.isArray(value) ? value : [];
1481
+ },
1482
+ values() {
1483
+ const bag = { ...form.offFormValues.value };
1484
+ for (const key of Object.keys(form.children)) bag[key] = get(key);
1485
+ for (const key of Object.keys(collections.value)) if (!(key in bag)) bag[key] = get(key);
1486
+ return bag;
1487
+ }
1488
+ };
1489
+ return Object.freeze(scope);
1490
+ }
1491
+ function evaluateInstance(runtime) {
1492
+ try {
1493
+ runtime.evaluate();
1494
+ } catch (err) {
1495
+ runtime.takeReports();
1496
+ const targets = runtime.options.targetFieldKeys ?? [];
1497
+ return findingsFromDescriptor(runtime, {
1498
+ messageId: UNEVALUABLE_MESSAGE_ID,
1499
+ data: { error: err instanceof Error ? err.message : String(err) },
1500
+ paths: targets.map((key) => [key])
1501
+ });
1502
+ }
1503
+ const reported = runtime.takeReports();
1504
+ const findings = [];
1505
+ for (const descriptor of reported) findings.push(...findingsFromDescriptor(runtime, descriptor));
1506
+ return findings;
1507
+ }
1508
+ function findingsFromDescriptor(runtime, descriptor) {
1509
+ const message = resolveMessage(runtime, descriptor);
1510
+ const paths = descriptor.paths;
1511
+ if (!paths || paths.length === 0) return [findingOf(runtime, {
1512
+ ...descriptor,
1513
+ message,
1514
+ path: []
1515
+ })];
1516
+ return paths.map((path) => findingOf(runtime, {
1517
+ ...descriptor,
1518
+ message,
1519
+ path
1520
+ }));
1521
+ }
1522
+ function resolveMessage(runtime, descriptor) {
1523
+ if (descriptor.message) return descriptor.message;
1524
+ if (descriptor.messageId) {
1525
+ const template = runtime.definition.meta.messages[descriptor.messageId];
1526
+ if (template) return interpolate(template, descriptor.data);
1527
+ }
1528
+ return "Failed.";
1529
+ }
1530
+ function findingOf(runtime, parts) {
1531
+ const options = runtime.options;
1532
+ const name = typeof options.name === "string" && options.name.length > 0 ? options.name : runtime.checkId;
1533
+ return {
1534
+ id: runtime.id,
1535
+ checkId: runtime.checkId,
1536
+ name,
1537
+ messageId: parts.messageId,
1538
+ message: parts.message,
1539
+ severity: runtime.severity,
1540
+ path: parts.path ?? []
1541
+ };
1542
+ }
1543
+
1544
+ //#endregion
1545
+ export { bindAdopted as A, wrapEstimate as B, withRelationRowIdentity as C, FieldSlotKey as D, envelopesKey as E, syncEstimateInput as F, getDirtyFieldInput as H, writeEnvelope as I, envelopesWire as L, decodeEstimateEnvelope as M, resolveEstimateMode as N, PluginKey as O, syncAmountOrPercentInput as P, isEnvelope as R, targetToKind as S, derivationKey as T, encodeScopeValues as V, resolveRowFallback as _, checksKey as a, relationParentFieldName as b, collectionKeys as c, visibility as d, visibilityKey as f, findRowStore as g, canonicalRowOf as h, formulaCheck as i, decodeAmountOrPercentEnvelope as j, adoptEnvelope as k, computeBag as l, resolveScopeValueAt as m, replaceCheckInstances as n, bagger as o, resolveScopeValue as p, UNEVALUABLE_MESSAGE_ID as r, baggerKey as s, checks as t, flattenSourceRow as u, resolveRowScopeValue as v, mergeCollectionRows as w, relationRowIdentityProps as x, readRelationConfig as y, wrapAmountOrPercent as z };