@cosmicdrift/kumiko-framework 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (424) hide show
  1. package/README.md +9 -38
  2. package/package.json +21 -2
  3. package/src/__tests__/anonymous-access.integration.test.ts +185 -0
  4. package/src/__tests__/consumer-cli.integration.test.ts +172 -0
  5. package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
  6. package/src/__tests__/full-stack.integration.test.ts +1 -1
  7. package/src/__tests__/schema-cli-temporal-polyfill.test.ts +48 -0
  8. package/src/__tests__/schema-cli.integration.test.ts +50 -0
  9. package/src/__tests__/store-table.integration.test.ts +93 -0
  10. package/src/api/__tests__/api.test.ts +373 -0
  11. package/src/api/__tests__/auth-middleware-anonymous-access-boot.test.ts +40 -0
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
  13. package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +253 -0
  14. package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +222 -0
  15. package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +249 -0
  16. package/src/api/__tests__/auth-routes-mfa-verify.test.ts +202 -0
  17. package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +135 -0
  18. package/src/api/__tests__/batch.integration.test.ts +101 -11
  19. package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
  20. package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
  21. package/src/api/__tests__/jwt.test.ts +200 -1
  22. package/src/api/__tests__/login-rate-limiter-sweep.test.ts +50 -0
  23. package/src/api/__tests__/pat-scope.test.ts +36 -0
  24. package/src/api/__tests__/pii-leak-guard.integration.test.ts +103 -0
  25. package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +138 -0
  26. package/src/api/__tests__/request-id-middleware.test.ts +51 -0
  27. package/src/api/__tests__/server-boot-guards.test.ts +71 -0
  28. package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
  29. package/src/api/__tests__/sse-broker.test.ts +57 -0
  30. package/src/api/__tests__/sse-route.test.ts +4 -0
  31. package/src/api/api-constants.ts +11 -0
  32. package/src/api/auth-middleware.ts +248 -40
  33. package/src/api/auth-routes.ts +576 -95
  34. package/src/api/index.ts +13 -4
  35. package/src/api/jwt.ts +170 -11
  36. package/src/api/pat-scope.ts +14 -0
  37. package/src/api/pii-leak-guard.ts +47 -0
  38. package/src/api/request-context.ts +3 -0
  39. package/src/api/request-id-middleware.ts +2 -0
  40. package/src/api/routes.ts +169 -2
  41. package/src/api/server.ts +112 -16
  42. package/src/api/sse-broker.ts +39 -0
  43. package/src/bun-db/__tests__/PATTERN.md +0 -1
  44. package/src/bun-db/__tests__/coerce-row-temporal.test.ts +41 -0
  45. package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
  46. package/src/bun-db/__tests__/write-brand.test.ts +21 -0
  47. package/src/bun-db/connection.ts +3 -3
  48. package/src/bun-db/index.ts +2 -0
  49. package/src/bun-db/query.ts +105 -32
  50. package/src/consumer-cli.ts +134 -0
  51. package/src/crypto/__tests__/blind-index.test.ts +130 -0
  52. package/src/crypto/__tests__/event-pii.test.ts +161 -0
  53. package/src/crypto/__tests__/kek-rotation.integration.test.ts +180 -0
  54. package/src/crypto/__tests__/kms-adapter-contract.ts +134 -0
  55. package/src/crypto/__tests__/kms-adapter.contract.test.ts +4 -0
  56. package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +117 -0
  57. package/src/crypto/__tests__/pii-field-encryption.test.ts +376 -0
  58. package/src/crypto/__tests__/request-kms-cache.test.ts +74 -0
  59. package/src/crypto/__tests__/subject-resolver.test.ts +108 -0
  60. package/src/crypto/blind-index.ts +118 -0
  61. package/src/crypto/event-pii.ts +70 -0
  62. package/src/crypto/in-memory-kms-adapter.ts +50 -0
  63. package/src/crypto/index.ts +67 -0
  64. package/src/crypto/kms-adapter.ts +2 -0
  65. package/src/crypto/pg-kms-adapter.ts +295 -0
  66. package/src/crypto/pii-field-encryption.ts +248 -0
  67. package/src/crypto/request-kms-cache.ts +38 -0
  68. package/src/crypto/subject-resolver.ts +91 -0
  69. package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +191 -0
  70. package/src/db/__tests__/blind-index.integration.test.ts +248 -0
  71. package/src/db/__tests__/build-filter-where.test.ts +34 -0
  72. package/src/db/__tests__/collect-table-metas.test.ts +10 -10
  73. package/src/db/__tests__/config-seed.integration.test.ts +13 -5
  74. package/src/db/__tests__/dialect-instant.test.ts +1 -4
  75. package/src/db/__tests__/entity-field-encryption.test.ts +7 -7
  76. package/src/db/__tests__/entity-table-meta-source.test.ts +50 -0
  77. package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
  78. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +402 -0
  79. package/src/db/__tests__/event-store-executor.integration.test.ts +299 -29
  80. package/src/db/__tests__/feature-table-sources.test.ts +34 -0
  81. package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +118 -39
  82. package/src/db/__tests__/instant-to-driver-temporal.test.ts +21 -0
  83. package/src/db/__tests__/located-timestamp.test.ts +19 -0
  84. package/src/db/__tests__/migrate-generator.test.ts +21 -0
  85. package/src/db/__tests__/migrate-runner.test.ts +61 -0
  86. package/src/db/__tests__/number-field-fractional.integration.test.ts +58 -0
  87. package/src/db/__tests__/rebuild-marker.test.ts +13 -3
  88. package/src/db/__tests__/replay-migration-sql.test.ts +384 -0
  89. package/src/db/__tests__/schema-inspection.test.ts +7 -0
  90. package/src/db/__tests__/schema-migration.integration.test.ts +1 -1
  91. package/src/db/__tests__/table-builder-meta-lockstep.test.ts +39 -0
  92. package/src/db/__tests__/tenant-db-where-merge.test.ts +49 -3
  93. package/src/db/__tests__/tenant-db.integration.test.ts +6 -2
  94. package/src/db/api.ts +2 -2
  95. package/src/db/apply-entity-event.ts +18 -14
  96. package/src/db/blind-index-cleanup.ts +55 -0
  97. package/src/db/bun-provider.ts +2 -2
  98. package/src/db/collect-table-metas.ts +6 -7
  99. package/src/db/config-seed.ts +9 -9
  100. package/src/db/connection.ts +7 -12
  101. package/src/db/cursor.ts +1 -18
  102. package/src/db/dialect.ts +20 -26
  103. package/src/db/entity-field-encryption.ts +81 -24
  104. package/src/db/entity-table-meta-types.ts +2 -0
  105. package/src/db/entity-table-meta.ts +63 -90
  106. package/src/db/event-store-executor-context.ts +404 -0
  107. package/src/db/event-store-executor-read.ts +275 -0
  108. package/src/db/event-store-executor-write.ts +644 -0
  109. package/src/db/event-store-executor.ts +28 -1155
  110. package/src/db/feature-table-sources.ts +6 -5
  111. package/src/db/index.ts +20 -3
  112. package/src/db/located-timestamp.ts +4 -0
  113. package/src/db/migrate-generator.ts +39 -6
  114. package/src/db/migrate-runner.ts +107 -11
  115. package/src/db/pg-error.ts +9 -1
  116. package/src/db/postgres-provider.ts +2 -2
  117. package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +80 -0
  118. package/src/db/queries/backfill-pii.ts +277 -0
  119. package/src/db/queries/ddl.ts +45 -0
  120. package/src/db/queries/event-consumer.ts +35 -2
  121. package/src/db/queries/event-store.ts +126 -19
  122. package/src/db/queries/projection-rebuild.ts +22 -8
  123. package/src/db/queries/shadow-swap.ts +183 -0
  124. package/src/db/queries/test-stack.ts +4 -30
  125. package/src/db/query-api.ts +1 -0
  126. package/src/db/query.ts +1 -0
  127. package/src/db/rebuild-marker.ts +18 -2
  128. package/src/db/reference-data.ts +2 -3
  129. package/src/db/replay-migration-sql.ts +257 -0
  130. package/src/db/schema-inspection.ts +1 -1
  131. package/src/db/table-builder.ts +102 -71
  132. package/src/db/tenant-db.ts +15 -53
  133. package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
  134. package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
  135. package/src/engine/__tests__/boot-validator-dashboard.test.ts +237 -0
  136. package/src/engine/__tests__/boot-validator-entity-list.test.ts +104 -0
  137. package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -69
  138. package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +26 -5
  139. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +3 -19
  140. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +246 -4
  141. package/src/engine/__tests__/boot-validator.test.ts +219 -15
  142. package/src/engine/__tests__/build-app-schema.test.ts +82 -0
  143. package/src/engine/__tests__/build-config-feature-schema.test.ts +125 -0
  144. package/src/engine/__tests__/build-target.test.ts +3 -11
  145. package/src/engine/__tests__/codemod-pipeline.test.ts +152 -21
  146. package/src/engine/__tests__/config-helpers.test.ts +16 -0
  147. package/src/engine/__tests__/define-feature-entity-mapping.test.ts +6 -0
  148. package/src/engine/__tests__/define-roles.test.ts +21 -0
  149. package/src/engine/__tests__/engine.test.ts +163 -1
  150. package/src/engine/__tests__/entity-handlers.test.ts +80 -0
  151. package/src/engine/__tests__/event-migration-declarative.test.ts +65 -0
  152. package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
  153. package/src/engine/__tests__/extend-entity-projection.test.ts +123 -0
  154. package/src/engine/__tests__/factories-time.test.ts +2 -66
  155. package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
  156. package/src/engine/__tests__/feature-manifest.test.ts +31 -1
  157. package/src/engine/__tests__/field-access.test.ts +23 -1
  158. package/src/engine/__tests__/hook-phases.test.ts +5 -5
  159. package/src/engine/__tests__/membership-roles.test.ts +4 -10
  160. package/src/engine/__tests__/nav.test.ts +86 -1
  161. package/src/engine/__tests__/pipeline-engine.test.ts +9 -9
  162. package/src/engine/__tests__/pipeline-handler.integration.test.ts +18 -18
  163. package/src/engine/__tests__/pipeline-observability.integration.test.ts +2 -2
  164. package/src/engine/__tests__/pipeline-performance.integration.test.ts +3 -3
  165. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +9 -9
  166. package/src/engine/__tests__/post-query-hook.test.ts +7 -7
  167. package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
  168. package/src/engine/__tests__/registry.test.ts +144 -0
  169. package/src/engine/__tests__/schema-builder.test.ts +18 -0
  170. package/src/engine/__tests__/screen.test.ts +147 -1
  171. package/src/engine/__tests__/soft-delete-cleanup.test.ts +3 -0
  172. package/src/engine/__tests__/store-table.test.ts +229 -0
  173. package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
  174. package/src/engine/__tests__/{visual-tree-patterns.test.ts → tree-actions-patterns.test.ts} +7 -95
  175. package/src/engine/__tests__/validate-projection-allowlist.test.ts +15 -15
  176. package/src/engine/boot-validator/__tests__/config-deps.test.ts +92 -0
  177. package/src/engine/boot-validator/action-wiring.ts +149 -0
  178. package/src/engine/boot-validator/boot-check.ts +21 -0
  179. package/src/engine/boot-validator/config-deps.ts +40 -4
  180. package/src/engine/boot-validator/entity-handler.ts +20 -21
  181. package/src/engine/boot-validator/entity-list-screens.ts +88 -0
  182. package/src/engine/boot-validator/gdpr-storage.ts +0 -20
  183. package/src/engine/boot-validator/i18n-keys.ts +58 -4
  184. package/src/engine/boot-validator/index.ts +33 -12
  185. package/src/engine/boot-validator/nav.ts +125 -0
  186. package/src/engine/boot-validator/pii-retention.ts +95 -10
  187. package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +214 -191
  188. package/src/engine/boot-validator/workspaces.ts +68 -0
  189. package/src/engine/build-app-schema.ts +16 -0
  190. package/src/engine/build-config-feature-schema.ts +44 -7
  191. package/src/engine/codemod/pipeline-codemod.ts +5 -5
  192. package/src/engine/config-helpers.ts +15 -0
  193. package/src/engine/constants.ts +32 -6
  194. package/src/engine/create-app.ts +11 -0
  195. package/src/engine/define-feature.ts +95 -947
  196. package/src/engine/define-handler.ts +28 -94
  197. package/src/engine/define-workflow.ts +1 -1
  198. package/src/engine/effective-features.ts +12 -2
  199. package/src/engine/entity-handlers.ts +76 -11
  200. package/src/engine/extensions/tenant-data.ts +19 -0
  201. package/src/engine/extensions/user-data.ts +29 -2
  202. package/src/engine/factories.ts +8 -49
  203. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +26 -29
  204. package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
  205. package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
  206. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
  207. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
  208. package/src/engine/feature-ast/__tests__/parse.test.ts +1640 -160
  209. package/src/engine/feature-ast/__tests__/patch.test.ts +206 -12
  210. package/src/engine/feature-ast/__tests__/patcher.test.ts +20 -23
  211. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +380 -5
  212. package/src/engine/feature-ast/extractors/events.ts +322 -0
  213. package/src/engine/feature-ast/extractors/handlers.ts +234 -0
  214. package/src/engine/feature-ast/extractors/hooks.ts +243 -0
  215. package/src/engine/feature-ast/extractors/index.ts +34 -31
  216. package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
  217. package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
  218. package/src/engine/feature-ast/extractors/round1.ts +3 -3
  219. package/src/engine/feature-ast/extractors/round5.ts +3 -3
  220. package/src/engine/feature-ast/extractors/round6.ts +2 -36
  221. package/src/engine/feature-ast/extractors/shared.ts +64 -6
  222. package/src/engine/feature-ast/index.ts +2 -4
  223. package/src/engine/feature-ast/parse.ts +130 -23
  224. package/src/engine/feature-ast/patch.ts +39 -50
  225. package/src/engine/feature-ast/patcher.ts +37 -38
  226. package/src/engine/feature-ast/patterns.ts +50 -62
  227. package/src/engine/feature-ast/render.ts +67 -44
  228. package/src/engine/feature-builder-state.ts +168 -0
  229. package/src/engine/feature-config-events-jobs.ts +403 -0
  230. package/src/engine/feature-entity-handlers.ts +208 -0
  231. package/src/engine/feature-manifest.ts +2 -1
  232. package/src/engine/feature-ui-extensions.ts +500 -0
  233. package/src/engine/field-access.ts +13 -2
  234. package/src/engine/field-helpers.ts +31 -0
  235. package/src/engine/handler-helpers.ts +26 -0
  236. package/src/engine/hook-helpers.ts +16 -0
  237. package/src/engine/index.ts +23 -7
  238. package/src/engine/membership-roles.ts +13 -0
  239. package/src/engine/object-form.ts +27 -0
  240. package/src/engine/ownership.ts +26 -79
  241. package/src/engine/pattern-library/__tests__/library.test.ts +7 -20
  242. package/src/engine/pattern-library/library.ts +44 -1152
  243. package/src/engine/pattern-library/mixed-schemas.ts +450 -0
  244. package/src/engine/pattern-library/opaque-schemas.ts +124 -0
  245. package/src/engine/pattern-library/shared-fields.ts +75 -0
  246. package/src/engine/pattern-library/static-schemas.ts +456 -0
  247. package/src/engine/pipeline.ts +6 -11
  248. package/src/engine/registry-facade.ts +362 -0
  249. package/src/engine/registry-ingest.ts +478 -0
  250. package/src/engine/registry-state.ts +388 -0
  251. package/src/engine/registry-validate.ts +642 -0
  252. package/src/engine/registry.ts +78 -1658
  253. package/src/engine/run-pipeline.ts +1 -1
  254. package/src/engine/schema-builder.ts +1 -0
  255. package/src/engine/screen-helpers.ts +54 -0
  256. package/src/engine/soft-delete-cleanup.ts +6 -2
  257. package/src/engine/steps/__tests__/duration-utils.test.ts +20 -0
  258. package/src/engine/steps/_duration-utils.ts +2 -0
  259. package/src/engine/steps/unsafe-projection-upsert.ts +1 -4
  260. package/src/engine/tier-resolver-extension.ts +3 -2
  261. package/src/engine/types/config.ts +2 -482
  262. package/src/engine/types/define-handler.ts +2 -0
  263. package/src/engine/types/entity-handlers.ts +2 -0
  264. package/src/engine/types/event-type-map.ts +1 -37
  265. package/src/engine/types/feature.ts +2 -972
  266. package/src/engine/types/fields.ts +2 -675
  267. package/src/engine/types/handlers.ts +2 -774
  268. package/src/engine/types/hooks.ts +2 -184
  269. package/src/engine/types/http-route.ts +1 -54
  270. package/src/engine/types/identifiers.ts +1 -47
  271. package/src/engine/types/index.ts +86 -40
  272. package/src/engine/types/nav.ts +2 -63
  273. package/src/engine/types/ownership.ts +2 -0
  274. package/src/engine/types/projection.ts +2 -138
  275. package/src/engine/types/relations.ts +1 -51
  276. package/src/engine/types/screen.ts +2 -574
  277. package/src/engine/types/step.ts +2 -334
  278. package/src/engine/types/target-ref.ts +1 -21
  279. package/src/engine/types/tree-node.ts +1 -132
  280. package/src/engine/types/workspace.ts +2 -49
  281. package/src/engine/validate-projection-allowlist.ts +6 -6
  282. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +120 -1
  283. package/src/entrypoint/index.ts +49 -6
  284. package/src/errors/classes.ts +22 -1
  285. package/src/errors/field-issue.ts +0 -3
  286. package/src/errors/index.ts +1 -1
  287. package/src/errors/write-error-info.ts +10 -22
  288. package/src/es-ops/README.md +1 -1
  289. package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
  290. package/src/es-ops/context.ts +4 -2
  291. package/src/es-ops/types.ts +8 -1
  292. package/src/event-store/__tests__/admin-api.integration.test.ts +27 -1
  293. package/src/event-store/__tests__/backfill-pii.integration.test.ts +279 -0
  294. package/src/event-store/__tests__/event-store.integration.test.ts +201 -0
  295. package/src/event-store/__tests__/row-to-stored-event.test.ts +2 -2
  296. package/src/event-store/__tests__/snapshot.integration.test.ts +86 -0
  297. package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +58 -0
  298. package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
  299. package/src/event-store/admin-api.ts +11 -4
  300. package/src/event-store/errors.ts +2 -35
  301. package/src/event-store/event-store.ts +64 -78
  302. package/src/event-store/events-schema.ts +11 -13
  303. package/src/event-store/index.ts +18 -3
  304. package/src/event-store/rebuild-dead-letter.ts +111 -0
  305. package/src/event-store/snapshot.ts +63 -40
  306. package/src/event-store/types.ts +2 -0
  307. package/src/files/__tests__/build-storage-key.test.ts +28 -0
  308. package/src/files/__tests__/file-ref-entity.test.ts +8 -0
  309. package/src/files/__tests__/files.integration.test.ts +24 -0
  310. package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
  311. package/src/files/__tests__/local-provider.test.ts +31 -0
  312. package/src/files/__tests__/provider-resolver.test.ts +70 -0
  313. package/src/files/__tests__/write-stream.test.ts +13 -0
  314. package/src/files/file-handle.ts +2 -19
  315. package/src/files/file-ref-entity.ts +2 -2
  316. package/src/files/file-routes.ts +13 -0
  317. package/src/files/index.ts +1 -1
  318. package/src/files/local-provider.ts +27 -8
  319. package/src/files/provider-resolver.ts +31 -20
  320. package/src/files/types.ts +13 -55
  321. package/src/i18n/__tests__/required-surface-keys.test.ts +17 -0
  322. package/src/i18n/required-surface-keys.ts +120 -1
  323. package/src/jobs/__tests__/job-queue-depth.integration.test.ts +82 -0
  324. package/src/jobs/__tests__/jobs.integration.test.ts +329 -3
  325. package/src/jobs/job-runner.ts +82 -12
  326. package/src/logging/types.ts +1 -7
  327. package/src/migrations/pending-rebuilds.ts +1 -1
  328. package/src/observability/__tests__/observability.integration.test.ts +4 -1
  329. package/src/observability/__tests__/recording-tracer.test.ts +6 -4
  330. package/src/observability/index.ts +2 -0
  331. package/src/observability/noop-provider.ts +0 -9
  332. package/src/observability/recording-tracer.ts +0 -12
  333. package/src/observability/standard-metrics.ts +64 -2
  334. package/src/observability/types/index.ts +1 -29
  335. package/src/observability/types/metric.ts +1 -56
  336. package/src/observability/types/provider.ts +1 -32
  337. package/src/observability/types/span.ts +1 -64
  338. package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
  339. package/src/pipeline/__tests__/dispatcher.test.ts +366 -1
  340. package/src/pipeline/__tests__/event-consumer-state.integration.test.ts +31 -0
  341. package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
  342. package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +263 -0
  343. package/src/pipeline/__tests__/event-dispatcher.integration.test.ts +10 -0
  344. package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +106 -0
  345. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +308 -77
  346. package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
  347. package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
  348. package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +80 -2
  349. package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +274 -0
  350. package/src/pipeline/__tests__/try-append-event.integration.test.ts +166 -0
  351. package/src/pipeline/dispatch-batch.ts +187 -0
  352. package/src/pipeline/dispatch-query.ts +165 -0
  353. package/src/pipeline/dispatch-shared.ts +826 -0
  354. package/src/pipeline/dispatch-stream.ts +90 -0
  355. package/src/pipeline/dispatch-write.ts +451 -0
  356. package/src/pipeline/dispatcher-utils.ts +1 -1
  357. package/src/pipeline/dispatcher.ts +44 -1372
  358. package/src/pipeline/entity-cache.ts +2 -33
  359. package/src/pipeline/event-consumer-state.ts +30 -2
  360. package/src/pipeline/event-dispatcher-admin.ts +293 -0
  361. package/src/pipeline/event-dispatcher-delivery.ts +305 -0
  362. package/src/pipeline/event-dispatcher.ts +85 -542
  363. package/src/pipeline/index.ts +2 -0
  364. package/src/pipeline/msp-rebuild.ts +42 -3
  365. package/src/pipeline/multi-stream-apply-context.ts +4 -42
  366. package/src/pipeline/projection-rebuild.ts +105 -3
  367. package/src/pipeline/system-hooks.ts +144 -1
  368. package/src/rate-limit/__tests__/resolver.integration.test.ts +18 -0
  369. package/src/rate-limit/resolver.ts +16 -32
  370. package/src/schema-cli.ts +76 -16
  371. package/src/search/__tests__/meilisearch-adapter.integration.test.ts +207 -185
  372. package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
  373. package/src/search/__tests__/reindex-entity.integration.test.ts +144 -0
  374. package/src/search/index.ts +6 -0
  375. package/src/search/meilisearch-adapter.ts +13 -12
  376. package/src/search/reindex-entity.ts +177 -0
  377. package/src/search/types.ts +1 -39
  378. package/src/secrets/__tests__/contains-secret.test.ts +34 -0
  379. package/src/secrets/__tests__/envelope-cipher.test.ts +59 -0
  380. package/src/secrets/__tests__/envelope.test.ts +1 -1
  381. package/src/secrets/dek-cache.ts +26 -5
  382. package/src/secrets/envelope-cipher.ts +53 -0
  383. package/src/secrets/envelope.ts +5 -3
  384. package/src/secrets/index.ts +13 -1
  385. package/src/secrets/stored-envelope.ts +46 -0
  386. package/src/secrets/types.ts +2 -162
  387. package/src/stack/__tests__/event-collector.test.ts +42 -0
  388. package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +125 -0
  389. package/src/stack/db.ts +2 -1
  390. package/src/stack/push-entity-projection-tables.ts +4 -3
  391. package/src/stack/redis.ts +8 -0
  392. package/src/stack/request-helper.ts +38 -2
  393. package/src/stack/table-helpers.ts +6 -4
  394. package/src/stack/test-stack.ts +249 -131
  395. package/src/testing/__tests__/late-bound.test.ts +32 -0
  396. package/src/testing/__tests__/wait-for.test.ts +59 -0
  397. package/src/testing/boot-validator-fixture.ts +121 -0
  398. package/src/testing/e2e-generator.ts +8 -0
  399. package/src/testing/file-provider-contract.ts +104 -0
  400. package/src/testing/handler-context.ts +3 -1
  401. package/src/testing/index.ts +5 -0
  402. package/src/testing/late-bound.ts +5 -3
  403. package/src/testing/mutable-master-key-provider.ts +29 -3
  404. package/src/testing/wait-for.ts +3 -0
  405. package/src/testing/without-ambient-temporal.ts +14 -0
  406. package/src/time/__tests__/tz-dateline.test.ts +8 -10
  407. package/src/time/geo-tz.ts +1 -32
  408. package/src/time/index.ts +1 -0
  409. package/src/time/legacy-date.ts +18 -0
  410. package/src/time/polyfill.ts +21 -38
  411. package/src/time/tz-context.ts +44 -85
  412. package/src/ui-types/app-schema.ts +12 -0
  413. package/src/ui-types/index.ts +20 -7
  414. package/src/utils/__tests__/safe-json-temporal.test.ts +18 -0
  415. package/src/utils/safe-json.ts +13 -1
  416. package/src/__tests__/raw-table.integration.test.ts +0 -116
  417. package/src/bun-db/__tests__/bun-test-stack.ts +0 -6
  418. package/src/db/__tests__/encryption.test.ts +0 -39
  419. package/src/db/encryption.ts +0 -39
  420. package/src/db/row-helpers.ts +0 -4
  421. package/src/engine/__tests__/raw-table.test.ts +0 -150
  422. package/src/engine/__tests__/unmanaged-table.test.ts +0 -127
  423. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +0 -184
  424. package/src/engine/feature-ast/extractors/round4.ts +0 -1366
@@ -1,16 +1,35 @@
1
1
  import { createReadStream, createWriteStream } from "node:fs";
2
2
  import { mkdir, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
3
+ import { dirname, join, resolve, sep } from "node:path";
4
4
  import { pipeline } from "node:stream/promises";
5
- import type { FileStorageProvider } from "./types";
5
+ import { assertSafeStorageKey, type FileStorageProvider } from "./types";
6
6
 
7
7
  // Local-filesystem backend — intended for dev + tests. Production deploys
8
8
  // pick an object-store provider (S3/R2/…). mimeType is ignored here; the
9
9
  // filesystem tracks no metadata beyond what the caller stores on FileRef.
10
10
  export function createLocalProvider(basePath: string): FileStorageProvider {
11
+ const resolvedBase = resolve(basePath);
12
+
13
+ // Callers are expected to only ever pass keys built by buildStorageKey(),
14
+ // but the storage layer shouldn't rely solely on that — this resolves the
15
+ // real path and rejects anything that escapes basePath (e.g. a `..`
16
+ // segment that slipped past an upstream check), regardless of the key's
17
+ // source.
18
+ function resolveContainedPath(key: string): string {
19
+ assertSafeStorageKey(key);
20
+ // assertSafeStorageKey rejects every literal "."/".." segment, which
21
+ // covers all string-based traversal a join+resolve can produce; this
22
+ // startsWith check is a second, independent line of defense in case a
23
+ // future caller feeds resolveContainedPath something pre-resolved.
24
+ const filePath = resolve(join(basePath, key));
25
+ if (filePath !== resolvedBase && !filePath.startsWith(resolvedBase + sep)) {
26
+ throw new Error(`storage key escapes basePath: "${key}"`);
27
+ }
28
+ return filePath;
29
+ }
11
30
  return {
12
31
  async write(key: string, data: Uint8Array, _mimeType?: string): Promise<void> {
13
- const filePath = join(basePath, key);
32
+ const filePath = resolveContainedPath(key);
14
33
  await mkdir(dirname(filePath), { recursive: true });
15
34
  await writeFile(filePath, data);
16
35
  },
@@ -26,7 +45,7 @@ export function createLocalProvider(basePath: string): FileStorageProvider {
26
45
  // Hygiene; ein periodischer cron-cleanup auf alten `.tmp`-Files
27
46
  // ist die saubere Loesung wenn das in Production realistisch
28
47
  // greift.
29
- const filePath = join(basePath, key);
48
+ const filePath = resolveContainedPath(key);
30
49
  const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
31
50
  await mkdir(dirname(filePath), { recursive: true });
32
51
  try {
@@ -41,7 +60,7 @@ export function createLocalProvider(basePath: string): FileStorageProvider {
41
60
  },
42
61
 
43
62
  async read(key: string): Promise<Uint8Array> {
44
- const filePath = join(basePath, key);
63
+ const filePath = resolveContainedPath(key);
45
64
  return readFile(filePath);
46
65
  },
47
66
 
@@ -55,7 +74,7 @@ export function createLocalProvider(basePath: string): FileStorageProvider {
55
74
  // (z.B. ENOENT bei Missing-File faellt erst beim ersten chunk-
56
75
  // pull, nicht beim readStream-Aufruf — gleiches Lazy-Verhalten
57
76
  // wie inmemory + S3).
58
- const filePath = join(basePath, key);
77
+ const filePath = resolveContainedPath(key);
59
78
  const stream = createReadStream(filePath);
60
79
  return {
61
80
  async *[Symbol.asyncIterator]() {
@@ -76,13 +95,13 @@ export function createLocalProvider(basePath: string): FileStorageProvider {
76
95
  },
77
96
 
78
97
  async delete(key: string): Promise<void> {
79
- const filePath = join(basePath, key);
98
+ const filePath = resolveContainedPath(key);
80
99
  await rm(filePath, { force: true });
81
100
  },
82
101
 
83
102
  async exists(key: string): Promise<boolean> {
84
103
  try {
85
- await stat(join(basePath, key));
104
+ await stat(resolveContainedPath(key));
86
105
  return true;
87
106
  } catch {
88
107
  return false;
@@ -12,11 +12,12 @@
12
12
  // file-foundation re-exports `createFileProviderForTenant` + the plugin types
13
13
  // (moved here from there) so existing imports keep working.
14
14
 
15
+ import type { FileProviderResolver } from "@cosmicdrift/kumiko-types/file-provider-resolver-types";
15
16
  import type { DbConnection } from "../db/connection";
16
17
  import type { TenantDb } from "../db/tenant-db";
17
18
  import { EXT_FILE_PROVIDER, FILE_PROVIDER_CONFIG_KEY } from "../engine/extension-names";
18
19
  import { SYSTEM_USER_ID } from "../engine/system-user";
19
- import type { ConfigAccessor, ConfigAccessorFactory, Registry, TenantId } from "../engine/types";
20
+ import type { ConfigAccessor, ConfigAccessorFactory, Registry } from "../engine/types";
20
21
  import type { SecretsContext } from "../secrets";
21
22
  import type { FileStorageProvider } from "./types";
22
23
 
@@ -120,10 +121,7 @@ export async function createFileProviderForTenant(
120
121
  return usage.options.build(ctx, tenantId);
121
122
  }
122
123
 
123
- // A bound, per-tenant provider resolver. One instance serves all tenants
124
- // (tenantId is the call argument) — the single spine shared by upload routes,
125
- // ctx.files and the GDPR jobs.
126
- export type FileProviderResolver = (tenantId: TenantId) => Promise<FileStorageProvider>;
124
+ export type { FileProviderResolver } from "@cosmicdrift/kumiko-types/file-provider-resolver-types";
127
125
 
128
126
  export type FileProviderResolverDeps = {
129
127
  readonly registry?: Registry;
@@ -138,21 +136,34 @@ export type FileProviderResolverDeps = {
138
136
  // SYSTEM identity: provider construction is an infra read, distinct from the
139
137
  // request user's authorization which stays at the route accessGuard.
140
138
  export function makeFileProviderResolver(deps: FileProviderResolverDeps): FileProviderResolver {
141
- return async (tenantId) => {
142
- if (!deps.registry || !deps._configAccessorFactory || !deps.db) {
143
- throw new Error(
144
- "makeFileProviderResolver: registry/_configAccessorFactory/db missing " +
145
- "mount the config + file-foundation features and wire context.db",
139
+ // Per-tenant cache (698/2) without it, every resolveProvider(tenantId)
140
+ // call re-reads config + secrets from the DB, even though the provider is
141
+ // effectively static per tenant for the process lifetime. A rejected
142
+ // promise is evicted so a transient failure (or a secret genuinely not
143
+ // configured yet) doesn't permanently poison this tenant's entry.
144
+ const cache = new Map<string, Promise<FileStorageProvider>>();
145
+ return (tenantId) => {
146
+ const cached = cache.get(tenantId);
147
+ if (cached) return cached;
148
+ const built = (async () => {
149
+ if (!deps.registry || !deps._configAccessorFactory || !deps.db) {
150
+ throw new Error(
151
+ "makeFileProviderResolver: registry/_configAccessorFactory/db missing — " +
152
+ "mount the config + file-foundation features and wire context.db",
153
+ );
154
+ }
155
+ const config = deps._configAccessorFactory({
156
+ user: { id: SYSTEM_USER_ID, tenantId },
157
+ db: deps.db,
158
+ secrets: deps.secrets,
159
+ });
160
+ return createFileProviderForTenant(
161
+ { config, registry: deps.registry, secrets: deps.secrets, _userId: SYSTEM_USER_ID },
162
+ tenantId,
146
163
  );
147
- }
148
- const config = deps._configAccessorFactory({
149
- user: { id: SYSTEM_USER_ID, tenantId },
150
- db: deps.db,
151
- secrets: deps.secrets,
152
- });
153
- return createFileProviderForTenant(
154
- { config, registry: deps.registry, secrets: deps.secrets, _userId: SYSTEM_USER_ID },
155
- tenantId,
156
- );
164
+ })();
165
+ built.catch(() => cache.delete(tenantId));
166
+ cache.set(tenantId, built);
167
+ return built;
157
168
  };
158
169
  }
@@ -6,60 +6,11 @@ export type FileMetadata = {
6
6
  readonly size: number;
7
7
  };
8
8
 
9
- // Options for `getSignedUrl`. `contentDisposition` lets the caller hint the
10
- // browser to download-with-name vs inline-display (maps to ResponseContent-
11
- // Disposition on S3). Keep the option-bag small and additive; provider impls
12
- // that don't support a given hint should ignore it rather than error.
13
- export type SignedUrlOptions = {
14
- readonly contentDisposition?: string;
15
- };
16
-
17
- // Options fuer `writeStream`. `mimeType` ist Content-Type-Hint analog zu
18
- // `write`. `contentLength` ist optional fuer Provider die einen Length-
19
- // Header brauchen (S3 multipart hat einen TransferManager, kann auch ohne
20
- // length); local-Provider ignoriert beides.
21
- export type WriteStreamOptions = {
22
- readonly mimeType?: string;
23
- readonly contentLength?: number;
24
- };
25
-
26
- // Primitive storage contract: key+bytes in, bytes out. Metadata (fileName,
27
- // mimeType, size) lives on the FileRef row — the provider only needs to
28
- // shuttle bytes. `mimeType` on write() is a hint for providers that need a
29
- // Content-Type header (S3/R2/…); local filesystems can ignore it.
30
- //
31
- // **Streaming (`writeStream` + `readStream`) ist PFLICHT** — beide
32
- // Methoden sind required, kein optional-feature. Begruendung:
33
- // - User-Data-Export (Atom 3c) braucht beide, sonst silent fail bei
34
- // erstem Job mit fileRefs in Production.
35
- // - Apps die nur kleine Files (Avatar-Uploads, Profile-Pics) handeln,
36
- // koennen trivial via `oneShot`-Pattern den Stream-Contract erfuellen
37
- // (single-chunk yield von write/read-Bytes). 5 Zeilen pro Provider.
38
- // - Optional-Type wuerde TypeScript-Lying erlauben: Type sagt "kann
39
- // fehlen", Worker throws zur Runtime → App-Authors sehen den Bug
40
- // erst in Production. Required + TS-enforced ist ehrlich.
41
- //
42
- // `getSignedUrl` BLEIBT optional: object-store backends (S3/R2/GCS)
43
- // implement it so clients can download directly from the provider after
44
- // the server has checked access — offloads bandwidth and enables browser-
45
- // native caching. Filesystem providers leave it undefined; the route then
46
- // returns 501 and the client falls back to streaming via GET /files/:id.
47
- // Callers must feature-detect via `typeof provider.getSignedUrl === "function"`.
48
- // Hier ist Optional korrekt weil die Fallback-Pfad existiert — kein
49
- // silent-fail, sondern 501 + alternativer download.
50
- export type FileStorageProvider = {
51
- write(key: string, data: Uint8Array, mimeType?: string): Promise<void>;
52
- writeStream(
53
- key: string,
54
- source: AsyncIterable<Uint8Array>,
55
- options?: WriteStreamOptions,
56
- ): Promise<void>;
57
- read(key: string): Promise<Uint8Array>;
58
- readStream(key: string): AsyncIterable<Uint8Array>;
59
- delete(key: string): Promise<void>;
60
- exists(key: string): Promise<boolean>;
61
- getSignedUrl?(key: string, expiresInSeconds: number, options?: SignedUrlOptions): Promise<string>;
62
- };
9
+ export type {
10
+ FileStorageProvider,
11
+ SignedUrlOptions,
12
+ WriteStreamOptions,
13
+ } from "@cosmicdrift/kumiko-types/file-storage-provider-types";
63
14
 
64
15
  export type FileValidationOptions = {
65
16
  readonly maxSize?: string | undefined;
@@ -133,6 +84,12 @@ export function validateFile(
133
84
  return null;
134
85
  }
135
86
 
87
+ export function assertSafeStorageKey(key: string): void {
88
+ if (key.split("/").some((segment) => segment === "." || segment === "..")) {
89
+ throw new Error(`storage key contains a path-traversal segment: "${key}"`);
90
+ }
91
+ }
92
+
136
93
  export function buildStorageKey(
137
94
  tenantId: TenantId,
138
95
  entityType: string,
@@ -141,6 +98,7 @@ export function buildStorageKey(
141
98
  fileName: string,
142
99
  uniqueId: string,
143
100
  ): string {
144
- const ext = fileName.split(".").pop() ?? "bin";
101
+ const rawExt = fileName.split(".").pop() ?? "";
102
+ const ext = /^[A-Za-z0-9]+$/.test(rawExt) ? rawExt.toLowerCase() : "bin";
145
103
  return `${tenantId}/${entityType}/${entityId}/${fieldName}/${uniqueId}.${ext}`;
146
104
  }
@@ -0,0 +1,17 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { booleanFacetOptionKeys, selectFacetOptionKey } from "../required-surface-keys";
3
+
4
+ describe("required-surface-keys helpers", () => {
5
+ test("booleanFacetOptionKeys emits true/false option keys", () => {
6
+ expect(booleanFacetOptionKeys("tenant", "tenant", "isEnabled")).toEqual([
7
+ "tenant:entity:tenant:field:isEnabled:option:true",
8
+ "tenant:entity:tenant:field:isEnabled:option:false",
9
+ ]);
10
+ });
11
+
12
+ test("selectFacetOptionKey encodes option value", () => {
13
+ expect(selectFacetOptionKey("user", "user", "status", "active")).toBe(
14
+ "user:entity:user:field:status:option:active",
15
+ );
16
+ });
17
+ });
@@ -1,16 +1,21 @@
1
+ import { isExtensionEditSection, normalizeListColumn } from "../engine/screen-helpers";
1
2
  import type {
2
3
  ActionFormScreenDefinition,
3
4
  ConfigEditScreenDefinition,
5
+ DashboardFilterDefinition,
6
+ DashboardPanelDefinition,
7
+ DashboardScreenDefinition,
4
8
  EntityEditScreenDefinition,
5
9
  EntityListScreenDefinition,
6
10
  FeatureDefinition,
7
11
  NavDefinition,
12
+ ProjectionDetailScreenDefinition,
13
+ ProjectionListScreenDefinition,
8
14
  RowAction,
9
15
  ScreenDefinition,
10
16
  ToolbarAction,
11
17
  WorkspaceDefinition,
12
18
  } from "../engine/types";
13
- import { isExtensionEditSection, normalizeListColumn } from "../engine/types/screen";
14
19
 
15
20
  /** Pseudo-entity for actionForm field labels (renderer action-form-shim). */
16
21
  export const ACTION_FORM_ENTITY = "__action-form__";
@@ -18,10 +23,31 @@ export const ACTION_FORM_ENTITY = "__action-form__";
18
23
  /** Pseudo-entity for configEdit field labels (renderer config-edit-shim). */
19
24
  export const CONFIG_EDIT_ENTITY = "__config-edit__";
20
25
 
26
+ /** Pseudo-entity for projectionDetail field labels (renderer projection-detail-shim). */
27
+ export const PROJECTION_DETAIL_ENTITY = "__projection-detail__";
28
+
21
29
  export function fieldLabelKey(featureName: string, entityName: string, fieldName: string): string {
22
30
  return `${featureName}:entity:${entityName}:field:${fieldName}`;
23
31
  }
24
32
 
33
+ export function booleanFacetOptionKeys(
34
+ featureName: string,
35
+ entityName: string,
36
+ fieldName: string,
37
+ ): readonly string[] {
38
+ const base = `${featureName}:entity:${entityName}:field:${fieldName}:option`;
39
+ return [`${base}:true`, `${base}:false`];
40
+ }
41
+
42
+ export function selectFacetOptionKey(
43
+ featureName: string,
44
+ entityName: string,
45
+ fieldName: string,
46
+ value: string,
47
+ ): string {
48
+ return `${featureName}:entity:${entityName}:field:${fieldName}:option:${value}`;
49
+ }
50
+
25
51
  export function screenTitleKey(screenId: string): string {
26
52
  return `screen:${screenId}.title`;
27
53
  }
@@ -46,6 +72,34 @@ function pushRowActionKeys(out: Set<string>, action: RowAction): void {
46
72
  }
47
73
  }
48
74
 
75
+ function pushDashboardScreenKeys(out: Set<string>, dashboard: DashboardScreenDefinition): void {
76
+ for (const panel of dashboard.panels) pushDashboardPanelKeys(out, panel);
77
+ if (dashboard.filter !== undefined) pushDashboardFilterKeys(out, dashboard.filter);
78
+ }
79
+
80
+ function pushDashboardPanelKeys(out: Set<string>, panel: DashboardPanelDefinition): void {
81
+ // skip: custom-Panel übersetzt sich selbst, kein Key hier
82
+ if (panel.kind === "custom") return;
83
+ pushKey(out, panel.label);
84
+ if (panel.kind === "stat-group") {
85
+ for (const stat of panel.stats) pushKey(out, stat.label);
86
+ }
87
+ if (panel.kind === "list") {
88
+ for (const col of panel.columns) {
89
+ const normalized = normalizeListColumn(col);
90
+ if (normalized.label !== undefined) pushKey(out, normalized.label);
91
+ }
92
+ }
93
+ if (panel.kind === "feed" && panel.emptyLabel !== undefined) pushKey(out, panel.emptyLabel);
94
+ }
95
+
96
+ function pushDashboardFilterKeys(out: Set<string>, filter: DashboardFilterDefinition): void {
97
+ pushKey(out, filter.label);
98
+ if (filter.allLabel !== undefined) pushKey(out, filter.allLabel);
99
+ if (filter.placeholder !== undefined) pushKey(out, filter.placeholder);
100
+ for (const opt of filter.options ?? []) pushKey(out, opt.label);
101
+ }
102
+
49
103
  function pushToolbarActionKeys(out: Set<string>, action: ToolbarAction): void {
50
104
  pushKey(out, action.label);
51
105
  if (action.kind === "writeHandler") {
@@ -76,6 +130,21 @@ export function requiredKeysFromScreen(
76
130
  for (const action of list.toolbarActions ?? []) pushToolbarActionKeys(out, action);
77
131
  break;
78
132
  }
133
+ case "projectionList": {
134
+ const list = screen as ProjectionListScreenDefinition;
135
+ // Keine Entity → keine field-label-Fallbacks; nur explizite Column-Labels.
136
+ for (const col of list.columns) {
137
+ const normalized = normalizeListColumn(col);
138
+ if (normalized.label !== undefined) pushKey(out, normalized.label);
139
+ }
140
+ for (const action of list.rowActions ?? []) pushRowActionKeys(out, action);
141
+ for (const action of list.toolbarActions ?? []) pushToolbarActionKeys(out, action);
142
+ break;
143
+ }
144
+ case "dashboard": {
145
+ pushDashboardScreenKeys(out, screen as DashboardScreenDefinition);
146
+ break;
147
+ }
79
148
  case "entityEdit": {
80
149
  const edit = screen as EntityEditScreenDefinition;
81
150
  pushKey(out, edit.submitLabel);
@@ -136,6 +205,20 @@ export function requiredKeysFromScreen(
136
205
  }
137
206
  break;
138
207
  }
208
+ case "projectionDetail": {
209
+ const detail = screen as ProjectionDetailScreenDefinition;
210
+ for (const section of detail.layout.sections) {
211
+ if (isExtensionEditSection(section)) continue; // rejected at boot, unreachable here
212
+ pushKey(out, section.title);
213
+ for (const f of section.fields) {
214
+ const fieldName = editFieldName(f);
215
+ const override = detail.fieldLabels?.[fieldName];
216
+ if (override !== undefined) pushKey(out, override);
217
+ else out.add(fieldLabelKey(featureName, PROJECTION_DETAIL_ENTITY, fieldName));
218
+ }
219
+ }
220
+ break;
221
+ }
139
222
  case "custom":
140
223
  break;
141
224
  }
@@ -155,6 +238,41 @@ export function requiredKeysFromWorkspace(ws: WorkspaceDefinition): readonly str
155
238
  return [...out];
156
239
  }
157
240
 
241
+ function collectEntityListFilterKeys(feature: FeatureDefinition, out: Set<string>): void {
242
+ for (const screen of Object.values(feature.screens)) {
243
+ if (screen.type !== "entityList") continue;
244
+ const entity = feature.entities?.[screen.entity];
245
+ if (!entity) continue;
246
+ for (const [fieldName, rawDef] of Object.entries(entity.fields)) {
247
+ const def = rawDef as {
248
+ readonly filterable?: boolean;
249
+ readonly type?: string;
250
+ readonly options?: readonly string[];
251
+ };
252
+ if (def.filterable !== true) continue;
253
+ if (def.type === "boolean") {
254
+ for (const key of booleanFacetOptionKeys(feature.name, screen.entity, fieldName)) {
255
+ out.add(key);
256
+ }
257
+ } else if (def.type === "select" && Array.isArray(def.options)) {
258
+ for (const value of def.options) {
259
+ out.add(selectFacetOptionKey(feature.name, screen.entity, fieldName, value));
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ export function featureHasI18nSurface(feature: FeatureDefinition): boolean {
267
+ if (Object.keys(feature.screens).length > 0) return true;
268
+ if (Object.keys(feature.navs).length > 0) return true;
269
+ if (Object.keys(feature.workspaces).length > 0) return true;
270
+ for (const def of Object.values(feature.configKeys)) {
271
+ if (def.mask !== undefined) return true;
272
+ }
273
+ return false;
274
+ }
275
+
158
276
  export function requiredKeysFromFeature(feature: FeatureDefinition): readonly string[] {
159
277
  const out = new Set<string>();
160
278
 
@@ -170,6 +288,7 @@ export function requiredKeysFromFeature(feature: FeatureDefinition): readonly st
170
288
  for (const def of Object.values(feature.configKeys)) {
171
289
  pushKey(out, def.mask?.title);
172
290
  }
291
+ collectEntityListFilterKeys(feature, out);
173
292
 
174
293
  return [...out];
175
294
  }
@@ -0,0 +1,82 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { createRegistry, defineFeature } from "../../engine";
3
+ import type { AppContext, Registry } from "../../engine/types";
4
+ import { createPrometheusMeter, registerStandardMetrics } from "../../observability";
5
+ import { createTestRedis, type TestRedis } from "../../stack";
6
+ import { sleep } from "../../testing";
7
+ import { createJobRunner } from "../job-runner";
8
+
9
+ let testRedis: TestRedis;
10
+ let redisUrl: string;
11
+
12
+ const testFeature = defineFeature("test-queue-depth", (r) => {
13
+ r.job("noop", { trigger: { manual: true } }, async () => {});
14
+ });
15
+
16
+ beforeAll(async () => {
17
+ testRedis = await createTestRedis();
18
+ redisUrl = `redis://${testRedis.redis.options.host}:${testRedis.redis.options.port}/${testRedis.redis.options.db}`;
19
+ });
20
+
21
+ afterAll(async () => {
22
+ await testRedis.cleanup();
23
+ });
24
+
25
+ describe("job-runner — kumiko_job_queue_depth", () => {
26
+ test("start() polls BullMQ counts into the gauge, stop() tears the poller down", async () => {
27
+ const registry: Registry = createRegistry([testFeature]);
28
+ const meter = createPrometheusMeter();
29
+ registerStandardMetrics(meter);
30
+ const context: AppContext = { meter };
31
+ const queueNamePrefix = `kumiko-test-qd-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
32
+
33
+ const runner = createJobRunner({
34
+ registry,
35
+ context,
36
+ redisUrl,
37
+ consumerLane: "worker",
38
+ queueNamePrefix,
39
+ });
40
+
41
+ try {
42
+ await runner.start();
43
+
44
+ const snapshot = meter.snapshot().get("kumiko_job_queue_depth");
45
+ expect(snapshot).toBeDefined();
46
+ const waitingSlot = snapshot?.slots.find(
47
+ (s) => s.labels?.["lane"] === "worker" && s.labels?.["state"] === "waiting",
48
+ );
49
+ expect(waitingSlot).toBeDefined();
50
+ expect((waitingSlot as { value: number }).value).toBe(0);
51
+ // BullMQ's fresh Worker connection is still settling right after
52
+ // start() returns (no boot/cron job here to have already warmed it
53
+ // up, unlike every other scenario in this suite) — stop() immediately
54
+ // after start() races the Worker's own connection teardown and throws
55
+ // an unrelated "Connection is closed" from ioredis. Production runners
56
+ // live far longer than this; the race is a test-only artifact of
57
+ // start()+stop() with zero work in between.
58
+ await sleep(50);
59
+ } finally {
60
+ await runner.stop();
61
+ const keys = await testRedis.redis.keys(`bull:${queueNamePrefix}-worker:*`);
62
+ if (keys.length > 0) await testRedis.redis.del(...keys);
63
+ }
64
+ });
65
+
66
+ test("enqueuer-only runner (no consumerLane) never polls — no gauge slots", async () => {
67
+ const registry: Registry = createRegistry([testFeature]);
68
+ const meter = createPrometheusMeter();
69
+ registerStandardMetrics(meter);
70
+ const context: AppContext = { meter };
71
+ const queueNamePrefix = `kumiko-test-qd-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
72
+
73
+ const runner = createJobRunner({ registry, context, redisUrl, queueNamePrefix });
74
+ try {
75
+ await runner.start();
76
+ const snapshot = meter.snapshot().get("kumiko_job_queue_depth");
77
+ expect(snapshot?.slots.length ?? 0).toBe(0);
78
+ } finally {
79
+ await runner.stop();
80
+ }
81
+ });
82
+ });