Skip to content

Commit 2e8e118

Browse files
os-muskclaude
andauthored
docs(engine): state the ruled readonly-on-INSERT contract at the seven non-published prose sites (#17020)
* docs(engine): state the ruled readonly-on-INSERT contract at the seven non-published prose sites The 2026-09-03 maintainer ruling (option C, #14147) put the static `readonly` strip inside `engine.insert` under the same `isSystem` gate as `engine.update` and deleted the metadata-protocol ingress copy. PR #15395 fixed every site that reached a published artefact; the 2026-09-05 E3 ruling carded the rest. These seven prose sites still stated the superseded contract in the present tense — that a non-system INSERT is exempt from the static strip, or that the strip lives at the DataProtocol create ingress. Each is rewritten to the ruled state, following the R3/R4 corrections in rest-server.ts, settings-service.ts and crud-nodes.ts: - objectql/src/readonly-strict-errors.ts — the module docblock's account of what the refusal union contains on INSERT (the `operation` TSDoc 190 lines below was already corrected in R4 and disagreed with it). - service-settings settings-secret-rotation.test.ts (header + a helper) and sys-secret-orphan-report.test.ts (a helper) — sys_setting's INSERT is still outside the strip, but by the platform-object carve-out in staticReadonlyInsertSubject (#15719), not by #3413. - rest/src/rest-batch-endpoint.test.ts — the create-ingress-parity comment; the routing it pins is unchanged, only the stated reason was superseded. - service-automation crud-dropped-fields.test.ts — the "future insert-side strip" is live. - spec/scripts/liveness/proof-registry.mts — the provenance comment on the readonly-static-write proof, which covers both write paths. Comments only: no behaviour change, no test deleted, skipped or reassigned, no exported symbol and no published payload key touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg * chore: add the patch changeset the readonly-INSERT prose diff was measured to need Measured, not assumed: the five touched packages were built before and after the edit and every dist file hashed, with `npm pack --dry-run` read for what ships. Two released packages publish changed bytes — @objectstack/objectql (4 sourcemaps) and @objectstack/rest (2 sourcemaps, because its bundle inlines objectql's source and the comment moves its line numbers). Every emitted .js / .mjs / .cjs and every .d.ts / .d.mts / .d.cts is byte-identical. skip-changeset is for a diff that publishes nothing from any released package, so it does not apply here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ba17017 commit 2e8e118

7 files changed

Lines changed: 68 additions & 25 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/objectql": patch
3+
"@objectstack/rest": patch
4+
---
5+
6+
Documentation only: seven in-source prose sites that still stated the superseded readonly-on-INSERT contract as live now state the ruled one.
7+
8+
The 2026-09-03 maintainer ruling (option C, #14147) put the static `readonly` strip inside `engine.insert` under the same `isSystem` gate as `engine.update`, and deleted the metadata-protocol create-ingress copy. Comments and test headers written before that ruling still said, in the present tense, that a non-system INSERT is exempt from the static strip, or that the strip lives at the DataProtocol create ingress. Each now states the ruled contract, and the superseded sentence is kept only as history, marked as superseded.
9+
10+
No behaviour changes and no test was deleted, skipped or re-scoped — the diff is comments only. It is a `patch` rather than `skip-changeset` because it was measured to publish: `@objectstack/objectql`'s comment edit moves source line numbers, so `dist/{index,core}.{js,mjs}.map` change, and `@objectstack/rest` inlines that same objectql source into its bundle, so `dist/index.{js,cjs}.map` change with it. Every emitted `.js` / `.mjs` / `.cjs` and every `.d.ts` / `.d.mts` / `.d.cts` is byte-identical before and after, and all six maps ship inside the published tarballs.

packages/objectql/src/readonly-strict-errors.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ import type { DroppedFieldsEvent } from '@objectstack/spec/data';
1818
* static `readonly` (#2948), a TRUE `readonlyWhen` predicate (#3042), the
1919
* implicitly-readonly runtime-owned types (#5503), and the `primary_key` strip
2020
* of a payload `id` the update dispatch ruled is not an identifier (#6437); on
21-
* INSERT only the runtime-owned ones, because a create is deliberately exempt
22-
* from the author-declared strips (#3413). One error names everything wrong
23-
* with the payload instead of forcing a round-trip per field. `drops`
24-
* keeps the per-reason breakdown (the same `DroppedFieldsEvent` shape
25-
* `onFieldsDropped` would have received, had the write been allowed to
26-
* complete), so a caller can tell a schema-level lock from a state-dependent
27-
* one without parsing the message.
21+
* INSERT the runtime-owned ones and, since the 2026-09-03 ruling (#14147)
22+
* superseded the create-side exemption #3413 had granted, static `readonly`
23+
* too — judged over `staticReadonlyInsertSubject`, which leaves a
24+
* `sys_`-prefixed or `managedBy` object to its own guards (#15719), while
25+
* `readonlyWhen` still locks nothing on a create (a conditional lock needs a
26+
* prior record). One error names everything wrong with the payload instead of
27+
* forcing a round-trip per field. `drops` keeps the per-reason breakdown (the
28+
* same `DroppedFieldsEvent` shape `onFieldsDropped` would have received, had
29+
* the write been allowed to complete), so a caller can tell a schema-level
30+
* lock from a state-dependent one without parsing the message.
2831
*
2932
* ## The message is composed from `drops`, not from the error's name (#6437)
3033
*

packages/rest/src/rest-batch-endpoint.test.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -305,11 +305,17 @@ describe('POST {basePath}/batch — cross-object transactional batch', () => {
305305

306306
// ── create ingress parity (#3835) ─────────────────────────────────────────
307307
//
308-
// The engine's INSERT path is static-`readonly`-exempt by design (#3413), so
309-
// the #3043 strip that stops a non-system caller from seeding a read-only
310-
// column lives at the protocol's create ingress. This route used to call
311-
// `ql.insert` directly and skip it, so `readonly` meant two different things
312-
// depending on which create endpoint you used.
308+
// When this was written the #3043 strip that stops a non-system caller from
309+
// seeding a read-only column lived at the protocol's create ingress, because
310+
// the engine's INSERT path was static-`readonly`-exempt (#3413). This route
311+
// used to call `ql.insert` directly and skip that ingress, so `readonly`
312+
// meant two different things depending on which create endpoint you used.
313+
// Since the maintainer ruling of 2026-09-03 (option C, #14147) the strip
314+
// runs inside `engine.insert` for every non-system caller and the ingress
315+
// copy is deleted, so both create routes are stripped identically. The
316+
// routing pinned below stands on what the ingress still owns: the #3770
317+
// object-existence gate, the #7823 `internal: true` response strip and the
318+
// `droppedFields` relay — one create ingress, one response contract.
313319

314320
it('routes create ops through the protocol create ingress, not ql.insert', async () => {
315321
const ql = makeQl();

packages/services/service-automation/src/builtin/crud-dropped-fields.test.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,14 @@ describe('update_record surfaces silently-stripped write fields (#3407)', () =>
142142
});
143143

144144
describe('create_record is wired symmetrically (#3407)', () => {
145-
// Today ObjectQL's insert path strips nothing (INSERT is readonly-exempt,
146-
// FLS write denial throws) — but the node listens anyway, so a future
147-
// insert-side strip surfaces instead of going silent.
145+
// #3407 wired this listener against an insert path that stripped nothing
146+
// (INSERT was readonly-exempt; FLS write denial throws), i.e. for a signal
147+
// it could not then receive. The maintainer ruling of 2026-09-03 (#14147)
148+
// put the static-`readonly` strip inside `engine.insert` under an
149+
// `isSystem` gate, so the signal is live: `crud-nodes.ts` relays it and
150+
// `create-record-readonly-drop.test.ts` drives it end to end. The cases
151+
// below feed the channel a synthetic drop, so they pin the node's relay
152+
// independently of which strips the engine runs.
148153
it('surfaces insert-side drop events as step warnings, keeping success', async () => {
149154
const engine = new AutomationEngine(makeLogger());
150155
const { data } = fakeDataWithDrops([

packages/services/service-settings/src/settings-secret-rotation.test.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@
99
* (`packages/platform-objects/src/system/sys-setting.object.ts`), and the
1010
* engine strips author-declared read-only columns from a **non-system**
1111
* caller's UPDATE payload (`stripReadonlyFields`, gated on
12-
* `if (!opCtx.context?.isSystem)` in `packages/objectql/src/engine.ts`). The
13-
* INSERT path is deliberately exempt (#3413).
12+
* `if (!opCtx.context?.isSystem)` in `packages/objectql/src/engine.ts`). On
13+
* THIS object the INSERT path is outside that strip — not by the 2026-07-24
14+
* "INSERT exempt" row (superseded by the 2026-09-03 ruling, #14147:
15+
* `engine.insert` runs the same strip for a non-system caller) but because
16+
* `sys_setting` is `sys_`-prefixed and `managedBy: 'engine-owned'`, which
17+
* `staticReadonlyInsertSubject` leaves to the platform object's own guards
18+
* while the UPDATE path applies no such carve-out (#15719).
1419
*
1520
* `SettingsService` wrote its rows through a plain, un-elevated
1621
* `engine.update`, so:
@@ -186,9 +191,14 @@ function makeMemoryDriver() {
186191
* That single omission is the whole hazard — `sys_setting.value_enc` is
187192
* declared `readonly: true` and the engine strips author-declared read-only
188193
* columns from a NON-system caller's UPDATE (`stripReadonlyFields`, gated on
189-
* `context.isSystem`), while the INSERT path is exempt (#3413). It is a
190-
* documented extension point, so the population that reaches it is real:
191-
* third-party adapter authors, who have no other discovery path.
194+
* `context.isSystem`), while on THIS object the INSERT path is outside that
195+
* strip — not by the superseded #3413 exemption (the 2026-09-03 ruling,
196+
* #14147, put the same strip inside `engine.insert` for every non-system
197+
* caller) but because `sys_setting` is `sys_`-prefixed and
198+
* `managedBy: 'engine-owned'`, which `staticReadonlyInsertSubject` leaves to
199+
* the platform object's own guards (#15719). It is a documented extension
200+
* point, so the population that reaches it is real: third-party adapter
201+
* authors, who have no other discovery path.
192202
*/
193203
function wrapEngineDroppingContext(engine: any): SettingsEngine {
194204
const real = wrapEngineAsSettingsEngine(engine);

packages/services/service-settings/src/sys-secret-orphan-report.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,15 @@ function makeMemoryDriver() {
187187
* That single omission is the whole defect — `sys_setting.value_enc` is
188188
* declared `readonly: true` and the engine strips author-declared read-only
189189
* columns from a NON-system caller's UPDATE (`stripReadonlyFields`, gated on
190-
* `context.isSystem`), while the INSERT path is exempt (#3413). It is also
191-
* exactly the breakage `SettingsEngine`'s doc comment warns an adapter author
192-
* about ("⛔ An adapter over `IDataEngine` MUST forward this"), so reproducing
193-
* the orphan mechanism and pinning that warning are the same measurement.
190+
* `context.isSystem`), while on THIS object the INSERT path is outside that
191+
* strip — not by the superseded #3413 exemption (the 2026-09-03 ruling,
192+
* #14147, put the same strip inside `engine.insert` for every non-system
193+
* caller) but because `sys_setting` is `sys_`-prefixed and
194+
* `managedBy: 'engine-owned'`, which `staticReadonlyInsertSubject` leaves to
195+
* the platform object's own guards (#15719). It is also exactly the breakage
196+
* `SettingsEngine`'s doc comment warns an adapter author about ("⛔ An adapter
197+
* over `IDataEngine` MUST forward this"), so reproducing the orphan mechanism
198+
* and pinning that warning are the same measurement.
194199
*/
195200
function wrapEngineDroppingContext(engine: any): SettingsEngine {
196201
const real = wrapEngineAsSettingsEngine(engine);

packages/spec/scripts/liveness/proof-registry.mts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,11 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [
153153
bound: true,
154154
// `readonly` was renderer-only until #2948 — declared ≠ enforced is exactly
155155
// the false-compliance class ADR-0049 closes. The proof pins the server-side
156-
// strip (forge dropped, sibling edit lands, insert exempt) over real HTTP.
156+
// strip over real HTTP on BOTH write paths — a forged create is dropped, a
157+
// forged PATCH is dropped, the sibling edit lands. Since the maintainer
158+
// ruling of 2026-09-03 (option C, #14147) `engine.insert` runs the same
159+
// `stripReadonlyFields` under the same `isSystem` gate as UPDATE, and the
160+
// #3043 protocol-ingress copy is deleted.
157161
ledgerBindings: [{ type: 'field', path: 'readonly' }],
158162
},
159163
{

0 commit comments

Comments
 (0)