@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
package/README.md CHANGED
@@ -4,7 +4,8 @@ Core logic, types, and validation for Object UI. Zero React dependencies.
4
4
 
5
5
  ## Features
6
6
 
7
- - 🎯 **Type Definitions** - Complete TypeScript schemas for all components
7
+ - 🎯 **Type Definitions** - Re-exported runtime types; the component schema
8
+ vocabulary itself is `@object-ui/types`
8
9
  - 🔍 **Component Registry** - Framework-agnostic component registration system
9
10
  - 📊 **Data Scope** - Data scope management and expression evaluation
10
11
  - ✅ **Validation** - Zod-based schema validation
@@ -20,15 +21,20 @@ npm install @object-ui/core
20
21
 
21
22
  ### Type Definitions
22
23
 
24
+ The component schema vocabulary lives in `@object-ui/types`. Core depends on
25
+ that package and does not re-export it, so import the types from there. The
26
+ page node type is `PageNodeSchema` — the SDUI node, as distinct from the
27
+ authored page document.
28
+
23
29
  ```typescript
24
- import type {
25
- PageSchema,
26
- FormSchema,
30
+ import type {
31
+ PageNodeSchema,
32
+ FormSchema,
27
33
  InputSchema,
28
- BaseSchema
29
- } from '@object-ui/core'
34
+ BaseSchema
35
+ } from '@object-ui/types'
30
36
 
31
- const mySchema: PageSchema = {
37
+ const mySchema: PageNodeSchema = {
32
38
  type: 'page',
33
39
  title: 'My Page',
34
40
  body: []
@@ -40,22 +46,30 @@ const mySchema: PageSchema = {
40
46
  ```typescript
41
47
  import { ComponentRegistry } from '@object-ui/core'
42
48
 
43
- const registry = new ComponentRegistry()
44
- registry.register('button', buttonMetadata)
45
- const metadata = registry.get('button')
49
+ ComponentRegistry.register('button', buttonMetadata)
50
+ const metadata = ComponentRegistry.get('button')
46
51
  ```
47
52
 
53
+ `ComponentRegistry` is a process-level singleton exported by `@object-ui/core`;
54
+ `SchemaRenderer` resolves every `type` against it, so a component registered
55
+ here is renderable from schema anywhere in the app.
56
+
48
57
  ### Data Scope
49
58
 
59
+ `DataScopeManager` owns the named scopes a component tree reads from, and
60
+ `evaluateExpression` evaluates a `${...}` expression against a context. They
61
+ are separate exports: a scope holds data, it does not evaluate.
62
+
50
63
  ```typescript
51
- import { DataScope } from '@object-ui/core'
64
+ import { DataScopeManager, evaluateExpression } from '@object-ui/core'
52
65
 
53
- const scope = new DataScope({
54
- user: { name: 'John', role: 'admin' }
55
- })
66
+ const manager = new DataScopeManager()
67
+ manager.registerScope('user', { data: { name: 'John', role: 'admin' } })
56
68
 
57
- const userName = scope.get('user.name') // 'John'
58
- const isAdmin = scope.evaluate('${user.role === "admin"}') // true
69
+ const userName = manager.getScope('user')?.data.name // 'John'
70
+ const isAdmin = evaluateExpression('${user.role === "admin"}', {
71
+ user: { name: 'John', role: 'admin' },
72
+ }) // true
59
73
  ```
60
74
 
61
75
  ### Server Action Dispatch (`createServerActionHandler`)
@@ -87,16 +101,16 @@ action identity (ADR-0110), the record-id resolution dance (`_rowRecord`,
87
101
  guard, and the `/actions` response-envelope rule (`interpretActionResponse` /
88
102
  `readActionPayload`, also exported).
89
103
 
90
- ### System Views (`defineView`)
104
+ ### System Views (`defineSystemView`)
91
105
 
92
106
  Schemas authored in source code are part of the product contract and must
93
- not be mutated at runtime. Wrap them with `defineView()` to deep-freeze the
94
- graph and tag it as a *System View*.
107
+ not be mutated at runtime. Wrap them with `defineSystemView()` to deep-freeze
108
+ the graph and tag it as a *System View*.
95
109
 
96
110
  ```typescript
97
- import { defineView, cloneAsOverride, isSystemView } from '@object-ui/core'
111
+ import { defineSystemView, cloneAsOverride, isSystemView } from '@object-ui/core'
98
112
 
99
- export const userListView = defineView({
113
+ export const userListView = defineSystemView({
100
114
  type: 'list',
101
115
  data: { object: 'User' },
102
116
  columns: [{ name: 'email' }],
@@ -115,7 +129,7 @@ isSystemView(draft) // false — clone is no longer Sys
115
129
 
116
130
  | Tier | Source | Mutable? | API |
117
131
  | ----------- | --------------------- | -------- | --------------------------- |
118
- | System View | code (`import` / `as const`) | ❌ frozen | `defineView()` |
132
+ | System View | code (`import` / `as const`) | ❌ frozen | `defineSystemView()` |
119
133
  | Tenant View | backend / DB | ⚠️ admin only | `cloneAsOverride()` + persist |
120
134
  | User View | localStorage / API | ✅ user-editable | `cloneAsOverride()` + persist |
121
135
 
@@ -141,21 +155,11 @@ This allows the core types and logic to be used in:
141
155
 
142
156
  ## API Reference
143
157
 
144
- See [full documentation](https://objectui.org/api/core) for detailed API reference.
145
-
146
- <!-- release-metadata:v3.3.0 -->
147
-
148
- ## Compatibility
149
-
150
- - **Node.js:** ≥ 18
151
- - **TypeScript:** ≥ 5.0 (strict mode)
152
- - **`@objectstack/spec`:** ^3.3.0
153
- - **`@objectstack/client`:** ^3.3.0
154
- - **Tailwind CSS:** ≥ 3.4 (for packages with UI)
158
+ See [full documentation](https://objectui.org/docs/api) for detailed API reference.
155
159
 
156
160
  ## Links
157
161
 
158
- - 📚 [Documentation](https://www.objectui.org/docs/core)
162
+ - 📚 [Documentation](https://www.objectui.org/docs/guide/architecture)
159
163
  - 📦 [npm package](https://www.npmjs.com/package/@object-ui/core)
160
164
  - 📝 [Changelog](./CHANGELOG.md)
161
165
  - 🐛 [Report an issue](https://github.com/objectstack-ai/objectui/issues)
@@ -8,14 +8,13 @@
8
8
  /**
9
9
  * @object-ui/core - Action Engine
10
10
  *
11
- * Declarative action dispatch engine. Manages action registration,
12
- * event-to-action mapping, location-based filtering, keyboard shortcuts,
13
- * and bulk operation support.
11
+ * Declarative action engine. Manages action registration, location-based
12
+ * filtering, keyboard shortcuts, and bulk operation support.
14
13
  *
15
14
  * Replaces callback-based patterns with a schema-driven pipeline:
16
15
  * ActionSchema[] → ActionEngine → ActionRunner → ActionResult
17
16
  */
18
- import { ActionRunner, type ActionDef, type ActionContext, type ActionResult } from './ActionRunner';
17
+ import { ActionRunner, type ActionDef, type ActionContext, type ActionResult } from './ActionRunner.js';
19
18
  /**
20
19
  * Action location types — re-exported from `@objectstack/spec/ui` so the
21
20
  * platform has one source of truth (`ACTION_LOCATIONS`). Do NOT redeclare
@@ -37,15 +36,6 @@ export interface RegisteredAction {
37
36
  /** Priority for ordering (lower = first) */
38
37
  priority?: number;
39
38
  }
40
- /** Event-to-action mapping */
41
- export interface ActionMapping {
42
- /** Event name (e.g., 'row:click', 'toolbar:save', 'keyboard:ctrl+s') */
43
- event: string;
44
- /** Action name to execute */
45
- actionName: string;
46
- /** Optional condition expression */
47
- condition?: string;
48
- }
49
39
  /** Keyboard shortcut handler */
50
40
  export interface ShortcutBinding {
51
41
  /** Key combination (e.g., 'ctrl+s', 'meta+k', 'shift+n') */
@@ -53,9 +43,35 @@ export interface ShortcutBinding {
53
43
  /** Action name to trigger */
54
44
  actionName: string;
55
45
  }
46
+ /**
47
+ * Registration + filtering + execution for declared actions.
48
+ *
49
+ * Entry points are by NAME (`executeAction`), by LOCATION
50
+ * (`getActionsForLocation`), by SHORTCUT (`handleShortcut`) and in BULK
51
+ * (`executeBulk`). There is deliberately no entry point by arbitrary EVENT
52
+ * name: this class used to carry a second, parallel one — `addMapping()`
53
+ * registered `{ event, actionName, condition }` triples into a private
54
+ * `mappings` array and `dispatch(event)` ran every triple matching an event
55
+ * string. It was retired under enforce-or-remove (objectui#3368) because it
56
+ * was a public export of `@object-ui/core` with zero production callers: in
57
+ * its whole lifetime nothing ever registered a mapping, so `dispatch()` had
58
+ * no reachable caller either, and its four call sites were all in this
59
+ * class's own test file.
60
+ *
61
+ * Its condition gate had drifted three ways from the `visible` contract
62
+ * `getActionsForLocation` below implements — it entered on a raw truthy check
63
+ * (so `condition: false` dispatched anyway), typed `condition` as `string`
64
+ * only (so a `{ dialect: 'cel', source }` envelope could not reach the
65
+ * canonical engine), and evaluated without `throwOnError` (so a throwing
66
+ * predicate failed OPEN, the opposite of `visible`'s fail-closed posture).
67
+ * All three were retired WITH the path rather than fixed on it: aligning the
68
+ * contract of an API nobody calls only widens behaviour nobody uses. If an
69
+ * event-keyed entry point is ever genuinely needed, it must be built on the
70
+ * shared predicate definitions (`hasDeclaredPredicate` + `toPredicateInput`)
71
+ * that the surviving gate below already uses, not on a fourth spelling.
72
+ */
56
73
  export declare class ActionEngine {
57
74
  private actions;
58
- private mappings;
59
75
  private shortcuts;
60
76
  private normalizedShortcutMap;
61
77
  private runner;
@@ -92,21 +108,24 @@ export declare class ActionEngine {
92
108
  registerActions(actions: ActionDef[]): void;
93
109
  /** Unregister an action by name */
94
110
  unregisterAction(name: string): void;
95
- /** Add an event-to-action mapping */
96
- addMapping(mapping: ActionMapping): void;
97
111
  /**
98
112
  * Get actions available at a specific location, sorted by priority.
99
113
  *
100
114
  * Filtering applied (in order):
101
115
  * 1. `locations.includes(location)` — location/region match
102
116
  * 2. `action.visible` — evaluated against the runner's current context
103
- * ({ record, recordId, objectName, user, … }). Missing or `true`
104
- * passes; any other value is coerced to boolean. Evaluator errors
105
- * hide the action (fail-closed) rather than throwing — this matches
106
- * the contract used by every individual action renderer
107
- * (`action-button`, `action-menu`, `action-bar`, …) so the same
108
- * action behaves identically whether surfaced via the engine or
109
- * consumed standalone.
117
+ * ({ record, recordId, objectName, user, … }) whenever a gate is
118
+ * DECLARED, which is asked through core's one definition
119
+ * `hasDeclaredPredicate` (objectui#3850's ruling, adopted here by
120
+ * objectui#3957). No gate — absent, `''`, blank predicate text in either
121
+ * spelling, an envelope with no evaluable `source`, or a value that is
122
+ * not a predicate at all — means visible. Evaluator errors hide the
123
+ * action (fail-closed) rather than throwing — this matches the contract
124
+ * used by every individual action renderer (`action-button`,
125
+ * `action-menu`, `action-bar`, …) so the same action behaves identically
126
+ * whether surfaced via the engine or consumed standalone, which is now
127
+ * true of the gate in front of the verdict as well as the verdict
128
+ * (objectui#3314: one value, one answer, whichever entry reads it).
110
129
  *
111
130
  * Predicate normalization goes through the shared `toPredicateInput`
112
131
  * (`@object-ui/core`'s canonical helper — semantically identical to
@@ -130,8 +149,6 @@ export declare class ActionEngine {
130
149
  getAction(name: string): ActionDef | undefined;
131
150
  /** Execute an action by name */
132
151
  executeAction(name: string, contextOverride?: Partial<ActionContext>): Promise<ActionResult>;
133
- /** Dispatch an event — finds mapped actions and executes them */
134
- dispatch(event: string, contextOverride?: Partial<ActionContext>): Promise<ActionResult[]>;
135
152
  /** Handle a keyboard shortcut event — returns true if handled */
136
153
  handleShortcut(keys: string, contextOverride?: Partial<ActionContext>): Promise<ActionResult | null>;
137
154
  /** Execute a bulk operation on multiple records */
@@ -146,6 +163,6 @@ export declare class ActionEngine {
146
163
  }>;
147
164
  /** Update the action context */
148
165
  updateContext(context: Partial<ActionContext>): void;
149
- /** Clear all registered actions and mappings */
166
+ /** Clear all registered actions and shortcuts */
150
167
  clear(): void;
151
168
  }
@@ -8,15 +8,15 @@
8
8
  /**
9
9
  * @object-ui/core - Action Engine
10
10
  *
11
- * Declarative action dispatch engine. Manages action registration,
12
- * event-to-action mapping, location-based filtering, keyboard shortcuts,
13
- * and bulk operation support.
11
+ * Declarative action engine. Manages action registration, location-based
12
+ * filtering, keyboard shortcuts, and bulk operation support.
14
13
  *
15
14
  * Replaces callback-based patterns with a schema-driven pipeline:
16
15
  * ActionSchema[] → ActionEngine → ActionRunner → ActionResult
17
16
  */
18
- import { ActionRunner } from './ActionRunner';
17
+ import { ActionRunner } from './ActionRunner.js';
19
18
  import { toPredicateInput } from '../evaluator/predicateInput.js';
19
+ import { hasDeclaredPredicate } from '../evaluator/declaredPredicate.js';
20
20
  export { ACTION_LOCATIONS } from '@object-ui/types';
21
21
  /** Normalize a shortcut string for comparison (lowercase, trimmed, sorted parts) */
22
22
  function normalizeShortcut(keys) {
@@ -38,11 +38,37 @@ function warnHiddenPredicate(name, raw, err) {
38
38
  return;
39
39
  _warnedPredicates.add(key);
40
40
  const msg = err instanceof Error ? err.message : String(err);
41
- // eslint-disable-next-line no-console
42
41
  console.warn(`[ActionEngine] action "${String(name)}" hidden: its \`visible\` predicate threw — ${msg}. ` +
43
42
  `Predicate: ${src}. Action predicates evaluate against { record, recordId, objectName, user, os.user, … }; ` +
44
43
  `use \`record.<field>\` (not a bare field).`);
45
44
  }
45
+ /**
46
+ * Registration + filtering + execution for declared actions.
47
+ *
48
+ * Entry points are by NAME (`executeAction`), by LOCATION
49
+ * (`getActionsForLocation`), by SHORTCUT (`handleShortcut`) and in BULK
50
+ * (`executeBulk`). There is deliberately no entry point by arbitrary EVENT
51
+ * name: this class used to carry a second, parallel one — `addMapping()`
52
+ * registered `{ event, actionName, condition }` triples into a private
53
+ * `mappings` array and `dispatch(event)` ran every triple matching an event
54
+ * string. It was retired under enforce-or-remove (objectui#3368) because it
55
+ * was a public export of `@object-ui/core` with zero production callers: in
56
+ * its whole lifetime nothing ever registered a mapping, so `dispatch()` had
57
+ * no reachable caller either, and its four call sites were all in this
58
+ * class's own test file.
59
+ *
60
+ * Its condition gate had drifted three ways from the `visible` contract
61
+ * `getActionsForLocation` below implements — it entered on a raw truthy check
62
+ * (so `condition: false` dispatched anyway), typed `condition` as `string`
63
+ * only (so a `{ dialect: 'cel', source }` envelope could not reach the
64
+ * canonical engine), and evaluated without `throwOnError` (so a throwing
65
+ * predicate failed OPEN, the opposite of `visible`'s fail-closed posture).
66
+ * All three were retired WITH the path rather than fixed on it: aligning the
67
+ * contract of an API nobody calls only widens behaviour nobody uses. If an
68
+ * event-keyed entry point is ever genuinely needed, it must be built on the
69
+ * shared predicate definitions (`hasDeclaredPredicate` + `toPredicateInput`)
70
+ * that the surviving gate below already uses, not on a fourth spelling.
71
+ */
46
72
  export class ActionEngine {
47
73
  /**
48
74
  * @param contextOrRunner Either an initial `ActionContext` (constructs a
@@ -60,12 +86,6 @@ export class ActionEngine {
60
86
  writable: true,
61
87
  value: new Map()
62
88
  });
63
- Object.defineProperty(this, "mappings", {
64
- enumerable: true,
65
- configurable: true,
66
- writable: true,
67
- value: []
68
- });
69
89
  Object.defineProperty(this, "shortcuts", {
70
90
  enumerable: true,
71
91
  configurable: true,
@@ -140,11 +160,6 @@ export class ActionEngine {
140
160
  }
141
161
  this.actions.delete(name);
142
162
  this.shortcuts = this.shortcuts.filter(s => s.actionName !== name);
143
- this.mappings = this.mappings.filter(m => m.actionName !== name);
144
- }
145
- /** Add an event-to-action mapping */
146
- addMapping(mapping) {
147
- this.mappings.push(mapping);
148
163
  }
149
164
  /**
150
165
  * Get actions available at a specific location, sorted by priority.
@@ -152,13 +167,18 @@ export class ActionEngine {
152
167
  * Filtering applied (in order):
153
168
  * 1. `locations.includes(location)` — location/region match
154
169
  * 2. `action.visible` — evaluated against the runner's current context
155
- * ({ record, recordId, objectName, user, … }). Missing or `true`
156
- * passes; any other value is coerced to boolean. Evaluator errors
157
- * hide the action (fail-closed) rather than throwing — this matches
158
- * the contract used by every individual action renderer
159
- * (`action-button`, `action-menu`, `action-bar`, …) so the same
160
- * action behaves identically whether surfaced via the engine or
161
- * consumed standalone.
170
+ * ({ record, recordId, objectName, user, … }) whenever a gate is
171
+ * DECLARED, which is asked through core's one definition
172
+ * `hasDeclaredPredicate` (objectui#3850's ruling, adopted here by
173
+ * objectui#3957). No gate — absent, `''`, blank predicate text in either
174
+ * spelling, an envelope with no evaluable `source`, or a value that is
175
+ * not a predicate at all — means visible. Evaluator errors hide the
176
+ * action (fail-closed) rather than throwing — this matches the contract
177
+ * used by every individual action renderer (`action-button`,
178
+ * `action-menu`, `action-bar`, …) so the same action behaves identically
179
+ * whether surfaced via the engine or consumed standalone, which is now
180
+ * true of the gate in front of the verdict as well as the verdict
181
+ * (objectui#3314: one value, one answer, whichever entry reads it).
162
182
  *
163
183
  * Predicate normalization goes through the shared `toPredicateInput`
164
184
  * (`@object-ui/core`'s canonical helper — semantically identical to
@@ -194,22 +214,36 @@ export class ActionEngine {
194
214
  })
195
215
  .filter(ra => {
196
216
  const raw = ra.action.visible;
197
- if (raw == null || raw === '' || raw === true)
217
+ // Ask "is a `visible` gate DECLARED?" through the repo's ONE definition
218
+ // (`evaluator/declaredPredicate.ts`, objectui#3850's ruling), which is
219
+ // what the renderers, `SchemaRenderer` and `ActionRunner`'s execution
220
+ // gates ask. This filter used to answer it with a range of its own —
221
+ // three empty spellings folded by hand (`null` / `''` / an envelope with
222
+ // an empty `source`) and everything else coerced with `Boolean(raw)` —
223
+ // and it was the LAST consumer to do so, so the same value got two
224
+ // answers depending on which entry read it: `visible: 0` / `NaN` hid the
225
+ // action here and showed it on the renderer face, and `visible: ' '`
226
+ // hid it here because the normalizer wraps a blank string into `'${ }'`
227
+ // whose verdict is falsy. That is the shape objectui#3314's invariant
228
+ // forbids, and it is fixed by deleting a range, not by adding one
229
+ // (objectui#3957). A value that is not a predicate, or a predicate that
230
+ // says nothing, must not be the reason an action is hidden from
231
+ // everyone — the same fail-open posture `ActionRunner` already committed
232
+ // to for `disabled` (`catch { isDisabled = false }`).
233
+ //
234
+ // Booleans need no branch of their own: they are DECLARED (`visible:
235
+ // false` is a verdict, objectui#3812) and `evaluateCondition`
236
+ // short-circuits them.
237
+ if (!hasDeclaredPredicate(raw))
198
238
  return true;
199
- if (raw === false)
200
- return false;
201
239
  // #3314 — one shared normalization, not two. Hand-rolling the envelope
202
240
  // unwrap here is what dropped `dialect: 'cel'` and demoted the
203
241
  // predicate to the legacy JS engine; `toPredicateInput` keeps the
204
242
  // envelope so `evaluateCondition` can route it to `@objectstack/formula`
205
243
  // (the engine the server enforces with), exactly as the renderers do.
244
+ // Declared ⇒ the normalizer left something to evaluate, so there is no
245
+ // `undefined` case left to handle here.
206
246
  const expr = toPredicateInput(raw);
207
- if (expr === undefined) {
208
- // Not an evaluable predicate: an envelope with an empty `source`
209
- // (→ nothing declared → visible), or a stray non-predicate value,
210
- // which keeps the historical `Boolean(raw)` coercion.
211
- return typeof raw === 'object' ? true : Boolean(raw);
212
- }
213
247
  try {
214
248
  // `throwOnError: true` is required for fail-closed semantics: the
215
249
  // evaluator's default behavior swallows expression errors and
@@ -262,26 +296,6 @@ export class ActionEngine {
262
296
  }
263
297
  return this.runner.execute(registered.action);
264
298
  }
265
- /** Dispatch an event — finds mapped actions and executes them */
266
- async dispatch(event, contextOverride) {
267
- const matchingMappings = this.mappings.filter(m => m.event === event);
268
- if (matchingMappings.length === 0) {
269
- return [];
270
- }
271
- const results = [];
272
- for (const mapping of matchingMappings) {
273
- // Check condition if present
274
- if (mapping.condition) {
275
- const evaluator = this.runner.getEvaluator();
276
- const shouldRun = evaluator.evaluateCondition(mapping.condition);
277
- if (!shouldRun)
278
- continue;
279
- }
280
- const result = await this.executeAction(mapping.actionName, contextOverride);
281
- results.push(result);
282
- }
283
- return results;
284
- }
285
299
  /** Handle a keyboard shortcut event — returns true if handled */
286
300
  async handleShortcut(keys, contextOverride) {
287
301
  const actionName = this.normalizedShortcutMap.get(normalizeShortcut(keys));
@@ -341,10 +355,9 @@ export class ActionEngine {
341
355
  updateContext(context) {
342
356
  this.runner.updateContext(context);
343
357
  }
344
- /** Clear all registered actions and mappings */
358
+ /** Clear all registered actions and shortcuts */
345
359
  clear() {
346
360
  this.actions.clear();
347
- this.mappings = [];
348
361
  this.shortcuts = [];
349
362
  this.normalizedShortcutMap.clear();
350
363
  }