@salsita-ai/sdk 0.50.0-develop.da56a0c6c3 → 0.50.0-develop.ea458ea381

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 (124) hide show
  1. package/dist/application-spec/errors/diagnostic-codes.d.ts +6 -0
  2. package/dist/application-spec/errors/diagnostic-codes.js +1 -1
  3. package/dist/application-spec/errors/diagnostic-messages.d.ts +6 -0
  4. package/dist/application-spec/errors/diagnostic-messages.js +1 -1
  5. package/dist/application-spec/generate-application/code/domain-runtime-files.js +1 -1
  6. package/dist/application-spec/generate-application/code/templates/application-types.js +6 -6
  7. package/dist/application-spec/generate-application/code/templates/configurator-domain-code.d.ts +1 -0
  8. package/dist/application-spec/generate-application/code/templates/configurator-domain-code.js +11 -8
  9. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-domain-block.d.ts +3 -1
  10. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-domain-block.js +70 -66
  11. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-phx-globals.js +3 -0
  12. package/dist/application-spec/generate-application/code/templates/domain-spec.js +5 -5
  13. package/dist/application-spec/generate-application/code/templates/domain.js +2 -2
  14. package/dist/application-spec/generate-application/code/templates/global-definition/generate-action-types.d.ts +11 -0
  15. package/dist/application-spec/generate-application/code/templates/global-definition/generate-action-types.js +8 -0
  16. package/dist/application-spec/generate-application/code/templates/global-definition/index.d.ts +1 -0
  17. package/dist/application-spec/generate-application/code/templates/global-definition/index.js +1 -1
  18. package/dist/application-spec/generate-application/code/templates/globals.js +1 -0
  19. package/dist/application-spec/generate-application/code/templates/skills/phoenix-review-skill.js +4 -2
  20. package/dist/application-spec/generate-application/code/templates/skills/sdk-content.d.ts +3 -1
  21. package/dist/application-spec/generate-application/code/templates/skills/sdk-content.js +2 -2
  22. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-runtime.d.ts +2 -0
  23. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-runtime.js +1 -1
  24. package/dist/application-spec/generate-application/runtime/sections/actions-runtime.d.ts +14 -0
  25. package/dist/application-spec/generate-application/runtime/sections/actions-runtime.js +1 -0
  26. package/dist/application-spec/generate-application/runtime/sections/index.d.ts +1 -0
  27. package/dist/application-spec/generate-application/runtime/sections/index.js +1 -1
  28. package/dist/application-spec/parse-application-files/categorize/categorize-files.d.ts +3 -1
  29. package/dist/application-spec/parse-application-files/categorize/categorize-files.js +1 -1
  30. package/dist/application-spec/parse-application-files/merge/index.d.ts +1 -0
  31. package/dist/application-spec/parse-application-files/merge/index.js +1 -1
  32. package/dist/application-spec/parse-application-files/merge/merged-actions.d.ts +11 -0
  33. package/dist/application-spec/parse-application-files/merge/merged-actions.js +1 -0
  34. package/dist/application-spec/parse-application-files/merge/merged-spec.d.ts +2 -0
  35. package/dist/application-spec/parse-application-files/merge/merged-spec.js +1 -1
  36. package/dist/application-spec/parse-application-files/raw-spec/index.d.ts +1 -0
  37. package/dist/application-spec/parse-application-files/raw-spec/index.js +1 -1
  38. package/dist/application-spec/parse-application-files/raw-spec/raw-actions.d.ts +18 -0
  39. package/dist/application-spec/parse-application-files/raw-spec/raw-actions.js +1 -0
  40. package/dist/application-spec/parse-application-files/raw-spec/raw-assistant.d.ts +14 -2
  41. package/dist/application-spec/parse-application-files/raw-spec/raw-assistant.js +1 -1
  42. package/dist/application-spec/parse-application-files/raw-spec/raw-spec-file.d.ts +2 -0
  43. package/dist/application-spec/parse-application-files/raw-spec/raw-spec-file.js +1 -1
  44. package/dist/application-spec/parse-application-files/resolve/resolve-import-modules.js +1 -1
  45. package/dist/application-spec/parse-application-files/spec/application/application-spec.js +1 -1
  46. package/dist/application-spec/parse-application-files/spec/application/configurator-spec/configurator-spec.d.ts +4 -2
  47. package/dist/application-spec/parse-application-files/spec/application/configurator-spec/configurator-spec.js +1 -1
  48. package/dist/application-spec/parse-application-files/spec/common/index.d.ts +1 -0
  49. package/dist/application-spec/parse-application-files/spec/common/index.js +1 -1
  50. package/dist/application-spec/parse-application-files/spec/common/schema-ref-resolver.d.ts +23 -0
  51. package/dist/application-spec/parse-application-files/spec/common/schema-ref-resolver.js +1 -0
  52. package/dist/application-spec/parse-application-files/spec/model/names/action-name.d.ts +10 -0
  53. package/dist/application-spec/parse-application-files/spec/model/names/action-name.js +1 -0
  54. package/dist/application-spec/parse-application-files/spec/model/names/index.d.ts +1 -0
  55. package/dist/application-spec/parse-application-files/spec/model/names/index.js +1 -1
  56. package/dist/application-spec/parse-application-files/spec/sections/actions.d.ts +35 -0
  57. package/dist/application-spec/parse-application-files/spec/sections/actions.js +1 -0
  58. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-field-type.d.ts +3 -14
  59. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-field-type.js +1 -1
  60. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-schema.d.ts +12 -3
  61. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-schema.js +1 -1
  62. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant.d.ts +5 -3
  63. package/dist/application-spec/parse-application-files/spec/sections/index.d.ts +1 -0
  64. package/dist/application-spec/parse-application-files/spec/sections/index.js +1 -1
  65. package/dist/configurator/application-runtime/extensions/configurator-extension-systems.d.ts +2 -1
  66. package/dist/configurator/application-runtime/extensions/configurator-extension-systems.js +1 -1
  67. package/dist/configurator/application-runtime/extensions/selectors.d.ts +2 -1
  68. package/dist/configurator/application-runtime/extensions/selectors.js +1 -1
  69. package/dist/configurator/application-runtime/extensions/systems/assistant-system/assistant-system.d.ts +35 -0
  70. package/dist/configurator/application-runtime/extensions/systems/assistant-system/assistant-system.js +1 -0
  71. package/dist/configurator/{assistant → application-runtime/extensions/systems/assistant-system}/browser-details.d.ts +1 -1
  72. package/dist/configurator/application-runtime/extensions/systems/assistant-system/browser-details.js +1 -0
  73. package/dist/configurator/{assistant → application-runtime/extensions/systems/assistant-system}/graphql-tools.d.ts +13 -14
  74. package/dist/configurator/application-runtime/extensions/systems/assistant-system/graphql-tools.js +1 -0
  75. package/dist/configurator/application-runtime/extensions/systems/graphql-system/graphql-system.js +1 -1
  76. package/dist/configurator/application-runtime/extensions/systems/index.d.ts +1 -0
  77. package/dist/configurator/application-runtime/extensions/systems/index.js +1 -1
  78. package/dist/configurator/assistant/use-assistant-chat.js +1 -1
  79. package/dist/docs/index.json +12 -2
  80. package/dist/docs/other-features/domain-actions.mdx +418 -0
  81. package/dist/docs/phoenix/best-practices.mdx +1 -0
  82. package/dist/docs/phoenix/phoenix-cli.mdx +4 -2
  83. package/dist/docs/phoenix/project-structure.mdx +3 -0
  84. package/dist/docs/phoenix/sdk-builder-workflow.mdx +2 -2
  85. package/dist/domain/actions/action-factory.d.ts +10 -0
  86. package/dist/domain/actions/action-factory.js +1 -0
  87. package/dist/domain/actions/prepare-action-implementations.d.ts +15 -0
  88. package/dist/domain/actions/prepare-action-implementations.js +5 -0
  89. package/dist/domain/application/application-config.d.ts +3 -1
  90. package/dist/domain/application/assistant-mutations.d.ts +15 -14
  91. package/dist/domain/application/assistant-mutations.js +1 -1
  92. package/dist/domain/core/action.d.ts +67 -0
  93. package/dist/domain/core/action.js +1 -0
  94. package/dist/domain/core/domain-factory.d.ts +4 -0
  95. package/dist/domain/core/index.d.ts +1 -0
  96. package/dist/domain/domain-builder.d.ts +7 -0
  97. package/dist/domain/domain-builder.js +1 -1
  98. package/dist/domain/domain-context/action-system/action-system.d.ts +40 -0
  99. package/dist/domain/domain-context/action-system/action-system.js +1 -0
  100. package/dist/domain/domain-context/domain-context-systems.d.ts +5 -2
  101. package/dist/domain/domain-context/index.d.ts +1 -0
  102. package/dist/domain/domain-context/index.js +1 -1
  103. package/dist/domain/domain-runtime/domain-context/domain-context.js +1 -1
  104. package/dist/domain/domain-runtime/domain-context/selectors.d.ts +4 -0
  105. package/dist/domain/domain-runtime/domain-context/selectors.js +1 -1
  106. package/dist/domain/domain-runtime/domain-runtime.d.ts +12 -0
  107. package/dist/domain/domain-runtime/spec/initialize.js +1 -1
  108. package/dist/domain/domain-runtime/spec/types.d.ts +15 -1
  109. package/dist/domain/index.d.ts +2 -0
  110. package/dist/domain/index.js +1 -1
  111. package/dist/domain/mocks/create-mock-domain-runtime.d.ts +6 -0
  112. package/dist/editor/domain-editors/DomainSpecContext.js +1 -1
  113. package/dist/editor/domain-editors/apply-spec-update.js +1 -1
  114. package/dist/editor/domain-editors/use-implementation-files.d.ts +7 -3
  115. package/dist/editor/domain-editors/use-implementation-files.js +1 -1
  116. package/dist/phoenix/application-spec/generate-assistant-prompt.d.ts +2 -2
  117. package/dist/phoenix/application-spec/generate-assistant-prompt.js +1 -1
  118. package/dist/phoenix/commands/login.d.ts +24 -6
  119. package/dist/phoenix/commands/login.js +2 -2
  120. package/dist/react-sdk/application-runtime/runtime/create-application-runtime.d.ts +12 -0
  121. package/dist/react-sdk/domain-context/common-hooks/use-domain-context.d.ts +3 -0
  122. package/package.json +3 -3
  123. package/dist/configurator/assistant/browser-details.js +0 -1
  124. package/dist/configurator/assistant/graphql-tools.js +0 -1
@@ -0,0 +1,418 @@
1
+ # Domain Actions
2
+
3
+ - [Use case](#use-case)
4
+ - [Declare the action](#declare-the-action)
5
+ - [Implement it](#implement-it)
6
+ - [Run it from the UI](#run-it-from-the-ui)
7
+ - [Expose it to the assistant](#expose-it-to-the-assistant)
8
+ - [Known gaps](#known-gaps)
9
+ - [Example](#example)
10
+
11
+ ## Use case
12
+
13
+ A plate carries holes, and a hole can be nudged by an offset as long as it stays on the plate. The arrow buttons of a selected hole and the AI assistant both make this change, so the rule ("stay on the plate") has to live in one place.
14
+
15
+ An **action** is that place: a named domain operation you declare once under `actions:`, implement in `src/actions/` with generated parameter and result types, and run from any control through `actionSystem.prepareAction()`. The same action is exposed to the assistant as a custom mutation by naming it under `assistant.actions`. Use an action whenever a change is more than writing one field: a swap, a move among siblings, a change that touches several entities at once.
16
+
17
+ ## Declare the action
18
+
19
+ `params` and `result` are maps of named JSON schemas, with the same `phx:` refs, `references`, `tsType` and `import` as entity shapes. A param whose schema allows `null` is optional; every other param is required. A result member typed `phx:$DomainEntity` is the enhanced entity after the change.
20
+
21
+ ```yaml
22
+ actions:
23
+ nudgeHole:
24
+ description: Moves a hole by an offset on the plate; the move fails when the hole would leave the plate
25
+ params:
26
+ holeId:
27
+ $ref: phx:DomainEntityId
28
+ references: Hole
29
+ description: The hole to move
30
+ dx:
31
+ type: integer
32
+ description: Offset along the x axis, in plate units
33
+ dy:
34
+ type: integer
35
+ description: Offset along the y axis, in plate units
36
+ result:
37
+ hole:
38
+ $ref: phx:$DomainEntity
39
+ references: Hole
40
+ description: The hole at its new position
41
+ plate:
42
+ $ref: phx:$DomainEntity
43
+ references: Plate
44
+ description: The plate after the move
45
+ ```
46
+
47
+ Generation derives the types `ActionTypes`, `ActionName`, `ActionParams<'nudgeHole'>` and `ActionResult<'nudgeHole'>` from it: here `ActionParams<'nudgeHole'>` is `{ holeId: Hole['id']; dx: number; dy: number }` and `ActionResult<'nudgeHole'>` is `{ hole: $Hole; plate: $Plate }`. The `description` of the action, of a param and of a result member becomes the doc comment of its generated property, so it shows in the editor wherever the action is implemented or called.
48
+
49
+ ## Implement it
50
+
51
+ A file in `src/actions/` default-exports `phx.domain.implementActions({...})` with any subset of the declared actions. The implementation receives the validated parameters, the state the action runs on, and a context with the `expressionContext`, the domain `systems`, `getIdSequence()` for entities it creates and the typed `updates` factory. It returns the updates it wants applied and `readResult`, which reads the result from the state those updates produced, including whatever the transformers added. It never stages or commits anything.
52
+
53
+ ```ts
54
+ // src/actions/nudge-hole.ts
55
+ export default phx.domain.implementActions({
56
+ nudgeHole: ({ holeId, dx, dy }, state, { systems: { domain }, updates }) => {
57
+ // `references: Hole` types the id, but any entity id passes the runtime check, so the entity is verified here.
58
+ const hole = state.entities.find(entity => entity.id === holeId)
59
+
60
+ if (hole?.type !== 'Hole') {
61
+ throw new Error(
62
+ `"${holeId}" is not a hole of the plate, so it cannot be nudged. Query the plate's holes and use an id they return.`
63
+ )
64
+ }
65
+
66
+ const x = hole.x + dx
67
+ const y = hole.y + dy
68
+
69
+ if (Math.abs(x) > 150 || Math.abs(y) > 150) {
70
+ throw new Error(
71
+ `Nudging "${holeId}" by (${dx}, ${dy}) would put it at (${x}, ${y}), outside the plate. Keep both coordinates between -150 and 150.`
72
+ )
73
+ }
74
+
75
+ return {
76
+ updates: [updates.DomainEntityModification({ entityId: hole.id, stateDelta: { x, y }, updateTag: 'nudgeHole' })],
77
+ readResult: ({ state: nudged }) => ({
78
+ hole: domain.get$Entity(holeId, nudged),
79
+ plate: domain.getRoot$Entity(nudged),
80
+ }),
81
+ }
82
+ },
83
+ })
84
+ ```
85
+
86
+ Throw to reject a call: the message reaches the user through the control's hint and the assistant through the failed mutation, so say what went wrong and what to send instead. An action declared without an implementation, or an implementation of a name the spec does not declare, is a type error. A declared action without an implementation also stops the domain from initializing, and under `phoenix dev` the editor reports it as a `MISSING_ACTION_IMPLEMENTATIONS` diagnostic and does not apply the change; an implementation of an undeclared name is logged.
87
+
88
+ ## Run it from the UI
89
+
90
+ `phx.frontend.useDomain().actionSystem.prepareAction(name, args, state)` validates `args` against the declared params and returns `{ updates, readResult }`. Hand the updates to `useUpdateControl()` like any other change: it dry-runs them for hints and commits them on click.
91
+
92
+ ```tsx
93
+ export default phx.frontend.createCustomPhoenixEntityElement('NudgeControl', ({ entityId }) => {
94
+ const { actionSystem } = phx.frontend.useDomain()
95
+ const state = phx.frontend.useCommittedState()
96
+ const prepare = phx.frontend.useUpdateControl()
97
+
98
+ const left = prepare(actionSystem.prepareAction('nudgeHole', { holeId: entityId, dx: -20, dy: 0 }, state).updates)
99
+
100
+ return (
101
+ <Button disabled={left.disabled} title={left.hints} onClick={left.update}>
102
+ ←
103
+ </Button>
104
+ )
105
+ })
106
+ ```
107
+
108
+ `prepareAction` throws for arguments that do not fit the params, for an unknown action and for whatever the implementation rejects, so a control that may ask for an impossible move catches the error and shows its message, as the example's control does.
109
+
110
+ ## Expose it to the assistant
111
+
112
+ Naming the action under `assistant.actions` adds a `Mutation` field with one argument per param and a `<Name>Result` type whose entity members are traversable edges. `as` renames the field, `description` replaces the action's.
113
+
114
+ ```yaml
115
+ assistant:
116
+ actions:
117
+ nudgeHole:
118
+ ```
119
+
120
+ ```graphql
121
+ type Mutation {
122
+ nudgeHole(holeId: ID!, dx: Int!, dy: Int!): NudgeHoleResult!
123
+ }
124
+
125
+ type NudgeHoleResult {
126
+ hole: Hole!
127
+ plate: Plate!
128
+ }
129
+ ```
130
+
131
+ The field runs the same `prepareAction`, applies the updates through the same validation as every other mutation, and fails with the action's message when the call is rejected.
132
+
133
+ ## Known gaps
134
+
135
+ - `references` on a `phx:DomainEntityId` param types the parameter and the GraphQL argument, but at runtime any entity id passes the params check: the implementation verifies that the id names an entity of the expected type, as the example does.
136
+ - Actions exist in the configurator domain only; the admin domain declares none.
137
+ - The result of `readResult` is not validated at runtime: the generated TypeScript type and the GraphQL coercion are the checks.
138
+ - The assistant cannot dry-run a custom mutation the way it dry-runs recipes and field options.
139
+ - `useDomain()` types `prepareAction` with the generic action map, so the name and the arguments are not checked against your `ActionTypes` at that call site; the implementation side is fully typed.
140
+
141
+ ## Example
142
+
143
+ Select a hole and nudge it with the arrows. A move that would leave the plate is disabled with the action's message.
144
+
145
+ ## Complete Implementation
146
+
147
+ {/* AUTO-GENERATED SECTION: This section is automatically updated by 'phoenix story build' */}
148
+
149
+ <details>
150
+ <summary>Click to view the complete implementation files</summary>
151
+
152
+ ### ./spec/domain.yaml
153
+
154
+ ```yaml
155
+ model:
156
+ latest:
157
+ entityTypes:
158
+ - Plate
159
+ - Hole
160
+ aliases:
161
+ RootEntity:
162
+ - Plate
163
+ shapes:
164
+ Hole:
165
+ x:
166
+ type: number
167
+ y:
168
+ type: number
169
+
170
+ relations:
171
+ Plate:
172
+ $holes:
173
+ kind: allOfType
174
+ target: Hole
175
+ cardinality: 0-N
176
+ composedOf: true
177
+ Hole:
178
+ $plate:
179
+ kind: allOfType
180
+ target: Plate
181
+ cardinality: 1-1
182
+ composedOf: false
183
+
184
+ expressions:
185
+ Hole:
186
+ $radius:
187
+ type: number
188
+
189
+ # A named domain operation: declared once, implemented in src/actions/, run by the NudgeControl
190
+ # element and by the assistant alike.
191
+ actions:
192
+ nudgeHole:
193
+ description: Moves a hole by an offset on the plate; the move fails when the hole would leave the plate
194
+ params:
195
+ holeId:
196
+ $ref: phx:DomainEntityId
197
+ references: Hole
198
+ description: The hole to move
199
+ dx:
200
+ type: integer
201
+ description: Offset along the x axis, in plate units
202
+ dy:
203
+ type: integer
204
+ description: Offset along the y axis, in plate units
205
+ result:
206
+ hole:
207
+ $ref: phx:$DomainEntity
208
+ references: Hole
209
+ description: The hole at its new position
210
+ plate:
211
+ $ref: phx:$DomainEntity
212
+ references: Plate
213
+ description: The plate after the move
214
+
215
+ assistant:
216
+ ai:
217
+ provider: openai
218
+ model: balanced
219
+ reasoning: low
220
+ entities:
221
+ Plate:
222
+ $holes:
223
+ description: The holes of the plate
224
+ Hole:
225
+ x:
226
+ y:
227
+ actions:
228
+ nudgeHole:
229
+
230
+ presets:
231
+ - rootEntityType: 'Plate'
232
+ initialDomainEntities:
233
+ - id: 'Plate-1'
234
+ type: 'Plate'
235
+ - id: 'Hole-1'
236
+ type: 'Hole'
237
+ x: -60
238
+ y: 40
239
+ - id: 'Hole-2'
240
+ type: 'Hole'
241
+ x: 70
242
+ y: -50
243
+
244
+ components:
245
+ Plate:
246
+ applicationConfig:
247
+ main:
248
+ layout: 'universal'
249
+ staticPerspectiveCamera:
250
+ main:
251
+ distance: 400
252
+ priority: 1
253
+ polarAngle: 1.2
254
+ azimuthAngle: -0.2
255
+ sceneEntityProperties:
256
+ main:
257
+ offsetX: -100
258
+ offsetY: -100
259
+ offsetZ: -10
260
+ sceneEntityObject:
261
+ Plate:
262
+ object:
263
+ $read: $render
264
+ detail:
265
+ main:
266
+ title: 'Plate'
267
+ subtitle: 'A plate with holes'
268
+ description: 'Select a hole to nudge it.'
269
+ Hole:
270
+ sceneEntityProperties:
271
+ main:
272
+ offsetX:
273
+ $read: x
274
+ offsetY:
275
+ $read: y
276
+ sceneEntitySelectHotspot:
277
+ main:
278
+ offsetX: 15
279
+ offsetY: 15
280
+ offsetZ: 30
281
+ detail:
282
+ main:
283
+ title: 'Hole'
284
+ subtitle: 'A hole in the plate'
285
+ description: 'Nudge it with the arrows; a hole cannot leave the plate.'
286
+ propertyPage:
287
+ main:
288
+ children:
289
+ - kind: propertyContainerGroup
290
+ props:
291
+ spacing: large
292
+ children:
293
+ - kind: customElement
294
+ props:
295
+ name: NudgeControl
296
+ ```
297
+
298
+ ### ./src/actions/nudge-hole.ts
299
+
300
+ ```ts
301
+ import { phx } from '../../.phoenix/phx.js'
302
+
303
+ /** How far a hole may sit from the plate's center on either axis, in plate units. */
304
+ const REACH = 150
305
+
306
+ export default phx.domain.implementActions({
307
+ nudgeHole: ({ holeId, dx, dy }, state, { systems: { domain }, updates }) => {
308
+ // `references: Hole` types the id, but any entity id passes the runtime check, so the entity is verified here.
309
+ const hole = state.entities.find(entity => entity.id === holeId)
310
+
311
+ if (hole?.type !== 'Hole') {
312
+ throw new Error(
313
+ `"${holeId}" is not a hole of the plate, so it cannot be nudged. Query the plate's holes and use an id they return.`
314
+ )
315
+ }
316
+
317
+ const x = hole.x + dx
318
+ const y = hole.y + dy
319
+
320
+ if (Math.abs(x) > REACH || Math.abs(y) > REACH) {
321
+ throw new Error(
322
+ `Nudging "${holeId}" by (${dx}, ${dy}) would put it at (${x}, ${y}), outside the plate. Keep both coordinates between ${-REACH} and ${REACH}.`
323
+ )
324
+ }
325
+
326
+ return {
327
+ updates: [updates.DomainEntityModification({ entityId: hole.id, stateDelta: { x, y }, updateTag: 'nudgeHole' })],
328
+ readResult: ({ state: nudged }) => ({
329
+ hole: domain.get$Entity(holeId, nudged),
330
+ plate: domain.getRoot$Entity(nudged),
331
+ }),
332
+ }
333
+ },
334
+ })
335
+ ```
336
+
337
+ ### ./src/elements/NudgeControl.tsx
338
+
339
+ ```tsx
340
+ import { Button, Stack } from '@mui/material'
341
+
342
+ import { PropertyContainer } from '@salsita-ai/sdk/ui'
343
+
344
+ import { phx } from '../../.phoenix/phx.js'
345
+
346
+ const STEP = 20
347
+
348
+ /** Nudges the selected hole with the same `nudgeHole` action the assistant calls, one step at a time. */
349
+ export default phx.frontend.createCustomPhoenixEntityElement('NudgeControl', ({ entityId }) => {
350
+ const { actionSystem } = phx.frontend.useDomain()
351
+ const state = phx.frontend.useCommittedState()
352
+ const prepare = phx.frontend.useUpdateControl()
353
+ const hole = state.entities.find(entity => entity.id === entityId)
354
+
355
+ if (hole?.type !== 'Hole') {
356
+ return <></>
357
+ }
358
+
359
+ // An action throws when the move leaves the plate; the button for that direction is then disabled with the message.
360
+ const nudge = (dx: number, dy: number) => {
361
+ try {
362
+ return prepare(actionSystem.prepareAction('nudgeHole', { holeId: hole.id, dx, dy }, state).updates)
363
+ } catch (error) {
364
+ return { disabled: true, hints: error instanceof Error ? error.message : String(error), update: () => {} }
365
+ }
366
+ }
367
+
368
+ const directions = [
369
+ { label: '←', dx: -STEP, dy: 0 },
370
+ { label: '→', dx: STEP, dy: 0 },
371
+ { label: '↑', dx: 0, dy: STEP },
372
+ { label: '↓', dx: 0, dy: -STEP },
373
+ ]
374
+
375
+ return (
376
+ <PropertyContainer.Item title="Nudge" description={`Moves the hole by ${STEP} units.`}>
377
+ <Stack direction="row" spacing={1}>
378
+ {directions.map(({ label, dx, dy }) => {
379
+ const control = nudge(dx, dy)
380
+
381
+ return (
382
+ <Button
383
+ key={label}
384
+ variant="outlined"
385
+ disabled={control.disabled}
386
+ title={control.hints ?? undefined}
387
+ onClick={event => {
388
+ event.stopPropagation()
389
+ control.update()
390
+ }}
391
+ >
392
+ {label}
393
+ </Button>
394
+ )
395
+ })}
396
+ </Stack>
397
+ </PropertyContainer.Item>
398
+ )
399
+ })
400
+ ```
401
+
402
+ ### ./src/expressions/plate.ts
403
+
404
+ ```ts
405
+ import { createPlate } from '../../../../common/plate-rendering.js'
406
+ import { phx } from '../../.phoenix/phx.js'
407
+
408
+ export default phx.domain.implementCustomExpressions({
409
+ Hole: {
410
+ $radius: () => 10,
411
+ },
412
+ Plate: {
413
+ $render: plate => createPlate({ features: plate.$holes }),
414
+ },
415
+ })
416
+ ```
417
+
418
+ </details>
@@ -5,6 +5,7 @@
5
5
  1. **YAML is king** - 99% of configuration should be in spec/\*.yaml files
6
6
  2. **Custom code locations** (in order of preference):
7
7
  - `src/expressions/` - Computed/derived properties
8
+ - `src/actions/` - Named domain operations declared under `actions:`
8
9
  - `src/invariants/` - Business rule constraints
9
10
  - `src/transformers/` - Automatic state fixers
10
11
  - `src/handlers/` - Event & lifecycle handlers
@@ -569,9 +569,11 @@ SDK-builder workspaces sign the CLI in with a personal API token instead of proj
569
569
 
570
570
  Signs the CLI in to the SDK-builder workspace `<customer>`:
571
571
 
572
- 1. Opens Studio, where you approve the sign-in (use `--no-open` to only print the link).
572
+ 1. Opens Studio, where you name the token and approve the sign-in (use `--no-open` to only print the link). The page shows the workspace, your account, the computer the CLI runs on and how long the link still works. The CLI suggests the name `phoenix CLI on <computer>`; change it so you can tell your machines and agents apart later.
573
573
  2. Studio sends a one-time code back to the CLI on `127.0.0.1`.
574
- 3. The CLI exchanges the code for a token.
574
+ 3. The CLI exchanges the code for a token with the name you approved, and sends the browser back to Studio, which confirms the sign-in.
575
+
576
+ **Deny** in Studio stops the CLI at once: it exits with `The sign-in was denied in Studio` and no token is created. The link works for five minutes and only for the CLI that printed it; after that, Studio shows that it no longer works and the CLI gives up.
575
577
 
576
578
  The token is stored in `~/.phoenix/credentials.json`, readable only by you, per server, together with the Studio URL of the sign-in; `PHOENIX_CREDENTIALS_FILE` moves the file. The token works until you revoke it. Signing in again to the same server revokes the token it replaces; the CLI warns if the server could not revoke it.
577
579
 
@@ -40,6 +40,7 @@ project-root/
40
40
  | `components/` | Phoenix system components (ECS) |
41
41
  | `elements/` | Page elements (pages, providers) |
42
42
  | `expressions/` | Domain logic expressions |
43
+ | `actions/` | Implementations of the domain actions declared under `actions:` |
43
44
  | `invariants/` | Domain constraints |
44
45
  | `transformers/` | Data transformation logic |
45
46
  | `handlers/` | Event & lifecycle handlers |
@@ -249,6 +250,7 @@ project-root/
249
250
  │ ├── components/ # Phoenix system components (ECS)
250
251
  │ ├── elements/ # UI elements (pages, providers)
251
252
  │ ├── expressions/ # Domain logic expressions
253
+ │ ├── actions/ # Domain action implementations
252
254
  │ ├── invariants/ # Domain constraints
253
255
  │ ├── transformers/ # Data transformation logic
254
256
  │ ├── handlers/ # Event & lifecycle handlers
@@ -362,6 +364,7 @@ Domain logic files define the business rules and computed properties of your con
362
364
  | Purpose | Location | Example Files |
363
365
  | ------------ | ------------------- | ---------------------------------------------- |
364
366
  | Expressions | `src/expressions/` | `table.ts`, `legs.ts`, `module.ts` |
367
+ | Actions | `src/actions/` | `move-part.ts`, `swap-cabinets.ts` |
365
368
  | Invariants | `src/invariants/` | `top.ts`, `legs.ts` |
366
369
  | Transformers | `src/transformers/` | `fix-table-top-material-update-transformer.ts` |
367
370
  | Migrations | `src/migrations/` | `v2.ts`, `v3.ts`, `v4.ts` |
@@ -36,11 +36,11 @@ The starter `spec/application.yaml` begins with `project: default`, which means
36
36
  pnpm phoenix login <workspace>
37
37
  ```
38
38
 
39
- `phoenix login` opens Studio, where you approve the sign-in. Studio sends a one-time code back to the CLI on `127.0.0.1`, and the CLI exchanges it for a personal token. The code only works for the CLI that asked for it and expires after five minutes. Use `--no-open` to print the link instead of opening a browser.
39
+ `phoenix login` opens Studio, where you name the token and approve the sign-in. The CLI suggests `phoenix CLI on <computer>`; a name of your own, such as the agent and the machine, tells your tokens apart later. Studio sends a one-time code back to the CLI on `127.0.0.1`, the CLI exchanges it for a personal token and returns the browser to Studio, which confirms the sign-in. The code only works for the CLI that asked for it and expires after five minutes. **Deny** stops the waiting CLI at once and creates no token. Use `--no-open` to print the link instead of opening a browser.
40
40
 
41
41
  The CLI stores the token in `~/.phoenix/credentials.json`, readable only by you, with one entry per server. `PHOENIX_CREDENTIALS_FILE` moves the file. The token acts for you in your workspace until you revoke it. It stops working when you are removed from the workspace or banned.
42
42
 
43
- `pnpm phoenix logout` revokes the token and removes it from the file. Studio's **CLI tokens** page lists your tokens with when they were last used, and revokes any of them, for example one left on another computer.
43
+ `pnpm phoenix logout` revokes the token and removes it from the file. Studio's **CLI tokens** page lists your tokens by the names you gave them, with when they were last used, and revokes any of them, for example one left on another computer. A revoked token leaves the list.
44
44
 
45
45
  `login`, `logout` and `deploy` follow the region, not the `SERVER_URL` in the project's `.env`. `dev` follows the region too when `.env` has no `SERVER_URL`:
46
46
 
@@ -0,0 +1,10 @@
1
+ import type { ActionImplementations, ActionTypes } from '../core/action.js';
2
+ import type { $DomainEntity, DomainEntity } from '../core/model.js';
3
+ /**
4
+ * Creates the `implementActions` factory the generated `domain-spec.ts` exposes as
5
+ * `phx.domain.implementActions`. A module in `src/actions/` implements any subset of the declared
6
+ * actions and gets each parameter and result typed from the spec; a name the spec does not declare
7
+ * is a type error, since its implementation would have to be `never`. The generated barrel merges
8
+ * the modules and checks that every declared action is implemented.
9
+ */
10
+ export declare const createActionFactory: <ENTITIES extends DomainEntity<string>, ALLOWED_ROOT_ENTITIES extends ENTITIES, $MAP extends Record<ENTITIES['type'], $DomainEntity<ENTITIES>>, PUBLISHED_DATA extends Record<string, unknown>, SESSION_DATA extends Record<string, unknown>, FRONTEND_DATA extends Record<string, unknown>, NAVIGATION_STATES extends string, VIEW_STATES extends string, TOOL_STATES extends string, EXPORT_STATES extends string, ACTION_TYPES extends ActionTypes>() => <IMPLEMENTATIONS extends Partial<ActionImplementations<ENTITIES, ALLOWED_ROOT_ENTITIES, $MAP, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, NAVIGATION_STATES, VIEW_STATES, TOOL_STATES, EXPORT_STATES, ACTION_TYPES>>>(implementations: IMPLEMENTATIONS & Readonly<Record<Exclude<keyof IMPLEMENTATIONS, keyof ACTION_TYPES>, never>>) => IMPLEMENTATIONS;
@@ -0,0 +1 @@
1
+ export const createActionFactory=()=>t=>t;
@@ -0,0 +1,15 @@
1
+ import type { ActionDefinitions, AnyActionImplementations } from '../core/index.js';
2
+ /** The declared actions that have no implementation, and the implementations of undeclared names. */
3
+ export interface ActionImplementationConflicts {
4
+ readonly missing: readonly string[];
5
+ readonly extra: readonly string[];
6
+ }
7
+ /** Compares the names of the declared actions with the implemented ones. */
8
+ export declare const findActionImplementationConflicts: (declared: readonly string[], implementations: AnyActionImplementations) => ActionImplementationConflicts;
9
+ /**
10
+ * Validates the implemented actions against the declared ones, as `prepareDomainExpressions` does
11
+ * for expressions: a declared action without an implementation throws, so the domain is not
12
+ * instantiated with an action that can only fail when it is called; an implementation of an
13
+ * undeclared name is logged. Returns the implementations unchanged.
14
+ */
15
+ export declare const prepareActionImplementations: (definitions: ActionDefinitions, implementations: AnyActionImplementations) => AnyActionImplementations;
@@ -0,0 +1,5 @@
1
+ export const findActionImplementationConflicts=(e,n)=>{let t=Object.keys(n);return{missing:e.filter(e=>!t.includes(e)),extra:t.filter(n=>!e.includes(n))}};export const prepareActionImplementations=(e,n)=>{let{missing:t,extra:i}=findActionImplementationConflicts(Object.values(e).map(e=>e.name),n);if(0===t.length&&0===i.length)return n;let o=Error("Domain cannot be instantiated because there are conflicts between declared and implemented actions."+(t.length>0?`
2
+
3
+ Missing actions: ${t.join(", ")}. Add each to a default export of phx.domain.implementActions({ ... }) in src/actions/.`:"")+(i.length>0?`
4
+
5
+ Extra actions: ${i.join(", ")}. Declare each under "actions" in a spec file or remove its implementation.`:"")+"\n");if(t.length>0)throw o;return console.error(o),n};
@@ -112,7 +112,8 @@ export interface AssistantFieldOptions {
112
112
  * the recipe groups `meta { childRecipes }` and `addChild` expose, in manifest order, and absent
113
113
  * means none; `replaceRecipes` lists the same for `meta { replaceRecipes }` and `replaceEntity`;
114
114
  * `fieldOptions` lists, per entity type, the options edges `meta` exposes for its
115
- * writable fields, and absent means none. Consumed by the configurator GraphQL system.
115
+ * writable fields, and absent means none; `actions` maps each custom `Mutation` field to the
116
+ * declared action it runs, and absent means none. Consumed by the configurator GraphQL system.
116
117
  */
117
118
  export interface AssistantManifest {
118
119
  readonly sdl: string;
@@ -122,6 +123,7 @@ export interface AssistantManifest {
122
123
  readonly childRecipes?: Readonly<Record<string, readonly AssistantRecipeGroup[]>>;
123
124
  readonly replaceRecipes?: Readonly<Record<string, readonly AssistantRecipeGroup[]>>;
124
125
  readonly fieldOptions?: Readonly<Record<string, readonly AssistantFieldOptions[]>>;
126
+ readonly actions?: Readonly<Record<string, string>>;
125
127
  }
126
128
  export type AdminPagesSpec = {
127
129
  readonly presets?: boolean | {
@@ -1,9 +1,9 @@
1
+ import type { Localizable } from '@salsita-ai/common/utils';
1
2
  import { type StateUpdate } from '../components/updates.js';
2
- import type { Domain } from '../core/domain.js';
3
- import { type $DomainEntity, type DomainEntity } from '../core/model.js';
3
+ import type { AnyActionContext } from '../core/action.js';
4
+ import { type DomainEntity } from '../core/model.js';
4
5
  import type { DomainState } from '../core/state.js';
5
6
  import type { AssistantManifest } from './application-config.js';
6
- import { type AssistantRecipeContext } from './assistant-recipe-groups.js';
7
7
  type AnyDomainState = DomainState<DomainEntity<string>>;
8
8
  /**
9
9
  * Update tag of user-intended entity deletions, from the configurator's delete controls and the
@@ -12,19 +12,19 @@ type AnyDomainState = DomainState<DomainEntity<string>>;
12
12
  export declare const deleteEntityUpdateTag = "deleteEntity";
13
13
  /** Update tag of the entity modifications the assistant's `modify<Type>` mutations make. */
14
14
  export declare const modifyEntityUpdateTag = "modifyEntity";
15
- /** The domain of any application, as far as a mutation implementation reads it. */
16
- type AnyDomain = Domain<DomainEntity<string>, DomainEntity<string>, Record<string, $DomainEntity<DomainEntity<string>>>>;
17
15
  /** The arguments of one mutation field, as the GraphQL executor coerced them against the field's schema. */
18
16
  export type AssistantMutationArgs = Readonly<Record<string, unknown>>;
19
17
  /**
20
- * What a mutation implementation reads besides its arguments and the state it runs against. The
21
- * recipe context is what `addChild` and `replaceEntity` read their recipe groups and cook their recipes with, and what
22
- * `modify<Type>` reads the choices of a field with.
18
+ * What a mutation implementation reads besides its arguments and the state it runs against: the
19
+ * manifest, and the context the application's actions run with on that state. The built-ins and the
20
+ * custom mutations share it, so a built-in reads the domain, the component reader and the recipe
21
+ * cooker through `action.systems`, the way an action implementation does.
23
22
  */
24
- export interface AssistantMutationContext extends AssistantRecipeContext {
23
+ export interface AssistantMutationContext {
25
24
  readonly manifest: AssistantManifest;
26
- /** Validates a changed entity against its shape, and reads entities for the result of a field. */
27
- readonly domain: Pick<AnyDomain, 'validateEntity' | 'getEntity' | 'get$Entity'>;
25
+ readonly action: AnyActionContext;
26
+ /** Resolves a title in the current language, for recipe keys and option names. */
27
+ readonly resolveLocalizable: (message: Localizable) => string;
28
28
  }
29
29
  /** The working state a mutation field ran against and the state its applied updates produced. */
30
30
  export interface AssistantMutationStateChange {
@@ -56,9 +56,10 @@ export interface AssistantMutationDeclaration {
56
56
  readonly implementation: AssistantMutationImplementation;
57
57
  }
58
58
  /**
59
- * The declaration resolving a `Mutation` field: a built-in by its name, or the shared modify
60
- * implementation when the name is `modify` followed by an entity type the manifest declares writable
61
- * fields for. `undefined` when the configurator has no implementation for the field.
59
+ * The declaration resolving a `Mutation` field: a built-in by its name, the action a custom mutation
60
+ * of that name runs, or the shared modify implementation when the name is `modify` followed by an
61
+ * entity type the manifest declares writable fields for. `undefined` when the configurator has no
62
+ * implementation for the field.
62
63
  */
63
64
  export declare const findAssistantMutation: (fieldName: string, manifest: AssistantManifest) => AssistantMutationDeclaration | undefined;
64
65
  export {};
@@ -1 +1 @@
1
- import{z as e}from"zod";import{systemUpdateFactory as t}from"../components/updates.js";import{DomainEntityIdSchema as i}from"../core/model.js";import{findAssistantFieldOptions as r,readAssistantFieldOptions as n}from"./assistant-field-options.js";import{assistantAddedEntityIds as o,findAssistantRecipeGroups as a,prepareAssistantRecipeUpdates as s,readAssistantRecipeGroup as d}from"./assistant-recipe-groups.js";export const deleteEntityUpdateTag="deleteEntity";export const modifyEntityUpdateTag="modifyEntity";let l=e.string().regex(new RegExp(i.pattern),'An entity id is the type name, a dash and a number, e.g. "Root-1". Query the current entities and use an id they return.'),p=e.object({id:l}),h=e.object({id:l,delta:e.record(e.unknown())}),u="modify",f=(e,t)=>Object.entries(e).find(([e])=>e===t)?.[1],y=e=>Array.isArray(e)?e.map(y):"object"!=typeof e||null===e||Array.isArray(e)?e:Object.fromEntries(Object.entries(e).map(([e,t])=>[e,y(t)])),c=e.object({parentId:l,recipeGroupId:e.string(),recipeKey:e.string()}),g=e.object({entityId:l,recipeGroupId:e.string(),recipeKey:e.string()}),m=e=>e.map(e=>`"${e}"`).join(", "),$=[{name:"deleteEntity",implementation:(e,i,{manifest:r})=>{let{id:n}=p.parse(e),o=i.entities.find(e=>e.id===n);if(!o)throw Error(`Entity "${n}" does not exist, so it cannot be deleted. It may have been deleted by an earlier operation or field. Query the current entities and use an id they return.`);let a=r.deletableTypes??[];if(!a.includes(o.type))throw Error(`Entity "${n}" is a ${o.type}, which is not deletable. ${a.length>0?`Only entities of these types can be deleted: ${a.join(", ")}.`:"No entity type is deletable."} Delete an entity of a deletable type instead.`);return{updates:[t.EntityDeletion({entityId:o.id,updateTag:"deleteEntity"})],readResult:({previousState:e,state:t})=>{var i;let r;return[o.id,...(i=o.id,r=new Set(t.entities.map(e=>e.id)),e.entities.map(e=>e.id).filter(e=>e!==i&&!r.has(e)))]}}}},{name:"addChild",implementation:(e,t,i)=>{let{parentId:r,recipeGroupId:n,recipeKey:l}=c.parse(e),p=t.entities.find(e=>e.id===r);if(!p)throw Error(`Entity "${r}" does not exist, so nothing can be added to it. It may have been deleted by an earlier operation or field. Query the current entities and use an id they return.`);let h=a(i.manifest,"child",p.type),u=h.find(e=>e.groupId===n);if(!u)throw Error(`Entity "${r}" is a ${p.type}, which has no recipe group "${n}". ${h.length>0?`Its recipe groups are: ${m(h.map(({groupId:e})=>e))}.`:`Nothing can be added to a ${p.type}.`} Query "meta { childRecipes { groupId } }" on the entity you want to add to and use a groupId it returns.`);let f=d("child",u,p.id,t,i);if(!f)throw Error(`The recipe group "${n}" is not available on "${r}" right now, so nothing can be added from it. The configurator offers a group only in some states of the configuration. Query "meta { childRecipes { groupId } }" on "${r}" to see the groups it offers now.`);let y=f.recipes.find(e=>e.key===l);if(!y)throw Error(`The recipe group "${n}" on "${r}" has no recipe with the key "${l}". Recipes and their keys change with the configuration and with the language. List the recipes of the group again and use a key it returns.`);let g=s(y,f,p.id,t,i);return{updates:g,readResult:e=>o(g,p.id,e).map(t=>i.domain.get$Entity(t,e.state))}}},{name:"replaceEntity",implementation:(e,t,i)=>{let{entityId:r,recipeGroupId:n,recipeKey:l}=g.parse(e),p=t.entities.find(e=>e.id===r);if(!p)throw Error(`Entity "${r}" does not exist, so it cannot be replaced. It may have been deleted or replaced by an earlier operation or field; a replaced entity gets a new id. Query the current entities and use an id they return.`);let h=a(i.manifest,"replace",p.type),u=h.find(e=>e.groupId===n);if(!u)throw Error(`Entity "${r}" is a ${p.type}, which has no replace recipe group "${n}". ${h.length>0?`Its replace recipe groups are: ${m(h.map(({groupId:e})=>e))}.`:`A ${p.type} cannot be replaced.`} Query "meta { replaceRecipes { groupId } }" on the entity you want to replace and use a groupId it returns.`);let f=d("replace",u,p.id,t,i);if(!f)throw Error(`The recipe group "${n}" is not available on "${r}" right now, so it cannot be replaced from it. The configurator offers a group only in some states of the configuration. Query "meta { replaceRecipes { groupId } }" on "${r}" to see the groups it offers now.`);let y=f.recipes.find(e=>e.key===l);if(!y)throw Error(`The recipe group "${n}" on "${r}" has no recipe with the key "${l}". Recipes and their keys change with the configuration and with the language. List the recipes of the group again and use a key it returns.`);let c=s(y,f,p.id,t,i);return{updates:c,readResult:e=>o(c,p.id,e).map(t=>i.domain.get$Entity(t,e.state))}}}];export const findAssistantMutation=(e,i)=>{let o,a,s=$.find(t=>t.name===e);if(s||!e.startsWith(u))return s;let d=e.slice(u.length),l=f(i.writableFields??{},d);return l&&(o=`${u}${d}`,a=Object.keys(l).join(", "),{name:o,implementation:(e,i,s)=>{let{manifest:p,domain:c}=s,{id:g,delta:m}=h.parse(e),$=i.entities.find(e=>e.id===g);if(!$)throw Error(`Entity "${g}" does not exist, so it cannot be modified. It may have been deleted by an earlier operation or field. Query the current entities and use an id they return.`);if($.type!==d)throw Error(`Entity "${g}" is a ${$.type}, but ${o} modifies only ${d} entities. ${f(p.writableFields??{},$.type)?`Use ${u}${$.type} for it instead.`:`${$.type} has no writable fields, so it cannot be modified; pass the id of a ${d}.`}`);let w=Object.entries(m).flatMap(([e,t])=>{let i=f(l,e);return void 0===i?[]:[{field:e,property:i,value:y(t)}]}),b=Object.fromEntries(w.map(({property:e,value:t})=>[e,t]));if(0===Object.keys(b).length)throw Error(`The delta of ${o} is empty, so there is nothing to change on "${g}". Set at least one of its fields: ${a}.`);let E=c.validateEntity({...$,...b});if(!E.success){let e=w.filter(({field:e,property:t})=>e!==t).map(({field:e,property:t})=>`${e} is stored as ${t}`);throw Error(`The delta would make "${g}" an invalid ${d}: ${E.errors.join("; ")}. ${e.length>0?`The messages name stored properties: ${e.join(", ")}. `:""}Nothing was changed. Send values that fit the types and the allowed values the schema describes for these fields.`)}let j=r(p,d);for(let{field:e,value:t}of w){let r=j.find(t=>t.field===e);if(!r?.validate||null===t)continue;let o=n(r,$.id,i,s);if(o&&!o.some(e=>e.value===t))throw Error(`${JSON.stringify(t)} is not a value "${e}" of "${g}" can take right now. ${o.length>0?`The configurator offers these values: ${o.map(e=>JSON.stringify(e.value)).join(", ")}.`:"The configurator offers no value for it in this state."} Nothing was changed. Send one of the offered values; "meta { ${r.edgeName} }" on the entity lists them with their names. When the values depend on another field of this delta, change that field in an earlier mutation field of the same operation.`)}return{updates:[t.DomainEntityModification({entityId:$.id,stateDelta:b,updateTag:"modifyEntity"})],readResult:({state:e})=>c.getEntity($.id,e)?c.get$Entity($.id,e):null}}})};
1
+ import{z as e}from"zod";import{systemUpdateFactory as t}from"../components/updates.js";import{DomainEntityIdSchema as i}from"../core/model.js";import{findAssistantFieldOptions as n,readAssistantFieldOptions as r}from"./assistant-field-options.js";import{assistantAddedEntityIds as o,findAssistantRecipeGroups as a,prepareAssistantRecipeUpdates as s,readAssistantRecipeGroup as d}from"./assistant-recipe-groups.js";export const deleteEntityUpdateTag="deleteEntity";export const modifyEntityUpdateTag="modifyEntity";let l=({action:e,resolveLocalizable:t})=>({componentReader:e.systems.componentReader,recipeCooker:e.systems.recipeCooker,resolveLocalizable:t}),p=e.string().regex(new RegExp(i.pattern),'An entity id is the type name, a dash and a number, e.g. "Root-1". Query the current entities and use an id they return.'),h=e.object({id:p}),c=e.object({id:p,delta:e.record(e.unknown())}),y="modify",u=(e,t)=>Object.entries(e).find(([e])=>e===t)?.[1],f=e=>Array.isArray(e)?e.map(f):"object"!=typeof e||null===e||Array.isArray(e)?e:Object.fromEntries(Object.entries(e).map(([e,t])=>[e,f(t)])),m=e.object({parentId:p,recipeGroupId:e.string(),recipeKey:e.string()}),g=e.object({entityId:p,recipeGroupId:e.string(),recipeKey:e.string()}),$=e=>e.map(e=>`"${e}"`).join(", "),w=[{name:"deleteEntity",implementation:(e,i,{manifest:n})=>{let{id:r}=h.parse(e),o=i.entities.find(e=>e.id===r);if(!o)throw Error(`Entity "${r}" does not exist, so it cannot be deleted. It may have been deleted by an earlier operation or field. Query the current entities and use an id they return.`);let a=n.deletableTypes??[];if(!a.includes(o.type))throw Error(`Entity "${r}" is a ${o.type}, which is not deletable. ${a.length>0?`Only entities of these types can be deleted: ${a.join(", ")}.`:"No entity type is deletable."} Delete an entity of a deletable type instead.`);return{updates:[t.EntityDeletion({entityId:o.id,updateTag:"deleteEntity"})],readResult:({previousState:e,state:t})=>{var i;let n;return[o.id,...(i=o.id,n=new Set(t.entities.map(e=>e.id)),e.entities.map(e=>e.id).filter(e=>e!==i&&!n.has(e)))]}}}},{name:"addChild",implementation:(e,t,i)=>{let{parentId:n,recipeGroupId:r,recipeKey:p}=m.parse(e),h=t.entities.find(e=>e.id===n);if(!h)throw Error(`Entity "${n}" does not exist, so nothing can be added to it. It may have been deleted by an earlier operation or field. Query the current entities and use an id they return.`);let c=a(i.manifest,"child",h.type),y=c.find(e=>e.groupId===r);if(!y)throw Error(`Entity "${n}" is a ${h.type}, which has no recipe group "${r}". ${c.length>0?`Its recipe groups are: ${$(c.map(({groupId:e})=>e))}.`:`Nothing can be added to a ${h.type}.`} Query "meta { childRecipes { groupId } }" on the entity you want to add to and use a groupId it returns.`);let u=l(i),f=d("child",y,h.id,t,u);if(!f)throw Error(`The recipe group "${r}" is not available on "${n}" right now, so nothing can be added from it. The configurator offers a group only in some states of the configuration. Query "meta { childRecipes { groupId } }" on "${n}" to see the groups it offers now.`);let g=f.recipes.find(e=>e.key===p);if(!g)throw Error(`The recipe group "${r}" on "${n}" has no recipe with the key "${p}". Recipes and their keys change with the configuration and with the language. List the recipes of the group again and use a key it returns.`);let w=s(g,f,h.id,t,u);return{updates:w,readResult:e=>o(w,h.id,e).map(t=>i.action.systems.domain.get$Entity(t,e.state))}}},{name:"replaceEntity",implementation:(e,t,i)=>{let{entityId:n,recipeGroupId:r,recipeKey:p}=g.parse(e),h=t.entities.find(e=>e.id===n);if(!h)throw Error(`Entity "${n}" does not exist, so it cannot be replaced. It may have been deleted or replaced by an earlier operation or field; a replaced entity gets a new id. Query the current entities and use an id they return.`);let c=a(i.manifest,"replace",h.type),y=c.find(e=>e.groupId===r);if(!y)throw Error(`Entity "${n}" is a ${h.type}, which has no replace recipe group "${r}". ${c.length>0?`Its replace recipe groups are: ${$(c.map(({groupId:e})=>e))}.`:`A ${h.type} cannot be replaced.`} Query "meta { replaceRecipes { groupId } }" on the entity you want to replace and use a groupId it returns.`);let u=l(i),f=d("replace",y,h.id,t,u);if(!f)throw Error(`The recipe group "${r}" is not available on "${n}" right now, so it cannot be replaced from it. The configurator offers a group only in some states of the configuration. Query "meta { replaceRecipes { groupId } }" on "${n}" to see the groups it offers now.`);let m=f.recipes.find(e=>e.key===p);if(!m)throw Error(`The recipe group "${r}" on "${n}" has no recipe with the key "${p}". Recipes and their keys change with the configuration and with the language. List the recipes of the group again and use a key it returns.`);let w=s(m,f,h.id,t,u);return{updates:w,readResult:e=>o(w,h.id,e).map(t=>i.action.systems.domain.get$Entity(t,e.state))}}}];export const findAssistantMutation=(e,i)=>{let o,a,s=w.find(t=>t.name===e),d=u(i.actions??{},e);if(s)return s;if(void 0!==d)return{name:e,implementation:(e,t,{action:i})=>i.systems.actionSystem.prepareAction(d,f(e),t)};if(!e.startsWith(y))return;let p=e.slice(y.length),h=u(i.writableFields??{},p);return h&&(o=`${y}${p}`,a=Object.keys(h).join(", "),{name:o,implementation:(e,i,s)=>{let{manifest:d}=s,{domain:m}=s.action.systems,{id:g,delta:$}=c.parse(e),w=i.entities.find(e=>e.id===g);if(!w)throw Error(`Entity "${g}" does not exist, so it cannot be modified. It may have been deleted by an earlier operation or field. Query the current entities and use an id they return.`);if(w.type!==p)throw Error(`Entity "${g}" is a ${w.type}, but ${o} modifies only ${p} entities. ${u(d.writableFields??{},w.type)?`Use ${y}${w.type} for it instead.`:`${w.type} has no writable fields, so it cannot be modified; pass the id of a ${p}.`}`);let b=Object.entries($).flatMap(([e,t])=>{let i=u(h,e);return void 0===i?[]:[{field:e,property:i,value:f(t)}]}),E=Object.fromEntries(b.map(({property:e,value:t})=>[e,t]));if(0===Object.keys(E).length)throw Error(`The delta of ${o} is empty, so there is nothing to change on "${g}". Set at least one of its fields: ${a}.`);let j=m.validateEntity({...w,...E});if(!j.success){let e=b.filter(({field:e,property:t})=>e!==t).map(({field:e,property:t})=>`${e} is stored as ${t}`);throw Error(`The delta would make "${g}" an invalid ${p}: ${j.errors.join("; ")}. ${e.length>0?`The messages name stored properties: ${e.join(", ")}. `:""}Nothing was changed. Send values that fit the types and the allowed values the schema describes for these fields.`)}let v=n(d,p);for(let{field:e,value:t}of b){let n=v.find(t=>t.field===e);if(!n?.validate||null===t)continue;let o=r(n,w.id,i,l(s));if(o&&!o.some(e=>e.value===t))throw Error(`${JSON.stringify(t)} is not a value "${e}" of "${g}" can take right now. ${o.length>0?`The configurator offers these values: ${o.map(e=>JSON.stringify(e.value)).join(", ")}.`:"The configurator offers no value for it in this state."} Nothing was changed. Send one of the offered values; "meta { ${n.edgeName} }" on the entity lists them with their names. When the values depend on another field of this delta, change that field in an earlier mutation field of the same operation.`)}return{updates:[t.DomainEntityModification({entityId:w.id,stateDelta:E,updateTag:"modifyEntity"})],readResult:({state:e})=>m.getEntity(w.id,e)?m.get$Entity(w.id,e):null}}})};