import type { ViaDescriptor } from './index.js'; import type { ViaPipeline } from './pipeline.js'; import type { DictKeyDescriptor, I18nTextDescriptor, StaticDictDescriptor } from '../../port/with/i18n-strategy.js'; import type { ComputedDescriptor } from '../../port/with/computed-strategy.js'; import type { LookupDescriptor } from '../../port/with/lookup-strategy.js'; /** Tagged container returned by {@link via}. Readonly — never mutated after construction. */ export interface ViaFieldSpec { readonly _noydbVia: true; readonly descriptors: readonly ViaDescriptor[]; } /** * Compose one or more Via feature descriptors for a single field. * * @example * ```ts * vault.collection('invoices', { * viaFields: { total: via(money({ currency: 'EUR' })) }, * }) * ``` */ export declare function via(...descriptors: ViaDescriptor[]): ViaFieldSpec; /** Runtime predicate for detecting a {@link ViaFieldSpec}. */ export declare function isViaFieldSpec(x: unknown): x is ViaFieldSpec; /** The money/i18n/lookup sugar keys + a `viaFields` map — the inputs {@link mergeViaFields} reconciles. */ export interface ViaFieldSources { readonly moneyFields?: Record | undefined; readonly i18nFields?: Record | undefined; readonly dictKeyFields?: Record | undefined; /** — lookup()/enumOf()/dict() sugar key (#650 Task 2). */ readonly lookupFields?: Record | undefined; readonly viaFields?: Record | undefined; } /** The effective per-feature field maps after merging sugar keys with `viaFields`. */ export interface MergedViaFields { readonly moneyFields: Record | undefined; readonly i18nFields: Record | undefined; readonly dictKeyFields: Record | undefined; /** `via(computed(fn, { deps, mode }))` entries (#638 Task 7) — there is no `computed` * sugar KEY to merge against here (unlike money/i18n/dictKey); `collection-config.ts` * unions this with the `computed:` option's own entries before splitting by mode. */ readonly computedFields: Record | undefined; /** `lookupFields` sugar key merged with `via(lookup(...))`/`via(enumOf(...))`/`via(dict(...))` * entries (#650 Task 2) — a SEPARATE binding from i18n; `dictKey()`/`staticDict()` stay * routed onto `dictKeyFields`/the i18n binding (the alias, unchanged). */ readonly lookupFields: Record | undefined; } /** * Merge `viaFields` (the {@link via} composer) with the money/i18n sugar * keys into the effective maps every consumer reads — the kernel's * `compileViaBindings` (the `ViaPipeline` bindings) and `vault.ts`'s own * i18n/dict registries (put-time validation, dict-join/search resolution) * both call this so a field declared via `viaFields` behaves identically to * one declared under its feature's sugar key. * * Each `viaFields` entry's descriptors are grouped by `_viaBrand`; an * `'i18n'`-branded descriptor further splits by shape (i18nText vs * dictKey/staticDict) via the existing descriptor-shape predicates. A field * name declared in BOTH a sugar key and `viaFields` throws `ValidationError` * — one declaration site per field (#623 Task 9). */ export declare function mergeViaFields(sources: ViaFieldSources): MergedViaFields; /** * #631 — declare-time cross-binding same-field collision guard. Today the same field named * in two DIFFERENT via-binding families (e.g. the same field in both `moneyFields` and * `blobFields`) resolves silently by compile-order first-wins in `ViaPipeline`'s per-field * posture/clause lookup — undefined pipeline behavior for a config that is almost certainly a * mistake. `mergeViaFields` already guards sugar-vs-`viaFields` and `dictKeyFields`-vs- * `lookupFields` collisions (#623 Task 9); this runs one step later, in `compileViaBindings` * (fresh construction) or {@link guardReconcileCollisions} (late-attach reconcile), because * those are the only seams that also see `classifiedFields`/`blobFields` — neither is part of * `ViaFieldSources`, so `mergeViaFields` never sees them. * * WHY FAMILY-BASED, NOT PROVENANCE-BASED (#631 review, round 2 — Controller ruling): a field * declared via `via(computed(fn), money(...))` and a field declared via two independent sugar * maps (`computed: { total: fn }` + `moneyFields: { total: money(...) }`) are NOT * distinguishable here — `mergeViaFields` folds both declaration STYLES into the IDENTICAL * merged per-family field maps this function receives; provenance (which style produced a given * map entry) is erased before this guard ever runs. Narrowing the exemption to "only * via()-composed fields" would therefore require threading a second, parallel * provenance-tracking data structure through `mergeViaFields` for the sole purpose of refusing a * config that is BEHAVIORALLY IDENTICAL to the sanctioned one — refusing it would be a * behavior-lock violation (breaking a legal, meaningful config), not a bug fix. The exemption is * therefore keyed on the FAMILY SET alone, and instead earned empirically per family (below). * * EXEMPT (test-earned, not merely asserted): `computed` colliding with EXACTLY ONE of * `money`/`i18n`/`lookup` on the same field. `via()`'s descriptor loop only accepts * money/i18n/computed/lookup `_viaBrand`s (`mergeViaFields` throws on any other brand), so a * computed+classified or computed+blob same-field pairing can never arise from composition — it * can only come from mistakenly naming the same field in `computed`/`viaFields` AND * `classifiedFields`/`blobFields`, which this guard still refuses. For the three families that * CAN legitimately arise, each is pinned by an end-to-end runtime test proving the composition * does something real (not just "doesn't throw") — see `computed/virtual.test.ts`: * - money: the "composed grammar" tests (materialized-mode money formatting the computed * output; the ORIGINAL evidence this exemption started from — see the money DOES/does NOT * format the computed output pair). * - i18n (dictKey) / lookup (dict()): the "composed grammar — computed + i18n/lookup * families (#631 collision-guard exemption pins)" block, BOTH declaration styles * (`via(computed(...), dictKey(...)/dict(...))` and the two-independent-sugar-maps form), * materialized mode — proven to dress `Label` off the computed output identically * either way (the provenance-erasure claim above, empirically confirmed). Virtual mode for * both families hits the SAME present-ORDER limitation money's own virtual composed-grammar * test already documents (i18n/lookup's `present()` runs BEFORE computed's — a virtual field * is never stored, so there is nothing to dress yet); pinned as a known limitation, not * grounds to drop the family from the exemption (materialized mode is what proves the family * genuinely composes). * * 3-CLAIMANT TIGHTENING (#631 review, round 2): the exemption requires EXACTLY TWO claimant * source-keys, not just two families. A field named in `computed` + BOTH `i18nFields` AND * `dictKeyFields` collapses to the same two families (`{computed, i18n}` — `dictKeyFields` * shares the `i18n` family) but is a genuine THREE-claimant collision: two independent i18n * sugar keys both claiming the field is itself a mistake this guard exists to catch, and must * not slip through because the family SET happens to match the earned exemption's shape. */ export declare function guardCrossBindingFieldCollisions(fieldMaps: Readonly | undefined>>, callerPrefix?: string): void; /** * #664 Part 1 — the late-attach reconcile-path sibling of {@link guardCrossBindingFieldCollisions}. * `vault.ts`'s reconcile ladder (a second-or-later `vault.collection(name, {...})` call on an * ALREADY-open collection) never ran the fresh-construction guard above, so two probe recipes * slipped through (round-2 fable review, 2026-07-13): * * - **(a) incoming×incoming** — the SAME reconcile call names a field in two different * families (e.g. `moneyFields`+`blobFields` both claiming `"amount"`). Fix: re-run * {@link guardCrossBindingFieldCollisions} verbatim over THIS call's own merged field maps — * identical semantics to fresh construction, just invoked one call later. * - **(b) existing×incoming** — an EARLIER call already compiled a binding covering a field * (money reconciled onto it, or it shipped with the collection's first declaration), and * THIS call's incoming family map claims the SAME field for a DIFFERENT family (e.g. call-1 * `classifiedFields:['ssn']`, call-2 `moneyFields:{ssn}`) — undetectable by (a) alone since * the collision spans two calls. Read via the LIVE collection's compiled bindings * (`coll._via.bindings`): each `ViaBinding.covers(field)` (`via/index.ts`) plus its `brand` tells us * which family already owns the field, mapped through the SAME {@link VIA_FIELD_MAP_FAMILY} * the incoming side uses — no new collection.ts surface needed. * * Both sub-checks honor the SAME #631 exemption (`{computed,money}`/`{computed,i18n}`/ * `{computed,lookup}`, exactly-two-claimants) as the fresh guard — a composition that would be * legal in one `vault.collection()` call (`via(computed(fn), money(...))`) must stay legal when * split across two calls (computed declared fresh, money late-attached), since the two paths are * behaviorally equivalent by the same provenance-erasure argument {@link guardCrossBindingFieldCollisions} * documents. `existingFamilies` is collected as a SET (every binding covering the field, not just * the first match) so an already-legally-composed pair (e.g. existing `{computed, money}`) is * checked against the incoming family individually — a THIRD family colliding with either half * is still refused. */ export declare function guardReconcileCollisions(existingVia: ViaPipeline | undefined, incomingFieldMaps: Readonly | undefined>>): void;