Repository navigation
Commit 4a2ca57
docs(data-modeling): field-types and validation-rules say what
Fixes #24716
Clause-②: no
## What changes
Docs only: two hand-written data-modeling pages, 11 lines added, 0
removed. No package code, no generated page, no gate.
- `content/docs/data-modeling/field-types.mdx`: a short subsection,
`expression` on a stored field, under `### formula`, with one example.
- `content/docs/data-modeling/validation-rules.mdx`: one paragraph under
its `### formula` section, linking to that subsection.
Both pages use the terms of the landed `FieldSchema.expression` describe
(`packages/spec/src/data/field.zod.ts:1840`), and both link to the
generated
[`FieldSchema`](https://github.com/objectstack-ai/objectstack/blob/main/content/docs/references/data/field.mdx)
reference instead of restating it. The describe is byte-identical
between `d20bf9b09b` (the dispatch's reading) and this branch's base
`ec2694dc19`, and PR #24673 (`c0803b04d2`) is the last commit to touch
it.
## The two passages
### `field-types.mdx`
Before: the page's only `expression` text is the `formula` table row
(`:411`, "CEL calculation expression") and the `formula` example
(`:415`).
After: inserted between the `formula` example and `### summary` (now
`:418`–`:425`):
```mdx
#### `expression` on a stored field
`expression` is not only a `formula` key. On a field of any other type, the engine computes it from the row each write stores and **stores** the result in the field's column, so a `summary` can roll the column up. It is computed on every insert and every update by id (the stored row merged with the patch). On a predicate update it is computed only when the patch moves an input: one value is stored for every matched row, and the update is refused (`VALIDATION_FAILED`, an `invalid_value` error on the field) when the rows would compute different values. Update those rows by id instead. A value a client sends for the field never stands: it is overwritten by the computed one, with no refusal and no dropped-field report. The full contract is the `expression` row of [`FieldSchema`](/docs/references/data/field).
// Stored, so a parent `summary` can sum it. A client-sent amount is overwritten.
Field.currency({ label: 'Amount', expression: 'record.quantity * record.unit_price' })
```
On the page, the example is a `typescript` fence.
### `validation-rules.mdx`
Before: `:354` ("**Required.** CEL formula expression"), `:357`
("Read-only. Value computed at runtime from `expression`. Not directly
writable.") and the `:552` row (`formula` → `expression`). All three
describe a `formula` only.
After: inserted between the `formula` example and `### summary` (now
`:368`):
```mdx
**`expression` on a stored field.** A field of any other type may declare `expression` too, and it validates differently from a `formula`. The engine computes the value on every insert and every update by id, and stores it. A value a client sends for the field is not refused and not reported as dropped: it is overwritten by the computed one. The one refusal is a predicate update that moves an input: when the matched rows would compute different values, the update is refused with `VALIDATION_FAILED` and an `invalid_value` error naming the field, and nothing is written. See [`expression` on a stored field](/docs/data-modeling/field-types#expression-on-a-stored-field) and the `expression` row of [`FieldSchema`](/docs/references/data/field).
```
The existing sentences (`field-types.mdx:407`–`:415`,
`validation-rules.mdx:354`, `:357`, `:552`) are not rewritten. Each sits
under a `formula` heading or in the `formula` row, and each page now
states the stored meaning right beside it, so none of them reads as
"only `formula` has an expression".
## Measured facts (engine and spec at base `ec2694dc19`)
**1. Is a stored field with an `expression` read-only to a client
write?** No. A client-sent value is overwritten, not refused, and not
reported.
- Insert (single, batch, `insertMany`) and update by id: the computed
value is written over the caller's key, and the write succeeds. No
`onFieldsDropped` event fires, so there is no `droppedFields` entry, and
`strictReadonlyWrites` does not refuse it. Two engine facts show why:
- The only computed-field strip, `stripComputedWriteFields`
(`packages/objectql/src/engine.ts:2274`), selects `def?.type ===
'formula'` only.
- `RUNTIME_OWNED_FIELD_TYPES` is `['autonumber']` (`field.zod.ts:291`).
- Pinned in
`packages/objectql/src/engine-stored-expression-write.test.ts`:
- `:142`–`:152`: `amount` 2999.4 is sent and 299.94 stored, and the
events equal only `[quantity] readonly_when`.
- `:173`–`:176`: a wrong amount is overwritten.
- `:183`–`:188`: on insert, none, a wrong and the right amount each
store the product.
- Predicate update: `settleBulkStoredExpressions` (`engine.ts:2979`)
deletes the caller's key from the patch.
- It sets the column only when the patch moves it, to one value on every
matched row.
- It throws `ValidationError` (`VALIDATION_FAILED`, field error
`invalid_value`) before any write when the computed values would differ
between rows.
- Pinned at test `:251`–`:271`.
- Only when the field also declares `readonly: true` does an update by
id report the caller's value as a `readonly` drop, and refuse it under
`strictReadonlyWrites` (test `:229`–`:247`). The pages do not say this:
it is beyond the describe and the card's ask.
**2. Which field types may carry a stored `expression`?** Every type
other than `formula`. Nothing narrows the type.
- Engine: `planStoredExpressions` (`engine.ts:2921`) plans every field
with `def.type !== 'formula' && def.expression`. It has no type list.
- Spec: `FieldSchema.safeParse` of each `FieldType` member with
`expression: 'record.quantity * record.unit_price'` accepts all 49 and
refuses 0. This was a one-off probe with tsx over `packages/spec/src` at
the base, and it is not kept as a file.
- Lint: the field-formula pass (`judgeFieldFormula`,
`packages/lint/src/validate-expressions.ts:2873`) judges any field's
`expression` as a record-scoped CEL `value`, with no type condition.
So both pages say "a field of any other type".
## The dispatch's two mechanism hypotheses
1. **The docs gates and a stored-field example.**
`check:doc-formula-expressions` admits a fenced site by structure only.
Rule A is a `Field.*({ expression })` call. Rule B is a literal carrying
`type: 'formula'`. To measure it, the gate's own
`extractFormulaExpressions` / `looksLikeFormulaBlock` / `judge` were
imported from a temporary truncated copy, deleted after the run:
- A raw literal is not admitted and trips no tripwire, even with bare
references. The input was `{ name: 'amount', label: 'Amount', type:
'currency', expression: 'quantity * unit_price' }`, and the probe read
admitted 0, tripwire false. It would pass unjudged.
- `Field.currency({ label: 'Amount', expression: 'quantity * unit_price'
})` is admitted and judged red (1 error).
- So the example uses the `Field.currency(...)` spelling, the one both
example apps write for this field. The gate judges it: `field-types.mdx`
sites went from 1 at base to 2 at head, the new one at `:424`, with 0
errors.
- `check:doc-authoring` is green on it.
2. **Is `validation-rules.mdx` the wrong page?** Not for the validation
half. Its `formula` section states the validation behaviour ("Read-only
… Not directly writable."), and a stored field differs exactly there.
Its value is overwritten, not refused, and the one refusal is the
predicate update. So that page's paragraph carries the validation
behaviour and points to `field-types.mdx` for the rest. The card's
two-page acceptance is kept.
## Verification (head `9dc3aba0af`, every gate run after the final
commit)
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 40 commands from the 2-path change
set against merge base `ec2694dc1`.
- All 40 exit 0. On the first pass, 5 refused before measuring:
- `check:doc-formula-expressions`, `check:doc-security-posture`,
`check:skill-examples` and `check:docs-transcript-drift` exited 3,
PREREQUISITE NOT MET, because packages were unbuilt.
- Spec `check:docs` exited 1, its own refusal on the missing
`gen:schema` tree.
- Two builds ran under `scripts/pm/os-verify-lock.sh`: `turbo run build
--filter='@objectstack/lint...'`, then
`--filter='@objectstack/client-react...'
--filter='@objectstack/client...'`. Both read `VERDICT command-exit 0`.
All five then re-ran to exit 0.
- `--ran`: "✓ dispatch-gates --ran: 40 derived famil(ies) accounted for
— 40 run, 0 NOT-MEASURED (a DERIVED zero — all 40 recorded an exit code
and none of them is 3)."
- Verdict lines:
- `check:doc-formula-expressions`: "23 record-scoped formula example(s)
across 476 files / 1394 TS blocks judged clean by @objectstack/formula."
- `check:doc-anchors`: "539 internal #fragment link(s) across 423 source
file(s) all resolve to a real heading". This includes the new
`#expression-on-a-stored-field` link.
- Spec `check:docs`: "227 generated files in sync with packages/spec".
- `check:skill-examples`: "262 prose examples type-check across 3
surface(s)".
- `check:nul-bytes`: "OK … no raw ASCII control bytes".
- `skip-changeset`: `content/` is in the workflow's "releases nothing"
set. Of the 73 published `package.json` files, none lists `content/` in
`files[]`, and no build script copies `content/docs/data-modeling/` into
a package.
- Not run locally: the CI-only families (the type-check lanes and the
`Test Core` shards). They are CI's.
## Acceptance notes
- `packages/spec/src/data/object.form.ts:363`: the object designer's
field grid shows `expression` only when `data.type == 'formula'`, with
help text "CEL formula expression". A stored `expression` is therefore
neither shown nor editable in that grid. The field designer,
`field.form.ts:340`, shows `expression` for every type, with help text
"CEL expression to calculate this field (makes it read-only)". Under the
engine the value is not client-settable, but the field is not
`readonly`: a sent value is neither reported nor refused. Neither form
was driven in a browser. This is an observation, not filed. Carrier:
none.
- `check:doc-formula-expressions` does not admit a raw non-`formula`
literal carrying `expression`, so a raw stored-expression example with
bare references would pass unjudged (measured above). The present
population is 0. A one-line grep of `content/` and `skills/` (generated
references excluded) for a `type:` literal beside `expression:` finds 12
lines, all `formula` or cron. This is an observation. No gate is added,
per the dispatch ruling.
- `origin/main` moved two commits past the base (`0904d856e3`,
`d0514e6863`). Neither touches these pages, the describe or the engine,
so the branch was not merged.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp)_
Co-authored-by: Claude <noreply@anthropic.com>expression means on a stored field, beside its formula meaning (#24734)1 parent b27f8de commit 4a2ca57
2 files changed
Lines changed: 11 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
415 | 415 | | |
416 | 416 | | |
417 | 417 | | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
418 | 427 | | |
419 | 428 | | |
420 | 429 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
365 | 365 | | |
366 | 366 | | |
367 | 367 | | |
| 368 | + | |
| 369 | + | |
368 | 370 | | |
369 | 371 | | |
370 | 372 | | |
| |||
0 commit comments