@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
@@ -15,7 +15,7 @@
15
15
  // 5. Snapshot erneut nehmen
16
16
  // 6. deep-equal: identische Rows in identischer Reihenfolge
17
17
 
18
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
18
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
19
19
  import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
20
20
  import { createBooleanField, createEntity, createTextField, defineFeature } from "../../engine";
21
21
  import { createRegistry } from "../../engine/registry";
@@ -222,26 +222,33 @@ describe("implicit-projection / Live==Rebuild equivalence", () => {
222
222
  });
223
223
  });
224
224
 
225
- // Sensitive-Drift ist eine bekannte Welle-3-Lücke: das Event-Log strippt
226
- // sensitive-Felder VOR dem Append (GDPR-Annahme), die Live-Read-Tabelle
227
- // bekommt sie über den unstripped flatData, der Rebuild-Pfad nur den
228
- // stripped event.payload. Bei Schema-Rebuilds gehen sensitive Daten
229
- // verloren.
230
- //
231
- // Dieser Test pinst die Drift explizit: Live row hat das sensitive Feld,
232
- // Rebuild row hat NULL. Wenn Welle 3 das fixt (z.B. via separater
233
- // sensitive-Spalte oder verschlüsseltem Event-Payload), bricht der Test
234
- // und zwingt zu Aufmerksamkeit.
235
-
225
+ // Sensitive-Rebuild-Parität (#967): das Event-Log trägt für sensitive-Felder
226
+ // den Tabellen-Ciphertext (boot-validiert pii/encrypted) Live==Rebuild gilt
227
+ // damit auch für sensitive Spalten + Blind-Index. Einzige legitime Divergenz
228
+ // bleibt Crypto-Shredding: DEK erased bidx NULL, Wert unlesbar.
229
+
230
+ import {
231
+ computeBlindIndex,
232
+ configureBlindIndexKey,
233
+ configurePiiSubjectKms,
234
+ decodeBlindIndexKey,
235
+ decryptPiiFieldValues,
236
+ InMemoryKmsAdapter,
237
+ isPiiCiphertext,
238
+ resetBlindIndexKeyForTests,
239
+ resetPiiSubjectKmsForTests,
240
+ } from "../../crypto";
236
241
  import { asRawClient, selectMany } from "../../db/query";
237
242
 
238
243
  const sensitiveTable = "read_implicit_sensitive_users";
244
+ const SENSITIVE_BIDX_KEY_B64 = Buffer.alloc(32, 9).toString("base64");
245
+ const SENSITIVE_BIDX_KEY = decodeBlindIndexKey(SENSITIVE_BIDX_KEY_B64);
239
246
 
240
247
  const sensitiveEntity = createEntity({
241
248
  table: sensitiveTable,
242
249
  fields: {
243
250
  email: createTextField({ required: true }),
244
- apiKey: createTextField({ sensitive: true }),
251
+ apiKey: createTextField({ sensitive: true, pii: true, lookupable: true }),
245
252
  },
246
253
  });
247
254
 
@@ -250,8 +257,11 @@ const sensitiveFeature = defineFeature("implicitsensitive", (r) => {
250
257
  });
251
258
 
252
259
  const sensitiveEntityTable = buildEntityTable("sensitive-user", sensitiveEntity);
260
+ const sensitiveProjection = "implicitsensitive:projection:sensitive-user-entity";
261
+
262
+ describe("implicit-projection / sensitive Rebuild-Parität (#967)", () => {
263
+ let kms: InMemoryKmsAdapter;
253
264
 
254
- describe("implicit-projection / dokumentierte Sensitive-Drift", () => {
255
265
  beforeAll(async () => {
256
266
  await unsafeCreateEntityTable(testDb.db, sensitiveEntity, "sensitive-user");
257
267
  });
@@ -260,29 +270,46 @@ describe("implicit-projection / dokumentierte Sensitive-Drift", () => {
260
270
  await asRawClient(testDb.db).unsafe(
261
271
  `TRUNCATE ${sensitiveTable}, kumiko_events, kumiko_projections RESTART IDENTITY CASCADE`,
262
272
  );
273
+ kms = new InMemoryKmsAdapter();
274
+ configurePiiSubjectKms(kms);
275
+ configureBlindIndexKey(SENSITIVE_BIDX_KEY_B64);
263
276
  });
264
277
 
265
- test("Live schreibt sensitive-Felder, Rebuild lässt sie NULL (Welle-3-Roadmap)", async () => {
266
- const crud = createEventStoreExecutor(sensitiveEntityTable, sensitiveEntity, {
267
- entityName: "sensitive-user",
268
- });
278
+ afterEach(() => {
279
+ resetPiiSubjectKmsForTests();
280
+ resetBlindIndexKeyForTests();
281
+ });
269
282
 
270
- // 1. Live: create mit apiKey (sensitive). Read-Tabelle bekommt den
271
- // Wert direkt vom Live-Pfad (unstripped flatData).
283
+ const crud = createEventStoreExecutor(sensitiveEntityTable, sensitiveEntity, {
284
+ entityName: "sensitive-user",
285
+ });
286
+
287
+ async function rawSensitiveRow(id: string): Promise<Record<string, unknown>> {
288
+ const rows = await asRawClient(testDb.db).unsafe<Record<string, unknown>>(
289
+ `SELECT * FROM ${sensitiveTable} WHERE id = $1`,
290
+ [id],
291
+ );
292
+ const row = rows[0];
293
+ if (!row) throw new Error(`no row for ${id}`);
294
+ return row;
295
+ }
296
+
297
+ test("Live==Rebuild inkl. sensitive-Ciphertext + bidx (byte-gleiche Kopie aus dem Event)", async () => {
272
298
  const created = await crud.create(
273
299
  { email: "x@test.de", apiKey: "secret-token-abc" },
274
300
  adminUser,
275
301
  tdb,
276
302
  );
277
303
  if (!created.isSuccess) throw new Error("setup failed");
304
+ const id = String(created.data.id);
278
305
 
279
- const [liveRow] = await selectMany(testDb.db, sensitiveEntityTable, {
280
- id: created.data.id as string,
281
- });
282
- expect(liveRow?.["apiKey"]).toBe("secret-token-abc");
283
- expect(liveRow?.["email"]).toBe("x@test.de");
306
+ // Live-Row: Ciphertext + bidx, nie Klartext.
307
+ const liveRow = await rawSensitiveRow(id);
308
+ const liveCipher = liveRow["api_key"];
309
+ expect(isPiiCiphertext(liveCipher)).toBe(true);
310
+ expect(liveRow["api_key_bidx"]).toBe(computeBlindIndex(SENSITIVE_BIDX_KEY, "secret-token-abc"));
284
311
 
285
- // 2. Verifiziere dass das Event-Log das Feld NICHT enthält (stripped).
312
+ // Event-Payload trägt exakt den Tabellen-Ciphertext (byte-gleich).
286
313
  const { eventsTable } = await import("../../event-store");
287
314
  const [event] = await selectMany(
288
315
  testDb.db,
@@ -290,23 +317,75 @@ describe("implicit-projection / dokumentierte Sensitive-Drift", () => {
290
317
  { aggregateId: created.data.id },
291
318
  { orderBy: { col: "version", direction: "asc" } },
292
319
  );
293
- expect(event?.payload?.["apiKey"]).toBeUndefined();
320
+ expect(event?.payload?.["apiKey"]).toBe(liveCipher);
294
321
  expect(event?.payload?.["email"]).toBe("x@test.de");
322
+ expect(JSON.stringify(event?.payload)).not.toContain("secret-token-abc");
295
323
 
296
- // 3. Rebuild über die ImplicitProjection. Read-Tabelle wird aus
297
- // event.payload neu materialisiert — apiKey ist nicht im Log,
298
- // landet also als NULL/undefined in der rebuilt Row.
299
324
  const registry = createRegistry([sensitiveFeature]);
300
- await rebuildProjection("implicitsensitive:projection:sensitive-user-entity", {
301
- db: testDb.db,
302
- registry,
303
- });
325
+ await rebuildProjection(sensitiveProjection, { db: testDb.db, registry });
326
+
327
+ const rebuiltRow = await rawSensitiveRow(id);
328
+ expect(rebuiltRow["email"]).toBe("x@test.de");
329
+ expect(rebuiltRow["api_key"]).toBe(liveCipher);
330
+ expect(rebuiltRow["api_key_bidx"]).toBe(liveRow["api_key_bidx"]);
331
+ });
304
332
 
305
- const [rebuiltRow] = await selectMany(testDb.db, sensitiveEntityTable, {
306
- id: created.data.id as string,
333
+ test("DEK-Stabilität: Update re-encryptet mit demselben Subject-DEK — alter Event-Ciphertext bleibt lesbar", async () => {
334
+ const created = await crud.create(
335
+ { email: "y@test.de", apiKey: "first-secret" },
336
+ adminUser,
337
+ tdb,
338
+ );
339
+ if (!created.isSuccess) throw new Error("setup failed");
340
+ const updated = await crud.update(
341
+ { id: created.data.id, version: 1, changes: { apiKey: "second-secret" } },
342
+ adminUser,
343
+ tdb,
344
+ );
345
+ if (!updated.isSuccess) throw new Error("update failed");
346
+
347
+ // Created-Event (v1-Ciphertext) muss mit dem aktuellen DEK lesbar bleiben —
348
+ // eine DEK-Rotation beim Update würde das immutable Log unlesbar machen.
349
+ const { eventsTable } = await import("../../event-store");
350
+ const [createdEvent] = await selectMany(
351
+ testDb.db,
352
+ eventsTable,
353
+ { aggregateId: created.data.id },
354
+ { orderBy: { col: "version", direction: "asc" } },
355
+ );
356
+ const v1Cipher = createdEvent?.payload?.["apiKey"];
357
+ expect(isPiiCiphertext(v1Cipher)).toBe(true);
358
+ const decrypted = await decryptPiiFieldValues({ apiKey: v1Cipher }, ["apiKey"], kms, {
359
+ requestId: "test",
307
360
  });
308
- expect(rebuiltRow?.["email"]).toBe("x@test.de");
309
- // DAS ist die Drift: sensitive Feld ist nach Rebuild weg.
310
- expect(rebuiltRow?.["apiKey"]).toBeNull();
361
+ expect(decrypted["apiKey"]).toBe("first-secret");
362
+
363
+ // Rebuild spielt created+updated: bidx-Recompute entschlüsselt beide
364
+ // Ciphertexte — schlägt bei rotiertem DEK fehl statt still zu heilen.
365
+ const registry = createRegistry([sensitiveFeature]);
366
+ await rebuildProjection(sensitiveProjection, { db: testDb.db, registry });
367
+ const rebuiltRow = await rawSensitiveRow(String(created.data.id));
368
+ expect(rebuiltRow["api_key_bidx"]).toBe(computeBlindIndex(SENSITIVE_BIDX_KEY, "second-secret"));
369
+ });
370
+
371
+ test("Erase-Divergenz bleibt die einzige legitime: DEK erased → Rebuild bidx NULL, Wert unlesbar", async () => {
372
+ const created = await crud.create(
373
+ { email: "z@test.de", apiKey: "gone-after-forget" },
374
+ adminUser,
375
+ tdb,
376
+ );
377
+ if (!created.isSuccess) throw new Error("setup failed");
378
+ const id = String(created.data.id);
379
+
380
+ await kms.eraseKey({ kind: "user", userId: id });
381
+
382
+ const registry = createRegistry([sensitiveFeature]);
383
+ await rebuildProjection(sensitiveProjection, { db: testDb.db, registry });
384
+
385
+ const rebuiltRow = await rawSensitiveRow(id);
386
+ expect(rebuiltRow["api_key_bidx"]).toBeNull();
387
+ // Ciphertext-Bytes werden kopiert, sind aber ohne DEK unlesbar.
388
+ expect(isPiiCiphertext(rebuiltRow["api_key"])).toBe(true);
389
+ expect(rebuiltRow["api_key"]).not.toBe("gone-after-forget");
311
390
  });
312
391
  });
@@ -0,0 +1,21 @@
1
+ // kumiko-framework#1480 twin bug: instantToDriver referenced the global
2
+ // `Temporal` the same way instantFromDriver in bun-db/query.ts did. Fixed
3
+ // the same way — static `import { Temporal } from "temporal-polyfill"` in
4
+ // dialect.ts. This test deletes globalThis.Temporal before calling it,
5
+ // proving the write path no longer depends on the global either.
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+ import { instantToDriver } from "../dialect";
9
+
10
+ describe("instantToDriver — without a global Temporal", () => {
11
+ test("coerces an ISO string without relying on a global Temporal", () => {
12
+ const savedGlobal = (globalThis as { Temporal?: unknown }).Temporal;
13
+ delete (globalThis as { Temporal?: unknown }).Temporal;
14
+ try {
15
+ expect(instantToDriver("2026-04-18T10:00:00Z")).toBe("2026-04-18T10:00:00Z");
16
+ } finally {
17
+ if (savedGlobal === undefined) delete (globalThis as { Temporal?: unknown }).Temporal;
18
+ else (globalThis as { Temporal?: unknown }).Temporal = savedGlobal;
19
+ }
20
+ });
21
+ });
@@ -103,6 +103,25 @@ describe("flattenLocatedTimestamp — Insert/Update Convert", () => {
103
103
  });
104
104
  });
105
105
 
106
+ describe("rehydrateLocatedTimestamp — kumiko-framework#1490: no ambient Temporal global", () => {
107
+ test("rehydrates without relying on globalThis.Temporal", () => {
108
+ const savedGlobal = (globalThis as { Temporal?: unknown }).Temporal;
109
+ delete (globalThis as { Temporal?: unknown }).Temporal;
110
+ try {
111
+ const out = rehydrateLocatedTimestamp(
112
+ { pickupUtc: "2026-04-15T09:00:00Z", pickupTz: "Europe/Lisbon" },
113
+ orderEntity,
114
+ );
115
+ expect(out).toEqual({
116
+ pickup: { at: "2026-04-15T10:00:00", tz: "Europe/Lisbon", utc: "2026-04-15T09:00:00Z" },
117
+ });
118
+ } finally {
119
+ if (savedGlobal === undefined) delete (globalThis as { Temporal?: unknown }).Temporal;
120
+ else (globalThis as { Temporal?: unknown }).Temporal = savedGlobal;
121
+ }
122
+ });
123
+ });
124
+
106
125
  describe("rehydrateLocatedTimestamp — Read Convert", () => {
107
126
  test("{ <name>Utc, <name>Tz } DB-Form → { at, tz, utc } API-Form (Pickup-Ort-lokal)", () => {
108
127
  const out = rehydrateLocatedTimestamp(
@@ -95,6 +95,10 @@ describe("renderMigrationSql — managed recreate vs unmanaged in-place", () =>
95
95
  expect(sql).toContain('DROP TABLE IF EXISTS "read_secrets";');
96
96
  expect(sql).toContain('CREATE TABLE IF NOT EXISTS "read_secrets"');
97
97
  expect(sql).not.toContain("ADD COLUMN");
98
+ expect(sql).toContain(
99
+ "-- WARN: destructive change (new NOT NULL column(s) without default: envelope) forces DROP+CREATE + full event replay.",
100
+ );
101
+ expect(sql).toContain("Consider an Expand/Contract split across two releases");
98
102
  });
99
103
 
100
104
  test("managed: column rename (drop + add NOT NULL) → DROP+CREATE with new shape", () => {
@@ -121,6 +125,7 @@ describe("renderMigrationSql — managed recreate vs unmanaged in-place", () =>
121
125
  const sql = renderMigrationSql(diffSnapshots(prev, next), { name: "note", sequenceNumber: 4 });
122
126
  expect(sql).toContain('ALTER TABLE "read_a" ADD COLUMN "note"');
123
127
  expect(sql).not.toContain("DROP TABLE");
128
+ expect(sql).not.toContain("WARN: destructive change");
124
129
  });
125
130
 
126
131
  test("unmanaged: NOT NULL column without default → in-place ADD (real data, never recreated)", () => {
@@ -134,5 +139,21 @@ describe("renderMigrationSql — managed recreate vs unmanaged in-place", () =>
134
139
  });
135
140
  expect(sql).toContain('ALTER TABLE "app_data" ADD COLUMN "envelope"');
136
141
  expect(sql).not.toContain("DROP TABLE");
142
+ expect(sql).not.toContain("WARN: destructive change");
143
+ });
144
+
145
+ test("managed: multiple recreate reasons at once → all named in the warning", () => {
146
+ const prev = snapshotFromMetas([
147
+ meta("read_b", { name: "old_col", pgType: "text", notNull: false }, "managed"),
148
+ ]);
149
+ const next = snapshotFromMetas([
150
+ meta("read_b", { name: "envelope", pgType: "jsonb", notNull: true }, "managed"),
151
+ ]);
152
+ const sql = renderMigrationSql(diffSnapshots(prev, next), {
153
+ name: "multi",
154
+ sequenceNumber: 6,
155
+ });
156
+ expect(sql).toContain("dropped column(s): old_col");
157
+ expect(sql).toContain("new NOT NULL column(s) without default: envelope");
137
158
  });
138
159
  });
@@ -16,4 +16,65 @@ describe("splitSqlStatements", () => {
16
16
  test("filters empty segments", () => {
17
17
  expect(splitSqlStatements("-- only comments\n; ;")).toEqual([]);
18
18
  });
19
+
20
+ test("does not split mid-statement on a semicolon inside a line comment", () => {
21
+ const sql = `
22
+ -- sets default on the entity; the create/update handlers fill it
23
+ CREATE TABLE "a" ("id" uuid);
24
+ `;
25
+ expect(splitSqlStatements(sql)).toEqual(['CREATE TABLE "a" ("id" uuid);']);
26
+ });
27
+
28
+ test("does not split on a semicolon inside a block comment", () => {
29
+ const sql = `
30
+ /* multi
31
+ line; with semi */
32
+ CREATE TABLE "a" ("id" uuid);
33
+ `;
34
+ expect(splitSqlStatements(sql)).toEqual(['CREATE TABLE "a" ("id" uuid);']);
35
+ });
36
+
37
+ test("a block-comment opener inside a line comment does not swallow the next statement", () => {
38
+ const sql = `
39
+ -- note: see /* details below
40
+ CREATE TABLE "a" ("id" uuid);
41
+ /* real block comment */
42
+ CREATE TABLE "b" ("id" uuid);
43
+ `;
44
+ expect(splitSqlStatements(sql)).toEqual([
45
+ 'CREATE TABLE "a" ("id" uuid);',
46
+ 'CREATE TABLE "b" ("id" uuid);',
47
+ ]);
48
+ });
49
+
50
+ test("does not split on a semicolon inside a single-quoted string literal", () => {
51
+ const sql = `INSERT INTO "a" ("v") VALUES ('a;b');`;
52
+ expect(splitSqlStatements(sql)).toEqual([`INSERT INTO "a" ("v") VALUES ('a;b');`]);
53
+ });
54
+
55
+ test("does not treat a double-dash inside a string literal as a comment", () => {
56
+ const sql = `INSERT INTO "a" ("v") VALUES ('a--b');`;
57
+ expect(splitSqlStatements(sql)).toEqual([`INSERT INTO "a" ("v") VALUES ('a--b');`]);
58
+ });
59
+
60
+ test("handles an escaped quote inside a single-quoted string literal ('')", () => {
61
+ const sql = `INSERT INTO "a" ("v") VALUES ('a'';b');`;
62
+ expect(splitSqlStatements(sql)).toEqual([`INSERT INTO "a" ("v") VALUES ('a'';b');`]);
63
+ });
64
+
65
+ test("does not split on a semicolon inside a double-quoted identifier", () => {
66
+ const sql = `CREATE TABLE "weird;name" ("id" uuid);`;
67
+ expect(splitSqlStatements(sql)).toEqual([`CREATE TABLE "weird;name" ("id" uuid);`]);
68
+ });
69
+
70
+ test("throws fail-loud on an unterminated block comment instead of silently dropping statements", () => {
71
+ const sql = `/* oops\nCREATE TABLE "a" ("id" uuid);`;
72
+ expect(() => splitSqlStatements(sql)).toThrow();
73
+ });
74
+
75
+ test("a trailing line comment without a newline terminates cleanly", () => {
76
+ expect(splitSqlStatements('CREATE TABLE "a" ("id" uuid);\n-- done')).toEqual([
77
+ 'CREATE TABLE "a" ("id" uuid);',
78
+ ]);
79
+ });
19
80
  });
@@ -0,0 +1,58 @@
1
+ // Prod-relevant bug: NumberFieldDef mapped unconditionally to a Postgres
2
+ // `integer` column regardless of the `integer` flag. `createNumberField()`
3
+ // (no `integer: true`) accepts fractional values at the Zod boundary but
4
+ // the column rejected them at the DB with "invalid input syntax for type
5
+ // integer" — Monte-Carlo-style stats fields (phronexsis simulation results)
6
+ // hit this on every non-integer result. Fix: `integer: true` → `integer`
7
+ // column, otherwise → `double precision` (fractional values round-trip).
8
+
9
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
10
+ import { seedRow } from "@cosmicdrift/kumiko-framework/testing";
11
+ import { selectMany } from "../../db/query";
12
+ import { buildEntityTable } from "../../db/table-builder";
13
+ import { createEntity, createNumberField } from "../../engine";
14
+ import { setupTestStack, type TestStack, unsafeCreateEntityTable } from "../../stack";
15
+
16
+ const statsEntity = createEntity({
17
+ table: "nff_stats",
18
+ fields: {
19
+ expectedValue: createNumberField({ sortable: true }),
20
+ wholeCount: createNumberField({ integer: true }),
21
+ },
22
+ });
23
+ const statsTable = buildEntityTable("nff-stats", statsEntity);
24
+
25
+ let stack: TestStack;
26
+
27
+ beforeAll(async () => {
28
+ stack = await setupTestStack({ features: [] });
29
+ await unsafeCreateEntityTable(stack.db, statsEntity, "nff-stats");
30
+ });
31
+
32
+ afterAll(async () => stack?.cleanup());
33
+
34
+ describe("createNumberField() without integer:true — fractional values round-trip", () => {
35
+ test("INSERT accepts a non-integer value and SELECT returns it unchanged", async () => {
36
+ await seedRow(stack.db, statsTable, {
37
+ expectedValue: 77.02269129478736,
38
+ wholeCount: 3,
39
+ tenantId: "00000000-0000-4000-8000-000000000001",
40
+ });
41
+ const rows = await selectMany(stack.db, statsTable);
42
+ expect(rows).toHaveLength(1);
43
+ expect(rows[0]?.["expectedValue"]).toBe(77.02269129478736);
44
+ expect(rows[0]?.["wholeCount"]).toBe(3);
45
+ });
46
+ });
47
+
48
+ describe("createNumberField({ integer: true }) — still rejects fractional values at the DB", () => {
49
+ test("INSERT with a non-integer value into an integer-flagged field throws", async () => {
50
+ await expect(
51
+ seedRow(stack.db, statsTable, {
52
+ expectedValue: 1,
53
+ wholeCount: 3.5,
54
+ tenantId: "00000000-0000-4000-8000-000000000001",
55
+ }),
56
+ ).rejects.toThrow(/invalid input syntax for type integer/);
57
+ });
58
+ });
@@ -28,14 +28,16 @@ function tmpDir(): string {
28
28
  }
29
29
 
30
30
  describe("rebuildTablesFromDiff", () => {
31
- test("managed: new + changed tables included, dropped excluded, sorted + unique", () => {
31
+ test("managed: new table rebuilt, additive-only change NOT rebuilt, dropped excluded", () => {
32
+ // read_a: nur eine nullable Spalte dazu → in-place ALTER, kein Rebuild.
33
+ // read_b: neue Tabelle → Rebuild. read_c gedroppt → nie im Marker.
32
34
  const prev = snapshotFromMetas([meta("read_a"), meta("read_c")]);
33
35
  const next = snapshotFromMetas([
34
36
  meta("read_a", { name: "title", pgType: "text", notNull: false }),
35
37
  meta("read_b"),
36
38
  ]);
37
39
  const diff = diffSnapshots(prev, next);
38
- expect(rebuildTablesFromDiff(diff)).toEqual(["read_a", "read_b"]);
40
+ expect(rebuildTablesFromDiff(diff)).toEqual(["read_b"]);
39
41
  });
40
42
 
41
43
  test("no schema change → empty", () => {
@@ -51,11 +53,19 @@ describe("rebuildTablesFromDiff", () => {
51
53
  expect(rebuildTablesFromDiff(diffSnapshots(prev, next))).toEqual([]);
52
54
  });
53
55
 
54
- test("managed new nullable column → rebuild (Backfill aus Events nötig)", () => {
56
+ test("managed new NULLABLE column → NO rebuild (in-place ADD COLUMN reicht; Rebuild würde die Spalte wischen, #835)", () => {
55
57
  const prev = snapshotFromMetas([meta("read_a")]);
56
58
  const next = snapshotFromMetas([
57
59
  meta("read_a", { name: "title", pgType: "text", notNull: false }),
58
60
  ]);
61
+ expect(rebuildTablesFromDiff(diffSnapshots(prev, next))).toEqual([]);
62
+ });
63
+
64
+ test("managed new NOT-NULL column ohne Default → rebuild (recreate, kann nicht in-place)", () => {
65
+ const prev = snapshotFromMetas([meta("read_a")]);
66
+ const next = snapshotFromMetas([
67
+ meta("read_a", { name: "title", pgType: "text", notNull: true }),
68
+ ]);
59
69
  expect(rebuildTablesFromDiff(diffSnapshots(prev, next))).toEqual(["read_a"]);
60
70
  });
61
71