@cosmicdrift/kumiko-framework 0.1.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 (388) hide show
  1. package/README.md +159 -0
  2. package/package.json +91 -0
  3. package/src/__tests__/anonymous-access.integration.ts +325 -0
  4. package/src/__tests__/error-contract.integration.ts +435 -0
  5. package/src/__tests__/field-access.integration.ts +269 -0
  6. package/src/__tests__/full-stack.integration.ts +914 -0
  7. package/src/__tests__/ownership.integration.ts +449 -0
  8. package/src/__tests__/reference-data.integration.ts +198 -0
  9. package/src/__tests__/transition-guard.integration.ts +340 -0
  10. package/src/api/__tests__/api.test.ts +337 -0
  11. package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
  13. package/src/api/__tests__/batch.integration.ts +404 -0
  14. package/src/api/__tests__/body-limit.test.ts +88 -0
  15. package/src/api/__tests__/csrf-middleware.test.ts +97 -0
  16. package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
  17. package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
  18. package/src/api/__tests__/nested-write.integration.ts +213 -0
  19. package/src/api/__tests__/readiness.test.ts +76 -0
  20. package/src/api/__tests__/request-id-middleware.test.ts +72 -0
  21. package/src/api/__tests__/sse-broker.test.ts +58 -0
  22. package/src/api/__tests__/sse-route.test.ts +112 -0
  23. package/src/api/anonymous-cookie.ts +60 -0
  24. package/src/api/api-constants.ts +64 -0
  25. package/src/api/auth-middleware.ts +418 -0
  26. package/src/api/auth-routes.ts +982 -0
  27. package/src/api/csrf-middleware.ts +77 -0
  28. package/src/api/index.ts +31 -0
  29. package/src/api/jwt.ts +66 -0
  30. package/src/api/observability-middleware.ts +89 -0
  31. package/src/api/readiness.ts +132 -0
  32. package/src/api/request-context.ts +49 -0
  33. package/src/api/request-id-middleware.ts +50 -0
  34. package/src/api/route-registrars.ts +195 -0
  35. package/src/api/routes.ts +135 -0
  36. package/src/api/server.ts +640 -0
  37. package/src/api/sse-broker.ts +71 -0
  38. package/src/api/sse-route.ts +62 -0
  39. package/src/api/tokens.ts +16 -0
  40. package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
  41. package/src/db/__tests__/compound-types.test.ts +114 -0
  42. package/src/db/__tests__/connection-options.test.ts +68 -0
  43. package/src/db/__tests__/cursor.test.ts +41 -0
  44. package/src/db/__tests__/db-helpers.test.ts +369 -0
  45. package/src/db/__tests__/dialect-instant.test.ts +50 -0
  46. package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
  47. package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
  48. package/src/db/__tests__/encryption.test.ts +39 -0
  49. package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
  50. package/src/db/__tests__/event-store-executor.integration.ts +235 -0
  51. package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
  52. package/src/db/__tests__/located-timestamp.test.ts +184 -0
  53. package/src/db/__tests__/money.test.ts +199 -0
  54. package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
  55. package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
  56. package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
  57. package/src/db/__tests__/row-helpers.test.ts +59 -0
  58. package/src/db/__tests__/schema-migration.integration.ts +273 -0
  59. package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
  60. package/src/db/__tests__/table-builder-required.test.ts +216 -0
  61. package/src/db/__tests__/tenant-db.integration.ts +606 -0
  62. package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
  63. package/src/db/apply-entity-event.ts +188 -0
  64. package/src/db/assert-exists-in.ts +59 -0
  65. package/src/db/compound-types.ts +47 -0
  66. package/src/db/connection.ts +104 -0
  67. package/src/db/cursor.ts +83 -0
  68. package/src/db/dialect.ts +109 -0
  69. package/src/db/eagerload.ts +174 -0
  70. package/src/db/encryption.ts +39 -0
  71. package/src/db/event-store-executor.ts +906 -0
  72. package/src/db/index.ts +55 -0
  73. package/src/db/located-timestamp.ts +114 -0
  74. package/src/db/money.ts +120 -0
  75. package/src/db/pg-error.ts +46 -0
  76. package/src/db/reference-data.ts +77 -0
  77. package/src/db/row-helpers.ts +53 -0
  78. package/src/db/schema-inspection.ts +25 -0
  79. package/src/db/table-builder.ts +475 -0
  80. package/src/db/tenant-db.ts +434 -0
  81. package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
  82. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
  83. package/src/engine/__tests__/boot-validator.test.ts +1865 -0
  84. package/src/engine/__tests__/build-app-schema.test.ts +154 -0
  85. package/src/engine/__tests__/claim-keys.test.ts +274 -0
  86. package/src/engine/__tests__/config-helpers.test.ts +236 -0
  87. package/src/engine/__tests__/effective-features.test.ts +86 -0
  88. package/src/engine/__tests__/engine.test.ts +1461 -0
  89. package/src/engine/__tests__/entity-handlers.test.ts +274 -0
  90. package/src/engine/__tests__/event-helpers.test.ts +68 -0
  91. package/src/engine/__tests__/extends-registrar.test.ts +159 -0
  92. package/src/engine/__tests__/factories-long-text.test.ts +84 -0
  93. package/src/engine/__tests__/factories-time.test.ts +158 -0
  94. package/src/engine/__tests__/field-predicates.test.ts +48 -0
  95. package/src/engine/__tests__/hook-phases.test.ts +132 -0
  96. package/src/engine/__tests__/identifiers.test.ts +35 -0
  97. package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
  98. package/src/engine/__tests__/nav.test.ts +267 -0
  99. package/src/engine/__tests__/ownership.test.ts +421 -0
  100. package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
  101. package/src/engine/__tests__/projection-helpers.test.ts +62 -0
  102. package/src/engine/__tests__/projection.test.ts +191 -0
  103. package/src/engine/__tests__/qualified-name.test.ts +264 -0
  104. package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
  105. package/src/engine/__tests__/run-in.test.ts +38 -0
  106. package/src/engine/__tests__/schema-builder.test.ts +380 -0
  107. package/src/engine/__tests__/screen.test.ts +408 -0
  108. package/src/engine/__tests__/state-machine.test.ts +148 -0
  109. package/src/engine/__tests__/system-user.test.ts +57 -0
  110. package/src/engine/__tests__/validation-hooks.test.ts +71 -0
  111. package/src/engine/access.ts +23 -0
  112. package/src/engine/boot-validator.ts +1528 -0
  113. package/src/engine/build-app-schema.ts +125 -0
  114. package/src/engine/config-helpers.ts +115 -0
  115. package/src/engine/constants.ts +85 -0
  116. package/src/engine/create-app.ts +98 -0
  117. package/src/engine/define-feature.ts +702 -0
  118. package/src/engine/define-handler.ts +78 -0
  119. package/src/engine/define-roles.ts +19 -0
  120. package/src/engine/effective-features.ts +87 -0
  121. package/src/engine/entity-handlers.ts +364 -0
  122. package/src/engine/event-helpers.ts +73 -0
  123. package/src/engine/factories.ts +328 -0
  124. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
  125. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
  126. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
  127. package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
  128. package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
  129. package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
  130. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
  131. package/src/engine/feature-ast/extractors.ts +2562 -0
  132. package/src/engine/feature-ast/index.ts +105 -0
  133. package/src/engine/feature-ast/parse.ts +369 -0
  134. package/src/engine/feature-ast/patch.ts +525 -0
  135. package/src/engine/feature-ast/patcher.ts +518 -0
  136. package/src/engine/feature-ast/patterns.ts +434 -0
  137. package/src/engine/feature-ast/render.ts +602 -0
  138. package/src/engine/feature-ast/source-location.ts +45 -0
  139. package/src/engine/field-access.ts +120 -0
  140. package/src/engine/index.ts +254 -0
  141. package/src/engine/ownership.ts +337 -0
  142. package/src/engine/parse-ref-target.ts +22 -0
  143. package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
  144. package/src/engine/pattern-library/index.ts +24 -0
  145. package/src/engine/pattern-library/library.ts +1117 -0
  146. package/src/engine/pattern-library/types.ts +255 -0
  147. package/src/engine/projection-helpers.ts +85 -0
  148. package/src/engine/qualified-name.ts +122 -0
  149. package/src/engine/read-claim.ts +31 -0
  150. package/src/engine/registry.ts +1325 -0
  151. package/src/engine/resolve-config-or-param.ts +153 -0
  152. package/src/engine/run-in.ts +29 -0
  153. package/src/engine/schema-builder.ts +175 -0
  154. package/src/engine/screen-filter-ops.ts +51 -0
  155. package/src/engine/state-machine.ts +70 -0
  156. package/src/engine/system-user.ts +32 -0
  157. package/src/engine/types/config.ts +306 -0
  158. package/src/engine/types/event-type-map.ts +37 -0
  159. package/src/engine/types/feature.ts +574 -0
  160. package/src/engine/types/fields.ts +422 -0
  161. package/src/engine/types/handlers.ts +742 -0
  162. package/src/engine/types/hooks.ts +142 -0
  163. package/src/engine/types/http-route.ts +54 -0
  164. package/src/engine/types/identifiers.ts +47 -0
  165. package/src/engine/types/index.ts +208 -0
  166. package/src/engine/types/nav.ts +46 -0
  167. package/src/engine/types/projection.ts +132 -0
  168. package/src/engine/types/relations.ts +51 -0
  169. package/src/engine/types/screen.ts +452 -0
  170. package/src/engine/types/workspace.ts +42 -0
  171. package/src/engine/validation.ts +33 -0
  172. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
  173. package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
  174. package/src/entrypoint/index.ts +442 -0
  175. package/src/errors/__tests__/classes.test.ts +371 -0
  176. package/src/errors/__tests__/write-failures.test.ts +109 -0
  177. package/src/errors/classes.ts +249 -0
  178. package/src/errors/i18n/de.yaml +83 -0
  179. package/src/errors/i18n/en.yaml +80 -0
  180. package/src/errors/index.ts +41 -0
  181. package/src/errors/kumiko-error.ts +67 -0
  182. package/src/errors/reasons.ts +36 -0
  183. package/src/errors/serialize.ts +136 -0
  184. package/src/errors/transition-details.ts +30 -0
  185. package/src/errors/write-error-info.ts +123 -0
  186. package/src/errors/zod-bridge.ts +49 -0
  187. package/src/event-store/__tests__/admin-api.integration.ts +361 -0
  188. package/src/event-store/__tests__/event-store.integration.ts +584 -0
  189. package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
  190. package/src/event-store/__tests__/perf.integration.ts +255 -0
  191. package/src/event-store/__tests__/snapshot.integration.ts +267 -0
  192. package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
  193. package/src/event-store/__tests__/upcaster.integration.ts +460 -0
  194. package/src/event-store/admin-api.ts +257 -0
  195. package/src/event-store/archive.ts +106 -0
  196. package/src/event-store/errors.ts +35 -0
  197. package/src/event-store/event-store.ts +405 -0
  198. package/src/event-store/events-schema.ts +90 -0
  199. package/src/event-store/index.ts +50 -0
  200. package/src/event-store/snapshot.ts +210 -0
  201. package/src/event-store/upcaster-dead-letter.ts +119 -0
  202. package/src/event-store/upcaster.ts +147 -0
  203. package/src/files/__tests__/content-disposition.test.ts +123 -0
  204. package/src/files/__tests__/file-field-column.integration.ts +103 -0
  205. package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
  206. package/src/files/__tests__/file-handle.test.ts +122 -0
  207. package/src/files/__tests__/files.integration.ts +830 -0
  208. package/src/files/__tests__/storage-tracking.integration.ts +153 -0
  209. package/src/files/content-disposition.ts +55 -0
  210. package/src/files/file-handle.ts +63 -0
  211. package/src/files/file-ref-table.ts +22 -0
  212. package/src/files/file-routes.ts +353 -0
  213. package/src/files/in-memory-provider.ts +62 -0
  214. package/src/files/index.ts +29 -0
  215. package/src/files/local-provider.ts +35 -0
  216. package/src/files/storage-tracking.ts +60 -0
  217. package/src/files/types.ts +118 -0
  218. package/src/i18n/__tests__/i18n.test.ts +72 -0
  219. package/src/i18n/index.ts +29 -0
  220. package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
  221. package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
  222. package/src/jobs/__tests__/jobs.integration.ts +566 -0
  223. package/src/jobs/index.ts +2 -0
  224. package/src/jobs/job-runner.ts +574 -0
  225. package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
  226. package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
  227. package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
  228. package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
  229. package/src/lifecycle/index.ts +13 -0
  230. package/src/lifecycle/lifecycle.ts +160 -0
  231. package/src/lifecycle/signal-handlers.ts +62 -0
  232. package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
  233. package/src/logging/index.ts +3 -0
  234. package/src/logging/pino-logger.ts +64 -0
  235. package/src/logging/types.ts +7 -0
  236. package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
  237. package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
  238. package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
  239. package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
  240. package/src/migrations/index.ts +28 -0
  241. package/src/migrations/projection-detection.ts +149 -0
  242. package/src/migrations/rebuild-marker.ts +64 -0
  243. package/src/migrations/schema-drift.ts +395 -0
  244. package/src/observability/__tests__/console-provider.test.ts +67 -0
  245. package/src/observability/__tests__/metric-validator.test.ts +87 -0
  246. package/src/observability/__tests__/noop-provider.test.ts +82 -0
  247. package/src/observability/__tests__/observability.integration.ts +559 -0
  248. package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
  249. package/src/observability/__tests__/recording-meter.test.ts +101 -0
  250. package/src/observability/__tests__/recording-tracer.test.ts +110 -0
  251. package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
  252. package/src/observability/console-provider.ts +130 -0
  253. package/src/observability/context.ts +26 -0
  254. package/src/observability/fallback.ts +34 -0
  255. package/src/observability/ids.ts +25 -0
  256. package/src/observability/index.ts +79 -0
  257. package/src/observability/metric-validator.ts +86 -0
  258. package/src/observability/metrics-handle.ts +56 -0
  259. package/src/observability/noop-provider.ts +146 -0
  260. package/src/observability/prometheus-meter.ts +284 -0
  261. package/src/observability/recording-meter.ts +156 -0
  262. package/src/observability/recording-tracer.ts +198 -0
  263. package/src/observability/redis-wrapper.ts +132 -0
  264. package/src/observability/sensitive-filter.ts +108 -0
  265. package/src/observability/standard-metrics.ts +213 -0
  266. package/src/observability/types/index.ts +29 -0
  267. package/src/observability/types/metric.ts +56 -0
  268. package/src/observability/types/provider.ts +32 -0
  269. package/src/observability/types/span.ts +64 -0
  270. package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
  271. package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
  272. package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
  273. package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
  274. package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
  275. package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
  276. package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
  277. package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
  278. package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
  279. package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
  280. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
  281. package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
  282. package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
  283. package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
  284. package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
  285. package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
  286. package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
  287. package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
  288. package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
  289. package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
  290. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
  291. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
  292. package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
  293. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
  294. package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
  295. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
  296. package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
  297. package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
  298. package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
  299. package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
  300. package/src/pipeline/append-event-core.ts +117 -0
  301. package/src/pipeline/auth-claims-resolver.ts +103 -0
  302. package/src/pipeline/cascade-handler.ts +113 -0
  303. package/src/pipeline/dispatcher.ts +1585 -0
  304. package/src/pipeline/distributed-lock.ts +37 -0
  305. package/src/pipeline/entity-cache.ts +113 -0
  306. package/src/pipeline/event-consumer-state.ts +108 -0
  307. package/src/pipeline/event-dedup.ts +23 -0
  308. package/src/pipeline/event-dispatcher.ts +1016 -0
  309. package/src/pipeline/event-retention.ts +154 -0
  310. package/src/pipeline/idempotency.ts +76 -0
  311. package/src/pipeline/index.ts +66 -0
  312. package/src/pipeline/lifecycle-pipeline.ts +409 -0
  313. package/src/pipeline/msp-rebuild.ts +242 -0
  314. package/src/pipeline/multi-stream-apply-context.ts +115 -0
  315. package/src/pipeline/projection-rebuild.ts +334 -0
  316. package/src/pipeline/projection-state.ts +72 -0
  317. package/src/pipeline/projections-runner.ts +56 -0
  318. package/src/pipeline/redis-keys.ts +11 -0
  319. package/src/pipeline/system-hooks.ts +190 -0
  320. package/src/random/__tests__/generate.test.ts +149 -0
  321. package/src/random/generate.ts +141 -0
  322. package/src/random/index.ts +8 -0
  323. package/src/random/words.ts +392 -0
  324. package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
  325. package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
  326. package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
  327. package/src/rate-limit/bucket.ts +36 -0
  328. package/src/rate-limit/index.ts +14 -0
  329. package/src/rate-limit/middleware.ts +152 -0
  330. package/src/rate-limit/resolver.ts +267 -0
  331. package/src/redis/__tests__/redis-options.test.ts +54 -0
  332. package/src/redis/index.ts +74 -0
  333. package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
  334. package/src/search/__tests__/search-adapter.test.ts +256 -0
  335. package/src/search/in-memory-adapter.ts +123 -0
  336. package/src/search/index.ts +12 -0
  337. package/src/search/meilisearch-adapter.ts +106 -0
  338. package/src/search/types.ts +39 -0
  339. package/src/secrets/__tests__/dek-cache.test.ts +213 -0
  340. package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
  341. package/src/secrets/__tests__/envelope.test.ts +74 -0
  342. package/src/secrets/__tests__/leak-guard.test.ts +92 -0
  343. package/src/secrets/__tests__/rotation.test.ts +149 -0
  344. package/src/secrets/dek-cache.ts +116 -0
  345. package/src/secrets/env-master-key-provider.ts +162 -0
  346. package/src/secrets/envelope.ts +55 -0
  347. package/src/secrets/index.ts +19 -0
  348. package/src/secrets/leak-guard.ts +87 -0
  349. package/src/secrets/rotation.ts +34 -0
  350. package/src/secrets/types.ts +107 -0
  351. package/src/stack/db.ts +104 -0
  352. package/src/stack/event-collector.ts +23 -0
  353. package/src/stack/index.ts +32 -0
  354. package/src/stack/redis.ts +44 -0
  355. package/src/stack/request-helper.ts +168 -0
  356. package/src/stack/table-helpers.ts +104 -0
  357. package/src/stack/test-stack.ts +357 -0
  358. package/src/stack/test-users.ts +37 -0
  359. package/src/testing/__tests__/e2e-generator.test.ts +230 -0
  360. package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
  361. package/src/testing/access-assertions.ts +15 -0
  362. package/src/testing/assertions.ts +35 -0
  363. package/src/testing/e2e-generator.ts +465 -0
  364. package/src/testing/expect-error.ts +25 -0
  365. package/src/testing/handler-context.ts +125 -0
  366. package/src/testing/http-cookies.ts +52 -0
  367. package/src/testing/index.ts +41 -0
  368. package/src/testing/late-bound.ts +39 -0
  369. package/src/testing/mutable-master-key-provider.ts +31 -0
  370. package/src/testing/observability-recorder.ts +54 -0
  371. package/src/testing/shared-entities.ts +49 -0
  372. package/src/testing/utils.ts +1 -0
  373. package/src/testing/wait-for.ts +31 -0
  374. package/src/time/__tests__/polyfill.test.ts +73 -0
  375. package/src/time/__tests__/tz-context.test.ts +121 -0
  376. package/src/time/index.ts +21 -0
  377. package/src/time/polyfill.ts +70 -0
  378. package/src/time/tz-context.ts +107 -0
  379. package/src/ui-types/app-schema.ts +57 -0
  380. package/src/ui-types/index.ts +65 -0
  381. package/src/utils/__tests__/assert.test.ts +17 -0
  382. package/src/utils/__tests__/env-parse.test.ts +54 -0
  383. package/src/utils/assert.ts +18 -0
  384. package/src/utils/env-parse.ts +16 -0
  385. package/src/utils/ids.ts +16 -0
  386. package/src/utils/index.ts +5 -0
  387. package/src/utils/safe-json.ts +30 -0
  388. package/src/utils/serialization.ts +7 -0
@@ -0,0 +1,43 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { parseRefTarget } from "../parse-ref-target";
3
+
4
+ // Tier 2.7e Cross-Feature: parser-Konvention für ReferenceFieldDef.
5
+ // Same-feature default ist der häufige Pfad; cross-feature verlangt
6
+ // expliziten ":"-Prefix. Diese Tests pinnen die Aufteilungsregel
7
+ // damit Boot-Validator und Renderer (über Re-Export) übereinstimmen.
8
+
9
+ describe("parseRefTarget", () => {
10
+ test("kurzer name: featureName = currentFeature, entityName = raw", () => {
11
+ expect(parseRefTarget("user", "users")).toEqual({ featureName: "users", entityName: "user" });
12
+ });
13
+
14
+ test('qualifiziert "feature:entity": splittet am ersten Doppelpunkt', () => {
15
+ expect(parseRefTarget("users:user", "shop")).toEqual({
16
+ featureName: "users",
17
+ entityName: "user",
18
+ });
19
+ });
20
+
21
+ test("self-reference (kurz, gleicher entity-name) — currentFeature wandert durch", () => {
22
+ expect(parseRefTarget("category", "shop")).toEqual({
23
+ featureName: "shop",
24
+ entityName: "category",
25
+ });
26
+ });
27
+
28
+ test("kebab-case-feature: bleibt erhalten beim split", () => {
29
+ expect(parseRefTarget("public-status:incident", "shop")).toEqual({
30
+ featureName: "public-status",
31
+ entityName: "incident",
32
+ });
33
+ });
34
+
35
+ test("zweiter Doppelpunkt im entity-name (selten, aber möglich) — splittet nur am ERSTEN", () => {
36
+ // Konvention: feature und entity sind kebab-segments; der parser
37
+ // splittet conservative am ersten ":". Falls der Author einen
38
+ // entity-name mit ":" deklariert (was Boot-Validator ohnehin
39
+ // ablehnt), läuft er hier durch — der downstream-Lookup findet
40
+ // ihn dann nicht und failt.
41
+ expect(parseRefTarget("a:b:c", "shop")).toEqual({ featureName: "a", entityName: "b:c" });
42
+ });
43
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import type { StoredEvent } from "../../event-store/event-store";
3
+ import { setFields } from "../projection-helpers";
4
+ import type { ProjectionTable } from "../types/projection";
5
+
6
+ // Minimal fake table: only the `id` column is needed for setFields, and
7
+ // setFields only hands it to `eq()` — no actual SQL runs in the unit test.
8
+ // The cast mirrors ProjectionTable's deliberate erasure: the framework
9
+ // doesn't know user table shapes at compile time, so real Drizzle tables
10
+ // go through the same `any`-ish generic parameter.
11
+ const fakeIdCol = { name: "id" };
12
+ const fakeTable = { id: fakeIdCol, __name: "fake_table" } as unknown as ProjectionTable;
13
+
14
+ function makeFakeEvent(overrides: Partial<StoredEvent> = {}): StoredEvent {
15
+ return {
16
+ id: "evt-1",
17
+ aggregateId: "agg-42",
18
+ aggregateType: "invoice",
19
+ // biome-ignore lint/suspicious/noExplicitAny: test shim — TenantId is a branded string and we don't exercise that branch here.
20
+ tenantId: "tenant-1" as any,
21
+ version: 1,
22
+ type: "invoice.sent",
23
+ eventVersion: 1,
24
+ payload: {},
25
+ metadata: { userId: "u-1", requestId: "r-1" },
26
+ createdAt: Temporal.Instant.from("2026-04-17T00:00:00Z"),
27
+ createdBy: "u-1",
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ // Drizzle's tx.update(...).set(...).where(...) chain — capture each step.
33
+ function makeFakeTx() {
34
+ const where = vi.fn().mockResolvedValue(undefined);
35
+ const set = vi.fn(() => ({ where }));
36
+ const update = vi.fn(() => ({ set }));
37
+ return { fakeTx: { update } as never, update, set, where };
38
+ }
39
+
40
+ describe("setFields", () => {
41
+ test("returns an apply fn that UPDATEs the passed fields WHERE id = aggregateId", async () => {
42
+ const apply = setFields(fakeTable, { status: "sent" });
43
+ const { fakeTx, update, set } = makeFakeTx();
44
+ await apply(makeFakeEvent(), fakeTx);
45
+ expect(update).toHaveBeenCalledWith(fakeTable);
46
+ expect(set).toHaveBeenCalledWith({ status: "sent" });
47
+ });
48
+
49
+ test("fields as a function receives the event and returns the field values", async () => {
50
+ const apply = setFields(fakeTable, (event) => ({
51
+ status: (event.payload as { newStatus: string }).newStatus,
52
+ }));
53
+ const { fakeTx, set } = makeFakeTx();
54
+ await apply(makeFakeEvent({ payload: { newStatus: "cancelled" } }), fakeTx);
55
+ expect(set).toHaveBeenCalledWith({ status: "cancelled" });
56
+ });
57
+
58
+ test("throws at construction time when the table has no 'id' column", () => {
59
+ const tableWithoutId = { __name: "weird_table" } as unknown as ProjectionTable;
60
+ expect(() => setFields(tableWithoutId, { status: "x" })).toThrow(/no 'id' column/);
61
+ });
62
+ });
@@ -0,0 +1,191 @@
1
+ import { integer, pgTable, uuid } from "drizzle-orm/pg-core";
2
+ import { describe, expect, test } from "vitest";
3
+ import type { ProjectionDefinition } from "../../engine/types";
4
+ import { defineFeature } from "../define-feature";
5
+ import { createEntity, createTextField } from "../factories";
6
+ import { createRegistry } from "../registry";
7
+
8
+ // Throwaway Drizzle table reused across these tests. The runtime-behaviour of
9
+ // the projection itself (apply, TX semantics) is covered by the MietNomade
10
+ // integration suite; this file focuses on the registrar/registry contracts
11
+ // that fire BEFORE any write happens.
12
+ const testTable = pgTable("test_projection", {
13
+ id: uuid("id").primaryKey(),
14
+ count: integer("count").notNull().default(0),
15
+ });
16
+
17
+ function exampleEntity(name = "unit") {
18
+ return createEntity({
19
+ table: name,
20
+ fields: { name: createTextField() },
21
+ });
22
+ }
23
+
24
+ function exampleProjection(overrides: Partial<ProjectionDefinition> = {}): ProjectionDefinition {
25
+ return {
26
+ name: "units-per-property",
27
+ source: "unit",
28
+ table: testTable,
29
+ apply: {},
30
+ ...overrides,
31
+ };
32
+ }
33
+
34
+ describe("r.projection() — registration", () => {
35
+ test("stores the projection on the FeatureDefinition", () => {
36
+ const feature = defineFeature("test", (r) => {
37
+ r.entity("unit", exampleEntity());
38
+ r.projection(exampleProjection());
39
+ });
40
+ expect(feature.projections["units-per-property"]).toBeDefined();
41
+ expect(feature.projections["units-per-property"]?.source).toBe("unit");
42
+ });
43
+
44
+ test("rejects duplicate projection names within the same feature", () => {
45
+ expect(() =>
46
+ defineFeature("test", (r) => {
47
+ r.entity("unit", exampleEntity());
48
+ r.projection(exampleProjection());
49
+ r.projection(exampleProjection());
50
+ }),
51
+ ).toThrow(/already registered/);
52
+ });
53
+
54
+ test("rejects non-kebab-case projection names", () => {
55
+ expect(() =>
56
+ defineFeature("test", (r) => {
57
+ r.entity("unit", exampleEntity());
58
+ r.projection(exampleProjection({ name: "units_per_property" }));
59
+ }),
60
+ ).toThrow(/kebab-case/);
61
+
62
+ expect(() =>
63
+ defineFeature("test", (r) => {
64
+ r.entity("unit", exampleEntity());
65
+ r.projection(exampleProjection({ name: "UnitsPerProperty" }));
66
+ }),
67
+ ).toThrow(/kebab-case/);
68
+ });
69
+
70
+ test("accepts kebab-case projection names", () => {
71
+ expect(() =>
72
+ defineFeature("test", (r) => {
73
+ r.entity("unit", exampleEntity());
74
+ r.projection(exampleProjection({ name: "units-per-property" }));
75
+ }),
76
+ ).not.toThrow();
77
+ });
78
+ });
79
+
80
+ describe("createRegistry — projection indexing", () => {
81
+ test("indexes projections by source entity for O(1) lookup", () => {
82
+ const feature = defineFeature("test", (r) => {
83
+ r.entity("unit", exampleEntity());
84
+ r.projection(exampleProjection());
85
+ });
86
+ const registry = createRegistry([feature]);
87
+ // Filter Implicit-Projections (Sprint G — eine pro r.entity auto-
88
+ // registriert). Dieser Test prüft die explicit-Projection-Indexierung.
89
+ const byUnit = registry.getProjectionsForSource("unit").filter((p) => !p.isImplicit);
90
+ expect(byUnit).toHaveLength(1);
91
+ expect(byUnit[0]?.source).toBe("unit");
92
+ });
93
+
94
+ test("returns empty list for entities with no explicit projections", () => {
95
+ const feature = defineFeature("test", (r) => {
96
+ r.entity("unit", exampleEntity());
97
+ });
98
+ const registry = createRegistry([feature]);
99
+ // r.entity registriert eine Implicit-Projection — die filtern wir hier
100
+ // explizit raus, weil dieser Test die Abwesenheit von explicit
101
+ // Projections beweisen will.
102
+ expect(registry.getProjectionsForSource("unit").filter((p) => !p.isImplicit)).toHaveLength(0);
103
+ });
104
+
105
+ test("fans out a multi-source projection to each entity's index", () => {
106
+ const feature = defineFeature("test", (r) => {
107
+ r.entity("unit", exampleEntity("unit"));
108
+ r.entity("lease", exampleEntity("lease"));
109
+ r.projection(exampleProjection({ name: "combined", source: ["unit", "lease"] }));
110
+ });
111
+ const registry = createRegistry([feature]);
112
+ expect(registry.getProjectionsForSource("unit").filter((p) => !p.isImplicit)).toHaveLength(1);
113
+ expect(registry.getProjectionsForSource("lease").filter((p) => !p.isImplicit)).toHaveLength(1);
114
+ });
115
+
116
+ test("boot-validates source entity — typos must fail loudly", () => {
117
+ const feature = defineFeature("test", (r) => {
118
+ r.entity("unit", exampleEntity());
119
+ // Typo: "unti" instead of "unit". Without the source-validation guard
120
+ // this would silently be a no-op at runtime. Post "events-only-source"
121
+ // framework change the error message shifted: registry now accepts
122
+ // unregistered sources IF the apply-keys are domain-events — so a
123
+ // typo hits the "not registered AND no domain-event apply-keys"
124
+ // branch, which is what this test actually guards against.
125
+ r.projection(exampleProjection({ source: "unti" }));
126
+ });
127
+ expect(() => createRegistry([feature])).toThrow(/unti/);
128
+ expect(() => createRegistry([feature])).toThrow(/no domain-event apply-keys/);
129
+ });
130
+
131
+ test("boot-validates apply-keys against the source's event types", () => {
132
+ const feature = defineFeature("test", (r) => {
133
+ r.entity("unit", exampleEntity());
134
+ r.projection(
135
+ exampleProjection({
136
+ // Typo: "creatd" instead of "created". Same motivation as source-
137
+ // validation — catch silent no-ops at boot.
138
+ apply: {
139
+ "unit.creatd": async () => {},
140
+ },
141
+ }),
142
+ );
143
+ });
144
+ expect(() => createRegistry([feature])).toThrow(/apply handler for "unit\.creatd"/);
145
+ });
146
+
147
+ test("accepts all four auto-generated event-type apply-keys", () => {
148
+ const feature = defineFeature("test", (r) => {
149
+ r.entity("unit", exampleEntity());
150
+ r.projection(
151
+ exampleProjection({
152
+ apply: {
153
+ "unit.created": async () => {},
154
+ "unit.updated": async () => {},
155
+ "unit.deleted": async () => {},
156
+ "unit.restored": async () => {},
157
+ },
158
+ }),
159
+ );
160
+ });
161
+ expect(() => createRegistry([feature])).not.toThrow();
162
+ });
163
+
164
+ test("exposes all projections via getAllProjections", () => {
165
+ const feature = defineFeature("test", (r) => {
166
+ r.entity("unit", exampleEntity());
167
+ r.projection(exampleProjection());
168
+ r.projection(exampleProjection({ name: "other" }));
169
+ });
170
+ const registry = createRegistry([feature]);
171
+ // 2 explicit projections + 1 implicit (für r.entity "unit")
172
+ const explicit = [...registry.getAllProjections().values()].filter((p) => !p.isImplicit);
173
+ expect(explicit).toHaveLength(2);
174
+ });
175
+
176
+ test("rejects the same projection name registered by two features", () => {
177
+ const a = defineFeature("a", (r) => {
178
+ r.entity("unit", exampleEntity());
179
+ r.projection(exampleProjection({ name: "shared" }));
180
+ });
181
+ const b = defineFeature("b", (r) => {
182
+ r.entity("unit2", exampleEntity("unit2"));
183
+ r.projection(exampleProjection({ name: "shared", source: "unit2" }));
184
+ });
185
+ // Names get qualified with the feature-prefix so same short name from
186
+ // different features doesn't actually collide. Using the same name from
187
+ // the same qualified-name (only possible with the same feature) is the
188
+ // duplicate case — already covered by the r.projection() duplicate test.
189
+ expect(() => createRegistry([a, b])).not.toThrow();
190
+ });
191
+ });
@@ -0,0 +1,264 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ isKebabSegment,
4
+ isValidQn,
5
+ type ParsedQn,
6
+ parseQn,
7
+ QnTypes,
8
+ qn,
9
+ toKebab,
10
+ } from "../qualified-name";
11
+ import type { CamelToKebab } from "../types/handlers";
12
+
13
+ describe("qn()", () => {
14
+ test("builds scope:type:name string", () => {
15
+ expect(qn("tasks", QnTypes.write, "task:create")).toBe("tasks:write:task:create");
16
+ expect(qn("system", QnTypes.hook, "audit-trail")).toBe("system:hook:audit-trail");
17
+ expect(qn("billing", QnTypes.notify, "invoice-assigned")).toBe(
18
+ "billing:notify:invoice-assigned",
19
+ );
20
+ });
21
+
22
+ test("name can contain colons for sub-structure", () => {
23
+ expect(qn("tasks", "write", "task:create")).toBe("tasks:write:task:create");
24
+ expect(qn("hr", "write", "employee:promote")).toBe("hr:write:employee:promote");
25
+ });
26
+
27
+ test("name without colons works (standalone handlers, config, etc)", () => {
28
+ expect(qn("admin", "write", "reset")).toBe("admin:write:reset");
29
+ expect(qn("system", "hook", "audit-trail")).toBe("system:hook:audit-trail");
30
+ });
31
+
32
+ test("rejects invalid scope", () => {
33
+ expect(() => qn("Tasks", QnTypes.write, "create")).toThrow("Invalid QN scope");
34
+ expect(() => qn("", QnTypes.write, "create")).toThrow("Invalid QN scope");
35
+ expect(() => qn("my.feature", QnTypes.write, "create")).toThrow("Invalid QN scope");
36
+ });
37
+
38
+ test("rejects invalid name segment", () => {
39
+ expect(() => qn("tasks", QnTypes.write, "Create")).toThrow("Invalid QN name");
40
+ expect(() => qn("tasks", QnTypes.write, "")).toThrow("Invalid QN name");
41
+ expect(() => qn("tasks", QnTypes.write, "task_create")).toThrow("Invalid QN name");
42
+ });
43
+
44
+ test("allows numbers and dashes in segments", () => {
45
+ expect(qn("feature2", QnTypes.job, "sync-v2")).toBe("feature2:job:sync-v2");
46
+ expect(qn("my-app", QnTypes.event, "user:created")).toBe("my-app:event:user:created");
47
+ });
48
+ });
49
+
50
+ describe("parseQn()", () => {
51
+ test("parses 3-segment QN (no sub-structure in name)", () => {
52
+ const result: ParsedQn = parseQn("system:hook:audit-trail");
53
+ expect(result).toEqual({ scope: "system", type: "hook", name: "audit-trail" });
54
+ });
55
+
56
+ test("parses 4-segment QN (entity:action in name)", () => {
57
+ const result = parseQn("tasks:write:task:create");
58
+ expect(result).toEqual({ scope: "tasks", type: "write", name: "task:create" });
59
+ });
60
+
61
+ test("parses all built-in QN types", () => {
62
+ for (const type of Object.values(QnTypes)) {
63
+ const result = parseQn(`scope:${type}:name`);
64
+ expect(result.type).toBe(type);
65
+ }
66
+ });
67
+
68
+ test("accepts custom types (types are open)", () => {
69
+ const result = parseQn("billing:workflow:invoice:approval");
70
+ expect(result).toEqual({ scope: "billing", type: "workflow", name: "invoice:approval" });
71
+ });
72
+
73
+ test("rejects fewer than 3 segments", () => {
74
+ expect(() => parseQn("tasks:write")).toThrow("expected at least 3");
75
+ expect(() => parseQn("justonestring")).toThrow("expected at least 3");
76
+ });
77
+
78
+ test("rejects invalid type format", () => {
79
+ expect(() => parseQn("tasks:WRITE:name")).toThrow("Invalid QN type");
80
+ expect(() => parseQn("tasks:my_type:name")).toThrow("Invalid QN type");
81
+ });
82
+
83
+ test("rejects invalid scope or name in parsed string", () => {
84
+ expect(() => parseQn("Tasks:write:name")).toThrow("Invalid QN scope");
85
+ expect(() => parseQn("tasks:write:Name")).toThrow("Invalid QN name");
86
+ });
87
+ });
88
+
89
+ describe("isValidQn()", () => {
90
+ test("returns true for valid QNs", () => {
91
+ expect(isValidQn("tasks:write:task:create")).toBe(true);
92
+ expect(isValidQn("system:hook:audit-trail")).toBe(true);
93
+ expect(isValidQn("a:write:b")).toBe(true);
94
+ });
95
+
96
+ test("returns true for custom types", () => {
97
+ expect(isValidQn("billing:workflow:invoice:approval")).toBe(true);
98
+ expect(isValidQn("auth:rule:must-be-admin")).toBe(true);
99
+ });
100
+
101
+ test("returns false for invalid QNs", () => {
102
+ expect(isValidQn("tasks.write.create")).toBe(false);
103
+ expect(isValidQn("")).toBe(false);
104
+ expect(isValidQn("tasks:write")).toBe(false);
105
+ expect(isValidQn("Tasks:write:name")).toBe(false);
106
+ });
107
+ });
108
+
109
+ describe("toKebab()", () => {
110
+ test("converts dot-separated to kebab-case", () => {
111
+ expect(toKebab("task.create")).toBe("task-create");
112
+ expect(toKebab("billing-period.create")).toBe("billing-period-create");
113
+ });
114
+
115
+ test("converts camelCase to kebab-case", () => {
116
+ expect(toKebab("ticketAssigned")).toBe("ticket-assigned");
117
+ expect(toKebab("monthlyReport")).toBe("monthly-report");
118
+ });
119
+
120
+ test("leaves kebab-case unchanged", () => {
121
+ expect(toKebab("task-create")).toBe("task-create");
122
+ expect(toKebab("audit-trail")).toBe("audit-trail");
123
+ });
124
+
125
+ test("preserves colons", () => {
126
+ expect(toKebab("task:create")).toBe("task:create");
127
+ expect(toKebab("invoice:markPaid")).toBe("invoice:mark-paid");
128
+ });
129
+
130
+ test("handles mixed patterns", () => {
131
+ expect(toKebab("invoice.markPaid")).toBe("invoice-mark-paid");
132
+ });
133
+
134
+ test("handles uppercase sequences", () => {
135
+ expect(toKebab("parseJSON")).toBe("parse-json");
136
+ expect(toKebab("SSEBroadcast")).toBe("sse-broadcast");
137
+ expect(toKebab("getHTTPResponse")).toBe("get-http-response");
138
+ });
139
+ });
140
+
141
+ describe("CamelToKebab type === toKebab() runtime", () => {
142
+ // Each test cross-checks the compile-time type and the runtime function
143
+ // for the same input. Two layers of guarantee:
144
+ //
145
+ // 1. `expect(toKebab(X)).toBe(Y)` — runtime equality.
146
+ // 2. `const _: Equals<CamelToKebab<X>, Y> = true` — compile-time
147
+ // equality. If the type doesn't reduce to exactly `Y`, the
148
+ // assignment fails to type-check (`true` doesn't fit `false`).
149
+ //
150
+ // Both must agree — otherwise apps with such names get inconsistent
151
+ // augmentation keys. The Equals helper uses the function-bivariance
152
+ // trick to catch `never` divergence (a one-way `extends` check would
153
+ // silently pass `never extends X`).
154
+ type Equals<A, B> =
155
+ (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
156
+
157
+ test("plain camelCase", () => {
158
+ expect(toKebab("orders")).toBe("orders");
159
+ expect(toKebab("driverOrders")).toBe("driver-orders");
160
+ expect(toKebab("monthlyReport")).toBe("monthly-report");
161
+ const _t1: Equals<CamelToKebab<"orders">, "orders"> = true;
162
+ const _t2: Equals<CamelToKebab<"driverOrders">, "driver-orders"> = true;
163
+ const _t3: Equals<CamelToKebab<"monthlyReport">, "monthly-report"> = true;
164
+ void _t1;
165
+ void _t2;
166
+ void _t3;
167
+ });
168
+
169
+ test("consecutive uppercase before camel-hump (the SSEFoo case)", () => {
170
+ expect(toKebab("SSEBroadcast")).toBe("sse-broadcast");
171
+ expect(toKebab("XMLId")).toBe("xml-id");
172
+ expect(toKebab("IOPort")).toBe("io-port");
173
+ expect(toKebab("getHTTPResponse")).toBe("get-http-response");
174
+ expect(toKebab("parseJSON")).toBe("parse-json");
175
+ const _t1: Equals<CamelToKebab<"SSEBroadcast">, "sse-broadcast"> = true;
176
+ const _t2: Equals<CamelToKebab<"XMLId">, "xml-id"> = true;
177
+ const _t3: Equals<CamelToKebab<"IOPort">, "io-port"> = true;
178
+ const _t4: Equals<CamelToKebab<"getHTTPResponse">, "get-http-response"> = true;
179
+ const _t5: Equals<CamelToKebab<"parseJSON">, "parse-json"> = true;
180
+ void _t1;
181
+ void _t2;
182
+ void _t3;
183
+ void _t4;
184
+ void _t5;
185
+ });
186
+
187
+ test("trailing uppercase run (no camel-hump)", () => {
188
+ expect(toKebab("IO")).toBe("io");
189
+ expect(toKebab("URL")).toBe("url");
190
+ expect(toKebab("userID")).toBe("user-id");
191
+ const _t1: Equals<CamelToKebab<"IO">, "io"> = true;
192
+ const _t2: Equals<CamelToKebab<"URL">, "url"> = true;
193
+ const _t3: Equals<CamelToKebab<"userID">, "user-id"> = true;
194
+ void _t1;
195
+ void _t2;
196
+ void _t3;
197
+ });
198
+
199
+ test("dot-separated", () => {
200
+ expect(toKebab("billing.period")).toBe("billing-period");
201
+ expect(toKebab("billing.PeriodCreate")).toBe("billing-period-create");
202
+ const _t1: Equals<CamelToKebab<"billing.period">, "billing-period"> = true;
203
+ const _t2: Equals<CamelToKebab<"billing.PeriodCreate">, "billing-period-create"> = true;
204
+ void _t1;
205
+ void _t2;
206
+ });
207
+
208
+ test("digits in name", () => {
209
+ expect(toKebab("MD5Hash")).toBe("md5-hash");
210
+ expect(toKebab("oauth2Provider")).toBe("oauth2-provider");
211
+ const _t1: Equals<CamelToKebab<"MD5Hash">, "md5-hash"> = true;
212
+ const _t2: Equals<CamelToKebab<"oauth2Provider">, "oauth2-provider"> = true;
213
+ void _t1;
214
+ void _t2;
215
+ });
216
+
217
+ test("already kebab-case (idempotent)", () => {
218
+ expect(toKebab("task-create")).toBe("task-create");
219
+ expect(toKebab("audit-trail")).toBe("audit-trail");
220
+ const _t1: Equals<CamelToKebab<"task-create">, "task-create"> = true;
221
+ const _t2: Equals<CamelToKebab<"audit-trail">, "audit-trail"> = true;
222
+ void _t1;
223
+ void _t2;
224
+ });
225
+ });
226
+
227
+ describe("isKebabSegment()", () => {
228
+ test("accepts valid kebab segments", () => {
229
+ expect(isKebabSegment("task")).toBe(true);
230
+ expect(isKebabSegment("task-create")).toBe(true);
231
+ expect(isKebabSegment("audit-trail-v2")).toBe(true);
232
+ expect(isKebabSegment("a")).toBe(true);
233
+ expect(isKebabSegment("x1")).toBe(true);
234
+ });
235
+
236
+ test("rejects camelCase", () => {
237
+ expect(isKebabSegment("taskCreate")).toBe(false);
238
+ expect(isKebabSegment("auditTrail")).toBe(false);
239
+ });
240
+
241
+ test("rejects dots", () => {
242
+ expect(isKebabSegment("task.create")).toBe(false);
243
+ });
244
+
245
+ test("rejects underscores (toKebab leaves them through — regex catches it)", () => {
246
+ expect(isKebabSegment("task_create")).toBe(false);
247
+ expect(isKebabSegment("my_projection")).toBe(false);
248
+ });
249
+
250
+ test("rejects uppercase", () => {
251
+ expect(isKebabSegment("Task")).toBe(false);
252
+ expect(isKebabSegment("TASK")).toBe(false);
253
+ });
254
+
255
+ test("rejects non-letter starts", () => {
256
+ expect(isKebabSegment("1task")).toBe(false);
257
+ expect(isKebabSegment("-task")).toBe(false);
258
+ expect(isKebabSegment("")).toBe(false);
259
+ });
260
+
261
+ test("rejects colons (single-segment check, not full QN)", () => {
262
+ expect(isKebabSegment("task:create")).toBe(false);
263
+ });
264
+ });