@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
@@ -4,6 +4,7 @@ import {
4
4
  createEntity,
5
5
  createRegistry,
6
6
  createTextField,
7
+ type DeleteContext,
7
8
  defineFeature,
8
9
  type PostSaveHookFn,
9
10
  type PreSaveHookFn,
@@ -201,74 +202,78 @@ describe("runPostSave", () => {
201
202
  test("postSave errors don't throw — logged and continued", async () => {
202
203
  const calls: string[] = [];
203
204
  const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
204
-
205
- const registry = makeRegistry({
206
- postSave: [
207
- async () => {
208
- throw new Error("feature-fail");
209
- },
210
- ],
211
- });
212
-
213
- const systemHooks: SystemHooks = {
214
- postSave: [
215
- {
216
- name: "search",
217
- priority: 1000,
218
- fn: async () => {
219
- calls.push("search-ran");
205
+ try {
206
+ const registry = makeRegistry({
207
+ postSave: [
208
+ async () => {
209
+ throw new Error("feature-fail");
220
210
  },
221
- },
222
- ],
223
- };
224
-
225
- const pipeline = createLifecycleHooks(registry, systemHooks);
226
- // Should not throw
227
- await pipeline.runPostSave("test:write:user", savectx, {});
211
+ ],
212
+ });
228
213
 
229
- // System hook still ran despite feature hook failure
230
- expect(calls).toContain("search-ran");
231
- expect(consoleSpy).toHaveBeenCalled();
232
- consoleSpy.mockRestore();
214
+ const systemHooks: SystemHooks = {
215
+ postSave: [
216
+ {
217
+ name: "search",
218
+ priority: 1000,
219
+ fn: async () => {
220
+ calls.push("search-ran");
221
+ },
222
+ },
223
+ ],
224
+ };
225
+
226
+ const pipeline = createLifecycleHooks(registry, systemHooks);
227
+ // Should not throw
228
+ await pipeline.runPostSave("test:write:user", savectx, {});
229
+
230
+ // System hook still ran despite feature hook failure
231
+ expect(calls).toContain("search-ran");
232
+ expect(consoleSpy).toHaveBeenCalled();
233
+ } finally {
234
+ consoleSpy.mockRestore();
235
+ }
233
236
  });
234
237
 
235
238
  test("system hook failure doesn't block other system hooks", async () => {
236
239
  const calls: string[] = [];
237
240
  const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
238
-
239
- const registry = makeRegistry();
240
-
241
- const systemHooks: SystemHooks = {
242
- postSave: [
243
- {
244
- name: "search",
245
- priority: 1000,
246
- fn: async () => {
247
- throw new Error("meili-down");
241
+ try {
242
+ const registry = makeRegistry();
243
+
244
+ const systemHooks: SystemHooks = {
245
+ postSave: [
246
+ {
247
+ name: "search",
248
+ priority: 1000,
249
+ fn: async () => {
250
+ throw new Error("meili-down");
251
+ },
248
252
  },
249
- },
250
- {
251
- name: "sse",
252
- priority: 1001,
253
- fn: async () => {
254
- calls.push("sse-ran");
253
+ {
254
+ name: "sse",
255
+ priority: 1001,
256
+ fn: async () => {
257
+ calls.push("sse-ran");
258
+ },
255
259
  },
256
- },
257
- {
258
- name: "audit",
259
- priority: 1002,
260
- fn: async () => {
261
- calls.push("audit-ran");
260
+ {
261
+ name: "audit",
262
+ priority: 1002,
263
+ fn: async () => {
264
+ calls.push("audit-ran");
265
+ },
262
266
  },
263
- },
264
- ],
265
- };
267
+ ],
268
+ };
266
269
 
267
- const pipeline = createLifecycleHooks(registry, systemHooks);
268
- await pipeline.runPostSave("test:write:user", savectx, {});
270
+ const pipeline = createLifecycleHooks(registry, systemHooks);
271
+ await pipeline.runPostSave("test:write:user", savectx, {});
269
272
 
270
- expect(calls).toEqual(["sse-ran", "audit-ran"]);
271
- consoleSpy.mockRestore();
273
+ expect(calls).toEqual(["sse-ran", "audit-ran"]);
274
+ } finally {
275
+ consoleSpy.mockRestore();
276
+ }
272
277
  });
273
278
  });
274
279
 
@@ -335,29 +340,32 @@ describe("runPostSave phase routing", () => {
335
340
 
336
341
  test("afterCommit phase: hook errors are logged, never thrown", async () => {
337
342
  const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
338
- const afterRan: string[] = [];
339
- const feature = defineFeature("phases", (r) => {
340
- r.entity("user", createEntity({ table: "Users", fields: {} }));
341
- r.writeHandler("user", z.object({}), async () => ({ isSuccess: true, data: null }), {
342
- access: { openToAll: true },
343
+ try {
344
+ const afterRan: string[] = [];
345
+ const feature = defineFeature("phases", (r) => {
346
+ r.entity("user", createEntity({ table: "Users", fields: {} }));
347
+ r.writeHandler("user", z.object({}), async () => ({ isSuccess: true, data: null }), {
348
+ access: { openToAll: true },
349
+ });
350
+ r.hook("postSave", "user", async () => {
351
+ throw new Error("afterCommit-boom");
352
+ });
353
+ r.hook("postSave", "user", async () => {
354
+ afterRan.push("second");
355
+ });
343
356
  });
344
- r.hook("postSave", "user", async () => {
345
- throw new Error("afterCommit-boom");
346
- });
347
- r.hook("postSave", "user", async () => {
348
- afterRan.push("second");
349
- });
350
- });
351
- const registry = createRegistry([feature]);
352
- const pipeline = createLifecycleHooks(registry);
357
+ const registry = createRegistry([feature]);
358
+ const pipeline = createLifecycleHooks(registry);
353
359
 
354
- // Must not throw — errors are swallowed + logged
355
- await pipeline.runPostSave("phases:write:user", savectx, {}, "afterCommit");
360
+ // Must not throw — errors are swallowed + logged
361
+ await pipeline.runPostSave("phases:write:user", savectx, {}, "afterCommit");
356
362
 
357
- // Subsequent hooks still fire (failure in one hook doesn't block the rest)
358
- expect(afterRan).toEqual(["second"]);
359
- expect(consoleSpy).toHaveBeenCalled();
360
- consoleSpy.mockRestore();
363
+ // Subsequent hooks still fire (failure in one hook doesn't block the rest)
364
+ expect(afterRan).toEqual(["second"]);
365
+ expect(consoleSpy).toHaveBeenCalled();
366
+ } finally {
367
+ consoleSpy.mockRestore();
368
+ }
361
369
  });
362
370
 
363
371
  test("system hooks respect their phase setting", async () => {
@@ -424,7 +432,7 @@ describe("Sprint 8a: per-tenant entity-hook filter", () => {
424
432
  });
425
433
 
426
434
  const featureB = defineFeature("feat-b", (r) => {
427
- r.entityHook("postSave", "widget", async (_result, ctx) => {
435
+ r.hook("postSave", { allOf: "widget" }, async (_result, ctx) => {
428
436
  calls.push({ tenant: ctx._tenantId ?? "no-tenant" });
429
437
  });
430
438
  });
@@ -528,3 +536,226 @@ describe("buildEventId — dedup key construction", () => {
528
536
  expect(buildEventId("handler", { id: 0 }, "phase")).toBeNull();
529
537
  });
530
538
  });
539
+
540
+ // --- PreDelete / PostDelete pipeline ---
541
+
542
+ const deletectx: DeleteContext = {
543
+ kind: "delete",
544
+ id: 1,
545
+ data: { email: "test@test.de" },
546
+ entityName: "user",
547
+ };
548
+
549
+ describe("runPreDelete", () => {
550
+ test("runs feature + entity + system hooks, all inTransaction (throw on error)", async () => {
551
+ const calls: string[] = [];
552
+ const feature = defineFeature("test", (r) => {
553
+ r.entity("user", createEntity({ table: "Users", fields: { email: createTextField() } }));
554
+ r.writeHandler("user", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
555
+ access: { openToAll: true },
556
+ });
557
+ r.hook("preDelete", "user", async () => {
558
+ calls.push("handler");
559
+ });
560
+ r.hook("preDelete", { allOf: "user" }, async () => {
561
+ calls.push("entity");
562
+ });
563
+ });
564
+ const registry = createRegistry([feature]);
565
+ const systemHooks: SystemHooks = {
566
+ preDelete: [
567
+ {
568
+ name: "sys",
569
+ priority: 1000,
570
+ fn: async () => {
571
+ calls.push("system");
572
+ },
573
+ },
574
+ ],
575
+ };
576
+ const pipeline = createLifecycleHooks(registry, systemHooks);
577
+ await pipeline.runPreDelete("test:write:user", deletectx, {});
578
+ expect(calls).toEqual(["handler", "entity", "system"]);
579
+ });
580
+
581
+ test("a hook throwing aborts the delete (rejects, not swallowed)", async () => {
582
+ const feature = defineFeature("test", (r) => {
583
+ r.entity("user", createEntity({ table: "Users", fields: { email: createTextField() } }));
584
+ r.writeHandler("user", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
585
+ access: { openToAll: true },
586
+ });
587
+ r.hook("preDelete", "user", async () => {
588
+ throw new Error("blocked-delete");
589
+ });
590
+ });
591
+ const registry = createRegistry([feature]);
592
+ const pipeline = createLifecycleHooks(registry);
593
+ await expect(pipeline.runPreDelete("test:write:user", deletectx, {})).rejects.toThrow(
594
+ "blocked-delete",
595
+ );
596
+ });
597
+
598
+ test("system hook with a non-inTransaction phase is skipped", async () => {
599
+ const registry = makeRegistry();
600
+ const calls: string[] = [];
601
+ const systemHooks: SystemHooks = {
602
+ preDelete: [
603
+ {
604
+ name: "sys",
605
+ priority: 1000,
606
+ phase: "afterCommit",
607
+ fn: async () => {
608
+ calls.push("system");
609
+ },
610
+ },
611
+ ],
612
+ };
613
+ const pipeline = createLifecycleHooks(registry, systemHooks);
614
+ await pipeline.runPreDelete("test:write:user", deletectx, {});
615
+ expect(calls).toEqual([]);
616
+ });
617
+ });
618
+
619
+ describe("runPostDelete", () => {
620
+ test("runs feature then system hooks (best-effort by default phase)", async () => {
621
+ const calls: string[] = [];
622
+ const feature = defineFeature("test", (r) => {
623
+ r.entity("user", createEntity({ table: "Users", fields: { email: createTextField() } }));
624
+ r.writeHandler("user", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
625
+ access: { openToAll: true },
626
+ });
627
+ r.hook("postDelete", "user", async () => {
628
+ calls.push("feature");
629
+ });
630
+ });
631
+ const registry = createRegistry([feature]);
632
+ const systemHooks: SystemHooks = {
633
+ postDelete: [
634
+ {
635
+ name: "sys",
636
+ priority: 1000,
637
+ fn: async () => {
638
+ calls.push("system");
639
+ },
640
+ },
641
+ ],
642
+ };
643
+ const pipeline = createLifecycleHooks(registry, systemHooks);
644
+ await pipeline.runPostDelete("test:write:user", deletectx, {});
645
+ expect(calls).toEqual(["feature", "system"]);
646
+ });
647
+
648
+ test("inTransaction phase: hook errors throw", async () => {
649
+ const feature = defineFeature("test", (r) => {
650
+ r.entity("user", createEntity({ table: "Users", fields: { email: createTextField() } }));
651
+ r.writeHandler("user", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
652
+ access: { openToAll: true },
653
+ });
654
+ r.hook(
655
+ "postDelete",
656
+ "user",
657
+ async () => {
658
+ throw new Error("postDelete-inTx-boom");
659
+ },
660
+ { phase: "inTransaction" },
661
+ );
662
+ });
663
+ const registry = createRegistry([feature]);
664
+ const pipeline = createLifecycleHooks(registry);
665
+ await expect(
666
+ pipeline.runPostDelete("test:write:user", deletectx, {}, "inTransaction"),
667
+ ).rejects.toThrow("postDelete-inTx-boom");
668
+ });
669
+ });
670
+
671
+ // --- Batch hooks ---
672
+
673
+ describe("runPostSaveBatch / runPostDeleteBatch", () => {
674
+ test("no batch hooks registered → resolves without throwing", async () => {
675
+ const pipeline = createLifecycleHooks(makeRegistry());
676
+ await expect(pipeline.runPostSaveBatch([savectx], {})).resolves.toBeUndefined();
677
+ await expect(pipeline.runPostDeleteBatch([deletectx], {})).resolves.toBeUndefined();
678
+ // Should not throw — nothing registered.
679
+ });
680
+
681
+ test("runPostSaveBatch runs all system hooks concurrently with the batch payload", async () => {
682
+ const seen: (readonly SaveContext[])[] = [];
683
+ const systemHooks: SystemHooks = {
684
+ postSaveBatch: [
685
+ {
686
+ name: "a",
687
+ priority: 1000,
688
+ fn: async (results) => {
689
+ seen.push(results);
690
+ },
691
+ },
692
+ ],
693
+ };
694
+ const pipeline = createLifecycleHooks(makeRegistry(), systemHooks);
695
+ await pipeline.runPostSaveBatch([savectx], {});
696
+ expect(seen).toEqual([[savectx]]);
697
+ });
698
+
699
+ test("runPostDeleteBatch runs all system hooks with the batch payload", async () => {
700
+ const seen: (readonly DeleteContext[])[] = [];
701
+ const systemHooks: SystemHooks = {
702
+ postDeleteBatch: [
703
+ {
704
+ name: "a",
705
+ priority: 1000,
706
+ fn: async (payloads) => {
707
+ seen.push(payloads);
708
+ },
709
+ },
710
+ ],
711
+ };
712
+ const pipeline = createLifecycleHooks(makeRegistry(), systemHooks);
713
+ await pipeline.runPostDeleteBatch([deletectx], {});
714
+ expect(seen).toEqual([[deletectx]]);
715
+ });
716
+
717
+ test.each([
718
+ [
719
+ "postSaveBatch",
720
+ (hooks: { name: string; priority: number; fn: () => Promise<void> }[]) =>
721
+ ({ postSaveBatch: hooks }) satisfies SystemHooks,
722
+ (pipeline: ReturnType<typeof createLifecycleHooks>) =>
723
+ pipeline.runPostSaveBatch([savectx], {}),
724
+ ],
725
+ [
726
+ "postDeleteBatch",
727
+ (hooks: { name: string; priority: number; fn: () => Promise<void> }[]) =>
728
+ ({ postDeleteBatch: hooks }) satisfies SystemHooks,
729
+ (pipeline: ReturnType<typeof createLifecycleHooks>) =>
730
+ pipeline.runPostDeleteBatch([deletectx], {}),
731
+ ],
732
+ ])("one %s hook throwing doesn't stop the others (Promise.allSettled) — logged, never thrown", async (_name, buildHooks, run) => {
733
+ const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
734
+ try {
735
+ const calls: string[] = [];
736
+ const systemHooks = buildHooks([
737
+ {
738
+ name: "failing",
739
+ priority: 1000,
740
+ fn: async () => {
741
+ throw new Error("batch-hook-boom");
742
+ },
743
+ },
744
+ {
745
+ name: "ok",
746
+ priority: 1001,
747
+ fn: async () => {
748
+ calls.push("ok-ran");
749
+ },
750
+ },
751
+ ]);
752
+ const pipeline = createLifecycleHooks(makeRegistry(), systemHooks);
753
+ // Must not throw.
754
+ await run(pipeline);
755
+ expect(calls).toEqual(["ok-ran"]);
756
+ expect(consoleSpy).toHaveBeenCalled();
757
+ } finally {
758
+ consoleSpy.mockRestore();
759
+ }
760
+ });
761
+ });
@@ -45,15 +45,23 @@ const asOfFeature = defineFeature("asoftest", (r) => {
45
45
  const approved = r.defineEvent(
46
46
  "approved",
47
47
  z.object({ amount: z.number().int(), approvedBy: z.string() }),
48
- { version: 2 },
48
+ {
49
+ version: 2,
50
+ migrations: [
51
+ {
52
+ fromVersion: 1,
53
+ toVersion: 2,
54
+ transform: (payload) => {
55
+ const p = payload as { amount: string; approvedBy: string };
56
+ return {
57
+ amount: Math.round(Number.parseFloat(p.amount) * 100),
58
+ approvedBy: p.approvedBy,
59
+ };
60
+ },
61
+ },
62
+ ],
63
+ },
49
64
  );
50
- r.eventMigration("approved", 1, 2, (payload) => {
51
- const p = payload as { amount: string; approvedBy: string };
52
- return {
53
- amount: Math.round(Number.parseFloat(p.amount) * 100),
54
- approvedBy: p.approvedBy,
55
- };
56
- });
57
65
 
58
66
  const executor = createEventStoreExecutor(invoiceTable, invoiceEntity, {
59
67
  entityName: "asof-invoice",
@@ -65,7 +65,7 @@ const postQueryFeature = defineFeature("postquerytest", (r) => {
65
65
  r.hook("postQuery", "widget:list", handlerKeyedHook);
66
66
 
67
67
  // Entity-keyed: fires for ALL query-handlers of widget-entity
68
- r.entityHook("postQuery", widget, entityKeyedHook);
68
+ r.hook("postQuery", { allOf: widget }, entityKeyedHook);
69
69
  });
70
70
 
71
71
  // --- Single-object-result invariant fixtures ---
@@ -93,13 +93,13 @@ const singleObjectFeature = defineFeature("singleobjtest", (r) => {
93
93
  r.queryHandler("gadget:get", z.object({}), async () => ({ id: "g1", name: "Gadget" }), {
94
94
  access: { openToAll: true },
95
95
  });
96
- r.entityHook("postQuery", gadget, dropRowHook);
96
+ r.hook("postQuery", { allOf: gadget }, dropRowHook);
97
97
 
98
98
  const gizmo = r.entity("gizmo", gizmoEntity);
99
99
  r.queryHandler("gizmo:get", z.object({}), async () => ({ id: "z1", name: "Gizmo" }), {
100
100
  access: { openToAll: true },
101
101
  });
102
- r.entityHook("postQuery", gizmo, duplicateRowHook);
102
+ r.hook("postQuery", { allOf: gizmo }, duplicateRowHook);
103
103
  });
104
104
 
105
105
  // --- Test stack ---
@@ -26,7 +26,7 @@ import {
26
26
  defineFeature,
27
27
  } from "../../engine";
28
28
  import type { ProjectionDefinition } from "../../engine/types";
29
- import { createEventsTable } from "../../event-store";
29
+ import { archiveStream, createEventsTable } from "../../event-store";
30
30
  import {
31
31
  createProjectionStateTable,
32
32
  getAllProjectionProgress,
@@ -116,7 +116,7 @@ afterAll(async () => {
116
116
 
117
117
  beforeEach(async () => {
118
118
  await asRawClient(testDb.db).unsafe(
119
- `TRUNCATE kumiko_events, read_rebuild_items, read_rebuild_items_per_group, kumiko_projections RESTART IDENTITY CASCADE`,
119
+ `TRUNCATE kumiko_events, read_rebuild_items, read_rebuild_items_per_group, kumiko_projections, kumiko_archived_streams RESTART IDENTITY CASCADE`,
120
120
  );
121
121
  });
122
122
 
@@ -210,6 +210,36 @@ describe("rebuildProjection — happy path", () => {
210
210
  expect(result.eventsProcessed).toBe(0);
211
211
  expect(result.lastProcessedEventId).toBe(0n);
212
212
  });
213
+
214
+ test("archived streams don't replay (fw#832) — stranded aggregate heals via archiveStream", async () => {
215
+ // Heilungs-Muster fuer eventlose Read-Side-Deletes im Bestand: ein
216
+ // Aggregat, dessen Live-Row (vor dem fw#832-Fix) read-side entfernt
217
+ // wurde, hinterlaesst ein created-Event ohne deleted — jeder Rebuild
218
+ // wuerde die Row resurrecten (bzw. an unique-Indexen kollidieren).
219
+ // archiveStream friert den Stream ein UND nimmt ihn aus dem Replay.
220
+ const group = "00000000-0000-4000-8000-000000000020";
221
+ const stranded = await executor.create({ groupId: group, name: "stranded" }, admin, tdb);
222
+ if (!stranded.isSuccess) throw new Error("create failed");
223
+ await appendCreatedEvent(group, "survivor");
224
+
225
+ await archiveStream(testDb.db, {
226
+ tenantId: admin.tenantId,
227
+ aggregateId: String(stranded.data.id),
228
+ aggregateType: "rebuild-item",
229
+ archivedBy: "fw832-heal-test",
230
+ });
231
+
232
+ const result = await rebuildProjection(qualifiedProjectionName, {
233
+ db: testDb.db,
234
+ registry,
235
+ });
236
+
237
+ // Nur das survivor-Event replayed; das archivierte Aggregat traegt
238
+ // nicht mehr bei — auch nicht im #443-ground-truth-count (sonst
239
+ // wuerde der Recompute-Pfad endlos nachziehen).
240
+ expect(result.eventsProcessed).toBe(1);
241
+ expect(await getCount(group)).toBe(1);
242
+ });
213
243
  });
214
244
 
215
245
  describe("rebuildProjection — state table lifecycle", () => {
@@ -860,3 +890,51 @@ describe("rebuildProjection — live-tail catch-up (#363 Phase 2)", () => {
860
890
  await asRawClient(testDb.db).unsafe(`DROP TABLE IF EXISTS "cutover_probe"`);
861
891
  });
862
892
  });
893
+
894
+ describe("rebuildProjection — stale-registry column fence (#835)", () => {
895
+ // Simulates the rolling-deploy race: a migration added a column to the live
896
+ // table, but the pod running the rebuild still carries the old registry
897
+ // (EntityTableMeta without that column). The rebuild must abort instead of
898
+ // swapping in a shadow that silently drops the migrated column (#494 class).
899
+ test("aborts when the live table has a column the registry meta lacks — data survives", async () => {
900
+ const group = "00000000-0000-4000-8000-000000000041";
901
+ await appendCreatedEvent(group, "item1");
902
+ await rebuildProjection(qualifiedProjectionName, { db: testDb.db, registry });
903
+ expect(await getCount(group)).toBe(1);
904
+
905
+ const raw = asRawClient(testDb.db);
906
+ await raw.unsafe(`ALTER TABLE "read_rebuild_items_per_group" ADD COLUMN migrated_col text`);
907
+ try {
908
+ await raw.unsafe(
909
+ `UPDATE "read_rebuild_items_per_group" SET migrated_col = 'must-survive' WHERE group_id = $1::uuid`,
910
+ [group],
911
+ );
912
+
913
+ await expect(
914
+ rebuildProjection(qualifiedProjectionName, { db: testDb.db, registry }),
915
+ ).rejects.toThrow(/live-only: migrated_col/);
916
+
917
+ // Live table untouched: column AND its data still there.
918
+ const [row] = await raw.unsafe<{ migrated_col: string | null }>(
919
+ `SELECT migrated_col FROM "read_rebuild_items_per_group" WHERE group_id = $1::uuid`,
920
+ [group],
921
+ );
922
+ expect(row?.migrated_col).toBe("must-survive");
923
+ expect(await getCount(group)).toBe(1);
924
+
925
+ // Failure is visible to ops.
926
+ const state = await getProjectionState(testDb.db, qualifiedProjectionName);
927
+ expect(state?.status).toBe("failed");
928
+ expect(state?.lastError).toContain("migrated_col");
929
+ } finally {
930
+ await raw.unsafe(
931
+ `ALTER TABLE "read_rebuild_items_per_group" DROP COLUMN IF EXISTS migrated_col`,
932
+ );
933
+ }
934
+
935
+ // Column sets aligned again → rebuild works.
936
+ const result = await rebuildProjection(qualifiedProjectionName, { db: testDb.db, registry });
937
+ expect(result.eventsProcessed).toBe(1);
938
+ expect(await getCount(group)).toBe(1);
939
+ });
940
+ });