@zvenigora/ng-eval-forms 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,7 +10,50 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
10
10
 
11
11
  ---
12
12
 
13
- ## [0.2.4] - 2026-09-29
13
+ ## [0.2.6] - 2026-10-02
14
+
15
+ Released because `eval-core` 0.8.0 and `eval-signals` 0.2.0 both fall outside 0.2.5's declared
16
+ peer ranges. No code in this package changes.
17
+
18
+ ### Changed
19
+ - **Peer ranges widened.** `@zvenigora/ng-eval-core`: `>=0.3.0 <0.8.0` → `>=0.3.0 <0.9.0`.
20
+ `@zvenigora/ng-eval-signals`: `^0.1.0` → `>=0.1.0 <0.3.0`, since on a `0.x` version `^0.1.0`
21
+ stops short of 0.2.0. Both lower bounds are unchanged. The `peerDependencies` block quoted in
22
+ the README shows the new ranges.
23
+ - **With `eval-core` 0.8.0 and `eval-signals` 0.2.0, a write error under `caseInsensitive` names
24
+ the key.** It named `'undefined'`. Through `createFieldContext`, a field-half key is named as
25
+ the field source spells it (`NAME = 1` over `{ name }` → `'name'`); a form-half key as the
26
+ expression wrote it (`COUNTRY = 1` → `'COUNTRY'`), because the form half is resolved by a
27
+ borrowed lookup rather than by the field context's own source. A `/signals` rule names the key
28
+ as written. `applyErrorPolicy` rethrows the error as before; only `key` and the message differ.
29
+ `/reactive` builds its field contexts case-sensitively and is unaffected.
30
+ Measured on eval-core 0.7.0 / eval-signals 0.1.4: both keys named 'undefined'.
31
+ [A4](../../docs/backlog-retired.md#a4), [C3](../../docs/backlog-retired.md#c3).
32
+
33
+ ---
34
+
35
+ ## [0.2.5] - 2026-10-01
36
+
37
+ Released because `eval-core` 0.7.0 falls outside 0.2.4's declared peer range. No exported symbol
38
+ changes, and the `.d.ts` files are byte-identical to 0.2.4's.
39
+
40
+ ### Changed
41
+ - **Peer range widened** to admit `@zvenigora/ng-eval-core` 0.7.0:
42
+ `>=0.3.0 <0.7.0` → `>=0.3.0 <0.8.0`. The lower bound is unchanged, so 0.3.0 to 0.6.1 remain
43
+ supported. The `peerDependencies` block quoted in the README shows the new range. The
44
+ `"@zvenigora/ng-eval-signals": "^0.1.0"` range is unchanged: it already admits 0.1.4.
45
+ - **With `eval-core` 0.7.0, a write nested inside a call is rethrown by `applyErrorPolicy`, in
46
+ every mode.** `[1].map(x => (country = 'CA'))` now reaches it as `SignalContextWriteError`,
47
+ like a direct assignment, where it arrived as a plain `Error` and was routed by the policy —
48
+ under the default `'undefined'`, a blank field with nothing in the console. `/reactive` gets the
49
+ same through `createEvalSignal`'s own bypass. The README's "When a rule fails" no longer
50
+ describes this as the guarantee's one boundary, and says the boundary returns with an
51
+ `eval-core` older than 0.7.0. [A6](../../docs/backlog-retired.md#a6).
52
+ - **`LICENSE` ships in the package** for the first time.
53
+
54
+ ---
55
+
56
+ ## [0.2.4] - 2026-09-30
14
57
 
15
58
  **Two `/signals` fixes that bring its model lookup into line with `createSignalContext`,
16
59
  [D6](../../docs/backlog-retired.md#d6) and [D4](../../docs/backlog-retired.md#d4).** A patch: no
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Mikhail Klygin (Zvenigora)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -67,8 +67,8 @@ set of ranges covering both adapters:
67
67
  "@angular/forms": ">=19.0.0",
68
68
  "rxjs": "^7.8.0",
69
69
  "acorn-walk": "^8.3.0",
70
- "@zvenigora/ng-eval-core": ">=0.3.0 <0.7.0",
71
- "@zvenigora/ng-eval-signals": "^0.1.0"
70
+ "@zvenigora/ng-eval-core": ">=0.3.0 <0.9.0",
71
+ "@zvenigora/ng-eval-signals": ">=0.1.0 <0.3.0"
72
72
  }
73
73
  ```
74
74
 
@@ -363,15 +363,13 @@ Two things are **not** routed through `options.onError`, in either adapter:
363
363
  dataset. Swallowing it under the default would hand you a silent blank for a syntax bug
364
364
  in the rule itself.
365
365
 
366
- **That guarantee has one boundary, and it is stated here because the guarantee is.** It
367
- holds for an assignment the expression makes directly. An assignment **nested inside a
368
- call** — `[1].map(x => (country = 'CA'))` — is caught by the evaluator's own call wrapper
369
- and re-raised as a plain `Error`, which loses the class the bypass matches on. It is then
370
- routed by `onError` like any other failure, so under the default you get a blank field with
371
- nothing in the console. It is a misuse inside a misuse — a rule author writing an assignment
372
- writes `country = 'CA'`, not one buried in a `.map` callback — but it is the one shape where
373
- the mechanism cannot see what it is for. The re-wrap belongs to
374
- `@zvenigora/ng-eval-core` and cannot be fixed from this side.
366
+ **It holds through a call too — with `@zvenigora/ng-eval-core` 0.7.0 or later.** An
367
+ assignment **nested inside a call**, `[1].map(x => (country = 'CA'))`, reaches the bypass
368
+ as `SignalContextWriteError` and is rethrown like a direct one. Up to `eval-core` 0.6.x
369
+ the evaluator's own call wrapper re-raised it as a plain `Error`, which lost the class the
370
+ bypass matches on, so it was routed by `onError` like any other failure: under the default,
371
+ a blank field with nothing in the console. The peer range still admits those versions, and
372
+ with one installed that boundary is back; it cannot be fixed from this side.
375
373
 
376
374
  ## Reactivity, and its two holes
377
375
 
@@ -1 +1 @@
1
- {"version":3,"file":"zvenigora-ng-eval-forms-reactive.mjs","sources":["../../../../modules/eval-forms/reactive/src/lib/control-source.ts","../../../../modules/eval-forms/reactive/src/lib/field-schema.ts","../../../../modules/eval-forms/reactive/src/zvenigora-ng-eval-forms-reactive.ts"],"sourcesContent":["import { DestroyRef, Injector, Signal } from '@angular/core';\r\nimport { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';\r\nimport type { AbstractControl, FormGroup } from '@angular/forms';\r\nimport type { SignalContextSource } from '@zvenigora/ng-eval-signals';\r\nimport { Subject, startWith, switchMap, takeUntil } from 'rxjs';\r\n\r\n/**\r\n * One key's mirror: the control it currently points at, the channel that\r\n * re-points it, and the one that ends it.\r\n *\r\n * Both channels exist because `toSignal(obs, { injector })` takes its cleanup\r\n * from that injector's `DestroyRef` and hands back no per-subscription handle.\r\n * Releasing *one* control's subscription while the injector is still alive\r\n * therefore has to happen inside the stream, and the two cases need different\r\n * operators:\r\n *\r\n * - **replaced** - `instances` emits, `switchMap` drops the previous inner\r\n * subscription and takes the new one. One release, one subscribe.\r\n * - **removed** - `released` emits and `takeUntil` unsubscribes the chain.\r\n * Completing `instances` is *not* enough and the difference is not academic:\r\n * `switchMap` completes only once its inner completes too, and\r\n * `valueChanges` never does - so the dead control keeps its subscriber. This\r\n * is measured, not reasoned; the removal spec fails against the version that\r\n * only completed `instances`.\r\n */\r\ninterface ControlChannel {\r\n control: AbstractControl;\r\n instances: Subject<AbstractControl>;\r\n released: Subject<void>;\r\n}\r\n\r\n/**\r\n * Builds the form-wide source of an expression context from a `FormGroup`.\r\n *\r\n * One mirror **per control, never per group** (plan S 3.5.1). A disabled\r\n * control is excluded from its parent's aggregate value and there is no\r\n * `rawValueChanges` to go with `getRawValue()`, so a group-backed mirror would\r\n * lose a field the moment anything disabled it - and this library computes\r\n * disablement, so one field's rule would silently blank the value every *other*\r\n * field's rules resolve against. A control's own `valueChanges` keeps emitting\r\n * regardless of its enabled state.\r\n *\r\n * **The record holds live values behind accessors, not the mirrored signals**\r\n * (S 3.5.5). Each key is an enumerable accessor that reads its change-signal -\r\n * which is what a surrounding `computed()` records as a dependency, per key -\r\n * and then answers with the control's *current* value. Reading the control\r\n * rather than the signal is what makes `invalidate()` a working hatch for\r\n * trap 3: a `{ emitEvent: false }` write leaves the change-signal stale, and a\r\n * record that handed out that signal would have nothing to recover, however\r\n * often it was re-evaluated.\r\n *\r\n * **One entry per key, for the life of that key** (S 3.5.5). A replaced control\r\n * instance re-points its channel; it does not get a new accessor or a new\r\n * signal. Building fresh ones would leave every property that had already read\r\n * the key tracking the *dead* control's signal - frozen at its last value, for\r\n * the life of the binding, which is the failure trap 5 exists to prevent\r\n * reappearing one layer up.\r\n *\r\n * **The returned record must be passed by reference, never spread or cloned.**\r\n * `{ ...source }` flattens the accessors to the values they happened to hold,\r\n * so the copy records no dependency and every property over it freezes at its\r\n * first value with no error. The type - `Record<string, unknown>` - gives no\r\n * hint of this, and phase-3-plan S 9.2 describes composing sources by\r\n * spreading, which is why it is said here.\r\n *\r\n * **`invalidate()` covers a stale *value*, not a suppressed *control-set*\r\n * change.** Trap 3's hatch works because the accessor re-reads the control, so\r\n * re-running the walk recovers. `setControl` / `addControl` / `removeControl`\r\n * with `{ emitEvent: false }` suppress `group.events` instead, so `sync` never\r\n * runs and the channel still points at the dead instance - there is nothing\r\n * for a re-run to recover, in the same structural sense that rules out a\r\n * signal-valued record. Current behaviour, pinned here rather than fixed:\r\n * the observable is the only signal there is.\r\n *\r\n * @param group - A flat `FormGroup`. Nested groups and `FormArray` are out of\r\n * scope for this phase.\r\n * @param options - `injector` scopes every subscription here to the form's\r\n * lifetime; it is required rather than optional because an\r\n * explicit injector means no `DestroyRef` auto-teardown, and\r\n * an optional one would silently vary that (plan S 3.7).\r\n */\r\nexport const createControlSource = (\r\n group: FormGroup,\r\n options: { injector: Injector }\r\n): SignalContextSource => {\r\n\r\n const source: SignalContextSource = {};\r\n const channels = new Map<string, ControlChannel>();\r\n\r\n // `FormGroup`'s default type argument is `any`, so its `controls` is too.\r\n // Narrowed once here rather than at four call sites.\r\n const controls = (): Record<string, AbstractControl> =>\r\n group.controls as Record<string, AbstractControl>;\r\n\r\n const open = (name: string, control: AbstractControl): void => {\r\n\r\n const channel: ControlChannel = {\r\n control,\r\n instances: new Subject<AbstractControl>(),\r\n released: new Subject<void>(),\r\n };\r\n\r\n // `startWith` twice, and both are load-bearing. The outer one subscribes\r\n // the control the key opened with. The inner one replays the *current*\r\n // instance's value on subscribe, so a replacement produces an emission\r\n // immediately instead of waiting for its first edit - without it a\r\n // property reading this key would sit on the dead control's value until\r\n // someone typed into the new one.\r\n //\r\n // The signal's own value is never read; it is a change ticker. It still\r\n // has to carry values, because `toSignal` dedupes with `Object.is` and a\r\n // stream emitting a constant would tick once and never again.\r\n const changed: Signal<unknown> = toSignal(\r\n channel.instances.pipe(\r\n startWith(control),\r\n switchMap((instance) =>\r\n instance.valueChanges.pipe(startWith(instance.value))\r\n ),\r\n takeUntil(channel.released)\r\n ),\r\n { injector: options.injector, requireSync: true }\r\n );\r\n\r\n Object.defineProperty(source, name, {\r\n get: () => {\r\n changed();\r\n return channel.control.value;\r\n },\r\n enumerable: true,\r\n configurable: true,\r\n });\r\n\r\n // Registered last, after everything that can throw. `toSignal` can\r\n // (NG0205 from a destroyed injector, NG0601 from a stream that does not\r\n // emit synchronously), and a channel registered before it would leave\r\n // `channels` holding a key `source` has no accessor for - which `sync`\r\n // then reads as \"unchanged\" forever, so the key is permanently\r\n // un-mirrored with no path back.\r\n channels.set(name, channel);\r\n };\r\n\r\n const close = (name: string, channel: ControlChannel): void => {\r\n channel.released.next();\r\n channel.released.complete();\r\n channel.instances.complete();\r\n channels.delete(name);\r\n delete source[name];\r\n };\r\n\r\n // Diffs the key -> instance map against the group's current controls\r\n // (S 3.5.5). Deleting the entry being visited is safe: `Map` iteration\r\n // tolerates it.\r\n const sync = (): void => {\r\n\r\n const live = controls();\r\n\r\n for (const [name, channel] of channels) {\r\n // Own-property, never a bare `live[name]` (plan S 3.4.2's rule, applied\r\n // on this side of the library too). `FormGroup` keeps the caller's\r\n // object literal and rejects only keys containing a dot, so a\r\n // server-supplied field named `constructor` or `toString` is legal -\r\n // and after its control is removed a bare read resolves the\r\n // *prototype's* value instead of `undefined`. This loop would then take\r\n // the \"replaced\" branch, push `Object` down the channel, and throw out\r\n // of the `group.events` subscriber - which unsubscribes it, silently\r\n // ending all diffing for the life of the form.\r\n const replacement: AbstractControl | undefined =\r\n Object.prototype.hasOwnProperty.call(live, name)\r\n ? live[name]\r\n : undefined;\r\n\r\n if (replacement === channel.control) {\r\n continue;\r\n }\r\n\r\n if (replacement === undefined) {\r\n close(name, channel);\r\n continue;\r\n }\r\n\r\n channel.control = replacement;\r\n channel.instances.next(replacement);\r\n }\r\n\r\n for (const [name, control] of Object.entries(live)) {\r\n if (!channels.has(name)) {\r\n open(name, control);\r\n }\r\n }\r\n };\r\n\r\n for (const [name, control] of Object.entries(controls())) {\r\n open(name, control);\r\n }\r\n\r\n // After the channels, deliberately: with the injector missing - which the\r\n // type forbids but a JavaScript caller can still do - `toSignal` above\r\n // throws NG0203 naming the injection context, where reading `DestroyRef` off\r\n // `undefined` first would throw a `TypeError` naming nothing.\r\n //\r\n // `group.events` and not `valueChanges`: `setControl` / `addControl` /\r\n // `removeControl` all reach `updateValueAndValidity`, so the control set\r\n // changing is observable here.\r\n //\r\n // It fires on ordinary interaction too, and on more than one event apiece -\r\n // value, status, pristine, touched and reset are all `ControlEvent`s - so\r\n // this runs several O(N) diffs per interaction rather than one. That is the\r\n // price of not handing the consumer a `rebind()` they will forget to call,\r\n // and there is no narrower signal available: Angular's collection-change\r\n // callback is private. Nothing per-node is touched, so eval-core's\r\n // performance gate is unaffected.\r\n group.events\r\n .pipe(takeUntilDestroyed(options.injector.get(DestroyRef)))\r\n .subscribe(() => sync());\r\n\r\n return source;\r\n};\r\n","import {\r\n DestroyRef,\r\n EnvironmentInjector,\r\n Injector,\r\n computed,\r\n createEnvironmentInjector,\r\n inject,\r\n} from '@angular/core';\r\nimport { AbstractControl, FormControl, FormGroup } from '@angular/forms';\r\nimport {\r\n ExpressionErrorPolicy,\r\n createFieldContext,\r\n toText,\r\n toVisible,\r\n} from '@zvenigora/ng-eval-forms';\r\nimport { EvalSignal, createEvalSignal } from '@zvenigora/ng-eval-signals';\r\nimport { createControlSource } from './control-source';\r\n\r\n/**\r\n * One field's rules, as strings resolved at runtime.\r\n *\r\n * This is the whole descriptor. It is deliberately not a schema *language*:\r\n * `visible` and `text` are the two properties this phase ships (plan S 3.6),\r\n * `disabled` is deferred with three reasons rather than a shrug, and\r\n * `required` is deferred with the validators.\r\n */\r\nexport interface FieldSchema {\r\n\r\n /**\r\n * The field's name, and the key its control is mirrored under.\r\n *\r\n * It does not have to name a control: a rule naming a field the group has\r\n * no control for resolves `undefined`, which is S 3.4.3's ordinary case\r\n * rather than an error. It may not be a member of `Object.prototype` -\r\n * see {@link bindFieldProperties}.\r\n */\r\n readonly name: string;\r\n\r\n /** An expression whose value is coerced by truthiness (`toVisible`). */\r\n readonly visible?: string;\r\n\r\n /** An expression whose value is coerced to a string (`toText`). */\r\n readonly text?: string;\r\n}\r\n\r\n/**\r\n * The signals bound for one field - one per rule the schema supplied.\r\n *\r\n * Typed as `EvalSignal`, not `Signal`, and both extra members are load-bearing\r\n * on this path (plan S 5): `invalidate()` is trap 3's hatch for a\r\n * `{ emitEvent: false }` write and S 3.4.2's hatch for a key-set change, and\r\n * `destroy()` is what step 5's teardown counts. Typing these as `Signal` would\r\n * put both out of reach through the published surface.\r\n *\r\n * The type arguments are `boolean` and `string` rather than S 5's original\r\n * `unknown`, **because the coercion of S 3.6 happens here**. That section was\r\n * written before the coercion's placement was settled and `coercion.ts` states\r\n * the settlement - \"both adapters call this\" - so `unknown` would now describe\r\n * the value the property is built *from* rather than the one it answers with.\r\n */\r\nexport interface FieldProperties {\r\n readonly visible?: EvalSignal<boolean>;\r\n readonly text?: EvalSignal<string>;\r\n}\r\n\r\n/**\r\n * What {@link bindFieldProperties} returns: the bound fields, and the one call\r\n * that ends them.\r\n *\r\n * **Nested rather than a `destroy` written onto the record** (plan S 5's step-5\r\n * amendment). The record is keyed by *field name*, `destroy` is a legal field\r\n * name, and S 0's premise is that those names arrive from a server rather than\r\n * from the consumer - so a flat shape would put a silent collision between the\r\n * consumer's data and this library's API in precisely the case where the\r\n * consumer controls the names least.\r\n */\r\nexport interface FormBinding {\r\n\r\n /** The bound properties, keyed by field name. */\r\n readonly fields: Record<string, FieldProperties>;\r\n\r\n /**\r\n * Destroys every `EvalSignal` this binding created and releases the mirror.\r\n *\r\n * **Call it.** Every signal here is built with an explicit injector, and\r\n * that means no `DestroyRef` registration of its own (`eval-signal.ts`) - so\r\n * nothing auto-destroys and all N x M are the caller's (plan S 3.7).\r\n *\r\n * There is one net under that, and it is a net rather than a substitute: the\r\n * binding registers this on the `DestroyRef` of the injector it was given,\r\n * so a binding wired to a component or route injector is released when that\r\n * injector dies even if nobody called it. It is not a licence to skip the\r\n * call - a binding built on the root injector is released at the end of the\r\n * application and no sooner.\r\n *\r\n * Idempotent, and that is load-bearing rather than polite: the scope is an\r\n * `EnvironmentInjector`, and `R3Injector.destroy()` throws NG0205 when it\r\n * has already run.\r\n */\r\n destroy(): void;\r\n}\r\n\r\n/**\r\n * Puts a coercion in front of an `EvalSignal` without losing the two members\r\n * that make it one.\r\n *\r\n * A second `computed` rather than a coercion inside the walk: the expression\r\n * is the consumer's and its *value* is what the error policy and the\r\n * dependency tracking are defined over, so the coercion belongs strictly\r\n * downstream of both. It is also what keeps a destroyed signal - which reads\r\n * `undefined` - answering `false` / `''` rather than leaking `undefined` into\r\n * a template.\r\n *\r\n * `Object.assign` then `defineProperty`, mirroring `createEvalSignal`'s own\r\n * construction: `assign` copies a getter's *current value*, which would freeze\r\n * `dependencies` at the empty set it holds before the first recompute.\r\n */\r\nconst coerce = <T>(\r\n inner: EvalSignal<unknown>,\r\n map: (value: unknown) => T\r\n): EvalSignal<T> => {\r\n\r\n const outer = Object.assign(computed(() => map(inner())), {\r\n invalidate: () => inner.invalidate(),\r\n destroy: () => inner.destroy(),\r\n }) as EvalSignal<T>;\r\n\r\n Object.defineProperty(outer, 'dependencies', {\r\n get: () => inner.dependencies,\r\n enumerable: true,\r\n configurable: true,\r\n });\r\n\r\n return outer;\r\n};\r\n\r\n/**\r\n * The three checks of open question 8.3, plus 8.5's half.\r\n *\r\n * Three checks, and the count is the boundary rather than a starting point.\r\n * Each catches something that is otherwise either silent or reported far from\r\n * its cause, and none of them is a rule about what an *expression* may say -\r\n * that is `eval-core`'s and is already answered.\r\n */\r\nconst validate = (schema: readonly FieldSchema[], group: FormGroup): void => {\r\n\r\n const seen = new Set<string>();\r\n\r\n for (const field of schema) {\r\n\r\n if (seen.has(field.name)) {\r\n throw new Error(\r\n `Duplicate field name '${field.name}' in the schema: ` +\r\n `each field is bound once, so the second would silently replace the first.`\r\n );\r\n }\r\n seen.add(field.name);\r\n\r\n // The check no other layer can make (S 3.4.3's third precedence layer).\r\n // A server-supplied field named `constructor`, `toString`, `valueOf` or\r\n // `__proto__` is legal to `FormGroup`, which rejects only keys containing\r\n // a dot - and when no control backs it, `EvalContext.get` consults\r\n // `original` first and `getContextValue` reads a plain object as a bare\r\n // property access, so the name resolves off `Object.prototype`. Every rule\r\n // reading it then gets a *function*, which is truthy: `visible` renders\r\n // precisely the field that has no data, with no error anywhere.\r\n //\r\n // It is not containable downstream - the resolution happens above this\r\n // library, in `createSignalContext`'s `original`, and S 2 forbids the\r\n // upstream fix - and this is the only layer that can name the offending\r\n // field in the message.\r\n if (Object.prototype.hasOwnProperty.call(Object.prototype, field.name)) {\r\n throw new Error(\r\n `Field name '${field.name}' is a member of Object.prototype and cannot be ` +\r\n `resolved reliably: an expression naming it reads the prototype's value ` +\r\n `whenever no control backs it. Rename the field.`\r\n );\r\n }\r\n\r\n for (const property of ['visible', 'text'] as const) {\r\n const rule = field[property];\r\n\r\n if (rule !== undefined && typeof rule !== 'string') {\r\n throw new Error(\r\n `Field '${field.name}': ${property} must be a string expression, ` +\r\n `not ${typeof rule}.`\r\n );\r\n }\r\n }\r\n }\r\n\r\n // Checked over the *group*, not over the schema's names, because the mirror\r\n // covers every control (S 3.5) - so a control nobody named still reaches\r\n // every field's context, where an expression finds it.\r\n //\r\n // Construction-time only, and that is a limitation rather than a guarantee:\r\n // `addControl` afterwards reaches `sync` -> `open` with no check, and\r\n // throwing from there is not available - a throw inside the `group.events`\r\n // subscriber unsubscribes it and silently ends all diffing for the life of\r\n // the form (S 3.5.5). The README says so.\r\n for (const [name, control] of Object.entries(\r\n group.controls as Record<string, AbstractControl>\r\n )) {\r\n // The same check as above, over the other source of names rather than a\r\n // fourth one - and the mirror does **not** rescue this case, which is\r\n // easy to believe and wrong. The mirror does define an own accessor for\r\n // `constructor` on its record, but the record is reached through\r\n // `lookups`, step 4 of `EvalContext.get`, while `original` - the empty\r\n // field source - is read at step 2 with a bare property access. Measured:\r\n // the accessor is present and the expression still reads\r\n // `function Object() { [native code] }`. So a control named off\r\n // `Object.prototype` is unreadable through every expression, silently and\r\n // truthily, and worse than the schema case because the value exists.\r\n if (Object.prototype.hasOwnProperty.call(Object.prototype, name)) {\r\n throw new Error(\r\n `Control '${name}' is a member of Object.prototype and cannot be read by any ` +\r\n `expression: the name resolves to the prototype's value before the form is ` +\r\n `ever consulted. Rename the control.`\r\n );\r\n }\r\n\r\n // Flat forms only (S 2, open question 8.5): a thrown error rather than a\r\n // documented limitation, because the alternative surfaces as a confusing\r\n // evaluation result far from its cause - a nested group's *aggregate\r\n // object* arriving where a value was expected.\r\n if (!(control instanceof FormControl)) {\r\n throw new Error(\r\n `Control '${name}' is not a FormControl. Nested groups and FormArrays are ` +\r\n `out of scope for this phase.`\r\n );\r\n }\r\n }\r\n};\r\n\r\n/**\r\n * Binds a schema to a `FormGroup`, producing one `EvalSignal` per rule.\r\n *\r\n * ```ts\r\n * const binding = bindFieldProperties(\r\n * [{ name: 'state', visible: \"country === 'US'\" }],\r\n * form,\r\n * { injector }\r\n * );\r\n *\r\n * binding.fields['state'].visible(); // recomputes when `country` changes\r\n *\r\n * binding.destroy(); // yours to call - nothing here\r\n * // auto-destroys (S 3.7)\r\n * ```\r\n *\r\n * **One `EvalContext` per field, never one shared** (plan S 3.4.1). `get`\r\n * resolves `scopes` and `original` *before* `lookups`, and an arrow function's\r\n * parameter scope sits on the context for as long as its body runs - so under\r\n * a shared context one field's arrow would shadow every other field's key of\r\n * the same name for that window, and anything that strands a scope rather than\r\n * popping it would do so for the life of the form. The count is N contexts for\r\n * N fields, and not N x M: the properties of one field resolve against the\r\n * same names.\r\n *\r\n * **The field half of each context is empty in this phase.**\r\n * `createFieldContext` takes two sources and the second is `{}` here. What a\r\n * field-local key set should *contain* is specified nowhere - S 3.4.3's\r\n * \"a field named `value`, `name` or `index`\" illustrates collision semantics\r\n * rather than naming keys - and inventing three keys to fill a parameter is\r\n * how a public surface acquires members nobody chose. The consequence, stated\r\n * rather than left to be found: S 3.4.3's precedence rule is asserted at the\r\n * core in `field-context.spec.ts` and ships **untested end to end**, because\r\n * no `/reactive` path produces a field-local key. It gets settled by whichever\r\n * phase first has a consumer for one.\r\n *\r\n * **`createControlSource` is called once, for the whole form.** Per field\r\n * would build N mirrors over one group: N x the live `toSignal` subscriptions\r\n * step 5 counts, and trap 5's O(N) diff running N times per `group.events`\r\n * emission, on a signal that already fires several times per interaction.\r\n *\r\n * **Constructed, not computed.** `toSignal` opens with\r\n * `assertNotInReactiveContext`, so calling this from inside an `effect()` or a\r\n * `computed()` throws out of the mirror with an error naming `toSignal` and\r\n * nothing naming this library. A form binding belongs in a service or a\r\n * factory, which is the same premise `injector` being required rests on.\r\n *\r\n * **Teardown runs off a child injector, and there is only one of them**\r\n * (S 3.7, amended in step 5). The binding opens an `EnvironmentInjector` under\r\n * the caller's and scopes *both* the mirror and every `createEvalSignal` to it,\r\n * so `destroy()` releases the whole mirror - every per-key `toSignal`\r\n * subscription and the `group.events` one - in a single call, and a bind that\r\n * throws part-way through releases exactly the same thing however far it got.\r\n * The plan's earlier premise, that releasing the mirror needed a handle\r\n * `createControlSource` does not return, was wrong: step 3 had already measured\r\n * that destroying the injector drops every control's observer count to 0 and\r\n * ends the diffing.\r\n *\r\n * The `EvalSignal`s are **not** covered by that, and that is the point of the\r\n * count: built with an explicit injector, they take no `DestroyRef`\r\n * registration, so `destroy()` walks them itself and N x M of them is what a\r\n * spec asserts rather than \"the form's destroy ran\".\r\n *\r\n * @param schema - The fields to bind. Validated at construction: a duplicate\r\n * name, a non-string rule, a name that resolves off\r\n * `Object.prototype`, and a control that is not a\r\n * `FormControl` all throw here rather than surfacing later as\r\n * an evaluation result nobody can trace.\r\n * @param group - A flat `FormGroup` of `FormControl`s.\r\n * @param options - `injector` is **required**, not optional: S 3.7's argument\r\n * is that every `EvalSignal` here has no auto-teardown, and\r\n * an optional injector would silently vary that for a call\r\n * that happened to sit in an injection context. `onError`\r\n * defaults to `'undefined'` - the opposite of\r\n * `eval-signals`' default, and resolved *here* rather than\r\n * forwarded absent, since `createEvalSignal` would otherwise\r\n * supply `'throw'` (S 3.4.4).\r\n * @returns The bound properties keyed by field name, and the `destroy()` that\r\n * ends them.\r\n */\r\nexport const bindFieldProperties = (\r\n schema: readonly FieldSchema[],\r\n group: FormGroup,\r\n options: { injector: Injector; onError?: ExpressionErrorPolicy }\r\n): FormBinding => {\r\n\r\n // Before the mirror, deliberately. A schema rejection then opens no\r\n // subscription at all, which is a stronger statement than \"the catch below\r\n // releases it\" and is asserted separately.\r\n validate(schema, group);\r\n\r\n // `?.` and the `inject` fallback are not defensive dressing: the type makes\r\n // `injector` required, but a JavaScript caller can omit it, and reading\r\n // `.get` off `undefined` is a `TypeError` naming nothing. `toSignal` solves\r\n // the same problem the same way (`rxjs-interop.mjs:106`), and it is what\r\n // keeps the error a caller sees NG0203 - naming the injection context -\r\n // rather than a property read on `undefined`. `control-source.ts` orders\r\n // its own statements for that error; this preserves it one layer up.\r\n const parent =\r\n options?.injector?.get(EnvironmentInjector) ?? inject(EnvironmentInjector);\r\n\r\n // The other arm of the same problem, and the fallback above is what opens\r\n // it: *inside* an injection context that fallback succeeds, so a caller who\r\n // omitted the required `injector` would get a working binding parented at\r\n // the ambient environment injector - with no auto-teardown and no\r\n // diagnostic. That is the exact silent variation S 3.7 makes `injector`\r\n // required to prevent, so it is rejected here by name rather than left to\r\n // surface as a leak. Outside a context the line above has already thrown\r\n // NG0203 and this is unreachable.\r\n if (!options?.injector) {\r\n throw new Error(\r\n `bindFieldProperties requires an 'injector': every signal it creates is built ` +\r\n `with one and therefore takes no DestroyRef registration, so an ambient ` +\r\n `injection context would silently vary when teardown runs.`\r\n );\r\n }\r\n\r\n const scope = createEnvironmentInjector([], parent);\r\n\r\n const onError = options.onError ?? 'undefined';\r\n\r\n const fields: Record<string, FieldProperties> = {};\r\n\r\n // The signals, in creation order, so the `catch` below releases exactly what\r\n // exists rather than walking `fields` - which holds nothing for the field\r\n // that threw and nothing at all for a throw in `createControlSource`.\r\n const created: EvalSignal<unknown>[] = [];\r\n\r\n let destroyed = false;\r\n\r\n // The registration on the *caller's* injector, held so `destroy()` can drop\r\n // it. See below for why it exists at all.\r\n let unregister: (() => void) | undefined;\r\n\r\n const destroy = (): void => {\r\n\r\n if (destroyed) {\r\n return;\r\n }\r\n\r\n destroyed = true;\r\n\r\n // Cleared before it is called, so a teardown driven *by* the caller's\r\n // injector does not turn around and mutate the hook list that injector is\r\n // iterating - `eval-signal.ts:408-414`'s pattern and its reason.\r\n const release = unregister;\r\n unregister = undefined;\r\n release?.();\r\n\r\n try {\r\n // Called through the property rather than a captured reference: it is\r\n // the object the caller holds, and `coerce`'s `destroy` delegates to the\r\n // inner signal, so this is the one call that reaches both.\r\n for (const signal of created) {\r\n signal.destroy();\r\n }\r\n } finally {\r\n // In a `finally` because `destroyed` is already set: a signal whose\r\n // `destroy` throws - a consumer calling this from inside a `computed()`\r\n // reaches NG0600 - would otherwise leave the scope alive with the retry\r\n // guarded into a no-op, and every mirror subscription live with no way\r\n // back.\r\n //\r\n // Last, and idempotence is why the guard above exists at all:\r\n // `R3Injector.destroy()` opens with `assertNotDestroyed` and throws\r\n // NG0205 on a second call.\r\n scope.destroy();\r\n }\r\n };\r\n\r\n // **The scope is detached, not merely shorter-lived.**\r\n // `createEnvironmentInjector` does not register the child with its parent's\r\n // destroy hooks, so without this the mirror would outlive the injector the\r\n // caller scoped it to - and step 4, which passed that injector to\r\n // `createControlSource` directly, released everything when it died. A\r\n // consumer who wires a binding to a component or route injector and forgets\r\n // `destroy()` would otherwise retain N subscriptions, the mirror, the N\r\n // contexts and the form itself for the life of the root injector, silently.\r\n // `destroy()` stays the documented call; this is the net under it.\r\n unregister = options.injector.get(DestroyRef).onDestroy(() => {\r\n unregister = undefined;\r\n destroy();\r\n });\r\n\r\n try {\r\n\r\n // Scoped to the child, not to the caller's injector: it is what makes the\r\n // mirror's two subscriptions releasable at all without a handle.\r\n const formSource = createControlSource(group, { injector: scope });\r\n\r\n for (const field of schema) {\r\n\r\n const context = createFieldContext(formSource, {});\r\n const properties: { visible?: EvalSignal<boolean>; text?: EvalSignal<string> } = {};\r\n\r\n // `options.injector`, **not** the scope, and the distinction is not\r\n // cosmetic: `createEvalSignal` uses the injector it is given only to\r\n // resolve `CompilerService` (`eval-signal.ts:215-216`) - it registers no\r\n // teardown against it - and the scope's parent is\r\n // `caller.get(EnvironmentInjector)`, which for a node injector is an\r\n // *ancestor*. Passing the scope would therefore skip providers the\r\n // caller declared, silently, and diverge from step 4 for no gain. The\r\n // scope exists for the one thing that does need a releasable lifetime,\r\n // which is the mirror.\r\n if (field.visible !== undefined) {\r\n properties.visible = coerce(\r\n createEvalSignal(field.visible, context, {\r\n injector: options.injector,\r\n onError,\r\n }),\r\n toVisible\r\n );\r\n created.push(properties.visible);\r\n }\r\n\r\n if (field.text !== undefined) {\r\n properties.text = coerce(\r\n createEvalSignal(field.text, context, {\r\n injector: options.injector,\r\n onError,\r\n }),\r\n toText\r\n );\r\n created.push(properties.text);\r\n }\r\n\r\n fields[field.name] = properties;\r\n }\r\n\r\n } catch (error) {\r\n // A *parse* error is the reachable case (S 3.4.4's amendment):\r\n // `createEvalSignal` compiles eagerly, so `visible: 'country ==='` throws\r\n // from inside this loop with the mirror already open - and the caller\r\n // holds no handle to any of it, because the binding never returned.\r\n destroy();\r\n throw error;\r\n }\r\n\r\n return { fields, destroy };\r\n};\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"zvenigora-ng-eval-forms-reactive.mjs","sources":["../../../../modules/eval-forms/reactive/src/lib/control-source.ts","../../../../modules/eval-forms/reactive/src/lib/field-schema.ts","../../../../modules/eval-forms/reactive/src/zvenigora-ng-eval-forms-reactive.ts"],"sourcesContent":["import { DestroyRef, Injector, Signal } from '@angular/core';\r\nimport { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';\r\nimport type { AbstractControl, FormGroup } from '@angular/forms';\r\nimport type { SignalContextSource } from '@zvenigora/ng-eval-signals';\r\nimport { Subject, startWith, switchMap, takeUntil } from 'rxjs';\r\n\r\n/**\r\n * One key's mirror: the control it currently points at, the channel that\r\n * re-points it, and the one that ends it.\r\n *\r\n * Both channels exist because `toSignal(obs, { injector })` takes its cleanup\r\n * from that injector's `DestroyRef` and hands back no per-subscription handle.\r\n * Releasing *one* control's subscription while the injector is still alive\r\n * therefore has to happen inside the stream, and the two cases need different\r\n * operators:\r\n *\r\n * - **replaced** - `instances` emits, `switchMap` drops the previous inner\r\n * subscription and takes the new one. One release, one subscribe.\r\n * - **removed** - `released` emits and `takeUntil` unsubscribes the chain.\r\n * Completing `instances` is *not* enough and the difference is not academic:\r\n * `switchMap` completes only once its inner completes too, and\r\n * `valueChanges` never does - so the dead control keeps its subscriber. This\r\n * is measured, not reasoned; the removal spec fails against the version that\r\n * only completed `instances`.\r\n */\r\ninterface ControlChannel {\r\n control: AbstractControl;\r\n instances: Subject<AbstractControl>;\r\n released: Subject<void>;\r\n}\r\n\r\n/**\r\n * Builds the form-wide source of an expression context from a `FormGroup`.\r\n *\r\n * One mirror **per control, never per group** (plan S 3.5.1). A disabled\r\n * control is excluded from its parent's aggregate value and there is no\r\n * `rawValueChanges` to go with `getRawValue()`, so a group-backed mirror would\r\n * lose a field the moment anything disabled it - and this library computes\r\n * disablement, so one field's rule would silently blank the value every *other*\r\n * field's rules resolve against. A control's own `valueChanges` keeps emitting\r\n * regardless of its enabled state.\r\n *\r\n * **The record holds live values behind accessors, not the mirrored signals**\r\n * (S 3.5.5). Each key is an enumerable accessor that reads its change-signal -\r\n * which is what a surrounding `computed()` records as a dependency, per key -\r\n * and then answers with the control's *current* value. Reading the control\r\n * rather than the signal is what makes `invalidate()` a working hatch for\r\n * trap 3: a `{ emitEvent: false }` write leaves the change-signal stale, and a\r\n * record that handed out that signal would have nothing to recover, however\r\n * often it was re-evaluated.\r\n *\r\n * **One entry per key, for the life of that key** (S 3.5.5). A replaced control\r\n * instance re-points its channel; it does not get a new accessor or a new\r\n * signal. Building fresh ones would leave every property that had already read\r\n * the key tracking the *dead* control's signal - frozen at its last value, for\r\n * the life of the binding, which is the failure trap 5 exists to prevent\r\n * reappearing one layer up.\r\n *\r\n * **The returned record must be passed by reference, never spread or cloned.**\r\n * `{ ...source }` flattens the accessors to the values they happened to hold,\r\n * so the copy records no dependency and every property over it freezes at its\r\n * first value with no error. The type - `Record<string, unknown>` - gives no\r\n * hint of this, and phase-3-plan S 9.2 describes composing sources by\r\n * spreading, which is why it is said here.\r\n *\r\n * **`invalidate()` covers a stale *value*, not a suppressed *control-set*\r\n * change.** Trap 3's hatch works because the accessor re-reads the control, so\r\n * re-running the walk recovers. `setControl` / `addControl` / `removeControl`\r\n * with `{ emitEvent: false }` suppress `group.events` instead, so `sync` never\r\n * runs and the channel still points at the dead instance - there is nothing\r\n * for a re-run to recover, in the same structural sense that rules out a\r\n * signal-valued record. Current behaviour, pinned here rather than fixed:\r\n * the observable is the only signal there is.\r\n *\r\n * @param group - A flat `FormGroup`. Nested groups and `FormArray` are out of\r\n * scope for this phase.\r\n * @param options - `injector` scopes every subscription here to the form's\r\n * lifetime; it is required rather than optional because an\r\n * explicit injector means no `DestroyRef` auto-teardown, and\r\n * an optional one would silently vary that (plan S 3.7).\r\n */\r\nexport const createControlSource = (\r\n group: FormGroup,\r\n options: { injector: Injector }\r\n): SignalContextSource => {\r\n\r\n const source: SignalContextSource = {};\r\n const channels = new Map<string, ControlChannel>();\r\n\r\n // `FormGroup`'s default type argument is `any`, so its `controls` is too.\r\n // Narrowed once here rather than at four call sites.\r\n const controls = (): Record<string, AbstractControl> =>\r\n group.controls as Record<string, AbstractControl>;\r\n\r\n const open = (name: string, control: AbstractControl): void => {\r\n\r\n const channel: ControlChannel = {\r\n control,\r\n instances: new Subject<AbstractControl>(),\r\n released: new Subject<void>(),\r\n };\r\n\r\n // `startWith` twice, and both are load-bearing. The outer one subscribes\r\n // the control the key opened with. The inner one replays the *current*\r\n // instance's value on subscribe, so a replacement produces an emission\r\n // immediately instead of waiting for its first edit - without it a\r\n // property reading this key would sit on the dead control's value until\r\n // someone typed into the new one.\r\n //\r\n // The signal's own value is never read; it is a change ticker. It still\r\n // has to carry values, because `toSignal` dedupes with `Object.is` and a\r\n // stream emitting a constant would tick once and never again.\r\n const changed: Signal<unknown> = toSignal(\r\n channel.instances.pipe(\r\n startWith(control),\r\n switchMap((instance) =>\r\n instance.valueChanges.pipe(startWith(instance.value))\r\n ),\r\n takeUntil(channel.released)\r\n ),\r\n { injector: options.injector, requireSync: true }\r\n );\r\n\r\n Object.defineProperty(source, name, {\r\n get: () => {\r\n changed();\r\n return channel.control.value;\r\n },\r\n enumerable: true,\r\n configurable: true,\r\n });\r\n\r\n // Registered last, after everything that can throw. `toSignal` can\r\n // (NG0205 from a destroyed injector, NG0601 from a stream that does not\r\n // emit synchronously), and a channel registered before it would leave\r\n // `channels` holding a key `source` has no accessor for - which `sync`\r\n // then reads as \"unchanged\" forever, so the key is permanently\r\n // un-mirrored with no path back.\r\n channels.set(name, channel);\r\n };\r\n\r\n const close = (name: string, channel: ControlChannel): void => {\r\n channel.released.next();\r\n channel.released.complete();\r\n channel.instances.complete();\r\n channels.delete(name);\r\n delete source[name];\r\n };\r\n\r\n // Diffs the key -> instance map against the group's current controls\r\n // (S 3.5.5). Deleting the entry being visited is safe: `Map` iteration\r\n // tolerates it.\r\n const sync = (): void => {\r\n\r\n const live = controls();\r\n\r\n for (const [name, channel] of channels) {\r\n // Own-property, never a bare `live[name]` (plan S 3.4.2's rule, applied\r\n // on this side of the library too). `FormGroup` keeps the caller's\r\n // object literal and rejects only keys containing a dot, so a\r\n // server-supplied field named `constructor` or `toString` is legal -\r\n // and after its control is removed a bare read resolves the\r\n // *prototype's* value instead of `undefined`. This loop would then take\r\n // the \"replaced\" branch, push `Object` down the channel, and throw out\r\n // of the `group.events` subscriber - which unsubscribes it, silently\r\n // ending all diffing for the life of the form.\r\n const replacement: AbstractControl | undefined =\r\n Object.prototype.hasOwnProperty.call(live, name)\r\n ? live[name]\r\n : undefined;\r\n\r\n if (replacement === channel.control) {\r\n continue;\r\n }\r\n\r\n if (replacement === undefined) {\r\n close(name, channel);\r\n continue;\r\n }\r\n\r\n channel.control = replacement;\r\n channel.instances.next(replacement);\r\n }\r\n\r\n for (const [name, control] of Object.entries(live)) {\r\n if (!channels.has(name)) {\r\n open(name, control);\r\n }\r\n }\r\n };\r\n\r\n for (const [name, control] of Object.entries(controls())) {\r\n open(name, control);\r\n }\r\n\r\n // After the channels, deliberately: with the injector missing - which the\r\n // type forbids but a JavaScript caller can still do - `toSignal` above\r\n // throws NG0203 naming the injection context, where reading `DestroyRef` off\r\n // `undefined` first would throw a `TypeError` naming nothing.\r\n //\r\n // `group.events` and not `valueChanges`: `setControl` / `addControl` /\r\n // `removeControl` all reach `updateValueAndValidity`, so the control set\r\n // changing is observable here.\r\n //\r\n // It fires on ordinary interaction too, and on more than one event apiece -\r\n // value, status, pristine, touched and reset are all `ControlEvent`s - so\r\n // this runs several O(N) diffs per interaction rather than one. That is the\r\n // price of not handing the consumer a `rebind()` they will forget to call,\r\n // and there is no narrower signal available: Angular's collection-change\r\n // callback is private. Nothing per-node is touched, so eval-core's\r\n // performance gate is unaffected.\r\n group.events\r\n .pipe(takeUntilDestroyed(options.injector.get(DestroyRef)))\r\n .subscribe(() => sync());\r\n\r\n return source;\r\n};\r\n","import {\r\n DestroyRef,\r\n EnvironmentInjector,\r\n Injector,\r\n computed,\r\n createEnvironmentInjector,\r\n inject,\r\n} from '@angular/core';\r\nimport { AbstractControl, FormControl, FormGroup } from '@angular/forms';\r\nimport {\r\n ExpressionErrorPolicy,\r\n createFieldContext,\r\n toText,\r\n toVisible,\r\n} from '@zvenigora/ng-eval-forms';\r\nimport { EvalSignal, createEvalSignal } from '@zvenigora/ng-eval-signals';\r\nimport { createControlSource } from './control-source';\r\n\r\n/**\r\n * One field's rules, as strings resolved at runtime.\r\n *\r\n * This is the whole descriptor. It is deliberately not a schema *language*:\r\n * `visible` and `text` are the two properties this phase ships (plan S 3.6),\r\n * `disabled` is deferred with three reasons rather than a shrug, and\r\n * `required` is deferred with the validators.\r\n */\r\nexport interface FieldSchema {\r\n\r\n /**\r\n * The field's name, and the key its control is mirrored under.\r\n *\r\n * It does not have to name a control: a rule naming a field the group has\r\n * no control for resolves `undefined`, which is S 3.4.3's ordinary case\r\n * rather than an error. It may not be a member of `Object.prototype` -\r\n * see {@link bindFieldProperties}.\r\n */\r\n readonly name: string;\r\n\r\n /** An expression whose value is coerced by truthiness (`toVisible`). */\r\n readonly visible?: string;\r\n\r\n /** An expression whose value is coerced to a string (`toText`). */\r\n readonly text?: string;\r\n}\r\n\r\n/**\r\n * The signals bound for one field - one per rule the schema supplied.\r\n *\r\n * Typed as `EvalSignal`, not `Signal`, and both extra members are load-bearing\r\n * on this path (plan S 5): `invalidate()` is trap 3's hatch for a\r\n * `{ emitEvent: false }` write and S 3.4.2's hatch for a key-set change, and\r\n * `destroy()` is what step 5's teardown counts. Typing these as `Signal` would\r\n * put both out of reach through the published surface.\r\n *\r\n * The type arguments are `boolean` and `string` rather than S 5's original\r\n * `unknown`, **because the coercion of S 3.6 happens here**. That section was\r\n * written before the coercion's placement was settled and `coercion.ts` states\r\n * the settlement - \"both adapters call this\" - so `unknown` would now describe\r\n * the value the property is built *from* rather than the one it answers with.\r\n */\r\nexport interface FieldProperties {\r\n readonly visible?: EvalSignal<boolean>;\r\n readonly text?: EvalSignal<string>;\r\n}\r\n\r\n/**\r\n * What {@link bindFieldProperties} returns: the bound fields, and the one call\r\n * that ends them.\r\n *\r\n * **Nested rather than a `destroy` written onto the record** (plan S 5's step-5\r\n * amendment). The record is keyed by *field name*, `destroy` is a legal field\r\n * name, and S 0's premise is that those names arrive from a server rather than\r\n * from the consumer - so a flat shape would put a silent collision between the\r\n * consumer's data and this library's API in precisely the case where the\r\n * consumer controls the names least.\r\n */\r\nexport interface FormBinding {\r\n\r\n /** The bound properties, keyed by field name. */\r\n readonly fields: Record<string, FieldProperties>;\r\n\r\n /**\r\n * Destroys every `EvalSignal` this binding created and releases the mirror.\r\n *\r\n * **Call it.** Every signal here is built with an explicit injector, and\r\n * that means no `DestroyRef` registration of its own (`eval-signal.ts`) - so\r\n * nothing auto-destroys and all N x M are the caller's (plan S 3.7).\r\n *\r\n * There is one net under that, and it is a net rather than a substitute: the\r\n * binding registers this on the `DestroyRef` of the injector it was given,\r\n * so a binding wired to a component or route injector is released when that\r\n * injector dies even if nobody called it. It is not a licence to skip the\r\n * call - a binding built on the root injector is released at the end of the\r\n * application and no sooner.\r\n *\r\n * Idempotent, and that is load-bearing rather than polite: the scope is an\r\n * `EnvironmentInjector`, and `R3Injector.destroy()` throws NG0205 when it\r\n * has already run.\r\n */\r\n destroy(): void;\r\n}\r\n\r\n/**\r\n * Puts a coercion in front of an `EvalSignal` without losing the two members\r\n * that make it one.\r\n *\r\n * A second `computed` rather than a coercion inside the walk: the expression\r\n * is the consumer's and its *value* is what the error policy and the\r\n * dependency tracking are defined over, so the coercion belongs strictly\r\n * downstream of both. It is also what keeps a destroyed signal - which reads\r\n * `undefined` - answering `false` / `''` rather than leaking `undefined` into\r\n * a template.\r\n *\r\n * `Object.assign` then `defineProperty`, mirroring `createEvalSignal`'s own\r\n * construction: `assign` copies a getter's *current value*, which would freeze\r\n * `dependencies` at the empty set it holds before the first recompute.\r\n */\r\nconst coerce = <T>(\r\n inner: EvalSignal<unknown>,\r\n map: (value: unknown) => T\r\n): EvalSignal<T> => {\r\n\r\n const outer = Object.assign(computed(() => map(inner())), {\r\n invalidate: () => inner.invalidate(),\r\n destroy: () => inner.destroy(),\r\n }) as EvalSignal<T>;\r\n\r\n Object.defineProperty(outer, 'dependencies', {\r\n get: () => inner.dependencies,\r\n enumerable: true,\r\n configurable: true,\r\n });\r\n\r\n return outer;\r\n};\r\n\r\n/**\r\n * The three checks of open question 8.3, plus 8.5's half.\r\n *\r\n * Three checks, and the count is the boundary rather than a starting point.\r\n * Each catches something that is otherwise either silent or reported far from\r\n * its cause, and none of them is a rule about what an *expression* may say -\r\n * that is `eval-core`'s and is already answered.\r\n */\r\nconst validate = (schema: readonly FieldSchema[], group: FormGroup): void => {\r\n\r\n const seen = new Set<string>();\r\n\r\n for (const field of schema) {\r\n\r\n if (seen.has(field.name)) {\r\n throw new Error(\r\n `Duplicate field name '${field.name}' in the schema: ` +\r\n `each field is bound once, so the second would silently replace the first.`\r\n );\r\n }\r\n seen.add(field.name);\r\n\r\n // The check no other layer can make (S 3.4.3's third precedence layer).\r\n // A server-supplied field named `constructor`, `toString`, `valueOf` or\r\n // `__proto__` is legal to `FormGroup`, which rejects only keys containing\r\n // a dot - and when no control backs it, `EvalContext.get` consults\r\n // `original` first and `getContextValue` reads a plain object as a bare\r\n // property access, so the name resolves off `Object.prototype`. Every rule\r\n // reading it then gets a *function*, which is truthy: `visible` renders\r\n // precisely the field that has no data, with no error anywhere.\r\n //\r\n // It is not containable downstream - the resolution happens above this\r\n // library, in `createSignalContext`'s `original`, and S 2 forbids the\r\n // upstream fix - and this is the only layer that can name the offending\r\n // field in the message.\r\n if (Object.prototype.hasOwnProperty.call(Object.prototype, field.name)) {\r\n throw new Error(\r\n `Field name '${field.name}' is a member of Object.prototype and cannot be ` +\r\n `resolved reliably: an expression naming it reads the prototype's value ` +\r\n `whenever no control backs it. Rename the field.`\r\n );\r\n }\r\n\r\n for (const property of ['visible', 'text'] as const) {\r\n const rule = field[property];\r\n\r\n if (rule !== undefined && typeof rule !== 'string') {\r\n throw new Error(\r\n `Field '${field.name}': ${property} must be a string expression, ` +\r\n `not ${typeof rule}.`\r\n );\r\n }\r\n }\r\n }\r\n\r\n // Checked over the *group*, not over the schema's names, because the mirror\r\n // covers every control (S 3.5) - so a control nobody named still reaches\r\n // every field's context, where an expression finds it.\r\n //\r\n // Construction-time only, and that is a limitation rather than a guarantee:\r\n // `addControl` afterwards reaches `sync` -> `open` with no check, and\r\n // throwing from there is not available - a throw inside the `group.events`\r\n // subscriber unsubscribes it and silently ends all diffing for the life of\r\n // the form (S 3.5.5). The README says so.\r\n for (const [name, control] of Object.entries(\r\n group.controls as Record<string, AbstractControl>\r\n )) {\r\n // The same check as above, over the other source of names rather than a\r\n // fourth one - and the mirror does **not** rescue this case, which is\r\n // easy to believe and wrong. The mirror does define an own accessor for\r\n // `constructor` on its record, but the record is reached through\r\n // `lookups`, step 4 of `EvalContext.get`, while `original` - the empty\r\n // field source - is read at step 2 with a bare property access. Measured:\r\n // the accessor is present and the expression still reads\r\n // `function Object() { [native code] }`. So a control named off\r\n // `Object.prototype` is unreadable through every expression, silently and\r\n // truthily, and worse than the schema case because the value exists.\r\n if (Object.prototype.hasOwnProperty.call(Object.prototype, name)) {\r\n throw new Error(\r\n `Control '${name}' is a member of Object.prototype and cannot be read by any ` +\r\n `expression: the name resolves to the prototype's value before the form is ` +\r\n `ever consulted. Rename the control.`\r\n );\r\n }\r\n\r\n // Flat forms only (S 2, open question 8.5): a thrown error rather than a\r\n // documented limitation, because the alternative surfaces as a confusing\r\n // evaluation result far from its cause - a nested group's *aggregate\r\n // object* arriving where a value was expected.\r\n if (!(control instanceof FormControl)) {\r\n throw new Error(\r\n `Control '${name}' is not a FormControl. Nested groups and FormArrays are ` +\r\n `out of scope for this phase.`\r\n );\r\n }\r\n }\r\n};\r\n\r\n/**\r\n * Binds a schema to a `FormGroup`, producing one `EvalSignal` per rule.\r\n *\r\n * ```ts\r\n * const binding = bindFieldProperties(\r\n * [{ name: 'state', visible: \"country === 'US'\" }],\r\n * form,\r\n * { injector }\r\n * );\r\n *\r\n * binding.fields['state'].visible(); // recomputes when `country` changes\r\n *\r\n * binding.destroy(); // yours to call - nothing here\r\n * // auto-destroys (S 3.7)\r\n * ```\r\n *\r\n * **One `EvalContext` per field, never one shared** (plan S 3.4.1). `get`\r\n * resolves `scopes` and `original` *before* `lookups`, and an arrow function's\r\n * parameter scope sits on the context for as long as its body runs - so under\r\n * a shared context one field's arrow would shadow every other field's key of\r\n * the same name for that window, and anything that strands a scope rather than\r\n * popping it would do so for the life of the form. The count is N contexts for\r\n * N fields, and not N x M: the properties of one field resolve against the\r\n * same names.\r\n *\r\n * **The field half of each context is empty in this phase.**\r\n * `createFieldContext` takes two sources and the second is `{}` here. What a\r\n * field-local key set should *contain* is specified nowhere - S 3.4.3's\r\n * \"a field named `value`, `name` or `index`\" illustrates collision semantics\r\n * rather than naming keys - and inventing three keys to fill a parameter is\r\n * how a public surface acquires members nobody chose. The consequence, stated\r\n * rather than left to be found: S 3.4.3's precedence rule is asserted at the\r\n * core in `field-context.spec.ts` and ships **untested end to end**, because\r\n * no `/reactive` path produces a field-local key. It gets settled by whichever\r\n * phase first has a consumer for one.\r\n *\r\n * **`createControlSource` is called once, for the whole form.** Per field\r\n * would build N mirrors over one group: N x the live `toSignal` subscriptions\r\n * step 5 counts, and trap 5's O(N) diff running N times per `group.events`\r\n * emission, on a signal that already fires several times per interaction.\r\n *\r\n * **Constructed, not computed.** `toSignal` opens with\r\n * `assertNotInReactiveContext`, so calling this from inside an `effect()` or a\r\n * `computed()` throws out of the mirror with an error naming `toSignal` and\r\n * nothing naming this library. A form binding belongs in a service or a\r\n * factory, which is the same premise `injector` being required rests on.\r\n *\r\n * **Teardown runs off a child injector, and there is only one of them**\r\n * (S 3.7, amended in step 5). The binding opens an `EnvironmentInjector` under\r\n * the caller's and scopes *both* the mirror and every `createEvalSignal` to it,\r\n * so `destroy()` releases the whole mirror - every per-key `toSignal`\r\n * subscription and the `group.events` one - in a single call, and a bind that\r\n * throws part-way through releases exactly the same thing however far it got.\r\n * The plan's earlier premise, that releasing the mirror needed a handle\r\n * `createControlSource` does not return, was wrong: step 3 had already measured\r\n * that destroying the injector drops every control's observer count to 0 and\r\n * ends the diffing.\r\n *\r\n * The `EvalSignal`s are **not** covered by that, and that is the point of the\r\n * count: built with an explicit injector, they take no `DestroyRef`\r\n * registration, so `destroy()` walks them itself and N x M of them is what a\r\n * spec asserts rather than \"the form's destroy ran\".\r\n *\r\n * @param schema - The fields to bind. Validated at construction: a duplicate\r\n * name, a non-string rule, a name that resolves off\r\n * `Object.prototype`, and a control that is not a\r\n * `FormControl` all throw here rather than surfacing later as\r\n * an evaluation result nobody can trace.\r\n * @param group - A flat `FormGroup` of `FormControl`s.\r\n * @param options - `injector` is **required**, not optional: S 3.7's argument\r\n * is that every `EvalSignal` here has no auto-teardown, and\r\n * an optional injector would silently vary that for a call\r\n * that happened to sit in an injection context. `onError`\r\n * defaults to `'undefined'` - the opposite of\r\n * `eval-signals`' default, and resolved *here* rather than\r\n * forwarded absent, since `createEvalSignal` would otherwise\r\n * supply `'throw'` (S 3.4.4).\r\n * @returns The bound properties keyed by field name, and the `destroy()` that\r\n * ends them.\r\n */\r\nexport const bindFieldProperties = (\r\n schema: readonly FieldSchema[],\r\n group: FormGroup,\r\n options: { injector: Injector; onError?: ExpressionErrorPolicy }\r\n): FormBinding => {\r\n\r\n // Before the mirror, deliberately. A schema rejection then opens no\r\n // subscription at all, which is a stronger statement than \"the catch below\r\n // releases it\" and is asserted separately.\r\n validate(schema, group);\r\n\r\n // `?.` and the `inject` fallback are not defensive dressing: the type makes\r\n // `injector` required, but a JavaScript caller can omit it, and reading\r\n // `.get` off `undefined` is a `TypeError` naming nothing. `toSignal` solves\r\n // the same problem the same way (`rxjs-interop.mjs:106`), and it is what\r\n // keeps the error a caller sees NG0203 - naming the injection context -\r\n // rather than a property read on `undefined`. `control-source.ts` orders\r\n // its own statements for that error; this preserves it one layer up.\r\n const parent =\r\n options?.injector?.get(EnvironmentInjector) ?? inject(EnvironmentInjector);\r\n\r\n // The other arm of the same problem, and the fallback above is what opens\r\n // it: *inside* an injection context that fallback succeeds, so a caller who\r\n // omitted the required `injector` would get a working binding parented at\r\n // the ambient environment injector - with no auto-teardown and no\r\n // diagnostic. That is the exact silent variation S 3.7 makes `injector`\r\n // required to prevent, so it is rejected here by name rather than left to\r\n // surface as a leak. Outside a context the line above has already thrown\r\n // NG0203 and this is unreachable.\r\n if (!options?.injector) {\r\n throw new Error(\r\n `bindFieldProperties requires an 'injector': every signal it creates is built ` +\r\n `with one and therefore takes no DestroyRef registration, so an ambient ` +\r\n `injection context would silently vary when teardown runs.`\r\n );\r\n }\r\n\r\n const scope = createEnvironmentInjector([], parent);\r\n\r\n const onError = options.onError ?? 'undefined';\r\n\r\n const fields: Record<string, FieldProperties> = {};\r\n\r\n // The signals, in creation order, so the `catch` below releases exactly what\r\n // exists rather than walking `fields` - which holds nothing for the field\r\n // that threw and nothing at all for a throw in `createControlSource`.\r\n const created: EvalSignal<unknown>[] = [];\r\n\r\n let destroyed = false;\r\n\r\n // The registration on the *caller's* injector, held so `destroy()` can drop\r\n // it. See below for why it exists at all.\r\n let unregister: (() => void) | undefined;\r\n\r\n const destroy = (): void => {\r\n\r\n if (destroyed) {\r\n return;\r\n }\r\n\r\n destroyed = true;\r\n\r\n // Cleared before it is called, so a teardown driven *by* the caller's\r\n // injector does not turn around and mutate the hook list that injector is\r\n // iterating - `eval-signal.ts:408-414`'s pattern and its reason.\r\n const release = unregister;\r\n unregister = undefined;\r\n release?.();\r\n\r\n try {\r\n // Called through the property rather than a captured reference: it is\r\n // the object the caller holds, and `coerce`'s `destroy` delegates to the\r\n // inner signal, so this is the one call that reaches both.\r\n for (const signal of created) {\r\n signal.destroy();\r\n }\r\n } finally {\r\n // In a `finally` because `destroyed` is already set: a signal whose\r\n // `destroy` throws - a consumer calling this from inside a `computed()`\r\n // reaches NG0600 - would otherwise leave the scope alive with the retry\r\n // guarded into a no-op, and every mirror subscription live with no way\r\n // back.\r\n //\r\n // Last, and idempotence is why the guard above exists at all:\r\n // `R3Injector.destroy()` opens with `assertNotDestroyed` and throws\r\n // NG0205 on a second call.\r\n scope.destroy();\r\n }\r\n };\r\n\r\n // **The scope is detached, not merely shorter-lived.**\r\n // `createEnvironmentInjector` does not register the child with its parent's\r\n // destroy hooks, so without this the mirror would outlive the injector the\r\n // caller scoped it to - and step 4, which passed that injector to\r\n // `createControlSource` directly, released everything when it died. A\r\n // consumer who wires a binding to a component or route injector and forgets\r\n // `destroy()` would otherwise retain N subscriptions, the mirror, the N\r\n // contexts and the form itself for the life of the root injector, silently.\r\n // `destroy()` stays the documented call; this is the net under it.\r\n unregister = options.injector.get(DestroyRef).onDestroy(() => {\r\n unregister = undefined;\r\n destroy();\r\n });\r\n\r\n try {\r\n\r\n // Scoped to the child, not to the caller's injector: it is what makes the\r\n // mirror's two subscriptions releasable at all without a handle.\r\n const formSource = createControlSource(group, { injector: scope });\r\n\r\n for (const field of schema) {\r\n\r\n const context = createFieldContext(formSource, {});\r\n const properties: { visible?: EvalSignal<boolean>; text?: EvalSignal<string> } = {};\r\n\r\n // `options.injector`, **not** the scope, and the distinction is not\r\n // cosmetic: `createEvalSignal` uses the injector it is given only to\r\n // resolve `CompilerService` (`eval-signal.ts:215-216`) - it registers no\r\n // teardown against it - and the scope's parent is\r\n // `caller.get(EnvironmentInjector)`, which for a node injector is an\r\n // *ancestor*. Passing the scope would therefore skip providers the\r\n // caller declared, silently, and diverge from step 4 for no gain. The\r\n // scope exists for the one thing that does need a releasable lifetime,\r\n // which is the mirror.\r\n if (field.visible !== undefined) {\r\n properties.visible = coerce(\r\n createEvalSignal(field.visible, context, {\r\n injector: options.injector,\r\n onError,\r\n }),\r\n toVisible\r\n );\r\n created.push(properties.visible);\r\n }\r\n\r\n if (field.text !== undefined) {\r\n properties.text = coerce(\r\n createEvalSignal(field.text, context, {\r\n injector: options.injector,\r\n onError,\r\n }),\r\n toText\r\n );\r\n created.push(properties.text);\r\n }\r\n\r\n fields[field.name] = properties;\r\n }\r\n\r\n } catch (error) {\r\n // A *parse* error is the reachable case (S 3.4.4's amendment):\r\n // `createEvalSignal` compiles eagerly, so `visible: 'country ==='` throws\r\n // from inside this loop with the mirror already open - and the caller\r\n // holds no handle to any of it, because the binding never returned.\r\n destroy();\r\n throw error;\r\n }\r\n\r\n return { fields, destroy };\r\n};\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;;;;;AA+BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDG;MACU,mBAAmB,GAAG,CACjC,KAAgB,EAChB,OAA+B,KACR;IAEvB,MAAM,MAAM,GAAwB,EAAE;AACtC,IAAA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA0B;;;IAIlD,MAAM,QAAQ,GAAG,MACf,KAAK,CAAC,QAA2C;AAEnD,IAAA,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,OAAwB,KAAU;AAE5D,QAAA,MAAM,OAAO,GAAmB;YAC9B,OAAO;YACP,SAAS,EAAE,IAAI,OAAO,EAAmB;YACzC,QAAQ,EAAE,IAAI,OAAO,EAAQ;SAC9B;;;;;;;;;;;QAYD,MAAM,OAAO,GAAoB,QAAQ,CACvC,OAAO,CAAC,SAAS,CAAC,IAAI,CACpB,SAAS,CAAC,OAAO,CAAC,EAClB,SAAS,CAAC,CAAC,QAAQ,KACjB,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CACtD,EACD,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAC5B,EACD,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,CAClD;AAED,QAAA,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE;YAClC,GAAG,EAAE,MAAK;AACR,gBAAA,OAAO,EAAE;AACT,gBAAA,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK;YAC9B,CAAC;AACD,YAAA,UAAU,EAAE,IAAI;AAChB,YAAA,YAAY,EAAE,IAAI;AACnB,SAAA,CAAC;;;;;;;AAQF,QAAA,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC;AAC7B,IAAA,CAAC;AAED,IAAA,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,OAAuB,KAAU;AAC5D,QAAA,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE;AACvB,QAAA,OAAO,CAAC,QAAQ,CAAC,QAAQ,EAAE;AAC3B,QAAA,OAAO,CAAC,SAAS,CAAC,QAAQ,EAAE;AAC5B,QAAA,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC;AACrB,QAAA,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,IAAA,CAAC;;;;IAKD,MAAM,IAAI,GAAG,MAAW;AAEtB,QAAA,MAAM,IAAI,GAAG,QAAQ,EAAE;QAEvB,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,QAAQ,EAAE;;;;;;;;;;AAUtC,YAAA,MAAM,WAAW,GACf,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI;AAC7C,kBAAE,IAAI,CAAC,IAAI;kBACT,SAAS;AAEf,YAAA,IAAI,WAAW,KAAK,OAAO,CAAC,OAAO,EAAE;gBACnC;YACF;AAEA,YAAA,IAAI,WAAW,KAAK,SAAS,EAAE;AAC7B,gBAAA,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC;gBACpB;YACF;AAEA,YAAA,OAAO,CAAC,OAAO,GAAG,WAAW;AAC7B,YAAA,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC;QACrC;AAEA,QAAA,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;YAClD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;AACvB,gBAAA,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC;YACrB;QACF;AACF,IAAA,CAAC;AAED,IAAA,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,EAAE;AACxD,QAAA,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC;IACrB;;;;;;;;;;;;;;;;;AAkBA,IAAA,KAAK,CAAC;AACH,SAAA,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;AACzD,SAAA,SAAS,CAAC,MAAM,IAAI,EAAE,CAAC;AAE1B,IAAA,OAAO,MAAM;AACf;;AClHA;;;;;;;;;;;;;;AAcG;AACH,MAAM,MAAM,GAAG,CACb,KAA0B,EAC1B,GAA0B,KACT;AAEjB,IAAA,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE;AACxD,QAAA,UAAU,EAAE,MAAM,KAAK,CAAC,UAAU,EAAE;AACpC,QAAA,OAAO,EAAE,MAAM,KAAK,CAAC,OAAO,EAAE;AAC/B,KAAA,CAAkB;AAEnB,IAAA,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,cAAc,EAAE;AAC3C,QAAA,GAAG,EAAE,MAAM,KAAK,CAAC,YAAY;AAC7B,QAAA,UAAU,EAAE,IAAI;AAChB,QAAA,YAAY,EAAE,IAAI;AACnB,KAAA,CAAC;AAEF,IAAA,OAAO,KAAK;AACd,CAAC;AAED;;;;;;;AAOG;AACH,MAAM,QAAQ,GAAG,CAAC,MAA8B,EAAE,KAAgB,KAAU;AAE1E,IAAA,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU;AAE9B,IAAA,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE;QAE1B,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE;AACxB,YAAA,MAAM,IAAI,KAAK,CACb,yBAAyB,KAAK,CAAC,IAAI,CAAA,iBAAA,CAAmB;AACtD,gBAAA,CAAA,yEAAA,CAA2E,CAC5E;QACH;AACA,QAAA,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;;;;;;;;;;;;;;AAepB,QAAA,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE;AACtE,YAAA,MAAM,IAAI,KAAK,CACb,eAAe,KAAK,CAAC,IAAI,CAAA,gDAAA,CAAkD;gBAC3E,CAAA,uEAAA,CAAyE;AACzE,gBAAA,CAAA,+CAAA,CAAiD,CAClD;QACH;QAEA,KAAK,MAAM,QAAQ,IAAI,CAAC,SAAS,EAAE,MAAM,CAAU,EAAE;AACnD,YAAA,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC;YAE5B,IAAI,IAAI,KAAK,SAAS,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE;gBAClD,MAAM,IAAI,KAAK,CACb,CAAA,OAAA,EAAU,KAAK,CAAC,IAAI,CAAA,GAAA,EAAM,QAAQ,CAAA,8BAAA,CAAgC;AAClE,oBAAA,CAAA,IAAA,EAAO,OAAO,IAAI,CAAA,CAAA,CAAG,CACtB;YACH;QACF;IACF;;;;;;;;;;AAWA,IAAA,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAC1C,KAAK,CAAC,QAA2C,CAClD,EAAE;;;;;;;;;;;AAWD,QAAA,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE;AAChE,YAAA,MAAM,IAAI,KAAK,CACb,CAAA,SAAA,EAAY,IAAI,CAAA,4DAAA,CAA8D;gBAC9E,CAAA,0EAAA,CAA4E;AAC5E,gBAAA,CAAA,mCAAA,CAAqC,CACtC;QACH;;;;;AAMA,QAAA,IAAI,EAAE,OAAO,YAAY,WAAW,CAAC,EAAE;AACrC,YAAA,MAAM,IAAI,KAAK,CACb,CAAA,SAAA,EAAY,IAAI,CAAA,yDAAA,CAA2D;AAC3E,gBAAA,CAAA,4BAAA,CAA8B,CAC/B;QACH;IACF;AACF,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+EG;AACI,MAAM,mBAAmB,GAAG,CACjC,MAA8B,EAC9B,KAAgB,EAChB,OAAgE,KACjD;;;;AAKf,IAAA,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;;;;;;;;AASvB,IAAA,MAAM,MAAM,GACV,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,mBAAmB,CAAC,IAAI,MAAM,CAAC,mBAAmB,CAAC;;;;;;;;;AAU5E,IAAA,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE;QACtB,MAAM,IAAI,KAAK,CACb,CAAA,6EAAA,CAA+E;YAC/E,CAAA,uEAAA,CAAyE;AACzE,YAAA,CAAA,yDAAA,CAA2D,CAC5D;IACH;IAEA,MAAM,KAAK,GAAG,yBAAyB,CAAC,EAAE,EAAE,MAAM,CAAC;AAEnD,IAAA,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,WAAW;IAE9C,MAAM,MAAM,GAAoC,EAAE;;;;IAKlD,MAAM,OAAO,GAA0B,EAAE;IAEzC,IAAI,SAAS,GAAG,KAAK;;;AAIrB,IAAA,IAAI,UAAoC;IAExC,MAAM,OAAO,GAAG,MAAW;QAEzB,IAAI,SAAS,EAAE;YACb;QACF;QAEA,SAAS,GAAG,IAAI;;;;QAKhB,MAAM,OAAO,GAAG,UAAU;QAC1B,UAAU,GAAG,SAAS;QACtB,OAAO,IAAI;AAEX,QAAA,IAAI;;;;AAIF,YAAA,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE;gBAC5B,MAAM,CAAC,OAAO,EAAE;YAClB;QACF;gBAAU;;;;;;;;;;YAUR,KAAK,CAAC,OAAO,EAAE;QACjB;AACF,IAAA,CAAC;;;;;;;;;;AAWD,IAAA,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,MAAK;QAC3D,UAAU,GAAG,SAAS;AACtB,QAAA,OAAO,EAAE;AACX,IAAA,CAAC,CAAC;AAEF,IAAA,IAAI;;;AAIF,QAAA,MAAM,UAAU,GAAG,mBAAmB,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAElE,QAAA,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE;YAE1B,MAAM,OAAO,GAAG,kBAAkB,CAAC,UAAU,EAAE,EAAE,CAAC;YAClD,MAAM,UAAU,GAAiE,EAAE;;;;;;;;;;AAWnF,YAAA,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,EAAE;AAC/B,gBAAA,UAAU,CAAC,OAAO,GAAG,MAAM,CACzB,gBAAgB,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE;oBACvC,QAAQ,EAAE,OAAO,CAAC,QAAQ;oBAC1B,OAAO;iBACR,CAAC,EACF,SAAS,CACV;AACD,gBAAA,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;YAClC;AAEA,YAAA,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE;AAC5B,gBAAA,UAAU,CAAC,IAAI,GAAG,MAAM,CACtB,gBAAgB,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE;oBACpC,QAAQ,EAAE,OAAO,CAAC,QAAQ;oBAC1B,OAAO;iBACR,CAAC,EACF,MAAM,CACP;AACD,gBAAA,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAC/B;AAEA,YAAA,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,UAAU;QACjC;IAEF;IAAE,OAAO,KAAK,EAAE;;;;;AAKd,QAAA,OAAO,EAAE;AACT,QAAA,MAAM,KAAK;IACb;AAEA,IAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;AAC5B;;ACzdA;;AAEG;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"zvenigora-ng-eval-forms-signals.mjs","sources":["../../../../modules/eval-forms/signals/src/lib/evaluate-rule.ts","../../../../modules/eval-forms/signals/src/lib/guard-identifiers.ts","../../../../modules/eval-forms/signals/src/lib/model-source.ts","../../../../modules/eval-forms/signals/src/lib/text-key.ts","../../../../modules/eval-forms/signals/src/lib/rules.ts","../../../../modules/eval-forms/signals/src/zvenigora-ng-eval-forms-signals.ts"],"sourcesContent":["import { EvalContext, EvalOptions, EvalState, call, stateCallback } from '@zvenigora/ng-eval-core';\r\n\r\n/**\r\n * Runs one compiled rule against one context, and leaves the context's scope\r\n * stack exactly as it found it.\r\n *\r\n * **This is the entry point's only path to the walk, and that is the whole\r\n * point of it** (plan S 3.3, `phase-4-plan.md` S 9.1). Every walk needs an\r\n * `EvalState`, and outside Angular DI there are exactly two ways to get one:\r\n * the static factory this function calls below, and the class's public\r\n * constructor. The one construction in this package is that line, inside this\r\n * function's body; S 6 gate 3 greps for both spellings and expects one hit and\r\n * zero.\r\n *\r\n * **Neither spelling is written out in this comment, deliberately.** Gate 3 is\r\n * a grep, and prose naming what it searches for is a standing hit on the file\r\n * the gate exists to bless - which is what revision 10 spent a revision\r\n * removing from that gate's third row, after a block comment in\r\n * `reactive/src/lib/field-schema.ts` made it read as failing. A gate with a\r\n * known pre-existing hit is one people learn to ignore.\r\n *\r\n * **Module-private to `/signals`, deliberately, and not merely unexported by\r\n * omission.** Placing it in the shared core was measured against this and\r\n * separates on one row that matters: a barrel re-exports whole modules, so a\r\n * helper the core's `public-api.ts` can reach **is published**, permanently,\r\n * on an entry point already released at 0.1.0 - and a published `evaluateRule`\r\n * *is* a second path to the walk by existing, since any consumer could call it\r\n * with a hand-built `EvalContext`. Here the set of callers is the set of files\r\n * in this directory, which is checkable by grep. The three placement\r\n * hypotheses that sound decisive - `/reactive` bundle weight, `@angular/core`\r\n * leakage, FESM size - were measured and are not: ng-packagr compiles each\r\n * entry point separately and the helper appears in `/reactive`'s FESM at zero\r\n * bytes either way (S 3.3's table).\r\n *\r\n * **Why the `finally` exists.** The scope stack is the third and longest-lived\r\n * of `eval-core`'s three stack invariants ([`CLAUDE.md`](../../../../../CLAUDE.md)):\r\n * unlike the value stack and the open-node stack, which live on the per-walk\r\n * `EvalState` and die with it, scopes live on the `EvalContext`. A rule holds\r\n * its context across every invocation Angular makes, and `EvalContext.get`\r\n * resolves `scopes` **first**, so one scope left behind shadows the source key\r\n * of that name for the life of the form. Nothing else drains it.\r\n *\r\n * **Retained, not redundant.** This was written against a specific defect -\r\n * `arrow-function-expression.ts` pushed a scope and popped it with no\r\n * `try`/`finally`, so an arrow body that threw skipped the pop - and Phase 2\r\n * step 0 fixed exactly that\r\n * ([backlog A9](../../../../../docs/backlog-retired.md#a9)). The loop stays anyway, for\r\n * two reasons that outlive the fix - and they are not the same *kind* of\r\n * reason, which is the part an earlier version of this docblock got wrong:\r\n *\r\n * - The `@zvenigora/ng-eval-core` peer range in `modules/eval-forms/package.json`\r\n * admits the *leaking* 0.3.0, so a supported installation can still be\r\n * running the defect. **Range-dependent**: it would stop being true if the\r\n * range's lower bound were ever raised past 0.3.0.\r\n * - `EvalContext.push` and `pop` are public methods on a published class: a\r\n * scope can be stranded with no visitor involved at all. That is the route\r\n * `evaluate-rule.spec.ts`'s containment cases now drive, because it is the\r\n * one no fix inside the core's visitors can close. **True at every version**,\r\n * and therefore the reason this loop is not removable at any peer range.\r\n *\r\n * **Raising the peer range does not make this removable**, and the sentence\r\n * that used to sit here said it did - \"removal is gated on raising the peer\r\n * range\" reads as a sufficient condition and is only a necessary one. Whoever\r\n * raises it retires the first reason and leaves the second untouched. The same\r\n * error was in the plan's step 0b, corrected there in Phase 2 step 7; this copy\r\n * of it was corrected in step 8.\r\n *\r\n * A third reason has expired and is recorded as gone rather than silently\r\n * dropped: this was also the backstop for the scope-push sites Phase 2 was\r\n * adding to the core. `Program`, `BlockStatement` and `ForStatement` all\r\n * shipped in 0.4.0, each popping in a `finally`.\r\n *\r\n * The unwind is a loop to a **depth mark**, not a single `pop()`: one throw\r\n * can leave more than one scope open, and unwinding to the bottom would drain\r\n * scopes this call did not push - `evaluate()` is re-entrant through the arrow\r\n * closure, so the depth on entry is not reliably zero.\r\n *\r\n * This helper deliberately does **not** apply the error policy. The throw\r\n * propagates, and `applyErrorPolicy` sits outside this call in the `LogicFn`\r\n * body (S 3.5) - containment first, then the policy - so an expression that\r\n * throws is contained whether the consumer asked for `'throw'`, `'undefined'`\r\n * or a handler.\r\n *\r\n * @param compiled - `eval-core`'s own `stateCallback`, from\r\n * `compile(parse(expression, ...))`. A registrar compiles\r\n * once and holds the callback; nothing recompiles per\r\n * invocation.\r\n * @param context - The rule's context, from `ModelSource.createRuleContext()`.\r\n * One per rule per `form()` (S 3.6), reused across every\r\n * invocation of that rule.\r\n * @param options - The walk's options, which are **not** the context's:\r\n * visitors read `caseInsensitive` off the state, so it has to\r\n * reach both to correct property names as well as identifier\r\n * keys.\r\n */\r\nexport const evaluateRule = (\r\n compiled: stateCallback,\r\n context: EvalContext,\r\n options?: EvalOptions\r\n): unknown => {\r\n\r\n const depth = context.scopes.length;\r\n\r\n // `fromContext` short-circuits on identity for an `EvalContext`, so this\r\n // builds a fresh per-walk state around the *same* context rather than\r\n // copying it - which is what lets one context back many invocations, and\r\n // equally what makes a leaked scope durable.\r\n const state = EvalState.fromContext(context, options);\r\n\r\n try {\r\n return call(compiled, state);\r\n } finally {\r\n while (context.scopes.length > depth) {\r\n context.pop();\r\n }\r\n }\r\n};\r\n","import type { parse } from '@zvenigora/ng-eval-core';\r\nimport { simple } from 'acorn-walk';\r\n\r\n/**\r\n * Throws if the expression names an identifier that is an own property of\r\n * `Object.prototype` (plan S 3.8).\r\n *\r\n * **The failure this prevents is silent and truthy.** `createSignalContext`\r\n * builds its context on an empty `original` object, and `EvalContext.get`\r\n * consults `original` *before* `lookups` - so an identifier like\r\n * `constructor` resolves off the prototype and never reaches the model\r\n * resolver at all. A function is truthy, so `toVisible` says visible and\r\n * `evalVisible(p.city, 'constructor')` renders the field with no data, no\r\n * error and nothing logged (Q10, Q11). It is not GHSA-pj3p-xpg7-h7gw's\r\n * case-variant bypass: the behaviour is identical with and without\r\n * `caseInsensitive`, and `CONSTRUCTOR` resolves `undefined` in both.\r\n *\r\n * **The subject is the expression, not the field name** - which is what makes\r\n * this the same answer `/reactive` gives (`field-schema.ts:172-178`) on a\r\n * different input. There the field names arrive in the library's own\r\n * `FieldSchema[]`; here the paths are compile-time `p.city` tokens and the\r\n * library never sees a name it could validate. What it does see, at\r\n * registration, is the expression. A model key nobody names harms nobody, so\r\n * the expression is the complete subject and this check inherits none of the\r\n * growing-key-set problem a scan of the model would have.\r\n *\r\n * **The predicate is normative and any list of names is illustrative**\r\n * (S 3.8.1, revision 18 item 2). `Object.getOwnPropertyNames(Object.prototype)`\r\n * is twelve names: the seven a form author might plausibly type -\r\n * `constructor`, `toString`, `valueOf`, `hasOwnProperty`, `isPrototypeOf`,\r\n * `propertyIsEnumerable`, `toLocaleString` - plus `__proto__` and the four\r\n * `__define*` / `__lookup*` accessors. Hard-coding the seven would satisfy\r\n * every other criterion of this step, which is why one of the other five is\r\n * asserted.\r\n *\r\n * **Deliberately over-rejecting** (S 3.8.1). An arrow's own frame is\r\n * genuinely safe - `EvalContext.get` resolves `scopes` at step 1 and\r\n * `original` at step 2, so a bound `valueOf` shadows the prototype and\r\n * resolves correctly - and `'[1].map(valueOf => valueOf)'` is refused anyway.\r\n * The scope-aware alternative is a second copy of `eval-core`'s frame logic,\r\n * tracking two scope-pushing visitors this library does not own, that fails\r\n * by *under*-rejecting when it drifts. The costs are asymmetric: a named\r\n * error at registration, whose fix is renaming a parameter, against a field\r\n * that always renders in production.\r\n *\r\n * **`acorn-walk`'s `simple`, borrowed rather than hand-rolled** (S 0.1) - the\r\n * same package `eval-core` walks with. Two of its properties are load-bearing\r\n * rather than incidental, and both are pinned by specs:\r\n *\r\n * - its base walker descends into `node.property` only when `node.computed`,\r\n * so `user.constructor` is **not** seen as an `Identifier`. That is\r\n * `eval-core`'s prototype-pollution guard's business and the stated upper\r\n * bound of this one;\r\n * - `base.Function` walks parameters under the `\"Pattern\"` override, which\r\n * `simple` suppresses, so a **binding** is never visited while a\r\n * **reference** is. `'[1].map(valueOf => 1)'` therefore registers.\r\n *\r\n * A hand-rolled scan over every node would reject both, which is the\r\n * difference the borrow is checked at.\r\n *\r\n * @param expression the source, named in the message so an author can tell\r\n * which of a schema's rules to fix\r\n * @param node the AST the registrar has just parsed. Typed\r\n * `ReturnType<typeof parse>` rather than `AnyNode`: `eval-core` publishes\r\n * `AnyNodeTypes` and not `AnyNode`, so spelling it out would force\r\n * `import type { AnyNode } from 'acorn'` - an undeclared dependency whose\r\n * shortest fix is the `acorn` peer S 3.8 rejects.\r\n */\r\nexport const guardIdentifiers = (expression: string, node: ReturnType<typeof parse>): void => {\r\n\r\n // `parse`'s declared return is `Program | AnyNode | undefined` and `simple`\r\n // takes a `Node`, so the narrowing is forced by the signature rather than\r\n // by a case this package can reach: `prepare` passes `defaultParserOptions`,\r\n // which sets `extractExpressions: false`, and `parse` then returns the\r\n // `Program` unconditionally. `undefined` is `extractExpression`'s answer\r\n // when a program's body is not exactly one `ExpressionStatement` - `'a; b'`\r\n // as much as the empty program - so a caller passing different options\r\n // could produce it.\r\n //\r\n // **No spec covers this branch, and skipping the walk is still right if one\r\n // ever reaches it**: nothing to walk is nothing to reject, and the caller\r\n // hands the same value to `compile`, whose `evaluate` already answers\r\n // `undefined` for a falsy node. The guard changes no contract by declining.\r\n if (node === undefined) {\r\n return;\r\n }\r\n\r\n simple(node, {\r\n Identifier(identifier) {\r\n if (Object.prototype.hasOwnProperty.call(Object.prototype, identifier.name)) {\r\n throw new Error(\r\n `Expression '${expression}': identifier '${identifier.name}' is a member of ` +\r\n `Object.prototype and cannot be resolved reliably: it reads the prototype's ` +\r\n `value whenever the model holds no such key, so the rule sees a function - ` +\r\n `which is truthy - rather than the absence it was written for. Rename the ` +\r\n `model key, or the parameter that binds it.`\r\n );\r\n }\r\n },\r\n });\r\n};\r\n","import { Signal, WritableSignal, computed, isSignal } from '@angular/core';\r\nimport { EvalContext, EvalOptions } from '@zvenigora/ng-eval-core';\r\nimport { createFieldContext } from '@zvenigora/ng-eval-forms';\r\n\r\n/**\r\n * Resolves one key against the model object, preferring an exact match.\r\n *\r\n * **This is a re-implementation of `eval-signals`' own `resolve`\r\n * (`signal-context.ts:127-144`), not a borrow, and the plan requires that be\r\n * said plainly rather than recorded as reuse** (plan S 0.1, S 3.2.1).\r\n * `resolve` is module-private there and cannot be called. The two must agree\r\n * on the same rule - exact match first, then the first key differing only in\r\n * case, in insertion order - and if upstream's ever changes, this is the\r\n * second copy that does not know.\r\n */\r\nconst readProperty = (\r\n model: Record<string, unknown>,\r\n key: string,\r\n caseInsensitive: boolean\r\n): unknown => {\r\n\r\n if (Object.prototype.hasOwnProperty.call(model, key)) {\r\n return model[key];\r\n }\r\n\r\n if (!caseInsensitive) {\r\n return undefined;\r\n }\r\n\r\n const lowered = key.toLowerCase();\r\n const match = Object.keys(model).find((candidate) => candidate.toLowerCase() === lowered);\r\n return match === undefined ? undefined : model[match];\r\n};\r\n\r\n/**\r\n * What one `createExpressionRules` factory holds: a private memo of per-key\r\n * `computed`s, and the contexts built off it (plan S 3.2.1, S 3.6).\r\n *\r\n * Module-private to the `/signals` entry point - a real export of this\r\n * module, absent from `signals/src/public-api.ts`, changeable by a later\r\n * phase without a release (S 5).\r\n */\r\nexport interface ModelSource {\r\n\r\n /**\r\n * One `computed` per key, per **factory**. Exported so the memo can be\r\n * compared by identity: the memo itself is private and the resolver returns\r\n * a *value*, so comparing two resolved values passes with or without it.\r\n */\r\n keySignal: (key: string) => Signal<unknown>;\r\n\r\n /**\r\n * S 3.6's one context per rule per `form()`, built off the shared memo.\r\n */\r\n createRuleContext: () => EvalContext;\r\n}\r\n\r\n/**\r\n * Builds the `/signals` adapter's source over the model signal a consumer\r\n * passed to `form()`.\r\n *\r\n * **The property read happens *inside* the `computed`, and that is the whole\r\n * design** (plan S 3.2.1). Two earlier shapes are withdrawn and both failed\r\n * in ways worth naming, because each looks correct until a second read:\r\n *\r\n * - A bare `() => model()[key]` in a `SignalContextSource` record resolves to\r\n * the **function object** - truthy, never called, never tracked - because\r\n * upstream's resolver unwraps signals and passes functions through\r\n * untouched. That is the silent freeze `src/lib/field-context.ts:60-64`\r\n * already records for the `/reactive` form half.\r\n * - A record whose entries are memoised `computed`s, written back as the\r\n * expression spelled the key, becomes an **exact** match on the second read\r\n * under `caseInsensitive` and shadows the model's own key for the life of\r\n * the form. Measured as Q4: the first read resolves and every read after it\r\n * is frozen.\r\n *\r\n * With resolution inside the computed there is nothing left for a record to\r\n * hold, so both `createFieldContext` sources are `{}` and the resolver\r\n * pushed onto `lookups` answers every key.\r\n *\r\n * **The read is what subscribes, and it happens even when the key resolves to\r\n * `undefined`.** `EvalContext.get` treats `undefined` as absent at every step,\r\n * so an expression naming a key the model does not hold yet resolves to\r\n * nothing - but `keySignal(key)()` has been *called* inside Angular's\r\n * derivation, so the rule is subscribed. When the model gains the key, the\r\n * computed's value changes and the rule re-runs. A record that simply lacked\r\n * the key would read nothing, subscribe to nothing, and stay frozen.\r\n *\r\n * **Per-key propagation survives even though every `computed` reads the whole\r\n * model.** Angular's `computed` memoises on `Object.is`, so a write to `zip`\r\n * re-evaluates each computed's property read and propagates only from\r\n * `zip`'s. The cost per model write is O(keys some expression actually\r\n * reads), not O(rules) and not O(model keys), because `computed` is lazy and\r\n * a key nothing names is never evaluated.\r\n *\r\n * **What is limited**, for the README beside `/reactive`'s key-set caveat -\r\n * and neither item is resolution, since every key an expression can name\r\n * resolves at any spelling `caseInsensitive` allows, whether or not the model\r\n * held it when the form was built:\r\n *\r\n * - The nested-signal diagnostic does not reach this adapter.\r\n * `findNestedSignals` only reports a key whose value `isPlainObject`, and\r\n * every value here is a `computed`. A *nested* property holding a signal -\r\n * `{ user: { name: signal('a') } }` - is read un-called by the member\r\n * visitor and nothing warns. A top-level key holding one is called, as\r\n * upstream's lookup does.\r\n * - Enumeration of the form's keys is not available upstream, because the\r\n * memo is deliberately private. That is the fix rather than a cost: the\r\n * record being enumerable by upstream's `resolve` is precisely what\r\n * produced Q4's freeze.\r\n *\r\n * @param model - The `WritableSignal` the consumer passes to `form()`.\r\n * `form()` does not copy it, so the model signal and the field\r\n * tree are two views of one thing.\r\n * @param options - Configures this source and the contexts it builds, not the\r\n * walk. As upstream, `caseInsensitive` corrects identifier\r\n * keys here but not *property* names.\r\n */\r\nexport const createModelSource = <TModel extends object>(\r\n model: WritableSignal<TModel>,\r\n options?: EvalOptions\r\n): ModelSource => {\r\n\r\n // Index access, not dotted: `EvalOptions` is an index signature and\r\n // `noPropertyAccessFromIndexSignature` is set in all three libraries.\r\n const caseInsensitive = !!options?.['caseInsensitive'];\r\n\r\n // A `Map`, deliberately, and **not** the record `createFieldContext` hands\r\n // to `createSignalContext` - see Q4 in the docblock above. Nothing upstream\r\n // can see it, so no memo entry can ever shadow a model key.\r\n const memo = new Map<string, Signal<unknown>>();\r\n\r\n const keySignal = (key: string): Signal<unknown> => {\r\n let cached = memo.get(key);\r\n\r\n if (!cached) {\r\n // Created inside Angular's reactive consumer, on the first read of any\r\n // key. That is allowed - `computed()` needs no injection context and is\r\n // not `effect()`. The inner computed becomes the active consumer for\r\n // its own body, so `model()` is attributed there and the outer consumer\r\n // records the inner one as a dependency, which is what makes the\r\n // per-key propagation above hold.\r\n cached = computed(() => readProperty(model() as Record<string, unknown>, key, caseInsensitive));\r\n memo.set(key, cached);\r\n }\r\n\r\n return cached;\r\n };\r\n\r\n const createRuleContext = (): EvalContext => {\r\n\r\n // `createFieldContext` is still what builds the context - not for its\r\n // sources, which are both empty, but for its **class**. It returns a\r\n // context whose `set` throws `SignalContextWriteError`, which is the\r\n // error the error policy must re-throw rather than swallow. A hand-built\r\n // `EvalContext` would silently accept an assigning expression.\r\n const context = createFieldContext({}, {}, options);\r\n\r\n // The resolver's parameter is deliberately unannotated. `EvalLookup` is\r\n // `(key: unknown, …) => unknown` and a parameter position is\r\n // contravariant under `strict`, so `(key: string) => …` does not compile;\r\n // upstream writes it the same way for the same reason.\r\n //\r\n // The narrowing decides which keys reach the memo, and it matches\r\n // upstream's `resolve` for both kinds an expression can produce. A string\r\n // passes as is. A number - `this[42]` is how a walk hands a lookup one,\r\n // since `this` is the context and a computed key arrives raw - is spelled\r\n // as the string JavaScript's own property access coerces it to, so\r\n // `this[42]` and `this[\"42\"]` share one memo entry and resolve as upstream\r\n // does. Passed raw it would make a second entry under the number, and\r\n // under `caseInsensitive` throw from `toLowerCase`. Anything else - a\r\n // symbol - resolves `undefined`, where upstream would find a symbol-keyed\r\n // own property (`docs/backlog.md` `BL-D6`).\r\n //\r\n // A value that is itself a signal is called, as upstream's lookup does\r\n // (`isSignal(value) ? value() : value`), so `{ ready: signal(false) }`\r\n // resolves `ready` to `false` rather than to a truthy function. The call\r\n // happens in the rule's own derivation, so the rule tracks the inner\r\n // signal as well as the key's computed (`docs/backlog.md` `BL-D4`).\r\n context.lookups.push((key) => {\r\n const name = typeof key === 'string' ? key : typeof key === 'number' ? String(key) : undefined;\r\n const value = name === undefined ? undefined : keySignal(name)();\r\n return isSignal(value) ? value() : value;\r\n });\r\n\r\n return context;\r\n };\r\n\r\n return { keySignal, createRuleContext };\r\n};\r\n","import { createMetadataKey } from '@angular/forms/signals';\r\n\r\n/**\r\n * The metadata key `evalText` writes through and a consumer reads back\r\n * (plan S 1.2.8, S 3.5).\r\n *\r\n * `text` has no dedicated primitive in `@angular/forms/signals` the way\r\n * `hidden` and `disabled` do, so it is `metadata(path, TEXT, logic)` against a\r\n * key created once - which is Angular's own mechanism rather than a second one\r\n * beside it, per the `/signals` reviewer checklist's item 4.\r\n *\r\n * **Created once, at module scope, deliberately.** `createMetadataKey` mints a\r\n * fresh key per call, and the write and the read-back have to name the same\r\n * object: a key re-created per access would let `metadata(p.x, TEXT, …)` write\r\n * under one identity and `f.x().metadata(TEXT)` read under another, and the\r\n * read would be `undefined` with nothing to show why. `text-key.spec.ts`\r\n * asserts the two halves of that - `createMetadataKey` is per-call, and this\r\n * is one of its results rather than the function itself.\r\n */\r\nexport const TEXT = createMetadataKey<string>();\r\n","import { WritableSignal } from '@angular/core';\r\nimport { type PathKind, type SchemaPath, type SchemaPathRules, disabled, hidden, metadata } from '@angular/forms/signals';\r\nimport { type EvalOptions, compile, defaultParserOptions, parse } from '@zvenigora/ng-eval-core';\r\nimport { type ExpressionErrorPolicy, applyErrorPolicy, toText, toVisible } from '@zvenigora/ng-eval-forms';\r\nimport { evaluateRule } from './evaluate-rule';\r\nimport { guardIdentifiers } from './guard-identifiers';\r\nimport { createModelSource } from './model-source';\r\nimport { TEXT } from './text-key';\r\n\r\n/**\r\n * What `createExpressionRules` takes, and what a single registration may\r\n * override (plan S 5).\r\n */\r\nexport interface ExpressionRuleOptions {\r\n\r\n /**\r\n * Passed to the context and to the walk. `caseInsensitive` has to reach\r\n * both: on the context it corrects identifier keys, and on the walk it\r\n * corrects *property* names, which the member visitor reads off the\r\n * state's options.\r\n *\r\n * **A per-registration value reaches exactly one of the three places it\r\n * has to: the walk** (plan S 3.5.3). The other two read the **factory's**\r\n * options, which are fixed when `createExpressionRules` is called: the memo\r\n * is built once, there; the context is minted per registration by\r\n * `createRuleContext()` but always from that same fixed setting, so a\r\n * registration cannot move it. So overriding this per registration corrects\r\n * *property* names and leaves *identifier* keys on the factory's setting,\r\n * and one expression then obeys two casing rules. Set `caseInsensitive` on\r\n * the **factory** unless that is the behaviour you want.\r\n *\r\n * Revision 19 item 2 corrects \"both are made once, at\r\n * `createExpressionRules` time\", which was wrong about the context and is\r\n * the claim the README states correctly.\r\n */\r\n eval?: EvalOptions;\r\n\r\n /**\r\n * What a field property does when its expression throws at runtime.\r\n * Defaults to `'undefined'` here, the opposite of `eval-signals`' own\r\n * default - see `ExpressionErrorPolicy` for why.\r\n */\r\n onError?: ExpressionErrorPolicy;\r\n}\r\n\r\n/**\r\n * The three registrars one factory returns, one per Angular rule.\r\n *\r\n * **One call per Angular primitive, never an aggregate** (plan S 3.5.1). A\r\n * single `evalRules(p.x, { visible, text, disabled })` would register three\r\n * different Angular rules behind one name, hiding which one each property\r\n * maps to - which is the `/signals` reviewer checklist's item 4 expressed as\r\n * an API.\r\n *\r\n * Named after the **property**, not after Angular's rule: `evalVisible`\r\n * registers `hidden`, inverted once inside the registrar rather than in every\r\n * consumer's expression, so the same expression string means the same thing\r\n * at this entry point and at `/reactive`.\r\n */\r\nexport interface ExpressionRules {\r\n\r\n /** Registers Angular's `hidden`, inverted (plan S 3.5.1). */\r\n evalVisible: <TValue, TPathKind extends PathKind = PathKind.Root>(\r\n path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>,\r\n expression: string,\r\n options?: ExpressionRuleOptions\r\n ) => void;\r\n\r\n /** Registers `metadata(path, TEXT, …)` (plan S 1.2.8, S 3.5). */\r\n evalText: <TValue, TPathKind extends PathKind = PathKind.Root>(\r\n path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>,\r\n expression: string,\r\n options?: ExpressionRuleOptions\r\n ) => void;\r\n\r\n /**\r\n * Registers Angular's `disabled`.\r\n *\r\n * The `reason` is authored as a static string and is never\r\n * expression-derived (plan S 3.5.2). Angular's `when` returns\r\n * `boolean | string` and a truthy string is *both* \"disabled\" and \"the\r\n * reason\", so a rule yielding `'false'` would otherwise disable the field\r\n * with the reason `\"false\"` - the `toVisible` truthiness trap in a new\r\n * shape. Keeping the expression boolean and the reason static is what kills\r\n * it.\r\n */\r\n evalDisabled: <TValue, TPathKind extends PathKind = PathKind.Root>(\r\n path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>,\r\n expression: string,\r\n options?: ExpressionRuleOptions & { reason?: string }\r\n ) => void;\r\n}\r\n\r\n/**\r\n * Binds one model signal and returns the three expression-driven registrars.\r\n *\r\n * **A factory rather than free functions, because a `LogicFn` cannot recover\r\n * the source** (plan S 3.2.1). `RootFieldContext` exposes the *current*\r\n * field's node plus compile-time-token accessors and no root or parent\r\n * handle, so a rule on `p.city` evaluating `country === \"US\"` has no route to\r\n * `country` from inside the `LogicFn`. The source must be closed over at\r\n * registration or it is unreachable.\r\n *\r\n * ```ts\r\n * const rules = createExpressionRules(model);\r\n * const s = schema<Model>((p) => {\r\n * required(p.email); // Angular's\r\n * rules.evalVisible(p.city, 'country === \"US\"'); // ours\r\n * });\r\n * const f = form(model, s);\r\n * ```\r\n *\r\n * **A schema *value* shared across models is the unsupported shape.** Nothing\r\n * stops two `form()` calls from one schema - Angular re-invokes the schema\r\n * body once per `form()`, so each form mints its own contexts - but the\r\n * registrars close over the **factory's** model, and the factory is bound to\r\n * one. A schema built from `createExpressionRules(modelA)` and reused for\r\n * `form(modelB, s)` re-registers every rule and every one of them still reads\r\n * model A: form B renders against form A's data, silently, with no error and\r\n * a fully functional form (Q9).\r\n *\r\n * The supported reuse shape is therefore a schema **function of the rules** -\r\n * `const makeSchema = (rules) => schema<Model>(p => …)`, called per form -\r\n * which keeps reuse while giving each form a factory bound to its own model.\r\n *\r\n * What one factory retains: **one private memo**, per factory, bounded by the\r\n * union of keys the rules mention; and, per rule per `form()`, one\r\n * `EvalContext` and one compiled callback. Nothing registers with a\r\n * `DestroyRef` and there is no `destroy()` - it all becomes garbage with the\r\n * form (S 3.6).\r\n */\r\nexport const createExpressionRules = <TModel extends object>(\r\n model: WritableSignal<TModel>,\r\n options?: ExpressionRuleOptions\r\n): ExpressionRules => {\r\n\r\n // Called **once** per factory: it is what fixes the memo's lifetime, one\r\n // per factory rather than one per rule (plan S 3.6). Calling it per\r\n // registrar instead would satisfy every behavioural criterion in this phase\r\n // while quietly making the memo per rule, which is why the count has a spec\r\n // of its own.\r\n const source = createModelSource(model, options?.eval);\r\n\r\n /**\r\n * Resolves the two levels `ExpressionRuleOptions` arrives at:\r\n * **registration wins, per key** (plan S 3.5.3). Each key is resolved\r\n * independently and neither is a deep merge, so a registration supplying\r\n * only `onError` keeps the factory's `eval` and vice versa.\r\n *\r\n * Exact for `onError`, partial for `eval.caseInsensitive` - see the note on\r\n * `ExpressionRuleOptions.eval`. The divergence is characterised in\r\n * `rules.spec.ts` rather than left to be discovered.\r\n */\r\n const resolveOptions = (rule?: ExpressionRuleOptions): ExpressionRuleOptions => ({\r\n eval: rule?.eval ?? options?.eval,\r\n onError: rule?.onError ?? options?.onError,\r\n });\r\n\r\n /**\r\n * Everything a registrar does before handing Angular a `LogicFn`, and the\r\n * shape of what it returns is the phase's one structural invariant.\r\n *\r\n * **Called once per registration**, so `parse` + `compile` and\r\n * `createRuleContext()` happen at schema-body time - which Angular re-runs\r\n * once per `form()` (Q8), giving S 3.6's count of one context and one\r\n * compiled callback per rule per form. Nothing here re-parses per\r\n * derivation; risk 8 is a compile that drifts into the returned closure, and\r\n * `rules.invocation-count.spec.ts` counts `compile` to say it has not.\r\n *\r\n * **`guardIdentifiers` runs here, between `parse` and `compile`, and one\r\n * call site is deliberate** (S 3.8, revision 18 item 1). Revision 16's rule\r\n * rejects \"the wrapper is shared\" as a substitute for a per-registrar case,\r\n * and the discriminator it states is whether the subject is a path Angular\r\n * owns. This one is not: the guard throws **before any Angular primitive is\r\n * reached**, so in the rejecting case `hidden`, `metadata` and\r\n * `addDisabledReasonRule` are never called and the three registrars have\r\n * nothing downstream that could diverge. Contrast the invocation count and\r\n * the write-error bypass, whose values arrive *through* those three\r\n * primitives - `rules.spec.ts` gives each of them a case per registrar for\r\n * exactly that reason. Anything the returned closure does stays\r\n * registrar-level; this runs before there is a closure.\r\n *\r\n * **`applyErrorPolicy` is the outermost call in the returned closure, and\r\n * that is load-bearing twice over** (S 3.5). The policy has to cover the\r\n * coercion's *input* rather than only the walk - a registrar that coerced\r\n * first would hand `toVisible` a value the policy never saw - and S 6.1.1's\r\n * invocation instrument counts this exact call. M7 measured what a guard\r\n * hoisted *above* the wrapper does to that number: ground truth 1,\r\n * instrument **0**, which reads as \"the rule did not re-run\" in every\r\n * negative case in this package. A later guard belongs **inside** the\r\n * `run` callback, never ahead of this call.\r\n */\r\n const prepare = (expression: string, ruleOptions?: ExpressionRuleOptions): (() => unknown) => {\r\n const resolved = resolveOptions(ruleOptions);\r\n const node = parse(expression, defaultParserOptions);\r\n\r\n guardIdentifiers(expression, node);\r\n\r\n const compiled = compile(node);\r\n const context = source.createRuleContext();\r\n\r\n return () =>\r\n applyErrorPolicy(() => evaluateRule(compiled, context, resolved.eval), resolved.onError);\r\n };\r\n\r\n return {\r\n\r\n // Angular's **config** overload (S 1.2.7); the deprecated one takes the\r\n // `LogicFn` positionally. `hidden`'s `when` is the only required config of\r\n // the three primitives this entry point registers.\r\n //\r\n // The `!` is S 3.5.1's inversion, and it lives here rather than in every\r\n // consumer's expression so the same rule string means the same thing at\r\n // this entry point and at `/reactive`'s `visible`.\r\n evalVisible: (path, expression, ruleOptions) => {\r\n const evaluated = prepare(expression, ruleOptions);\r\n\r\n hidden(path, { when: () => !toVisible(evaluated()) });\r\n },\r\n\r\n // `text` has no dedicated primitive, so it is Angular's `metadata` against\r\n // the module-scope key of `text-key.ts` (S 1.2.8) - its own mechanism\r\n // rather than a second one beside it.\r\n evalText: (path, expression, ruleOptions) => {\r\n const evaluated = prepare(expression, ruleOptions);\r\n\r\n metadata(path, TEXT, () => toText(evaluated()));\r\n },\r\n\r\n // Angular's polarity, uninverted: `/reactive` ships no `disabled`, so no\r\n // expression has to mean the same thing at two entry points, and `true`\r\n // disabling is what an author expects (S 3.5.1).\r\n //\r\n // **The reason is a static option, never the expression's return**\r\n // (S 3.5.2). Angular's `when` is a single field carrying both the\r\n // condition and the reason - it returns `boolean | string`, and a truthy\r\n // string is *both* (1.2.7) - so a registrar forwarding `evaluated()` raw\r\n // would disable a field on the string `'false'` **with the reason\r\n // `\"false\"`**. Coercing through `toVisible` first, and sourcing the reason\r\n // from the registration instead, is what kills that: the string never\r\n // comes from the expression at all.\r\n //\r\n // A *dynamic* reason stays out of scope - it reopens the trap and needs a\r\n // coercion rule of its own.\r\n evalDisabled: (path, expression, ruleOptions) => {\r\n const evaluated = prepare(expression, ruleOptions);\r\n const reason = ruleOptions?.reason;\r\n\r\n disabled(path, {\r\n when: () => {\r\n // `evaluated()` first, so `applyErrorPolicy` stays the outermost\r\n // call in this body as it is in the other two (S 3.5). `reason` is\r\n // read from a closure rather than branched on ahead of the call:\r\n // a guard hoisted above the wrapper is M7's arrangement, which\r\n // reads 0 on an instrument whose ground truth is 1.\r\n const on = toVisible(evaluated());\r\n\r\n return on && reason !== undefined ? reason : on;\r\n },\r\n });\r\n },\r\n };\r\n};\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"zvenigora-ng-eval-forms-signals.mjs","sources":["../../../../modules/eval-forms/signals/src/lib/evaluate-rule.ts","../../../../modules/eval-forms/signals/src/lib/guard-identifiers.ts","../../../../modules/eval-forms/signals/src/lib/model-source.ts","../../../../modules/eval-forms/signals/src/lib/text-key.ts","../../../../modules/eval-forms/signals/src/lib/rules.ts","../../../../modules/eval-forms/signals/src/zvenigora-ng-eval-forms-signals.ts"],"sourcesContent":["import { EvalContext, EvalOptions, EvalState, call, stateCallback } from '@zvenigora/ng-eval-core';\r\n\r\n/**\r\n * Runs one compiled rule against one context, and leaves the context's scope\r\n * stack exactly as it found it.\r\n *\r\n * **This is the entry point's only path to the walk, and that is the whole\r\n * point of it** (plan S 3.3, `phase-4-plan.md` S 9.1). Every walk needs an\r\n * `EvalState`, and outside Angular DI there are exactly two ways to get one:\r\n * the static factory this function calls below, and the class's public\r\n * constructor. The one construction in this package is that line, inside this\r\n * function's body; S 6 gate 3 greps for both spellings and expects one hit and\r\n * zero.\r\n *\r\n * **Neither spelling is written out in this comment, deliberately.** Gate 3 is\r\n * a grep, and prose naming what it searches for is a standing hit on the file\r\n * the gate exists to bless - which is what revision 10 spent a revision\r\n * removing from that gate's third row, after a block comment in\r\n * `reactive/src/lib/field-schema.ts` made it read as failing. A gate with a\r\n * known pre-existing hit is one people learn to ignore.\r\n *\r\n * **Module-private to `/signals`, deliberately, and not merely unexported by\r\n * omission.** Placing it in the shared core was measured against this and\r\n * separates on one row that matters: a barrel re-exports whole modules, so a\r\n * helper the core's `public-api.ts` can reach **is published**, permanently,\r\n * on an entry point already released at 0.1.0 - and a published `evaluateRule`\r\n * *is* a second path to the walk by existing, since any consumer could call it\r\n * with a hand-built `EvalContext`. Here the set of callers is the set of files\r\n * in this directory, which is checkable by grep. The three placement\r\n * hypotheses that sound decisive - `/reactive` bundle weight, `@angular/core`\r\n * leakage, FESM size - were measured and are not: ng-packagr compiles each\r\n * entry point separately and the helper appears in `/reactive`'s FESM at zero\r\n * bytes either way (S 3.3's table).\r\n *\r\n * **Why the `finally` exists.** The scope stack is the third and longest-lived\r\n * of `eval-core`'s three stack invariants ([`CLAUDE.md`](../../../../../CLAUDE.md)):\r\n * unlike the value stack and the open-node stack, which live on the per-walk\r\n * `EvalState` and die with it, scopes live on the `EvalContext`. A rule holds\r\n * its context across every invocation Angular makes, and `EvalContext.get`\r\n * resolves `scopes` **first**, so one scope left behind shadows the source key\r\n * of that name for the life of the form. Nothing else drains it.\r\n *\r\n * **Retained, not redundant.** This was written against a specific defect -\r\n * `arrow-function-expression.ts` pushed a scope and popped it with no\r\n * `try`/`finally`, so an arrow body that threw skipped the pop - and Phase 2\r\n * step 0 fixed exactly that\r\n * ([backlog A9](../../../../../docs/backlog-retired.md#a9)). The loop stays anyway, for\r\n * two reasons that outlive the fix - and they are not the same *kind* of\r\n * reason, which is the part an earlier version of this docblock got wrong:\r\n *\r\n * - The `@zvenigora/ng-eval-core` peer range in `modules/eval-forms/package.json`\r\n * admits the *leaking* 0.3.0, so a supported installation can still be\r\n * running the defect. **Range-dependent**: it would stop being true if the\r\n * range's lower bound were ever raised past 0.3.0.\r\n * - `EvalContext.push` and `pop` are public methods on a published class: a\r\n * scope can be stranded with no visitor involved at all. That is the route\r\n * `evaluate-rule.spec.ts`'s containment cases now drive, because it is the\r\n * one no fix inside the core's visitors can close. **True at every version**,\r\n * and therefore the reason this loop is not removable at any peer range.\r\n *\r\n * **Raising the peer range does not make this removable**, and the sentence\r\n * that used to sit here said it did - \"removal is gated on raising the peer\r\n * range\" reads as a sufficient condition and is only a necessary one. Whoever\r\n * raises it retires the first reason and leaves the second untouched. The same\r\n * error was in the plan's step 0b, corrected there in Phase 2 step 7; this copy\r\n * of it was corrected in step 8.\r\n *\r\n * A third reason has expired and is recorded as gone rather than silently\r\n * dropped: this was also the backstop for the scope-push sites Phase 2 was\r\n * adding to the core. `Program`, `BlockStatement` and `ForStatement` all\r\n * shipped in 0.4.0, each popping in a `finally`.\r\n *\r\n * The unwind is a loop to a **depth mark**, not a single `pop()`: one throw\r\n * can leave more than one scope open, and unwinding to the bottom would drain\r\n * scopes this call did not push - `evaluate()` is re-entrant through the arrow\r\n * closure, so the depth on entry is not reliably zero.\r\n *\r\n * This helper deliberately does **not** apply the error policy. The throw\r\n * propagates, and `applyErrorPolicy` sits outside this call in the `LogicFn`\r\n * body (S 3.5) - containment first, then the policy - so an expression that\r\n * throws is contained whether the consumer asked for `'throw'`, `'undefined'`\r\n * or a handler.\r\n *\r\n * @param compiled - `eval-core`'s own `stateCallback`, from\r\n * `compile(parse(expression, ...))`. A registrar compiles\r\n * once and holds the callback; nothing recompiles per\r\n * invocation.\r\n * @param context - The rule's context, from `ModelSource.createRuleContext()`.\r\n * One per rule per `form()` (S 3.6), reused across every\r\n * invocation of that rule.\r\n * @param options - The walk's options, which are **not** the context's:\r\n * visitors read `caseInsensitive` off the state, so it has to\r\n * reach both to correct property names as well as identifier\r\n * keys.\r\n */\r\nexport const evaluateRule = (\r\n compiled: stateCallback,\r\n context: EvalContext,\r\n options?: EvalOptions\r\n): unknown => {\r\n\r\n const depth = context.scopes.length;\r\n\r\n // `fromContext` short-circuits on identity for an `EvalContext`, so this\r\n // builds a fresh per-walk state around the *same* context rather than\r\n // copying it - which is what lets one context back many invocations, and\r\n // equally what makes a leaked scope durable.\r\n const state = EvalState.fromContext(context, options);\r\n\r\n try {\r\n return call(compiled, state);\r\n } finally {\r\n while (context.scopes.length > depth) {\r\n context.pop();\r\n }\r\n }\r\n};\r\n","import type { parse } from '@zvenigora/ng-eval-core';\r\nimport { simple } from 'acorn-walk';\r\n\r\n/**\r\n * Throws if the expression names an identifier that is an own property of\r\n * `Object.prototype` (plan S 3.8).\r\n *\r\n * **The failure this prevents is silent and truthy.** `createSignalContext`\r\n * builds its context on an empty `original` object, and `EvalContext.get`\r\n * consults `original` *before* `lookups` - so an identifier like\r\n * `constructor` resolves off the prototype and never reaches the model\r\n * resolver at all. A function is truthy, so `toVisible` says visible and\r\n * `evalVisible(p.city, 'constructor')` renders the field with no data, no\r\n * error and nothing logged (Q10, Q11). It is not GHSA-pj3p-xpg7-h7gw's\r\n * case-variant bypass: the behaviour is identical with and without\r\n * `caseInsensitive`, and `CONSTRUCTOR` resolves `undefined` in both.\r\n *\r\n * **The subject is the expression, not the field name** - which is what makes\r\n * this the same answer `/reactive` gives (`field-schema.ts:172-178`) on a\r\n * different input. There the field names arrive in the library's own\r\n * `FieldSchema[]`; here the paths are compile-time `p.city` tokens and the\r\n * library never sees a name it could validate. What it does see, at\r\n * registration, is the expression. A model key nobody names harms nobody, so\r\n * the expression is the complete subject and this check inherits none of the\r\n * growing-key-set problem a scan of the model would have.\r\n *\r\n * **The predicate is normative and any list of names is illustrative**\r\n * (S 3.8.1, revision 18 item 2). `Object.getOwnPropertyNames(Object.prototype)`\r\n * is twelve names: the seven a form author might plausibly type -\r\n * `constructor`, `toString`, `valueOf`, `hasOwnProperty`, `isPrototypeOf`,\r\n * `propertyIsEnumerable`, `toLocaleString` - plus `__proto__` and the four\r\n * `__define*` / `__lookup*` accessors. Hard-coding the seven would satisfy\r\n * every other criterion of this step, which is why one of the other five is\r\n * asserted.\r\n *\r\n * **Deliberately over-rejecting** (S 3.8.1). An arrow's own frame is\r\n * genuinely safe - `EvalContext.get` resolves `scopes` at step 1 and\r\n * `original` at step 2, so a bound `valueOf` shadows the prototype and\r\n * resolves correctly - and `'[1].map(valueOf => valueOf)'` is refused anyway.\r\n * The scope-aware alternative is a second copy of `eval-core`'s frame logic,\r\n * tracking two scope-pushing visitors this library does not own, that fails\r\n * by *under*-rejecting when it drifts. The costs are asymmetric: a named\r\n * error at registration, whose fix is renaming a parameter, against a field\r\n * that always renders in production.\r\n *\r\n * **`acorn-walk`'s `simple`, borrowed rather than hand-rolled** (S 0.1) - the\r\n * same package `eval-core` walks with. Two of its properties are load-bearing\r\n * rather than incidental, and both are pinned by specs:\r\n *\r\n * - its base walker descends into `node.property` only when `node.computed`,\r\n * so `user.constructor` is **not** seen as an `Identifier`. That is\r\n * `eval-core`'s prototype-pollution guard's business and the stated upper\r\n * bound of this one;\r\n * - `base.Function` walks parameters under the `\"Pattern\"` override, which\r\n * `simple` suppresses, so a **binding** is never visited while a\r\n * **reference** is. `'[1].map(valueOf => 1)'` therefore registers.\r\n *\r\n * A hand-rolled scan over every node would reject both, which is the\r\n * difference the borrow is checked at.\r\n *\r\n * @param expression the source, named in the message so an author can tell\r\n * which of a schema's rules to fix\r\n * @param node the AST the registrar has just parsed. Typed\r\n * `ReturnType<typeof parse>` rather than `AnyNode`: `eval-core` publishes\r\n * `AnyNodeTypes` and not `AnyNode`, so spelling it out would force\r\n * `import type { AnyNode } from 'acorn'` - an undeclared dependency whose\r\n * shortest fix is the `acorn` peer S 3.8 rejects.\r\n */\r\nexport const guardIdentifiers = (expression: string, node: ReturnType<typeof parse>): void => {\r\n\r\n // `parse`'s declared return is `Program | AnyNode | undefined` and `simple`\r\n // takes a `Node`, so the narrowing is forced by the signature rather than\r\n // by a case this package can reach: `prepare` passes `defaultParserOptions`,\r\n // which sets `extractExpressions: false`, and `parse` then returns the\r\n // `Program` unconditionally. `undefined` is `extractExpression`'s answer\r\n // when a program's body is not exactly one `ExpressionStatement` - `'a; b'`\r\n // as much as the empty program - so a caller passing different options\r\n // could produce it.\r\n //\r\n // **No spec covers this branch, and skipping the walk is still right if one\r\n // ever reaches it**: nothing to walk is nothing to reject, and the caller\r\n // hands the same value to `compile`, whose `evaluate` already answers\r\n // `undefined` for a falsy node. The guard changes no contract by declining.\r\n if (node === undefined) {\r\n return;\r\n }\r\n\r\n simple(node, {\r\n Identifier(identifier) {\r\n if (Object.prototype.hasOwnProperty.call(Object.prototype, identifier.name)) {\r\n throw new Error(\r\n `Expression '${expression}': identifier '${identifier.name}' is a member of ` +\r\n `Object.prototype and cannot be resolved reliably: it reads the prototype's ` +\r\n `value whenever the model holds no such key, so the rule sees a function - ` +\r\n `which is truthy - rather than the absence it was written for. Rename the ` +\r\n `model key, or the parameter that binds it.`\r\n );\r\n }\r\n },\r\n });\r\n};\r\n","import { Signal, WritableSignal, computed, isSignal } from '@angular/core';\r\nimport { EvalContext, EvalOptions } from '@zvenigora/ng-eval-core';\r\nimport { createFieldContext } from '@zvenigora/ng-eval-forms';\r\n\r\n/**\r\n * Resolves one key against the model object, preferring an exact match.\r\n *\r\n * **This is a re-implementation of `eval-signals`' own `resolve`\r\n * (`signal-context.ts:127-144`), not a borrow, and the plan requires that be\r\n * said plainly rather than recorded as reuse** (plan S 0.1, S 3.2.1).\r\n * `resolve` is module-private there and cannot be called. The two must agree\r\n * on the same rule - exact match first, then the first key differing only in\r\n * case, in insertion order - and if upstream's ever changes, this is the\r\n * second copy that does not know.\r\n */\r\nconst readProperty = (\r\n model: Record<string, unknown>,\r\n key: string,\r\n caseInsensitive: boolean\r\n): unknown => {\r\n\r\n if (Object.prototype.hasOwnProperty.call(model, key)) {\r\n return model[key];\r\n }\r\n\r\n if (!caseInsensitive) {\r\n return undefined;\r\n }\r\n\r\n const lowered = key.toLowerCase();\r\n const match = Object.keys(model).find((candidate) => candidate.toLowerCase() === lowered);\r\n return match === undefined ? undefined : model[match];\r\n};\r\n\r\n/**\r\n * What one `createExpressionRules` factory holds: a private memo of per-key\r\n * `computed`s, and the contexts built off it (plan S 3.2.1, S 3.6).\r\n *\r\n * Module-private to the `/signals` entry point - a real export of this\r\n * module, absent from `signals/src/public-api.ts`, changeable by a later\r\n * phase without a release (S 5).\r\n */\r\nexport interface ModelSource {\r\n\r\n /**\r\n * One `computed` per key, per **factory**. Exported so the memo can be\r\n * compared by identity: the memo itself is private and the resolver returns\r\n * a *value*, so comparing two resolved values passes with or without it.\r\n */\r\n keySignal: (key: string) => Signal<unknown>;\r\n\r\n /**\r\n * S 3.6's one context per rule per `form()`, built off the shared memo.\r\n */\r\n createRuleContext: () => EvalContext;\r\n}\r\n\r\n/**\r\n * Builds the `/signals` adapter's source over the model signal a consumer\r\n * passed to `form()`.\r\n *\r\n * **The property read happens *inside* the `computed`, and that is the whole\r\n * design** (plan S 3.2.1). Two earlier shapes are withdrawn and both failed\r\n * in ways worth naming, because each looks correct until a second read:\r\n *\r\n * - A bare `() => model()[key]` in a `SignalContextSource` record resolves to\r\n * the **function object** - truthy, never called, never tracked - because\r\n * upstream's resolver unwraps signals and passes functions through\r\n * untouched. That is the silent freeze `src/lib/field-context.ts:60-64`\r\n * already records for the `/reactive` form half.\r\n * - A record whose entries are memoised `computed`s, written back as the\r\n * expression spelled the key, becomes an **exact** match on the second read\r\n * under `caseInsensitive` and shadows the model's own key for the life of\r\n * the form. Measured as Q4: the first read resolves and every read after it\r\n * is frozen.\r\n *\r\n * With resolution inside the computed there is nothing left for a record to\r\n * hold, so both `createFieldContext` sources are `{}` and the resolver\r\n * pushed onto `lookups` answers every key.\r\n *\r\n * **The read is what subscribes, and it happens even when the key resolves to\r\n * `undefined`.** `EvalContext.get` treats `undefined` as absent at every step,\r\n * so an expression naming a key the model does not hold yet resolves to\r\n * nothing - but `keySignal(key)()` has been *called* inside Angular's\r\n * derivation, so the rule is subscribed. When the model gains the key, the\r\n * computed's value changes and the rule re-runs. A record that simply lacked\r\n * the key would read nothing, subscribe to nothing, and stay frozen.\r\n *\r\n * **Per-key propagation survives even though every `computed` reads the whole\r\n * model.** Angular's `computed` memoises on `Object.is`, so a write to `zip`\r\n * re-evaluates each computed's property read and propagates only from\r\n * `zip`'s. The cost per model write is O(keys some expression actually\r\n * reads), not O(rules) and not O(model keys), because `computed` is lazy and\r\n * a key nothing names is never evaluated.\r\n *\r\n * **What is limited**, for the README beside `/reactive`'s key-set caveat -\r\n * and neither item is resolution, since every key an expression can name\r\n * resolves at any spelling `caseInsensitive` allows, whether or not the model\r\n * held it when the form was built:\r\n *\r\n * - The nested-signal diagnostic does not reach this adapter.\r\n * `findNestedSignals` only reports a key whose value `isPlainObject`, and\r\n * every value here is a `computed`. A *nested* property holding a signal -\r\n * `{ user: { name: signal('a') } }` - is read un-called by the member\r\n * visitor and nothing warns. A top-level key holding one is called, as\r\n * upstream's lookup does.\r\n * - Enumeration of the form's keys is not available upstream, because the\r\n * memo is deliberately private. That is the fix rather than a cost: the\r\n * record being enumerable by upstream's `resolve` is precisely what\r\n * produced Q4's freeze.\r\n *\r\n * @param model - The `WritableSignal` the consumer passes to `form()`.\r\n * `form()` does not copy it, so the model signal and the field\r\n * tree are two views of one thing.\r\n * @param options - Configures this source and the contexts it builds, not the\r\n * walk. As upstream, `caseInsensitive` corrects identifier\r\n * keys here but not *property* names.\r\n */\r\nexport const createModelSource = <TModel extends object>(\r\n model: WritableSignal<TModel>,\r\n options?: EvalOptions\r\n): ModelSource => {\r\n\r\n // Index access, not dotted: `EvalOptions` is an index signature and\r\n // `noPropertyAccessFromIndexSignature` is set in all three libraries.\r\n const caseInsensitive = !!options?.['caseInsensitive'];\r\n\r\n // A `Map`, deliberately, and **not** the record `createFieldContext` hands\r\n // to `createSignalContext` - see Q4 in the docblock above. Nothing upstream\r\n // can see it, so no memo entry can ever shadow a model key.\r\n const memo = new Map<string, Signal<unknown>>();\r\n\r\n const keySignal = (key: string): Signal<unknown> => {\r\n let cached = memo.get(key);\r\n\r\n if (!cached) {\r\n // Created inside Angular's reactive consumer, on the first read of any\r\n // key. That is allowed - `computed()` needs no injection context and is\r\n // not `effect()`. The inner computed becomes the active consumer for\r\n // its own body, so `model()` is attributed there and the outer consumer\r\n // records the inner one as a dependency, which is what makes the\r\n // per-key propagation above hold.\r\n cached = computed(() => readProperty(model() as Record<string, unknown>, key, caseInsensitive));\r\n memo.set(key, cached);\r\n }\r\n\r\n return cached;\r\n };\r\n\r\n const createRuleContext = (): EvalContext => {\r\n\r\n // `createFieldContext` is still what builds the context - not for its\r\n // sources, which are both empty, but for its **class**. It returns a\r\n // context whose `set` throws `SignalContextWriteError`, which is the\r\n // error the error policy must re-throw rather than swallow. A hand-built\r\n // `EvalContext` would silently accept an assigning expression.\r\n const context = createFieldContext({}, {}, options);\r\n\r\n // The resolver's parameter is deliberately unannotated. `EvalLookup` is\r\n // `(key: unknown, …) => unknown` and a parameter position is\r\n // contravariant under `strict`, so `(key: string) => …` does not compile;\r\n // upstream writes it the same way for the same reason.\r\n //\r\n // The narrowing decides which keys reach the memo, and it matches\r\n // upstream's `resolve` for both kinds an expression can produce. A string\r\n // passes as is. A number - `this[42]` is how a walk hands a lookup one,\r\n // since `this` is the context and a computed key arrives raw - is spelled\r\n // as the string JavaScript's own property access coerces it to, so\r\n // `this[42]` and `this[\"42\"]` share one memo entry and resolve as upstream\r\n // does. Passed raw it would make a second entry under the number, and\r\n // under `caseInsensitive` throw from `toLowerCase`. Anything else - a\r\n // symbol - resolves `undefined`, where upstream would find a symbol-keyed\r\n // own property (`docs/backlog.md` `BL-D6`).\r\n //\r\n // A value that is itself a signal is called, as upstream's lookup does\r\n // (`isSignal(value) ? value() : value`), so `{ ready: signal(false) }`\r\n // resolves `ready` to `false` rather than to a truthy function. The call\r\n // happens in the rule's own derivation, so the rule tracks the inner\r\n // signal as well as the key's computed (`docs/backlog.md` `BL-D4`).\r\n context.lookups.push((key) => {\r\n const name = typeof key === 'string' ? key : typeof key === 'number' ? String(key) : undefined;\r\n const value = name === undefined ? undefined : keySignal(name)();\r\n return isSignal(value) ? value() : value;\r\n });\r\n\r\n return context;\r\n };\r\n\r\n return { keySignal, createRuleContext };\r\n};\r\n","import { createMetadataKey } from '@angular/forms/signals';\r\n\r\n/**\r\n * The metadata key `evalText` writes through and a consumer reads back\r\n * (plan S 1.2.8, S 3.5).\r\n *\r\n * `text` has no dedicated primitive in `@angular/forms/signals` the way\r\n * `hidden` and `disabled` do, so it is `metadata(path, TEXT, logic)` against a\r\n * key created once - which is Angular's own mechanism rather than a second one\r\n * beside it, per the `/signals` reviewer checklist's item 4.\r\n *\r\n * **Created once, at module scope, deliberately.** `createMetadataKey` mints a\r\n * fresh key per call, and the write and the read-back have to name the same\r\n * object: a key re-created per access would let `metadata(p.x, TEXT, …)` write\r\n * under one identity and `f.x().metadata(TEXT)` read under another, and the\r\n * read would be `undefined` with nothing to show why. `text-key.spec.ts`\r\n * asserts the two halves of that - `createMetadataKey` is per-call, and this\r\n * is one of its results rather than the function itself.\r\n */\r\nexport const TEXT = createMetadataKey<string>();\r\n","import { WritableSignal } from '@angular/core';\r\nimport { type PathKind, type SchemaPath, type SchemaPathRules, disabled, hidden, metadata } from '@angular/forms/signals';\r\nimport { type EvalOptions, compile, defaultParserOptions, parse } from '@zvenigora/ng-eval-core';\r\nimport { type ExpressionErrorPolicy, applyErrorPolicy, toText, toVisible } from '@zvenigora/ng-eval-forms';\r\nimport { evaluateRule } from './evaluate-rule';\r\nimport { guardIdentifiers } from './guard-identifiers';\r\nimport { createModelSource } from './model-source';\r\nimport { TEXT } from './text-key';\r\n\r\n/**\r\n * What `createExpressionRules` takes, and what a single registration may\r\n * override (plan S 5).\r\n */\r\nexport interface ExpressionRuleOptions {\r\n\r\n /**\r\n * Passed to the context and to the walk. `caseInsensitive` has to reach\r\n * both: on the context it corrects identifier keys, and on the walk it\r\n * corrects *property* names, which the member visitor reads off the\r\n * state's options.\r\n *\r\n * **A per-registration value reaches exactly one of the three places it\r\n * has to: the walk** (plan S 3.5.3). The other two read the **factory's**\r\n * options, which are fixed when `createExpressionRules` is called: the memo\r\n * is built once, there; the context is minted per registration by\r\n * `createRuleContext()` but always from that same fixed setting, so a\r\n * registration cannot move it. So overriding this per registration corrects\r\n * *property* names and leaves *identifier* keys on the factory's setting,\r\n * and one expression then obeys two casing rules. Set `caseInsensitive` on\r\n * the **factory** unless that is the behaviour you want.\r\n *\r\n * Revision 19 item 2 corrects \"both are made once, at\r\n * `createExpressionRules` time\", which was wrong about the context and is\r\n * the claim the README states correctly.\r\n */\r\n eval?: EvalOptions;\r\n\r\n /**\r\n * What a field property does when its expression throws at runtime.\r\n * Defaults to `'undefined'` here, the opposite of `eval-signals`' own\r\n * default - see `ExpressionErrorPolicy` for why.\r\n */\r\n onError?: ExpressionErrorPolicy;\r\n}\r\n\r\n/**\r\n * The three registrars one factory returns, one per Angular rule.\r\n *\r\n * **One call per Angular primitive, never an aggregate** (plan S 3.5.1). A\r\n * single `evalRules(p.x, { visible, text, disabled })` would register three\r\n * different Angular rules behind one name, hiding which one each property\r\n * maps to - which is the `/signals` reviewer checklist's item 4 expressed as\r\n * an API.\r\n *\r\n * Named after the **property**, not after Angular's rule: `evalVisible`\r\n * registers `hidden`, inverted once inside the registrar rather than in every\r\n * consumer's expression, so the same expression string means the same thing\r\n * at this entry point and at `/reactive`.\r\n */\r\nexport interface ExpressionRules {\r\n\r\n /** Registers Angular's `hidden`, inverted (plan S 3.5.1). */\r\n evalVisible: <TValue, TPathKind extends PathKind = PathKind.Root>(\r\n path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>,\r\n expression: string,\r\n options?: ExpressionRuleOptions\r\n ) => void;\r\n\r\n /** Registers `metadata(path, TEXT, …)` (plan S 1.2.8, S 3.5). */\r\n evalText: <TValue, TPathKind extends PathKind = PathKind.Root>(\r\n path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>,\r\n expression: string,\r\n options?: ExpressionRuleOptions\r\n ) => void;\r\n\r\n /**\r\n * Registers Angular's `disabled`.\r\n *\r\n * The `reason` is authored as a static string and is never\r\n * expression-derived (plan S 3.5.2). Angular's `when` returns\r\n * `boolean | string` and a truthy string is *both* \"disabled\" and \"the\r\n * reason\", so a rule yielding `'false'` would otherwise disable the field\r\n * with the reason `\"false\"` - the `toVisible` truthiness trap in a new\r\n * shape. Keeping the expression boolean and the reason static is what kills\r\n * it.\r\n */\r\n evalDisabled: <TValue, TPathKind extends PathKind = PathKind.Root>(\r\n path: SchemaPath<TValue, SchemaPathRules.Supported, TPathKind>,\r\n expression: string,\r\n options?: ExpressionRuleOptions & { reason?: string }\r\n ) => void;\r\n}\r\n\r\n/**\r\n * Binds one model signal and returns the three expression-driven registrars.\r\n *\r\n * **A factory rather than free functions, because a `LogicFn` cannot recover\r\n * the source** (plan S 3.2.1). `RootFieldContext` exposes the *current*\r\n * field's node plus compile-time-token accessors and no root or parent\r\n * handle, so a rule on `p.city` evaluating `country === \"US\"` has no route to\r\n * `country` from inside the `LogicFn`. The source must be closed over at\r\n * registration or it is unreachable.\r\n *\r\n * ```ts\r\n * const rules = createExpressionRules(model);\r\n * const s = schema<Model>((p) => {\r\n * required(p.email); // Angular's\r\n * rules.evalVisible(p.city, 'country === \"US\"'); // ours\r\n * });\r\n * const f = form(model, s);\r\n * ```\r\n *\r\n * **A schema *value* shared across models is the unsupported shape.** Nothing\r\n * stops two `form()` calls from one schema - Angular re-invokes the schema\r\n * body once per `form()`, so each form mints its own contexts - but the\r\n * registrars close over the **factory's** model, and the factory is bound to\r\n * one. A schema built from `createExpressionRules(modelA)` and reused for\r\n * `form(modelB, s)` re-registers every rule and every one of them still reads\r\n * model A: form B renders against form A's data, silently, with no error and\r\n * a fully functional form (Q9).\r\n *\r\n * The supported reuse shape is therefore a schema **function of the rules** -\r\n * `const makeSchema = (rules) => schema<Model>(p => …)`, called per form -\r\n * which keeps reuse while giving each form a factory bound to its own model.\r\n *\r\n * What one factory retains: **one private memo**, per factory, bounded by the\r\n * union of keys the rules mention; and, per rule per `form()`, one\r\n * `EvalContext` and one compiled callback. Nothing registers with a\r\n * `DestroyRef` and there is no `destroy()` - it all becomes garbage with the\r\n * form (S 3.6).\r\n */\r\nexport const createExpressionRules = <TModel extends object>(\r\n model: WritableSignal<TModel>,\r\n options?: ExpressionRuleOptions\r\n): ExpressionRules => {\r\n\r\n // Called **once** per factory: it is what fixes the memo's lifetime, one\r\n // per factory rather than one per rule (plan S 3.6). Calling it per\r\n // registrar instead would satisfy every behavioural criterion in this phase\r\n // while quietly making the memo per rule, which is why the count has a spec\r\n // of its own.\r\n const source = createModelSource(model, options?.eval);\r\n\r\n /**\r\n * Resolves the two levels `ExpressionRuleOptions` arrives at:\r\n * **registration wins, per key** (plan S 3.5.3). Each key is resolved\r\n * independently and neither is a deep merge, so a registration supplying\r\n * only `onError` keeps the factory's `eval` and vice versa.\r\n *\r\n * Exact for `onError`, partial for `eval.caseInsensitive` - see the note on\r\n * `ExpressionRuleOptions.eval`. The divergence is characterised in\r\n * `rules.spec.ts` rather than left to be discovered.\r\n */\r\n const resolveOptions = (rule?: ExpressionRuleOptions): ExpressionRuleOptions => ({\r\n eval: rule?.eval ?? options?.eval,\r\n onError: rule?.onError ?? options?.onError,\r\n });\r\n\r\n /**\r\n * Everything a registrar does before handing Angular a `LogicFn`, and the\r\n * shape of what it returns is the phase's one structural invariant.\r\n *\r\n * **Called once per registration**, so `parse` + `compile` and\r\n * `createRuleContext()` happen at schema-body time - which Angular re-runs\r\n * once per `form()` (Q8), giving S 3.6's count of one context and one\r\n * compiled callback per rule per form. Nothing here re-parses per\r\n * derivation; risk 8 is a compile that drifts into the returned closure, and\r\n * `rules.invocation-count.spec.ts` counts `compile` to say it has not.\r\n *\r\n * **`guardIdentifiers` runs here, between `parse` and `compile`, and one\r\n * call site is deliberate** (S 3.8, revision 18 item 1). Revision 16's rule\r\n * rejects \"the wrapper is shared\" as a substitute for a per-registrar case,\r\n * and the discriminator it states is whether the subject is a path Angular\r\n * owns. This one is not: the guard throws **before any Angular primitive is\r\n * reached**, so in the rejecting case `hidden`, `metadata` and\r\n * `addDisabledReasonRule` are never called and the three registrars have\r\n * nothing downstream that could diverge. Contrast the invocation count and\r\n * the write-error bypass, whose values arrive *through* those three\r\n * primitives - `rules.spec.ts` gives each of them a case per registrar for\r\n * exactly that reason. Anything the returned closure does stays\r\n * registrar-level; this runs before there is a closure.\r\n *\r\n * **`applyErrorPolicy` is the outermost call in the returned closure, and\r\n * that is load-bearing twice over** (S 3.5). The policy has to cover the\r\n * coercion's *input* rather than only the walk - a registrar that coerced\r\n * first would hand `toVisible` a value the policy never saw - and S 6.1.1's\r\n * invocation instrument counts this exact call. M7 measured what a guard\r\n * hoisted *above* the wrapper does to that number: ground truth 1,\r\n * instrument **0**, which reads as \"the rule did not re-run\" in every\r\n * negative case in this package. A later guard belongs **inside** the\r\n * `run` callback, never ahead of this call.\r\n */\r\n const prepare = (expression: string, ruleOptions?: ExpressionRuleOptions): (() => unknown) => {\r\n const resolved = resolveOptions(ruleOptions);\r\n const node = parse(expression, defaultParserOptions);\r\n\r\n guardIdentifiers(expression, node);\r\n\r\n const compiled = compile(node);\r\n const context = source.createRuleContext();\r\n\r\n return () =>\r\n applyErrorPolicy(() => evaluateRule(compiled, context, resolved.eval), resolved.onError);\r\n };\r\n\r\n return {\r\n\r\n // Angular's **config** overload (S 1.2.7); the deprecated one takes the\r\n // `LogicFn` positionally. `hidden`'s `when` is the only required config of\r\n // the three primitives this entry point registers.\r\n //\r\n // The `!` is S 3.5.1's inversion, and it lives here rather than in every\r\n // consumer's expression so the same rule string means the same thing at\r\n // this entry point and at `/reactive`'s `visible`.\r\n evalVisible: (path, expression, ruleOptions) => {\r\n const evaluated = prepare(expression, ruleOptions);\r\n\r\n hidden(path, { when: () => !toVisible(evaluated()) });\r\n },\r\n\r\n // `text` has no dedicated primitive, so it is Angular's `metadata` against\r\n // the module-scope key of `text-key.ts` (S 1.2.8) - its own mechanism\r\n // rather than a second one beside it.\r\n evalText: (path, expression, ruleOptions) => {\r\n const evaluated = prepare(expression, ruleOptions);\r\n\r\n metadata(path, TEXT, () => toText(evaluated()));\r\n },\r\n\r\n // Angular's polarity, uninverted: `/reactive` ships no `disabled`, so no\r\n // expression has to mean the same thing at two entry points, and `true`\r\n // disabling is what an author expects (S 3.5.1).\r\n //\r\n // **The reason is a static option, never the expression's return**\r\n // (S 3.5.2). Angular's `when` is a single field carrying both the\r\n // condition and the reason - it returns `boolean | string`, and a truthy\r\n // string is *both* (1.2.7) - so a registrar forwarding `evaluated()` raw\r\n // would disable a field on the string `'false'` **with the reason\r\n // `\"false\"`**. Coercing through `toVisible` first, and sourcing the reason\r\n // from the registration instead, is what kills that: the string never\r\n // comes from the expression at all.\r\n //\r\n // A *dynamic* reason stays out of scope - it reopens the trap and needs a\r\n // coercion rule of its own.\r\n evalDisabled: (path, expression, ruleOptions) => {\r\n const evaluated = prepare(expression, ruleOptions);\r\n const reason = ruleOptions?.reason;\r\n\r\n disabled(path, {\r\n when: () => {\r\n // `evaluated()` first, so `applyErrorPolicy` stays the outermost\r\n // call in this body as it is in the other two (S 3.5). `reason` is\r\n // read from a closure rather than branched on ahead of the call:\r\n // a guard hoisted above the wrapper is M7's arrangement, which\r\n // reads 0 on an instrument whose ground truth is 1.\r\n const on = toVisible(evaluated());\r\n\r\n return on && reason !== undefined ? reason : on;\r\n },\r\n });\r\n },\r\n };\r\n};\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"zvenigora-ng-eval-forms.mjs","sources":["../../../../modules/eval-forms/src/lib/coercion.ts","../../../../modules/eval-forms/src/lib/error-policy.ts","../../../../modules/eval-forms/src/lib/field-context.ts","../../../../modules/eval-forms/src/zvenigora-ng-eval-forms.ts"],"sourcesContent":["/**\r\n * Coerces an expression's result to the boolean a `visible` rule needs.\r\n *\r\n * **JavaScript truthiness, and nothing cleverer** (plan S 3.6). A rule author\r\n * writing `visible: \"country === 'US'\"` gets a boolean already; the coercion\r\n * exists for the ones who write `visible: \"country\"`.\r\n *\r\n * `undefined` is therefore the load-bearing input rather than an edge case.\r\n * An *empty* field and a *missing* field are indistinguishable here - S 3.4.3:\r\n * a field key bound to `undefined` is treated as absent, and an empty\r\n * `FormControl` is the common case - so both arrive as `undefined`, and both\r\n * mean \"not visible\".\r\n *\r\n * The string `'false'` is **visible**, because it is a non-empty string. That\r\n * is truthiness rather than parsing, and it is the answer a form-builder UI\r\n * storing every value as a string will hit. Documented in the README rather\r\n * than special-cased: a coercion that parsed `'false'` would then owe an\r\n * answer for `'no'`, `'0'` and `'off'`, none of which JavaScript has one for.\r\n *\r\n * Both adapters call this, which is why it is in the core: `/reactive` and\r\n * `/signals` differ in how they schedule the recompute and not in what the\r\n * result means (S 3.4.5).\r\n *\r\n * @param value - Whatever the expression evaluated to.\r\n * @returns `true` when the value is truthy.\r\n */\r\nexport const toVisible = (value: unknown): boolean => !!value;\r\n\r\n/**\r\n * Coerces an expression's result to the string a `text` rule needs.\r\n *\r\n * `String(value)`, with `null` and `undefined` mapping to `''` (plan S 3.6).\r\n * Those two are the carve-out and the only one: rendering the literal text\r\n * `undefined` into a label is the failure this rule exists to prevent, and it\r\n * is what a bare `String(value)` would do for the most ordinary input a form\r\n * has - the empty control of S 3.4.3.\r\n *\r\n * Every *other* falsy value stringifies normally: `0` is `'0'` and `false` is\r\n * `'false'`. Mapping all falsy values to `''` would be the obvious way to\r\n * write this rule wrongly, and it would blank a field whose value is\r\n * legitimately zero.\r\n *\r\n * @param value - Whatever the expression evaluated to.\r\n * @returns The value as a string, or `''` when it is `null` or `undefined`.\r\n */\r\nexport const toText = (value: unknown): string =>\r\n value === null || value === undefined ? '' : String(value);\r\n","// A **value** import, and it has to stay one: `instanceof` needs the\r\n// constructor, not the type. `field-context.ts` already imports a value from\r\n// this package, so this adds no dependency edge.\r\n//\r\n// **The compiler enforces the form, and the plan said otherwise** - S 3.4 and\r\n// S 8.1 both call a type-only import here a change that \"would compile and the\r\n// guard would silently never fire\". Measured in step 3: `import type` is\r\n// **TS1361** at `applyErrorPolicy`'s `instanceof`, so it fails\r\n// `build:production` and gate 7, and under ts-jest's transpile path the elided\r\n// binding is a `ReferenceError` inside the `catch` that reddens *both* policy\r\n// arms. Loud at every gate, in other words - see the plan's revision 13.\r\n//\r\n// The failure mode that *is* silent is a different one and no import syntax\r\n// prevents it: two resolved copies of `@zvenigora/ng-eval-signals` give two\r\n// distinct constructors, and `instanceof` is then false for an error the other\r\n// copy threw. That is what the write-error arm of `evaluate-rule.spec.ts`\r\n// covers by throwing across the real package boundary rather than\r\n// constructing the error itself.\r\nimport { SignalContextWriteError } from '@zvenigora/ng-eval-signals';\r\n\r\n/**\r\n * What a field property does when its expression throws at runtime.\r\n *\r\n * - `'throw'` - rethrow, matching `eval-signals`' own default.\r\n * - `'undefined'` - the property resolves `undefined`, which `toVisible`\r\n * reads as not visible and `toText` reads as `''`.\r\n * - a function - called with the error; its return value becomes the\r\n * property's value.\r\n *\r\n * **The default here is `'undefined'`, the opposite of `eval-signals`'**, and\r\n * the consumer is the reason (plan S 3.4.4). An expression that fails in\r\n * `eval-signals` was written by the developer reading the stack trace. An\r\n * expression that fails here may have been typed into a form builder by an\r\n * end user, and the right response to \"the admin wrote a bad rule\" is a field\r\n * that does not render, not an application that throws on every change\r\n * detection pass. Consumers who want the strict behaviour pass `'throw'`.\r\n *\r\n * Resolving that default is a real step and not a formality: `createEvalSignal`\r\n * does `options?.onError ?? 'throw'`, so forwarding an *absent* policy verbatim\r\n * inherits `'throw'` - the opposite of this default. The `/reactive` binding\r\n * substitutes its own before the call.\r\n *\r\n * The union is structurally identical to `EvalSignalOptions['onError']`\r\n * **deliberately**: `/reactive` resolves the default here and forwards the\r\n * value to `createEvalSignal` with no mapping between the two unions, which is\r\n * where two near-identical types would drift apart.\r\n *\r\n * `SignalContextWriteError` is **not** routed through this, in either adapter,\r\n * matching `phase-3-plan.md` S 3.6.3. A write violation is static - illegal on\r\n * every recompute with every dataset - and swallowing it under a default of\r\n * `'undefined'` would hand every consumer a silent blank for a bug in the\r\n * rule's own syntax. `createEvalSignal` already bypasses `onError` for it.\r\n *\r\n * **{@link applyErrorPolicy} ships beside this type as of Phase 6** (its\r\n * S 3.4). Through Phase 4 only the type shipped, because the helper would have\r\n * had no caller: on the `/reactive` path - the only path Phase 4 shipped -\r\n * `createEvalSignal` applies the three cases internally, and shipping an\r\n * unexercised code path is what S 9.1 declined to do for the scope-leak\r\n * containment. The `/signals` adapter is that caller **from step 4**, where\r\n * the registrars stop being stubs; `/reactive` does not use the helper at all.\r\n */\r\nexport type ExpressionErrorPolicy =\r\n | 'throw'\r\n | 'undefined'\r\n | ((error: unknown) => unknown);\r\n\r\n/**\r\n * Runs one rule under one {@link ExpressionErrorPolicy}, with the one error\r\n * that must never be routed through it taken out first.\r\n *\r\n * ```ts\r\n * applyErrorPolicy(() => evaluateRule(compiled, context, options), onError)\r\n * ```\r\n *\r\n * **`SignalContextWriteError` re-throws in every mode, including a handler\r\n * function** (plan S 3.4, 1.2.11). On `/reactive` that bypass came free -\r\n * `createEvalSignal` re-throws it regardless of `onError` (`eval-signal.ts:353`)\r\n * - and that behaviour lives *inside* `createEvalSignal`, which the `/signals`\r\n * path never calls. It does not travel with the type, so it is re-implemented\r\n * here or it does not exist. An assigning expression is illegal on every\r\n * recompute with every dataset, so under this module's default of\r\n * `'undefined'` it would otherwise render as a permanently blank field with\r\n * nothing in the console.\r\n *\r\n * **The `instanceof` needs the constructor, so the import of that class is a\r\n * *value* import** - see the note above it, and the plan's revision 13, for\r\n * why that form is protected by the compiler rather than by this spec.\r\n * `evaluate-rule.spec.ts`'s write-error arm asserts on the **class** rather\r\n * than on a message because what it covers is the bypass's *behaviour* across\r\n * a real package boundary, which no compiler check reaches.\r\n *\r\n * **It lives in the core while `/signals`' choke point does not, and the pair\r\n * is not inconsistent** (S 3.4.1). The discriminator is what the symbol grants\r\n * a caller who reaches it: `evaluateRule` published *is* a second path to the\r\n * walk, which S 9.1 requires there be only one of, while this is a pure\r\n * function over an error and a policy that takes no context, holds no compiled\r\n * callback and cannot reach a walk. Publishing it grants a caller nothing they\r\n * could not write in four lines, and buys the thing duplication would lose -\r\n * one implementation of the write-error bypass for both adapters, which cannot\r\n * then drift into disagreeing about the one error that must never be\r\n * swallowed.\r\n *\r\n * **It re-throws the error it caught; `/reactive` re-wraps.** `createEvalSignal`\r\n * throws a *new* `SignalContextWriteError` carrying the offending expression\r\n * string, which it has and this path does not - a `LogicFn` holds a compiled\r\n * callback, not the source text. So the same misuse surfaces a different\r\n * object at each entry point: wrapped with the expression under `/reactive`,\r\n * the original here. Stated rather than hidden; it is the one thing a consumer\r\n * catching this error across both adapters would notice.\r\n *\r\n * @param run - The rule invocation. On `/signals` this is the choke point,\r\n * never a bare walk: containment is inside `run`, so a throw is\r\n * contained before this function decides what to do about it.\r\n * @param policy - Defaults to `'undefined'`, per {@link ExpressionErrorPolicy}.\r\n * Resolving the default *here* is what makes an absent policy\r\n * mean `'undefined'` rather than inheriting `eval-signals`'\r\n * opposite one.\r\n */\r\nexport const applyErrorPolicy = <T>(\r\n run: () => T,\r\n policy: ExpressionErrorPolicy = 'undefined'\r\n): T | undefined => {\r\n\r\n try {\r\n return run();\r\n } catch (error) {\r\n if (error instanceof SignalContextWriteError) {\r\n throw error;\r\n }\r\n\r\n if (policy === 'throw') {\r\n throw error;\r\n }\r\n\r\n if (policy === 'undefined') {\r\n return undefined;\r\n }\r\n\r\n return policy(error) as T | undefined;\r\n }\r\n};\r\n","import { EvalContext, EvalOptions } from '@zvenigora/ng-eval-core';\r\nimport { SignalContextSource, createSignalContext } from '@zvenigora/ng-eval-signals';\r\n\r\n/**\r\n * Composes one `EvalContext` for one field, out of two live sources.\r\n *\r\n * The mechanism is **two lookups, not a joined record** (plan S 3.4.2). The\r\n * field half is a `createSignalContext` over `fieldSource`; the form half is a\r\n * second resolver pushed onto the same context's `lookups`. Both close over\r\n * their source and read it at resolve time, so a key added to either after\r\n * construction resolves - there is no third object to keep in sync, and no\r\n * write-through to N contexts.\r\n *\r\n * `EvalContext.get` walks `lookups` in order and takes the first\r\n * non-`undefined`, so the field half is consulted first.\r\n *\r\n * One context per field, never one shared across fields (plan S 3.4.1):\r\n * `get` resolves `scopes` and `original` *before* `lookups`, so anything left\r\n * on a shared context - an arrow function's leaked scope, say - would be read\r\n * ahead of every other field's source key of the same name, for the life of\r\n * the form.\r\n *\r\n * **Resolution is live; appearance is not reactive.** An expression that read\r\n * a then-missing key subscribed to nothing, so the owner of the source calls\r\n * `invalidate()` when the *key set* changes. A change to a *value* is\r\n * Angular's job and needs nothing.\r\n *\r\n * **Precedence is value-dependent, not permanent.** A field key wins while\r\n * its value is *present*: `EvalContext.get` treats `undefined` as absent at\r\n * every step, and the field resolver returns `undefined` both for \"no such\r\n * key\" and for \"key bound to `undefined`\" - so an empty `FormControl`, which\r\n * is the common case rather than an exotic one, falls through to the form\r\n * value. Documented as a limitation (plan S 3.4.3): distinguishing the two\r\n * would need a sentinel threaded through `EvalContext.get`, which is\r\n * `eval-core`'s and out of this phase's scope.\r\n *\r\n * @param formSource - The form-wide keys, shared by every field of the form.\r\n * @param fieldSource - The field-local keys. These win on a name collision,\r\n * while their value is not `undefined`.\r\n * @param options - Passed to `createSignalContext` for **both** halves;\r\n * configures this context and its resolvers, not the walk.\r\n * As upstream, `caseInsensitive` corrects identifier keys\r\n * here but not *property* names - the member visitor reads\r\n * those off the state's options, so the same options must\r\n * also reach `simpleEval` / `createState`.\r\n */\r\nexport const createFieldContext = (\r\n formSource: SignalContextSource,\r\n fieldSource: SignalContextSource,\r\n options?: EvalOptions\r\n): EvalContext => {\r\n\r\n const context = createSignalContext(fieldSource, options);\r\n\r\n // The form half **borrows upstream's resolver rather than rewriting it**\r\n // (S 3.4.2, candidate A, settled in step 2). The second context is\r\n // discarded and only its closure survives, so this is still one\r\n // `EvalContext` per field and the field half is still consulted first.\r\n //\r\n // The deciding reason is row 2 of S 3.4.2's measured table, and it is that\r\n // the hand-written read is *wrong*, not merely thinner: it returns a signal\r\n // un-called, so a form key holding `signal(undefined)` resolves to the\r\n // signal **function**, which is truthy - and `visible: \"country\"` then\r\n // renders a field precisely when its value is absent, silently, on an empty\r\n // control. That is every form's first render. Recorded here because the\r\n // alternative was rejected on correctness and stays rejected even once\r\n // S 3.5 names a mechanism that keeps a plain-value record live.\r\n //\r\n // Borrowing also brings own-property semantics - so a server-supplied field\r\n // named `constructor` cannot resolve off `Object.prototype` *through this\r\n // half*, which is a narrower claim than it looks and no test reaches it:\r\n // `get` consults `original` before `lookups`, and `getContextValue` reads a\r\n // plain object as a bare property access, so an inherited name resolves\r\n // there first and never arrives here (S 3.4.3's third layer, upstream and\r\n // out of this phase's scope). It also brings `caseInsensitive` correction\r\n // and `warnOnNestedSignals` over the form source, with no import beyond the\r\n // one already here.\r\n //\r\n // The scan is the one cost: `createSignalContext` runs\r\n // `warnOnNestedSignals` unconditionally, so a form of N fields scans the\r\n // same form source N times and, in dev mode, would warn N times under\r\n // *upstream's* symbol name for a call this library made. Construction-time\r\n // only - no per-node or per-recompute path is touched.\r\n context.lookups.push(...createSignalContext(formSource, options).lookups);\r\n\r\n return context;\r\n};\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;AAyBG;AACI,MAAM,SAAS,GAAG,CAAC,KAAc,KAAc,CAAC,CAAC;AAExD;;;;;;;;;;;;;;;;AAgBG;AACI,MAAM,MAAM,GAAG,CAAC,KAAc,KACnC,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK;;AC9C3D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAiDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDG;AACI,MAAM,gBAAgB,GAAG,CAC9B,GAAY,EACZ,MAAA,GAAgC,WAAW,KAC1B;AAEjB,IAAA,IAAI;QACF,OAAO,GAAG,EAAE;IACd;IAAE,OAAO,KAAK,EAAE;AACd,QAAA,IAAI,KAAK,YAAY,uBAAuB,EAAE;AAC5C,YAAA,MAAM,KAAK;QACb;AAEA,QAAA,IAAI,MAAM,KAAK,OAAO,EAAE;AACtB,YAAA,MAAM,KAAK;QACb;AAEA,QAAA,IAAI,MAAM,KAAK,WAAW,EAAE;AAC1B,YAAA,OAAO,SAAS;QAClB;AAEA,QAAA,OAAO,MAAM,CAAC,KAAK,CAAkB;IACvC;AACF;;ACzIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CG;AACI,MAAM,kBAAkB,GAAG,CAChC,UAA+B,EAC/B,WAAgC,EAChC,OAAqB,KACN;IAEf,MAAM,OAAO,GAAG,mBAAmB,CAAC,WAAW,EAAE,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BzD,IAAA,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC;AAEzE,IAAA,OAAO,OAAO;AAChB;;ACtFA;;AAEG;;;;"}
1
+ {"version":3,"file":"zvenigora-ng-eval-forms.mjs","sources":["../../../../modules/eval-forms/src/lib/coercion.ts","../../../../modules/eval-forms/src/lib/error-policy.ts","../../../../modules/eval-forms/src/lib/field-context.ts","../../../../modules/eval-forms/src/zvenigora-ng-eval-forms.ts"],"sourcesContent":["/**\r\n * Coerces an expression's result to the boolean a `visible` rule needs.\r\n *\r\n * **JavaScript truthiness, and nothing cleverer** (plan S 3.6). A rule author\r\n * writing `visible: \"country === 'US'\"` gets a boolean already; the coercion\r\n * exists for the ones who write `visible: \"country\"`.\r\n *\r\n * `undefined` is therefore the load-bearing input rather than an edge case.\r\n * An *empty* field and a *missing* field are indistinguishable here - S 3.4.3:\r\n * a field key bound to `undefined` is treated as absent, and an empty\r\n * `FormControl` is the common case - so both arrive as `undefined`, and both\r\n * mean \"not visible\".\r\n *\r\n * The string `'false'` is **visible**, because it is a non-empty string. That\r\n * is truthiness rather than parsing, and it is the answer a form-builder UI\r\n * storing every value as a string will hit. Documented in the README rather\r\n * than special-cased: a coercion that parsed `'false'` would then owe an\r\n * answer for `'no'`, `'0'` and `'off'`, none of which JavaScript has one for.\r\n *\r\n * Both adapters call this, which is why it is in the core: `/reactive` and\r\n * `/signals` differ in how they schedule the recompute and not in what the\r\n * result means (S 3.4.5).\r\n *\r\n * @param value - Whatever the expression evaluated to.\r\n * @returns `true` when the value is truthy.\r\n */\r\nexport const toVisible = (value: unknown): boolean => !!value;\r\n\r\n/**\r\n * Coerces an expression's result to the string a `text` rule needs.\r\n *\r\n * `String(value)`, with `null` and `undefined` mapping to `''` (plan S 3.6).\r\n * Those two are the carve-out and the only one: rendering the literal text\r\n * `undefined` into a label is the failure this rule exists to prevent, and it\r\n * is what a bare `String(value)` would do for the most ordinary input a form\r\n * has - the empty control of S 3.4.3.\r\n *\r\n * Every *other* falsy value stringifies normally: `0` is `'0'` and `false` is\r\n * `'false'`. Mapping all falsy values to `''` would be the obvious way to\r\n * write this rule wrongly, and it would blank a field whose value is\r\n * legitimately zero.\r\n *\r\n * @param value - Whatever the expression evaluated to.\r\n * @returns The value as a string, or `''` when it is `null` or `undefined`.\r\n */\r\nexport const toText = (value: unknown): string =>\r\n value === null || value === undefined ? '' : String(value);\r\n","// A **value** import, and it has to stay one: `instanceof` needs the\r\n// constructor, not the type. `field-context.ts` already imports a value from\r\n// this package, so this adds no dependency edge.\r\n//\r\n// **The compiler enforces the form, and the plan said otherwise** - S 3.4 and\r\n// S 8.1 both call a type-only import here a change that \"would compile and the\r\n// guard would silently never fire\". Measured in step 3: `import type` is\r\n// **TS1361** at `applyErrorPolicy`'s `instanceof`, so it fails\r\n// `build:production` and gate 7, and under ts-jest's transpile path the elided\r\n// binding is a `ReferenceError` inside the `catch` that reddens *both* policy\r\n// arms. Loud at every gate, in other words - see the plan's revision 13.\r\n//\r\n// The failure mode that *is* silent is a different one and no import syntax\r\n// prevents it: two resolved copies of `@zvenigora/ng-eval-signals` give two\r\n// distinct constructors, and `instanceof` is then false for an error the other\r\n// copy threw. That is what the write-error arm of `evaluate-rule.spec.ts`\r\n// covers by throwing across the real package boundary rather than\r\n// constructing the error itself.\r\nimport { SignalContextWriteError } from '@zvenigora/ng-eval-signals';\r\n\r\n/**\r\n * What a field property does when its expression throws at runtime.\r\n *\r\n * - `'throw'` - rethrow, matching `eval-signals`' own default.\r\n * - `'undefined'` - the property resolves `undefined`, which `toVisible`\r\n * reads as not visible and `toText` reads as `''`.\r\n * - a function - called with the error; its return value becomes the\r\n * property's value.\r\n *\r\n * **The default here is `'undefined'`, the opposite of `eval-signals`'**, and\r\n * the consumer is the reason (plan S 3.4.4). An expression that fails in\r\n * `eval-signals` was written by the developer reading the stack trace. An\r\n * expression that fails here may have been typed into a form builder by an\r\n * end user, and the right response to \"the admin wrote a bad rule\" is a field\r\n * that does not render, not an application that throws on every change\r\n * detection pass. Consumers who want the strict behaviour pass `'throw'`.\r\n *\r\n * Resolving that default is a real step and not a formality: `createEvalSignal`\r\n * does `options?.onError ?? 'throw'`, so forwarding an *absent* policy verbatim\r\n * inherits `'throw'` - the opposite of this default. The `/reactive` binding\r\n * substitutes its own before the call.\r\n *\r\n * The union is structurally identical to `EvalSignalOptions['onError']`\r\n * **deliberately**: `/reactive` resolves the default here and forwards the\r\n * value to `createEvalSignal` with no mapping between the two unions, which is\r\n * where two near-identical types would drift apart.\r\n *\r\n * `SignalContextWriteError` is **not** routed through this, in either adapter,\r\n * matching `phase-3-plan.md` S 3.6.3. A write violation is static - illegal on\r\n * every recompute with every dataset - and swallowing it under a default of\r\n * `'undefined'` would hand every consumer a silent blank for a bug in the\r\n * rule's own syntax. `createEvalSignal` already bypasses `onError` for it.\r\n *\r\n * **{@link applyErrorPolicy} ships beside this type as of Phase 6** (its\r\n * S 3.4). Through Phase 4 only the type shipped, because the helper would have\r\n * had no caller: on the `/reactive` path - the only path Phase 4 shipped -\r\n * `createEvalSignal` applies the three cases internally, and shipping an\r\n * unexercised code path is what S 9.1 declined to do for the scope-leak\r\n * containment. The `/signals` adapter is that caller **from step 4**, where\r\n * the registrars stop being stubs; `/reactive` does not use the helper at all.\r\n */\r\nexport type ExpressionErrorPolicy =\r\n | 'throw'\r\n | 'undefined'\r\n | ((error: unknown) => unknown);\r\n\r\n/**\r\n * Runs one rule under one {@link ExpressionErrorPolicy}, with the one error\r\n * that must never be routed through it taken out first.\r\n *\r\n * ```ts\r\n * applyErrorPolicy(() => evaluateRule(compiled, context, options), onError)\r\n * ```\r\n *\r\n * **`SignalContextWriteError` re-throws in every mode, including a handler\r\n * function** (plan S 3.4, 1.2.11). On `/reactive` that bypass came free -\r\n * `createEvalSignal` re-throws it regardless of `onError` (`eval-signal.ts:353`)\r\n * - and that behaviour lives *inside* `createEvalSignal`, which the `/signals`\r\n * path never calls. It does not travel with the type, so it is re-implemented\r\n * here or it does not exist. An assigning expression is illegal on every\r\n * recompute with every dataset, so under this module's default of\r\n * `'undefined'` it would otherwise render as a permanently blank field with\r\n * nothing in the console.\r\n *\r\n * **The `instanceof` needs the constructor, so the import of that class is a\r\n * *value* import** - see the note above it, and the plan's revision 13, for\r\n * why that form is protected by the compiler rather than by this spec.\r\n * `evaluate-rule.spec.ts`'s write-error arm asserts on the **class** rather\r\n * than on a message because what it covers is the bypass's *behaviour* across\r\n * a real package boundary, which no compiler check reaches.\r\n *\r\n * **It lives in the core while `/signals`' choke point does not, and the pair\r\n * is not inconsistent** (S 3.4.1). The discriminator is what the symbol grants\r\n * a caller who reaches it: `evaluateRule` published *is* a second path to the\r\n * walk, which S 9.1 requires there be only one of, while this is a pure\r\n * function over an error and a policy that takes no context, holds no compiled\r\n * callback and cannot reach a walk. Publishing it grants a caller nothing they\r\n * could not write in four lines, and buys the thing duplication would lose -\r\n * one implementation of the write-error bypass for both adapters, which cannot\r\n * then drift into disagreeing about the one error that must never be\r\n * swallowed.\r\n *\r\n * **It re-throws the error it caught; `/reactive` re-wraps.** `createEvalSignal`\r\n * throws a *new* `SignalContextWriteError` carrying the offending expression\r\n * string, which it has and this path does not - a `LogicFn` holds a compiled\r\n * callback, not the source text. So the same misuse surfaces a different\r\n * object at each entry point: wrapped with the expression under `/reactive`,\r\n * the original here. Stated rather than hidden; it is the one thing a consumer\r\n * catching this error across both adapters would notice.\r\n *\r\n * @param run - The rule invocation. On `/signals` this is the choke point,\r\n * never a bare walk: containment is inside `run`, so a throw is\r\n * contained before this function decides what to do about it.\r\n * @param policy - Defaults to `'undefined'`, per {@link ExpressionErrorPolicy}.\r\n * Resolving the default *here* is what makes an absent policy\r\n * mean `'undefined'` rather than inheriting `eval-signals`'\r\n * opposite one.\r\n */\r\nexport const applyErrorPolicy = <T>(\r\n run: () => T,\r\n policy: ExpressionErrorPolicy = 'undefined'\r\n): T | undefined => {\r\n\r\n try {\r\n return run();\r\n } catch (error) {\r\n if (error instanceof SignalContextWriteError) {\r\n throw error;\r\n }\r\n\r\n if (policy === 'throw') {\r\n throw error;\r\n }\r\n\r\n if (policy === 'undefined') {\r\n return undefined;\r\n }\r\n\r\n return policy(error) as T | undefined;\r\n }\r\n};\r\n","import { EvalContext, EvalOptions } from '@zvenigora/ng-eval-core';\r\nimport { SignalContextSource, createSignalContext } from '@zvenigora/ng-eval-signals';\r\n\r\n/**\r\n * Composes one `EvalContext` for one field, out of two live sources.\r\n *\r\n * The mechanism is **two lookups, not a joined record** (plan S 3.4.2). The\r\n * field half is a `createSignalContext` over `fieldSource`; the form half is a\r\n * second resolver pushed onto the same context's `lookups`. Both close over\r\n * their source and read it at resolve time, so a key added to either after\r\n * construction resolves - there is no third object to keep in sync, and no\r\n * write-through to N contexts.\r\n *\r\n * `EvalContext.get` walks `lookups` in order and takes the first\r\n * non-`undefined`, so the field half is consulted first.\r\n *\r\n * One context per field, never one shared across fields (plan S 3.4.1):\r\n * `get` resolves `scopes` and `original` *before* `lookups`, so anything left\r\n * on a shared context - an arrow function's leaked scope, say - would be read\r\n * ahead of every other field's source key of the same name, for the life of\r\n * the form.\r\n *\r\n * **Resolution is live; appearance is not reactive.** An expression that read\r\n * a then-missing key subscribed to nothing, so the owner of the source calls\r\n * `invalidate()` when the *key set* changes. A change to a *value* is\r\n * Angular's job and needs nothing.\r\n *\r\n * **Precedence is value-dependent, not permanent.** A field key wins while\r\n * its value is *present*: `EvalContext.get` treats `undefined` as absent at\r\n * every step, and the field resolver returns `undefined` both for \"no such\r\n * key\" and for \"key bound to `undefined`\" - so an empty `FormControl`, which\r\n * is the common case rather than an exotic one, falls through to the form\r\n * value. Documented as a limitation (plan S 3.4.3): distinguishing the two\r\n * would need a sentinel threaded through `EvalContext.get`, which is\r\n * `eval-core`'s and out of this phase's scope.\r\n *\r\n * @param formSource - The form-wide keys, shared by every field of the form.\r\n * @param fieldSource - The field-local keys. These win on a name collision,\r\n * while their value is not `undefined`.\r\n * @param options - Passed to `createSignalContext` for **both** halves;\r\n * configures this context and its resolvers, not the walk.\r\n * As upstream, `caseInsensitive` corrects identifier keys\r\n * here but not *property* names - the member visitor reads\r\n * those off the state's options, so the same options must\r\n * also reach `simpleEval` / `createState`.\r\n */\r\nexport const createFieldContext = (\r\n formSource: SignalContextSource,\r\n fieldSource: SignalContextSource,\r\n options?: EvalOptions\r\n): EvalContext => {\r\n\r\n const context = createSignalContext(fieldSource, options);\r\n\r\n // The form half **borrows upstream's resolver rather than rewriting it**\r\n // (S 3.4.2, candidate A, settled in step 2). The second context is\r\n // discarded and only its closure survives, so this is still one\r\n // `EvalContext` per field and the field half is still consulted first.\r\n //\r\n // The deciding reason is row 2 of S 3.4.2's measured table, and it is that\r\n // the hand-written read is *wrong*, not merely thinner: it returns a signal\r\n // un-called, so a form key holding `signal(undefined)` resolves to the\r\n // signal **function**, which is truthy - and `visible: \"country\"` then\r\n // renders a field precisely when its value is absent, silently, on an empty\r\n // control. That is every form's first render. Recorded here because the\r\n // alternative was rejected on correctness and stays rejected even once\r\n // S 3.5 names a mechanism that keeps a plain-value record live.\r\n //\r\n // Borrowing also brings own-property semantics - so a server-supplied field\r\n // named `constructor` cannot resolve off `Object.prototype` *through this\r\n // half*, which is a narrower claim than it looks and no test reaches it:\r\n // `get` consults `original` before `lookups`, and `getContextValue` reads a\r\n // plain object as a bare property access, so an inherited name resolves\r\n // there first and never arrives here (S 3.4.3's third layer, upstream and\r\n // out of this phase's scope). It also brings `caseInsensitive` correction\r\n // and `warnOnNestedSignals` over the form source, with no import beyond the\r\n // one already here.\r\n //\r\n // The scan is the one cost: `createSignalContext` runs\r\n // `warnOnNestedSignals` unconditionally, so a form of N fields scans the\r\n // same form source N times and, in dev mode, would warn N times under\r\n // *upstream's* symbol name for a call this library made. Construction-time\r\n // only - no per-node or per-recompute path is touched.\r\n context.lookups.push(...createSignalContext(formSource, options).lookups);\r\n\r\n return context;\r\n};\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;AAyBG;AACI,MAAM,SAAS,GAAG,CAAC,KAAc,KAAc,CAAC,CAAC;AAExD;;;;;;;;;;;;;;;;AAgBG;AACI,MAAM,MAAM,GAAG,CAAC,KAAc,KACnC,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK;;AC9C3D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAiDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDG;AACI,MAAM,gBAAgB,GAAG,CAC9B,GAAY,EACZ,MAAA,GAAgC,WAAW,KAC1B;AAEjB,IAAA,IAAI;QACF,OAAO,GAAG,EAAE;IACd;IAAE,OAAO,KAAK,EAAE;AACd,QAAA,IAAI,KAAK,YAAY,uBAAuB,EAAE;AAC5C,YAAA,MAAM,KAAK;QACb;AAEA,QAAA,IAAI,MAAM,KAAK,OAAO,EAAE;AACtB,YAAA,MAAM,KAAK;QACb;AAEA,QAAA,IAAI,MAAM,KAAK,WAAW,EAAE;AAC1B,YAAA,OAAO,SAAS;QAClB;AAEA,QAAA,OAAO,MAAM,CAAC,KAAK,CAAkB;IACvC;AACF;;ACzIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CG;AACI,MAAM,kBAAkB,GAAG,CAChC,UAA+B,EAC/B,WAAgC,EAChC,OAAqB,KACN;IAEf,MAAM,OAAO,GAAG,mBAAmB,CAAC,WAAW,EAAE,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BzD,IAAA,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC;AAEzE,IAAA,OAAO,OAAO;AAChB;;ACtFA;;AAEG;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zvenigora/ng-eval-forms",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Angular form field properties from ng-eval expressions",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,8 +19,8 @@
19
19
  "@angular/forms": ">=19.0.0",
20
20
  "rxjs": "^7.8.0",
21
21
  "acorn-walk": "^8.3.0",
22
- "@zvenigora/ng-eval-core": ">=0.3.0 <0.7.0",
23
- "@zvenigora/ng-eval-signals": "^0.1.0"
22
+ "@zvenigora/ng-eval-core": ">=0.3.0 <0.9.0",
23
+ "@zvenigora/ng-eval-signals": ">=0.1.0 <0.3.0"
24
24
  },
25
25
  "publishConfig": {
26
26
  "access": "public"