@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,888 @@
1
+ // Tests for the feature-ast parser. Two layers:
2
+ //
3
+ // 1. Structural contract — defineFeature discovery, walker, dynamic
4
+ // registrar param name, source-order, SourceLocation. Every
5
+ // extractor relies on these.
6
+ // 2. Per-extractor coverage (C1.5) — one focused test per concrete
7
+ // extractor as it lands.
8
+ //
9
+ // Methods without an extractor yet still get caught by the dispatcher
10
+ // and surfaced as UnknownPattern with the correct methodName, so the
11
+ // Designer/AI know the call exists.
12
+
13
+ import { Project } from "ts-morph";
14
+ import { describe, expect, test } from "vitest";
15
+ import { parseSourceFile } from "../parse";
16
+
17
+ function createProject() {
18
+ return new Project({
19
+ skipAddingFilesFromTsConfig: true,
20
+ skipFileDependencyResolution: true,
21
+ useInMemoryFileSystem: true,
22
+ });
23
+ }
24
+
25
+ // Helper: parse an inline source snippet without writing a file.
26
+ // Centralised here per the test-setup-centralize feedback rule —
27
+ // otherwise every test would repeat the project + sourceFile boilerplate.
28
+ function parseInline(source: string) {
29
+ const project = createProject();
30
+ const sourceFile = project.createSourceFile("inline.ts", source);
31
+ return parseSourceFile(sourceFile);
32
+ }
33
+
34
+ describe("parseSourceFile", () => {
35
+ test("extracts featureName from defineFeature(name, setup)", () => {
36
+ const project = createProject();
37
+ const sourceFile = project.createSourceFile(
38
+ "inline.ts",
39
+ `
40
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
41
+ defineFeature("myFeature", (r) => {
42
+ r.entity("task", { fields: { name: { type: "text" } } });
43
+ });
44
+ `,
45
+ );
46
+
47
+ const result = parseSourceFile(sourceFile);
48
+
49
+ expect(result.featureName).toBe("myFeature");
50
+ });
51
+
52
+ test("returns one pattern per r.* call, in source order", () => {
53
+ const result = parseInline(`
54
+ defineFeature("foo", (r) => {
55
+ r.entity("task", { fields: {} });
56
+ r.requires("auth");
57
+ r.systemScope();
58
+ });
59
+ `);
60
+
61
+ expect(result.patterns).toHaveLength(3);
62
+ expect(result.patterns[0]?.kind).toBe("entity");
63
+ expect(result.patterns[1]?.kind).toBe("requires");
64
+ expect(result.patterns[2]?.kind).toBe("systemScope");
65
+ });
66
+
67
+ test("follows the setup callback's parameter name (NOT hardcoded 'r')", () => {
68
+ const result = parseInline(`
69
+ defineFeature("alt", (registrar) => {
70
+ registrar.entity("task", { fields: {} });
71
+ registrar.requires("auth");
72
+ });
73
+ `);
74
+
75
+ expect(result.patterns).toHaveLength(2);
76
+ expect(result.patterns[0]?.kind).toBe("entity");
77
+ expect(result.patterns[1]?.kind).toBe("requires");
78
+ });
79
+
80
+ test("ignores method calls on receivers that aren't the registrar", () => {
81
+ const result = parseInline(`
82
+ defineFeature("isolated", (r) => {
83
+ const helper = { entity: () => {} };
84
+ helper.entity(); // must not be reported
85
+ console.log("noise"); // must not be reported
86
+ r.entity("task", { fields: {} });
87
+ });
88
+ `);
89
+
90
+ // Only the actual r.entity call shows up — helper.entity and
91
+ // console.log are filtered out by extractRegistrarMethodName.
92
+ expect(result.patterns).toHaveLength(1);
93
+ expect(result.patterns[0]).toMatchObject({ kind: "entity", entityName: "task" });
94
+ });
95
+
96
+ test("returns empty result when no defineFeature is present", () => {
97
+ const result = parseInline("export const x = 1;");
98
+
99
+ expect(result.featureName).toBeUndefined();
100
+ expect(result.patterns).toEqual([]);
101
+ expect(result.errors).toEqual([]);
102
+ });
103
+
104
+ test("attaches a 1-based SourceLocation pointing at the call", () => {
105
+ const result = parseInline(`defineFeature("loc", (r) => {
106
+ r.entity("task", { fields: {} });
107
+ });
108
+ `);
109
+
110
+ expect(result.patterns).toHaveLength(1);
111
+ const source = result.patterns[0]?.source;
112
+ expect(source).toBeDefined();
113
+ // The r.entity call sits on line 2 of the snippet (1-based).
114
+ expect(source?.start.line).toBe(2);
115
+ // Raw text round-trips the original call.
116
+ expect(source?.raw).toContain("r.entity");
117
+ });
118
+
119
+ test("falls back to UnknownPattern when defineFeature is missing the setup callback", () => {
120
+ const result = parseInline(`defineFeature("nameOnly");`);
121
+
122
+ expect(result.featureName).toBe("nameOnly");
123
+ expect(result.patterns).toEqual([]);
124
+ });
125
+ });
126
+
127
+ // =============================================================================
128
+ // Round 1 extractors — concrete patterns for the simplest static APIs.
129
+ // =============================================================================
130
+
131
+ describe("extractRequires", () => {
132
+ test("captures every string-literal argument as featureNames", () => {
133
+ const result = parseInline(`
134
+ defineFeature("f", (r) => {
135
+ r.requires("auth", "tenant");
136
+ });
137
+ `);
138
+
139
+ expect(result.patterns[0]).toMatchObject({
140
+ kind: "requires",
141
+ featureNames: ["auth", "tenant"],
142
+ });
143
+ expect(result.errors).toEqual([]);
144
+ });
145
+
146
+ test("emits a ParseError when an argument is not a string literal", () => {
147
+ const result = parseInline(`
148
+ const dep = "auth";
149
+ defineFeature("f", (r) => {
150
+ r.requires(dep);
151
+ });
152
+ `);
153
+
154
+ expect(result.patterns).toEqual([]);
155
+ expect(result.errors).toHaveLength(1);
156
+ expect(result.errors[0]?.methodName).toBe("requires");
157
+ });
158
+ });
159
+
160
+ describe("extractOptionalRequires", () => {
161
+ test("captures featureNames analogous to requires", () => {
162
+ const result = parseInline(`
163
+ defineFeature("f", (r) => {
164
+ r.optionalRequires("billing");
165
+ });
166
+ `);
167
+
168
+ expect(result.patterns[0]).toMatchObject({
169
+ kind: "optionalRequires",
170
+ featureNames: ["billing"],
171
+ });
172
+ });
173
+ });
174
+
175
+ describe("extractReadsConfig", () => {
176
+ test("captures qualifiedKeys", () => {
177
+ const result = parseInline(`
178
+ defineFeature("f", (r) => {
179
+ r.readsConfig("auth:config:jwt-ttl", "tenant:config:locale");
180
+ });
181
+ `);
182
+
183
+ expect(result.patterns[0]).toMatchObject({
184
+ kind: "readsConfig",
185
+ qualifiedKeys: ["auth:config:jwt-ttl", "tenant:config:locale"],
186
+ });
187
+ });
188
+ });
189
+
190
+ describe("extractSystemScope", () => {
191
+ test("produces a SystemScopePattern with no payload", () => {
192
+ const result = parseInline(`
193
+ defineFeature("f", (r) => {
194
+ r.systemScope();
195
+ });
196
+ `);
197
+
198
+ expect(result.patterns[0]).toMatchObject({ kind: "systemScope" });
199
+ });
200
+ });
201
+
202
+ describe("extractToggleable", () => {
203
+ test("reads the default flag from a literal object", () => {
204
+ const result = parseInline(`
205
+ defineFeature("f", (r) => {
206
+ r.toggleable({ default: true });
207
+ });
208
+ `);
209
+
210
+ expect(result.patterns[0]).toMatchObject({
211
+ kind: "toggleable",
212
+ default: true,
213
+ });
214
+ });
215
+
216
+ test("emits a ParseError when the argument is missing", () => {
217
+ const result = parseInline(`
218
+ defineFeature("f", (r) => {
219
+ r.toggleable();
220
+ });
221
+ `);
222
+
223
+ expect(result.patterns).toEqual([]);
224
+ expect(result.errors).toHaveLength(1);
225
+ expect(result.errors[0]?.methodName).toBe("toggleable");
226
+ });
227
+
228
+ test("emits a ParseError when default is not a literal boolean", () => {
229
+ const result = parseInline(`
230
+ const flag = true;
231
+ defineFeature("f", (r) => {
232
+ r.toggleable({ default: flag });
233
+ });
234
+ `);
235
+
236
+ expect(result.patterns).toEqual([]);
237
+ expect(result.errors).toHaveLength(1);
238
+ expect(result.errors[0]?.methodName).toBe("toggleable");
239
+ });
240
+ });
241
+
242
+ // =============================================================================
243
+ // Round 2 extractors — object-literal-based statics
244
+ // =============================================================================
245
+
246
+ describe("extractEntity", () => {
247
+ test("captures entityName + plain-data definition", () => {
248
+ const result = parseInline(`
249
+ defineFeature("f", (r) => {
250
+ r.entity("task", {
251
+ fields: {
252
+ title: { type: "text", required: true },
253
+ done: { type: "boolean", default: false },
254
+ },
255
+ });
256
+ });
257
+ `);
258
+
259
+ expect(result.patterns[0]).toMatchObject({
260
+ kind: "entity",
261
+ entityName: "task",
262
+ definition: {
263
+ fields: {
264
+ title: { type: "text", required: true },
265
+ done: { type: "boolean", default: false },
266
+ },
267
+ },
268
+ });
269
+ expect(result.errors).toEqual([]);
270
+ });
271
+
272
+ test("walks through `as const` and `satisfies` wrappers", () => {
273
+ const result = parseInline(`
274
+ defineFeature("f", (r) => {
275
+ r.entity("task", { fields: { name: { type: "text" as const } } });
276
+ });
277
+ `);
278
+
279
+ expect(result.patterns[0]).toMatchObject({
280
+ kind: "entity",
281
+ definition: { fields: { name: { type: "text" } } },
282
+ });
283
+ });
284
+
285
+ test("emits a ParseError when the definition contains a function value", () => {
286
+ const result = parseInline(`
287
+ defineFeature("f", (r) => {
288
+ r.entity("task", {
289
+ fields: {
290
+ title: { type: "text", default: () => "untitled" },
291
+ },
292
+ });
293
+ });
294
+ `);
295
+
296
+ expect(result.patterns).toEqual([]);
297
+ expect(result.errors[0]?.methodName).toBe("entity");
298
+ });
299
+
300
+ test("emits a ParseError when the name is not a string literal", () => {
301
+ const result = parseInline(`
302
+ const ENTITY = "task";
303
+ defineFeature("f", (r) => {
304
+ r.entity(ENTITY, { fields: {} });
305
+ });
306
+ `);
307
+
308
+ expect(result.errors[0]?.methodName).toBe("entity");
309
+ });
310
+ });
311
+
312
+ describe("extractRelation", () => {
313
+ test("reads entity ref + relation name + plain-data definition", () => {
314
+ const result = parseInline(`
315
+ defineFeature("f", (r) => {
316
+ r.relation("task", "owner", { kind: "belongsTo", to: "user" });
317
+ });
318
+ `);
319
+
320
+ expect(result.patterns[0]).toMatchObject({
321
+ kind: "relation",
322
+ entityName: "task",
323
+ relationName: "owner",
324
+ definition: { kind: "belongsTo", to: "user" },
325
+ });
326
+ });
327
+
328
+ test("accepts inline { name: '...' } literal as entity ref", () => {
329
+ const result = parseInline(`
330
+ defineFeature("f", (r) => {
331
+ r.relation({ name: "task" }, "owner", { kind: "belongsTo", to: "user" });
332
+ });
333
+ `);
334
+
335
+ expect(result.patterns[0]).toMatchObject({
336
+ kind: "relation",
337
+ entityName: "task",
338
+ relationName: "owner",
339
+ });
340
+ });
341
+
342
+ test("emits a ParseError when entity ref is an unresolvable identifier", () => {
343
+ const result = parseInline(`
344
+ const taskRef = { name: "task" };
345
+ defineFeature("f", (r) => {
346
+ r.relation(taskRef, "owner", { kind: "belongsTo", to: "user" });
347
+ });
348
+ `);
349
+
350
+ expect(result.errors[0]?.methodName).toBe("relation");
351
+ });
352
+ });
353
+
354
+ describe("extractNav", () => {
355
+ test("captures the NavDefinition", () => {
356
+ const result = parseInline(`
357
+ defineFeature("f", (r) => {
358
+ r.nav({ id: "tasks", label: "Tasks", screen: "tasks:screen:list" });
359
+ });
360
+ `);
361
+
362
+ expect(result.patterns[0]).toMatchObject({
363
+ kind: "nav",
364
+ definition: { id: "tasks", label: "Tasks", screen: "tasks:screen:list" },
365
+ });
366
+ });
367
+ });
368
+
369
+ describe("extractWorkspace", () => {
370
+ test("captures the WorkspaceDefinition", () => {
371
+ const result = parseInline(`
372
+ defineFeature("f", (r) => {
373
+ r.workspace({ id: "admin", label: "Admin" });
374
+ });
375
+ `);
376
+
377
+ expect(result.patterns[0]).toMatchObject({
378
+ kind: "workspace",
379
+ definition: { id: "admin", label: "Admin" },
380
+ });
381
+ });
382
+ });
383
+
384
+ // =============================================================================
385
+ // Round 3 extractors — complex static patterns
386
+ // =============================================================================
387
+
388
+ describe("extractConfig", () => {
389
+ test("captures keys map", () => {
390
+ const result = parseInline(`
391
+ defineFeature("f", (r) => {
392
+ r.config({
393
+ keys: {
394
+ jwtTtl: { type: "number", default: 3600 },
395
+ locale: { type: "string", default: "en" },
396
+ },
397
+ });
398
+ });
399
+ `);
400
+
401
+ expect(result.patterns[0]).toMatchObject({
402
+ kind: "config",
403
+ keys: {
404
+ jwtTtl: { type: "number", default: 3600 },
405
+ locale: { type: "string", default: "en" },
406
+ },
407
+ });
408
+ });
409
+
410
+ test("emits ParseError when keys property is missing", () => {
411
+ const result = parseInline(`
412
+ defineFeature("f", (r) => {
413
+ r.config({});
414
+ });
415
+ `);
416
+
417
+ expect(result.errors[0]?.methodName).toBe("config");
418
+ });
419
+ });
420
+
421
+ describe("extractTranslations", () => {
422
+ test("captures the locale tree", () => {
423
+ const result = parseInline(`
424
+ defineFeature("f", (r) => {
425
+ r.translations({
426
+ keys: {
427
+ en: { greeting: "hello" },
428
+ de: { greeting: "hallo" },
429
+ },
430
+ });
431
+ });
432
+ `);
433
+
434
+ expect(result.patterns[0]).toMatchObject({
435
+ kind: "translations",
436
+ keys: {
437
+ en: { greeting: "hello" },
438
+ de: { greeting: "hallo" },
439
+ },
440
+ });
441
+ });
442
+ });
443
+
444
+ describe("extractMetric", () => {
445
+ test("captures shortName + options", () => {
446
+ const result = parseInline(`
447
+ defineFeature("f", (r) => {
448
+ r.metric("requests", { type: "counter", description: "API requests" });
449
+ });
450
+ `);
451
+
452
+ expect(result.patterns[0]).toMatchObject({
453
+ kind: "metric",
454
+ shortName: "requests",
455
+ options: { type: "counter", description: "API requests" },
456
+ });
457
+ });
458
+ });
459
+
460
+ describe("extractSecret", () => {
461
+ test("captures shortName + options", () => {
462
+ const result = parseInline(`
463
+ defineFeature("f", (r) => {
464
+ r.secret("apiKey", { description: "Stripe API key" });
465
+ });
466
+ `);
467
+
468
+ expect(result.patterns[0]).toMatchObject({
469
+ kind: "secret",
470
+ shortName: "apiKey",
471
+ options: { description: "Stripe API key" },
472
+ });
473
+ });
474
+ });
475
+
476
+ describe("extractClaimKey", () => {
477
+ test("captures shortName + claim type", () => {
478
+ const result = parseInline(`
479
+ defineFeature("f", (r) => {
480
+ r.claimKey("teamId", { type: "string" });
481
+ });
482
+ `);
483
+
484
+ expect(result.patterns[0]).toMatchObject({
485
+ kind: "claimKey",
486
+ shortName: "teamId",
487
+ claimType: "string",
488
+ });
489
+ });
490
+
491
+ test("emits ParseError on invalid claim type", () => {
492
+ const result = parseInline(`
493
+ defineFeature("f", (r) => {
494
+ r.claimKey("teamId", { type: "bigint" });
495
+ });
496
+ `);
497
+
498
+ expect(result.errors[0]?.methodName).toBe("claimKey");
499
+ });
500
+ });
501
+
502
+ describe("extractReferenceData", () => {
503
+ test("captures entity name + data array", () => {
504
+ const result = parseInline(`
505
+ defineFeature("f", (r) => {
506
+ r.referenceData("status", [
507
+ { id: "open", label: "Open" },
508
+ { id: "closed", label: "Closed" },
509
+ ]);
510
+ });
511
+ `);
512
+
513
+ expect(result.patterns[0]).toMatchObject({
514
+ kind: "referenceData",
515
+ entityName: "status",
516
+ data: [
517
+ { id: "open", label: "Open" },
518
+ { id: "closed", label: "Closed" },
519
+ ],
520
+ });
521
+ });
522
+
523
+ test("captures the optional upsertKey", () => {
524
+ const result = parseInline(`
525
+ defineFeature("f", (r) => {
526
+ r.referenceData("status", [{ id: "open" }], { upsertKey: "id" });
527
+ });
528
+ `);
529
+
530
+ expect(result.patterns[0]).toMatchObject({
531
+ kind: "referenceData",
532
+ entityName: "status",
533
+ upsertKey: "id",
534
+ });
535
+ });
536
+ });
537
+
538
+ describe("extractUseExtension", () => {
539
+ test("captures extension name + entity ref", () => {
540
+ const result = parseInline(`
541
+ defineFeature("f", (r) => {
542
+ r.useExtension("audit", "task");
543
+ });
544
+ `);
545
+
546
+ expect(result.patterns[0]).toMatchObject({
547
+ kind: "useExtension",
548
+ extensionName: "audit",
549
+ entityName: "task",
550
+ });
551
+ });
552
+
553
+ test("captures optional options", () => {
554
+ const result = parseInline(`
555
+ defineFeature("f", (r) => {
556
+ r.useExtension("audit", "task", { mode: "verbose" });
557
+ });
558
+ `);
559
+
560
+ expect(result.patterns[0]).toMatchObject({
561
+ kind: "useExtension",
562
+ extensionName: "audit",
563
+ entityName: "task",
564
+ options: { mode: "verbose" },
565
+ });
566
+ });
567
+ });
568
+
569
+ // =============================================================================
570
+ // Round 4 extractors — mixed patterns (header data + opaque body source)
571
+ // =============================================================================
572
+
573
+ describe("extractHook", () => {
574
+ test("captures hookType, target and the function body", () => {
575
+ const result = parseInline(`
576
+ defineFeature("f", (r) => {
577
+ r.hook("postSave", "task", (event, ctx) => { console.log(event); });
578
+ });
579
+ `);
580
+
581
+ expect(result.patterns[0]).toMatchObject({
582
+ kind: "hook",
583
+ hookType: "postSave",
584
+ target: "task",
585
+ });
586
+ const fnBody = (result.patterns[0] as { fnBody?: { raw: string } } | undefined)?.fnBody;
587
+ expect(fnBody?.raw).toContain("(event, ctx)");
588
+ });
589
+
590
+ test("captures the optional phase from the options object", () => {
591
+ const result = parseInline(`
592
+ defineFeature("f", (r) => {
593
+ r.hook("postSave", "task", (event, ctx) => {}, { phase: "afterCommit" });
594
+ });
595
+ `);
596
+
597
+ expect(result.patterns[0]).toMatchObject({ kind: "hook", phase: "afterCommit" });
598
+ });
599
+
600
+ test("rejects an unknown hook type", () => {
601
+ const result = parseInline(`
602
+ defineFeature("f", (r) => {
603
+ r.hook("postCommit", "task", () => {});
604
+ });
605
+ `);
606
+
607
+ expect(result.errors[0]?.methodName).toBe("hook");
608
+ });
609
+ });
610
+
611
+ describe("extractEntityHook", () => {
612
+ test("captures hookType, entity and the function body", () => {
613
+ const result = parseInline(`
614
+ defineFeature("f", (r) => {
615
+ r.entityHook("postSave", "task", (event, ctx) => {});
616
+ });
617
+ `);
618
+
619
+ expect(result.patterns[0]).toMatchObject({
620
+ kind: "entityHook",
621
+ hookType: "postSave",
622
+ entityName: "task",
623
+ });
624
+ });
625
+
626
+ test("rejects validation as entity-hook type (only postSave/preDelete/postDelete allowed)", () => {
627
+ const result = parseInline(`
628
+ defineFeature("f", (r) => {
629
+ r.entityHook("validation", "task", () => {});
630
+ });
631
+ `);
632
+
633
+ expect(result.errors[0]?.methodName).toBe("entityHook");
634
+ });
635
+ });
636
+
637
+ describe("extractAuthClaims", () => {
638
+ test("captures the function body as a SourceLocation", () => {
639
+ const result = parseInline(`
640
+ defineFeature("f", (r) => {
641
+ r.authClaims(async (user, ctx) => ({ teamId: "t1" }));
642
+ });
643
+ `);
644
+
645
+ expect(result.patterns[0]).toMatchObject({ kind: "authClaims" });
646
+ });
647
+ });
648
+
649
+ describe("extractWriteHandler", () => {
650
+ test("inline form: name, schema, handler, options", () => {
651
+ const result = parseInline(`
652
+ defineFeature("f", (r) => {
653
+ r.writeHandler("task:create", z.object({ title: z.string() }), async (event, ctx) => {
654
+ return { isSuccess: true, data: {} };
655
+ }, { access: { roles: ["admin"] } });
656
+ });
657
+ `);
658
+
659
+ expect(result.patterns[0]).toMatchObject({
660
+ kind: "writeHandler",
661
+ handlerName: "task:create",
662
+ access: { roles: ["admin"] },
663
+ });
664
+ });
665
+
666
+ test("object form: defineWriteHandler shape", () => {
667
+ const result = parseInline(`
668
+ defineFeature("f", (r) => {
669
+ r.writeHandler({
670
+ name: "task:approve",
671
+ schema: z.object({ id: z.string() }),
672
+ handler: async (event, ctx) => ({ isSuccess: true, data: {} }),
673
+ access: { openToAll: true },
674
+ });
675
+ });
676
+ `);
677
+
678
+ expect(result.patterns[0]).toMatchObject({
679
+ kind: "writeHandler",
680
+ handlerName: "task:approve",
681
+ access: { openToAll: true },
682
+ });
683
+ });
684
+ });
685
+
686
+ describe("extractQueryHandler", () => {
687
+ test("inline form returns kind=queryHandler", () => {
688
+ const result = parseInline(`
689
+ defineFeature("f", (r) => {
690
+ r.queryHandler("task:list", z.object({}), async (q, ctx) => []);
691
+ });
692
+ `);
693
+
694
+ expect(result.patterns[0]).toMatchObject({
695
+ kind: "queryHandler",
696
+ handlerName: "task:list",
697
+ });
698
+ });
699
+ });
700
+
701
+ describe("extractJob", () => {
702
+ test("captures jobName, options and handler body", () => {
703
+ const result = parseInline(`
704
+ defineFeature("f", (r) => {
705
+ r.job("daily-cleanup", { trigger: { cron: "0 3 * * *" } }, async (ctx) => {});
706
+ });
707
+ `);
708
+
709
+ expect(result.patterns[0]).toMatchObject({
710
+ kind: "job",
711
+ jobName: "daily-cleanup",
712
+ });
713
+ });
714
+ });
715
+
716
+ describe("extractHttpRoute", () => {
717
+ test("captures method, path, anonymous, handler", () => {
718
+ const result = parseInline(`
719
+ defineFeature("f", (r) => {
720
+ r.httpRoute({
721
+ method: "GET",
722
+ path: "/feed.xml",
723
+ anonymous: true,
724
+ handler: async (c) => new Response("ok"),
725
+ });
726
+ });
727
+ `);
728
+
729
+ expect(result.patterns[0]).toMatchObject({
730
+ kind: "httpRoute",
731
+ method: "GET",
732
+ path: "/feed.xml",
733
+ anonymous: true,
734
+ });
735
+ });
736
+ });
737
+
738
+ describe("extractDefineEvent", () => {
739
+ test("captures eventName + version", () => {
740
+ const result = parseInline(`
741
+ defineFeature("f", (r) => {
742
+ r.defineEvent("incidentOpened", z.object({ id: z.string() }), { version: 2 });
743
+ });
744
+ `);
745
+
746
+ expect(result.patterns[0]).toMatchObject({
747
+ kind: "defineEvent",
748
+ eventName: "incidentOpened",
749
+ version: 2,
750
+ });
751
+ });
752
+ });
753
+
754
+ describe("extractEventMigration", () => {
755
+ test("captures fromVersion / toVersion / transform body", () => {
756
+ const result = parseInline(`
757
+ defineFeature("f", (r) => {
758
+ r.eventMigration("incidentOpened", 1, 2, (payload) => ({ ...payload, severity: "low" }));
759
+ });
760
+ `);
761
+
762
+ expect(result.patterns[0]).toMatchObject({
763
+ kind: "eventMigration",
764
+ eventName: "incidentOpened",
765
+ fromVersion: 1,
766
+ toVersion: 2,
767
+ });
768
+ });
769
+ });
770
+
771
+ describe("extractNotification", () => {
772
+ test("captures trigger + recipient + data bodies", () => {
773
+ const result = parseInline(`
774
+ defineFeature("f", (r) => {
775
+ r.notification("incidentOpened", {
776
+ trigger: { on: "incident:create" },
777
+ recipient: (event) => ({ tenant: event.tenantId }),
778
+ data: (event) => ({ id: event.id }),
779
+ });
780
+ });
781
+ `);
782
+
783
+ expect(result.patterns[0]).toMatchObject({
784
+ kind: "notification",
785
+ notificationName: "incidentOpened",
786
+ trigger: { on: "incident:create" },
787
+ });
788
+ });
789
+ });
790
+
791
+ describe("extractProjection", () => {
792
+ test("captures name, sourceEntity, applyBodies map", () => {
793
+ const result = parseInline(`
794
+ defineFeature("f", (r) => {
795
+ r.projection({
796
+ name: "task-counter",
797
+ source: "task",
798
+ table: taskCounter,
799
+ apply: {
800
+ "task.created": async (event, tx) => {},
801
+ "task.updated": async (event, tx) => {},
802
+ },
803
+ });
804
+ });
805
+ `);
806
+
807
+ expect(result.patterns[0]).toMatchObject({
808
+ kind: "projection",
809
+ name: "task-counter",
810
+ sourceEntity: "task",
811
+ });
812
+ const applyBodies = (
813
+ result.patterns[0] as { applyBodies?: Record<string, unknown> } | undefined
814
+ )?.applyBodies;
815
+ expect(Object.keys(applyBodies ?? {})).toEqual(["task.created", "task.updated"]);
816
+ });
817
+ });
818
+
819
+ describe("extractMultiStreamProjection", () => {
820
+ test("captures name + applyBodies + delivery", () => {
821
+ const result = parseInline(`
822
+ defineFeature("f", (r) => {
823
+ r.multiStreamProjection({
824
+ name: "audit-log",
825
+ apply: {
826
+ "task.created": async (event, tx, ctx) => {},
827
+ },
828
+ delivery: "shared",
829
+ });
830
+ });
831
+ `);
832
+
833
+ expect(result.patterns[0]).toMatchObject({
834
+ kind: "multiStreamProjection",
835
+ name: "audit-log",
836
+ delivery: "shared",
837
+ });
838
+ });
839
+ });
840
+
841
+ describe("extractScreen", () => {
842
+ test("captures static layout and reports closure props as opaque", () => {
843
+ const result = parseInline(`
844
+ defineFeature("f", (r) => {
845
+ r.screen({
846
+ id: "task-list",
847
+ type: "entityList",
848
+ entity: "task",
849
+ columns: ["title", "status"],
850
+ rowActions: [
851
+ {
852
+ id: "edit",
853
+ label: "Edit",
854
+ handler: "task:update",
855
+ visible: (row) => row.status !== "done",
856
+ },
857
+ ],
858
+ });
859
+ });
860
+ `);
861
+
862
+ expect(result.patterns[0]).toMatchObject({
863
+ kind: "screen",
864
+ });
865
+ const opaque = (result.patterns[0] as { opaqueProps?: Record<string, unknown> } | undefined)
866
+ ?.opaqueProps;
867
+ expect(Object.keys(opaque ?? {})).toContain("rowActions.0.visible");
868
+ });
869
+ });
870
+
871
+ // =============================================================================
872
+ // Round 5 extractors — opaque patterns
873
+ // =============================================================================
874
+
875
+ describe("extractExtendsRegistrar", () => {
876
+ test("captures extension name + opaque def body", () => {
877
+ const result = parseInline(`
878
+ defineFeature("f", (r) => {
879
+ r.extendsRegistrar("audit", { hooks: { postSave: () => {} } });
880
+ });
881
+ `);
882
+
883
+ expect(result.patterns[0]).toMatchObject({
884
+ kind: "extendsRegistrar",
885
+ extensionName: "audit",
886
+ });
887
+ });
888
+ });