|
| 1 | +--- |
| 2 | +'@objectstack/spec': minor |
| 3 | +'@objectstack/service-automation': minor |
| 4 | +--- |
| 5 | + |
| 6 | +A flow screen field can now express a numeric bound, help text and a lookup target — spelled with the object field's own key names |
| 7 | + |
| 8 | +<!-- adr-0087: registered screen-field-lookup-reference-required --> |
| 9 | + |
| 10 | +`ScreenFieldConfigSchema` was `.strict` over exactly |
| 11 | +`name`/`label`/`type`/`required`/`options`/`defaultValue`/`placeholder`/`visibleWhen`, |
| 12 | +so three ordinary authoring intents had **no expression at all**. They did not |
| 13 | +degrade quietly — `max`, `helpText` and every lookup-target spelling were |
| 14 | +refused BY NAME — but a loud refusal with no landing key is still a dead end, |
| 15 | +and the reference app worked around all three in prose: a discount ceiling |
| 16 | +interpolated into the `label` and the `placeholder` (with a comment explaining |
| 17 | +why there was no `max`), and a `type: 'lookup'` field whose `placeholder` asked |
| 18 | +a human to type a record id because the picker could not be pointed anywhere. |
| 19 | + |
| 20 | +Four keys land, and **their names are derived from `FieldSchema`, not invented** |
| 21 | +— one platform, one field vocabulary, so a name learned on an object field means |
| 22 | +the same thing on a screen field: |
| 23 | + |
| 24 | +| Key | Derived from | | |
| 25 | +|:---|:---|:---| |
| 26 | +| `min` / `max` | `FieldSchema.min` / `.max` | the bound pair | |
| 27 | +| `inlineHelpText` | `FieldSchema.inlineHelpText` | help under the input — `FieldSchema` renames `help`/`helpText`/`hint`/`tooltip` onto it, so a screen-local `helpText` would have been a second contract for one question | |
| 28 | +| `reference` | `FieldSchema.reference` | the object a `type: 'lookup'` field picks records from | |
| 29 | + |
| 30 | +**The bound is enforced, not advisory.** It rides to the client on |
| 31 | +`ScreenFieldSpec` so the user is stopped at the input, **and** |
| 32 | +`validateScreenInputs` re-checks it when the run resumes (`min_value` / |
| 33 | +`max_value`, both already in the ADR-0114 D2 field-error catalog — no new error |
| 34 | +code). A screen field's declared contract is the only contract behind it, so a |
| 35 | +bound the dialog alone applied would be bypassed by any caller posting to |
| 36 | +`resume` directly — the gap #4477 closed for `required`. |
| 37 | + |
| 38 | +That sentence needs no "when the value is a number" qualifier, because the |
| 39 | +value SHAPE is checked first: on a `type: 'number'` field a present value that |
| 40 | +is not a finite JSON number is refused with `invalid_type` (also already in the |
| 41 | +catalog — still no new code), ⛔ **not coerced**. Before this, a bound pass that |
| 42 | +compares numbers was satisfied by anything that never reached it, so `"25"` |
| 43 | +under a `max` of `20` was conformant. One member of the open `type` vocabulary |
| 44 | +is read as a value domain; every other widget hint stays open, and a bound on a |
| 45 | +non-numeric field still constrains nothing. |
| 46 | + |
| 47 | +**Delivered with its rendering, not ahead of it.** The executor forwards all |
| 48 | +four onto the wire and the Studio designer form offers all four as repeater |
| 49 | +columns; `builtin-node-form-zod-ledger.test.ts` reconciles the two key sets |
| 50 | +against the Zod in both directions, so a key declared here and absent from the |
| 51 | +form fails that test rather than shipping as a field nobody can author. |
| 52 | + |
| 53 | +**BREAKING** in the accept-set sense, in TWO places — landing as `minor` on |
| 54 | +both packages because the launch-window guard (`check-changeset-no-major`) |
| 55 | +keeps breaking changes off `major` outside pre-mode, not because the narrowing |
| 56 | +is small. Both were ruled (maintainer ruling A′, decision batch #130 item 1, |
| 57 | +2026-09-13); this release is **not** purely additive. |
| 58 | + |
| 59 | +1. `reference` is **required** when `type` is `lookup`, as it is on an object |
| 60 | + field. A picker with no target object resolves nothing — ADR-0078's own |
| 61 | + example of silently-inert metadata — and a degraded shape that ships today |
| 62 | + is not a reason to bend the contract to it. A stored flow with a bare |
| 63 | + `lookup` screen field parsed before and does not now. There is **no lossless |
| 64 | + conversion**: nothing in the metadata says which object the author meant, so |
| 65 | + this is an ADR-0087 **semantic** migration entry — a structured TODO |
| 66 | + (`screen-field-lookup-reference-required`) that names the flow and the field |
| 67 | + for a human to answer — and ⛔ never a D2 conversion that would have to |
| 68 | + invent a target. |
| 69 | +2. A non-number submitted for a `type: 'number'` screen field is refused on |
| 70 | + resume (`invalid_type`) instead of passing silently. A resume bag that was |
| 71 | + accepted before can be refused now; it was never doing what its author |
| 72 | + declared. |
| 73 | + |
| 74 | +Everything else is additive: the bound itself fires only on a field that |
| 75 | +declares one, which nothing did before this release. |
| 76 | + |
| 77 | +The neighbouring spellings are refused **with their landing key** rather than |
| 78 | +with a bare key list: `help`/`helpText`/`hint`/`tooltip` name `inlineHelpText`, |
| 79 | +and `object`/`referenceTo`/`targetObject`/`lookupObject`/`relatedTo`/`target` |
| 80 | +name `reference`. ⚠️ `object` means different things one level apart — on the |
| 81 | +screen **node** it renames to `objectName`, on a screen **field** it can only |
| 82 | +mean the lookup target — so it earns its own row on both. |
| 83 | + |
| 84 | +**One stale claim corrected in passing, because this change falsified it.** The |
| 85 | +flows translation surface documented `help`'s exclusion as *"`ScreenFieldConfig` |
| 86 | +declares nothing help-shaped at all"*, in `translation.zod.ts`'s guidance string |
| 87 | +(which enumerated the old key set verbatim), its doc block, and |
| 88 | +`i18n-resolver.ts`'s `FLOW_SCREEN_FIELD_COPY_KEYS`. The screen field now |
| 89 | +declares `inlineHelpText`, so the copy is real. The exclusion **stands** — the |
| 90 | +flows bundle still carries `label` and `placeholder` only, and growing that face |
| 91 | +is a ruled step against the #7646 enumeration, not a resolver-side accretion — |
| 92 | +but its reason is now stated as a not-yet instead of telling an author the field |
| 93 | +has no help copy when it has. ⛔ No translation key was added and no resolver |
| 94 | +behaviour moved. |
0 commit comments