@api-client/core 0.19.0 → 0.19.2

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 (121) hide show
  1. package/build/src/decorators/observed.d.ts.map +1 -1
  2. package/build/src/decorators/observed.js +49 -8
  3. package/build/src/decorators/observed.js.map +1 -1
  4. package/build/src/mocking/ModelingMock.d.ts +2 -0
  5. package/build/src/mocking/ModelingMock.d.ts.map +1 -1
  6. package/build/src/mocking/ModelingMock.js +2 -0
  7. package/build/src/mocking/ModelingMock.js.map +1 -1
  8. package/build/src/mocking/lib/Ai.d.ts +11 -0
  9. package/build/src/mocking/lib/Ai.d.ts.map +1 -0
  10. package/build/src/mocking/lib/Ai.js +53 -0
  11. package/build/src/mocking/lib/Ai.js.map +1 -0
  12. package/build/src/modeling/ai/DataDomainDelta.d.ts +146 -0
  13. package/build/src/modeling/ai/DataDomainDelta.d.ts.map +1 -0
  14. package/build/src/modeling/ai/DataDomainDelta.js +729 -0
  15. package/build/src/modeling/ai/DataDomainDelta.js.map +1 -0
  16. package/build/src/modeling/ai/DomainSerialization.d.ts +20 -0
  17. package/build/src/modeling/ai/DomainSerialization.d.ts.map +1 -0
  18. package/build/src/modeling/ai/DomainSerialization.js +185 -0
  19. package/build/src/modeling/ai/DomainSerialization.js.map +1 -0
  20. package/build/src/modeling/ai/domain_response_schema.d.ts +806 -0
  21. package/build/src/modeling/ai/domain_response_schema.d.ts.map +1 -0
  22. package/build/src/modeling/ai/domain_response_schema.js +289 -0
  23. package/build/src/modeling/ai/domain_response_schema.js.map +1 -0
  24. package/build/src/modeling/ai/domain_tools.d.ts +68 -0
  25. package/build/src/modeling/ai/domain_tools.d.ts.map +1 -0
  26. package/build/src/modeling/ai/domain_tools.js +71 -0
  27. package/build/src/modeling/ai/domain_tools.js.map +1 -0
  28. package/build/src/modeling/ai/index.d.ts +10 -0
  29. package/build/src/modeling/ai/index.d.ts.map +1 -0
  30. package/build/src/modeling/ai/index.js +9 -0
  31. package/build/src/modeling/ai/index.js.map +1 -0
  32. package/build/src/modeling/ai/message_parser.d.ts +23 -0
  33. package/build/src/modeling/ai/message_parser.d.ts.map +1 -0
  34. package/build/src/modeling/ai/message_parser.js +93 -0
  35. package/build/src/modeling/ai/message_parser.js.map +1 -0
  36. package/build/src/modeling/ai/prompts/domain_system.d.ts +6 -0
  37. package/build/src/modeling/ai/prompts/domain_system.d.ts.map +1 -0
  38. package/build/src/modeling/ai/prompts/domain_system.js +80 -0
  39. package/build/src/modeling/ai/prompts/domain_system.js.map +1 -0
  40. package/build/src/modeling/ai/tools/DataDomain.tools.d.ts +25 -0
  41. package/build/src/modeling/ai/tools/DataDomain.tools.d.ts.map +1 -0
  42. package/build/src/modeling/ai/tools/DataDomain.tools.js +334 -0
  43. package/build/src/modeling/ai/tools/DataDomain.tools.js.map +1 -0
  44. package/build/src/modeling/ai/tools/Semantic.tools.d.ts +48 -0
  45. package/build/src/modeling/ai/tools/Semantic.tools.d.ts.map +1 -0
  46. package/build/src/modeling/ai/tools/Semantic.tools.js +36 -0
  47. package/build/src/modeling/ai/tools/Semantic.tools.js.map +1 -0
  48. package/build/src/modeling/ai/tools/config.d.ts +13 -0
  49. package/build/src/modeling/ai/tools/config.d.ts.map +1 -0
  50. package/build/src/modeling/ai/tools/config.js +2 -0
  51. package/build/src/modeling/ai/tools/config.js.map +1 -0
  52. package/build/src/modeling/ai/types.d.ts +302 -0
  53. package/build/src/modeling/ai/types.d.ts.map +1 -0
  54. package/build/src/modeling/ai/types.js +40 -0
  55. package/build/src/modeling/ai/types.js.map +1 -0
  56. package/build/src/models/AiMessage.d.ts +185 -0
  57. package/build/src/models/AiMessage.d.ts.map +1 -0
  58. package/build/src/models/AiMessage.js +203 -0
  59. package/build/src/models/AiMessage.js.map +1 -0
  60. package/build/src/models/AiSession.d.ts +80 -0
  61. package/build/src/models/AiSession.d.ts.map +1 -0
  62. package/build/src/models/AiSession.js +102 -0
  63. package/build/src/models/AiSession.js.map +1 -0
  64. package/build/src/models/kinds.d.ts +2 -0
  65. package/build/src/models/kinds.d.ts.map +1 -1
  66. package/build/src/models/kinds.js +2 -0
  67. package/build/src/models/kinds.js.map +1 -1
  68. package/build/src/sdk/AiSdk.d.ts +93 -0
  69. package/build/src/sdk/AiSdk.d.ts.map +1 -0
  70. package/build/src/sdk/AiSdk.js +348 -0
  71. package/build/src/sdk/AiSdk.js.map +1 -0
  72. package/build/src/sdk/RouteBuilder.d.ts +7 -0
  73. package/build/src/sdk/RouteBuilder.d.ts.map +1 -1
  74. package/build/src/sdk/RouteBuilder.js +18 -0
  75. package/build/src/sdk/RouteBuilder.js.map +1 -1
  76. package/build/src/sdk/Sdk.d.ts +2 -0
  77. package/build/src/sdk/Sdk.d.ts.map +1 -1
  78. package/build/src/sdk/Sdk.js +2 -0
  79. package/build/src/sdk/Sdk.js.map +1 -1
  80. package/build/src/sdk/SdkBase.d.ts +4 -0
  81. package/build/src/sdk/SdkBase.d.ts.map +1 -1
  82. package/build/src/sdk/SdkBase.js.map +1 -1
  83. package/build/src/sdk/SdkMock.d.ts +15 -0
  84. package/build/src/sdk/SdkMock.d.ts.map +1 -1
  85. package/build/src/sdk/SdkMock.js +118 -0
  86. package/build/src/sdk/SdkMock.js.map +1 -1
  87. package/build/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +3 -3
  89. package/src/decorators/observed.ts +51 -9
  90. package/src/mocking/ModelingMock.ts +2 -0
  91. package/src/mocking/lib/Ai.ts +71 -0
  92. package/src/modeling/ai/DataDomainDelta.ts +798 -0
  93. package/src/modeling/ai/DomainSerialization.ts +199 -0
  94. package/src/modeling/ai/domain_response_schema.ts +301 -0
  95. package/src/modeling/ai/domain_tools.ts +76 -0
  96. package/src/modeling/ai/message_parser.ts +101 -0
  97. package/src/modeling/ai/prompts/domain_system.ts +79 -0
  98. package/src/modeling/ai/readme.md +8 -0
  99. package/src/modeling/ai/tools/DataDomain.tools.ts +365 -0
  100. package/src/modeling/ai/tools/Semantic.tools.ts +38 -0
  101. package/src/modeling/ai/tools/config.ts +13 -0
  102. package/src/modeling/ai/tools/readme.md +3 -0
  103. package/src/modeling/ai/types.ts +306 -0
  104. package/src/models/AiMessage.ts +335 -0
  105. package/src/models/AiSession.ts +160 -0
  106. package/src/models/kinds.ts +2 -0
  107. package/src/sdk/AiSdk.ts +395 -0
  108. package/src/sdk/RouteBuilder.ts +27 -0
  109. package/src/sdk/Sdk.ts +3 -0
  110. package/src/sdk/SdkBase.ts +4 -0
  111. package/src/sdk/SdkMock.ts +185 -0
  112. package/tests/unit/decorators/observed_recursive.spec.ts +39 -0
  113. package/tests/unit/mocking/current/Ai.spec.ts +109 -0
  114. package/tests/unit/modeling/ai/DataDomainDelta.spec.ts +419 -0
  115. package/tests/unit/modeling/ai/DomainAiTools.spec.ts +29 -0
  116. package/tests/unit/modeling/ai/DomainSerialization.spec.ts +143 -0
  117. package/tests/unit/modeling/ai/message_parser.spec.ts +157 -0
  118. package/tests/unit/modeling/ai/tools/DataDomain.tools.spec.ts +64 -0
  119. package/tests/unit/modeling/ai/tools/Semantic.tools.spec.ts +55 -0
  120. package/tests/unit/models/AiMessage.spec.ts +216 -0
  121. package/tests/unit/models/AiSession.spec.ts +147 -0
@@ -0,0 +1,798 @@
1
+ import type { DataDomain } from '@api-client/core/modeling/DataDomain.js'
2
+ import type {
3
+ AiDataModelSchema,
4
+ AiDomainAssociation,
5
+ AiDomainAssociationDelta,
6
+ AiDomainDelta,
7
+ AiDomainEntityDelta,
8
+ AiDomainEntityResponseSchema,
9
+ AiDomainProperty,
10
+ AiDomainPropertyDelta,
11
+ AiDomainSemantic,
12
+ } from './types.js'
13
+ import type {
14
+ DomainAssociation,
15
+ DomainEntity,
16
+ DomainEntitySchema,
17
+ DomainModel,
18
+ DomainProperty,
19
+ } from '@api-client/core/modeling/index.js'
20
+ import { nanoid } from '@api-client/core/nanoid.js'
21
+ import { type SemanticType, SemanticScope, DataSemantics } from '@api-client/core/modeling/Semantics.js'
22
+ import { DomainEntityKind, DomainPropertyKind, DomainAssociationKind } from '@api-client/core/models/kinds.js'
23
+
24
+ /**
25
+ * Normalizes the delta by removing duplicates. For example, when the model
26
+ * adds and entity more than once to the `modifiedEntities` array, This will merge all the changes into a single entity.
27
+ * @param delta The delta to normalize.
28
+ * @returns The normalized delta.
29
+ */
30
+ function mergeArrayByKey<T>(
31
+ items: T[] | undefined,
32
+ mergeFn: (a: T, b: T) => void,
33
+ keyProp: keyof T = 'key' as keyof T
34
+ ): T[] | undefined {
35
+ if (!items || items.length === 0) return items
36
+ const map = new Map<unknown, T>()
37
+ const result: T[] = []
38
+ for (const item of items) {
39
+ const k = item[keyProp]
40
+ if (!k) {
41
+ result.push({ ...item })
42
+ continue
43
+ }
44
+ const existing = map.get(k)
45
+ if (existing) {
46
+ mergeFn(existing, item)
47
+ } else {
48
+ const copy = { ...item }
49
+ map.set(k, copy)
50
+ result.push(copy)
51
+ }
52
+ }
53
+ return result
54
+ }
55
+
56
+ /**
57
+ * A utility class responsible for applying AI-generated modifications (deltas)
58
+ * to an existing `DataDomain` model. It handles adding, modifying, and deleting
59
+ * entities, properties, associations, semantics, and models based on the
60
+ * prescribed changes.
61
+ *
62
+ * It also maintains a mapping of AI-generated provisional keys to the
63
+ * actual nanoid keys used within the data domain to ensure references remain intact.
64
+ */
65
+ export class DataDomainDelta {
66
+ /**
67
+ * The data domain to which the delta will be applied.
68
+ */
69
+ protected domain: DataDomain
70
+
71
+ /**
72
+ * The map containing the mappings of AI generated keys to proper nanoids.
73
+ */
74
+ protected keyMap: Map<string, string>
75
+
76
+ /**
77
+ * Initializes a new instance of the `DataDomainDelta` class.
78
+ *
79
+ * @param domain The target data domain where changes will be applied.
80
+ * @param keyMap An optional map tracking AI-generated provisional keys to actual domain nanoids.
81
+ */
82
+ constructor(domain: DataDomain, keyMap = new Map<string, string>()) {
83
+ this.domain = domain
84
+ this.keyMap = keyMap
85
+ }
86
+
87
+ /**
88
+ * Applies the entire given delta structure to the data domain in a single pass.
89
+ * Operations include processing added/modified/deleted components across
90
+ * models, entities, properties, associations, and semantics.
91
+ *
92
+ * @param delta The AI-generated delta schema containing all requested changes.
93
+ */
94
+ apply(delta: AiDomainDelta): void {
95
+ if (delta.addedModels) {
96
+ for (const m of delta.addedModels) {
97
+ const existing = this.domain.findModel(this.keyMap.get(m.key) || m.key)
98
+ if (!existing) {
99
+ let newKey = this.keyMap.get(m.key)
100
+ if (!newKey) {
101
+ newKey = nanoid()
102
+ this.keyMap.set(m.key, newKey)
103
+ }
104
+ this.domain.addModel({ key: newKey, info: { name: m.name, description: m.description } })
105
+ }
106
+ }
107
+ }
108
+
109
+ if (delta.modifiedModels) {
110
+ for (const mod of delta.modifiedModels) {
111
+ const actualKey = this.keyMap.get(mod.key) || mod.key
112
+ const model = this.domain.findModel(actualKey)
113
+ if (model) {
114
+ if (mod.name !== undefined) model.info.name = mod.name
115
+ if (mod.description !== undefined) model.info.description = mod.description
116
+ }
117
+ }
118
+ }
119
+
120
+ if (delta.addedEntities) {
121
+ for (const e of delta.addedEntities) {
122
+ this.handleAddEntity(this.domain, e)
123
+ }
124
+ }
125
+
126
+ if (delta.deletedEntityKeys) {
127
+ for (const key of delta.deletedEntityKeys) {
128
+ const actualKey = this.keyMap.get(key) || key
129
+ this.domain.removeEntity(actualKey)
130
+ }
131
+ }
132
+
133
+ if (delta.modifiedEntities) {
134
+ for (const mod of delta.modifiedEntities) {
135
+ const actualEntityKey = this.keyMap.get(mod.key) || mod.key
136
+ const entity = this.domain.findEntity(actualEntityKey)
137
+ if (!entity) {
138
+ continue
139
+ }
140
+
141
+ if (mod.modelKey) {
142
+ const currentModel = entity.getParentInstance() as DomainModel
143
+ const actualModelKey = this.keyMap.get(mod.modelKey) || mod.modelKey
144
+ if (currentModel.key !== actualModelKey && currentModel.info.name !== mod.modelKey) {
145
+ const newModel = this.getModel(this.domain, actualModelKey)
146
+ newModel.attachEntity(entity.key)
147
+ }
148
+ }
149
+
150
+ if (mod.name) entity.info.name = mod.name
151
+ if (mod.displayName) entity.info.displayName = mod.displayName
152
+ if (mod.description) entity.info.description = mod.description
153
+
154
+ this.applySemantics(entity, mod.addedSemantics, mod.modifiedSemantics, mod.deletedSemanticIds)
155
+
156
+ this.handlePropertyAdds(entity, mod.addedProperties)
157
+ this.handlePropertyDeletes(this.domain, mod.deletedPropertyKeys)
158
+ this.handlePropertyMods(this.domain, mod.modifiedProperties)
159
+ this.handleAssociationAdds(entity, mod.addedAssociations)
160
+ this.handleAssociationDeletes(this.domain, mod.deletedAssociationKeys)
161
+ this.handleAssociationMods(this.domain, mod.modifiedAssociations)
162
+ }
163
+ }
164
+
165
+ if (delta.deletedModelKeys) {
166
+ for (const key of delta.deletedModelKeys) {
167
+ const actualKey = this.keyMap.get(key) || key
168
+ try {
169
+ this.domain.removeModel(actualKey)
170
+ } catch {
171
+ // ignore
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Internal handler to add a new entity to the data domain based on the provided schema.
179
+ * It also recursively handles adding the entity's semantics, properties, and associations.
180
+ *
181
+ * @param domain The data domain where the entity will reside.
182
+ * @param e The schema definition of the entity to be added.
183
+ */
184
+ protected handleAddEntity(domain: DataDomain, e: AiDomainEntityResponseSchema): void {
185
+ const targetModel = this.getModel(domain, e.modelKey || 'ai_generated')
186
+ const entityInput = { ...e }
187
+ delete entityInput.semantics
188
+ let entityKey = this.keyMap.get(e.key)
189
+ if (!entityKey) {
190
+ entityKey = nanoid()
191
+ this.keyMap.set(e.key, entityKey)
192
+ }
193
+
194
+ const entityInfo: Partial<DomainEntitySchema> = {
195
+ ...entityInput,
196
+ key: entityKey,
197
+ info: {
198
+ name: e.name,
199
+ displayName: e.displayName,
200
+ description: e.description,
201
+ },
202
+ }
203
+ const createdEntity = targetModel.addEntity(entityInfo)
204
+ this.applySemantics(createdEntity, e.semantics)
205
+ if (e.properties) {
206
+ for (const p of e.properties) {
207
+ this.handleAddProperty(createdEntity, p)
208
+ }
209
+ }
210
+ if (e.associations) {
211
+ for (const a of e.associations) {
212
+ this.handleAddAssociation(createdEntity, a)
213
+ }
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Processes an array of newly added properties for a specific entity.
219
+ *
220
+ * @param entity The target domain entity to receive the new properties.
221
+ * @param adds The list of property definitions to add, if any.
222
+ */
223
+ protected handlePropertyAdds(entity: DomainEntity, adds?: AiDomainProperty[]): void {
224
+ if (!adds) {
225
+ return
226
+ }
227
+ for (const p of adds) {
228
+ this.handleAddProperty(entity, p)
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Internal handler to add a single property to an entity, generating keys
234
+ * and applying semantics as defined in the AI schema.
235
+ *
236
+ * @param entity The target domain entity.
237
+ * @param p The schema definition of the property to add.
238
+ */
239
+ protected handleAddProperty(entity: DomainEntity, p: AiDomainProperty): void {
240
+ let propKey = p.key ? this.keyMap.get(p.key) : undefined
241
+ if (!propKey) {
242
+ propKey = nanoid()
243
+ if (p.key) {
244
+ this.keyMap.set(p.key, propKey)
245
+ }
246
+ }
247
+ const prop = entity.addProperty({
248
+ key: propKey,
249
+ type: p.type,
250
+ info: {
251
+ name: p.name,
252
+ displayName: p.displayName,
253
+ description: p.description,
254
+ },
255
+ required: p.constraints?.required,
256
+ unique: p.constraints?.unique,
257
+ index: p.constraints?.index,
258
+ primary: p.constraints?.primary,
259
+ multiple: p.constraints?.multiple,
260
+ readOnly: p.constraints?.readOnly,
261
+ writeOnly: p.constraints?.writeOnly,
262
+ deprecated: p.deprecated,
263
+ schema: p.schema,
264
+ tags: p.tags,
265
+ })
266
+ this.applySemantics(prop, p.semantics)
267
+ }
268
+
269
+ /**
270
+ * Processes the deletion of properties by their keys from the given domain.
271
+ * Fails silently if a key is not found, maintaining fault tolerance.
272
+ *
273
+ * @param domain The target data domain.
274
+ * @param keys The array of property keys (AI-generated or actual) to remove.
275
+ */
276
+ handlePropertyDeletes(domain: DataDomain, keys?: string[]): void {
277
+ if (!keys) {
278
+ return
279
+ }
280
+ for (const key of keys) {
281
+ const actualKey = this.keyMap.get(key) || key
282
+ try {
283
+ domain.removeProperty(actualKey)
284
+ } catch {
285
+ // we can ignore in case the model got the wrong key.
286
+ }
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Processes modifications to existing properties within the domain, updating
292
+ * specific metadata, constraints, schema rules, and semantics based on the delta.
293
+ *
294
+ * @param domain The target data domain.
295
+ * @param mods The list of property modifications to apply.
296
+ */
297
+ handlePropertyMods(domain: DataDomain, mods?: AiDomainPropertyDelta[]): void {
298
+ if (!mods) {
299
+ return
300
+ }
301
+ for (const mod of mods) {
302
+ const actualKey = this.keyMap.get(mod.key) || mod.key
303
+ const prop = domain.findProperty(actualKey)
304
+ if (!prop) {
305
+ continue
306
+ }
307
+ if (mod.name) {
308
+ prop.info.name = mod.name
309
+ }
310
+ if (mod.displayName) {
311
+ prop.info.displayName = mod.displayName
312
+ }
313
+ if (mod.description) {
314
+ prop.info.description = mod.description
315
+ }
316
+ if (mod.type) {
317
+ prop.type = mod.type
318
+ }
319
+ if (mod.deprecated !== undefined) {
320
+ prop.deprecated = mod.deprecated
321
+ }
322
+ if (mod.constraints) {
323
+ if (mod.constraints.required !== undefined) {
324
+ prop.required = mod.constraints.required
325
+ }
326
+ if (mod.constraints.unique !== undefined) {
327
+ prop.unique = mod.constraints.unique
328
+ }
329
+ if (mod.constraints.index !== undefined) {
330
+ prop.index = mod.constraints.index
331
+ }
332
+ if (mod.constraints.primary !== undefined) {
333
+ prop.primary = mod.constraints.primary
334
+ }
335
+ if (mod.constraints.multiple !== undefined) {
336
+ prop.multiple = mod.constraints.multiple
337
+ }
338
+ if (mod.constraints.readOnly !== undefined) {
339
+ prop.readOnly = mod.constraints.readOnly
340
+ }
341
+ if (mod.constraints.writeOnly !== undefined) {
342
+ prop.writeOnly = mod.constraints.writeOnly
343
+ }
344
+ }
345
+ if (mod.schema) {
346
+ if (!prop.schema) {
347
+ prop.schema = {}
348
+ }
349
+ if (mod.schema.defaultValue) {
350
+ if (!prop.schema.defaultValue) {
351
+ prop.schema.defaultValue = {
352
+ value: '',
353
+ type: 'literal',
354
+ }
355
+ }
356
+ if (mod.schema.defaultValue.type) {
357
+ prop.schema.defaultValue.type = mod.schema.defaultValue.type
358
+ }
359
+ if (mod.schema.defaultValue.value) {
360
+ prop.schema.defaultValue.value = mod.schema.defaultValue.value
361
+ }
362
+ }
363
+ if (mod.schema.pattern !== undefined) {
364
+ prop.schema.pattern = mod.schema.pattern
365
+ }
366
+ if (mod.schema.minimum !== undefined) {
367
+ prop.schema.minimum = mod.schema.minimum
368
+ }
369
+ if (mod.schema.maximum !== undefined) {
370
+ prop.schema.maximum = mod.schema.maximum
371
+ }
372
+ if (mod.schema.exclusiveMinimum !== undefined) {
373
+ prop.schema.exclusiveMinimum = mod.schema.exclusiveMinimum
374
+ }
375
+ if (mod.schema.exclusiveMaximum !== undefined) {
376
+ prop.schema.exclusiveMaximum = mod.schema.exclusiveMaximum
377
+ }
378
+ if (mod.schema.multipleOf !== undefined) {
379
+ prop.schema.multipleOf = mod.schema.multipleOf
380
+ }
381
+ if (mod.schema.enum !== undefined) {
382
+ prop.schema.enum = [...mod.schema.enum]
383
+ }
384
+ if (mod.schema.examples !== undefined) {
385
+ prop.schema.examples = [...mod.schema.examples]
386
+ }
387
+ }
388
+ this.applySemantics(prop, mod.addedSemantics, mod.modifiedSemantics, mod.deletedSemanticIds)
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Processes an array of newly added associations for a specific entity.
394
+ *
395
+ * @param entity The target domain entity referencing the newly created associations.
396
+ * @param adds An array of association definitions to add, if any.
397
+ */
398
+ handleAssociationAdds(entity: DomainEntity, adds: AiDomainAssociation[] | undefined): void {
399
+ if (!adds) {
400
+ return
401
+ }
402
+ for (const a of adds) {
403
+ this.handleAddAssociation(entity, a)
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Internal handler to append a single association to an entity, effectively creating
409
+ * a relationship to one or more targets. Attempts an optimized exact O(1) loopup
410
+ * before falling back to a full domain search.
411
+ *
412
+ * @param entity The source domain entity creating the association.
413
+ * @param a The defining schema for the new association.
414
+ */
415
+ handleAddAssociation(entity: DomainEntity, a: AiDomainAssociation): void {
416
+ let assocKey = this.keyMap.get(a.key)
417
+ if (!assocKey) {
418
+ assocKey = nanoid()
419
+ this.keyMap.set(a.key, assocKey)
420
+ }
421
+ const assoc = entity.addAssociation({
422
+ key: assocKey,
423
+ info: { name: a.name, displayName: a.displayName, description: a.description },
424
+ required: a.required,
425
+ multiple: a.multiple,
426
+ onDelete: a.onDelete,
427
+ schema: a.schema,
428
+ })
429
+ if (a.targets) {
430
+ for (const t of a.targets) {
431
+ const actualTargetKey = this.keyMap.get(t.key) || t.key
432
+ // 1. Try exact match by key
433
+ const exactMatch = entity.domain.findEntity(actualTargetKey)
434
+ if (exactMatch) {
435
+ if (exactMatch.namespace !== entity.domain.key) {
436
+ assoc.addTarget(exactMatch.key, exactMatch.namespace)
437
+ } else {
438
+ assoc.addTarget(exactMatch.key)
439
+ }
440
+ } else {
441
+ // 2. Try name/display name search
442
+ const target = entity.domain.search({
443
+ query: actualTargetKey,
444
+ includeForeignDomains: true,
445
+ nodeTypes: [DomainEntityKind],
446
+ })
447
+ if (target.length > 0) {
448
+ const found = target[0]
449
+ if (found.isForeign) {
450
+ assoc.addTarget(found.key, found.node.namespace)
451
+ } else {
452
+ assoc.addTarget(found.key)
453
+ }
454
+ } else {
455
+ assoc.addTarget(actualTargetKey)
456
+ }
457
+ }
458
+ }
459
+ }
460
+ this.applySemantics(assoc, a.semantics)
461
+ }
462
+
463
+ /**
464
+ * Processes the deletion of associations by their keys from the given domain.
465
+ * Safely ignores errors if the specified association does not exist.
466
+ *
467
+ * @param domain The target data domain.
468
+ * @param keys An array of association keys to safely remove.
469
+ */
470
+ handleAssociationDeletes(domain: DataDomain, keys?: string[]): void {
471
+ if (!keys) {
472
+ return
473
+ }
474
+ for (const key of keys) {
475
+ const actualKey = this.keyMap.get(key) || key
476
+ try {
477
+ domain.removeAssociation(actualKey)
478
+ } catch {
479
+ // we can ignore in case the model got the wrong key.
480
+ }
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Processes modifications to existing associations within the domain.
486
+ * Updates attributes, semantics, and handles adding/replacing association targets.
487
+ *
488
+ * @param domain The target data domain containing the associations.
489
+ * @param mods An array of requested association modifications.
490
+ */
491
+ handleAssociationMods(domain: DataDomain, mods: AiDomainAssociationDelta[] | undefined): void {
492
+ if (!mods) {
493
+ return
494
+ }
495
+ for (const mod of mods) {
496
+ const actualKey = this.keyMap.get(mod.key) || mod.key
497
+ const assoc = domain.findAssociation(actualKey)
498
+ if (!assoc) {
499
+ continue
500
+ }
501
+ if (mod.name) {
502
+ assoc.info.name = mod.name
503
+ }
504
+ if (mod.displayName) {
505
+ assoc.info.displayName = mod.displayName
506
+ }
507
+ if (mod.description) {
508
+ assoc.info.description = mod.description
509
+ }
510
+ if (mod.required !== undefined) {
511
+ assoc.required = mod.required
512
+ }
513
+ if (mod.onDelete !== undefined) {
514
+ assoc.onDelete = mod.onDelete
515
+ }
516
+
517
+ if (mod.targets) {
518
+ // Clear existing targets before adding new ones
519
+ assoc.targets.splice(0, assoc.targets.length)
520
+ for (const t of mod.targets) {
521
+ const actualTargetKey = this.keyMap.get(t.key) || t.key
522
+ // 1. Try exact match by key
523
+ const exactMatch = domain.findEntity(actualTargetKey)
524
+ if (exactMatch) {
525
+ if (exactMatch.namespace !== domain.key) {
526
+ assoc.addTarget(exactMatch.key, exactMatch.namespace)
527
+ } else {
528
+ assoc.addTarget(exactMatch.key)
529
+ }
530
+ } else {
531
+ // 2. Try name/display name search
532
+ const targetSearch = domain.search({
533
+ query: actualTargetKey,
534
+ includeForeignDomains: true,
535
+ nodeTypes: [DomainEntityKind],
536
+ })
537
+ if (targetSearch.length > 0) {
538
+ const found = targetSearch[0]
539
+ if (found.isForeign) {
540
+ assoc.addTarget(found.key, found.node.namespace)
541
+ } else {
542
+ assoc.addTarget(found.key)
543
+ }
544
+ } else {
545
+ assoc.addTarget(actualTargetKey)
546
+ }
547
+ }
548
+ }
549
+ }
550
+ this.applySemantics(assoc, mod.addedSemantics, mod.modifiedSemantics, mod.deletedSemanticIds)
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Retrieves an existing `DomainModel` by its given key. If the model
556
+ * does not exist in the domain (e.g., AI generated a brand new model name natively),
557
+ * a newly instantiated one is created and registered within the domain automatically.
558
+ *
559
+ * @param domain The target data domain.
560
+ * @param modelKey The AI-suggested or actual nanoid key of the model.
561
+ * @returns The resolved or newly created `DomainModel`.
562
+ */
563
+ protected getModel(domain: DataDomain, modelKey: string): DomainModel {
564
+ const mappedKey = this.keyMap.get(modelKey) || modelKey
565
+ let m = domain.findModel(mappedKey)
566
+ if (!m) {
567
+ // AI generated a new model name (like "shipping"). Use it as the name, let core generate the nanoid key.
568
+ const newKey = this.keyMap.get(modelKey) || nanoid()
569
+ this.keyMap.set(modelKey, newKey)
570
+ m = domain.addModel({ key: newKey, info: { name: modelKey } })
571
+ }
572
+ return m
573
+ }
574
+
575
+ /**
576
+ * Helper function to batch apply semantic patches (add, modify, delete)
577
+ * to a specific domain component (Entity, Property, or Association).
578
+ * Validates semantic scopes heavily before applying.
579
+ *
580
+ * @param target The domain element (Entity, Property, Association) to receive the semantics.
581
+ * @param added List of semantic blocks to newly append.
582
+ * @param modified List of existing semantics to override/update.
583
+ * @param deleted Array of semantic IDs indicating removals.
584
+ */
585
+ protected applySemantics(
586
+ target: DomainEntity | DomainProperty | DomainAssociation,
587
+ added?: AiDomainSemantic[],
588
+ modified?: AiDomainSemantic[],
589
+ deleted?: SemanticType[]
590
+ ): void {
591
+ // Determine the expected scope for the target
592
+ let expectedScope: SemanticScope
593
+ if (target.kind === DomainEntityKind) {
594
+ expectedScope = SemanticScope.Entity
595
+ } else if (target.kind === DomainPropertyKind) {
596
+ expectedScope = SemanticScope.Property
597
+ } else if (target.kind === DomainAssociationKind) {
598
+ expectedScope = SemanticScope.Association
599
+ } else {
600
+ throw new Error(`Unsupported target kind for semantics: ${(target as { kind: string }).kind}`)
601
+ }
602
+
603
+ const isValidSemantic = (id: SemanticType): boolean => {
604
+ const semanticDef = DataSemantics[id]
605
+ if (!semanticDef) {
606
+ return false
607
+ }
608
+ if (semanticDef.scope !== expectedScope) {
609
+ return false
610
+ }
611
+ return true
612
+ }
613
+
614
+ if (added) {
615
+ for (const s of added) {
616
+ const typeId = this.ensureSemanticId(s.id)
617
+ if (isValidSemantic(typeId)) {
618
+ target.addSemantic({ id: typeId, config: s.config })
619
+ }
620
+ }
621
+ }
622
+ if (modified) {
623
+ for (const s of modified) {
624
+ const typeId = this.ensureSemanticId(s.id)
625
+ if (isValidSemantic(typeId)) {
626
+ // Technically addSemantic overrides existing if they have the same ID
627
+ target.addSemantic({ id: typeId, config: s.config })
628
+ }
629
+ }
630
+ }
631
+ if (deleted) {
632
+ for (const id of deleted) {
633
+ const typeId = this.ensureSemanticId(id)
634
+ if (isValidSemantic(typeId)) {
635
+ target.removeSemantic(typeId)
636
+ }
637
+ }
638
+ }
639
+ }
640
+
641
+ /**
642
+ * Forces the given semantic ID to follow the structured format `Semantic#{id}`.
643
+ *
644
+ * @param id The raw semantic string given by the LLM/Delta payload.
645
+ * @returns The corrected semantic ID structured for internal parsing.
646
+ */
647
+ protected ensureSemanticId(id: string): SemanticType {
648
+ let result = id
649
+ if (!id.startsWith('Semantic#')) {
650
+ result = `Semantic#${id}`
651
+ }
652
+ return result as SemanticType
653
+ }
654
+
655
+ /**
656
+ * Normalizes the delta by removing duplicates. For example, when the model
657
+ * adds an entity more than once to the `modifiedEntities` array, this will merge all the changes
658
+ * into a single entity.
659
+ * @param delta The delta to normalize.
660
+ * @returns The normalized delta.
661
+ */
662
+ static normalize(delta: AiDomainDelta): AiDomainDelta {
663
+ const result: AiDomainDelta = { ...delta }
664
+
665
+ const mergeProperties = (
666
+ a: AiDomainProperty | AiDomainPropertyDelta,
667
+ b: AiDomainProperty | AiDomainPropertyDelta
668
+ ) => {
669
+ const aConstraints = a.constraints
670
+ Object.assign(a, b)
671
+ if (aConstraints && b.constraints) {
672
+ a.constraints = { ...aConstraints, ...b.constraints }
673
+ }
674
+ }
675
+
676
+ const mergeAssociations = (
677
+ a: AiDomainAssociation | AiDomainAssociationDelta,
678
+ b: AiDomainAssociation | AiDomainAssociationDelta
679
+ ) => {
680
+ Object.assign(a, b)
681
+ }
682
+
683
+ const mergeSemantics = (a: AiDomainSemantic, b: AiDomainSemantic) => {
684
+ const aConfig = a.config
685
+ Object.assign(a, b)
686
+ if (aConfig && b.config) {
687
+ a.config = { ...aConfig, ...b.config }
688
+ }
689
+ }
690
+
691
+ const mergeEntities = (a: AiDomainEntityResponseSchema, b: AiDomainEntityResponseSchema) => {
692
+ const aTags = a.tags
693
+ const aProps = a.properties
694
+ const aAssoc = a.associations
695
+ const aSem = a.semantics
696
+
697
+ Object.assign(a, b)
698
+
699
+ if (aTags && b.tags) {
700
+ a.tags = Array.from(new Set([...aTags, ...b.tags]))
701
+ }
702
+
703
+ if (aProps || b.properties) {
704
+ a.properties = mergeArrayByKey([...(aProps || []), ...(b.properties || [])], mergeProperties)
705
+ }
706
+ if (aAssoc || b.associations) {
707
+ a.associations = mergeArrayByKey([...(aAssoc || []), ...(b.associations || [])], mergeAssociations)
708
+ }
709
+ if (aSem || b.semantics) {
710
+ a.semantics = mergeArrayByKey([...(aSem || []), ...(b.semantics || [])], mergeSemantics, 'id')
711
+ }
712
+ }
713
+
714
+ const mergeEntityDeltas = (a: AiDomainEntityDelta, b: AiDomainEntityDelta) => {
715
+ const aTags = a.tags
716
+ const aAddedProps = a.addedProperties
717
+ const aModProps = a.modifiedProperties
718
+ const aDelProps = a.deletedPropertyKeys
719
+ const aAddedAssoc = a.addedAssociations
720
+ const aModAssoc = a.modifiedAssociations
721
+ const aDelAssoc = a.deletedAssociationKeys
722
+ const aAddedSem = a.addedSemantics
723
+ const aModSem = a.modifiedSemantics
724
+ const aDelSem = a.deletedSemanticIds
725
+
726
+ Object.assign(a, b)
727
+
728
+ if (aTags && b.tags) {
729
+ a.tags = Array.from(new Set([...aTags, ...b.tags]))
730
+ }
731
+
732
+ if (aAddedProps || b.addedProperties) {
733
+ a.addedProperties = mergeArrayByKey([...(aAddedProps || []), ...(b.addedProperties || [])], mergeProperties)
734
+ }
735
+ if (aModProps || b.modifiedProperties) {
736
+ a.modifiedProperties = mergeArrayByKey([...(aModProps || []), ...(b.modifiedProperties || [])], mergeProperties)
737
+ }
738
+ if (aDelProps && b.deletedPropertyKeys) {
739
+ a.deletedPropertyKeys = Array.from(new Set([...aDelProps, ...b.deletedPropertyKeys]))
740
+ }
741
+
742
+ if (aAddedAssoc || b.addedAssociations) {
743
+ a.addedAssociations = mergeArrayByKey(
744
+ [...(aAddedAssoc || []), ...(b.addedAssociations || [])],
745
+ mergeAssociations
746
+ )
747
+ }
748
+ if (aModAssoc || b.modifiedAssociations) {
749
+ a.modifiedAssociations = mergeArrayByKey(
750
+ [...(aModAssoc || []), ...(b.modifiedAssociations || [])],
751
+ mergeAssociations
752
+ )
753
+ }
754
+ if (aDelAssoc && b.deletedAssociationKeys) {
755
+ a.deletedAssociationKeys = Array.from(new Set([...aDelAssoc, ...b.deletedAssociationKeys]))
756
+ }
757
+
758
+ if (aAddedSem || b.addedSemantics) {
759
+ a.addedSemantics = mergeArrayByKey([...(aAddedSem || []), ...(b.addedSemantics || [])], mergeSemantics, 'id')
760
+ }
761
+ if (aModSem || b.modifiedSemantics) {
762
+ a.modifiedSemantics = mergeArrayByKey(
763
+ [...(aModSem || []), ...(b.modifiedSemantics || [])],
764
+ mergeSemantics,
765
+ 'id'
766
+ )
767
+ }
768
+ if (aDelSem && b.deletedSemanticIds) {
769
+ a.deletedSemanticIds = Array.from(new Set([...aDelSem, ...b.deletedSemanticIds]))
770
+ }
771
+ }
772
+
773
+ const mergeModels = (a: AiDataModelSchema, b: AiDataModelSchema) => {
774
+ Object.assign(a, b)
775
+ }
776
+
777
+ if (result.addedEntities) {
778
+ result.addedEntities = mergeArrayByKey(result.addedEntities, mergeEntities)
779
+ }
780
+ if (result.modifiedEntities) {
781
+ result.modifiedEntities = mergeArrayByKey(result.modifiedEntities, mergeEntityDeltas)
782
+ }
783
+ if (result.addedModels) {
784
+ result.addedModels = mergeArrayByKey(result.addedModels, mergeModels)
785
+ }
786
+ if (result.modifiedModels) {
787
+ result.modifiedModels = mergeArrayByKey(result.modifiedModels, mergeModels)
788
+ }
789
+ if (result.deletedModelKeys) {
790
+ result.deletedModelKeys = Array.from(new Set(result.deletedModelKeys))
791
+ }
792
+ if (result.deletedEntityKeys) {
793
+ result.deletedEntityKeys = Array.from(new Set(result.deletedEntityKeys))
794
+ }
795
+
796
+ return result
797
+ }
798
+ }