|
2206 | 2206 | "toMajor": 18, |
2207 | 2207 | "rationale": "ADR-0032 Decision 3 fixes one template delimiter, double braces, and deletes the single brace: it collides with CEL map literals, and an author who meets both dialects in one flow mixes them. The 17.x interpolator and the template engine render the same text for a path holding a string, a number, a boolean, null, an absent key or variable, an ISO date string, an object or an array, but not for every value — a Date rendered JSON-quoted under the interpolator and as its ISO text under the engine, and a screen title, screen description or end message that was one token holding an object, an array or a Date rendered String(value) — so no conversion is lossless (ADR-0087 D2) and none is applied. Arithmetic, function calls, the date macros and the run-user paths have no hole spelling: a hole is a path with a formatter, never logic. A flow carrying a single-brace token in a text slot is refused at registration, by objectstack validate and by the node contract; a stored flow carrying one is skipped at boot with a warn naming it." |
2208 | 2208 | }, |
| 2209 | + { |
| 2210 | + "surface": "flows[].nodes[].config of a notify node (title, message), a screen node (title, description) and an end node (message) — a string, or the source of a template envelope, carrying a double-brace hole whose root is a dollar-named variable the flow engine does not bind, such as {{ $User.Id }}", |
| 2211 | + "replacement": "a variable the run has, written as a hole. The run user is computed first, with an assignment node whose value slot still reads the run-user path (assignments: { by: '{$User.Id}' }), then written as {{ by }}. A variable the flow binds itself (a declared variable, an assignment target, an outputVariable, a try_catch errorVariable) is named without the dollar sign and written as {{ caught.message }}. The engine's own variables stay holes: {{ $error.message }}, {{ $record.name }}, {{ $runId }}, {{ $flowName }}, {{ $flowLabel }}, and a flat-graph loop's {{ $loopItems }} / {{ $loopIndex }}", |
| 2212 | + "migrationId": "flow-text-slot-unbound-dollar-root-refused", |
| 2213 | + "toMajor": 18, |
| 2214 | + "rationale": "The dollar-named variables are the flow engine's own: it binds $record, $runId, $flowName, $flowLabel and $error, and a flat-graph loop binds $loopItems and $loopIndex. A hole over any other dollar name answers to no variable — {{ $User.Id }} looks like the run user and is not one, since the run user has no hole spelling. In 17.x the slot was a plain string read by the single-brace interpolator, which substituted the inner token and left a literal brace on each side; the 18 text slots render holes through the template engine, where such a hole renders nothing and the run reports success. It is now refused by the node contract, at registration and by objectstack validate, with the remedy its single-brace spelling gets; a stored flow carrying one is skipped at boot with a warn naming it. No D2 conversion exists: what the author meant the hole to read is not in the flow, and the template engine binds no new variable to answer it." |
| 2215 | + }, |
2209 | 2216 | { |
2210 | 2217 | "surface": "the record and previous roots a record-change flow receives — a password or secret field, and an internal field, of the triggering record, on every object", |
2211 | 2218 | "replacement": "read a credential through a privileged binder — the flow credential channel for an http node's signing secret, or a privileged server-side read such as the engine's resolveSecretField — never off `record` or `previous`; on those roots a set credential-class field now reads as the mask `SECRET_MASK`, an unset one as null, and an `internal: true` field is absent", |
|
3508 | 3515 | "toMajor": 18, |
3509 | 3516 | "rationale": "ADR-0049 enforce-or-remove, the enforce arm: the four keys stay declared (they are the selection of every non-joined report), and the one report type that never reads them now refuses them. A `joined` report selects nothing itself, and the refinement already said so for `order` alone — it refused a container `order` with a pointer onto `blocks[]` while the four selection keys beside it parsed green. Measured at this repo's `.objectui-sha` pin `f8a9d0fb0596f4521076628e2bbfe27e6ce67d52`: `DatasetReportRenderer`'s joined branch (`DatasetReportRenderer.tsx:1462`) reads `blocks`, plus the container `runtimeFilter` and `drilldown` resolved above it, and returns before the top-level reads of `columns` / `dataset` / `rows` / `values` begin (line 1529 onward) — so each was accepted by the metadata layer and dropped by the renderer without a word. The alias tables made it reachable: `fields` / `measures` / `metrics` route to `values`, `groupings` / `groupBy` / `dimensions` to `rows`, and `objectName` / `object` / `dataSet` / `source` to `dataset`, on a joined report as on any other. Studio's report inspector hides the top-level binding for a joined report (`ReportDefaultInspector.tsx:328`) but its type picker patches only `type`, so a report bound first and switched to `joined` second carries the keys invisibly. An empty list is NOT refused: it selects nothing, which is what a joined container selects — the container `order` refusal's own threshold. Ships at once, no deprecation window: there is no window in which a key the renderer never reads does anything." |
3510 | 3517 | }, |
3511 | | - { |
3512 | | - "surface": "A list view whose type is chart and whose effective chart binding names no dataset: no chart block and no options.chart bag, or, on a flattened view overlay saved through the metadata write door, an options.chart bag missing its dataset or its values while no chart block replaces it. Judged at every list-view door: views[].list and views[].listViews, objects[].listViews, a view item config, and the flattened list overlay.", |
3513 | | - "replacement": "Bind the chart: declare a top-level `chart` block naming the ADR-0021 `dataset` to plot and at least one of its measures in `values` (`dimensions`, the X / group axis, stays optional, and `chartType` defaults to `bar`). A view whose only binding is the legacy `options.chart` bag completes the bag with `dataset` and `values`, or, preferred, moves the binding to the top-level `chart` block, which replaces the bag whole. A view that is not meant to be a chart takes another `type`.", |
3514 | | - "migrationId": "view-chart-binding-dataset-required", |
3515 | | - "toMajor": 18, |
3516 | | - "rationale": "ADR-0021 single form, enforced (ADR-0049 enforce-or-remove, the enforce arm; ADR-0078, a view that renders nothing is refused rather than warned). A chart list view plots only the dataset its effective binding names, and the renderer reads that binding as the `chart` block, else the `options.chart` bag, the block replacing the bag whole (objectui plugin-list `ListView`, `resolveListChartBinding`, at this repo's `.objectui-sha` pin and at objectui main alike). The authoring `chart` block already required `dataset` and `values`, but a view with no block at all, or with only the bag, never met that schema. Measured on `origin/main` at `e148ca98`: the flattened overlay member accepted `type: 'chart'` with no block and an `options.chart` bag holding only `chartType`, and both authoring doors accepted the block-less view. What such a view rendered was a dead screen: at the pin the renderer fabricated a binding nobody wrote (an aggregate over a field named name and a measure named value), and objectui has since retired that floor, after which the chart component refuses on screen. Now refused at the view's own path, `chart`, or at `options.chart.dataset` / `options.chart.values`, with the binding to declare. Ships at once, no grace window and no dual spelling (2026-08-27 maintainer ruling 「短期不考虑渐进」). Not convertible: only the author knows which dataset a chart was meant to show." |
3517 | | - }, |
3518 | 3518 | { |
3519 | 3519 | "surface": "ui.ViewFilterRule with NO value on an operator that takes one — the value key omitted, or present and undefined, on equals, not_equals, contains, not_contains, icontains, starts_with, ends_with, greater_than, less_than, greater_than_or_equal, less_than_or_equal, before or after (an alias spelling of any of them included), on every carrier of ViewFilterRuleSchema", |
3520 | 3520 | "replacement": "the value the rule compares against — value: \"open\" on equals, value: \"2026-01-01\" on after. A rule that meant \"the field has no value\" becomes one of the four operators that take none — is_empty / is_not_empty / is_null / is_not_null — which read their direction from their name and still parse with or without a value. A rule that was an unfinished row is deleted. The list operators (in / not_in) and the range operator (between) refused an absent value before this change and still do, in their own words", |
|
5816 | 5816 | "toMajor": 18, |
5817 | 5817 | "rationale": "ADR-0032 Decision 3 fixes one template delimiter, double braces, and deletes the single brace: it collides with CEL map literals, and an author who meets both dialects in one flow mixes them. The 17.x interpolator and the template engine render the same text for a path holding a string, a number, a boolean, null, an absent key or variable, an ISO date string, an object or an array, but not for every value — a Date rendered JSON-quoted under the interpolator and as its ISO text under the engine, and a screen title, screen description or end message that was one token holding an object, an array or a Date rendered String(value) — so no conversion is lossless (ADR-0087 D2) and none is applied. Arithmetic, function calls, the date macros and the run-user paths have no hole spelling: a hole is a path with a formatter, never logic. A flow carrying a single-brace token in a text slot is refused at registration, by objectstack validate and by the node contract; a stored flow carrying one is skipped at boot with a warn naming it." |
5818 | 5818 | }, |
| 5819 | + { |
| 5820 | + "surface": "flows[].nodes[].config of a notify node (title, message), a screen node (title, description) and an end node (message) — a string, or the source of a template envelope, carrying a double-brace hole whose root is a dollar-named variable the flow engine does not bind, such as {{ $User.Id }}", |
| 5821 | + "replacement": "a variable the run has, written as a hole. The run user is computed first, with an assignment node whose value slot still reads the run-user path (assignments: { by: '{$User.Id}' }), then written as {{ by }}. A variable the flow binds itself (a declared variable, an assignment target, an outputVariable, a try_catch errorVariable) is named without the dollar sign and written as {{ caught.message }}. The engine's own variables stay holes: {{ $error.message }}, {{ $record.name }}, {{ $runId }}, {{ $flowName }}, {{ $flowLabel }}, and a flat-graph loop's {{ $loopItems }} / {{ $loopIndex }}", |
| 5822 | + "migrationId": "flow-text-slot-unbound-dollar-root-refused", |
| 5823 | + "toMajor": 18, |
| 5824 | + "rationale": "The dollar-named variables are the flow engine's own: it binds $record, $runId, $flowName, $flowLabel and $error, and a flat-graph loop binds $loopItems and $loopIndex. A hole over any other dollar name answers to no variable — {{ $User.Id }} looks like the run user and is not one, since the run user has no hole spelling. In 17.x the slot was a plain string read by the single-brace interpolator, which substituted the inner token and left a literal brace on each side; the 18 text slots render holes through the template engine, where such a hole renders nothing and the run reports success. It is now refused by the node contract, at registration and by objectstack validate, with the remedy its single-brace spelling gets; a stored flow carrying one is skipped at boot with a warn naming it. No D2 conversion exists: what the author meant the hole to read is not in the flow, and the template engine binds no new variable to answer it." |
| 5825 | + }, |
5819 | 5826 | { |
5820 | 5827 | "surface": "the record and previous roots a record-change flow receives — a password or secret field, and an internal field, of the triggering record, on every object", |
5821 | 5828 | "replacement": "read a credential through a privileged binder — the flow credential channel for an http node's signing secret, or a privileged server-side read such as the engine's resolveSecretField — never off `record` or `previous`; on those roots a set credential-class field now reads as the mask `SECRET_MASK`, an unset one as null, and an `internal: true` field is absent", |
|
7118 | 7125 | "toMajor": 18, |
7119 | 7126 | "rationale": "ADR-0049 enforce-or-remove, the enforce arm: the four keys stay declared (they are the selection of every non-joined report), and the one report type that never reads them now refuses them. A `joined` report selects nothing itself, and the refinement already said so for `order` alone — it refused a container `order` with a pointer onto `blocks[]` while the four selection keys beside it parsed green. Measured at this repo's `.objectui-sha` pin `f8a9d0fb0596f4521076628e2bbfe27e6ce67d52`: `DatasetReportRenderer`'s joined branch (`DatasetReportRenderer.tsx:1462`) reads `blocks`, plus the container `runtimeFilter` and `drilldown` resolved above it, and returns before the top-level reads of `columns` / `dataset` / `rows` / `values` begin (line 1529 onward) — so each was accepted by the metadata layer and dropped by the renderer without a word. The alias tables made it reachable: `fields` / `measures` / `metrics` route to `values`, `groupings` / `groupBy` / `dimensions` to `rows`, and `objectName` / `object` / `dataSet` / `source` to `dataset`, on a joined report as on any other. Studio's report inspector hides the top-level binding for a joined report (`ReportDefaultInspector.tsx:328`) but its type picker patches only `type`, so a report bound first and switched to `joined` second carries the keys invisibly. An empty list is NOT refused: it selects nothing, which is what a joined container selects — the container `order` refusal's own threshold. Ships at once, no deprecation window: there is no window in which a key the renderer never reads does anything." |
7120 | 7127 | }, |
7121 | | - { |
7122 | | - "surface": "A list view whose type is chart and whose effective chart binding names no dataset: no chart block and no options.chart bag, or, on a flattened view overlay saved through the metadata write door, an options.chart bag missing its dataset or its values while no chart block replaces it. Judged at every list-view door: views[].list and views[].listViews, objects[].listViews, a view item config, and the flattened list overlay.", |
7123 | | - "replacement": "Bind the chart: declare a top-level `chart` block naming the ADR-0021 `dataset` to plot and at least one of its measures in `values` (`dimensions`, the X / group axis, stays optional, and `chartType` defaults to `bar`). A view whose only binding is the legacy `options.chart` bag completes the bag with `dataset` and `values`, or, preferred, moves the binding to the top-level `chart` block, which replaces the bag whole. A view that is not meant to be a chart takes another `type`.", |
7124 | | - "migrationId": "view-chart-binding-dataset-required", |
7125 | | - "toMajor": 18, |
7126 | | - "rationale": "ADR-0021 single form, enforced (ADR-0049 enforce-or-remove, the enforce arm; ADR-0078, a view that renders nothing is refused rather than warned). A chart list view plots only the dataset its effective binding names, and the renderer reads that binding as the `chart` block, else the `options.chart` bag, the block replacing the bag whole (objectui plugin-list `ListView`, `resolveListChartBinding`, at this repo's `.objectui-sha` pin and at objectui main alike). The authoring `chart` block already required `dataset` and `values`, but a view with no block at all, or with only the bag, never met that schema. Measured on `origin/main` at `e148ca98`: the flattened overlay member accepted `type: 'chart'` with no block and an `options.chart` bag holding only `chartType`, and both authoring doors accepted the block-less view. What such a view rendered was a dead screen: at the pin the renderer fabricated a binding nobody wrote (an aggregate over a field named name and a measure named value), and objectui has since retired that floor, after which the chart component refuses on screen. Now refused at the view's own path, `chart`, or at `options.chart.dataset` / `options.chart.values`, with the binding to declare. Ships at once, no grace window and no dual spelling (2026-08-27 maintainer ruling 「短期不考虑渐进」). Not convertible: only the author knows which dataset a chart was meant to show." |
7127 | | - }, |
7128 | 7128 | { |
7129 | 7129 | "surface": "ui.ViewFilterRule with NO value on an operator that takes one — the value key omitted, or present and undefined, on equals, not_equals, contains, not_contains, icontains, starts_with, ends_with, greater_than, less_than, greater_than_or_equal, less_than_or_equal, before or after (an alias spelling of any of them included), on every carrier of ViewFilterRuleSchema", |
7130 | 7130 | "replacement": "the value the rule compares against — value: \"open\" on equals, value: \"2026-01-01\" on after. A rule that meant \"the field has no value\" becomes one of the four operators that take none — is_empty / is_not_empty / is_null / is_not_null — which read their direction from their name and still parse with or without a value. A rule that was an unfinished row is deleted. The list operators (in / not_in) and the range operator (between) refused an absent value before this change and still do, in their own words", |
|
0 commit comments