@cosmicdrift/kumiko-framework 0.2.2 → 0.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 (191) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/package.json +124 -38
  3. package/src/__tests__/full-stack.integration.ts +2 -2
  4. package/src/api/auth-routes.ts +5 -5
  5. package/src/api/jwt.ts +2 -2
  6. package/src/api/route-registrars.ts +1 -1
  7. package/src/api/routes.ts +3 -3
  8. package/src/api/server.ts +6 -7
  9. package/src/auth/__tests__/roles.test.ts +24 -0
  10. package/src/auth/index.ts +7 -0
  11. package/src/auth/roles.ts +42 -0
  12. package/src/compliance/__tests__/duration-spec.test.ts +72 -0
  13. package/src/compliance/__tests__/profiles.test.ts +308 -0
  14. package/src/compliance/__tests__/sub-processors.test.ts +139 -0
  15. package/src/compliance/duration-spec.ts +44 -0
  16. package/src/compliance/index.ts +31 -0
  17. package/src/compliance/override-schema.ts +136 -0
  18. package/src/compliance/profiles.ts +427 -0
  19. package/src/compliance/sub-processors.ts +152 -0
  20. package/src/db/__tests__/big-int-field.test.ts +131 -0
  21. package/src/db/assert-exists-in.ts +2 -2
  22. package/src/db/cursor.ts +3 -3
  23. package/src/db/event-store-executor.ts +19 -13
  24. package/src/db/located-timestamp.ts +1 -1
  25. package/src/db/money.ts +12 -2
  26. package/src/db/pg-error.ts +1 -1
  27. package/src/db/row-helpers.ts +1 -1
  28. package/src/db/table-builder.ts +20 -5
  29. package/src/db/tenant-db.ts +9 -9
  30. package/src/engine/__tests__/_pipeline-test-utils.ts +23 -0
  31. package/src/engine/__tests__/boot-validator-api-exposure.test.ts +142 -0
  32. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +570 -0
  33. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +160 -0
  34. package/src/engine/__tests__/build-target.test.ts +135 -0
  35. package/src/engine/__tests__/codemod-pipeline.test.ts +551 -0
  36. package/src/engine/__tests__/entity-handlers.test.ts +3 -3
  37. package/src/engine/__tests__/event-helpers.test.ts +4 -4
  38. package/src/engine/__tests__/pipeline-engine.test.ts +215 -0
  39. package/src/engine/__tests__/pipeline-handler.integration.ts +894 -0
  40. package/src/engine/__tests__/pipeline-observability.integration.ts +142 -0
  41. package/src/engine/__tests__/pipeline-performance.integration.ts +152 -0
  42. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +288 -0
  43. package/src/engine/__tests__/raw-table.test.ts +2 -2
  44. package/src/engine/__tests__/steps-aggregate-append-event.test.ts +115 -0
  45. package/src/engine/__tests__/steps-aggregate-create.test.ts +92 -0
  46. package/src/engine/__tests__/steps-aggregate-update.test.ts +127 -0
  47. package/src/engine/__tests__/steps-call-feature.test.ts +123 -0
  48. package/src/engine/__tests__/steps-mail-send.test.ts +136 -0
  49. package/src/engine/__tests__/steps-read.test.ts +142 -0
  50. package/src/engine/__tests__/steps-resolver-utils.test.ts +50 -0
  51. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +69 -0
  52. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +117 -0
  53. package/src/engine/__tests__/steps-webhook-send.test.ts +135 -0
  54. package/src/engine/__tests__/steps-workflow.test.ts +198 -0
  55. package/src/engine/__tests__/validate-projection-allowlist.test.ts +491 -0
  56. package/src/engine/__tests__/visual-tree-patterns.test.ts +251 -0
  57. package/src/engine/boot-validator/api-ext.ts +77 -0
  58. package/src/engine/boot-validator/config-deps.ts +163 -0
  59. package/src/engine/boot-validator/entity-handler.ts +466 -0
  60. package/src/engine/boot-validator/index.ts +159 -0
  61. package/src/engine/boot-validator/ownership.ts +198 -0
  62. package/src/engine/boot-validator/pii-retention.ts +155 -0
  63. package/src/engine/boot-validator/screens-nav.ts +624 -0
  64. package/src/engine/boot-validator.ts +1 -1528
  65. package/src/engine/build-app-schema.ts +1 -1
  66. package/src/engine/build-target.ts +99 -0
  67. package/src/engine/codemod/index.ts +15 -0
  68. package/src/engine/codemod/pipeline-codemod.ts +641 -0
  69. package/src/engine/config-helpers.ts +9 -19
  70. package/src/engine/constants.ts +1 -1
  71. package/src/engine/define-feature.ts +127 -9
  72. package/src/engine/define-handler.ts +89 -3
  73. package/src/engine/define-roles.ts +2 -2
  74. package/src/engine/define-step.ts +28 -0
  75. package/src/engine/define-workflow.ts +110 -0
  76. package/src/engine/entity-handlers.ts +10 -9
  77. package/src/engine/event-helpers.ts +4 -4
  78. package/src/engine/extension-names.ts +105 -0
  79. package/src/engine/extensions/user-data.ts +106 -0
  80. package/src/engine/factories.ts +26 -16
  81. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +184 -0
  82. package/src/engine/feature-ast/extractors/index.ts +74 -0
  83. package/src/engine/feature-ast/extractors/round1.ts +110 -0
  84. package/src/engine/feature-ast/extractors/round2.ts +253 -0
  85. package/src/engine/feature-ast/extractors/round3.ts +471 -0
  86. package/src/engine/feature-ast/extractors/round4.ts +1365 -0
  87. package/src/engine/feature-ast/extractors/round5.ts +72 -0
  88. package/src/engine/feature-ast/extractors/round6.ts +66 -0
  89. package/src/engine/feature-ast/extractors/shared.ts +177 -0
  90. package/src/engine/feature-ast/parse.ts +13 -0
  91. package/src/engine/feature-ast/patch.ts +9 -1
  92. package/src/engine/feature-ast/patcher.ts +10 -3
  93. package/src/engine/feature-ast/patterns.ts +71 -1
  94. package/src/engine/feature-ast/render.ts +31 -1
  95. package/src/engine/index.ts +66 -2
  96. package/src/engine/pattern-library/__tests__/library.test.ts +11 -0
  97. package/src/engine/pattern-library/library.ts +78 -2
  98. package/src/engine/pipeline.ts +88 -0
  99. package/src/engine/projection-helpers.ts +1 -1
  100. package/src/engine/read-claim.ts +1 -1
  101. package/src/engine/registry.ts +30 -2
  102. package/src/engine/resolve-config-or-param.ts +4 -0
  103. package/src/engine/run-pipeline.ts +162 -0
  104. package/src/engine/schema-builder.ts +10 -4
  105. package/src/engine/state-machine.ts +1 -1
  106. package/src/engine/steps/_drizzle-boundary.ts +19 -0
  107. package/src/engine/steps/_duration-utils.ts +33 -0
  108. package/src/engine/steps/_no-return-guard.ts +21 -0
  109. package/src/engine/steps/_resolver-utils.ts +42 -0
  110. package/src/engine/steps/_step-dispatch-constants.ts +38 -0
  111. package/src/engine/steps/aggregate-append-event.ts +56 -0
  112. package/src/engine/steps/aggregate-create.ts +56 -0
  113. package/src/engine/steps/aggregate-update.ts +68 -0
  114. package/src/engine/steps/branch.ts +84 -0
  115. package/src/engine/steps/call-feature.ts +49 -0
  116. package/src/engine/steps/compute.ts +41 -0
  117. package/src/engine/steps/for-each.ts +111 -0
  118. package/src/engine/steps/mail-send.ts +44 -0
  119. package/src/engine/steps/read-find-many.ts +51 -0
  120. package/src/engine/steps/read-find-one.ts +58 -0
  121. package/src/engine/steps/retry.ts +87 -0
  122. package/src/engine/steps/return.ts +34 -0
  123. package/src/engine/steps/unsafe-projection-delete.ts +46 -0
  124. package/src/engine/steps/unsafe-projection-upsert.ts +69 -0
  125. package/src/engine/steps/wait-for-event.ts +71 -0
  126. package/src/engine/steps/wait.ts +69 -0
  127. package/src/engine/steps/webhook-send.ts +71 -0
  128. package/src/engine/system-user.ts +1 -1
  129. package/src/engine/types/feature.ts +143 -1
  130. package/src/engine/types/fields.ts +134 -10
  131. package/src/engine/types/handlers.ts +18 -10
  132. package/src/engine/types/identifiers.ts +1 -0
  133. package/src/engine/types/index.ts +15 -1
  134. package/src/engine/types/step.ts +334 -0
  135. package/src/engine/types/target-ref.ts +21 -0
  136. package/src/engine/types/tree-node.ts +130 -0
  137. package/src/engine/types/workspace.ts +7 -0
  138. package/src/engine/validate-projection-allowlist.ts +161 -0
  139. package/src/event-store/snapshot.ts +1 -1
  140. package/src/event-store/upcaster-dead-letter.ts +1 -1
  141. package/src/event-store/upcaster.ts +1 -1
  142. package/src/files/__tests__/read-stream.test.ts +105 -0
  143. package/src/files/__tests__/write-stream.test.ts +233 -0
  144. package/src/files/__tests__/zip-stream.test.ts +357 -0
  145. package/src/files/file-routes.ts +1 -1
  146. package/src/files/in-memory-provider.ts +38 -0
  147. package/src/files/index.ts +3 -0
  148. package/src/files/local-provider.ts +58 -1
  149. package/src/files/types.ts +36 -8
  150. package/src/files/zip-stream.ts +251 -0
  151. package/src/jobs/job-runner.ts +10 -10
  152. package/src/lifecycle/lifecycle.ts +0 -3
  153. package/src/logging/index.ts +1 -0
  154. package/src/logging/pino-logger.ts +11 -7
  155. package/src/logging/utils.ts +24 -0
  156. package/src/observability/prometheus-meter.ts +7 -5
  157. package/src/pipeline/__tests__/archive-stream.integration.ts +1 -1
  158. package/src/pipeline/__tests__/causation-chain.integration.ts +1 -1
  159. package/src/pipeline/__tests__/domain-events-projections.integration.ts +3 -3
  160. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +4 -4
  161. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +1 -1
  162. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +3 -3
  163. package/src/pipeline/__tests__/msp-rebuild.integration.ts +3 -3
  164. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +2 -2
  165. package/src/pipeline/__tests__/query-projection.integration.ts +5 -5
  166. package/src/pipeline/append-event-core.ts +22 -6
  167. package/src/pipeline/dispatcher-utils.ts +188 -0
  168. package/src/pipeline/dispatcher.ts +63 -283
  169. package/src/pipeline/distributed-lock.ts +1 -1
  170. package/src/pipeline/entity-cache.ts +2 -2
  171. package/src/pipeline/event-consumer-state.ts +0 -13
  172. package/src/pipeline/event-dispatcher.ts +4 -4
  173. package/src/pipeline/index.ts +0 -2
  174. package/src/pipeline/lifecycle-pipeline.ts +6 -12
  175. package/src/pipeline/msp-rebuild.ts +5 -5
  176. package/src/pipeline/multi-stream-apply-context.ts +6 -7
  177. package/src/pipeline/projection-rebuild.ts +2 -2
  178. package/src/pipeline/projection-state.ts +0 -12
  179. package/src/rate-limit/__tests__/resolver.integration.ts +8 -4
  180. package/src/rate-limit/resolver.ts +1 -1
  181. package/src/search/in-memory-adapter.ts +1 -1
  182. package/src/search/meilisearch-adapter.ts +3 -3
  183. package/src/search/types.ts +1 -1
  184. package/src/secrets/leak-guard.ts +2 -2
  185. package/src/stack/request-helper.ts +9 -5
  186. package/src/stack/test-stack.ts +1 -1
  187. package/src/testing/handler-context.ts +4 -4
  188. package/src/testing/http-cookies.ts +1 -1
  189. package/src/time/tz-context.ts +1 -2
  190. package/src/ui-types/index.ts +4 -0
  191. package/src/engine/feature-ast/extractors.ts +0 -2562
@@ -56,15 +56,20 @@ import type {
56
56
  SecretOptions,
57
57
  TranslationKeys,
58
58
  TranslationsDef,
59
+ TreeActionDef,
60
+ TreeActionsHandle,
61
+ TreeChildrenSubscribe,
59
62
  ValidationHookFn,
60
63
  WriteHandlerDef,
61
64
  WriteHandlerFn,
62
65
  } from "./types";
63
66
  import { HookPhases } from "./types";
67
+ import type { RequiresApi } from "./types/feature";
64
68
  import { resolveName } from "./types/handlers";
65
69
  import type { HttpRouteDefinition } from "./types/http-route";
66
70
  import type { NavDefinition } from "./types/nav";
67
71
  import type { ScreenDefinition } from "./types/screen";
72
+ import type { PipelineDef } from "./types/step";
68
73
  import type { WorkspaceDefinition } from "./types/workspace";
69
74
 
70
75
  const LIFECYCLE_TYPES = Object.values(LifecycleHookTypes);
@@ -87,6 +92,11 @@ export function defineFeature<const TName extends string, TExports = undefined>(
87
92
  ): FeatureDefinition & { readonly exports: TExports } {
88
93
  const requires: string[] = [];
89
94
  const optionalRequires: string[] = [];
95
+ // Read-side projection-tables declared via r.requires.projection("table").
96
+ // Boot-validator checks unsafeProjection-* step calls against this set.
97
+ const requiredProjections = new Set<string>();
98
+ // Tier-2 step kinds declared via r.requires.step("webhook.send"). Q9.
99
+ const requiredSteps = new Set<string>();
90
100
  const entities: Record<string, EntityDefinition> = {};
91
101
  const relations: Record<string, Record<string, RelationDefinition>> = {};
92
102
  const writeHandlers: Record<string, WriteHandlerDef> = {};
@@ -112,6 +122,8 @@ export function defineFeature<const TName extends string, TExports = undefined>(
112
122
  const notifications: Record<string, NotificationDefinition> = {};
113
123
  const registrarExtensions: Record<string, RegistrarExtensionDef> = {};
114
124
  const extensionUsages: RegistrarExtensionRegistration[] = [];
125
+ const exposedApis: Set<string> = new Set();
126
+ const usedApis: Set<string> = new Set();
115
127
  const referenceData: ReferenceDataDef[] = [];
116
128
  const handlerEntityMappings: Record<string, string> = {};
117
129
  const metrics: Record<string, FeatureMetricDef> = {};
@@ -133,6 +145,14 @@ export function defineFeature<const TName extends string, TExports = undefined>(
133
145
 
134
146
  let isSystemScoped = false;
135
147
  let toggleableDefault: boolean | undefined;
148
+ // Visual-Tree-Slots — at-most-one per feature, only-once-guard im
149
+ // registrar (siehe r.treeActions / r.tree). Undefined wenn das Feature
150
+ // keinen Visual-Tree-Beitrag liefert (Zero-Whitelist-Filter).
151
+ // Name-Collision-Sicherheit: object-literal-method-Names im registrar
152
+ // sind keine bindings im closure-scope, daher kollidiert die `treeActions`
153
+ // closure-let-var nicht mit der `treeActions(...)` registrar-Methode.
154
+ let treeActions: Readonly<Record<string, TreeActionDef>> | undefined;
155
+ let treeProvider: TreeChildrenSubscribe | undefined;
136
156
 
137
157
  // Map handler name to entity via colon convention.
138
158
  // "task:create" → entity "task". No colon → standalone handler, no mapping.
@@ -151,9 +171,18 @@ export function defineFeature<const TName extends string, TExports = undefined>(
151
171
  isSystemScoped = true;
152
172
  },
153
173
 
154
- requires(...featureNames: string[]): void {
155
- requires.push(...featureNames);
156
- },
174
+ requires: (() => {
175
+ const fn = (...featureNames: string[]) => {
176
+ requires.push(...featureNames);
177
+ };
178
+ fn.projection = (tableName: string) => {
179
+ requiredProjections.add(tableName);
180
+ };
181
+ fn.step = (stepKind: string) => {
182
+ requiredSteps.add(stepKind);
183
+ };
184
+ return fn as RequiresApi;
185
+ })(),
157
186
 
158
187
  optionalRequires(...featureNames: string[]): void {
159
188
  optionalRequires.push(...featureNames);
@@ -184,11 +213,27 @@ export function defineFeature<const TName extends string, TExports = undefined>(
184
213
  writeHandlers[def.name] = {
185
214
  name: def.name,
186
215
  schema: def.schema,
187
- // @cast-boundary engine-bridge — typed Dev-API erased internal storage
216
+ // @cast-boundary engine-bridge — typed Dev-API's handler is
217
+ // generic over the schema's parsed payload (`WriteEvent<output<TSchema>>`),
218
+ // the storage form WriteHandlerFn carries `WriteEvent<unknown>`.
219
+ // Function-arg variance: TS sees the typed handler as stricter
220
+ // than the loose storage shape and rejects direct assignment.
221
+ // The runtime value is identical — the cast crosses that boundary.
222
+ // `satisfies` does not work here (it asserts assignability, which
223
+ // is what fails). Explicit cast is the right tool.
188
224
  handler: def.handler as WriteHandlerFn,
189
225
  ...(def.access && { access: def.access }),
190
- ...(def.skipTransitionGuard && { skipTransitionGuard: true }),
226
+ ...(def.unsafeSkipTransitionGuard && { unsafeSkipTransitionGuard: true }),
191
227
  ...(def.rateLimit && { rateLimit: def.rateLimit }),
228
+ // Forward the pipeline-build closure so boot-validators and
229
+ // Designer/AI tooling can inspect the step list. Absent on
230
+ // free-form handlers — defineWriteHandler only sets `perform`
231
+ // when the author used the pipeline form. Variance cast
232
+ // mirrors the handler-cast above: PipelineDef<output<TSchema>>
233
+ // is stricter than PipelineDef<unknown> for the same reason.
234
+ ...(def.perform !== undefined && {
235
+ perform: def.perform as PipelineDef,
236
+ }),
192
237
  };
193
238
  tryMapEntity(def.name);
194
239
  return { name: def.name };
@@ -218,7 +263,7 @@ export function defineFeature<const TName extends string, TExports = undefined>(
218
263
  name: def.name,
219
264
  schema: def.schema,
220
265
  // @cast-boundary engine-bridge — typed Dev-API → erased internal storage
221
- handler: def.handler as QueryHandlerFn,
266
+ handler: def.handler as QueryHandlerFn, // @cast-boundary engine-bridge
222
267
  ...(def.access && { access: def.access }),
223
268
  ...(def.rateLimit && { rateLimit: def.rateLimit }),
224
269
  };
@@ -343,7 +388,7 @@ export function defineFeature<const TName extends string, TExports = undefined>(
343
388
  ? {
344
389
  on: Array.isArray(options.trigger.on)
345
390
  ? options.trigger.on.map(resolveName)
346
- : resolveName(options.trigger.on as NameOrRef),
391
+ : resolveName(options.trigger.on as NameOrRef), // @cast-boundary engine-bridge
347
392
  }
348
393
  : options.trigger;
349
394
  jobs[jobName] = { ...options, trigger, name: jobName, handler };
@@ -464,6 +509,41 @@ export function defineFeature<const TName extends string, TExports = undefined>(
464
509
  extensionUsages.push({ extensionName, entityName: resolveName(entityRef), options });
465
510
  },
466
511
 
512
+ /**
513
+ * Marker-Deklaration: dieses Feature stellt eine Cross-Feature-API
514
+ * unter dem genannten Namen bereit. Die eigentliche Implementation
515
+ * wird separat als Query- oder Write-Handler unter dem QN-Pattern
516
+ * registriert; r.exposesApi ist reine Boot-Check-Surface.
517
+ *
518
+ * Beispiel:
519
+ * defineFeature("compliance-profiles", (r) => {
520
+ * r.exposesApi("compliance.forTenant");
521
+ * r.queryHandler({ name: "compliance:query:for-tenant", ... });
522
+ * });
523
+ * defineFeature("user-data-rights", (r) => {
524
+ * r.requires("compliance-profiles");
525
+ * r.usesApi("compliance.forTenant");
526
+ * // ruft im Handler: ctx.callQuery("compliance:query:for-tenant", ...)
527
+ * });
528
+ */
529
+ exposesApi(apiName: string): void {
530
+ if (exposedApis.has(apiName)) {
531
+ throw new Error(
532
+ `[Feature ${name}] r.exposesApi("${apiName}") called twice — API names must be unique within a feature.`,
533
+ );
534
+ }
535
+ exposedApis.add(apiName);
536
+ },
537
+
538
+ /**
539
+ * Declares that this feature calls a cross-feature API. Boot-Validator
540
+ * checkt dass irgendein anderes Feature `r.exposesApi(name)` macht und
541
+ * dass dieses Feature `r.requires` darauf hat.
542
+ */
543
+ usesApi(apiName: string): void {
544
+ usedApis.add(apiName);
545
+ },
546
+
467
547
  metric(shortName: string, options: MetricOptions): void {
468
548
  if (metrics[shortName]) {
469
549
  throw new Error(
@@ -686,9 +766,41 @@ export function defineFeature<const TName extends string, TExports = undefined>(
686
766
  };
687
767
  return { name: qualifiedName, type: options.type };
688
768
  },
769
+
770
+ treeActions<const TActions extends Record<string, TreeActionDef>>(
771
+ actions: TActions,
772
+ ): TreeActionsHandle<TName, TActions> {
773
+ // Only-once-guard: zweiter Aufruf ist Author-Bug, soll am
774
+ // Feature-File aufschlagen (gleicher Stil wie r.toggleable).
775
+ if (treeActions !== undefined) {
776
+ throw new Error(
777
+ `[Feature ${name}] r.treeActions() already called. ` +
778
+ `Each feature may declare a single tree-actions schema.`,
779
+ );
780
+ }
781
+ treeActions = actions;
782
+ // Return typed handle für setup-export. Frozen damit Caller die
783
+ // Map nicht nachträglich mutieren (würde Pattern-AST + Runtime-
784
+ // Lookup divergieren lassen).
785
+ return Object.freeze({
786
+ id: name,
787
+ treeActions: actions,
788
+ });
789
+ },
790
+
791
+ tree(provider: TreeChildrenSubscribe): void {
792
+ // Only-once-guard analog zu r.treeActions.
793
+ if (treeProvider !== undefined) {
794
+ throw new Error(
795
+ `[Feature ${name}] r.tree() already called. ` +
796
+ `Each feature may declare a single tree-provider.`,
797
+ );
798
+ }
799
+ treeProvider = provider;
800
+ },
689
801
  };
690
802
 
691
- const exports = setup(registrar) as TExports;
803
+ const exports = setup(registrar) as TExports; // @cast-boundary engine-bridge
692
804
 
693
805
  return {
694
806
  name,
@@ -696,6 +808,8 @@ export function defineFeature<const TName extends string, TExports = undefined>(
696
808
  exports,
697
809
  requires,
698
810
  optionalRequires,
811
+ requiredProjections,
812
+ requiredSteps,
699
813
  ...(toggleableDefault !== undefined && { toggleableDefault }),
700
814
  entities,
701
815
  relations,
@@ -709,7 +823,7 @@ export function defineFeature<const TName extends string, TExports = undefined>(
709
823
  preDelete: phasedLifecycleHooks.preDelete,
710
824
  postDelete: phasedLifecycleHooks.postDelete,
711
825
  preQuery: lifecycleHooks["preQuery"] ?? {},
712
- } as HookMap,
826
+ } as HookMap, // @cast-boundary engine-payload
713
827
  entityHooks: {
714
828
  postSave: entityPostSave,
715
829
  preDelete: entityPreDelete,
@@ -720,6 +834,8 @@ export function defineFeature<const TName extends string, TExports = undefined>(
720
834
  notifications,
721
835
  registrarExtensions,
722
836
  extensionUsages,
837
+ exposedApis,
838
+ usedApis,
723
839
  referenceData,
724
840
  events,
725
841
  eventMigrations,
@@ -736,5 +852,7 @@ export function defineFeature<const TName extends string, TExports = undefined>(
736
852
  workspaces,
737
853
  httpRoutes,
738
854
  rawTables,
855
+ ...(treeActions !== undefined && { treeActions }),
856
+ ...(treeProvider !== undefined && { treeProvider }),
739
857
  };
740
858
  }
@@ -1,4 +1,5 @@
1
1
  import type { ZodType, z } from "zod";
2
+ import { runPipeline } from "./run-pipeline";
2
3
  import type {
3
4
  AccessRule,
4
5
  HandlerContext,
@@ -8,6 +9,7 @@ import type {
8
9
  WriteEvent,
9
10
  WriteResult,
10
11
  } from "./types";
12
+ import type { PipelineDef } from "./types/step";
11
13
 
12
14
  // --- Write Handler Definition ---
13
15
  //
@@ -19,6 +21,10 @@ import type {
19
21
  // definition-site (framework's compile, where the augmentation isn't
20
22
  // visible) and collapse `keyof TMap` to `never`. See the spike-findings
21
23
  // memory for the empirical proof.
24
+ //
25
+ // Two authoring forms — `handler` (free-form) or `perform: pipeline(...)`
26
+ // (step-pipeline). A `perform` is compiled to a handler-function at
27
+ // definition time; the dispatcher only ever sees `handler`.
22
28
 
23
29
  export type WriteHandlerDefinition<
24
30
  TName extends string = string,
@@ -29,23 +35,103 @@ export type WriteHandlerDefinition<
29
35
  readonly name: TName;
30
36
  readonly schema: TSchema;
31
37
  readonly access?: AccessRule;
32
- readonly skipTransitionGuard?: boolean;
38
+ readonly unsafeSkipTransitionGuard?: boolean;
33
39
  readonly rateLimit?: RateLimitOption;
34
40
  readonly handler: (
35
41
  event: WriteEvent<z.infer<TSchema>>,
36
42
  context: HandlerContext<TMap>,
37
43
  ) => Promise<WriteResult<TData>>;
44
+ // Preserved when the author wrote a `perform` block — the original
45
+ // PipelineDef. Designer/AI/AST tools read this when present; the
46
+ // dispatcher ignores it and just calls `handler`. Absent on free-form
47
+ // handlers.
48
+ readonly perform?: PipelineDef<z.infer<TSchema>, TData>;
38
49
  };
39
50
 
51
+ // Author-facing input — accepts either the free-form `handler` or the
52
+ // pipeline-form `perform`. defineWriteHandler narrows them to the
53
+ // canonical WriteHandlerDefinition shape.
54
+ export type WriteHandlerInput<
55
+ TName extends string = string,
56
+ TSchema extends ZodType = ZodType,
57
+ TData = unknown,
58
+ TMap extends object = KumikoEventTypeMap,
59
+ > = {
60
+ readonly name: TName;
61
+ readonly schema: TSchema;
62
+ readonly access?: AccessRule;
63
+ readonly unsafeSkipTransitionGuard?: boolean;
64
+ readonly rateLimit?: RateLimitOption;
65
+ } & (
66
+ | {
67
+ readonly handler: (
68
+ event: WriteEvent<z.infer<TSchema>>,
69
+ context: HandlerContext<TMap>,
70
+ ) => Promise<WriteResult<TData>>;
71
+ readonly perform?: never;
72
+ }
73
+ | {
74
+ readonly perform: PipelineDef<z.infer<TSchema>, TData>;
75
+ readonly handler?: never;
76
+ }
77
+ );
78
+
40
79
  export function defineWriteHandler<
41
80
  const TName extends string,
42
81
  TSchema extends ZodType,
43
82
  TData = unknown,
44
83
  TMap extends object = KumikoEventTypeMap,
45
84
  >(
46
- def: WriteHandlerDefinition<TName, TSchema, TData, TMap>,
85
+ def: WriteHandlerInput<TName, TSchema, TData, TMap>,
47
86
  ): WriteHandlerDefinition<TName, TSchema, TData, TMap> {
48
- return def;
87
+ // Runtime-guard against accidentally setting BOTH handler+perform.
88
+ // The discriminated-union type-error
89
+ // "Type 'PipelineDef<...>' is not assignable to type 'undefined'."
90
+ // is functional but cryptic for less TS-experienced users; this throws
91
+ // a name-and-explanation error message instead. Followup #3.
92
+ // The cast is necessary because the discriminated union narrows
93
+ // `handler` away once `perform` is present (and vice-versa) — at this
94
+ // boundary we want to read both regardless of the narrowing.
95
+ const probe = def as {
96
+ readonly handler?: unknown;
97
+ readonly perform?: unknown;
98
+ readonly name: TName;
99
+ };
100
+ if (probe.handler !== undefined && probe.perform !== undefined) {
101
+ throw new Error(
102
+ `defineWriteHandler("${def.name}"): both \`handler\` and \`perform\` are set. ` +
103
+ `Pick one — \`handler\` for the free-form async function, ` +
104
+ `\`perform: pipeline(...)\` for the step-pipeline form. ` +
105
+ `(See step-vocabulary.md for which form fits.)`,
106
+ );
107
+ }
108
+
109
+ // Conditional spreads (`...(def.access && { access: def.access })`)
110
+ // mirror the existing convention in entity-handlers.ts /
111
+ // define-feature.ts — optional fields stay absent rather than being
112
+ // serialised as `key: undefined`.
113
+ const base = {
114
+ name: def.name,
115
+ schema: def.schema,
116
+ ...(def.access && { access: def.access }),
117
+ ...(def.unsafeSkipTransitionGuard && {
118
+ unsafeSkipTransitionGuard: def.unsafeSkipTransitionGuard,
119
+ }),
120
+ ...(def.rateLimit && { rateLimit: def.rateLimit }),
121
+ };
122
+
123
+ if ("perform" in def && def.perform !== undefined) {
124
+ const performDef = def.perform;
125
+ const compiledHandler = async (
126
+ event: WriteEvent<z.infer<TSchema>>,
127
+ ctx: HandlerContext<TMap>,
128
+ ): Promise<WriteResult<TData>> => {
129
+ return runPipeline<z.infer<TSchema>, TData, TMap>(performDef, event, ctx);
130
+ };
131
+ return { ...base, handler: compiledHandler, perform: performDef };
132
+ }
133
+
134
+ return { ...base, handler: def.handler };
49
135
  }
50
136
 
51
137
  // --- Query Handler Definition ---
@@ -11,9 +11,9 @@ type RoleMap<T extends readonly string[]> = {
11
11
  };
12
12
 
13
13
  export function defineRoles<const T extends readonly string[]>(roles: T): RoleMap<T> {
14
- const map = {} as Record<string, string>;
14
+ const map = {} as Record<string, string>; // @cast-boundary schema-walk
15
15
  for (const role of roles) {
16
16
  map[role] = role;
17
17
  }
18
- return map as RoleMap<T>;
18
+ return map as RoleMap<T>; // @cast-boundary schema-walk
19
19
  }
@@ -0,0 +1,28 @@
1
+ // Step-Registry + defineStep factory.
2
+ //
3
+ // Steps are registered at module-load time via defineStep(). The runtime
4
+ // (run-pipeline.ts) looks them up by `kind` to dispatch run-calls.
5
+ //
6
+ // The registry is process-global; Tier-2 step opt-in via
7
+ // `r.requires.step("…")` (Q9 in step-vocabulary.md) is a future pass.
8
+
9
+ import type { StepDef, StepKind } from "./types/step";
10
+
11
+ const stepRegistry = new Map<StepKind, StepDef>();
12
+
13
+ export function defineStep<TArgs, TResult>(def: StepDef<TArgs, TResult>): StepDef<TArgs, TResult> {
14
+ const existing = stepRegistry.get(def.kind);
15
+ if (existing && existing !== def) {
16
+ throw new Error(`Step kind "${def.kind}" is already registered with a different definition`);
17
+ }
18
+ stepRegistry.set(def.kind, def as StepDef);
19
+ return def;
20
+ }
21
+
22
+ export function getStep(kind: StepKind): StepDef | undefined {
23
+ return stepRegistry.get(kind);
24
+ }
25
+
26
+ export function listStepKinds(): readonly StepKind[] {
27
+ return Array.from(stepRegistry.keys());
28
+ }
@@ -0,0 +1,110 @@
1
+ // defineWorkflow — define a persistent, suspendable workflow.
2
+ // Tier-3 / Workflow-only mount-point. See step-vocabulary.md Sample 2 for
3
+ // the full lifecycle (run.started → wait → resume → run.completed) and
4
+ // Q7 (Snapshot-at-Start) for the in-flight upgrade story.
5
+
6
+ import { createHash } from "node:crypto";
7
+ import type { WriteEvent } from "./types/handlers";
8
+ import type { PipelineDef } from "./types/step";
9
+
10
+ /**
11
+ * Trigger configuration for a workflow. Determines what starts a run.
12
+ */
13
+ export type WorkflowTrigger =
14
+ | {
15
+ readonly kind: "event";
16
+ readonly eventType: string;
17
+ readonly filter?: (event: WriteEvent) => boolean;
18
+ }
19
+ | {
20
+ readonly kind: "cron";
21
+ readonly schedule: string; // cron expression
22
+ }
23
+ | {
24
+ readonly kind: "webhook";
25
+ readonly path: string;
26
+ };
27
+
28
+ /**
29
+ * Workflow definition — the result of defineWorkflow().
30
+ */
31
+ export type WorkflowDefinition<TPayload = unknown, TData = unknown> = {
32
+ readonly __kind: "workflow";
33
+ readonly name: string;
34
+ readonly trigger: WorkflowTrigger;
35
+ /** The pipeline definition containing the step list closure. */
36
+ readonly pipelineDef: PipelineDef<TPayload, TData>;
37
+ /** Idempotency key for deduplication — prevents duplicate runs. */
38
+ readonly idempotencyKey?: string | ((event: WriteEvent<TPayload>) => string);
39
+ };
40
+
41
+ /**
42
+ * Input shape for defineWorkflow() — the user-facing API.
43
+ */
44
+ export type WorkflowInput<TPayload = unknown, TData = unknown> = {
45
+ readonly name: string;
46
+ readonly trigger: WorkflowTrigger;
47
+ readonly steps: PipelineDef<TPayload, TData>;
48
+ readonly idempotencyKey?: string | ((event: WriteEvent<TPayload>) => string);
49
+ readonly onError?: PipelineDef<unknown>;
50
+ };
51
+
52
+ /**
53
+ * Define a suspendable workflow.
54
+ *
55
+ * Example:
56
+ * ```ts
57
+ * defineWorkflow({
58
+ * name: "user-onboarding",
59
+ * trigger: { kind: "event", eventType: "user.signed-up" },
60
+ * steps: pipeline(({ event, r }) => [
61
+ * r.step.mail.send({ to: () => event.payload.email, subject: "Welcome!", body: "..." }),
62
+ * r.step.wait({ for: "P1D" }),
63
+ * r.step.read.findOne("user", { table: userTable, where: ... }),
64
+ * r.step.branch({ if: ({ steps }) => ..., onTrue: [...], onFalse: [...] }),
65
+ * r.step.retry({ times: 3, backoff: "exponential", do: [
66
+ * r.step.webhook.send({ url: "...", mode: "deferred" }),
67
+ * ]}),
68
+ * ]),
69
+ * });
70
+ * ```
71
+ */
72
+ export function defineWorkflow<TPayload = unknown, TData = unknown>(
73
+ input: WorkflowInput<TPayload, TData>,
74
+ ): WorkflowDefinition<TPayload, TData> {
75
+ return {
76
+ __kind: "workflow",
77
+ name: input.name,
78
+ trigger: input.trigger,
79
+ pipelineDef: input.steps,
80
+ idempotencyKey: input.idempotencyKey,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Q7 Snapshot-at-Start fingerprint. SHA-256 over the workflow's stable
86
+ * identity (name + trigger + serialized pipeline-closure source). Persisted
87
+ * in `workflow.run.started` and re-checked at every resume so a library-
88
+ * upgrade that changes the closure source surfaces as a loud
89
+ * `workflow-definition-changed` failure on in-flight runs instead of a
90
+ * silent semantic drift.
91
+ *
92
+ * Limitations (will be tightened in M.5 with the Designer/AST layer):
93
+ * - `build.toString()` captures the closure source but not bindings —
94
+ * two definitions that import different external helpers with the
95
+ * same source bytes would collide. Acceptable for M.4 because the
96
+ * fingerprint is a *change-detector*, not a deep semantic identity.
97
+ * - Minifiers / source-maps will produce different fingerprints across
98
+ * environments. Run-the-fingerprint-in-the-same-environment is the
99
+ * contract; cross-env replay is out of scope.
100
+ */
101
+ export function computeDefinitionFingerprint(
102
+ workflow: Pick<WorkflowDefinition, "name" | "trigger" | "pipelineDef">,
103
+ ): string {
104
+ const material = JSON.stringify({
105
+ name: workflow.name,
106
+ trigger: workflow.trigger,
107
+ source: workflow.pipelineDef.build.toString(),
108
+ });
109
+ return createHash("sha256").update(material).digest("hex");
110
+ }
@@ -112,8 +112,8 @@ function parseHandlerName<TVerb extends string>(
112
112
  if (!entityName) {
113
113
  throw new Error(`Handler name "${name}" is missing the entity part before the colon.`);
114
114
  }
115
- // @cast-boundary engine-bridge — verbCandidate validated against validVerbs union
116
- if (!(validVerbs as readonly string[]).includes(verbCandidate)) {
115
+ const verbs = validVerbs as readonly string[]; // @cast-boundary engine-bridge
116
+ if (!verbs.includes(verbCandidate)) {
117
117
  throw new Error(
118
118
  `Unknown verb "${verbCandidate}" in handler name "${name}". Standard verbs: ${validVerbs.join("/")}. For custom verbs use the explicit r.writeHandler / r.queryHandler form.`,
119
119
  );
@@ -151,17 +151,17 @@ export function defineEntityWriteHandler(
151
151
  changes: buildUpdateSchema(entity),
152
152
  });
153
153
  handler = async (event, ctx) =>
154
- executor.update(event.payload as UpdatePayload, event.user, ctx.db);
154
+ executor.update(event.payload as UpdatePayload, event.user, ctx.db); // @cast-boundary engine-payload
155
155
  break;
156
156
  case "delete":
157
157
  schema = idSchema;
158
158
  handler = async (event, ctx) =>
159
- executor.delete(event.payload as IdPayload, event.user, ctx.db);
159
+ executor.delete(event.payload as IdPayload, event.user, ctx.db); // @cast-boundary engine-payload
160
160
  break;
161
161
  case "restore":
162
162
  schema = idSchema;
163
163
  handler = async (event, ctx) =>
164
- executor.restore(event.payload as IdPayload, event.user, ctx.db);
164
+ executor.restore(event.payload as IdPayload, event.user, ctx.db); // @cast-boundary engine-payload
165
165
  break;
166
166
  default:
167
167
  assertUnreachable(verb, "write verb");
@@ -205,7 +205,8 @@ export function defineEntityQueryHandler(
205
205
  // läuft (Remote-Combobox-Search). Der executor wird beim
206
206
  // Definition-Time gebaut, kennt den Adapter also nicht —
207
207
  // Runtime-Override holt das.
208
- const result = await executor.list(query.payload as ListPayload, query.user, ctx.db, {
208
+ const listPayload = query.payload as ListPayload; // @cast-boundary engine-payload
209
+ const result = await executor.list(listPayload, query.user, ctx.db, {
209
210
  ...(ctx.searchAdapter !== undefined && { searchAdapter: ctx.searchAdapter }),
210
211
  });
211
212
  if (!hasRefFields) return result;
@@ -221,7 +222,7 @@ export function defineEntityQueryHandler(
221
222
  case "detail":
222
223
  schema = idSchema;
223
224
  handler = async (query, ctx) => {
224
- const row = await executor.detail(query.payload as IdPayload, query.user, ctx.db);
225
+ const row = await executor.detail(query.payload as IdPayload, query.user, ctx.db); // @cast-boundary engine-payload
225
226
  if (row === null || !hasRefFields) return row;
226
227
  return enrichRowWithReferences(row, entity, (name) => ctx.registry.getEntity(name), ctx.db);
227
228
  };
@@ -345,7 +346,7 @@ export function createEntityExecutor(
345
346
  export function defineProjectionQueryHandler(
346
347
  name: string,
347
348
  projectionQualifiedName: string,
348
- options?: { access?: AccessRule; allTenants?: boolean },
349
+ options?: { access?: AccessRule; unsafeAllTenants?: boolean },
349
350
  ): QueryHandlerDef {
350
351
  return {
351
352
  name,
@@ -357,7 +358,7 @@ export function defineProjectionQueryHandler(
357
358
  handler: async (_query, ctx) =>
358
359
  ctx.queryProjection(
359
360
  projectionQualifiedName,
360
- options?.allTenants ? { allTenants: true } : undefined,
361
+ options?.unsafeAllTenants ? { unsafeAllTenants: true } : undefined,
361
362
  ),
362
363
  ...(options?.access && { access: options.access }),
363
364
  };
@@ -4,11 +4,11 @@ import type { AppendEventArgs, EventDef, HandlerContext } from "./types/handlers
4
4
  // MultiStreamApplyContext-style callers pass their own appendEvent without
5
5
  // a full HandlerContext. Real handlers just pass `ctx`.
6
6
  //
7
- // Uses appendEventUnsafe internally because EventDef's TPayload comes from
7
+ // Uses unsafeAppendEvent internally because EventDef's TPayload comes from
8
8
  // a runtime-defined zod-schema — emitEvent does the type-check itself via
9
9
  // the EventDef generic, so it doesn't need the strict KumikoEventTypeMap
10
10
  // path. The strict appendEvent is for direct in-handler callsites.
11
- export type EmitCtx = Pick<HandlerContext, "appendEventUnsafe">;
11
+ export type EmitCtx = Pick<HandlerContext, "unsafeAppendEvent">;
12
12
 
13
13
  // Typed wrapper around ctx.appendEvent. Two wins over the raw call:
14
14
  //
@@ -42,7 +42,7 @@ export async function emitEvent<TPayload>(
42
42
  type: eventDef.name,
43
43
  payload: args.payload,
44
44
  };
45
- await ctx.appendEventUnsafe(appendArgs);
45
+ await ctx.unsafeAppendEvent(appendArgs);
46
46
  }
47
47
 
48
48
  // Read-side counterpart: narrow a StoredEvent's `payload` (declared as
@@ -69,5 +69,5 @@ export function typedPayload<TPayload>(
69
69
  `Check the projection-apply / reducer mapping — the event was routed to the wrong handler.`,
70
70
  );
71
71
  }
72
- return event.payload as TPayload;
72
+ return event.payload as TPayload; // @cast-boundary engine-payload
73
73
  }