|
| 1 | +--- |
| 2 | +"@objectstack/formula": minor |
| 3 | +"@objectstack/lint": minor |
| 4 | +"@objectstack/objectql": patch |
| 5 | +--- |
| 6 | + |
| 7 | +fix(formula,lint)!: a formula whose `label()` names a field that declares no options is refused at author time |
| 8 | + |
| 9 | +`label(record.FIELD)` answers on a field whose definition carries an `options` list; on any other field the evaluator faults on every row, so the formula read `null` forever with a WARN line as the only trace. The `expression-invalid` gate refused `label(record.nope)` (unknown field) and `sqrt(4)` (unregistered function) but admitted `label(record.first)` on a text field. It now refuses that too, at `os validate`, `os build`, `os lint` and every door that runs the active authoring gate (the object save door, per-item publish, history restore and a draft's `_diagnostics`; one evaluation, `runRuntimeAuthoringRules`), in the evaluator's own words (the same sentence its WARN line carries), naming the field and the remedy. |
| 10 | + |
| 11 | +Clause-②: no (narrowing) — the object save door and the CLI authoring commands refuse a formula whose `label()` argument names a declared field that declares no options, which they stored before. Nothing is widened. |
| 12 | + |
| 13 | +Metadata-protocol: narrowed (a formula field's `expression` may no longer call `label()` on a field that declares no `options`) |
| 14 | + |
| 15 | +<!-- adr-0087: not-required (no-migration-prescription) No key is added, removed or renamed. The refused value never worked: the evaluator faults on that call on every row, so a stored formula of this shape reads null for every reader today, and no conversion could give it a label to read. The refusal names the field and the remedy (read the field directly, or point label() at a field that declares options). --> |
| 16 | + |
| 17 | +**BREAKING** accept-set narrowing at the authoring gate (`@objectstack/formula`, `@objectstack/lint`), shipped as `minor`. |
| 18 | + |
| 19 | +- **Before.** `PUT /api/v1/meta/object/NAME` with a formula `label(record.first)` (`first` a text field), or `label(record.kind) + label(record.first)`, answered 200; every row then read the formula as `null`. |
| 20 | +- **Now.** The same write answers 422 `expression-invalid` naming `first`, located at the formula field's `expression`; `os validate` / `os build` / `os lint` report the same finding. |
| 21 | +- **Unchanged.** `label()` on a field that declares options is admitted and evaluates as before: inline `options` on any type (`select`, `multiselect`, `radio`, …), or a `picklist` naming a shared list, which the registry resolves onto the served field. The unknown-field and unregistered-function refusals are unchanged and an unknown field gets no second refusal. |
| 22 | +- **Who sees it.** An author whose formula calls `label()` on a field without options, a formula that already reads null on every row. |
| 23 | + |
| 24 | +**The remedy.** Read the field directly (`record.first`), or point `label()` at a field that declares `options` or names a `picklist`. |
| 25 | + |
| 26 | +`validateExpression` gains an opt-in `ExprSchemaHint.optionFields` (the object's option-bearing fields) and the closed refusal-code set gains `label-field-no-options` (`params: { field }`). `@objectstack/formula` exports the two readings of "declares options" side by side: `declaresOptions(def)` on the served form (`@objectstack/objectql` builds `label()`'s map through it) and `declaresOptionsOnceServed(def)` on the authored form (inline `options`, or a `picklist` the registry writes onto the served field), which `@objectstack/lint`'s formula pass hands the validator, so the gate refuses exactly the fields the evaluator faults on. |
0 commit comments