ai-database 2.1.1 → 2.3.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 (268) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +1063 -186
  3. package/dist/actions.d.ts +2 -2
  4. package/dist/actions.d.ts.map +1 -1
  5. package/dist/actions.js +1 -1
  6. package/dist/actions.js.map +1 -1
  7. package/dist/ai-promise-db.d.ts +52 -23
  8. package/dist/ai-promise-db.d.ts.map +1 -1
  9. package/dist/ai-promise-db.js +185 -164
  10. package/dist/ai-promise-db.js.map +1 -1
  11. package/dist/authorization.d.ts.map +1 -1
  12. package/dist/authorization.js +38 -30
  13. package/dist/authorization.js.map +1 -1
  14. package/dist/cascade-orchestrator.d.ts +404 -0
  15. package/dist/cascade-orchestrator.d.ts.map +1 -0
  16. package/dist/cascade-orchestrator.js +828 -0
  17. package/dist/cascade-orchestrator.js.map +1 -0
  18. package/dist/cascade-write-strategy.d.ts +584 -0
  19. package/dist/cascade-write-strategy.d.ts.map +1 -0
  20. package/dist/cascade-write-strategy.js +590 -0
  21. package/dist/cascade-write-strategy.js.map +1 -0
  22. package/dist/ch-adapter.d.ts +358 -0
  23. package/dist/ch-adapter.d.ts.map +1 -0
  24. package/dist/ch-adapter.js +929 -0
  25. package/dist/ch-adapter.js.map +1 -0
  26. package/dist/client/index.d.ts +42 -0
  27. package/dist/client/index.d.ts.map +1 -0
  28. package/dist/client/index.js +43 -0
  29. package/dist/client/index.js.map +1 -0
  30. package/dist/client.d.ts +266 -0
  31. package/dist/client.d.ts.map +1 -0
  32. package/dist/client.js +81 -0
  33. package/dist/client.js.map +1 -0
  34. package/dist/constants.d.ts +64 -1
  35. package/dist/constants.d.ts.map +1 -1
  36. package/dist/constants.js +52 -2
  37. package/dist/constants.js.map +1 -1
  38. package/dist/dataloader.d.ts +99 -0
  39. package/dist/dataloader.d.ts.map +1 -0
  40. package/dist/dataloader.js +225 -0
  41. package/dist/dataloader.js.map +1 -0
  42. package/dist/db-provider-port.d.ts +501 -0
  43. package/dist/db-provider-port.d.ts.map +1 -0
  44. package/dist/db-provider-port.js +113 -0
  45. package/dist/db-provider-port.js.map +1 -0
  46. package/dist/digital-objects-provider.d.ts +49 -0
  47. package/dist/digital-objects-provider.d.ts.map +1 -0
  48. package/dist/digital-objects-provider.js +55 -0
  49. package/dist/digital-objects-provider.js.map +1 -0
  50. package/dist/do-sqlite-adapter.d.ts +402 -0
  51. package/dist/do-sqlite-adapter.d.ts.map +1 -0
  52. package/dist/do-sqlite-adapter.js +745 -0
  53. package/dist/do-sqlite-adapter.js.map +1 -0
  54. package/dist/docs-rels/custom-types.d.ts +134 -0
  55. package/dist/docs-rels/custom-types.d.ts.map +1 -0
  56. package/dist/docs-rels/custom-types.js +70 -0
  57. package/dist/docs-rels/custom-types.js.map +1 -0
  58. package/dist/docs-rels/index.d.ts +16 -0
  59. package/dist/docs-rels/index.d.ts.map +1 -0
  60. package/dist/docs-rels/index.js +16 -0
  61. package/dist/docs-rels/index.js.map +1 -0
  62. package/dist/docs-rels/migrations/index.d.ts +30 -0
  63. package/dist/docs-rels/migrations/index.d.ts.map +1 -0
  64. package/dist/docs-rels/migrations/index.js +128 -0
  65. package/dist/docs-rels/migrations/index.js.map +1 -0
  66. package/dist/docs-rels/schema.d.ts +2961 -0
  67. package/dist/docs-rels/schema.d.ts.map +1 -0
  68. package/dist/docs-rels/schema.js +244 -0
  69. package/dist/docs-rels/schema.js.map +1 -0
  70. package/dist/durable-clickhouse.d.ts.map +1 -1
  71. package/dist/durable-clickhouse.js +16 -13
  72. package/dist/durable-clickhouse.js.map +1 -1
  73. package/dist/durable-promise.d.ts.map +1 -1
  74. package/dist/durable-promise.js +34 -15
  75. package/dist/durable-promise.js.map +1 -1
  76. package/dist/errors.d.ts +127 -0
  77. package/dist/errors.d.ts.map +1 -0
  78. package/dist/errors.js +210 -0
  79. package/dist/errors.js.map +1 -0
  80. package/dist/eventbridge.d.ts +117 -0
  81. package/dist/eventbridge.d.ts.map +1 -0
  82. package/dist/eventbridge.js +238 -0
  83. package/dist/eventbridge.js.map +1 -0
  84. package/dist/events.d.ts +2 -2
  85. package/dist/events.d.ts.map +1 -1
  86. package/dist/events.js +1 -1
  87. package/dist/events.js.map +1 -1
  88. package/dist/execution-queue.d.ts.map +1 -1
  89. package/dist/execution-queue.js +4 -5
  90. package/dist/execution-queue.js.map +1 -1
  91. package/dist/index.d.ts +37 -8
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +112 -6
  94. package/dist/index.js.map +1 -1
  95. package/dist/linguistic.d.ts +3 -108
  96. package/dist/linguistic.d.ts.map +1 -1
  97. package/dist/linguistic.js +3 -372
  98. package/dist/linguistic.js.map +1 -1
  99. package/dist/logger.d.ts +132 -0
  100. package/dist/logger.d.ts.map +1 -0
  101. package/dist/logger.js +137 -0
  102. package/dist/logger.js.map +1 -0
  103. package/dist/memory-provider.d.ts +129 -0
  104. package/dist/memory-provider.d.ts.map +1 -1
  105. package/dist/memory-provider.js +592 -257
  106. package/dist/memory-provider.js.map +1 -1
  107. package/dist/pg-adapter.d.ts +424 -0
  108. package/dist/pg-adapter.d.ts.map +1 -0
  109. package/dist/pg-adapter.js +921 -0
  110. package/dist/pg-adapter.js.map +1 -0
  111. package/dist/pipelines-iceberg-emitter.d.ts +327 -0
  112. package/dist/pipelines-iceberg-emitter.d.ts.map +1 -0
  113. package/dist/pipelines-iceberg-emitter.js +351 -0
  114. package/dist/pipelines-iceberg-emitter.js.map +1 -0
  115. package/dist/provider-capabilities.d.ts +146 -0
  116. package/dist/provider-capabilities.d.ts.map +1 -0
  117. package/dist/provider-capabilities.js +214 -0
  118. package/dist/provider-capabilities.js.map +1 -0
  119. package/dist/rdb-provider-adapter.d.ts +195 -0
  120. package/dist/rdb-provider-adapter.d.ts.map +1 -0
  121. package/dist/rdb-provider-adapter.js +291 -0
  122. package/dist/rdb-provider-adapter.js.map +1 -0
  123. package/dist/schema/cascade.d.ts +49 -10
  124. package/dist/schema/cascade.d.ts.map +1 -1
  125. package/dist/schema/cascade.js +491 -273
  126. package/dist/schema/cascade.js.map +1 -1
  127. package/dist/schema/definition-caches.d.ts +24 -0
  128. package/dist/schema/definition-caches.d.ts.map +1 -0
  129. package/dist/schema/definition-caches.js +26 -0
  130. package/dist/schema/definition-caches.js.map +1 -0
  131. package/dist/schema/dependency-graph.d.ts +45 -0
  132. package/dist/schema/dependency-graph.d.ts.map +1 -0
  133. package/dist/schema/dependency-graph.js +47 -0
  134. package/dist/schema/dependency-graph.js.map +1 -0
  135. package/dist/schema/diff.d.ts +103 -0
  136. package/dist/schema/diff.d.ts.map +1 -0
  137. package/dist/schema/diff.js +329 -0
  138. package/dist/schema/diff.js.map +1 -0
  139. package/dist/schema/entity-operations.d.ts +99 -0
  140. package/dist/schema/entity-operations.d.ts.map +1 -0
  141. package/dist/schema/entity-operations.js +818 -0
  142. package/dist/schema/entity-operations.js.map +1 -0
  143. package/dist/schema/generation-context.d.ts +202 -0
  144. package/dist/schema/generation-context.d.ts.map +1 -0
  145. package/dist/schema/generation-context.js +393 -0
  146. package/dist/schema/generation-context.js.map +1 -0
  147. package/dist/schema/index.d.ts +32 -34
  148. package/dist/schema/index.d.ts.map +1 -1
  149. package/dist/schema/index.js +462 -519
  150. package/dist/schema/index.js.map +1 -1
  151. package/dist/schema/migration.d.ts +205 -0
  152. package/dist/schema/migration.d.ts.map +1 -0
  153. package/dist/schema/migration.js +327 -0
  154. package/dist/schema/migration.js.map +1 -0
  155. package/dist/schema/nl-query-generator.d.ts +68 -0
  156. package/dist/schema/nl-query-generator.d.ts.map +1 -0
  157. package/dist/schema/nl-query-generator.js +362 -0
  158. package/dist/schema/nl-query-generator.js.map +1 -0
  159. package/dist/schema/nl-query.d.ts +65 -0
  160. package/dist/schema/nl-query.d.ts.map +1 -0
  161. package/dist/schema/nl-query.js +178 -0
  162. package/dist/schema/nl-query.js.map +1 -0
  163. package/dist/schema/parse.d.ts.map +1 -1
  164. package/dist/schema/parse.js +152 -89
  165. package/dist/schema/parse.js.map +1 -1
  166. package/dist/schema/provider.d.ts +38 -0
  167. package/dist/schema/provider.d.ts.map +1 -1
  168. package/dist/schema/provider.js +15 -7
  169. package/dist/schema/provider.js.map +1 -1
  170. package/dist/schema/resolve.d.ts +46 -5
  171. package/dist/schema/resolve.d.ts.map +1 -1
  172. package/dist/schema/resolve.js +334 -117
  173. package/dist/schema/resolve.js.map +1 -1
  174. package/dist/schema/search-utils.d.ts +76 -0
  175. package/dist/schema/search-utils.d.ts.map +1 -0
  176. package/dist/schema/search-utils.js +86 -0
  177. package/dist/schema/search-utils.js.map +1 -0
  178. package/dist/schema/seed.d.ts +53 -0
  179. package/dist/schema/seed.d.ts.map +1 -0
  180. package/dist/schema/seed.js +94 -0
  181. package/dist/schema/seed.js.map +1 -0
  182. package/dist/schema/semantic.d.ts +11 -0
  183. package/dist/schema/semantic.d.ts.map +1 -1
  184. package/dist/schema/semantic.js +262 -68
  185. package/dist/schema/semantic.js.map +1 -1
  186. package/dist/schema/sub-apis.d.ts +52 -0
  187. package/dist/schema/sub-apis.d.ts.map +1 -0
  188. package/dist/schema/sub-apis.js +216 -0
  189. package/dist/schema/sub-apis.js.map +1 -0
  190. package/dist/schema/system-entities.d.ts +42 -0
  191. package/dist/schema/system-entities.d.ts.map +1 -0
  192. package/dist/schema/system-entities.js +101 -0
  193. package/dist/schema/system-entities.js.map +1 -0
  194. package/dist/schema/types.d.ts +91 -9
  195. package/dist/schema/types.d.ts.map +1 -1
  196. package/dist/schema/union-fallback.d.ts +219 -0
  197. package/dist/schema/union-fallback.d.ts.map +1 -0
  198. package/dist/schema/union-fallback.js +331 -0
  199. package/dist/schema/union-fallback.js.map +1 -0
  200. package/dist/schema/value-generators/ai.d.ts +54 -0
  201. package/dist/schema/value-generators/ai.d.ts.map +1 -0
  202. package/dist/schema/value-generators/ai.js +136 -0
  203. package/dist/schema/value-generators/ai.js.map +1 -0
  204. package/dist/schema/value-generators/index.d.ts +126 -0
  205. package/dist/schema/value-generators/index.d.ts.map +1 -0
  206. package/dist/schema/value-generators/index.js +219 -0
  207. package/dist/schema/value-generators/index.js.map +1 -0
  208. package/dist/schema/value-generators/placeholder.d.ts +52 -0
  209. package/dist/schema/value-generators/placeholder.d.ts.map +1 -0
  210. package/dist/schema/value-generators/placeholder.js +328 -0
  211. package/dist/schema/value-generators/placeholder.js.map +1 -0
  212. package/dist/schema/value-generators/types.d.ts +116 -0
  213. package/dist/schema/value-generators/types.d.ts.map +1 -0
  214. package/dist/schema/value-generators/types.js +11 -0
  215. package/dist/schema/value-generators/types.js.map +1 -0
  216. package/dist/schema/verb-derivation.d.ts +167 -0
  217. package/dist/schema/verb-derivation.d.ts.map +1 -0
  218. package/dist/schema/verb-derivation.js +281 -0
  219. package/dist/schema/verb-derivation.js.map +1 -0
  220. package/dist/schema/version.d.ts +111 -0
  221. package/dist/schema/version.d.ts.map +1 -0
  222. package/dist/schema/version.js +190 -0
  223. package/dist/schema/version.js.map +1 -0
  224. package/dist/schema.d.ts +1095 -23
  225. package/dist/schema.d.ts.map +1 -1
  226. package/dist/schema.js +2854 -38
  227. package/dist/schema.js.map +1 -1
  228. package/dist/semantic-vectors.d.ts +39 -0
  229. package/dist/semantic-vectors.d.ts.map +1 -0
  230. package/dist/semantic-vectors.js +334 -0
  231. package/dist/semantic-vectors.js.map +1 -0
  232. package/dist/semantic.d.ts +29 -1
  233. package/dist/semantic.d.ts.map +1 -1
  234. package/dist/semantic.js +26 -16
  235. package/dist/semantic.js.map +1 -1
  236. package/dist/telemetry.d.ts +128 -0
  237. package/dist/telemetry.d.ts.map +1 -0
  238. package/dist/telemetry.js +305 -0
  239. package/dist/telemetry.js.map +1 -0
  240. package/dist/tests.d.ts.map +1 -1
  241. package/dist/tests.js +30 -22
  242. package/dist/tests.js.map +1 -1
  243. package/dist/type-guards.d.ts +212 -0
  244. package/dist/type-guards.d.ts.map +1 -0
  245. package/dist/type-guards.js +318 -0
  246. package/dist/type-guards.js.map +1 -0
  247. package/dist/types.d.ts +33 -245
  248. package/dist/types.d.ts.map +1 -1
  249. package/dist/types.js +62 -72
  250. package/dist/types.js.map +1 -1
  251. package/dist/validation.d.ts +165 -0
  252. package/dist/validation.d.ts.map +1 -0
  253. package/dist/validation.js +639 -0
  254. package/dist/validation.js.map +1 -0
  255. package/dist/worker/db-provider.d.ts +168 -0
  256. package/dist/worker/db-provider.d.ts.map +1 -0
  257. package/dist/worker/db-provider.js +277 -0
  258. package/dist/worker/db-provider.js.map +1 -0
  259. package/dist/worker/index.d.ts +35 -0
  260. package/dist/worker/index.d.ts.map +1 -0
  261. package/dist/worker/index.js +37 -0
  262. package/dist/worker/index.js.map +1 -0
  263. package/dist/worker.d.ts +779 -0
  264. package/dist/worker.d.ts.map +1 -0
  265. package/dist/worker.js +2786 -0
  266. package/dist/worker.js.map +1 -0
  267. package/package.json +38 -8
  268. package/src/docs-rels/migrations/0001-init.sql +125 -0
@@ -6,21 +6,208 @@
6
6
  *
7
7
  * @packageDocumentation
8
8
  */
9
+ import { AIGenerationError } from '../errors.js';
9
10
  import { isPrimitiveType } from './parse.js';
10
- import { resolveNestedPending, prefetchContext, resolveInstructions } from './resolve.js';
11
+ import { resolveNestedPending, prefetchContext, resolveInstructions, isPromptField, } from './resolve.js';
12
+ import { PlaceholderValueGenerator } from './value-generators/index.js';
13
+ /**
14
+ * Hard safety limit for cascade/entity generation recursion depth.
15
+ * Applied even when maxDepth is not explicitly configured to prevent
16
+ * infinite recursion with circular schemas.
17
+ */
18
+ export const DEFAULT_MAX_DEPTH = 10;
19
+ // Create a singleton placeholder generator for synchronous calls
20
+ const placeholderGenerator = new PlaceholderValueGenerator();
21
+ // =============================================================================
22
+ // Value Generator Configuration
23
+ // =============================================================================
24
+ /**
25
+ * Current value generator instance
26
+ * Defaults to PlaceholderValueGenerator
27
+ */
28
+ let currentValueGenerator = placeholderGenerator;
29
+ /**
30
+ * Configure the value generator to use for field generation
31
+ *
32
+ * @param generator - The value generator instance to use
33
+ */
34
+ export function setValueGenerator(generator) {
35
+ currentValueGenerator = generator;
36
+ }
37
+ /**
38
+ * Get the current value generator
39
+ *
40
+ * @returns The current value generator instance
41
+ */
42
+ export function getValueGenerator() {
43
+ return currentValueGenerator;
44
+ }
45
+ // =============================================================================
46
+ // AI Generation Configuration
47
+ // =============================================================================
48
+ // Default configuration - uses 'sonnet' as the default model
49
+ let aiConfig = {
50
+ model: 'sonnet',
51
+ enabled: true,
52
+ };
53
+ /**
54
+ * Configure AI generation settings
55
+ *
56
+ * @param config - Partial configuration to merge with defaults
57
+ */
58
+ export function configureAIGeneration(config) {
59
+ aiConfig = { ...aiConfig, ...config };
60
+ }
61
+ /**
62
+ * Get current AI generation configuration
63
+ */
64
+ export function getAIGenerationConfig() {
65
+ return { ...aiConfig };
66
+ }
67
+ // =============================================================================
68
+ // AI-Powered Entity Generation
69
+ // =============================================================================
70
+ /**
71
+ * Build a simple schema object for generateObject from entity fields
72
+ *
73
+ * @param entity - The parsed entity definition
74
+ * @returns A simple schema object mapping field names to type descriptions
75
+ */
76
+ function buildSchemaForEntity(entity) {
77
+ const schema = {};
78
+ for (const [fieldName, field] of entity.fields) {
79
+ // Only include non-relation scalar fields
80
+ if (!field.isRelation) {
81
+ const isPrompt = isPromptField(field);
82
+ if (field.type === 'string') {
83
+ // Use the field prompt if available, otherwise a generic description
84
+ schema[fieldName] = field.prompt || `Generate a ${fieldName}`;
85
+ }
86
+ else if (isPrompt) {
87
+ // For prompt fields, use the type (which is the prompt) as the schema
88
+ schema[fieldName] = field.type;
89
+ }
90
+ else if (field.type === 'number') {
91
+ schema[fieldName] = `number`;
92
+ }
93
+ else if (field.type === 'boolean') {
94
+ schema[fieldName] = `boolean`;
95
+ }
96
+ // Note: Arrays handled separately for now
97
+ }
98
+ }
99
+ return schema;
100
+ }
101
+ /**
102
+ * Generate entity data using AI via generateObject from ai-functions
103
+ *
104
+ * @param type - The type name of the entity to generate
105
+ * @param entity - The parsed entity definition
106
+ * @param prompt - The generation prompt (from field definition)
107
+ * @param context - Context from parent entity for informed generation
108
+ * @param injectedConfig - Optional AI config to use instead of module-level config (for DI)
109
+ * @returns Generated entity data, or null if AI generation failed/unavailable
110
+ */
111
+ async function generateEntityDataWithAI(type, entity, prompt, context, injectedConfig) {
112
+ // Use injected config if provided, otherwise fall back to module-level config
113
+ const config = injectedConfig ?? aiConfig;
114
+ if (!config.enabled) {
115
+ return null;
116
+ }
117
+ try {
118
+ // Dynamically import generateObject to avoid circular dependencies
119
+ // and to allow the mock to work in tests
120
+ const { generateObject } = await import('ai-functions');
121
+ // Build schema for the target entity
122
+ const schema = buildSchemaForEntity(entity);
123
+ // If no fields to generate, return null
124
+ if (Object.keys(schema).length === 0) {
125
+ return null;
126
+ }
127
+ // Build comprehensive prompt with context
128
+ const promptParts = [];
129
+ // Add the field prompt (e.g., "What problem does this solve?")
130
+ if (prompt && prompt.trim()) {
131
+ promptParts.push(prompt);
132
+ }
133
+ // Add $instructions if available
134
+ if (context.instructions) {
135
+ promptParts.push(`Context: ${context.instructions}`);
136
+ }
137
+ // Add $context if available
138
+ if (context.schemaContext) {
139
+ promptParts.push(context.schemaContext);
140
+ }
141
+ // Add relevant parent data for context
142
+ const parentContextEntries = [];
143
+ for (const [key, value] of Object.entries(context.parentData)) {
144
+ if (!key.startsWith('$') && !key.startsWith('_') && typeof value === 'string' && value) {
145
+ parentContextEntries.push(`${key}: ${value}`);
146
+ }
147
+ }
148
+ if (parentContextEntries.length > 0) {
149
+ promptParts.push(`Parent entity: ${parentContextEntries.join(', ')}`);
150
+ }
151
+ // Add type context
152
+ promptParts.push(`Generate a ${type} entity with the following fields.`);
153
+ const fullPrompt = promptParts.join('\n');
154
+ // Call generateObject with the schema and prompt, tracking timing
155
+ const startTime = Date.now();
156
+ const result = await generateObject({
157
+ model: config.model,
158
+ schema,
159
+ prompt: fullPrompt,
160
+ });
161
+ const latencyMs = Date.now() - startTime;
162
+ // Call onGenerate callback if configured
163
+ if (config.onGenerate) {
164
+ config.onGenerate({
165
+ entityType: type,
166
+ model: config.model,
167
+ prompt: fullPrompt,
168
+ result: result.object,
169
+ latencyMs,
170
+ timestamp: new Date(),
171
+ });
172
+ }
173
+ return result.object;
174
+ }
175
+ catch (error) {
176
+ // Call onGenerate callback with error if configured
177
+ if (config.onGenerate) {
178
+ config.onGenerate({
179
+ entityType: type,
180
+ model: config.model,
181
+ prompt: prompt || '',
182
+ result: null,
183
+ latencyMs: 0,
184
+ error: error instanceof Error ? error.message : String(error),
185
+ timestamp: new Date(),
186
+ });
187
+ }
188
+ // Throw AIGenerationError - don't silently fall back to placeholder
189
+ const cause = error instanceof Error ? error : undefined;
190
+ throw new AIGenerationError(cause?.message || 'Unknown AI generation failure', type, undefined, cause);
191
+ }
192
+ }
11
193
  // =============================================================================
12
194
  // Context-Aware Value Generation
13
195
  // =============================================================================
14
196
  /**
15
197
  * Generate a context-aware value for a field
16
198
  *
17
- * Uses hint, instructions, schema context, and parent data to generate
18
- * contextually appropriate values. This is a minimal deterministic implementation
19
- * for testing - real AI generation would integrate with LLMs.
199
+ * **DELEGATED TO VALUE-GENERATORS MODULE**
20
200
  *
21
- * The function uses keyword matching on the context and hint to produce
22
- * contextually relevant values for common field names like 'name', 'description',
23
- * 'headline', 'background', etc.
201
+ * This function now delegates to PlaceholderValueGenerator for backward
202
+ * compatibility. The actual generation logic has been extracted to:
203
+ * `./value-generators/placeholder.ts`
204
+ *
205
+ * For new code, prefer using the ValueGenerator interface directly:
206
+ * ```ts
207
+ * import { getValueGenerator } from './value-generators/index.js'
208
+ * const generator = getValueGenerator()
209
+ * const result = await generator.generate({ fieldName, type, fullContext, hint, parentData })
210
+ * ```
24
211
  *
25
212
  * @param fieldName - The name of the field being generated
26
213
  * @param type - The entity type name
@@ -39,180 +226,108 @@ import { resolveNestedPending, prefetchContext, resolveInstructions } from './re
39
226
  * ```
40
227
  */
41
228
  export function generateContextAwareValue(fieldName, type, fullContext, hint, parentData = {}) {
42
- // If parent has the same field, copy its value (for self-referential types like Company.competitor)
43
- const parentValue = parentData[fieldName];
44
- if (typeof parentValue === 'string' && parentValue) {
45
- return parentValue;
46
- }
47
- // If no context provided, fall back to static placeholder
48
- if (!fullContext || fullContext.trim() === '') {
49
- return `Generated ${fieldName} for ${type}`;
50
- }
51
- const contextLower = fullContext.toLowerCase();
52
- const hintLower = (hint || '').toLowerCase();
53
- // For 'name' field, use hint-based generation with keyword matching
54
- if (fieldName === 'name') {
55
- if (hintLower.includes('philosopher') || contextLower.includes('philosopher'))
56
- return 'Aristotle';
57
- if (hintLower.includes('tech entrepreneur') || hintLower.includes('startup'))
58
- return 'Alex Chen';
59
- if (hint && hint.trim())
60
- return `${type}: ${hint}`;
61
- return `Generated ${fieldName} for ${type}`;
62
- }
63
- // For 'style' field
64
- if (fieldName === 'style') {
65
- if (hintLower.includes('energetic') || contextLower.includes('energetic'))
66
- return 'Energetic and engaging presentation style';
67
- if (contextLower.includes('horror') || contextLower.includes('dark'))
68
- return 'Dark and atmospheric horror style';
69
- if (contextLower.includes('sci-fi') || contextLower.includes('futuristic'))
70
- return 'Atmospheric sci-fi suspense style';
71
- return `${fieldName}: ${fullContext}`;
72
- }
73
- // For 'background' field
74
- if (fieldName === 'background') {
75
- if (hintLower.includes('tech entrepreneur') || hintLower.includes('startup'))
76
- return 'Tech startup founder with 10 years experience';
77
- if (hintLower.includes('aristocrat') || hintLower.includes('noble'))
78
- return 'English aristocrat from old noble family';
79
- if (contextLower.includes('renewable') || contextLower.includes('energy'))
80
- return 'Background in renewable energy sector';
81
- return `${fieldName}: ${fullContext}`;
82
- }
83
- // For 'specialty' field
84
- if (fieldName === 'specialty') {
85
- if (contextLower.includes('french') || contextLower.includes('restaurant'))
86
- return 'French classical cuisine';
87
- if (hintLower.includes('security') || contextLower.includes('security'))
88
- return 'Security and authentication systems';
89
- if (hintLower.includes('history') || hintLower.includes('medieval'))
90
- return 'Medieval history specialist';
91
- return `${fieldName}: ${fullContext}`;
92
- }
93
- // For 'training' field
94
- if (fieldName === 'training') {
95
- if (contextLower.includes('french') || contextLower.includes('restaurant'))
96
- return 'Trained in classical French culinary techniques';
97
- return `${fieldName}: ${fullContext}`;
98
- }
99
- // For 'backstory' field
100
- if (fieldName === 'backstory') {
101
- if (contextLower.includes('medieval') || contextLower.includes('fantasy'))
102
- return 'A noble knight who served the King in the great castle, completing many quests across the kingdom';
103
- if (contextLower.includes('sci-fi') || contextLower.includes('space'))
104
- return 'A starship captain with years of deep space exploration';
105
- return `${fieldName}: ${fullContext}`;
106
- }
107
- // For 'headline' field
108
- if (fieldName === 'headline') {
109
- // Check for name mentions in context for personalized headlines
110
- if (contextLower.includes('codehelper'))
111
- return 'CodeHelper: Dev Tools';
112
- if (contextLower.includes('techcorp'))
113
- return 'TechCorp Solutions';
114
- if (contextLower.includes('software engineer'))
115
- return 'For Dev Teams';
116
- if (contextLower.includes('tech') || contextLower.includes('startup'))
117
- return 'Tech Startup Solutions';
118
- return `Headline for ${type}`.slice(0, 30);
119
- }
120
- // For 'copy' field
121
- if (fieldName === 'copy') {
122
- if (contextLower.includes('tech') || contextLower.includes('startup'))
123
- return 'Innovative tech solutions for startups and growing companies';
124
- if (contextLower.includes('marketing') || contextLower.includes('campaign'))
125
- return 'Effective marketing campaign for tech launch';
126
- return `${fieldName}: ${fullContext}`;
127
- }
128
- // For 'tagline' field
129
- if (fieldName === 'tagline') {
130
- if (contextLower.includes('luxury') || contextLower.includes('premium'))
131
- return 'Luxury craftsmanship meets elegant design';
132
- if (contextLower.includes('quality') || contextLower.includes('craftsmanship'))
133
- return 'Premium quality with expert craftsmanship';
134
- if (contextLower.includes('tech'))
135
- return 'Technology for the future';
136
- return `${fieldName}: ${fullContext}`;
137
- }
138
- // For 'description' field
139
- if (fieldName === 'description') {
140
- if (contextLower.includes('cyberpunk') || contextLower.includes('neon') || contextLower.includes('futuristic'))
141
- return 'Cyberpunk character with neural augmentations';
142
- if (contextLower.includes('luxury') || contextLower.includes('high-end') || contextLower.includes('premium'))
143
- return 'A luxury premium product with elegant craftsmanship';
144
- if (contextLower.includes('enterprise') || contextLower.includes('b2b'))
145
- return 'Enterprise solution for business customers';
146
- if (contextLower.includes('nurse') || contextLower.includes('healthcare'))
147
- return 'Healthcare documentation solution for nurses and medical staff';
148
- return `${fieldName}: ${fullContext}`;
149
- }
150
- // For 'abilities' field
151
- if (fieldName === 'abilities') {
152
- if (contextLower.includes('cyberpunk') || contextLower.includes('futuristic'))
153
- return 'Neural hacking and digital infiltration';
154
- return `${fieldName}: ${fullContext}`;
155
- }
156
- // For 'method' field
157
- if (fieldName === 'method') {
158
- if (hintLower.includes('wit') || hintLower.includes('sharp'))
159
- return 'Brilliant deduction and clever observation';
160
- return `${fieldName}: ${fullContext}`;
161
- }
162
- // For 'expertise' field
163
- if (fieldName === 'expertise') {
164
- if (contextLower.includes('machine learning') || contextLower.includes('medical') || contextLower.includes('ai'))
165
- return 'Machine learning for medical applications';
166
- if (hintLower.includes('physics') || hintLower.includes('professor'))
167
- return 'Physics professor specializing in quantum mechanics';
168
- if (hintLower.includes('journalist') || hintLower.includes('science'))
169
- return 'Science journalist covering physics research';
170
- return `${fieldName}: ${fullContext}`;
171
- }
172
- // For 'focus' field
173
- if (fieldName === 'focus') {
174
- if (contextLower.includes('renewable') || contextLower.includes('energy') || contextLower.includes('green'))
175
- return 'Focus on sustainable energy transformation';
176
- if (contextLower.includes('tech') || contextLower.includes('programming'))
177
- return 'Focus on technical programming topics';
178
- return `${fieldName}: ${fullContext}`;
179
- }
180
- // For 'qualifications' field
181
- if (fieldName === 'qualifications') {
182
- if (contextLower.includes('astrophysics') || contextLower.includes('astronomy') || contextLower.includes('space'))
183
- return 'PhD in Astrophysics from MIT';
184
- return `${fieldName}: ${fullContext}`;
185
- }
186
- // For 'teachingStyle' field
187
- if (fieldName === 'teachingStyle') {
188
- if (contextLower.includes('beginner') || contextLower.includes('introduct'))
189
- return 'Patient and accessible approach for beginners';
190
- return `${fieldName}: ${fullContext}`;
191
- }
192
- // For 'experience' field
193
- if (fieldName === 'experience') {
194
- if (contextLower.includes('horror') || contextLower.includes('film'))
195
- return 'Experience in horror film production';
196
- return `${fieldName}: ${fullContext}`;
197
- }
198
- // For 'role' field
199
- if (fieldName === 'role') {
200
- if (hintLower.includes('research') || hintLower.includes('machine learning') || hintLower.includes('phd'))
201
- return 'Machine learning researcher';
202
- return `${fieldName}: ${fullContext}`;
203
- }
204
- // For 'portfolio' field
205
- if (fieldName === 'portfolio') {
206
- if (hintLower.includes('award') || hintLower.includes('beaux-arts') || hintLower.includes('ecole'))
207
- return 'Award-winning design portfolio from Beaux-Arts';
208
- return `${fieldName}: ${fullContext}`;
209
- }
210
- // Default: include context in the generated value
211
- return `${fieldName}: ${fullContext}`;
229
+ // Use the configured value generator
230
+ // Note: This is synchronous, so we rely on PlaceholderValueGenerator's sync method
231
+ // If a custom generator is configured, it must support synchronous generation
232
+ const generator = currentValueGenerator;
233
+ // If it's a PlaceholderValueGenerator, use the sync method
234
+ if (generator instanceof PlaceholderValueGenerator) {
235
+ return generator.generateSync({
236
+ fieldName,
237
+ type,
238
+ fullContext,
239
+ parentData,
240
+ ...(hint !== undefined && { hint }),
241
+ });
242
+ }
243
+ // For non-PlaceholderValueGenerator instances, fall back to placeholder
244
+ // for backward compatibility (async generators require different call pattern)
245
+ return placeholderGenerator.generateSync({
246
+ fieldName,
247
+ type,
248
+ fullContext,
249
+ parentData,
250
+ ...(hint !== undefined && { hint }),
251
+ });
212
252
  }
213
253
  // =============================================================================
214
254
  // AI Field Generation
215
255
  // =============================================================================
256
+ /**
257
+ * Build a combined entity object with pre-fetched context data merged in.
258
+ * This replaces UUID references with actual entity objects for template resolution.
259
+ */
260
+ function buildCombinedEntityWithContext(entityData, contextData) {
261
+ const combined = { ...entityData };
262
+ // Sort context keys by length so we process parent paths before nested paths
263
+ // This ensures 'project' is set before 'project.lead'
264
+ const sortedKeys = [...contextData.keys()].sort((a, b) => a.length - b.length);
265
+ for (const key of sortedKeys) {
266
+ const value = contextData.get(key);
267
+ const pathParts = key.split('.');
268
+ if (pathParts.length === 1) {
269
+ // Simple path like 'project' - set at top level if currently a UUID
270
+ const currentValue = combined[key];
271
+ const isCurrentUUID = typeof currentValue === 'string' && currentValue.includes('-');
272
+ if (!combined[key] || isCurrentUUID) {
273
+ combined[key] = { ...value };
274
+ }
275
+ }
276
+ else {
277
+ // Nested path like 'project.lead' - traverse and set nested value
278
+ let current = combined;
279
+ for (let i = 0; i < pathParts.length - 1; i++) {
280
+ const part = pathParts[i];
281
+ if (current[part] && typeof current[part] === 'object') {
282
+ current = current[part];
283
+ }
284
+ else {
285
+ break;
286
+ }
287
+ }
288
+ const lastPart = pathParts[pathParts.length - 1];
289
+ if (current && typeof current === 'object') {
290
+ current[lastPart] = value;
291
+ }
292
+ }
293
+ // Add leaf entity at top level for direct access (e.g., {lead.name})
294
+ if (pathParts.length > 1 && typeof value === 'object' && value !== null) {
295
+ const leafKey = pathParts[pathParts.length - 1];
296
+ if (!combined[leafKey]) {
297
+ combined[leafKey] = value;
298
+ }
299
+ }
300
+ }
301
+ return combined;
302
+ }
303
+ /**
304
+ * Build a context string from entity data and pre-fetched context.
305
+ * Extracts string fields (excluding internal fields) and formats them for AI context.
306
+ */
307
+ function buildContextString(resolvedInstructions, entityData, contextData) {
308
+ const parts = [];
309
+ if (resolvedInstructions) {
310
+ parts.push(resolvedInstructions);
311
+ }
312
+ // Add entity data fields
313
+ for (const [key, value] of Object.entries(entityData)) {
314
+ if (!key.startsWith('$') && !key.startsWith('_') && typeof value === 'string' && value) {
315
+ parts.push(`${key}: ${value}`);
316
+ }
317
+ }
318
+ // Add context from pre-fetched entities
319
+ for (const [key, ctxEntity] of contextData) {
320
+ for (const [fieldName, fieldValue] of Object.entries(ctxEntity)) {
321
+ if (!fieldName.startsWith('$') &&
322
+ !fieldName.startsWith('_') &&
323
+ typeof fieldValue === 'string' &&
324
+ fieldValue) {
325
+ parts.push(`${key}.${fieldName}: ${fieldValue}`);
326
+ }
327
+ }
328
+ }
329
+ return parts.join(' | ');
330
+ }
216
331
  /**
217
332
  * Generate AI fields based on $instructions and field prompts
218
333
  *
@@ -224,13 +339,15 @@ export function generateContextAwareValue(fieldName, type, fullContext, hint, pa
224
339
  * @param entityDef - The parsed entity definition
225
340
  * @param schema - The parsed schema
226
341
  * @param provider - The database provider
342
+ * @param injectedConfig - Optional AI config to use instead of module-level config (for DI)
227
343
  * @returns The data with AI-generated fields populated
228
344
  */
229
- export async function generateAIFields(data, typeName, entityDef, schema, provider) {
345
+ export async function generateAIFields(data, typeName, entityDef, schema, provider, injectedConfig) {
346
+ const config = injectedConfig ?? aiConfig;
230
347
  const result = { ...data };
231
348
  const entitySchema = entityDef.schema || {};
232
- const instructions = entitySchema.$instructions;
233
- const contextDeps = entitySchema.$context;
349
+ const instructions = entitySchema['$instructions'];
350
+ const contextDeps = entitySchema['$context'];
234
351
  // Pre-fetch context dependencies if declared
235
352
  let contextData = new Map();
236
353
  if (contextDeps && Array.isArray(contextDeps)) {
@@ -239,54 +356,91 @@ export async function generateAIFields(data, typeName, entityDef, schema, provid
239
356
  // Resolve instructions template variables
240
357
  let resolvedInstructions = instructions;
241
358
  if (instructions) {
242
- // Build a combined entity with context data for template resolution
243
- const combinedEntity = { ...result };
244
- for (const [key, value] of contextData) {
245
- const topLevelKey = key.split('.')[0];
246
- if (!combinedEntity[topLevelKey]) {
247
- combinedEntity[topLevelKey] = value;
248
- }
249
- }
359
+ const combinedEntity = buildCombinedEntityWithContext(result, contextData);
250
360
  resolvedInstructions = await resolveInstructions(instructions, combinedEntity, typeName, schema, provider);
251
361
  }
252
- // Build context string from resolved instructions and entity data
253
- const contextParts = [];
254
- if (resolvedInstructions)
255
- contextParts.push(resolvedInstructions);
256
- // Add relevant entity data as context
257
- for (const [key, value] of Object.entries(result)) {
258
- if (!key.startsWith('$') && !key.startsWith('_') && typeof value === 'string' && value) {
259
- contextParts.push(`${key}: ${value}`);
362
+ const fullContext = buildContextString(resolvedInstructions, result, contextData);
363
+ // Collect fields that need generation
364
+ const fieldsToGenerate = [];
365
+ // When entity has $context or template variables, allow regenerating draft-generated prompt fields
366
+ const hasRichContext = !!(contextDeps && contextDeps.length > 0) || !!(instructions && instructions.includes('{'));
367
+ for (const [fieldName, field] of entityDef.fields) {
368
+ // Skip relation fields (handled separately)
369
+ if (field.isRelation) {
370
+ continue;
371
+ }
372
+ const isPrompt = isPromptField(field);
373
+ // Skip if value already provided (unless AI can improve it)
374
+ if (result[fieldName] !== undefined && result[fieldName] !== null) {
375
+ // When AI is enabled, always attempt to generate prompt fields
376
+ // (overriding draft-generated placeholder values with AI-generated content).
377
+ // When AI is disabled, skip already-populated fields unless rich context is available.
378
+ if (isPrompt && (config.enabled || hasRichContext)) {
379
+ fieldsToGenerate.push({ fieldName, prompt: field.type });
380
+ }
381
+ continue;
382
+ }
383
+ if (isPrompt) {
384
+ // Use the field type (which is actually the prompt) as the prompt
385
+ fieldsToGenerate.push({ fieldName, prompt: field.type });
386
+ }
387
+ else if (field.type === 'string' && instructions) {
388
+ // Generate plain string fields when we have $instructions context
389
+ fieldsToGenerate.push({ fieldName, prompt: undefined });
260
390
  }
261
391
  }
262
- // Add context from pre-fetched entities
263
- for (const [key, ctxEntity] of contextData) {
264
- for (const [fieldName, fieldValue] of Object.entries(ctxEntity)) {
265
- if (!fieldName.startsWith('$') && !fieldName.startsWith('_') && typeof fieldValue === 'string' && fieldValue) {
266
- contextParts.push(`${key}.${fieldName}: ${fieldValue}`);
392
+ // Try AI generation for all fields that need it
393
+ if (fieldsToGenerate.length > 0 && config.enabled) {
394
+ try {
395
+ const { generateObject } = await import('ai-functions');
396
+ // Build a schema for all fields to generate
397
+ const fieldsSchema = {};
398
+ for (const { fieldName, prompt } of fieldsToGenerate) {
399
+ fieldsSchema[fieldName] = prompt || `Generate a ${fieldName}`;
400
+ }
401
+ // Build prompt with resolved instructions
402
+ const promptParts = [];
403
+ if (resolvedInstructions) {
404
+ promptParts.push(resolvedInstructions);
405
+ }
406
+ promptParts.push(`Generate a ${typeName} with the following fields.`);
407
+ const aiPrompt = promptParts.join('\n');
408
+ const aiResult = await generateObject({
409
+ model: config.model,
410
+ schema: fieldsSchema,
411
+ prompt: aiPrompt,
412
+ });
413
+ // Apply generated values
414
+ const generated = aiResult.object;
415
+ for (const { fieldName } of fieldsToGenerate) {
416
+ if (generated[fieldName] !== undefined) {
417
+ result[fieldName] = generated[fieldName];
418
+ }
267
419
  }
268
420
  }
421
+ catch (error) {
422
+ // Throw AIGenerationError - don't silently fall back to placeholder
423
+ const cause = error instanceof Error ? error : undefined;
424
+ throw new AIGenerationError(cause?.message || 'Unknown AI field generation failure', typeName, undefined, cause);
425
+ }
269
426
  }
270
- const fullContext = contextParts.join(' | ');
271
- // Generate values for prompt fields that don't have values
272
- for (const [fieldName, field] of entityDef.fields) {
273
- // Skip if value already provided
274
- if (result[fieldName] !== undefined && result[fieldName] !== null)
275
- continue;
276
- // Skip relation fields (handled separately)
277
- if (field.isRelation)
278
- continue;
279
- // Check if this is a prompt field (type contains spaces) or needs generation
280
- const fieldDef = entitySchema[fieldName];
281
- const isPrompt = typeof fieldDef === 'string' && fieldDef.includes(' ') && !fieldDef.includes('->');
282
- if (isPrompt || (field.type === 'string' && !isPrimitiveType(field.type))) {
283
- // Use the field definition as the prompt
284
- const prompt = typeof fieldDef === 'string' ? fieldDef : undefined;
427
+ // Fill in any remaining fields with placeholder values
428
+ for (const { fieldName, prompt } of fieldsToGenerate) {
429
+ if (result[fieldName] === undefined || result[fieldName] === null) {
285
430
  result[fieldName] = generateContextAwareValue(fieldName, typeName, fullContext, prompt, result);
286
431
  }
287
- else if (field.type === 'string' && instructions) {
288
- // Generate string fields when we have $instructions context
289
- result[fieldName] = generateContextAwareValue(fieldName, typeName, fullContext, undefined, result);
432
+ }
433
+ // Enforce length constraints from field type hints like 'string (30 chars)'
434
+ for (const { fieldName, prompt } of fieldsToGenerate) {
435
+ if (typeof result[fieldName] === 'string' && prompt) {
436
+ const charMatch = prompt.match(/\((\d+)\s*chars?\)/);
437
+ if (charMatch) {
438
+ const maxLen = parseInt(charMatch[1], 10);
439
+ const value = result[fieldName];
440
+ if (value.length > maxLen) {
441
+ result[fieldName] = value.slice(0, maxLen);
442
+ }
443
+ }
290
444
  }
291
445
  }
292
446
  return result;
@@ -294,27 +448,87 @@ export async function generateAIFields(data, typeName, entityDef, schema, provid
294
448
  // =============================================================================
295
449
  // Entity Generation
296
450
  // =============================================================================
451
+ /**
452
+ * Check if a field is a backward relation to a specific parent type
453
+ */
454
+ function isBackwardRelationToParent(field, parentType) {
455
+ return (field.operator === '<-' && field.direction === 'backward' && field.relatedType === parentType);
456
+ }
457
+ /**
458
+ * Check if a field is a required forward single relation (not array, not optional)
459
+ */
460
+ function isRequiredForwardSingleRelation(field) {
461
+ return (field.operator === '->' && field.direction === 'forward' && !field.isArray && !field.isOptional);
462
+ }
463
+ /**
464
+ * Process relation fields and populate data with backward refs and pending forward relations
465
+ *
466
+ * This shared helper handles relations consistently whether AI generation succeeded
467
+ * or fell back to placeholder generation.
468
+ */
469
+ async function processRelationFields(data, entity, type, context, schema, _depth, injectedConfig) {
470
+ for (const [fieldName, field] of entity.fields) {
471
+ if (!field.isRelation)
472
+ continue;
473
+ if (isBackwardRelationToParent(field, context.parent) && context.parentId) {
474
+ data[fieldName] = context.parentId;
475
+ }
476
+ else if (isRequiredForwardSingleRelation(field)) {
477
+ const nestedGenerated = await generateEntity(field.relatedType, field.prompt, { parent: type, parentData: data }, schema, _depth + 1, injectedConfig);
478
+ data[`_pending_${fieldName}`] = { type: field.relatedType, data: nestedGenerated };
479
+ }
480
+ }
481
+ }
297
482
  /**
298
483
  * Generate an entity based on its type and context
299
484
  *
300
- * For testing, generates deterministic content based on the prompt and type.
301
- * In production, this would integrate with AI generation.
485
+ * Uses AI generation via generateObject from ai-functions when available,
486
+ * falling back to deterministic placeholder values for testing or when AI fails.
302
487
  *
303
488
  * @param type - The type of entity to generate
304
489
  * @param prompt - Optional prompt for generation context
305
490
  * @param context - Parent context information (parent type name, parentData, and optional parentId)
306
491
  * @param schema - The parsed schema
492
+ * @param _depth - Current recursion depth (internal)
493
+ * @param injectedConfig - Optional AI config to use instead of module-level config (for DI)
307
494
  */
308
- export async function generateEntity(type, prompt, context, schema) {
495
+ export async function generateEntity(type, prompt, context, schema, _depth = 0, injectedConfig) {
496
+ // Hard recursion guard to prevent infinite recursion with circular schemas
497
+ if (_depth >= DEFAULT_MAX_DEPTH) {
498
+ return {};
499
+ }
309
500
  const entity = schema.entities.get(type);
310
501
  if (!entity)
311
502
  throw new Error(`Unknown type: ${type}`);
312
503
  // Gather context for generation
313
504
  const parentEntity = schema.entities.get(context.parent);
314
505
  const parentSchema = parentEntity?.schema || {};
315
- const instructions = parentSchema.$instructions;
316
- const schemaContext = parentSchema.$context;
317
- // Extract relevant parent data for context (excluding metadata fields)
506
+ const instructions = parentSchema['$instructions'];
507
+ const schemaContext = parentSchema['$context'];
508
+ // Try AI-powered generation first
509
+ let aiGeneratedData = null;
510
+ try {
511
+ aiGeneratedData = await generateEntityDataWithAI(type, entity, prompt, {
512
+ parentData: context.parentData,
513
+ ...(instructions !== undefined && { instructions }),
514
+ ...(schemaContext !== undefined && { schemaContext }),
515
+ }, injectedConfig);
516
+ }
517
+ catch (error) {
518
+ // If AI generation fails (e.g., ai-functions not available in tests),
519
+ // fall through to placeholder generation below
520
+ if (!(error instanceof AIGenerationError)) {
521
+ throw error; // Re-throw unexpected errors
522
+ }
523
+ // AIGenerationError: fall through to placeholder generation
524
+ }
525
+ // If AI generation succeeded, use that data but still handle relations
526
+ if (aiGeneratedData) {
527
+ const data = { ...aiGeneratedData };
528
+ await processRelationFields(data, entity, type, context, schema, _depth, injectedConfig);
529
+ return data;
530
+ }
531
+ // Fallback to placeholder generation if AI is not available or failed
318
532
  const parentContextFields = [];
319
533
  for (const [key, value] of Object.entries(context.parentData)) {
320
534
  if (!key.startsWith('$') && !key.startsWith('_') && typeof value === 'string' && value) {
@@ -335,35 +549,21 @@ export async function generateEntity(type, prompt, context, schema) {
335
549
  const data = {};
336
550
  for (const [fieldName, field] of entity.fields) {
337
551
  if (!field.isRelation) {
338
- if (field.type === 'string') {
339
- // Generate context-aware content
340
- data[fieldName] = generateContextAwareValue(fieldName, type, fullContext, prompt, context.parentData);
341
- }
342
- else if (field.isArray && field.type === 'string') {
343
- // Generate array of strings
344
- data[fieldName] = [generateContextAwareValue(fieldName, type, fullContext, prompt, context.parentData)];
345
- }
346
- }
347
- else if (field.operator === '<-' && field.direction === 'backward') {
348
- // Backward relation to parent - set the parent's ID if this entity's
349
- // related type matches the parent type
350
- if (field.relatedType === context.parent && context.parentId) {
351
- // Store the parent ID directly - this is a reference back to the parent
352
- data[fieldName] = context.parentId;
552
+ const isPrompt = isPromptField(field);
553
+ if (field.type === 'string' || isPrompt) {
554
+ const fieldHint = isPrompt ? field.type : prompt;
555
+ data[fieldName] = generateContextAwareValue(fieldName, type, fullContext, fieldHint, context.parentData);
353
556
  }
354
- }
355
- else if (field.operator === '->' && field.direction === 'forward') {
356
- // Recursively generate nested forward exact relations
357
- // This handles cases like Person.bio -> Bio
358
- if (!field.isOptional) {
359
- const nestedGenerated = await generateEntity(field.relatedType, field.prompt, { parent: type, parentData: data }, schema);
360
- // We need to create the nested entity too, but we can't do that here
361
- // because we don't have access to the provider yet.
362
- // This will be handled by resolveForwardExact when it calls us
363
- data[`_pending_${fieldName}`] = { type: field.relatedType, data: nestedGenerated };
557
+ else if (field.isArray && (field.type === 'string' || isPrompt)) {
558
+ const fieldHint = isPrompt ? field.type : prompt;
559
+ data[fieldName] = [
560
+ generateContextAwareValue(fieldName, type, fullContext, fieldHint, context.parentData),
561
+ ];
364
562
  }
365
563
  }
366
564
  }
565
+ // Process relation fields using shared helper
566
+ await processRelationFields(data, entity, type, context, schema, _depth, injectedConfig);
367
567
  return data;
368
568
  }
369
569
  // =============================================================================
@@ -402,7 +602,11 @@ export async function resolveForwardExact(typeName, data, entity, schema, provid
402
602
  continue;
403
603
  if (field.isArray) {
404
604
  // Forward array relation - check if we should auto-generate
405
- const relatedEntity = schema.entities.get(field.relatedType);
605
+ // For union types, use the first union type as the generation target
606
+ const generateType = field.unionTypes && field.unionTypes.length > 0
607
+ ? field.unionTypes[0]
608
+ : field.relatedType;
609
+ const relatedEntity = schema.entities.get(generateType);
406
610
  if (!relatedEntity)
407
611
  continue;
408
612
  // Check if related entity has a backward ref to this type (symmetric relationship)
@@ -426,31 +630,41 @@ export async function resolveForwardExact(typeName, data, entity, schema, provid
426
630
  // Decide whether to auto-generate:
427
631
  // - If there's a symmetric backward ref AND required scalars, skip (prevents duplicates)
428
632
  // - Otherwise, generate if the related entity can be meaningfully generated
429
- const shouldSkip = hasBackwardRef && hasRequiredScalarFields;
430
- const canGenerate = !shouldSkip && (hasBackwardRef || // Symmetric ref without required scalars
431
- field.prompt || // Has a generation prompt
432
- !hasRequiredScalarFields // No required fields to worry about
433
- );
633
+ // - For union types, always allow generation (we have explicit type to generate)
634
+ const hasUnionTypes = field.unionTypes && field.unionTypes.length > 0;
635
+ const shouldSkip = hasBackwardRef && hasRequiredScalarFields && !hasUnionTypes;
636
+ const canGenerate = !shouldSkip &&
637
+ (hasBackwardRef || // Symmetric ref without required scalars
638
+ field.prompt || // Has a generation prompt
639
+ !hasRequiredScalarFields || // No required fields to worry about
640
+ hasUnionTypes); // Union types should generate the first type
434
641
  if (!canGenerate)
435
642
  continue;
436
- const generated = await generateEntity(field.relatedType, field.prompt, { parent: typeName, parentData: data, parentId }, schema);
643
+ const generated = await generateEntity(generateType, field.prompt, { parent: typeName, parentData: data, parentId }, schema);
437
644
  // Resolve any pending nested relations in the generated data
438
645
  const resolvedGenerated = await resolveNestedPending(generated, relatedEntity, schema, provider);
439
- const created = await provider.create(field.relatedType, undefined, resolvedGenerated);
440
- resolved[fieldName] = [created.$id];
646
+ const created = await provider.create(generateType, undefined, {
647
+ ...resolvedGenerated,
648
+ $matchedType: generateType,
649
+ });
650
+ resolved[fieldName] = [created['$id']];
651
+ resolved[`${fieldName}$matchedType`] = generateType;
441
652
  // Queue relationship creation for after parent entity is created
442
- pendingRelations.push({ fieldName, targetType: field.relatedType, targetId: created.$id });
653
+ pendingRelations.push({
654
+ fieldName,
655
+ targetType: generateType,
656
+ targetId: created['$id'],
657
+ });
443
658
  }
444
659
  else {
445
660
  // Single non-optional forward relation - generate the related entity
446
- // Generate single entity
447
661
  const generated = await generateEntity(field.relatedType, field.prompt, { parent: typeName, parentData: data, parentId }, schema);
448
662
  // Resolve any pending nested relations in the generated data
449
663
  const relatedEntity = schema.entities.get(field.relatedType);
450
664
  if (relatedEntity) {
451
665
  const resolvedGenerated = await resolveNestedPending(generated, relatedEntity, schema, provider);
452
666
  const created = await provider.create(field.relatedType, undefined, resolvedGenerated);
453
- resolved[fieldName] = created.$id;
667
+ resolved[fieldName] = created['$id'];
454
668
  }
455
669
  }
456
670
  }
@@ -472,10 +686,10 @@ export function generateNaturalLanguageContent(fieldName, prompt, targetType, co
472
686
  // Extract key words from prompt for natural language
473
687
  const keyWords = prompt.toLowerCase();
474
688
  if (keyWords.includes('idea') || keyWords.includes('concept')) {
475
- return `A innovative idea for ${context.name || targetType}`;
689
+ return `A innovative idea for ${context['name'] || targetType}`;
476
690
  }
477
691
  if (keyWords.includes('customer') || keyWords.includes('buyer') || keyWords.includes('user')) {
478
- return `The target customer segment for ${context.name || targetType}`;
692
+ return `The target customer segment for ${context['name'] || targetType}`;
479
693
  }
480
694
  if (keyWords.includes('related') || keyWords.includes('similar')) {
481
695
  return `Related ${targetType.toLowerCase()} content`;
@@ -487,37 +701,41 @@ export function generateNaturalLanguageContent(fieldName, prompt, targetType, co
487
701
  // Generate based on field name patterns
488
702
  const fieldLower = fieldName.toLowerCase();
489
703
  if (fieldLower.includes('idea')) {
490
- return `A compelling idea for ${context.name || 'the project'}`;
704
+ return `A compelling idea for ${context['name'] || 'the project'}`;
491
705
  }
492
706
  if (fieldLower.includes('customer')) {
493
- return `The ideal customer for ${context.name || 'the business'}`;
707
+ return `The ideal customer for ${context['name'] || 'the business'}`;
494
708
  }
495
- if (fieldLower.includes('founder') || fieldLower.includes('lead') || fieldLower.includes('ceo') || fieldLower.includes('cto') || fieldLower.includes('cfo')) {
709
+ if (fieldLower.includes('founder') ||
710
+ fieldLower.includes('lead') ||
711
+ fieldLower.includes('ceo') ||
712
+ fieldLower.includes('cto') ||
713
+ fieldLower.includes('cfo')) {
496
714
  return `A qualified ${fieldName} candidate`;
497
715
  }
498
716
  if (fieldLower.includes('author') || fieldLower.includes('reviewer')) {
499
717
  return `An experienced ${fieldName}`;
500
718
  }
501
719
  if (fieldLower.includes('assignee') || fieldLower.includes('owner')) {
502
- return `The right person for ${context.title || context.name || 'this task'}`;
720
+ return `The right person for ${context['title'] || context['name'] || 'this task'}`;
503
721
  }
504
722
  if (fieldLower.includes('department') || fieldLower.includes('team')) {
505
- return `A department for ${context.name || 'the organization'}`;
723
+ return `A department for ${context['name'] || 'the organization'}`;
506
724
  }
507
725
  if (fieldLower.includes('client') || fieldLower.includes('sponsor')) {
508
- return `A ${fieldName} for ${context.name || context.title || 'the project'}`;
726
+ return `A ${fieldName} for ${context['name'] || context['title'] || 'the project'}`;
509
727
  }
510
728
  if (fieldLower.includes('item') || fieldLower.includes('component')) {
511
729
  return `${targetType} component`;
512
730
  }
513
731
  if (fieldLower.includes('member') || fieldLower.includes('project')) {
514
- return `${targetType} for ${context.name || 'the team'}`;
732
+ return `${targetType} for ${context['name'] || 'the team'}`;
515
733
  }
516
734
  if (fieldLower.includes('character')) {
517
- return `A character for ${context.title || context.name || 'the story'}`;
735
+ return `A character for ${context['title'] || context['name'] || 'the story'}`;
518
736
  }
519
737
  if (fieldLower.includes('setting') || fieldLower.includes('location')) {
520
- return `A setting for ${context.title || context.name || 'the story'}`;
738
+ return `A setting for ${context['title'] || context['name'] || 'the story'}`;
521
739
  }
522
740
  if (fieldLower.includes('address')) {
523
741
  return `Address information`;