/** * Pure dict-registry helpers — extracted from `kernel/vault.ts`'s dict * bodies (#650 Task 1 — via-lookup extraction, phase D). * * These take the vault-resident registry Maps (and a couple of vault-bound * callbacks) as ARGUMENTS instead of closing over `this` — no `Vault` * import here, so this module stays a plain, testable function library. * `kernel/vault.ts` keeps the registry Maps themselves (they're populated * by `vault.collection()` and read by the backup path) and calls these * helpers through the `port/with/lookup-strategy.ts` seam. */ import type { JoinableSource } from '../../kernel/query/index.js'; import type { ViaGraph } from '../../kernel/via/graph.js'; import type { StaticDictDescriptor } from '../../port/with/i18n-strategy.js'; import { type LookupHandle } from './handle.js'; import type { LookupDescriptor } from './descriptor.js'; /** * Validate staticDict codes on a `put()`. For each `staticDict()` field, * every stored code must be a declared key of the descriptor's table, else * `UnknownDictCodeError`. Opt out per descriptor with `{ validateCodes: * false }`. Supports scalar, dotted, and `[].`-wildcard field paths via * `getAtPath` (same path support as i18n validation). * * `staticFields` is the collection's `field → StaticDictDescriptor` map * (`Vault#staticDescriptorByField.get(collectionName)`) — `undefined`/empty * is a no-op. */ export declare function enforceStaticDictOnPut(staticFields: Record | undefined, record: unknown): void; /** * Build a `JoinableSource` for a dictKey field, for use in dict joins. * Returns a source whose snapshot contains `{ key, labels, ...labels }` * records — one per dictionary entry — keyed by the stable key. * * staticDict: a code-table-backed source — snapshot() materialises the * in-memory table into rows, mirroring `LookupHandle.snapshotEntries()`. * Carries `displayLocale` so a locale-less `{ by: 'label' }` query has a * default locale to resolve at. * * Plain dictKey: the snapshot is built synchronously from the * `LookupHandle`'s write-through cache, which is populated on every * `put()`, `rename()`, `delete()`, and `list()` call. For pre-existing * data not yet touched this session, call `await vault.dictionary(name).list()` * first to warm the cache. * * Returns `null` when `field` is not a dictKey in `leftCollection`. */ export declare function resolveDictSource(leftCollection: string, field: string, staticDescriptorByField: ReadonlyMap>, dictKeyFieldRegistry: ReadonlyMap>, getDictionaryHandle: (name: string) => LookupHandle): JoinableSource | null; /** The minimal collection surface `updateReferencingRecords` needs. */ export interface DictReferencingCollection { list(): Promise[]>; put(id: string, record: Record): Promise; } /** * Find and rewrite records in every registered collection whose * dictKeyField points at `name`, replacing `oldKey` with `newKey`. Used by * `LookupHandle.rename()` (the only sanctioned mass-mutation path for * dictKey fields) via the vault's `findAndUpdateReferences` callback. * * `registry` is `Vault#dictKeyFieldRegistry` (collection name → field name * → dictionary name); `getCollection` is the vault's collection accessor — * rewrites go through the public `coll.put()` choke point, same as before. */ export declare function updateReferencingRecords(registry: ReadonlyMap>, getCollection: (collectionName: string) => DictReferencingCollection, name: string, oldKey: string, newKey: string): Promise; /** * Resolve a label from an in-memory `{ locale -> label }` map, walking the * same fallback chain semantics as `LookupHandle.resolveLabel` (#650 Task 2 * — moved here from `kernel/vault.ts` so the SAME chain serves both the * i18n binding's `dictLabelResolver` and the lookup binding's * `lookupLabelResolver`, which #650 Task 2 wires to the identical closure). */ export declare function resolveLabelFromMap(labels: Readonly>, locale: string, fallback?: string | readonly string[]): string | undefined; /** * Project a native `lookup(dimension, { backing:'static', table, … })` * descriptor into the legacy `StaticDictDescriptor` shape — the * alias-equivalence compat seam (#650 Task 2) that lets a native * static-tier lookup field reuse the SAME vault registries * (`staticByName`/`staticDescriptorByField`) — and therefore the same * `dictLabelResolver`/`resolveDictSource` machinery — as its `staticDict()` * alias. `vocabulary:'closed'` maps to `validateCodes:true` (closed = only * declared codes are legal); `'open'` maps to `validateCodes:false`. * Returns `undefined` for non-static or table-less (bare `enumOf`) * descriptors — those have nothing to register. */ export declare function lookupToStaticDictCompat(desc: LookupDescriptor): StaticDictDescriptor | undefined; /** The vault-registry entries a collection's `lookupFields` contribute — the alias-equivalence bridge (#650 Task 2). */ export interface LookupDictCompat { /** Reserved-tier fields: field name -> dimension (dictionary) name — merges into `dictKeyFieldRegistry`. */ readonly dictFieldMap: Record; /** Static-tier (table-bearing) fields, projected — merges into `staticDescriptorByField`/`staticByName`. */ readonly staticEntries: ReadonlyArray; } /** * Bridge a collection's `lookupFields` into the SAME shape the legacy dict * registries expect, so `resolveDictSource`/`dictLabelResolver` (and * therefore `.join()`/`orderBy({by:'label'})`) serve a native `dict()`/ * `lookup(static)` field identically to its `dictKey()`/`staticDict()` * alias — the reserved-vs-first-class-backing "matrix" tier is NOT bridged * here (no vault registry backs it; Task 5/6 build its own graph edge / * snapshot seam). */ export declare function collectLookupDictCompat(lookupFields: Record | undefined): LookupDictCompat; /** * Reserved-dict deps of a set of named collections (#653) — the `_dict_*` * collections partial sync must auto-include alongside a `collections` * filter, mirroring `SyncEngine`'s existing satellite-pair expansion. * One hop is provably complete: `vault.collection()` refuses `_dict_*` * names, so a dict can never itself declare lookup fields — no * dict-of-dict chain to recurse through. */ export declare function reservedDictDepsOf(names: readonly string[], dictKeyFieldRegistry: ReadonlyMap>): readonly string[]; /** * A lookup dimension's sync membership/altKey table, materialized from its * backing rows (#650 Task 3). `keys` is every canonical key present; * `altIndex` maps an altKey candidate VALUE to its owning canonical key. */ export interface MaterializedBacking { /** Canonical key values present in the materialized rows. */ readonly keys: ReadonlySet; /** altKey candidate value -> canonical key. */ readonly altIndex: ReadonlyMap; } /** * Build a lookup dimension's altKey index from its backing rows, enforcing * declare/warm-time uniqueness across `key ∪ altKeys` values (#650 Task 3 — * the CHE/SWZ drift class: two different rows must never claim the same * candidate key). `rows` is keyed by canonical key (`row[descriptor.key]` * for the matrix tier; the dimension's own key for static/reserved). * An altKey candidate VALUE may be a string or number — both normalize via * `coerceLookupKey` (#651 Task 3); non-scalar/absent values are skipped. * Pure — no I/O. Throws `ValidationError` on collision. */ export declare function materializeBackingTable(descriptor: LookupDescriptor, rows: ReadonlyMap>): MaterializedBacking; /** * Closed-vocabulary membership test for one candidate key (#650 Task 3). * Static tier: sync, against the in-config key set (declared `keys`, or the * table's own keys when table-bearing). Reserved tier: sync — the declared * `keys` union the reserved handle's live write-through snapshot (closes * #649 for the native `dict()` spelling: the declared-keys promise the old * `dictKey()` doc comment made falsely). Matrix (collection) tier: sync — * delegates to `buildLookupAltIndex` so membership is checked against the * SAME `row[descriptor.key]` keying the altKey index uses (review fix, * Important 1: an earlier PUT-id `.get(key)` scan disagreed with the * altIndex whenever `descriptor.key !== 'id'`, wrongly rejecting valid * non-id candidates and wrongly accepting an unrelated row's PUT-id). */ export declare function checkLookupMembership(descriptor: LookupDescriptor, key: string, getDictionary: (dimension: string) => LookupHandle, getCollection: (dimension: string) => { querySourceForJoin(): JoinableSource; }): boolean; /** * The ONE guarded key coercion (#651 Task 3, dm12) — string/number values * coerce to their canonical `String()` form; everything else (`null`, * `undefined`, objects, …) coerces to `undefined`. Every consumer that turns * a raw record field into a lookup key routes through this, closing the * bare-`String()` `"undefined"`/`"null"`-key poisoning class (seam map * finding 6): a row whose key field is genuinely absent must never mint the * literal candidate string `"undefined"`. */ export declare function coerceLookupKey(raw: unknown): string | undefined; /** * Resolve a backing row's canonical key VALUE — `coerceLookupKey(row[descriptor.key])` * (#651 Task 3). The matrix tier's `row[descriptor.key]`, never the row's own PUT-id * when the two differ (`descriptor.key !== 'id'`). */ export declare function resolveBackingRowKey(descriptor: LookupDescriptor, row: Record): string | undefined; /** * The referencing-side match predicate — does `rec[field]` (coerced) equal an * already-coerced `compareKey` (#651 Task 3)? Shared by every site that scans a * referencing collection for rows pointing at a given backing key * (`with-shape/links/vault-facade.ts`'s ref-delete propagation, `kernel/via/dispatch.ts`'s * forget-fanout twin). */ export declare function matchesReferencingValue(rec: Record, field: string, compareKey: string): boolean; /** * Materialize a lookup dimension's altKey index from whatever backing data * is synchronously available (#650 Task 3 — the `ingest` source). Static: * the in-config table. Reserved: the reserved handle's live write-through * cache (the same warm-via-put()/list() cache `resolveDictSource` already * relies on). Matrix (collection): the backing collection's own in-memory * eager cache via `querySourceForJoin()` — already public, already the * mechanism `.join()` uses (`with-shape/links/vault-facade.ts`'s * `resolveSource`); no new I/O, no fire-and-forget warm step. Like that * existing join precedent, a dimension collection this vault session has * not yet opened/populated sees an empty snapshot (no altKey normalization * until it has rows) — open/populate it first for normalization to apply. * A matrix row whose `descriptor.key` field coerces to `undefined` (missing/ * non-scalar) is SKIPPED — never enters the index under a poisoned * `"undefined"` key (#651 Task 3, dm12). */ export declare function buildLookupAltIndex(descriptor: LookupDescriptor, getDictionary: (dimension: string) => LookupHandle, getCollection: (dimension: string) => { querySourceForJoin(): JoinableSource; }): MaterializedBacking; /** Thin wrapper — `collectLookupRefEdges` + one `graph.registerDerived` call per edge. Lets * `vault.collection()` (kernel-surface-budgeted, #650 Task 5) register a collection's lookup-ref * edges with a single line, keeping the ceiling-guarded call site a thin call (route logic here). */ export declare function registerLookupRefEdges(graph: ViaGraph, collectionName: string, lookupFields: Record | undefined): void; /** * `LookupViaConfig.snapshotFor`'s vault-built row source (#650 Task 6, spec * §5; matrix-tier coverage added #650 Task 7, spec §6 — Task 6 deferred it, * see `task-6-report.md`'s Concerns #1; the reviewer's Task-7 dispatch * refuted the "needs a new vault-resident registry" premise: the descriptor * is already in hand at both call sites, so `snapshotFor` just needs to * ACCEPT it). Takes the full `descriptor` (not a bare dimension name) so it * can route the matrix tier's `key` field, which — unlike reserved tier's * hardcoded `'id'` — varies per collection. Routes on `descriptor.backing`: * * - **reserved**: rows come straight from the SAME `LookupHandle. * snapshotEntries()` write-through cache `dictLabelResolver`/ * `resolveDictSource` already read (no second copy), keyed by each * entry's own canonical `key` (always `'id'` by construction for this * tier — `dict()`'s factory hardcodes it). * - **collection** (matrix): rows come from `getCollection(dimension). * querySourceForJoin().snapshot()` — the SAME sync, already-live cache * `buildLookupAltIndex`'s matrix branch (above, this file) and `.join()` * itself already read — re-keyed via `resolveBackingRowKey(descriptor, row)`, * NOT the row's own PUT-id (which may differ when `key !== 'id'`; the exact * distinction the #650 Task 3 review fix already applies to * `checkLookupMembership`'s matrix branch). A row whose `descriptor.key` * field coerces to `undefined` is skipped (#651 Task 3, dm12). * - **static**: never routed here — `descriptor.table` is read directly by * the caller (`binding.ts`'s `compareLookupOrder`/`resolveLookupOrderLabel`, * `snapshot.ts`'s `presentLookupForJoin`); no vault call needed. * * `isReservedDimension` is the vault's `reservedLookupCollections` * membership test. */ export declare function buildLookupSnapshotRows(descriptor: LookupDescriptor, isReservedDimension: (dimension: string) => boolean, getDictionary: (dimension: string) => LookupHandle, getCollection: (dimension: string) => { querySourceForJoin(): JoinableSource; }): ReadonlyMap> | undefined;