Skip to content

Commit c5eef1d

Browse files
docs(spec): strictReadonlyWrites contract no longer claims INSERT ignores it (#7064) (#7109)
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. Claude-Session: https://claude.ai/code/session_018ffcE95NaMJcL9XJ9VDYgk Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9136327 commit c5eef1d

2 files changed

Lines changed: 53 additions & 4 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
docs(spec): `WriteObservabilityOptions.strictReadonlyWrites` no longer claims INSERT ignores it (#7064)
6+
7+
The contract's closing paragraph still said "INSERT ignores it … insert is
8+
exempt from both strips, so there is nothing to refuse" — true when #5126
9+
shipped the option, false since #5503 wired `engine.insert` to REFUSE a
10+
payload carrying a runtime-owned value (`RUNTIME_OWNED_FIELD_TYPES`, today
11+
`autonumber`) under `strictReadonlyWrites: true`, throwing
12+
`ReadonlyFieldRejectedError` (`ERR_READONLY_FIELD_REJECTED`,
13+
`operation: 'insert'`) and writing nothing.
14+
15+
The TSDoc now states, measured against the engine: insert stays exempt from
16+
the two author-declared strips at this seam (#3413 — an in-process create may
17+
seed a `readonly: true` field's initial value; `readonlyWhen` cannot lock a
18+
create), while the runtime-owned strip runs on insert and is exactly what
19+
strict refuses; the exempt writers are the ones the error message names
20+
(`isSystem`, and `preserveAudit` for a #3493 historical import), explicitly
21+
scoped to this in-process seam so the DataProtocol ingress policy
22+
(#3043/#6640, `FieldSchema.readonly`) stays a distinct layer. Prose only — no
23+
key, type, or behaviour changes.

packages/spec/src/contracts/data-engine.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import type { IDataDriver } from './data-driver.js';
1717
*
1818
* `onFieldsDropped` is invoked by the engine when caller-supplied write fields
1919
* are LEGALLY stripped from the payload before the driver write — static
20-
* `readonly` (#2948) or a TRUE `readonlyWhen` predicate (#3042). The write
20+
* `readonly` (#2948), a TRUE `readonlyWhen` predicate (#3042), or an
21+
* implicitly-readonly runtime-owned type (#5503; `RUNTIME_OWNED_FIELD_TYPES`,
22+
* today `autonumber` — the one strip that also runs on INSERT). The write
2123
* still succeeds; the listener exists so callers that report per-field success
2224
* (e.g. a flow's `update_record` step) can surface a warning instead of a
2325
* silent success (#3356's masked stage write-backs).
@@ -48,7 +50,9 @@ export interface WriteObservabilityOptions {
4850
* It covers every drop `onFieldsDropped` reports, i.e. both
4951
* `DroppedFieldsEvent['reason']` arms: static `readonly: true` (#2948, which
5052
* only runs for non-system callers) and a TRUE `readonlyWhen` predicate
51-
* (#3042, which runs for every caller, `isSystem` included). Covering only
53+
* (#3042, which runs for every caller, `isSystem` included) — plus, since
54+
* #5503, the implicitly-readonly runtime-owned strip, which reports under
55+
* the same `'readonly'` arm (see the INSERT section below). Covering only
5256
* the static arm would leave a trusted caller — the very caller this option
5357
* exists for, one that already passes `{ context: { isSystem: true } }` and
5458
* is therefore exempt from the static strip — still losing `readonlyWhen`
@@ -81,8 +85,30 @@ export interface WriteObservabilityOptions {
8185
* client toggle write-refusal on a security-adjacent path. Widening strict to
8286
* the wire is a SEPARATE decision, not a side effect of this one.
8387
*
84-
* INSERT ignores it, for the same reason `onFieldsDropped` never fires there:
85-
* insert is exempt from both strips, so there is nothing to refuse.
88+
* ## INSERT — refuses runtime-owned values (since #5503)
89+
*
90+
* Until #5503 this paragraph declared the option inert on insert — true
91+
* when written (#5126 predates the runtime-owned strip), false since. At
92+
* this seam insert remains deliberately exempt from the two AUTHOR-DECLARED
93+
* strips (#3413: an in-process create may seed a `readonly: true` field's
94+
* initial value, and `readonlyWhen` cannot lock anything on a create at
95+
* all), but the implicitly-readonly runtime-owned strip #5503 added runs on
96+
* insert too — and it is exactly the one strict refuses. An insert whose
97+
* payload carries a runtime-owned value (`RUNTIME_OWNED_FIELD_TYPES`, today
98+
* `autonumber` — a caller-supplied record number) behaves like update at
99+
* this seam: with this option `true` it throws `ReadonlyFieldRejectedError`
100+
* (`operation: 'insert'`) and nothing is written; without it the value is
101+
* stripped, the write completes, and `onFieldsDropped` fires with
102+
* `reason: 'readonly'`. The engine-level writers exempt from that strip —
103+
* and therefore never refused — are the two the error message itself names:
104+
* `isSystem`, and the `preserveAudit` historical import reinstating legacy
105+
* record numbers (#3493). Layer note: that exemption pair is THIS
106+
* in-process seam's. The DataProtocol ingress enforces its own
107+
* author-declared `readonly` policy on create (#3043), where
108+
* `preserveAudit` is UPDATE-only (#6640) — see `FieldSchema.readonly`;
109+
* nothing here widens or narrows it. `ReadonlyFieldRejectedError`'s own doc
110+
* records the same contract from the error's side: "Thrown by
111+
* `engine.update` — and, since #5503, by `engine.insert`".
86112
*/
87113
strictReadonlyWrites?: boolean;
88114
}

0 commit comments

Comments
 (0)