Skip to content

Commit 0c910d5

Browse files
fix(spec): field and object form expression help states what the engine does (#24803)
Closes #24735 Clause-②: no ## What The `expression` help text in the field designer (`packages/spec/src/data/field.form.ts`) said the expression "makes it read-only". The engine does not do that. The object designer's field grid (`packages/spec/src/data/object.form.ts`) showed the `expression` row for `formula` fields only, with the help text "CEL formula expression". Both rows now carry the same sentence, which matches the landed `FieldSchema.expression` describe: > CEL expression the field's value is computed from. On a formula field: computed on read, no stored column. On any other field: computed by the engine from the row each write stores, and stored, so a client-sent value never stands. Nothing changes in behaviour, and no gate is added. ## Engine behaviour the text was checked against (`origin/main` 46bb5e3) - `planStoredExpressions` (`packages/objectql/src/engine.ts`): every non-`formula` field that declares an `expression` is computed over the row the write stores and written onto the payload. A client-sent value is overwritten. - `stripComputedWriteFields`: strips `formula` fields only. A `formula` has no column and is computed on read. - `settleBulkStoredExpressions`: a predicate update that moves an input stores the single value when every matched row agrees, and refuses the update otherwise. - Only `readonly: true` makes a client value a readonly drop. The sentence makes no claim about whether the overwrite is reported. It stays true before and after the open draft for #24690, which would add a `stored_expression` dropped-field report. "A client-sent value never stands" holds either way. ## Grid row decision: widened to every type `FieldSchema` accepts `expression` on every field type: there is no type gate in `field.zod.ts`, and `planStoredExpressions` acts on any non-formula type. The field designer's own row has no `visibleWhen`. With the formula-only row, a stored field's live `expression` could not be seen or edited in the grid, while the field form showed it. I found no concrete reason to keep the narrower row, so the `visibleWhen` is removed and the help text matches `field.form.ts`. A comment at the row records why. ## Translations The zh-CN, ja-JP and es-ES values of both leaves made the same false claim, or the formula-only one. Two examples: zh-CN "自动设为只读" and es-ES "lo hace de solo lectura". AGENTS.md says translated-locale values are hand-written. The extractor's merge mode keeps every existing translated value, and authored leaves carry no source hash, so regenerating could not correct them. I re-translated the six leaves by hand. The `en` bundle was regenerated with `node scripts/check-i18n-bundles.mjs --write --filter=platform-objects`, which rewrote only `en.metadata-forms.generated.ts`. The en pin for `object.fields['fields.expression'].helpText` in `object-field-editor-panel-echo-decisions.test.ts` follows the new source, with its reason amended. ## Verification (head 28b565d) - `pnpm --filter @objectstack/spec build`: exit 0. Then `pnpm --filter @objectstack/spec check:generated`: "All 14 generated artifacts are up to date". - `pnpm --filter @objectstack/spec test`: 702 files / 21352 tests passed. `pnpm --filter @objectstack/spec typecheck`: exit 0. - `pnpm --filter @objectstack/platform-objects test`: 79 files / 1195 tests passed. `pnpm --filter @objectstack/platform-objects typecheck`: exit 0. - `pnpm check:i18n`: "OK (10 package(s) — all bundles in sync, no undeclared authoring keys)". `check:i18n-stale-fill`: OK. - `node scripts/pm/dispatch-gates.mjs --ran`: 88 derived families accounted for, 87 run at exit 0. NOT MEASURED: `check:dual-build-cjs-loads`. It exited 3 with PREREQUISITE NOT MET because unrelated workspace packages have no `dist/`. CI builds the whole workspace and runs it. - `pnpm lint` (repo-wide eslint) was not run locally and is left to CI. ## Acceptance notes - Nothing was driven in a browser. The objectui grid renders the row from this form metadata, and the only change it sees is that `visibleWhen` is gone. --- _Generated by [Claude Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1366740 commit 0c910d5

9 files changed

Lines changed: 37 additions & 18 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
Field and object designer forms: the `expression` help text now states what the engine does instead of claiming the expression "makes it read-only". On a `formula` field the expression is computed on read with no stored column; on any other field the engine computes it from the row each write stores and stores the result, so a client-sent value never stands (only `readonly: true` makes a client value a readonly drop). The object designer's field grid now shows the `expression` row for every field type, as the field designer already did, since `FieldSchema` accepts `expression` on any type. The zh-CN, ja-JP and es-ES translations of both rows are updated to match.

‎packages/lint/src/validate-predicate-path-refs.test.ts‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -682,7 +682,12 @@ describe('#7010 corpus — shipped METADATA_FORM_REGISTRY', () => {
682682
// It is 89 today, an ADDITION of ONE: #23735 offered the field form's
683683
// `referenceDeletedWhen` composite row, gated like `referenceVia` on
684684
// `data.type == 'text'`. It reaches the literal-comparison census below.
685-
expect(predicates, 'the shipped metadata forms carry no predicates at all').toBe(89);
685+
// It is 88 today, a REMOVAL of ONE: #24735 dropped the object form's
686+
// `object :: expression :: data.type == 'formula'` gate. `FieldSchema`
687+
// accepts `expression` on every type and the engine computes and stores it
688+
// on a non-formula field, so the formula-only row hid a live key; the row
689+
// is now shown for every type, as the field form's own row already was.
690+
expect(predicates, 'the shipped metadata forms carry no predicates at all').toBe(88);
686691

687692
const findings = validatePredicatePathRefs(corrupted);
688693
expect(findings).toHaveLength(predicates);
@@ -793,7 +798,9 @@ describe('#7010 corpus — shipped METADATA_FORM_REGISTRY', () => {
793798
// `data.type == 'email'`, a single-quoted literal comparison.
794799
// It is 66 today: #23735's `field :: referenceDeletedWhen` row is gated on
795800
// `data.type == 'text'`, a single-quoted literal comparison.
796-
expect(comparisons, 'no shipped predicate carries an `==`/`!=` literal comparison').toBe(66);
801+
// It is 65 today: #24735 removed the object form's `expression` gate,
802+
// `data.type == 'formula'`, one single-quoted literal comparison.
803+
expect(comparisons, 'no shipped predicate carries an `==`/`!=` literal comparison').toBe(65);
797804

798805
const rhsFindings = validatePredicatePathRefs(corrupted)
799806
.filter((f) => f.rule === PREDICATE_RHS_PATH_SHAPED);
@@ -822,7 +829,9 @@ describe('#7010 corpus — shipped METADATA_FORM_REGISTRY', () => {
822829
// tree adds over its merge base — the two corpora were enumerated and
823830
// differenced by `<form>::<field>::<source>`, and the object-form half of
824831
// the two-entry delta is exactly it. #6254's own measurement was 16 and
825-
// stays 16 — that number is history, this one is a census.
832+
// stays 16 — that number is history, this one is a census. It is 18 today:
833+
// #24735 removed the `expression` row's `data.type == 'formula'` gate, so
834+
// that row is shown for every field type.
826835
const objectForm = structuredClone(METADATA_FORM_REGISTRY.object) as Record<string, unknown>;
827836
let restored = 0;
828837
const debare = (node: unknown): void => {
@@ -852,10 +861,10 @@ describe('#7010 corpus — shipped METADATA_FORM_REGISTRY', () => {
852861
expect(
853862
restored,
854863
"the object form's `data.type`-rooted predicates are no longer where this test looks",
855-
).toBe(19);
864+
).toBe(18);
856865

857866
const findings = validatePredicatePathRefs({ views: [objectForm] });
858-
expect(findings).toHaveLength(19);
867+
expect(findings).toHaveLength(18);
859868
expect(new Set(findings.map((f) => f.rule))).toEqual(new Set([PREDICATE_PATH_UNROOTED]));
860869
expect(findings[0].message).toContain('`type`');
861870
});

‎packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
226226
},
227227
"fields.expression": {
228228
label: "Expression",
229-
helpText: "CEL formula expression"
229+
helpText: "CEL expression the field's value is computed from. On a formula field: computed on read, no stored column. On any other field: computed by the engine from the row each write stores, and stored, so a client-sent value never stands."
230230
},
231231
"fields.returnType": {
232232
label: "Return Type",
@@ -763,7 +763,7 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
763763
},
764764
expression: {
765765
label: "Expression",
766-
helpText: "CEL expression to calculate this field (makes it read-only)"
766+
helpText: "CEL expression the field's value is computed from. On a formula field: computed on read, no stored column. On any other field: computed by the engine from the row each write stores, and stored, so a client-sent value never stands."
767767
},
768768
returnType: {
769769
label: "Return Type",

‎packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export const esESMetadataForms: NonNullable<TranslationData['metadataForms']> =
226226
},
227227
"fields.expression": {
228228
label: "Expresión",
229-
helpText: "Expresión de fórmula CEL"
229+
helpText: "Expresión CEL con la que se calcula el valor del campo. En un campo de fórmula: se calcula al leer, sin columna almacenada. En cualquier otro campo: el motor lo calcula a partir de la fila que guarda cada escritura y lo almacena, por lo que un valor enviado por el cliente nunca prevalece."
230230
},
231231
"fields.returnType": {
232232
label: "Tipo de retorno",
@@ -763,7 +763,7 @@ export const esESMetadataForms: NonNullable<TranslationData['metadataForms']> =
763763
},
764764
expression: {
765765
label: "Expresión",
766-
helpText: "Expresión CEL para calcular este campo (lo hace de solo lectura)"
766+
helpText: "Expresión CEL con la que se calcula el valor del campo. En un campo de fórmula: se calcula al leer, sin columna almacenada. En cualquier otro campo: el motor lo calcula a partir de la fila que guarda cada escritura y lo almacena, por lo que un valor enviado por el cliente nunca prevalece."
767767
},
768768
returnType: {
769769
label: "Tipo de retorno",

‎packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export const jaJPMetadataForms: NonNullable<TranslationData['metadataForms']> =
226226
},
227227
"fields.expression": {
228228
label: "式",
229-
helpText: "CEL 数式"
229+
helpText: "このフィールドの値を計算する CEL 式。数式フィールドでは読み取り時に計算され、列は保存されません。その他のフィールドでは、書き込みごとに保存される行からエンジンが計算して保存するため、クライアントが送信した値は反映されません。"
230230
},
231231
"fields.returnType": {
232232
label: "戻り値の型",
@@ -763,7 +763,7 @@ export const jaJPMetadataForms: NonNullable<TranslationData['metadataForms']> =
763763
},
764764
expression: {
765765
label: "式",
766-
helpText: "このフィールドを計算する CEL 式(読み取り専用化)"
766+
helpText: "このフィールドの値を計算する CEL 式。数式フィールドでは読み取り時に計算され、列は保存されません。その他のフィールドでは、書き込みごとに保存される行からエンジンが計算して保存するため、クライアントが送信した値は反映されません。"
767767
},
768768
returnType: {
769769
label: "数式の戻り値型",

‎packages/platform-objects/src/apps/translations/object-field-editor-panel-echo-decisions.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,10 @@ const DECISIONS: readonly Decision[] = [
217217
type: 'object',
218218
key: 'fields.expression',
219219
prop: 'helpText',
220-
en: 'CEL formula expression',
220+
en: "CEL expression the field's value is computed from. On a formula field: computed on read, no stored column. On any other field: computed by the engine from the row each write stores, and stored, so a client-sent value never stands.",
221221
verdict: ALL_TRANSLATE,
222222
reason:
223-
'CEL is a language name and stays verbatim; the rest is prose. The formula vocabulary already exists on this very panel — returnType.helpText ("Result type for formulas") is authored in all three locales — so this leaf was simply never reached.',
223+
'CEL is a language name and stays verbatim; the rest is prose. The formula vocabulary already exists on this very panel — returnType.helpText ("Result type for formulas") is authored in all three locales — so this leaf was simply never reached. Re-translated when the row widened past `formula` to every type: the sentence is the field designer\'s own expression.helpText, authored in the same words in all three locales.',
224224
},
225225
{
226226
type: 'object',

‎packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export const zhCNMetadataForms: NonNullable<TranslationData['metadataForms']> =
226226
},
227227
"fields.expression": {
228228
label: "表达式",
229-
helpText: "CEL 公式表达式"
229+
helpText: "计算此字段值所用的 CEL 表达式。公式字段:读取时计算,不存储列。其他字段:由引擎根据每次写入所存储的行计算并存储,因此客户端提交的值不会生效。"
230230
},
231231
"fields.returnType": {
232232
label: "返回类型",
@@ -763,7 +763,7 @@ export const zhCNMetadataForms: NonNullable<TranslationData['metadataForms']> =
763763
},
764764
expression: {
765765
label: "表达式",
766-
helpText: "用 CEL 表达式计算此字段的值(自动设为只读)"
766+
helpText: "计算此字段值所用的 CEL 表达式。公式字段:读取时计算,不存储列。其他字段:由引擎根据每次写入所存储的行计算并存储,因此客户端提交的值不会生效。"
767767
},
768768
returnType: {
769769
label: "公式返回类型",

‎packages/spec/src/data/field.form.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ export const fieldForm = defineForm({
337337
collapsible: true,
338338
collapsed: true,
339339
fields: [
340-
{ field: 'expression', widget: 'textarea', helpText: 'CEL expression to calculate this field (makes it read-only)' },
340+
{ field: 'expression', widget: 'textarea', helpText: 'CEL expression the field\'s value is computed from. On a formula field: computed on read, no stored column. On any other field: computed by the engine from the row each write stores, and stored, so a client-sent value never stands.' },
341341
// The five members are what FieldSchema declares — an explicit list, not
342342
// the derived enum, so this row cannot pick up a member the formula
343343
// return type does not have. `currency` joined with #22727; its currency

‎packages/spec/src/data/object.form.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,8 +359,12 @@ export const objectForm = defineForm({
359359

360360
// Formula / summary
361361
// `expression`, not `formula` — the key is named for what it holds,
362-
// not for the field type that uses it.
363-
{ field: 'expression', type: 'code', language: 'expression', helpText: 'CEL formula expression', visibleWhen: "data.type == 'formula'" },
362+
// not for the field type that uses it. Shown for every type, as the
363+
// field designer's own row in `field.form.ts` is: `FieldSchema`
364+
// accepts `expression` on any type, and on a non-formula field the
365+
// engine computes and stores it on write (`planStoredExpressions`),
366+
// so a formula-only row hid a live key on a stored field.
367+
{ field: 'expression', type: 'code', language: 'expression', helpText: 'CEL expression the field\'s value is computed from. On a formula field: computed on read, no stored column. On any other field: computed by the engine from the row each write stores, and stored, so a client-sent value never stands.' },
364368
// The five members are what `FieldSchema.returnType` declares, and
365369
// the same explicit list the field designer's own control carries
366370
// in `field.form.ts` (#19677). This grid additionally offered

0 commit comments

Comments
 (0)