import type { ViaBinding, ViaPosture, ViaWriteCtx, ViaReadCtx, ViaCryptoCtx, SealedSlotRef, ViaEraseCtx, ViaEraseReport } from './index.js'; /** Opaque per-clause query payload carried on FieldClause (replaces the money-only slot). */ export interface ViaClause { readonly brand: string; readonly payload: unknown; } /** * The graph-computed taint overlay (#638 Task 3) — `postureFor`'s * assignment→enforcement bridge. `postures`/`sealFields` are the * enforcement-facing shapes `via/taint-binding.ts#buildTaintOverlay` * produces from `ViaGraph.taintedPostures`/`taintSealedFields`; * `provenance` (optional — introspection only, never consulted by * `postureFor`) is `ViaGraph.taintProvenance`'s output, surfaced by * `describe()`. */ export interface ViaTaintOverlay { readonly postures: ReadonlyMap; readonly sealFields: ReadonlySet; readonly provenance?: ReadonlyMap; /** #642 — the whole-record floor for a derivation/MV/overlay OUTPUT collection (the '*' target's * folded, clamped effective posture). postureFor falls back to it for ANY field; redactForExport * picks it up per-field; a sealed default drives taintBinding's sealAllFields mode. O(1) read. */ readonly defaultPosture?: ViaPosture; } export declare class ViaPipeline { readonly bindings: readonly ViaBinding[]; readonly taint?: ViaTaintOverlay | undefined; /** * Present-phase-ONLY order (#665, corrected under #665 review) — a stable * THREE-WAY partition of `bindings` computed once here at construction: * every `'money'`-brand binding first (existing relative order), then * every `'computed'`-brand binding (existing relative order), then * everything else (existing relative order). `present()` folds over this * instead of `bindings` so a `mode: 'virtual'` computed field's value * exists before i18n/lookup's dressing `present()` hooks run on it * (today's `compileViaBindings` order is money→i18n→lookup→classified→ * blob→computed, `collection-config.ts:554`, so dressing unconditionally * ran BEFORE the value it dresses existed). This reorder is binding-level, * not conditioned on any particular field composition — it applies to * EVERY collection that compiles a `'computed'` binding at all, not only * ones stacking a dresser on the SAME field as a computed one. EVERY other * phase (`ingest`/`encodeWrite`/`encodeAtRest`/`enforceWrite`/etc.) keeps * folding over `bindings` unchanged — those write/query phases need * money-first (`_applyMoneyFields` PREPENDS money, `_applyClassifiedFields` * APPENDS classified to preserve it, `kernel/collection.ts:1275-1284,1364- * 1368`). * * Money is deliberately carved OUT of the generic "everything else" slice * and kept FIRST — the original two-way `[computed..., rest...]` partition * regressed money's composed-field behavior from a benign missing-dressing * gap into VALUE CORRUPTION: money's `present()` DECODES its input as a * STORED SCALED-INT (and derives `Formatted` from it) — it is not a * pure dressing CONSUMER like i18n/lookup, which only ADD a `Label` * key and never rewrite the field's own value. Running money AFTER * computed on a field composing `computed(virtual)` + `money(...)` on * ITSELF means money's decode sees the virtual field's raw MAJOR-unit * output (e.g. `21`) and misreads it as a scaled-int, producing a * corrupted value (`'0.21'`) instead of the pre-#665 result (the raw `21` * surviving untouched, with no `Formatted` key added — a benign * missing-dressing gap, not a wrong value). Money-first restores money's * exact pre-#665 present position — byte-identical money behavior; the * materialized composition pins from the #631 work and the money suites * all ran under money-first. i18n/lookup are genuine dressing consumers * (they only ADD, never rewrite), so they stay after computed, which is * the fix #665 intended. A `'taint'` binding (appended LAST by * `via/graph-wiring.ts#applyTaintOverlay`) stays last here too (it's * neither money nor computed, so it lands at the tail of the "everything * else" slice), preserving its "runs after computed, redacts tainted * virtual output" contract (`via/taint-binding.ts`'s `taintBinding` doc * comment). * * Money-dressing a virtual computed field's OWN output (the composed * `computed(virtual) + money` case) is NOT resolved by this partition * alone — #669 resolves it with a separate mid-fold hook instead: * `ViaBinding.presentLate` runs after every binding's `present()` in this * money+computed segment, before the "everything else" segment below, so * money can quantize/reinterpret a virtual field's fresh MAJOR-UNITS * output as if it were a stored value, without perturbing this ordering * or the #665 invariant above it (`_presentLateBoundary` below marks * exactly where that mid-fold point sits within `_presentOrder`). */ private readonly _presentOrder; /** #669 — index into `_presentOrder` where the money+computed segment ends and the * "everything else" segment begins (`moneyBindings.length + computedBindings.length`); * `present()` runs every binding's `presentLate` at exactly this boundary. */ private readonly _presentLateBoundary; private constructor(); /** undefined when there is nothing to enforce — the zero-via fast path is * `this.via === undefined` (#553: keeps an all-plain collection sync). A * `'*'`-defaulted output collection (#642) counts as "something to enforce" * even with zero field-specific postures and zero bindings. */ static build(bindings: readonly ViaBinding[], taint?: ViaTaintOverlay): ViaPipeline | undefined; /** Refuse a write before crypto runs: awaits each binding's `enforceWrite` in order — first throw wins. */ enforceWrite(record: Record, ctx: ViaWriteCtx): Promise; ingest(record: Record): Record; canonicalizeStored(record: Record): Record; encodeWrite(record: Record, ctx: ViaWriteCtx): Promise>; /** * Final write-pipeline stage: folds each binding's `encodeAtRest` in * order, threading the record and accumulating the sealed-slot map. A * field belongs to exactly one via feature — two bindings sealing the * same field is a brand-keyed collision and throws. */ encodeAtRest(record: Record, crypto: ViaCryptoCtx): Promise<{ record: Record; sealed?: Record; }>; /** First read-pipeline stage: folds each binding's `decodeAtRest` in order. */ decodeAtRest(record: Record, sealed: Record, crypto: ViaCryptoCtx, opts: { asHandles: boolean; }): Promise>; present(record: Record, ctx: ViaReadCtx): Promise>; buildClause(field: string, op: string, value: unknown): ViaClause | undefined; evaluateClause(clause: ViaClause, actual: unknown, op: string): boolean; /** * Resolve a `buildClause` payload to an index-probe operand (#625) — * mirrors `evaluateClause`'s brand dispatch. `undefined` (a binding * with no `indexProbe`, or one that declines to probe this op/payload) * means the caller (`candidateRecords()`) must fall back to a scan. */ indexProbe(clause: ViaClause, op: string): unknown | undefined; /** * Resolve a raw STORED field value to its canonical eager-index bucket * key (#672) — folds every binding's `canonicalizeIndexKey`, first * non-undefined wins (same fold discipline as `buildClause`). `undefined` * means no binding claims `field` (or the value can't be canonicalized) * — the caller buckets the raw stringified value, unchanged from before * this hook existed. */ canonicalizeIndexKey(field: string, rawValue: unknown): string | undefined; decodeResults(record: unknown): unknown; /** undefined = no binding covers the field → caller falls back to generic compare. */ compareForOrder(field: string, a: unknown, b: unknown): number | undefined; /** undefined = no binding resolves an order-sort label for `key` at `field`/`locale` (#650 Task 7 — * the `orderBy(..., {by:'label'})` per-call-locale channel `compareForOrder` above can't serve). */ resolveOrderLabel(field: string, key: string, locale: string | undefined): string | undefined; /** * Which posture governs `field`, if any binding covers it — `undefined` * means no binding declares `field` (the generic, non-via query path * applies). #629 Task 8's posture consumer: `.where()`/`.orderBy()` * consult this before building/evaluating a clause, refusing fields whose * posture is `queryable: 'none'` (e.g. blob); every other posture * ('det-exact'/'ordered'/'full') is left to the existing per-binding * buildClause/evaluateClause/compareForOrder machinery, unchanged. * * #638 Task 3: the graph-computed taint overlay is consulted FIRST — a * derived field's assigned (most-restrictive-of-sources) posture wins over * whatever a binding would otherwise report for that field name (no * binding covers a computed/derived field today, so this never actually * shadows one — see `via/taint-binding.ts`'s `taintBinding.covers`, which * only claims the sealed subset for `encodeAtRest`/`decodeAtRest`, not for * this lookup). */ postureFor(field: string): ViaPosture | undefined; /** * Refuse any field-based reducer over a `queryable: 'none'` field (blob) — * metadata-only: walks each reducer's `.field` and checks posture, no * rewriting. Reducers with no `.field` (e.g. `count()`) are skipped. * * Shared by `wrapReducers` below (which ALSO applies each binding's * rewrite, e.g. money's exact-BigInt reducer swap) and by * `ScanBuilder.aggregate()` (#629 Task 8 review fix wave 1), which must * call THIS, not `wrapReducers` — wiring full `wrapReducers` into * `ScanBuilder.aggregate()` would newly activate money/i18n reducer * wrapping on a path that has never run it, a parity-breaking behavior * change for existing brands. */ refuseUnqueryableReducers(spec: S): void; /** * Rewrite an aggregate spec (money exact reducers), then refuse any * field-based reducer over a `queryable: 'none'` field (blob) — the same * posture gate `.where()`/`.orderBy()` apply, extended to `.aggregate()` * (both the bare-spec and builder forms funnel through here). */ wrapReducers(spec: S): S; /** * Deliberate export-layer redaction (#629 Task 9's posture consumer): * for every OWN field the decoded record carries, if the covering * binding's posture declares `exportable: false` (classified today; * money/i18n/blob are all `true`), replace the value with * {@link EXPORT_REDACTION_MARKER} — the same `'[sealed]'` string * `SealedHandle.toJSON()` (`kernel/types.ts`) already emits, so this is * byte-parity with today's accidental redaction, now independent of the * field's runtime shape. `SealedHandle.toJSON()` stays untouched as * defense-in-depth: a record read via `collection.get()` and * JSON.stringify'd outside `exportStream()` still gets the accident's * redaction on its own. Never mutates the input; returns a new object * only when a field was actually redacted. */ redactForExport(record: Record): Record; /** * Shared fold over a subset of `this.bindings`: runs each binding's * `erase`, concatenating shredded/retainedShared counts and residue. * Consults `posture.forgettable` (#629 Task 10) — a binding declaring * `forgettable: false` is skipped even if it defines `erase` (none does * today; a future non-forgettable binding is supported without a brand check). */ private foldErase; /** `forget()`'s per-ref erasure door: folds every binding's `erase`. */ erase(ctx: ViaEraseCtx): Promise; /** * `forget()`'s SEALED-posture-only erase fold (#629 Task 10, `Collection._onViaErase`'s * one caller) — classified today; metadata-filtered on * `posture.encryptedAtRest`, not brand-checked. `undefined` when no * sealed-posture binding is compiled in — `vault.ts` then falls back to * its own bare-`sensitive` classification, which no via binding covers. */ eraseSealed(ctx: ViaEraseCtx): Promise; /** True iff any binding implements decodeResults (query fast-path gating). */ get hasResultDecode(): boolean; /** * True iff any binding declares an at-rest hook (`encodeAtRest`/ * `decodeAtRest`) — the codec boundary's async-stack gate (#629 Task 3 * consults this to choose between the hook path and today's inline * sealed-slot path; a stack with only sync hooks, e.g. money-only, stays * `false`). */ get hasAtRestHooks(): boolean; /** * Fold every binding's `describeFragment()` into one `brand -> fragment` * map (#650 Task 7 — the first-ever consumer; `describeFragment` was * declared at `via/index.ts:136` since #623 with zero callers until this task). * `undefined` when no compiled binding implements it. Consumed by * `Collection.describe()`/`describeAsync()`, threaded to `buildDescription` * as `BuildDescriptionInput.viaFragments`. */ describeFragments(): Record> | undefined; } /** * The marker `redactForExport` writes for a non-exportable field — * intentionally the same literal `SealedHandle.toJSON()` (`kernel/types.ts`) * returns, so the two independent redaction layers (deliberate + accident) * produce byte-identical export output. Duplicated, not imported, because * `SealedHandle.toJSON()` is untouched by #629 Task 9 (belt-and-braces — * `via/export-posture-b.test.ts` asserts both layers agree on this string). */ export declare const EXPORT_REDACTION_MARKER = "[sealed]"; /** * Structural shape `exportRedact` needs from a collection: just the typed * `_via` accessor `Collection` exposes (`kernel/collection.ts`, #634). A * named interface rather than the real `Collection` type, because * `collection.ts` imports `ViaPipeline` from this module — importing * `Collection` back here would be circular. */ interface HasViaPipeline { readonly _via: ViaPipeline | undefined; } /** * Widened sibling of {@link HasViaPipeline}: `via/graph-wiring.ts`'s * `applyTaintOverlay` (#666) needs to REASSIGN the live pipeline, not just * read it — `_setVia` is `Collection`'s typed writer seam (`kernel/ * collection.ts`, beside `_via`), replacing the earlier untyped `coll as { * via; codec: { setVia } }` cast. Exported (unlike `HasViaPipeline`) because * its one consumer lives in a different module. */ export interface HasWritableViaPipeline extends HasViaPipeline { _setVia(pipeline: ViaPipeline | undefined): void; } /** * `vault.ts`'s `exportStream()` reach-in (#629 Task 9): apply the owning * collection's export redaction to one decoded record. Typed via {@link * HasViaPipeline} against `Collection`'s `_via` accessor (#634) — replaces * the earlier untyped any-cast reach-in. */ export declare function exportRedact(coll: HasViaPipeline, record: unknown): unknown; export {};