From afd46c73186006389bda92ba9a732750ec862324 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 16:16:15 +0000 Subject: [PATCH] docs(spec): strictReadonlyWrites contract no longer claims INSERT ignores it (#7064) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The closing paragraph of the strictReadonlyWrites TSDoc still asserted the option is inert on insert ('INSERT ignores it ... insert is exempt from both strips, so there is nothing to refuse') — true when #5126 shipped, false since #5503 wired engine.insert to REFUSE a payload carrying a runtime-owned value under strict, throwing ReadonlyFieldRejectedError (operation: 'insert') and writing nothing. Verified by execution against origin/main before rewriting (three-probe matrix): strict insert with an autonumber value throws ERR_READONLY_FIELD_REJECTED / operation 'insert' with nothing written; the same insert without strict silently strips and fires onFieldsDropped with reason 'readonly'; an author-declared readonly field on insert stays exempt at this seam (#3413) even under strict. Prose only: the interface header and the Semantics arm enumeration gain the runtime-owned strip (#5503), and the INSERT paragraph now states what insert refuses (runtime-owned values only), names the engine-level exempt writers the error message names (isSystem, preserveAudit #3493), and pins the layer boundary against the DataProtocol ingress policy (#3043/#6640) so the two never read as one rule. No key/type/behaviour change; all 10 spec generated artifacts verified unchanged. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018ffcE95NaMJcL9XJ9VDYgk --- .changeset/strict-insert-contract-tsdoc.md | 23 +++++++++++++++ packages/spec/src/contracts/data-engine.ts | 34 +++++++++++++++++++--- 2 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 .changeset/strict-insert-contract-tsdoc.md 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; }