ai-database 2.1.3 → 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 (260) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/README.md +880 -669
  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 +49 -23
  8. package/dist/ai-promise-db.d.ts.map +1 -1
  9. package/dist/ai-promise-db.js +91 -63
  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 +35 -8
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +106 -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 +128 -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 +48 -17
  124. package/dist/schema/cascade.d.ts.map +1 -1
  125. package/dist/schema/cascade.js +477 -278
  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 +21 -109
  132. package/dist/schema/dependency-graph.d.ts.map +1 -1
  133. package/dist/schema/dependency-graph.js +25 -333
  134. package/dist/schema/dependency-graph.js.map +1 -1
  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/index.d.ts +28 -34
  144. package/dist/schema/index.d.ts.map +1 -1
  145. package/dist/schema/index.js +454 -521
  146. package/dist/schema/index.js.map +1 -1
  147. package/dist/schema/migration.d.ts +205 -0
  148. package/dist/schema/migration.d.ts.map +1 -0
  149. package/dist/schema/migration.js +327 -0
  150. package/dist/schema/migration.js.map +1 -0
  151. package/dist/schema/nl-query-generator.d.ts +68 -0
  152. package/dist/schema/nl-query-generator.d.ts.map +1 -0
  153. package/dist/schema/nl-query-generator.js +362 -0
  154. package/dist/schema/nl-query-generator.js.map +1 -0
  155. package/dist/schema/nl-query.d.ts +65 -0
  156. package/dist/schema/nl-query.d.ts.map +1 -0
  157. package/dist/schema/nl-query.js +178 -0
  158. package/dist/schema/nl-query.js.map +1 -0
  159. package/dist/schema/parse.d.ts.map +1 -1
  160. package/dist/schema/parse.js +144 -89
  161. package/dist/schema/parse.js.map +1 -1
  162. package/dist/schema/provider.d.ts +37 -0
  163. package/dist/schema/provider.d.ts.map +1 -1
  164. package/dist/schema/provider.js +15 -7
  165. package/dist/schema/provider.js.map +1 -1
  166. package/dist/schema/resolve.d.ts +46 -5
  167. package/dist/schema/resolve.d.ts.map +1 -1
  168. package/dist/schema/resolve.js +237 -95
  169. package/dist/schema/resolve.js.map +1 -1
  170. package/dist/schema/search-utils.d.ts +76 -0
  171. package/dist/schema/search-utils.d.ts.map +1 -0
  172. package/dist/schema/search-utils.js +86 -0
  173. package/dist/schema/search-utils.js.map +1 -0
  174. package/dist/schema/seed.d.ts +53 -0
  175. package/dist/schema/seed.d.ts.map +1 -0
  176. package/dist/schema/seed.js +94 -0
  177. package/dist/schema/seed.js.map +1 -0
  178. package/dist/schema/semantic.d.ts +10 -0
  179. package/dist/schema/semantic.d.ts.map +1 -1
  180. package/dist/schema/semantic.js +192 -86
  181. package/dist/schema/semantic.js.map +1 -1
  182. package/dist/schema/sub-apis.d.ts +52 -0
  183. package/dist/schema/sub-apis.d.ts.map +1 -0
  184. package/dist/schema/sub-apis.js +216 -0
  185. package/dist/schema/sub-apis.js.map +1 -0
  186. package/dist/schema/system-entities.d.ts +42 -0
  187. package/dist/schema/system-entities.d.ts.map +1 -0
  188. package/dist/schema/system-entities.js +101 -0
  189. package/dist/schema/system-entities.js.map +1 -0
  190. package/dist/schema/types.d.ts +91 -9
  191. package/dist/schema/types.d.ts.map +1 -1
  192. package/dist/schema/union-fallback.d.ts.map +1 -1
  193. package/dist/schema/union-fallback.js +21 -15
  194. package/dist/schema/union-fallback.js.map +1 -1
  195. package/dist/schema/value-generators/ai.d.ts +54 -0
  196. package/dist/schema/value-generators/ai.d.ts.map +1 -0
  197. package/dist/schema/value-generators/ai.js +136 -0
  198. package/dist/schema/value-generators/ai.js.map +1 -0
  199. package/dist/schema/value-generators/index.d.ts +126 -0
  200. package/dist/schema/value-generators/index.d.ts.map +1 -0
  201. package/dist/schema/value-generators/index.js +219 -0
  202. package/dist/schema/value-generators/index.js.map +1 -0
  203. package/dist/schema/value-generators/placeholder.d.ts +52 -0
  204. package/dist/schema/value-generators/placeholder.d.ts.map +1 -0
  205. package/dist/schema/value-generators/placeholder.js +328 -0
  206. package/dist/schema/value-generators/placeholder.js.map +1 -0
  207. package/dist/schema/value-generators/types.d.ts +116 -0
  208. package/dist/schema/value-generators/types.d.ts.map +1 -0
  209. package/dist/schema/value-generators/types.js +11 -0
  210. package/dist/schema/value-generators/types.js.map +1 -0
  211. package/dist/schema/version.d.ts +111 -0
  212. package/dist/schema/version.d.ts.map +1 -0
  213. package/dist/schema/version.js +190 -0
  214. package/dist/schema/version.js.map +1 -0
  215. package/dist/schema.d.ts +1095 -24
  216. package/dist/schema.d.ts.map +1 -1
  217. package/dist/schema.js +2852 -40
  218. package/dist/schema.js.map +1 -1
  219. package/dist/semantic-vectors.d.ts +39 -0
  220. package/dist/semantic-vectors.d.ts.map +1 -0
  221. package/dist/semantic-vectors.js +334 -0
  222. package/dist/semantic-vectors.js.map +1 -0
  223. package/dist/semantic.d.ts +29 -1
  224. package/dist/semantic.d.ts.map +1 -1
  225. package/dist/semantic.js +26 -16
  226. package/dist/semantic.js.map +1 -1
  227. package/dist/telemetry.d.ts +128 -0
  228. package/dist/telemetry.d.ts.map +1 -0
  229. package/dist/telemetry.js +305 -0
  230. package/dist/telemetry.js.map +1 -0
  231. package/dist/tests.d.ts.map +1 -1
  232. package/dist/tests.js +30 -22
  233. package/dist/tests.js.map +1 -1
  234. package/dist/type-guards.d.ts +50 -5
  235. package/dist/type-guards.d.ts.map +1 -1
  236. package/dist/type-guards.js +87 -16
  237. package/dist/type-guards.js.map +1 -1
  238. package/dist/types.d.ts +33 -245
  239. package/dist/types.d.ts.map +1 -1
  240. package/dist/types.js +62 -72
  241. package/dist/types.js.map +1 -1
  242. package/dist/validation.d.ts +2 -5
  243. package/dist/validation.d.ts.map +1 -1
  244. package/dist/validation.js +65 -93
  245. package/dist/validation.js.map +1 -1
  246. package/dist/worker/db-provider.d.ts +168 -0
  247. package/dist/worker/db-provider.d.ts.map +1 -0
  248. package/dist/worker/db-provider.js +277 -0
  249. package/dist/worker/db-provider.js.map +1 -0
  250. package/dist/worker/index.d.ts +35 -0
  251. package/dist/worker/index.d.ts.map +1 -0
  252. package/dist/worker/index.js +37 -0
  253. package/dist/worker/index.js.map +1 -0
  254. package/dist/worker.d.ts +779 -0
  255. package/dist/worker.d.ts.map +1 -0
  256. package/dist/worker.js +2786 -0
  257. package/dist/worker.js.map +1 -0
  258. package/package.json +46 -16
  259. package/src/docs-rels/migrations/0001-init.sql +125 -0
  260. package/LICENSE +0 -21
@@ -6,29 +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`
24
204
  *
25
- * **PLACEHOLDER IMPLEMENTATION**: This function contains hardcoded test values
26
- * and keyword-based generation rules. In a production system, this would be
27
- * replaced with actual AI/LLM integration to generate contextually appropriate
28
- * content. The current implementation is designed to:
29
- * - Provide deterministic, predictable outputs for testing
30
- * - Demonstrate the expected behavior and API contract
31
- * - Allow tests to make specific assertions about generated content
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
+ * ```
32
211
  *
33
212
  * @param fieldName - The name of the field being generated
34
213
  * @param type - The entity type name
@@ -47,180 +226,108 @@ import { resolveNestedPending, prefetchContext, resolveInstructions } from './re
47
226
  * ```
48
227
  */
49
228
  export function generateContextAwareValue(fieldName, type, fullContext, hint, parentData = {}) {
50
- // If parent has the same field, copy its value (for self-referential types like Company.competitor)
51
- const parentValue = parentData[fieldName];
52
- if (typeof parentValue === 'string' && parentValue) {
53
- return parentValue;
54
- }
55
- // If no context provided, fall back to static placeholder
56
- if (!fullContext || fullContext.trim() === '') {
57
- return `Generated ${fieldName} for ${type}`;
58
- }
59
- const contextLower = fullContext.toLowerCase();
60
- const hintLower = (hint || '').toLowerCase();
61
- // For 'name' field, use hint-based generation with keyword matching
62
- if (fieldName === 'name') {
63
- if (hintLower.includes('philosopher') || contextLower.includes('philosopher'))
64
- return 'Aristotle';
65
- if (hintLower.includes('tech entrepreneur') || hintLower.includes('startup'))
66
- return 'Alex Chen';
67
- if (hint && hint.trim())
68
- return `${type}: ${hint}`;
69
- return `Generated ${fieldName} for ${type}`;
70
- }
71
- // For 'style' field
72
- if (fieldName === 'style') {
73
- if (hintLower.includes('energetic') || contextLower.includes('energetic'))
74
- return 'Energetic and engaging presentation style';
75
- if (contextLower.includes('horror') || contextLower.includes('dark'))
76
- return 'Dark and atmospheric horror style';
77
- if (contextLower.includes('sci-fi') || contextLower.includes('futuristic'))
78
- return 'Atmospheric sci-fi suspense style';
79
- return `${fieldName}: ${fullContext}`;
80
- }
81
- // For 'background' field
82
- if (fieldName === 'background') {
83
- if (hintLower.includes('tech entrepreneur') || hintLower.includes('startup'))
84
- return 'Tech startup founder with 10 years experience';
85
- if (hintLower.includes('aristocrat') || hintLower.includes('noble'))
86
- return 'English aristocrat from old noble family';
87
- if (contextLower.includes('renewable') || contextLower.includes('energy'))
88
- return 'Background in renewable energy sector';
89
- return `${fieldName}: ${fullContext}`;
90
- }
91
- // For 'specialty' field
92
- if (fieldName === 'specialty') {
93
- if (contextLower.includes('french') || contextLower.includes('restaurant'))
94
- return 'French classical cuisine';
95
- if (hintLower.includes('security') || contextLower.includes('security'))
96
- return 'Security and authentication systems';
97
- if (hintLower.includes('history') || hintLower.includes('medieval'))
98
- return 'Medieval history specialist';
99
- return `${fieldName}: ${fullContext}`;
100
- }
101
- // For 'training' field
102
- if (fieldName === 'training') {
103
- if (contextLower.includes('french') || contextLower.includes('restaurant'))
104
- return 'Trained in classical French culinary techniques';
105
- return `${fieldName}: ${fullContext}`;
106
- }
107
- // For 'backstory' field
108
- if (fieldName === 'backstory') {
109
- if (contextLower.includes('medieval') || contextLower.includes('fantasy'))
110
- return 'A noble knight who served the King in the great castle, completing many quests across the kingdom';
111
- if (contextLower.includes('sci-fi') || contextLower.includes('space'))
112
- return 'A starship captain with years of deep space exploration';
113
- return `${fieldName}: ${fullContext}`;
114
- }
115
- // For 'headline' field
116
- if (fieldName === 'headline') {
117
- // Check for name mentions in context for personalized headlines
118
- if (contextLower.includes('codehelper'))
119
- return 'CodeHelper: Dev Tools';
120
- if (contextLower.includes('techcorp'))
121
- return 'TechCorp Solutions';
122
- if (contextLower.includes('software engineer'))
123
- return 'For Dev Teams';
124
- if (contextLower.includes('tech') || contextLower.includes('startup'))
125
- return 'Tech Startup Solutions';
126
- return `Headline for ${type}`.slice(0, 30);
127
- }
128
- // For 'copy' field
129
- if (fieldName === 'copy') {
130
- if (contextLower.includes('tech') || contextLower.includes('startup'))
131
- return 'Innovative tech solutions for startups and growing companies';
132
- if (contextLower.includes('marketing') || contextLower.includes('campaign'))
133
- return 'Effective marketing campaign for tech launch';
134
- return `${fieldName}: ${fullContext}`;
135
- }
136
- // For 'tagline' field
137
- if (fieldName === 'tagline') {
138
- if (contextLower.includes('luxury') || contextLower.includes('premium'))
139
- return 'Luxury craftsmanship meets elegant design';
140
- if (contextLower.includes('quality') || contextLower.includes('craftsmanship'))
141
- return 'Premium quality with expert craftsmanship';
142
- if (contextLower.includes('tech'))
143
- return 'Technology for the future';
144
- return `${fieldName}: ${fullContext}`;
145
- }
146
- // For 'description' field
147
- if (fieldName === 'description') {
148
- if (contextLower.includes('cyberpunk') || contextLower.includes('neon') || contextLower.includes('futuristic'))
149
- return 'Cyberpunk character with neural augmentations';
150
- if (contextLower.includes('luxury') || contextLower.includes('high-end') || contextLower.includes('premium'))
151
- return 'A luxury premium product with elegant craftsmanship';
152
- if (contextLower.includes('enterprise') || contextLower.includes('b2b'))
153
- return 'Enterprise solution for business customers';
154
- if (contextLower.includes('nurse') || contextLower.includes('healthcare'))
155
- return 'Healthcare documentation solution for nurses and medical staff';
156
- return `${fieldName}: ${fullContext}`;
157
- }
158
- // For 'abilities' field
159
- if (fieldName === 'abilities') {
160
- if (contextLower.includes('cyberpunk') || contextLower.includes('futuristic'))
161
- return 'Neural hacking and digital infiltration';
162
- return `${fieldName}: ${fullContext}`;
163
- }
164
- // For 'method' field
165
- if (fieldName === 'method') {
166
- if (hintLower.includes('wit') || hintLower.includes('sharp'))
167
- return 'Brilliant deduction and clever observation';
168
- return `${fieldName}: ${fullContext}`;
169
- }
170
- // For 'expertise' field
171
- if (fieldName === 'expertise') {
172
- if (contextLower.includes('machine learning') || contextLower.includes('medical') || contextLower.includes('ai'))
173
- return 'Machine learning for medical applications';
174
- if (hintLower.includes('physics') || hintLower.includes('professor'))
175
- return 'Physics professor specializing in quantum mechanics';
176
- if (hintLower.includes('journalist') || hintLower.includes('science'))
177
- return 'Science journalist covering physics research';
178
- return `${fieldName}: ${fullContext}`;
179
- }
180
- // For 'focus' field
181
- if (fieldName === 'focus') {
182
- if (contextLower.includes('renewable') || contextLower.includes('energy') || contextLower.includes('green'))
183
- return 'Focus on sustainable energy transformation';
184
- if (contextLower.includes('tech') || contextLower.includes('programming'))
185
- return 'Focus on technical programming topics';
186
- return `${fieldName}: ${fullContext}`;
187
- }
188
- // For 'qualifications' field
189
- if (fieldName === 'qualifications') {
190
- if (contextLower.includes('astrophysics') || contextLower.includes('astronomy') || contextLower.includes('space'))
191
- return 'PhD in Astrophysics from MIT';
192
- return `${fieldName}: ${fullContext}`;
193
- }
194
- // For 'teachingStyle' field
195
- if (fieldName === 'teachingStyle') {
196
- if (contextLower.includes('beginner') || contextLower.includes('introduct'))
197
- return 'Patient and accessible approach for beginners';
198
- return `${fieldName}: ${fullContext}`;
199
- }
200
- // For 'experience' field
201
- if (fieldName === 'experience') {
202
- if (contextLower.includes('horror') || contextLower.includes('film'))
203
- return 'Experience in horror film production';
204
- return `${fieldName}: ${fullContext}`;
205
- }
206
- // For 'role' field
207
- if (fieldName === 'role') {
208
- if (hintLower.includes('research') || hintLower.includes('machine learning') || hintLower.includes('phd'))
209
- return 'Machine learning researcher';
210
- return `${fieldName}: ${fullContext}`;
211
- }
212
- // For 'portfolio' field
213
- if (fieldName === 'portfolio') {
214
- if (hintLower.includes('award') || hintLower.includes('beaux-arts') || hintLower.includes('ecole'))
215
- return 'Award-winning design portfolio from Beaux-Arts';
216
- return `${fieldName}: ${fullContext}`;
217
- }
218
- // Default: include context in the generated value
219
- 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
+ });
220
252
  }
221
253
  // =============================================================================
222
254
  // AI Field Generation
223
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
+ }
224
331
  /**
225
332
  * Generate AI fields based on $instructions and field prompts
226
333
  *
@@ -232,13 +339,15 @@ export function generateContextAwareValue(fieldName, type, fullContext, hint, pa
232
339
  * @param entityDef - The parsed entity definition
233
340
  * @param schema - The parsed schema
234
341
  * @param provider - The database provider
342
+ * @param injectedConfig - Optional AI config to use instead of module-level config (for DI)
235
343
  * @returns The data with AI-generated fields populated
236
344
  */
237
- 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;
238
347
  const result = { ...data };
239
348
  const entitySchema = entityDef.schema || {};
240
- const instructions = entitySchema.$instructions;
241
- const contextDeps = entitySchema.$context;
349
+ const instructions = entitySchema['$instructions'];
350
+ const contextDeps = entitySchema['$context'];
242
351
  // Pre-fetch context dependencies if declared
243
352
  let contextData = new Map();
244
353
  if (contextDeps && Array.isArray(contextDeps)) {
@@ -247,54 +356,91 @@ export async function generateAIFields(data, typeName, entityDef, schema, provid
247
356
  // Resolve instructions template variables
248
357
  let resolvedInstructions = instructions;
249
358
  if (instructions) {
250
- // Build a combined entity with context data for template resolution
251
- const combinedEntity = { ...result };
252
- for (const [key, value] of contextData) {
253
- const topLevelKey = key.split('.')[0];
254
- if (!combinedEntity[topLevelKey]) {
255
- combinedEntity[topLevelKey] = value;
256
- }
257
- }
359
+ const combinedEntity = buildCombinedEntityWithContext(result, contextData);
258
360
  resolvedInstructions = await resolveInstructions(instructions, combinedEntity, typeName, schema, provider);
259
361
  }
260
- // Build context string from resolved instructions and entity data
261
- const contextParts = [];
262
- if (resolvedInstructions)
263
- contextParts.push(resolvedInstructions);
264
- // Add relevant entity data as context
265
- for (const [key, value] of Object.entries(result)) {
266
- if (!key.startsWith('$') && !key.startsWith('_') && typeof value === 'string' && value) {
267
- 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 });
268
390
  }
269
391
  }
270
- // Add context from pre-fetched entities
271
- for (const [key, ctxEntity] of contextData) {
272
- for (const [fieldName, fieldValue] of Object.entries(ctxEntity)) {
273
- if (!fieldName.startsWith('$') && !fieldName.startsWith('_') && typeof fieldValue === 'string' && fieldValue) {
274
- 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
+ }
275
419
  }
276
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
+ }
277
426
  }
278
- const fullContext = contextParts.join(' | ');
279
- // Generate values for prompt fields that don't have values
280
- for (const [fieldName, field] of entityDef.fields) {
281
- // Skip if value already provided
282
- if (result[fieldName] !== undefined && result[fieldName] !== null)
283
- continue;
284
- // Skip relation fields (handled separately)
285
- if (field.isRelation)
286
- continue;
287
- // Check if this is a prompt field (type contains spaces) or needs generation
288
- const fieldDef = entitySchema[fieldName];
289
- const isPrompt = typeof fieldDef === 'string' && fieldDef.includes(' ') && !fieldDef.includes('->');
290
- if (isPrompt || (field.type === 'string' && !isPrimitiveType(field.type))) {
291
- // Use the field definition as the prompt
292
- 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) {
293
430
  result[fieldName] = generateContextAwareValue(fieldName, typeName, fullContext, prompt, result);
294
431
  }
295
- else if (field.type === 'string' && instructions) {
296
- // Generate string fields when we have $instructions context
297
- 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
+ }
298
444
  }
299
445
  }
300
446
  return result;
@@ -302,27 +448,87 @@ export async function generateAIFields(data, typeName, entityDef, schema, provid
302
448
  // =============================================================================
303
449
  // Entity Generation
304
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
+ }
305
482
  /**
306
483
  * Generate an entity based on its type and context
307
484
  *
308
- * For testing, generates deterministic content based on the prompt and type.
309
- * 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.
310
487
  *
311
488
  * @param type - The type of entity to generate
312
489
  * @param prompt - Optional prompt for generation context
313
490
  * @param context - Parent context information (parent type name, parentData, and optional parentId)
314
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)
315
494
  */
316
- 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
+ }
317
500
  const entity = schema.entities.get(type);
318
501
  if (!entity)
319
502
  throw new Error(`Unknown type: ${type}`);
320
503
  // Gather context for generation
321
504
  const parentEntity = schema.entities.get(context.parent);
322
505
  const parentSchema = parentEntity?.schema || {};
323
- const instructions = parentSchema.$instructions;
324
- const schemaContext = parentSchema.$context;
325
- // 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
326
532
  const parentContextFields = [];
327
533
  for (const [key, value] of Object.entries(context.parentData)) {
328
534
  if (!key.startsWith('$') && !key.startsWith('_') && typeof value === 'string' && value) {
@@ -343,35 +549,21 @@ export async function generateEntity(type, prompt, context, schema) {
343
549
  const data = {};
344
550
  for (const [fieldName, field] of entity.fields) {
345
551
  if (!field.isRelation) {
346
- if (field.type === 'string') {
347
- // Generate context-aware content
348
- data[fieldName] = generateContextAwareValue(fieldName, type, fullContext, prompt, context.parentData);
349
- }
350
- else if (field.isArray && field.type === 'string') {
351
- // Generate array of strings
352
- data[fieldName] = [generateContextAwareValue(fieldName, type, fullContext, prompt, context.parentData)];
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 === 'backward') {
356
- // Backward relation to parent - set the parent's ID if this entity's
357
- // related type matches the parent type
358
- if (field.relatedType === context.parent && context.parentId) {
359
- // Store the parent ID directly - this is a reference back to the parent
360
- data[fieldName] = context.parentId;
361
- }
362
- }
363
- else if (field.operator === '->' && field.direction === 'forward') {
364
- // Recursively generate nested forward exact relations
365
- // This handles cases like Person.bio -> Bio
366
- if (!field.isOptional) {
367
- const nestedGenerated = await generateEntity(field.relatedType, field.prompt, { parent: type, parentData: data }, schema);
368
- // We need to create the nested entity too, but we can't do that here
369
- // because we don't have access to the provider yet.
370
- // This will be handled by resolveForwardExact when it calls us
371
- 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
+ ];
372
562
  }
373
563
  }
374
564
  }
565
+ // Process relation fields using shared helper
566
+ await processRelationFields(data, entity, type, context, schema, _depth, injectedConfig);
375
567
  return data;
376
568
  }
377
569
  // =============================================================================
@@ -441,11 +633,11 @@ export async function resolveForwardExact(typeName, data, entity, schema, provid
441
633
  // - For union types, always allow generation (we have explicit type to generate)
442
634
  const hasUnionTypes = field.unionTypes && field.unionTypes.length > 0;
443
635
  const shouldSkip = hasBackwardRef && hasRequiredScalarFields && !hasUnionTypes;
444
- const canGenerate = !shouldSkip && (hasBackwardRef || // Symmetric ref without required scalars
445
- field.prompt || // Has a generation prompt
446
- !hasRequiredScalarFields || // No required fields to worry about
447
- hasUnionTypes // Union types should generate the first type
448
- );
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
449
641
  if (!canGenerate)
450
642
  continue;
451
643
  const generated = await generateEntity(generateType, field.prompt, { parent: typeName, parentData: data, parentId }, schema);
@@ -453,23 +645,26 @@ export async function resolveForwardExact(typeName, data, entity, schema, provid
453
645
  const resolvedGenerated = await resolveNestedPending(generated, relatedEntity, schema, provider);
454
646
  const created = await provider.create(generateType, undefined, {
455
647
  ...resolvedGenerated,
456
- $matchedType: generateType
648
+ $matchedType: generateType,
457
649
  });
458
- resolved[fieldName] = [created.$id];
650
+ resolved[fieldName] = [created['$id']];
459
651
  resolved[`${fieldName}$matchedType`] = generateType;
460
652
  // Queue relationship creation for after parent entity is created
461
- pendingRelations.push({ fieldName, targetType: generateType, targetId: created.$id });
653
+ pendingRelations.push({
654
+ fieldName,
655
+ targetType: generateType,
656
+ targetId: created['$id'],
657
+ });
462
658
  }
463
659
  else {
464
660
  // Single non-optional forward relation - generate the related entity
465
- // Generate single entity
466
661
  const generated = await generateEntity(field.relatedType, field.prompt, { parent: typeName, parentData: data, parentId }, schema);
467
662
  // Resolve any pending nested relations in the generated data
468
663
  const relatedEntity = schema.entities.get(field.relatedType);
469
664
  if (relatedEntity) {
470
665
  const resolvedGenerated = await resolveNestedPending(generated, relatedEntity, schema, provider);
471
666
  const created = await provider.create(field.relatedType, undefined, resolvedGenerated);
472
- resolved[fieldName] = created.$id;
667
+ resolved[fieldName] = created['$id'];
473
668
  }
474
669
  }
475
670
  }
@@ -491,10 +686,10 @@ export function generateNaturalLanguageContent(fieldName, prompt, targetType, co
491
686
  // Extract key words from prompt for natural language
492
687
  const keyWords = prompt.toLowerCase();
493
688
  if (keyWords.includes('idea') || keyWords.includes('concept')) {
494
- return `A innovative idea for ${context.name || targetType}`;
689
+ return `A innovative idea for ${context['name'] || targetType}`;
495
690
  }
496
691
  if (keyWords.includes('customer') || keyWords.includes('buyer') || keyWords.includes('user')) {
497
- return `The target customer segment for ${context.name || targetType}`;
692
+ return `The target customer segment for ${context['name'] || targetType}`;
498
693
  }
499
694
  if (keyWords.includes('related') || keyWords.includes('similar')) {
500
695
  return `Related ${targetType.toLowerCase()} content`;
@@ -506,37 +701,41 @@ export function generateNaturalLanguageContent(fieldName, prompt, targetType, co
506
701
  // Generate based on field name patterns
507
702
  const fieldLower = fieldName.toLowerCase();
508
703
  if (fieldLower.includes('idea')) {
509
- return `A compelling idea for ${context.name || 'the project'}`;
704
+ return `A compelling idea for ${context['name'] || 'the project'}`;
510
705
  }
511
706
  if (fieldLower.includes('customer')) {
512
- return `The ideal customer for ${context.name || 'the business'}`;
707
+ return `The ideal customer for ${context['name'] || 'the business'}`;
513
708
  }
514
- 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')) {
515
714
  return `A qualified ${fieldName} candidate`;
516
715
  }
517
716
  if (fieldLower.includes('author') || fieldLower.includes('reviewer')) {
518
717
  return `An experienced ${fieldName}`;
519
718
  }
520
719
  if (fieldLower.includes('assignee') || fieldLower.includes('owner')) {
521
- return `The right person for ${context.title || context.name || 'this task'}`;
720
+ return `The right person for ${context['title'] || context['name'] || 'this task'}`;
522
721
  }
523
722
  if (fieldLower.includes('department') || fieldLower.includes('team')) {
524
- return `A department for ${context.name || 'the organization'}`;
723
+ return `A department for ${context['name'] || 'the organization'}`;
525
724
  }
526
725
  if (fieldLower.includes('client') || fieldLower.includes('sponsor')) {
527
- return `A ${fieldName} for ${context.name || context.title || 'the project'}`;
726
+ return `A ${fieldName} for ${context['name'] || context['title'] || 'the project'}`;
528
727
  }
529
728
  if (fieldLower.includes('item') || fieldLower.includes('component')) {
530
729
  return `${targetType} component`;
531
730
  }
532
731
  if (fieldLower.includes('member') || fieldLower.includes('project')) {
533
- return `${targetType} for ${context.name || 'the team'}`;
732
+ return `${targetType} for ${context['name'] || 'the team'}`;
534
733
  }
535
734
  if (fieldLower.includes('character')) {
536
- return `A character for ${context.title || context.name || 'the story'}`;
735
+ return `A character for ${context['title'] || context['name'] || 'the story'}`;
537
736
  }
538
737
  if (fieldLower.includes('setting') || fieldLower.includes('location')) {
539
- return `A setting for ${context.title || context.name || 'the story'}`;
738
+ return `A setting for ${context['title'] || context['name'] || 'the story'}`;
540
739
  }
541
740
  if (fieldLower.includes('address')) {
542
741
  return `Address information`;