@cosmicdrift/kumiko-framework 0.14.0 → 0.16.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 (342) hide show
  1. package/package.json +6 -6
  2. package/src/__tests__/{anonymous-access.integration.ts → anonymous-access.integration.test.ts} +12 -9
  3. package/src/__tests__/{error-contract.integration.ts → error-contract.integration.test.ts} +5 -4
  4. package/src/__tests__/{field-access.integration.ts → field-access.integration.test.ts} +3 -3
  5. package/src/__tests__/{full-stack.integration.ts → full-stack.integration.test.ts} +7 -16
  6. package/src/__tests__/{ownership.integration.ts → ownership.integration.test.ts} +3 -2
  7. package/src/__tests__/{raw-table.integration.ts → raw-table.integration.test.ts} +18 -30
  8. package/src/__tests__/{reference-data.integration.ts → reference-data.integration.test.ts} +24 -11
  9. package/src/__tests__/{transition-guard.integration.ts → transition-guard.integration.test.ts} +12 -10
  10. package/src/api/__tests__/api.test.ts +1 -1
  11. package/src/api/__tests__/auth-middleware-transport.test.ts +1 -1
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +1 -1
  13. package/src/api/__tests__/{batch.integration.ts → batch.integration.test.ts} +30 -30
  14. package/src/api/__tests__/body-limit.test.ts +1 -1
  15. package/src/api/__tests__/csrf-middleware.test.ts +1 -1
  16. package/src/api/__tests__/{dispatcher-live.integration.ts → dispatcher-live.integration.test.ts} +10 -9
  17. package/src/api/__tests__/metrics-endpoint.test.ts +1 -1
  18. package/src/api/__tests__/{nested-write.integration.ts → nested-write.integration.test.ts} +13 -16
  19. package/src/api/__tests__/readiness.test.ts +1 -1
  20. package/src/api/__tests__/request-id-middleware.test.ts +1 -1
  21. package/src/api/__tests__/sse-broker.test.ts +12 -12
  22. package/src/api/__tests__/sse-route.test.ts +1 -1
  23. package/src/api/auth-routes.ts +2 -5
  24. package/src/api/readiness.ts +2 -2
  25. package/src/auth/__tests__/roles.test.ts +2 -2
  26. package/src/bun-db/__tests__/PATTERN.md +73 -0
  27. package/src/bun-db/__tests__/_helpers.ts +103 -0
  28. package/src/bun-db/__tests__/batch-methods.integration.test.ts +143 -0
  29. package/src/bun-db/__tests__/batch-methods.test.ts +20 -0
  30. package/src/bun-db/__tests__/bun-test-db.ts +19 -0
  31. package/src/bun-db/__tests__/bun-test-stack.ts +6 -0
  32. package/src/bun-db/__tests__/column-types.integration.test.ts +132 -0
  33. package/src/bun-db/__tests__/compound-types.integration.test.ts +134 -0
  34. package/src/bun-db/__tests__/jsonb-edge-cases.integration.test.ts +235 -0
  35. package/src/bun-db/__tests__/smoke.integration.test.ts +43 -0
  36. package/src/bun-db/__tests__/sql-methods.integration.test.ts +231 -0
  37. package/src/bun-db/__tests__/where-patterns.integration.test.ts +185 -0
  38. package/src/bun-db/connection.ts +84 -0
  39. package/src/bun-db/index.ts +31 -0
  40. package/src/bun-db/query.ts +842 -0
  41. package/src/compliance/__tests__/duration-spec.test.ts +1 -1
  42. package/src/compliance/__tests__/profiles.test.ts +1 -1
  43. package/src/compliance/__tests__/sub-processors.test.ts +1 -1
  44. package/src/compliance/profiles.ts +1 -4
  45. package/src/db/__tests__/{apply-entity-event-tenant.integration.ts → apply-entity-event-tenant.integration.test.ts} +13 -11
  46. package/src/db/__tests__/big-int-field.test.ts +15 -14
  47. package/src/db/__tests__/column-ddl.integration.test.ts +113 -0
  48. package/src/db/__tests__/compound-types.test.ts +1 -1
  49. package/src/db/__tests__/{config-seed.integration.ts → config-seed.integration.test.ts} +32 -27
  50. package/src/db/__tests__/connection-options.test.ts +1 -1
  51. package/src/db/__tests__/cursor.test.ts +8 -32
  52. package/src/db/__tests__/dialect-instant.test.ts +1 -1
  53. package/src/db/__tests__/encryption.test.ts +1 -1
  54. package/src/db/__tests__/{drizzle-table-types.test.ts → entity-table-types.test.ts} +16 -16
  55. package/src/db/__tests__/{event-store-executor-list.integration.ts → event-store-executor-list.integration.test.ts} +12 -7
  56. package/src/db/__tests__/{event-store-executor.integration.ts → event-store-executor.integration.test.ts} +19 -12
  57. package/src/db/__tests__/{implicit-projection-equivalence.integration.ts → implicit-projection-equivalence.integration.test.ts} +35 -29
  58. package/src/db/__tests__/located-timestamp.test.ts +1 -1
  59. package/src/db/__tests__/migrate-generator.test.ts +71 -0
  60. package/src/db/__tests__/migrate-runner.test.ts +19 -0
  61. package/src/db/__tests__/money.test.ts +1 -1
  62. package/src/db/__tests__/{multi-row-insert.integration.ts → multi-row-insert.integration.test.ts} +18 -11
  63. package/src/db/__tests__/parse-auto-verb.test.ts +1 -1
  64. package/src/db/__tests__/pg-error.test.ts +43 -0
  65. package/src/db/__tests__/{required-not-null-migration-safety.integration.ts → required-not-null-migration-safety.integration.test.ts} +28 -24
  66. package/src/db/__tests__/{schema-migration.integration.ts → schema-migration.integration.test.ts} +32 -28
  67. package/src/db/__tests__/sql-inventory.test.ts +56 -0
  68. package/src/db/__tests__/table-builder-indexes.test.ts +30 -11
  69. package/src/db/__tests__/table-builder-required.test.ts +20 -22
  70. package/src/db/__tests__/{tenant-db.integration.ts → tenant-db.integration.test.ts} +106 -144
  71. package/src/db/__tests__/{unique-violation-mapping.integration.ts → unique-violation-mapping.integration.test.ts} +13 -8
  72. package/src/db/api.ts +46 -0
  73. package/src/db/apply-entity-event.ts +45 -36
  74. package/src/db/assert-exists-in.ts +5 -16
  75. package/src/db/bun-provider.ts +37 -0
  76. package/src/db/config-seed.ts +4 -4
  77. package/src/db/connection.ts +14 -57
  78. package/src/db/cursor.ts +5 -56
  79. package/src/db/dialect.ts +472 -99
  80. package/src/db/eagerload.ts +5 -12
  81. package/src/db/entity-table-meta.ts +390 -0
  82. package/src/db/event-store-executor.ts +158 -100
  83. package/src/db/index.ts +33 -5
  84. package/src/db/migrate-generator.ts +350 -0
  85. package/src/db/migrate-runner.ts +206 -0
  86. package/src/db/postgres-provider.ts +25 -0
  87. package/src/db/queries/entity-read.ts +15 -0
  88. package/src/db/queries/es-ops.ts +17 -0
  89. package/src/db/queries/event-consumer.ts +170 -0
  90. package/src/db/queries/event-store-admin.ts +127 -0
  91. package/src/db/queries/event-store.ts +155 -0
  92. package/src/db/queries/projection-rebuild.ts +59 -0
  93. package/src/db/queries/raw-sql.ts +15 -0
  94. package/src/db/queries/schema-drift.ts +35 -0
  95. package/src/db/queries/seed-context.ts +58 -0
  96. package/src/db/queries/table-ops.ts +11 -0
  97. package/src/db/queries/test-stack.ts +56 -0
  98. package/src/db/query-api.ts +22 -0
  99. package/src/db/query.ts +30 -0
  100. package/src/db/reference-data.ts +19 -22
  101. package/src/db/render-ddl.ts +57 -0
  102. package/src/db/row-helpers.ts +3 -52
  103. package/src/db/schema-inspection.ts +17 -4
  104. package/src/db/sql-inventory.ts +208 -0
  105. package/src/db/table-builder.ts +54 -46
  106. package/src/db/tenant-db.ts +105 -326
  107. package/src/engine/__tests__/auth-claims-registrar.test.ts +1 -1
  108. package/src/engine/__tests__/boot-validator-api-exposure.test.ts +3 -3
  109. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +1 -1
  110. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +5 -5
  111. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +3 -3
  112. package/src/engine/__tests__/boot-validator.test.ts +4 -3
  113. package/src/engine/__tests__/build-app-schema.test.ts +1 -1
  114. package/src/engine/__tests__/build-target.test.ts +1 -1
  115. package/src/engine/__tests__/claim-keys.test.ts +1 -1
  116. package/src/engine/__tests__/codemod-pipeline.test.ts +3 -3
  117. package/src/engine/__tests__/config-helpers.test.ts +1 -1
  118. package/src/engine/__tests__/duration-utils.test.ts +16 -0
  119. package/src/engine/__tests__/effective-features.test.ts +1 -1
  120. package/src/engine/__tests__/engine.test.ts +1 -1
  121. package/src/engine/__tests__/entity-handlers.test.ts +3 -3
  122. package/src/engine/__tests__/event-helpers.test.ts +3 -3
  123. package/src/engine/__tests__/extends-registrar.test.ts +4 -4
  124. package/src/engine/__tests__/factories-long-text.test.ts +1 -1
  125. package/src/engine/__tests__/factories-time.test.ts +1 -1
  126. package/src/engine/__tests__/field-access.test.ts +38 -0
  127. package/src/engine/__tests__/field-predicates.test.ts +1 -1
  128. package/src/engine/__tests__/hook-phases.test.ts +1 -1
  129. package/src/engine/__tests__/identifiers.test.ts +1 -1
  130. package/src/engine/__tests__/lifecycle-hooks.test.ts +1 -1
  131. package/src/engine/__tests__/nav.test.ts +1 -1
  132. package/src/engine/__tests__/no-return-guard.test.ts +17 -0
  133. package/src/engine/__tests__/ownership.test.ts +10 -11
  134. package/src/engine/__tests__/parse-ref-target.test.ts +1 -1
  135. package/src/engine/__tests__/pipeline-engine.test.ts +1 -1
  136. package/src/engine/__tests__/{pipeline-handler.integration.ts → pipeline-handler.integration.test.ts} +38 -52
  137. package/src/engine/__tests__/{pipeline-observability.integration.ts → pipeline-observability.integration.test.ts} +1 -1
  138. package/src/engine/__tests__/{pipeline-performance.integration.ts → pipeline-performance.integration.test.ts} +1 -1
  139. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +1 -1
  140. package/src/engine/__tests__/post-query-hook.test.ts +1 -1
  141. package/src/engine/__tests__/projection-helpers.test.ts +25 -17
  142. package/src/engine/__tests__/projection.test.ts +4 -4
  143. package/src/engine/__tests__/qualified-name.test.ts +1 -1
  144. package/src/engine/__tests__/raw-table.test.ts +9 -8
  145. package/src/engine/__tests__/resolve-config-or-param.test.ts +5 -5
  146. package/src/engine/__tests__/run-in.test.ts +1 -1
  147. package/src/engine/__tests__/schema-builder.test.ts +1 -1
  148. package/src/engine/__tests__/screen.test.ts +1 -1
  149. package/src/engine/__tests__/search-payload-extension.test.ts +3 -3
  150. package/src/engine/__tests__/state-machine.test.ts +1 -1
  151. package/src/engine/__tests__/steps-aggregate-append-event.test.ts +7 -7
  152. package/src/engine/__tests__/steps-aggregate-create.test.ts +4 -4
  153. package/src/engine/__tests__/steps-aggregate-update.test.ts +3 -3
  154. package/src/engine/__tests__/steps-call-feature.test.ts +5 -5
  155. package/src/engine/__tests__/steps-mail-send.test.ts +7 -7
  156. package/src/engine/__tests__/steps-read.test.ts +34 -40
  157. package/src/engine/__tests__/steps-resolver-utils.test.ts +6 -6
  158. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +24 -19
  159. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +28 -17
  160. package/src/engine/__tests__/steps-webhook-send.test.ts +6 -6
  161. package/src/engine/__tests__/steps-workflow.test.ts +7 -7
  162. package/src/engine/__tests__/system-user.test.ts +1 -1
  163. package/src/engine/__tests__/unmanaged-table.test.ts +98 -0
  164. package/src/engine/__tests__/validate-projection-allowlist.test.ts +4 -5
  165. package/src/engine/__tests__/validation-hooks.test.ts +1 -1
  166. package/src/engine/__tests__/visual-tree-patterns.test.ts +1 -1
  167. package/src/engine/boot-validator/entity-handler.ts +3 -3
  168. package/src/engine/boot-validator/ownership.ts +1 -1
  169. package/src/engine/define-feature.ts +37 -2
  170. package/src/engine/entity-handlers.ts +5 -5
  171. package/src/engine/factories.ts +1 -1
  172. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +1 -1
  173. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -1
  174. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +2 -2
  175. package/src/engine/feature-ast/__tests__/parse.test.ts +1 -1
  176. package/src/engine/feature-ast/__tests__/patch.test.ts +1 -1
  177. package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -1
  178. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +1 -1
  179. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +1 -1
  180. package/src/engine/feature-ast/extractors/shared.ts +2 -3
  181. package/src/engine/ownership.ts +113 -41
  182. package/src/engine/pattern-library/__tests__/library.test.ts +2 -2
  183. package/src/engine/projection-helpers.ts +2 -11
  184. package/src/engine/registry.ts +21 -2
  185. package/src/engine/steps/read-find-many.ts +13 -13
  186. package/src/engine/steps/read-find-one.ts +7 -9
  187. package/src/engine/steps/unsafe-projection-delete.ts +4 -5
  188. package/src/engine/steps/unsafe-projection-upsert.ts +63 -31
  189. package/src/engine/types/feature.ts +47 -2
  190. package/src/engine/types/fields.ts +4 -5
  191. package/src/engine/types/index.ts +2 -0
  192. package/src/engine/types/step.ts +10 -10
  193. package/src/engine/validate-projection-allowlist.ts +23 -3
  194. package/src/entrypoint/__tests__/{entrypoint-job-wiring.integration.ts → entrypoint-job-wiring.integration.test.ts} +4 -3
  195. package/src/entrypoint/__tests__/{split-deploy.integration.ts → split-deploy.integration.test.ts} +4 -3
  196. package/src/env/__tests__/compose-env-schema.test.ts +1 -1
  197. package/src/env/__tests__/dry-run.test.ts +1 -1
  198. package/src/errors/__tests__/classes.test.ts +1 -1
  199. package/src/errors/__tests__/error-helpers.test.ts +44 -0
  200. package/src/errors/__tests__/field-issue-compat.test.ts +16 -0
  201. package/src/errors/__tests__/write-failures.test.ts +1 -1
  202. package/src/errors/classes.ts +5 -19
  203. package/src/errors/field-issue.ts +11 -0
  204. package/src/errors/index.ts +1 -0
  205. package/src/errors/zod-bridge.ts +3 -2
  206. package/src/es-ops/__tests__/{context.integration.ts → context.integration.test.ts} +43 -29
  207. package/src/es-ops/__tests__/{runner.integration.ts → runner.integration.test.ts} +25 -23
  208. package/src/es-ops/__tests__/runner.test.ts +29 -19
  209. package/src/es-ops/context.ts +11 -56
  210. package/src/es-ops/operations-schema.ts +2 -2
  211. package/src/es-ops/runner.ts +12 -26
  212. package/src/event-store/__tests__/{admin-api.integration.ts → admin-api.integration.test.ts} +71 -45
  213. package/src/event-store/__tests__/{event-store.integration.ts → event-store.integration.test.ts} +7 -5
  214. package/src/event-store/__tests__/{get-stream-version-perf.integration.ts → get-stream-version-perf.integration.test.ts} +5 -3
  215. package/src/event-store/__tests__/{perf.integration.ts → perf.integration.test.ts} +24 -16
  216. package/src/event-store/__tests__/{snapshot.integration.ts → snapshot.integration.test.ts} +34 -28
  217. package/src/event-store/__tests__/{upcaster-dead-letter.integration.ts → upcaster-dead-letter.integration.test.ts} +11 -12
  218. package/src/event-store/__tests__/{upcaster.integration.ts → upcaster.integration.test.ts} +19 -32
  219. package/src/event-store/admin-api.ts +55 -83
  220. package/src/event-store/archive.ts +15 -39
  221. package/src/event-store/event-store.ts +92 -86
  222. package/src/event-store/events-schema.ts +2 -1
  223. package/src/event-store/index.ts +1 -0
  224. package/src/event-store/snapshot.ts +26 -24
  225. package/src/event-store/upcaster-dead-letter.ts +19 -18
  226. package/src/files/__tests__/content-disposition.test.ts +1 -1
  227. package/src/files/__tests__/{file-field-pipeline.integration.ts → file-field-pipeline.integration.test.ts} +8 -5
  228. package/src/files/__tests__/file-handle.test.ts +1 -1
  229. package/src/files/__tests__/{files.integration.ts → files.integration.test.ts} +32 -17
  230. package/src/files/__tests__/read-stream.test.ts +1 -1
  231. package/src/files/__tests__/{storage-tracking.integration.ts → storage-tracking.integration.test.ts} +26 -30
  232. package/src/files/__tests__/write-stream.test.ts +1 -1
  233. package/src/files/__tests__/zip-stream.test.ts +1 -1
  234. package/src/files/file-ref-table.ts +2 -2
  235. package/src/files/file-routes.ts +7 -9
  236. package/src/files/storage-tracking.ts +9 -17
  237. package/src/i18n/__tests__/i18n.test.ts +1 -1
  238. package/src/jobs/__tests__/{job-event-trigger.integration.ts → job-event-trigger.integration.test.ts} +6 -3
  239. package/src/jobs/__tests__/{job-multi-trigger.integration.ts → job-multi-trigger.integration.test.ts} +6 -3
  240. package/src/jobs/__tests__/{jobs.integration.ts → jobs.integration.test.ts} +5 -7
  241. package/src/lifecycle/__tests__/{lifecycle-server.integration.ts → lifecycle-server.integration.test.ts} +1 -1
  242. package/src/lifecycle/__tests__/lifecycle.test.ts +6 -6
  243. package/src/lifecycle/__tests__/signal-handlers.test.ts +6 -6
  244. package/src/logging/__tests__/pino-trace-bridge.test.ts +1 -1
  245. package/src/migrations/__tests__/compare-snapshots.test.ts +1 -1
  246. package/src/migrations/__tests__/{detect-drift.integration.ts → detect-drift.integration.test.ts} +34 -26
  247. package/src/migrations/__tests__/{detect-projections-to-rebuild.integration.ts → detect-projections-to-rebuild.integration.test.ts} +1 -1
  248. package/src/migrations/__tests__/rebuild-marker.test.ts +1 -1
  249. package/src/migrations/projection-detection.ts +12 -1
  250. package/src/migrations/schema-drift.ts +7 -23
  251. package/src/observability/__tests__/console-provider.test.ts +1 -1
  252. package/src/observability/__tests__/metric-validator.test.ts +1 -1
  253. package/src/observability/__tests__/noop-provider.test.ts +1 -1
  254. package/src/observability/__tests__/{observability.integration.ts → observability.integration.test.ts} +5 -8
  255. package/src/observability/__tests__/prometheus-meter.test.ts +1 -1
  256. package/src/observability/__tests__/recording-meter.test.ts +1 -1
  257. package/src/observability/__tests__/recording-tracer.test.ts +1 -1
  258. package/src/observability/__tests__/sensitive-filter.test.ts +1 -1
  259. package/src/pipeline/__tests__/{archive-stream.integration.ts → archive-stream.integration.test.ts} +3 -3
  260. package/src/pipeline/__tests__/auth-claims-resolver.test.ts +9 -9
  261. package/src/pipeline/__tests__/{cascade-handler.integration.ts → cascade-handler.integration.test.ts} +18 -15
  262. package/src/pipeline/__tests__/cascade-handler.test.ts +1 -1
  263. package/src/pipeline/__tests__/{causation-chain.integration.ts → causation-chain.integration.test.ts} +12 -13
  264. package/src/pipeline/__tests__/{ctx-bridge.integration.ts → ctx-bridge.integration.test.ts} +12 -11
  265. package/src/pipeline/__tests__/dispatcher-utils.test.ts +107 -0
  266. package/src/pipeline/__tests__/dispatcher.test.ts +2 -2
  267. package/src/pipeline/__tests__/{distributed-lock.integration.ts → distributed-lock.integration.test.ts} +1 -1
  268. package/src/pipeline/__tests__/{domain-events-projections.integration.ts → domain-events-projections.integration.test.ts} +13 -15
  269. package/src/pipeline/__tests__/{event-dedup.integration.ts → event-dedup.integration.test.ts} +1 -1
  270. package/src/pipeline/__tests__/{event-define-event-strict.integration.ts → event-define-event-strict.integration.test.ts} +6 -16
  271. package/src/pipeline/__tests__/{event-dispatcher-lifecycle.integration.ts → event-dispatcher-lifecycle.integration.test.ts} +1 -1
  272. package/src/pipeline/__tests__/{event-dispatcher-multi-instance.integration.ts → event-dispatcher-multi-instance.integration.test.ts} +3 -2
  273. package/src/pipeline/__tests__/{event-dispatcher-pg-listen.integration.ts → event-dispatcher-pg-listen.integration.test.ts} +1 -1
  274. package/src/pipeline/__tests__/{event-dispatcher-recovery.integration.ts → event-dispatcher-recovery.integration.test.ts} +2 -2
  275. package/src/pipeline/__tests__/{event-dispatcher-second-audit.integration.ts → event-dispatcher-second-audit.integration.test.ts} +17 -16
  276. package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +14 -12
  277. package/src/pipeline/__tests__/{event-dispatcher.integration.ts → event-dispatcher.integration.test.ts} +8 -15
  278. package/src/pipeline/__tests__/{event-retention.integration.ts → event-retention.integration.test.ts} +28 -25
  279. package/src/pipeline/__tests__/{fetch-for-writing.integration.ts → fetch-for-writing.integration.test.ts} +6 -6
  280. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +4 -4
  281. package/src/pipeline/__tests__/{load-aggregate-query.integration.ts → load-aggregate-query.integration.test.ts} +9 -5
  282. package/src/pipeline/__tests__/{msp-error-mode.integration.ts → msp-error-mode.integration.test.ts} +1 -1
  283. package/src/pipeline/__tests__/{msp-multi-hop.integration.ts → msp-multi-hop.integration.test.ts} +9 -8
  284. package/src/pipeline/__tests__/{msp-rebuild.integration.ts → msp-rebuild.integration.test.ts} +47 -55
  285. package/src/pipeline/__tests__/{multi-stream-projection.integration.ts → multi-stream-projection.integration.test.ts} +19 -53
  286. package/src/pipeline/__tests__/{perf-rebuild.integration.ts → perf-rebuild.integration.test.ts} +36 -34
  287. package/src/pipeline/__tests__/{post-query-hook.integration.ts → post-query-hook.integration.test.ts} +1 -1
  288. package/src/pipeline/__tests__/{projection-rebuild.integration.ts → projection-rebuild.integration.test.ts} +21 -30
  289. package/src/pipeline/__tests__/{query-projection.integration.ts → query-projection.integration.test.ts} +6 -5
  290. package/src/pipeline/__tests__/redis-keys.test.ts +12 -0
  291. package/src/pipeline/__tests__/{redis-pipeline.integration.ts → redis-pipeline.integration.test.ts} +3 -1
  292. package/src/pipeline/cascade-handler.ts +13 -21
  293. package/src/pipeline/dispatcher-utils.ts +8 -7
  294. package/src/pipeline/dispatcher.ts +43 -48
  295. package/src/pipeline/event-consumer-state.ts +11 -2
  296. package/src/pipeline/event-dispatcher.ts +86 -146
  297. package/src/pipeline/event-retention.ts +14 -24
  298. package/src/pipeline/msp-rebuild.ts +54 -78
  299. package/src/pipeline/projection-rebuild.ts +65 -67
  300. package/src/pipeline/projection-state.ts +2 -2
  301. package/src/random/__tests__/generate.test.ts +13 -13
  302. package/src/rate-limit/__tests__/{dispatcher-l3.integration.ts → dispatcher-l3.integration.test.ts} +1 -1
  303. package/src/rate-limit/__tests__/{middleware.integration.ts → middleware.integration.test.ts} +1 -1
  304. package/src/rate-limit/__tests__/{resolver.integration.ts → resolver.integration.test.ts} +1 -1
  305. package/src/redis/__tests__/redis-options.test.ts +1 -1
  306. package/src/search/__tests__/{meilisearch-adapter.integration.ts → meilisearch-adapter.integration.test.ts} +1 -1
  307. package/src/search/__tests__/search-adapter.test.ts +1 -1
  308. package/src/secrets/__tests__/dek-cache.test.ts +1 -3
  309. package/src/secrets/__tests__/env-master-key-provider.test.ts +1 -1
  310. package/src/secrets/__tests__/envelope.test.ts +1 -1
  311. package/src/secrets/__tests__/leak-guard.test.ts +1 -1
  312. package/src/secrets/__tests__/rotation.test.ts +1 -1
  313. package/src/stack/db.ts +25 -48
  314. package/src/stack/push-entity-projection-tables.ts +2 -4
  315. package/src/stack/table-helpers.ts +98 -61
  316. package/src/stack/test-stack.ts +10 -9
  317. package/src/testing/__tests__/db-cleanup.test.ts +40 -0
  318. package/src/testing/__tests__/e2e-generator.test.ts +1 -1
  319. package/src/testing/__tests__/{ensure-entity-table.integration.ts → ensure-entity-table.integration.test.ts} +7 -14
  320. package/src/testing/db-cleanup.ts +44 -0
  321. package/src/testing/expect-error.ts +1 -1
  322. package/src/testing/index.ts +2 -0
  323. package/src/testing/multipart-helper.ts +94 -0
  324. package/src/testing/shared-entities.ts +5 -5
  325. package/src/time/__tests__/polyfill.test.ts +1 -1
  326. package/src/time/__tests__/tz-context.test.ts +1 -1
  327. package/src/utils/__tests__/assert.test.ts +1 -1
  328. package/src/utils/__tests__/case.test.ts +16 -0
  329. package/src/utils/__tests__/env-parse.test.ts +1 -1
  330. package/src/utils/__tests__/is-plain-object.test.ts +16 -0
  331. package/src/utils/__tests__/parse-string-array-json.test.ts +16 -0
  332. package/src/utils/__tests__/safe-json.test.ts +22 -0
  333. package/src/utils/case.ts +6 -0
  334. package/src/utils/index.ts +3 -0
  335. package/src/utils/is-plain-object.ts +4 -0
  336. package/src/utils/parse-string-array-json.ts +14 -0
  337. package/CHANGELOG.md +0 -474
  338. package/src/db/__tests__/db-helpers.test.ts +0 -369
  339. package/src/db/__tests__/drizzle-helpers.integration.ts +0 -186
  340. package/src/db/__tests__/row-helpers.test.ts +0 -59
  341. package/src/engine/steps/_drizzle-boundary.ts +0 -19
  342. package/src/files/__tests__/file-field-column.integration.ts +0 -103
@@ -11,56 +11,88 @@
11
11
  // rejected by boot-validation — domain mutation MUST go through
12
12
  // r.step.aggregate.*.
13
13
 
14
- import { getTableColumns, type Table } from "drizzle-orm";
15
- import type { PgColumn } from "drizzle-orm/pg-core";
14
+ import { executeRawQuery } from "../../db/queries/raw-sql";
16
15
  import { defineStep } from "../define-step";
17
16
  import type { PipelineCtx, StepInstance, StepResolver } from "../types/step";
18
- import { asQueryTarget } from "./_drizzle-boundary";
19
17
  import { resolveRequired } from "./_resolver-utils";
20
18
 
21
19
  type UnsafeProjectionUpsertArgs = {
22
- readonly table: Table;
20
+ readonly table: unknown;
23
21
  readonly on: readonly string[];
24
22
  readonly row: StepResolver<Record<string, unknown>>;
25
23
  };
26
24
 
25
+ // @cast-boundary drizzle-bridge — reads table name + column snake_case
26
+ // names from drizzle Symbol-based metadata without importing drizzle-orm.
27
+ const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
28
+ const KUMIKO_COLUMNS_SYMBOL = Symbol.for("kumiko:schema:Columns");
29
+
30
+ function resolveTableName(table: unknown): string {
31
+ if (typeof table !== "object" || table === null) {
32
+ throw new Error("unsafeProjectionUpsert: table is not an object");
33
+ }
34
+ const name = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
35
+ if (typeof name !== "string") {
36
+ throw new Error("unsafeProjectionUpsert: table has no kumiko:schema:Name symbol");
37
+ }
38
+ return name;
39
+ }
40
+
41
+ function resolveColumnName(table: unknown, field: string): string {
42
+ if (typeof table !== "object" || table === null) return field;
43
+ const cols = (table as Record<symbol, unknown>)[KUMIKO_COLUMNS_SYMBOL];
44
+ if (typeof cols !== "object" || cols === null) return field;
45
+ const col = (cols as Record<string, unknown>)[field];
46
+ if (typeof col === "object" && col !== null) {
47
+ const nameVal = (col as Record<string, unknown>)["name"];
48
+ if (typeof nameVal === "string") return nameVal;
49
+ }
50
+ return field;
51
+ }
52
+
53
+ function quoteIdent(name: string): string {
54
+ return `"${name.replace(/"/g, '""')}"`;
55
+ }
56
+
27
57
  defineStep<UnsafeProjectionUpsertArgs, void>({
28
58
  kind: "unsafeProjectionUpsert",
29
59
  defaultFailureStrategy: "throw",
30
60
  run: async (args, ctx: PipelineCtx) => {
31
61
  const resolvedRow = resolveRequired(args.row, ctx);
32
62
 
33
- const columns = getTableColumns(args.table) as Record<string, unknown>;
34
- const conflictTargets = args.on.map((key) => {
35
- const col = columns[key];
36
- if (!col) {
37
- throw new Error(`unsafeProjectionUpsert: column "${key}" not found on target table`);
63
+ // Validate conflict-key columns exist in the row.
64
+ for (const key of args.on) {
65
+ if (!(key in resolvedRow)) {
66
+ throw new Error(`unsafeProjectionUpsert: column "${key}" not found in row`);
38
67
  }
39
- return col;
40
- });
68
+ }
69
+
70
+ const tableName = resolveTableName(args.table);
71
+ const entries = Object.entries(resolvedRow);
72
+ const params: unknown[] = [];
41
73
 
42
- // SET clause is the same row minus the conflict-key columns —
43
- // updating a key to itself is harmless but verbose.
44
- const updateSet: Record<string, unknown> = {};
45
- for (const [k, v] of Object.entries(resolvedRow)) {
46
- if (!args.on.includes(k)) updateSet[k] = v;
74
+ const colNames = entries.map(([k]) => quoteIdent(resolveColumnName(args.table, k)));
75
+ const placeholders = entries.map((_, i) => `$${i + 1}`);
76
+ for (const [, v] of entries) params.push(v);
77
+
78
+ const conflictCols = args.on
79
+ .map((k) => quoteIdent(resolveColumnName(args.table, k)))
80
+ .join(", ");
81
+
82
+ // SET clause excludes conflict-key columns.
83
+ const setClauses: string[] = [];
84
+ let paramIdx = entries.length + 1;
85
+ for (const [k, v] of entries) {
86
+ if (args.on.includes(k)) continue;
87
+ setClauses.push(`${quoteIdent(resolveColumnName(args.table, k))} = $${paramIdx++}`);
88
+ params.push(v);
47
89
  }
48
90
 
49
- // @cast-boundary drizzle-bridge — The values + set + target casts
50
- // cross the drizzle type-boundary for the same reason as
51
- // asQueryTarget: resolvedRow is Record<string, unknown> by design
52
- // (M.1 phantom-typing limit), drizzle's typed-builder expects
53
- // table-specific shapes. Step-author owns shape correctness.
54
- // `as never` (not `as any`) — never is contravariantly assignable to
55
- // every drizzle Insert-shape; explicit "this bypass cannot be made
56
- // type-safe without lifting <TTable extends Table>" marker.
57
- await ctx.db
58
- .insert(asQueryTarget(args.table))
59
- .values(resolvedRow as never)
60
- .onConflictDoUpdate({
61
- target: conflictTargets as unknown as PgColumn[],
62
- set: updateSet as never,
63
- });
91
+ const sqlText =
92
+ `INSERT INTO ${quoteIdent(tableName)} (${colNames.join(", ")}) VALUES (${placeholders.join(", ")}) ` +
93
+ `ON CONFLICT (${conflictCols}) DO UPDATE SET ${setClauses.join(", ")}`;
94
+
95
+ await executeRawQuery(ctx.db.raw, sqlText, params);
64
96
  },
65
97
  });
66
98
 
@@ -1,5 +1,11 @@
1
- import type { PgTable } from "drizzle-orm/pg-core";
2
1
  import type { ZodType, z } from "zod";
2
+ import type { EntityTableMeta } from "../../db/entity-table-meta";
3
+
4
+ // PgTable historically came from drizzle-orm/pg-core; the native dialect
5
+ // no longer carries drizzle internal class types. Every caller really
6
+ // needs "an opaque table-object with Symbol-based introspection".
7
+ type PgTable = unknown;
8
+
3
9
  import type { QueryHandlerDefinition, WriteHandlerDefinition } from "../define-handler";
4
10
  import type {
5
11
  ConfigKeyDefinition,
@@ -143,6 +149,23 @@ export type RawTableDef = RawTableEntry & {
143
149
  readonly featureName: string;
144
150
  };
145
151
 
152
+ // --- Unmanaged tables (declared by features via r.unmanagedTable()) ---
153
+
154
+ /** Per-feature unmanaged-table registration. `meta` is the
155
+ * `EntityTableMeta` (framework-native shape used by `migrate-runner`).
156
+ * The `reason` justifies the bypass at the registration site — same
157
+ * contract as `r.rawTable`. */
158
+ export type UnmanagedTableEntry = {
159
+ readonly name: string;
160
+ readonly meta: EntityTableMeta;
161
+ readonly reason: string;
162
+ };
163
+
164
+ /** Registry-aggregated unmanaged-table — adds the owning feature name. */
165
+ export type UnmanagedTableDef = UnmanagedTableEntry & {
166
+ readonly featureName: string;
167
+ };
168
+
146
169
  // --- Feature Definition (output of defineFeature) ---
147
170
 
148
171
  export type FeatureDefinition = {
@@ -176,7 +199,7 @@ export type FeatureDefinition = {
176
199
  // F3 search-payload-extension — per-entity contributors that add flat fields
177
200
  // to the search-index payload during indexing. Keyed by entityName. Wrapped
178
201
  // in OwnedFn for feature-toggle filtering (consistent with postQuery-Hooks).
179
- readonly searchPayloadExtensions: Readonly<
202
+ readonly searchPayloadExtensions?: Readonly<
180
203
  Record<string, readonly OwnedFn<SearchPayloadContributorFn>[]>
181
204
  >;
182
205
  readonly configKeys: Readonly<Record<string, ConfigKeyDefinition>>;
@@ -269,6 +292,12 @@ export type FeatureDefinition = {
269
292
  // system. Keyed by feature-local short name. The registry attaches
270
293
  // featureName on aggregation, lifting RawTableEntry → RawTableDef.
271
294
  readonly rawTables: Readonly<Record<string, RawTableEntry>>;
295
+ // Unmanaged tables declared via r.unmanagedTable() — `EntityTableMeta`
296
+ // shape (post-drizzle), keyed by feature-local table-name. Cousin of
297
+ // rawTables: same bypass-justification contract, different storage
298
+ // shape. `kumiko schema generate` aggregates these alongside
299
+ // r.entity()-derived metas to build the full schema.
300
+ readonly unmanagedTables: Readonly<Record<string, UnmanagedTableEntry>>;
272
301
  // Optional Zod-schema for env-vars this feature reads at runtime.
273
302
  // Declared via `r.envSchema(z.object({...}))`. `composeEnvSchema` reads
274
303
  // this to build one app-wide schema for boot-validation + dry-run
@@ -577,6 +606,22 @@ export type FeatureRegistrar<TFeature extends string = string> = {
577
606
  // declare data via `r.entity()` instead.
578
607
  rawTable(name: string, table: PgTable, options: RawTableOptions): void;
579
608
 
609
+ // Declare an "unmanaged" framework-native table (post-drizzle).
610
+ // EntityTableMeta carries the same column-shape that r.entity() builds,
611
+ // minus the audit-trail + base-columns scaffolding — used for read-side
612
+ // projections of event-streams (delivery-attempts, job-run-logs) where
613
+ // r.entity()'s aggregate-lifecycle assumptions don't fit.
614
+ //
615
+ // The `meta` argument is the result of `defineUnmanagedTable(...)` from
616
+ // `@cosmicdrift/kumiko-framework/db`. Reason-justification + audit-trail
617
+ // contract identical to `r.rawTable`.
618
+ //
619
+ // Why this exists separate from `r.rawTable`: rawTable carries a Drizzle
620
+ // `PgTable` (legacy), unmanagedTable carries the new `EntityTableMeta`
621
+ // shape that `migrate-runner` consumes. After the full drizzle-cut they
622
+ // will likely merge; for now they coexist.
623
+ unmanagedTable(meta: EntityTableMeta, options: RawTableOptions): void;
624
+
580
625
  // Register the tree-actions schema for this feature — a map of
581
626
  // action-name → action-definition (with optional typed args). At-most-
582
627
  // one call per feature.
@@ -5,7 +5,6 @@
5
5
  // accepted at the type layer during migration: features that pass an
6
6
  // array are auto-normalized to { [role]: "all" } at registry build.
7
7
  // Long-term: string[] disappears.
8
- import type { SQL } from "drizzle-orm";
9
8
  import type { OwnershipMap } from "../ownership";
10
9
 
11
10
  export type FieldAccess = {
@@ -475,7 +474,7 @@ export function isFileField(field: FieldDefinition | undefined): field is AnyFil
475
474
  export type TransitionMap = Readonly<Record<string, readonly string[]>>;
476
475
 
477
476
  /** Composite-Index auf einer Entity. Spalten werden via field-Name
478
- * referenziert (camelCase). buildDrizzleTable mapped sie auf snake_case-
477
+ * referenziert (camelCase). buildEntityTable mapped sie auf snake_case-
479
478
  * Spaltennamen und benennt den Index nach Convention:
480
479
  *
481
480
  * <table>_<col1>_<col2>_idx (non-unique)
@@ -484,7 +483,7 @@ export type TransitionMap = Readonly<Record<string, readonly string[]>>;
484
483
  * Eine `name`-Override ist erlaubt — Convention-Bruch in Bestandscode
485
484
  * vermeidet Migration-Churn beim Refactor.
486
485
  *
487
- * Single-column indices über `tenantId` sind redundant (buildDrizzleTable
486
+ * Single-column indices über `tenantId` sind redundant (buildEntityTable
488
487
  * legt die immer automatisch an); die Boot-Validation warnt. */
489
488
  export type EntityIndexDef = {
490
489
  readonly columns: readonly [string, ...string[]];
@@ -503,7 +502,7 @@ export type EntityIndexDef = {
503
502
  * man z.B. fuer scharfe BTREE-Indexes nur auf einer Status-Teilmenge
504
503
  * statt voller Tabelle).
505
504
  */
506
- readonly where?: SQL;
505
+ readonly where?: unknown;
507
506
  };
508
507
 
509
508
  export type FieldsMap = Readonly<Record<string, FieldDefinition>>;
@@ -517,7 +516,7 @@ export type EntityDefinition<F extends FieldsMap = FieldsMap> = {
517
516
  /** Allowed state transitions per field. Boot validates against select options. */
518
517
  readonly transitions?: Readonly<Record<string, TransitionMap>>;
519
518
  /** Composite-Indices über mehrere Felder. Single-column FK-Indices und
520
- * der tenant_id-Index werden weiterhin automatisch von buildDrizzleTable
519
+ * der tenant_id-Index werden weiterhin automatisch von buildEntityTable
521
520
  * angelegt — diese Liste ist nur für Custom-Indices die der Author
522
521
  * explizit deklariert (z.B. `{ unique: true, columns: ["key", "tenantId", "userId"] }`). */
523
522
  readonly indexes?: readonly EntityIndexDef[];
@@ -70,6 +70,8 @@ export type {
70
70
  SecretKeyDefinition,
71
71
  SecretKeyHandle,
72
72
  SecretOptions,
73
+ UnmanagedTableDef,
74
+ UnmanagedTableEntry,
73
75
  } from "./feature";
74
76
  export type {
75
77
  AnyFileFieldDef,
@@ -9,8 +9,8 @@
9
9
  // (see TS-typing notes in the design doc). M.1 uses unsafeAppendEvent
10
10
  // semantics under the hood for r.step.aggregate.appendEvent.
11
11
 
12
- import type { SQL, Table } from "drizzle-orm";
13
12
  import type { EventStoreExecutor } from "../../db/event-store-executor";
13
+ import type { WhereObject } from "../../db/query";
14
14
  import type { KumikoEventTypeMap } from "./event-type-map";
15
15
  import type { HandlerContext, WriteEvent, WriteResult } from "./handlers";
16
16
  import type { SaveContext } from "./hooks";
@@ -200,31 +200,31 @@ export type StepNamespace = {
200
200
  // and NOT registered as an aggregate-table via r.entity. See
201
201
  // step-vocabulary.md "Was unsafeProjection.* überspringt".
202
202
  readonly unsafeProjectionUpsert: (args: {
203
- readonly table: Table;
203
+ readonly table: unknown;
204
204
  readonly on: readonly string[];
205
205
  readonly row: StepResolver<Record<string, unknown>>;
206
206
  }) => StepInstance;
207
207
  // Sibling: delete row(s) from a read-side projection table. Same
208
208
  // boot-validation contract as unsafeProjectionUpsert.
209
209
  readonly unsafeProjectionDelete: (args: {
210
- readonly table: Table;
211
- readonly where: StepResolver<SQL>;
210
+ readonly table: unknown;
211
+ readonly where: StepResolver<WhereObject>;
212
212
  }) => StepInstance;
213
- // Read sub-namespace — thin wrapper on ctx.db.select(). Caller-owned
214
- // tenant-filter (does NOT auto-inject like ctx.queryProjection does).
213
+ // Read sub-namespace — thin wrapper on selectMany/fetchOne (bun-db).
214
+ // Caller-owned tenant-filter (does NOT auto-inject like ctx.queryProjection does).
215
215
  readonly read: {
216
216
  readonly findOne: (
217
217
  name: string,
218
218
  opts: {
219
- readonly table: Table;
220
- readonly where: StepResolver<SQL | undefined>;
219
+ readonly table: unknown;
220
+ readonly where: StepResolver<WhereObject | undefined>;
221
221
  },
222
222
  ) => StepInstance;
223
223
  readonly findMany: (
224
224
  name: string,
225
225
  opts: {
226
- readonly table: Table;
227
- readonly where?: StepResolver<SQL | undefined>;
226
+ readonly table: unknown;
227
+ readonly where?: StepResolver<WhereObject | undefined>;
228
228
  readonly limit?: number;
229
229
  },
230
230
  ) => StepInstance;
@@ -23,7 +23,6 @@
23
23
  // caught by this validator. A future lint-rule will enforce the contract
24
24
  // statically; today it lives in this comment + the StepBuilder doc.
25
25
 
26
- import { getTableName, type Table } from "drizzle-orm";
27
26
  import { getStep } from "./define-step";
28
27
  import { buildPipelineSteps } from "./pipeline";
29
28
  import type { FeatureDefinition, SessionUser, TenantId, WriteEvent } from "./types";
@@ -56,7 +55,28 @@ function* walkAllSteps(steps: readonly StepInstance[]): Generator<StepInstance,
56
55
  }
57
56
  }
58
57
 
59
- type UnsafeProjectionStepArgs = { readonly table: Table };
58
+ // @cast-boundary drizzle-bridge reads table name from drizzle Symbol
59
+ // without importing drizzle-orm (bun-db pattern, see bun-db/query.ts).
60
+ const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
61
+
62
+ function resolveTableNameFromStep(table: unknown): string {
63
+ if (typeof table === "object" && table !== null) {
64
+ // EntityTableMeta discriminator
65
+ if (
66
+ "source" in table &&
67
+ "tableName" in table &&
68
+ typeof (table as Record<string, unknown>)["tableName"] === "string"
69
+ ) {
70
+ return (table as Record<string, unknown>)["tableName"] as string;
71
+ }
72
+ // drizzle pgTable
73
+ const name = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
74
+ if (typeof name === "string") return name;
75
+ }
76
+ throw new Error(`validate-projection-allowlist: cannot resolve table name from ${String(table)}`);
77
+ }
78
+
79
+ type UnsafeProjectionStepArgs = { readonly table: unknown };
60
80
 
61
81
  const DUMMY_USER: SessionUser = {
62
82
  id: "00000000-0000-0000-0000-000000000000",
@@ -137,7 +157,7 @@ export function validateProjectionAllowlist(features: readonly FeatureDefinition
137
157
  `without a \`table\` argument.`,
138
158
  );
139
159
  }
140
- const tableName = getTableName(stepArgs.table);
160
+ const tableName = resolveTableNameFromStep(stepArgs.table);
141
161
 
142
162
  const aggregateOwner = aggregateTables.get(tableName);
143
163
  if (aggregateOwner) {
@@ -12,12 +12,13 @@
12
12
  // so a future refactor that drops the merge fails here instead of silently
13
13
  // regressing to the Welle-2.5 state.
14
14
 
15
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
15
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
16
16
  import { z } from "zod";
17
+ import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
17
18
  import { createRegistry, defineFeature } from "../../engine";
18
19
  import { createArchivedStreamsTable, createEventsTable } from "../../event-store";
19
20
  import { createEventConsumerStateTable } from "../../pipeline";
20
- import { createTestDb, createTestRedis, type TestDb, type TestRedis, TestUsers } from "../../stack";
21
+ import { createTestRedis, type TestRedis, TestUsers } from "../../stack";
21
22
  import { waitFor } from "../../testing";
22
23
  import { createAllInOneEntrypoint } from "../index";
23
24
 
@@ -72,7 +73,7 @@ const mixedLaneFeature = defineFeature("mixed", (r) => {
72
73
  const JWT = "entrypoint-wiring-test-secret-must-be-32-chars!";
73
74
  const adminUser = TestUsers.admin;
74
75
 
75
- let testDb: TestDb;
76
+ let testDb: BunTestDb;
76
77
  let testRedis: TestRedis;
77
78
 
78
79
  beforeAll(async () => {
@@ -11,12 +11,13 @@
11
11
  // guard — buildServer always wires an SSE consumer so this only
12
12
  // fires with systemConsumers explicitly disabled).
13
13
 
14
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
14
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
15
15
  import { z } from "zod";
16
+ import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
16
17
  import { createRegistry, defineFeature } from "../../engine";
17
18
  import { createArchivedStreamsTable, createEventsTable } from "../../event-store";
18
19
  import { createEventConsumerStateTable } from "../../pipeline";
19
- import { createTestDb, createTestRedis, type TestDb, type TestRedis } from "../../stack";
20
+ import { createTestRedis, type TestRedis } from "../../stack";
20
21
  import { createAllInOneEntrypoint, createApiEntrypoint, createWorkerEntrypoint } from "../index";
21
22
 
22
23
  const splitFeature = defineFeature("split", (r) => {
@@ -38,7 +39,7 @@ function uniquePrefix(label: string): string {
38
39
  return `${label}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
39
40
  }
40
41
 
41
- let testDb: TestDb;
42
+ let testDb: BunTestDb;
42
43
  let testRedis: TestRedis;
43
44
 
44
45
  beforeAll(async () => {
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { describe, expect, it } from "bun:test";
2
2
  import { z } from "zod";
3
3
  import { defineFeature } from "../../engine/define-feature";
4
4
  import { camelCase, composeEnvSchema, KumikoBootError, parseEnv, pulumiConfigKey } from "../index";
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { describe, expect, it } from "bun:test";
2
2
  import { z } from "zod";
3
3
  import { defineFeature } from "../../engine/define-feature";
4
4
  import { renderDryRun } from "../dry-run";
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "vitest";
1
+ import { describe, expect, test } from "bun:test";
2
2
  import { z } from "zod";
3
3
  import {
4
4
  AccessDeniedError,
@@ -0,0 +1,44 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { NotFoundError } from "../classes";
3
+ import { FrameworkReasons } from "../reasons";
4
+ import { buildInvalidTransitionDetails } from "../transition-details";
5
+ import { reraiseAsKumikoError, toWriteErrorInfo, writeFailure } from "../write-error-info";
6
+
7
+ describe("writeFailure", () => {
8
+ test("wraps KumikoError into WriteFailure envelope", () => {
9
+ const failure = writeFailure(new NotFoundError("invoice", "inv-1"));
10
+ expect(failure.isSuccess).toBe(false);
11
+ expect(failure.error.code).toBe("not_found");
12
+ expect(failure.error.httpStatus).toBe(404);
13
+ });
14
+ });
15
+
16
+ describe("reraiseAsKumikoError", () => {
17
+ test("round-trips WriteErrorInfo through KumikoError", () => {
18
+ const info = toWriteErrorInfo(new NotFoundError("task", 7));
19
+ const err = reraiseAsKumikoError(info);
20
+ expect(err.code).toBe("not_found");
21
+ expect(err.httpStatus).toBe(404);
22
+ expect(err.message).toContain("task");
23
+ });
24
+ });
25
+
26
+ describe("buildInvalidTransitionDetails", () => {
27
+ test("builds structured from/to/allowed + message", () => {
28
+ const details = buildInvalidTransitionDetails("draft", "paid", ["sent"]);
29
+ expect(details).toMatchObject({
30
+ from: "draft",
31
+ to: "paid",
32
+ allowed: ["sent"],
33
+ });
34
+ expect(details.message).toContain("draft");
35
+ expect(details.message).toContain("sent");
36
+ });
37
+ });
38
+
39
+ describe("FrameworkReasons", () => {
40
+ test("exposes stable snake_case reason codes", () => {
41
+ expect(FrameworkReasons.invalidTransition).toBe("invalid_transition");
42
+ expect(FrameworkReasons.staleState).toBe("stale_state");
43
+ });
44
+ });
@@ -0,0 +1,16 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { FieldIssue as FrameworkFieldIssue } from "@cosmicdrift/kumiko-framework/errors";
3
+ import type { FieldIssue as HeadlessFieldIssue } from "@cosmicdrift/kumiko-headless";
4
+
5
+ describe("FieldIssue cross-package contract", () => {
6
+ test("framework and headless FieldIssue shapes are assignable", () => {
7
+ const frameworkIssue: FrameworkFieldIssue = {
8
+ path: "title",
9
+ code: "too_small",
10
+ i18nKey: "errors.validation.too_small",
11
+ params: { minimum: 1 },
12
+ };
13
+ const headlessIssue: HeadlessFieldIssue = frameworkIssue;
14
+ expect(headlessIssue.path).toBe("title");
15
+ });
16
+ });
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "vitest";
1
+ import { describe, expect, test } from "bun:test";
2
2
  import { failNotFound, failTransition, failUnprocessable } from "../write-error-info";
3
3
 
4
4
  describe("failNotFound", () => {
@@ -1,16 +1,11 @@
1
+ import { toSnakeCase } from "../utils/case";
2
+ import type { FieldIssue } from "./field-issue";
1
3
  import { type ErrorOpts, KumikoError } from "./kumiko-error";
2
4
 
3
- // Per-field validation issue. Shared shape between Zod-derived and
4
- // hook-derived validation errors so the client sees one list.
5
- export type ValidationFieldIssue = {
6
- readonly path: string;
7
- readonly code: string;
8
- readonly i18nKey: string;
9
- readonly params?: Readonly<Record<string, unknown>>;
10
- };
5
+ export type { FieldIssue, ValidationFieldIssue } from "./field-issue";
11
6
 
12
7
  export type ValidationDetails = {
13
- readonly fields: readonly ValidationFieldIssue[];
8
+ readonly fields: readonly FieldIssue[];
14
9
  };
15
10
 
16
11
  export class ValidationError extends KumikoError {
@@ -89,7 +84,7 @@ export class NotFoundError extends KumikoError {
89
84
  // The reason string follows `<snake_entity>_not_found` — keeps a stable,
90
85
  // client-friendly tag that survives wire serialization even if the entity
91
86
  // name is later renamed for display purposes.
92
- const reason = `${toSnake(entity)}_not_found`;
87
+ const reason = `${toSnakeCase(entity)}_not_found`;
93
88
  const details: NotFoundDetails & { reason: string } = { reason, entity, id: idStr };
94
89
  super({
95
90
  message: idStr !== undefined ? `${entity} ${idStr} not found` : `${entity} not found`,
@@ -101,15 +96,6 @@ export class NotFoundError extends KumikoError {
101
96
  }
102
97
  }
103
98
 
104
- // Accepts camelCase OR kebab-case entity names and produces snake_case for
105
- // the reason tag. New code uses kebab; legacy camelCase still flows through.
106
- function toSnake(s: string): string {
107
- return s
108
- .replace(/-/g, "_")
109
- .replace(/([a-z])([A-Z])/g, "$1_$2")
110
- .toLowerCase();
111
- }
112
-
113
99
  // Generic 409. Features that need a narrower shape should subclass (see
114
100
  // VersionConflictError) — this way the HTTP layer stays uniform while callers
115
101
  // can still instanceof on the concrete subtype in handlers.
@@ -0,0 +1,11 @@
1
+ // Canonical per-field validation issue shape — shared between server-side
2
+ // ValidationError, Zod-bridge, and client-side DispatcherError.details.fields.
3
+ export type FieldIssue = {
4
+ readonly path: string;
5
+ readonly code: string;
6
+ readonly i18nKey: string;
7
+ readonly params?: Readonly<Record<string, unknown>>;
8
+ };
9
+
10
+ /** @deprecated Use `FieldIssue` — kept for existing imports. */
11
+ export type ValidationFieldIssue = FieldIssue;
@@ -1,5 +1,6 @@
1
1
  export type {
2
2
  FeatureDisabledDetails,
3
+ FieldIssue,
3
4
  NotFoundDetails,
4
5
  RateLimitDetails,
5
6
  UniqueViolationDetails,
@@ -1,5 +1,6 @@
1
1
  import type { ZodError, ZodIssue } from "zod";
2
- import { ValidationError, type ValidationFieldIssue } from "./classes";
2
+ import { ValidationError } from "./classes";
3
+ import type { FieldIssue } from "./field-issue";
3
4
 
4
5
  // Zod issues carry a .code and sometimes issue-specific params (min, max, etc).
5
6
  // We surface those under `params` so the client can render "must be at least N"
@@ -24,7 +25,7 @@ const ISSUE_PARAM_KEYS = [
24
25
  ] as const;
25
26
 
26
27
  export function validationErrorFromZod(error: ZodError): ValidationError {
27
- const fields = error.issues.map<ValidationFieldIssue>((issue) => {
28
+ const fields = error.issues.map<FieldIssue>((issue) => {
28
29
  const params = extractIssueParams(issue);
29
30
  return {
30
31
  path: issue.path.map(String).join(".") || "(root)",