@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,602 @@
1
+ // Pattern → TS-Source renderer. Produces canonical Object-Form for every
2
+ // FeaturePattern kind: a single object-literal argument per r.* call.
3
+ // Output is biome-format-stable so consumers can write the file directly
4
+ // without an extra format pass.
5
+ //
6
+ // **Source-of-Truth Contract:**
7
+ // - Static patterns (entity, nav, config, etc.) round-trip cleanly:
8
+ // parse → render → parse yields the same patterns.
9
+ // - Mixed patterns (writeHandler, hook, screen) embed the original
10
+ // source-text of opaque bodies (handler/fn/closure) verbatim via
11
+ // SourceLocation.raw — the renderer doesn't re-print closure code.
12
+ // - Comments inside an existing pattern are NOT preserved (Designer
13
+ // edits via forms; for AI generation the output is fresh anyway).
14
+ //
15
+ // **Schema-Version-Header:** every renderFeatureFile output starts with
16
+ // `// kumiko-feature-version: 1`. Future format bumps run a dedicated
17
+ // migrator over the version comment.
18
+
19
+ import type {
20
+ AuthClaimsPattern,
21
+ ClaimKeyPattern,
22
+ ConfigPattern,
23
+ DefineEventPattern,
24
+ EntityHookPattern,
25
+ EntityPattern,
26
+ EventMigrationPattern,
27
+ ExtendsRegistrarPattern,
28
+ FeaturePattern,
29
+ HookPattern,
30
+ HttpRoutePattern,
31
+ JobPattern,
32
+ MetricPattern,
33
+ MultiStreamProjectionPattern,
34
+ NavPattern,
35
+ NotificationPattern,
36
+ OptionalRequiresPattern,
37
+ ProjectionPattern,
38
+ QueryHandlerPattern,
39
+ ReadsConfigPattern,
40
+ ReferenceDataPattern,
41
+ RelationPattern,
42
+ RequiresPattern,
43
+ ScreenPattern,
44
+ SecretPattern,
45
+ SystemScopePattern,
46
+ ToggleablePattern,
47
+ TranslationsPattern,
48
+ UnknownPattern,
49
+ UseExtensionPattern,
50
+ WorkspacePattern,
51
+ WriteHandlerPattern,
52
+ } from "./patterns";
53
+ import { SCREEN_OPAQUE_MARKER } from "./patterns";
54
+
55
+ export const FEATURE_FILE_VERSION = 1 as const;
56
+ export const VERSION_HEADER = `// kumiko-feature-version: ${FEATURE_FILE_VERSION}`;
57
+
58
+ /**
59
+ * Render a single FeaturePattern back to TypeScript source — a `r.<kind>(...)`
60
+ * call in canonical Object-Form. The result is a single statement WITHOUT a
61
+ * trailing newline; callers compose statements with their own joiner.
62
+ */
63
+ export function renderPattern(pattern: FeaturePattern): string {
64
+ switch (pattern.kind) {
65
+ case "requires":
66
+ return renderRequires(pattern);
67
+ case "optionalRequires":
68
+ return renderOptionalRequires(pattern);
69
+ case "readsConfig":
70
+ return renderReadsConfig(pattern);
71
+ case "systemScope":
72
+ return renderSystemScope(pattern);
73
+ case "toggleable":
74
+ return renderToggleable(pattern);
75
+ case "entity":
76
+ return renderEntity(pattern);
77
+ case "relation":
78
+ return renderRelation(pattern);
79
+ case "nav":
80
+ return renderNav(pattern);
81
+ case "workspace":
82
+ return renderWorkspace(pattern);
83
+ case "config":
84
+ return renderConfig(pattern);
85
+ case "translations":
86
+ return renderTranslations(pattern);
87
+ case "metric":
88
+ return renderMetric(pattern);
89
+ case "secret":
90
+ return renderSecret(pattern);
91
+ case "claimKey":
92
+ return renderClaimKey(pattern);
93
+ case "referenceData":
94
+ return renderReferenceData(pattern);
95
+ case "useExtension":
96
+ return renderUseExtension(pattern);
97
+ case "screen":
98
+ return renderScreen(pattern);
99
+ case "writeHandler":
100
+ return renderWriteHandler(pattern);
101
+ case "queryHandler":
102
+ return renderQueryHandler(pattern);
103
+ case "hook":
104
+ return renderHook(pattern);
105
+ case "entityHook":
106
+ return renderEntityHook(pattern);
107
+ case "job":
108
+ return renderJob(pattern);
109
+ case "notification":
110
+ return renderNotification(pattern);
111
+ case "authClaims":
112
+ return renderAuthClaims(pattern);
113
+ case "httpRoute":
114
+ return renderHttpRoute(pattern);
115
+ case "projection":
116
+ return renderProjection(pattern);
117
+ case "multiStreamProjection":
118
+ return renderMultiStreamProjection(pattern);
119
+ case "defineEvent":
120
+ return renderDefineEvent(pattern);
121
+ case "eventMigration":
122
+ return renderEventMigration(pattern);
123
+ case "extendsRegistrar":
124
+ return renderExtendsRegistrar(pattern);
125
+ case "unknown":
126
+ return renderUnknown(pattern);
127
+ default: {
128
+ const _exhaustive: never = pattern;
129
+ return _exhaustive;
130
+ }
131
+ }
132
+ }
133
+
134
+ // =============================================================================
135
+ // JSON-Like Value Renderer — emits TypeScript-source-compatible literals.
136
+ // Used for declarative pattern bodies (entity definitions, config keys, etc.).
137
+ // =============================================================================
138
+
139
+ /**
140
+ * Threshold above which a single-line array/object renders multi-line.
141
+ * Biome's default print-width is 100 columns; we leave a margin so a
142
+ * pattern at indent=4 still fits before wrapping. Short arrays/objects
143
+ * stay on one line, long ones go multi-line — biome-stable in both.
144
+ */
145
+ const SINGLE_LINE_WIDTH = 80;
146
+
147
+ /**
148
+ * Render a JSON-compatible value as TypeScript source. Matches what
149
+ * `readDataLiteralNode` accepts on the parser side: strings, numbers,
150
+ * booleans, null, arrays, plain objects. Unsupported values (functions,
151
+ * undefined) throw — they should never reach here for a static pattern.
152
+ *
153
+ * Keys are quoted only when they are not valid JS identifiers. The
154
+ * renderer prefers single-line output for short arrays/objects (≤80
155
+ * chars including indent and no nested newlines) and falls back to
156
+ * multi-line otherwise — biome-stable in both branches.
157
+ */
158
+ export function renderValue(value: unknown, indent = 0): string {
159
+ if (value === null) return "null";
160
+ if (typeof value === "string") return JSON.stringify(value);
161
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
162
+ if (Array.isArray(value)) {
163
+ if (value.length === 0) return "[]";
164
+ const items = value.map((v) => renderValue(v, indent + 2));
165
+ const singleLine = `[${items.join(", ")}]`;
166
+ if (singleLine.length + indent <= SINGLE_LINE_WIDTH && !singleLine.includes("\n")) {
167
+ return singleLine;
168
+ }
169
+ const inner = items.map((item) => `${spaces(indent + 2)}${item}`).join(",\n");
170
+ return `[\n${inner},\n${spaces(indent)}]`;
171
+ }
172
+ if (typeof value === "object") {
173
+ const entries = Object.entries(value as Record<string, unknown>);
174
+ if (entries.length === 0) return "{}";
175
+ const items = entries.map(([k, v]) => `${renderKey(k)}: ${renderValue(v, indent + 2)}`);
176
+ const singleLine = `{ ${items.join(", ")} }`;
177
+ if (singleLine.length + indent <= SINGLE_LINE_WIDTH && !singleLine.includes("\n")) {
178
+ return singleLine;
179
+ }
180
+ const inner = items.map((item) => `${spaces(indent + 2)}${item}`).join(",\n");
181
+ return `{\n${inner},\n${spaces(indent)}}`;
182
+ }
183
+ throw new Error(`renderValue: unsupported type for value ${String(value)}`);
184
+ }
185
+
186
+ const VALID_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
187
+
188
+ function renderKey(key: string): string {
189
+ return VALID_IDENT.test(key) ? key : JSON.stringify(key);
190
+ }
191
+
192
+ function spaces(n: number): string {
193
+ return " ".repeat(n);
194
+ }
195
+
196
+ // =============================================================================
197
+ // Static patterns
198
+ // =============================================================================
199
+
200
+ function renderRequires(p: RequiresPattern): string {
201
+ return `r.requires({ features: ${renderValue([...p.featureNames])} });`;
202
+ }
203
+
204
+ function renderOptionalRequires(p: OptionalRequiresPattern): string {
205
+ return `r.optionalRequires({ features: ${renderValue([...p.featureNames])} });`;
206
+ }
207
+
208
+ function renderReadsConfig(p: ReadsConfigPattern): string {
209
+ return `r.readsConfig({ keys: ${renderValue([...p.qualifiedKeys])} });`;
210
+ }
211
+
212
+ function renderSystemScope(_p: SystemScopePattern): string {
213
+ return "r.systemScope();";
214
+ }
215
+
216
+ function renderToggleable(p: ToggleablePattern): string {
217
+ return `r.toggleable({ default: ${p.default} });`;
218
+ }
219
+
220
+ function renderEntity(p: EntityPattern): string {
221
+ // Inline `name` into the definition object — canonical Object-Form
222
+ // is a single arg with name-as-property.
223
+ const merged = { name: p.entityName, ...p.definition };
224
+ return `r.entity(${renderValue(merged)});`;
225
+ }
226
+
227
+ function renderRelation(p: RelationPattern): string {
228
+ const merged = { entity: p.entityName, name: p.relationName, ...p.definition };
229
+ return `r.relation(${renderValue(merged)});`;
230
+ }
231
+
232
+ function renderNav(p: NavPattern): string {
233
+ return `r.nav(${renderValue(p.definition)});`;
234
+ }
235
+
236
+ function renderWorkspace(p: WorkspacePattern): string {
237
+ return `r.workspace(${renderValue(p.definition)});`;
238
+ }
239
+
240
+ function renderConfig(p: ConfigPattern): string {
241
+ return `r.config(${renderValue({ keys: p.keys })});`;
242
+ }
243
+
244
+ function renderTranslations(p: TranslationsPattern): string {
245
+ return `r.translations(${renderValue({ keys: p.keys })});`;
246
+ }
247
+
248
+ function renderMetric(p: MetricPattern): string {
249
+ const merged = { name: p.shortName, ...p.options };
250
+ return `r.metric(${renderValue(merged)});`;
251
+ }
252
+
253
+ function renderSecret(p: SecretPattern): string {
254
+ const merged = { name: p.shortName, ...p.options };
255
+ return `r.secret(${renderValue(merged)});`;
256
+ }
257
+
258
+ function renderClaimKey(p: ClaimKeyPattern): string {
259
+ return `r.claimKey(${renderValue({ name: p.shortName, type: p.claimType })});`;
260
+ }
261
+
262
+ function renderReferenceData(p: ReferenceDataPattern): string {
263
+ const merged: Record<string, unknown> = {
264
+ entity: p.entityName,
265
+ data: [...p.data],
266
+ ...(p.upsertKey !== undefined && { upsertKey: p.upsertKey }),
267
+ };
268
+ return `r.referenceData(${renderValue(merged)});`;
269
+ }
270
+
271
+ function renderUseExtension(p: UseExtensionPattern): string {
272
+ const merged: Record<string, unknown> = {
273
+ name: p.extensionName,
274
+ entity: p.entityName,
275
+ ...(p.options ?? {}),
276
+ };
277
+ return `r.useExtension(${renderValue(merged)});`;
278
+ }
279
+
280
+ // =============================================================================
281
+ // Mixed patterns — header is data, body is opaque source-span (raw TS).
282
+ //
283
+ // We embed `SourceLocation.raw` verbatim. The static parts get rendered
284
+ // as JSON-like values; the closure / schema / template body slots in as
285
+ // the original text. Indentation matters for biome-stability — opaque
286
+ // bodies are placed at the property's indent level.
287
+ // =============================================================================
288
+
289
+ function renderScreen(p: ScreenPattern): string {
290
+ // ScreenDefinition may carry $opaque markers where closures lived.
291
+ // We swap each marker for the raw source span from opaqueProps. Walking
292
+ // the definition by JSON-path matches how the parser keys the spans.
293
+ const woven = weaveOpaque(p.definition, p.opaqueProps, "");
294
+ return `r.screen(${renderValueWithRawSlots(woven, 0)});`;
295
+ }
296
+
297
+ /**
298
+ * Re-indent a multi-line opaque source span so its continuation lines
299
+ * align with the new context. The body's first line is left as-is (it's
300
+ * inserted right after `: ` in the property assignment); follow-up
301
+ * lines have their *minimum leading whitespace* stripped, then the new
302
+ * indent prepended. Single-line bodies pass through.
303
+ *
304
+ * Why: bodies are captured verbatim from the original file at whatever
305
+ * indent they sat at. When embedded into a different surrounding
306
+ * structure (e.g. positional → object form), the relative indent
307
+ * shifts. Without this normalisation the rendered output looks ragged
308
+ * and the roundtrip equality test sees `raw` strings that differ in
309
+ * whitespace, even though the code is identical.
310
+ */
311
+ function reindentBody(raw: string, newIndent: string): string {
312
+ const lines = raw.split("\n");
313
+ if (lines.length <= 1) return raw;
314
+ // Determine the smallest leading-whitespace of non-empty continuation lines.
315
+ let minIndent = Infinity;
316
+ for (let i = 1; i < lines.length; i++) {
317
+ const line = lines[i];
318
+ if (line === undefined) continue;
319
+ if (line.trim() === "") continue;
320
+ const lead = line.match(/^[ \t]*/)?.[0].length ?? 0;
321
+ if (lead < minIndent) minIndent = lead;
322
+ }
323
+ if (!Number.isFinite(minIndent)) return raw;
324
+ const out = [lines[0] ?? ""];
325
+ for (let i = 1; i < lines.length; i++) {
326
+ const line = lines[i] ?? "";
327
+ if (line.trim() === "") {
328
+ out.push("");
329
+ } else {
330
+ out.push(newIndent + line.slice(minIndent));
331
+ }
332
+ }
333
+ return out.join("\n");
334
+ }
335
+
336
+ function renderWriteHandler(p: WriteHandlerPattern): string {
337
+ const lines: string[] = ["r.writeHandler({"];
338
+ lines.push(` name: ${JSON.stringify(p.handlerName)},`);
339
+ lines.push(` schema: ${reindentBody(p.schemaSource.raw, PATTERN_INDENT)},`);
340
+ lines.push(` handler: ${reindentBody(p.handlerBody.raw, PATTERN_INDENT)},`);
341
+ if (p.access !== undefined) lines.push(` access: ${renderValue(p.access)},`);
342
+ if (p.rateLimit !== undefined) lines.push(` rateLimit: ${renderValue(p.rateLimit)},`);
343
+ if (p.skipTransitionGuard === true) lines.push(" skipTransitionGuard: true,");
344
+ lines.push("});");
345
+ return lines.join("\n");
346
+ }
347
+
348
+ function renderQueryHandler(p: QueryHandlerPattern): string {
349
+ const lines: string[] = ["r.queryHandler({"];
350
+ lines.push(` name: ${JSON.stringify(p.handlerName)},`);
351
+ lines.push(` schema: ${reindentBody(p.schemaSource.raw, PATTERN_INDENT)},`);
352
+ lines.push(` handler: ${reindentBody(p.handlerBody.raw, PATTERN_INDENT)},`);
353
+ if (p.access !== undefined) lines.push(` access: ${renderValue(p.access)},`);
354
+ if (p.rateLimit !== undefined) lines.push(` rateLimit: ${renderValue(p.rateLimit)},`);
355
+ lines.push("});");
356
+ return lines.join("\n");
357
+ }
358
+
359
+ function renderHook(p: HookPattern): string {
360
+ const lines: string[] = ["r.hook({"];
361
+ lines.push(` type: ${JSON.stringify(p.hookType)},`);
362
+ lines.push(` target: ${renderValue(typeof p.target === "string" ? p.target : [...p.target])},`);
363
+ lines.push(` handler: ${reindentBody(p.fnBody.raw, PATTERN_INDENT)},`);
364
+ if (p.phase !== undefined) lines.push(` phase: ${JSON.stringify(p.phase)},`);
365
+ lines.push("});");
366
+ return lines.join("\n");
367
+ }
368
+
369
+ function renderEntityHook(p: EntityHookPattern): string {
370
+ const lines: string[] = ["r.entityHook({"];
371
+ lines.push(` type: ${JSON.stringify(p.hookType)},`);
372
+ lines.push(` entity: ${JSON.stringify(p.entityName)},`);
373
+ lines.push(` handler: ${reindentBody(p.fnBody.raw, PATTERN_INDENT)},`);
374
+ if (p.phase !== undefined) lines.push(` phase: ${JSON.stringify(p.phase)},`);
375
+ lines.push("});");
376
+ return lines.join("\n");
377
+ }
378
+
379
+ function renderJob(p: JobPattern): string {
380
+ const lines: string[] = ["r.job({"];
381
+ lines.push(` name: ${JSON.stringify(p.jobName)},`);
382
+ for (const [k, v] of Object.entries(p.options)) {
383
+ lines.push(` ${renderKey(k)}: ${renderValue(v)},`);
384
+ }
385
+ lines.push(` handler: ${p.handlerBody.raw},`);
386
+ lines.push("});");
387
+ return lines.join("\n");
388
+ }
389
+
390
+ function renderNotification(p: NotificationPattern): string {
391
+ const lines: string[] = ["r.notification({"];
392
+ lines.push(` name: ${JSON.stringify(p.notificationName)},`);
393
+ lines.push(` trigger: { on: ${JSON.stringify(p.trigger.on)} },`);
394
+ lines.push(` recipient: ${p.recipientBody.raw},`);
395
+ lines.push(` data: ${p.dataBody.raw},`);
396
+ if (p.templates && Object.keys(p.templates).length > 0) {
397
+ lines.push(" templates: {");
398
+ for (const [k, loc] of Object.entries(p.templates)) {
399
+ lines.push(` ${renderKey(k)}: ${loc.raw},`);
400
+ }
401
+ lines.push(" },");
402
+ }
403
+ lines.push("});");
404
+ return lines.join("\n");
405
+ }
406
+
407
+ function renderAuthClaims(p: AuthClaimsPattern): string {
408
+ return `r.authClaims(${p.fnBody.raw});`;
409
+ }
410
+
411
+ function renderHttpRoute(p: HttpRoutePattern): string {
412
+ const lines: string[] = ["r.httpRoute({"];
413
+ lines.push(` method: ${JSON.stringify(p.method)},`);
414
+ lines.push(` path: ${JSON.stringify(p.path)},`);
415
+ if (p.anonymous === true) lines.push(" anonymous: true,");
416
+ lines.push(` handler: ${p.handlerBody.raw},`);
417
+ lines.push("});");
418
+ return lines.join("\n");
419
+ }
420
+
421
+ function renderProjection(p: ProjectionPattern): string {
422
+ const lines: string[] = ["r.projection({"];
423
+ lines.push(` name: ${JSON.stringify(p.name)},`);
424
+ // ProjectionPattern.sourceEntity is the typed field; the runtime
425
+ // r.projection({...}) call uses `source` (matches ProjectionDefinition).
426
+ lines.push(
427
+ ` source: ${renderValue(typeof p.sourceEntity === "string" ? p.sourceEntity : [...p.sourceEntity])},`,
428
+ );
429
+ lines.push(" apply: {");
430
+ for (const [eventType, loc] of Object.entries(p.applyBodies)) {
431
+ lines.push(` ${renderKey(eventType)}: ${loc.raw},`);
432
+ }
433
+ lines.push(" },");
434
+ lines.push("});");
435
+ return lines.join("\n");
436
+ }
437
+
438
+ function renderMultiStreamProjection(p: MultiStreamProjectionPattern): string {
439
+ const lines: string[] = ["r.multiStreamProjection({"];
440
+ lines.push(` name: ${JSON.stringify(p.name)},`);
441
+ lines.push(" apply: {");
442
+ for (const [eventType, loc] of Object.entries(p.applyBodies)) {
443
+ lines.push(` ${renderKey(eventType)}: ${loc.raw},`);
444
+ }
445
+ lines.push(" },");
446
+ if (p.errorMode !== undefined) lines.push(` errorMode: ${renderValue(p.errorMode)},`);
447
+ if (p.runIn !== undefined) lines.push(` runIn: ${renderValue(p.runIn)},`);
448
+ if (p.delivery !== undefined) lines.push(` delivery: ${JSON.stringify(p.delivery)},`);
449
+ lines.push("});");
450
+ return lines.join("\n");
451
+ }
452
+
453
+ function renderDefineEvent(p: DefineEventPattern): string {
454
+ const lines: string[] = ["r.defineEvent({"];
455
+ lines.push(` name: ${JSON.stringify(p.eventName)},`);
456
+ lines.push(` schema: ${p.schemaSource.raw},`);
457
+ if (p.version !== undefined) lines.push(` version: ${p.version},`);
458
+ lines.push("});");
459
+ return lines.join("\n");
460
+ }
461
+
462
+ function renderEventMigration(p: EventMigrationPattern): string {
463
+ const lines: string[] = ["r.eventMigration({"];
464
+ lines.push(` event: ${JSON.stringify(p.eventName)},`);
465
+ lines.push(` fromVersion: ${p.fromVersion},`);
466
+ lines.push(` toVersion: ${p.toVersion},`);
467
+ lines.push(` transform: ${p.transformBody.raw},`);
468
+ lines.push("});");
469
+ return lines.join("\n");
470
+ }
471
+
472
+ function renderExtendsRegistrar(p: ExtendsRegistrarPattern): string {
473
+ return `r.extendsRegistrar(${JSON.stringify(p.extensionName)}, ${p.defBody.raw});`;
474
+ }
475
+
476
+ function renderUnknown(p: UnknownPattern): string {
477
+ // Round-trip preservation only: emit the raw call text from the
478
+ // SourceLocation so the rendered file stays semantically identical
479
+ // to the input.
480
+ //
481
+ // **Patch-Surprise warning:** an UnknownPattern cannot be added via
482
+ // FeaturePatcher (no typed `addUnknown` exists, by design — typed
483
+ // adds force the caller to commit to a known pattern-kind). It also
484
+ // cannot be replaced/removed cleanly, because no PatternId variant
485
+ // matches an UnknownPattern's free-form shape. Treat UnknownPattern
486
+ // as read-only in the patcher pipeline; the only way to "edit" one
487
+ // is to convert it to a known pattern-kind first (i.e. add a typed
488
+ // extractor + pattern type).
489
+ return p.source.raw;
490
+ }
491
+
492
+ // =============================================================================
493
+ // Screen-Pattern body weaving — replaces $opaque markers with raw spans.
494
+ // =============================================================================
495
+
496
+ type WovenValue = unknown | { readonly __raw: string };
497
+
498
+ function weaveOpaque(
499
+ value: unknown,
500
+ opaqueProps: Readonly<Record<string, { readonly raw: string }>>,
501
+ path: string,
502
+ ): WovenValue {
503
+ if (value === SCREEN_OPAQUE_MARKER) {
504
+ const span = opaqueProps[path];
505
+ if (!span) throw new Error(`weaveOpaque: missing span for path "${path}"`);
506
+ return { __raw: span.raw };
507
+ }
508
+ if (Array.isArray(value)) {
509
+ return value.map((el, idx) => weaveOpaque(el, opaqueProps, `${path}.${idx}`));
510
+ }
511
+ if (value && typeof value === "object") {
512
+ const out: Record<string, WovenValue> = {};
513
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
514
+ const childPath = path ? `${path}.${k}` : k;
515
+ out[k] = weaveOpaque(v, opaqueProps, childPath);
516
+ }
517
+ return out;
518
+ }
519
+ return value;
520
+ }
521
+
522
+ function renderValueWithRawSlots(value: WovenValue, indent: number): string {
523
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && "__raw" in value) {
524
+ return (value as { __raw: string }).__raw;
525
+ }
526
+ if (Array.isArray(value)) {
527
+ if (value.length === 0) return "[]";
528
+ const inner = value
529
+ .map((v) => `${spaces(indent + 2)}${renderValueWithRawSlots(v, indent + 2)}`)
530
+ .join(",\n");
531
+ return `[\n${inner},\n${spaces(indent)}]`;
532
+ }
533
+ if (value !== null && typeof value === "object") {
534
+ const entries = Object.entries(value as Record<string, WovenValue>);
535
+ if (entries.length === 0) return "{}";
536
+ const inner = entries
537
+ .map(
538
+ ([k, v]) =>
539
+ `${spaces(indent + 2)}${renderKey(k)}: ${renderValueWithRawSlots(v, indent + 2)}`,
540
+ )
541
+ .join(",\n");
542
+ return `{\n${inner},\n${spaces(indent)}}`;
543
+ }
544
+ return renderValue(value, indent);
545
+ }
546
+
547
+ // =============================================================================
548
+ // Feature-File rendering
549
+ // =============================================================================
550
+
551
+ export type RenderFeatureFileInput = {
552
+ readonly featureName: string;
553
+ readonly patterns: readonly FeaturePattern[];
554
+ /** Extra import lines emitted between the version header and defineFeature.
555
+ * Defaults to the minimum: defineFeature + zod. */
556
+ readonly imports?: readonly string[];
557
+ };
558
+
559
+ const DEFAULT_IMPORTS = [
560
+ 'import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";',
561
+ 'import { z } from "zod";',
562
+ ] as const;
563
+
564
+ /**
565
+ * Render a complete feature-file: schema-version header, imports, the
566
+ * defineFeature call wrapping every pattern in source order. The output
567
+ * is biome-format-stable so callers can persist it directly.
568
+ */
569
+ export function renderFeatureFile(input: RenderFeatureFileInput): string {
570
+ const imports = input.imports ?? DEFAULT_IMPORTS;
571
+ const body = input.patterns.map((p) => indent(renderPattern(p), PATTERN_INDENT)).join("\n\n");
572
+ return [
573
+ VERSION_HEADER,
574
+ "",
575
+ ...imports,
576
+ "",
577
+ `defineFeature(${JSON.stringify(input.featureName)}, (r) => {`,
578
+ body,
579
+ "});",
580
+ "",
581
+ ].join("\n");
582
+ }
583
+
584
+ /**
585
+ * Prefix every non-empty line of `text` with `prefix`. Re-used from the
586
+ * patcher (patch.ts imports this) so indent helpers stay in one place
587
+ * — when canonical-form indentation conventions ever change, only this
588
+ * function needs to follow.
589
+ */
590
+ export function indent(text: string, prefix: string): string {
591
+ return text
592
+ .split("\n")
593
+ .map((line) => (line.length === 0 ? line : prefix + line))
594
+ .join("\n");
595
+ }
596
+
597
+ /**
598
+ * Indentation prefix used inside `defineFeature((r) => { ... })` for
599
+ * every top-level r.* statement. Two-space convention matches biome's
600
+ * default and the parse-happy-path test fixture.
601
+ */
602
+ export const PATTERN_INDENT = " ";
@@ -0,0 +1,45 @@
1
+ // SourceLocation — where a recognised `r.*` call (or an opaque code
2
+ // region inside one) lives in the feature file. Attached to every
3
+ // FeaturePattern by the AST visitor so:
4
+ //
5
+ // - the Designer can scroll to the file region ("show source")
6
+ // - the AI patcher can replace that exact region without regenerating
7
+ // the rest of the file
8
+ // - opaque bodies (writeHandler closures, hook fns, etc.) can be
9
+ // rendered as read-only code blocks (raw carries the full source)
10
+ //
11
+ // Lines + columns are 1-based to match the LSP / Monaco / CodeMirror
12
+ // convention — the Designer can pass them through unchanged.
13
+
14
+ import type { Node, SourceFile } from "ts-morph";
15
+
16
+ export type SourcePosition = {
17
+ readonly line: number;
18
+ readonly column: number;
19
+ };
20
+
21
+ export type SourceLocation = {
22
+ readonly file: string;
23
+ readonly start: SourcePosition;
24
+ readonly end: SourcePosition;
25
+ // Raw source text from the start..end range. For round-trip display
26
+ // (rendering custom bodies as read-only blocks in the Designer) +
27
+ // diff generation when patching (compare original vs new).
28
+ readonly raw: string;
29
+ };
30
+
31
+ /**
32
+ * Build a SourceLocation from a ts-morph Node. Lives here (not in
33
+ * parse.ts) so extractors can use it without importing parse.ts —
34
+ * keeps the dependency graph one-way.
35
+ */
36
+ export function sourceLocationFromNode(node: Node, sourceFile: SourceFile): SourceLocation {
37
+ const start = sourceFile.getLineAndColumnAtPos(node.getStart());
38
+ const end = sourceFile.getLineAndColumnAtPos(node.getEnd());
39
+ return {
40
+ file: sourceFile.getFilePath(),
41
+ start: { line: start.line, column: start.column },
42
+ end: { line: end.line, column: end.column },
43
+ raw: node.getText(),
44
+ };
45
+ }