@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,452 @@
1
+ import type { FieldDefinition } from "./fields";
2
+ import type { AccessRule } from "./handlers";
3
+
4
+ // Screen definitions describe how a feature surfaces data to the user.
5
+ // Pure data — the engine stores these verbatim and ui-core / the renderer
6
+ // packages decide what to do with them. The framework must not import
7
+ // React / react-native from here; renderer components stay opaque so
8
+ // `engine/` imports don't pull the whole UI toolchain into every bundle.
9
+ //
10
+ // Note on `id`: feature authors write the short form ("product-list"); the
11
+ // registry overwrites `id` with the qualified name ("shop:screen:product-list")
12
+ // in its stored copies. Callers of `registry.getScreen(qn)` /
13
+ // `getAllScreens()` / `getScreensByEntity(...)` always see the qualified id.
14
+ // `feature.screens[shortId]` on the unregistered FeatureDefinition keeps
15
+ // the short form.
16
+
17
+ // A per-platform component pair — used anywhere a feature attaches a
18
+ // rendered component (screens, slots, routes, field renderers, future
19
+ // r.uiComponent). Both fields are `unknown` so the engine doesn't depend
20
+ // on React types; ui-core resolves the correct platform at mount-time.
21
+ // Framework code only checks structural presence.
22
+ export type PlatformComponent = {
23
+ readonly react?: unknown;
24
+ readonly native?: unknown;
25
+ };
26
+
27
+ // Level-2 field renderer (ui-architecture.md §Renderer Customization):
28
+ // - PlatformComponent → platform-specific component from the same feature
29
+ // - string → cross-feature QN reference (resolved by the renderer)
30
+ // - function → inline value formatter (e.g. `v => `${v} €``)
31
+ // Function-Form bekommt optional die ganze Row als 2. Argument —
32
+ // nützlich für context-aware Renderer (Tier 2.7e-Eagerload nutzt das
33
+ // um aus row._refs den resolved Display-Wert zu lesen). Renderer die
34
+ // nur den value brauchen ignorieren das Argument einfach.
35
+ export type FieldRenderer =
36
+ | PlatformComponent
37
+ | string
38
+ | ((value: unknown, row?: Readonly<Record<string, unknown>>) => string);
39
+
40
+ // Conditional field-state evaluator. `data` is the current form row and
41
+ // `ctx` carries user / session info — the form-controller in ui-core passes
42
+ // both at evaluation time. Engine-side defaults are `unknown` because the
43
+ // framework has nothing to assert about the shapes; feature code can narrow
44
+ // them by passing type args (e.g. `FieldCondition<OrderRow>`) to skip the
45
+ // cast at call sites.
46
+ export type FieldCondition<TData = unknown, TCtx = unknown> = (data: TData, ctx: TCtx) => boolean;
47
+
48
+ // --- entityList ---
49
+
50
+ // `string` shorthand when the column only needs its field name; the object
51
+ // form carries renderer / display overrides. normalizeListColumn() below
52
+ // collapses both into the object form for downstream consumers.
53
+ export type ListColumnSpec =
54
+ | string
55
+ | {
56
+ readonly field: string;
57
+ readonly renderer?: FieldRenderer;
58
+ };
59
+
60
+ // Pagination-Modi für entityList:
61
+ // - "pages": klassischer Pager (← 1 2 ... N →) — bookmarkable
62
+ // via ?page=N in der URL. Server liefert `total`
63
+ // damit der Pager "Page X of Y" rendern kann.
64
+ // Default für CRUD-/Admin-Listen.
65
+ // - "infinite": IntersectionObserver am Bottom — beim Sichtbar
66
+ // werden lädt cursor-basiert die nächste Page und
67
+ // appended an `rows`. Kein page-State in URL (Scroll-
68
+ // Position ist Browser-eigen).
69
+ // - false: Pagination aus — `executor.list()` lädt alles
70
+ // was zur Tenant-Sicht passt. Sinnvoll für kleine
71
+ // Lookup-/Master-Daten (≤ ~200 Rows).
72
+ export type ListPaginationMode = "pages" | "infinite" | false;
73
+
74
+ // Sort-State auf der Wire — Field-Name muss zur Entity-Definition
75
+ // passen UND als sortable: true markiert sein. Validator lehnt sonst
76
+ // beim Boot ab.
77
+ export type ListSortDir = "asc" | "desc";
78
+ export type ListSortSpec = {
79
+ readonly field: string;
80
+ readonly dir: ListSortDir;
81
+ };
82
+
83
+ // Screen-Level Filter — Author deklariert pro List-Screen einen festen
84
+ // Filter der bei jedem Query angewendet wird. Use-case: drei Buckets
85
+ // derselben Entity ("Upcoming Maintenance" / "Active Maintenance" /
86
+ // "Past Maintenance") ohne drei Custom-Pages — Filter pro Screen
87
+ // unterscheidet sie.
88
+ //
89
+ // Operatoren (Drizzle-konsistent):
90
+ // eq/ne → field = value | field != value
91
+ // lt/gt → field < value | field > value (numerisch / temporal)
92
+ // in → field IN (...values), value muss readonly array sein
93
+ //
94
+ // Field muss in der Entity existieren UND `filterable: true` haben
95
+ // (Boot-Validator pinned beides). `lt`/`gt` nur auf vergleichbaren
96
+ // Field-Types (number/money/date/timestamp/locatedTimestamp); auf
97
+ // text/boolean/select/multiSelect lehnt der Validator das ab.
98
+ //
99
+ // Security-Modell: Filter ist UX-Bucketing, KEINE Access-Boundary. Der
100
+ // Server appliziert den filter aus dem Payload — der Client kann ihn
101
+ // weglassen oder durch einen anderen ersetzen. Boundary bleiben
102
+ // access-rule + Tenant-Scope; Felder mit Sicherheits-Bias (encrypted,
103
+ // restricted) müssen dort geschützt werden, nicht über den Screen-Filter.
104
+ export type ScreenFilterOp = "eq" | "ne" | "lt" | "gt" | "in";
105
+ export type ScreenFilter = {
106
+ readonly field: string;
107
+ readonly op: ScreenFilterOp;
108
+ readonly value: unknown;
109
+ };
110
+
111
+ // RowAction — per-Row Button/Dropdown-Item das einen Write-Handler
112
+ // triggert. Lebt im Schema (Author deklariert pro List-Screen welche
113
+ // Aktionen möglich sind), Caller liefert die handler-QN + optional
114
+ // payload-Builder + Confirm-Prompt.
115
+ //
116
+ // Pattern: row-level Lifecycle-Operations (Maintenance start/cancel/
117
+ // complete, Incident resolve, Order ship etc.) — Sachen die in einem
118
+ // CRUD-update kein passendes Verb haben aber als WriteHandler existieren.
119
+ //
120
+ // ⚠️ Function-Props (`payload`, `params`, `visible`) leben nur im
121
+ // Monolith-Bundle-Pattern (Server + Client teilen Source-Bundle, wie
122
+ // Showcase mit dev-server). In setups mit JSON-injected window.__
123
+ // KUMIKO_SCHEMA__ werden Functions silent gedroppt (`buildAppSchema`
124
+ // whitelist-projeziert + JSON.stringify entfernt sie). Für solche Apps:
125
+ // - `payload`/`params` weglassen → Default `{ id: row.id }` greift.
126
+ // - `visible` über server-side Filter im Handler statt Client-side
127
+ // Visibility lösen.
128
+ // Declarative Alternative kommt wenn ein konkreter Use-Case das fordert.
129
+ //
130
+ // Discriminated Union mit `kind`:
131
+ // - "writeHandler" (default für Backwards-Compat): dispatched einen
132
+ // Write-Handler mit Payload pro Row.
133
+ // - "navigate" (Tier 2.7e): navigiert zu einem anderen Screen,
134
+ // optional mit URL-Search-Params aus `params(row)`. Use-case:
135
+ // "Edit", "View Audit-Log", "Open in actionForm" etc.
136
+ export type RowAction = RowActionWriteHandler | RowActionNavigate;
137
+
138
+ export type RowActionWriteHandler = {
139
+ /** Default für RowActions ohne explizit gesetzten `kind` —
140
+ * Backwards-kompatible Form. Kann auch explizit gesetzt werden. */
141
+ readonly kind?: "writeHandler";
142
+ /** Stable id pro Screen — kebab-case, eindeutig im Action-Set. */
143
+ readonly id: string;
144
+ /** Anzeige-Text (i18n-Key). */
145
+ readonly label: string;
146
+ /** Qualified-Name des Server-Handlers, z.B.
147
+ * "publicstatus:write:maintenance:start". Wird via useDispatcher
148
+ * dispatcht. */
149
+ readonly handler: string;
150
+ /** Payload-Builder pro Row. Default = `{ id: row.id }`. ⚠️ Function-
151
+ * Form nur im Monolith-Bundle-Pattern — siehe Type-Header. */
152
+ readonly payload?: (row: Readonly<Record<string, unknown>>) => Record<string, unknown>;
153
+ /** i18n-Key für die Confirm-Dialog-Description. Wenn gesetzt, öffnet
154
+ * ein Modal vor der Ausführung — der User muss explizit bestätigen.
155
+ * Zusammen mit `style: "danger"` ist das die Standard-Sicherheits-
156
+ * Garde für destruktive Aktionen. */
157
+ readonly confirm?: string;
158
+ /** i18n-Key für den Confirm-Button-Text im Dialog. Default = `label`
159
+ * (also "Delete" → "Delete"-Button im Confirm). Setzen wenn die
160
+ * Action einen langen Namen hat ("Mark Subscription as Cancelled")
161
+ * und der Button kürzer sein soll ("Cancel Subscription"). */
162
+ readonly confirmLabel?: string;
163
+ /** Conditional Visibility pro Row — Action erscheint nur wenn die
164
+ * Bedingung true returnt. Beispiel: nur "Start" zeigen wenn
165
+ * status === "scheduled". ⚠️ Function-Form nur im Monolith-Bundle-
166
+ * Pattern — siehe Type-Header. */
167
+ readonly visible?: FieldCondition;
168
+ /** Visual-Style. "danger" rendert rot + erzwingt einen Confirm-
169
+ * Dialog (auch ohne expliziten `confirm`-Key). */
170
+ readonly style?: "primary" | "secondary" | "danger";
171
+ };
172
+
173
+ export type RowActionNavigate = {
174
+ readonly kind: "navigate";
175
+ readonly id: string;
176
+ readonly label: string;
177
+ /** Screen-id (kurz, unqualified) zu dem navigiert wird. Boot-
178
+ * Validator prüft Existenz im selben Feature. */
179
+ readonly screen: string;
180
+ /** Optional: URL-Search-Params aus row-Context. Wird in actionForm-
181
+ * Targets als initial values gelesen ("Edit Customer X" → URL hat
182
+ * `?customerId=row-uuid`, actionForm initial values pre-fillen).
183
+ * ⚠️ Function-Form nur im Monolith-Bundle-Pattern. */
184
+ readonly params?: (row: Readonly<Record<string, unknown>>) => Record<string, unknown>;
185
+ /** Conditional Visibility pro Row — analog zu writeHandler-Variante. */
186
+ readonly visible?: FieldCondition;
187
+ readonly style?: "primary" | "secondary";
188
+ };
189
+
190
+ // ToolbarAction — Button im List-Header. Zwei Varianten: navigate auf
191
+ // einen anderen Screen (z.B. zu einem actionForm) oder direkt einen
192
+ // Handler dispatchen (z.B. "Sync All" ohne Form).
193
+ //
194
+ // ⚠️ Function-Props (`payload`) leben nur im Monolith-Bundle-Pattern
195
+ // (siehe RowAction-JSDoc) — JSON-injected Schemas droppen sie silent.
196
+ // Für solche Apps payload weglassen oder im writeHandler server-side
197
+ // die Tenant-/Session-Kontext-Werte ableiten.
198
+ export type ToolbarAction =
199
+ | {
200
+ readonly kind: "navigate";
201
+ readonly id: string;
202
+ readonly label: string;
203
+ /** Screen-id (kurz, unqualified) zu dem navigiert wird. */
204
+ readonly screen: string;
205
+ readonly style?: "primary" | "secondary";
206
+ }
207
+ | {
208
+ readonly kind: "writeHandler";
209
+ readonly id: string;
210
+ readonly label: string;
211
+ readonly handler: string;
212
+ /** Optional: Payload-Builder ohne row-Context. Default = `{}`. */
213
+ readonly payload?: () => Record<string, unknown>;
214
+ /** i18n-Key für Confirm-Dialog-Description. Wenn gesetzt UND/ODER
215
+ * style="danger": Modal vor der Ausführung. */
216
+ readonly confirm?: string;
217
+ /** i18n-Key für Confirm-Button-Text im Dialog. Default = `label`. */
218
+ readonly confirmLabel?: string;
219
+ readonly style?: "primary" | "secondary" | "danger";
220
+ };
221
+
222
+ export type EntityListScreenDefinition = {
223
+ readonly id: string;
224
+ readonly type: "entityList";
225
+ readonly entity: string;
226
+ readonly columns: readonly ListColumnSpec[];
227
+ // Row renderer (Desktop) — when omitted, renderer draws the default table
228
+ // from `columns`. cardRenderer fills the same role on compact layouts.
229
+ readonly rowRenderer?: PlatformComponent;
230
+ readonly cardRenderer?: PlatformComponent;
231
+ /** Per-Row-Aktionen — rendert eine Actions-Spalte rechts in der Tabelle.
232
+ * Bis zu 2 actions als inline-Buttons; >2 als Kebab-Dropdown.
233
+ * Reihenfolge im Array = Reihenfolge in der UI. */
234
+ readonly rowActions?: readonly RowAction[];
235
+ /** Toolbar-Aktionen (List-Header). "Open Incident", "Schedule Maintenance"
236
+ * etc. — neben "+ Neu" wenn vorhanden. Reihenfolge im Array = UI-
237
+ * Reihenfolge, primary-style links. */
238
+ readonly toolbarActions?: readonly ToolbarAction[];
239
+ /** Server-side Filter, fest am Screen — drei Buckets derselben
240
+ * Entity ohne Custom-Pages (z.B. "Upcoming" / "Active" / "Past"
241
+ * Maintenance). User-side q-Search läuft AUF dem gefilterten Set
242
+ * oben drauf. Boot-Validator pinst dass field in der Entity existiert. */
243
+ readonly filter?: ScreenFilter;
244
+ // Pagination-Modus (Default "pages"). Bestimmt UI (Pager vs Scroll-
245
+ // Sentinel) und ob der Server `total` mitliefern muss.
246
+ readonly pagination?: ListPaginationMode;
247
+ // Page-Größe. Default 50 — guter Kompromiss zwischen "viel sichtbar"
248
+ // und "DB liefert schnell". Apps mit teurem Read (Joins, Computed-
249
+ // Fields) gehen runter; Power-User-Listen (z.B. internal Analytics)
250
+ // gehen hoch.
251
+ readonly pageSize?: number;
252
+ // Default-Sortierung beim Erst-Mount. Wenn URL-Param `?sort=…`
253
+ // gesetzt ist, gewinnt der; sonst nutzt RenderList diesen Default.
254
+ // `field` muss in der Entity sortable: true sein — Boot-Validator
255
+ // pinnt das.
256
+ readonly defaultSort?: ListSortSpec;
257
+ // Search-Toolbar im UI an/aus. Server-Search geht IMMER über den
258
+ // SearchAdapter (Meilisearch) — kein DB-ILIKE-Drift. Default true
259
+ // wenn die Entity searchable Felder hat, sonst false.
260
+ readonly searchable?: boolean;
261
+ readonly slots?: ScreenSlots;
262
+ readonly access?: AccessRule;
263
+ };
264
+
265
+ // --- entityEdit ---
266
+
267
+ // camelCase `readOnly` instead of the spec's lowercase `readonly`: TS's
268
+ // `readonly` modifier on the same line would make the declaration read
269
+ // `readonly readonly?: FieldCondition`, which is legal but a real lese-knick.
270
+ // Mirrors React's `readOnly` prop so the ergonomic cost of the divergence
271
+ // from ui-architecture.md is minimal.
272
+ export type EditFieldSpec =
273
+ | string
274
+ | {
275
+ readonly field: string;
276
+ readonly span?: number;
277
+ readonly visible?: FieldCondition;
278
+ readonly readOnly?: FieldCondition;
279
+ readonly required?: FieldCondition;
280
+ readonly renderer?: FieldRenderer;
281
+ };
282
+
283
+ export type EditSectionSpec = {
284
+ readonly title: string;
285
+ readonly columns?: number;
286
+ readonly fields: readonly EditFieldSpec[];
287
+ };
288
+
289
+ export type EditLayout = {
290
+ readonly sections: readonly EditSectionSpec[];
291
+ };
292
+
293
+ export type EntityEditScreenDefinition = {
294
+ readonly id: string;
295
+ readonly type: "entityEdit";
296
+ readonly entity: string;
297
+ readonly layout: EditLayout;
298
+ readonly slots?: ScreenSlots;
299
+ readonly access?: AccessRule;
300
+ };
301
+
302
+ // --- actionForm ---
303
+
304
+ // Form-Screen für non-CRUD Write-Handler. Wird gerendert wie ein
305
+ // EntityEditScreen (sections + fields), aber:
306
+ // - kein detail-fetch beim Mount (initial-state = field-defaults)
307
+ // - kein CRUD-verb-mapping ("create"/"update") — Author gibt
308
+ // explizit die Write-Handler-QN an
309
+ // - Form-Object landet 1:1 als payload beim Handler; sein Zod-Schema
310
+ // validiert weiter
311
+ // - optional `redirect` zu einem anderen Screen nach Submit-Success
312
+ //
313
+ // Beispiele: "Send invitation" (mit recipient-email + role), "Approve
314
+ // invoice" (mit notes), "Bulk-import" (mit CSV-string + mode).
315
+ //
316
+ // Field-Shape: inline am Screen statt entity-Reference. Author hat
317
+ // explizite Kontrolle was die Form rendert ohne eine ganze Entity
318
+ // dafür anzulegen. Die FieldDefinitions sind dieselben wie auf
319
+ // Entities (text/select/number/...) — alle DefaultInput-Renderer
320
+ // greifen unverändert.
321
+ export type ActionFormScreenDefinition = {
322
+ readonly id: string;
323
+ readonly type: "actionForm";
324
+ /** Write-Handler-QN der bei Submit gerufen wird. Form-Object landet
325
+ * 1:1 als payload — Handler-Schema (Zod) validiert weiter. */
326
+ readonly handler: string;
327
+ /** Form-Shape: Field-Map pro Name. Nutzt dieselben FieldDefinitions
328
+ * wie Entity-Felder. Mindestens ein Feld erforderlich (Boot-
329
+ * Validator). */
330
+ readonly fields: Readonly<Record<string, FieldDefinition>>;
331
+ /** Layout analog zu EntityEditScreen: sections mit fields aus dem
332
+ * fields-Map oben. */
333
+ readonly layout: EditLayout;
334
+ /** i18n-key für den Submit-Button. Default: i18n-Default des
335
+ * Renderers (typischerweise "actions.submit"). */
336
+ readonly submitLabel?: string;
337
+ /** Nach erfolgreichem Submit zu dieser Screen-ID navigieren (kurze
338
+ * ID, z.B. "item-list" — gleiche Feature, der nav-Router resolved
339
+ * zum vollen Pfad). Cross-Feature-Redirect ist nicht supported.
340
+ * Wenn nicht gesetzt, bleibt der User auf dem Form-Screen. Boot-
341
+ * Validator prüft dass die ID einen registrierten Screen meint. */
342
+ readonly redirect?: string;
343
+ readonly slots?: ScreenSlots;
344
+ readonly access?: AccessRule;
345
+ };
346
+
347
+ // --- custom ---
348
+
349
+ // Sub-route declared by a custom screen (Expo Router / URL-routing use).
350
+ // `path` is the route-segment appended to the screen's own path; the
351
+ // framework owns the outer routing. Components stay opaque.
352
+ export type CustomScreenRoute = {
353
+ readonly path: string;
354
+ readonly component: PlatformComponent;
355
+ };
356
+
357
+ export type CustomScreenDefinition = {
358
+ readonly id: string;
359
+ readonly type: "custom";
360
+ readonly renderer: PlatformComponent;
361
+ readonly routes?: readonly CustomScreenRoute[];
362
+ readonly access?: AccessRule;
363
+ };
364
+
365
+ // --- configEdit ---
366
+
367
+ // Form-Screen der Tenant-/User-/System-Settings aus dem bundled
368
+ // config-Feature liest und schreibt. Wird gerendert wie ein
369
+ // EntityEditScreen (sections + fields), aber:
370
+ // - Detail-Load via `config:query:values` (statt `<entity>:detail`)
371
+ // - Pre-Fill nutzt `configKeys[shortName]` → qualifizierter Key, dann
372
+ // `values[qualifiedKey].value`
373
+ // - Submit feuert pro geändertem Feld einen `config:write:set` mit
374
+ // {key, value, scope}; das config-feature behandelt jeden Key als
375
+ // eigenes Aggregate (configValue.<keyHash>) und alle N Writes laufen
376
+ // parallel (Promise.all). Per-Key idempotent → Retry safe.
377
+ // - kein Singleton-Hack nötig: pro (key+tenantId) gibt's by-design
378
+ // genau eine Row, der Bridge-Pattern aus dem Branding-MVP fällt weg
379
+ //
380
+ // Partial-Failure-Semantik: wenn von N parallelen Writes einer scheitert,
381
+ // bleiben die anderen committed (pro-Aggregate, kein Multi-Stream-Rollback).
382
+ // Das Form bleibt dirty bis der User retried — die schon erfolgreichen
383
+ // Writes feuern dann nochmal mit demselben Wert. Für `text` / `number` /
384
+ // `boolean` Keys ist das idempotent. Wer einen ConfigKey mit nicht-
385
+ // idempotentem Setter baut (Counter, append-only-list o.ä.) muss die
386
+ // Idempotenz im Setter sicherstellen.
387
+ //
388
+ // Field-Shape: inline am Screen wie bei `actionForm`. Author hat damit
389
+ // explizite Kontrolle über Input-Type (text/number/select/...) ohne
390
+ // Resolve-Ceremony — die FieldDefinitions sind dieselben wie auf
391
+ // Entities, alle DefaultInput-Renderer greifen unverändert. Field-
392
+ // Labels gehen über bestehende i18n-Konventionen (`<feature>:entity:
393
+ // <namespace>:field:<name>` o.ä. — der Author wählt den Namespace).
394
+ //
395
+ // scope MUSS mit der `createTenantConfig`/`createSystemConfig`/
396
+ // `createUserConfig`-Deklaration der referenzierten Keys
397
+ // übereinstimmen — der Boot-Validator pinnt das.
398
+ export type ConfigEditScreenDefinition = {
399
+ readonly id: string;
400
+ readonly type: "configEdit";
401
+ /** scope für config:write:set Calls. Muss zur Scope-Deklaration der
402
+ * in `configKeys` referenzierten Keys passen — Boot-Validator
403
+ * prüft das gegen die Registry. */
404
+ readonly scope: "tenant" | "system" | "user";
405
+ /** Map: form-field-name (kurz, wie im Layout referenziert) → voll-
406
+ * qualifizierter Config-Key (`<feature>:config:<short>`). Boot-
407
+ * Validator prüft dass jeder qualifizierte Key in der Registry
408
+ * bekannt ist. */
409
+ readonly configKeys: Readonly<Record<string, string>>;
410
+ /** Form-Shape pro Field-Name. Selbe FieldDefinitions wie auf
411
+ * Entities/ActionForm. Field-Names matchen die Keys in `configKeys`
412
+ * — Boot-Validator pinnt das. */
413
+ readonly fields: Readonly<Record<string, FieldDefinition>>;
414
+ /** Layout: Sections mit Field-Refs. Identisch zu entityEdit/
415
+ * actionForm. */
416
+ readonly layout: EditLayout;
417
+ /** i18n-key für den Submit-Button. Default: "kumiko.actions.save". */
418
+ readonly submitLabel?: string;
419
+ readonly slots?: ScreenSlots;
420
+ readonly access?: AccessRule;
421
+ };
422
+
423
+ // --- shared slots (Level 4 from ui-architecture.md) ---
424
+
425
+ export type ScreenSlots = {
426
+ readonly header?: PlatformComponent;
427
+ readonly beforeForm?: PlatformComponent;
428
+ readonly afterForm?: PlatformComponent;
429
+ readonly sidebar?: PlatformComponent;
430
+ readonly footer?: PlatformComponent;
431
+ readonly toolbar?: PlatformComponent;
432
+ };
433
+
434
+ // --- discriminated union ---
435
+
436
+ export type ScreenDefinition =
437
+ | EntityListScreenDefinition
438
+ | EntityEditScreenDefinition
439
+ | ActionFormScreenDefinition
440
+ | ConfigEditScreenDefinition
441
+ | CustomScreenDefinition;
442
+
443
+ // Collapse the string-shorthand into the object form. Both the boot-validator
444
+ // and (later) ui-core's view-model builder iterate over fields/columns — the
445
+ // helper keeps that loop from growing two branches everywhere.
446
+ export function normalizeListColumn(c: ListColumnSpec): Exclude<ListColumnSpec, string> {
447
+ return typeof c === "string" ? { field: c } : c;
448
+ }
449
+
450
+ export function normalizeEditField(f: EditFieldSpec): Exclude<EditFieldSpec, string> {
451
+ return typeof f === "string" ? { field: f } : f;
452
+ }
@@ -0,0 +1,42 @@
1
+ import type { AccessRule } from "./handlers";
2
+
3
+ // Workspace declaration. A workspace is a persona-/role-scoped UI surface:
4
+ // pure UI composition with no backend, DB or auth impact. The engine stores
5
+ // these verbatim and the active web shell (shellWorkspaces) renders the
6
+ // switcher and filters the nav tree by membership + access.
7
+ //
8
+ // Membership is computed from two sources, merged at boot:
9
+ // 1. r.workspace({ nav: [...] }) — explicit list of nav QNs
10
+ // 2. r.nav({ workspaces: [...] }) — nav entry self-assigns to workspaces
11
+ // A nav entry that appears in neither source belongs to no workspace and
12
+ // only shows up when no workspace is active (legacy / non-workspace apps).
13
+ //
14
+ // Cross-feature references are allowed: `nav` may point at any registered
15
+ // nav QN. The boot validator checks references exist and that workspace
16
+ // IDs referenced from r.nav are real.
17
+ export type WorkspaceDefinition = {
18
+ // Feature author writes the short id ("disposition"); the registry
19
+ // overwrites `id` with the qualified name ("bmc:workspace:disposition")
20
+ // in its stored copy. Same pattern as NavDefinition / ScreenDefinition.
21
+ readonly id: string;
22
+ // i18n translation key. Resolved at render time by the renderer's
23
+ // useTranslation hook; engine keeps it opaque.
24
+ readonly label: string;
25
+ // Icon key — whatever the icon registry of the active renderer understands.
26
+ // Engine doesn't validate; unknown icons surface as a missing icon on
27
+ // screen, not a boot failure (mirrors NavDefinition.icon).
28
+ readonly icon?: string;
29
+ // Sort weight in the workspace switcher (lower = earlier). Ties broken
30
+ // by registration order — features registered later appear lower.
31
+ readonly order?: number;
32
+ // Role / openToAll gate. Only users matching this rule see the workspace
33
+ // in their switcher. Mirrors NavDefinition.access — same semantics across
34
+ // the UI surface, so a default-deny app can do `{ roles: [] }`.
35
+ readonly access?: AccessRule;
36
+ // Explicit nav QNs that belong to this workspace. Merged with any nav
37
+ // entries that self-assign via r.nav({ workspaces: [...] }).
38
+ readonly nav?: readonly string[];
39
+ // Default workspace at login when the user has access to multiple. Boot
40
+ // validator rejects more than one default per app.
41
+ readonly default?: boolean;
42
+ };
@@ -0,0 +1,33 @@
1
+ import { parseQn, toKebab } from "./qualified-name";
2
+ import type { Registry, ValidationError } from "./types";
3
+
4
+ export type { ValidationError };
5
+
6
+ export function runValidation(
7
+ registry: Registry,
8
+ hookName: string,
9
+ data: Readonly<Record<string, unknown>>,
10
+ ): readonly ValidationError[] | null {
11
+ const errors: ValidationError[] = [];
12
+
13
+ // hookName is a qualified name like "feature:write:task:create".
14
+ // Validation hooks are stored with the unqualified short name in the feature definition.
15
+ const parsed = parseQn(hookName);
16
+
17
+ for (const [featureName, feature] of registry.features) {
18
+ if (toKebab(featureName) !== parsed.scope) continue;
19
+
20
+ const validationHooks = feature.hooks.validation;
21
+ if (!validationHooks) continue;
22
+
23
+ // Find the hook by matching the QN name segment against the stored short name.
24
+ // Both use colon convention (e.g. "task:create"), so direct match works.
25
+ const hook = validationHooks[parsed.name];
26
+ if (hook) {
27
+ const result = hook(data);
28
+ if (result) errors.push(...result);
29
+ }
30
+ }
31
+
32
+ return errors.length > 0 ? errors : null;
33
+ }