@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
@@ -14,6 +14,7 @@ import {
14
14
  createTextField,
15
15
  } from "../factories";
16
16
  import { createApp, createRegistry, defineFeature } from "../index";
17
+ import { buildMinimalCtx } from "./_pipeline-test-utils";
17
18
 
18
19
  // --- defineFeature ---
19
20
 
@@ -41,7 +42,9 @@ describe("defineFeature", () => {
41
42
  r.describe("first");
42
43
  r.describe("second");
43
44
  }),
44
- ).toThrow(/r\.describe\(\) called twice/);
45
+ ).toThrow(
46
+ "[Feature test] r.describe() called twice — a feature's description is declared once",
47
+ );
45
48
  });
46
49
 
47
50
  test("r.describe() throws on empty or whitespace-only text", () => {
@@ -116,6 +119,33 @@ describe("defineFeature", () => {
116
119
  expect(ref?.name).toBe("order:list");
117
120
  });
118
121
 
122
+ test("streamHandler registers on feature.streamHandlers with schema/access/handler intact", async () => {
123
+ let ref: { name: string } | undefined;
124
+ const feature = defineFeature("test", (r) => {
125
+ ref = r.streamHandler(
126
+ "chat:complete",
127
+ z.object({ prompt: z.string() }),
128
+ async function* (query) {
129
+ yield `echo:${query.payload.prompt}`;
130
+ },
131
+ { access: { openToAll: true } },
132
+ );
133
+ });
134
+ expect(ref?.name).toBe("chat:complete");
135
+
136
+ const stored = feature.streamHandlers["chat:complete"];
137
+ expect(stored?.access).toEqual({ openToAll: true });
138
+
139
+ const chunks: string[] = [];
140
+ for await (const chunk of stored!.handler(
141
+ { type: "chat:complete", payload: { prompt: "hi" }, user: createTestUser() },
142
+ buildMinimalCtx(),
143
+ )) {
144
+ chunks.push(chunk as string);
145
+ }
146
+ expect(chunks).toEqual(["echo:hi"]);
147
+ });
148
+
119
149
  test("r.defineEvent returns typed EventDef and registers on feature", () => {
120
150
  let eventRef: { name: string } | undefined;
121
151
  const feature = defineFeature("orders", (r) => {
@@ -274,6 +304,24 @@ describe("defineFeature", () => {
274
304
  }),
275
305
  ).toThrow(/r\.uiHints\(\) called twice/);
276
306
  });
307
+
308
+ test("r.toggleable() throws when called twice", () => {
309
+ expect(() =>
310
+ defineFeature("test", (r) => {
311
+ r.toggleable({ default: true });
312
+ r.toggleable({ default: false });
313
+ }),
314
+ ).toThrow(/r\.toggleable\(\) called twice/);
315
+ });
316
+
317
+ test("r.envSchema() throws when called twice", () => {
318
+ expect(() =>
319
+ defineFeature("test", (r) => {
320
+ r.envSchema(z.object({ FOO: z.string() }));
321
+ r.envSchema(z.object({ BAR: z.string() }));
322
+ }),
323
+ ).toThrow(/r\.envSchema\(\) called twice/);
324
+ });
277
325
  });
278
326
 
279
327
  // --- Field Factories ---
@@ -419,6 +467,22 @@ describe("createRegistry", () => {
419
467
  expect(all["profile:nav.title"]).toEqual({ de: "Profil", en: "Profile" });
420
468
  });
421
469
 
470
+ // #1105: a feature that already qualifies its own translation keys (nav
471
+ // labels referencing "featureName:..." verbatim, see cap-counter) must not
472
+ // be double-prefixed — else server-side t() can never resolve them.
473
+ test("does not double-prefix translation keys that already carry the feature's own namespace", () => {
474
+ const f = defineFeature("cap-counter", (r) => {
475
+ r.translations({
476
+ keys: { "cap-counter:nav.cap-list": { de: "Limits", en: "Caps" } },
477
+ });
478
+ });
479
+
480
+ const registry = createRegistry([f]);
481
+ const all = registry.getAllTranslations();
482
+ expect(all["cap-counter:nav.cap-list"]).toEqual({ de: "Limits", en: "Caps" });
483
+ expect(all["cap-counter:cap-counter:nav.cap-list"]).toBeUndefined();
484
+ });
485
+
422
486
  test("throws when write handler is not entity-mapped in feature with field-access", () => {
423
487
  const feature = defineFeature("hr", (r) => {
424
488
  r.entity(
@@ -675,6 +739,36 @@ describe("createApp", () => {
675
739
  ).not.toThrow();
676
740
  });
677
741
 
742
+ test("validates stream handler roles against app-defined roles (#1442)", () => {
743
+ const feature = defineFeature("admin", (r) => {
744
+ r.streamHandler("admin.tail", z.object({}), async function* () {}, {
745
+ access: { roles: ["SuperAdmin"] },
746
+ });
747
+ });
748
+
749
+ expect(() =>
750
+ createApp({
751
+ roles: ["Admin", "User"] as const,
752
+ features: [feature],
753
+ }),
754
+ ).toThrow(/unknown role.*SuperAdmin/i);
755
+ });
756
+
757
+ test("passes when stream handler roles are valid (#1442)", () => {
758
+ const feature = defineFeature("admin", (r) => {
759
+ r.streamHandler("admin.tail", z.object({}), async function* () {}, {
760
+ access: { roles: ["Admin"] },
761
+ });
762
+ });
763
+
764
+ expect(() =>
765
+ createApp({
766
+ roles: ["Admin", "User"] as const,
767
+ features: [feature],
768
+ }),
769
+ ).not.toThrow();
770
+ });
771
+
678
772
  test("createApp returns registry", () => {
679
773
  const feature = defineFeature("test", () => {});
680
774
  const app = createApp({ roles: ["Admin"] as const, features: [feature] });
@@ -1188,6 +1282,50 @@ describe("r.config()", () => {
1188
1282
  });
1189
1283
  });
1190
1284
 
1285
+ // --- r.config() single-key overload ---
1286
+
1287
+ describe("r.config() single-key overload", () => {
1288
+ test("returns a bare handle with the same qualified name the multi-key form would produce", () => {
1289
+ let viaSingleKey!: { readonly name: string; readonly type: "boolean" };
1290
+ let viaMultiKey!: { readonly defaultVat: { readonly name: string; readonly type: "number" } };
1291
+ defineFeature("invoicing", (r) => {
1292
+ viaSingleKey = r.config("enabled", createTenantConfig("boolean", { default: false }));
1293
+ });
1294
+ defineFeature("invoicing2", (r) => {
1295
+ viaMultiKey = r.config({
1296
+ keys: { defaultVat: createTenantConfig("number", { default: 19 }) },
1297
+ });
1298
+ });
1299
+
1300
+ expect(viaSingleKey.name).toBe("invoicing:config:enabled");
1301
+ expect(viaSingleKey.type).toBe("boolean");
1302
+ // Structural check: the single-key form's handle is exactly what
1303
+ // {keys:{one}} would have produced for that single key — same shape,
1304
+ // same qualification scheme.
1305
+ expect(viaMultiKey.defaultVat.name).toBe("invoicing2:config:default-vat");
1306
+ });
1307
+
1308
+ test("registers the key on the feature exactly like the multi-key form would", () => {
1309
+ const feature = defineFeature("shop", (r) => {
1310
+ r.config("maxItems", createTenantConfig("number", { default: 10 }));
1311
+ });
1312
+ expect(feature.configKeys["maxItems"]).toBeDefined();
1313
+ expect(feature.configKeys["maxItems"]?.type).toBe("number");
1314
+ expect(feature.configKeys["maxItems"]?.scope).toBe("tenant");
1315
+
1316
+ const registry = createRegistry([feature]);
1317
+ expect(registry.getConfigKey("shop:config:max-items")).toBeDefined();
1318
+ });
1319
+
1320
+ test("camelCase feature + key are kebab-cased in the handle name", () => {
1321
+ let handle!: { readonly name: string };
1322
+ defineFeature("billingCore", (r) => {
1323
+ handle = r.config("monthlyTotalCents", createSystemConfig("number", { default: 0 }));
1324
+ });
1325
+ expect(handle.name).toBe("billing-core:config:monthly-total-cents");
1326
+ });
1327
+ });
1328
+
1191
1329
  // --- Relations ---
1192
1330
 
1193
1331
  describe("r.relation()", () => {
@@ -1325,6 +1463,30 @@ describe("registry relations", () => {
1325
1463
  // Last write wins
1326
1464
  expect((rels["roles"] as { through: { table: string } }).through.table).toBe("UR2");
1327
1465
  });
1466
+
1467
+ test("getRelations aggregates relations on the same entity from two different features", () => {
1468
+ const core = defineFeature("core", (r) => {
1469
+ r.entity("user", createEntity({ table: "Users", fields: {} }));
1470
+ r.entity("role", createEntity({ table: "Roles", fields: {} }));
1471
+ r.relation("user", "roles", {
1472
+ type: "manyToMany",
1473
+ target: "role",
1474
+ through: { table: "UserRoles", sourceKey: "userId", targetKey: "roleId" },
1475
+ });
1476
+ });
1477
+ const billing = defineFeature("billing", (r) => {
1478
+ r.entity("invoice", createEntity({ table: "Invoices", fields: {} }));
1479
+ r.relation("user", "invoices", {
1480
+ type: "hasMany",
1481
+ target: "invoice",
1482
+ foreignKey: "userId",
1483
+ });
1484
+ });
1485
+
1486
+ const registry = createRegistry([core, billing]);
1487
+ const rels = registry.getRelations("user");
1488
+ expect(Object.keys(rels).sort()).toEqual(["invoices", "roles"]);
1489
+ });
1328
1490
  });
1329
1491
 
1330
1492
  // --- Global Search (new tenant-based API) ---
@@ -10,8 +10,11 @@ import {
10
10
  defineEntityUpdateHandler,
11
11
  defineEntityWriteHandler,
12
12
  defineProjectionQueryHandler,
13
+ type EntityCrudRegistrar,
14
+ registerEntityCrud,
13
15
  } from "../entity-handlers";
14
16
  import { createEntity, createTextField } from "../factories";
17
+ import type { QueryHandlerDef, WriteHandlerDef } from "../types";
15
18
 
16
19
  const VALID_UUID = "00000000-0000-4000-8000-000000000001";
17
20
 
@@ -272,3 +275,80 @@ describe("Verb-specific entity-handler factories", () => {
272
275
  expect(typeof legacyApi.handler).toBe("function");
273
276
  });
274
277
  });
278
+
279
+ function createCrudRegistrarMock(): {
280
+ readonly r: EntityCrudRegistrar;
281
+ readonly entities: Record<string, unknown>;
282
+ readonly writes: WriteHandlerDef[];
283
+ readonly queries: QueryHandlerDef[];
284
+ } {
285
+ const entities: Record<string, unknown> = {};
286
+ const writes: WriteHandlerDef[] = [];
287
+ const queries: QueryHandlerDef[] = [];
288
+ const r: EntityCrudRegistrar = {
289
+ entity(name, definition) {
290
+ entities[name] = definition;
291
+ },
292
+ writeHandler(def) {
293
+ writes.push(def);
294
+ },
295
+ queryHandler(def) {
296
+ queries.push(def);
297
+ },
298
+ };
299
+ return { r, entities, writes, queries };
300
+ }
301
+
302
+ describe("registerEntityCrud", () => {
303
+ test("registers entity + standard handlers (restore only when softDelete)", () => {
304
+ const { r, entities, writes, queries } = createCrudRegistrarMock();
305
+ registerEntityCrud(r, "note", noteEntitySoftDelete, {
306
+ write: { access: { roles: ["Admin"] } },
307
+ read: { access: { openToAll: true } },
308
+ });
309
+ expect(entities["note"]).toBe(noteEntitySoftDelete);
310
+ expect(writes.map((w) => w.name)).toEqual([
311
+ "note:create",
312
+ "note:update",
313
+ "note:delete",
314
+ "note:restore",
315
+ ]);
316
+ expect(queries.map((q) => q.name)).toEqual(["note:list", "note:detail"]);
317
+ expect(writes[0]?.access).toEqual({ roles: ["Admin"] });
318
+ expect(queries[0]?.access).toEqual({ openToAll: true });
319
+ });
320
+
321
+ test("verbs.delete: false skips delete handler", () => {
322
+ const { r, writes } = createCrudRegistrarMock();
323
+ registerEntityCrud(r, "note", noteEntity, {
324
+ verbs: { delete: false },
325
+ });
326
+ expect(writes.map((w) => w.name)).toEqual(["note:create", "note:update"]);
327
+ });
328
+
329
+ test("no options → handlers have no access (no implicit openToAll)", () => {
330
+ const { r, writes, queries } = createCrudRegistrarMock();
331
+ registerEntityCrud(r, "note", noteEntity);
332
+ for (const def of [...writes, ...queries]) {
333
+ expect(def.access).toBeUndefined();
334
+ }
335
+ });
336
+
337
+ test("restore verb on entity without softDelete throws", () => {
338
+ const { r } = createCrudRegistrarMock();
339
+ expect(() => registerEntityCrud(r, "note", noteEntity, { verbs: { restore: true } })).toThrow(
340
+ /softDelete/,
341
+ );
342
+ });
343
+
344
+ test("registerEntity: false skips r.entity when already registered", () => {
345
+ const { r, entities, writes } = createCrudRegistrarMock();
346
+ r.entity("note", noteEntity);
347
+ registerEntityCrud(r, "note", noteEntity, {
348
+ registerEntity: false,
349
+ verbs: { update: false, delete: false, restore: false, list: false, detail: false },
350
+ });
351
+ expect(Object.keys(entities)).toEqual(["note"]);
352
+ expect(writes.map((w) => w.name)).toEqual(["note:create"]);
353
+ });
354
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { z } from "zod";
3
+ import { defineFeature } from "../define-feature";
4
+ import type { DeclarativeEventMigration, EventUpcastCtx } from "../types";
5
+
6
+ // Transforms under test are pure — ctx is never touched.
7
+ const upcastCtx = {} as EventUpcastCtx;
8
+
9
+ function compile(spec: DeclarativeEventMigration) {
10
+ const feature = defineFeature("billing", (r) => {
11
+ r.defineEvent("invoicePaid", z.unknown(), {
12
+ version: 2,
13
+ migrations: [{ fromVersion: 1, toVersion: 2, transform: spec }],
14
+ });
15
+ });
16
+ const def = feature.eventMigrations["invoicePaid"]?.[0];
17
+ if (!def) throw new Error("migration not registered");
18
+ return (payload: unknown) => def.transform(payload, upcastCtx);
19
+ }
20
+
21
+ describe("declarative eventMigration", () => {
22
+ test("rename moves the value and drops the old key; missing source is a no-op", async () => {
23
+ const run = compile({ rename: { amount: "amountCents", missing: "other" } });
24
+ expect(await run({ amount: 5, currency: "EUR" })).toEqual({
25
+ amountCents: 5,
26
+ currency: "EUR",
27
+ });
28
+ });
29
+
30
+ test("default fills absent keys only — existing values win", async () => {
31
+ const run = compile({ default: { currency: "EUR", status: "open" } });
32
+ expect(await run({ currency: "CHF" })).toEqual({ currency: "CHF", status: "open" });
33
+ });
34
+
35
+ test("map runs after rename on the new key; absent keys are skipped", async () => {
36
+ const run = compile({
37
+ rename: { amount: "amountCents" },
38
+ map: { amountCents: (v) => Math.round(Number(v) * 100), missing: () => "never" },
39
+ });
40
+ expect(await run({ amount: 19.99 })).toEqual({ amountCents: 1999 });
41
+ });
42
+
43
+ test("non-object payload fails loud", () => {
44
+ const run = compile({ default: { a: 1 } });
45
+ expect(() => run("not-an-object")).toThrow(/object payload/);
46
+ expect(() => run([1, 2])).toThrow(/object payload/);
47
+ });
48
+
49
+ test("two rename sources mapping to the same target fail loud at registration", () => {
50
+ expect(() => compile({ rename: { amount: "total", subtotal: "total" } })).toThrow(
51
+ /rename collision.*"total"/,
52
+ );
53
+ });
54
+
55
+ test("imperative function variant is stored untouched", () => {
56
+ const fn = (payload: unknown) => payload;
57
+ const feature = defineFeature("billing", (r) => {
58
+ r.defineEvent("invoicePaid", z.unknown(), {
59
+ version: 2,
60
+ migrations: [{ fromVersion: 1, toVersion: 2, transform: fn }],
61
+ });
62
+ });
63
+ expect(feature.eventMigrations["invoicePaid"]?.[0]?.transform).toBe(fn);
64
+ });
65
+ });
@@ -0,0 +1,24 @@
1
+ // Type-only fixture for the KumikoEventTypeMap declaration-merging channel
2
+ // (#1394). event-type-map.ts moved the marker interfaces behind a re-export
3
+ // chain (kumiko-types -> engine/types/event-type-map.ts shim -> engine
4
+ // barrel), while the codegen augmentation channel
5
+ // (dev-server/src/codegen/render.ts) still targets
6
+ // `declare module "@cosmicdrift/kumiko-framework/engine"`. If a future
7
+ // change breaks that re-export chain, augmentation silently stops merging
8
+ // and every event payload falls back to `unknown` with no compile error —
9
+ // this test only catches it because tsc checks the `declare module` below
10
+ // against the SAME specifier the codegen emits.
11
+ import { expectTypeOf, test } from "bun:test";
12
+ import type { KumikoEventTypeMap } from "@cosmicdrift/kumiko-framework/engine";
13
+
14
+ declare module "@cosmicdrift/kumiko-framework/engine" {
15
+ interface KumikoEventTypeMap {
16
+ "event-type-map-fixture:probe.created": { readonly probe: true };
17
+ }
18
+ }
19
+
20
+ test("augmenting KumikoEventTypeMap via the engine specifier merges into the relocated interface", () => {
21
+ expectTypeOf<KumikoEventTypeMap["event-type-map-fixture:probe.created"]>().toEqualTypeOf<{
22
+ readonly probe: true;
23
+ }>();
24
+ });
@@ -0,0 +1,123 @@
1
+ // r.extendEntityProjection — registrar/registry contracts (#759). The
2
+ // runtime rebuild-replay behaviour is proven in
3
+ // custom-fields/__tests__/custom-fields.integration.test.ts.
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { z } from "zod";
7
+ import { defineFeature } from "../define-feature";
8
+ import { createEntity, createTextField } from "../factories";
9
+ import { createRegistry } from "../registry";
10
+
11
+ function exampleEntity(name = "unit") {
12
+ return createEntity({
13
+ table: name,
14
+ fields: { name: createTextField() },
15
+ });
16
+ }
17
+
18
+ const noopApply = async (): Promise<void> => {};
19
+
20
+ describe("r.extendEntityProjection — registration", () => {
21
+ test("merges apply keys + extraSources into the implicit projection", () => {
22
+ const feature = defineFeature("test", (r) => {
23
+ r.entity("unit", exampleEntity());
24
+ const setEvent = r.defineEvent("custom-field-set", z.object({ fieldKey: z.string() }));
25
+ r.extendEntityProjection("unit", {
26
+ sources: ["field-definition"],
27
+ apply: { [setEvent.name]: noopApply },
28
+ });
29
+ });
30
+ const registry = createRegistry([feature]);
31
+ const projection = registry.getAllProjections().get("test:projection:unit-entity");
32
+ expect(projection).toBeDefined();
33
+ expect(projection?.apply["test:event:custom-field-set"]).toBe(noopApply);
34
+ expect(projection?.apply["unit.created"]).toBeDefined();
35
+ expect(projection?.extraSources).toEqual(["field-definition"]);
36
+ expect(projection?.source).toBe("unit");
37
+ });
38
+
39
+ test("extension source equal to the entity name is not duplicated into extraSources", () => {
40
+ const feature = defineFeature("test", (r) => {
41
+ r.entity("unit", exampleEntity());
42
+ const setEvent = r.defineEvent("custom-field-set", z.object({ fieldKey: z.string() }));
43
+ r.extendEntityProjection("unit", {
44
+ sources: ["unit"],
45
+ apply: { [setEvent.name]: noopApply },
46
+ });
47
+ });
48
+ const registry = createRegistry([feature]);
49
+ const projection = registry.getAllProjections().get("test:projection:unit-entity");
50
+ expect(projection?.extraSources).toBeUndefined();
51
+ });
52
+
53
+ test("auto-verb of a registered extraSources entity is a valid apply-key", () => {
54
+ const feature = defineFeature("test", (r) => {
55
+ r.entity("unit", exampleEntity());
56
+ r.entity("field-definition", exampleEntity("field_definitions"));
57
+ r.extendEntityProjection("unit", {
58
+ sources: ["field-definition"],
59
+ apply: { "field-definition.deleted": noopApply },
60
+ });
61
+ });
62
+ const registry = createRegistry([feature]);
63
+ const projection = registry.getAllProjections().get("test:projection:unit-entity");
64
+ expect(projection?.apply["field-definition.deleted"]).toBe(noopApply);
65
+ });
66
+
67
+ test("registration order is free: extension before r.entity in the same feature", () => {
68
+ const feature = defineFeature("test", (r) => {
69
+ const setEvent = r.defineEvent("custom-field-set", z.object({ fieldKey: z.string() }));
70
+ r.extendEntityProjection("unit", { apply: { [setEvent.name]: noopApply } });
71
+ r.entity("unit", exampleEntity());
72
+ });
73
+ const registry = createRegistry([feature]);
74
+ const projection = registry.getAllProjections().get("test:projection:unit-entity");
75
+ expect(projection?.apply["test:event:custom-field-set"]).toBe(noopApply);
76
+ });
77
+
78
+ test("empty apply throws at registration time", () => {
79
+ expect(() =>
80
+ defineFeature("test", (r) => {
81
+ r.entity("unit", exampleEntity());
82
+ r.extendEntityProjection("unit", { apply: {} });
83
+ }),
84
+ ).toThrow(/no apply handlers/);
85
+ });
86
+ });
87
+
88
+ describe("r.extendEntityProjection — registry-build validation", () => {
89
+ test("unknown entity fails at registry build", () => {
90
+ const feature = defineFeature("test", (r) => {
91
+ r.entity("unit", exampleEntity());
92
+ const setEvent = r.defineEvent("custom-field-set", z.object({ fieldKey: z.string() }));
93
+ r.extendEntityProjection("typo-entity", { apply: { [setEvent.name]: noopApply } });
94
+ });
95
+ expect(() => createRegistry([feature])).toThrow(/no r\.entity/);
96
+ });
97
+
98
+ test("apply-key collision with a lifecycle apply fails at registry build", () => {
99
+ const feature = defineFeature("test", (r) => {
100
+ r.entity("unit", exampleEntity());
101
+ r.extendEntityProjection("unit", { apply: { "unit.created": noopApply } });
102
+ });
103
+ expect(() => createRegistry([feature])).toThrow(/collides/);
104
+ });
105
+
106
+ test("apply-key collision between two extensions fails at registry build", () => {
107
+ const feature = defineFeature("test", (r) => {
108
+ r.entity("unit", exampleEntity());
109
+ const setEvent = r.defineEvent("custom-field-set", z.object({ fieldKey: z.string() }));
110
+ r.extendEntityProjection("unit", { apply: { [setEvent.name]: noopApply } });
111
+ r.extendEntityProjection("unit", { apply: { [setEvent.name]: noopApply } });
112
+ });
113
+ expect(() => createRegistry([feature])).toThrow(/collides/);
114
+ });
115
+
116
+ test("unknown apply-key (neither auto-verb nor domain event) fails at registry build", () => {
117
+ const feature = defineFeature("test", (r) => {
118
+ r.entity("unit", exampleEntity());
119
+ r.extendEntityProjection("unit", { apply: { "customField.set": noopApply } });
120
+ });
121
+ expect(() => createRegistry([feature])).toThrow(/no such event/);
122
+ });
123
+ });
@@ -1,17 +1,12 @@
1
1
  // Unit-Tests für die neuen Time-Field-Factories
2
- // (createTimestampField, createTzField, locatedTimestamp).
2
+ // (createTimestampField, createTzField, createLocatedTimestampField).
3
3
  //
4
4
  // Test-Fokus: korrektes Field-Shape + locatedBy-Marker-Verdrahtung. Die
5
5
  // echte TZ-Konvertierung (Wall-Clock ↔ UTC) testen wir später beim
6
6
  // DB-Wrapper-Schritt.
7
7
 
8
8
  import { describe, expect, test } from "bun:test";
9
- import {
10
- createLocatedTimestampField,
11
- createTimestampField,
12
- createTzField,
13
- locatedTimestamp,
14
- } from "../factories";
9
+ import { createLocatedTimestampField, createTimestampField, createTzField } from "../factories";
15
10
 
16
11
  describe("createTimestampField", () => {
17
12
  test("default-Form ist nicht-required UTC-Instant ohne locatedBy", () => {
@@ -57,65 +52,6 @@ describe("createTzField", () => {
57
52
  });
58
53
  });
59
54
 
60
- describe("locatedTimestamp(name) Helper", () => {
61
- test("erzeugt korrektes Pair aus <name>At + <name>Tz mit locatedBy-Verdrahtung", () => {
62
- const fields = locatedTimestamp("pickup");
63
- expect(fields).toEqual({
64
- pickupAt: { type: "timestamp", locatedBy: "pickupTz" },
65
- pickupTz: { type: "tz" },
66
- });
67
- });
68
-
69
- test("required-Override propagiert auf BEIDE Felder", () => {
70
- const fields = locatedTimestamp("delivery", { required: true });
71
- expect(fields).toEqual({
72
- deliveryAt: { type: "timestamp", locatedBy: "deliveryTz", required: true },
73
- deliveryTz: { type: "tz", required: true },
74
- });
75
- });
76
-
77
- test("access-Override propagiert auf BEIDE Felder (Field-Level Read-Access)", () => {
78
- const fields = locatedTimestamp("internal", {
79
- access: { read: ["Dispatcher"] },
80
- });
81
- expect(fields).toEqual({
82
- internalAt: {
83
- type: "timestamp",
84
- locatedBy: "internalTz",
85
- access: { read: ["Dispatcher"] },
86
- },
87
- internalTz: { type: "tz", access: { read: ["Dispatcher"] } },
88
- });
89
- });
90
-
91
- test("locatedBy-Marker zeigt immer auf das EIGENE Tz-Feld (nicht auf einen anderen Namen)", () => {
92
- // Das ist der Kern des Patterns — wenn die zwei Felder nicht
93
- // konsistent verdrahtet sind, fliegt der Boot-Validator (kommt in
94
- // späterer Iteration). Hier prüfen wir die Helper-Garantie.
95
- for (const name of ["a", "x_y", "long_field_name"]) {
96
- const fields = locatedTimestamp(name);
97
- const at = fields[`${name}At`];
98
- if (at?.type !== "timestamp") throw new Error("at field missing");
99
- expect(at.locatedBy).toBe(`${name}Tz`);
100
- }
101
- });
102
-
103
- test("Spread in createEntity-fields Kompositions-tauglich", () => {
104
- // Realer Use-Case: pickup + delivery in einer Entity, plus normale Felder.
105
- const entityFields = {
106
- ...locatedTimestamp("pickup"),
107
- ...locatedTimestamp("delivery"),
108
- // Kein Konflikt zwischen den beiden Pairs.
109
- };
110
- expect(Object.keys(entityFields).sort()).toEqual([
111
- "deliveryAt",
112
- "deliveryTz",
113
- "pickupAt",
114
- "pickupTz",
115
- ]);
116
- });
117
- });
118
-
119
55
  describe("createLocatedTimestampField (Phase A — atomarer Field-Type)", () => {
120
56
  test("default-Form ist nicht-required mit type 'locatedTimestamp'", () => {
121
57
  expect(createLocatedTimestampField()).toEqual({
@@ -0,0 +1,28 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { defineFeature } from "../define-feature";
3
+ import { createEntity, createTextField } from "../factories";
4
+
5
+ const taskEntity = createEntity({
6
+ table: "crud_shorthand_tasks",
7
+ fields: { title: createTextField({ required: true }) },
8
+ softDelete: true,
9
+ });
10
+
11
+ describe("r.crud", () => {
12
+ test("registers the entity + full CRUD handler set, same as registerEntityCrud", () => {
13
+ const write = { access: { roles: ["Admin"] } } as const;
14
+ const read = { access: { openToAll: true } } as const;
15
+
16
+ const feature = defineFeature("via-crud", (r) => {
17
+ r.crud("task", taskEntity, { write, read });
18
+ });
19
+
20
+ expect(Object.keys(feature.entities ?? {})).toEqual(["task"]);
21
+ expect(Object.keys(feature.writeHandlers ?? {}).sort()).toEqual(
22
+ ["task:create", "task:delete", "task:restore", "task:update"].sort(),
23
+ );
24
+ expect(Object.keys(feature.queryHandlers ?? {}).sort()).toEqual(
25
+ ["task:detail", "task:list"].sort(),
26
+ );
27
+ });
28
+ });
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { ConfigScopes } from "../constants";
3
- import { buildManifestFromRegistry, createRegistry, defineFeature } from "../index";
3
+ import {
4
+ buildManifestFromRegistry,
5
+ createRegistry,
6
+ createSystemConfig,
7
+ defineFeature,
8
+ } from "../index";
4
9
 
5
10
  const boolKey = {
6
11
  type: "boolean",
@@ -104,4 +109,29 @@ describe("buildManifestFromRegistry — deterministic codepoint sort (#330)", ()
104
109
  });
105
110
  expect(noHints && "uiHints" in noHints).toBe(false);
106
111
  });
112
+
113
+ // #1039: backing:"secrets" routes the value through the envelope-encrypted
114
+ // secrets store — the manifest must report encrypted:true even without an
115
+ // explicit `encrypted` flag, so generated docs don't mislabel Stripe-style
116
+ // secret-backed config keys as plaintext. (createConfigKey never emits an
117
+ // explicit `encrypted: false` — falsy opts.encrypted just omits the field —
118
+ // so there's no "explicit false wins" case to pin here.)
119
+ test("encrypted flag — backing:secrets implies encrypted:true without an explicit flag", () => {
120
+ const feature = defineFeature("demo", (r) => {
121
+ r.config({
122
+ keys: {
123
+ "api-key": createSystemConfig("text", { backing: "secrets" }),
124
+ "plain-flag": createSystemConfig("boolean", {}),
125
+ },
126
+ });
127
+ });
128
+ const registry = createRegistry([feature]);
129
+
130
+ const manifest = buildManifestFromRegistry(registry, { source: "test" });
131
+ const demo = manifest.features.find((f) => f.name === "demo");
132
+ const byKey = (key: string) => demo?.configKeys.find((k) => k.key === key);
133
+
134
+ expect(byKey("api-key")?.encrypted).toBe(true);
135
+ expect(byKey("plain-flag")?.encrypted).toBe(false);
136
+ });
107
137
  });