@cosmicdrift/kumiko-framework 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (424) hide show
  1. package/README.md +9 -38
  2. package/package.json +21 -2
  3. package/src/__tests__/anonymous-access.integration.test.ts +185 -0
  4. package/src/__tests__/consumer-cli.integration.test.ts +172 -0
  5. package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
  6. package/src/__tests__/full-stack.integration.test.ts +1 -1
  7. package/src/__tests__/schema-cli-temporal-polyfill.test.ts +48 -0
  8. package/src/__tests__/schema-cli.integration.test.ts +50 -0
  9. package/src/__tests__/store-table.integration.test.ts +93 -0
  10. package/src/api/__tests__/api.test.ts +373 -0
  11. package/src/api/__tests__/auth-middleware-anonymous-access-boot.test.ts +40 -0
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
  13. package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +253 -0
  14. package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +222 -0
  15. package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +249 -0
  16. package/src/api/__tests__/auth-routes-mfa-verify.test.ts +202 -0
  17. package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +135 -0
  18. package/src/api/__tests__/batch.integration.test.ts +101 -11
  19. package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
  20. package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
  21. package/src/api/__tests__/jwt.test.ts +200 -1
  22. package/src/api/__tests__/login-rate-limiter-sweep.test.ts +50 -0
  23. package/src/api/__tests__/pat-scope.test.ts +36 -0
  24. package/src/api/__tests__/pii-leak-guard.integration.test.ts +103 -0
  25. package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +138 -0
  26. package/src/api/__tests__/request-id-middleware.test.ts +51 -0
  27. package/src/api/__tests__/server-boot-guards.test.ts +71 -0
  28. package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
  29. package/src/api/__tests__/sse-broker.test.ts +57 -0
  30. package/src/api/__tests__/sse-route.test.ts +4 -0
  31. package/src/api/api-constants.ts +11 -0
  32. package/src/api/auth-middleware.ts +248 -40
  33. package/src/api/auth-routes.ts +576 -95
  34. package/src/api/index.ts +13 -4
  35. package/src/api/jwt.ts +170 -11
  36. package/src/api/pat-scope.ts +14 -0
  37. package/src/api/pii-leak-guard.ts +47 -0
  38. package/src/api/request-context.ts +3 -0
  39. package/src/api/request-id-middleware.ts +2 -0
  40. package/src/api/routes.ts +169 -2
  41. package/src/api/server.ts +112 -16
  42. package/src/api/sse-broker.ts +39 -0
  43. package/src/bun-db/__tests__/PATTERN.md +0 -1
  44. package/src/bun-db/__tests__/coerce-row-temporal.test.ts +41 -0
  45. package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
  46. package/src/bun-db/__tests__/write-brand.test.ts +21 -0
  47. package/src/bun-db/connection.ts +3 -3
  48. package/src/bun-db/index.ts +2 -0
  49. package/src/bun-db/query.ts +105 -32
  50. package/src/consumer-cli.ts +134 -0
  51. package/src/crypto/__tests__/blind-index.test.ts +130 -0
  52. package/src/crypto/__tests__/event-pii.test.ts +161 -0
  53. package/src/crypto/__tests__/kek-rotation.integration.test.ts +180 -0
  54. package/src/crypto/__tests__/kms-adapter-contract.ts +134 -0
  55. package/src/crypto/__tests__/kms-adapter.contract.test.ts +4 -0
  56. package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +117 -0
  57. package/src/crypto/__tests__/pii-field-encryption.test.ts +376 -0
  58. package/src/crypto/__tests__/request-kms-cache.test.ts +74 -0
  59. package/src/crypto/__tests__/subject-resolver.test.ts +108 -0
  60. package/src/crypto/blind-index.ts +118 -0
  61. package/src/crypto/event-pii.ts +70 -0
  62. package/src/crypto/in-memory-kms-adapter.ts +50 -0
  63. package/src/crypto/index.ts +67 -0
  64. package/src/crypto/kms-adapter.ts +2 -0
  65. package/src/crypto/pg-kms-adapter.ts +295 -0
  66. package/src/crypto/pii-field-encryption.ts +248 -0
  67. package/src/crypto/request-kms-cache.ts +38 -0
  68. package/src/crypto/subject-resolver.ts +91 -0
  69. package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +191 -0
  70. package/src/db/__tests__/blind-index.integration.test.ts +248 -0
  71. package/src/db/__tests__/build-filter-where.test.ts +34 -0
  72. package/src/db/__tests__/collect-table-metas.test.ts +10 -10
  73. package/src/db/__tests__/config-seed.integration.test.ts +13 -5
  74. package/src/db/__tests__/dialect-instant.test.ts +1 -4
  75. package/src/db/__tests__/entity-field-encryption.test.ts +7 -7
  76. package/src/db/__tests__/entity-table-meta-source.test.ts +50 -0
  77. package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
  78. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +402 -0
  79. package/src/db/__tests__/event-store-executor.integration.test.ts +299 -29
  80. package/src/db/__tests__/feature-table-sources.test.ts +34 -0
  81. package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +118 -39
  82. package/src/db/__tests__/instant-to-driver-temporal.test.ts +21 -0
  83. package/src/db/__tests__/located-timestamp.test.ts +19 -0
  84. package/src/db/__tests__/migrate-generator.test.ts +21 -0
  85. package/src/db/__tests__/migrate-runner.test.ts +61 -0
  86. package/src/db/__tests__/number-field-fractional.integration.test.ts +58 -0
  87. package/src/db/__tests__/rebuild-marker.test.ts +13 -3
  88. package/src/db/__tests__/replay-migration-sql.test.ts +384 -0
  89. package/src/db/__tests__/schema-inspection.test.ts +7 -0
  90. package/src/db/__tests__/schema-migration.integration.test.ts +1 -1
  91. package/src/db/__tests__/table-builder-meta-lockstep.test.ts +39 -0
  92. package/src/db/__tests__/tenant-db-where-merge.test.ts +49 -3
  93. package/src/db/__tests__/tenant-db.integration.test.ts +6 -2
  94. package/src/db/api.ts +2 -2
  95. package/src/db/apply-entity-event.ts +18 -14
  96. package/src/db/blind-index-cleanup.ts +55 -0
  97. package/src/db/bun-provider.ts +2 -2
  98. package/src/db/collect-table-metas.ts +6 -7
  99. package/src/db/config-seed.ts +9 -9
  100. package/src/db/connection.ts +7 -12
  101. package/src/db/cursor.ts +1 -18
  102. package/src/db/dialect.ts +20 -26
  103. package/src/db/entity-field-encryption.ts +81 -24
  104. package/src/db/entity-table-meta-types.ts +2 -0
  105. package/src/db/entity-table-meta.ts +63 -90
  106. package/src/db/event-store-executor-context.ts +404 -0
  107. package/src/db/event-store-executor-read.ts +275 -0
  108. package/src/db/event-store-executor-write.ts +644 -0
  109. package/src/db/event-store-executor.ts +28 -1155
  110. package/src/db/feature-table-sources.ts +6 -5
  111. package/src/db/index.ts +20 -3
  112. package/src/db/located-timestamp.ts +4 -0
  113. package/src/db/migrate-generator.ts +39 -6
  114. package/src/db/migrate-runner.ts +107 -11
  115. package/src/db/pg-error.ts +9 -1
  116. package/src/db/postgres-provider.ts +2 -2
  117. package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +80 -0
  118. package/src/db/queries/backfill-pii.ts +277 -0
  119. package/src/db/queries/ddl.ts +45 -0
  120. package/src/db/queries/event-consumer.ts +35 -2
  121. package/src/db/queries/event-store.ts +126 -19
  122. package/src/db/queries/projection-rebuild.ts +22 -8
  123. package/src/db/queries/shadow-swap.ts +183 -0
  124. package/src/db/queries/test-stack.ts +4 -30
  125. package/src/db/query-api.ts +1 -0
  126. package/src/db/query.ts +1 -0
  127. package/src/db/rebuild-marker.ts +18 -2
  128. package/src/db/reference-data.ts +2 -3
  129. package/src/db/replay-migration-sql.ts +257 -0
  130. package/src/db/schema-inspection.ts +1 -1
  131. package/src/db/table-builder.ts +102 -71
  132. package/src/db/tenant-db.ts +15 -53
  133. package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
  134. package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
  135. package/src/engine/__tests__/boot-validator-dashboard.test.ts +237 -0
  136. package/src/engine/__tests__/boot-validator-entity-list.test.ts +104 -0
  137. package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -69
  138. package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +26 -5
  139. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +3 -19
  140. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +246 -4
  141. package/src/engine/__tests__/boot-validator.test.ts +219 -15
  142. package/src/engine/__tests__/build-app-schema.test.ts +82 -0
  143. package/src/engine/__tests__/build-config-feature-schema.test.ts +125 -0
  144. package/src/engine/__tests__/build-target.test.ts +3 -11
  145. package/src/engine/__tests__/codemod-pipeline.test.ts +152 -21
  146. package/src/engine/__tests__/config-helpers.test.ts +16 -0
  147. package/src/engine/__tests__/define-feature-entity-mapping.test.ts +6 -0
  148. package/src/engine/__tests__/define-roles.test.ts +21 -0
  149. package/src/engine/__tests__/engine.test.ts +163 -1
  150. package/src/engine/__tests__/entity-handlers.test.ts +80 -0
  151. package/src/engine/__tests__/event-migration-declarative.test.ts +65 -0
  152. package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
  153. package/src/engine/__tests__/extend-entity-projection.test.ts +123 -0
  154. package/src/engine/__tests__/factories-time.test.ts +2 -66
  155. package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
  156. package/src/engine/__tests__/feature-manifest.test.ts +31 -1
  157. package/src/engine/__tests__/field-access.test.ts +23 -1
  158. package/src/engine/__tests__/hook-phases.test.ts +5 -5
  159. package/src/engine/__tests__/membership-roles.test.ts +4 -10
  160. package/src/engine/__tests__/nav.test.ts +86 -1
  161. package/src/engine/__tests__/pipeline-engine.test.ts +9 -9
  162. package/src/engine/__tests__/pipeline-handler.integration.test.ts +18 -18
  163. package/src/engine/__tests__/pipeline-observability.integration.test.ts +2 -2
  164. package/src/engine/__tests__/pipeline-performance.integration.test.ts +3 -3
  165. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +9 -9
  166. package/src/engine/__tests__/post-query-hook.test.ts +7 -7
  167. package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
  168. package/src/engine/__tests__/registry.test.ts +144 -0
  169. package/src/engine/__tests__/schema-builder.test.ts +18 -0
  170. package/src/engine/__tests__/screen.test.ts +147 -1
  171. package/src/engine/__tests__/soft-delete-cleanup.test.ts +3 -0
  172. package/src/engine/__tests__/store-table.test.ts +229 -0
  173. package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
  174. package/src/engine/__tests__/{visual-tree-patterns.test.ts → tree-actions-patterns.test.ts} +7 -95
  175. package/src/engine/__tests__/validate-projection-allowlist.test.ts +15 -15
  176. package/src/engine/boot-validator/__tests__/config-deps.test.ts +92 -0
  177. package/src/engine/boot-validator/action-wiring.ts +149 -0
  178. package/src/engine/boot-validator/boot-check.ts +21 -0
  179. package/src/engine/boot-validator/config-deps.ts +40 -4
  180. package/src/engine/boot-validator/entity-handler.ts +20 -21
  181. package/src/engine/boot-validator/entity-list-screens.ts +88 -0
  182. package/src/engine/boot-validator/gdpr-storage.ts +0 -20
  183. package/src/engine/boot-validator/i18n-keys.ts +58 -4
  184. package/src/engine/boot-validator/index.ts +33 -12
  185. package/src/engine/boot-validator/nav.ts +125 -0
  186. package/src/engine/boot-validator/pii-retention.ts +95 -10
  187. package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +214 -191
  188. package/src/engine/boot-validator/workspaces.ts +68 -0
  189. package/src/engine/build-app-schema.ts +16 -0
  190. package/src/engine/build-config-feature-schema.ts +44 -7
  191. package/src/engine/codemod/pipeline-codemod.ts +5 -5
  192. package/src/engine/config-helpers.ts +15 -0
  193. package/src/engine/constants.ts +32 -6
  194. package/src/engine/create-app.ts +11 -0
  195. package/src/engine/define-feature.ts +95 -947
  196. package/src/engine/define-handler.ts +28 -94
  197. package/src/engine/define-workflow.ts +1 -1
  198. package/src/engine/effective-features.ts +12 -2
  199. package/src/engine/entity-handlers.ts +76 -11
  200. package/src/engine/extensions/tenant-data.ts +19 -0
  201. package/src/engine/extensions/user-data.ts +29 -2
  202. package/src/engine/factories.ts +8 -49
  203. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +26 -29
  204. package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
  205. package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
  206. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
  207. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
  208. package/src/engine/feature-ast/__tests__/parse.test.ts +1640 -160
  209. package/src/engine/feature-ast/__tests__/patch.test.ts +206 -12
  210. package/src/engine/feature-ast/__tests__/patcher.test.ts +20 -23
  211. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +380 -5
  212. package/src/engine/feature-ast/extractors/events.ts +322 -0
  213. package/src/engine/feature-ast/extractors/handlers.ts +234 -0
  214. package/src/engine/feature-ast/extractors/hooks.ts +243 -0
  215. package/src/engine/feature-ast/extractors/index.ts +34 -31
  216. package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
  217. package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
  218. package/src/engine/feature-ast/extractors/round1.ts +3 -3
  219. package/src/engine/feature-ast/extractors/round5.ts +3 -3
  220. package/src/engine/feature-ast/extractors/round6.ts +2 -36
  221. package/src/engine/feature-ast/extractors/shared.ts +64 -6
  222. package/src/engine/feature-ast/index.ts +2 -4
  223. package/src/engine/feature-ast/parse.ts +130 -23
  224. package/src/engine/feature-ast/patch.ts +39 -50
  225. package/src/engine/feature-ast/patcher.ts +37 -38
  226. package/src/engine/feature-ast/patterns.ts +50 -62
  227. package/src/engine/feature-ast/render.ts +67 -44
  228. package/src/engine/feature-builder-state.ts +168 -0
  229. package/src/engine/feature-config-events-jobs.ts +403 -0
  230. package/src/engine/feature-entity-handlers.ts +208 -0
  231. package/src/engine/feature-manifest.ts +2 -1
  232. package/src/engine/feature-ui-extensions.ts +500 -0
  233. package/src/engine/field-access.ts +13 -2
  234. package/src/engine/field-helpers.ts +31 -0
  235. package/src/engine/handler-helpers.ts +26 -0
  236. package/src/engine/hook-helpers.ts +16 -0
  237. package/src/engine/index.ts +23 -7
  238. package/src/engine/membership-roles.ts +13 -0
  239. package/src/engine/object-form.ts +27 -0
  240. package/src/engine/ownership.ts +26 -79
  241. package/src/engine/pattern-library/__tests__/library.test.ts +7 -20
  242. package/src/engine/pattern-library/library.ts +44 -1152
  243. package/src/engine/pattern-library/mixed-schemas.ts +450 -0
  244. package/src/engine/pattern-library/opaque-schemas.ts +124 -0
  245. package/src/engine/pattern-library/shared-fields.ts +75 -0
  246. package/src/engine/pattern-library/static-schemas.ts +456 -0
  247. package/src/engine/pipeline.ts +6 -11
  248. package/src/engine/registry-facade.ts +362 -0
  249. package/src/engine/registry-ingest.ts +478 -0
  250. package/src/engine/registry-state.ts +388 -0
  251. package/src/engine/registry-validate.ts +642 -0
  252. package/src/engine/registry.ts +78 -1658
  253. package/src/engine/run-pipeline.ts +1 -1
  254. package/src/engine/schema-builder.ts +1 -0
  255. package/src/engine/screen-helpers.ts +54 -0
  256. package/src/engine/soft-delete-cleanup.ts +6 -2
  257. package/src/engine/steps/__tests__/duration-utils.test.ts +20 -0
  258. package/src/engine/steps/_duration-utils.ts +2 -0
  259. package/src/engine/steps/unsafe-projection-upsert.ts +1 -4
  260. package/src/engine/tier-resolver-extension.ts +3 -2
  261. package/src/engine/types/config.ts +2 -482
  262. package/src/engine/types/define-handler.ts +2 -0
  263. package/src/engine/types/entity-handlers.ts +2 -0
  264. package/src/engine/types/event-type-map.ts +1 -37
  265. package/src/engine/types/feature.ts +2 -972
  266. package/src/engine/types/fields.ts +2 -675
  267. package/src/engine/types/handlers.ts +2 -774
  268. package/src/engine/types/hooks.ts +2 -184
  269. package/src/engine/types/http-route.ts +1 -54
  270. package/src/engine/types/identifiers.ts +1 -47
  271. package/src/engine/types/index.ts +86 -40
  272. package/src/engine/types/nav.ts +2 -63
  273. package/src/engine/types/ownership.ts +2 -0
  274. package/src/engine/types/projection.ts +2 -138
  275. package/src/engine/types/relations.ts +1 -51
  276. package/src/engine/types/screen.ts +2 -574
  277. package/src/engine/types/step.ts +2 -334
  278. package/src/engine/types/target-ref.ts +1 -21
  279. package/src/engine/types/tree-node.ts +1 -132
  280. package/src/engine/types/workspace.ts +2 -49
  281. package/src/engine/validate-projection-allowlist.ts +6 -6
  282. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +120 -1
  283. package/src/entrypoint/index.ts +49 -6
  284. package/src/errors/classes.ts +22 -1
  285. package/src/errors/field-issue.ts +0 -3
  286. package/src/errors/index.ts +1 -1
  287. package/src/errors/write-error-info.ts +10 -22
  288. package/src/es-ops/README.md +1 -1
  289. package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
  290. package/src/es-ops/context.ts +4 -2
  291. package/src/es-ops/types.ts +8 -1
  292. package/src/event-store/__tests__/admin-api.integration.test.ts +27 -1
  293. package/src/event-store/__tests__/backfill-pii.integration.test.ts +279 -0
  294. package/src/event-store/__tests__/event-store.integration.test.ts +201 -0
  295. package/src/event-store/__tests__/row-to-stored-event.test.ts +2 -2
  296. package/src/event-store/__tests__/snapshot.integration.test.ts +86 -0
  297. package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +58 -0
  298. package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
  299. package/src/event-store/admin-api.ts +11 -4
  300. package/src/event-store/errors.ts +2 -35
  301. package/src/event-store/event-store.ts +64 -78
  302. package/src/event-store/events-schema.ts +11 -13
  303. package/src/event-store/index.ts +18 -3
  304. package/src/event-store/rebuild-dead-letter.ts +111 -0
  305. package/src/event-store/snapshot.ts +63 -40
  306. package/src/event-store/types.ts +2 -0
  307. package/src/files/__tests__/build-storage-key.test.ts +28 -0
  308. package/src/files/__tests__/file-ref-entity.test.ts +8 -0
  309. package/src/files/__tests__/files.integration.test.ts +24 -0
  310. package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
  311. package/src/files/__tests__/local-provider.test.ts +31 -0
  312. package/src/files/__tests__/provider-resolver.test.ts +70 -0
  313. package/src/files/__tests__/write-stream.test.ts +13 -0
  314. package/src/files/file-handle.ts +2 -19
  315. package/src/files/file-ref-entity.ts +2 -2
  316. package/src/files/file-routes.ts +13 -0
  317. package/src/files/index.ts +1 -1
  318. package/src/files/local-provider.ts +27 -8
  319. package/src/files/provider-resolver.ts +31 -20
  320. package/src/files/types.ts +13 -55
  321. package/src/i18n/__tests__/required-surface-keys.test.ts +17 -0
  322. package/src/i18n/required-surface-keys.ts +120 -1
  323. package/src/jobs/__tests__/job-queue-depth.integration.test.ts +82 -0
  324. package/src/jobs/__tests__/jobs.integration.test.ts +329 -3
  325. package/src/jobs/job-runner.ts +82 -12
  326. package/src/logging/types.ts +1 -7
  327. package/src/migrations/pending-rebuilds.ts +1 -1
  328. package/src/observability/__tests__/observability.integration.test.ts +4 -1
  329. package/src/observability/__tests__/recording-tracer.test.ts +6 -4
  330. package/src/observability/index.ts +2 -0
  331. package/src/observability/noop-provider.ts +0 -9
  332. package/src/observability/recording-tracer.ts +0 -12
  333. package/src/observability/standard-metrics.ts +64 -2
  334. package/src/observability/types/index.ts +1 -29
  335. package/src/observability/types/metric.ts +1 -56
  336. package/src/observability/types/provider.ts +1 -32
  337. package/src/observability/types/span.ts +1 -64
  338. package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
  339. package/src/pipeline/__tests__/dispatcher.test.ts +366 -1
  340. package/src/pipeline/__tests__/event-consumer-state.integration.test.ts +31 -0
  341. package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
  342. package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +263 -0
  343. package/src/pipeline/__tests__/event-dispatcher.integration.test.ts +10 -0
  344. package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +106 -0
  345. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +308 -77
  346. package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
  347. package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
  348. package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +80 -2
  349. package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +274 -0
  350. package/src/pipeline/__tests__/try-append-event.integration.test.ts +166 -0
  351. package/src/pipeline/dispatch-batch.ts +187 -0
  352. package/src/pipeline/dispatch-query.ts +165 -0
  353. package/src/pipeline/dispatch-shared.ts +826 -0
  354. package/src/pipeline/dispatch-stream.ts +90 -0
  355. package/src/pipeline/dispatch-write.ts +451 -0
  356. package/src/pipeline/dispatcher-utils.ts +1 -1
  357. package/src/pipeline/dispatcher.ts +44 -1372
  358. package/src/pipeline/entity-cache.ts +2 -33
  359. package/src/pipeline/event-consumer-state.ts +30 -2
  360. package/src/pipeline/event-dispatcher-admin.ts +293 -0
  361. package/src/pipeline/event-dispatcher-delivery.ts +305 -0
  362. package/src/pipeline/event-dispatcher.ts +85 -542
  363. package/src/pipeline/index.ts +2 -0
  364. package/src/pipeline/msp-rebuild.ts +42 -3
  365. package/src/pipeline/multi-stream-apply-context.ts +4 -42
  366. package/src/pipeline/projection-rebuild.ts +105 -3
  367. package/src/pipeline/system-hooks.ts +144 -1
  368. package/src/rate-limit/__tests__/resolver.integration.test.ts +18 -0
  369. package/src/rate-limit/resolver.ts +16 -32
  370. package/src/schema-cli.ts +76 -16
  371. package/src/search/__tests__/meilisearch-adapter.integration.test.ts +207 -185
  372. package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
  373. package/src/search/__tests__/reindex-entity.integration.test.ts +144 -0
  374. package/src/search/index.ts +6 -0
  375. package/src/search/meilisearch-adapter.ts +13 -12
  376. package/src/search/reindex-entity.ts +177 -0
  377. package/src/search/types.ts +1 -39
  378. package/src/secrets/__tests__/contains-secret.test.ts +34 -0
  379. package/src/secrets/__tests__/envelope-cipher.test.ts +59 -0
  380. package/src/secrets/__tests__/envelope.test.ts +1 -1
  381. package/src/secrets/dek-cache.ts +26 -5
  382. package/src/secrets/envelope-cipher.ts +53 -0
  383. package/src/secrets/envelope.ts +5 -3
  384. package/src/secrets/index.ts +13 -1
  385. package/src/secrets/stored-envelope.ts +46 -0
  386. package/src/secrets/types.ts +2 -162
  387. package/src/stack/__tests__/event-collector.test.ts +42 -0
  388. package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +125 -0
  389. package/src/stack/db.ts +2 -1
  390. package/src/stack/push-entity-projection-tables.ts +4 -3
  391. package/src/stack/redis.ts +8 -0
  392. package/src/stack/request-helper.ts +38 -2
  393. package/src/stack/table-helpers.ts +6 -4
  394. package/src/stack/test-stack.ts +249 -131
  395. package/src/testing/__tests__/late-bound.test.ts +32 -0
  396. package/src/testing/__tests__/wait-for.test.ts +59 -0
  397. package/src/testing/boot-validator-fixture.ts +121 -0
  398. package/src/testing/e2e-generator.ts +8 -0
  399. package/src/testing/file-provider-contract.ts +104 -0
  400. package/src/testing/handler-context.ts +3 -1
  401. package/src/testing/index.ts +5 -0
  402. package/src/testing/late-bound.ts +5 -3
  403. package/src/testing/mutable-master-key-provider.ts +29 -3
  404. package/src/testing/wait-for.ts +3 -0
  405. package/src/testing/without-ambient-temporal.ts +14 -0
  406. package/src/time/__tests__/tz-dateline.test.ts +8 -10
  407. package/src/time/geo-tz.ts +1 -32
  408. package/src/time/index.ts +1 -0
  409. package/src/time/legacy-date.ts +18 -0
  410. package/src/time/polyfill.ts +21 -38
  411. package/src/time/tz-context.ts +44 -85
  412. package/src/ui-types/app-schema.ts +12 -0
  413. package/src/ui-types/index.ts +20 -7
  414. package/src/utils/__tests__/safe-json-temporal.test.ts +18 -0
  415. package/src/utils/safe-json.ts +13 -1
  416. package/src/__tests__/raw-table.integration.test.ts +0 -116
  417. package/src/bun-db/__tests__/bun-test-stack.ts +0 -6
  418. package/src/db/__tests__/encryption.test.ts +0 -39
  419. package/src/db/encryption.ts +0 -39
  420. package/src/db/row-helpers.ts +0 -4
  421. package/src/engine/__tests__/raw-table.test.ts +0 -150
  422. package/src/engine/__tests__/unmanaged-table.test.ts +0 -127
  423. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +0 -184
  424. package/src/engine/feature-ast/extractors/round4.ts +0 -1366
@@ -1,3 +1,4 @@
1
+ import type { ExecutorOnly } from "@cosmicdrift/kumiko-types/executor-brand";
1
2
  import type {
2
3
  EntityDefinition,
3
4
  EntityRelations,
@@ -15,6 +16,7 @@ import {
15
16
  type ColumnBuilder,
16
17
  type ColumnHandle,
17
18
  decimalColumn,
19
+ doublePrecision,
18
20
  type IndexBuilderWithCols,
19
21
  index,
20
22
  instant,
@@ -69,7 +71,13 @@ function fieldToColumns(
69
71
  const base = text(snakeName);
70
72
  const withDefault =
71
73
  "default" in field && field.default !== undefined ? base.default(field.default) : base;
72
- return { [name]: field.required ? withDefault.notNull() : withDefault };
74
+ const mainCol = { [name]: field.required ? withDefault.notNull() : withDefault };
75
+ // lookupable → nullable HMAC-Blind-Index-Spalte (lock-step mit
76
+ // fieldToColumnMeta in entity-table-meta.ts).
77
+ if (field.type === "text" && field.lookupable === true) {
78
+ return { ...mainCol, [`${name}Bidx`]: text(`${snakeName}_bidx`) };
79
+ }
80
+ return mainCol;
73
81
  }
74
82
  case "boolean":
75
83
  return {
@@ -101,7 +109,7 @@ function fieldToColumns(
101
109
  // multi-select.
102
110
  return { [name]: jsonb(snakeName).default([]).notNull() };
103
111
  case "number": {
104
- const base = integer(snakeName);
112
+ const base = field.integer === true ? integer(snakeName) : doublePrecision(snakeName);
105
113
  const col = field.default !== undefined ? base.default(field.default) : base;
106
114
  return { [name]: field.required ? col.notNull() : col };
107
115
  }
@@ -272,63 +280,72 @@ type NullCol<_T> = ColumnHandle & { readonly __notNull: false };
272
280
  // columns (resolved via FileRef table). `notNull` propagiert von
273
281
  // `field.required` (literal preserved by createXField generics).
274
282
  type ColumnsForField<K extends string, F extends FieldDefinition> = F extends {
275
- type: "text" | "select" | "tz";
283
+ type: "text";
284
+ lookupable: true;
276
285
  }
277
- ? F extends { required: true }
278
- ? { readonly [P in K]: Col<string> }
279
- : { readonly [P in K]: NullCol<string> }
280
- : F extends { type: "boolean" }
281
- ? // boolean default OR required → notNull (DB has DEFAULT, structurally never-null)
282
- F extends { default: boolean } | { required: true }
283
- ? { readonly [P in K]: Col<boolean> }
284
- : { readonly [P in K]: NullCol<boolean> }
285
- : F extends { type: "multiSelect" }
286
- ? // jsonb default `[]`, immer notNull
287
- { readonly [P in K]: Col<readonly string[]> }
288
- : F extends { type: "number" }
289
- ? F extends { required: true }
290
- ? { readonly [P in K]: Col<number> }
291
- : { readonly [P in K]: NullCol<number> }
292
- : F extends { type: "decimal" }
286
+ ? (F extends { required: true }
287
+ ? { readonly [P in K]: Col<string> }
288
+ : { readonly [P in K]: NullCol<string> }) & {
289
+ readonly [P in `${K}Bidx`]: NullCol<string>;
290
+ }
291
+ : F extends {
292
+ type: "text" | "select" | "tz";
293
+ }
294
+ ? F extends { required: true }
295
+ ? { readonly [P in K]: Col<string> }
296
+ : { readonly [P in K]: NullCol<string> }
297
+ : F extends { type: "boolean" }
298
+ ? // boolean default OR required notNull (DB has DEFAULT, structurally never-null)
299
+ F extends { default: boolean } | { required: true }
300
+ ? { readonly [P in K]: Col<boolean> }
301
+ : { readonly [P in K]: NullCol<boolean> }
302
+ : F extends { type: "multiSelect" }
303
+ ? // jsonb default `[]`, immer notNull
304
+ { readonly [P in K]: Col<readonly string[]> }
305
+ : F extends { type: "number" }
293
306
  ? F extends { required: true }
294
307
  ? { readonly [P in K]: Col<number> }
295
308
  : { readonly [P in K]: NullCol<number> }
296
- : F extends { type: "money" }
309
+ : F extends { type: "decimal" }
297
310
  ? F extends { required: true }
298
- ? { readonly [P in K]: Col<number> } & {
299
- readonly [P in `${K}Currency`]: Col<string>;
300
- }
301
- : { readonly [P in K]: NullCol<number> } & {
302
- readonly [P in `${K}Currency`]: Col<string>;
303
- }
304
- : F extends { type: "reference"; multiple: true }
305
- ? { readonly [P in K]: Col<readonly string[]> }
306
- : F extends { type: "reference" }
307
- ? F extends { required: true }
308
- ? { readonly [P in K]: Col<string> }
309
- : { readonly [P in K]: NullCol<string> }
310
- : F extends { type: "embedded" }
311
- ? // jsonb default `{}`, immer notNull
312
- { readonly [P in K]: Col<Readonly<Record<string, unknown>>> }
313
- : F extends { type: "date" | "timestamp" }
314
- ? F extends { required: true }
315
- ? { readonly [P in K]: Col<Temporal.Instant> }
316
- : { readonly [P in K]: NullCol<Temporal.Instant> }
317
- : F extends { type: "locatedTimestamp" }
311
+ ? { readonly [P in K]: Col<number> }
312
+ : { readonly [P in K]: NullCol<number> }
313
+ : F extends { type: "money" }
314
+ ? F extends { required: true }
315
+ ? { readonly [P in K]: Col<number> } & {
316
+ readonly [P in `${K}Currency`]: Col<string>;
317
+ }
318
+ : { readonly [P in K]: NullCol<number> } & {
319
+ readonly [P in `${K}Currency`]: Col<string>;
320
+ }
321
+ : F extends { type: "reference"; multiple: true }
322
+ ? { readonly [P in K]: Col<readonly string[]> }
323
+ : F extends { type: "reference" }
324
+ ? F extends { required: true }
325
+ ? { readonly [P in K]: Col<string> }
326
+ : { readonly [P in K]: NullCol<string> }
327
+ : F extends { type: "embedded" }
328
+ ? // jsonb default `{}`, immer notNull
329
+ { readonly [P in K]: Col<Readonly<Record<string, unknown>>> }
330
+ : F extends { type: "date" | "timestamp" }
318
331
  ? F extends { required: true }
319
- ? { readonly [P in `${K}Utc`]: Col<Temporal.Instant> } & {
320
- readonly [P in `${K}Tz`]: Col<string>;
321
- }
322
- : { readonly [P in `${K}Utc`]: NullCol<Temporal.Instant> } & {
323
- readonly [P in `${K}Tz`]: NullCol<string>;
324
- }
325
- : F extends { type: "file" | "image" }
332
+ ? { readonly [P in K]: Col<Temporal.Instant> }
333
+ : { readonly [P in K]: NullCol<Temporal.Instant> }
334
+ : F extends { type: "locatedTimestamp" }
326
335
  ? F extends { required: true }
327
- ? { readonly [P in K]: Col<string> }
328
- : { readonly [P in K]: NullCol<string> }
329
- : F extends { type: "files" | "images" }
330
- ? Record<never, never>
331
- : never;
336
+ ? { readonly [P in `${K}Utc`]: Col<Temporal.Instant> } & {
337
+ readonly [P in `${K}Tz`]: Col<string>;
338
+ }
339
+ : { readonly [P in `${K}Utc`]: NullCol<Temporal.Instant> } & {
340
+ readonly [P in `${K}Tz`]: NullCol<string>;
341
+ }
342
+ : F extends { type: "file" | "image" }
343
+ ? F extends { required: true }
344
+ ? { readonly [P in K]: Col<string> }
345
+ : { readonly [P in K]: NullCol<string> }
346
+ : F extends { type: "files" | "images" }
347
+ ? Record<never, never>
348
+ : never;
332
349
 
333
350
  type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (
334
351
  k: infer I,
@@ -366,25 +383,7 @@ type SoftDeleteColumnsType = {
366
383
  };
367
384
 
368
385
  // ── ES-write brand ──────────────────────────────────────────────────────
369
- // A managed projection is only writable through the executor (event →
370
- // rebuild-safe). To make a direct write a *compile* error rather than a
371
- // convention, EntityTable carries a phantom `unique symbol` prop; the public
372
- // write helpers reject anything that has it (see NotExecutorOnly + query.ts).
373
- // The symbol key dodges SchemaTable's `[field: string]: unknown` index
374
- // signature (string index sigs don't cover symbol keys), so the brand
375
- // survives the type-erasure that would swallow a plain marker prop — and the
376
- // executor seam (applyEntityEvent) erases `table` to TableColumns<any>, which
377
- // carries no such prop, so the one legitimate writer stays green.
378
- declare const EXECUTOR_ONLY: unique symbol;
379
- export interface ExecutorOnly {
380
- readonly [EXECUTOR_ONLY]: true;
381
- }
382
- // Negative brand for write-helper params: a branded EntityTable is NOT
383
- // assignable (its `true` clashes with `never`), while unmanaged EntityTableMeta
384
- // and erased SchemaTable pass (they lack the prop, so `?: never` is satisfied).
385
- export type NotExecutorOnly = {
386
- readonly [EXECUTOR_ONLY]?: never;
387
- };
386
+ export type { ExecutorOnly, NotExecutorOnly } from "@cosmicdrift/kumiko-types/executor-brand";
388
387
 
389
388
  export type EntityTable<E extends EntityDefinition = EntityDefinition> =
390
389
  TableColumns<// biome-ignore lint/suspicious/noExplicitAny: drizzle's internal table-config stays generic; we layer typed columns on top via the intersection below.
@@ -526,6 +525,18 @@ export function buildEntityTable<E extends EntityDefinition>(
526
525
  ).on(column);
527
526
  }
528
527
  }
528
+ // lookupable-Felder: Index auf der bidx-Spalte (lock-step mit
529
+ // buildEntityTableMeta).
530
+ const bidxFieldByField = new Map<string, string>();
531
+ for (const [name, field] of Object.entries(entity.fields)) {
532
+ if (field.type !== "text" || field.lookupable !== true) continue;
533
+ bidxFieldByField.set(name, `${name}Bidx`);
534
+ const column = tHandle[`${name}Bidx`];
535
+ if (column) {
536
+ const idxName = `${tableName}_${toSnakeCase(name)}_bidx_idx`;
537
+ indexes[idxName] = index(idxName).on(column);
538
+ }
539
+ }
529
540
  // entity.indexes = composite/unique-Indices die der Author explizit
530
541
  // deklariert hat. Spalten werden via field-name (camelCase) angesprochen,
531
542
  // der Index-Name folgt der Convention <table>_<col1>_<col2>_<unique|idx>
@@ -546,6 +557,26 @@ export function buildEntityTable<E extends EntityDefinition>(
546
557
  chain = chain.where(def.where as SqlExpression);
547
558
  }
548
559
  indexes[indexName] = chain;
560
+ // Partielles bidx-Pendant für unique-Indices über lookupable-Spalten
561
+ // (lock-step mit buildEntityTableMeta).
562
+ if (def.unique === true && def.where === undefined) {
563
+ const bidxFieldNames = def.columns.map((c) => bidxFieldByField.get(c) ?? c);
564
+ if (bidxFieldNames.some((c, i) => c !== def.columns[i])) {
565
+ const bidxCols = bidxFieldNames
566
+ .map((c) => tHandle[c])
567
+ .filter((col): col is ColumnHandle => col !== undefined);
568
+ if (bidxCols.length === bidxFieldNames.length) {
569
+ const whereText = bidxFieldNames
570
+ .filter((c, i) => c !== def.columns[i])
571
+ .map((c) => `"${toSnakeCase(c)}" IS NOT NULL`)
572
+ .join(" AND ");
573
+ const partialWhere: SqlExpression = { kind: "sql-expr", text: whereText, params: [] };
574
+ indexes[`${indexName}_bidx`] = uniqueIndex(`${indexName}_bidx`)
575
+ .on(...bidxCols)
576
+ .where(partialWhere);
577
+ }
578
+ }
579
+ }
549
580
  }
550
581
  return indexes;
551
582
  },
@@ -1,4 +1,7 @@
1
+ import { KUMIKO_NAME_SYMBOL, type SchemaTable } from "@cosmicdrift/kumiko-types/schema-table-types";
2
+ import type { TenantDb, TenantDbMode } from "@cosmicdrift/kumiko-types/tenant-db-types";
1
3
  import {
4
+ asEntityTableMeta,
2
5
  asRawClient,
3
6
  deleteMany as bunDeleteMany,
4
7
  fetchOne as bunFetchOne,
@@ -11,70 +14,29 @@ import {
11
14
  import { SYSTEM_TENANT_ID, type TenantId } from "../engine/types/identifiers";
12
15
  import { emitDbQuery, type Meter, registerStandardMetrics, type Tracer } from "../observability";
13
16
  import type { DbRunner } from "./connection";
14
- import type { TableColumns } from "./dialect";
15
-
16
- // biome-ignore lint/suspicious/noExplicitAny: Dynamic tables — keys depend on user-defined schema.
17
- type Table = TableColumns<any>;
18
-
19
- /**
20
- * TenantDb scope modes:
21
- *
22
- * - "tenant" (default): SELECT/UPDATE/DELETE filtered by tenantId + reference data (tenantId=SYSTEM_TENANT_ID).
23
- * INSERT forces tenantId — handler cannot override.
24
- *
25
- * - "system" (r.systemScope()): No tenant filter on reads/updates/deletes.
26
- * INSERT uses tenantId as default but handler can override.
27
- *
28
- * Tables without a tenantId column are always unfiltered regardless of mode.
29
- */
30
- export type TenantDbMode = "tenant" | "system";
31
-
32
- export type TenantDb = {
33
- readonly tenantId: TenantId;
34
- readonly mode: TenantDbMode;
35
- /**
36
- * Underlying DbRunner. Framework-internal use (event-store, migrations) —
37
- * bypasses tenant-filter. Feature code uses the typed helpers above so the
38
- * automatic scoping stays intact.
39
- */
40
- readonly raw: DbRunner;
41
- selectMany<T = Record<string, unknown>>(
42
- table: Table,
43
- where?: WhereObject,
44
- options?: SelectOptions,
45
- ): Promise<readonly T[]>;
46
- fetchOne<T = Record<string, unknown>>(table: Table, where: WhereObject): Promise<T | undefined>;
47
- insertOne<T = Record<string, unknown>>(
48
- table: Table,
49
- values: Record<string, unknown>,
50
- ): Promise<T | undefined>;
51
- updateMany<T = Record<string, unknown>>(
52
- table: Table,
53
- set: Record<string, unknown>,
54
- where: WhereObject,
55
- ): Promise<readonly T[]>;
56
- // Method-form writes (ctx.db.insertOne/updateMany/deleteMany) keep the erased
57
- // Table param: the executor-only brand is enforced on the free-function
58
- // helpers (the reflexive path all production writes take), while method-form
59
- // on a branded table is covered by the guard-direct-entity-writes AST guard
60
- // (P5) — branding it here would force every push+write test into casts for
61
- // zero production benefit (no prod code writes a projection via method-form).
62
- deleteMany(table: Table, where: WhereObject): Promise<void>;
63
- };
17
+
18
+ type Table = SchemaTable;
19
+
20
+ export type { TenantDb, TenantDbMode } from "@cosmicdrift/kumiko-types/tenant-db-types";
64
21
 
65
22
  // @cast-boundary tenant-db-row
66
23
  export function castTenantRows<T>(rows: readonly Record<string, unknown>[]): readonly T[] {
67
24
  return rows as unknown as readonly T[];
68
25
  }
69
26
 
70
- const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
71
-
72
27
  function tableNameOf(table: Table): string {
73
- const sym = (table as unknown as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
28
+ const sym = table[KUMIKO_NAME_SYMBOL];
74
29
  return typeof sym === "string" ? sym : "<unknown>";
75
30
  }
76
31
 
32
+ // Checks the canonical EntityTableMeta (branded EntityTable's KUMIKO_META_SYMBOL
33
+ // or a plain buildEntityTableMeta/defineUnmanagedTable result), not a direct
34
+ // `table.tenantId` property read — the latter only exists on branded EntityTables
35
+ // and silently returned false (no tenant filter!) for plain EntityTableMeta
36
+ // tables like unmanaged direct-write stores, e.g. userSessionTable.
77
37
  function hasTenantColumn(table: Table): boolean {
38
+ const meta = asEntityTableMeta(table);
39
+ if (meta) return meta.columns.some((c) => c.name === "tenant_id");
78
40
  return (table as Record<string, unknown>)["tenantId"] !== undefined;
79
41
  }
80
42
 
@@ -0,0 +1,242 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { z } from "zod";
3
+ import { validateBoot } from "../boot-validator";
4
+ import { defineFeature } from "../define-feature";
5
+ import { createEntity, createTextField } from "../factories";
6
+
7
+ describe("validateBoot — action wiring (no function values)", () => {
8
+ test("rowAction writeHandler payload as function → Throw", () => {
9
+ const feature = defineFeature("shop", (r) => {
10
+ r.entity("product", createEntity({ fields: { name: createTextField() } }));
11
+ r.screen({
12
+ id: "product-list",
13
+ type: "entityList",
14
+ entity: "product",
15
+ columns: ["name"],
16
+ rowActions: [
17
+ {
18
+ kind: "writeHandler",
19
+ id: "sync",
20
+ label: "actions.sync",
21
+ handler: "shop:write:sync",
22
+ // biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
23
+ payload: ((row: unknown) => ({ id: row })) as any,
24
+ },
25
+ ],
26
+ });
27
+ r.writeHandler("sync", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
28
+ access: { roles: ["Admin"] },
29
+ });
30
+ });
31
+ expect(() => validateBoot([feature])).toThrow(/rowAction "sync" payload is a function/);
32
+ });
33
+
34
+ test("rowAction writeHandler payload as declarative pick → kein Throw", () => {
35
+ const feature = defineFeature("shop", (r) => {
36
+ r.entity("product", createEntity({ fields: { name: createTextField({ sortable: true }) } }));
37
+ r.screen({
38
+ id: "product-list",
39
+ type: "entityList",
40
+ entity: "product",
41
+ columns: ["name"],
42
+ defaultSort: { field: "name", dir: "asc" },
43
+ rowActions: [
44
+ {
45
+ kind: "writeHandler",
46
+ id: "sync",
47
+ label: "actions.sync",
48
+ handler: "shop:write:sync",
49
+ payload: { pick: ["name"] },
50
+ },
51
+ ],
52
+ });
53
+ r.writeHandler("sync", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
54
+ access: { roles: ["Admin"] },
55
+ });
56
+ r.translations({
57
+ keys: {
58
+ "screen:product-list.title": { de: "Liste", en: "List" },
59
+ "shop:entity:product:field:name": { de: "Name", en: "Name" },
60
+ },
61
+ });
62
+ });
63
+ expect(() => validateBoot([feature])).not.toThrow();
64
+ });
65
+
66
+ test("rowAction navigate visible as function → Throw", () => {
67
+ const feature = defineFeature("shop", (r) => {
68
+ r.entity("product", createEntity({ fields: { name: createTextField() } }));
69
+ r.screen({
70
+ id: "product-list",
71
+ type: "entityList",
72
+ entity: "product",
73
+ columns: ["name"],
74
+ rowActions: [
75
+ {
76
+ kind: "navigate",
77
+ id: "edit",
78
+ label: "actions.edit",
79
+ screen: "product-list",
80
+ // biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
81
+ visible: (() => true) as any,
82
+ },
83
+ ],
84
+ });
85
+ });
86
+ expect(() => validateBoot([feature])).toThrow(/rowAction "edit" visible is a function/);
87
+ });
88
+
89
+ test("toolbarAction writeHandler payload as function → Throw", () => {
90
+ const feature = defineFeature("shop", (r) => {
91
+ r.entity("product", createEntity({ fields: { name: createTextField() } }));
92
+ r.screen({
93
+ id: "product-list",
94
+ type: "entityList",
95
+ entity: "product",
96
+ columns: ["name"],
97
+ toolbarActions: [
98
+ {
99
+ kind: "writeHandler",
100
+ id: "sync",
101
+ label: "actions.sync",
102
+ handler: "shop:write:sync",
103
+ // biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
104
+ payload: (() => ({})) as any,
105
+ },
106
+ ],
107
+ });
108
+ r.writeHandler("sync", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
109
+ access: { roles: ["Admin"] },
110
+ });
111
+ });
112
+ expect(() => validateBoot([feature])).toThrow(/toolbarAction "sync" payload is a function/);
113
+ });
114
+
115
+ test("entityList column renderer as function → Throw", () => {
116
+ const feature = defineFeature("shop", (r) => {
117
+ r.entity("product", createEntity({ fields: { name: createTextField() } }));
118
+ r.screen({
119
+ id: "product-list",
120
+ type: "entityList",
121
+ entity: "product",
122
+ columns: [
123
+ // biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
124
+ { field: "name", renderer: ((v: unknown) => String(v)) as any },
125
+ ],
126
+ });
127
+ });
128
+ expect(() => validateBoot([feature])).toThrow(/column "name" renderer is a function/);
129
+ });
130
+
131
+ test("entityEdit field visible as function → Throw", () => {
132
+ const feature = defineFeature("shop", (r) => {
133
+ r.entity("product", createEntity({ fields: { name: createTextField() } }));
134
+ r.screen({
135
+ id: "product-edit",
136
+ type: "entityEdit",
137
+ entity: "product",
138
+ layout: {
139
+ sections: [
140
+ {
141
+ columns: 1,
142
+ // biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
143
+ fields: [{ field: "name", visible: (() => true) as any }],
144
+ },
145
+ ],
146
+ },
147
+ });
148
+ });
149
+ expect(() => validateBoot([feature])).toThrow(/field "name" visible is a function/);
150
+ });
151
+
152
+ test("entityEdit field renderer as function → Throw", () => {
153
+ const feature = defineFeature("shop", (r) => {
154
+ r.entity("product", createEntity({ fields: { name: createTextField() } }));
155
+ r.screen({
156
+ id: "product-edit",
157
+ type: "entityEdit",
158
+ entity: "product",
159
+ layout: {
160
+ sections: [
161
+ {
162
+ columns: 1,
163
+ // biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
164
+ fields: [{ field: "name", renderer: ((v: unknown) => String(v)) as any }],
165
+ },
166
+ ],
167
+ },
168
+ });
169
+ });
170
+ expect(() => validateBoot([feature])).toThrow(/field "name" renderer is a function/);
171
+ });
172
+
173
+ test("entityEdit field with declarative visible/readOnly/required → kein Throw", () => {
174
+ const feature = defineFeature("shop", (r) => {
175
+ r.entity("product", createEntity({ fields: { name: createTextField() } }));
176
+ r.screen({
177
+ id: "product-edit",
178
+ type: "entityEdit",
179
+ entity: "product",
180
+ layout: {
181
+ sections: [
182
+ {
183
+ columns: 1,
184
+ fields: [
185
+ {
186
+ field: "name",
187
+ visible: { field: "name", ne: "" },
188
+ readOnly: false,
189
+ required: true,
190
+ },
191
+ ],
192
+ },
193
+ ],
194
+ },
195
+ });
196
+ r.translations({
197
+ keys: {
198
+ "screen:product-edit.title": { de: "Bearbeiten", en: "Edit" },
199
+ "shop:entity:product:field:name": { de: "Name", en: "Name" },
200
+ },
201
+ });
202
+ });
203
+ expect(() => validateBoot([feature])).not.toThrow();
204
+ });
205
+
206
+ test("projectionList column renderer as function → Throw", () => {
207
+ const feature = defineFeature("shop", (r) => {
208
+ r.screen({
209
+ id: "sales-list",
210
+ type: "projectionList",
211
+ query: "shop:query:sales",
212
+ columns: [
213
+ // biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
214
+ { field: "amount", renderer: ((v: unknown) => String(v)) as any },
215
+ ],
216
+ });
217
+ });
218
+ expect(() => validateBoot([feature])).toThrow(/column "amount" renderer is a function/);
219
+ });
220
+
221
+ test("projectionList rowAction payload as function → Throw", () => {
222
+ const feature = defineFeature("shop", (r) => {
223
+ r.screen({
224
+ id: "sales-list",
225
+ type: "projectionList",
226
+ query: "shop:query:sales",
227
+ columns: ["amount"],
228
+ rowActions: [
229
+ {
230
+ kind: "writeHandler",
231
+ id: "sync",
232
+ label: "actions.sync",
233
+ handler: "shop:write:sync",
234
+ // biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
235
+ payload: (() => ({})) as any,
236
+ },
237
+ ],
238
+ });
239
+ });
240
+ expect(() => validateBoot([feature])).toThrow(/rowAction "sync" payload is a function/);
241
+ });
242
+ });
@@ -0,0 +1,99 @@
1
+ // r.bootCheck(fn) — feature-declared mount invariants, checked at boot.
2
+ // Mirrors the prompt-store trap (kumiko-enterprise#229): a feature with
3
+ // PII-annotated fields was mounted without its required companion feature,
4
+ // and nothing caught it at boot. The conditional-invariant tests below
5
+ // reproduce that shape: the check only fails when the feature actually has
6
+ // PII fields AND the companion is missing — a bare "is X mounted" check
7
+ // would already be covered by r.requires and wouldn't justify this API.
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+ import { validateFeatureBootChecks } from "../boot-validator/boot-check";
11
+ import { defineFeature } from "../define-feature";
12
+ import { createEntity, createTextField } from "../factories";
13
+
14
+ function catchMessage(fn: () => void): string {
15
+ try {
16
+ fn();
17
+ } catch (e) {
18
+ return e instanceof Error ? e.message : String(e);
19
+ }
20
+ throw new Error("expected function to throw, but it did not");
21
+ }
22
+
23
+ const requiresUserDataHook = (features: readonly { readonly name: string }[]) =>
24
+ features.some((f) => f.name === "user-data-hook");
25
+
26
+ const promptStore = () =>
27
+ defineFeature("prompt-store", (r) => {
28
+ const promptFields = { text: createTextField({ pii: true }) };
29
+ r.entity("prompt", createEntity({ fields: promptFields }));
30
+ r.bootCheck(({ features }) => {
31
+ // Conditional on this feature's own shape (has a pii field), closed
32
+ // over from setup — r.requires("user-data-hook") can't express that.
33
+ const hasPiiField = Object.values(promptFields).some((field) => field.pii);
34
+ if (hasPiiField && !requiresUserDataHook(features)) {
35
+ throw new Error("prompt-store has PII fields but no user-data-hook feature is mounted");
36
+ }
37
+ });
38
+ });
39
+
40
+ describe("r.bootCheck / validateFeatureBootChecks", () => {
41
+ test("no bootChecks registered → no-op", () => {
42
+ const noop = defineFeature("noop", () => {});
43
+ expect(() => validateFeatureBootChecks([noop])).not.toThrow();
44
+ });
45
+
46
+ test("conditional invariant satisfied (companion mounted) → boot succeeds", () => {
47
+ const userDataHook = defineFeature("user-data-hook", () => {});
48
+ expect(() => validateFeatureBootChecks([userDataHook, promptStore()])).not.toThrow();
49
+ });
50
+
51
+ test("conditional invariant violated (PII field, no companion) → boot fails with feature-prefixed message", () => {
52
+ const message = catchMessage(() => validateFeatureBootChecks([promptStore()]));
53
+ expect(message).toContain("[Feature prompt-store]");
54
+ expect(message).toContain(
55
+ "prompt-store has PII fields but no user-data-hook feature is mounted",
56
+ );
57
+ });
58
+
59
+ test("no PII field → boot succeeds even without the companion", () => {
60
+ const noPiiFields = { text: createTextField() };
61
+ const noPii = defineFeature("prompt-store-no-pii", (r) => {
62
+ r.entity("note", createEntity({ fields: noPiiFields }));
63
+ r.bootCheck(({ features }) => {
64
+ const hasPiiField = Object.values(noPiiFields).some((field) => field.pii);
65
+ if (hasPiiField && !requiresUserDataHook(features)) {
66
+ throw new Error("unreachable in this test");
67
+ }
68
+ });
69
+ });
70
+ expect(() => validateFeatureBootChecks([noPii])).not.toThrow();
71
+ });
72
+
73
+ test("multiple bootChecks on one feature all run in order until one throws", () => {
74
+ const calls: string[] = [];
75
+ const feature = defineFeature("multi-check", (r) => {
76
+ r.bootCheck(() => {
77
+ calls.push("first");
78
+ });
79
+ r.bootCheck(() => {
80
+ calls.push("second");
81
+ throw new Error("second check failed");
82
+ });
83
+ });
84
+ expect(() => validateFeatureBootChecks([feature])).toThrow("second check failed");
85
+ expect(calls).toEqual(["first", "second"]);
86
+ });
87
+
88
+ test("ctx.features exposes every mounted feature, not just the declaring one", () => {
89
+ const a = defineFeature("feature-a", () => {});
90
+ let seenNames: string[] = [];
91
+ const b = defineFeature("feature-b", (r) => {
92
+ r.bootCheck(({ features }) => {
93
+ seenNames = features.map((f) => f.name);
94
+ });
95
+ });
96
+ validateFeatureBootChecks([a, b]);
97
+ expect(seenNames).toEqual(["feature-a", "feature-b"]);
98
+ });
99
+ });