/** * `buildDescription` — pure assembler that merges collection config * (moneyFields / dictKeyFields / refs / computed / fieldMeta) with an * optional validator-derived `zodFields` map into a normalised * {@link CollectionDescription}. * * The sync path (`collection.describe()`) passes `zodFields: undefined`. * The async path supplies a populated map and validator-derived type * strings. * * @module */ import type { FieldMeta } from './field-meta.js'; import { FieldMetaUnknownFieldError } from './field-meta.js'; import type { CollectionMeta } from './meta.js'; import type { ViaDescriptor, ViaPosture } from '../../kernel/via/index.js'; import type { DictKeyDescriptor, StaticDictDescriptor } from '../../via/i18n/dictionary.js'; import type { LookupDescriptor } from '../../via/lookup/descriptor.js'; import type { I18nTextDescriptor } from '../../via/i18n/core.js'; import type { ComputedFields } from '../../with-formula/computed/index.js'; import type { RefDescriptor } from '../../kernel/refs.js'; import type { ClassifiedFieldSpec } from '../../via/classified/descriptor.js'; export interface DescribedField { readonly key: string; /** Sync: inferred from config ('number'|'enum'|'string'|'array'|'unknown'). Async (Task 4): validator-derived. */ readonly type: string; readonly optional: boolean; readonly constraints?: Record; readonly label: string; readonly description?: string; /** Card/section grouping hint for detail & form layouts. From fieldMeta/zod .meta(). */ readonly group?: string; /** Relative ordering hint within and across groups. Lower renders first. */ readonly order?: number; readonly semanticType?: string; readonly unit?: string; readonly sensitivity?: 'public' | 'pii' | 'secret'; readonly aggregate?: 'sum' | 'count' | 'distinct' | 'none'; readonly aliases?: readonly string[]; readonly ref?: { target: string; mode: string; isArray?: true; }; readonly displayFor?: string; readonly money?: { mode: 'fixed' | 'multi'; currency?: string; scale?: number; rounding?: string; }; readonly dict?: { name: string; static: boolean; values?: readonly { value: string; label?: string; }[]; }; /** * Normalized lookup metadata (#650 Task 7 — the first-ever * `ViaBinding.describeFragment` consumer; sourced from the `'lookup'` * binding's fragment, NOT from config directly — see `buildDescription`). * Present for every `lookup()`/`enumOf()`/`dict()` field, ALONGSIDE the * pre-existing `dict` block above (kept byte-stable for the * `dictKey()`/`staticDict()` alias — this is additive, not a replacement). * `dimension` is omitted for a bare `enumOf()` (no backing store, no * dimension name — the #650 Task 2 `dimension:''` sentinel resolved). * `keys` is the statically-known closed-vocabulary key set (declared * `keys`, or a static table's own keys) — omitted when membership lives * only in the backing collection/dictionary. */ readonly lookup?: { readonly dimension?: string; readonly backing: 'static' | 'reserved' | 'collection'; readonly vocabulary: 'open' | 'closed'; readonly key: string; readonly altKeys?: readonly string[]; readonly present?: { readonly label: string; readonly by?: string; }; readonly sortBy?: string; readonly onDelete: 'restrict' | 'cascade' | 'nullify'; readonly keys?: readonly string[]; }; readonly computed?: true; /** i18n metadata for fields declared with i18nText(). Present only when the field is i18n-enabled. */ readonly i18n?: { readonly locales?: readonly string[]; readonly densify?: boolean; }; /** Widget hint derived from semanticType+type, overridable via fieldMeta.widget. */ readonly widget: string; /** Whether the field is user-editable. False for computed, id, and provenance-stamped fields. */ readonly editable: boolean; /** Present when the field is classified. Serialized read-projection contract. */ readonly classified?: { readonly preset: string; readonly storage: 'recoverable' | 'never' | 'digest-only'; readonly list: 'omit' | { readonly mask: string; } | { readonly rider: string; }; /** * Present (and `true`) only when the field opted into the equatable blind * index. Additive metadata — emitted UNGATED (no `withClassified()` needed; * `toJSONSchema()` carries it too), so it advertises *that* the field is * equatable beyond the DEK-consent boundary. Intended: it's a structural * property a schema consumer legitimately needs and discloses no value. A * deployment wanting it gated can gate this behind its value-bearing door. */ readonly equatable?: true; }; /** * Present only for a field declared via `blobFields` (#657) — out-of-band * attachment storage (`collection.blob(id)`), never a sealed or plaintext * record field. `queryable` mirrors the binding's fixed `ViaPosture` * (blob content is never indexed — always `'none'`); the rest mirrors the * declared `blobFields[field]` policy verbatim (scalars) or as a presence * flag (predicate knobs — a predicate has no serializable form). Sourced * from the `'blob'` binding's `describeFragment()`, the same * `viaFragments` door the `lookup` block above is sourced from. */ readonly blob?: { readonly retainDays?: number; readonly evictWhen?: true; readonly legalHold?: true; readonly retainUntil?: true; readonly external?: true; readonly public?: true; readonly backlink?: string; readonly queryable: 'none'; }; /** * Present only for a graph-tainted derived field (#638 Task 3 — computed/ * derivation output whose effective posture was forced away from the * plain baseline by a source's posture, e.g. a computed field reading a * classified field). `forcedBy` names the immediate declared source * field(s) responsible. */ readonly taint?: { readonly posture: ViaPosture; readonly forcedBy: readonly string[]; }; } export interface CollectionDescription { readonly collection: string; readonly fields: readonly DescribedField[]; /** Collection-level descriptive metadata; label falls back to the humanized collection name. */ readonly meta: CollectionMeta; } /** Options for the async describe(opts) overload. */ export interface DescribeOptions { /** * When true, resolve dynamic-dict labels from vault.dictionary(name).list() * and populate dict.values[].label for dynamic dictKey fields. */ readonly resolveDictLabels?: boolean; } export interface ZodFieldSlot { readonly type?: string; readonly optional?: boolean; readonly constraints?: Record; readonly meta?: Partial; } /** * Derive per-field slots from a Standard Schema validator. * - Calls `derivePersistedSchema` to get the JSON Schema. * - Falls back to `{}` for non-Zod or unknown validators. * - For zod-4 schemas: reads recognized `.meta()` keys from inline property annotations. * - Returns: `Record`. * * No static zod import — all zod access is lazy via derivePersistedSchema. */ export declare function deriveZodFields(schema: unknown): Promise>; export interface BuildDescriptionInput { readonly collection: string; readonly fieldMeta: Record | undefined; /** * Kernel-visible collections only carry the opaque {@link ViaDescriptor} * marker (the kernel never inspects a Via feature's concrete descriptor * shape). Every entry here is actually a {@link MoneyDescriptor} — only * `money()` constructs them — narrowed locally below where its * mode/currency/scale are read for the describe() output. */ readonly moneyFields: Record | undefined; readonly dictKeyFields: Record | undefined; /** Native lookup()/enumOf()/dict() fields (#650 Task 2) — declared only via `viaFields`/`via()`, no sugar key. */ readonly lookupFields?: Record | undefined; readonly computed: ComputedFields | undefined; readonly refs: Record; /** Async path fills this; sync path passes `undefined`. */ readonly zodFields: Record | undefined; /** * Async path: when `resolveDictLabels` was true, this map holds * `{ dictName -> { value -> label } }` for dynamic dictKey fields. * Used to populate `dict.values[].label`. */ readonly dictLabels?: Record> | undefined; /** Collection-level descriptive metadata. Label falls back to humanized collection name. */ readonly meta?: CollectionMeta | undefined; /** * Map of field name → I18nTextDescriptor for fields declared with i18nText(). * When present, describe() surfaces an `i18n` block on matching DescribedField entries. */ readonly i18nFields?: Record | undefined; /** Per-field classified specs (already resolved/flattened). */ readonly classified?: Record | undefined; /** Graph-computed taint overlay (#638 Task 3) — `Collection.via?.taint`. */ readonly taint?: { readonly postures: ReadonlyMap; readonly provenance?: ReadonlyMap; } | undefined; /** * Per-binding `describeFragment()` output, keyed by binding brand (#650 * Task 7 — first-ever consumer of `ViaBinding.describeFragment`, * `via/index.ts:136`). `Collection.describe()`/`describeAsync()` build this * from `this.via?.describeFragments()`. The `'lookup'` brand's fragment * feeds the `lookup` block below; `'blob'`'s feeds the `blob` block * (#657, the second consumer). Other brands' fragments ride along unread * until they gain a consumer too. */ readonly viaFragments?: Record> | undefined; } export { FieldMetaUnknownFieldError }; /** * Pure assembler: no I/O, no side effects. * Builds a {@link CollectionDescription} from the collection's in-memory config. * * When `zodFields` is supplied (async path), validates that every `fieldMeta` key * refers to a known field (config ∪ zodFields). Throws `FieldMetaUnknownFieldError` * on the first unknown key — this is the real validation that the vault.ts no-op * couldn't do (schema fields weren't knowable synchronously). */ export declare function buildDescription(input: BuildDescriptionInput): CollectionDescription;