@object-ui/core 17.3.0 → 17.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +2018 -0
  2. package/README.md +38 -34
  3. package/dist/actions/ActionEngine.d.ts +43 -26
  4. package/dist/actions/ActionEngine.js +67 -54
  5. package/dist/actions/ActionRunner.d.ts +281 -31
  6. package/dist/actions/ActionRunner.js +458 -36
  7. package/dist/actions/actionKeys.d.ts +163 -27
  8. package/dist/actions/actionKeys.js +238 -27
  9. package/dist/actions/actionResponse.js +1 -1
  10. package/dist/actions/index.d.ts +1 -0
  11. package/dist/actions/index.js +1 -0
  12. package/dist/actions/recordIdParam.d.ts +77 -0
  13. package/dist/actions/recordIdParam.js +37 -0
  14. package/dist/actions/serverActionHandler.d.ts +1 -1
  15. package/dist/actions/serverActionHandler.js +1 -1
  16. package/dist/data-scope/ViewDataProvider.d.ts +37 -12
  17. package/dist/data-scope/ViewDataProvider.js +83 -5
  18. package/dist/data-scope/element-data-source.d.ts +113 -0
  19. package/dist/data-scope/element-data-source.js +216 -0
  20. package/dist/data-scope/index.d.ts +2 -1
  21. package/dist/data-scope/index.js +1 -0
  22. package/dist/evaluator/ExpressionEvaluator.d.ts +42 -1
  23. package/dist/evaluator/ExpressionEvaluator.js +42 -1
  24. package/dist/evaluator/SafeExpressionParser.js +0 -4
  25. package/dist/evaluator/declaredPredicate.d.ts +119 -0
  26. package/dist/evaluator/declaredPredicate.js +152 -0
  27. package/dist/evaluator/fieldRules.d.ts +35 -0
  28. package/dist/evaluator/fieldRules.js +79 -20
  29. package/dist/evaluator/index.d.ts +2 -0
  30. package/dist/evaluator/index.js +2 -0
  31. package/dist/evaluator/listConditional.d.ts +54 -4
  32. package/dist/evaluator/listConditional.js +125 -19
  33. package/dist/evaluator/optionRules.d.ts +55 -0
  34. package/dist/evaluator/optionRules.js +60 -0
  35. package/dist/evaluator/predicateInput.d.ts +51 -3
  36. package/dist/evaluator/predicateInput.js +62 -5
  37. package/dist/evaluator/rowPredicateCanon.d.ts +67 -0
  38. package/dist/evaluator/rowPredicateCanon.js +208 -0
  39. package/dist/index.d.ts +8 -1
  40. package/dist/index.js +31 -1
  41. package/dist/protocols/DndProtocol.d.ts +42 -1
  42. package/dist/protocols/KeyboardProtocol.d.ts +31 -1
  43. package/dist/protocols/SharingProtocol.d.ts +11 -1
  44. package/dist/registry/Registry.d.ts +62 -16
  45. package/dist/theme/ThemeEngine.d.ts +0 -8
  46. package/dist/theme/ThemeEngine.js +22 -103
  47. package/dist/theme/index.d.ts +1 -1
  48. package/dist/theme/index.js +7 -1
  49. package/dist/types/index.d.ts +125 -12
  50. package/dist/utils/chart-presentation.d.ts +205 -0
  51. package/dist/utils/chart-presentation.js +280 -0
  52. package/dist/utils/chart-series.d.ts +390 -23
  53. package/dist/utils/chart-series.js +776 -32
  54. package/dist/utils/column-identity.d.ts +43 -0
  55. package/dist/utils/column-identity.js +47 -1
  56. package/dist/utils/compare-to.d.ts +40 -13
  57. package/dist/utils/compare-to.js +4 -32
  58. package/dist/utils/dashboard-filters.d.ts +54 -8
  59. package/dist/utils/dashboard-filters.js +261 -12
  60. package/dist/utils/dataset-format.d.ts +73 -14
  61. package/dist/utils/dataset-format.js +142 -9
  62. package/dist/utils/dataset-pivot.d.ts +28 -7
  63. package/dist/utils/dataset-pivot.js +27 -6
  64. package/dist/utils/debug.js +7 -0
  65. package/dist/utils/dom-props.d.ts +137 -0
  66. package/dist/utils/dom-props.js +153 -0
  67. package/dist/utils/expand-fields.d.ts +97 -7
  68. package/dist/utils/expand-fields.js +97 -7
  69. package/dist/utils/filter-tokens.js +0 -1
  70. package/dist/utils/freeze-schema.d.ts +38 -1
  71. package/dist/utils/freeze-schema.js +10 -0
  72. package/dist/utils/humanize-label.d.ts +60 -0
  73. package/dist/utils/humanize-label.js +62 -0
  74. package/dist/utils/managedBy.d.ts +27 -2
  75. package/dist/utils/normalize-list-view.d.ts +36 -6
  76. package/dist/utils/normalize-list-view.js +93 -9
  77. package/dist/utils/number-display.d.ts +155 -0
  78. package/dist/utils/number-display.js +71 -0
  79. package/dist/utils/predicate-fields.d.ts +29 -4
  80. package/dist/utils/predicate-fields.js +44 -4
  81. package/dist/utils/resolve-view-id.d.ts +31 -0
  82. package/dist/utils/resolve-view-id.js +45 -0
  83. package/dist/utils/retired-field-types.d.ts +90 -0
  84. package/dist/utils/retired-field-types.js +124 -0
  85. package/dist/utils/sort-query.d.ts +57 -0
  86. package/dist/utils/sort-query.js +35 -0
  87. package/dist/utils/unmaterialized-fields.d.ts +52 -0
  88. package/dist/utils/unmaterialized-fields.js +57 -0
  89. package/dist/validation/index.d.ts +1 -0
  90. package/dist/validation/index.js +1 -0
  91. package/dist/validation/schema-validator.js +105 -0
  92. package/dist/validation/server-owned-value.d.ts +62 -0
  93. package/dist/validation/server-owned-value.js +99 -0
  94. package/dist/validation/validators/object-validation-engine.js +0 -2
  95. package/package.json +5 -5
  96. package/dist/__benchmarks__/core.bench.d.ts +0 -8
  97. package/dist/__benchmarks__/core.bench.js +0 -53
  98. package/dist/utils/merge-views-into-objects.d.ts +0 -19
  99. package/dist/utils/merge-views-into-objects.js +0 -39
@@ -6,26 +6,46 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  /**
9
- * What an action is allowed to carry — objectstack#4075 step 1.
9
+ * What an action is allowed to carry — objectstack#4075, all three steps.
10
10
  *
11
- * `ActionDef` ends with `[key: string]: any`, so it accepts any key of any type.
12
- * Concretely (objectui#2990): deleting `ActionDef.execute` produced ZERO compile
13
- * errors even though the field had just been removed, and stale metadata still
14
- * authoring `execute: 'markDone'` type-checks today. The same deletion against
15
- * `@object-ui/types`' `ActionSchema` — which has no index signature — correctly
16
- * produced `TS2353` at the authoring site. One of the two readers can catch a
17
- * retired key; the other is structurally incapable.
11
+ * `ActionDef` used to end with `[key: string]: any`, so it accepted any key of
12
+ * any type. Concretely (objectui#2990): deleting `ActionDef.execute` produced
13
+ * ZERO compile errors even though the field had just been removed, and stale
14
+ * metadata authoring `execute: 'markDone'` type-checked. The same deletion
15
+ * against `@object-ui/types`' `ActionSchema` — which has no index signature —
16
+ * correctly produced `TS2353` at the authoring site. One of the two readers
17
+ * could catch a retired key; the other was structurally incapable.
18
18
  *
19
- * That asymmetry is the issue. An open key set on a DECLARED METADATA CONTRACT
19
+ * That asymmetry was the issue. An open key set on a DECLARED METADATA CONTRACT
20
20
  * is what lets a typo (`targt`, `exectue`) and a tombstoned key (`execute`) both
21
21
  * sail through to a runner that then silently does nothing — the #2169 "Mark
22
22
  * Done does nothing" shape.
23
23
  *
24
- * This module is step 1 of the staged narrowing: it makes the key set VISIBLE
25
- * and warns on anything outside it, without changing a single type. Nothing
26
- * breaks, and an invisible failure becomes an audible one. Steps 2 and 3 —
27
- * promoting the legitimate keys to explicit optional fields, then removing the
28
- * index signature — are what finally let `tsc` catch typos and retired keys.
24
+ * This module was step 1 of the staged narrowing: make the key set VISIBLE and
25
+ * warn on anything outside it, without changing a single type. Step 2 promoted
26
+ * the legitimate keys to explicit optional fields; **step 3 removed the index
27
+ * signature**, so `tsc` now rejects a typo or a retired key outright.
28
+ *
29
+ * ── Why this module SURVIVES step 3 ──────────────────────────────────────────
30
+ * Step 1 wrote that the dev-mode warning "can retire" once the index signature
31
+ * came down. Step 3 measured that and kept it, because the two mechanisms cover
32
+ * DISJOINT populations rather than the same one twice:
33
+ *
34
+ * - `tsc` sees action literals AUTHORED IN CODE. It rejects an unknown key at
35
+ * the construction site, which is the strongest possible signal — but only
36
+ * for actions that exist as TypeScript.
37
+ * - the warning below sees actions that arrive as DATA. objectstack#3903
38
+ * established that stored `sys_metadata` rows are rehydrated UNPARSED, so
39
+ * authored metadata reaches `runner.execute()` as a plain object no
40
+ * compiler ever looked at. That is the population `execute: 'markDone'`
41
+ * actually lives in, and no type can reach it.
42
+ *
43
+ * Retiring the warning would therefore have re-opened the runtime half of the
44
+ * gap while closing the compile-time half — which is why `executeScript`'s
45
+ * rename-prescription branch also survives, for the same rows and the same
46
+ * reason. What DID retire with the index signature is the inverted pin in
47
+ * `actionKeys.pin.test.ts` that asserted the signature was still there; it now
48
+ * asserts the opposite, which was always its stated completion condition.
29
49
  *
30
50
  * The lists are pinned by `__tests__/actionKeys.pin.test.ts`, which re-derives
31
51
  * each one from its actual source. A hand-maintained list that drifts from the
@@ -66,17 +86,32 @@
66
86
  * deprecating them toward a spelling that does not exist would be worse than
67
87
  * leaving them declared.
68
88
  *
69
- * Step 3 remains: remove the index signature, at which point `tsc` catches both
70
- * typos and retired keys and the dev-mode warning below can retire with it.
89
+ * ── What step 3 found the first two steps had missed ─────────────────────────
90
+ * Deleting the index signature is the only thing that can name the keys still
91
+ * hiding behind it, and it named five, in two groups:
92
+ *
93
+ * `to` / `external` / `newTab` / `replace` — the `navigation` alias's own
94
+ * spelling. Step 1 had already ruled these legitimate and listed them below in
95
+ * {@link NAVIGATION_ALIAS_KEYS}, but step 2's scope was the SPEC-owned keys, so
96
+ * they were declared as data and never as fields; `executeNavigation` read them
97
+ * through the index signature. Promoted, and deliberately not `@deprecated` —
98
+ * step 2's acceptance ruled on exactly that point.
99
+ *
100
+ * `description` — objectui vocabulary from `@object-ui/types`' renderer view of
101
+ * an action, forwarded by all four action renderers (`check:action-forward-parity`
102
+ * requires it) and read by the param-collection dialog for its subtitle
103
+ * (objectui#4192). Authorable, forwarded, read — and undeclared. These were the
104
+ * only two `TS2353`s the deletion produced across the whole workspace.
71
105
  */
72
106
  /**
73
107
  * Every property `ActionDef` declares, in declaration order.
74
108
  *
75
- * Kept as data because TypeScript types do not survive to runtime — and
76
- * `keyof ActionDef` cannot substitute for this list: the index signature widens
77
- * it to `string | number`, which is precisely the problem being worked around.
109
+ * Kept as data because TypeScript types do not survive to runtime — `keyof
110
+ * ActionDef` is erased along with the interface, so it cannot hand a runtime
111
+ * classifier a list of anything. `actionKeys.pin.test.ts` re-derives this one
112
+ * from the interface's AST, which is what keeps the data true.
78
113
  */
79
- export declare const ACTION_DEF_KEYS: readonly ["type", "actionType", "name", "label", "confirmText", "confirm", "condition", "disabled", "api", "endpoint", "method", "navigate", "onClick", "reload", "close", "redirect", "toast", "successMessage", "errorMessage", "refreshAfter", "undoable", "params", "actionParams", "resultDialog", "target", "body", "openIn", "modal", "chain", "chainMode", "onSuccess", "onFailure", "opensInNewTab", "newTabUrl", "locations", "visible", "requiredPermissions", "icon", "variant", "order", "component", "objectName", "ai", "aria", "bodyExtra", "bodyShape", "mode", "recordIdField", "recordIdParam", "requiresFeature", "shortcut", "bulkEnabled"];
114
+ export declare const ACTION_DEF_KEYS: readonly ["type", "actionType", "name", "label", "description", "confirmText", "confirm", "condition", "disabled", "api", "endpoint", "method", "navigate", "to", "external", "newTab", "replace", "onClick", "reload", "close", "redirect", "toast", "successMessage", "errorMessage", "refreshAfter", "undoable", "params", "actionParams", "resultDialog", "target", "body", "openIn", "modal", "chain", "chainMode", "onSuccess", "onFailure", "opensInNewTab", "newTabUrl", "locations", "visible", "requiredPermissions", "icon", "variant", "order", "component", "objectName", "ai", "aria", "bodyExtra", "bodyShape", "mode", "recordIdField", "recordIdParam", "requiresFeature", "shortcut", "bulkEnabled"];
80
115
  /**
81
116
  * Every property `@objectstack/spec`'s `ActionSchema` declares.
82
117
  *
@@ -91,7 +126,7 @@ export declare const ACTION_DEF_KEYS: readonly ["type", "actionType", "name", "l
91
126
  * test and wrong in shipped code — `@object-ui/types` made the same call for
92
127
  * `ActionComponent`. The pin test does the walk and fails the day this drifts.
93
128
  */
94
- export declare const SPEC_ACTION_KEYS: readonly ["_lock", "_lockDocsUrl", "_lockReason", "_lockSource", "_packageId", "_packageVersion", "_provenance", "ai", "aria", "body", "bodyExtra", "bodyShape", "bulkEnabled", "component", "confirmText", "disabled", "errorMessage", "execute", "icon", "label", "locations", "method", "mode", "name", "newTabUrl", "objectName", "openIn", "opensInNewTab", "order", "params", "recordIdField", "recordIdParam", "refreshAfter", "requiredPermissions", "requiresFeature", "resultDialog", "shortcut", "successMessage", "target", "type", "undoable", "variant", "visible"];
129
+ export declare const SPEC_ACTION_KEYS: readonly ["_lock", "_lockDocsUrl", "_lockReason", "_lockSource", "_packageId", "_packageVersion", "_provenance", "ai", "aria", "body", "bodyExtra", "bodyShape", "bulkEnabled", "component", "confirmText", "description", "disabled", "errorMessage", "execute", "icon", "label", "locations", "method", "mode", "name", "newTabUrl", "objectName", "onSuccess", "openIn", "opensInNewTab", "order", "params", "recordIdField", "recordIdParam", "refreshAfter", "requiredPermissions", "requiresFeature", "resultDialog", "shortcut", "successMessage", "target", "type", "undoable", "variant", "visible"];
95
130
  /**
96
131
  * The `navigation` alias's own spelling of a target, read off the action itself
97
132
  * when no nested `navigate` object is present (`ActionRunner.executeNavigation`).
@@ -103,6 +138,54 @@ export declare const SPEC_ACTION_KEYS: readonly ["_lock", "_lockDocsUrl", "_lock
103
138
  * runner itself supports.
104
139
  */
105
140
  export declare const NAVIGATION_ALIAS_KEYS: readonly ["to", "external", "newTab", "replace"];
141
+ /**
142
+ * Keys a HOST composes on the DISPATCH OBJECT at dispatch time — chrome the
143
+ * runtime reads once on its way to the dialog, that no author ever writes and
144
+ * no store ever holds.
145
+ *
146
+ * Distinct from every other list in this module, and the distinction is the
147
+ * whole point: each of the three lists above restates one AUTHORED surface —
148
+ * what `ActionDef` itself declares, what the spec's `ActionSchema` declares,
149
+ * and the `navigation` alias's objectui dialect — so a key joining one of them
150
+ * is a key an author may legally write. This list claims no such derivation and
151
+ * must not acquire one: a key here is the opposite, and writing it in metadata
152
+ * is still a compile error that must stay one. Maintainer ruling
153
+ * 2026-08-22 fixes both halves of that: ⛔ the key is NOT declared on
154
+ * `ActionDef` and ⛔ NOT added to {@link ACTION_DEF_KEYS}; it is declared at the
155
+ * seam, on `packages/app-shell/src/consoleActionDispatch.ts`'s
156
+ * `ConsoleActionDispatch`, and appears HERE only so the dev-mode warning below
157
+ * stops calling it unknown.
158
+ *
159
+ * Why the warning has to know: `classifyActionKeys` sees the object the runner
160
+ * was handed, which is the DISPATCH, not the stored metadata row. With the key
161
+ * absent from the inventory, `warnOnUnknownActionKeys` told the author that a
162
+ * key TWO files read is one "no reader recognizes", and prescribed promoting it
163
+ * to `ActionDef` — the one shape the ruling forbids for it. A diagnostic that
164
+ * fires on the product's own privileged path is how a console gets muted.
165
+ *
166
+ * {@link HOST_STASHED_PARAM_KEYS} below is the same category one level down —
167
+ * host-composed keys inside `params` rather than on the action itself — and
168
+ * cites `DeclaredActionsBar` by name for the same reason this does. Two lists
169
+ * because the two warnings ask different questions of different objects, not
170
+ * because the categories differ.
171
+ *
172
+ * `overrideNotice` — the privileged-override safety copy (objectui#5178),
173
+ * naming the approvers about to be bypassed.
174
+ * - producer: `packages/app-shell/src/views/DeclaredActionsBar.tsx`, on the
175
+ * `can_act:false && can_override:true` branch.
176
+ * - readers: `packages/app-shell/src/hooks/useConsoleActionRuntime.tsx` and
177
+ * `packages/app-shell/src/views/RecordDetailView.tsx`, the console's two
178
+ * param-collection handlers, which render it ahead of the declared
179
+ * description in the dialog's subtitle.
180
+ *
181
+ * Anything added here must satisfy all three of: composed by a host in code,
182
+ * never read back out of stored metadata, and read by the runtime. A key an
183
+ * AUTHOR is meant to write belongs on `ActionDef` — or, when the spec owns it,
184
+ * in `@objectstack/spec` first — and not in this list. `actionKeys.pin.test.ts`
185
+ * pins the contents exactly, so growing the set is a deliberate, reviewed edit
186
+ * rather than a quiet one.
187
+ */
188
+ export declare const HOST_DISPATCH_ACTION_KEYS: readonly ["overrideNotice"];
106
189
  /**
107
190
  * Keys the spec has TOMBSTONED: still present in `ActionSchema` so the parser can
108
191
  * reject them BY NAME with a rename prescription, rather than fail with a bare
@@ -111,11 +194,27 @@ export declare const NAVIGATION_ALIAS_KEYS: readonly ["to", "external", "newTab"
111
194
  * Warned about separately from unknown keys, and more loudly: an unknown key is
112
195
  * probably a typo, while a retired key is metadata that used to work. That
113
196
  * distinction is why `executeScript` carries a runtime branch returning the
114
- * rename prescription — a branch that exists solely to compensate for the index
115
- * signature, and that can retire with it in step 3.
197
+ * rename prescription.
198
+ *
199
+ * Step 1 expected that branch to retire with the index signature. It does not,
200
+ * and the reason is the one in this module's header: `tsc` closed the
201
+ * code-authored half of the gap, while `execute: 'markDone'` lives in stored
202
+ * rows that reach the runner UNPARSED (objectstack#3903). The branch reads the
203
+ * key off an untyped view of the action precisely so that `ActionDef` can go on
204
+ * NOT declaring it — declaring it to make the read compile would re-legitimize
205
+ * a tombstone the spec keeps only in order to reject it by name.
116
206
  */
117
207
  export declare const RETIRED_ACTION_KEYS: Readonly<Record<string, string>>;
118
- /** Every key an action may legitimately carry today. */
208
+ /**
209
+ * Every key an action may legitimately carry today — authored or host-composed.
210
+ *
211
+ * Four inputs, three of them mirrors of an authored surface and the fourth
212
+ * ({@link HOST_DISPATCH_ACTION_KEYS}) the host-composed dispatch chrome. The
213
+ * union is what `classifyActionKeys` consults, and it is deliberately wider
214
+ * than the authored surface: the runner classifies the object it was HANDED,
215
+ * and a host hands it a dispatch. Membership here grants nothing to an author —
216
+ * `ActionDef` is what decides that, and it is closed.
217
+ */
119
218
  export declare const KNOWN_ACTION_KEYS: ReadonlySet<string>;
120
219
  /** Split an action's own keys into the two things worth saying out loud. */
121
220
  export declare function classifyActionKeys(action: object | null | undefined): {
@@ -128,8 +227,45 @@ export declare function resetActionKeyWarnings(): void;
128
227
  * Dev-mode only: name the keys an action carries that no reader will ever look
129
228
  * at. Non-breaking by construction — it changes no types and rejects nothing.
130
229
  *
131
- * Silently binding no handler is the #2169 "Mark Done does nothing" shape, and
132
- * the compiler cannot help while the index signature stands. Until it comes
133
- * down, this is the audible half.
230
+ * Silently binding no handler is the #2169 "Mark Done does nothing" shape. The
231
+ * compiler now catches it in action literals AUTHORED IN CODE — objectstack#4075
232
+ * step 3 closed `ActionDef` — but not in the actions that arrive as DATA, which
233
+ * reach the runner unparsed and which no compiler ever looked at. This is the
234
+ * audible half for that population; see this module's header.
134
235
  */
135
236
  export declare function warnOnUnknownActionKeys(action: object | null | undefined, where?: string): void;
237
+ /**
238
+ * Does this action carry the DEPRECATED object-form `params` — a static request
239
+ * payload written under the key that means "parameter definitions"?
240
+ *
241
+ * The discriminator is `Array.isArray` and the type guard is `api`, mirroring
242
+ * `@objectstack/spec`'s `inline-action-api-params-to-body-extra` conversion
243
+ * (ADR-0087 D2) key for key. Contract parity with the producer is the point: the
244
+ * conversion rewrites exactly this shape to `bodyExtra` at load, so the runner
245
+ * must recognize exactly the same shape as "the thing that got rewritten".
246
+ *
247
+ * The `api` guard is also what keeps the deprecation out of objectstack#6828's
248
+ * territory — object-form `params` on a `type: 'url'` action is a THIRD meaning
249
+ * (`interpolateTarget`'s `${param.X}` scope, `executeUrl`'s `params.newTab`) and
250
+ * `bodyExtra` is NOT its replacement. Do not widen this guard without that
251
+ * ruling.
252
+ */
253
+ export declare function hasDeprecatedObjectParams(action: object | null | undefined): boolean;
254
+ /**
255
+ * Dev-mode only: name the object-form `params` that `@objectstack/spec` 17 already
256
+ * refuses at the authoring door, and prescribe `bodyExtra`.
257
+ *
258
+ * This is the audible half of the compat window the maintainer's 2026-08-06
259
+ * ruling on objectstack#5777 ordered (direction A — a separate key, no same-name
260
+ * union): the runner keeps READING a non-array `params` as the static payload for
261
+ * one version window, says so, and the arm is deleted at 18. The loud half lives
262
+ * at the producer, where it belongs — spec's `params` field refuses the object
263
+ * form outright with a message naming `bodyExtra`, and the ADR-0087 conversion
264
+ * rewrites sources that still carry it. So a warning is the correct severity
265
+ * HERE: by the time metadata reaches this runner the producer has already had its
266
+ * say, and what is left to catch is a host composing an ActionDef in code.
267
+ *
268
+ * Dev-only and warn-once, matching {@link warnOnUnknownActionKeys} — a warning
269
+ * that floods a production console is a warning that gets muted.
270
+ */
271
+ export declare function warnOnDeprecatedObjectParams(action: object | null | undefined, where?: string): void;
@@ -6,26 +6,46 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  /**
9
- * What an action is allowed to carry — objectstack#4075 step 1.
9
+ * What an action is allowed to carry — objectstack#4075, all three steps.
10
10
  *
11
- * `ActionDef` ends with `[key: string]: any`, so it accepts any key of any type.
12
- * Concretely (objectui#2990): deleting `ActionDef.execute` produced ZERO compile
13
- * errors even though the field had just been removed, and stale metadata still
14
- * authoring `execute: 'markDone'` type-checks today. The same deletion against
15
- * `@object-ui/types`' `ActionSchema` — which has no index signature — correctly
16
- * produced `TS2353` at the authoring site. One of the two readers can catch a
17
- * retired key; the other is structurally incapable.
11
+ * `ActionDef` used to end with `[key: string]: any`, so it accepted any key of
12
+ * any type. Concretely (objectui#2990): deleting `ActionDef.execute` produced
13
+ * ZERO compile errors even though the field had just been removed, and stale
14
+ * metadata authoring `execute: 'markDone'` type-checked. The same deletion
15
+ * against `@object-ui/types`' `ActionSchema` — which has no index signature —
16
+ * correctly produced `TS2353` at the authoring site. One of the two readers
17
+ * could catch a retired key; the other was structurally incapable.
18
18
  *
19
- * That asymmetry is the issue. An open key set on a DECLARED METADATA CONTRACT
19
+ * That asymmetry was the issue. An open key set on a DECLARED METADATA CONTRACT
20
20
  * is what lets a typo (`targt`, `exectue`) and a tombstoned key (`execute`) both
21
21
  * sail through to a runner that then silently does nothing — the #2169 "Mark
22
22
  * Done does nothing" shape.
23
23
  *
24
- * This module is step 1 of the staged narrowing: it makes the key set VISIBLE
25
- * and warns on anything outside it, without changing a single type. Nothing
26
- * breaks, and an invisible failure becomes an audible one. Steps 2 and 3 —
27
- * promoting the legitimate keys to explicit optional fields, then removing the
28
- * index signature — are what finally let `tsc` catch typos and retired keys.
24
+ * This module was step 1 of the staged narrowing: make the key set VISIBLE and
25
+ * warn on anything outside it, without changing a single type. Step 2 promoted
26
+ * the legitimate keys to explicit optional fields; **step 3 removed the index
27
+ * signature**, so `tsc` now rejects a typo or a retired key outright.
28
+ *
29
+ * ── Why this module SURVIVES step 3 ──────────────────────────────────────────
30
+ * Step 1 wrote that the dev-mode warning "can retire" once the index signature
31
+ * came down. Step 3 measured that and kept it, because the two mechanisms cover
32
+ * DISJOINT populations rather than the same one twice:
33
+ *
34
+ * - `tsc` sees action literals AUTHORED IN CODE. It rejects an unknown key at
35
+ * the construction site, which is the strongest possible signal — but only
36
+ * for actions that exist as TypeScript.
37
+ * - the warning below sees actions that arrive as DATA. objectstack#3903
38
+ * established that stored `sys_metadata` rows are rehydrated UNPARSED, so
39
+ * authored metadata reaches `runner.execute()` as a plain object no
40
+ * compiler ever looked at. That is the population `execute: 'markDone'`
41
+ * actually lives in, and no type can reach it.
42
+ *
43
+ * Retiring the warning would therefore have re-opened the runtime half of the
44
+ * gap while closing the compile-time half — which is why `executeScript`'s
45
+ * rename-prescription branch also survives, for the same rows and the same
46
+ * reason. What DID retire with the index signature is the inverted pin in
47
+ * `actionKeys.pin.test.ts` that asserted the signature was still there; it now
48
+ * asserts the opposite, which was always its stated completion condition.
29
49
  *
30
50
  * The lists are pinned by `__tests__/actionKeys.pin.test.ts`, which re-derives
31
51
  * each one from its actual source. A hand-maintained list that drifts from the
@@ -66,21 +86,40 @@
66
86
  * deprecating them toward a spelling that does not exist would be worse than
67
87
  * leaving them declared.
68
88
  *
69
- * Step 3 remains: remove the index signature, at which point `tsc` catches both
70
- * typos and retired keys and the dev-mode warning below can retire with it.
89
+ * ── What step 3 found the first two steps had missed ─────────────────────────
90
+ * Deleting the index signature is the only thing that can name the keys still
91
+ * hiding behind it, and it named five, in two groups:
92
+ *
93
+ * `to` / `external` / `newTab` / `replace` — the `navigation` alias's own
94
+ * spelling. Step 1 had already ruled these legitimate and listed them below in
95
+ * {@link NAVIGATION_ALIAS_KEYS}, but step 2's scope was the SPEC-owned keys, so
96
+ * they were declared as data and never as fields; `executeNavigation` read them
97
+ * through the index signature. Promoted, and deliberately not `@deprecated` —
98
+ * step 2's acceptance ruled on exactly that point.
99
+ *
100
+ * `description` — objectui vocabulary from `@object-ui/types`' renderer view of
101
+ * an action, forwarded by all four action renderers (`check:action-forward-parity`
102
+ * requires it) and read by the param-collection dialog for its subtitle
103
+ * (objectui#4192). Authorable, forwarded, read — and undeclared. These were the
104
+ * only two `TS2353`s the deletion produced across the whole workspace.
71
105
  */
72
106
  /**
73
107
  * Every property `ActionDef` declares, in declaration order.
74
108
  *
75
- * Kept as data because TypeScript types do not survive to runtime — and
76
- * `keyof ActionDef` cannot substitute for this list: the index signature widens
77
- * it to `string | number`, which is precisely the problem being worked around.
109
+ * Kept as data because TypeScript types do not survive to runtime — `keyof
110
+ * ActionDef` is erased along with the interface, so it cannot hand a runtime
111
+ * classifier a list of anything. `actionKeys.pin.test.ts` re-derives this one
112
+ * from the interface's AST, which is what keeps the data true.
78
113
  */
79
114
  export const ACTION_DEF_KEYS = [
80
115
  'type',
81
116
  'actionType',
82
117
  'name',
83
118
  'label',
119
+ // Promoted by step 3 — objectui vocabulary (`@object-ui/types`' renderer view
120
+ // of an action), forwarded by all four action renderers and read by the
121
+ // param-collection dialog, but never declared on `ActionDef`.
122
+ 'description',
84
123
  'confirmText',
85
124
  'confirm',
86
125
  'condition',
@@ -89,6 +128,14 @@ export const ACTION_DEF_KEYS = [
89
128
  'endpoint',
90
129
  'method',
91
130
  'navigate',
131
+ // The `navigation` alias's own spelling, promoted to real fields by step 3.
132
+ // Listed a second time in `NAVIGATION_ALIAS_KEYS` below, which keeps the
133
+ // separate fact that these four are objectui dialect with a spec-adoption
134
+ // tripwire; this list is only "what the interface declares".
135
+ 'to',
136
+ 'external',
137
+ 'newTab',
138
+ 'replace',
92
139
  'onClick',
93
140
  'reload',
94
141
  'close',
@@ -169,6 +216,15 @@ export const SPEC_ACTION_KEYS = [
169
216
  'bulkEnabled',
170
217
  'component',
171
218
  'confirmText',
219
+ // Added to `ActionSchema` in @objectstack/spec 17.0.0-rc.6. Listed here purely
220
+ // because this array's contract is "every property the spec's `ActionSchema`
221
+ // declares" — restating the spec, not adopting a feature. `ActionDef` does NOT
222
+ // declare `description` and is not changed: whether the runner or any renderer
223
+ // should READ it is a separate question, and `actionKeys.pin.test.ts`'s
224
+ // `ActionDef` half stays green without it. What this entry buys is that an
225
+ // action carrying a `description` is no longer reported as having an unknown
226
+ // key, which is the only thing the inventory is consulted for.
227
+ 'description',
172
228
  'disabled',
173
229
  'errorMessage',
174
230
  'execute',
@@ -180,6 +236,14 @@ export const SPEC_ACTION_KEYS = [
180
236
  'name',
181
237
  'newTabUrl',
182
238
  'objectName',
239
+ // Declared by `ActionSchema` as of @objectstack/spec 17.1.0 (objectui#5328).
240
+ // Listing it here is a DIAGNOSTIC statement only — `KNOWN_ACTION_KEYS` feeds
241
+ // `warnOnUnknownActionKeys`, so without this row an author writing the key the
242
+ // spec now accepts would be warned it is unknown. It says nothing about the
243
+ // key being forwarded: the four declared action surfaces still drop it before
244
+ // the runner, tracked as KNOWN_GAPS in check-action-forward-parity.mjs and
245
+ // filed as objectui#5493.
246
+ 'onSuccess',
183
247
  'openIn',
184
248
  'opensInNewTab',
185
249
  'order',
@@ -209,6 +273,54 @@ export const SPEC_ACTION_KEYS = [
209
273
  * runner itself supports.
210
274
  */
211
275
  export const NAVIGATION_ALIAS_KEYS = ['to', 'external', 'newTab', 'replace'];
276
+ /**
277
+ * Keys a HOST composes on the DISPATCH OBJECT at dispatch time — chrome the
278
+ * runtime reads once on its way to the dialog, that no author ever writes and
279
+ * no store ever holds.
280
+ *
281
+ * Distinct from every other list in this module, and the distinction is the
282
+ * whole point: each of the three lists above restates one AUTHORED surface —
283
+ * what `ActionDef` itself declares, what the spec's `ActionSchema` declares,
284
+ * and the `navigation` alias's objectui dialect — so a key joining one of them
285
+ * is a key an author may legally write. This list claims no such derivation and
286
+ * must not acquire one: a key here is the opposite, and writing it in metadata
287
+ * is still a compile error that must stay one. Maintainer ruling
288
+ * 2026-08-22 fixes both halves of that: ⛔ the key is NOT declared on
289
+ * `ActionDef` and ⛔ NOT added to {@link ACTION_DEF_KEYS}; it is declared at the
290
+ * seam, on `packages/app-shell/src/consoleActionDispatch.ts`'s
291
+ * `ConsoleActionDispatch`, and appears HERE only so the dev-mode warning below
292
+ * stops calling it unknown.
293
+ *
294
+ * Why the warning has to know: `classifyActionKeys` sees the object the runner
295
+ * was handed, which is the DISPATCH, not the stored metadata row. With the key
296
+ * absent from the inventory, `warnOnUnknownActionKeys` told the author that a
297
+ * key TWO files read is one "no reader recognizes", and prescribed promoting it
298
+ * to `ActionDef` — the one shape the ruling forbids for it. A diagnostic that
299
+ * fires on the product's own privileged path is how a console gets muted.
300
+ *
301
+ * {@link HOST_STASHED_PARAM_KEYS} below is the same category one level down —
302
+ * host-composed keys inside `params` rather than on the action itself — and
303
+ * cites `DeclaredActionsBar` by name for the same reason this does. Two lists
304
+ * because the two warnings ask different questions of different objects, not
305
+ * because the categories differ.
306
+ *
307
+ * `overrideNotice` — the privileged-override safety copy (objectui#5178),
308
+ * naming the approvers about to be bypassed.
309
+ * - producer: `packages/app-shell/src/views/DeclaredActionsBar.tsx`, on the
310
+ * `can_act:false && can_override:true` branch.
311
+ * - readers: `packages/app-shell/src/hooks/useConsoleActionRuntime.tsx` and
312
+ * `packages/app-shell/src/views/RecordDetailView.tsx`, the console's two
313
+ * param-collection handlers, which render it ahead of the declared
314
+ * description in the dialog's subtitle.
315
+ *
316
+ * Anything added here must satisfy all three of: composed by a host in code,
317
+ * never read back out of stored metadata, and read by the runtime. A key an
318
+ * AUTHOR is meant to write belongs on `ActionDef` — or, when the spec owns it,
319
+ * in `@objectstack/spec` first — and not in this list. `actionKeys.pin.test.ts`
320
+ * pins the contents exactly, so growing the set is a deliberate, reviewed edit
321
+ * rather than a quiet one.
322
+ */
323
+ export const HOST_DISPATCH_ACTION_KEYS = ['overrideNotice'];
212
324
  /**
213
325
  * Keys the spec has TOMBSTONED: still present in `ActionSchema` so the parser can
214
326
  * reject them BY NAME with a rename prescription, rather than fail with a bare
@@ -217,19 +329,36 @@ export const NAVIGATION_ALIAS_KEYS = ['to', 'external', 'newTab', 'replace'];
217
329
  * Warned about separately from unknown keys, and more loudly: an unknown key is
218
330
  * probably a typo, while a retired key is metadata that used to work. That
219
331
  * distinction is why `executeScript` carries a runtime branch returning the
220
- * rename prescription — a branch that exists solely to compensate for the index
221
- * signature, and that can retire with it in step 3.
332
+ * rename prescription.
333
+ *
334
+ * Step 1 expected that branch to retire with the index signature. It does not,
335
+ * and the reason is the one in this module's header: `tsc` closed the
336
+ * code-authored half of the gap, while `execute: 'markDone'` lives in stored
337
+ * rows that reach the runner UNPARSED (objectstack#3903). The branch reads the
338
+ * key off an untyped view of the action precisely so that `ActionDef` can go on
339
+ * NOT declaring it — declaring it to make the read compile would re-legitimize
340
+ * a tombstone the spec keeps only in order to reject it by name.
222
341
  */
223
342
  export const RETIRED_ACTION_KEYS = {
224
343
  execute: '`execute` was removed in @objectstack/spec 17 (#3855) — rename the key to `target`. ' +
225
344
  'The value (a script name or expression) is unchanged. ' +
226
345
  'Run `os migrate meta --from 16` to rewrite it automatically.',
227
346
  };
228
- /** Every key an action may legitimately carry today. */
347
+ /**
348
+ * Every key an action may legitimately carry today — authored or host-composed.
349
+ *
350
+ * Four inputs, three of them mirrors of an authored surface and the fourth
351
+ * ({@link HOST_DISPATCH_ACTION_KEYS}) the host-composed dispatch chrome. The
352
+ * union is what `classifyActionKeys` consults, and it is deliberately wider
353
+ * than the authored surface: the runner classifies the object it was HANDED,
354
+ * and a host hands it a dispatch. Membership here grants nothing to an author —
355
+ * `ActionDef` is what decides that, and it is closed.
356
+ */
229
357
  export const KNOWN_ACTION_KEYS = new Set([
230
358
  ...ACTION_DEF_KEYS,
231
359
  ...SPEC_ACTION_KEYS,
232
360
  ...NAVIGATION_ALIAS_KEYS,
361
+ ...HOST_DISPATCH_ACTION_KEYS,
233
362
  ].filter((key) => !(key in RETIRED_ACTION_KEYS)));
234
363
  /** Split an action's own keys into the two things worth saying out loud. */
235
364
  export function classifyActionKeys(action) {
@@ -264,9 +393,11 @@ const isDev = () => globalThis.process?.env?.NODE_ENV !==
264
393
  * Dev-mode only: name the keys an action carries that no reader will ever look
265
394
  * at. Non-breaking by construction — it changes no types and rejects nothing.
266
395
  *
267
- * Silently binding no handler is the #2169 "Mark Done does nothing" shape, and
268
- * the compiler cannot help while the index signature stands. Until it comes
269
- * down, this is the audible half.
396
+ * Silently binding no handler is the #2169 "Mark Done does nothing" shape. The
397
+ * compiler now catches it in action literals AUTHORED IN CODE — objectstack#4075
398
+ * step 3 closed `ActionDef` — but not in the actions that arrive as DATA, which
399
+ * reach the runner unparsed and which no compiler ever looked at. This is the
400
+ * audible half for that population; see this module's header.
270
401
  */
271
402
  export function warnOnUnknownActionKeys(action, where = 'ActionRunner') {
272
403
  if (!isDev())
@@ -289,6 +420,86 @@ export function warnOnUnknownActionKeys(action, where = 'ActionRunner') {
289
420
  console.warn(`[${where}] action "${String(name)}" carries ${unknown.length === 1 ? 'a key' : 'keys'} no reader ` +
290
421
  `recognizes: \`${unknown.join('`, `')}\`. Nothing will read ${unknown.length === 1 ? 'it' : 'them'} — ` +
291
422
  'check for a typo, or promote the key to an explicit field on `ActionDef` ' +
292
- '(packages/core/src/actions/actionKeys.ts). Warned rather than rejected because `ActionDef` ' +
293
- 'still carries `[key: string]: any`, so the compiler cannot see this (objectstack#4075).');
423
+ '(packages/core/src/actions/ActionRunner.ts), adding it to `ACTION_DEF_KEYS` ' +
424
+ '(packages/core/src/actions/actionKeys.ts) in the SAME commit — the pin test derives one ' +
425
+ 'from the other, so either edit alone goes red. Warned rather than rejected because `tsc` ' +
426
+ 'only sees actions authored in code: this one may have arrived as DATA, and stored ' +
427
+ '`sys_metadata` rows reach the runner unparsed (objectstack#3903).');
428
+ }
429
+ /**
430
+ * Keys a HOST stashes into an action's `params` for the runner's own plumbing,
431
+ * rather than an author writing them there as a request payload.
432
+ *
433
+ * {@link warnOnDeprecatedObjectParams} has to tell "the author put the payload in
434
+ * `params`" (the shape objectstack#5777 retires) apart from "a host stashed row
435
+ * context there" (a shape the runner itself asks for, and that is not going
436
+ * away). `DeclaredActionsBar` dispatches `params: { _rowRecord: record }`,
437
+ * `RelatedRecordActionsBridge` does the same, and `ObjectGrid` adds
438
+ * `_selectedIds`; the api handler lifts `recordId` out of the field values
439
+ * before it updates. Warning on those would fire on nearly every declared-action
440
+ * click and prescribe `bodyExtra`, which is the wrong home for all of them.
441
+ *
442
+ * The `_` prefix is the existing convention for the stash — `recordId` is its one
443
+ * unprefixed member, so it is listed rather than inferred.
444
+ */
445
+ const HOST_STASHED_PARAM_KEYS = new Set(['recordId']);
446
+ const isAuthoredParamKey = (key) => !key.startsWith('_') && !HOST_STASHED_PARAM_KEYS.has(key);
447
+ /**
448
+ * Does this action carry the DEPRECATED object-form `params` — a static request
449
+ * payload written under the key that means "parameter definitions"?
450
+ *
451
+ * The discriminator is `Array.isArray` and the type guard is `api`, mirroring
452
+ * `@objectstack/spec`'s `inline-action-api-params-to-body-extra` conversion
453
+ * (ADR-0087 D2) key for key. Contract parity with the producer is the point: the
454
+ * conversion rewrites exactly this shape to `bodyExtra` at load, so the runner
455
+ * must recognize exactly the same shape as "the thing that got rewritten".
456
+ *
457
+ * The `api` guard is also what keeps the deprecation out of objectstack#6828's
458
+ * territory — object-form `params` on a `type: 'url'` action is a THIRD meaning
459
+ * (`interpolateTarget`'s `${param.X}` scope, `executeUrl`'s `params.newTab`) and
460
+ * `bodyExtra` is NOT its replacement. Do not widen this guard without that
461
+ * ruling.
462
+ */
463
+ export function hasDeprecatedObjectParams(action) {
464
+ if (!action || typeof action !== 'object')
465
+ return false;
466
+ const { type, actionType, params } = action;
467
+ if ((type ?? actionType) !== 'api')
468
+ return false;
469
+ if (!params || typeof params !== 'object' || Array.isArray(params))
470
+ return false;
471
+ return Object.keys(params).some(isAuthoredParamKey);
472
+ }
473
+ /**
474
+ * Dev-mode only: name the object-form `params` that `@objectstack/spec` 17 already
475
+ * refuses at the authoring door, and prescribe `bodyExtra`.
476
+ *
477
+ * This is the audible half of the compat window the maintainer's 2026-08-06
478
+ * ruling on objectstack#5777 ordered (direction A — a separate key, no same-name
479
+ * union): the runner keeps READING a non-array `params` as the static payload for
480
+ * one version window, says so, and the arm is deleted at 18. The loud half lives
481
+ * at the producer, where it belongs — spec's `params` field refuses the object
482
+ * form outright with a message naming `bodyExtra`, and the ADR-0087 conversion
483
+ * rewrites sources that still carry it. So a warning is the correct severity
484
+ * HERE: by the time metadata reaches this runner the producer has already had its
485
+ * say, and what is left to catch is a host composing an ActionDef in code.
486
+ *
487
+ * Dev-only and warn-once, matching {@link warnOnUnknownActionKeys} — a warning
488
+ * that floods a production console is a warning that gets muted.
489
+ */
490
+ export function warnOnDeprecatedObjectParams(action, where = 'ActionRunner') {
491
+ if (!isDev())
492
+ return;
493
+ if (!hasDeprecatedObjectParams(action))
494
+ return;
495
+ const name = action?.name ?? '(unnamed)';
496
+ const memo = `deprecated-object-params:${String(name)}`;
497
+ if (warned.has(memo))
498
+ return;
499
+ warned.add(memo);
500
+ console.warn(`[${where}] action "${String(name)}" carries a static request payload in \`params\`. ` +
501
+ '`params` is the parameter DEFINITION array (fields collected from the user before the ' +
502
+ 'action runs), never a payload map — put the payload in `bodyExtra` instead ' +
503
+ '(objectstack#5777). Still read as the payload for one version window; the arm is ' +
504
+ 'removed at 18. `bodyExtra` is merged LAST, so it overrides anything left here.');
294
505
  }
@@ -47,7 +47,7 @@
47
47
  * - **transport-level `success: false`** — the outer envelope reporting a
48
48
  * failure directly.
49
49
  */
50
- import { actionErrorDetail } from './actionErrorDetail';
50
+ import { actionErrorDetail } from './actionErrorDetail.js';
51
51
  /**
52
52
  * The handler's own return value — one level deeper than the action envelope.
53
53
  *
@@ -14,3 +14,4 @@ export * from './bulkFastPath.js';
14
14
  export * from './actionErrorDetail.js';
15
15
  export * from './actionResponse.js';
16
16
  export * from './serverActionHandler.js';
17
+ export * from './recordIdParam.js';
@@ -14,3 +14,4 @@ export * from './bulkFastPath.js';
14
14
  export * from './actionErrorDetail.js';
15
15
  export * from './actionResponse.js';
16
16
  export * from './serverActionHandler.js';
17
+ export * from './recordIdParam.js';