@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
@@ -0,0 +1,141 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { defineFeature } from "../index";
3
+
4
+ // Object-Form is the shape the feature-ast renderer emits for Designer/
5
+ // AI-generated code (`r.entity({ name: "item", ... })` instead of
6
+ // `r.entity("item", { ... })`) — see kumiko-framework#1113. The compile
7
+ // checks in feature-ast/__tests__/render-roundtrip.test.ts prove the TYPES
8
+ // line up; they never execute the runtime dispatch that unpacks Object-Form
9
+ // back into the same state mutations as the positional form. These tests
10
+ // cover that: one per distinct dispatch shape, asserting object-form and
11
+ // positional-form produce identical FeatureDefinition state.
12
+
13
+ describe("registrar Object-Form — runtime parity with positional form", () => {
14
+ test("requires: array-unwrap ({ features }) matches variadic-string form", () => {
15
+ const positional = defineFeature("a", (r) => {
16
+ r.requires("auth", "tenant");
17
+ });
18
+ const objectForm = defineFeature("b", (r) => {
19
+ r.requires({ features: ["auth", "tenant"] });
20
+ });
21
+ expect(objectForm.requires).toEqual(positional.requires);
22
+ });
23
+
24
+ test("optionalRequires: array-unwrap matches variadic-string form", () => {
25
+ const positional = defineFeature("a", (r) => {
26
+ r.optionalRequires("promotions");
27
+ });
28
+ const objectForm = defineFeature("b", (r) => {
29
+ r.optionalRequires({ features: ["promotions"] });
30
+ });
31
+ expect(objectForm.optionalRequires).toEqual(positional.optionalRequires);
32
+ });
33
+
34
+ test("readsConfig: array-unwrap ({ keys }) matches variadic-string form", () => {
35
+ const positional = defineFeature("a", (r) => {
36
+ r.readsConfig("auth.smtpHost");
37
+ });
38
+ const objectForm = defineFeature("b", (r) => {
39
+ r.readsConfig({ keys: ["auth.smtpHost"] });
40
+ });
41
+ expect(objectForm.configReads).toEqual(positional.configReads);
42
+ });
43
+
44
+ test("entity: split-name ({ name, ...fields }) matches (name, definition) form", () => {
45
+ const definition = { fields: { title: { type: "text" as const, required: true } } };
46
+ const positional = defineFeature("a", (r) => {
47
+ r.entity("item", definition);
48
+ });
49
+ const objectForm = defineFeature("b", (r) => {
50
+ r.entity({ name: "item", ...definition });
51
+ });
52
+ expect(objectForm.entities).toEqual(positional.entities);
53
+ });
54
+
55
+ test("metric: split-name matches (shortName, options) form", () => {
56
+ const positional = defineFeature("a", (r) => {
57
+ r.metric("created", { type: "counter" });
58
+ });
59
+ const objectForm = defineFeature("b", (r) => {
60
+ r.metric({ name: "created", type: "counter" });
61
+ });
62
+ expect(objectForm.metrics).toEqual(positional.metrics);
63
+ });
64
+
65
+ test("secret: split-name matches (shortName, options) form (same feature name for qualifiedName parity)", () => {
66
+ const positional = defineFeature("stripe", (r) => {
67
+ r.secret("apiKey", { label: { en: "Stripe API Key" }, scope: "tenant" });
68
+ });
69
+ const objectForm = defineFeature("stripe", (r) => {
70
+ r.secret({ name: "apiKey", label: { en: "Stripe API Key" }, scope: "tenant" });
71
+ });
72
+ expect(objectForm.secretKeys).toEqual(positional.secretKeys);
73
+ });
74
+
75
+ test("relation: two-field ({ entity, name, ...def }) matches (entity, name, definition) form", () => {
76
+ const definition = { type: "belongsTo" as const, target: "user", foreignKey: "supplierId" };
77
+ const positional = defineFeature("a", (r) => {
78
+ r.entity("item", { fields: {} });
79
+ r.relation("item", "supplier", definition);
80
+ });
81
+ const objectForm = defineFeature("b", (r) => {
82
+ r.entity("item", { fields: {} });
83
+ r.relation({ entity: "item", name: "supplier", ...definition });
84
+ });
85
+ expect(objectForm.relations).toEqual(positional.relations);
86
+ });
87
+
88
+ test("referenceData: two-field ({ entity, data, ...opts }) matches (entity, data, options) form", () => {
89
+ const data = [{ id: "a", label: "A" }];
90
+ const positional = defineFeature("a", (r) => {
91
+ r.referenceData("category", data, { upsertKey: "id" });
92
+ });
93
+ const objectForm = defineFeature("b", (r) => {
94
+ r.referenceData({ entity: "category", data, upsertKey: "id" });
95
+ });
96
+ expect(objectForm.referenceData).toEqual(positional.referenceData);
97
+ });
98
+
99
+ test("useExtension: two-field ({ name, entity, ...opts }) matches (extensionName, entity, options) form", () => {
100
+ const positional = defineFeature("a", (r) => {
101
+ r.useExtension("audit-log", "item", { verbose: true });
102
+ });
103
+ const objectForm = defineFeature("b", (r) => {
104
+ r.useExtension({ name: "audit-log", entity: "item", verbose: true });
105
+ });
106
+ expect(objectForm.extensionUsages).toEqual(positional.extensionUsages);
107
+ });
108
+
109
+ test("claimKey: generic split-name matches (shortName, options) form", () => {
110
+ const positional = defineFeature("drivers", (r) => {
111
+ r.claimKey("teamId", { type: "string" });
112
+ });
113
+ const objectForm = defineFeature("drivers", (r) => {
114
+ r.claimKey({ name: "teamId", type: "string" });
115
+ });
116
+ expect(objectForm.claimKeys).toEqual(positional.claimKeys);
117
+ });
118
+
119
+ test("job: full-definition object matches (name, options, handler) form", () => {
120
+ const handler = async () => {};
121
+ const positional = defineFeature("a", (r) => {
122
+ r.job("reconcile", { trigger: { manual: true } }, handler);
123
+ });
124
+ const objectForm = defineFeature("b", (r) => {
125
+ r.job({ name: "reconcile", trigger: { manual: true }, handler });
126
+ });
127
+ expect(objectForm.jobs).toEqual(positional.jobs);
128
+ });
129
+
130
+ test("notification: split-name matches (name, definition) form", () => {
131
+ const recipient = () => null;
132
+ const data = () => ({});
133
+ const positional = defineFeature("a", (r) => {
134
+ r.notification("itemLowStock", { trigger: { on: "item:lowStock" }, recipient, data });
135
+ });
136
+ const objectForm = defineFeature("b", (r) => {
137
+ r.notification({ name: "itemLowStock", trigger: { on: "item:lowStock" }, recipient, data });
138
+ });
139
+ expect(objectForm.notifications).toEqual(positional.notifications);
140
+ });
141
+ });
@@ -1,6 +1,8 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { z } from "zod";
2
3
  import { createTenantConfig } from "../config-helpers";
3
4
  import { defineFeature } from "../define-feature";
5
+ import { createEntity, createTextField } from "../factories";
4
6
  import { createRegistry } from "../registry";
5
7
  import type { FeatureDefinition } from "../types/feature";
6
8
 
@@ -59,6 +61,122 @@ describe("createRegistry slot robustness", () => {
59
61
  });
60
62
  });
61
63
 
64
+ describe("getAllQueryHandlers", () => {
65
+ test("returns every registered query handler, qualified, across multiple features", () => {
66
+ const taskEntity = createEntity({
67
+ table: "registry_test_tasks",
68
+ fields: { title: createTextField({ required: true }) },
69
+ });
70
+ const noteEntity = createEntity({
71
+ table: "registry_test_notes",
72
+ fields: { body: createTextField({ required: true }) },
73
+ });
74
+ const taskFeature = defineFeature("registry-test-task", (r) => {
75
+ r.crud("task", taskEntity, {
76
+ write: { access: { roles: ["Admin"] } },
77
+ read: { access: { openToAll: true } },
78
+ });
79
+ });
80
+ const noteFeature = defineFeature("registry-test-note", (r) => {
81
+ r.crud("note", noteEntity, {
82
+ write: { access: { roles: ["Admin"] } },
83
+ read: { access: { openToAll: true } },
84
+ });
85
+ });
86
+
87
+ const registry = createRegistry([taskFeature, noteFeature]);
88
+ const handlers = registry.getAllQueryHandlers();
89
+
90
+ expect(handlers.get("registry-test-task:query:task:list")).toBeDefined();
91
+ expect(handlers.get("registry-test-note:query:note:list")).toBeDefined();
92
+ // Same Map instance getQueryHandler reads from, not a copy that can drift.
93
+ expect(handlers.get("registry-test-task:query:task:list")).toBe(
94
+ registry.getQueryHandler("registry-test-task:query:task:list"),
95
+ );
96
+ });
97
+
98
+ test("empty registry returns an empty map, not undefined", () => {
99
+ const registry = createRegistry([]);
100
+ expect(registry.getAllQueryHandlers().size).toBe(0);
101
+ });
102
+ });
103
+
104
+ describe("getAllStreamHandlers", () => {
105
+ test("returns every registered stream handler, qualified, across multiple features", () => {
106
+ const aiFeature = defineFeature("registry-test-ai", (r) => {
107
+ r.streamHandler("chat:complete", z.object({ prompt: z.string() }), async function* () {}, {
108
+ access: { openToAll: true },
109
+ });
110
+ });
111
+ const otherFeature = defineFeature("registry-test-other", (r) => {
112
+ r.streamHandler("chat:complete", z.object({ prompt: z.string() }), async function* () {}, {
113
+ access: { openToAll: true },
114
+ });
115
+ });
116
+
117
+ const registry = createRegistry([aiFeature, otherFeature]);
118
+ const handlers = registry.getAllStreamHandlers();
119
+
120
+ expect(handlers.get("registry-test-ai:stream:chat:complete")).toBeDefined();
121
+ expect(handlers.get("registry-test-other:stream:chat:complete")).toBeDefined();
122
+ // Same Map instance getStreamHandler reads from, not a copy that can drift.
123
+ expect(handlers.get("registry-test-ai:stream:chat:complete")).toBe(
124
+ registry.getStreamHandler("registry-test-ai:stream:chat:complete"),
125
+ );
126
+ });
127
+
128
+ test("empty registry returns an empty map, not undefined", () => {
129
+ const registry = createRegistry([]);
130
+ expect(registry.getAllStreamHandlers().size).toBe(0);
131
+ });
132
+
133
+ test("duplicate stream-handler short-name across features qualifies independently, no collision", () => {
134
+ const aiFeature = defineFeature("registry-test-dup-a", (r) => {
135
+ r.streamHandler("chat:complete", z.object({}), async function* () {});
136
+ });
137
+ const otherFeature = defineFeature("registry-test-dup-b", (r) => {
138
+ r.streamHandler("chat:complete", z.object({}), async function* () {});
139
+ });
140
+ expect(() => createRegistry([aiFeature, otherFeature])).not.toThrow();
141
+ });
142
+
143
+ test("two distinct feature names that kebab-collide throw on the qualified stream-handler clash", () => {
144
+ // "registry-test-kebab-dup" and "registryTestKebabDup" are different raw
145
+ // feature.name values (so the earlier Duplicate-feature guard doesn't
146
+ // fire) but toKebab() collapses both to the same qualified name.
147
+ const featureA = defineFeature("registry-test-kebab-dup", (r) => {
148
+ r.streamHandler("chat:complete", z.object({}), async function* () {});
149
+ });
150
+ const featureB = defineFeature("registryTestKebabDup", (r) => {
151
+ r.streamHandler("chat:complete", z.object({}), async function* () {});
152
+ });
153
+ expect(() => createRegistry([featureA, featureB])).toThrow(/Duplicate stream handler/);
154
+ });
155
+
156
+ test("object-form streamHandler registration preserves schema/access/rateLimit", () => {
157
+ const schema = z.object({ prompt: z.string() });
158
+ const handlerFn = async function* () {};
159
+ const feature = defineFeature("registry-test-object-form", (r) => {
160
+ r.streamHandler({
161
+ name: "chat:complete",
162
+ schema,
163
+ handler: handlerFn,
164
+ access: { openToAll: true },
165
+ rateLimit: { per: "ip+handler", limit: 5, windowSeconds: 60 },
166
+ });
167
+ });
168
+
169
+ const registry = createRegistry([feature]);
170
+ const registered = registry.getStreamHandler("registry-test-object-form:stream:chat:complete");
171
+
172
+ expect(registered).toBeDefined();
173
+ expect(registered?.schema).toBe(schema);
174
+ expect(registered?.handler).toBe(handlerFn);
175
+ expect(registered?.access).toEqual({ openToAll: true });
176
+ expect(registered?.rateLimit).toEqual({ per: "ip+handler", limit: 5, windowSeconds: 60 });
177
+ });
178
+ });
179
+
62
180
  describe("extensionSelector boot-validation", () => {
63
181
  function foundationFeature() {
64
182
  return defineFeature("probe-foundation", (r) => {
@@ -130,4 +248,30 @@ describe("extensionSelector boot-validation", () => {
130
248
  }),
131
249
  ).toThrow(/declared twice/);
132
250
  });
251
+
252
+ // 437/2: createRegistry now delegates this check to
253
+ // validateExtensionPreSaveWiring (shared with validateBoot's standalone
254
+ // callers) instead of a duplicate inline computation — pins that
255
+ // createRegistry itself still enforces it, not just validateBoot.
256
+ test("throws when extension preSave targets entity with no mapped write handlers", () => {
257
+ const ext = defineFeature("cap-ext", (r) => {
258
+ r.extendsRegistrar("credit-cap", {
259
+ hooks: { preSave: async (changes) => changes },
260
+ });
261
+ });
262
+ const consumer = defineFeature("money-horse", (r) => {
263
+ r.requires("cap-ext");
264
+ r.entity("credit", createEntity({ table: "Credits", fields: { name: createTextField() } }));
265
+ r.writeHandler(
266
+ "doSomething",
267
+ z.object({}),
268
+ async () => ({ isSuccess: true as const, data: {} }),
269
+ { access: { openToAll: true } },
270
+ );
271
+ r.useExtension("credit-cap", "credit");
272
+ });
273
+ expect(() => createRegistry([ext, consumer])).toThrow(
274
+ /no write handler is entity-mapped to "credit"/i,
275
+ );
276
+ });
133
277
  });
@@ -113,6 +113,24 @@ describe("buildInsertSchema", () => {
113
113
  valid: { age: 25 },
114
114
  invalid: { age: "old" },
115
115
  },
116
+ {
117
+ name: "number field rejects below min",
118
+ fields: { age: createNumberField({ min: 0 }) },
119
+ valid: { age: 0 },
120
+ invalid: { age: -1 },
121
+ },
122
+ {
123
+ name: "number field rejects above max",
124
+ fields: { age: createNumberField({ max: 100 }) },
125
+ valid: { age: 100 },
126
+ invalid: { age: 101 },
127
+ },
128
+ {
129
+ name: "number field min+max bounds",
130
+ fields: { age: createNumberField({ min: 1, max: 10 }) },
131
+ valid: { age: 5 },
132
+ invalid: { age: 11 },
133
+ },
116
134
  {
117
135
  name: "date field",
118
136
  fields: { born: createDateField() },
@@ -1,10 +1,15 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { validateBoot } from "../boot-validator";
2
+ import { withBootValidatorFixture } from "../../testing/boot-validator-fixture";
3
+ import { validateBoot as validateBootRaw } from "../boot-validator";
3
4
  import { defineFeature } from "../define-feature";
4
5
  import { createDerivedField, createEntity, createTextField } from "../factories";
5
6
  import { createRegistry } from "../registry";
6
7
  import type { ScreenDefinition } from "../types/screen";
7
8
 
9
+ function validateBoot(features: Parameters<typeof validateBootRaw>[0]): void {
10
+ validateBootRaw(withBootValidatorFixture(features));
11
+ }
12
+
8
13
  function productEntity() {
9
14
  return createEntity({
10
15
  table: "products",
@@ -45,6 +50,121 @@ describe("r.screen() — registration", () => {
45
50
  expect(feature.screens["product-list"]?.type).toBe("entityList");
46
51
  });
47
52
 
53
+ test("stores a projectionList screen bound to an explicit (cross-feature) query", () => {
54
+ const feature = defineFeature("app", (r) => {
55
+ r.screen({
56
+ id: "rent-list",
57
+ type: "projectionList",
58
+ query: "ledger:query:schedule:list",
59
+ columns: [
60
+ { field: "description", label: "app:col.desc" },
61
+ {
62
+ field: "amount",
63
+ label: "app:col.amount",
64
+ renderer: { react: { __component: "EuroCell" } },
65
+ },
66
+ ],
67
+ });
68
+ });
69
+ const screen = feature.screens["rent-list"];
70
+ expect(screen?.type).toBe("projectionList");
71
+ if (screen?.type !== "projectionList") throw new Error("type-narrow failed");
72
+ expect(screen.query).toBe("ledger:query:schedule:list");
73
+ });
74
+
75
+ test("validateBoot rejects a projectionList with empty columns", () => {
76
+ const features = [
77
+ defineFeature("app", (r) => {
78
+ r.screen({ id: "x", type: "projectionList", query: "app:query:foo:list", columns: [] });
79
+ }),
80
+ ];
81
+ expect(() => validateBoot(features)).toThrow(/empty columns list/i);
82
+ });
83
+
84
+ test("validateBoot rejects a projectionList with an empty query", () => {
85
+ const features = [
86
+ defineFeature("app", (r) => {
87
+ r.screen({ id: "x", type: "projectionList", query: "", columns: ["name"] });
88
+ }),
89
+ ];
90
+ expect(() => validateBoot(features)).toThrow(/empty or non-string query/i);
91
+ });
92
+
93
+ test("stores a projectionDetail screen bound to an explicit query", () => {
94
+ const feature = defineFeature("app", (r) => {
95
+ r.screen({
96
+ id: "rent-detail",
97
+ type: "projectionDetail",
98
+ query: "ledger:query:schedule:detail",
99
+ layout: { sections: [{ title: "app:section.basics", fields: ["description"] }] },
100
+ });
101
+ });
102
+ const screen = feature.screens["rent-detail"];
103
+ expect(screen?.type).toBe("projectionDetail");
104
+ if (screen?.type !== "projectionDetail") throw new Error("type-narrow failed");
105
+ expect(screen.query).toBe("ledger:query:schedule:detail");
106
+ });
107
+
108
+ test("validateBoot rejects a projectionDetail with an empty query", () => {
109
+ const features = [
110
+ defineFeature("app", (r) => {
111
+ r.screen({
112
+ id: "x",
113
+ type: "projectionDetail",
114
+ query: "",
115
+ layout: { sections: [{ title: "s", fields: ["name"] }] },
116
+ });
117
+ }),
118
+ ];
119
+ expect(() => validateBoot(features)).toThrow(/empty or non-string query/i);
120
+ });
121
+
122
+ test("validateBoot rejects a projectionDetail with empty sections", () => {
123
+ const features = [
124
+ defineFeature("app", (r) => {
125
+ r.screen({
126
+ id: "x",
127
+ type: "projectionDetail",
128
+ query: "app:query:foo:detail",
129
+ layout: { sections: [] },
130
+ });
131
+ }),
132
+ ];
133
+ expect(() => validateBoot(features)).toThrow(/empty sections list/i);
134
+ });
135
+
136
+ test("validateBoot rejects a projectionDetail section with zero fields", () => {
137
+ const features = [
138
+ defineFeature("app", (r) => {
139
+ r.screen({
140
+ id: "x",
141
+ type: "projectionDetail",
142
+ query: "app:query:foo:detail",
143
+ layout: { sections: [{ title: "s", fields: [] }] },
144
+ });
145
+ }),
146
+ ];
147
+ expect(() => validateBoot(features)).toThrow(/zero fields/i);
148
+ });
149
+
150
+ test("validateBoot rejects a projectionDetail with an extension section (no entity to persist against)", () => {
151
+ const features = [
152
+ defineFeature("app", (r) => {
153
+ r.screen({
154
+ id: "x",
155
+ type: "projectionDetail",
156
+ query: "app:query:foo:detail",
157
+ layout: {
158
+ sections: [
159
+ { kind: "extension", title: "s", component: { react: { __component: "c" } } },
160
+ ],
161
+ },
162
+ });
163
+ }),
164
+ ];
165
+ expect(() => validateBoot(features)).toThrow(/extension section/i);
166
+ });
167
+
48
168
  test("stores an entityEdit screen with sections + conditional fields", () => {
49
169
  const feature = defineFeature("shop", (r) => {
50
170
  r.entity("product", productEntity());
@@ -219,6 +339,32 @@ describe("createRegistry — screen indexing", () => {
219
339
  ]);
220
340
  });
221
341
 
342
+ test("getScreensByEntity aggregates screens on the same entity from two different features", () => {
343
+ const shop = defineFeature("shop", (r) => {
344
+ r.entity("product", productEntity());
345
+ r.screen({
346
+ id: "product-list",
347
+ type: "entityList",
348
+ entity: "product",
349
+ columns: ["name"],
350
+ });
351
+ });
352
+ const reporting = defineFeature("reporting", (r) => {
353
+ r.screen({
354
+ id: "product-stats",
355
+ type: "entityList",
356
+ entity: "product",
357
+ columns: ["name"],
358
+ });
359
+ });
360
+ const registry = createRegistry([shop, reporting]);
361
+ const byProduct = registry.getScreensByEntity("product");
362
+ expect(byProduct.map((s) => s.id).sort()).toEqual([
363
+ "reporting:screen:product-stats",
364
+ "shop:screen:product-list",
365
+ ]);
366
+ });
367
+
222
368
  test("getScreen / getScreensByEntity return stored screens with qualified id", () => {
223
369
  const feature = defineFeature("shop", (r) => {
224
370
  r.entity("product", productEntity());
@@ -31,6 +31,9 @@ describe("registry soft-delete auto-wiring", () => {
31
31
  const key = registry.getConfigKey(SOFT_DELETE_GRACE_DAYS_KEY);
32
32
  expect(key?.type).toBe("number");
33
33
  expect(key?.default).toBe(DEFAULT_GRACE_DAYS);
34
+ // 565/2: min must be 1, not 0 — graceDays: 0 means "hard-delete every
35
+ // currently soft-deleted row on the next cron run", not "no cleanup".
36
+ expect(key?.bounds?.min).toBe(1);
34
37
  });
35
38
 
36
39
  test("also injects the system-scope cleanup job (not perTenant)", () => {