@object-ui/core 17.6.0 → 18.0.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 (147) hide show
  1. package/CHANGELOG.md +7260 -0
  2. package/README.md +228 -14
  3. package/dist/actions/ActionEngine.d.ts +4 -2
  4. package/dist/actions/ActionEngine.js +13 -8
  5. package/dist/actions/ActionRunner.d.ts +473 -69
  6. package/dist/actions/ActionRunner.js +792 -125
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/UndoManager.d.ts +8 -13
  9. package/dist/actions/actionKeys.d.ts +9 -7
  10. package/dist/actions/actionKeys.js +67 -14
  11. package/dist/actions/actionResponse.js +2 -2
  12. package/dist/actions/index.d.ts +2 -0
  13. package/dist/actions/index.js +2 -0
  14. package/dist/actions/navigationTarget.d.ts +41 -0
  15. package/dist/actions/navigationTarget.js +20 -0
  16. package/dist/actions/recordDelete.d.ts +154 -0
  17. package/dist/actions/recordDelete.js +110 -0
  18. package/dist/adapters/ApiDataSource.d.ts +8 -4
  19. package/dist/adapters/ApiDataSource.js +19 -7
  20. package/dist/adapters/ValueDataSource.d.ts +6 -1
  21. package/dist/adapters/ValueDataSource.js +1090 -98
  22. package/dist/adapters/batchTransaction.d.ts +1 -1
  23. package/dist/adapters/batchTransaction.js +1 -1
  24. package/dist/adapters/resolveDataSource.d.ts +9 -6
  25. package/dist/adapters/resolveDataSource.js +9 -12
  26. package/dist/builder/schema-builder.d.ts +38 -81
  27. package/dist/builder/schema-builder.js +33 -152
  28. package/dist/data-scope/ViewDataProvider.d.ts +8 -25
  29. package/dist/data-scope/ViewDataProvider.js +27 -54
  30. package/dist/data-scope/element-data-source.d.ts +261 -7
  31. package/dist/data-scope/element-data-source.js +342 -52
  32. package/dist/data-scope/index.d.ts +9 -4
  33. package/dist/data-scope/index.js +9 -4
  34. package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
  35. package/dist/evaluator/ExpressionEvaluator.js +184 -12
  36. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  37. package/dist/evaluator/FormulaFunctions.js +166 -27
  38. package/dist/evaluator/deadlineSettled.d.ts +40 -0
  39. package/dist/evaluator/deadlineSettled.js +45 -0
  40. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  41. package/dist/evaluator/declaredPredicate.js +117 -16
  42. package/dist/evaluator/fieldRules.d.ts +88 -4
  43. package/dist/evaluator/fieldRules.js +208 -31
  44. package/dist/evaluator/index.d.ts +2 -0
  45. package/dist/evaluator/index.js +2 -0
  46. package/dist/evaluator/listConditional.d.ts +103 -34
  47. package/dist/evaluator/listConditional.js +127 -50
  48. package/dist/evaluator/optionRules.d.ts +77 -6
  49. package/dist/evaluator/optionRules.js +87 -10
  50. package/dist/evaluator/predicateInput.d.ts +15 -4
  51. package/dist/evaluator/predicateInput.js +35 -2
  52. package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
  53. package/dist/evaluator/rowPredicateCanon.js +65 -115
  54. package/dist/evaluator/subjectPermissions.d.ts +105 -0
  55. package/dist/evaluator/subjectPermissions.js +86 -0
  56. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  57. package/dist/evaluator/unevaluablePredicate.js +70 -0
  58. package/dist/index.d.ts +15 -0
  59. package/dist/index.js +83 -2
  60. package/dist/protocols/index.d.ts +0 -1
  61. package/dist/protocols/index.js +9 -1
  62. package/dist/registry/Registry.d.ts +382 -34
  63. package/dist/registry/Registry.js +247 -12
  64. package/dist/registry/WidgetRegistry.js +7 -3
  65. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  66. package/dist/registry/html-tier-intrinsics.js +110 -0
  67. package/dist/registry/public-blocks.d.ts +8 -0
  68. package/dist/registry/public-blocks.js +57 -0
  69. package/dist/theme/ThemeEngine.js +1 -1
  70. package/dist/utils/affordanceGrants.d.ts +371 -0
  71. package/dist/utils/affordanceGrants.js +252 -0
  72. package/dist/utils/chart-category-key.d.ts +82 -0
  73. package/dist/utils/chart-category-key.js +84 -0
  74. package/dist/utils/chart-measure-key.d.ts +64 -0
  75. package/dist/utils/chart-measure-key.js +65 -0
  76. package/dist/utils/chart-presentation.d.ts +95 -112
  77. package/dist/utils/chart-presentation.js +169 -104
  78. package/dist/utils/chart-series.d.ts +165 -5
  79. package/dist/utils/chart-series.js +123 -18
  80. package/dist/utils/column-sortability.d.ts +157 -0
  81. package/dist/utils/column-sortability.js +217 -0
  82. package/dist/utils/dashboard-filters.d.ts +38 -6
  83. package/dist/utils/dashboard-filters.js +107 -61
  84. package/dist/utils/dataset-format.d.ts +211 -30
  85. package/dist/utils/dataset-format.js +389 -46
  86. package/dist/utils/date-display.d.ts +346 -0
  87. package/dist/utils/date-display.js +644 -0
  88. package/dist/utils/date-macros.js +7 -0
  89. package/dist/utils/drill-down.d.ts +78 -0
  90. package/dist/utils/drill-down.js +93 -0
  91. package/dist/utils/emptiness.d.ts +96 -0
  92. package/dist/utils/emptiness.js +101 -0
  93. package/dist/utils/expand-fields.d.ts +81 -9
  94. package/dist/utils/expand-fields.js +81 -9
  95. package/dist/utils/extract-records.d.ts +65 -5
  96. package/dist/utils/extract-records.js +65 -8
  97. package/dist/utils/filter-converter.d.ts +158 -6
  98. package/dist/utils/filter-converter.js +1098 -24
  99. package/dist/utils/filter-tokens.d.ts +49 -5
  100. package/dist/utils/filter-tokens.js +105 -33
  101. package/dist/utils/grouping-fields.d.ts +61 -0
  102. package/dist/utils/grouping-fields.js +84 -0
  103. package/dist/utils/managedBy.d.ts +3 -3
  104. package/dist/utils/managedBy.js +5 -5
  105. package/dist/utils/native-date-value.d.ts +48 -0
  106. package/dist/utils/native-date-value.js +167 -0
  107. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  108. package/dist/utils/non-grid-row-ceiling.js +141 -0
  109. package/dist/utils/normalize-list-view.d.ts +135 -11
  110. package/dist/utils/normalize-list-view.js +243 -64
  111. package/dist/utils/number-display.d.ts +63 -14
  112. package/dist/utils/number-display.js +34 -12
  113. package/dist/utils/object-aggregate-query.d.ts +113 -0
  114. package/dist/utils/object-aggregate-query.js +50 -0
  115. package/dist/utils/parent-scope.d.ts +123 -0
  116. package/dist/utils/parent-scope.js +140 -0
  117. package/dist/utils/predicate-fields.d.ts +45 -12
  118. package/dist/utils/predicate-fields.js +141 -12
  119. package/dist/utils/record-color.d.ts +75 -0
  120. package/dist/utils/record-color.js +65 -0
  121. package/dist/utils/record-source.d.ts +298 -0
  122. package/dist/utils/record-source.js +331 -0
  123. package/dist/utils/record-title.d.ts +148 -8
  124. package/dist/utils/record-title.js +220 -26
  125. package/dist/utils/reference-keys.d.ts +21 -22
  126. package/dist/utils/reference-keys.js +532 -29
  127. package/dist/utils/sort-query.d.ts +115 -15
  128. package/dist/utils/sort-query.js +240 -13
  129. package/dist/utils/text-comparand.d.ts +92 -0
  130. package/dist/utils/text-comparand.js +99 -0
  131. package/dist/utils/without-denied-fields.d.ts +49 -0
  132. package/dist/utils/without-denied-fields.js +52 -0
  133. package/dist/validation/index.d.ts +8 -4
  134. package/dist/validation/index.js +8 -4
  135. package/dist/validation/schema-validator.d.ts +1 -1
  136. package/dist/validation/schema-validator.js +225 -83
  137. package/dist/validation/server-owned-value.d.ts +5 -1
  138. package/dist/validation/server-owned-value.js +5 -1
  139. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  140. package/dist/validation/validators/object-validation-engine.js +14 -3
  141. package/package.json +8 -9
  142. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  143. package/dist/data-scope/DataScopeManager.js +0 -229
  144. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  145. package/dist/protocols/ResponsiveProtocol.js +0 -158
  146. package/dist/validation/validation-engine.d.ts +0 -88
  147. package/dist/validation/validation-engine.js +0 -434
@@ -19,43 +19,37 @@
19
19
  * Features: conditional execution, confirmation, toast notifications,
20
20
  * redirect handling, action chaining, custom handler registration.
21
21
  */
22
- import type { UIActionSchema } from '@object-ui/types';
22
+ import type { ActionContext, ActionResult, UIActionSchema } from '@object-ui/types';
23
23
  import type { Action as SpecActionInput } from '@objectstack/spec/ui';
24
24
  import { ExpressionEvaluator } from '../evaluator/ExpressionEvaluator.js';
25
- import { type UndoableOperation } from './UndoManager.js';
26
- export interface ActionResult {
27
- success: boolean;
28
- data?: any;
29
- error?: string;
30
- reload?: boolean;
31
- close?: boolean;
32
- redirect?: string;
33
- /** Modal schema to render (for type: 'modal') */
34
- modal?: any;
35
- /**
36
- * Suppress the automatic success toast for this result. A handler sets this
37
- * when the action only HANDED OFF to a follow-up UI rather than completing —
38
- * e.g. a `flow` action that paused at a screen and opened the flow-runner. The
39
- * action hasn't "completed yet", so a "success" toast on open would be
40
- * misleading; the follow-up surface owns its own completion messaging.
41
- */
42
- silent?: boolean;
43
- /**
44
- * An undoable operation captured by the handler (e.g. an `undoable` update
45
- * action's prior field values). When present, the runner pushes it onto the
46
- * global UndoManager and the success toast offers an "Undo" affordance.
47
- */
48
- undo?: UndoableOperation;
49
- }
50
- export interface ActionContext {
51
- data?: Record<string, any>;
52
- record?: any;
53
- selectedRecords?: Record<string, any>[];
54
- /** Live page-variable snapshot (ADR-0049), published by PageVariableActionBridge. */
55
- pageVariables?: Record<string, any>;
56
- user?: any;
57
- [key: string]: any;
58
- }
25
+ import { type FieldContainerLike } from '../utils/predicate-record.js';
26
+ /**
27
+ * What an action handler returns and the runner reads — ONE authority, in
28
+ * `@object-ui/types` (objectui#6349, batch 4), re-exported here exactly as
29
+ * {@link ActionContext} is below. The members this module used to declare
30
+ * (`reload`, `redirect`, `modal`, `silent`, `undo`) moved down with it, and
31
+ * `UndoableOperation`, which types `undo`, moved down beside it
32
+ * (`./UndoManager.js` re-exports that one). The gate that counts authorities is
33
+ * `scripts/__tests__/one-authority-per-exported-name-6273.test.ts`; a re-export
34
+ * is not one.
35
+ */
36
+ export type { ActionResult } from '@object-ui/types';
37
+ /**
38
+ * The context a runner executes actions in — ONE authority, in
39
+ * `@object-ui/types` (objectui#6349, batch 4). This module RE-EXPORTS that
40
+ * declaration instead of declaring a second one, so an import of
41
+ * `ActionContext` from `@object-ui/core` and one from `@object-ui/types` are
42
+ * the same type. `@object-ui/types` is the dependency-legal side: it depends on
43
+ * no `@object-ui/*` package, and this package depends on it.
44
+ *
45
+ * A re-export is not a second authority —
46
+ * `scripts/__tests__/one-authority-per-exported-name-6273.test.ts` counts
47
+ * declarations and ALIASING re-exports, never `export type { X } from …` —
48
+ * which is why this convergence takes `ActionContext` off that gate's
49
+ * `KNOWN_COLLISIONS` baseline. The member drift it removed is recorded on the
50
+ * declaration itself.
51
+ */
52
+ export type { ActionContext } from '@object-ui/types';
59
53
  /**
60
54
  * API configuration for complex requests.
61
55
  */
@@ -168,8 +162,8 @@ export interface ActionDef {
168
162
  * field, because the 2026-08-06 maintainer ruling on objectstack#4075 gave
169
163
  * both keys ONE shape: `boolean | string(CEL) | { dialect, source }` —
170
164
  * "boolean = 条件的退化形字面量,string = CEL 简写,信封 = 完整形". The
171
- * envelope arm arrived in `@objectstack/spec` 17.0.0-rc.6 (objectstack#5970,
172
- * PR objectstack#6450); until then this was a hand-written `string | boolean`
165
+ * envelope arm arrived in `@objectstack/spec` 17.0.0-rc.6 (objectstack
166
+ * `97e7e3caa`); until then this was a hand-written `string | boolean`
173
167
  * that could not describe the envelope, which is why `DeclaredActionsBar`
174
168
  * read it through an `(action as any).disabled` cast. Derived, not restated,
175
169
  * so the two keys cannot drift apart again.
@@ -233,7 +227,8 @@ export interface ActionDef {
233
227
  showOnError?: boolean;
234
228
  duration?: number;
235
229
  };
236
- /** Success message (from UIActionSchema) */
230
+ /** Success message (from UIActionSchema). The success toast's second rung,
231
+ * after {@link outcomeMessages}; may interpolate `${result.*}`. */
237
232
  successMessage?: string;
238
233
  /** Error message (from UIActionSchema) */
239
234
  errorMessage?: string;
@@ -256,9 +251,9 @@ export interface ActionDef {
256
251
  * `'url'`, flow name for `'flow'`, modal/page for `'modal'`, endpoint for
257
252
  * `'api'`, FormView name for `'form'`.
258
253
  *
259
- * The `execute` alias was REMOVED in @objectstack/spec 17 (#3855) and is not
260
- * read here (#3856) — don't re-add it. Two handler slots is how one action ran
261
- * one script server-side and a different one client-side (#3713).
254
+ * The `execute` alias was REMOVED in @objectstack/spec 17 (objectstack-ai/objectstack#3855) and is not
255
+ * read here (objectstack-ai/objectstack#3856) — don't re-add it. Two handler slots is how one action ran
256
+ * one script server-side and a different one client-side (objectstack-ai/objectstack#3713).
262
257
  */
263
258
  target?: string;
264
259
  /**
@@ -291,8 +286,6 @@ export interface ActionDef {
291
286
  chain?: ActionDef[];
292
287
  /** Chain execution mode */
293
288
  chainMode?: 'sequential' | 'parallel';
294
- /** Callback on success */
295
- onSuccess?: ActionDef | ActionDef[];
296
289
  /** Callback on failure */
297
290
  onFailure?: ActionDef | ActionDef[];
298
291
  /** When true, the runner pre-opens about:blank synchronously on click so the
@@ -323,7 +316,7 @@ export interface ActionDef {
323
316
  * spec 采纳 —— `visible` / `disabled` 两键在 spec 侧统一收敛为
324
317
  * `boolean | string(CEL) | {dialect, source}`(boolean = 条件的退化形字面量,
325
318
  * string = CEL 简写,信封 = 完整形)". `@objectstack/spec` 17.0.0-rc.6 carries
326
- * it (objectstack#5970, PR objectstack#6450), so the local `| boolean` is no
319
+ * it (objectstack `97e7e3caa`), so the local `| boolean` is no
327
320
  * longer a tolerance to declare — it is part of the derived type, and adding
328
321
  * it back would restate a spec arm rather than widen anything.
329
322
  */
@@ -343,12 +336,42 @@ export interface ActionDef {
343
336
  objectName?: SpecActionInput['objectName'];
344
337
  /** AI affordance metadata (tool exposure, prompts). */
345
338
  ai?: SpecActionInput['ai'];
346
- /** Accessibility overrides for the rendered control. */
347
- aria?: SpecActionInput['aria'];
348
339
  /** Extra properties merged into the request body alongside the collected params. */
349
340
  bodyExtra?: SpecActionInput['bodyExtra'];
350
341
  /** How collected params are shaped into the request body (`flat` or nested). */
351
342
  bodyShape?: SpecActionInput['bodyShape'];
343
+ /**
344
+ * The declarative single-record field write — a parallel key BESIDE `type`,
345
+ * never a member of it.
346
+ *
347
+ * `ActionType` did not gain a member: the spec materializes `type: 'script'`
348
+ * on an `operation: 'update'` action, because the write is performed on the
349
+ * platform action route — `POST /api/v1/actions/{object}/{action}`, the route
350
+ * the registered `script` dispatch already posts — as the CALLER, so the
351
+ * caller's permissions, the object's hooks and its validations all fire as
352
+ * for a user edit. That is why {@link execute} reads this key BEFORE `type`:
353
+ * by the time `type` is consulted an update action is indistinguishable from
354
+ * any other script action, and it would run without the update path's Undo
355
+ * capture and its no-route refusal.
356
+ */
357
+ operation?: SpecActionInput['operation'];
358
+ /**
359
+ * For `operation: 'update'` — static field values written to the current
360
+ * record, merged UNDER the collected `params` so a fixed value can be
361
+ * declared without exposing it in the dialog. The spec refuses it on an
362
+ * action without `operation: 'update'`, where it would be silently dropped.
363
+ *
364
+ * The ROUTE applies it, from the declaration it resolves by name; this
365
+ * runner never sends it (objectui#12941) and reads it only to know which
366
+ * fields an Undo must restore.
367
+ *
368
+ * It is a DECLARATION's key here: a host that dispatches an object's action
369
+ * whole hands it along. No action renderer forwards a page row's own `patch`
370
+ * (objectui#13022, following `@objectstack/spec` 18's retirement of the row
371
+ * key), and the runner reads the host-published declaration ahead of this
372
+ * one (`declaredUpdatePatch`).
373
+ */
374
+ patch?: SpecActionInput['patch'];
352
375
  /** Execution mode of the action. */
353
376
  mode?: SpecActionInput['mode'];
354
377
  /** Field on the record supplying the record id sent to the endpoint. */
@@ -357,6 +380,41 @@ export interface ActionDef {
357
380
  recordIdParam?: SpecActionInput['recordIdParam'];
358
381
  /** Auth/tenancy feature the action requires before it is offered. */
359
382
  requiresFeature?: SpecActionInput['requiresFeature'];
383
+ /**
384
+ * Declared post-success navigation — the spec's closed strict
385
+ * `{ navigate, openIn }` block (`ActionSchema.onSuccess`, authorable since
386
+ * `@objectstack/spec` 17.1.0, objectui#5328). Read by `handlePostExecution`
387
+ * → `readOnSuccessNavigation` → `navigateOnSuccess`.
388
+ *
389
+ * This key carried a SECOND, older meaning until objectui#5934: the runner's
390
+ * own chained-callback channel, `ActionDef | ActionDef[]`, dispatched through
391
+ * `executeChain`. The maintainer retired that channel on 2026-08-31 — the
392
+ * spec strict-refuses a callback shape here (`{ type: … }` fails parse with
393
+ * `unrecognized_keys`, so no validated metadata could ever reach it), and the
394
+ * census found zero producers outside the channel's own pins. `onSuccess`
395
+ * now means exactly what the contract declares, nothing else; a callback
396
+ * shape gets NO reading (not a fallback, not an error — the same "a shape
397
+ * the spec refuses gets no new reading here" rule the discrimination branch
398
+ * used to apply, now with nothing left to discriminate). `onFailure`, in the
399
+ * runner-native section above, is untouched: the spec declares no such key,
400
+ * so it has only ever had its one runner-native meaning.
401
+ */
402
+ onSuccess?: SpecActionInput['onSuccess'];
403
+ /**
404
+ * Success copy per handler outcome (`ActionSchema.outcomeMessages`, ruling A
405
+ * on objectstack-ai/cloud#2315): the server answers with a closed `outcome`
406
+ * fact and this map holds the sentence for each, keyed by the snake_case
407
+ * outcome name. Read by `handlePostExecution` → `composeSuccessMessage`, the
408
+ * success toast's FIRST rung: the entry named by the payload's top-level
409
+ * `outcome` (the payload `${result.*}` reads), then {@link successMessage},
410
+ * then the runner's own default text.
411
+ *
412
+ * Derived, so an entry is an `I18nLabel` — a string or an inline per-locale
413
+ * map. The runner composes from strings only: the host's localizer
414
+ * (`useActionTextLocalizer` in `@object-ui/react`) resolves each entry to the
415
+ * active language before dispatch, because the runner has no language.
416
+ */
417
+ outcomeMessages?: SpecActionInput['outcomeMessages'];
360
418
  /**
361
419
  * @deprecated Retired in `@objectstack/spec` 17 as a `retiredKey()` tombstone —
362
420
  * authoring it is a hard parse rejection, so this resolves to `undefined` and
@@ -404,6 +462,11 @@ export type ActionRunnerHandler = (action: ActionDef, context: ActionContext) =>
404
462
  * `.cancel`), and `app-shell`'s `ActionConfirmDialog.tsx` renders them in
405
463
  * place of its generic `actionConfirm.*` fallbacks. Line numbers are left out
406
464
  * on purpose: #5205's pointer had already drifted by the time it was executed.
465
+ * The record delete is a second producer (objectui#11695): `app-shell`'s
466
+ * `useObjectActions` (the list's row and bulk Delete) and `RecordDetailView`
467
+ * (the record page's Delete) hand it `recordDelete.confirmCopy`'s title and
468
+ * confirm label with `destructive: true`, which paints the confirm button in
469
+ * the destructive button style.
407
470
  *
408
471
  * So re-measure across the repo, not this package: grep for two-argument
409
472
  * calls of `ConfirmationHandler`-typed values (`confirmHandler(`, `onConfirm(`)
@@ -417,6 +480,8 @@ export type ConfirmationHandler = (message: string, options?: {
417
480
  title?: string;
418
481
  confirmText?: string;
419
482
  cancelText?: string;
483
+ /** The confirmed action destroys data: paint the confirm button destructive. */
484
+ destructive?: boolean;
420
485
  }) => Promise<boolean>;
421
486
  /**
422
487
  * Toast handler — consumers can wire to Sonner or any toast library.
@@ -446,12 +511,58 @@ export type NavigationHandler = (url: string, options?: {
446
511
  * Param collection handler — consumers provide to show a dialog
447
512
  * for collecting ActionParam values before action execution.
448
513
  * Returns collected values, or null if cancelled.
514
+ *
515
+ * `interpolate` fills the `${...}` holes of the dialog's authored copy (the
516
+ * action's `description`, each param's `helpText`) in the same template scope
517
+ * the runner reads `confirmText` in — for a row dispatch, the row is `record`
518
+ * (see `templateEvaluatorFor`). The runner cannot fill them itself before the
519
+ * call: the handler localizes that copy AFTER it receives it (a translation
520
+ * bundle entry or an inline per-locale map), so the hole has to be filled on
521
+ * the text the user will actually read (objectui#12967).
522
+ *
523
+ * `run` lets the handler keep its dialog open until the run its values start
524
+ * has an outcome (objectui#13161). Called with the collected values, it
525
+ * dispatches the action with them and resolves with that run's
526
+ * {@link ActionResult} (a dispatch that throws resolves as a refused run, the
527
+ * way `execute` reports one). A refused run has already had its
528
+ * post-execution (the error toast, `onFailure`), so the handler can show the
529
+ * refusal beside the values the user typed and call `run` again with the
530
+ * edited values. A successful run's post-execution (success toast, result
531
+ * dialog, chain, `onSuccess`) waits until the handler settles its promise,
532
+ * so the dialog is closed before any of it shows. A call made while an
533
+ * earlier one is still in flight returns that earlier run, and dispatches
534
+ * nothing new.
535
+ *
536
+ * Once `run` has been called, the handler's own promise only ends the
537
+ * session: the runner never dispatches the values it resolves with. `null`
538
+ * after a refused run is a cancel, reported exactly as a cancel before any
539
+ * run. A handler that never calls `run` keeps the old contract: the runner
540
+ * dispatches once with the values it resolves.
541
+ */
542
+ export type ParamCollectionHandler = (params: ActionParamDef[], action?: ActionDef, interpolate?: (text: string) => string, run?: (values: Record<string, any>) => Promise<ActionResult>) => Promise<Record<string, any> | null>;
543
+ /**
544
+ * The contract's own result-dialog block, and one entry of its field list.
545
+ *
546
+ * ⭐ The two interfaces below DERIVE their label members from these instead of
547
+ * restating them, and that is the whole repair of `43c0d1710`: the three
548
+ * label members were hand-written `string` while the producer declares each as
549
+ * `I18nLabel` — a plain string **or** an inline per-locale map, both authorized
550
+ * and neither deprecated — so this mirror refused what the platform accepts
551
+ * while its own docblock claimed the alignment. A hand copy can drift again the
552
+ * moment the contract moves; a derived member makes `tsc` re-check the claim on
553
+ * every build instead of trusting a sentence written once.
554
+ *
555
+ * ⛔ Local, deliberately not exported: the two interfaces below stay the only
556
+ * names this module publishes for the block.
449
557
  */
450
- export type ParamCollectionHandler = (params: ActionParamDef[], action?: ActionDef) => Promise<Record<string, any> | null>;
558
+ type SpecResultDialog = NonNullable<SpecActionInput['resultDialog']>;
559
+ type SpecResultDialogField = NonNullable<SpecResultDialog['fields']>[number];
451
560
  /**
452
561
  * Result dialog spec — declarative description of how to render a
453
562
  * one-shot reveal of an action's API response. Mirrors
454
- * `Action.resultDialog` in @objectstack/spec.
563
+ * `Action.resultDialog` in @objectstack/spec — derived from it member by
564
+ * member for the labels, so the claim in this sentence is enforced rather
565
+ * than asserted.
455
566
  *
456
567
  * When set on an action and the action succeeds, the runner suppresses
457
568
  * the success toast and awaits a ResultDialogHandler instead. Used for
@@ -460,13 +571,22 @@ export type ParamCollectionHandler = (params: ActionParamDef[], action?: ActionD
460
571
  */
461
572
  export interface ResultDialogFieldSpec {
462
573
  path: string;
463
- label?: string;
574
+ /**
575
+ * Derived `I18nLabel` — a plain string, or an inline per-locale map. A
576
+ * renderer must RESOLVE it (`resolveI18nLabel` in `@objectstack/spec/ui`)
577
+ * before it reaches the DOM; passed through as-is, the map arm is an object
578
+ * and React refuses an object as a child.
579
+ */
580
+ label?: SpecResultDialogField['label'];
464
581
  format?: 'qrcode' | 'code-list' | 'secret' | 'text' | 'json';
465
582
  }
466
583
  export interface ResultDialogSpec {
467
- title?: string;
468
- description?: string;
469
- acknowledge?: string;
584
+ /** Derived `I18nLabel` — see `ResultDialogFieldSpec.label` on resolving it. */
585
+ title?: SpecResultDialog['title'];
586
+ /** Derived `I18nLabel` — see `ResultDialogFieldSpec.label` on resolving it. */
587
+ description?: SpecResultDialog['description'];
588
+ /** Derived `I18nLabel` — see `ResultDialogFieldSpec.label` on resolving it. */
589
+ acknowledge?: SpecResultDialog['acknowledge'];
470
590
  format?: 'qrcode' | 'code-list' | 'secret' | 'text' | 'json';
471
591
  fields?: ResultDialogFieldSpec[];
472
592
  }
@@ -486,7 +606,7 @@ export type ResultDialogHandler = (spec: ResultDialogSpec, data: unknown, action
486
606
  * `visibleWhen` (the CEL predicate `resolveVisibleOptions()` filters on —
487
607
  * ADR-0058 / #2284), `color`, `icon`, `disabled`.
488
608
  *
489
- * The catch-all is deliberate rather than a closed key list (objectui#3559).
609
+ * The catch-all is deliberate rather than a closed key list (`fbc23e094`).
490
610
  * A field's option vocabulary is owned by the field metadata (`@objectstack/spec`'s
491
611
  * `SelectOptionSchema`) and read by the option widgets; a param's option list is
492
612
  * only a CONDUIT between the two. When this type restated that vocabulary as
@@ -530,19 +650,36 @@ export interface ActionParamDef {
530
650
  placeholder?: string;
531
651
  /**
532
652
  * Visibility predicate (CEL) evaluated against the same scope as action
533
- * `visible` (`current_user` / `app` / `data` / `features`). When it evaluates
653
+ * `visible` (`current_user` / `data` / `features` — ⛔ not `app`, unbound by
654
+ * objectui#8155). When it evaluates
534
655
  * false the param dialog omits this param — used to hide a param the backend
535
656
  * only accepts under an opt-in capability (e.g. `create_user.phoneNumber`
536
657
  * gated on `features.phoneNumber`). Absent = always visible.
537
658
  */
538
659
  visible?: string;
660
+ /**
661
+ * Carry-over declaration — `@objectstack/spec`'s `ActionParamSchema.carryOver`
662
+ * (the 2026-08-25 ruling whose spec half is objectstack `0e4e51b0a`,
663
+ * objectui#6246), passed through unchanged by
664
+ * `resolveActionParams()`. The param's value is carried through the dialog
665
+ * rather than collected from the user: seeded from the row (the spec refuses
666
+ * the key without `defaultFromRow: true`), rendered by `ActionParamDialog` as
667
+ * a collapsed read-only summary with no editing affordance, and submitted
668
+ * verbatim.
669
+ *
670
+ * ⛔ Not a styling hint. The permission-set Clone action declares it on its
671
+ * JSON permission facets because an editable facet lets a hand-edited but
672
+ * valid blob clone a set that grants MORE than its base; a renderer that
673
+ * merely greys the control out still offers that edit.
674
+ */
675
+ carryOver?: boolean;
539
676
  /** Accepted upload types (MIME types / extensions) for `file`/`image` params. */
540
677
  accept?: string[];
541
678
  /** Max upload size in bytes for `file`/`image` params. */
542
679
  maxSize?: number;
543
680
  /** Allow multiple values (file/image/lookup/user params → array value). */
544
681
  multiple?: boolean;
545
- /** Object name the lookup picker queries (`reference_to` on the field). */
682
+ /** Object name the lookup picker queries (`reference` on the field). */
546
683
  referenceTo?: string;
547
684
  /** Field on the referenced record used as the human label (default `name`). */
548
685
  displayField?: string;
@@ -560,7 +697,87 @@ export interface ActionParamDef {
560
697
  lookupPageSize?: number;
561
698
  /** Form-field dependencies that gate / parameterise the picker query. */
562
699
  dependsOn?: unknown[];
700
+ /**
701
+ * Set by `resolveActionParams()` when a FIELD-BACKED param (`{ field }`)
702
+ * named a field that is not in the object metadata the resolver was given,
703
+ * carrying `<object>.<field>` — the pair that could not be resolved.
704
+ *
705
+ * It exists because the alternative is a SILENT one. Without it the resolver
706
+ * hands back `type: param.type ?? 'text'`, and every downstream reader of the
707
+ * degradation is blind to it: the param is a `text` param by then, so
708
+ * `paramDegradesWithoutTarget()` answers false, `paramToField()` emits no
709
+ * "no reference target" warning, and the objectstack-ai/objectstack#3405 "paste a record id" hints do
710
+ * not apply either. A lookup param that should have rendered a record picker
711
+ * renders an unannotated empty box instead — no options, no dropdown, and no
712
+ * request for the referenced object on the wire, because no picker was ever
713
+ * built (`6cc910b6d`).
714
+ *
715
+ * ⛔ It is not a widget config key and `paramToField()` deliberately does not
716
+ * map it: the whole point is that the param's type is UNKNOWN, so there is no
717
+ * widget to configure. `ActionParamDialog` reads it and refuses.
718
+ */
719
+ unresolvedField?: string;
563
720
  }
721
+ /**
722
+ * The fields an `operation: 'update'` action writes, and their prior values.
723
+ *
724
+ * `undoable` promises to restore EXACTLY the fields the action wrote, so the
725
+ * captured set is the effective payload — `patch` merged under the collected
726
+ * `params` — and never `patch` alone: a param-collected field left out of the
727
+ * capture would make Undo restore SOME of the edit and report a full one, which
728
+ * is worse than offering no Undo at all.
729
+ *
730
+ * Prior values come from the row record the invoking surface stashed under
731
+ * `params._rowRecord` (the same client-side stash the record-id dance reads).
732
+ * A field the row carries as `null` is captured as `null`: it was empty, and
733
+ * clearing it back to empty is what restoring it means on the data plane.
734
+ *
735
+ * ⛔ A field the row does not CARRY is never captured as `null`
736
+ * (objectui#10404). Absent is not empty: a list row is projected by `$select`,
737
+ * and a written field no column shows is missing from the row while the server
738
+ * holds a real value for it. Recording `null` there made Undo write `null` over
739
+ * the value it existed to restore. The list harvest (`listViewPredicates`) now
740
+ * asks for the written fields of an `undoable` action, so a projected row
741
+ * carries them; this is the backstop for the ones it cannot ask for (a field
742
+ * the object does not declare or the principal may not read, a row no harvest
743
+ * shaped). "Carries" means an own key whose value is not `undefined`: JSON
744
+ * cannot send `undefined`, so such a value says nothing about what is stored.
745
+ *
746
+ * Answers `undefined` when any written field is not carried, and the caller
747
+ * then offers no Undo at all. Capturing only the carried fields is not an
748
+ * option, for the reason above: a partial restore reported as a full one is
749
+ * worse than no Undo.
750
+ *
751
+ * ⛔ A relation's prior value is its STORED id, never the record `$expand`
752
+ * put in its place (objectui#11122). Surfaces read rows with `$expand` on the
753
+ * relations they show (a grid, its visible and grouping columns; the record
754
+ * page, every relation the reader may read), and the server replaces the id
755
+ * in place with the related record. Copied verbatim, that record became the
756
+ * Undo value and Undo wrote it into the reference slot, which stores an id:
757
+ * refused under a strict value-shape posture, stored as corruption under the
758
+ * lenient one. So each carried value goes through `toPredicateRecord`, the one
759
+ * rule that binds a fetched record the way the server stores it: a field the
760
+ * object DECLARES relational (`EXPANDABLE_FIELD_TYPES`, the set that decides
761
+ * what is expanded in the first place) collapses to its id, element-wise for a
762
+ * `multiple` relation, and every other field is left exactly as the row
763
+ * carries it. It is read from `fields`, never from the value's shape: a `json`
764
+ * field may hold an object with an `id`, and that object is its stored value.
765
+ *
766
+ * `fields` is the written object's field definitions (`objectSchema.fields`,
767
+ * either served shape), and it is REQUIRED so that a caller without them has
768
+ * to say so: `undefined` collapses nothing, because a caller that cannot name
769
+ * the relations must not guess them.
770
+ *
771
+ * Exported (objectui#11082) because it is THE rule, not this runner's: every
772
+ * surface that builds an `update` Undo snapshot itself calls it rather than
773
+ * restating it. Today that is the console runtime's `api` handler
774
+ * (`useConsoleActionRuntime`) and the record page's own `api` handler
775
+ * (`RecordDetailView`), which reads prior values off the page's loaded record;
776
+ * that record lacks a written field when field-level security hides it from
777
+ * the reader. To name the fields that blocked a capture, ask per field:
778
+ * `captureUpdateUndoData([field], rowRecord, fields) === undefined`.
779
+ */
780
+ export declare function captureUpdateUndoData(writtenFields: readonly string[], rowRecord: Record<string, unknown>, fields: FieldContainerLike): Record<string, unknown> | undefined;
564
781
  export declare class ActionRunner {
565
782
  private handlers;
566
783
  private scripts;
@@ -572,6 +789,7 @@ export declare class ActionRunner {
572
789
  private navigationHandler;
573
790
  private paramCollectionHandler;
574
791
  private resultDialogHandler;
792
+ private translate;
575
793
  /**
576
794
  * Built-in dispatch, one entry per runnable action type.
577
795
  *
@@ -588,6 +806,20 @@ export declare class ActionRunner {
588
806
  */
589
807
  private readonly builtinExecutors;
590
808
  constructor(context?: ActionContext);
809
+ /**
810
+ * Set the translator for the text the runner supplies itself — the host's
811
+ * `t`, injected so this package takes no i18n dependency. That text is the
812
+ * `RUNNER_TEXT` table in this file: the generic success toast shown when an
813
+ * action declares neither an `outcomeMessages` entry for the answer nor a
814
+ * `successMessage`, the error fallbacks when no readable error message
815
+ * reached the runner, and the Undo label of an undoable success toast. An
816
+ * author's `outcomeMessages` / `successMessage` / `errorMessage` and an
817
+ * action's own error never go through this translator: the host localizes
818
+ * authored copy before dispatch. With no translator the text stays English.
819
+ */
820
+ setTranslator(translate: (key: string, options: {
821
+ defaultValue: string;
822
+ }) => string): void;
591
823
  /**
592
824
  * Set a custom confirmation handler (e.g., Shadcn AlertDialog).
593
825
  */
@@ -625,11 +857,72 @@ export declare class ActionRunner {
625
857
  */
626
858
  registerScript(scriptName: string, handler: ActionRunnerHandler): void;
627
859
  unregisterScript(scriptName: string): void;
628
- execute(action: ActionDef): Promise<ActionResult>;
860
+ /**
861
+ * Run `action`; with `callContext`, run it against that context for this
862
+ * one dispatch (objectui#13063).
863
+ *
864
+ * The runner's context names the PAGE: its `record` and its `objectName`.
865
+ * A dispatch that runs against some other record, such as a related-list
866
+ * row on a record page, which is a record of the CHILD object, hands that
867
+ * record here as `{ record, objectName }`. Every executor then reads it as
868
+ * the record the action runs against, the way a dispatch from that record's
869
+ * own page does: `executeForm` forwards its id and object, and its
870
+ * boundary check compares the target view against the child object instead
871
+ * of the page's.
872
+ *
873
+ * The runner's own context is not touched: the dispatch runs on a one-shot
874
+ * runner over the merged context that shares this runner's handlers (see
875
+ * {@link ActionRunner.scopedTo}).
876
+ */
877
+ execute(action: ActionDef, callContext?: Partial<ActionContext>): Promise<ActionResult>;
878
+ /**
879
+ * Dispatch an action whose params are collected, and answer what it
880
+ * returned — WITHOUT post-execution, which each caller runs itself:
881
+ * `execute` right after a lone run, and the param-collection session
882
+ * (`ParamCollectionHandler`'s `run`, objectui#13161) once per refused attempt
883
+ * and once for the successful one when the session ends.
884
+ */
885
+ private dispatchCollected;
629
886
  /**
630
887
  * Execute multiple actions in sequence or parallel.
631
888
  */
632
889
  executeChain(actions: ActionDef[], mode?: 'sequential' | 'parallel'): Promise<ActionResult>;
890
+ /**
891
+ * One entry of `RUNNER_TEXT`, in the installed translator's language. An
892
+ * empty answer from the translator falls back to the English source rather
893
+ * than raising an empty toast or an empty label.
894
+ */
895
+ private runnerText;
896
+ /**
897
+ * The success toast's text, by the contract's three rungs
898
+ * (`ActionSchema.outcomeMessages`, objectui#11344):
899
+ *
900
+ * 1. `outcomeMessages[outcome]`, where `outcome` is the top-level `outcome`
901
+ * of the handler's return value — the payload `${result.*}` reads, so it
902
+ * is the same fact an author writes as `${result.outcome}`;
903
+ * 2. `successMessage`;
904
+ * 3. the runner's own default text, in its translator's language.
905
+ *
906
+ * Rungs 1 and 2 are the author's copy. Both arrive already localized — the
907
+ * host's localizer resolves the bundle entry or the inline `I18nLabel` map
908
+ * before dispatch — and both have their `${result.*}` tokens filled in here,
909
+ * through the one token grammar and scope `onSuccess.navigate` reads
910
+ * (`substituteScopeTokens` over `readActionPayload(result.data)`). Only the
911
+ * sink differs: a toast is text, so a value is inserted as-is, where a URL
912
+ * position percent-encodes it.
913
+ *
914
+ * ⛔ `result.data.message` is not a rung. It used to outrank both rungs of
915
+ * author copy, which put whatever language the server happened to write in
916
+ * front of the user's locale; the contract names no server-message rung, and
917
+ * an author who wants the server's sentence can still write
918
+ * `${result.message}` as the copy.
919
+ *
920
+ * A rung counts only when it is a non-empty string. An `I18nLabel` map that
921
+ * reached the runner without passing through a localizer has no language to
922
+ * resolve against here, so it is skipped rather than handed to the toast as
923
+ * an object — the React #31 crash the error branch below guards against.
924
+ */
925
+ private composeSuccessMessage;
633
926
  /**
634
927
  * Post-execution: emit toast notifications, handle chaining, callbacks.
635
928
  */
@@ -642,7 +935,9 @@ export declare class ActionRunner {
642
935
  * react-router's `navigate` — so `openIn: 'self'` is a real SPA route hop,
643
936
  * in place, immune to popup blocking. No `window.location` / `window.open`
644
937
  * mechanism is introduced here; the no-handler fallback is the same
645
- * `result.redirect` hand-off `navigateTo` already uses.
938
+ * `result.redirect` hand-off `navigateTo` already uses. The one exception is
939
+ * shared with `navigateTo`: a `mailto:` target is not a route, so it is a
940
+ * full-page navigation in place (objectui#12191).
646
941
  *
647
942
  * **No default is written here.** `openIn` is a materialised
648
943
  * `.default('self')` in the spec, so parse output always carries a resolved
@@ -658,6 +953,59 @@ export declare class ActionRunner {
658
953
  * one authors are validated against.
659
954
  */
660
955
  private navigateOnSuccess;
956
+ /**
957
+ * `operation: 'update'` — the declarative single-record field write.
958
+ *
959
+ * ## The write is the ROUTE's, never this client's
960
+ *
961
+ * This method composes a request and hands it to the registered `script`
962
+ * dispatch; it does not touch a `DataSource`. That is a security boundary,
963
+ * not an implementation preference. `POST /api/v1/actions/{object}/{action}`
964
+ * runs the write AS THE CALLER — the caller's permissions, the object's hooks
965
+ * and its validations all fire as for a user edit — while a client-side
966
+ * `dataSource.update()` reaches the data plane having consulted none of them.
967
+ * The spec says the same thing from its end by refusing `target`, `body`,
968
+ * `method` and `bodyExtra` beside this key and pinning `type` to `'script'`:
969
+ * there is exactly one execution surface for an update action, and it is the
970
+ * route.
971
+ *
972
+ * Consequently, a consumer that has registered NO server-action dispatch gets
973
+ * a loud refusal naming the remedy. The two silent alternatives were both
974
+ * available and both wrong: falling through to `executeScript` ends at "No
975
+ * script provided" (an update action declares no `target` — the author is
976
+ * sent hunting for a field they were right not to write), and falling through
977
+ * to `executeActionSchema` reports on an action that never ran.
978
+ *
979
+ * ## `patch` is the route's: it never rides on the wire (objectui#12941)
980
+ *
981
+ * The request carries the values collected for the action's declared
982
+ * `params`, plus the transport keys the dispatch reads (`recordId`, and the
983
+ * `_rowRecord` stash it strips before POSTing) — and NOTHING from `patch`.
984
+ * The route applies the declared `patch` itself, from the declaration it
985
+ * resolves by the action's name (`@objectstack/runtime`'s
986
+ * `declarativeUpdateWrite`, which writes `{ ...patch, ...params }`). The
987
+ * patch keys are not params the action declares, so posting them makes every
988
+ * declarative-update button a request carrying undeclared params — the very
989
+ * request the ADR-0104 D2 param contract refuses (objectstack#24268). A
990
+ * client copy of `patch` on the wire also adds nothing: the server never
991
+ * reads it.
992
+ *
993
+ * The spec's words for the merge the route performs: static field values
994
+ * "merged UNDER the user-supplied `params` so a fixed value can be declared
995
+ * without exposing it in the dialog". So a collected param of the same name
996
+ * WINS — the author declared a default, and the user answered it. This
997
+ * runner still computes that same merge, but only to know what the route
998
+ * WROTE, which is what an Undo snapshot must cover; it is never sent.
999
+ *
1000
+ * ## The `patch` in that merge is the DECLARATION's (objectui#13022)
1001
+ *
1002
+ * The route writes the `patch` of the declaration the action's `name` names,
1003
+ * so that is the `patch` the snapshot covers: read by
1004
+ * {@link declaredUpdatePatch} from the declarations the host published, else
1005
+ * off a def that is the declaration itself. A page action row's own `patch`
1006
+ * is never written (`@objectstack/spec` 18 retires it) and never covered.
1007
+ */
1008
+ private executeUpdateOperation;
661
1009
  /**
662
1010
  * Execute script action — evaluates client-side expression via ExpressionEvaluator.
663
1011
  * Supports ${} template expressions referencing data, record, user context.
@@ -668,6 +1016,24 @@ export declare class ActionRunner {
668
1016
  * Uses navigationHandler for SPA routing, falls back to window.location.
669
1017
  */
670
1018
  private executeUrl;
1019
+ /**
1020
+ * A navigation announces nothing unless its action declares the copy
1021
+ * (objectui#12755).
1022
+ *
1023
+ * `type: 'url'`, its `navigation` alias and `type: 'form'` only change the
1024
+ * route: nothing was written, so there is nothing to confirm. Their
1025
+ * `{ success: true }` used to reach `handlePostExecution` like a write's, and
1026
+ * the generic `completedSuccessfully` toast ("操作已成功完成") told the user an
1027
+ * operation had completed when a page had merely opened (objectstack-ai/cloud#2909:
1028
+ * an "invite member" button that only navigated read as "invitation sent").
1029
+ *
1030
+ * The result keeps `success: true` (callers read it as "the navigation
1031
+ * happened") and is marked `silent`, the flag that already means "no success
1032
+ * toast for this result". An author who wants a toast after the navigation
1033
+ * declares `successMessage`, and that copy still shows: the declaration is the
1034
+ * only signal, so there is no separate switch to forget.
1035
+ */
1036
+ private announceNavigation;
671
1037
  /**
672
1038
  * The one navigator behind `type: 'url'` and its `navigation` alias.
673
1039
  *
@@ -772,7 +1138,7 @@ export declare class ActionRunner {
772
1138
  * old object-form `params` page — POSTed an empty body here.
773
1139
  *
774
1140
  * `params` contributes only in its DEPRECATED non-array form (the compat window
775
- * #5777 opened; see `warnOnDeprecatedObjectParams`). An ARRAY `params` is a
1141
+ * objectstack-ai/objectstack#5777 opened; see `warnOnDeprecatedObjectParams`). An ARRAY `params` is a
776
1142
  * parameter DEFINITION list, not a payload: it reaches this method unconsumed
777
1143
  * only when no `paramCollectionHandler` is mounted, and POSTing the definitions
778
1144
  * as the request body was never a payload any endpoint wanted. It now falls
@@ -792,10 +1158,6 @@ export declare class ActionRunner {
792
1158
  * Execute API action — supports both simple string endpoint and complex ApiConfig.
793
1159
  */
794
1160
  private executeAPI;
795
- /**
796
- * Validate URL to prevent javascript: or data: protocol injection.
797
- */
798
- private isValidUrl;
799
1161
  /**
800
1162
  * Substitute `${param.X}` and `${ctx.X}` tokens in an action target.
801
1163
  *
@@ -830,8 +1192,51 @@ export declare class ActionRunner {
830
1192
  * placed under context.ctx is merged on top.
831
1193
  */
832
1194
  private buildInterpolationContext;
1195
+ /**
1196
+ * The evaluator a dispatch's authored copy is read through: `confirmText`,
1197
+ * and the param dialog's `description` / `helpText` via the handler's
1198
+ * `interpolate` argument.
1199
+ *
1200
+ * One rule for every door (objectui#12967): the record the action runs
1201
+ * against is `record`. The record header binds it in the runner's context
1202
+ * (`RecordDetailView`'s `ActionProvider`). A row surface does not — it
1203
+ * stashes the clicked row under `params._rowRecord` (ObjectGrid's row
1204
+ * actions, a related list's row actions, `DeclaredActionsBar`, a page
1205
+ * header, `action:button`'s update write) and fires through the shared
1206
+ * runner, whose context names the page, not the row. So when the dispatch
1207
+ * carries a row, the row is bound as `record` for this dispatch's templates,
1208
+ * and it wins over a page-level `record` (a related-list row on a record page
1209
+ * is the child, not the page's record). Nothing else in the scope changes,
1210
+ * and the runner's own context is not touched.
1211
+ *
1212
+ * A hole naming a field the record does not carry degrades the way the
1213
+ * header's always has: the evaluator reads `undefined` and the hole prints
1214
+ * empty.
1215
+ */
1216
+ private templateEvaluatorFor;
833
1217
  updateContext(newContext: Partial<ActionContext>): void;
834
1218
  getContext(): ActionContext;
1219
+ /**
1220
+ * A one-shot runner for {@link ActionRunner.execute}'s `callContext`
1221
+ * (objectui#13063): this runner's context with `callContext` merged over it,
1222
+ * and this runner's handlers, translator and registries, shared rather than
1223
+ * copied, so the dispatch confirms, collects params, toasts and navigates
1224
+ * exactly as this runner does.
1225
+ *
1226
+ * Two keys follow the merge rather than the base:
1227
+ *
1228
+ * - `ctx`, the `ctx.*` mirror `ActionProvider` publishes beside the flat
1229
+ * keys. A `${ctx.record.…}` hole has to read the same record as a
1230
+ * `${record.…}` one.
1231
+ * - `objectFields` / `objectActions`. A host publishes them BESIDE
1232
+ * `objectName`, so the pair names one object (objectui#11122,
1233
+ * objectui#13022). When `callContext` names another object and brings no
1234
+ * definitions of its own, the base's are dropped: the page object's field
1235
+ * types and declarations must not answer for the child's record. That is
1236
+ * what a dispatch with no `callContext` already gets for a retargeted
1237
+ * action, so the update path's capture sees the same input either way.
1238
+ */
1239
+ private scopedTo;
835
1240
  /**
836
1241
  * Get the expression evaluator (for components that need to evaluate visibility, etc.)
837
1242
  */
@@ -848,14 +1253,13 @@ export interface OnSuccessNavigation {
848
1253
  openIn?: unknown;
849
1254
  }
850
1255
  /**
851
- * Is this `onSuccess` the SPEC's navigation block, or this runner's older
852
- * chained-callback channel (`ActionDef | ActionDef[]`)?
1256
+ * Is this `onSuccess` the spec's navigation block?
853
1257
  *
854
1258
  * The test IS the spec's declaration: a non-array object carrying a STRING
855
- * `navigate`. Nothing else can produce that shape — the spec object is strict
856
- * with `navigate: z.string()` required, and on a callback `ActionDef`,
857
- * `navigate` is the deprecated nested navigation ENVELOPE that
858
- * `executeNavigation` reads `to`/`target`/`redirect` off, so a bare string
859
- * there has never been runnable.
1259
+ * `navigate`. Stored rows are rehydrated UNPARSED (objectstack-ai/objectstack#3903), so the runner reads
1260
+ * the value as data and anything else gets NO reading — since objectui#5934
1261
+ * retired the legacy chained-callback channel (`ActionDef | ActionDef[]`),
1262
+ * there is no other channel for an off-contract shape to fall into. This is a
1263
+ * shape GUARD on unparsed data, not a discriminator between two meanings.
860
1264
  */
861
1265
  export declare function readOnSuccessNavigation(value: unknown): OnSuccessNavigation | null;