Repository navigation
Commit 03c12a5
fix(objectql)!: under strictReadonlyWrites, a write whose value a stored expression replaces is refused, and nothing is written (#25086)
Closes #25000
Clause-②: no (narrowing: under `strictReadonlyWrites`, a write whose
value a stored expression replaces is refused instead of reported; it
ships now under the amended 「甲」, #6015 `6303865025`)
## What changes
`strictReadonlyWrites` refuses every drop that `onFieldsDropped`
reports. Since the `stored_expression` reason landed (#24690, PR #24778
`7532ea3c08`), one reported drop was left out of the refusal: a caller
value for a stored (non-`formula`) field that declares an `expression`,
which the engine replaces with the computed value. Under strict such a
write completed and was only reported. The amended ruling 「甲」 (#6015
`6303865025`) ships the refusal in 18.x as a BREAKING minor, and triage
re-timed this card to 18.x (`6305854988`).
- **Insert (single, batch, `insertMany`).** The strict check in
`engine.insert` now judges `insertDrops`, the one event list that
includes the stored-expression replacement, after the compute pass.
Before, it judged only the strips' drops, ahead of that pass. A replaced
value throws `ReadonlyFieldRejectedError` (`operation: 'insert'`) before
validation and before any driver dispatch, and no listener fires.
`insertMany` delegates to `insert`, so it refuses the whole call, as it
does for the other strict drops.
- **Update by id and predicate update.** The `stored_expression` report
now goes through `emitDrop`, the channel every strip reports through,
and no longer calls `notifyDrop` directly. On both branches the one
`assertNoStrictDrops()` call now comes after the stored-expression pass.
So under strict the write is refused before the statement and nothing is
written. An object that declares no stored expression skips that pass,
so its refusal happens exactly where it did before.
- **One refusal names everything.** A payload that carries another
strip's drop as well as a replaced value is now refused once, naming
both, with `stored_expression` last. Before, it was refused for the
other drop alone.
- **Wording.** `REASON_PHRASE.stored_expression` (written by #24690
ahead of this refusal) is reused. The per-reason message gains a
`stored_expression` remedy (leave the field out, or send the computed
value). That remedy is spoken only when the reason is present, so every
other refusal's message is unchanged byte for byte, by the same rule the
`computed` remedy follows.
- **Docs.** The TSDoc in `packages/spec/src/contracts/data-engine.ts`
(`strictReadonlyWrites`) and `packages/spec/src/data/data-engine.zod.ts`
(`DroppedFieldsEvent.reason`) no longer names `stored_expression` as the
arm strict does not refuse. The objectql comments in `engine.ts` and
`readonly-strict-errors.ts` follow. No `.describe()` text moved:
`check:generated` reports all 14 artifacts current, so the generated
reference docs have nothing to regenerate.
**Unchanged:** a strict write that sends the computed value, or no
value, is not refused. A stored expression field that is also `readonly`
is still refused once, as `readonly`. Without strict, the write
completes with the computed value and is reported. The error code is
unchanged.
## Tests
`packages/objectql/src/engine-stored-expression-dropped-fields.test.ts`:
the strict block changes from "reported, not refused" to "refused,
nothing written". It covers update by id, insert single, batch,
`insertMany` and predicate update. Each refusal asserts the `code`,
`drops` (and `operation`/`fields` where they matter), that the store is
unchanged, and that the listener stayed silent. A non-strict control
sits beside each refusal (the write succeeds and is reported), together
with the strict controls for "computed value sent / no value". The union
case is also covered (`readonly_when` + `stored_expression` in one
refusal), and so is the `readonly`-only insert refusal.
All runs were taken on the final code commit `a8e9d3611c`:
- `pnpm --filter @objectstack/objectql test`: 455 files / 8569 tests
passed. `pnpm --filter @objectstack/objectql typecheck`: `tsc --noEmit`
clean, and `check:test-typecheck` OK (ledger held).
- `pnpm --filter @objectstack/spec build && check:generated`: all 14
generated artifacts up to date. `spec test`: 705 files / 21424 passed (1
todo). `spec typecheck`: OK. `spec test:repo`: 72 files / 1237 passed.
- `node scripts/pm/dispatch-gates.mjs --commands` derived 94 commands
from merge base `688052e14`, and all 94 exited 0. On the first pass,
`check:dual-build-cjs-loads` and `check:type-check-debt` printed
PREREQUISITE NOT MET (exit 3, nothing measured). I re-ran
`check:type-check-debt` under the verify lock: its closure build ran and
it re-measured OK, `none above its recorded number`. That full build
also produced the missing `dist/` trees, and
`check:dual-build-cjs-loads` then passed (112 entry points across 69
packages load). `dispatch-gates --ran` reconciles 94 derived, 94 run, 0
NOT-MEASURED, 0 UNRUN. The changeset gates are among the 94:
`check-adr-0087-registration`, `check-changeset-no-major`,
`check-empty-changeset`, `check:changeset-gate-self-tests`.
**Ablation.** The fix was committed first. Each leg went through
`scripts/ablation-replace.mjs`, with an anchor hit of 1 to 0 and the
blob proven back to the HEAD blob `3e8c0ec2bd1a` with `git diff HEAD`
empty. The suite imports `./engine.js` from source, so no build is
involved.
- A, update paths: `emitDrop` lets `stored_expression` past strict.
Three tests go red (update by id, predicate update, the union case), and
the other 22 stay green.
- B, insert: the strict check ignores a `stored_expression`-only list.
Three tests go red (insert single, batch, `insertMany`), and the other
22 stay green.
## Producers measured
No non-test caller in this repository passes `strictReadonlyWrites`: the
examples, `packages/qa` dogfood and the runtime and REST doors pass
none. The option is also not on the serializable options bag, so no wire
caller can set it. No in-repo producer moves. In the objectql suite, the
in-package strict tests on stored-expression objects
(`engine-stored-expression-write.test.ts`,
`engine-write-formula-hydration.test.ts`,
`engine-formula-write-strip.test.ts`) stay green unchanged.
## Changeset
`.changeset/25000-strict-stored-expression-refusal.md`:
`@objectstack/objectql` `minor`, with a BREAKING section that maps FROM
(completed, reported) TO (refused, nothing written), gives the remedy,
and carries the claim's `Clause-②` line and an ADR-0087 `not-required
(no-migration-prescription)` marker (the precedent shape of
`bb4f5cc005`). `@objectstack/spec` is not listed: only TSDoc comments
moved there, and no `.describe()` text or export did.
## Acceptance notes
- The release-owned 18.x upgrade checklist (`content/docs/releases/`) is
not edited here. The changeset carries the text that release compilation
reads.
- The pending `.changeset/24690-stored-expression-dropped-fields.md` (in
the open Version Packages PR) says strict "is unchanged" and that the
refusal is deferred to 19.0. That file is outside this card's claimed
file surface, so this PR leaves it alone. The new changeset says it
supersedes that bullet. The report names the one-line amendment for the
seat to decide.
- `content/docs/data-modeling/validation-rules.mdx` (the `expression` on
a stored field paragraph) still says a sent value is "not reported as
dropped", which has been false since #24690. That is a hand-written doc
outside this card's surface, and it is noted in the report.
Open PRs touching the same files were checked: #25072 and #24893 edit
`engine.ts` in disjoint hunks (lines 4628–6300 and 9043–12250). No open
PR touches `data-engine.zod.ts`, `contracts/data-engine.ts` or
`readonly-strict-errors.ts`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent ba244f1 commit 03c12a5
6 files changed
Lines changed: 285 additions & 96 deletions
File tree
- .changeset
- packages
- objectql/src
- spec/src
- contracts
- data
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
23 | | - | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
28 | 30 | | |
29 | 31 | | |
30 | 32 | | |
| |||
128 | 130 | | |
129 | 131 | | |
130 | 132 | | |
| 133 | + | |
| 134 | + | |
131 | 135 | | |
132 | 136 | | |
133 | 137 | | |
| |||
254 | 258 | | |
255 | 259 | | |
256 | 260 | | |
257 | | - | |
258 | | - | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
259 | 283 | | |
260 | | - | |
| 284 | + | |
261 | 285 | | |
262 | 286 | | |
263 | 287 | | |
264 | 288 | | |
265 | | - | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
266 | 306 | | |
267 | | - | |
| 307 | + | |
268 | 308 | | |
269 | 309 | | |
270 | 310 | | |
271 | 311 | | |
272 | 312 | | |
273 | 313 | | |
274 | | - | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
275 | 332 | | |
276 | | - | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
277 | 364 | | |
278 | 365 | | |
279 | 366 | | |
280 | 367 | | |
281 | | - | |
| 368 | + | |
282 | 369 | | |
283 | | - | |
284 | | - | |
285 | | - | |
286 | | - | |
287 | | - | |
288 | | - | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
289 | 376 | | |
290 | 377 | | |
291 | 378 | | |
292 | 379 | | |
293 | | - | |
| 380 | + | |
294 | 381 | | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
295 | 393 | | |
296 | | - | |
| 394 | + | |
297 | 395 | | |
298 | | - | |
299 | | - | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
300 | 405 | | |
301 | 406 | | |
302 | 407 | | |
| |||
0 commit comments