Repository navigation
Commit 82cfaf7
docs(data-modeling,kernel): a value a stored expression replaces is reported under stored_expression and refused under strictReadonlyWrites (#25172)
Fixes #25089
Clause-②: no
## What changed
Hand-written docs only. Two pages said that a value a client sends for a
stored field that declares `expression` is neither refused nor reported
as dropped. Both halves are false on `main` (`b93c750c7`):
- **Reported.** objectql reports the replaced value through the engine's
`onFieldsDropped` listener under `reason: 'stored_expression'`. The data
API answers that as `droppedFields`. This landed with PR #24778
(`7532ea3c08`, an ancestor of the base: `merge-base --is-ancestor` exit
0).
- **Refused under strict.** Since PR #25086 (`03c12a52ef`, an ancestor:
exit 0), an in-process write that passes `strictReadonlyWrites: true` is
refused with `ERR_READONLY_FIELD_REJECTED`, and nothing is written.
| Page | Before | After |
|:---|:---|:---|
| `content/docs/data-modeling/validation-rules.mdx` (`:368`) | "is not
refused and not reported as dropped" | The value never stands. A value
that differs from the computed one is reported under
`stored_expression`. Sending the computed value, or no value, reports
nothing. Under the in-process `strictReadonlyWrites` option the write is
refused with `ERR_READONLY_FIELD_REJECTED`, and a request body cannot
set that option. "The one refusal" (the predicate update) now reads
"Without it, the one refusal". |
| `content/docs/data-modeling/field-types.mdx` (`:424`, the `expression`
on a stored field section) | "with no refusal and no dropped-field
report" | The same correction. This page carried the same stale claim as
`validation-rules.mdx`; both were written by `4a2ca57e7`. |
| `content/docs/kernel/contracts/data-engine.mdx` | The strip table had
no `stored_expression` row, and the `DroppedFieldsEvent.reason` union
listed four arms | A `stored_expression` row in the existing columns,
with Verbs `insert` and `update` and Writers it skips "none — every
context, `isSystem` included". One sentence follows the table: the row
is a replacement, not a removal. `'stored_expression'` is added to the
`reason` union in the marked (type-checked) block, matching
`DroppedFieldsEvent.reason` in
`packages/spec/src/data/data-engine.zod.ts`. |
| `content/docs/permissions/system-context.mdx` (row 22, "Strict-drop
refusal never fires" under `isSystem`) | "One exception: a `formula`
value ... so strict still refuses it" | "Two exceptions": the `formula`
value and the `stored_expression` replacement. Under `isSystem`, strict
also refuses the replacement, so "one exception" is the same stale "not
refused" claim in another place. |
## Evidence: source reads at `b93c750c7`
- **Report.** `packages/objectql/src/engine.ts` `droppedFieldEvents`
pushes `reason: 'stored_expression'`. `replacedStoredExpressionFields`
reports a field only when the caller sent it and the stored value
differs from it, so sending the computed value reports nothing. The
update by id and the predicate update both call `emitDrop(...,
'stored_expression')`. `packages/metadata-protocol/src/protocol.ts`
forwards every `onFieldsDropped` event as `droppedFields` from
`createData`, `cloneData`, `updateData`, `createManyData`,
`insertManyData` and `updateManyData`.
`packages/rest/src/rest-stored-expression-dropped-fields.test.ts` pins
the PATCH and `/batch` doors.
- **Refusal.** The insert path throws `ReadonlyFieldRejectedError` when
`insertDrops`, which include the replacement, is non-empty and
`options?.strictReadonlyWrites === true`. On update, `emitDrop` pushes
into `strictDrops` and `assertNoStrictDrops` throws. Neither path checks
`isSystem`, and `engine-stored-expression-dropped-fields.test.ts` pins
the `isSystem` report. The code is `READONLY_FIELD_REJECTED_CODE =
'ERR_READONLY_FIELD_REJECTED'`
(`packages/objectql/src/readonly-strict-errors.ts`), registered in
`ERROR_CODE_LEDGER`.
- **No HTTP status.** No package source passes `strictReadonlyWrites:
true`, and the option is not in the serializable options bag. The
`packages/spec/src/contracts/data-engine.ts` section "In-process only —
what a REMOTE caller observes" says so. For that reason the pages give
no HTTP status for this refusal.
## Sites checked and left unchanged
- `git grep stored_expression` over hand-written `content/docs` (not
`references/`, not `releases/`) has zero hits before this change. As a
control, `strictReadonlyWrites` has 4 hits on `data-engine.mdx`.
- A whole-tree `git grep` for the stale phrasings ("not reported as
dropped", "no dropped-field report", "not refused and not", "no refusal
and no") hits only the two pages above, plus unrelated subjects:
hook-stamped fields, nested views and similar.
- `FieldSchema.expression` (`packages/spec/src/data/field.zod.ts`) says
"a client-sent value never stands", which is accurate. The
`engine.ts:3036` docblock is accurate too.
- `data-engine.mdx` already says, in its strict section, that every row
in the table is refused and that a new `reason` adds a refusal. With the
new row that sentence is true as written, so it is not changed.
## Gates
These were run locally after the last commit, at head `b7a6d9449` (merge
base `b93c750c7`). The gate list comes from `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` with no paths, which derives 44 commands.
- All 44 exited 0. Exit codes were captured before any pipe.
- Reconciliation reads: `dispatch-gates --ran: 44 derived famil(ies)
accounted for — 44 run, 0 NOT-MEASURED (a DERIVED zero — all 44 recorded
an exit code and none of them is 3)`.
- The first pass of four gates exited 3 (`PREREQUISITE NOT MET`) because
the packages they read were not built: `check:doc-formula-expressions`,
`check:doc-security-posture`, `check:docs-transcript-drift` and
`check:skill-examples`. No measurement was taken in those runs. After
building `@objectstack/spec`, the `@objectstack/lint` closure,
`@objectstack/formula` and the `@objectstack/client-react` closure, all
four were re-run and exited 0.
- Verdict lines include:
- `check-doc-anchors: 570 internal #fragment link(s) ... all resolve to
a real heading`, which covers the two new `#writeobservabilityoptions`
links.
- `check:skill-examples: 262 prose examples type-check across 3
surface(s)`, which covers the edited marked block on `data-engine.mdx`.
- `check:docs: 227 generated files in sync`.
- `check-system-context-census: OK`.
- `check-nul-bytes: OK`.
- No package build is owed by the diff itself; the builds above were
only for gates that read `dist/`.
## Changeset
`skip-changeset`. The diff touches only `content/docs/**` (not
`releases/`). No published package's `files[]` includes a `content/`
path or a path outside the package. Every non-private `package.json` was
read, and `@objectstack/objectql` ships `dist`, `README.md` and
`CHANGELOG.md`. The docs site `@objectstack/docs` is `private: true`.
The generators that name `content/docs` write into it and read nothing
from these pages.
## Acceptance notes
- **Out of scope, reported to the seat to file** (published skill text,
a governed surface).
`skills/objectstack-data/rules/relationships.md:296` says the server
treats only `type: 'formula'` as computed. It goes on: on a stored field
the `expression` is a client compute hint and the sent value is stored
verbatim. That contradicts `FieldSchema.expression` ("a client-sent
value never stands") and the engine, which computes and stores the value
on every write. The text ships in `@objectstack/skills`. It is not
changed here because `skills/**` is Tier H.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01T1Y1g92DfSEz93iuCisju4)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent c24203f commit 82cfaf7
4 files changed
Lines changed: 10 additions & 5 deletions
File tree
- content/docs
- data-modeling
- kernel/contracts
- permissions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
421 | 421 | | |
422 | 422 | | |
423 | 423 | | |
424 | | - | |
| 424 | + | |
425 | 425 | | |
426 | 426 | | |
427 | 427 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
365 | 365 | | |
366 | 366 | | |
367 | 367 | | |
368 | | - | |
| 368 | + | |
369 | 369 | | |
370 | 370 | | |
371 | 371 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
346 | 346 | | |
347 | 347 | | |
348 | 348 | | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
349 | 354 | | |
350 | 355 | | |
351 | 356 | | |
| |||
361 | 366 | | |
362 | 367 | | |
363 | 368 | | |
364 | | - | |
| 369 | + | |
365 | 370 | | |
366 | 371 | | |
367 | | - | |
| 372 | + | |
368 | 373 | | |
369 | 374 | | |
370 | 375 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
128 | | - | |
| 128 | + | |
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
| |||
0 commit comments