@cosmicdrift/kumiko-framework 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (424) hide show
  1. package/README.md +9 -38
  2. package/package.json +21 -2
  3. package/src/__tests__/anonymous-access.integration.test.ts +185 -0
  4. package/src/__tests__/consumer-cli.integration.test.ts +172 -0
  5. package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
  6. package/src/__tests__/full-stack.integration.test.ts +1 -1
  7. package/src/__tests__/schema-cli-temporal-polyfill.test.ts +48 -0
  8. package/src/__tests__/schema-cli.integration.test.ts +50 -0
  9. package/src/__tests__/store-table.integration.test.ts +93 -0
  10. package/src/api/__tests__/api.test.ts +373 -0
  11. package/src/api/__tests__/auth-middleware-anonymous-access-boot.test.ts +40 -0
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
  13. package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +253 -0
  14. package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +222 -0
  15. package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +249 -0
  16. package/src/api/__tests__/auth-routes-mfa-verify.test.ts +202 -0
  17. package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +135 -0
  18. package/src/api/__tests__/batch.integration.test.ts +101 -11
  19. package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
  20. package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
  21. package/src/api/__tests__/jwt.test.ts +200 -1
  22. package/src/api/__tests__/login-rate-limiter-sweep.test.ts +50 -0
  23. package/src/api/__tests__/pat-scope.test.ts +36 -0
  24. package/src/api/__tests__/pii-leak-guard.integration.test.ts +103 -0
  25. package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +138 -0
  26. package/src/api/__tests__/request-id-middleware.test.ts +51 -0
  27. package/src/api/__tests__/server-boot-guards.test.ts +71 -0
  28. package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
  29. package/src/api/__tests__/sse-broker.test.ts +57 -0
  30. package/src/api/__tests__/sse-route.test.ts +4 -0
  31. package/src/api/api-constants.ts +11 -0
  32. package/src/api/auth-middleware.ts +248 -40
  33. package/src/api/auth-routes.ts +576 -95
  34. package/src/api/index.ts +13 -4
  35. package/src/api/jwt.ts +170 -11
  36. package/src/api/pat-scope.ts +14 -0
  37. package/src/api/pii-leak-guard.ts +47 -0
  38. package/src/api/request-context.ts +3 -0
  39. package/src/api/request-id-middleware.ts +2 -0
  40. package/src/api/routes.ts +169 -2
  41. package/src/api/server.ts +112 -16
  42. package/src/api/sse-broker.ts +39 -0
  43. package/src/bun-db/__tests__/PATTERN.md +0 -1
  44. package/src/bun-db/__tests__/coerce-row-temporal.test.ts +41 -0
  45. package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
  46. package/src/bun-db/__tests__/write-brand.test.ts +21 -0
  47. package/src/bun-db/connection.ts +3 -3
  48. package/src/bun-db/index.ts +2 -0
  49. package/src/bun-db/query.ts +105 -32
  50. package/src/consumer-cli.ts +134 -0
  51. package/src/crypto/__tests__/blind-index.test.ts +130 -0
  52. package/src/crypto/__tests__/event-pii.test.ts +161 -0
  53. package/src/crypto/__tests__/kek-rotation.integration.test.ts +180 -0
  54. package/src/crypto/__tests__/kms-adapter-contract.ts +134 -0
  55. package/src/crypto/__tests__/kms-adapter.contract.test.ts +4 -0
  56. package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +117 -0
  57. package/src/crypto/__tests__/pii-field-encryption.test.ts +376 -0
  58. package/src/crypto/__tests__/request-kms-cache.test.ts +74 -0
  59. package/src/crypto/__tests__/subject-resolver.test.ts +108 -0
  60. package/src/crypto/blind-index.ts +118 -0
  61. package/src/crypto/event-pii.ts +70 -0
  62. package/src/crypto/in-memory-kms-adapter.ts +50 -0
  63. package/src/crypto/index.ts +67 -0
  64. package/src/crypto/kms-adapter.ts +2 -0
  65. package/src/crypto/pg-kms-adapter.ts +295 -0
  66. package/src/crypto/pii-field-encryption.ts +248 -0
  67. package/src/crypto/request-kms-cache.ts +38 -0
  68. package/src/crypto/subject-resolver.ts +91 -0
  69. package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +191 -0
  70. package/src/db/__tests__/blind-index.integration.test.ts +248 -0
  71. package/src/db/__tests__/build-filter-where.test.ts +34 -0
  72. package/src/db/__tests__/collect-table-metas.test.ts +10 -10
  73. package/src/db/__tests__/config-seed.integration.test.ts +13 -5
  74. package/src/db/__tests__/dialect-instant.test.ts +1 -4
  75. package/src/db/__tests__/entity-field-encryption.test.ts +7 -7
  76. package/src/db/__tests__/entity-table-meta-source.test.ts +50 -0
  77. package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
  78. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +402 -0
  79. package/src/db/__tests__/event-store-executor.integration.test.ts +299 -29
  80. package/src/db/__tests__/feature-table-sources.test.ts +34 -0
  81. package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +118 -39
  82. package/src/db/__tests__/instant-to-driver-temporal.test.ts +21 -0
  83. package/src/db/__tests__/located-timestamp.test.ts +19 -0
  84. package/src/db/__tests__/migrate-generator.test.ts +21 -0
  85. package/src/db/__tests__/migrate-runner.test.ts +61 -0
  86. package/src/db/__tests__/number-field-fractional.integration.test.ts +58 -0
  87. package/src/db/__tests__/rebuild-marker.test.ts +13 -3
  88. package/src/db/__tests__/replay-migration-sql.test.ts +384 -0
  89. package/src/db/__tests__/schema-inspection.test.ts +7 -0
  90. package/src/db/__tests__/schema-migration.integration.test.ts +1 -1
  91. package/src/db/__tests__/table-builder-meta-lockstep.test.ts +39 -0
  92. package/src/db/__tests__/tenant-db-where-merge.test.ts +49 -3
  93. package/src/db/__tests__/tenant-db.integration.test.ts +6 -2
  94. package/src/db/api.ts +2 -2
  95. package/src/db/apply-entity-event.ts +18 -14
  96. package/src/db/blind-index-cleanup.ts +55 -0
  97. package/src/db/bun-provider.ts +2 -2
  98. package/src/db/collect-table-metas.ts +6 -7
  99. package/src/db/config-seed.ts +9 -9
  100. package/src/db/connection.ts +7 -12
  101. package/src/db/cursor.ts +1 -18
  102. package/src/db/dialect.ts +20 -26
  103. package/src/db/entity-field-encryption.ts +81 -24
  104. package/src/db/entity-table-meta-types.ts +2 -0
  105. package/src/db/entity-table-meta.ts +63 -90
  106. package/src/db/event-store-executor-context.ts +404 -0
  107. package/src/db/event-store-executor-read.ts +275 -0
  108. package/src/db/event-store-executor-write.ts +644 -0
  109. package/src/db/event-store-executor.ts +28 -1155
  110. package/src/db/feature-table-sources.ts +6 -5
  111. package/src/db/index.ts +20 -3
  112. package/src/db/located-timestamp.ts +4 -0
  113. package/src/db/migrate-generator.ts +39 -6
  114. package/src/db/migrate-runner.ts +107 -11
  115. package/src/db/pg-error.ts +9 -1
  116. package/src/db/postgres-provider.ts +2 -2
  117. package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +80 -0
  118. package/src/db/queries/backfill-pii.ts +277 -0
  119. package/src/db/queries/ddl.ts +45 -0
  120. package/src/db/queries/event-consumer.ts +35 -2
  121. package/src/db/queries/event-store.ts +126 -19
  122. package/src/db/queries/projection-rebuild.ts +22 -8
  123. package/src/db/queries/shadow-swap.ts +183 -0
  124. package/src/db/queries/test-stack.ts +4 -30
  125. package/src/db/query-api.ts +1 -0
  126. package/src/db/query.ts +1 -0
  127. package/src/db/rebuild-marker.ts +18 -2
  128. package/src/db/reference-data.ts +2 -3
  129. package/src/db/replay-migration-sql.ts +257 -0
  130. package/src/db/schema-inspection.ts +1 -1
  131. package/src/db/table-builder.ts +102 -71
  132. package/src/db/tenant-db.ts +15 -53
  133. package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
  134. package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
  135. package/src/engine/__tests__/boot-validator-dashboard.test.ts +237 -0
  136. package/src/engine/__tests__/boot-validator-entity-list.test.ts +104 -0
  137. package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -69
  138. package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +26 -5
  139. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +3 -19
  140. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +246 -4
  141. package/src/engine/__tests__/boot-validator.test.ts +219 -15
  142. package/src/engine/__tests__/build-app-schema.test.ts +82 -0
  143. package/src/engine/__tests__/build-config-feature-schema.test.ts +125 -0
  144. package/src/engine/__tests__/build-target.test.ts +3 -11
  145. package/src/engine/__tests__/codemod-pipeline.test.ts +152 -21
  146. package/src/engine/__tests__/config-helpers.test.ts +16 -0
  147. package/src/engine/__tests__/define-feature-entity-mapping.test.ts +6 -0
  148. package/src/engine/__tests__/define-roles.test.ts +21 -0
  149. package/src/engine/__tests__/engine.test.ts +163 -1
  150. package/src/engine/__tests__/entity-handlers.test.ts +80 -0
  151. package/src/engine/__tests__/event-migration-declarative.test.ts +65 -0
  152. package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
  153. package/src/engine/__tests__/extend-entity-projection.test.ts +123 -0
  154. package/src/engine/__tests__/factories-time.test.ts +2 -66
  155. package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
  156. package/src/engine/__tests__/feature-manifest.test.ts +31 -1
  157. package/src/engine/__tests__/field-access.test.ts +23 -1
  158. package/src/engine/__tests__/hook-phases.test.ts +5 -5
  159. package/src/engine/__tests__/membership-roles.test.ts +4 -10
  160. package/src/engine/__tests__/nav.test.ts +86 -1
  161. package/src/engine/__tests__/pipeline-engine.test.ts +9 -9
  162. package/src/engine/__tests__/pipeline-handler.integration.test.ts +18 -18
  163. package/src/engine/__tests__/pipeline-observability.integration.test.ts +2 -2
  164. package/src/engine/__tests__/pipeline-performance.integration.test.ts +3 -3
  165. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +9 -9
  166. package/src/engine/__tests__/post-query-hook.test.ts +7 -7
  167. package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
  168. package/src/engine/__tests__/registry.test.ts +144 -0
  169. package/src/engine/__tests__/schema-builder.test.ts +18 -0
  170. package/src/engine/__tests__/screen.test.ts +147 -1
  171. package/src/engine/__tests__/soft-delete-cleanup.test.ts +3 -0
  172. package/src/engine/__tests__/store-table.test.ts +229 -0
  173. package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
  174. package/src/engine/__tests__/{visual-tree-patterns.test.ts → tree-actions-patterns.test.ts} +7 -95
  175. package/src/engine/__tests__/validate-projection-allowlist.test.ts +15 -15
  176. package/src/engine/boot-validator/__tests__/config-deps.test.ts +92 -0
  177. package/src/engine/boot-validator/action-wiring.ts +149 -0
  178. package/src/engine/boot-validator/boot-check.ts +21 -0
  179. package/src/engine/boot-validator/config-deps.ts +40 -4
  180. package/src/engine/boot-validator/entity-handler.ts +20 -21
  181. package/src/engine/boot-validator/entity-list-screens.ts +88 -0
  182. package/src/engine/boot-validator/gdpr-storage.ts +0 -20
  183. package/src/engine/boot-validator/i18n-keys.ts +58 -4
  184. package/src/engine/boot-validator/index.ts +33 -12
  185. package/src/engine/boot-validator/nav.ts +125 -0
  186. package/src/engine/boot-validator/pii-retention.ts +95 -10
  187. package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +214 -191
  188. package/src/engine/boot-validator/workspaces.ts +68 -0
  189. package/src/engine/build-app-schema.ts +16 -0
  190. package/src/engine/build-config-feature-schema.ts +44 -7
  191. package/src/engine/codemod/pipeline-codemod.ts +5 -5
  192. package/src/engine/config-helpers.ts +15 -0
  193. package/src/engine/constants.ts +32 -6
  194. package/src/engine/create-app.ts +11 -0
  195. package/src/engine/define-feature.ts +95 -947
  196. package/src/engine/define-handler.ts +28 -94
  197. package/src/engine/define-workflow.ts +1 -1
  198. package/src/engine/effective-features.ts +12 -2
  199. package/src/engine/entity-handlers.ts +76 -11
  200. package/src/engine/extensions/tenant-data.ts +19 -0
  201. package/src/engine/extensions/user-data.ts +29 -2
  202. package/src/engine/factories.ts +8 -49
  203. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +26 -29
  204. package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
  205. package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
  206. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
  207. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
  208. package/src/engine/feature-ast/__tests__/parse.test.ts +1640 -160
  209. package/src/engine/feature-ast/__tests__/patch.test.ts +206 -12
  210. package/src/engine/feature-ast/__tests__/patcher.test.ts +20 -23
  211. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +380 -5
  212. package/src/engine/feature-ast/extractors/events.ts +322 -0
  213. package/src/engine/feature-ast/extractors/handlers.ts +234 -0
  214. package/src/engine/feature-ast/extractors/hooks.ts +243 -0
  215. package/src/engine/feature-ast/extractors/index.ts +34 -31
  216. package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
  217. package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
  218. package/src/engine/feature-ast/extractors/round1.ts +3 -3
  219. package/src/engine/feature-ast/extractors/round5.ts +3 -3
  220. package/src/engine/feature-ast/extractors/round6.ts +2 -36
  221. package/src/engine/feature-ast/extractors/shared.ts +64 -6
  222. package/src/engine/feature-ast/index.ts +2 -4
  223. package/src/engine/feature-ast/parse.ts +130 -23
  224. package/src/engine/feature-ast/patch.ts +39 -50
  225. package/src/engine/feature-ast/patcher.ts +37 -38
  226. package/src/engine/feature-ast/patterns.ts +50 -62
  227. package/src/engine/feature-ast/render.ts +67 -44
  228. package/src/engine/feature-builder-state.ts +168 -0
  229. package/src/engine/feature-config-events-jobs.ts +403 -0
  230. package/src/engine/feature-entity-handlers.ts +208 -0
  231. package/src/engine/feature-manifest.ts +2 -1
  232. package/src/engine/feature-ui-extensions.ts +500 -0
  233. package/src/engine/field-access.ts +13 -2
  234. package/src/engine/field-helpers.ts +31 -0
  235. package/src/engine/handler-helpers.ts +26 -0
  236. package/src/engine/hook-helpers.ts +16 -0
  237. package/src/engine/index.ts +23 -7
  238. package/src/engine/membership-roles.ts +13 -0
  239. package/src/engine/object-form.ts +27 -0
  240. package/src/engine/ownership.ts +26 -79
  241. package/src/engine/pattern-library/__tests__/library.test.ts +7 -20
  242. package/src/engine/pattern-library/library.ts +44 -1152
  243. package/src/engine/pattern-library/mixed-schemas.ts +450 -0
  244. package/src/engine/pattern-library/opaque-schemas.ts +124 -0
  245. package/src/engine/pattern-library/shared-fields.ts +75 -0
  246. package/src/engine/pattern-library/static-schemas.ts +456 -0
  247. package/src/engine/pipeline.ts +6 -11
  248. package/src/engine/registry-facade.ts +362 -0
  249. package/src/engine/registry-ingest.ts +478 -0
  250. package/src/engine/registry-state.ts +388 -0
  251. package/src/engine/registry-validate.ts +642 -0
  252. package/src/engine/registry.ts +78 -1658
  253. package/src/engine/run-pipeline.ts +1 -1
  254. package/src/engine/schema-builder.ts +1 -0
  255. package/src/engine/screen-helpers.ts +54 -0
  256. package/src/engine/soft-delete-cleanup.ts +6 -2
  257. package/src/engine/steps/__tests__/duration-utils.test.ts +20 -0
  258. package/src/engine/steps/_duration-utils.ts +2 -0
  259. package/src/engine/steps/unsafe-projection-upsert.ts +1 -4
  260. package/src/engine/tier-resolver-extension.ts +3 -2
  261. package/src/engine/types/config.ts +2 -482
  262. package/src/engine/types/define-handler.ts +2 -0
  263. package/src/engine/types/entity-handlers.ts +2 -0
  264. package/src/engine/types/event-type-map.ts +1 -37
  265. package/src/engine/types/feature.ts +2 -972
  266. package/src/engine/types/fields.ts +2 -675
  267. package/src/engine/types/handlers.ts +2 -774
  268. package/src/engine/types/hooks.ts +2 -184
  269. package/src/engine/types/http-route.ts +1 -54
  270. package/src/engine/types/identifiers.ts +1 -47
  271. package/src/engine/types/index.ts +86 -40
  272. package/src/engine/types/nav.ts +2 -63
  273. package/src/engine/types/ownership.ts +2 -0
  274. package/src/engine/types/projection.ts +2 -138
  275. package/src/engine/types/relations.ts +1 -51
  276. package/src/engine/types/screen.ts +2 -574
  277. package/src/engine/types/step.ts +2 -334
  278. package/src/engine/types/target-ref.ts +1 -21
  279. package/src/engine/types/tree-node.ts +1 -132
  280. package/src/engine/types/workspace.ts +2 -49
  281. package/src/engine/validate-projection-allowlist.ts +6 -6
  282. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +120 -1
  283. package/src/entrypoint/index.ts +49 -6
  284. package/src/errors/classes.ts +22 -1
  285. package/src/errors/field-issue.ts +0 -3
  286. package/src/errors/index.ts +1 -1
  287. package/src/errors/write-error-info.ts +10 -22
  288. package/src/es-ops/README.md +1 -1
  289. package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
  290. package/src/es-ops/context.ts +4 -2
  291. package/src/es-ops/types.ts +8 -1
  292. package/src/event-store/__tests__/admin-api.integration.test.ts +27 -1
  293. package/src/event-store/__tests__/backfill-pii.integration.test.ts +279 -0
  294. package/src/event-store/__tests__/event-store.integration.test.ts +201 -0
  295. package/src/event-store/__tests__/row-to-stored-event.test.ts +2 -2
  296. package/src/event-store/__tests__/snapshot.integration.test.ts +86 -0
  297. package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +58 -0
  298. package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
  299. package/src/event-store/admin-api.ts +11 -4
  300. package/src/event-store/errors.ts +2 -35
  301. package/src/event-store/event-store.ts +64 -78
  302. package/src/event-store/events-schema.ts +11 -13
  303. package/src/event-store/index.ts +18 -3
  304. package/src/event-store/rebuild-dead-letter.ts +111 -0
  305. package/src/event-store/snapshot.ts +63 -40
  306. package/src/event-store/types.ts +2 -0
  307. package/src/files/__tests__/build-storage-key.test.ts +28 -0
  308. package/src/files/__tests__/file-ref-entity.test.ts +8 -0
  309. package/src/files/__tests__/files.integration.test.ts +24 -0
  310. package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
  311. package/src/files/__tests__/local-provider.test.ts +31 -0
  312. package/src/files/__tests__/provider-resolver.test.ts +70 -0
  313. package/src/files/__tests__/write-stream.test.ts +13 -0
  314. package/src/files/file-handle.ts +2 -19
  315. package/src/files/file-ref-entity.ts +2 -2
  316. package/src/files/file-routes.ts +13 -0
  317. package/src/files/index.ts +1 -1
  318. package/src/files/local-provider.ts +27 -8
  319. package/src/files/provider-resolver.ts +31 -20
  320. package/src/files/types.ts +13 -55
  321. package/src/i18n/__tests__/required-surface-keys.test.ts +17 -0
  322. package/src/i18n/required-surface-keys.ts +120 -1
  323. package/src/jobs/__tests__/job-queue-depth.integration.test.ts +82 -0
  324. package/src/jobs/__tests__/jobs.integration.test.ts +329 -3
  325. package/src/jobs/job-runner.ts +82 -12
  326. package/src/logging/types.ts +1 -7
  327. package/src/migrations/pending-rebuilds.ts +1 -1
  328. package/src/observability/__tests__/observability.integration.test.ts +4 -1
  329. package/src/observability/__tests__/recording-tracer.test.ts +6 -4
  330. package/src/observability/index.ts +2 -0
  331. package/src/observability/noop-provider.ts +0 -9
  332. package/src/observability/recording-tracer.ts +0 -12
  333. package/src/observability/standard-metrics.ts +64 -2
  334. package/src/observability/types/index.ts +1 -29
  335. package/src/observability/types/metric.ts +1 -56
  336. package/src/observability/types/provider.ts +1 -32
  337. package/src/observability/types/span.ts +1 -64
  338. package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
  339. package/src/pipeline/__tests__/dispatcher.test.ts +366 -1
  340. package/src/pipeline/__tests__/event-consumer-state.integration.test.ts +31 -0
  341. package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
  342. package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +263 -0
  343. package/src/pipeline/__tests__/event-dispatcher.integration.test.ts +10 -0
  344. package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +106 -0
  345. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +308 -77
  346. package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
  347. package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
  348. package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +80 -2
  349. package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +274 -0
  350. package/src/pipeline/__tests__/try-append-event.integration.test.ts +166 -0
  351. package/src/pipeline/dispatch-batch.ts +187 -0
  352. package/src/pipeline/dispatch-query.ts +165 -0
  353. package/src/pipeline/dispatch-shared.ts +826 -0
  354. package/src/pipeline/dispatch-stream.ts +90 -0
  355. package/src/pipeline/dispatch-write.ts +451 -0
  356. package/src/pipeline/dispatcher-utils.ts +1 -1
  357. package/src/pipeline/dispatcher.ts +44 -1372
  358. package/src/pipeline/entity-cache.ts +2 -33
  359. package/src/pipeline/event-consumer-state.ts +30 -2
  360. package/src/pipeline/event-dispatcher-admin.ts +293 -0
  361. package/src/pipeline/event-dispatcher-delivery.ts +305 -0
  362. package/src/pipeline/event-dispatcher.ts +85 -542
  363. package/src/pipeline/index.ts +2 -0
  364. package/src/pipeline/msp-rebuild.ts +42 -3
  365. package/src/pipeline/multi-stream-apply-context.ts +4 -42
  366. package/src/pipeline/projection-rebuild.ts +105 -3
  367. package/src/pipeline/system-hooks.ts +144 -1
  368. package/src/rate-limit/__tests__/resolver.integration.test.ts +18 -0
  369. package/src/rate-limit/resolver.ts +16 -32
  370. package/src/schema-cli.ts +76 -16
  371. package/src/search/__tests__/meilisearch-adapter.integration.test.ts +207 -185
  372. package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
  373. package/src/search/__tests__/reindex-entity.integration.test.ts +144 -0
  374. package/src/search/index.ts +6 -0
  375. package/src/search/meilisearch-adapter.ts +13 -12
  376. package/src/search/reindex-entity.ts +177 -0
  377. package/src/search/types.ts +1 -39
  378. package/src/secrets/__tests__/contains-secret.test.ts +34 -0
  379. package/src/secrets/__tests__/envelope-cipher.test.ts +59 -0
  380. package/src/secrets/__tests__/envelope.test.ts +1 -1
  381. package/src/secrets/dek-cache.ts +26 -5
  382. package/src/secrets/envelope-cipher.ts +53 -0
  383. package/src/secrets/envelope.ts +5 -3
  384. package/src/secrets/index.ts +13 -1
  385. package/src/secrets/stored-envelope.ts +46 -0
  386. package/src/secrets/types.ts +2 -162
  387. package/src/stack/__tests__/event-collector.test.ts +42 -0
  388. package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +125 -0
  389. package/src/stack/db.ts +2 -1
  390. package/src/stack/push-entity-projection-tables.ts +4 -3
  391. package/src/stack/redis.ts +8 -0
  392. package/src/stack/request-helper.ts +38 -2
  393. package/src/stack/table-helpers.ts +6 -4
  394. package/src/stack/test-stack.ts +249 -131
  395. package/src/testing/__tests__/late-bound.test.ts +32 -0
  396. package/src/testing/__tests__/wait-for.test.ts +59 -0
  397. package/src/testing/boot-validator-fixture.ts +121 -0
  398. package/src/testing/e2e-generator.ts +8 -0
  399. package/src/testing/file-provider-contract.ts +104 -0
  400. package/src/testing/handler-context.ts +3 -1
  401. package/src/testing/index.ts +5 -0
  402. package/src/testing/late-bound.ts +5 -3
  403. package/src/testing/mutable-master-key-provider.ts +29 -3
  404. package/src/testing/wait-for.ts +3 -0
  405. package/src/testing/without-ambient-temporal.ts +14 -0
  406. package/src/time/__tests__/tz-dateline.test.ts +8 -10
  407. package/src/time/geo-tz.ts +1 -32
  408. package/src/time/index.ts +1 -0
  409. package/src/time/legacy-date.ts +18 -0
  410. package/src/time/polyfill.ts +21 -38
  411. package/src/time/tz-context.ts +44 -85
  412. package/src/ui-types/app-schema.ts +12 -0
  413. package/src/ui-types/index.ts +20 -7
  414. package/src/utils/__tests__/safe-json-temporal.test.ts +18 -0
  415. package/src/utils/safe-json.ts +13 -1
  416. package/src/__tests__/raw-table.integration.test.ts +0 -116
  417. package/src/bun-db/__tests__/bun-test-stack.ts +0 -6
  418. package/src/db/__tests__/encryption.test.ts +0 -39
  419. package/src/db/encryption.ts +0 -39
  420. package/src/db/row-helpers.ts +0 -4
  421. package/src/engine/__tests__/raw-table.test.ts +0 -150
  422. package/src/engine/__tests__/unmanaged-table.test.ts +0 -127
  423. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +0 -184
  424. package/src/engine/feature-ast/extractors/round4.ts +0 -1366
@@ -0,0 +1,478 @@
1
+ import { resolveTableName } from "../db/entity-table-meta";
2
+ import { buildMetricName, validateMetricName } from "../observability";
3
+ import type { RegistryState } from "./registry-state";
4
+ import { mergeHookList, mergeHookListQualified, qualify } from "./registry-state";
5
+ import type { FeatureDefinition } from "./types";
6
+
7
+ // Feature registration + entities (globally-unique, physical-table-checked) + relations
8
+ // (additive per entity, duplicate-per-name guarded).
9
+ export function populateFeatureCore(state: RegistryState, feature: FeatureDefinition): void {
10
+ if (state.featureMap.has(feature.name)) {
11
+ throw new Error(`Duplicate feature: "${feature.name}"`);
12
+ }
13
+ state.featureMap.set(feature.name, feature);
14
+
15
+ // Entities: NOT prefixed — entity names must be globally unique
16
+ for (const [name, entity] of Object.entries(feature.entities ?? {})) {
17
+ if (state.entityMap.has(name)) {
18
+ throw new Error(`Duplicate entity: "${name}" (registered by multiple features)`);
19
+ }
20
+ state.entityMap.set(name, entity);
21
+ const physical = resolveTableName(name, entity, feature.name);
22
+ const clash = state.physicalTableOwners.get(physical);
23
+ if (clash?.kind === "store") {
24
+ throw new Error(
25
+ `Entity "${name}" (feature "${feature.name}") has physical table "${physical}" which ` +
26
+ `collides with r.storeTable("${physical}") (feature "${clash.featureName}"). ` +
27
+ `Pick a different tableName — both would emit CREATE TABLE "${physical}".`,
28
+ );
29
+ }
30
+ // Entity-vs-entity ist genauso fatal: zwei Entities mit explizitem,
31
+ // identischem tableName überschrieben sich hier vorher still —
32
+ // doppeltes CREATE TABLE bzw. eine Projektion frisst die andere.
33
+ if (clash?.kind === "entity") {
34
+ throw new Error(
35
+ `Entity "${name}" (feature "${feature.name}") has physical table "${physical}" which ` +
36
+ `collides with entity "${clash.owner}" (feature "${clash.featureName}"). ` +
37
+ `Pick a different tableName — both would project into "${physical}".`,
38
+ );
39
+ }
40
+ state.physicalTableOwners.set(physical, {
41
+ kind: "entity",
42
+ owner: name,
43
+ featureName: feature.name,
44
+ });
45
+ }
46
+
47
+ // Relations: entityName (not prefixed)
48
+ for (const [entityName, rels] of Object.entries(feature.relations ?? {})) {
49
+ const existing = state.relationMap.get(entityName) ?? {};
50
+ for (const [relName, relDef] of Object.entries(rels)) {
51
+ if (existing[relName]) {
52
+ throw new Error(
53
+ `Duplicate relation: "${entityName}.${relName}" (registered by multiple features)`,
54
+ );
55
+ }
56
+ existing[relName] = relDef;
57
+ }
58
+ state.relationMap.set(entityName, existing);
59
+ }
60
+ }
61
+
62
+ // Write + query handlers: qualified scope:type:name, duplicate-guarded.
63
+ export function populateHandlers(state: RegistryState, feature: FeatureDefinition): void {
64
+ // Write handlers: scope:write:name
65
+ for (const [name, handler] of Object.entries(feature.writeHandlers ?? {})) {
66
+ const qualified = qualify(feature.name, "write", name);
67
+ if (state.writeHandlerMap.has(qualified)) {
68
+ throw new Error(`Duplicate write handler: "${qualified}" (registered by multiple features)`);
69
+ }
70
+ state.writeHandlerMap.set(qualified, { ...handler, name: qualified });
71
+ state.handlerFeatureMap.set(qualified, feature.name);
72
+ }
73
+
74
+ // Query handlers: scope:query:name
75
+ for (const [name, handler] of Object.entries(feature.queryHandlers ?? {})) {
76
+ const qualified = qualify(feature.name, "query", name);
77
+ if (state.queryHandlerMap.has(qualified)) {
78
+ throw new Error(`Duplicate query handler: "${qualified}" (registered by multiple features)`);
79
+ }
80
+ state.queryHandlerMap.set(qualified, { ...handler, name: qualified });
81
+ state.handlerFeatureMap.set(qualified, feature.name);
82
+ }
83
+
84
+ // Stream handlers: scope:stream:name
85
+ for (const [name, handler] of Object.entries(feature.streamHandlers ?? {})) {
86
+ const qualified = qualify(feature.name, "stream", name);
87
+ if (state.streamHandlerMap.has(qualified)) {
88
+ throw new Error(`Duplicate stream handler: "${qualified}" (registered by multiple features)`);
89
+ }
90
+ state.streamHandlerMap.set(qualified, { ...handler, name: qualified });
91
+ state.handlerFeatureMap.set(qualified, feature.name);
92
+ }
93
+ }
94
+
95
+ // Config keys: scope:config:name, duplicate-guarded.
96
+ export function populateConfigKeys(state: RegistryState, feature: FeatureDefinition): void {
97
+ // Config keys: scope:config:name
98
+ for (const [key, keyDef] of Object.entries(feature.configKeys ?? {})) {
99
+ const qualifiedKey = qualify(feature.name, "config", key);
100
+ if (state.configKeyMap.has(qualifiedKey)) {
101
+ throw new Error(`Duplicate config key: "${qualifiedKey}" (registered by multiple features)`);
102
+ }
103
+ state.configKeyMap.set(qualifiedKey, keyDef);
104
+ }
105
+ }
106
+
107
+ // Jobs (runIn-pinned, duplicate-guarded) + notifications (trigger resolved later).
108
+ export function populateJobsAndNotifications(
109
+ state: RegistryState,
110
+ feature: FeatureDefinition,
111
+ ): void {
112
+ // Jobs: scope:job:name
113
+ for (const [name, jobDef] of Object.entries(feature.jobs ?? {})) {
114
+ const qualifiedName = qualify(feature.name, "job", name);
115
+ if (state.jobMap.has(qualifiedName)) {
116
+ throw new Error(`Duplicate job: "${qualifiedName}" (registered by multiple features)`);
117
+ }
118
+ // runIn runtime-check. TS's JobRunIn = Exclude<RunIn, "both"> already
119
+ // rejects "both" at compile time, but dynamically-constructed jobs
120
+ // (serialized config, plugin authors using `as any`) could slip it
121
+ // past the type system. Fail loud — "both" for jobs would mean "fan
122
+ // out to both lane-queues", which over-delivers; the routing assumes
123
+ // exactly one target queue per dispatch.
124
+ // @cast-boundary schema-walk — defensive runtime-check against bypassed type-system
125
+ const runIn = (jobDef as { runIn?: unknown }).runIn;
126
+ if (runIn !== undefined && runIn !== "api" && runIn !== "worker") {
127
+ throw new Error(
128
+ `Invalid runIn "${String(runIn)}" on job "${qualifiedName}" — jobs must be pinned to a single lane ("api" or "worker"). "both" is not allowed because BullMQ queues are lane-scoped.`,
129
+ );
130
+ }
131
+ state.jobMap.set(qualifiedName, { ...jobDef, name: qualifiedName });
132
+ }
133
+
134
+ // Notifications: scope:notify:name
135
+ for (const [name, notifDef] of Object.entries(feature.notifications ?? {})) {
136
+ const qualifiedName = qualify(feature.name, "notify", name);
137
+ state.notificationMap.set(qualifiedName, {
138
+ ...notifDef,
139
+ name: qualifiedName,
140
+ trigger: { on: notifDef.trigger.on },
141
+ });
142
+ state.notificationFeatureMap.set(qualifiedName, feature.name);
143
+ }
144
+ }
145
+
146
+ // Events: scope:event:name. Upcaster chains stitched after full ingest (see validateEventUpcasters).
147
+ export function populateEvents(state: RegistryState, feature: FeatureDefinition): void {
148
+ // Events: scope:event:name. Migrations stay keyed by feature+short-name
149
+ // in the FeatureDefinition and get stitched into the state.eventUpcasterMap
150
+ // below (after ALL features are ingested) so cross-feature validation has
151
+ // the complete picture.
152
+ for (const [eventName, eventDef] of Object.entries(feature.events ?? {})) {
153
+ const qualified = qualify(feature.name, "event", eventName);
154
+ state.eventMap.set(qualified, { ...eventDef, name: qualified });
155
+ }
156
+ }
157
+
158
+ export function populateTranslations(state: RegistryState, feature: FeatureDefinition): void {
159
+ // Translations prefixed with featureName: (i18next namespace convention).
160
+ // Keys that already carry the feature's own namespace prefix (e.g. a nav
161
+ // label referencing "cap-counter:nav.cap-list" verbatim) must NOT be
162
+ // re-prefixed, else server-side t() can never resolve them (#1105). Both
163
+ // bare and qualified spellings for the same key are accepted; if a
164
+ // feature defines both, whichever is declared last wins (Object.entries
165
+ // = insertion order, not "qualified always wins").
166
+ const prefix = `${feature.name}:`;
167
+ for (const [key, value] of Object.entries(feature.translations ?? {})) {
168
+ const qualifiedKey = key.startsWith(prefix) ? key : `${prefix}${key}`;
169
+ state.mergedTranslations[qualifiedKey] = value;
170
+ }
171
+ }
172
+
173
+ // Lifecycle hooks (handler-targeted, qualified) + entity hooks (entity-targeted,
174
+ // unprefixed) + search-payload-extensions (additive per entity).
175
+ export function populateHooks(state: RegistryState, feature: FeatureDefinition): void {
176
+ // Lifecycle hooks: keyed by handler QN. featureName rides along on each
177
+ // hook entry — defineFeature sets it, the registry just appends.
178
+ // Save/delete hooks target write handlers, query hooks target query handlers.
179
+ mergeHookListQualified(state.preSaveHooks, feature.hooks?.preSave, feature.name, "write");
180
+ mergeHookListQualified(state.postSaveHooks, feature.hooks?.postSave, feature.name, "write");
181
+ mergeHookListQualified(state.preDeleteHooks, feature.hooks?.preDelete, feature.name, "write");
182
+ mergeHookListQualified(state.postDeleteHooks, feature.hooks?.postDelete, feature.name, "write");
183
+ mergeHookListQualified(state.preQueryHooks, feature.hooks?.preQuery, feature.name, "query");
184
+ mergeHookListQualified(state.postQueryHooks, feature.hooks?.postQuery, feature.name, "query");
185
+
186
+ // Entity hooks: NOT prefixed, keyed by entity name
187
+ mergeHookList(state.entityPostSaveHooks, feature.entityHooks?.postSave);
188
+ mergeHookList(state.entityPreDeleteHooks, feature.entityHooks?.preDelete);
189
+ mergeHookList(state.entityPostDeleteHooks, feature.entityHooks?.postDelete);
190
+ mergeHookList(state.entityPostQueryHooks, feature.entityHooks?.postQuery);
191
+
192
+ // F3 search-payload-extensions: per-entity contributors merged additively
193
+ for (const [entityName, contributors] of Object.entries(feature.searchPayloadExtensions ?? {})) {
194
+ const existing = state.searchPayloadExtensions.get(entityName) ?? [];
195
+ for (const c of contributors) existing.push(c);
196
+ state.searchPayloadExtensions.set(entityName, existing);
197
+ }
198
+ }
199
+
200
+ // Registrar extension definitions + usages + selectors + reference-data + config-seeds.
201
+ export function populateExtensionsAndSeeds(state: RegistryState, feature: FeatureDefinition): void {
202
+ // Registrar extensions: collect definitions and usages
203
+ for (const [extName, extDef] of Object.entries(feature.registrarExtensions ?? {})) {
204
+ if (state.extensionMap.has(extName)) {
205
+ throw new Error(
206
+ `Duplicate registrar extension: "${extName}" (registered by multiple features)`,
207
+ );
208
+ }
209
+ state.extensionMap.set(extName, extDef);
210
+ }
211
+ // Annotate the owner so consumers (readiness gating) can map a
212
+ // registration back to the feature's config keys + secrets.
213
+ state.extensionUsages.push(
214
+ ...(feature.extensionUsages ?? []).map((u) => ({ ...u, featureName: feature.name })),
215
+ );
216
+ for (const sel of feature.extensionSelectors ?? []) {
217
+ if (state.extensionSelectorMap.has(sel.extensionName)) {
218
+ throw new Error(
219
+ `Duplicate extension selector for "${sel.extensionName}" ` +
220
+ `(feature "${feature.name}") — one owning feature declares the selector.`,
221
+ );
222
+ }
223
+ state.extensionSelectorMap.set(sel.extensionName, sel.qualifiedKey);
224
+ }
225
+ state.allReferenceData.push(...(feature.referenceData ?? []));
226
+ state.allConfigSeeds.push(...(feature.configSeeds ?? []));
227
+ }
228
+
229
+ // Metrics (name-validated, globally-unique) + secret keys (already qualified).
230
+ export function populateMetricsAndSecrets(state: RegistryState, feature: FeatureDefinition): void {
231
+ // Metrics: validate + qualify per feature. Collisions across features are
232
+ // rejected here — two features can't both register "created_total" under
233
+ // different shapes (labels/type) because the resulting fully qualified
234
+ // names differ, but same short+feature combo would already fail in
235
+ // defineFeature. This loop catches cross-feature/extension edge cases.
236
+ for (const [shortName, def] of Object.entries(feature.metrics ?? {})) {
237
+ const fullName = buildMetricName(feature.name, shortName);
238
+ validateMetricName(fullName, def.type);
239
+ if (state.metricMap.has(fullName)) {
240
+ throw new Error(
241
+ `[Kumiko Observability] Metric "${fullName}" registered multiple times ` +
242
+ `(Feature: ${feature.name}). Metric names must be globally unique.`,
243
+ );
244
+ }
245
+ state.metricMap.set(fullName, { ...def, featureName: feature.name });
246
+ }
247
+
248
+ // Secret keys: already qualified during defineFeature (same "<feature>:<short>"
249
+ // convention used elsewhere). Reject cross-feature duplicates — extensions
250
+ // could theoretically register on another feature's namespace.
251
+ for (const def of Object.values(feature.secretKeys ?? {})) {
252
+ if (state.secretKeyMap.has(def.qualifiedName)) {
253
+ throw new Error(
254
+ `[Kumiko Secrets] Secret key "${def.qualifiedName}" registered multiple times. ` +
255
+ "Secret names must be globally unique across features.",
256
+ );
257
+ }
258
+ state.secretKeyMap.set(def.qualifiedName, def);
259
+ }
260
+ }
261
+
262
+ // Explicit + multi-stream projections (source-entity indexed) + store tables +
263
+ // unmanaged tables (both cross-feature-uniqueness-by-physical-name guarded).
264
+ export function populateProjectionsAndTables(
265
+ state: RegistryState,
266
+ feature: FeatureDefinition,
267
+ ): void {
268
+ // Projections: qualified by feature name. Build the source-entity index so
269
+ // the event-store-executor can fetch matching projections in O(1) per write.
270
+ for (const [projName, projDef] of Object.entries(feature.projections ?? {})) {
271
+ const qualified = qualify(feature.name, "projection", projName);
272
+ if (state.projectionMap.has(qualified)) {
273
+ throw new Error(`Duplicate projection: "${qualified}" (registered by multiple features)`);
274
+ }
275
+ const stored = { ...projDef, name: qualified };
276
+ state.projectionMap.set(qualified, stored);
277
+ const sources = Array.isArray(projDef.source) ? projDef.source : [projDef.source];
278
+ for (const src of sources) {
279
+ const existing = state.projectionsBySource.get(src) ?? [];
280
+ existing.push(stored);
281
+ state.projectionsBySource.set(src, existing);
282
+ }
283
+ }
284
+
285
+ // Multi-stream projections: qualified + stored for later wiring into
286
+ // event-dispatcher. Namespace is shared with single-stream projections —
287
+ // defineFeature already catches name collisions inside one feature, but
288
+ // we also guard the cross-feature case here.
289
+ for (const [mspName, mspDef] of Object.entries(feature.multiStreamProjections ?? {})) {
290
+ const qualified = qualify(feature.name, "projection", mspName);
291
+ if (state.projectionMap.has(qualified) || state.multiStreamProjectionMap.has(qualified)) {
292
+ throw new Error(`Duplicate projection: "${qualified}" (registered by multiple features)`);
293
+ }
294
+ // runIn runtime-check. TS's RunIn union already enforces the three
295
+ // values at compile time; this guards dynamically-constructed MSPs
296
+ // (config-driven, plugin authors) that could slip a typo through.
297
+ // @cast-boundary schema-walk — defensive runtime-check against bypassed type-system
298
+ const mspRunIn = (mspDef as { runIn?: unknown }).runIn;
299
+ if (
300
+ mspRunIn !== undefined &&
301
+ mspRunIn !== "api" &&
302
+ mspRunIn !== "worker" &&
303
+ mspRunIn !== "both"
304
+ ) {
305
+ throw new Error(
306
+ `Invalid runIn "${String(mspRunIn)}" on MSP "${qualified}" — must be "api", "worker", or "both".`,
307
+ );
308
+ }
309
+ state.multiStreamProjectionMap.set(qualified, { ...mspDef, name: qualified });
310
+ state.multiStreamProjectionFeatureMap.set(qualified, feature.name);
311
+ }
312
+
313
+ // Store tables: aggregated by feature-local short name (unprefixed —
314
+ // these bypass the qualified-name namespace because they have no
315
+ // event-stream binding to disambiguate). Reject cross-feature
316
+ // duplicates at boot so the dev-server doesn't race two CREATE TABLE
317
+ // statements that target the same physical table name. Two features
318
+ // registering the same physical tableName would also race two CREATE
319
+ // TABLE statements via migrate-runner.
320
+ for (const [storeName, storeDef] of Object.entries(feature.storeTables ?? {})) {
321
+ const existing = state.storeTableMap.get(storeName);
322
+ if (existing) {
323
+ throw new Error(
324
+ `Store-table "${storeName}" registered by both feature "${existing.featureName}" and ` +
325
+ `"${feature.name}". Pick a feature-prefixed name to disambiguate.`,
326
+ );
327
+ }
328
+ const physicalClash = state.physicalTableOwners.get(storeName);
329
+ if (physicalClash?.kind === "entity") {
330
+ throw new Error(
331
+ `Store-table "${storeName}" (feature "${feature.name}") collides with the physical ` +
332
+ `table of entity "${physicalClash.owner}" (feature "${physicalClash.featureName}"). ` +
333
+ `Pick a different tableName — both would emit CREATE TABLE "${storeName}".`,
334
+ );
335
+ }
336
+ const piiFields = storeDef.meta.piiSubjectFields ?? [];
337
+ if (piiFields.length > 0 && !storeDef.piiEncryptedOnWrite) {
338
+ throw new Error(
339
+ `Store-table "${storeName}" (feature "${feature.name}") has PII-annotated fields ` +
340
+ `(${piiFields.join(", ")}) but direct writes bypass the executor's PII encryption. ` +
341
+ `Encrypt those fields before every insert/update (encryptPiiFieldValues) and declare ` +
342
+ `{ piiEncryptedOnWrite: true }, or drop the subject annotations.`,
343
+ );
344
+ }
345
+ state.physicalTableOwners.set(storeName, {
346
+ kind: "store",
347
+ owner: storeName,
348
+ featureName: feature.name,
349
+ });
350
+ state.storeTableMap.set(storeName, { ...storeDef, featureName: feature.name });
351
+ }
352
+ }
353
+
354
+ // Claim keys + auth-claims hooks (declaredShortNames threads the auto-prefix
355
+ // warning-set from claim-key declarations into the hooks registered right after —
356
+ // reordered next to each other; originally separated by the screens/nav/workspace
357
+ // block below, which has no dependency on either).
358
+ export function populateClaimsAndAuth(state: RegistryState, feature: FeatureDefinition): void {
359
+ // Claim keys: aggregated by qualified name. Two features cannot collide
360
+ // here (qualified by feature name), but we still guard for explicit
361
+ // correctness — the only way to hit this is a hand-built FeatureDefinition
362
+ // bypassing defineFeature's per-feature duplicate check.
363
+ const declaredShortNames = new Set<string>();
364
+ for (const def of Object.values(feature.claimKeys ?? {})) {
365
+ if (state.claimKeyMap.has(def.qualifiedName)) {
366
+ throw new Error(
367
+ `[Kumiko ClaimKeys] Claim key "${def.qualifiedName}" registered multiple times. ` +
368
+ "Claim short-names must be globally unique across features.",
369
+ );
370
+ }
371
+ state.claimKeyMap.set(def.qualifiedName, def);
372
+ declaredShortNames.add(def.shortName);
373
+ }
374
+ // Auth-claims hooks: order of registration is preserved. Feature name is
375
+ // captured alongside so the resolver can apply the auto-prefix at merge
376
+ // time — the feature author never ships pre-prefixed keys.
377
+ //
378
+ // If the feature declared ANY claim keys, every hook from that feature
379
+ // gets the declaredShortNames set attached. The resolver uses it to warn
380
+ // on undeclared inner-keys (typo / rename drift). Features that don't
381
+ // declare claimKeys skip the check entirely — it's opt-in.
382
+ const declaredKeys = declaredShortNames.size > 0 ? declaredShortNames : undefined;
383
+ for (const fn of feature.authClaimsHooks ?? []) {
384
+ state.authClaimsHooks.push({
385
+ featureName: feature.name,
386
+ fn,
387
+ ...(declaredKeys && { declaredKeys }),
388
+ });
389
+ }
390
+ }
391
+
392
+ // Screens + nav (qualified, entity/parent indexed) + workspaces (nav membership
393
+ // pass 1 — pass 2 folds self-assigned nav entries in after full ingest, see
394
+ // finalizeWorkspaceNavMembership) + tree-actions (at-most-one per feature).
395
+ export function populateScreensNavWorkspaces(
396
+ state: RegistryState,
397
+ feature: FeatureDefinition,
398
+ ): void {
399
+ // Screens: qualified + stored. Uniqueness per-feature is enforced in
400
+ // defineFeature; cross-feature collisions are impossible because the
401
+ // qualified name includes the feature-prefix. The separate state.featureMap
402
+ // entry lets the nav resolver pause screens owned by disabled features
403
+ // in O(1) without walking every screen.
404
+ for (const [screenId, screenDef] of Object.entries(feature.screens ?? {})) {
405
+ const qualified = qualify(feature.name, "screen", screenId);
406
+ // Stored version overwrites `id` with the qualified name so callers
407
+ // never need a reverse index (NavDef → qn) during tree-walking.
408
+ // Same pattern as state.writeHandlerMap/state.projectionMap/state.multiStreamProjectionMap
409
+ // (see `{ ...def, name: qualified }` above). Feature-side
410
+ // `feature.screens[shortId]` keeps the short id — only the registry
411
+ // surface flips.
412
+ const stored = { ...screenDef, id: qualified };
413
+ state.screenMap.set(qualified, stored);
414
+ state.screenFeatureMap.set(qualified, feature.name);
415
+ // entity-Index nur für Screens die direkt an einer Entity hängen.
416
+ // entityList/entityEdit haben `entity`; custom + actionForm haben
417
+ // keinen entity-Bezug (custom ist opaque, actionForm hat inline
418
+ // fields ohne Entity-Reference).
419
+ if (stored.type === "entityList" || stored.type === "entityEdit") {
420
+ const existing = state.screensByEntity.get(stored.entity) ?? [];
421
+ existing.push(stored);
422
+ state.screensByEntity.set(stored.entity, existing);
423
+ }
424
+ }
425
+
426
+ // Nav entries: same qualification pattern as screens. The parent/screen
427
+ // refs are boot-validated below (after all features are ingested, so
428
+ // cross-feature parents can resolve). parent-index is built in the same
429
+ // loop because `parent` refers to a qualified name that doesn't need
430
+ // resolution — just string equality with whatever's in the target
431
+ // entry's QN.
432
+ for (const [navId, navDef] of Object.entries(feature.navs ?? {})) {
433
+ const qualified = qualify(feature.name, "nav", navId);
434
+ // See screens above — stored version carries the qualified id so
435
+ // resolveNavigation can recurse via getNavsByParent(child.id) without
436
+ // hand-building a reverse index.
437
+ const stored = { ...navDef, id: qualified };
438
+ state.navMap.set(qualified, stored);
439
+ state.navFeatureMap.set(qualified, feature.name);
440
+ if (stored.parent === undefined) {
441
+ state.topLevelNavs.push(stored);
442
+ } else {
443
+ const existing = state.navsByParent.get(stored.parent) ?? [];
444
+ existing.push(stored);
445
+ state.navsByParent.set(stored.parent, existing);
446
+ }
447
+ }
448
+
449
+ // Workspaces: same qualification pattern as nav/screen. Step one stores
450
+ // the workspace itself + its explicit nav list; step two (after every
451
+ // feature has been ingested) folds nav-self-assigned QNs into the same
452
+ // member list. Doing it in two passes keeps cross-feature workspace
453
+ // refs valid — a nav entry can self-assign to a workspace whose feature
454
+ // hasn't been ingested yet.
455
+ for (const [wsId, wsDef] of Object.entries(feature.workspaces ?? {})) {
456
+ const qualified = qualify(feature.name, "workspace", wsId);
457
+ const stored = { ...wsDef, id: qualified };
458
+ state.workspaceMap.set(qualified, stored);
459
+ state.workspaceFeatureMap.set(qualified, feature.name);
460
+ // Seed the membership list with the workspace's explicit nav refs in
461
+ // declaration order. Boot-validator checks the QNs resolve.
462
+ state.navsByWorkspace.set(qualified, [...(stored.nav ?? [])]);
463
+ if (stored.default === true) {
464
+ // Boot-validator enforces uniqueness; here we just remember the
465
+ // first one and let validateBoot complain if there's a second.
466
+ if (state.defaultWorkspace === undefined) {
467
+ state.defaultWorkspace = stored;
468
+ }
469
+ }
470
+ }
471
+
472
+ // Tree-Actions slot — at-most-one per feature (only-once-guard im
473
+ // registrar). Erased Map für Runtime-Lookup; compile-time-typed
474
+ // Surface läuft über FeatureDefinition.exports (TreeActionsHandle).
475
+ if (feature.treeActions !== undefined) {
476
+ state.treeActionsMap.set(feature.name, feature.treeActions);
477
+ }
478
+ }