@cosmicdrift/kumiko-framework 1.0.0 → 2.0.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 (424) hide show
  1. package/README.md +9 -38
  2. package/package.json +21 -2
  3. package/src/__tests__/anonymous-access.integration.test.ts +185 -0
  4. package/src/__tests__/consumer-cli.integration.test.ts +172 -0
  5. package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
  6. package/src/__tests__/full-stack.integration.test.ts +1 -1
  7. package/src/__tests__/schema-cli-temporal-polyfill.test.ts +48 -0
  8. package/src/__tests__/schema-cli.integration.test.ts +50 -0
  9. package/src/__tests__/store-table.integration.test.ts +93 -0
  10. package/src/api/__tests__/api.test.ts +373 -0
  11. package/src/api/__tests__/auth-middleware-anonymous-access-boot.test.ts +40 -0
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
  13. package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +253 -0
  14. package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +222 -0
  15. package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +249 -0
  16. package/src/api/__tests__/auth-routes-mfa-verify.test.ts +202 -0
  17. package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +135 -0
  18. package/src/api/__tests__/batch.integration.test.ts +101 -11
  19. package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
  20. package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
  21. package/src/api/__tests__/jwt.test.ts +200 -1
  22. package/src/api/__tests__/login-rate-limiter-sweep.test.ts +50 -0
  23. package/src/api/__tests__/pat-scope.test.ts +36 -0
  24. package/src/api/__tests__/pii-leak-guard.integration.test.ts +103 -0
  25. package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +138 -0
  26. package/src/api/__tests__/request-id-middleware.test.ts +51 -0
  27. package/src/api/__tests__/server-boot-guards.test.ts +71 -0
  28. package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
  29. package/src/api/__tests__/sse-broker.test.ts +57 -0
  30. package/src/api/__tests__/sse-route.test.ts +4 -0
  31. package/src/api/api-constants.ts +11 -0
  32. package/src/api/auth-middleware.ts +248 -40
  33. package/src/api/auth-routes.ts +576 -95
  34. package/src/api/index.ts +13 -4
  35. package/src/api/jwt.ts +170 -11
  36. package/src/api/pat-scope.ts +14 -0
  37. package/src/api/pii-leak-guard.ts +47 -0
  38. package/src/api/request-context.ts +3 -0
  39. package/src/api/request-id-middleware.ts +2 -0
  40. package/src/api/routes.ts +169 -2
  41. package/src/api/server.ts +112 -16
  42. package/src/api/sse-broker.ts +39 -0
  43. package/src/bun-db/__tests__/PATTERN.md +0 -1
  44. package/src/bun-db/__tests__/coerce-row-temporal.test.ts +41 -0
  45. package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
  46. package/src/bun-db/__tests__/write-brand.test.ts +21 -0
  47. package/src/bun-db/connection.ts +3 -3
  48. package/src/bun-db/index.ts +2 -0
  49. package/src/bun-db/query.ts +105 -32
  50. package/src/consumer-cli.ts +134 -0
  51. package/src/crypto/__tests__/blind-index.test.ts +130 -0
  52. package/src/crypto/__tests__/event-pii.test.ts +161 -0
  53. package/src/crypto/__tests__/kek-rotation.integration.test.ts +180 -0
  54. package/src/crypto/__tests__/kms-adapter-contract.ts +134 -0
  55. package/src/crypto/__tests__/kms-adapter.contract.test.ts +4 -0
  56. package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +117 -0
  57. package/src/crypto/__tests__/pii-field-encryption.test.ts +376 -0
  58. package/src/crypto/__tests__/request-kms-cache.test.ts +74 -0
  59. package/src/crypto/__tests__/subject-resolver.test.ts +108 -0
  60. package/src/crypto/blind-index.ts +118 -0
  61. package/src/crypto/event-pii.ts +70 -0
  62. package/src/crypto/in-memory-kms-adapter.ts +50 -0
  63. package/src/crypto/index.ts +67 -0
  64. package/src/crypto/kms-adapter.ts +2 -0
  65. package/src/crypto/pg-kms-adapter.ts +295 -0
  66. package/src/crypto/pii-field-encryption.ts +248 -0
  67. package/src/crypto/request-kms-cache.ts +38 -0
  68. package/src/crypto/subject-resolver.ts +91 -0
  69. package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +191 -0
  70. package/src/db/__tests__/blind-index.integration.test.ts +248 -0
  71. package/src/db/__tests__/build-filter-where.test.ts +34 -0
  72. package/src/db/__tests__/collect-table-metas.test.ts +10 -10
  73. package/src/db/__tests__/config-seed.integration.test.ts +13 -5
  74. package/src/db/__tests__/dialect-instant.test.ts +1 -4
  75. package/src/db/__tests__/entity-field-encryption.test.ts +7 -7
  76. package/src/db/__tests__/entity-table-meta-source.test.ts +50 -0
  77. package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
  78. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +402 -0
  79. package/src/db/__tests__/event-store-executor.integration.test.ts +299 -29
  80. package/src/db/__tests__/feature-table-sources.test.ts +34 -0
  81. package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +118 -39
  82. package/src/db/__tests__/instant-to-driver-temporal.test.ts +21 -0
  83. package/src/db/__tests__/located-timestamp.test.ts +19 -0
  84. package/src/db/__tests__/migrate-generator.test.ts +21 -0
  85. package/src/db/__tests__/migrate-runner.test.ts +61 -0
  86. package/src/db/__tests__/number-field-fractional.integration.test.ts +58 -0
  87. package/src/db/__tests__/rebuild-marker.test.ts +13 -3
  88. package/src/db/__tests__/replay-migration-sql.test.ts +384 -0
  89. package/src/db/__tests__/schema-inspection.test.ts +7 -0
  90. package/src/db/__tests__/schema-migration.integration.test.ts +1 -1
  91. package/src/db/__tests__/table-builder-meta-lockstep.test.ts +39 -0
  92. package/src/db/__tests__/tenant-db-where-merge.test.ts +49 -3
  93. package/src/db/__tests__/tenant-db.integration.test.ts +6 -2
  94. package/src/db/api.ts +2 -2
  95. package/src/db/apply-entity-event.ts +18 -14
  96. package/src/db/blind-index-cleanup.ts +55 -0
  97. package/src/db/bun-provider.ts +2 -2
  98. package/src/db/collect-table-metas.ts +6 -7
  99. package/src/db/config-seed.ts +9 -9
  100. package/src/db/connection.ts +7 -12
  101. package/src/db/cursor.ts +1 -18
  102. package/src/db/dialect.ts +20 -26
  103. package/src/db/entity-field-encryption.ts +81 -24
  104. package/src/db/entity-table-meta-types.ts +2 -0
  105. package/src/db/entity-table-meta.ts +63 -90
  106. package/src/db/event-store-executor-context.ts +404 -0
  107. package/src/db/event-store-executor-read.ts +275 -0
  108. package/src/db/event-store-executor-write.ts +644 -0
  109. package/src/db/event-store-executor.ts +28 -1155
  110. package/src/db/feature-table-sources.ts +6 -5
  111. package/src/db/index.ts +20 -3
  112. package/src/db/located-timestamp.ts +4 -0
  113. package/src/db/migrate-generator.ts +39 -6
  114. package/src/db/migrate-runner.ts +107 -11
  115. package/src/db/pg-error.ts +9 -1
  116. package/src/db/postgres-provider.ts +2 -2
  117. package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +80 -0
  118. package/src/db/queries/backfill-pii.ts +277 -0
  119. package/src/db/queries/ddl.ts +45 -0
  120. package/src/db/queries/event-consumer.ts +35 -2
  121. package/src/db/queries/event-store.ts +126 -19
  122. package/src/db/queries/projection-rebuild.ts +22 -8
  123. package/src/db/queries/shadow-swap.ts +183 -0
  124. package/src/db/queries/test-stack.ts +4 -30
  125. package/src/db/query-api.ts +1 -0
  126. package/src/db/query.ts +1 -0
  127. package/src/db/rebuild-marker.ts +18 -2
  128. package/src/db/reference-data.ts +2 -3
  129. package/src/db/replay-migration-sql.ts +257 -0
  130. package/src/db/schema-inspection.ts +1 -1
  131. package/src/db/table-builder.ts +102 -71
  132. package/src/db/tenant-db.ts +15 -53
  133. package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
  134. package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
  135. package/src/engine/__tests__/boot-validator-dashboard.test.ts +237 -0
  136. package/src/engine/__tests__/boot-validator-entity-list.test.ts +104 -0
  137. package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -69
  138. package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +26 -5
  139. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +3 -19
  140. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +246 -4
  141. package/src/engine/__tests__/boot-validator.test.ts +219 -15
  142. package/src/engine/__tests__/build-app-schema.test.ts +82 -0
  143. package/src/engine/__tests__/build-config-feature-schema.test.ts +125 -0
  144. package/src/engine/__tests__/build-target.test.ts +3 -11
  145. package/src/engine/__tests__/codemod-pipeline.test.ts +152 -21
  146. package/src/engine/__tests__/config-helpers.test.ts +16 -0
  147. package/src/engine/__tests__/define-feature-entity-mapping.test.ts +6 -0
  148. package/src/engine/__tests__/define-roles.test.ts +21 -0
  149. package/src/engine/__tests__/engine.test.ts +163 -1
  150. package/src/engine/__tests__/entity-handlers.test.ts +80 -0
  151. package/src/engine/__tests__/event-migration-declarative.test.ts +65 -0
  152. package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
  153. package/src/engine/__tests__/extend-entity-projection.test.ts +123 -0
  154. package/src/engine/__tests__/factories-time.test.ts +2 -66
  155. package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
  156. package/src/engine/__tests__/feature-manifest.test.ts +31 -1
  157. package/src/engine/__tests__/field-access.test.ts +23 -1
  158. package/src/engine/__tests__/hook-phases.test.ts +5 -5
  159. package/src/engine/__tests__/membership-roles.test.ts +4 -10
  160. package/src/engine/__tests__/nav.test.ts +86 -1
  161. package/src/engine/__tests__/pipeline-engine.test.ts +9 -9
  162. package/src/engine/__tests__/pipeline-handler.integration.test.ts +18 -18
  163. package/src/engine/__tests__/pipeline-observability.integration.test.ts +2 -2
  164. package/src/engine/__tests__/pipeline-performance.integration.test.ts +3 -3
  165. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +9 -9
  166. package/src/engine/__tests__/post-query-hook.test.ts +7 -7
  167. package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
  168. package/src/engine/__tests__/registry.test.ts +144 -0
  169. package/src/engine/__tests__/schema-builder.test.ts +18 -0
  170. package/src/engine/__tests__/screen.test.ts +147 -1
  171. package/src/engine/__tests__/soft-delete-cleanup.test.ts +3 -0
  172. package/src/engine/__tests__/store-table.test.ts +229 -0
  173. package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
  174. package/src/engine/__tests__/{visual-tree-patterns.test.ts → tree-actions-patterns.test.ts} +7 -95
  175. package/src/engine/__tests__/validate-projection-allowlist.test.ts +15 -15
  176. package/src/engine/boot-validator/__tests__/config-deps.test.ts +92 -0
  177. package/src/engine/boot-validator/action-wiring.ts +149 -0
  178. package/src/engine/boot-validator/boot-check.ts +21 -0
  179. package/src/engine/boot-validator/config-deps.ts +40 -4
  180. package/src/engine/boot-validator/entity-handler.ts +20 -21
  181. package/src/engine/boot-validator/entity-list-screens.ts +88 -0
  182. package/src/engine/boot-validator/gdpr-storage.ts +0 -20
  183. package/src/engine/boot-validator/i18n-keys.ts +58 -4
  184. package/src/engine/boot-validator/index.ts +33 -12
  185. package/src/engine/boot-validator/nav.ts +125 -0
  186. package/src/engine/boot-validator/pii-retention.ts +95 -10
  187. package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +214 -191
  188. package/src/engine/boot-validator/workspaces.ts +68 -0
  189. package/src/engine/build-app-schema.ts +16 -0
  190. package/src/engine/build-config-feature-schema.ts +44 -7
  191. package/src/engine/codemod/pipeline-codemod.ts +5 -5
  192. package/src/engine/config-helpers.ts +15 -0
  193. package/src/engine/constants.ts +32 -6
  194. package/src/engine/create-app.ts +11 -0
  195. package/src/engine/define-feature.ts +95 -947
  196. package/src/engine/define-handler.ts +28 -94
  197. package/src/engine/define-workflow.ts +1 -1
  198. package/src/engine/effective-features.ts +12 -2
  199. package/src/engine/entity-handlers.ts +76 -11
  200. package/src/engine/extensions/tenant-data.ts +19 -0
  201. package/src/engine/extensions/user-data.ts +29 -2
  202. package/src/engine/factories.ts +8 -49
  203. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +26 -29
  204. package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
  205. package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
  206. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
  207. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
  208. package/src/engine/feature-ast/__tests__/parse.test.ts +1640 -160
  209. package/src/engine/feature-ast/__tests__/patch.test.ts +206 -12
  210. package/src/engine/feature-ast/__tests__/patcher.test.ts +20 -23
  211. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +380 -5
  212. package/src/engine/feature-ast/extractors/events.ts +322 -0
  213. package/src/engine/feature-ast/extractors/handlers.ts +234 -0
  214. package/src/engine/feature-ast/extractors/hooks.ts +243 -0
  215. package/src/engine/feature-ast/extractors/index.ts +34 -31
  216. package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
  217. package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
  218. package/src/engine/feature-ast/extractors/round1.ts +3 -3
  219. package/src/engine/feature-ast/extractors/round5.ts +3 -3
  220. package/src/engine/feature-ast/extractors/round6.ts +2 -36
  221. package/src/engine/feature-ast/extractors/shared.ts +64 -6
  222. package/src/engine/feature-ast/index.ts +2 -4
  223. package/src/engine/feature-ast/parse.ts +130 -23
  224. package/src/engine/feature-ast/patch.ts +39 -50
  225. package/src/engine/feature-ast/patcher.ts +37 -38
  226. package/src/engine/feature-ast/patterns.ts +50 -62
  227. package/src/engine/feature-ast/render.ts +67 -44
  228. package/src/engine/feature-builder-state.ts +168 -0
  229. package/src/engine/feature-config-events-jobs.ts +403 -0
  230. package/src/engine/feature-entity-handlers.ts +208 -0
  231. package/src/engine/feature-manifest.ts +2 -1
  232. package/src/engine/feature-ui-extensions.ts +500 -0
  233. package/src/engine/field-access.ts +13 -2
  234. package/src/engine/field-helpers.ts +31 -0
  235. package/src/engine/handler-helpers.ts +26 -0
  236. package/src/engine/hook-helpers.ts +16 -0
  237. package/src/engine/index.ts +23 -7
  238. package/src/engine/membership-roles.ts +13 -0
  239. package/src/engine/object-form.ts +27 -0
  240. package/src/engine/ownership.ts +26 -79
  241. package/src/engine/pattern-library/__tests__/library.test.ts +7 -20
  242. package/src/engine/pattern-library/library.ts +44 -1152
  243. package/src/engine/pattern-library/mixed-schemas.ts +450 -0
  244. package/src/engine/pattern-library/opaque-schemas.ts +124 -0
  245. package/src/engine/pattern-library/shared-fields.ts +75 -0
  246. package/src/engine/pattern-library/static-schemas.ts +456 -0
  247. package/src/engine/pipeline.ts +6 -11
  248. package/src/engine/registry-facade.ts +362 -0
  249. package/src/engine/registry-ingest.ts +478 -0
  250. package/src/engine/registry-state.ts +388 -0
  251. package/src/engine/registry-validate.ts +642 -0
  252. package/src/engine/registry.ts +78 -1658
  253. package/src/engine/run-pipeline.ts +1 -1
  254. package/src/engine/schema-builder.ts +1 -0
  255. package/src/engine/screen-helpers.ts +54 -0
  256. package/src/engine/soft-delete-cleanup.ts +6 -2
  257. package/src/engine/steps/__tests__/duration-utils.test.ts +20 -0
  258. package/src/engine/steps/_duration-utils.ts +2 -0
  259. package/src/engine/steps/unsafe-projection-upsert.ts +1 -4
  260. package/src/engine/tier-resolver-extension.ts +3 -2
  261. package/src/engine/types/config.ts +2 -482
  262. package/src/engine/types/define-handler.ts +2 -0
  263. package/src/engine/types/entity-handlers.ts +2 -0
  264. package/src/engine/types/event-type-map.ts +1 -37
  265. package/src/engine/types/feature.ts +2 -972
  266. package/src/engine/types/fields.ts +2 -675
  267. package/src/engine/types/handlers.ts +2 -774
  268. package/src/engine/types/hooks.ts +2 -184
  269. package/src/engine/types/http-route.ts +1 -54
  270. package/src/engine/types/identifiers.ts +1 -47
  271. package/src/engine/types/index.ts +86 -40
  272. package/src/engine/types/nav.ts +2 -63
  273. package/src/engine/types/ownership.ts +2 -0
  274. package/src/engine/types/projection.ts +2 -138
  275. package/src/engine/types/relations.ts +1 -51
  276. package/src/engine/types/screen.ts +2 -574
  277. package/src/engine/types/step.ts +2 -334
  278. package/src/engine/types/target-ref.ts +1 -21
  279. package/src/engine/types/tree-node.ts +1 -132
  280. package/src/engine/types/workspace.ts +2 -49
  281. package/src/engine/validate-projection-allowlist.ts +6 -6
  282. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +120 -1
  283. package/src/entrypoint/index.ts +49 -6
  284. package/src/errors/classes.ts +22 -1
  285. package/src/errors/field-issue.ts +0 -3
  286. package/src/errors/index.ts +1 -1
  287. package/src/errors/write-error-info.ts +10 -22
  288. package/src/es-ops/README.md +1 -1
  289. package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
  290. package/src/es-ops/context.ts +4 -2
  291. package/src/es-ops/types.ts +8 -1
  292. package/src/event-store/__tests__/admin-api.integration.test.ts +27 -1
  293. package/src/event-store/__tests__/backfill-pii.integration.test.ts +279 -0
  294. package/src/event-store/__tests__/event-store.integration.test.ts +201 -0
  295. package/src/event-store/__tests__/row-to-stored-event.test.ts +2 -2
  296. package/src/event-store/__tests__/snapshot.integration.test.ts +86 -0
  297. package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +58 -0
  298. package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
  299. package/src/event-store/admin-api.ts +11 -4
  300. package/src/event-store/errors.ts +2 -35
  301. package/src/event-store/event-store.ts +64 -78
  302. package/src/event-store/events-schema.ts +11 -13
  303. package/src/event-store/index.ts +18 -3
  304. package/src/event-store/rebuild-dead-letter.ts +111 -0
  305. package/src/event-store/snapshot.ts +63 -40
  306. package/src/event-store/types.ts +2 -0
  307. package/src/files/__tests__/build-storage-key.test.ts +28 -0
  308. package/src/files/__tests__/file-ref-entity.test.ts +8 -0
  309. package/src/files/__tests__/files.integration.test.ts +24 -0
  310. package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
  311. package/src/files/__tests__/local-provider.test.ts +31 -0
  312. package/src/files/__tests__/provider-resolver.test.ts +70 -0
  313. package/src/files/__tests__/write-stream.test.ts +13 -0
  314. package/src/files/file-handle.ts +2 -19
  315. package/src/files/file-ref-entity.ts +2 -2
  316. package/src/files/file-routes.ts +13 -0
  317. package/src/files/index.ts +1 -1
  318. package/src/files/local-provider.ts +27 -8
  319. package/src/files/provider-resolver.ts +31 -20
  320. package/src/files/types.ts +13 -55
  321. package/src/i18n/__tests__/required-surface-keys.test.ts +17 -0
  322. package/src/i18n/required-surface-keys.ts +120 -1
  323. package/src/jobs/__tests__/job-queue-depth.integration.test.ts +82 -0
  324. package/src/jobs/__tests__/jobs.integration.test.ts +329 -3
  325. package/src/jobs/job-runner.ts +82 -12
  326. package/src/logging/types.ts +1 -7
  327. package/src/migrations/pending-rebuilds.ts +1 -1
  328. package/src/observability/__tests__/observability.integration.test.ts +4 -1
  329. package/src/observability/__tests__/recording-tracer.test.ts +6 -4
  330. package/src/observability/index.ts +2 -0
  331. package/src/observability/noop-provider.ts +0 -9
  332. package/src/observability/recording-tracer.ts +0 -12
  333. package/src/observability/standard-metrics.ts +64 -2
  334. package/src/observability/types/index.ts +1 -29
  335. package/src/observability/types/metric.ts +1 -56
  336. package/src/observability/types/provider.ts +1 -32
  337. package/src/observability/types/span.ts +1 -64
  338. package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
  339. package/src/pipeline/__tests__/dispatcher.test.ts +366 -1
  340. package/src/pipeline/__tests__/event-consumer-state.integration.test.ts +31 -0
  341. package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
  342. package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +263 -0
  343. package/src/pipeline/__tests__/event-dispatcher.integration.test.ts +10 -0
  344. package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +106 -0
  345. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +308 -77
  346. package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
  347. package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
  348. package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +80 -2
  349. package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +274 -0
  350. package/src/pipeline/__tests__/try-append-event.integration.test.ts +166 -0
  351. package/src/pipeline/dispatch-batch.ts +187 -0
  352. package/src/pipeline/dispatch-query.ts +165 -0
  353. package/src/pipeline/dispatch-shared.ts +826 -0
  354. package/src/pipeline/dispatch-stream.ts +90 -0
  355. package/src/pipeline/dispatch-write.ts +451 -0
  356. package/src/pipeline/dispatcher-utils.ts +1 -1
  357. package/src/pipeline/dispatcher.ts +44 -1372
  358. package/src/pipeline/entity-cache.ts +2 -33
  359. package/src/pipeline/event-consumer-state.ts +30 -2
  360. package/src/pipeline/event-dispatcher-admin.ts +293 -0
  361. package/src/pipeline/event-dispatcher-delivery.ts +305 -0
  362. package/src/pipeline/event-dispatcher.ts +85 -542
  363. package/src/pipeline/index.ts +2 -0
  364. package/src/pipeline/msp-rebuild.ts +42 -3
  365. package/src/pipeline/multi-stream-apply-context.ts +4 -42
  366. package/src/pipeline/projection-rebuild.ts +105 -3
  367. package/src/pipeline/system-hooks.ts +144 -1
  368. package/src/rate-limit/__tests__/resolver.integration.test.ts +18 -0
  369. package/src/rate-limit/resolver.ts +16 -32
  370. package/src/schema-cli.ts +76 -16
  371. package/src/search/__tests__/meilisearch-adapter.integration.test.ts +207 -185
  372. package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
  373. package/src/search/__tests__/reindex-entity.integration.test.ts +144 -0
  374. package/src/search/index.ts +6 -0
  375. package/src/search/meilisearch-adapter.ts +13 -12
  376. package/src/search/reindex-entity.ts +177 -0
  377. package/src/search/types.ts +1 -39
  378. package/src/secrets/__tests__/contains-secret.test.ts +34 -0
  379. package/src/secrets/__tests__/envelope-cipher.test.ts +59 -0
  380. package/src/secrets/__tests__/envelope.test.ts +1 -1
  381. package/src/secrets/dek-cache.ts +26 -5
  382. package/src/secrets/envelope-cipher.ts +53 -0
  383. package/src/secrets/envelope.ts +5 -3
  384. package/src/secrets/index.ts +13 -1
  385. package/src/secrets/stored-envelope.ts +46 -0
  386. package/src/secrets/types.ts +2 -162
  387. package/src/stack/__tests__/event-collector.test.ts +42 -0
  388. package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +125 -0
  389. package/src/stack/db.ts +2 -1
  390. package/src/stack/push-entity-projection-tables.ts +4 -3
  391. package/src/stack/redis.ts +8 -0
  392. package/src/stack/request-helper.ts +38 -2
  393. package/src/stack/table-helpers.ts +6 -4
  394. package/src/stack/test-stack.ts +249 -131
  395. package/src/testing/__tests__/late-bound.test.ts +32 -0
  396. package/src/testing/__tests__/wait-for.test.ts +59 -0
  397. package/src/testing/boot-validator-fixture.ts +121 -0
  398. package/src/testing/e2e-generator.ts +8 -0
  399. package/src/testing/file-provider-contract.ts +104 -0
  400. package/src/testing/handler-context.ts +3 -1
  401. package/src/testing/index.ts +5 -0
  402. package/src/testing/late-bound.ts +5 -3
  403. package/src/testing/mutable-master-key-provider.ts +29 -3
  404. package/src/testing/wait-for.ts +3 -0
  405. package/src/testing/without-ambient-temporal.ts +14 -0
  406. package/src/time/__tests__/tz-dateline.test.ts +8 -10
  407. package/src/time/geo-tz.ts +1 -32
  408. package/src/time/index.ts +1 -0
  409. package/src/time/legacy-date.ts +18 -0
  410. package/src/time/polyfill.ts +21 -38
  411. package/src/time/tz-context.ts +44 -85
  412. package/src/ui-types/app-schema.ts +12 -0
  413. package/src/ui-types/index.ts +20 -7
  414. package/src/utils/__tests__/safe-json-temporal.test.ts +18 -0
  415. package/src/utils/safe-json.ts +13 -1
  416. package/src/__tests__/raw-table.integration.test.ts +0 -116
  417. package/src/bun-db/__tests__/bun-test-stack.ts +0 -6
  418. package/src/db/__tests__/encryption.test.ts +0 -39
  419. package/src/db/encryption.ts +0 -39
  420. package/src/db/row-helpers.ts +0 -4
  421. package/src/engine/__tests__/raw-table.test.ts +0 -150
  422. package/src/engine/__tests__/unmanaged-table.test.ts +0 -127
  423. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +0 -184
  424. package/src/engine/feature-ast/extractors/round4.ts +0 -1366
@@ -1,84 +1,10 @@
1
- import type { ZodType, z } from "zod";
2
- import type { EntityTableMeta } from "../db/entity-table-meta";
3
- import { toTableName } from "../db/table-builder";
4
- import { LifecycleHookTypes } from "./constants";
5
- import type { QueryHandlerDefinition, WriteHandlerDefinition } from "./define-handler";
6
- import { isKebabSegment, QnTypes, qn, toKebab } from "./qualified-name";
7
- import type {
8
- AccessRule,
9
- AuthClaimsFn,
10
- ClaimKeyDefinition,
11
- ClaimKeyHandle,
12
- ClaimKeyType,
13
- ConfigKeyDefinition,
14
- ConfigKeyHandle,
15
- ConfigKeyType,
16
- ConfigSeedDef,
17
- EntityDefinition,
18
- EntityRef,
19
- EventDef,
20
- EventMigrationDef,
21
- EventUpcastFn,
22
- ExtensionSelectorDef,
23
- FeatureDefinition,
24
- FeatureMetricDef,
25
- FeatureRegistrar,
26
- HandlerRef,
27
- HookMap,
28
- HookPhase,
29
- JobDefinition,
30
- JobHandlerFn,
31
- LifecycleHookFn,
32
- LifecycleHookType,
33
- MetricOptions,
34
- MultiStreamProjectionDefinition,
35
- NameOrRef,
36
- NotificationDataFn,
37
- NotificationDefinition,
38
- NotificationRecipientFn,
39
- NotificationTemplateFn,
40
- OwnedFn,
41
- PhasedHook,
42
- PostDeleteHookFn,
43
- PostQueryHookFn,
44
- PostSaveHookFn,
45
- PreDeleteHookFn,
46
- ProjectionDefinition,
47
- QualifiedEventName,
48
- QueryHandlerDef,
49
- QueryHandlerFn,
50
- RateLimitOption,
51
- RawTableEntry,
52
- RawTableOptions,
53
- ReferenceDataDef,
54
- RegistrarExtensionDef,
55
- RegistrarExtensionRegistration,
56
- RelationDefinition,
57
- SearchPayloadContributorFn,
58
- SecretKeyDefinition,
59
- SecretKeyHandle,
60
- SecretOptions,
61
- TranslationKeys,
62
- TranslationsDef,
63
- TreeActionDef,
64
- TreeActionsHandle,
65
- TreeChildrenSubscribe,
66
- UiHints,
67
- UnmanagedTableEntry,
68
- ValidationHookFn,
69
- WriteHandlerDef,
70
- WriteHandlerFn,
71
- } from "./types";
72
- import { HookPhases } from "./types";
1
+ import { createInitialFeatureBuilderState } from "./feature-builder-state";
2
+ import { buildConfigEventsJobsMethods } from "./feature-config-events-jobs";
3
+ import { buildEntityHandlerMethods } from "./feature-entity-handlers";
4
+ import { buildUiExtensionsMethods } from "./feature-ui-extensions";
5
+ import { unwrapArrayForm } from "./object-form";
6
+ import type { FeatureDefinition, FeatureRegistrar, HookMap, UiHints } from "./types";
73
7
  import type { RequiresApi } from "./types/feature";
74
- import { resolveName } from "./types/handlers";
75
- import type { HttpRouteDefinition } from "./types/http-route";
76
- import type { NavDefinition } from "./types/nav";
77
- import type { ScreenDefinition } from "./types/screen";
78
- import type { PipelineDef } from "./types/step";
79
- import type { WorkspaceDefinition } from "./types/workspace";
80
-
81
- const LIFECYCLE_TYPES = Object.values(LifecycleHookTypes);
82
8
 
83
9
  // `TExports` lets the setup callback hand back a typed object that
84
10
  // downstream features can import (e.g. `tenantFeature.exports.config`). The
@@ -92,120 +18,29 @@ const LIFECYCLE_TYPES = Object.values(LifecycleHookTypes);
92
18
  // for `ctx.appendEvent({ type: eventDef.name, ... })` lights up. Apps
93
19
  // that don't care can keep the default-string and use the wrapper-based
94
20
  // strict-mode (string-literal types per call-site) like before.
21
+
22
+ // requires/optionalRequires accept either variadic strings (hand-written
23
+ // call sites) or a single `{ features }` object (the feature-ast renderer's
24
+ // canonical Object-Form for Designer/AI-generated code) — see the matching
25
+ // overloads on RequiresApi / FeatureRegistrar['optionalRequires'].
26
+ function resolveFeatureNamesArgs(
27
+ args: readonly [{ readonly features: readonly string[] }] | readonly string[],
28
+ ): readonly string[] {
29
+ return unwrapArrayForm(args, "features");
30
+ }
31
+
95
32
  export function defineFeature<const TName extends string, TExports = undefined>(
96
33
  name: TName,
97
34
  setup: (r: FeatureRegistrar<TName>) => TExports,
98
35
  ): FeatureDefinition & { readonly exports: TExports } {
99
- const requires: string[] = [];
100
- const optionalRequires: string[] = [];
101
- // Read-side projection-tables declared via r.requires.projection("table").
102
- // Boot-validator checks unsafeProjection-* step calls against this set.
103
- const requiredProjections = new Set<string>();
104
- // Tier-2 step kinds declared via r.requires.step("webhook.send"). Q9.
105
- const requiredSteps = new Set<string>();
106
- const entities: Record<string, EntityDefinition> = {};
107
- const entityTables: Record<string, unknown> = {};
108
- const relations: Record<string, Record<string, RelationDefinition>> = {};
109
- const writeHandlers: Record<string, WriteHandlerDef> = {};
110
- const queryHandlers: Record<string, QueryHandlerDef> = {};
111
- const validationHooks: Record<string, ValidationHookFn> = {};
112
- // preSave/preQuery stay unphased (owned-fn); postSave/preDelete/postDelete
113
- // are phased (owned-fn + phase). Each hook carries its owning feature so
114
- // the lifecycle pipeline can filter by effectiveFeatures without a parallel
115
- // bookkeeping structure.
116
- const lifecycleHooks: Record<string, Record<string, OwnedFn<LifecycleHookFn>[]>> = {};
117
- const phasedLifecycleHooks: Record<
118
- "postSave" | "preDelete" | "postDelete",
119
- Record<string, PhasedHook<LifecycleHookFn>[]>
120
- > = { postSave: {}, preDelete: {}, postDelete: {} };
121
- const configKeys: Record<string, ConfigKeyDefinition> = {};
122
- const configSeeds: ConfigSeedDef[] = [];
123
- const jobs: Record<string, JobDefinition> = {};
124
- const events: Record<string, { name: string; schema: ZodType; version: number }> = {};
125
- const eventMigrations: Record<string, EventMigrationDef[]> = {};
126
- const configReads: string[] = [];
127
- const entityPostSave: Record<string, PhasedHook<PostSaveHookFn>[]> = {};
128
- const entityPreDelete: Record<string, PhasedHook<PreDeleteHookFn>[]> = {};
129
- const entityPostDelete: Record<string, PhasedHook<PostDeleteHookFn>[]> = {};
130
- const entityPostQuery: Record<string, OwnedFn<PostQueryHookFn>[]> = {};
131
- const searchPayloadExtensions: Record<string, OwnedFn<SearchPayloadContributorFn>[]> = {};
132
- const notifications: Record<string, NotificationDefinition> = {};
133
- const registrarExtensions: Record<string, RegistrarExtensionDef> = {};
134
- const extensionUsages: RegistrarExtensionRegistration[] = [];
135
- const extensionSelectors: ExtensionSelectorDef[] = [];
136
- const exposedApis: Set<string> = new Set();
137
- const usedApis: Set<string> = new Set();
138
- const referenceData: ReferenceDataDef[] = [];
139
- const handlerEntityMappings: Record<string, string> = {};
140
- const metrics: Record<string, FeatureMetricDef> = {};
141
- const secretKeys: Record<string, SecretKeyDefinition> = {};
142
- const projections: Record<string, ProjectionDefinition> = {};
143
- const multiStreamProjections: Record<string, MultiStreamProjectionDefinition> = {};
144
- const rawTables: Record<string, RawTableEntry> = {};
145
- const unmanagedTables: Record<string, UnmanagedTableEntry> = {};
146
- const authClaimsHooks: AuthClaimsFn[] = [];
147
- const claimKeys: Record<string, ClaimKeyDefinition> = {};
148
- const screens: Record<string, ScreenDefinition> = {};
149
- const navs: Record<string, NavDefinition> = {};
150
- const workspaces: Record<string, WorkspaceDefinition> = {};
151
- const httpRoutes: Record<string, HttpRouteDefinition> = {};
152
- let translations: TranslationKeys = {};
153
-
154
- for (const t of LIFECYCLE_TYPES) {
155
- lifecycleHooks[t] = {};
156
- }
157
-
158
- let isSystemScoped = false;
159
- let toggleableDefault: boolean | undefined;
160
- let description: string | undefined;
161
- let uiHints: UiHints | undefined;
162
- // Visual-Tree-Slots — at-most-one per feature, only-once-guard im
163
- // registrar (siehe r.treeActions / r.tree). Undefined wenn das Feature
164
- // keinen Visual-Tree-Beitrag liefert (Zero-Whitelist-Filter).
165
- // Name-Collision-Sicherheit: object-literal-method-Names im registrar
166
- // sind keine bindings im closure-scope, daher kollidiert die `treeActions`
167
- // closure-let-var nicht mit der `treeActions(...)` registrar-Methode.
168
- let treeActions: Readonly<Record<string, TreeActionDef>> | undefined;
169
- let treeProvider: TreeChildrenSubscribe | undefined;
170
- // Optional Zod-schema for env-vars this feature reads at runtime,
171
- // declared via r.envSchema(). At-most-one per feature.
172
- let envSchema: z.ZodObject<z.ZodRawShape> | undefined;
173
-
174
- // Map handler name to entity via colon convention.
175
- // "task:create" → entity "task". Bare CRUD verbs (create/update/delete) map
176
- // when feature name matches an entity or the feature owns exactly one entity.
177
- const CRUD_VERBS = new Set(["create", "update", "delete"]);
178
-
179
- function tryMapEntity(handlerName: string): void {
180
- const colonIdx = handlerName.indexOf(":");
181
- if (colonIdx >= 0) {
182
- const candidate = handlerName.slice(0, colonIdx);
183
- if (entities[candidate]) {
184
- handlerEntityMappings[handlerName] = candidate;
185
- }
186
- // skip: colon-prefixed handler processed (mapped or not), bare CRUD path not applicable
187
- return;
188
- }
189
- if (CRUD_VERBS.has(handlerName)) {
190
- if (entities[name]) {
191
- handlerEntityMappings[handlerName] = name;
192
- // skip: feature-name entity match is the preferred mapping
193
- return;
194
- }
195
- const entityKeys = Object.keys(entities);
196
- if (entityKeys.length === 1) {
197
- handlerEntityMappings[handlerName] = entityKeys[0] as string;
198
- }
199
- }
200
- }
36
+ const state = createInitialFeatureBuilderState();
201
37
 
202
38
  const registrar: FeatureRegistrar<TName> = {
203
39
  systemScope(): void {
204
- isSystemScoped = true;
40
+ state.isSystemScoped = true;
205
41
  },
206
-
207
42
  describe(text: string): void {
208
- if (description !== undefined) {
43
+ if (state.description !== undefined) {
209
44
  throw new Error(
210
45
  `[Feature ${name}] r.describe() called twice — a feature's description is declared once`,
211
46
  );
@@ -213,801 +48,114 @@ export function defineFeature<const TName extends string, TExports = undefined>(
213
48
  if (typeof text !== "string" || text.trim().length === 0) {
214
49
  throw new Error(`[Feature ${name}] r.describe(): text must be a non-empty string`);
215
50
  }
216
- description = text.trim();
51
+ state.description = text.trim();
217
52
  },
218
-
219
53
  requires: (() => {
220
- const fn = (...featureNames: string[]) => {
221
- requires.push(...featureNames);
54
+ const fn = (
55
+ ...args: readonly [{ readonly features: readonly string[] }] | readonly string[]
56
+ ) => {
57
+ state.requires.push(...resolveFeatureNamesArgs(args));
222
58
  };
223
59
  fn.projection = (tableName: string) => {
224
- requiredProjections.add(tableName);
60
+ state.requiredProjections.add(tableName);
225
61
  };
226
62
  fn.step = (stepKind: string) => {
227
- requiredSteps.add(stepKind);
63
+ state.requiredSteps.add(stepKind);
228
64
  };
229
65
  return fn as RequiresApi;
230
66
  })(),
231
-
232
- optionalRequires(...featureNames: string[]): void {
233
- optionalRequires.push(...featureNames);
67
+ optionalRequires(
68
+ ...args: readonly [{ readonly features: readonly string[] }] | readonly string[]
69
+ ): void {
70
+ state.optionalRequires.push(...resolveFeatureNamesArgs(args));
234
71
  },
235
-
236
72
  toggleable(options: { default: boolean }): void {
237
- if (toggleableDefault !== undefined) {
73
+ if (state.toggleableDefault !== undefined) {
238
74
  throw new Error(
239
75
  `[Feature ${name}] r.toggleable() called twice — a feature's toggleable status is declared once`,
240
76
  );
241
77
  }
242
- toggleableDefault = options.default;
78
+ state.toggleableDefault = options.default;
243
79
  },
244
-
245
80
  uiHints(hints: UiHints): void {
246
- if (uiHints !== undefined) {
81
+ if (state.uiHints !== undefined) {
247
82
  throw new Error(`[Feature ${name}] r.uiHints() called twice — UI hints are declared once`);
248
83
  }
249
- uiHints = hints;
250
- },
251
-
252
- entity(
253
- entityName: string,
254
- definition: EntityDefinition,
255
- options?: { readonly table?: unknown },
256
- ): EntityRef {
257
- entities[entityName] = definition;
258
- if (options?.table !== undefined) entityTables[entityName] = options.table;
259
- return { name: entityName, table: definition.table ?? toTableName(entityName) };
260
- },
261
-
262
- writeHandler<TName extends string, TSchema extends ZodType>(
263
- nameOrDef: string | WriteHandlerDefinition<TName, TSchema>,
264
- schema?: TSchema,
265
- handler?: WriteHandlerFn<z.infer<TSchema>>,
266
- options?: { access?: AccessRule; rateLimit?: RateLimitOption },
267
- ): HandlerRef {
268
- if (typeof nameOrDef === "object") {
269
- const def = nameOrDef;
270
- writeHandlers[def.name] = {
271
- name: def.name,
272
- schema: def.schema,
273
- // @cast-boundary engine-bridge — typed Dev-API's handler is
274
- // generic over the schema's parsed payload (`WriteEvent<output<TSchema>>`),
275
- // the storage form WriteHandlerFn carries `WriteEvent<unknown>`.
276
- // Function-arg variance: TS sees the typed handler as stricter
277
- // than the loose storage shape and rejects direct assignment.
278
- // The runtime value is identical — the cast crosses that boundary.
279
- // `satisfies` does not work here (it asserts assignability, which
280
- // is what fails). Explicit cast is the right tool.
281
- handler: def.handler as WriteHandlerFn,
282
- ...(def.access && { access: def.access }),
283
- ...(def.unsafeSkipTransitionGuard && { unsafeSkipTransitionGuard: true }),
284
- ...(def.rateLimit && { rateLimit: def.rateLimit }),
285
- // Forward the pipeline-build closure so boot-validators and
286
- // Designer/AI tooling can inspect the step list. Absent on
287
- // free-form handlers — defineWriteHandler only sets `perform`
288
- // when the author used the pipeline form. Variance cast
289
- // mirrors the handler-cast above: PipelineDef<output<TSchema>>
290
- // is stricter than PipelineDef<unknown> for the same reason.
291
- ...(def.perform !== undefined && {
292
- perform: def.perform as PipelineDef,
293
- }),
294
- };
295
- tryMapEntity(def.name);
296
- return { name: def.name };
297
- }
298
- if (!schema || !handler)
299
- throw new Error("writeHandler inline form requires schema + handler");
300
- writeHandlers[nameOrDef] = {
301
- name: nameOrDef,
302
- schema,
303
- handler: handler as WriteHandlerFn, // @cast-boundary engine-bridge
304
- ...(options?.access && { access: options.access }),
305
- ...(options?.rateLimit && { rateLimit: options.rateLimit }),
306
- };
307
- tryMapEntity(nameOrDef);
308
- return { name: nameOrDef };
309
- },
310
-
311
- queryHandler<TName extends string, TSchema extends ZodType>(
312
- nameOrDef: string | QueryHandlerDefinition<TName, TSchema>,
313
- schema?: TSchema,
314
- handler?: QueryHandlerFn<z.infer<TSchema>>,
315
- options?: { access?: AccessRule; rateLimit?: RateLimitOption },
316
- ): HandlerRef {
317
- if (typeof nameOrDef === "object") {
318
- const def = nameOrDef;
319
- queryHandlers[def.name] = {
320
- name: def.name,
321
- schema: def.schema,
322
- // @cast-boundary engine-bridge — typed Dev-API → erased internal storage
323
- handler: def.handler as QueryHandlerFn, // @cast-boundary engine-bridge
324
- ...(def.access && { access: def.access }),
325
- ...(def.rateLimit && { rateLimit: def.rateLimit }),
326
- };
327
- tryMapEntity(def.name);
328
- return { name: def.name };
329
- }
330
- if (!schema || !handler)
331
- throw new Error("queryHandler inline form requires schema + handler");
332
- queryHandlers[nameOrDef] = {
333
- name: nameOrDef,
334
- schema,
335
- handler: handler as QueryHandlerFn, // @cast-boundary engine-bridge
336
- ...(options?.access && { access: options.access }),
337
- ...(options?.rateLimit && { rateLimit: options.rateLimit }),
338
- };
339
- tryMapEntity(nameOrDef);
340
- return { name: nameOrDef };
341
- },
342
-
343
- relation(entityRef: NameOrRef, relationName: string, definition: RelationDefinition): void {
344
- const entityName = resolveName(entityRef);
345
- if (!relations[entityName]) relations[entityName] = {};
346
- relations[entityName][relationName] = definition;
347
- },
348
-
349
- hook(
350
- type: LifecycleHookType | "validation",
351
- target: NameOrRef | readonly NameOrRef[],
352
- fn: LifecycleHookFn | ValidationHookFn,
353
- options?: { phase?: HookPhase },
354
- ): void {
355
- const targets = Array.isArray(target) ? target : [target];
356
- const names = targets.map(resolveName);
357
-
358
- // Hook-fn casts unten alle: @cast-boundary engine-bridge
359
- // — typed Dev-API (LifecycleHookFn|ValidationHookFn) → erased Map<name, fn>.
360
- if (type === "validation") {
361
- for (const n of names) {
362
- validationHooks[n] = fn as ValidationHookFn; // @cast-boundary engine-bridge
363
- }
364
- // skip: validation hooks have no phase, stored and done
365
- return;
366
- }
367
-
368
- if (
369
- type === LifecycleHookTypes.preSave ||
370
- type === LifecycleHookTypes.preQuery ||
371
- type === LifecycleHookTypes.postQuery
372
- ) {
373
- if (!lifecycleHooks[type]) lifecycleHooks[type] = {};
374
- for (const n of names) {
375
- if (!lifecycleHooks[type][n]) lifecycleHooks[type][n] = [];
376
- lifecycleHooks[type][n].push({ fn: fn as LifecycleHookFn, featureName: name }); // @cast-boundary engine-bridge
377
- }
378
- // skip: pre/post-hooks without phase semantics, stored and done
379
- return;
380
- }
381
-
382
- // Phased storage. preDelete has no phase option (always inTransaction);
383
- // postSave/postDelete default to afterCommit.
384
- const phase =
385
- type === LifecycleHookTypes.preDelete
386
- ? HookPhases.inTransaction
387
- : (options?.phase ?? HookPhases.afterCommit);
388
- const bucket = phasedLifecycleHooks[type];
389
- for (const n of names) {
390
- if (!bucket[n]) bucket[n] = [];
391
- bucket[n].push({ fn: fn as LifecycleHookFn, phase, featureName: name }); // @cast-boundary engine-bridge
392
- }
393
- },
394
-
395
- entityHook(
396
- type: "postSave" | "preDelete" | "postDelete" | "postQuery",
397
- entityRef: NameOrRef,
398
- fn: LifecycleHookFn,
399
- options?: { phase?: HookPhase },
400
- ): void {
401
- const entityName = resolveName(entityRef);
402
- if (type === LifecycleHookTypes.postSave) {
403
- const phase = options?.phase ?? HookPhases.afterCommit;
404
- if (!entityPostSave[entityName]) entityPostSave[entityName] = [];
405
- entityPostSave[entityName].push({ fn: fn as PostSaveHookFn, phase, featureName: name }); // @cast-boundary engine-bridge
406
- } else if (type === LifecycleHookTypes.preDelete) {
407
- if (!entityPreDelete[entityName]) entityPreDelete[entityName] = [];
408
- entityPreDelete[entityName].push({
409
- fn: fn as PreDeleteHookFn, // @cast-boundary engine-bridge
410
- phase: HookPhases.inTransaction,
411
- featureName: name,
412
- });
413
- } else if (type === LifecycleHookTypes.postDelete) {
414
- const phase = options?.phase ?? HookPhases.afterCommit;
415
- if (!entityPostDelete[entityName]) entityPostDelete[entityName] = [];
416
- entityPostDelete[entityName].push({ fn: fn as PostDeleteHookFn, phase, featureName: name }); // @cast-boundary engine-bridge
417
- } else if (type === LifecycleHookTypes.postQuery) {
418
- // postQuery is unphased (no inTransaction/afterCommit semantics — fires
419
- // synchronously after query-handler-execute, before field-access-filter)
420
- if (!entityPostQuery[entityName]) entityPostQuery[entityName] = [];
421
- entityPostQuery[entityName].push({ fn: fn as PostQueryHookFn, featureName: name }); // @cast-boundary engine-bridge
422
- }
423
- },
424
-
425
- searchPayloadExtension(entityRef: NameOrRef, fn: SearchPayloadContributorFn): void {
426
- const entityName = resolveName(entityRef);
427
- if (!searchPayloadExtensions[entityName]) searchPayloadExtensions[entityName] = [];
428
- searchPayloadExtensions[entityName].push({ fn, featureName: name });
429
- },
430
-
431
- config<TKeys extends Readonly<Record<string, ConfigKeyDefinition<ConfigKeyType>>>>(definition: {
432
- readonly keys: TKeys;
433
- readonly seeds?: Readonly<Record<string, ConfigSeedDef>>;
434
- }): { readonly [K in keyof TKeys]: ConfigKeyHandle<TKeys[K]["type"]> } {
435
- // Qualify eagerly (same as defineEvent) so the handle name matches what
436
- // the registry stores — lazy qualification would break compile-time
437
- // autocomplete and hand-built test registries.
438
- const handles: Record<string, ConfigKeyHandle<ConfigKeyType>> = {};
439
- for (const [key, keyDef] of Object.entries(definition.keys)) {
440
- configKeys[key] = keyDef;
441
- handles[key] = {
442
- name: qn(toKebab(name), "config", toKebab(key)),
443
- type: keyDef.type,
444
- };
445
- }
446
- // Parse seeds: resolve qualified key names and validate scope
447
- if (definition.seeds) {
448
- for (const [shortKey, seedDef] of Object.entries(definition.seeds)) {
449
- const keyDef = definition.keys[shortKey];
450
- if (!keyDef) continue; // skip — boot-validator reports unknown keys
451
- const qualifiedKey = qn(toKebab(name), "config", toKebab(shortKey));
452
- const scope = seedDef.scope ?? keyDef.scope;
453
- configSeeds.push({
454
- key: qualifiedKey,
455
- value: seedDef.value,
456
- scope,
457
- tenantId: seedDef.tenantId,
458
- userId: seedDef.userId,
459
- });
460
- }
461
- }
462
- return handles as {
463
- readonly [K in keyof TKeys]: ConfigKeyHandle<TKeys[K]["type"]>;
464
- }; // @cast-boundary engine-bridge — Mapped-Type-Inference at config()-callsite
465
- },
466
-
467
- job(
468
- jobName: string,
469
- options: Omit<JobDefinition, "name" | "handler">,
470
- handler: JobHandlerFn,
471
- ): void {
472
- // Resolve NameOrRef(s) in trigger.on. Multi-Trigger-Form: Array
473
- // wird zu Array von resolved strings, Single bleibt single string —
474
- // job-runner unterscheidet anhand Array.isArray.
475
- const trigger =
476
- "on" in options.trigger
477
- ? {
478
- on: Array.isArray(options.trigger.on)
479
- ? options.trigger.on.map(resolveName)
480
- : resolveName(options.trigger.on as NameOrRef), // @cast-boundary engine-bridge
481
- }
482
- : options.trigger;
483
- jobs[jobName] = { ...options, trigger, name: jobName, handler };
484
- },
485
-
486
- notification(
487
- notificationName: string,
488
- definition: {
489
- readonly trigger: { readonly on: NameOrRef };
490
- readonly recipient: NotificationRecipientFn;
491
- readonly data: NotificationDataFn;
492
- readonly templates?: Readonly<Record<string, NotificationTemplateFn>>;
493
- },
494
- ): void {
495
- notifications[notificationName] = {
496
- name: notificationName,
497
- trigger: { on: resolveName(definition.trigger.on) },
498
- recipient: definition.recipient,
499
- data: definition.data,
500
- templates: definition.templates,
501
- };
502
- },
503
-
504
- translations(def: TranslationsDef): void {
505
- translations = { ...translations, ...def.keys };
506
- },
507
-
508
- defineEvent: <const TInner extends string, TPayload>(
509
- eventName: TInner,
510
- schema: ZodType<TPayload>,
511
- options?: { readonly version?: number },
512
- ): EventDef<TPayload, QualifiedEventName<TName, TInner>> => {
513
- // Return the fully-qualified event name so callers can pass it
514
- // straight to ctx.appendEvent without hand-building the
515
- // "<feature>:event:<name>" shape. Registry keeps events keyed by
516
- // short name — qualification is the framework's job, not the feature
517
- // author's.
518
- //
519
- // The runtime kebab-step (`qn(toKebab(feature), …)`) is mirrored at
520
- // the type-level by `QualifiedEventName<TName, TInner>` so the
521
- // returned `name` carries the literal qualified shape that the
522
- // augmented `KumikoEventTypeMap` keys against.
523
- const qualified = qn(toKebab(name), "event", toKebab(eventName));
524
- const version = options?.version ?? 1;
525
- if (!Number.isInteger(version) || version < 1) {
526
- throw new Error(
527
- `[Feature ${name}] defineEvent("${eventName}"): version must be a positive integer, got ${String(version)}`,
528
- );
529
- }
530
- // @cast-boundary engine-bridge — runtime-string mirrors the
531
- // template-literal-type via QualifiedEventName + toKebab. Both
532
- // sides are tested (CamelToKebab type-tests + scan-events kebab
533
- // tests), so the cast is a contract, not a typing-loss.
534
- const def: EventDef<TPayload, QualifiedEventName<TName, TInner>> = {
535
- name: qualified as QualifiedEventName<TName, TInner>,
536
- schema,
537
- version,
538
- };
539
- events[eventName] = def;
540
- return def;
541
- },
542
-
543
- eventMigration(
544
- eventName: string,
545
- fromVersion: number,
546
- toVersion: number,
547
- transform: EventUpcastFn,
548
- ): void {
549
- if (toVersion !== fromVersion + 1) {
550
- throw new Error(
551
- `[Feature ${name}] eventMigration("${eventName}", ${fromVersion}, ${toVersion}): ` +
552
- `only single-step migrations are allowed — toVersion must be fromVersion + 1. ` +
553
- `Chain larger jumps by registering each step separately.`,
554
- );
555
- }
556
- if (!Number.isInteger(fromVersion) || fromVersion < 1) {
557
- throw new Error(
558
- `[Feature ${name}] eventMigration("${eventName}", ...): fromVersion must be >= 1, got ${String(fromVersion)}`,
559
- );
560
- }
561
- const qualified = qn(toKebab(name), "event", toKebab(eventName));
562
- const list = eventMigrations[eventName] ?? [];
563
- if (list.some((m) => m.fromVersion === fromVersion)) {
564
- throw new Error(
565
- `[Feature ${name}] eventMigration("${eventName}", ${fromVersion}, ${toVersion}): ` +
566
- `a migration from v${fromVersion} is already registered. Each step may only be declared once.`,
567
- );
568
- }
569
- list.push({ eventName: qualified, fromVersion, toVersion, transform });
570
- eventMigrations[eventName] = list;
571
- },
572
-
573
- readsConfig(...qualifiedKeys: string[]): void {
574
- configReads.push(...qualifiedKeys);
575
- },
576
-
577
- referenceData(
578
- entityRef: NameOrRef,
579
- data: readonly Record<string, unknown>[],
580
- options?: { upsertKey?: string },
581
- ): void {
582
- referenceData.push({
583
- entityName: resolveName(entityRef),
584
- data,
585
- upsertKey: options?.upsertKey,
586
- });
587
- },
588
-
589
- extendsRegistrar(extensionName: string, def: RegistrarExtensionDef): void {
590
- registrarExtensions[extensionName] = def;
591
- },
592
-
593
- useExtension(
594
- extensionName: string,
595
- entityRef: NameOrRef,
596
- options?: Record<string, unknown>,
597
- ): void {
598
- extensionUsages.push({ extensionName, entityName: resolveName(entityRef), options });
599
- },
600
-
601
- extensionSelector(extensionName: string, key: { readonly name: string } | string): void {
602
- if (extensionSelectors.some((s) => s.extensionName === extensionName)) {
603
- throw new Error(
604
- `[Feature ${name}] extensionSelector("${extensionName}") declared twice — ` +
605
- `one selector key per extension point.`,
606
- );
607
- }
608
- const qualifiedKey = typeof key === "string" ? key : key.name;
609
- extensionSelectors.push({ extensionName, qualifiedKey });
610
- },
611
-
612
- /**
613
- * Marker-Deklaration: dieses Feature stellt eine Cross-Feature-API
614
- * unter dem genannten Namen bereit. Die eigentliche Implementation
615
- * wird separat als Query- oder Write-Handler unter dem QN-Pattern
616
- * registriert; r.exposesApi ist reine Boot-Check-Surface.
617
- *
618
- * Beispiel:
619
- * defineFeature("compliance-profiles", (r) => {
620
- * r.exposesApi("compliance.forTenant");
621
- * r.queryHandler({ name: "compliance:query:for-tenant", ... });
622
- * });
623
- * defineFeature("user-data-rights", (r) => {
624
- * r.requires("compliance-profiles");
625
- * r.usesApi("compliance.forTenant");
626
- * // ruft im Handler: ctx.callQuery("compliance:query:for-tenant", ...)
627
- * });
628
- */
629
- exposesApi(apiName: string): void {
630
- if (exposedApis.has(apiName)) {
631
- throw new Error(
632
- `[Feature ${name}] r.exposesApi("${apiName}") called twice — API names must be unique within a feature.`,
633
- );
634
- }
635
- exposedApis.add(apiName);
636
- },
637
-
638
- /**
639
- * Declares that this feature calls a cross-feature API. Boot-Validator
640
- * checkt dass irgendein anderes Feature `r.exposesApi(name)` macht und
641
- * dass dieses Feature `r.requires` darauf hat.
642
- */
643
- usesApi(apiName: string): void {
644
- usedApis.add(apiName);
645
- },
646
-
647
- metric(shortName: string, options: MetricOptions): void {
648
- if (metrics[shortName]) {
649
- throw new Error(
650
- `[Feature ${name}] Metric "${shortName}" already registered. ` +
651
- `Metric names must be unique per feature.`,
652
- );
653
- }
654
- metrics[shortName] = { shortName, ...options };
655
- },
656
-
657
- envSchema(schema: z.ZodObject<z.ZodRawShape>): void {
658
- if (envSchema !== undefined) {
659
- throw new Error(
660
- `[Feature ${name}] r.envSchema() called twice — declare one composed Zod-object per feature.`,
661
- );
662
- }
663
- envSchema = schema;
664
- },
665
-
666
- secret(shortName: string, options: SecretOptions): SecretKeyHandle {
667
- if (secretKeys[shortName]) {
668
- throw new Error(
669
- `[Feature ${name}] Secret "${shortName}" already registered. ` +
670
- `Secret key names must be unique per feature.`,
671
- );
672
- }
673
- // Qualified name follows the framework's "<feature>:<type>:<name>"
674
- // QN convention — same pattern config / jobs / events use. toKebab
675
- // handles the common input shapes ("stripe.apiKey" → "stripe-api-key")
676
- // so features can declare keys in their natural style without
677
- // thinking about kebab-case on every call.
678
- const qualifiedName = qn(toKebab(name), QnTypes.secret, toKebab(shortName));
679
- secretKeys[shortName] = {
680
- shortName,
681
- qualifiedName,
682
- ...options,
683
- };
684
- return { name: qualifiedName };
685
- },
686
-
687
- projection(definition: ProjectionDefinition): void {
688
- // Reject names that would blow up at registry-boot when we qualify them.
689
- // Catch it at the registration site so the stack trace points at the
690
- // feature file, not at framework internals.
691
- if (!isKebabSegment(definition.name)) {
692
- throw new Error(
693
- `[Feature ${name}] Projection name "${definition.name}" must be kebab-case ` +
694
- `(lowercase letters, digits, dashes; start with a letter). ` +
695
- `Got "${definition.name}" — try "${toKebab(definition.name).replace(/_/g, "-")}".`,
696
- );
697
- }
698
- if (projections[definition.name]) {
699
- throw new Error(
700
- `[Feature ${name}] Projection "${definition.name}" already registered. ` +
701
- `Projection names must be unique per feature.`,
702
- );
703
- }
704
- projections[definition.name] = definition;
705
- },
706
-
707
- multiStreamProjection(definition: MultiStreamProjectionDefinition): void {
708
- if (!isKebabSegment(definition.name)) {
709
- throw new Error(
710
- `[Feature ${name}] MultiStreamProjection name "${definition.name}" must be kebab-case ` +
711
- `(lowercase letters, digits, dashes; start with a letter). ` +
712
- `Got "${definition.name}" — try "${toKebab(definition.name).replace(/_/g, "-")}".`,
713
- );
714
- }
715
- if (multiStreamProjections[definition.name] || projections[definition.name]) {
716
- throw new Error(
717
- `[Feature ${name}] Projection name "${definition.name}" already registered. ` +
718
- `r.projection and r.multiStreamProjection share a namespace — pick a unique short name.`,
719
- );
720
- }
721
- if (Object.keys(definition.apply).length === 0) {
722
- throw new Error(
723
- `[Feature ${name}] MultiStreamProjection "${definition.name}" has no apply handlers. ` +
724
- `Declare at least one event type it reacts to, otherwise the dispatcher has nothing to route.`,
725
- );
726
- }
727
- multiStreamProjections[definition.name] = definition;
728
- },
729
-
730
- authClaims(fn: AuthClaimsFn): void {
731
- authClaimsHooks.push(fn);
732
- },
733
-
734
- screen(definition: ScreenDefinition): void {
735
- // Reject kebab-drift at registration-time so the stack trace points at
736
- // the feature file, not at registry-boot. Same guard pattern as
737
- // r.projection / r.multiStreamProjection.
738
- if (!isKebabSegment(definition.id)) {
739
- throw new Error(
740
- `[Feature ${name}] Screen id "${definition.id}" must be kebab-case ` +
741
- `(lowercase letters, digits, dashes; start with a letter). ` +
742
- `Got "${definition.id}" — try "${toKebab(definition.id).replace(/_/g, "-")}".`,
743
- );
744
- }
745
- if (screens[definition.id]) {
746
- throw new Error(
747
- `[Feature ${name}] Screen "${definition.id}" already registered. ` +
748
- `Screen ids must be unique per feature.`,
749
- );
750
- }
751
- screens[definition.id] = definition;
752
- },
753
-
754
- nav(definition: NavDefinition): void {
755
- // Reject kebab-drift at registration-time so the stack trace points at
756
- // the feature file, not at registry-boot. Same guard pattern as
757
- // r.projection / r.multiStreamProjection / r.screen.
758
- if (!isKebabSegment(definition.id)) {
759
- throw new Error(
760
- `[Feature ${name}] Nav id "${definition.id}" must be kebab-case ` +
761
- `(lowercase letters, digits, dashes; start with a letter). ` +
762
- `Got "${definition.id}" — try "${toKebab(definition.id).replace(/_/g, "-")}".`,
763
- );
764
- }
765
- if (navs[definition.id]) {
766
- throw new Error(
767
- `[Feature ${name}] Nav entry "${definition.id}" already registered. ` +
768
- `Nav ids must be unique per feature.`,
769
- );
770
- }
771
- navs[definition.id] = definition;
772
- },
773
-
774
- workspace(definition: WorkspaceDefinition): void {
775
- // Same kebab guard as r.screen / r.nav so authoring-time mistakes
776
- // surface at the feature file, not deep in registry boot.
777
- if (!isKebabSegment(definition.id)) {
778
- throw new Error(
779
- `[Feature ${name}] Workspace id "${definition.id}" must be kebab-case ` +
780
- `(lowercase letters, digits, dashes; start with a letter). ` +
781
- `Got "${definition.id}" — try "${toKebab(definition.id).replace(/_/g, "-")}".`,
782
- );
783
- }
784
- if (workspaces[definition.id]) {
785
- throw new Error(
786
- `[Feature ${name}] Workspace "${definition.id}" already registered. ` +
787
- `Workspace ids must be unique per feature.`,
788
- );
789
- }
790
- workspaces[definition.id] = definition;
791
- },
792
-
793
- httpRoute(definition: HttpRouteDefinition): void {
794
- // Path-Validation: muss mit "/" beginnen, keine /api/-Routes (die
795
- // sind dem Dispatcher reserviert; eine HTTP-Route die /api/foo
796
- // belegt, würde die Auth-Middleware umgehen ohne dass der Author
797
- // das ausgesprochen hat — bewusster Block).
798
- if (!definition.path.startsWith("/")) {
799
- throw new Error(
800
- `[Feature ${name}] httpRoute path "${definition.path}" must start with "/". ` +
801
- `Got "${definition.path}".`,
802
- );
803
- }
804
- if (definition.path === "/api" || definition.path.startsWith("/api/")) {
805
- throw new Error(
806
- `[Feature ${name}] httpRoute path "${definition.path}" is in the /api/* namespace ` +
807
- `which is reserved for the dispatcher (write/query/batch/auth/sse). ` +
808
- `Pick a different path or use r.queryHandler / r.writeHandler.`,
809
- );
810
- }
811
- const key = `${definition.method} ${definition.path}`;
812
- if (httpRoutes[key]) {
813
- throw new Error(
814
- `[Feature ${name}] HTTP-Route "${key}" already registered. ` +
815
- `method + path must be unique per feature.`,
816
- );
817
- }
818
- httpRoutes[key] = definition;
819
- },
820
-
821
- rawTable(rawTableName: string, table: unknown, options: RawTableOptions): void {
822
- // Same kebab guard as r.projection / r.screen / r.nav so authoring-time
823
- // mistakes surface at the feature file, not deep in registry boot.
824
- if (!isKebabSegment(rawTableName)) {
825
- throw new Error(
826
- `[Feature ${name}] Raw-table name "${rawTableName}" must be kebab-case ` +
827
- `(lowercase letters, digits, dashes; start with a letter). ` +
828
- `Got "${rawTableName}" — try "${toKebab(rawTableName).replace(/_/g, "-")}".`,
829
- );
830
- }
831
- if (rawTables[rawTableName]) {
832
- throw new Error(
833
- `[Feature ${name}] r.rawTable("${rawTableName}") already registered. ` +
834
- `Raw-table names must be unique per feature.`,
835
- );
836
- }
837
- // The `reason` is the marker that justifies the bypass — empty
838
- // strings would defeat the audit trail. Reject early so the
839
- // failure points at the feature file.
840
- if (typeof options.reason !== "string" || options.reason.trim().length === 0) {
841
- throw new Error(
842
- `[Feature ${name}] r.rawTable("${rawTableName}"): options.reason must be a ` +
843
- `non-empty string. The reason is the marker that justifies the bypass — ` +
844
- `if you can't write one, declare data via r.entity() instead.`,
845
- );
846
- }
847
- rawTables[rawTableName] = {
848
- name: rawTableName,
849
- table,
850
- reason: options.reason,
851
- };
852
- },
853
-
854
- unmanagedTable(meta: EntityTableMeta, options: RawTableOptions): void {
855
- // Name comes from the meta itself — apps already give the table a
856
- // name when calling defineUnmanagedTable, no need to repeat it.
857
- const tableName = meta.tableName;
858
- if (!isKebabSegment(tableName.replace(/_/g, "-"))) {
859
- // EntityTableMeta uses snake_case for tableName (matches Postgres
860
- // convention); we just guard against truly broken input.
861
- throw new Error(
862
- `[Feature ${name}] Unmanaged-table name "${tableName}" must be a ` +
863
- `valid identifier (lowercase letters, digits, underscores; start with a letter).`,
864
- );
865
- }
866
- if (unmanagedTables[tableName]) {
867
- throw new Error(
868
- `[Feature ${name}] r.unmanagedTable("${tableName}") already registered. ` +
869
- `Unmanaged-table names must be unique per feature.`,
870
- );
871
- }
872
- if (typeof options.reason !== "string" || options.reason.trim().length === 0) {
873
- throw new Error(
874
- `[Feature ${name}] r.unmanagedTable("${tableName}"): options.reason must be a ` +
875
- `non-empty string. The reason justifies the audit-trail bypass — ` +
876
- `if you can't write one, declare data via r.entity() instead.`,
877
- );
878
- }
879
- unmanagedTables[tableName] = {
880
- name: tableName,
881
- meta,
882
- reason: options.reason,
883
- };
884
- },
885
-
886
- claimKey<T extends ClaimKeyType>(
887
- shortName: string,
888
- options: { readonly type: T },
889
- ): ClaimKeyHandle<T> {
890
- if (claimKeys[shortName]) {
891
- throw new Error(
892
- `[Feature ${name}] Claim key "${shortName}" already declared. ` +
893
- "Claim short-names must be unique per feature.",
894
- );
895
- }
896
- // Claim keys are NOT full QNs — the JWT shape is 2-segment
897
- // "<featureName>:<shortName>" (same as Translation keys), not
898
- // kebab-cased. The authClaims resolver prefixes with the raw
899
- // feature.name + the raw inner key the hook returns, so the handle's
900
- // `name` must match that literal string exactly for `readClaim` to
901
- // find the value. kebab-conversion here would break the round-trip.
902
- const qualifiedName = `${name}:${shortName}`;
903
- claimKeys[shortName] = {
904
- shortName,
905
- qualifiedName,
906
- type: options.type,
907
- };
908
- return { name: qualifiedName, type: options.type };
909
- },
910
-
911
- treeActions<const TActions extends Record<string, TreeActionDef>>(
912
- actions: TActions,
913
- ): TreeActionsHandle<TName, TActions> {
914
- // Only-once-guard: zweiter Aufruf ist Author-Bug, soll am
915
- // Feature-File aufschlagen (gleicher Stil wie r.toggleable).
916
- if (treeActions !== undefined) {
917
- throw new Error(
918
- `[Feature ${name}] r.treeActions() already called. ` +
919
- `Each feature may declare a single tree-actions schema.`,
920
- );
921
- }
922
- treeActions = actions;
923
- // Return typed handle für setup-export. Frozen damit Caller die
924
- // Map nicht nachträglich mutieren (würde Pattern-AST + Runtime-
925
- // Lookup divergieren lassen).
926
- return Object.freeze({
927
- id: name,
928
- treeActions: actions,
929
- });
930
- },
931
-
932
- tree(provider: TreeChildrenSubscribe): void {
933
- // Only-once-guard analog zu r.treeActions.
934
- if (treeProvider !== undefined) {
935
- throw new Error(
936
- `[Feature ${name}] r.tree() already called. ` +
937
- `Each feature may declare a single tree-provider.`,
938
- );
939
- }
940
- treeProvider = provider;
84
+ state.uiHints = hints;
941
85
  },
86
+ ...buildEntityHandlerMethods(state, name),
87
+ ...buildConfigEventsJobsMethods(state, name),
88
+ ...buildUiExtensionsMethods(state, name),
942
89
  };
943
90
 
944
91
  const exports = setup(registrar) as TExports; // @cast-boundary engine-bridge
945
92
 
946
93
  return {
947
94
  name,
948
- ...(description !== undefined && { description }),
949
- systemScope: isSystemScoped,
95
+ ...(state.description !== undefined && { description: state.description }),
96
+ systemScope: state.isSystemScoped,
950
97
  exports,
951
- requires,
952
- optionalRequires,
953
- requiredProjections,
954
- requiredSteps,
955
- ...(toggleableDefault !== undefined && { toggleableDefault }),
956
- ...(uiHints !== undefined && { uiHints }),
957
- entities,
958
- entityTables,
959
- relations,
960
- writeHandlers,
961
- queryHandlers,
962
- translations,
98
+ requires: state.requires,
99
+ optionalRequires: state.optionalRequires,
100
+ requiredProjections: state.requiredProjections,
101
+ requiredSteps: state.requiredSteps,
102
+ ...(state.toggleableDefault !== undefined && { toggleableDefault: state.toggleableDefault }),
103
+ ...(state.uiHints !== undefined && { uiHints: state.uiHints }),
104
+ entities: state.entities,
105
+ entityTables: state.entityTables,
106
+ relations: state.relations,
107
+ writeHandlers: state.writeHandlers,
108
+ queryHandlers: state.queryHandlers,
109
+ streamHandlers: state.streamHandlers,
110
+ translations: state.translations,
963
111
  hooks: {
964
- validation: validationHooks,
965
- preSave: lifecycleHooks["preSave"] ?? {},
966
- postSave: phasedLifecycleHooks.postSave,
967
- preDelete: phasedLifecycleHooks.preDelete,
968
- postDelete: phasedLifecycleHooks.postDelete,
969
- preQuery: lifecycleHooks["preQuery"] ?? {},
970
- postQuery: lifecycleHooks["postQuery"] ?? {},
112
+ validation: state.validationHooks,
113
+ preSave: state.lifecycleHooks["preSave"] ?? {},
114
+ postSave: state.phasedLifecycleHooks.postSave,
115
+ preDelete: state.phasedLifecycleHooks.preDelete,
116
+ postDelete: state.phasedLifecycleHooks.postDelete,
117
+ preQuery: state.lifecycleHooks["preQuery"] ?? {},
118
+ postQuery: state.lifecycleHooks["postQuery"] ?? {},
971
119
  // @cast-boundary engine-bridge — die Hook-Registrierung erased die
972
120
  // per-Slot-Signaturen zu LifecycleHookFn (Union, s. Cast in
973
121
  // addLifecycleHook); die Branches dort sind die einzigen Producer und
974
122
  // schreiben pro Slot typrichtig.
975
123
  } as HookMap,
976
124
  entityHooks: {
977
- postSave: entityPostSave,
978
- preDelete: entityPreDelete,
979
- postDelete: entityPostDelete,
980
- postQuery: entityPostQuery,
981
- },
982
- searchPayloadExtensions,
983
- configKeys,
984
- configSeeds,
985
- jobs,
986
- notifications,
987
- registrarExtensions,
988
- extensionUsages,
989
- extensionSelectors,
990
- exposedApis,
991
- usedApis,
992
- referenceData,
993
- events,
994
- eventMigrations,
995
- configReads,
996
- handlerEntityMappings,
997
- metrics,
998
- secretKeys,
999
- projections,
1000
- multiStreamProjections,
1001
- authClaimsHooks,
1002
- claimKeys,
1003
- screens,
1004
- navs,
1005
- workspaces,
1006
- httpRoutes,
1007
- rawTables,
1008
- unmanagedTables,
1009
- ...(treeActions !== undefined && { treeActions }),
1010
- ...(treeProvider !== undefined && { treeProvider }),
1011
- ...(envSchema !== undefined && { envSchema }),
125
+ postSave: state.entityPostSave,
126
+ preDelete: state.entityPreDelete,
127
+ postDelete: state.entityPostDelete,
128
+ postQuery: state.entityPostQuery,
129
+ },
130
+ searchPayloadExtensions: state.searchPayloadExtensions,
131
+ configKeys: state.configKeys,
132
+ configSeeds: state.configSeeds,
133
+ jobs: state.jobs,
134
+ notifications: state.notifications,
135
+ registrarExtensions: state.registrarExtensions,
136
+ extensionUsages: state.extensionUsages,
137
+ extensionSelectors: state.extensionSelectors,
138
+ exposedApis: state.exposedApis,
139
+ usedApis: state.usedApis,
140
+ bootChecks: state.bootChecks,
141
+ referenceData: state.referenceData,
142
+ events: state.events,
143
+ eventMigrations: state.eventMigrations,
144
+ configReads: state.configReads,
145
+ handlerEntityMappings: state.handlerEntityMappings,
146
+ metrics: state.metrics,
147
+ secretKeys: state.secretKeys,
148
+ projections: state.projections,
149
+ entityProjectionExtensions: state.entityProjectionExtensions,
150
+ multiStreamProjections: state.multiStreamProjections,
151
+ authClaimsHooks: state.authClaimsHooks,
152
+ claimKeys: state.claimKeys,
153
+ screens: state.screens,
154
+ navs: state.navs,
155
+ workspaces: state.workspaces,
156
+ httpRoutes: state.httpRoutes,
157
+ storeTables: state.storeTables,
158
+ ...(state.treeActions !== undefined && { treeActions: state.treeActions }),
159
+ ...(state.envSchema !== undefined && { envSchema: state.envSchema }),
1012
160
  };
1013
161
  }