@@ -331,10 +331,11 @@ interface EngineUpdateOptions {
331331
332332### WriteObservabilityOptions
333333
334- The write methods (` insert ` / ` update ` ) additionally accept two ** in-process**
335- options that govern what happens when caller-supplied write fields are legally
334+ The write methods (` insert ` / ` update ` ) additionally accept three ** in-process**
335+ options. Two govern what happens when caller-supplied write fields are legally
336336stripped from the payload before the driver write: observe the strip
337- (` onFieldsDropped ` ) or refuse the write outright (` strictReadonlyWrites ` ).
337+ (` onFieldsDropped ` ) or refuse the write outright (` strictReadonlyWrites ` ). The
338+ third, ` onValidationAdvisory ` , reports the write's advisory validation-rule hits.
338339
339340The strips these two options cover are the engine's legal ones:
340341
@@ -354,6 +355,7 @@ on `insert` too since the 2026-09-03 ruling — see **On `insert`** below.
354355``` typescript
355356interface WriteObservabilityOptions {
356357 onFieldsDropped? : (event : DroppedFieldsEvent ) => void ;
358+ onValidationAdvisory? : (event : ValidationAdvisoryEvent ) => void ;
357359 strictReadonlyWrites? : boolean ; // refuse instead of stripping. Default: false
358360}
359361
@@ -364,6 +366,14 @@ interface DroppedFieldsEvent {
364366 // path's legal strips; branch on it exhaustively, never with a binary test
365367 reason: ' readonly' | ' readonly_when' | ' primary_key' | ' computed' ;
366368}
369+
370+ interface ValidationAdvisoryEvent {
371+ rule: string ; // the advisory validation rule's declared `name`
372+ severity: ' warning' | ' info' ; // never `error`: an `error` rule refuses the write instead
373+ field: string ; // the rule's first declared field, or `_record`
374+ code: string ; // the finding code a refused write reports for the same rule
375+ message: string ; // the rule's author-written message, in the caller's locale
376+ }
367377```
368378
369379#### ` onFieldsDropped ` — quiet and observable
@@ -425,6 +435,24 @@ legacy record numbers (#3493) exempts the runtime-owned strip only, and a
425435non-system create that asks for it still has its static ` readonly ` fields stripped,
426436with a ` warn ` saying the exemption is UPDATE-only (#6640 ).
427437
438+ #### ` onValidationAdvisory ` — the write's advisory rule hits
439+
440+ The engine calls the listener once per ADVISORY validation-rule hit on the write
441+ the options were passed to: a ` severity: 'warning' ` or ` 'info' ` rule whose verdict
442+ on the written record was "violated". Advisory rules never block, so the write
443+ proceeds. It is called per row on ` insert ` , and on ` update ` both by id and per
444+ matched row of a ` multi ` update. A nested write that a hook or a flow makes
445+ inside this one carries its own options, so that write's hits never reach this
446+ listener. A rule that could not be evaluated is not a hit, and is reported in the
447+ server log only. The listener fires when the rules are evaluated, before the
448+ driver write, so a hit is provisional until the write resolves. Passing it
449+ changes no server-side reporting: the per-write ` warn ` line, and a seed / boot
450+ load's one summary line per rule.
451+
452+ The DataProtocol's ` createData ` , ` updateData ` and ` cloneData ` pass this listener
453+ and answer the hits as ` warnings ` (` ValidateDataIssue ` entries carrying ` rule `
454+ and ` severity ` ), present only when there is at least one hit.
455+
428456<Callout type = " warn" >
429457** Layering — this is the engine seam, and since the 2026-09-03 ruling the only
430458one.** The DataProtocol create faces (` createData ` , ` cloneData ` , ` createManyData ` ,
@@ -436,15 +464,19 @@ declares one; none carries a `readonly` policy of its own any more — see
436464
437465<Callout type = " warn" >
438466` WriteObservabilityOptions ` is a ** TS-contract-level, in-process-only** bag —
439- both members. It is deliberately not part of the serializable Zod options schemas:
467+ all three members. It is deliberately not part of the serializable Zod options schemas:
440468a function is unrepresentable in JSON Schema and cannot cross the RPC (Virtual
441469Data Engine) boundary, so remote callers never receive these events; and putting
442470` strictReadonlyWrites ` in the serializable bag would let any client toggle
443471write-refusal on a security-adjacent path (#5126 ruling). A remote caller can set
444- neither and gets NEITHER behaviour: its write is stripped and committed, silently
445- from its side — a 200 whose read-only columns kept their stored values. Widening
446- strict to the wire is a SEPARATE decision. A listener that throws never breaks the
447- write — the engine catches and logs.
472+ none of the three, and for the strips it gets NEITHER behaviour: its write is
473+ stripped and committed, silently from its side — a 200 whose read-only columns
474+ kept their stored values. Widening strict to the wire is a SEPARATE decision.
475+ Advisory rule hits do reach a remote caller, through the answer rather than the
476+ listener: the DataProtocol's ` createData ` , ` updateData ` and ` cloneData ` pass
477+ ` onValidationAdvisory ` in-process and answer the hits as ` warnings ` , which is how
478+ a remote caller sees them. A listener that throws never breaks the write — the
479+ engine catches and logs.
448480</Callout >
449481
450482### delete
0 commit comments