diff --git a/.changeset/strict-insert-contract-tsdoc.md b/.changeset/strict-insert-contract-tsdoc.md new file mode 100644 index 0000000000..2496612641 --- /dev/null +++ b/.changeset/strict-insert-contract-tsdoc.md @@ -0,0 +1,23 @@ +--- +"@objectstack/spec": patch +--- + +docs(spec): `WriteObservabilityOptions.strictReadonlyWrites` no longer claims INSERT ignores it (#7064) + +The contract's closing paragraph still said "INSERT ignores it … insert is +exempt from both strips, so there is nothing to refuse" — true when #5126 +shipped the option, false since #5503 wired `engine.insert` to REFUSE a +payload carrying a runtime-owned value (`RUNTIME_OWNED_FIELD_TYPES`, today +`autonumber`) under `strictReadonlyWrites: true`, throwing +`ReadonlyFieldRejectedError` (`ERR_READONLY_FIELD_REJECTED`, +`operation: 'insert'`) and writing nothing. + +The TSDoc now states, measured against the engine: insert stays exempt from +the two author-declared strips at this seam (#3413 — an in-process create may +seed a `readonly: true` field's initial value; `readonlyWhen` cannot lock a +create), while the runtime-owned strip runs on insert and is exactly what +strict refuses; the exempt writers are the ones the error message names +(`isSystem`, and `preserveAudit` for a #3493 historical import), explicitly +scoped to this in-process seam so the DataProtocol ingress policy +(#3043/#6640, `FieldSchema.readonly`) stays a distinct layer. Prose only — no +key, type, or behaviour changes. diff --git a/packages/spec/src/contracts/data-engine.ts b/packages/spec/src/contracts/data-engine.ts index 2cb9128c48..d7203dc4ab 100644 --- a/packages/spec/src/contracts/data-engine.ts +++ b/packages/spec/src/contracts/data-engine.ts @@ -17,7 +17,9 @@ import type { IDataDriver } from './data-driver.js'; * * `onFieldsDropped` is invoked by the engine when caller-supplied write fields * are LEGALLY stripped from the payload before the driver write — static - * `readonly` (#2948) or a TRUE `readonlyWhen` predicate (#3042). The write + * `readonly` (#2948), a TRUE `readonlyWhen` predicate (#3042), or an + * implicitly-readonly runtime-owned type (#5503; `RUNTIME_OWNED_FIELD_TYPES`, + * today `autonumber` — the one strip that also runs on INSERT). The write * still succeeds; the listener exists so callers that report per-field success * (e.g. a flow's `update_record` step) can surface a warning instead of a * silent success (#3356's masked stage write-backs). @@ -48,7 +50,9 @@ export interface WriteObservabilityOptions { * It covers every drop `onFieldsDropped` reports, i.e. both * `DroppedFieldsEvent['reason']` arms: static `readonly: true` (#2948, which * only runs for non-system callers) and a TRUE `readonlyWhen` predicate - * (#3042, which runs for every caller, `isSystem` included). Covering only + * (#3042, which runs for every caller, `isSystem` included) — plus, since + * #5503, the implicitly-readonly runtime-owned strip, which reports under + * the same `'readonly'` arm (see the INSERT section below). Covering only * the static arm would leave a trusted caller — the very caller this option * exists for, one that already passes `{ context: { isSystem: true } }` and * is therefore exempt from the static strip — still losing `readonlyWhen` @@ -81,8 +85,30 @@ export interface WriteObservabilityOptions { * client toggle write-refusal on a security-adjacent path. Widening strict to * the wire is a SEPARATE decision, not a side effect of this one. * - * INSERT ignores it, for the same reason `onFieldsDropped` never fires there: - * insert is exempt from both strips, so there is nothing to refuse. + * ## INSERT — refuses runtime-owned values (since #5503) + * + * Until #5503 this paragraph declared the option inert on insert — true + * when written (#5126 predates the runtime-owned strip), false since. At + * this seam insert remains deliberately exempt from the two AUTHOR-DECLARED + * strips (#3413: an in-process create may seed a `readonly: true` field's + * initial value, and `readonlyWhen` cannot lock anything on a create at + * all), but the implicitly-readonly runtime-owned strip #5503 added runs on + * insert too — and it is exactly the one strict refuses. An insert whose + * payload carries a runtime-owned value (`RUNTIME_OWNED_FIELD_TYPES`, today + * `autonumber` — a caller-supplied record number) behaves like update at + * this seam: with this option `true` it throws `ReadonlyFieldRejectedError` + * (`operation: 'insert'`) and nothing is written; without it the value is + * stripped, the write completes, and `onFieldsDropped` fires with + * `reason: 'readonly'`. The engine-level writers exempt from that strip — + * and therefore never refused — are the two the error message itself names: + * `isSystem`, and the `preserveAudit` historical import reinstating legacy + * record numbers (#3493). Layer note: that exemption pair is THIS + * in-process seam's. The DataProtocol ingress enforces its own + * author-declared `readonly` policy on create (#3043), where + * `preserveAudit` is UPDATE-only (#6640) — see `FieldSchema.readonly`; + * nothing here widens or narrows it. `ReadonlyFieldRejectedError`'s own doc + * records the same contract from the error's side: "Thrown by + * `engine.update` — and, since #5503, by `engine.insert`". */ strictReadonlyWrites?: boolean; }