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
@@ -0,0 +1,828 @@
1
+ /**
2
+ * Cascade Orchestrator — the moat work
3
+ *
4
+ * This module is the canonical, real cascade-generation engine for
5
+ * `ai-database`. It replaces the placeholder generator (`schema/cascade.ts`'s
6
+ * `PlaceholderValueGenerator`) for the SVO-shaped cascade surface and absorbs
7
+ * the algorithmic patterns proven by the
8
+ * `2026-05-06-cascade-via-ai-database-poc` (sibling-parallel `[->Type]`
9
+ * expansion, pre-derived child ids for backref correctness, rubric-style
10
+ * `$validate` with four verdict policies, embed-on-write) — but rebuilt on
11
+ * the canonical SVO foundation:
12
+ *
13
+ * - LLM calls go through `generate()` from `ai-functions` (auto-routed via
14
+ * `ai-providers`), wrapped with the per-model `ModelPolicy` from
15
+ * `language-models` (`RetryPolicy.forModel` + `FallbackChain.forModel`)
16
+ * so retry/circuit-breaker/fallback behaviour is consistent with the rest
17
+ * of the platform.
18
+ * - Generated entities are recorded as **Things** via the canonical
19
+ * `DBProvider` surface; relations are recorded as **Actions** with proper
20
+ * Frame role assignments (`subject`, `object`, plus optional Frame-role
21
+ * slots) per the `digital-objects` SVO ontology.
22
+ * - Verbs auto-register via `provider.defineVerb(...)` on first use when
23
+ * the adapter exposes the Verb registry surface.
24
+ * - Sharded writes flow through {@link CascadeWriteStrategy} so per-cascade
25
+ * DO isolation (Stack B) and partitioned-by-tenant Postgres (Stack A) get
26
+ * the right shape without leaking the choice into orchestrator code.
27
+ * - Read-back-during-traversal goes through {@link CascadeWriteStrategy.readShardLocal}
28
+ * so cascade reads land on the same shard that received the write.
29
+ *
30
+ * ## What's deliberately NOT in this module
31
+ *
32
+ * - **Pipelines → Iceberg dual-write.** That's bead `aip-0ypt`; the
33
+ * orchestrator surfaces an `analyticalEmitter` on the underlying
34
+ * {@link CascadeWriteStrategy} which `aip-0ypt` will wire later.
35
+ * - **CLI / Worker entrypoint.** The library function {@link generateCascade}
36
+ * is the surface; deployment shapes live downstream.
37
+ * - **Observability beyond debug logging.** Telemetry / tracing is its own
38
+ * bead.
39
+ * - **The schema-DSL cascade engine** (`schema/cascade.ts`). That module
40
+ * drives generation from the parsed `[->Type]` / `<-Type` schema syntax;
41
+ * this orchestrator drives generation from a {@link CascadeSpec} at the
42
+ * SVO level. Both can coexist; this is the canonical moat path.
43
+ *
44
+ * @see {@link ../docs/adr/0003-storage-strategy-pg-clickhouse-default.md}
45
+ * @see {@link ../docs/reviews/2026-05-05-cascade-poc-evaluation.md}
46
+ * @see {@link ../docs/plans/2026-05-05-cascade-storage-execution-implementation.md}
47
+ * @packageDocumentation
48
+ */
49
+ import { hasActionRecording, hasVerbRegistry } from './db-provider-port.js';
50
+ import { CascadeWriteStrategy, createCascadeWriteStrategy, } from './cascade-write-strategy.js';
51
+ /**
52
+ * Thrown when a rubric verdict is `'escalate'` — the caller must catch
53
+ * this and decide whether to fail the cascade or recover.
54
+ */
55
+ export class CascadeValidationEscalation extends Error {
56
+ noun;
57
+ data;
58
+ scores;
59
+ path;
60
+ constructor(input) {
61
+ super(`Cascade validation escalated for ${input.noun} at ${input.path.join('/')}: ${input.feedback ?? 'rubric verdict was "escalate"'}`);
62
+ this.name = 'CascadeValidationEscalation';
63
+ this.noun = input.noun;
64
+ this.data = input.data;
65
+ this.scores = input.scores;
66
+ this.path = input.path;
67
+ }
68
+ }
69
+ // =============================================================================
70
+ // Public API
71
+ // =============================================================================
72
+ /**
73
+ * Run a cascade generation against an adapter.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * import { generateCascade, createMemoryProvider } from 'ai-database'
78
+ *
79
+ * const result = await generateCascade({
80
+ * adapter: createMemoryProvider(),
81
+ * rootNoun: 'Customer',
82
+ * rootHints: { industry: 'B2B SaaS', size: 'enterprise' },
83
+ * children: [
84
+ * {
85
+ * noun: 'Order',
86
+ * count: 5,
87
+ * verb: 'placedBy',
88
+ * hints: { stage: 'closed-won' },
89
+ * children: [
90
+ * { noun: 'OrderItem', count: [2, 5], verb: 'partOf' },
91
+ * ],
92
+ * },
93
+ * ],
94
+ * model: 'sonnet',
95
+ * })
96
+ *
97
+ * console.log(result.root.$id) // root Customer id
98
+ * console.log(result.stats.written) // 1 + 5 + (2..5)*5
99
+ * ```
100
+ */
101
+ export async function generateCascade(options) {
102
+ const startedAt = Date.now();
103
+ const model = options.model ?? 'sonnet';
104
+ const validationModel = options.validationModel ?? model;
105
+ const maxDepth = options.maxDepth ?? 5;
106
+ const maxRegenerationAttempts = options.maxRegenerationAttempts ?? 2;
107
+ const debug = options.debug ?? (() => { });
108
+ // Stable cascade id — caller-supplied wins; otherwise content-hash the spec.
109
+ const cascadeId = options.cascadeId ??
110
+ contentHashId('cascade', {
111
+ rootNoun: options.rootNoun,
112
+ rootHints: options.rootHints ?? {},
113
+ children: options.children ?? [],
114
+ });
115
+ // Build the write strategy (caller-supplied wins).
116
+ const strategy = options.strategy ??
117
+ createCascadeWriteStrategy({
118
+ adapter: options.adapter,
119
+ ...(options.sharding !== undefined && { sharding: options.sharding }),
120
+ defaultCascadeId: cascadeId,
121
+ ...(options.tenantId !== undefined && { defaultTenantId: options.tenantId }),
122
+ ...(options.analyticalEmitter !== undefined && {
123
+ analyticalEmitter: options.analyticalEmitter,
124
+ }),
125
+ });
126
+ const shard = strategy.pickShard({
127
+ cascadeId,
128
+ ...(options.tenantId !== undefined && { tenantId: options.tenantId }),
129
+ rootEntity: { $id: '__pending__', $type: options.rootNoun },
130
+ });
131
+ // LLM hooks — defaults call ai-functions; tests pass mocks.
132
+ const generator = options.generator ?? defaultGenerator;
133
+ const validator = options.validator ?? defaultValidator;
134
+ // State accumulators.
135
+ const thingsById = new Map();
136
+ const actions = [];
137
+ const rejected = [];
138
+ let regenerationAttempts = 0;
139
+ let embedded = 0;
140
+ // Verb registry — auto-define on first use.
141
+ const definedVerbs = new Set();
142
+ const ensureVerb = async (verb) => {
143
+ if (definedVerbs.has(verb))
144
+ return;
145
+ definedVerbs.add(verb);
146
+ if (hasVerbRegistry(options.adapter)) {
147
+ try {
148
+ await options.adapter.defineVerb({ name: verb });
149
+ }
150
+ catch {
151
+ // Verb may already exist; ignore.
152
+ }
153
+ }
154
+ };
155
+ // ---------------------------------------------------------------------------
156
+ // Generate root.
157
+ // ---------------------------------------------------------------------------
158
+ debug('cascade:start', { cascadeId, rootNoun: options.rootNoun });
159
+ const rootGen = await generateAndValidate({
160
+ noun: options.rootNoun,
161
+ hints: options.rootHints ?? {},
162
+ rootNoun: options.rootNoun,
163
+ cascadeId,
164
+ path: [options.rootNoun],
165
+ parentKey: cascadeId,
166
+ rubric: options.validate,
167
+ generator,
168
+ validator,
169
+ model,
170
+ validationModel,
171
+ maxRegenerationAttempts,
172
+ debug,
173
+ onRegenerate: () => {
174
+ regenerationAttempts += 1;
175
+ },
176
+ });
177
+ if (rootGen.outcome === 'rejected') {
178
+ rejected.push({
179
+ noun: options.rootNoun,
180
+ data: rootGen.data,
181
+ scores: rootGen.scores,
182
+ ...(rootGen.feedback !== undefined && { feedback: rootGen.feedback }),
183
+ path: [options.rootNoun],
184
+ });
185
+ return finishResult({
186
+ cascadeId,
187
+ root: undefined,
188
+ thingsById,
189
+ actions,
190
+ rejected,
191
+ regenerationAttempts,
192
+ embedded,
193
+ startedAt,
194
+ });
195
+ }
196
+ // Embed before write so reads-back-during-traversal see the embedding.
197
+ if (options.embedder) {
198
+ const text = buildEmbedText(rootGen.data);
199
+ if (text) {
200
+ try {
201
+ const vec = await options.embedder(text);
202
+ rootGen.data['$embedding'] = vec;
203
+ embedded += 1;
204
+ }
205
+ catch (err) {
206
+ debug('cascade:embed:fail', { noun: options.rootNoun, error: errorMessage(err) });
207
+ }
208
+ }
209
+ }
210
+ const rootId = deriveEntityId(options.rootNoun, rootGen.data, cascadeId, [options.rootNoun]);
211
+ const rootEntity = {
212
+ $id: rootId,
213
+ $type: options.rootNoun,
214
+ data: rootGen.data,
215
+ path: [options.rootNoun],
216
+ ...(rootGen.scores && Object.keys(rootGen.scores).length > 0
217
+ ? { validationScores: rootGen.scores }
218
+ : {}),
219
+ };
220
+ thingsById.set(rootId, rootEntity);
221
+ // Write the root via the strategy fast path.
222
+ await strategy.writeBatch(shard, {
223
+ things: [{ id: rootId, type: options.rootNoun, data: rootGen.data }],
224
+ });
225
+ debug('cascade:root:written', { id: rootId, noun: options.rootNoun });
226
+ // ---------------------------------------------------------------------------
227
+ // Descend into children — sibling-parallel within each spec.
228
+ // ---------------------------------------------------------------------------
229
+ if (options.children && options.children.length > 0 && maxDepth > 1) {
230
+ await descendChildren({
231
+ parent: rootEntity,
232
+ childSpecs: options.children,
233
+ depth: 1,
234
+ maxDepth,
235
+ cascadeId,
236
+ strategy,
237
+ shard,
238
+ generator,
239
+ validator,
240
+ model,
241
+ validationModel,
242
+ maxRegenerationAttempts,
243
+ embedder: options.embedder,
244
+ debug,
245
+ thingsById,
246
+ actions,
247
+ rejected,
248
+ ensureVerb,
249
+ initiator: options.initiator,
250
+ onRegenerate: () => {
251
+ regenerationAttempts += 1;
252
+ },
253
+ onEmbed: () => {
254
+ embedded += 1;
255
+ },
256
+ });
257
+ }
258
+ return finishResult({
259
+ cascadeId,
260
+ root: rootEntity,
261
+ thingsById,
262
+ actions,
263
+ rejected,
264
+ regenerationAttempts,
265
+ embedded,
266
+ startedAt,
267
+ });
268
+ }
269
+ async function descendChildren(args) {
270
+ if (args.depth >= args.maxDepth) {
271
+ args.debug('cascade:max-depth-reached', { depth: args.depth, parent: args.parent.$id });
272
+ return;
273
+ }
274
+ // Pre-derive child ids per spec so grandchildren backrefs land cleanly,
275
+ // and so the rubric-rejected children don't leak ids into the cascade.
276
+ for (const spec of args.childSpecs) {
277
+ await args.ensureVerb(spec.verb);
278
+ const count = resolveCount(spec.count);
279
+ if (count <= 0)
280
+ continue;
281
+ args.debug('cascade:children:start', {
282
+ parent: args.parent.$id,
283
+ noun: spec.noun,
284
+ count,
285
+ verb: spec.verb,
286
+ });
287
+ // Sibling-parallel fan-out — every sibling generated concurrently.
288
+ const siblingPromises = Array.from({ length: count }, (_, siblingIndex) => generateOneSibling({
289
+ spec,
290
+ siblingIndex,
291
+ siblingCount: count,
292
+ parent: args.parent,
293
+ cascadeId: args.cascadeId,
294
+ generator: args.generator,
295
+ validator: args.validator,
296
+ model: args.model,
297
+ validationModel: args.validationModel,
298
+ maxRegenerationAttempts: args.maxRegenerationAttempts,
299
+ embedder: args.embedder,
300
+ rootData: args.thingsById.get(args.parent.path[0] ?? args.parent.$id)?.data,
301
+ rootNoun: args.parent.path[0] ?? args.parent.$type,
302
+ debug: args.debug,
303
+ onRegenerate: args.onRegenerate,
304
+ onEmbed: args.onEmbed,
305
+ }));
306
+ const siblingResults = await Promise.all(siblingPromises);
307
+ // Collect accepted siblings; queue Things + Actions for the batch commit.
308
+ const things = [];
309
+ const cascadeActions = [];
310
+ const acceptedSiblings = [];
311
+ for (const result of siblingResults) {
312
+ if (result.outcome === 'rejected') {
313
+ args.rejected.push({
314
+ noun: spec.noun,
315
+ data: result.data,
316
+ scores: result.scores,
317
+ ...(result.feedback !== undefined && { feedback: result.feedback }),
318
+ path: result.path,
319
+ parentId: args.parent.$id,
320
+ });
321
+ continue;
322
+ }
323
+ const childEntity = {
324
+ $id: result.id,
325
+ $type: spec.noun,
326
+ data: result.data,
327
+ path: result.path,
328
+ parentId: args.parent.$id,
329
+ ...(result.scores && Object.keys(result.scores).length > 0
330
+ ? { validationScores: result.scores }
331
+ : {}),
332
+ };
333
+ args.thingsById.set(result.id, childEntity);
334
+ acceptedSiblings.push(childEntity);
335
+ things.push({ id: result.id, type: spec.noun, data: result.data });
336
+ // Build the Action with proper Frame role assignment. Subject is the
337
+ // child (it acts via the verb); object is the parent. Tokens
338
+ // `'$parent'`/`'$root'` in `spec.roles` get resolved.
339
+ const resolvedRoles = resolveRoleTokens(spec.roles, args.parent, args.thingsById);
340
+ cascadeActions.push({
341
+ verb: spec.verb,
342
+ subject: result.id,
343
+ object: args.parent.$id,
344
+ ...(args.initiator !== undefined && {
345
+ roles: { ...resolvedRoles, source: args.initiator },
346
+ }),
347
+ ...(args.initiator === undefined && Object.keys(resolvedRoles).length > 0
348
+ ? { roles: resolvedRoles }
349
+ : {}),
350
+ status: 'completed',
351
+ });
352
+ args.actions.push({
353
+ verb: spec.verb,
354
+ subject: result.id,
355
+ object: args.parent.$id,
356
+ ...(Object.keys(resolvedRoles).length > 0 ? { roles: resolvedRoles } : {}),
357
+ });
358
+ }
359
+ // Single batch commit per child spec — Things + Actions in one round-trip
360
+ // through the strategy's fast path (CTE jsonb-bulk for PG).
361
+ if (things.length > 0 || cascadeActions.length > 0) {
362
+ await args.strategy.writeBatch(args.shard, { things, actions: cascadeActions });
363
+ }
364
+ args.debug('cascade:children:written', {
365
+ parent: args.parent.$id,
366
+ noun: spec.noun,
367
+ accepted: acceptedSiblings.length,
368
+ rejected: count - acceptedSiblings.length,
369
+ });
370
+ // Recurse into grandchildren — sequentially per accepted sibling so that
371
+ // the cascade tree builds depth-first within each branch. Sibling-level
372
+ // parallelism is preserved at the same level (we just generated all
373
+ // siblings concurrently).
374
+ if (spec.children && spec.children.length > 0 && args.depth + 1 < args.maxDepth) {
375
+ for (const sibling of acceptedSiblings) {
376
+ await descendChildren({
377
+ ...args,
378
+ parent: sibling,
379
+ childSpecs: spec.children,
380
+ depth: args.depth + 1,
381
+ });
382
+ }
383
+ }
384
+ }
385
+ }
386
+ async function generateAndValidate(args) {
387
+ let attempt = 0;
388
+ let feedback;
389
+ while (true) {
390
+ const data = await args.generator({
391
+ noun: args.noun,
392
+ hints: args.hints,
393
+ ...(args.parentNoun !== undefined && { parentNoun: args.parentNoun }),
394
+ ...(args.parentData !== undefined && { parentData: args.parentData }),
395
+ rootNoun: args.rootNoun,
396
+ ...(args.rootData !== undefined && { rootData: args.rootData }),
397
+ cascadeId: args.cascadeId,
398
+ path: args.path,
399
+ ...(args.siblingIndex !== undefined && { siblingIndex: args.siblingIndex }),
400
+ ...(args.siblingCount !== undefined && { siblingCount: args.siblingCount }),
401
+ ...(feedback !== undefined && { feedback }),
402
+ model: args.model,
403
+ });
404
+ // No rubric → accept verbatim.
405
+ if (!args.rubric) {
406
+ return { outcome: 'accepted', data, scores: {} };
407
+ }
408
+ const verdict = await args.validator({
409
+ noun: args.noun,
410
+ data,
411
+ rubric: args.rubric,
412
+ model: args.validationModel,
413
+ ...(args.parentData !== undefined && { parentData: args.parentData }),
414
+ });
415
+ const passed = applyRubricPolicy(args.rubric, verdict.scores);
416
+ args.debug('cascade:validate', {
417
+ noun: args.noun,
418
+ path: args.path,
419
+ passed,
420
+ attempt,
421
+ scores: verdict.scores,
422
+ });
423
+ if (passed) {
424
+ return {
425
+ outcome: 'accepted',
426
+ data,
427
+ scores: verdict.scores,
428
+ ...(verdict.feedback !== undefined && { feedback: verdict.feedback }),
429
+ };
430
+ }
431
+ // Failed verdict — apply onFail policy.
432
+ const onFail = args.rubric.onFail ?? 'reject';
433
+ if (onFail === 'escalate') {
434
+ throw new CascadeValidationEscalation({
435
+ noun: args.noun,
436
+ data,
437
+ scores: verdict.scores,
438
+ path: args.path,
439
+ ...(verdict.feedback !== undefined && { feedback: verdict.feedback }),
440
+ });
441
+ }
442
+ if (onFail === 'regenerate' && attempt < args.maxRegenerationAttempts) {
443
+ attempt += 1;
444
+ args.onRegenerate();
445
+ feedback =
446
+ verdict.feedback ??
447
+ `Previous attempt failed validation: ${JSON.stringify(verdict.scores)}. Improve.`;
448
+ continue;
449
+ }
450
+ // reject (or regenerate exhausted) → return rejected.
451
+ return {
452
+ outcome: 'rejected',
453
+ data,
454
+ scores: verdict.scores,
455
+ ...(verdict.feedback !== undefined && { feedback: verdict.feedback }),
456
+ };
457
+ }
458
+ }
459
+ async function generateOneSibling(args) {
460
+ const path = [...args.parent.path, `${args.spec.noun}:${args.siblingIndex}`];
461
+ const result = await generateAndValidate({
462
+ noun: args.spec.noun,
463
+ hints: args.spec.hints ?? {},
464
+ rootNoun: args.rootNoun,
465
+ cascadeId: args.cascadeId,
466
+ path,
467
+ parentKey: args.parent.$id,
468
+ parentNoun: args.parent.$type,
469
+ parentData: args.parent.data,
470
+ ...(args.rootData !== undefined && { rootData: args.rootData }),
471
+ siblingIndex: args.siblingIndex,
472
+ siblingCount: args.siblingCount,
473
+ ...(args.spec.validate !== undefined && { rubric: args.spec.validate }),
474
+ rubric: args.spec.validate,
475
+ generator: args.generator,
476
+ validator: args.validator,
477
+ model: args.model,
478
+ validationModel: args.validationModel,
479
+ maxRegenerationAttempts: args.maxRegenerationAttempts,
480
+ debug: args.debug,
481
+ onRegenerate: args.onRegenerate,
482
+ });
483
+ if (result.outcome === 'rejected') {
484
+ return {
485
+ outcome: 'rejected',
486
+ id: '',
487
+ data: result.data,
488
+ scores: result.scores,
489
+ ...(result.feedback !== undefined && { feedback: result.feedback }),
490
+ path,
491
+ };
492
+ }
493
+ // Embed BEFORE write so the embedding lands in the same write as the data.
494
+ if (args.embedder) {
495
+ const text = buildEmbedText(result.data);
496
+ if (text) {
497
+ try {
498
+ const vec = await args.embedder(text);
499
+ result.data['$embedding'] = vec;
500
+ args.onEmbed();
501
+ }
502
+ catch (err) {
503
+ args.debug('cascade:embed:fail', { noun: args.spec.noun, error: errorMessage(err) });
504
+ }
505
+ }
506
+ }
507
+ const id = deriveEntityId(args.spec.noun, result.data, args.parent.$id, path);
508
+ return {
509
+ outcome: 'accepted',
510
+ id,
511
+ data: result.data,
512
+ scores: result.scores,
513
+ ...(result.feedback !== undefined && { feedback: result.feedback }),
514
+ path,
515
+ };
516
+ }
517
+ // =============================================================================
518
+ // Internal — id derivation, content hashing, role token resolution
519
+ // =============================================================================
520
+ /**
521
+ * Derive a stable entity id from (parentKey + noun + path + sortable data).
522
+ *
523
+ * This is the lineage-key discipline from `cf-substrate/tierA-cascade`:
524
+ * every child's id includes the parent's full idempotency key, so sibling
525
+ * children at the same position across two cascade runs cannot collide.
526
+ *
527
+ * Same inputs → same id → idempotency on the write path
528
+ * (`ON CONFLICT DO NOTHING`).
529
+ */
530
+ function deriveEntityId(noun, data, parentKey, path) {
531
+ return `${noun.toLowerCase()}-${contentHashId(noun, {
532
+ parentKey,
533
+ path: [...path],
534
+ data: stableData(data),
535
+ })}`;
536
+ }
537
+ /**
538
+ * Content-hash an object to a short stable string. FNV-1a 32-bit, hex, 8 chars.
539
+ */
540
+ function contentHashId(prefix, value) {
541
+ const json = stableStringify(value);
542
+ let h = 0x811c9dc5;
543
+ for (let i = 0; i < json.length; i++) {
544
+ h ^= json.charCodeAt(i);
545
+ h = (h * 0x01000193) >>> 0;
546
+ }
547
+ // 32 bits of stable hash → 8 hex chars. Combine with a second pass for
548
+ // a 16-char id (still short, much lower collision risk than 8).
549
+ const second = (h ^ 0xdeadbeef) >>> 0;
550
+ let h2 = 0x811c9dc5;
551
+ for (let i = 0; i < json.length; i++) {
552
+ h2 ^= json.charCodeAt((i * 7) % json.length) ^ ((second >>> i % 32) & 0xff);
553
+ h2 = (h2 * 0x01000193) >>> 0;
554
+ }
555
+ const a = h.toString(16).padStart(8, '0');
556
+ const b = h2.toString(16).padStart(8, '0');
557
+ return `${prefix.toLowerCase().replace(/[^a-z0-9]/g, '')}-${a}${b}`;
558
+ }
559
+ /**
560
+ * Stable JSON stringify — keys sorted recursively. Required because object
561
+ * key order isn't guaranteed across runs / engines, which would yield
562
+ * different content hashes for "the same" data.
563
+ */
564
+ function stableStringify(value) {
565
+ return JSON.stringify(value, (_key, val) => {
566
+ if (val === null || typeof val !== 'object' || Array.isArray(val))
567
+ return val;
568
+ const sortedKeys = Object.keys(val).sort();
569
+ const result = {};
570
+ for (const k of sortedKeys) {
571
+ result[k] = val[k];
572
+ }
573
+ return result;
574
+ });
575
+ }
576
+ /**
577
+ * Strip transient fields (timestamps, embeddings, internal `$_` fields)
578
+ * before hashing — id stability shouldn't depend on volatile data.
579
+ */
580
+ function stableData(data) {
581
+ const out = {};
582
+ for (const [k, v] of Object.entries(data)) {
583
+ if (k === 'createdAt' || k === 'updatedAt' || k === '$embedding')
584
+ continue;
585
+ if (k.startsWith('_'))
586
+ continue;
587
+ out[k] = v;
588
+ }
589
+ return out;
590
+ }
591
+ /**
592
+ * Resolve `'$parent'`/`'$root'` tokens in role assignments.
593
+ */
594
+ function resolveRoleTokens(roles, parent, thingsById) {
595
+ if (!roles)
596
+ return {};
597
+ const out = {};
598
+ for (const [role, value] of Object.entries(roles)) {
599
+ if (typeof value !== 'string')
600
+ continue;
601
+ if (value === '$parent') {
602
+ out[role] = parent.$id;
603
+ }
604
+ else if (value === '$root') {
605
+ // Find the root by walking parent.path (root is path[0]); thingsById's
606
+ // first entry is the root in our orchestrator order.
607
+ const rootEntry = thingsById.values().next().value;
608
+ if (rootEntry)
609
+ out[role] = rootEntry.$id;
610
+ }
611
+ else {
612
+ out[role] = value;
613
+ }
614
+ }
615
+ return out;
616
+ }
617
+ /**
618
+ * Resolve a `count` spec to an integer: literal, or randomized between
619
+ * `[min, max]` (deterministic via the cascade content hash, but for now
620
+ * we use Math.random — the LLM honours hints and the count is a soft
621
+ * target).
622
+ */
623
+ function resolveCount(spec) {
624
+ if (typeof spec === 'number')
625
+ return Math.max(0, Math.floor(spec));
626
+ const [min, max] = spec;
627
+ if (min < 0 || max < min)
628
+ return 0;
629
+ if (min === max)
630
+ return min;
631
+ return Math.floor(min + Math.random() * (max - min + 1));
632
+ }
633
+ // =============================================================================
634
+ // Internal — rubric policy evaluation
635
+ // =============================================================================
636
+ function applyRubricPolicy(rubric, scores) {
637
+ const threshold = rubric.threshold ?? 0.7;
638
+ switch (rubric.policy) {
639
+ case 'all-pass': {
640
+ for (const c of rubric.criteria) {
641
+ const s = scores[c.name];
642
+ if (s === undefined || s < (rubric.threshold ?? 1))
643
+ return false;
644
+ }
645
+ return true;
646
+ }
647
+ case 'all-load-bearing-pass': {
648
+ for (const c of rubric.criteria) {
649
+ if (!c.loadBearing)
650
+ continue;
651
+ const s = scores[c.name];
652
+ if (s === undefined || s < (rubric.threshold ?? 1))
653
+ return false;
654
+ }
655
+ return true;
656
+ }
657
+ case 'mean-ge-threshold': {
658
+ const values = [];
659
+ for (const c of rubric.criteria) {
660
+ const s = scores[c.name];
661
+ if (s === undefined)
662
+ return false;
663
+ values.push(s);
664
+ }
665
+ if (values.length === 0)
666
+ return false;
667
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
668
+ return mean >= threshold;
669
+ }
670
+ case 'weighted-ge-threshold': {
671
+ let weightedSum = 0;
672
+ let weightTotal = 0;
673
+ for (const c of rubric.criteria) {
674
+ const s = scores[c.name];
675
+ if (s === undefined)
676
+ return false;
677
+ const w = c.weight ?? 1;
678
+ weightedSum += s * w;
679
+ weightTotal += w;
680
+ }
681
+ if (weightTotal === 0)
682
+ return false;
683
+ return weightedSum / weightTotal >= threshold;
684
+ }
685
+ }
686
+ }
687
+ // =============================================================================
688
+ // Internal — embed-text builder
689
+ // =============================================================================
690
+ /**
691
+ * Build the embedding text from a Thing's data, preferring human-readable
692
+ * fields per the POC's `embedTextFor`. Falls back to JSON if no preferred
693
+ * fields exist.
694
+ */
695
+ export function buildEmbedText(data) {
696
+ const preferred = ['name', 'label', 'title', 'description', 'tagline', 'summary', 'rationale'];
697
+ const parts = [];
698
+ for (const key of preferred) {
699
+ const v = data[key];
700
+ if (typeof v === 'string' && v.trim().length > 0) {
701
+ parts.push(v.trim());
702
+ }
703
+ }
704
+ if (parts.length > 0)
705
+ return parts.join(' — ');
706
+ // Fallback: JSON-stringify scalar fields.
707
+ const scalars = [];
708
+ for (const [k, v] of Object.entries(data)) {
709
+ if (k.startsWith('$') || k.startsWith('_'))
710
+ continue;
711
+ if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
712
+ scalars.push(`${k}: ${String(v)}`);
713
+ }
714
+ }
715
+ return scalars.join(' | ');
716
+ }
717
+ // =============================================================================
718
+ // Internal — default LLM hooks (lazy import; tests pass mocks)
719
+ // =============================================================================
720
+ const defaultGenerator = async (input) => {
721
+ // Lazy import so tests / non-LLM environments don't pay the dep cost.
722
+ const { generateObject } = await import('ai-functions');
723
+ // Compose a simple schema: every hint key becomes a string field; the LLM
724
+ // fills in additional fields. Callers wanting strict schemas should pass a
725
+ // custom generator.
726
+ const schema = {};
727
+ for (const [k, v] of Object.entries(input.hints)) {
728
+ schema[k] = typeof v === 'string' ? v : `Generate a ${k}`;
729
+ }
730
+ // Default-fill at least one field so generateObject has work to do.
731
+ if (Object.keys(schema).length === 0) {
732
+ schema['name'] = `Generate a name for the ${input.noun}`;
733
+ schema['description'] = `Generate a description for the ${input.noun}`;
734
+ }
735
+ const promptParts = [`Generate a ${input.noun} entity for cascade ${input.cascadeId}.`];
736
+ if (input.parentNoun) {
737
+ promptParts.push(`Parent: ${input.parentNoun}${input.parentData ? ` — ${stableStringify(stableData(input.parentData))}` : ''}`);
738
+ }
739
+ if (input.siblingCount && input.siblingCount > 1 && input.siblingIndex !== undefined) {
740
+ promptParts.push(`This is sibling ${input.siblingIndex + 1} of ${input.siblingCount}; vary from siblings.`);
741
+ }
742
+ if (input.feedback) {
743
+ promptParts.push(`Reviewer feedback to address: ${input.feedback}`);
744
+ }
745
+ // Wrap the call with model policy (retry + fallback). Failures bubble.
746
+ let policy = null;
747
+ try {
748
+ const { RetryPolicy } = await import('ai-functions');
749
+ policy = RetryPolicy.forModel(input.model);
750
+ }
751
+ catch {
752
+ // language-models / retry not available — call generateObject directly.
753
+ }
754
+ const call = async () => {
755
+ const { object } = await generateObject({
756
+ model: input.model,
757
+ schema,
758
+ prompt: promptParts.join('\n'),
759
+ });
760
+ return object;
761
+ };
762
+ if (policy) {
763
+ return policy.execute(() => call());
764
+ }
765
+ return call();
766
+ };
767
+ const defaultValidator = async (input) => {
768
+ const { generateObject } = await import('ai-functions');
769
+ // Build a schema that asks for one score per criterion, plus optional feedback.
770
+ const schema = { feedback: 'Brief feedback on what to improve.' };
771
+ for (const c of input.rubric.criteria) {
772
+ schema[c.name] = `Score 0-1 for: ${c.description}`;
773
+ }
774
+ const prompt = [
775
+ `Evaluate this ${input.noun} against the rubric. Return a numeric score 0..1 for each criterion, and brief feedback.`,
776
+ `Entity: ${stableStringify(stableData(input.data))}`,
777
+ input.parentData ? `Parent context: ${stableStringify(stableData(input.parentData))}` : '',
778
+ `Criteria: ${input.rubric.criteria.map((c) => `- ${c.name}: ${c.description}`).join('\n')}`,
779
+ ]
780
+ .filter(Boolean)
781
+ .join('\n\n');
782
+ const { object } = await generateObject({
783
+ model: input.model,
784
+ schema,
785
+ prompt,
786
+ });
787
+ const obj = object;
788
+ const scores = {};
789
+ for (const c of input.rubric.criteria) {
790
+ const v = obj[c.name];
791
+ scores[c.name] = typeof v === 'number' ? Math.max(0, Math.min(1, v)) : 0;
792
+ }
793
+ return {
794
+ scores,
795
+ ...(typeof obj['feedback'] === 'string' && { feedback: obj['feedback'] }),
796
+ };
797
+ };
798
+ function finishResult(args) {
799
+ return {
800
+ cascadeId: args.cascadeId,
801
+ root: args.root ?? {
802
+ $id: '',
803
+ $type: '',
804
+ data: {},
805
+ path: [],
806
+ },
807
+ thingsById: args.thingsById,
808
+ actions: args.actions,
809
+ rejected: args.rejected,
810
+ stats: {
811
+ generated: args.thingsById.size + args.rejected.length,
812
+ written: args.thingsById.size,
813
+ actionsRecorded: args.actions.length,
814
+ rejectedCount: args.rejected.length,
815
+ regenerationAttempts: args.regenerationAttempts,
816
+ embedded: args.embedded,
817
+ durationMs: Date.now() - args.startedAt,
818
+ },
819
+ };
820
+ }
821
+ function errorMessage(err) {
822
+ return err instanceof Error ? err.message : String(err);
823
+ }
824
+ // Mark the orchestrator's awareness that hasActionRecording is available
825
+ // (callers may invoke it directly on the adapter for ad-hoc Action recording
826
+ // outside the cascade — the orchestrator itself goes through CascadeWriteStrategy).
827
+ export { hasActionRecording };
828
+ //# sourceMappingURL=cascade-orchestrator.js.map