Repository navigation
docs(data-modeling): field-types and validation-rules say what expression means on a stored field, beside its formula meaning - #24734
Conversation
…eside its `formula` meaning field-types.mdx gains a short subsection under `formula`, and validation-rules.mdx a short paragraph under its `formula` section: on a field of any other type the engine computes the declared expression on every insert and every update by id and stores it, a predicate update computes it only when the patch moves an input and is refused when the matched rows would compute different values, and a client-sent value is overwritten, not refused or reported. Both link to the generated FieldSchema reference. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude <noreply@anthropic.com>
Seat reviewACCEPT ·
This seat readies the PR and queues it with auto-merge. Generated by Claude Code |
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,expressionon a stored field, under### formula, with one example.content/docs/data-modeling/validation-rules.mdx: one paragraph under its### formulasection, linking to that subsection.Both pages use the terms of the landed
FieldSchema.expressiondescribe (packages/spec/src/data/field.zod.ts:1840), and both link to the generatedFieldSchemareference instead of restating it. The describe is byte-identical betweend20bf9b09b(the dispatch's reading) and this branch's baseec2694dc19, and PR #24673 (c0803b04d2) is the last commit to touch it.The two passages
field-types.mdxBefore: the page's only
expressiontext is theformulatable row (:411, "CEL calculation expression") and theformulaexample (:415).After: inserted between the
formulaexample and### summary(now:418–:425):On the page, the example is a
typescriptfence.validation-rules.mdxBefore:
:354("Required. CEL formula expression"),:357("Read-only. Value computed at runtime fromexpression. Not directly writable.") and the:552row (formula→expression). All three describe aformulaonly.After: inserted between the
formulaexample and### summary(now:368):The existing sentences (
field-types.mdx:407–:415,validation-rules.mdx:354,:357,:552) are not rewritten. Each sits under aformulaheading or in theformularow, and each page now states the stored meaning right beside it, so none of them reads as "onlyformulahas an expression".Measured facts (engine and spec at base
ec2694dc19)1. Is a stored field with an
expressionread-only to a client write? No. A client-sent value is overwritten, not refused, and not reported.insertMany) and update by id: the computed value is written over the caller's key, and the write succeeds. NoonFieldsDroppedevent fires, so there is nodroppedFieldsentry, andstrictReadonlyWritesdoes not refuse it. Two engine facts show why:stripComputedWriteFields(packages/objectql/src/engine.ts:2274), selectsdef?.type === 'formula'only.RUNTIME_OWNED_FIELD_TYPESis['autonumber'](field.zod.ts:291).packages/objectql/src/engine-stored-expression-write.test.ts::142–:152:amount2999.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.settleBulkStoredExpressions(engine.ts:2979) deletes the caller's key from the patch.ValidationError(VALIDATION_FAILED, field errorinvalid_value) before any write when the computed values would differ between rows.:251–:271.readonly: truedoes an update by id report the caller's value as areadonlydrop, and refuse it understrictReadonlyWrites(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 thanformula. Nothing narrows the type.planStoredExpressions(engine.ts:2921) plans every field withdef.type !== 'formula' && def.expression. It has no type list.FieldSchema.safeParseof eachFieldTypemember withexpression: 'record.quantity * record.unit_price'accepts all 49 and refuses 0. This was a one-off probe with tsx overpackages/spec/srcat the base, and it is not kept as a file.judgeFieldFormula,packages/lint/src/validate-expressions.ts:2873) judges any field'sexpressionas a record-scoped CELvalue, with no type condition.So both pages say "a field of any other type".
The dispatch's two mechanism hypotheses
check:doc-formula-expressionsadmits a fenced site by structure only. Rule A is aField.*({ expression })call. Rule B is a literal carryingtype: 'formula'. To measure it, the gate's ownextractFormulaExpressions/looksLikeFormulaBlock/judgewere imported from a temporary truncated copy, deleted after the run:{ 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).Field.currency(...)spelling, the one both example apps write for this field. The gate judges it:field-types.mdxsites went from 1 at base to 2 at head, the new one at:424, with 0 errors.check:doc-authoringis green on it.validation-rules.mdxthe wrong page? Not for the validation half. Itsformulasection 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 tofield-types.mdxfor 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/objectstackderived 40 commands from the 2-path change set against merge baseec2694dc1.check:doc-formula-expressions,check:doc-security-posture,check:skill-examplesandcheck:docs-transcript-driftexited 3, PREREQUISITE NOT MET, because packages were unbuilt.check:docsexited 1, its own refusal on the missinggen:schematree.scripts/pm/os-verify-lock.sh:turbo run build --filter='@objectstack/lint...', then--filter='@objectstack/client-react...' --filter='@objectstack/client...'. Both readVERDICT 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)."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-fieldlink.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 publishedpackage.jsonfiles, none listscontent/infiles[], and no build script copiescontent/docs/data-modeling/into a package.Test Coreshards). They are CI's.Acceptance notes
packages/spec/src/data/object.form.ts:363: the object designer's field grid showsexpressiononly whendata.type == 'formula', with help text "CEL formula expression". A storedexpressionis therefore neither shown nor editable in that grid. The field designer,field.form.ts:340, showsexpressionfor 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 notreadonly: 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-expressionsdoes not admit a raw non-formulaliteral carryingexpression, so a raw stored-expression example with bare references would pass unjudged (measured above). The present population is 0. A one-line grep ofcontent/andskills/(generated references excluded) for atype:literal besideexpression:finds 12 lines, allformulaor cron. This is an observation. No gate is added, per the dispatch ruling.origin/mainmoved 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