@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
@@ -47,6 +47,38 @@ describe("buildAppSchema", () => {
47
47
  expect(fleet?.entities["vehicle"]).toBeDefined();
48
48
  });
49
49
 
50
+ // #1059: r.translations({keys}) must flow into FeatureSchema.translations
51
+ // BYTE-IDENTICAL — nav/screen labels resolve these keys verbatim via
52
+ // t(label) client-side, so any re-prefixing (like the registry's internal
53
+ // mergedTranslations does for features whose keys already carry a colon)
54
+ // would break the lookup. Mirrors cap-counter's real shape exactly: nav
55
+ // label references its own already-qualified translation key.
56
+ test("r.translations landet verbatim in FeatureSchema.translations, ohne Re-Prefixing", () => {
57
+ const capCounterLikeFeature = defineFeature("cap-counter", (r) => {
58
+ r.nav({ id: "cap-list", label: "cap-counter:nav.cap-list" });
59
+ r.translations({
60
+ keys: {
61
+ "cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
62
+ },
63
+ });
64
+ });
65
+
66
+ const app = buildAppSchema(createRegistry([capCounterLikeFeature]));
67
+ const feature = app.features.find((f) => f.featureName === "cap-counter");
68
+
69
+ expect(feature?.translations).toEqual({
70
+ "cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
71
+ });
72
+ });
73
+
74
+ test("Feature ohne r.translations lässt das Feld weg (omit-undefined-Pattern)", () => {
75
+ const f = defineFeature("bare", (r) => {
76
+ r.nav({ id: "x", label: "X" });
77
+ });
78
+ const app = buildAppSchema(createRegistry([f]));
79
+ expect(app.features[0]?.translations).toBeUndefined();
80
+ });
81
+
50
82
  test("Workspaces — definition + aufgelöste navMembers landen auf AppSchema-Ebene", () => {
51
83
  const ordersFeature = defineFeature("orders", (r) => {
52
84
  r.nav({ id: "list", label: "List" });
@@ -111,6 +143,35 @@ describe("buildAppSchema", () => {
111
143
  expect(idField?.["type"]).toBe("text"); // type kommt durch
112
144
  });
113
145
 
146
+ test("Reference-Field: entity + labelField + multiple überleben die Projection", () => {
147
+ // Regression: ohne diese Properties im Client-Schema baut der ReferenceInput
148
+ // die Options-Query als `<feature>:query::list` (leeres refEntity) → 404 →
149
+ // Dropdown zeigt „Keine Treffer" obwohl die referenzierte Entity Rows hat.
150
+ const entity = {
151
+ fields: {
152
+ name: { type: "text" },
153
+ parentId: { type: "reference", entity: "component", labelField: "name" },
154
+ tags: { type: "reference", entity: "tag", labelField: "label", multiple: true },
155
+ },
156
+ } as unknown as EntityDefinition;
157
+
158
+ const f = defineFeature("ent", (r) => {
159
+ r.entity("thing", entity);
160
+ });
161
+ const app = buildAppSchema(createRegistry([f]));
162
+ const fields = (
163
+ app.features[0]?.entities["thing"] as unknown as {
164
+ fields: Record<string, Record<string, unknown>>;
165
+ }
166
+ ).fields;
167
+
168
+ expect(fields["parentId"]?.["type"]).toBe("reference");
169
+ expect(fields["parentId"]?.["entity"]).toBe("component");
170
+ expect(fields["parentId"]?.["labelField"]).toBe("name");
171
+ expect(fields["tags"]?.["entity"]).toBe("tag");
172
+ expect(fields["tags"]?.["multiple"]).toBe(true);
173
+ });
174
+
114
175
  test("JSON-Safety: literal Defaults bleiben erhalten", () => {
115
176
  const entity = {
116
177
  fields: {
@@ -155,6 +216,27 @@ describe("buildAppSchema", () => {
155
216
  expect(fields["name"]?.["filterable"]).toBeUndefined();
156
217
  });
157
218
 
219
+ test("searchable kommt ins Client-Schema (steuert den EntityList-Default, #1194)", () => {
220
+ const entity = {
221
+ fields: {
222
+ title: { type: "text", searchable: true },
223
+ name: { type: "text" },
224
+ },
225
+ } as unknown as EntityDefinition;
226
+ const f = defineFeature("ent", (r) => {
227
+ r.entity("thing", entity);
228
+ });
229
+ const app = buildAppSchema(createRegistry([f]));
230
+ const fields = (
231
+ app.features[0]?.entities["thing"] as unknown as {
232
+ fields: Record<string, Record<string, unknown>>;
233
+ }
234
+ ).fields;
235
+ expect(fields["title"]?.["searchable"]).toBe(true);
236
+ // Fields without searchable don't carry the key (no false-litter).
237
+ expect(fields["name"]?.["searchable"]).toBeUndefined();
238
+ });
239
+
158
240
  test("AppSchema ist via JSON.stringify roundtrip-sicher", () => {
159
241
  // Echter Smoke-Test des Vertrags — wenn jemand in den project-
160
242
  // Helper eine Function reinschmuggelt, würde das hier brennen.
@@ -90,6 +90,9 @@ describe("buildConfigFeatureSchema — structure", () => {
90
90
  expect(sys?.screen).toBeUndefined();
91
91
  expect(sys?.parent).toBeUndefined();
92
92
  expect(ten?.label).toBe("config.settings.tenant");
93
+ expect(sys?.icon).toBe("shield");
94
+ expect(ten?.icon).toBe("building");
95
+ expect(usr?.icon).toBe("user");
93
96
  expect((sys?.order ?? 0) < (ten?.order ?? 0) && (ten?.order ?? 0) < (usr?.order ?? 0)).toBe(
94
97
  true,
95
98
  );
@@ -256,6 +259,27 @@ describe("buildConfigFeatureSchema — access + workspace", () => {
256
259
  expect(out.navs.find((n) => n.id === "audience-tenant")?.access).toEqual({ openToAll: true });
257
260
  });
258
261
 
262
+ test("openToAll collapse holds regardless of key declaration order (516/2)", () => {
263
+ // The test above only ever puts the openToAll key first, so
264
+ // unionAccessRules's short-circuit (`if ("openToAll" in rule) return`)
265
+ // never actually has to walk past a role-restricted rule to prove it —
266
+ // `gated` here comes FIRST so a hypothetical short-circuit-on-first-only
267
+ // implementation would fail this one even though it passed the other.
268
+ const mixedWriteReversed = defineFeature("mixedwritereversed", (r) => {
269
+ r.config({
270
+ keys: {
271
+ gated: createTenantConfig("text", { mask: { title: "mwr.gated" } }), // default admin write
272
+ open: createTenantConfig("text", { write: access.all, mask: { title: "mwr.open" } }),
273
+ },
274
+ });
275
+ });
276
+ const out = buildConfigFeatureSchema(createRegistry([mixedWriteReversed]));
277
+ const screen = out.screens.find((s) => s.id === "mixedwritereversed-tenant");
278
+ if (screen?.type !== "configEdit")
279
+ throw new Error('expected configEdit screen "mixedwritereversed-tenant"');
280
+ expect(screen.access).toEqual({ openToAll: true });
281
+ });
282
+
259
283
  test("returns empty (no workspace) when no key opts into the hub via mask", () => {
260
284
  const plain = defineFeature("plain", (r) => {
261
285
  r.config({ keys: { secret: createSystemConfig("text", {}) } });
@@ -327,3 +351,104 @@ describe("buildConfigFeatureSchema — access + workspace", () => {
327
351
  expect([...roles].sort()).toEqual(["Admin", "SystemAdmin", "TenantAdmin"]);
328
352
  });
329
353
  });
354
+
355
+ describe("buildConfigFeatureSchema — cross-feature group", () => {
356
+ test("two features sharing a group bucket under one screen/nav per scope, not two", () => {
357
+ const timeTracking = defineFeature("time-tracking", (r) => {
358
+ r.config({
359
+ keys: {
360
+ enabled: createTenantConfig("boolean", {
361
+ group: "bmc-settings",
362
+ mask: { title: "bmc.time-tracking-enabled" },
363
+ }),
364
+ },
365
+ });
366
+ });
367
+ const autoProtocol = defineFeature("auto-protocol", (r) => {
368
+ r.config({
369
+ keys: {
370
+ enabled: createTenantConfig("boolean", {
371
+ group: "bmc-settings",
372
+ mask: { title: "bmc.auto-protocol-enabled" },
373
+ }),
374
+ },
375
+ });
376
+ });
377
+
378
+ const out = buildConfigFeatureSchema(createRegistry([timeTracking, autoProtocol]));
379
+ expect(out.screens.filter((s) => s.id === "bmc-settings-tenant")).toHaveLength(1);
380
+ expect(out.navs.filter((n) => n.id === "bmc-settings-tenant")).toHaveLength(1);
381
+
382
+ const screen = out.screens.find((s) => s.id === "bmc-settings-tenant");
383
+ if (screen?.type !== "configEdit") throw new Error("expected configEdit screen");
384
+ // Neither feature owns the group name, so field ids are prefixed by the
385
+ // owning feature to stay unique — the whole point of the collision guard.
386
+ expect(screen.configKeys).toEqual({
387
+ "time-tracking-enabled": "time-tracking:config:enabled",
388
+ "auto-protocol-enabled": "auto-protocol:config:enabled",
389
+ });
390
+ });
391
+
392
+ test("same group + colliding owner-prefixed field id throws instead of silently overwriting", () => {
393
+ // "a-b" owner + shortKey "x" and "a" owner + shortKey "b-x" both produce
394
+ // the prefixed field id "a-b-x" — the exact clobbering hazard the
395
+ // owner-prefix scheme must catch, not just avoid in the common case.
396
+ const ab = defineFeature("a-b", (r) => {
397
+ r.config({
398
+ keys: { x: createTenantConfig("boolean", { group: "shared", mask: { title: "ab.x" } }) },
399
+ });
400
+ });
401
+ const a = defineFeature("a", (r) => {
402
+ r.config({
403
+ keys: {
404
+ bX: createTenantConfig("boolean", { group: "shared", mask: { title: "a.b-x" } }),
405
+ },
406
+ });
407
+ });
408
+ expect(() => buildConfigFeatureSchema(createRegistry([ab, a]))).toThrow(
409
+ /both resolve to field id "a-b-x"/,
410
+ );
411
+ });
412
+
413
+ test("group unset → behavior unchanged (buckets under the owning feature, plain shortKey)", () => {
414
+ const feature = defineFeature("ungrouped", (r) => {
415
+ r.config({
416
+ keys: { flag: createTenantConfig("boolean", { mask: { title: "u.flag" } }) },
417
+ });
418
+ });
419
+ const out = buildConfigFeatureSchema(createRegistry([feature]));
420
+ const screen = out.screens.find((s) => s.id === "ungrouped-tenant");
421
+ if (screen?.type !== "configEdit") throw new Error("expected configEdit screen");
422
+ expect(screen.configKeys).toEqual({ flag: "ungrouped:config:flag" });
423
+ });
424
+
425
+ test("invalid (non-kebab) group value throws at schema-build time", () => {
426
+ const feature = defineFeature("badgroup", (r) => {
427
+ r.config({
428
+ keys: {
429
+ flag: createTenantConfig("boolean", {
430
+ group: "Not Kebab!",
431
+ mask: { title: "bg.flag" },
432
+ }),
433
+ },
434
+ });
435
+ });
436
+ expect(() => buildConfigFeatureSchema(createRegistry([feature]))).toThrow(/kebab-case/);
437
+ });
438
+
439
+ test("invalid (non-kebab) group value throws even on a maskless key", () => {
440
+ // Maskless keys never appear in the hub — group is a no-op for them at
441
+ // runtime — but the type contract still promises kebab-case is
442
+ // boot-validated regardless of `mask`. collectMaskedKeys used to check
443
+ // the mask-gate before the group check, so a maskless key with a bad
444
+ // group silently passed boot; the check now runs before the mask-gate.
445
+ const feature = defineFeature("badgroupmaskless", (r) => {
446
+ r.config({
447
+ keys: {
448
+ flag: createTenantConfig("boolean", { group: "Not Kebab!" }),
449
+ },
450
+ });
451
+ });
452
+ expect(() => buildConfigFeatureSchema(createRegistry([feature]))).toThrow(/kebab-case/);
453
+ });
454
+ });
@@ -95,11 +95,7 @@ describe("buildTarget — Compile-Time-Safety (verified via @ts-expect-error)",
95
95
 
96
96
  test("falsche args-shape wird vom Compiler abgelehnt", () => {
97
97
  // @ts-expect-error — slug muss string sein, nicht number
98
- buildTarget({
99
- target: textContentStub,
100
- action: "edit",
101
- args: { slug: 42 },
102
- });
98
+ buildTarget({ target: textContentStub, action: "edit", args: { slug: 42 } });
103
99
  // Runtime: korrekt-typed args liefern valid TargetRef
104
100
  const ref = buildTarget({
105
101
  target: textContentStub,
@@ -110,12 +106,8 @@ describe("buildTarget — Compile-Time-Safety (verified via @ts-expect-error)",
110
106
  });
111
107
 
112
108
  test("args bei NoArgs-Action wird vom Compiler abgelehnt", () => {
113
- buildTarget({
114
- target: textContentStub,
115
- action: "list",
116
- // @ts-expect-error — list hat keine args, args-Feld nicht erlaubt
117
- args: { x: 1 },
118
- });
109
+ // @ts-expect-error — list hat keine args, args-Feld nicht erlaubt
110
+ buildTarget({ target: textContentStub, action: "list", args: { x: 1 } });
119
111
  // Runtime: NoArgs-Action ohne args-Feld liefert valid TargetRef
120
112
  const ref = buildTarget({ target: textContentStub, action: "list" });
121
113
  expect("args" in ref).toBe(false);
@@ -1,19 +1,32 @@
1
1
  import { afterAll, beforeAll, describe, expect, it, mock } from "bun:test";
2
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
 
5
- // Bun.Glob is only used by scanForCandidates / runCodemod, not by
6
- // analyzeFile or convertFile. Those higher-level functions are tested
7
- // in the integration suite; unit tests here focus on file-level
8
- // operations which don't need Glob. A stub class prevents import
9
- // errors in bun (which runs on Node, not Bun).
5
+ // Bun.Glob is only used by scanForCandidates / runCodemod. Emulate
6
+ // **/*.write.ts via node:fs so scan/convert paths stay testable.
7
+ function collectWriteTsRelPaths(rootDir: string, dir = rootDir, prefix = ""): string[] {
8
+ const out: string[] = [];
9
+ for (const name of readdirSync(dir)) {
10
+ const rel = prefix ? `${prefix}/${name}` : name;
11
+ const full = join(dir, name);
12
+ if (statSync(full).isDirectory()) {
13
+ out.push(...collectWriteTsRelPaths(rootDir, full, rel));
14
+ } else if (name.endsWith(".write.ts")) {
15
+ out.push(rel);
16
+ }
17
+ }
18
+ return out;
19
+ }
20
+
10
21
  mock.module("bun", () => {
11
- class StubGlob {
12
- scanSync(): never {
13
- throw new Error("Bun.Glob is not available in unit-test mode");
22
+ class FsGlob {
23
+ constructor(private readonly pattern: string) {}
24
+ scanSync(rootDir: string): string[] {
25
+ if (this.pattern !== "**/*.write.ts") return [];
26
+ return collectWriteTsRelPaths(rootDir).sort();
14
27
  }
15
28
  }
16
- return { Glob: StubGlob };
29
+ return { Glob: FsGlob };
17
30
  });
18
31
 
19
32
  import {
@@ -21,6 +34,8 @@ import {
21
34
  analyzeHandlerArrow,
22
35
  convertFile,
23
36
  generatePerformBlock,
37
+ runCodemod,
38
+ scanForCandidates,
24
39
  } from "../codemod/index";
25
40
 
26
41
  const tmpDir = join(__dirname, "__codemod_fixtures__");
@@ -31,6 +46,20 @@ function writeFixture(name: string, content: string): string {
31
46
  return p;
32
47
  }
33
48
 
49
+ function writeFixtureIn(dir: string, name: string, content: string): string {
50
+ const p = join(dir, name);
51
+ mkdirSync(join(p, ".."), { recursive: true });
52
+ writeFileSync(p, content, "utf8");
53
+ return p;
54
+ }
55
+
56
+ function freshScanRoot(name: string): string {
57
+ const dir = join(tmpDir, name);
58
+ rmSync(dir, { recursive: true, force: true });
59
+ mkdirSync(dir, { recursive: true });
60
+ return dir;
61
+ }
62
+
34
63
  beforeAll(() => {
35
64
  rmSync(tmpDir, { recursive: true, force: true });
36
65
  mkdirSync(tmpDir, { recursive: true });
@@ -103,14 +132,14 @@ export const myHandler = defineWriteHandler({
103
132
  `;
104
133
 
105
134
  const alreadyPipelineContent = `\
106
- import { access, defineWriteHandler, pipeline } from "@cosmicdrift/kumiko-framework/engine";
135
+ import { access, defineWriteHandler, stepsPipeline } from "@cosmicdrift/kumiko-framework/engine";
107
136
  import { z } from "zod";
108
137
 
109
138
  export const myHandler = defineWriteHandler({
110
139
  name: "test:pipeline",
111
140
  schema: z.object({}),
112
141
  access: { roles: access.authenticated },
113
- perform: pipeline(({ event, r }) => [
142
+ perform: stepsPipeline(({ event, r }) => [
114
143
  r.step.return((ctx) => ({ isSuccess: true, data: { ok: true } })),
115
144
  ]),
116
145
  });
@@ -173,14 +202,14 @@ describe("analyzeFile", () => {
173
202
 
174
203
  describe("convertFile", () => {
175
204
  describe("static return handler", () => {
176
- it("converts handler to perform: pipeline(...)", async () => {
205
+ it("converts handler to perform: stepsPipeline(...)", async () => {
177
206
  const p = writeFixture("static-convert.write.ts", staticReturnContent);
178
207
  const result = await convertFile(p);
179
208
  expect(result.status).toBe("converted");
180
209
  const content = readFileSync(p, "utf8");
181
- expect(content).toContain("perform: pipeline");
210
+ expect(content).toContain("perform: stepsPipeline");
182
211
  expect(content).toContain("r.step.return");
183
- expect(content).toContain("import { access, defineWriteHandler, pipeline }");
212
+ expect(content).toContain("import { access, defineWriteHandler, stepsPipeline }");
184
213
  expect(content).not.toContain("handler:");
185
214
  });
186
215
 
@@ -198,7 +227,7 @@ export const h = defineWriteHandler({
198
227
  const result = await convertFile(p);
199
228
  expect(result.status).toBe("converted");
200
229
  const converted = readFileSync(p, "utf8");
201
- expect(converted).toContain("pipeline<typeof MySchema, unknown>");
230
+ expect(converted).toContain("stepsPipeline<typeof MySchema, unknown>");
202
231
  });
203
232
  });
204
233
 
@@ -271,12 +300,12 @@ export const h = defineWriteHandler({
271
300
  const result = await convertFile(p);
272
301
  expect(result.status).toBe("converted");
273
302
  const content = readFileSync(p, "utf8");
274
- expect(content).toContain("import { access, defineWriteHandler, pipeline }");
303
+ expect(content).toContain("import { access, defineWriteHandler, stepsPipeline }");
275
304
  });
276
305
 
277
306
  it("does not duplicate pipeline import when already present", async () => {
278
307
  const content = `\
279
- import { access, defineWriteHandler, pipeline } from "@cosmicdrift/kumiko-framework/engine";
308
+ import { access, defineWriteHandler, stepsPipeline } from "@cosmicdrift/kumiko-framework/engine";
280
309
  import { z } from "zod";
281
310
 
282
311
  export const h = defineWriteHandler({
@@ -288,7 +317,9 @@ export const h = defineWriteHandler({
288
317
  const result = await convertFile(p);
289
318
  expect(result.status).toBe("converted");
290
319
  const final = readFileSync(p, "utf8").split("\n");
291
- const pipelineImports = final.filter((l) => l.includes("import") && l.includes("pipeline"));
320
+ const pipelineImports = final.filter(
321
+ (l) => l.includes("import") && l.includes("stepsPipeline"),
322
+ );
292
323
  // Only one import line should contain "pipeline"
293
324
  expect(pipelineImports.length).toBe(1);
294
325
  });
@@ -397,6 +428,31 @@ export const h = defineWriteHandler({
397
428
  // ts-morph should handle this gracefully (parse error → no call found)
398
429
  expect(result.status).toBe("skipped");
399
430
  });
431
+
432
+ it("returns error when the path is not a readable file", async () => {
433
+ const dirPath = join(tmpDir, "dir-not-file.write.ts");
434
+ rmSync(dirPath, { recursive: true, force: true });
435
+ mkdirSync(dirPath, { recursive: true });
436
+ const result = await convertFile(dirPath);
437
+ expect(result.status).toBe("error");
438
+ expect(result.reason).toBeDefined();
439
+ });
440
+
441
+ it("verbose logs when skipping a non-trivial handler", async () => {
442
+ const p = writeFixture("verbose-skip-file.write.ts", complexContent);
443
+ const logs: string[] = [];
444
+ const origLog = console.log;
445
+ console.log = (...args: unknown[]) => {
446
+ logs.push(args.map(String).join(" "));
447
+ };
448
+ try {
449
+ const result = await convertFile(p, undefined, { verbose: true });
450
+ expect(result.status).toBe("skipped");
451
+ expect(logs.some((l) => l.includes("non-trivial handler"))).toBe(true);
452
+ } finally {
453
+ console.log = origLog;
454
+ }
455
+ });
400
456
  });
401
457
  });
402
458
 
@@ -507,20 +563,95 @@ describe("analyzeHandlerArrow", () => {
507
563
  expect(result.isGuardedCreate).toBe(true);
508
564
  expect(result.isSimpleExecutorCreate).toBe(false);
509
565
  });
566
+
567
+ it("returns non-convertible defaults for async ( without =>", () => {
568
+ const result = analyzeHandlerArrow("async (event, ctx) { return { isSuccess: true } }");
569
+ expect(result.isStaticReturn).toBe(false);
570
+ expect(result.isSimpleExecutorCreate).toBe(false);
571
+ expect(result.isSimpleExecutorUpdate).toBe(false);
572
+ expect(result.hasConditionalLogic).toBe(false);
573
+ expect(result.isExpressionBodyCreate).toBe(false);
574
+ expect(result.isGuardedCreate).toBe(false);
575
+ });
576
+ });
577
+
578
+ describe("scanForCandidates", () => {
579
+ it("returns empty list when no *.write.ts files exist", () => {
580
+ const root = freshScanRoot("scan-empty");
581
+ expect(scanForCandidates(root)).toEqual([]);
582
+ });
583
+
584
+ it("finds and classifies *.write.ts fixtures under rootDir", () => {
585
+ const root = freshScanRoot("scan-mixed");
586
+ writeFixtureIn(root, "freeform.write.ts", staticReturnContent);
587
+ writeFixtureIn(root, "already-pipeline.write.ts", alreadyPipelineContent);
588
+ writeFixtureIn(root, "nested/deep.write.ts", queryHandlerContent);
589
+
590
+ const results = scanForCandidates(root);
591
+ expect(results.map((r) => r.filePath).sort()).toEqual(
592
+ [
593
+ join(root, "already-pipeline.write.ts"),
594
+ join(root, "freeform.write.ts"),
595
+ join(root, "nested/deep.write.ts"),
596
+ ].sort(),
597
+ );
598
+ expect(results.find((r) => r.pattern === "free-form-write")?.convertible).toBe(true);
599
+ expect(results.find((r) => r.pattern === "pipeline-write")?.convertible).toBe(false);
600
+ expect(results.find((r) => r.pattern === "query-handler")?.convertible).toBe(false);
601
+ });
602
+ });
603
+
604
+ describe("runCodemod", () => {
605
+ it("returns empty report when scan finds no convertible handlers", async () => {
606
+ const root = freshScanRoot("run-empty");
607
+ const report = await runCodemod(root);
608
+ expect(report).toEqual({ results: [], converted: 0, skipped: 0, errors: 0 });
609
+ });
610
+
611
+ it("converts free-form write handlers discovered by scan", async () => {
612
+ const root = freshScanRoot("run-convert");
613
+ const p = writeFixtureIn(root, "to-convert.write.ts", staticReturnContent);
614
+ const report = await runCodemod(root);
615
+ expect(report.converted).toBe(1);
616
+ expect(report.skipped).toBe(0);
617
+ expect(report.errors).toBe(0);
618
+ expect(readFileSync(p, "utf8")).toContain("perform: stepsPipeline");
619
+ });
620
+
621
+ it("verbose logs per-file outcomes", async () => {
622
+ const root = freshScanRoot("run-verbose");
623
+ writeFixtureIn(root, "verbose-convert.write.ts", staticReturnContent);
624
+ writeFixtureIn(root, "verbose-skip.write.ts", complexContent);
625
+
626
+ const logs: string[] = [];
627
+ const origLog = console.log;
628
+ console.log = (...args: unknown[]) => {
629
+ logs.push(args.map(String).join(" "));
630
+ };
631
+ try {
632
+ const report = await runCodemod(root, { verbose: true });
633
+ expect(report.converted).toBe(1);
634
+ expect(report.skipped).toBe(1);
635
+ expect(logs.some((l) => l.includes("verbose-convert.write.ts"))).toBe(true);
636
+ expect(logs.some((l) => l.includes("verbose-skip.write.ts"))).toBe(true);
637
+ } finally {
638
+ console.log = origLog;
639
+ }
640
+ });
510
641
  });
511
642
 
512
643
  describe("generatePerformBlock", () => {
513
644
  it("generates pipeline block for static return", () => {
514
645
  const analysis = analyzeHandlerArrow("async () => ({ isSuccess: true, data: { ok: true } })");
515
646
  const block = generatePerformBlock(analysis, "", " ");
516
- expect(block).toContain("perform: pipeline(");
647
+ expect(block).toContain("perform: stepsPipeline(");
517
648
  expect(block).toContain("r.step.return((ctx) => ({ isSuccess: true, data: { ok: true } })");
518
649
  });
519
650
 
520
651
  it("generates pipeline block with schema type parameter", () => {
521
652
  const analysis = analyzeHandlerArrow("async () => ({ isSuccess: true, data: { ok: true } })");
522
653
  const block = generatePerformBlock(analysis, "typeof InvoiceSchema", " ");
523
- expect(block).toContain("pipeline<typeof InvoiceSchema, unknown>");
654
+ expect(block).toContain("stepsPipeline<typeof InvoiceSchema, unknown>");
524
655
  });
525
656
 
526
657
  it("returns null for non-convertible analysis", () => {
@@ -257,6 +257,22 @@ describe("config helpers — allowPerRequest opt-in", () => {
257
257
  });
258
258
  });
259
259
 
260
+ describe("config helpers — group (Settings-Hub namespace override)", () => {
261
+ test("group is carried when set", () => {
262
+ const key = createTenantConfig("boolean", { group: "tenant-settings" });
263
+ expect(key.group).toBe("tenant-settings");
264
+ });
265
+
266
+ test("no group → field absent (defaults to the owning feature)", () => {
267
+ expect(createTenantConfig("boolean").group).toBeUndefined();
268
+ });
269
+
270
+ test("group is available on every scope factory", () => {
271
+ expect(createSystemConfig("text", { group: "shared" }).group).toBe("shared");
272
+ expect(createUserConfig("text", { group: "shared" }).group).toBe("shared");
273
+ });
274
+ });
275
+
260
276
  describe("config helpers — computed (plan-based / derived values)", () => {
261
277
  test("computed function attaches to the definition and returns the typed value", async () => {
262
278
  const key = createTenantConfig("number", {
@@ -29,8 +29,14 @@ describe("defineFeature — bare CRUD verb → entity mapping", () => {
29
29
  });
30
30
 
31
31
  test("feature-name match wins (the fallback is the secondary path)", () => {
32
+ // Two entities (516/1): with only "note" registered, this would also
33
+ // pass via the single-entity fallback, proving nothing about the
34
+ // name-match branch specifically. A second entity ("tag") disables the
35
+ // fallback (entityKeys.length !== 1), so success here can only come
36
+ // from entities[name] matching — the branch this test claims to cover.
32
37
  const f = defineFeature("note", (r) => {
33
38
  r.entity("note", noteEntity);
39
+ r.entity("tag", tagEntity);
34
40
  r.writeHandler(bareCreate);
35
41
  });
36
42
  expect(f.handlerEntityMappings?.["create"]).toBe("note");
@@ -0,0 +1,21 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { defineRoles } from "../define-roles";
3
+
4
+ describe("defineRoles", () => {
5
+ test("maps each role name to itself", () => {
6
+ const roles = defineRoles(["Admin", "SystemAdmin", "Driver"] as const);
7
+ expect(roles.Admin).toBe("Admin");
8
+ expect(roles.SystemAdmin).toBe("SystemAdmin");
9
+ expect(roles.Driver).toBe("Driver");
10
+ });
11
+
12
+ test("returns an object with exactly the given keys", () => {
13
+ const roles = defineRoles(["A", "B"] as const);
14
+ expect(Object.keys(roles).sort()).toEqual(["A", "B"]);
15
+ });
16
+
17
+ test("an empty role list returns an empty object", () => {
18
+ const roles = defineRoles([] as const);
19
+ expect(roles).toEqual({});
20
+ });
21
+ });