/** * Collection construction config — the PURE half of the `Collection` * constructor. * * `resolveCollectionConfig` takes the raw `CollectionOpts` the Vault threads in * and resolves every `?? default`, every normalized/derived field, and runs the * pure construction-time validations (embeddings-on-CRDT, money-field paths, * deterministic-risk acknowledgement). It holds **no `this`** and performs no * external side effect — the only objects it allocates (`Set`s, `VectorSet`, * the CEK `Lru`) are the same instances the collection then owns by reference. * * The `this`-dependent construction (the search-index store whose persisted * callback closes over `this.searchContext()`, the `RecordCodec`, and the * SyncEngine conflict-resolver registration) stays in the constructor — those * genuinely capture the live instance (and the conflict-resolver closures close * over `conflictPolicy: ConflictPolicy`, whose invariant-in-T custom-merge * signature can't be exposed via a method without breaking `Collection` * assignability to `Collection`). */ import type { NoydbStore, ConflictPolicy, CollectionConflictResolver, HistoryConfig, TierMode, CrossTierAccessEvent, VdigFieldPolicy } from './types.js'; import type { EnclaveKey } from './enclave/index.js'; import type { UnlockedKeyring } from '../with-party/team/keyring.js'; import type { NoydbEventEmitter } from './events.js'; import type { WriteQueueTracker } from './write-queue.js'; import type { WriteHookRegistry } from '../port/with/write-hooks.js'; import type { ServiceBus } from '../port/with/service-bus.js'; import type { SchemaUpdateGate } from '../with-shape/schema-update/gate.js'; import type { SchemaFenceController } from '../with-shape/schema-update/fence-controller.js'; import type { StandardSchemaV1 } from './schema.js'; import type { LedgerStore } from '../with-commit/history/ledger/index.js'; import type { CrdtMode } from '../with-commit/crdt/crdt.js'; import type { CrdtStrategy } from './types.js'; import { type HistoryStrategy } from '../with-commit/history/strategy.js'; import { type I18nStrategy } from '../port/with/i18n-strategy.js'; import { type SyncStrategy } from '../with-party/team/sync-strategy.js'; import { type BlobStrategy } from '../port/with/blob-strategy.js'; import { type AggregateStrategy } from '../with-lookup/aggregate/strategy.js'; import { type TiersStrategy } from '../with-audit/tiers/strategy.js'; import { type SearchStrategy } from '../with-lookup/search/strategy.js'; import type { ObjectProjection } from '../with-shape/blobs/object-projection.js'; import type { BlobFieldsConfig } from '../with-shape/blobs/blob-compaction.js'; import type { IndexStrategy } from '../with-lookup/indexing/strategy.js'; import type { IndexDef } from '../with-lookup/indexing/eager-indexes.js'; import type { I18nTextDescriptor, DictKeyDescriptor, StaticDictDescriptor, DictionaryHandle } from '../port/with/i18n-strategy.js'; import type { LookupDescriptor, MaterializedBacking } from '../port/with/lookup-strategy.js'; import type { ComputedFields, ComputedFn, ComputedFieldEntry } from '../with-formula/computed/index.js'; import type { RollupDeleteIntent } from './via/dispatch.js'; import '../port/with/computed-strategy.js'; import { type ClassifiedEntry, type ResolvedClassified, type ClassifiedGuardCtx, type ClassifiedStrategy, type ClassifiedViaConfig } from '../port/with/classified-strategy.js'; import type { FieldRef, Grain } from './via/graph.js'; import type { FieldMeta } from '../with-shape/introspection/field-meta.js'; import type { CollectionMeta } from '../with-shape/introspection/meta.js'; import type { RefDescriptor } from './refs.js'; import type { JoinableSource } from './query/index.js'; import { VectorSet, type EmbeddingDescriptor } from '../with-lookup/embeddings/index.js'; import { Lru } from './cache/index.js'; import type { ReadOnlyVaultFacade } from '../with-audit/guards/types.js'; import type { DerivationRegistry } from '../with-formula/derivations/registry.js'; import type { TxContext } from '../with-commit/tx/transaction.js'; import type { MaterializedViewRegistry } from '../with-formula/materialized-views/registry.js'; import type { MVQueryContext } from '../with-formula/materialized-views/types.js'; import type { Collection, OnDirtyCallback, CacheOptions } from './collection.js'; import type { LazyStrategy } from '../port/with/lazy-strategy.js'; import { ViaPipeline } from './via/pipeline.js'; import { type ViaBinding, type ViaDescriptor } from './via/index.js'; import { type ViaFieldSpec } from './via/compose.js'; /** * Raw options handed to the {@link Collection} constructor by the Vault. * One named type shared by the constructor and the pure resolver. */ export interface CollectionOpts { adapter: NoydbStore; vault: string; name: string; keyring: UnlockedKeyring; encrypted: boolean; /** * Opt-in: keep the working set encrypted in RAM, decrypting on read (future phase). * Default false — the working set is plaintext. */ ramCiphertext?: boolean; emitter: NoydbEventEmitter; /** * Vault-level in-flight write tracker. When present, * `put`/`delete` run inside `writeQueue.track()` so `hub.writeQueue` * reflects outstanding writes. Optional so direct Collection * construction in tests still works untracked. */ writeQueue?: WriteQueueTracker | undefined; /** Per-collection schema-update gate; `put`/`delete` await it. */ schemaUpdateGate?: SchemaUpdateGate | undefined; /** Vault-level fence controller; `put`/`delete` consult it. */ schemaFence?: SchemaFenceController | undefined; /** Hub-level write-hook registry; fired around put/delete. */ writeHooks?: WriteHookRegistry | undefined; /** The observe bus, threaded from Noydb. */ subsystemBus?: ServiceBus | undefined; /** Active transaction id supplier (null outside a transaction). */ activeTxId?: (() => string | null) | undefined; getDEK: (collectionName: string) => Promise; historyConfig?: HistoryConfig | undefined; /** * When `true`, the caller explicitly provided `historyConfig` rather than * inheriting the vault-wide default. Used by `getConfig()` to decide * whether to surface `history: true` in the schema dump. */ historyConfigExplicit?: boolean | undefined; onDirty?: OnDirtyCallback | undefined; /** * #693: live check for whether multi-tab coordination is active at all * (`Noydb._tabCoordinationActive`). When true, the shared local store may be * written out-of-band by another tab, so the #606 marker-id-set gate falls * back to an unconditional store read on re-create. */ tabCoordinated?: (() => boolean) | undefined; /** * tree-shake seam. When omitted, `collection.blob(id)` throws * with a pointer at the `@noy-db/hub/blobs` subpath. When set (via * `createNoydb({ blobStrategy: blobs() })`), blob storage is live. * `@internal` by virtue of `BlobStrategy` being `@internal`. */ blobStrategy?: BlobStrategy | undefined; objectStore?: ObjectProjection | undefined; blobFields?: BlobFieldsConfig | undefined; /** * #724 Arc 10 Task 3: how `elevate()`/`demote()`/`putAtTier()` rehome a * SHARED blob (`refCount > 1`) it can't rewrap in place. `'isolate'` * (default) forks a private tier-scoped copy for the moving record and * releases its hold on the shared object — co-owners stay byte-for-byte * untouched. `'dedup'` (#741, opt-in) leaves the shared object in place; * the Task-1 runtime read gate still hides it, but the chunks stay * decryptable at rest under the flat `_blob` DEK (documented residue). */ blobTierPolicy?: 'isolate' | 'dedup' | undefined; aggregateStrategy?: AggregateStrategy | undefined; crdtStrategy?: CrdtStrategy | undefined; /** * tree-shake seam — strategy for optional history/ledger/ * time-machine. When omitted, history snapshots and ledger appends * become silent no-ops (data still writes); the read APIs * (`history`, `getVersion`, `revert`, `diff`, `clearHistory`, * `pruneRecordHistory`) throw with a pointer at `@noy-db/hub/history`. */ historyStrategy?: HistoryStrategy | undefined; i18nStrategy?: I18nStrategy | undefined; syncStrategy?: SyncStrategy | undefined; /** * tree-shake seam. When omitted, indexing is off for this * collection — every `.lazyQuery()` call throws, `.rebuildIndexes()` * is a no-op, and `indexes: [...]` declarations are ignored. Enable * by passing `withIndexing()` from `@noy-db/hub/indexing` at * `createNoydb` time. */ indexStrategy?: IndexStrategy | undefined; indexes?: IndexDef[] | undefined; /** * Auto-reconcile behavior for persisted-index drift on lazy-mode * collections. Defaults to `'off'` — operators call * `collection.reconcileIndex(field)` explicitly. * * - `'off'` (default): no implicit work. Same semantics as. * - `'dry-run'`: on first lazy-mode query, run * `reconcileIndex(field, { dryRun: true })` per declared field * and emit `index:reconciled` with the diff. Nothing is written. * - `'auto'`: same walk as `'dry-run'` but with `dryRun: false`. * Drift is repaired in-place and the fix count surfaces on the * event. * * Unattended long-lived processes (Workers, Node services with no * human operator) should set `'auto'`. Attended desktop apps should * leave it `'off'` and surface a manual "rebuild indexes" button. */ reconcileOnOpen?: 'off' | 'dry-run' | 'auto'; /** * Hydration mode. `'eager'` (default) loads everything into memory on * first access — matches behavior exactly. `'lazy'` defers loads * to per-id `get()` calls and bounds memory via the `cache` option. */ prefetch?: boolean; /** * LRU cache options. Only meaningful when `prefetch: false`. At least * one of `maxRecords` or `maxBytes` must be set in lazy mode — an * unbounded lazy cache defeats the purpose. */ cache?: CacheOptions | undefined; /** * Lazy service seam (#267) — supplies the bounded-LRU working-set cache * when `prefetch: false`. Omitted → the deprecated IMPLICIT_LAZY * back-compat default (identical behavior + one-time warn). */ lazyStrategy?: LazyStrategy | undefined; /** * Optional Standard Schema v1 validator (Zod, Valibot, ArkType, * Effect Schema, etc.). When set, every `put()` is validated before * encryption and every read is validated after decryption. See the * `schema` field docstring for the error semantics. */ schema?: StandardSchemaV1 | undefined; /** Declares this collection a satellite of `satelliteOf` (spec #591). */ satelliteOf?: string | undefined; /** Satellite routing table — the fields owned by this satellite (required with satelliteOf). */ fields?: readonly string[] | undefined; /** Registers the full-record joined handle under this name (optional; see vault.joined()). */ joined?: string | undefined; /** * Optional reference to the compartment's hash-chained ledger. * When present, successful mutations append a ledger entry via * `LedgerStore.append()`. Constructed at the Vault level and * threaded through — see the Vault.collection() source for * the wiring. */ ledger?: LedgerStore | undefined; /** * Optional back-reference to the owning compartment's ref * enforcer`). * Collection.put calls `enforceRefsOnPut` before the adapter * write; Collection.delete calls `enforceRefsOnDelete` before * its own adapter delete. See the `refEnforcer` field docstring * for the full protocol. */ refEnforcer?: { enforceRefsOnPut(collectionName: string, record: unknown): Promise; enforceRefsOnDelete(collectionName: string, id: string): Promise; } | undefined; /** * Optional back-reference to the owning compartment's join * resolver. When present, `query()` builds a * `JoinContext` so `.join(field)` can resolve through the * existing `ref()` declaration into the target collection. * Absent in tests that construct a Collection directly without * a vault; production usage always has one because * Vault.collection() passes `this` through. */ joinResolver?: { resolveSource(collectionName: string): JoinableSource | null; resolveRef(leftCollection: string, field: string): RefDescriptor | null; resolveDictSource?: (leftCollection: string, field: string) => JoinableSource | null; } | undefined; /** — i18nText field descriptors for locale-aware reads. */ i18nFields?: Record | undefined; /** — embedding config for write-time vector derivation + semantic retrieval. */ embeddings?: EmbeddingDescriptor | undefined; /** — string fields exposed to client-side `retrieve()`. */ textIndexes?: readonly string[] | undefined; /** — pre-build the lexical index on open (eager-only). */ warmIndexOnOpen?: boolean | undefined; /** — persist the lexical index as an opaque encrypted blob at `_ftindex/`. */ textIndexPersist?: boolean | undefined; /** — dictKey field descriptors for label resolution on reads. */ dictKeyFields?: Record | undefined; /** — consumer-neutral per-field descriptors. Read via getFieldMeta(). */ fieldMeta?: Record | undefined; /** — collection-level descriptive metadata. Read via getMeta(). */ meta?: CollectionMeta | undefined; moneyFields?: Record | undefined; /** — declare via() composed fields; grouped by `_viaBrand` and merged with the money/i18n sugar keys above (a field in both throws). */ viaFields?: Record | undefined; /** — outbound ref declarations (snapshot from vault refRegistry). Used by describe(). */ declaredRefs?: Record | undefined; computed?: ComputedFields | undefined; /** — declare classified() sensitive-field descriptors (sealed + riders + projections). */ classifiedFields?: Record | undefined; /** * The forget-cascade subject key for this collection (from * `withForgetCascade({ subjects })`), plumbed by the Vault. Consumed by the * classified refusal matrix (R4: a digest-only field cannot be the subject key). */ subjectKeyField?: string | undefined; /** * Register a record's forget-subject ref directly (bypassing the write-hook * pipeline), wired by the Vault when this collection declares a forget- * subject field. Consumed by `putAtTier` (#766, via `TiersContext.addSubjectRef`) * so a record whose first write is a tier write still enters `_subject_index`. * `undefined` when no forget strategy declares this collection (no-op). */ addSubjectRef?: ((id: string, record: unknown) => Promise) | undefined; /** — tree-shake seam for `collection.reveal()`. Defaults to `NO_CLASSIFIED`. */ classifiedStrategy?: ClassifiedStrategy | undefined; /** * async callback that resolves a dict key to its label * for a given locale. Provided by the Vault. */ dictLabelResolver?: ((dictName: string, key: string, locale: string, fallback?: string | readonly string[]) => Promise) | undefined; /** * Async callback to open a dynamic dictionary handle. * Provided by the Vault for dynamic-dict label-map resolution in * the search index. Static dicts bypass this. */ getDictionary?: ((name: string) => Promise) | undefined; /** * synchronous callback that validates i18nText fields * on put. Provided by the Vault. Throws MissingTranslationError. */ i18nPutValidator?: ((record: unknown) => void) | undefined; /** — declare lookup()/enumOf()/dict() fields (#650 Task 2 sugar key, mirrors dictKeyFields), merged with `via(lookup(...))` entries. */ lookupFields?: Record | undefined; /** * async label resolver for the `'lookup'` binding's `'static'`/`'reserved'` * tiers. Provided by the Vault — the SAME closure as `dictLabelResolver` * (static table first, else the `vault.dictionary()` handle), so a * native `dict()`/`lookup(static)` field resolves through identical * label data as its `dictKey()`/`staticDict()` alias. */ lookupLabelResolver?: ((dimension: string, key: string, locale: string, fallback?: string | readonly string[]) => Promise) | undefined; /** — the matrix (`backing:'collection'`) tier's present-time row accessor, keyed by the full * descriptor so it can resolve by `descriptor.key`, not the backing row's PUT-id (#651 Task 3). * Provided by the Vault. */ getLookupBacking?: ((descriptor: LookupDescriptor) => (key: string) => Promise | undefined>) | undefined; /** — closed-vocabulary write-time membership test (#650 Task 3). Provided by the Vault. */ membership?: ((field: string, key: string) => boolean | Promise) | undefined; /** — altKeys `ingest` normalization source (#650 Task 3). Provided by the Vault. */ getAltIndex?: ((desc: LookupDescriptor) => MaterializedBacking | undefined) | undefined; /** * — sync materialized `key -> row` rows for a lookup descriptor (#650 * Task 6, spec §5's snapshot+locale seam; matrix-tier routing added Task * 7). Provided by the Vault; feeds the `'lookup'` binding's * `compareForOrder`/`resolveOrderLabel` and this collection's * `presentForJoin` hook (`resolveCollectionConfig` builds the latter via * `buildPresentForJoin`, below). */ snapshotFor?: ((descriptor: LookupDescriptor) => ReadonlyMap> | undefined) | undefined; /** * translator callback from Noydb. When present, missing * translations for `autoTranslate: true` i18nText fields are generated * before the i18n validator runs. */ autoTranslateHook?: ((text: string, from: string, to: string, field: string, collection: string) => Promise) | undefined; /** * vault-default locale, inherited from * `openVault(name, { locale })` or `vault.setLocale()`. */ defaultLocale?: string | undefined; /** * collection-level conflict resolution policy. * Overrides the db-level `conflict` option for this collection only. */ conflictPolicy?: ConflictPolicy | undefined; /** * callback to register an envelope-level resolver with the * SyncEngine. Provided by the Vault (wired from the SyncEngine). */ onRegisterConflictResolver?: ((name: string, resolver: CollectionConflictResolver) => void) | undefined; /** * CRDT mode for this collection. When set, `put()` stores * CRDT state in the envelope and `get()` returns the resolved snapshot. * `getRaw(id)` returns the full CRDT state for merge operations. */ crdt?: CrdtMode | undefined; /** * optional remote/sync adapter. When present, `presence()` * writes heartbeats to this adapter so other devices can read them. * If the adapter implements pub/sub, presence updates are real-time. */ syncAdapter?: NoydbStore | undefined; /** * called by the collection after every successful * `get` / `put` / `delete`. The Vault installs a callback that * appends a consent-audit entry when `withConsent` is active; * outside a consent scope the callback is a no-op. Awaited so a * thrown audit write surfaces to the caller. */ onAccess?: (op: 'get' | 'put' | 'delete' | 'reveal' | 'verify' | 'find', id: string) => Promise; /** * invoked by `put`/`delete` before any adapter * write. Receives the prior envelope timestamp + decrypted * record (or `null` if no prior) and the incoming record (or * `null` for delete). Throws `PeriodClosedError` to abort. */ /** * opt-in deterministic-encryption index. * * Field names listed here get a deterministic AES-GCM ciphertext * attached to every envelope's `_det` map, which enables blind * equality search via `collection.findByDet(field, value)`. * * **Leaks equality.** Two records with the same value in a * deterministic field produce identical ciphertexts, so anyone * with store access can tell which records share a value without * learning the value itself. This is the textbook trade-off of * deterministic encryption — strictly opt-in for that reason. * * Declaring any field here without also passing * `acknowledgeDeterministicRisk: true` throws at construction, * so the risk must be explicitly acknowledged. */ deterministicFields?: readonly string[] | undefined; /** * gate for `deterministicFields`. Must be `true` when * any deterministic field is declared. Any other value throws. */ acknowledgeDeterministicRisk?: boolean | undefined; /** * gate for the classified `equatable` knob (R8 double door). Must be `true` * when any classified field declares `equatable: true`; otherwise construction * throws. One-directional — setting it with zero equatable members is a * silent no-op (mirrors `acknowledgeDeterministicRisk`). */ acknowledgeEquatableRisk?: boolean | undefined; /** * Structural group-encryption. Fields listed here are * encrypted into their own `_sealed[field]` envelope slot — each under * an HKDF-derived per-field key — instead of sitting inside the open * `_data` blob. Default-off: with no `sensitive` fields the envelope is * byte-identical to today. Read merges them back inline (the * `Sealed`/`reveal()` access restriction is a separate follow-up). * * **Incompatible with `perRecordKeys`/forget-cascade:** sealed * field keys derive off the *collection* DEK, not the per-record CEK, so * crypto-shredding a record does not erase its sealed fields. */ sensitive?: readonly string[] | undefined; /** * Per-record content-encryption keys. When `true`, every record body * (and every history version of it) is encrypted under a fresh * per-record CEK, AES-KW-wrapped under the collection DEK and stored * on the envelope's `_cek`. Off by default. Foundation for per-record * erasure and record-scoped sealing. `_det` slots stay * keyed to the collection DEK regardless. */ perRecordKeys?: boolean | undefined; /** * Per-record provenance tracking. When `true`, `put()` calls that * supply a `source` option stamp `_source` (opaque source id) and * `_sourceTs` (ISO-8601 timestamp) onto the unencrypted envelope * metadata. Off by default — zero cost for collections that don't * need lineage tracking. */ provenance?: boolean | undefined; /** * declared tiers this collection supports. An * undefined or empty list disables the hierarchical-tier surface * on this collection (`putAtTier`, `getAtTier`, `elevate`, `demote` * throw). Tier 0 is implicit and always available. */ tiers?: readonly number[] | undefined; /** * tree-shake seam — strategy for the collection-level tier operations * (`putAtTier`/`getAtTier`/`listAtTier`/`elevate`/`demote`). When omitted, * every tier operation throws `TiersNotEnabledError`. Enable by passing * `withTiers()` from `@noy-db/hub/tiers` at `createNoydb` time. */ tiersStrategy?: TiersStrategy | undefined; /** * Search / retrieval capability strategy. When omitted, a collection's * `search`/`retrieve`/`similarTo`/`warmIndex`/`flushIndex` methods and the * embedding write-hook throw `SearchNotEnabledError`. Enable by passing * `withSearch()` from `@noy-db/hub` at `createNoydb` time. */ searchStrategy?: SearchStrategy | undefined; /** * what a lower-tier caller sees for above-tier * records. Default `'invisibility'`. */ tierMode?: TierMode | undefined; /** * optional callback fired on every cross-tier access. * Provided by the Vault; collects notification events and writes * to the ledger. */ onCrossTierAccess?: ((event: CrossTierAccessEvent) => void) | undefined; /** /** * Optional back-reference to the owning vault's derivation * registry + collection accessor. When present, successful * `put()` dispatches registered derivation strategies for the * source collection. */ derivationSource?: { registry(): DerivationRegistry; getCollection(name: string): Collection>; /** * Read-only vault facade handed to `derive(source, ctx)` so a * derivation can fetch sibling records. Same shape and * instance the guards service uses for `check(incoming, ctx)`. */ getReadOnlyFacade(): ReadOnlyVaultFacade; /** * Read access to the owning Noydb's currently-active multi-record * transaction context, or `null` when no transaction is running. * `dispatchDerivations` consults this so a recursive derived-output * write can register its pre-write envelope onto `ctx._executed` * and roll back alongside the source op on mid-batch failure. */ getActiveTxContext(): TxContext | null; /** * Construct a transient TxContext bound to the owning Noydb. Used * by `Collection.putManyAtomic` to publish an active context for * its Phase 2 loop. */ createTxContext(): TxContext; /** Publish a TxContext for the duration of a bulk-atomic loop. */ setActiveTxContext(ctx: TxContext): void; /** Drop a previously-published TxContext. */ clearActiveTxContext(ctx: TxContext): void; } | undefined; /** * Vault-internal hook for materialized-view dispatch. * Parallel to `derivationSource`. When set, `Collection.put` fires * registered MV `onSourceWrite` after the standard derivation * dispatch. */ materializedViewSource?: { registry(): MaterializedViewRegistry; getCollection(name: string): Collection; getActiveTxContext(): TxContext | null; getQueryContext(): MVQueryContext; } | undefined; /** * #638 Task 4 — thin collector hook wired by the Vault (`Vault._collectGraphTouch`). Every * `sync-apply`/`cutover`/`restore` mutation origin calls `collect(this.name, id)`; a no-op * when no batch is open (`_beginGraphBatch()` wasn't called) or the vault has no graph. * `collectDelete` (#640) is the sync-apply delete socket — the resolved rollup-parent intents * for a deleted record, captured pre-invalidation. */ graphDispatch?: { collect(collection: string, id: string): void; collectDelete(collection: string, id: string, intents: readonly RollupDeleteIntent[]): void; } | undefined; } /** One normalized `ComputedFields` entry — a plain `ComputedFn` or a `ComputedFieldEntry` * reduced to its parts, `mode` always defaulted (#638 Task 7). Exported so * `via/graph-wiring.ts`'s reconcile-path guard can check `mode` without duplicating * the plain-fn-vs-object-form normalization. */ export interface ComputedEntryParts { readonly fn: ComputedFn; readonly deps?: readonly string[]; readonly mode: 'materialized' | 'virtual'; } export declare function computedEntryParts(entry: ComputedFn | ComputedFieldEntry): ComputedEntryParts; /** * Compile a collection's declared config into the ordered list of `ViaBinding`s * for its `ViaPipeline`. * * money then i18n then lookup then classified then blob then computed — order * pinned for pipeline parity with the hand-wired baseline this replaces: money * encode ran before the i18n write stages, and money decode ran before i18n * locale/dict-label resolution on read. Lookup compiles right after i18n * (#650 Task 2) — a separate binding for native lookup()/enumOf()/dict() * fields (`dictKeyFields`/`dictKey()`/`staticDict()` stay on the i18n * binding above, unchanged); its `present` hook only adds `Label`, * same shape of read-time addition as i18n's own dict-label dressing, and * declares no write-pipeline hooks yet (Task 3 adds `ingest`/`enforceWrite`). * Classified compiles after those (#629 Task 6): its * `encodeAtRest`/`decodeAtRest` hooks make the pipeline's `hasAtRestHooks` * true, retiring the codec's inline `sensitiveFields` seal path * (record-codec.ts) for any collection that declares `classifiedFields` — * `classifiedGuardCtx` is the SAME `ClassifiedGuardCtx` * `resolveCollectionConfig` already built for door 1's * `guardClassifiedCompat` call, threaded in by its one caller below. Blob * compiles next (#629 Task 7) but its position is inert: the blob binding * declares NO write/read pipeline hooks (blob content is out-of-band * `BlobSet` side-collections — it must never flip `hasAtRestHooks`), only * `erase`/`describeFragment`. Computed compiles LAST (#638 Task 7) — its * `present` hook must run AFTER money's own `present` so a virtual field's * `deps` read the decoded (quantized) amount, not the raw stored form; at * present-time i18n/lookup's DRESSING now runs AFTER computed instead (#665 * `_presentOrder`, `via/pipeline.ts` — compile order here is unchanged). * `via/graph-wiring.ts#applyTaintOverlay` appends the `taint` * binding after WHATEVER this function returns, so taint's present-time * redaction always runs after computed's regardless of this ordering. * {@link Collection._applyMoneyFields} PREPENDS money for the same reason * on its own (MV-precreation reconcile) path — see its docstring; * {@link Collection._applyClassifiedFields} APPENDS classified on that same * reconcile path (blobFields/computed have no late-attach reconcile door — * `viaFields`, like i18nFields/dictKeyFields, is construction-only). * * `eraseCfgOut` (#629 Task 10, optional out-param) — this function runs * before the owning `Collection` exists (`this.codec` isn't built yet), so * the classified binding's `classifySealedShred` closure can't be wired * here. When supplied, `eraseCfgOut.classified` is set to the SAME cfg * object instance handed to `viaBinder('classified')`, so a caller * (`resolveCollectionConfig`) can thread it out to the `Collection` * constructor, which mutates `classifySealedShred` in place once * `this.codec` exists. Additive — every existing caller omits it and keeps * getting a plain `ViaBinding[]`. */ export declare function compileViaBindings(opts: CollectionOpts, classifiedGuardCtx: ClassifiedGuardCtx, eraseCfgOut?: { classified?: ClassifiedViaConfig; }): ViaBinding[]; /** * The money∩virtual-mode-computed field-name intersection (#669) — shared by * `compileViaBindings` above (fresh construction, `isVirtual` closes over the just-built * `virtualFields` Map) and {@link Collection._applyMoneyFields} (late-attach, `isVirtual` * closes over the already-compiled pipeline's `computed` binding instead — virtual-mode * computed fields have no late-attach door of their own (declaring one on a reconcile call * throws), so by the time money reconciles onto an existing collection, any virtual field * is already sitting in `bindings`, never in `this.computed`: `resolveCollectionConfig` * deliberately excludes virtual-mode entries from the map it assigns to `this.computed`, * since that field feeds ONLY the write-time `evalComputedFields` path — see * `materializedComputed` above). Both callers get the identical intersection semantics from * one function, so money's `presentLate` MAJOR-UNITS dressing (`via/money/binding.ts`) can * never silently disagree between the fresh and late-attach paths. */ export declare function resolveVirtualMoneyFields(moneyFieldNames: Iterable, isVirtual: (field: string) => boolean): Set; /** One `ViaGraph.registerDerived` call's worth of edge data — target + sources, * `kind` chosen by the caller (#638 Task 2 edge extraction). `grain` defaults to * `'record'` when absent; `resolveComputedEdges` (#638 Task 7) sets it to `'virtual'` * for a `mode: 'virtual'` computed field's edge. */ export interface GraphEdge { readonly target: FieldRef; readonly sources: readonly FieldRef[]; readonly grain?: Grain; } /** * The known-field-name universe builder (#638 Task 2 fix wave 2) — used by * {@link resolveCollectionConfig} to validate `resolveViaBindingDepsEdges`'s `deps` * entries, and by {@link resolveComputedEdges}'s classified-collection dep-name check * (Finding I2ii's "shared, so the two paths cannot silently drift apart" rationale). * #638 Task 7 dropped `resolveComputedEdges`'s dep-name check entirely (legalizing a * plain-field dep on a non-classified collection); the Task 7 review's CRITICAL finding * (a typo'd dep on a CLASSIFIED collection reopens the #636 leak) restored a * classified-only slice of it, scoped through this SAME helper rather than a * hand-rolled second universe — exported so `via/graph-wiring.ts`'s reconcile path can * build its own call-scoped knownFields the identical way the fresh path does. */ export declare function collectKnownFieldNames(parts: { readonly moneyFields?: Record | undefined; readonly i18nFields?: Record | undefined; readonly dictKeyFields?: Record | undefined; readonly classifiedFields?: Record | undefined; readonly computed?: Record | undefined; readonly lookupFields?: Record | undefined; }): Set; /** * Validate each `computed` entry's `deps` well-formedness and resolve them into graph * edges (#638 Task 2; #638 Task 7 folded the formerly-separate `computedDeps` sibling * option into each entry's own `{ fn, deps?, mode? }` shape — see * `with-formula/computed/index.ts#ComputedFieldEntry`). `computed` is the RAW * user-declared map — `unifyComputedFields`'s union of the `computed:` sugar option and * `via(computed(...))` entries, NEVER `mergedComputed`: a classified preset's * `riderComputed` companions are a sanctioned, already-vetted classified→computed channel * and are deliberately NOT subject to this guard. A depsless entry is fine UNLESS the * collection also declares classified fields (`hasClassifiedFields`), in which case an * opaque computed function could silently copy a classified field's plaintext into an * ordinary, unredacted field (the #636 leak) — refused at declare time, regardless of * `mode` (a virtual field's read-time redaction, `via/taint-binding.ts`, only fires for a * field the graph actually taints — a depsless one never is). * * A `deps` entry may name ANY field — including a PLAIN field with no via feature * declared on it at all (#638 Task 7; Task 2's original design only ever validated a * `deps` entry against `moneyFields`/`i18nFields`/`dictKeyFields`/`classifiedFields`/ * `computed`'s own declared names — `collection-config.ts` has no schema-introspection * API to check a `deps` entry against the record's FULL field set, since * `StandardSchemaV1` — Zod/Valibot/ArkType/Effect, deliberately schema-library-agnostic — * exposes no "list of field names" capability). This is safe on a NON-classified * collection: a dep with no registered `ViaGraph` node contributes `DEFAULT_POSTURE` * when folded (`ViaGraph._contribution`'s `?? DEFAULT_POSTURE` fallback) — i.e. nothing, * exactly like any other untainted source. * * Task 7 review CRITICAL fix (empirically confirmed leak reopening): on a collection * that DOES declare classified fields, an UNKNOWN `deps` entry (a typo — e.g. * `deps: ['sssn']` instead of `['ssn']`) used to fold to `DEFAULT_POSTURE` exactly like * the "harmless plain field" case above, silently reopening the #636 leak — construction * didn't throw, and the derived field was written/read UNSEALED even though its `fn` * actually read a classified field. `knownFields` (built by {@link collectKnownFieldNames} * the SAME way at every call site — never a hand-rolled second universe, the exact drift * that caused a Task 2 bug) restores the "every dep must name a known field" check, but * ONLY when `hasClassifiedFields` — a non-classified collection keeps the Task 7 freedom * (any string is a legal dep, per the paragraph above) since an untainted fold there is * always safe regardless of typos. * * KNOWN LIMIT (pre-existing since Task 2, phase-E territory — pinned by * `via/taint.test.ts`'s "KNOWN LIMIT" test): this only checks that a `deps` entry names * SOME known field, not that it names the RIGHT one. A `deps` entry naming a real, * declared-but-WRONG field (e.g. `fn` reads classified field `ssn` but `deps: ['amount']` * — a genuine, known, non-classified field) still passes this check and still leaks: * the graph edge folds from `amount`'s posture, not `ssn`'s, so the derived field is * folded/sealed as if it read `amount`, while its actual output is `ssn`'s plaintext. * There is no schema-introspection capability (see above) to verify a `deps` entry * actually corresponds to what `fn` reads — closing this fully would need runtime * read-tracking or a schema-aware capability, out of this fix's scope. */ export declare function resolveComputedEdges(collectionName: string, computed: ComputedFields | undefined, hasClassifiedFields: boolean, knownFields?: ReadonlySet): readonly GraphEdge[]; /** * Extract graph edges from `ViaBinding.deps` (#638 Task 2 — `deps` goes from * inert to validated). For any compiled binding declaring `deps`, every field * it `covers()` (tested against `knownFields`) becomes a derived target whose * sources are `deps`; an unknown source field throws declare-time * `ValidationError`. No shipped binding declares `deps` today (money/i18n/ * classified/blob don't) — this is the general path a future derive-bearing * binding (phase C Task 7's `computed` via-binding) plugs into. */ export declare function resolveViaBindingDepsEdges(collectionName: string, bindings: readonly ViaBinding[], knownFields: ReadonlySet): readonly GraphEdge[]; /** * Resolve the raw {@link CollectionOpts} into the concrete field values the * {@link Collection} constructor assigns — every `?? default`, every derived * `Set`/subset, plus the three pure construction-time validations. Pure: no * `this`, no external side effect. The allocated `Set`/`VectorSet`/`Lru` * instances become the collection's own by reference. */ export declare function resolveCollectionConfig(opts: CollectionOpts): { adapter: NoydbStore; vault: string; name: string; keyring: UnlockedKeyring; storeCiphertext: boolean; ramCiphertext: boolean; emitter: NoydbEventEmitter; writeQueue: WriteQueueTracker | undefined; schemaUpdateGate: SchemaUpdateGate | undefined; schemaFence: SchemaFenceController | undefined; writeHooks: WriteHookRegistry | undefined; subsystemBus: ServiceBus | undefined; activeTxId: (() => string | null) | undefined; blobStrategy: BlobStrategy; objectStore: ObjectProjection | undefined; blobFields: BlobFieldsConfig | undefined; aggregateStrategy: AggregateStrategy; crdtStrategy: CrdtStrategy; historyStrategy: HistoryStrategy; i18nStrategy: I18nStrategy; syncStrategy: SyncStrategy; reconcileOnOpen: "auto" | "off" | "dry-run"; getDEK: (collectionName: string) => Promise; onDirty: OnDirtyCallback | undefined; tabCoordinated: (() => boolean) | undefined; historyConfig: HistoryConfig; historyConfigExplicit: boolean; schema: StandardSchemaV1 | undefined; ledger: LedgerStore | undefined; refEnforcer: { enforceRefsOnPut(collectionName: string, record: unknown): Promise; enforceRefsOnDelete(collectionName: string, id: string): Promise; } | undefined; joinResolver: { resolveSource(collectionName: string): JoinableSource | null; resolveRef(leftCollection: string, field: string): RefDescriptor | null; resolveDictSource?: (leftCollection: string, field: string) => JoinableSource | null; } | undefined; i18nFields: Record | undefined; textIndexes: readonly string[] | undefined; embeddings: EmbeddingDescriptor | undefined; vectorSet: VectorSet | undefined; dictKeyFields: Record | DictKeyDescriptor> | undefined; lookupFields: Record> | undefined; presentForJoin: ((record: unknown, locale: string) => unknown) | undefined; fieldMeta: Record | undefined; meta: CollectionMeta | undefined; _refs: Record; via: ViaPipeline | undefined; classifiedEraseCfg: ClassifiedViaConfig | undefined; moneyFields: Record | undefined; classified: ResolvedClassified | undefined; classifiedGuardCtx: ClassifiedGuardCtx; classifiedStrategy: ClassifiedStrategy; computed: ComputedFields | undefined; computedEdges: readonly GraphEdge[]; viaDepsEdges: readonly GraphEdge[]; computedFieldNames: string[]; dictLabelResolver: ((dictName: string, key: string, locale: string, fallback?: string | readonly string[]) => Promise) | undefined; getDictionary: ((name: string) => Promise) | undefined; i18nPutValidator: ((record: unknown) => void) | undefined; autoTranslateHook: ((text: string, from: string, to: string, field: string, collection: string) => Promise) | undefined; defaultLocale: string | undefined; crdtMode: CrdtMode | undefined; syncAdapter: NoydbStore | undefined; onAccess: ((op: "get" | "put" | "delete" | "reveal" | "verify" | "find", id: string) => Promise) | undefined; derivationSource: { registry(): DerivationRegistry; getCollection(name: string): Collection>; /** * Read-only vault facade handed to `derive(source, ctx)` so a * derivation can fetch sibling records. Same shape and * instance the guards service uses for `check(incoming, ctx)`. */ getReadOnlyFacade(): ReadOnlyVaultFacade; /** * Read access to the owning Noydb's currently-active multi-record * transaction context, or `null` when no transaction is running. * `dispatchDerivations` consults this so a recursive derived-output * write can register its pre-write envelope onto `ctx._executed` * and roll back alongside the source op on mid-batch failure. */ getActiveTxContext(): TxContext | null; /** * Construct a transient TxContext bound to the owning Noydb. Used * by `Collection.putManyAtomic` to publish an active context for * its Phase 2 loop. */ createTxContext(): TxContext; /** Publish a TxContext for the duration of a bulk-atomic loop. */ setActiveTxContext(ctx: TxContext): void; /** Drop a previously-published TxContext. */ clearActiveTxContext(ctx: TxContext): void; } | undefined; materializedViewSource: { registry(): MaterializedViewRegistry; getCollection(name: string): Collection; getActiveTxContext(): TxContext | null; getQueryContext(): MVQueryContext; } | undefined; graphDispatch: { collect(collection: string, id: string): void; collectDelete(collection: string, id: string, intents: readonly RollupDeleteIntent[]): void; } | undefined; tiers: Set | null; tiersStrategy: TiersStrategy; searchStrategy: SearchStrategy; tierMode: TierMode; blobTierPolicy: "isolate" | "dedup"; onCrossTierAccess: ((event: CrossTierAccessEvent) => void) | undefined; addSubjectRef: ((id: string, record: unknown) => Promise) | undefined; deterministicFields: ReadonlySet | null; sensitiveFields: ReadonlySet; vdigFields: ReadonlyMap | null; perRecordCek: boolean; cekCache: Lru | null; provenance: boolean; };