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
package/dist/schema.js CHANGED
@@ -1,17 +1,8 @@
1
1
  /**
2
2
  * Schema-first Database Definition
3
3
  *
4
- * This file re-exports all schema functionality from the modular `./schema/` directory.
5
- * It serves as the main entry point for backwards compatibility.
6
- *
7
- * The actual implementation is split into smaller modules:
8
- * - schema/types.ts - TypeScript types/interfaces
9
- * - schema/parse.ts - Schema parsing logic
10
- * - schema/provider.ts - Database provider interface and resolution
11
- * - schema/resolve.ts - Resolution functions for entity hydration
12
- * - schema/cascade.ts - Cascade generation and context-aware value generation
13
- * - schema/semantic.ts - Fuzzy/semantic resolution functions
14
- * - schema/index.ts - Main factory and entity operations
4
+ * Declarative schema with automatic bi-directional relationships.
5
+ * Uses mdxld conventions for entity structure.
15
6
  *
16
7
  * @example
17
8
  * ```ts
@@ -36,51 +27,2872 @@
36
27
  * post.author // Author (single)
37
28
  * post.tags // Tag[] (array)
38
29
  * ```
39
- *
40
- * @packageDocumentation
41
30
  */
31
+ import { DBPromise, wrapEntityOperations, setSchemaRelationInfo, } from './ai-promise-db.js';
32
+ import { cosineSimilarity, computeRRF, extractEmbeddableText, generateContentHash, } from './semantic.js';
33
+ import { isDraft, extractRefs, hasFactoryFunction, isPlainObject, getSchemaMetadata, } from './type-guards.js';
34
+ import { createEventBridge } from './eventbridge.js';
35
+ import { loadEntity } from './dataloader.js';
36
+ import { logWarn } from './logger.js';
42
37
  export { toExpanded, toFlat, Verbs, resolveUrl, resolveShortUrl, parseUrl } from './types.js';
38
+ // Re-export from schema/ modules (only items not defined locally in this file)
39
+ export {
40
+ // AI Generation configuration
41
+ configureAIGeneration, getAIGenerationConfig,
42
+ // Cascade functions
43
+ setValueGenerator, getValueGenerator,
44
+ // Entity operations
45
+ createEntityOperations, createEdgeEntityOperations,
46
+ // Verb derivation
47
+ FORWARD_TO_REVERSE, BIDIRECTIONAL_PAIRS, deriveReverseVerb, fieldNameToVerb, isPassiveVerb, registerVerbPair, registerBidirectionalPair, registerFieldVerb,
48
+ // Provider type guards
49
+ hasSemanticSearch, hasHybridSearch, hasEventsAPI, hasActionsAPI, hasArtifactsAPI, hasEmbeddingsConfig,
50
+ // Schema versioning
51
+ computeSchemaHash, getSchemaVersion, setSchemaVersion, hasSchemaChanged,
52
+ // Schema diff
53
+ diffSchemas, describeDiff,
54
+ // Migrations
55
+ defineMigration, runMigrations, getPendingMigrations, rollbackLastMigration,
56
+ // Parse functions - delegated to schema/parse.ts
57
+ parseOperator, parseField, parseSchema, isPrimitiveType, } from './schema/index.js';
58
+ // Import generateAIFields and generateEntity directly from cascade.js to avoid potential circular dependency
59
+ import { generateAIFields, generateEntity as cascadeGenerateEntity, generateContextAwareValue, generateNaturalLanguageContent, DEFAULT_MAX_DEPTH, } from './schema/cascade.js';
60
+ // Import entity operation factories for internal use
61
+ import { createEdgeEntityOperations, createNounEntityOperations, createVerbEntityOperations, } from './schema/entity-operations.js';
62
+ // Import type guards for internal use
63
+ import { hasSemanticSearch, hasHybridSearch, hasEventsAPI, hasActionsAPI, hasArtifactsAPI, hasEmbeddingsConfig, isPromptField, } from './schema/index.js';
64
+ // Import parse functions for internal use (DB() factory needs parseSchema)
65
+ import { parseSchema, parseOperator, parseField, isPrimitiveType } from './schema/parse.js';
66
+ // Import extracted DB() helper modules
67
+ import { addSystemEntities, SYSTEM_ENTITY_NAMES } from './schema/system-entities.js';
68
+ import { buildDefinitionCaches } from './schema/definition-caches.js';
69
+ import { createEventsAPI, createActionsPublicAPI, createArtifactsAPI, createNounsAPI, createVerbsAPI, } from './schema/sub-apis.js';
70
+ // Import module-level state setters for bridging state between schema.ts and schema/ modules
71
+ import { setProvider as setModuleProvider } from './schema/provider.js';
72
+ import { setNLQueryGenerator as setModuleNLQueryGenerator } from './schema/nl-query.js';
73
+ // Import error handling utilities
74
+ import { isNotFoundError, isEntityExistsError, wrapDatabaseError, DatabaseError, CapabilityNotSupportedError, } from './errors.js';
75
+ // Re-export linguistic utilities from linguistic.ts
76
+ export { conjugate, pluralize, singularize, inferNoun, createTypeMeta, getTypeMeta, Type, getVerbFields, } from './linguistic.js';
77
+ import { Verbs } from './types.js';
78
+ import { inferNoun, getTypeMeta, conjugate } from './linguistic.js';
79
+ // Note: EntityOperationsMap type removed - using inline type with explanation below
80
+ /**
81
+ * Create a Noun definition with type inference
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const Post = defineNoun({
86
+ * singular: 'post',
87
+ * plural: 'posts',
88
+ * description: 'A blog post',
89
+ * properties: {
90
+ * title: { type: 'string', description: 'Post title' },
91
+ * content: { type: 'markdown' },
92
+ * },
93
+ * relationships: {
94
+ * author: { type: 'Author', backref: 'posts' },
95
+ * },
96
+ * })
97
+ * ```
98
+ */
99
+ export function defineNoun(noun) {
100
+ return noun;
101
+ }
102
+ /**
103
+ * Create a Verb definition with type inference
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * const publish = defineVerb({
108
+ * action: 'publish',
109
+ * actor: 'publisher',
110
+ * act: 'publishes',
111
+ * activity: 'publishing',
112
+ * result: 'publication',
113
+ * reverse: { at: 'publishedAt', by: 'publishedBy' },
114
+ * inverse: 'unpublish',
115
+ * })
116
+ * ```
117
+ */
118
+ export function defineVerb(verb) {
119
+ return verb;
120
+ }
121
+ /**
122
+ * Convert a Noun to an EntitySchema for use with DB()
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * const postNoun = defineNoun({
127
+ * singular: 'post',
128
+ * plural: 'posts',
129
+ * properties: { title: { type: 'string' } },
130
+ * relationships: { author: { type: 'Author', backref: 'posts' } },
131
+ * })
132
+ *
133
+ * const db = DB({
134
+ * Post: nounToSchema(postNoun),
135
+ * })
136
+ * ```
137
+ */
138
+ export function nounToSchema(noun) {
139
+ const schema = {};
140
+ // Add properties
141
+ if (noun.properties) {
142
+ for (const [name, prop] of Object.entries(noun.properties)) {
143
+ let type = prop.type;
144
+ if (prop.array)
145
+ type = `${type}[]`;
146
+ if (prop.optional)
147
+ type = `${type}?`;
148
+ schema[name] = type;
149
+ }
150
+ }
151
+ // Add relationships
152
+ if (noun.relationships) {
153
+ for (const [name, rel] of Object.entries(noun.relationships)) {
154
+ const baseType = rel.type.replace('[]', '');
155
+ const isArray = rel.type.endsWith('[]');
156
+ if (rel.backref) {
157
+ schema[name] = isArray ? [`${baseType}.${rel.backref}`] : `${baseType}.${rel.backref}`;
158
+ }
159
+ else {
160
+ schema[name] = rel.type;
161
+ }
162
+ }
163
+ }
164
+ return schema;
165
+ }
43
166
  // =============================================================================
44
- // Re-exports from linguistic.ts
167
+ // Built-in Schema Types - Self-Describing Database
45
168
  // =============================================================================
46
- export { conjugate, pluralize, singularize, inferNoun, createTypeMeta, getTypeMeta, Type, getVerbFields, } from './linguistic.js';
169
+ /**
170
+ * Built-in Thing schema - base type for all entities
171
+ *
172
+ * Every entity instance is a Thing with a relationship to its Noun.
173
+ * This creates a complete graph: Thing.type -> Noun.things
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * // Every post instance:
178
+ * post.$type // 'Post' (string)
179
+ * post.type // -> Noun('Post') (relationship)
180
+ *
181
+ * // From Noun, get all instances:
182
+ * const postNoun = await db.Noun.get('Post')
183
+ * const allPosts = await postNoun.things // -> Post[]
184
+ * ```
185
+ */
186
+ export const ThingSchema = {
187
+ // Every Thing has a type that links to its Noun
188
+ type: 'Noun.things', // Thing.type -> Noun, Noun.things -> Thing[]
189
+ };
190
+ /**
191
+ * Built-in Noun schema for storing type definitions
192
+ *
193
+ * Every Type/Collection automatically gets a Noun record stored in the database.
194
+ * This enables introspection and self-describing schemas.
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * // When you define:
199
+ * const db = DB({ Post: { title: 'string' } })
200
+ *
201
+ * // The database auto-creates:
202
+ * // db.Noun.get('Post') => { singular: 'post', plural: 'posts', ... }
203
+ *
204
+ * // Query all types:
205
+ * const types = await db.Noun.list()
206
+ *
207
+ * // Get all instances of a type:
208
+ * const postNoun = await db.Noun.get('Post')
209
+ * const allPosts = await postNoun.things
210
+ *
211
+ * // Listen for new types:
212
+ * on.Noun.created(noun => console.log(`New type: ${noun.name}`))
213
+ * ```
214
+ */
215
+ export const NounSchema = {
216
+ // Identity
217
+ name: 'string', // 'Post', 'BlogPost'
218
+ singular: 'string', // 'post', 'blog post'
219
+ plural: 'string', // 'posts', 'blog posts'
220
+ slug: 'string', // 'post', 'blog-post'
221
+ slugPlural: 'string', // 'posts', 'blog-posts'
222
+ description: 'string?', // Human description
223
+ // Schema
224
+ properties: 'json?', // Property definitions
225
+ relationships: 'json?', // Relationship definitions
226
+ // Behavior
227
+ actions: 'json?', // Available actions (verbs)
228
+ events: 'json?', // Event types
229
+ // Metadata
230
+ metadata: 'json?', // Additional metadata
231
+ // Relationships - auto-created by bi-directional system
232
+ // things: Thing[] // All instances of this type (backref from Thing.type)
233
+ };
234
+ /**
235
+ * Built-in Verb schema for storing action definitions
236
+ */
237
+ export const VerbSchema = {
238
+ action: 'string', // 'create', 'publish'
239
+ actor: 'string?', // 'creator', 'publisher'
240
+ act: 'string?', // 'creates', 'publishes'
241
+ activity: 'string?', // 'creating', 'publishing'
242
+ result: 'string?', // 'creation', 'publication'
243
+ reverse: 'json?', // { at, by, in, for }
244
+ inverse: 'string?', // 'delete', 'unpublish'
245
+ description: 'string?',
246
+ };
247
+ /**
248
+ * Built-in Edge schema for relationships between types
249
+ *
250
+ * Every relationship in a schema creates an Edge record.
251
+ * This enables graph queries across the type system.
252
+ *
253
+ * @example
254
+ * ```ts
255
+ * // Post.author -> Author creates:
256
+ * // Edge { from: 'Post', name: 'author', to: 'Author', backref: 'posts', cardinality: 'many-to-one' }
257
+ *
258
+ * // Query the graph:
259
+ * const edges = await db.Edge.find({ to: 'Author' })
260
+ * // => [{ from: 'Post', name: 'author' }, { from: 'Comment', name: 'author' }]
261
+ *
262
+ * // What types reference Author?
263
+ * const referencing = edges.map(e => e.from) // ['Post', 'Comment']
264
+ * ```
265
+ */
266
+ export const EdgeSchema = {
267
+ from: 'string', // Source type: 'Post'
268
+ name: 'string', // Field name: 'author'
269
+ to: 'string', // Target type: 'Author'
270
+ backref: 'string?', // Inverse field: 'posts'
271
+ cardinality: 'string', // 'one-to-one', 'one-to-many', 'many-to-one', 'many-to-many'
272
+ direction: 'string', // 'forward' | 'backward'
273
+ matchMode: 'string?', // 'exact' | 'fuzzy'
274
+ required: 'boolean?', // Is this relationship required?
275
+ description: 'string?', // Human description
276
+ };
277
+ /**
278
+ * System types that are auto-created in every database
279
+ *
280
+ * The graph structure:
281
+ * - Thing.type -> Noun (every instance links to its type)
282
+ * - Noun.things -> Thing[] (every type has its instances)
283
+ * - Edge connects Nouns (relationships between types)
284
+ * - Verb describes actions on Nouns
285
+ */
286
+ export const SystemSchema = {
287
+ Thing: ThingSchema,
288
+ Noun: NounSchema,
289
+ Verb: VerbSchema,
290
+ Edge: EdgeSchema,
291
+ };
292
+ /**
293
+ * Create Edge records from schema relationships
294
+ *
295
+ * @internal Used by DB() to auto-populate Edge records
296
+ *
297
+ * For backward edges (direction === 'backward'), the from/to are inverted:
298
+ * - Forward: from = typeName, to = relatedType
299
+ * - Backward: from = relatedType, to = typeName
300
+ *
301
+ * This enables proper graph traversal where backward edges represent
302
+ * "pointing to" relationships (e.g., Post.comments -> Comments that point TO Post)
303
+ */
304
+ export function createEdgeRecords(typeName, schema, parsedEntity) {
305
+ const edges = [];
306
+ for (const [fieldName, field] of parsedEntity.fields) {
307
+ if (field.isRelation && field.relatedType) {
308
+ const direction = field.direction ?? 'forward';
309
+ const matchMode = field.matchMode ?? 'exact';
310
+ // For backward edges, invert from/to and adjust cardinality
311
+ const isBackward = direction === 'backward';
312
+ const from = isBackward ? field.relatedType : typeName;
313
+ const to = isBackward ? typeName : field.relatedType;
314
+ // Cardinality from the perspective of the field definition
315
+ // - Array with backref = many-to-many (Post.tags <-> Tag.posts)
316
+ // - Array without backref = one-to-many (one source points to many targets)
317
+ // - Single = many-to-one (many sources point to one target)
318
+ // The 'one-to-one' case is rare and typically requires explicit constraint
319
+ let cardinality;
320
+ if (field.isArray) {
321
+ cardinality = field.backref ? 'many-to-many' : 'one-to-many';
322
+ }
323
+ else {
324
+ // Single reference: by default many-to-one (many posts -> one author)
325
+ cardinality = 'many-to-one';
326
+ }
327
+ edges.push({
328
+ from,
329
+ name: fieldName,
330
+ to,
331
+ backref: field.backref,
332
+ cardinality,
333
+ direction,
334
+ matchMode,
335
+ });
336
+ }
337
+ }
338
+ return edges;
339
+ }
340
+ /**
341
+ * Create a Noun record from a type name and optional schema
342
+ *
343
+ * @internal Used by DB() to auto-populate Noun records
344
+ */
345
+ export function createNounRecord(typeName, schema, nounDef) {
346
+ const meta = getTypeMeta(typeName);
347
+ const inferred = inferNoun(typeName);
348
+ return {
349
+ name: typeName,
350
+ singular: nounDef?.singular ?? meta.singular,
351
+ plural: nounDef?.plural ?? meta.plural,
352
+ slug: meta.slug,
353
+ slugPlural: meta.slugPlural,
354
+ description: nounDef?.description,
355
+ properties: nounDef?.properties ?? (schema ? schemaToProperties(schema) : undefined),
356
+ relationships: nounDef?.relationships,
357
+ actions: nounDef?.actions ?? inferred.actions,
358
+ events: nounDef?.events ?? inferred.events,
359
+ metadata: nounDef?.metadata,
360
+ };
361
+ }
362
+ /**
363
+ * Convert EntitySchema to NounProperty format
364
+ */
365
+ function schemaToProperties(schema) {
366
+ const properties = {};
367
+ for (const [name, def] of Object.entries(schema)) {
368
+ // Skip metadata fields (prefixed with $) like $context, $instructions
369
+ if (name.startsWith('$'))
370
+ continue;
371
+ // Skip if definition is invalid (null, undefined)
372
+ if (!def)
373
+ continue;
374
+ const defStr = Array.isArray(def) ? def[0] : def;
375
+ const isOptional = defStr.endsWith('?');
376
+ const isArray = defStr.endsWith('[]') || Array.isArray(def);
377
+ const baseType = defStr.replace(/[\?\[\]]/g, '').split('.')[0];
378
+ properties[name] = {
379
+ type: baseType,
380
+ optional: isOptional,
381
+ array: isArray,
382
+ };
383
+ }
384
+ return properties;
385
+ }
47
386
  // =============================================================================
48
- // Re-exports from schema/parse.ts
387
+ // Natural Language Query Implementation
49
388
  // =============================================================================
50
- export { parseOperator, parseField, parseSchema, isPrimitiveType } from './schema/parse.js';
51
- export { setProvider, resolveProvider } from './schema/provider.js';
389
+ // NOTE: NLQueryGenerator, NLQueryContext, NLQueryPlan types are imported from ./schema/types.js
390
+ // and re-exported via ./schema/index.js for the public API
391
+ // The functions below maintain local state and bridge to the module state in schema/nl-query.ts
392
+ let nlQueryGenerator = null;
393
+ /**
394
+ * Set the AI generator for natural language queries
395
+ *
396
+ * @example
397
+ * ```ts
398
+ * import { generate } from 'ai-functions'
399
+ *
400
+ * setNLQueryGenerator(async (prompt, context) => {
401
+ * return generate({
402
+ * prompt: `Given this schema: ${JSON.stringify(context.types)}
403
+ * Answer this question: ${prompt}
404
+ * Return a query plan as JSON.`,
405
+ * schema: NLQueryPlanSchema
406
+ * })
407
+ * })
408
+ * ```
409
+ */
410
+ export function setNLQueryGenerator(generator) {
411
+ nlQueryGenerator = generator;
412
+ // Bridge to schema/nl-query.ts module state so extracted modules share the same generator
413
+ setModuleNLQueryGenerator(generator);
414
+ }
415
+ /**
416
+ * Get the currently configured NL query generator
417
+ */
418
+ export function getNLQueryGenerator() {
419
+ return nlQueryGenerator;
420
+ }
421
+ /**
422
+ * Build schema context for NL queries
423
+ */
424
+ export function buildNLQueryContext(schema, targetType) {
425
+ const types = [];
426
+ for (const [name, entity] of schema.entities) {
427
+ const fields = [];
428
+ const relationships = [];
429
+ for (const [fieldName, field] of entity.fields) {
430
+ if (field.isRelation && field.relatedType) {
431
+ relationships.push({
432
+ name: fieldName,
433
+ to: field.relatedType,
434
+ cardinality: field.isArray ? 'many' : 'one',
435
+ });
436
+ }
437
+ else {
438
+ fields.push(fieldName);
439
+ }
440
+ }
441
+ const meta = getTypeMeta(name);
442
+ types.push({
443
+ name,
444
+ singular: meta.singular,
445
+ plural: meta.plural,
446
+ fields,
447
+ relationships,
448
+ });
449
+ }
450
+ return { types, ...(targetType !== undefined && { targetType }) };
451
+ }
452
+ /**
453
+ * Execute a natural language query
454
+ */
455
+ export async function executeNLQuery(question, schema, targetType) {
456
+ // Import applyFilters for MongoDB-style filter support
457
+ const { applyFilters } = await import('./schema/nl-query-generator.js');
458
+ // If no AI generator configured, fall back to search
459
+ if (!nlQueryGenerator) {
460
+ const provider = await resolveProvider();
461
+ const results = [];
462
+ // Simple heuristic for common "list all" patterns in fallback mode
463
+ const lowerQuestion = question.toLowerCase().trim();
464
+ const isListAllQuery = /^(show|list|get|find|display)\s+(all|every|the)?\s*/i.test(lowerQuestion) ||
465
+ lowerQuestion === '' ||
466
+ /\ball\b/i.test(lowerQuestion);
467
+ if (targetType) {
468
+ if (isListAllQuery) {
469
+ const listResults = await provider.list(targetType);
470
+ results.push(...listResults);
471
+ }
472
+ else {
473
+ const searchResults = await provider.search(targetType, question);
474
+ results.push(...searchResults);
475
+ }
476
+ }
477
+ else {
478
+ for (const [typeName] of schema.entities) {
479
+ if (isListAllQuery) {
480
+ const listResults = await provider.list(typeName);
481
+ results.push(...listResults);
482
+ }
483
+ else {
484
+ const searchResults = await provider.search(typeName, question);
485
+ results.push(...searchResults);
486
+ }
487
+ }
488
+ }
489
+ return {
490
+ interpretation: `Search for "${question}"`,
491
+ confidence: 0.5,
492
+ results,
493
+ explanation: 'Fallback to keyword search (no AI generator configured)',
494
+ };
495
+ }
496
+ // Build context and get AI-generated query plan
497
+ const context = buildNLQueryContext(schema, targetType);
498
+ const plan = await nlQueryGenerator(question, context);
499
+ // Execute the plan
500
+ const provider = await resolveProvider();
501
+ let results = [];
502
+ for (const typeName of plan.types) {
503
+ let typeResults;
504
+ if (plan.search) {
505
+ typeResults = await provider.search(typeName, plan.search);
506
+ }
507
+ else {
508
+ typeResults = await provider.list(typeName);
509
+ }
510
+ // Apply MongoDB-style filters in memory
511
+ if (plan.filters && Object.keys(plan.filters).length > 0) {
512
+ typeResults = applyFilters(typeResults, plan.filters);
513
+ }
514
+ results.push(...typeResults);
515
+ }
516
+ return {
517
+ interpretation: plan.interpretation,
518
+ confidence: plan.confidence,
519
+ results,
520
+ query: JSON.stringify({ types: plan.types, filters: plan.filters, search: plan.search }),
521
+ };
522
+ }
523
+ /**
524
+ * Create a natural language query function for a specific type
525
+ */
526
+ export function createNLQueryFn(schema, typeName) {
527
+ return async (strings, ...values) => {
528
+ // Interpolate the template
529
+ const question = strings.reduce((acc, str, i) => {
530
+ return acc + str + (values[i] !== undefined ? String(values[i]) : '');
531
+ }, '');
532
+ return executeNLQuery(question, schema, typeName);
533
+ };
534
+ }
52
535
  // =============================================================================
53
- // Re-exports from schema/resolve.ts
536
+ // Provider Interface - Delegated to schema/provider.ts
54
537
  // =============================================================================
55
- export { isEntityId, inferTypeFromField, resolveContextPath, resolveInstructions, prefetchContext, isPromptField, resolveNestedPending, resolveReferenceSpec, hydrateEntity, } from './schema/resolve.js';
538
+ // NOTE: DBProvider interface is imported from ./schema/provider.js for internal use
539
+ // and re-exported via ./schema/index.js for the public API
56
540
  // =============================================================================
57
- // Re-exports from schema/cascade.ts
541
+ // Provider Resolution
58
542
  // =============================================================================
59
- export { generateContextAwareValue, generateAIFields, generateEntity, resolveForwardExact, generateNaturalLanguageContent, } from './schema/cascade.js';
543
+ let globalProvider = null;
544
+ let providerPromise = null;
545
+ /** File count threshold for suggesting ClickHouse upgrade */
546
+ const FILE_COUNT_THRESHOLD = 10_000;
547
+ /**
548
+ * Set the global database provider
549
+ */
550
+ export function setProvider(provider) {
551
+ globalProvider = provider;
552
+ providerPromise = null;
553
+ // Bridge to schema/provider.ts module state so extracted modules share the same provider
554
+ setModuleProvider(provider);
555
+ }
556
+ /**
557
+ * Parse DATABASE_URL into provider type and paths
558
+ *
559
+ * Local storage (all use .db/ folder):
560
+ * - `./content` → fs (default)
561
+ * - `sqlite://./content` → sqlite stored in ./content/.db/index.sqlite
562
+ * - `chdb://./content` → clickhouse stored in ./content/.db/clickhouse/
563
+ *
564
+ * Remote:
565
+ * - `libsql://your-db.turso.io` → Turso SQLite
566
+ * - `clickhouse://host:8123` → ClickHouse HTTP
567
+ * - `:memory:` → in-memory
568
+ */
569
+ function parseDatabaseUrl(url) {
570
+ if (!url)
571
+ return { provider: 'fs', root: './content' };
572
+ // In-memory
573
+ if (url === ':memory:') {
574
+ return { provider: 'memory', root: '' };
575
+ }
576
+ // Remote Turso
577
+ if (url.startsWith('libsql://') || url.includes('.turso.io')) {
578
+ return { provider: 'sqlite', root: '', remoteUrl: url };
579
+ }
580
+ // Remote ClickHouse
581
+ if (url.startsWith('clickhouse://') && url.includes(':')) {
582
+ // clickhouse://host:port/db
583
+ return { provider: 'clickhouse', root: '', remoteUrl: url.replace('clickhouse://', 'https://') };
584
+ }
585
+ // Local SQLite: sqlite://./content → ./content/.db/index.sqlite
586
+ if (url.startsWith('sqlite://')) {
587
+ const root = url.replace('sqlite://', '') || './content';
588
+ return { provider: 'sqlite', root };
589
+ }
590
+ // Local ClickHouse (chDB): chdb://./content → ./content/.db/clickhouse/
591
+ if (url.startsWith('chdb://')) {
592
+ const root = url.replace('chdb://', '') || './content';
593
+ return { provider: 'clickhouse', root };
594
+ }
595
+ // Default: filesystem
596
+ return { provider: 'fs', root: url };
597
+ }
598
+ /**
599
+ * Resolve provider from DATABASE_URL environment variable
600
+ *
601
+ * @example
602
+ * ```bash
603
+ * # Filesystem (default) - stores in ./content with .db/ metadata
604
+ * DATABASE_URL=./content
605
+ *
606
+ * # Local SQLite - stores in ./content/.db/index.sqlite
607
+ * DATABASE_URL=sqlite://./content
608
+ *
609
+ * # Remote Turso
610
+ * DATABASE_URL=libsql://your-db.turso.io
611
+ *
612
+ * # Local ClickHouse (chDB) - stores in ./content/.db/clickhouse/
613
+ * DATABASE_URL=chdb://./content
614
+ *
615
+ * # Remote ClickHouse
616
+ * DATABASE_URL=clickhouse://localhost:8123
617
+ *
618
+ * # In-memory (testing)
619
+ * DATABASE_URL=:memory:
620
+ * ```
621
+ */
622
+ async function resolveProvider() {
623
+ if (globalProvider)
624
+ return globalProvider;
625
+ if (providerPromise)
626
+ return providerPromise;
627
+ providerPromise = (async () => {
628
+ const databaseUrl = (typeof process !== 'undefined' && process.env?.['DATABASE_URL']) || './content';
629
+ const parsed = parseDatabaseUrl(databaseUrl);
630
+ switch (parsed.provider) {
631
+ case 'memory': {
632
+ const { createMemoryProvider } = await import('./memory-provider.js');
633
+ globalProvider = createMemoryProvider();
634
+ break;
635
+ }
636
+ case 'fs': {
637
+ try {
638
+ // Dynamic import with runtime validation using type guard
639
+ const fsModule = await import('@mdxdb/fs');
640
+ if (!hasFactoryFunction(fsModule, 'createFsProvider')) {
641
+ throw new Error('@mdxdb/fs does not export createFsProvider');
642
+ }
643
+ globalProvider = fsModule.createFsProvider({ root: parsed.root });
644
+ // Check file count and warn if approaching threshold
645
+ checkFileCountThreshold(parsed.root);
646
+ }
647
+ catch (err) {
648
+ console.warn('@mdxdb/fs not available, falling back to memory provider');
649
+ const { createMemoryProvider } = await import('./memory-provider.js');
650
+ globalProvider = createMemoryProvider();
651
+ }
652
+ break;
653
+ }
654
+ case 'sqlite': {
655
+ try {
656
+ // Dynamic import with runtime validation using type guard
657
+ const sqliteModule = await import('@mdxdb/sqlite');
658
+ if (!hasFactoryFunction(sqliteModule, 'createSqliteProvider')) {
659
+ throw new Error('@mdxdb/sqlite does not export createSqliteProvider');
660
+ }
661
+ if (parsed.remoteUrl) {
662
+ // Remote Turso
663
+ globalProvider = (await sqliteModule.createSqliteProvider({
664
+ url: parsed.remoteUrl,
665
+ }));
666
+ }
667
+ else {
668
+ // Local SQLite in .db folder
669
+ const dbPath = `${parsed.root}/.db/index.sqlite`;
670
+ globalProvider = (await sqliteModule.createSqliteProvider({
671
+ url: `file:${dbPath}`,
672
+ }));
673
+ }
674
+ }
675
+ catch (err) {
676
+ console.warn('@mdxdb/sqlite not available, falling back to memory provider');
677
+ const { createMemoryProvider } = await import('./memory-provider.js');
678
+ globalProvider = createMemoryProvider();
679
+ }
680
+ break;
681
+ }
682
+ case 'clickhouse': {
683
+ try {
684
+ // Dynamic import with runtime validation using type guard
685
+ const chModule = await import('@mdxdb/clickhouse');
686
+ if (!hasFactoryFunction(chModule, 'createClickhouseProvider')) {
687
+ throw new Error('@mdxdb/clickhouse does not export createClickhouseProvider');
688
+ }
689
+ if (parsed.remoteUrl) {
690
+ // Remote ClickHouse
691
+ globalProvider = (await chModule.createClickhouseProvider({
692
+ mode: 'http',
693
+ url: parsed.remoteUrl,
694
+ }));
695
+ }
696
+ else {
697
+ // Local chDB in .db folder
698
+ const dbPath = `${parsed.root}/.db/clickhouse`;
699
+ globalProvider = (await chModule.createClickhouseProvider({
700
+ mode: 'chdb',
701
+ url: dbPath,
702
+ }));
703
+ }
704
+ }
705
+ catch (err) {
706
+ console.warn('@mdxdb/clickhouse not available, falling back to memory provider');
707
+ const { createMemoryProvider } = await import('./memory-provider.js');
708
+ globalProvider = createMemoryProvider();
709
+ }
710
+ break;
711
+ }
712
+ default: {
713
+ const { createMemoryProvider } = await import('./memory-provider.js');
714
+ globalProvider = createMemoryProvider();
715
+ }
716
+ }
717
+ return globalProvider;
718
+ })();
719
+ return providerPromise;
720
+ }
721
+ /**
722
+ * Check file count and warn if approaching threshold
723
+ */
724
+ async function checkFileCountThreshold(root) {
725
+ try {
726
+ const fs = await import('node:fs/promises');
727
+ const path = await import('node:path');
728
+ async function countFiles(dir) {
729
+ let count = 0;
730
+ try {
731
+ const entries = await fs.readdir(dir, { withFileTypes: true });
732
+ for (const entry of entries) {
733
+ if (entry.name.startsWith('.'))
734
+ continue;
735
+ if (entry.isDirectory()) {
736
+ count += await countFiles(path.join(dir, entry.name));
737
+ }
738
+ else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {
739
+ count++;
740
+ }
741
+ }
742
+ }
743
+ catch {
744
+ // Directory doesn't exist yet
745
+ }
746
+ return count;
747
+ }
748
+ const count = await countFiles(root);
749
+ if (count > FILE_COUNT_THRESHOLD) {
750
+ console.warn(`\n⚠️ You have ${count.toLocaleString()} MDX files. ` +
751
+ `Consider upgrading to ClickHouse for better performance:\n` +
752
+ ` DATABASE_URL=chdb://./data/clickhouse\n`);
753
+ }
754
+ }
755
+ catch {
756
+ // Ignore errors in file counting
757
+ }
758
+ }
60
759
  // =============================================================================
61
- // Re-exports from schema/semantic.ts
760
+ // DB Factory
62
761
  // =============================================================================
63
- export { resolveBackwardFuzzy, resolveForwardFuzzy } from './schema/semantic.js';
762
+ /**
763
+ * Create a typed database from a schema definition
764
+ *
765
+ * Supports both direct usage and destructuring for flexibility:
766
+ *
767
+ * @example Direct usage - everything on one object
768
+ * ```ts
769
+ * const db = DB({
770
+ * Post: { title: 'string', author: 'Author.posts' },
771
+ * Author: { name: 'string' },
772
+ * })
773
+ *
774
+ * // Entity operations
775
+ * const post = await db.Post.create({ title: 'Hello' })
776
+ *
777
+ * // Events, actions, etc. are also available directly
778
+ * db.events.on('Post.created', (event) => console.log(event))
779
+ * db.actions.create({ type: 'generate', data: {} })
780
+ * ```
781
+ *
782
+ * @example Destructured usage - cleaner separation
783
+ * ```ts
784
+ * const { db, events, actions, artifacts, nouns, verbs } = DB({
785
+ * Post: { title: 'string', author: 'Author.posts' },
786
+ * Author: { name: 'string' },
787
+ * })
788
+ *
789
+ * // CRUD operations on db
790
+ * const post = await db.Post.create({ title: 'Hello' })
791
+ * await db.Post.update(post.$id, { title: 'Updated' })
792
+ *
793
+ * // Separate events API
794
+ * events.on('Post.created', (event) => console.log(event))
795
+ *
796
+ * // Separate actions API
797
+ * const action = await actions.create({ type: 'generate', data: {} })
798
+ * ```
799
+ */
800
+ export function DB(schema, options) {
801
+ const parsedSchema = parseSchema(schema);
802
+ // Validate union types - ensure all union type references point to existing types
803
+ // This is done here rather than in parseSchema() to allow parseSchema() to be used
804
+ // for pure parsing tests without requiring all union types to be defined
805
+ for (const [entityName, entity] of parsedSchema.entities) {
806
+ for (const [fieldName, field] of entity.fields) {
807
+ if (field.isRelation && field.operator && field.unionTypes && field.unionTypes.length > 0) {
808
+ for (const unionType of field.unionTypes) {
809
+ if (unionType === entityName)
810
+ continue; // Skip self-references
811
+ if (!parsedSchema.entities.has(unionType)) {
812
+ throw new Error(`Invalid schema: ${entityName}.${fieldName} references non-existent type '${unionType}'`);
813
+ }
814
+ }
815
+ }
816
+ }
817
+ }
818
+ // Build and set schema relation info for batch loading
819
+ // Maps entityType -> fieldName -> relatedType
820
+ const relationInfo = new Map();
821
+ for (const [entityName, entity] of parsedSchema.entities) {
822
+ const fieldRelations = new Map();
823
+ for (const [fieldName, field] of entity.fields) {
824
+ if (field.isRelation && field.relatedType) {
825
+ fieldRelations.set(fieldName, field.relatedType);
826
+ }
827
+ }
828
+ if (fieldRelations.size > 0) {
829
+ relationInfo.set(entityName, fieldRelations);
830
+ }
831
+ }
832
+ setSchemaRelationInfo(relationInfo);
833
+ // Add system entities to the parsed schema (Noun, Verb, Edge)
834
+ addSystemEntities(parsedSchema);
835
+ // Create local getProvider function for dependency injection
836
+ // If options.provider is provided, use it; otherwise fall back to global resolveProvider()
837
+ let cachedProvider = null;
838
+ let localProviderPromise = null;
839
+ async function getProvider() {
840
+ // Return cached provider if available
841
+ if (cachedProvider)
842
+ return cachedProvider;
843
+ // Return pending promise if resolution is in progress
844
+ if (localProviderPromise)
845
+ return localProviderPromise;
846
+ // Check if options.provider is provided (dependency injection)
847
+ if (options?.provider) {
848
+ localProviderPromise = (async () => {
849
+ const providerOption = options.provider;
850
+ if (typeof providerOption === 'function') {
851
+ // It's a factory function - call it
852
+ const result = providerOption();
853
+ // Handle both sync and async factory functions
854
+ cachedProvider = result instanceof Promise ? await result : result;
855
+ }
856
+ else {
857
+ // It's a direct provider instance
858
+ cachedProvider = providerOption;
859
+ }
860
+ return cachedProvider;
861
+ })();
862
+ return localProviderPromise;
863
+ }
864
+ // Fall back to global resolveProvider()
865
+ localProviderPromise = resolveProvider().then((p) => {
866
+ cachedProvider = p;
867
+ return p;
868
+ });
869
+ return localProviderPromise;
870
+ }
871
+ // Configure provider with embeddings settings if provided
872
+ if (options?.embeddings) {
873
+ const embeddingsConfig = options.embeddings;
874
+ getProvider()
875
+ .then((provider) => {
876
+ if (hasEmbeddingsConfig(provider)) {
877
+ provider.setEmbeddingsConfig(embeddingsConfig);
878
+ }
879
+ else {
880
+ // Warn that embeddings configuration was provided but provider doesn't support it
881
+ logWarn('Embeddings configuration provided but current provider does not support semantic search. ' +
882
+ 'Embeddings will not be generated. Use a provider with embedding support (e.g., createMemoryProvider()).');
883
+ }
884
+ })
885
+ .catch((error) => {
886
+ console.error('Failed to configure embeddings on provider:', error);
887
+ });
888
+ }
889
+ // Collect all edge records from the schema (user-defined entities only)
890
+ const allEdgeRecords = [];
891
+ for (const [entityName, entity] of parsedSchema.entities) {
892
+ if (!SYSTEM_ENTITY_NAMES.has(entityName)) {
893
+ const edgeRecords = createEdgeRecords(entityName, schema[entityName] ?? {}, entity);
894
+ allEdgeRecords.push(...edgeRecords);
895
+ }
896
+ }
897
+ // Collect all noun records from the schema (user-defined entities only)
898
+ const allNounRecords = [];
899
+ for (const [entityName] of parsedSchema.entities) {
900
+ if (!SYSTEM_ENTITY_NAMES.has(entityName)) {
901
+ const nounRecord = createNounRecord(entityName, schema[entityName]);
902
+ allNounRecords.push(nounRecord);
903
+ }
904
+ }
905
+ // Collect all verb records from the standard verbs
906
+ const allVerbRecords = Object.values(Verbs).map((verb) => ({
907
+ ...verb,
908
+ $id: verb.action,
909
+ $type: 'Verb',
910
+ }));
911
+ // Create Actions API early so it can be injected into entity operations
912
+ const actionsAPI = {
913
+ async create(options) {
914
+ const provider = await getProvider();
915
+ if (hasActionsAPI(provider)) {
916
+ return provider.createAction(options);
917
+ }
918
+ throw new Error('Provider does not support actions');
919
+ },
920
+ async get(id) {
921
+ const provider = await getProvider();
922
+ if (hasActionsAPI(provider)) {
923
+ return provider.getAction(id);
924
+ }
925
+ return null;
926
+ },
927
+ async update(id, updates) {
928
+ const provider = await getProvider();
929
+ if (hasActionsAPI(provider)) {
930
+ return provider.updateAction(id, updates);
931
+ }
932
+ throw new Error('Provider does not support actions');
933
+ },
934
+ };
935
+ // Create ForEachActionsAPI adapter for wrapEntityOperations
936
+ const forEachActionsAPI = {
937
+ async create(data) {
938
+ const result = await actionsAPI.create(data);
939
+ return { id: result.id };
940
+ },
941
+ async get(id) {
942
+ return actionsAPI.get(id);
943
+ },
944
+ async update(id, updates) {
945
+ // Filter to only the properties that actionsAPI.update accepts
946
+ const filteredUpdates = {};
947
+ if (updates.status !== undefined)
948
+ filteredUpdates.status = updates.status;
949
+ if (updates.progress !== undefined)
950
+ filteredUpdates.progress = updates.progress;
951
+ return actionsAPI.update(id, filteredUpdates);
952
+ },
953
+ };
954
+ // Create entity operations for each type with promise pipelining
955
+ // NOTE: Using Record<string, unknown> here because entity operations types vary by schema.
956
+ // The actual types are determined at runtime and enforced via wrapEntityOperations.
957
+ // Attempts to use stricter types conflict with wrapEntityOperations return type.
958
+ const entityOperations = {};
959
+ // Internal event emitter for draft/resolve events (defined early for use in entity operations)
960
+ const eventHandlersForOps = new Map();
961
+ function emitInternalEventForOps(eventType, data) {
962
+ const handlers = eventHandlersForOps.get(eventType);
963
+ if (handlers) {
964
+ const snapshot = [...handlers];
965
+ for (const handler of snapshot) {
966
+ try {
967
+ handler(data);
968
+ }
969
+ catch (e) {
970
+ console.error(`Error in event handler for ${eventType}:`, e);
971
+ }
972
+ }
973
+ }
974
+ }
975
+ /**
976
+ * Make entity operations callable as a tagged template literal.
977
+ * This allows both: db.Lead.get('id') and db.Lead`natural language query`
978
+ */
979
+ function makeCallableEntityOps(ops, entityName) {
980
+ const nlQueryFn = createNLQueryFn(parsedSchema, entityName);
981
+ const callableOps = function (strings, ...values) {
982
+ return nlQueryFn(strings, ...values);
983
+ };
984
+ Object.assign(callableOps, ops);
985
+ return callableOps;
986
+ }
987
+ for (const [entityName, entity] of parsedSchema.entities) {
988
+ if (entityName === 'Edge') {
989
+ // Special handling for Edge entity - query from in-memory edge records + runtime edges
990
+ const edgeOps = createEdgeEntityOperations(allEdgeRecords, resolveProvider);
991
+ entityOperations[entityName] = makeCallableEntityOps(wrapEntityOperations(entityName, edgeOps, forEachActionsAPI), entityName);
992
+ }
993
+ else if (entityName === 'Noun') {
994
+ // Noun entity - auto-generated from schema entity types
995
+ const nounOps = createNounEntityOperations(allNounRecords);
996
+ entityOperations[entityName] = makeCallableEntityOps(wrapEntityOperations(entityName, nounOps, forEachActionsAPI), entityName);
997
+ }
998
+ else if (entityName === 'Verb') {
999
+ // Verb entity - standard verbs with conjugation forms
1000
+ const verbOps = createVerbEntityOperations(allVerbRecords);
1001
+ entityOperations[entityName] = makeCallableEntityOps(wrapEntityOperations(entityName, verbOps, forEachActionsAPI), entityName);
1002
+ }
1003
+ else {
1004
+ const baseOps = createEntityOperations(entityName, entity, parsedSchema);
1005
+ // Wrap with DBPromise for chainable queries, inject actions for forEach persistence
1006
+ const wrappedOps = wrapEntityOperations(entityName, baseOps, forEachActionsAPI);
1007
+ // Add draft and resolve with event emission
1008
+ const draftFn = async (data, options) => {
1009
+ // baseOps.draft is defined in createEntityOperations
1010
+ if (!baseOps.draft) {
1011
+ throw new Error(`Draft method not available for ${entityName}`);
1012
+ }
1013
+ const draft = await baseOps.draft(data, options);
1014
+ // Draft objects are always Record<string, unknown> - safe to set $type
1015
+ if (draft && typeof draft === 'object' && !Array.isArray(draft)) {
1016
+ const draftRecord = draft;
1017
+ draftRecord['$type'] = entityName;
1018
+ }
1019
+ emitInternalEventForOps('draft', draft);
1020
+ return draft;
1021
+ };
1022
+ wrappedOps.draft = draftFn;
1023
+ const resolveFn = async (draft, options) => {
1024
+ // baseOps.resolve is defined in createEntityOperations
1025
+ if (!baseOps.resolve) {
1026
+ throw new Error(`Resolve method not available for ${entityName}`);
1027
+ }
1028
+ // Draft<T> is always a Record with $phase - this cast is safe after validation
1029
+ const resolved = await baseOps.resolve(draft, options);
1030
+ if (resolved && typeof resolved === 'object' && !Array.isArray(resolved)) {
1031
+ const resolvedRecord = resolved;
1032
+ resolvedRecord['$type'] = entityName;
1033
+ }
1034
+ emitInternalEventForOps('resolve', resolved);
1035
+ return resolved;
1036
+ };
1037
+ wrappedOps.resolve = resolveFn;
1038
+ // Update create to support draftOnly option and wire through draft/resolve
1039
+ const originalCreate = wrappedOps.create;
1040
+ wrappedOps.create = async (...args) => {
1041
+ // Parse arguments - can be (data, options?) or (id, data, options?)
1042
+ // Type assertions here are necessary because variadic args are typed as unknown[]
1043
+ let data;
1044
+ let options;
1045
+ if (typeof args[0] === 'string') {
1046
+ // (id, data, options?) - args[1] is the data object
1047
+ data = isPlainObject(args[1]) ? args[1] : {};
1048
+ options = isPlainObject(args[2]) ? args[2] : undefined;
1049
+ }
1050
+ else {
1051
+ // (data, options?) - args[0] is the data object
1052
+ data = isPlainObject(args[0]) ? args[0] : {};
1053
+ // Check if second arg is options (has option-like properties)
1054
+ options = isPlainObject(args[1]) ? args[1] : undefined;
1055
+ }
1056
+ if (options?.draftOnly) {
1057
+ const draft = await draftFn(data);
1058
+ return draft;
1059
+ }
1060
+ // Pre-generate entity ID so it's available during resolve phase
1061
+ // This allows generated child entities to reference the parent via $generatedBy
1062
+ const preGeneratedId = typeof args[0] === 'string' ? args[0] : crypto.randomUUID();
1063
+ // Run draft phase first - draftFn returns an object with draft properties and emits 'draft' event
1064
+ const draftResult = await draftFn(data);
1065
+ const draft = isPlainObject(draftResult) ? draftResult : {};
1066
+ // Inject $id into draft so resolve can pass it as context to resolveReferenceSpec
1067
+ draft['$id'] = preGeneratedId;
1068
+ // Always strip array forward relation refs from the draft phase.
1069
+ // When cascade is enabled, cascadeGenerate handles array relations with proper
1070
+ // depth control and cascadeTypes filtering. When cascade is not enabled,
1071
+ // array relations should not be auto-generated.
1072
+ const draftRefs = draft['$refs'];
1073
+ if (draftRefs) {
1074
+ for (const [refFieldName, refSpec] of Object.entries(draftRefs)) {
1075
+ if (Array.isArray(refSpec)) {
1076
+ delete draftRefs[refFieldName];
1077
+ draft[refFieldName] = undefined;
1078
+ }
1079
+ }
1080
+ }
1081
+ // Then resolve - resolveFn emits 'resolve' event
1082
+ const resolveResult = await resolveFn(draft);
1083
+ const resolved = isPlainObject(resolveResult) ? resolveResult : {};
1084
+ // Create the final entity (without phase markers)
1085
+ const finalData = { ...resolved };
1086
+ delete finalData['$phase'];
1087
+ delete finalData['$refs'];
1088
+ delete finalData['$errors'];
1089
+ delete finalData['$type'];
1090
+ // Call originalCreate with the resolved data using the pre-generated ID
1091
+ // Pass _preResolved flag to skip internal draft/resolve in entity-operations.create()
1092
+ // Type assertion to Function is necessary for dynamic method call with spread args
1093
+ return originalCreate.call(wrappedOps, preGeneratedId, finalData, { ...options, _preResolved: true });
1094
+ };
1095
+ // Add seed method if entity has seed configuration
1096
+ if (entity.seedConfig) {
1097
+ const seedConfig = entity.seedConfig;
1098
+ wrappedOps['seed'] = async () => {
1099
+ const { loadSeedData } = await import('./schema/seed.js');
1100
+ const records = await loadSeedData(seedConfig);
1101
+ const provider = await getProvider();
1102
+ for (const record of records) {
1103
+ const { $id, ...data } = record;
1104
+ // Upsert: check if exists, then update or create
1105
+ const existing = await provider.get(entityName, $id);
1106
+ if (existing) {
1107
+ await provider.update(entityName, $id, data);
1108
+ }
1109
+ else {
1110
+ await provider.create(entityName, $id, data);
1111
+ }
1112
+ }
1113
+ return { count: records.length };
1114
+ };
1115
+ }
1116
+ entityOperations[entityName] = makeCallableEntityOps(wrappedOps, entityName);
1117
+ }
1118
+ }
1119
+ // Build noun and verb definition caches
1120
+ const { nounDefinitions, verbDefinitions } = buildDefinitionCaches(parsedSchema);
1121
+ // Use the event handlers defined earlier for entity operations
1122
+ function onInternal(eventType, handler) {
1123
+ if (!eventHandlersForOps.has(eventType)) {
1124
+ eventHandlersForOps.set(eventType, new Set());
1125
+ }
1126
+ eventHandlersForOps.get(eventType).add(handler);
1127
+ return () => {
1128
+ eventHandlersForOps.get(eventType)?.delete(handler);
1129
+ };
1130
+ }
1131
+ // Create the typed DB object
1132
+ const db = {
1133
+ $schema: parsedSchema,
1134
+ async get(url) {
1135
+ const provider = await getProvider();
1136
+ const parsed = parseUrl(url);
1137
+ return provider.get(parsed.type, parsed.id);
1138
+ },
1139
+ async search(query, options) {
1140
+ const provider = await getProvider();
1141
+ const results = [];
1142
+ for (const [typeName] of parsedSchema.entities) {
1143
+ const typeResults = await provider.search(typeName, query, options);
1144
+ results.push(...typeResults);
1145
+ }
1146
+ return results;
1147
+ },
1148
+ async semanticSearch(query, options) {
1149
+ const provider = await getProvider();
1150
+ if (!hasSemanticSearch(provider)) {
1151
+ throw new CapabilityNotSupportedError('hasSemanticSearch', `Semantic search is not supported by the current provider. ` +
1152
+ `The provider does not implement the semanticSearch method required for vector similarity search.`, `Use the regular search() method instead, which performs basic text matching.`);
1153
+ }
1154
+ const results = [];
1155
+ for (const [typeName] of parsedSchema.entities) {
1156
+ const typeResults = await provider.semanticSearch(typeName, query, options);
1157
+ results.push(...typeResults);
1158
+ }
1159
+ // Sort by score across all types
1160
+ results.sort((a, b) => b.$score - a.$score);
1161
+ // Apply limit if specified
1162
+ const limit = options?.limit ?? results.length;
1163
+ return results.slice(0, limit);
1164
+ },
1165
+ async count(type, where) {
1166
+ const provider = await getProvider();
1167
+ const results = await provider.list(type, where ? { where } : undefined);
1168
+ return results.length;
1169
+ },
1170
+ async forEach(options, callback) {
1171
+ const provider = await getProvider();
1172
+ const results = await provider.list(options.type, options.where ? { where: options.where } : undefined);
1173
+ const concurrency = options.concurrency ?? 1;
1174
+ if (concurrency === 1) {
1175
+ for (const entity of results) {
1176
+ await callback(entity);
1177
+ }
1178
+ }
1179
+ else {
1180
+ // Process in batches with concurrency
1181
+ const { Semaphore } = await import('./memory-provider.js');
1182
+ const semaphore = new Semaphore(concurrency);
1183
+ await semaphore.map(results, callback);
1184
+ }
1185
+ },
1186
+ async set(type, id, data) {
1187
+ const provider = await getProvider();
1188
+ const existing = await provider.get(type, id);
1189
+ if (existing) {
1190
+ // Replace entirely (not merge)
1191
+ return provider.update(type, id, data);
1192
+ }
1193
+ return provider.create(type, id, data);
1194
+ },
1195
+ async generate(options) {
1196
+ // Placeholder - actual AI generation would be implemented here
1197
+ // For now, just create with provided data
1198
+ const provider = await getProvider();
1199
+ if (options.mode === 'background') {
1200
+ // Return action ID for tracking
1201
+ const { createMemoryProvider } = await import('./memory-provider.js');
1202
+ const memProvider = provider;
1203
+ if ('createAction' in memProvider) {
1204
+ return memProvider.createAction({
1205
+ type: 'generate',
1206
+ data: options,
1207
+ total: options.count ?? 1,
1208
+ });
1209
+ }
1210
+ }
1211
+ // Sync mode - create single entity
1212
+ return provider.create(options.type, undefined, options.data ?? {});
1213
+ },
1214
+ ask: createNLQueryFn(parsedSchema),
1215
+ on: onInternal,
1216
+ ...entityOperations,
1217
+ };
1218
+ // Create sub-APIs using extracted factory functions
1219
+ const events = createEventsAPI(getProvider);
1220
+ const actions = createActionsPublicAPI(getProvider);
1221
+ const artifacts = createArtifactsAPI(getProvider);
1222
+ const nouns = createNounsAPI(nounDefinitions);
1223
+ const verbs = createVerbsAPI(verbDefinitions);
1224
+ const eventBridge = createEventBridge();
1225
+ // Return combined object that supports both direct usage and destructuring
1226
+ // db.User.create() works, db.events.on() works
1227
+ // const { db, events } = DB(...) also works
1228
+ return Object.assign(db, {
1229
+ db, // self-reference for destructuring
1230
+ events,
1231
+ actions,
1232
+ artifacts,
1233
+ nouns,
1234
+ verbs,
1235
+ eventBridge,
1236
+ });
1237
+ }
1238
+ /**
1239
+ * Parse a URL into type and id
1240
+ */
1241
+ function parseUrl(url) {
1242
+ // Handle full URLs
1243
+ if (url.includes('://')) {
1244
+ const parsed = new URL(url);
1245
+ const parts = parsed.pathname.split('/').filter(Boolean);
1246
+ return {
1247
+ type: parts[0] || '',
1248
+ id: parts.slice(1).join('/'),
1249
+ };
1250
+ }
1251
+ // Handle type/id format
1252
+ if (url.includes('/')) {
1253
+ const parts = url.split('/');
1254
+ return {
1255
+ type: parts[0],
1256
+ id: parts.slice(1).join('/'),
1257
+ };
1258
+ }
1259
+ // Just id
1260
+ return { type: '', id: url };
1261
+ }
64
1262
  // =============================================================================
65
- // Re-exports from schema/verb-derivation.ts
1263
+ // Forward Exact Resolution - Auto-generate related entities
66
1264
  // =============================================================================
67
- export { FORWARD_TO_REVERSE, BIDIRECTIONAL_PAIRS, deriveReverseVerb, fieldNameToVerb, isPassiveVerb, registerVerbPair, registerBidirectionalPair, registerFieldVerb, } from './schema/verb-derivation.js';
1265
+ /**
1266
+ * Generate an entity based on its type and context
1267
+ *
1268
+ * For testing, generates deterministic content based on the prompt and type.
1269
+ * In production, this would integrate with AI generation.
1270
+ *
1271
+ * @param type - The type of entity to generate
1272
+ * @param prompt - Optional prompt for generation context
1273
+ * @param context - Parent context information (parent type name, parentData, and optional parentId)
1274
+ * @param schema - The parsed schema
1275
+ */
1276
+ async function generateEntity(type, prompt, context, schema) {
1277
+ const entity = schema.entities.get(type);
1278
+ if (!entity)
1279
+ throw new Error(`Unknown type: ${type}`);
1280
+ // Gather context for generation
1281
+ const parentEntity = schema.entities.get(context.parent);
1282
+ // EntitySchema is Record<string, FieldDefinition | unknown> - safe to treat as Record
1283
+ const parentSchema = parentEntity?.schema ?? {};
1284
+ // Use type guard helper to safely extract schema metadata
1285
+ const instructions = getSchemaMetadata(parentSchema, '$instructions');
1286
+ const schemaContext = getSchemaMetadata(parentSchema, '$context');
1287
+ // Extract relevant parent data for context (excluding metadata fields)
1288
+ const parentContextFields = [];
1289
+ for (const [key, value] of Object.entries(context.parentData)) {
1290
+ if (!key.startsWith('$') && !key.startsWith('_') && typeof value === 'string' && value) {
1291
+ parentContextFields.push(`${key}: ${value}`);
1292
+ }
1293
+ }
1294
+ // Build context string for generation
1295
+ const contextParts = [];
1296
+ if (prompt && prompt.trim()) {
1297
+ contextParts.push(prompt);
1298
+ }
1299
+ if (instructions) {
1300
+ contextParts.push(instructions);
1301
+ }
1302
+ if (schemaContext) {
1303
+ contextParts.push(schemaContext);
1304
+ }
1305
+ if (parentContextFields.length > 0) {
1306
+ contextParts.push(parentContextFields.join(', '));
1307
+ }
1308
+ const fullContext = contextParts.join(' | ');
1309
+ const data = {};
1310
+ for (const [fieldName, field] of entity.fields) {
1311
+ if (!field.isRelation) {
1312
+ if (field.type === 'string') {
1313
+ // Generate context-aware content
1314
+ data[fieldName] = generateContextAwareValue(fieldName, type, fullContext, prompt);
1315
+ }
1316
+ else if (field.isArray && field.type === 'string') {
1317
+ // Generate array of strings
1318
+ data[fieldName] = [generateContextAwareValue(fieldName, type, fullContext, prompt)];
1319
+ }
1320
+ }
1321
+ else if (field.operator === '<-' && field.direction === 'backward') {
1322
+ // Backward relation to parent - set the parent's ID if this entity's
1323
+ // related type matches the parent type
1324
+ if (field.relatedType === context.parent && context.parentId) {
1325
+ // Store the parent ID directly - this is a reference back to the parent
1326
+ data[fieldName] = context.parentId;
1327
+ }
1328
+ }
1329
+ else if (field.operator === '->' && field.direction === 'forward') {
1330
+ // Recursively generate nested forward exact relations
1331
+ // This handles cases like Person.bio -> Bio (single relations, not arrays)
1332
+ // Array relations are handled by resolveForwardExact or cascadeGenerate
1333
+ if (!field.isOptional && !field.isArray) {
1334
+ const nestedGenerated = await generateEntity(field.relatedType, field.prompt, { parent: type, parentData: data }, schema);
1335
+ // We need to create the nested entity too, but we can't do that here
1336
+ // because we don't have access to the provider yet.
1337
+ // This will be handled by resolveForwardExact when it calls us
1338
+ data[`_pending_${fieldName}`] = { type: field.relatedType, data: nestedGenerated };
1339
+ }
1340
+ }
1341
+ }
1342
+ return data;
1343
+ }
1344
+ /**
1345
+ * Resolve forward exact (->) fields by auto-generating related entities
1346
+ *
1347
+ * When creating an entity with a -> field, if no value is provided,
1348
+ * we auto-generate the related entity and link it.
1349
+ *
1350
+ * Returns resolved data and pending relationships that need to be created
1351
+ * after the parent entity is created (for array fields).
1352
+ *
1353
+ * @param parentId - Pre-generated ID of the parent entity, so generated children
1354
+ * can set backward references to it
1355
+ */
1356
+ async function resolveForwardExact(typeName, data, entity, schema, provider, parentId, resolveOptions) {
1357
+ const resolved = { ...data };
1358
+ const pendingRelations = [];
1359
+ /**
1360
+ * For union types, find which type an entity ID belongs to
1361
+ */
1362
+ async function findEntityType(id, types) {
1363
+ for (const type of types) {
1364
+ const entity = await provider.get(type, id);
1365
+ if (entity)
1366
+ return type;
1367
+ }
1368
+ return null;
1369
+ }
1370
+ for (const [fieldName, field] of entity.fields) {
1371
+ if (field.operator === '->' && field.direction === 'forward') {
1372
+ // Get all possible types (union types or just the single related type)
1373
+ const possibleTypes = field.unionTypes || [field.relatedType];
1374
+ // Skip if value already provided
1375
+ if (resolved[fieldName] !== undefined && resolved[fieldName] !== null) {
1376
+ // If value is provided for array field, we still need to create relationships
1377
+ if (field.isArray && Array.isArray(resolved[fieldName])) {
1378
+ const ids = resolved[fieldName];
1379
+ const matchedTypes = [];
1380
+ for (const targetId of ids) {
1381
+ // For union types, determine the actual type of each ID
1382
+ const actualType = field.unionTypes
1383
+ ? (await findEntityType(targetId, possibleTypes)) || field.relatedType
1384
+ : field.relatedType;
1385
+ pendingRelations.push({ fieldName, targetType: actualType, targetId });
1386
+ matchedTypes.push(actualType);
1387
+ }
1388
+ // Store matched types for union type arrays
1389
+ if (field.unionTypes && matchedTypes.length > 0) {
1390
+ resolved[`${fieldName}$matchedTypes`] = matchedTypes;
1391
+ }
1392
+ }
1393
+ else if (!field.isArray) {
1394
+ // Single value provided - for union types, determine the actual type
1395
+ const providedId = resolved[fieldName];
1396
+ if (field.unionTypes) {
1397
+ const actualType = await findEntityType(providedId, possibleTypes);
1398
+ if (actualType) {
1399
+ resolved[`${fieldName}$matchedType`] = actualType;
1400
+ }
1401
+ }
1402
+ }
1403
+ continue;
1404
+ }
1405
+ // Skip optional fields - they shouldn't auto-generate
1406
+ if (field.isOptional)
1407
+ continue;
1408
+ if (field.isArray) {
1409
+ // When cascade is enabled, skip array generation - cascadeGenerate will handle it
1410
+ // with proper depth control
1411
+ if (resolveOptions?.skipArrayGeneration)
1412
+ continue;
1413
+ // Forward array relation - check if we should auto-generate
1414
+ const relatedEntity = schema.entities.get(field.relatedType);
1415
+ if (!relatedEntity)
1416
+ continue;
1417
+ // Check if related entity has a backward ref to this type (symmetric relationship)
1418
+ let hasBackwardRef = false;
1419
+ for (const [, relField] of relatedEntity.fields) {
1420
+ if (relField.isRelation &&
1421
+ relField.relatedType === typeName &&
1422
+ relField.direction === 'backward') {
1423
+ hasBackwardRef = true;
1424
+ break;
1425
+ }
1426
+ }
1427
+ // Check if related entity has required non-relation fields
1428
+ let hasRequiredScalarFields = false;
1429
+ for (const [, relField] of relatedEntity.fields) {
1430
+ if (!relField.isRelation && !relField.isOptional) {
1431
+ hasRequiredScalarFields = true;
1432
+ break;
1433
+ }
1434
+ }
1435
+ // Decide whether to auto-generate:
1436
+ // - If there's a symmetric backward ref AND required scalars, skip (prevents duplicates)
1437
+ // - Otherwise, generate if the related entity can be meaningfully generated
1438
+ // - For union types, be more lenient to allow polymorphic generation
1439
+ const shouldSkip = hasBackwardRef && hasRequiredScalarFields;
1440
+ const canGenerate = !shouldSkip &&
1441
+ (hasBackwardRef || // Symmetric ref without required scalars
1442
+ field.prompt || // Has a generation prompt
1443
+ field.unionTypes || // Union types should generate from first type
1444
+ !hasRequiredScalarFields); // No required fields to worry about
1445
+ if (!canGenerate)
1446
+ continue;
1447
+ // For union types, use first type for generation
1448
+ const generateType = field.relatedType;
1449
+ const generated = await generateEntity(generateType, field.prompt, { parent: typeName, parentData: data, parentId }, schema);
1450
+ // Resolve any pending nested relations in the generated data
1451
+ const resolvedGenerated = await resolveNestedPending(generated, relatedEntity, schema, provider);
1452
+ const created = await provider.create(generateType, undefined, resolvedGenerated);
1453
+ resolved[fieldName] = [created['$id']];
1454
+ // Store matched type for union types
1455
+ if (field.unionTypes) {
1456
+ resolved[`${fieldName}$matchedTypes`] = [generateType];
1457
+ }
1458
+ // Queue relationship creation for after parent entity is created
1459
+ pendingRelations.push({
1460
+ fieldName,
1461
+ targetType: generateType,
1462
+ targetId: created['$id'],
1463
+ });
1464
+ }
1465
+ else {
1466
+ // Single non-optional forward relation - generate the related entity
1467
+ // For union types, use first type for generation
1468
+ const generateType = field.relatedType;
1469
+ const generated = await generateEntity(generateType, field.prompt, { parent: typeName, parentData: data, parentId }, schema);
1470
+ // Resolve any pending nested relations in the generated data
1471
+ const relatedEntity = schema.entities.get(generateType);
1472
+ if (relatedEntity) {
1473
+ const resolvedGenerated = await resolveNestedPending(generated, relatedEntity, schema, provider);
1474
+ const created = await provider.create(generateType, undefined, resolvedGenerated);
1475
+ resolved[fieldName] = created['$id'];
1476
+ // Mark this forward ref as auto-generated so hydrateEntity knows to create a proxy
1477
+ resolved[`${fieldName}$autoGenerated`] = true;
1478
+ // Store matched type for union types
1479
+ if (field.unionTypes) {
1480
+ resolved[`${fieldName}$matchedType`] = generateType;
1481
+ }
1482
+ }
1483
+ }
1484
+ }
1485
+ }
1486
+ return { data: resolved, pendingRelations };
1487
+ }
1488
+ /**
1489
+ * Generate a simple entity with only scalar fields populated
1490
+ *
1491
+ * This is used by cascade generation to avoid infinite recursion.
1492
+ * Unlike generateEntity, this does NOT recursively generate nested relations.
1493
+ *
1494
+ * @param type - The type of entity to generate
1495
+ * @param prompt - Optional prompt for generation context
1496
+ * @param context - Parent context information
1497
+ * @param entityDef - The parsed entity definition
1498
+ */
1499
+ function generateSimpleEntity(type, prompt, context, entityDef, schema) {
1500
+ const data = {};
1501
+ // Build context from parent $instructions and child $instructions
1502
+ const contextParts = [];
1503
+ if (schema) {
1504
+ const parentEntity = schema.entities.get(context.parent);
1505
+ const parentInstructions = parentEntity?.schema?.['$instructions'];
1506
+ const childInstructions = entityDef.schema?.['$instructions'];
1507
+ if (parentInstructions)
1508
+ contextParts.push(parentInstructions);
1509
+ if (childInstructions)
1510
+ contextParts.push(childInstructions);
1511
+ }
1512
+ if (prompt)
1513
+ contextParts.push(prompt);
1514
+ for (const [key, value] of Object.entries(context.parentData)) {
1515
+ if (!key.startsWith('$') && !key.startsWith('_') && typeof value === 'string' && value) {
1516
+ contextParts.push(`${key}: ${value}`);
1517
+ }
1518
+ }
1519
+ const fullContext = contextParts.filter(Boolean).join(' | ');
1520
+ for (const [fieldName, field] of entityDef.fields) {
1521
+ if (!field.isRelation) {
1522
+ const isPrompt = isPromptField(field);
1523
+ if (field.type === 'string' || isPrompt) {
1524
+ const fieldHint = isPrompt ? field.type : prompt;
1525
+ data[fieldName] = generateContextAwareValue(fieldName, type, fullContext, fieldHint);
1526
+ }
1527
+ else if (field.isArray && (field.type === 'string' || isPrompt)) {
1528
+ const fieldHint = isPrompt ? field.type : prompt;
1529
+ data[fieldName] = [generateContextAwareValue(fieldName, type, fullContext, fieldHint)];
1530
+ }
1531
+ }
1532
+ else if (field.operator === '<-' && field.direction === 'backward') {
1533
+ // Backward relation to parent
1534
+ if (field.relatedType === context.parent && context.parentId) {
1535
+ data[fieldName] = context.parentId;
1536
+ }
1537
+ }
1538
+ // Skip forward relations - cascade will handle them
1539
+ }
1540
+ return data;
1541
+ }
1542
+ /**
1543
+ * Recursively generate related entities through cascade relationships
1544
+ *
1545
+ * This function traverses -> and ~> array relationships and generates
1546
+ * child entities at each level, respecting depth limits and type filters.
1547
+ *
1548
+ * @param entity - The parent entity data
1549
+ * @param entityDef - The parsed entity definition
1550
+ * @param schema - The parsed schema
1551
+ * @param provider - The database provider
1552
+ * @param options - Cascade options including maxDepth and type filters
1553
+ * @param depth - Current recursion depth
1554
+ * @param progress - Progress tracking object (mutated)
1555
+ */
1556
+ async function cascadeGenerate(entity, entityDef, schema, provider, options, depth, progress) {
1557
+ const maxDepth = options.maxDepth ?? 3;
1558
+ // Hard safety cap to prevent infinite recursion with circular schemas
1559
+ const effectiveMax = Math.min(maxDepth, DEFAULT_MAX_DEPTH);
1560
+ // Stop if we've reached max depth
1561
+ if (depth >= effectiveMax)
1562
+ return;
1563
+ // Report progress at this depth (even if no relations to process)
1564
+ progress.currentDepth = depth;
1565
+ progress.depth = depth;
1566
+ options.onProgress?.({ ...progress, phase: 'generating' });
1567
+ const entityId = (entity['$id'] || entity['id']);
1568
+ for (const [fieldName, field] of entityDef.fields) {
1569
+ // Only cascade through forward relationships (-> or ~>) that are arrays
1570
+ // or single references that haven't been populated yet
1571
+ const isForwardRelation = field.operator === '->' || field.operator === '~>';
1572
+ const isGeneratableRelation = isForwardRelation && field.relatedType;
1573
+ if (!isGeneratableRelation)
1574
+ continue;
1575
+ // Check if this type should be cascaded (if cascadeTypes filter is set)
1576
+ if (options.cascadeTypes && !options.cascadeTypes.includes(field.relatedType)) {
1577
+ continue;
1578
+ }
1579
+ // Report progress for this type
1580
+ progress.currentDepth = depth;
1581
+ progress.depth = depth;
1582
+ if (field.relatedType !== undefined) {
1583
+ progress.currentType = field.relatedType;
1584
+ }
1585
+ options.onProgress?.({ ...progress, phase: 'generating' });
1586
+ try {
1587
+ const relatedEntityDef = schema.entities.get(field.relatedType);
1588
+ if (!relatedEntityDef)
1589
+ continue;
1590
+ // Check if field already has values (from existing resolution)
1591
+ const existingValue = entity[fieldName];
1592
+ if (existingValue && Array.isArray(existingValue) && existingValue.length > 0) {
1593
+ // Already has values, cascade into each child
1594
+ for (const childId of existingValue) {
1595
+ const childData = await provider.get(field.relatedType, childId);
1596
+ if (childData) {
1597
+ await cascadeGenerate(childData, relatedEntityDef, schema, provider, options, depth + 1, progress);
1598
+ }
1599
+ }
1600
+ continue;
1601
+ }
1602
+ else if (existingValue && typeof existingValue === 'string') {
1603
+ // Single reference already populated, cascade into it
1604
+ const childData = await provider.get(field.relatedType, existingValue);
1605
+ if (childData) {
1606
+ await cascadeGenerate(childData, relatedEntityDef, schema, provider, options, depth + 1, progress);
1607
+ }
1608
+ continue;
1609
+ }
1610
+ // Generate new related entities
1611
+ if (field.isArray) {
1612
+ // Generate array of related entities using AI-enabled generation
1613
+ const generated = await cascadeGenerateEntity(field.relatedType, field.prompt, { parent: entityDef.name, parentData: entity, parentId: entityId }, schema);
1614
+ const created = await provider.create(field.relatedType, undefined, generated);
1615
+ // Update the parent entity with the new relation
1616
+ const existingIds = entity[fieldName] || [];
1617
+ const newIds = [...existingIds, created['$id']];
1618
+ await provider.update(entityDef.name, entityId, { [fieldName]: newIds });
1619
+ entity[fieldName] = newIds;
1620
+ // Create relationship
1621
+ await provider.relate(entityDef.name, entityId, fieldName, field.relatedType, created['$id']);
1622
+ progress.totalEntitiesCreated++;
1623
+ if (!progress.typesGenerated.includes(field.relatedType)) {
1624
+ progress.typesGenerated.push(field.relatedType);
1625
+ }
1626
+ // Recursively cascade into the new child
1627
+ await cascadeGenerate(created, relatedEntityDef, schema, provider, options, depth + 1, progress);
1628
+ }
1629
+ else {
1630
+ // Generate single related entity using AI-enabled generation
1631
+ const generated = await cascadeGenerateEntity(field.relatedType, field.prompt, { parent: entityDef.name, parentData: entity, parentId: entityId }, schema);
1632
+ const created = await provider.create(field.relatedType, undefined, generated);
1633
+ // Update the parent entity with the new relation
1634
+ await provider.update(entityDef.name, entityId, { [fieldName]: created['$id'] });
1635
+ entity[fieldName] = created['$id'];
1636
+ progress.totalEntitiesCreated++;
1637
+ if (!progress.typesGenerated.includes(field.relatedType)) {
1638
+ progress.typesGenerated.push(field.relatedType);
1639
+ }
1640
+ // Recursively cascade into the new child
1641
+ await cascadeGenerate(created, relatedEntityDef, schema, provider, options, depth + 1, progress);
1642
+ }
1643
+ }
1644
+ catch (error) {
1645
+ options.onError?.(error, { type: field.relatedType, depth });
1646
+ if (options.stopOnError)
1647
+ throw error;
1648
+ }
1649
+ }
1650
+ }
1651
+ /**
1652
+ * Resolve backward fuzzy (<~) fields by using semantic search to find existing entities
1653
+ *
1654
+ * The <~ operator differs from <- in that it uses semantic/fuzzy matching:
1655
+ * - Uses AI/embedding-based similarity to find the best match from existing entities
1656
+ * - Does NOT generate new entities - only grounds to existing reference data
1657
+ * - Uses hint fields (e.g., categoryHint for category field) to guide matching
1658
+ *
1659
+ * @param typeName - The type of entity being created
1660
+ * @param data - The input data including hint fields
1661
+ * @param entity - The parsed entity definition
1662
+ * @param schema - The parsed schema
1663
+ * @param provider - The database provider (must support semanticSearch)
1664
+ * @returns The resolved data with backward fuzzy fields populated with matched entity IDs
1665
+ */
1666
+ async function resolveBackwardFuzzy(typeName, data, entity, schema, provider) {
1667
+ const resolved = { ...data };
1668
+ // Type-safe access to schema metadata with default fallback
1669
+ const schemaWithMeta = entity.schema;
1670
+ const threshold = schemaWithMeta?.$fuzzyThreshold ?? 0.75;
1671
+ /**
1672
+ * Search all union types in parallel and return matches
1673
+ */
1674
+ async function searchUnionTypes(types, searchQuery, threshold, limit) {
1675
+ if (!hasSemanticSearch(provider))
1676
+ return [];
1677
+ // Search all types in parallel
1678
+ const allMatches = await Promise.all(types.map(async (type) => {
1679
+ const matches = await provider.semanticSearch(type, searchQuery, {
1680
+ minScore: threshold,
1681
+ limit,
1682
+ });
1683
+ return matches.map((m) => ({ id: m.$id, type, score: m.$score }));
1684
+ }));
1685
+ // Flatten and sort by score (best first)
1686
+ return allMatches.flat().sort((a, b) => b.score - a.score);
1687
+ }
1688
+ for (const [fieldName, field] of entity.fields) {
1689
+ if (field.operator === '<~' && field.direction === 'backward') {
1690
+ // Skip if value already provided
1691
+ if (resolved[fieldName] !== undefined && resolved[fieldName] !== null) {
1692
+ continue;
1693
+ }
1694
+ // Get the hint field value - uses fieldNameHint convention
1695
+ const hintKey = `${fieldName}Hint`;
1696
+ const searchQuery = data[hintKey] || field.prompt || '';
1697
+ // Skip if no search query available (optional fields without hint)
1698
+ if (!searchQuery) {
1699
+ continue;
1700
+ }
1701
+ // Get all types to search (union types or just the single related type)
1702
+ const typesToSearch = field.unionTypes || [field.relatedType];
1703
+ // Check if provider supports semantic search
1704
+ if (hasSemanticSearch(provider)) {
1705
+ if (field.unionTypes && field.unionTypes.length > 0) {
1706
+ // Union type - search all types
1707
+ const matches = await searchUnionTypes(typesToSearch, searchQuery, threshold, field.isArray ? 10 : 1);
1708
+ if (matches.length > 0) {
1709
+ if (field.isArray) {
1710
+ // For array fields, return all matches above threshold
1711
+ const validMatches = matches.filter((m) => m.score >= threshold);
1712
+ resolved[fieldName] = validMatches.map((m) => m.id);
1713
+ resolved[`${fieldName}$matchedTypes`] = validMatches.map((m) => m.type);
1714
+ resolved[`${fieldName}$scores`] = validMatches.map((m) => m.score);
1715
+ resolved[`${fieldName}$searchOrder`] = typesToSearch;
1716
+ // Check if any match came from a non-first type (fallback)
1717
+ const firstType = typesToSearch[0];
1718
+ const fallbackUsed = validMatches.some((m) => m.type !== firstType);
1719
+ if (fallbackUsed) {
1720
+ resolved[`${fieldName}$fallbackUsed`] = true;
1721
+ }
1722
+ }
1723
+ else {
1724
+ // For single fields, return the best match
1725
+ const firstMatch = matches[0];
1726
+ if (firstMatch) {
1727
+ resolved[fieldName] = firstMatch.id;
1728
+ resolved[`${fieldName}$matchedType`] = firstMatch.type;
1729
+ resolved[`${fieldName}$score`] = firstMatch.score;
1730
+ resolved[`${fieldName}$searchOrder`] = typesToSearch;
1731
+ // Fallback is triggered if the matched type is not the first type in the union
1732
+ const firstType = typesToSearch[0];
1733
+ if (firstMatch.type !== firstType) {
1734
+ resolved[`${fieldName}$fallbackUsed`] = true;
1735
+ }
1736
+ }
1737
+ }
1738
+ }
1739
+ }
1740
+ else {
1741
+ // Non-union type - use standard search
1742
+ const matches = await provider.semanticSearch(field.relatedType, searchQuery, {
1743
+ minScore: threshold,
1744
+ limit: field.isArray ? 10 : 1,
1745
+ });
1746
+ if (matches.length > 0) {
1747
+ if (field.isArray) {
1748
+ // For array fields, return all matches above threshold
1749
+ // SemanticSearchResult has $id and $score properties
1750
+ resolved[fieldName] = matches.filter((m) => m.$score >= threshold).map((m) => m.$id);
1751
+ }
1752
+ else {
1753
+ // For single fields, return the best match
1754
+ const firstMatch = matches[0];
1755
+ if (firstMatch) {
1756
+ resolved[fieldName] = firstMatch.$id;
1757
+ }
1758
+ }
1759
+ }
1760
+ }
1761
+ }
1762
+ // Note: <~ typically doesn't generate - it grounds to existing data
1763
+ // If no match found and field is optional, leave as undefined
1764
+ }
1765
+ }
1766
+ return resolved;
1767
+ }
1768
+ /**
1769
+ * Resolve forward fuzzy (~>) fields via semantic search then generation
1770
+ *
1771
+ * The ~> operator differs from -> in that it first attempts semantic search:
1772
+ * - Searches existing entities via embedding similarity
1773
+ * - If a match is found above threshold, reuses the existing entity
1774
+ * - If no match is found, generates a new entity
1775
+ * - Respects configurable similarity threshold ($fuzzyThreshold or field-level)
1776
+ *
1777
+ * @param typeName - The type of entity being created
1778
+ * @param data - The input data including hint fields
1779
+ * @param entity - The parsed entity definition
1780
+ * @param schema - The parsed schema
1781
+ * @param provider - The database provider (must support semanticSearch)
1782
+ * @param parentId - Pre-generated ID of the parent entity for backward refs
1783
+ * @returns Object with resolved data and pending relations for array fields
1784
+ */
1785
+ async function resolveForwardFuzzy(typeName, data, entity, schema, provider, parentId) {
1786
+ const resolved = { ...data };
1787
+ const pendingRelations = [];
1788
+ // Type-safe access to schema metadata with default fallback
1789
+ const schemaWithMeta = entity.schema;
1790
+ const defaultThreshold = schemaWithMeta?.$fuzzyThreshold ?? 0.75;
1791
+ /**
1792
+ * Search all union types in parallel and return the best match
1793
+ */
1794
+ async function searchUnionTypes(types, searchQuery, threshold) {
1795
+ if (!hasSemanticSearch(provider))
1796
+ return null;
1797
+ const allMatches = await Promise.all(types.map(async (type) => {
1798
+ const matches = await provider.semanticSearch(type, searchQuery, {
1799
+ minScore: threshold,
1800
+ limit: 3,
1801
+ });
1802
+ return matches.map((m) => ({ ...m, $matchedType: type }));
1803
+ }));
1804
+ // Flatten and find the best match
1805
+ const flat = allMatches.flat();
1806
+ if (flat.length === 0)
1807
+ return null;
1808
+ const best = flat.reduce((a, b) => (a.$score > b.$score ? a : b));
1809
+ return best.$score >= threshold
1810
+ ? { id: best.$id, type: best.$matchedType, score: best.$score }
1811
+ : null;
1812
+ }
1813
+ for (const [fieldName, field] of entity.fields) {
1814
+ if (field.operator === '~>' && field.direction === 'forward') {
1815
+ // Skip if value already provided (e.g., resolved by draft/resolve pipeline)
1816
+ if (resolved[fieldName] !== undefined && resolved[fieldName] !== null) {
1817
+ if (field.isArray && Array.isArray(resolved[fieldName])) {
1818
+ // Array field - create relationships with matched type metadata
1819
+ const ids = resolved[fieldName];
1820
+ const matchedTypes = resolved[`${fieldName}$matchedTypes`];
1821
+ for (let i = 0; i < ids.length; i++) {
1822
+ const targetType = matchedTypes?.[i] || field.relatedType;
1823
+ pendingRelations.push({
1824
+ fieldName,
1825
+ targetType,
1826
+ targetId: ids[i],
1827
+ matchedType: targetType,
1828
+ });
1829
+ }
1830
+ }
1831
+ else if (typeof resolved[fieldName] === 'string') {
1832
+ // Single field - create relationship with matched type metadata
1833
+ const targetId = resolved[fieldName];
1834
+ const matchedType = resolved[`${fieldName}$matchedType`] || field.relatedType;
1835
+ const score = resolved[`${fieldName}$score`];
1836
+ pendingRelations.push({
1837
+ fieldName,
1838
+ targetType: matchedType,
1839
+ targetId,
1840
+ ...(score !== undefined ? { similarity: score } : {}),
1841
+ matchedType,
1842
+ });
1843
+ }
1844
+ continue;
1845
+ }
1846
+ // Get the hint field value - uses fieldNameHint convention
1847
+ const hintKey = `${fieldName}Hint`;
1848
+ const hintValue = data[hintKey];
1849
+ const searchQuery = (typeof hintValue === 'string' ? hintValue : undefined) || field.prompt || fieldName;
1850
+ // Get threshold - field-level overrides entity-level
1851
+ const threshold = field.threshold ?? defaultThreshold;
1852
+ // Get all types to search (union types or just the single related type)
1853
+ const typesToSearch = field.unionTypes || [field.relatedType];
1854
+ if (field.isArray) {
1855
+ // Array fuzzy field - can contain both matched and generated
1856
+ const hints = Array.isArray(hintValue) ? hintValue : [hintValue].filter(Boolean);
1857
+ const resultIds = [];
1858
+ const matchedTypes = [];
1859
+ for (const hint of hints) {
1860
+ const hintStr = String(hint || fieldName);
1861
+ let matched = false;
1862
+ // Try semantic search across all union types
1863
+ const match = await searchUnionTypes(typesToSearch, hintStr, threshold);
1864
+ if (match) {
1865
+ resultIds.push(match.id);
1866
+ matchedTypes.push(match.type);
1867
+ pendingRelations.push({
1868
+ fieldName,
1869
+ targetType: match.type,
1870
+ targetId: match.id,
1871
+ similarity: match.score,
1872
+ matchedType: match.type,
1873
+ });
1874
+ matched = true;
1875
+ }
1876
+ // Generate if no match found - use first type in union
1877
+ if (!matched) {
1878
+ const generateType = typesToSearch[0];
1879
+ const generated = await generateEntity(generateType, hintStr, { parent: typeName, parentData: data, parentId }, schema);
1880
+ // Resolve any pending nested relations
1881
+ const relatedEntity = schema.entities.get(generateType);
1882
+ if (relatedEntity) {
1883
+ const resolvedGenerated = await resolveNestedPending(generated, relatedEntity, schema, provider);
1884
+ const created = await provider.create(generateType, undefined, {
1885
+ ...resolvedGenerated,
1886
+ $generated: true,
1887
+ $generatedBy: parentId,
1888
+ $sourceField: fieldName,
1889
+ });
1890
+ resultIds.push(created['$id']);
1891
+ matchedTypes.push(generateType);
1892
+ pendingRelations.push({
1893
+ fieldName,
1894
+ targetType: generateType,
1895
+ targetId: created['$id'],
1896
+ matchedType: generateType,
1897
+ });
1898
+ }
1899
+ }
1900
+ }
1901
+ resolved[fieldName] = resultIds;
1902
+ // Store matched types for array fields
1903
+ if (matchedTypes.length > 0) {
1904
+ resolved[`${fieldName}$matchedTypes`] = matchedTypes;
1905
+ }
1906
+ }
1907
+ else {
1908
+ // Single fuzzy field
1909
+ let matched = false;
1910
+ // Try semantic search across all union types
1911
+ const match = await searchUnionTypes(typesToSearch, searchQuery, threshold);
1912
+ if (match) {
1913
+ resolved[fieldName] = match.id;
1914
+ resolved[`${fieldName}$matched`] = true;
1915
+ resolved[`${fieldName}$score`] = match.score;
1916
+ resolved[`${fieldName}$matchedType`] = match.type;
1917
+ pendingRelations.push({
1918
+ fieldName,
1919
+ targetType: match.type,
1920
+ targetId: match.id,
1921
+ similarity: match.score,
1922
+ matchedType: match.type,
1923
+ });
1924
+ matched = true;
1925
+ }
1926
+ // Generate if no match found - use first type in union
1927
+ if (!matched) {
1928
+ const generateType = typesToSearch[0];
1929
+ const generated = await generateEntity(generateType, searchQuery, // Use searchQuery which prioritizes hint over field.prompt
1930
+ { parent: typeName, parentData: data, parentId }, schema);
1931
+ // Resolve any pending nested relations
1932
+ const relatedEntity = schema.entities.get(generateType);
1933
+ if (relatedEntity) {
1934
+ const resolvedGenerated = await resolveNestedPending(generated, relatedEntity, schema, provider);
1935
+ const created = await provider.create(generateType, undefined, {
1936
+ ...resolvedGenerated,
1937
+ $generated: true,
1938
+ $generatedBy: parentId,
1939
+ $sourceField: fieldName,
1940
+ });
1941
+ resolved[fieldName] = created['$id'];
1942
+ resolved[`${fieldName}$matchedType`] = generateType;
1943
+ pendingRelations.push({
1944
+ fieldName,
1945
+ targetType: generateType,
1946
+ targetId: created['$id'],
1947
+ matchedType: generateType,
1948
+ });
1949
+ }
1950
+ }
1951
+ }
1952
+ }
1953
+ }
1954
+ return { data: resolved, pendingRelations };
1955
+ }
1956
+ /**
1957
+ * Resolve pending nested relations in generated data
1958
+ *
1959
+ * When generateEntity encounters nested -> relations, it stores them as
1960
+ * _pending_fieldName entries. This function creates those entities and
1961
+ * replaces the pending entries with actual IDs.
1962
+ */
1963
+ async function resolveNestedPending(data, entity, schema, provider) {
1964
+ const resolved = { ...data };
1965
+ for (const key of Object.keys(resolved)) {
1966
+ if (key.startsWith('_pending_')) {
1967
+ const fieldName = key.replace('_pending_', '');
1968
+ const pending = resolved[key];
1969
+ delete resolved[key];
1970
+ // Get the field definition to check if it's an array
1971
+ const field = entity.fields.get(fieldName);
1972
+ // Get the related entity to resolve its nested pending relations too
1973
+ const relatedEntity = schema.entities.get(pending.type);
1974
+ if (relatedEntity) {
1975
+ const resolvedNested = await resolveNestedPending(pending.data, relatedEntity, schema, provider);
1976
+ const created = await provider.create(pending.type, undefined, resolvedNested);
1977
+ // Set as array or single value based on field definition
1978
+ resolved[fieldName] = field?.isArray ? [created['$id']] : created['$id'];
1979
+ }
1980
+ }
1981
+ }
1982
+ return resolved;
1983
+ }
68
1984
  // =============================================================================
69
- // Re-exports from schema/index.ts (main factory and operations)
1985
+ // Two-Phase Draft/Resolve Helper Functions
70
1986
  // =============================================================================
71
- export {
72
- // Noun/Verb helpers
73
- defineNoun, defineVerb, nounToSchema,
74
- // Built-in schemas
75
- ThingSchema, NounSchema, VerbSchema, EdgeSchema, SystemSchema,
76
- // Schema utilities
77
- createEdgeRecords, createNounRecord,
78
- // NL Query
79
- setNLQueryGenerator,
80
- // DB Factory
81
- DB, } from './schema/index.js';
1987
+ /**
1988
+ * Resolve a single reference specification to an entity ID
1989
+ *
1990
+ * For exact matches (-> and <-), creates new entities.
1991
+ * For fuzzy matches (~> and <~), searches for existing entities first.
1992
+ */
1993
+ async function resolveReferenceSpec(spec, contextData, schema, provider) {
1994
+ const targetEntity = schema.entities.get(spec.type);
1995
+ if (!targetEntity) {
1996
+ throw new Error(`Unknown target type: ${spec.type}`);
1997
+ }
1998
+ if (spec.matchMode === 'fuzzy') {
1999
+ // For fuzzy references, try to find an existing entity first
2000
+ if (hasSemanticSearch(provider)) {
2001
+ const searchQuery = spec.generatedText || spec.prompt || spec.field;
2002
+ const threshold = spec.threshold ?? 0.75;
2003
+ // Search across all union types (or just the single type)
2004
+ const typesToSearch = spec.unionTypes || [spec.type];
2005
+ let bestMatch = null;
2006
+ for (const searchType of typesToSearch) {
2007
+ const matches = await provider.semanticSearch(searchType, searchQuery, {
2008
+ minScore: threshold,
2009
+ limit: 1,
2010
+ });
2011
+ if (matches.length > 0 && matches[0]) {
2012
+ if (!bestMatch || matches[0].$score > bestMatch.$score) {
2013
+ bestMatch = {
2014
+ $id: matches[0].$id,
2015
+ $score: matches[0].$score,
2016
+ $matchedType: searchType,
2017
+ };
2018
+ }
2019
+ }
2020
+ }
2021
+ if (bestMatch) {
2022
+ // Set metadata on contextData so it's available in the resolved entity
2023
+ contextData[`${spec.field}$matched`] = true;
2024
+ contextData[`${spec.field}$score`] = bestMatch.$score;
2025
+ contextData[`${spec.field}$matchedType`] = bestMatch.$matchedType;
2026
+ return bestMatch.$id;
2027
+ }
2028
+ }
2029
+ // If no match found for fuzzy, fall through to create
2030
+ }
2031
+ // Create a new entity
2032
+ const generatedData = {};
2033
+ // Build context for generation from contextData
2034
+ const genCtxParts = [];
2035
+ // Include $instructions from source entity schema (injected during resolve)
2036
+ if (typeof contextData['$instructions'] === 'string') {
2037
+ genCtxParts.push(contextData['$instructions']);
2038
+ }
2039
+ // Include target entity's $instructions
2040
+ const tgtInstructions = targetEntity.schema?.['$instructions'];
2041
+ if (typeof tgtInstructions === 'string') {
2042
+ genCtxParts.push(tgtInstructions);
2043
+ }
2044
+ // Include spec prompt
2045
+ if (spec.prompt) {
2046
+ genCtxParts.push(spec.prompt);
2047
+ }
2048
+ for (const [key, value] of Object.entries(contextData)) {
2049
+ if (!key.startsWith('$') && !key.startsWith('_') && typeof value === 'string' && value) {
2050
+ genCtxParts.push(`${key}: ${value}`);
2051
+ }
2052
+ }
2053
+ const genCtx = genCtxParts.filter(Boolean).join(' | ');
2054
+ // Generate default values for the target entity's fields
2055
+ for (const [fieldName, field] of targetEntity.fields) {
2056
+ if (!field.isRelation) {
2057
+ const isPrompt = isPromptField(field);
2058
+ // Generate both required fields and optional prompt fields
2059
+ if (!field.isOptional || isPrompt) {
2060
+ if (field.type === 'string' || isPrompt) {
2061
+ const fldHint = isPrompt ? field.type : undefined;
2062
+ generatedData[fieldName] = generateContextAwareValue(fieldName, spec.type, genCtx, fldHint);
2063
+ }
2064
+ else if (field.type === 'number') {
2065
+ generatedData[fieldName] = 0;
2066
+ }
2067
+ else if (field.type === 'boolean') {
2068
+ generatedData[fieldName] = false;
2069
+ }
2070
+ }
2071
+ }
2072
+ else if (field.isRelation && field.operator === '->' && !field.isArray && !field.isOptional) {
2073
+ // Recursively resolve nested forward exact relations
2074
+ const nestedSpec = {
2075
+ field: fieldName,
2076
+ operator: '->',
2077
+ type: field.relatedType,
2078
+ matchMode: 'exact',
2079
+ resolved: false,
2080
+ ...(field.prompt !== undefined && { prompt: field.prompt }),
2081
+ };
2082
+ const nestedId = await resolveReferenceSpec(nestedSpec, generatedData, schema, provider);
2083
+ if (nestedId) {
2084
+ generatedData[fieldName] = nestedId;
2085
+ }
2086
+ }
2087
+ }
2088
+ const created = await provider.create(spec.type, undefined, {
2089
+ ...generatedData,
2090
+ $generated: true,
2091
+ $generatedBy: contextData['$id'] ||
2092
+ (spec.matchMode === 'fuzzy' ? 'fuzzy-resolution' : 'reference-resolution'),
2093
+ $sourceField: spec.field,
2094
+ });
2095
+ return created['$id'];
2096
+ }
2097
+ /**
2098
+ * Create operations for a single entity type
2099
+ */
2100
+ function createEntityOperations(typeName, entity, schema) {
2101
+ return {
2102
+ async get(id) {
2103
+ const provider = await resolveProvider();
2104
+ const result = await provider.get(typeName, id);
2105
+ if (!result)
2106
+ return null;
2107
+ return hydrateEntity(result, entity, schema);
2108
+ },
2109
+ async list(options) {
2110
+ try {
2111
+ const provider = await resolveProvider();
2112
+ const results = await provider.list(typeName, options);
2113
+ return Promise.all(results.map((r) => hydrateEntity(r, entity, schema)));
2114
+ }
2115
+ catch (error) {
2116
+ // Handle error with callback if provided
2117
+ if (options?.onError) {
2118
+ const wrappedError = error instanceof Error ? error : new Error(String(error));
2119
+ const fallback = options.onError(wrappedError);
2120
+ return (fallback ?? []);
2121
+ }
2122
+ // Suppress errors if requested
2123
+ if (options?.suppressErrors) {
2124
+ return [];
2125
+ }
2126
+ throw error;
2127
+ }
2128
+ },
2129
+ async find(where) {
2130
+ const provider = await resolveProvider();
2131
+ // Partial<T> is a subset of Record<string, unknown> - safe cast for provider API
2132
+ const results = await provider.list(typeName, {
2133
+ where: where,
2134
+ });
2135
+ // hydrateEntity returns Record<string, unknown>, cast to T for type-safe API
2136
+ return Promise.all(results.map((r) => hydrateEntity(r, entity, schema)));
2137
+ },
2138
+ async search(query, options) {
2139
+ const provider = await resolveProvider();
2140
+ const results = await provider.search(typeName, query, options);
2141
+ return Promise.all(results.map((r) => hydrateEntity(r, entity, schema)));
2142
+ },
2143
+ async create(idOrData, maybeDataOrOptions, maybeOptions) {
2144
+ const provider = await resolveProvider();
2145
+ // Parse arguments: support both (data, options) and (id, data, options) signatures
2146
+ // Type assertions are necessary here because T extends Record<string, unknown>
2147
+ // but TypeScript can't infer this from the Omit<T, ...> type
2148
+ let providedId;
2149
+ let data;
2150
+ let options;
2151
+ if (typeof idOrData === 'string') {
2152
+ // First arg is ID - maybeDataOrOptions must be the data object
2153
+ providedId = idOrData;
2154
+ // Safe cast: maybeDataOrOptions is Omit<T, '$id' | '$type'> which extends Record
2155
+ data = (maybeDataOrOptions ?? {});
2156
+ options = maybeOptions;
2157
+ }
2158
+ else {
2159
+ // First arg is data - safe cast since idOrData is Omit<T, '$id' | '$type'>
2160
+ providedId = undefined;
2161
+ data = idOrData;
2162
+ // Check if second arg is options by detecting option-specific properties
2163
+ if (maybeDataOrOptions &&
2164
+ typeof maybeDataOrOptions === 'object' &&
2165
+ ('cascade' in maybeDataOrOptions ||
2166
+ 'maxDepth' in maybeDataOrOptions ||
2167
+ 'onProgress' in maybeDataOrOptions ||
2168
+ 'onError' in maybeDataOrOptions ||
2169
+ 'draftOnly' in maybeDataOrOptions ||
2170
+ 'cascadeTypes' in maybeDataOrOptions ||
2171
+ 'stopOnError' in maybeDataOrOptions)) {
2172
+ options = maybeDataOrOptions;
2173
+ }
2174
+ }
2175
+ // Pre-generate entity ID so child entities can reference us
2176
+ const entityId = providedId || crypto.randomUUID();
2177
+ // Resolve forward exact (->) fields by auto-generating related entities
2178
+ // Pass the entityId so generated children can set backward references
2179
+ // When cascade is enabled, skip auto-generation for array fields - cascadeGenerate will handle them
2180
+ const { data: resolvedData, pendingRelations } = await resolveForwardExact(typeName, data, entity, schema, provider, entityId, { skipArrayGeneration: true });
2181
+ // Resolve forward fuzzy (~>) fields by semantic search then generation
2182
+ const { data: fuzzyResolvedData, pendingRelations: fuzzyPendingRelations } = await resolveForwardFuzzy(typeName, resolvedData, entity, schema, provider, entityId);
2183
+ // Resolve backward fuzzy (<~) fields by semantic search against existing entities
2184
+ const backwardResolvedData = await resolveBackwardFuzzy(typeName, fuzzyResolvedData, entity, schema, provider);
2185
+ // Generate AI fields for entities with $context dependencies
2186
+ // This handles prompt fields like 'string (compelling title)' that need context
2187
+ const finalData = await generateAIFields(backwardResolvedData, typeName, entity, schema, provider);
2188
+ let result;
2189
+ try {
2190
+ result = await provider.create(typeName, entityId, finalData);
2191
+ }
2192
+ catch (error) {
2193
+ if (error instanceof DatabaseError)
2194
+ throw error;
2195
+ throw wrapDatabaseError(error, 'create', typeName, entityId);
2196
+ }
2197
+ // Create relationships for array fields (exact)
2198
+ for (const rel of pendingRelations) {
2199
+ await provider.relate(typeName, entityId, rel.fieldName, rel.targetType, rel.targetId);
2200
+ }
2201
+ // Create relationships for fuzzy fields with metadata
2202
+ // Track created Edge IDs to avoid duplicates
2203
+ const createdEdgeIds = new Set();
2204
+ for (const rel of fuzzyPendingRelations) {
2205
+ await provider.relate(typeName, entityId, rel.fieldName, rel.targetType, rel.targetId, {
2206
+ matchMode: 'fuzzy',
2207
+ ...(rel.similarity !== undefined && { similarity: rel.similarity }),
2208
+ });
2209
+ // Also create an Edge entity to store the fuzzy match metadata
2210
+ // Use a unique ID based on the relationship
2211
+ const edgeId = `${typeName}-${rel.fieldName}-${entityId}-${rel.targetId}`;
2212
+ if (!createdEdgeIds.has(edgeId)) {
2213
+ createdEdgeIds.add(edgeId);
2214
+ try {
2215
+ await provider.create('Edge', edgeId, {
2216
+ from: typeName,
2217
+ name: rel.fieldName,
2218
+ to: rel.targetType,
2219
+ direction: 'forward',
2220
+ matchMode: 'fuzzy',
2221
+ similarity: rel.similarity,
2222
+ matchedType: rel.matchedType,
2223
+ fromId: entityId,
2224
+ toId: rel.targetId,
2225
+ });
2226
+ }
2227
+ catch (error) {
2228
+ // Only ignore actual duplicate key errors, propagate other errors
2229
+ if (!isEntityExistsError(error)) {
2230
+ throw wrapDatabaseError(error, 'create', 'Edge', edgeId);
2231
+ }
2232
+ }
2233
+ }
2234
+ }
2235
+ // If cascade is enabled, recursively generate related entities
2236
+ if (options?.cascade) {
2237
+ const progress = {
2238
+ phase: 'generating',
2239
+ currentDepth: 0,
2240
+ depth: 0,
2241
+ currentType: typeName,
2242
+ totalEntitiesCreated: 1, // Count the root entity
2243
+ typesGenerated: [typeName],
2244
+ };
2245
+ // Report initial progress
2246
+ options.onProgress?.({ ...progress });
2247
+ try {
2248
+ await cascadeGenerate(result, entity, schema, provider, options, 0, progress);
2249
+ // Report completion
2250
+ progress.phase = 'complete';
2251
+ options.onProgress?.({ ...progress });
2252
+ }
2253
+ catch (error) {
2254
+ progress.phase = 'error';
2255
+ options.onProgress?.({ ...progress });
2256
+ if (options.stopOnError)
2257
+ throw error;
2258
+ }
2259
+ }
2260
+ return hydrateEntity(result, entity, schema);
2261
+ },
2262
+ async update(id, data) {
2263
+ try {
2264
+ const provider = await resolveProvider();
2265
+ // Partial<Omit<T, ...>> is a subset of Record<string, unknown> - safe cast for provider API
2266
+ const result = await provider.update(typeName, id, data);
2267
+ return hydrateEntity(result, entity, schema);
2268
+ }
2269
+ catch (error) {
2270
+ if (error instanceof DatabaseError)
2271
+ throw error;
2272
+ throw wrapDatabaseError(error, 'update', typeName, id);
2273
+ }
2274
+ },
2275
+ async upsert(id, data) {
2276
+ try {
2277
+ const provider = await resolveProvider();
2278
+ const existing = await provider.get(typeName, id);
2279
+ // Omit<T, ...> is a subset of Record<string, unknown> - safe cast for provider API
2280
+ if (existing) {
2281
+ const result = await provider.update(typeName, id, data);
2282
+ return hydrateEntity(result, entity, schema);
2283
+ }
2284
+ const result = await provider.create(typeName, id, data);
2285
+ return hydrateEntity(result, entity, schema);
2286
+ }
2287
+ catch (error) {
2288
+ if (error instanceof DatabaseError)
2289
+ throw error;
2290
+ throw wrapDatabaseError(error, 'upsert', typeName, id);
2291
+ }
2292
+ },
2293
+ async delete(id) {
2294
+ try {
2295
+ const provider = await resolveProvider();
2296
+ const result = await provider.delete(typeName, id);
2297
+ return result;
2298
+ }
2299
+ catch (error) {
2300
+ if (error instanceof DatabaseError)
2301
+ throw error;
2302
+ throw wrapDatabaseError(error, 'delete', typeName, id);
2303
+ }
2304
+ },
2305
+ async forEach(optionsOrCallback, maybeCallback) {
2306
+ const options = typeof optionsOrCallback === 'function' ? undefined : optionsOrCallback;
2307
+ const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback;
2308
+ const items = await this.list(options);
2309
+ for (const item of items) {
2310
+ await callback(item);
2311
+ }
2312
+ },
2313
+ async semanticSearch(query, options) {
2314
+ const provider = await resolveProvider();
2315
+ if (!hasSemanticSearch(provider)) {
2316
+ throw new CapabilityNotSupportedError('hasSemanticSearch', `Semantic search is not supported by the current provider. ` +
2317
+ `The provider does not implement the semanticSearch method required for vector similarity search.`, `Use the regular search() method instead, which performs basic text matching.`);
2318
+ }
2319
+ const results = await provider.semanticSearch(typeName, query, options);
2320
+ return Promise.all(results.map((r) => ({
2321
+ ...hydrateEntity(r, entity, schema),
2322
+ $score: r.$score,
2323
+ })));
2324
+ },
2325
+ async hybridSearch(query, options) {
2326
+ const provider = await resolveProvider();
2327
+ if (!hasHybridSearch(provider)) {
2328
+ throw new CapabilityNotSupportedError('hasHybridSearch', `Hybrid search is not supported by the current provider. ` +
2329
+ `The provider does not implement the hybridSearch method required for combined FTS and vector search.`, `Use the regular search() method instead, which performs basic text matching.`);
2330
+ }
2331
+ const results = await provider.hybridSearch(typeName, query, options);
2332
+ return Promise.all(results.map((r) => ({
2333
+ ...hydrateEntity(r, entity, schema),
2334
+ $rrfScore: r.$rrfScore,
2335
+ $ftsRank: r.$ftsRank,
2336
+ $semanticRank: r.$semanticRank,
2337
+ $score: r.$score,
2338
+ })));
2339
+ },
2340
+ async draft(data, options) {
2341
+ const draftData = { ...data, $phase: 'draft' };
2342
+ const refs = {};
2343
+ // Process each field that has a relationship operator
2344
+ for (const [fieldName, field] of entity.fields) {
2345
+ // Skip if value already provided
2346
+ if (draftData[fieldName] !== undefined && draftData[fieldName] !== null) {
2347
+ continue;
2348
+ }
2349
+ // Handle non-relation prompt fields (like 'Write a detailed article')
2350
+ if (!field.isRelation) {
2351
+ const isPrompt = isPromptField(field);
2352
+ if (isPrompt) {
2353
+ // Build context for generation
2354
+ const ctxParts = [];
2355
+ const entitySchemaForCtx = entity.schema;
2356
+ if (entitySchemaForCtx?.['$instructions']) {
2357
+ ctxParts.push(entitySchemaForCtx['$instructions']);
2358
+ }
2359
+ for (const [k, v] of Object.entries(data)) {
2360
+ if (!k.startsWith('$') && !k.startsWith('_') && typeof v === 'string' && v) {
2361
+ ctxParts.push(`${k}: ${v}`);
2362
+ }
2363
+ }
2364
+ const draftCtx = ctxParts.filter(Boolean).join(' | ');
2365
+ const generatedText = generateContextAwareValue(fieldName, typeName, draftCtx, field.type);
2366
+ draftData[fieldName] = generatedText;
2367
+ if (options?.stream && options.onChunk) {
2368
+ options.onChunk(generatedText);
2369
+ }
2370
+ }
2371
+ continue;
2372
+ }
2373
+ // Only process fields with relationship operators
2374
+ if (field.operator && field.relatedType) {
2375
+ // Skip optional relation fields - they shouldn't auto-generate
2376
+ if (field.isOptional)
2377
+ continue;
2378
+ // Skip backward references - they are resolved lazily via hydrateEntity
2379
+ if (field.operator === '<-' || field.operator === '<~')
2380
+ continue;
2381
+ const matchMode = field.matchMode ?? (field.operator.includes('~') ? 'fuzzy' : 'exact');
2382
+ // data is Partial<Omit<T, ...>> which extends Record<string, unknown>
2383
+ // The cast is safe because we're accessing arbitrary properties
2384
+ const dataRecord = data;
2385
+ // Get fuzzy threshold: field-level overrides entity-level
2386
+ const entitySchemaRaw = entity.schema;
2387
+ const entityThreshold = entitySchemaRaw && '$fuzzyThreshold' in entitySchemaRaw
2388
+ ? entitySchemaRaw['$fuzzyThreshold']
2389
+ : undefined;
2390
+ const threshold = field.threshold ?? entityThreshold;
2391
+ if (field.isArray) {
2392
+ // Array relationship - check for hint values
2393
+ const hintKey = `${fieldName}Hint`;
2394
+ const hintValue = dataRecord[hintKey];
2395
+ const hints = Array.isArray(hintValue)
2396
+ ? hintValue
2397
+ : hintValue
2398
+ ? [hintValue]
2399
+ : [
2400
+ generateNaturalLanguageContent(fieldName, field.prompt, field.relatedType, dataRecord),
2401
+ ];
2402
+ const refSpecs = hints.map((hint) => {
2403
+ const generatedText = String(hint);
2404
+ return {
2405
+ field: fieldName,
2406
+ operator: field.operator,
2407
+ type: field.relatedType,
2408
+ matchMode,
2409
+ resolved: false,
2410
+ ...(field.unionTypes !== undefined && { unionTypes: field.unionTypes }),
2411
+ ...(field.prompt !== undefined && { prompt: field.prompt }),
2412
+ ...(generatedText !== undefined && { generatedText }),
2413
+ ...(threshold !== undefined && { threshold }),
2414
+ };
2415
+ });
2416
+ draftData[fieldName] = hints.map(String).join(', ');
2417
+ refs[fieldName] = refSpecs;
2418
+ if (options?.stream && options.onChunk) {
2419
+ for (const spec of refSpecs) {
2420
+ if (spec.generatedText) {
2421
+ options.onChunk(spec.generatedText);
2422
+ }
2423
+ }
2424
+ }
2425
+ }
2426
+ else {
2427
+ // Single relationship - check for hint values
2428
+ const hintKey = `${fieldName}Hint`;
2429
+ const hintValue = dataRecord[hintKey];
2430
+ const generatedText = hintValue ||
2431
+ generateNaturalLanguageContent(fieldName, field.prompt, field.relatedType, dataRecord);
2432
+ draftData[fieldName] = generatedText;
2433
+ refs[fieldName] = {
2434
+ field: fieldName,
2435
+ operator: field.operator,
2436
+ type: field.relatedType,
2437
+ matchMode,
2438
+ resolved: false,
2439
+ ...(field.unionTypes !== undefined && { unionTypes: field.unionTypes }),
2440
+ ...(field.prompt !== undefined && { prompt: field.prompt }),
2441
+ ...(generatedText !== undefined && { generatedText }),
2442
+ ...(threshold !== undefined && { threshold }),
2443
+ };
2444
+ if (options?.stream && options.onChunk) {
2445
+ options.onChunk(generatedText);
2446
+ }
2447
+ }
2448
+ }
2449
+ }
2450
+ draftData['$refs'] = refs;
2451
+ // draftData has all Draft<T> properties - safe to return as Draft<T>
2452
+ return draftData;
2453
+ },
2454
+ async resolve(draft, options) {
2455
+ // Validate that this is actually a draft - use type guard for safe access
2456
+ if (!isDraft(draft)) {
2457
+ throw new Error('Cannot resolve entity: not a draft (missing $phase: "draft")');
2458
+ }
2459
+ const provider = await resolveProvider();
2460
+ const resolved = { ...draft };
2461
+ const errors = [];
2462
+ // Inject $instructions from entity schema into resolved context for reference resolution
2463
+ const entityInstructions = entity.schema?.['$instructions'];
2464
+ if (entityInstructions && typeof entityInstructions === 'string') {
2465
+ resolved['$instructions'] = entityInstructions;
2466
+ }
2467
+ // Remove draft markers
2468
+ delete resolved['$refs'];
2469
+ resolved['$phase'] = 'resolved';
2470
+ // Extract refs using type guard - safe access since isDraft validated
2471
+ const refs = (extractRefs(draft) || {});
2472
+ // Resolve each reference
2473
+ for (const [fieldName, refSpec] of Object.entries(refs)) {
2474
+ try {
2475
+ if (Array.isArray(refSpec)) {
2476
+ // Array of references
2477
+ const resolvedIds = [];
2478
+ for (const spec of refSpec) {
2479
+ const resolvedId = await resolveReferenceSpec(spec, resolved, schema, provider);
2480
+ if (resolvedId) {
2481
+ resolvedIds.push(resolvedId);
2482
+ options?.onResolved?.(fieldName, resolvedId);
2483
+ }
2484
+ }
2485
+ resolved[fieldName] = resolvedIds;
2486
+ }
2487
+ else {
2488
+ // Single reference
2489
+ const resolvedId = await resolveReferenceSpec(refSpec, resolved, schema, provider);
2490
+ if (resolvedId) {
2491
+ resolved[fieldName] = resolvedId;
2492
+ // Mark as auto-generated so hydrateEntity creates a thenable proxy
2493
+ resolved[`${fieldName}$autoGenerated`] = true;
2494
+ options?.onResolved?.(fieldName, resolvedId);
2495
+ }
2496
+ }
2497
+ }
2498
+ catch (err) {
2499
+ const errorMsg = err instanceof Error ? err.message : String(err);
2500
+ if (options?.onError === 'skip') {
2501
+ errors.push({ field: fieldName, error: errorMsg });
2502
+ }
2503
+ else {
2504
+ throw err;
2505
+ }
2506
+ }
2507
+ }
2508
+ // Add $errors if onError mode is 'skip' (even if empty, to indicate skip mode was used)
2509
+ // or if there are actual errors
2510
+ if (errors.length > 0 || options?.onError === 'skip') {
2511
+ resolved['$errors'] = errors;
2512
+ }
2513
+ return resolved;
2514
+ },
2515
+ };
2516
+ }
2517
+ /**
2518
+ * Hydrate an entity with lazy-loaded relations
2519
+ *
2520
+ * For backward edges (direction === 'backward'), we query for entities
2521
+ * of the related type that have a reference pointing TO this entity.
2522
+ * This enables reverse lookups like "get all comments for a post".
2523
+ *
2524
+ * Backward reference resolution:
2525
+ * - Single backward ref with stored ID: resolve directly (e.g., member.team = teamId -> get Team by ID)
2526
+ * - Single backward ref without stored ID: find related entity that points to us via relations
2527
+ * - Array backward ref: find all entities of related type where their forward ref points to us
2528
+ */
2529
+ function hydrateEntity(data, entity, schema) {
2530
+ const hydrated = { ...data };
2531
+ const id = (data['$id'] || data['id']);
2532
+ const typeName = entity.name;
2533
+ // Add lazy getters for relations
2534
+ for (const [fieldName, field] of entity.fields) {
2535
+ if (field.isRelation && field.relatedType) {
2536
+ const relatedEntity = schema.entities.get(field.relatedType);
2537
+ if (!relatedEntity)
2538
+ continue;
2539
+ // Check if this is a backward edge
2540
+ const isBackward = field.direction === 'backward';
2541
+ // For backward single relations with stored IDs (user-provided backward refs),
2542
+ // create a thenable proxy so `await entity.parent` resolves to the related entity.
2543
+ if (isBackward && !field.isArray && data[fieldName] && typeof data[fieldName] === 'string') {
2544
+ const storedId = data[fieldName];
2545
+ const proxyTarget = {};
2546
+ const thenableProxy = new Proxy(proxyTarget, {
2547
+ get(target, prop) {
2548
+ if (prop === 'then') {
2549
+ return (resolve, reject) => {
2550
+ return (async () => {
2551
+ const provider = await resolveProvider();
2552
+ const result = await provider.get(field.relatedType, storedId);
2553
+ if (!result)
2554
+ return null;
2555
+ return hydrateEntity(result, relatedEntity, schema);
2556
+ })().then(resolve, reject);
2557
+ };
2558
+ }
2559
+ if (prop === Symbol.toPrimitive || prop === 'valueOf') {
2560
+ return () => storedId;
2561
+ }
2562
+ if (prop === 'toString') {
2563
+ return () => storedId;
2564
+ }
2565
+ if (prop === 'match') {
2566
+ return (regex) => storedId.match(regex);
2567
+ }
2568
+ if (prop === '$id') {
2569
+ return storedId;
2570
+ }
2571
+ return undefined;
2572
+ },
2573
+ });
2574
+ hydrated[fieldName] = thenableProxy;
2575
+ continue;
2576
+ }
2577
+ // For forward single relations with stored IDs, create a thenable proxy that:
2578
+ // - Acts like a string (for .toMatch(), String(), etc.)
2579
+ // - Can be awaited to get the related entity (thenable)
2580
+ if (!isBackward && !field.isArray && data[fieldName]) {
2581
+ const storedId = data[fieldName];
2582
+ // For union types, get the actual matched type from stored metadata
2583
+ const matchedType = data[`${fieldName}$matchedType`] || field.relatedType;
2584
+ const actualRelatedEntity = schema.entities.get(matchedType) || relatedEntity;
2585
+ // Create a thenable proxy - the empty object target is just a placeholder for the Proxy
2586
+ const proxyTarget = {};
2587
+ const thenableProxy = new Proxy(proxyTarget, {
2588
+ get(target, prop) {
2589
+ if (prop === 'then') {
2590
+ return (resolve, reject) => {
2591
+ return (async () => {
2592
+ const provider = await resolveProvider();
2593
+ const result = await loadEntity(provider, matchedType, storedId);
2594
+ if (!result)
2595
+ return null;
2596
+ const hydratedResult = hydrateEntity(result, actualRelatedEntity, schema);
2597
+ // Add $matchedType to the result for union type tracking
2598
+ if (field.unionTypes && field.unionTypes.length > 0) {
2599
+ hydratedResult.$matchedType = matchedType;
2600
+ }
2601
+ return hydratedResult;
2602
+ })().then(resolve, reject);
2603
+ };
2604
+ }
2605
+ if (prop === Symbol.toPrimitive || prop === 'valueOf') {
2606
+ return () => storedId;
2607
+ }
2608
+ if (prop === 'toString') {
2609
+ return () => storedId;
2610
+ }
2611
+ if (prop === 'match') {
2612
+ return (regex) => storedId.match(regex);
2613
+ }
2614
+ if (prop === '$type') {
2615
+ return matchedType;
2616
+ }
2617
+ return undefined;
2618
+ },
2619
+ });
2620
+ hydrated[fieldName] = thenableProxy;
2621
+ continue;
2622
+ }
2623
+ // For forward array relations with stored IDs, create a real array proxy
2624
+ // that passes Array.isArray() and supports synchronous .map()/.length
2625
+ // while also being thenable for async hydration
2626
+ if (!isBackward && field.isArray && data[fieldName]) {
2627
+ const storedIds = data[fieldName];
2628
+ if (Array.isArray(storedIds)) {
2629
+ const matchedTypes = data[`${fieldName}$matchedTypes`];
2630
+ // Create a real array so Array.isArray() returns true
2631
+ // Each element is a thenable proxy for the related entity
2632
+ const arrayResult = storedIds.map((targetId, index) => {
2633
+ const targetType = (field.unionTypes && matchedTypes?.[index]) || field.relatedType;
2634
+ const targetEntity = schema.entities.get(targetType) || relatedEntity;
2635
+ const proxyTarget = {};
2636
+ return new Proxy(proxyTarget, {
2637
+ get(_target, prop) {
2638
+ if (prop === 'then') {
2639
+ return (resolve, reject) => {
2640
+ return (async () => {
2641
+ const prov = await resolveProvider();
2642
+ const result = await loadEntity(prov, targetType, targetId);
2643
+ if (!result)
2644
+ return null;
2645
+ const hydratedResult = hydrateEntity(result, targetEntity, schema);
2646
+ if (field.unionTypes && field.unionTypes.length > 0) {
2647
+ hydratedResult.$matchedType = targetType;
2648
+ }
2649
+ return hydratedResult;
2650
+ })().then(resolve, reject);
2651
+ };
2652
+ }
2653
+ if (prop === Symbol.toPrimitive || prop === 'valueOf') {
2654
+ return () => targetId;
2655
+ }
2656
+ if (prop === 'toString') {
2657
+ return () => targetId;
2658
+ }
2659
+ if (prop === '$type') {
2660
+ return targetType;
2661
+ }
2662
+ if (prop === '$id') {
2663
+ return targetId;
2664
+ }
2665
+ return undefined;
2666
+ },
2667
+ });
2668
+ });
2669
+ arrayResult['$type'] = field.relatedType;
2670
+ arrayResult['$isArrayRelation'] = true;
2671
+ arrayResult['then'] = (resolve, reject) => {
2672
+ return (async () => {
2673
+ const prov = await resolveProvider();
2674
+ const results = await Promise.all(storedIds.map(async (targetId, index) => {
2675
+ const targetType = (field.unionTypes && matchedTypes?.[index]) || field.relatedType;
2676
+ const targetEntity = schema.entities.get(targetType) || relatedEntity;
2677
+ const result = await loadEntity(prov, targetType, targetId);
2678
+ if (!result)
2679
+ return null;
2680
+ const hydratedResult = hydrateEntity(result, targetEntity, schema);
2681
+ if (field.unionTypes && field.unionTypes.length > 0) {
2682
+ hydratedResult.$matchedType = targetType;
2683
+ }
2684
+ return hydratedResult;
2685
+ }));
2686
+ return results.filter((r) => r !== null);
2687
+ })().then(resolve, reject);
2688
+ };
2689
+ hydrated[fieldName] = arrayResult;
2690
+ continue;
2691
+ }
2692
+ }
2693
+ // For forward array relations with explicitly empty array data, set to empty array
2694
+ if (!isBackward &&
2695
+ field.isArray &&
2696
+ Array.isArray(data[fieldName]) &&
2697
+ data[fieldName].length === 0) {
2698
+ const emptyArray = [];
2699
+ emptyArray['$type'] = field.relatedType;
2700
+ emptyArray['$isArrayRelation'] = true;
2701
+ emptyArray['then'] = (resolve, _reject) => {
2702
+ return Promise.resolve([]).then(resolve);
2703
+ };
2704
+ hydrated[fieldName] = emptyArray;
2705
+ continue;
2706
+ }
2707
+ // Define lazy getter
2708
+ Object.defineProperty(hydrated, fieldName, {
2709
+ get: () => {
2710
+ // Check if this is a backward edge
2711
+ if (isBackward && !field.isArray) {
2712
+ // Case 1: Single backward ref
2713
+ // Returns a Promise that resolves to the related entity
2714
+ const storedId = data[fieldName];
2715
+ // For backward fuzzy with union types (single field, not array)
2716
+ // storedId is a single ID and matchedType is the type that matched
2717
+ if (field.unionTypes && field.operator === '<~') {
2718
+ return (async () => {
2719
+ const singleStoredId = data[fieldName];
2720
+ const matchedType = data[`${fieldName}$matchedType`];
2721
+ if (singleStoredId) {
2722
+ const provider = await resolveProvider();
2723
+ const targetType = matchedType || field.relatedType;
2724
+ const targetEntity = schema.entities.get(targetType);
2725
+ const result = await provider.get(targetType, singleStoredId);
2726
+ if (!result)
2727
+ return null;
2728
+ const hydratedEntity = targetEntity
2729
+ ? hydrateEntity(result, targetEntity, schema)
2730
+ : result;
2731
+ if (hydratedEntity) {
2732
+ ;
2733
+ hydratedEntity.$matchedType = targetType;
2734
+ }
2735
+ return hydratedEntity;
2736
+ }
2737
+ // No match found - return null for single fields (not empty array)
2738
+ return null;
2739
+ })();
2740
+ }
2741
+ return (async () => {
2742
+ const provider = await resolveProvider();
2743
+ if (storedId) {
2744
+ // Has stored ID - directly fetch the related entity
2745
+ const result = await provider.get(field.relatedType, storedId);
2746
+ return result ? hydrateEntity(result, relatedEntity, schema) : null;
2747
+ }
2748
+ // No stored ID - find via inverse relation lookup
2749
+ // Find entities of relatedType that have this entity in their relations
2750
+ for (const [relFieldName, relField] of relatedEntity.fields) {
2751
+ if (relField.isRelation &&
2752
+ relField.relatedType === typeName &&
2753
+ relField.direction !== 'backward' &&
2754
+ relField.isArray) {
2755
+ // Found a forward array relation on related entity pointing to us
2756
+ // Check if any entity of relatedType has this entity in that relation
2757
+ const allRelated = await provider.list(field.relatedType);
2758
+ for (const candidate of allRelated) {
2759
+ const candidateId = (candidate['$id'] || candidate['id']);
2760
+ const related = await provider.related(field.relatedType, candidateId, relFieldName);
2761
+ if (related.some((r) => (r['$id'] || r['id']) === id)) {
2762
+ return hydrateEntity(candidate, relatedEntity, schema);
2763
+ }
2764
+ }
2765
+ }
2766
+ }
2767
+ return null;
2768
+ })();
2769
+ }
2770
+ // For forward relations and backward arrays, return async resolver
2771
+ return (async () => {
2772
+ const provider = await resolveProvider();
2773
+ if (isBackward) {
2774
+ // Case 2: Array backward ref
2775
+ // Check if we have stored IDs (e.g., from backward fuzzy resolution)
2776
+ const storedIds = data[fieldName];
2777
+ const matchedTypes = data[`${fieldName}$matchedTypes`];
2778
+ if (Array.isArray(storedIds) && storedIds.length > 0) {
2779
+ // Use stored IDs directly - this handles backward fuzzy (<~) array fields
2780
+ // For union types, fetch from the correct type
2781
+ if (field.unionTypes && matchedTypes) {
2782
+ const results = await Promise.all(storedIds.map(async (targetId, index) => {
2783
+ const targetType = matchedTypes[index] || field.relatedType;
2784
+ const targetEntity = schema.entities.get(targetType);
2785
+ const result = await provider.get(targetType, targetId);
2786
+ if (!result)
2787
+ return null;
2788
+ const hydratedEntity = targetEntity
2789
+ ? hydrateEntity(result, targetEntity, schema)
2790
+ : result;
2791
+ if (hydratedEntity) {
2792
+ ;
2793
+ hydratedEntity.$matchedType = targetType;
2794
+ }
2795
+ return hydratedEntity;
2796
+ }));
2797
+ return results.filter((r) => r !== null);
2798
+ }
2799
+ else {
2800
+ const results = await Promise.all(storedIds.map((targetId) => provider.get(field.relatedType, targetId)));
2801
+ return Promise.all(results
2802
+ .filter((r) => r !== null)
2803
+ .map((r) => hydrateEntity(r, relatedEntity, schema)));
2804
+ }
2805
+ }
2806
+ // For backward fuzzy union types without stored IDs, return empty array
2807
+ // (the test expects at least an empty array, not null)
2808
+ if (field.unionTypes && field.operator === '<~') {
2809
+ return [];
2810
+ }
2811
+ // No stored IDs - use backref lookup
2812
+ // e.g., Blog.posts: ['<-Post'] - find Posts where post.blog === blog.$id
2813
+ // The backref tells us which field on the related type stores our ID
2814
+ // If no explicit backref, infer from schema relationships
2815
+ let backrefField = field.backref;
2816
+ if (!backrefField) {
2817
+ // Infer backref: look for a field on related entity that points to us
2818
+ for (const [relFieldName, relField] of relatedEntity.fields) {
2819
+ if (relField.isRelation &&
2820
+ relField.relatedType === typeName &&
2821
+ relField.direction !== 'backward' &&
2822
+ !relField.isArray) {
2823
+ // Found a forward single relation pointing to us - use its name
2824
+ backrefField = relFieldName;
2825
+ break;
2826
+ }
2827
+ }
2828
+ // Fallback to entity name lowercase if no explicit relation found
2829
+ if (!backrefField) {
2830
+ backrefField = typeName.toLowerCase();
2831
+ }
2832
+ }
2833
+ // Query the related type for entities that reference this entity
2834
+ const results = await provider.list(field.relatedType, {
2835
+ where: { [backrefField]: id },
2836
+ });
2837
+ return Promise.all(results.map((r) => hydrateEntity(r, relatedEntity, schema)));
2838
+ }
2839
+ else if (field.isArray) {
2840
+ // Forward array relation - get related entities
2841
+ // For union types, we need to look up each entity from its matched type
2842
+ const storedIds = data[fieldName];
2843
+ const matchedTypes = data[`${fieldName}$matchedTypes`];
2844
+ if (storedIds && storedIds.length > 0) {
2845
+ if (field.unionTypes && matchedTypes) {
2846
+ // Union type array - fetch each entity from its specific type
2847
+ const results = await Promise.all(storedIds.map(async (targetId, index) => {
2848
+ const targetType = matchedTypes[index] || field.relatedType;
2849
+ const targetEntity = schema.entities.get(targetType);
2850
+ const result = await provider.get(targetType, targetId);
2851
+ if (!result)
2852
+ return null;
2853
+ const hydratedEntity = targetEntity
2854
+ ? hydrateEntity(result, targetEntity, schema)
2855
+ : result;
2856
+ // Add $matchedType for union type tracking
2857
+ if (hydratedEntity) {
2858
+ ;
2859
+ hydratedEntity.$matchedType = targetType;
2860
+ }
2861
+ return hydratedEntity;
2862
+ }));
2863
+ return results.filter((r) => r !== null);
2864
+ }
2865
+ else {
2866
+ // Non-union type array with stored IDs - fetch directly by ID
2867
+ const results = await Promise.all(storedIds.map((targetId) => provider.get(field.relatedType, targetId)));
2868
+ return Promise.all(results
2869
+ .filter((r) => r !== null)
2870
+ .map((r) => hydrateEntity(r, relatedEntity, schema)));
2871
+ }
2872
+ }
2873
+ // No stored IDs - use standard relation lookup
2874
+ const results = await provider.related(entity.name, id, fieldName);
2875
+ return Promise.all(results.map((r) => hydrateEntity(r, relatedEntity, schema)));
2876
+ }
2877
+ else {
2878
+ // Forward single relation - get the stored ID and fetch
2879
+ const relatedId = data[fieldName];
2880
+ if (!relatedId)
2881
+ return null;
2882
+ const result = await provider.get(field.relatedType, relatedId);
2883
+ return result ? hydrateEntity(result, relatedEntity, schema) : null;
2884
+ }
2885
+ })();
2886
+ },
2887
+ enumerable: true,
2888
+ configurable: true,
2889
+ });
2890
+ }
2891
+ }
2892
+ return hydrated;
2893
+ }
82
2894
  // =============================================================================
83
- // Convenience alias
2895
+ // Re-export for convenience
84
2896
  // =============================================================================
85
- export { parseSchema as parse } from './schema/parse.js';
2897
+ export { parseSchema as parse };
86
2898
  //# sourceMappingURL=schema.js.map