@cosmicdrift/kumiko-framework 0.386.0 → 0.388.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 (468) hide show
  1. package/AGENTS.md +6 -1
  2. package/LICENSE +21 -57
  3. package/README.md +2 -2
  4. package/agents/examples/anonymous-access.md +1 -1
  5. package/agents/examples/apex-landing.md +204 -0
  6. package/agents/examples/api-worker-split.md +64 -0
  7. package/agents/examples/app-config.md +14 -0
  8. package/agents/examples/auth-mfa.md +115 -0
  9. package/agents/examples/env-composition.md +28 -1
  10. package/agents/examples/legal-pages.md +1 -1
  11. package/agents/examples/prod-boot-options.md +16 -0
  12. package/agents/examples/rate-limiting.md +48 -0
  13. package/agents/examples/record-detail-layout.md +692 -0
  14. package/agents/examples/seed-migration.md +212 -0
  15. package/agents/examples/wizard-form.md +941 -0
  16. package/agents/examples/writeform-section.md +548 -0
  17. package/agents/features.md +19 -2
  18. package/agents/widgets.md +40 -39
  19. package/dist/api/api-constants.js +2 -2
  20. package/dist/api/auth-middleware.js +7 -7
  21. package/dist/api/auth-rate-limiters.js +7 -3
  22. package/dist/api/auth-routes-mfa.js +1 -1
  23. package/dist/api/auth-routes-session.js +1 -1
  24. package/dist/api/auth-routes-shared.js +7 -3
  25. package/dist/api/auth-routes-signup.js +1 -1
  26. package/dist/api/client-error-route.js +8 -2
  27. package/dist/api/client-ip.js +3 -1
  28. package/dist/api/extra-route.js +1 -1
  29. package/dist/api/index.d.ts +3 -1
  30. package/dist/api/index.js +3 -1
  31. package/dist/api/pii-leak-guard.js +4 -2
  32. package/dist/api/readiness.js +5 -5
  33. package/dist/api/redis-sse-broker.js +8 -6
  34. package/dist/api/request-context.d.ts +1 -1
  35. package/dist/api/request-context.js +1 -1
  36. package/dist/api/routes.js +2 -2
  37. package/dist/api/server-boot-checks.d.ts +10 -0
  38. package/dist/api/server-boot-checks.js +157 -0
  39. package/dist/api/server-event-dispatcher.d.ts +26 -0
  40. package/dist/api/server-event-dispatcher.js +208 -0
  41. package/dist/api/server-extra-routes.d.ts +11 -0
  42. package/dist/api/server-extra-routes.js +203 -0
  43. package/dist/api/server-middleware.d.ts +20 -0
  44. package/dist/api/server-middleware.js +211 -0
  45. package/dist/api/server-routes.d.ts +19 -0
  46. package/dist/api/server-routes.js +293 -0
  47. package/dist/api/server-websocket-routes.d.ts +4 -0
  48. package/dist/api/server-websocket-routes.js +108 -0
  49. package/dist/api/server-wiring.d.ts +32 -0
  50. package/dist/api/server-wiring.js +236 -0
  51. package/dist/api/server.d.ts +10 -15
  52. package/dist/api/server.js +48 -1250
  53. package/dist/api/session-cookies.js +4 -4
  54. package/dist/api/sse-broker.js +1 -1
  55. package/dist/api/sse-route.d.ts +13 -13
  56. package/dist/api/sse-route.js +31 -27
  57. package/dist/auth/index.js +4 -4
  58. package/dist/auth/roles.d.ts +4 -4
  59. package/dist/auth/roles.js +24 -24
  60. package/dist/compliance/duration-spec.js +18 -20
  61. package/dist/compliance/index.js +2 -3
  62. package/dist/compliance/override-schema.d.ts +4 -4
  63. package/dist/compliance/override-schema.js +19 -22
  64. package/dist/compliance/profiles.d.ts +26 -29
  65. package/dist/compliance/profiles.js +62 -69
  66. package/dist/compliance/sub-processors.d.ts +28 -30
  67. package/dist/compliance/sub-processors.js +17 -21
  68. package/dist/consumer-cli.js +4 -4
  69. package/dist/crypto/blind-index.js +18 -17
  70. package/dist/crypto/ciphertext-pattern.js +1 -1
  71. package/dist/crypto/event-pii.js +3 -3
  72. package/dist/crypto/kek-source.d.ts +1 -1
  73. package/dist/crypto/kek-source.js +3 -2
  74. package/dist/crypto/kms-adapter.d.ts +1 -1
  75. package/dist/crypto/kms-adapter.js +4 -6
  76. package/dist/crypto/kms-wiring.d.ts +1 -2
  77. package/dist/crypto/kms-wiring.js +2 -2
  78. package/dist/crypto/owner-transition-rekey.d.ts +5 -1
  79. package/dist/crypto/owner-transition-rekey.js +1 -1
  80. package/dist/crypto/pg-kms-adapter.js +2 -6
  81. package/dist/crypto/pii-field-encryption.d.ts +5 -1
  82. package/dist/crypto/pii-field-encryption.js +7 -5
  83. package/dist/crypto/subject-resolver.d.ts +1 -1
  84. package/dist/crypto/subject-resolver.js +2 -2
  85. package/dist/db/aggregate-sql.d.ts +21 -0
  86. package/dist/db/aggregate-sql.js +155 -0
  87. package/dist/db/apply-entity-event.d.ts +6 -6
  88. package/dist/db/apply-entity-event.js +55 -55
  89. package/dist/db/blind-index-cleanup.js +7 -7
  90. package/dist/db/closed-connection.d.ts +9 -0
  91. package/dist/db/closed-connection.js +95 -0
  92. package/dist/db/collect-table-metas.js +10 -11
  93. package/dist/db/compound-types.d.ts +1 -1
  94. package/dist/db/compound-types.js +12 -12
  95. package/dist/db/cursor.js +3 -4
  96. package/dist/db/dialect.d.ts +2 -2
  97. package/dist/db/dialect.js +3 -3
  98. package/dist/db/distinct-on.d.ts +1 -1
  99. package/dist/db/driver-row-coercion.d.ts +2 -0
  100. package/dist/db/driver-row-coercion.js +186 -0
  101. package/dist/db/driver-value-preparation.d.ts +16 -0
  102. package/dist/db/driver-value-preparation.js +99 -0
  103. package/dist/db/eagerload.d.ts +3 -3
  104. package/dist/db/eagerload.js +21 -12
  105. package/dist/db/entity-field-encryption.d.ts +1 -0
  106. package/dist/db/entity-field-encryption.js +1 -1
  107. package/dist/db/entity-table-meta.d.ts +2 -2
  108. package/dist/db/entity-table-meta.js +105 -112
  109. package/dist/db/event-store-executor-context.d.ts +3 -2
  110. package/dist/db/event-store-executor-context.js +13 -13
  111. package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
  112. package/dist/db/event-store-executor-read-aggregate.js +112 -0
  113. package/dist/db/event-store-executor-read-filter.d.ts +31 -0
  114. package/dist/db/event-store-executor-read-filter.js +203 -0
  115. package/dist/db/event-store-executor-read-paging.d.ts +14 -0
  116. package/dist/db/event-store-executor-read-paging.js +99 -0
  117. package/dist/db/event-store-executor-read-rows.d.ts +6 -0
  118. package/dist/db/event-store-executor-read-rows.js +32 -0
  119. package/dist/db/event-store-executor-read-search.d.ts +28 -0
  120. package/dist/db/event-store-executor-read-search.js +221 -0
  121. package/dist/db/event-store-executor-read-sort.d.ts +41 -0
  122. package/dist/db/event-store-executor-read-sort.js +130 -0
  123. package/dist/db/event-store-executor-read-where.d.ts +24 -0
  124. package/dist/db/event-store-executor-read-where.js +81 -0
  125. package/dist/db/event-store-executor-read.d.ts +2 -9
  126. package/dist/db/event-store-executor-read.js +50 -704
  127. package/dist/db/event-store-executor-write-steps.d.ts +20 -0
  128. package/dist/db/event-store-executor-write-steps.js +100 -0
  129. package/dist/db/event-store-executor-write.js +401 -510
  130. package/dist/db/index.d.ts +6 -2
  131. package/dist/db/index.js +5 -1
  132. package/dist/db/list-aggregate-labels.d.ts +14 -0
  133. package/dist/db/list-aggregate-labels.js +26 -0
  134. package/dist/db/located-timestamp.d.ts +11 -11
  135. package/dist/db/located-timestamp.js +29 -30
  136. package/dist/db/migrate-generator.d.ts +1 -1
  137. package/dist/db/migrate-generator.js +97 -105
  138. package/dist/db/migrate-runner.js +29 -26
  139. package/dist/db/money.d.ts +6 -6
  140. package/dist/db/money.js +29 -27
  141. package/dist/db/queries/backfill-pii.js +5 -5
  142. package/dist/db/queries/ddl.js +1 -1
  143. package/dist/db/queries/es-ops.d.ts +1 -1
  144. package/dist/db/queries/es-ops.js +1 -1
  145. package/dist/db/queries/event-consumer.js +3 -3
  146. package/dist/db/queries/event-store.js +4 -2
  147. package/dist/db/queries/projection-rebuild.js +3 -3
  148. package/dist/db/queries/raw-sql.d.ts +2 -2
  149. package/dist/db/queries/raw-sql.js +2 -2
  150. package/dist/db/queries/shadow-swap.js +19 -19
  151. package/dist/db/query.d.ts +3 -16
  152. package/dist/db/query.js +8 -761
  153. package/dist/db/rebuild-marker.js +20 -21
  154. package/dist/db/reference-data.js +6 -1
  155. package/dist/db/replay-migration-sql.js +2 -2
  156. package/dist/db/schema-inspection.js +4 -5
  157. package/dist/db/table-builder.d.ts +1 -2
  158. package/dist/db/table-builder.js +137 -190
  159. package/dist/db/table-info.d.ts +10 -0
  160. package/dist/db/table-info.js +45 -0
  161. package/dist/db/temporal-value-guards.d.ts +6 -0
  162. package/dist/db/temporal-value-guards.js +15 -0
  163. package/dist/db/tenant-db-runner.d.ts +1 -0
  164. package/dist/db/tenant-db-runner.js +3 -0
  165. package/dist/db/tenant-db.d.ts +2 -2
  166. package/dist/db/tenant-db.js +48 -11
  167. package/dist/db/where-clause.d.ts +6 -0
  168. package/dist/db/where-clause.js +236 -0
  169. package/dist/derivatives/delete-stored-file.js +6 -2
  170. package/dist/derivatives/derivatives-context.js +1 -1
  171. package/dist/engine/boot-validator/access-roles.js +4 -3
  172. package/dist/engine/boot-validator/api-ext.js +14 -14
  173. package/dist/engine/boot-validator/boot-check.js +1 -1
  174. package/dist/engine/boot-validator/config-deps.js +7 -6
  175. package/dist/engine/boot-validator/detail-screens.js +4 -4
  176. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  177. package/dist/engine/boot-validator/entity-handler.js +77 -61
  178. package/dist/engine/boot-validator/form-derivations.js +35 -1
  179. package/dist/engine/boot-validator/gdpr-storage.js +4 -4
  180. package/dist/engine/boot-validator/i18n-keys.js +6 -4
  181. package/dist/engine/boot-validator/index.d.ts +2 -2
  182. package/dist/engine/boot-validator/index.js +22 -20
  183. package/dist/engine/boot-validator/nav.js +5 -5
  184. package/dist/engine/boot-validator/ownership.js +12 -9
  185. package/dist/engine/boot-validator/parent-ref.js +1 -1
  186. package/dist/engine/boot-validator/pii-retention.js +258 -187
  187. package/dist/engine/boot-validator/projection-list-screens.js +7 -7
  188. package/dist/engine/boot-validator/query-refs.js +1 -1
  189. package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
  190. package/dist/engine/boot-validator/screens-actions.js +23 -17
  191. package/dist/engine/boot-validator/screens-dashboard.js +2 -5
  192. package/dist/engine/boot-validator/screens-forms.js +12 -17
  193. package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
  194. package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
  195. package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
  196. package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
  197. package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
  198. package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
  199. package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
  200. package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
  201. package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
  202. package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
  203. package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
  204. package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
  205. package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
  206. package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
  207. package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
  208. package/dist/engine/boot-validator/screens-lists.js +26 -24
  209. package/dist/engine/boot-validator/screens.d.ts +0 -2
  210. package/dist/engine/boot-validator/screens.js +102 -869
  211. package/dist/engine/boot-validator/security-baseline.js +3 -2
  212. package/dist/engine/boot-validator/transfer-graph.js +2 -2
  213. package/dist/engine/boot-validator/workspaces.js +3 -3
  214. package/dist/engine/bridge-stub.js +3 -4
  215. package/dist/engine/build-app-schema.js +130 -170
  216. package/dist/engine/build-config-feature-schema.js +50 -50
  217. package/dist/engine/build-target.js +20 -23
  218. package/dist/engine/codemod/pipeline-codemod.js +20 -13
  219. package/dist/engine/config-helpers.js +4 -4
  220. package/dist/engine/constants.js +2 -2
  221. package/dist/engine/create-app.js +2 -2
  222. package/dist/engine/define-feature.js +21 -13
  223. package/dist/engine/define-handler.js +13 -11
  224. package/dist/engine/define-step.js +1 -1
  225. package/dist/engine/define-workflow.d.ts +5 -5
  226. package/dist/engine/define-workflow.js +6 -6
  227. package/dist/engine/entity-handlers.d.ts +13 -0
  228. package/dist/engine/entity-handlers.js +22 -34
  229. package/dist/engine/entity-list-runtime-options.d.ts +8 -0
  230. package/dist/engine/entity-list-runtime-options.js +18 -0
  231. package/dist/engine/event-def.js +2 -2
  232. package/dist/engine/extension-names.d.ts +16 -17
  233. package/dist/engine/extension-names.js +37 -38
  234. package/dist/engine/extensions/escape-hatch-usage.js +1 -1
  235. package/dist/engine/extensions/tenant-data.js +2 -2
  236. package/dist/engine/extensions/user-data.d.ts +23 -24
  237. package/dist/engine/factories.d.ts +14 -13
  238. package/dist/engine/factories.js +38 -37
  239. package/dist/engine/feature-ast/entity-field-types.js +1 -1
  240. package/dist/engine/feature-ast/extractors/handlers.js +2 -1
  241. package/dist/engine/feature-ast/extractors/hooks.js +1 -1
  242. package/dist/engine/feature-ast/extractors/round2.js +25 -6
  243. package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
  244. package/dist/engine/feature-ast/extractors/shared.js +2 -3
  245. package/dist/engine/feature-ast/parse.js +13 -13
  246. package/dist/engine/feature-ast/patch.js +4 -182
  247. package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
  248. package/dist/engine/feature-ast/pattern-change-schema.js +2 -1
  249. package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
  250. package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
  251. package/dist/engine/feature-ast/patterns.d.ts +11 -2
  252. package/dist/engine/feature-ast/patterns.js +43 -51
  253. package/dist/engine/feature-ast/render.js +50 -89
  254. package/dist/engine/feature-config-events-jobs.js +15 -12
  255. package/dist/engine/feature-entity-handlers.js +12 -5
  256. package/dist/engine/feature-manifest.d.ts +9 -9
  257. package/dist/engine/feature-manifest.js +6 -7
  258. package/dist/engine/feature-ui-extensions.d.ts +3 -3
  259. package/dist/engine/feature-ui-extensions.js +30 -22
  260. package/dist/engine/field-access.js +5 -6
  261. package/dist/engine/form-derivations.js +18 -7
  262. package/dist/engine/handler-field-references.js +1 -1
  263. package/dist/engine/index.d.ts +1 -0
  264. package/dist/engine/index.js +4 -4
  265. package/dist/engine/object-form.d.ts +1 -0
  266. package/dist/engine/object-form.js +14 -4
  267. package/dist/engine/ownership.js +3 -3
  268. package/dist/engine/parse-ref-target.js +5 -7
  269. package/dist/engine/pattern-library/index.js +2 -2
  270. package/dist/engine/pattern-library/library.js +2 -2
  271. package/dist/engine/pattern-library/types.js +3 -3
  272. package/dist/engine/pipeline.js +2 -2
  273. package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
  274. package/dist/engine/project-app-schema-for-roles.js +7 -5
  275. package/dist/engine/projection-helpers.js +9 -9
  276. package/dist/engine/qualified-name.js +2 -2
  277. package/dist/engine/registry-facade.js +1 -1
  278. package/dist/engine/registry-ingest.js +13 -13
  279. package/dist/engine/registry-state.js +17 -19
  280. package/dist/engine/registry-validate.js +12 -12
  281. package/dist/engine/run-in.js +2 -2
  282. package/dist/engine/run-pipeline.js +7 -0
  283. package/dist/engine/schema-builder.js +251 -233
  284. package/dist/engine/screen-filter-ops.js +17 -20
  285. package/dist/engine/screen-helpers.js +4 -3
  286. package/dist/engine/screen-kind.d.ts +7 -0
  287. package/dist/engine/screen-kind.js +1 -0
  288. package/dist/engine/state-machine.d.ts +12 -12
  289. package/dist/engine/state-machine.js +7 -7
  290. package/dist/engine/steps/_resolver-utils.js +4 -3
  291. package/dist/engine/steps/for-each.js +4 -4
  292. package/dist/engine/steps/unsafe-projection-delete.js +1 -1
  293. package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
  294. package/dist/engine/steps/wait-for-event.js +4 -4
  295. package/dist/engine/steps/workflow-system-events.js +3 -3
  296. package/dist/engine/strip-disabled-features-from-app-schema.js +51 -14
  297. package/dist/engine/tier-resolver-extension.d.ts +29 -27
  298. package/dist/engine/tier-resolver-extension.js +26 -26
  299. package/dist/engine/transfer-adjacency.js +2 -2
  300. package/dist/engine/validate-projection-allowlist.js +9 -28
  301. package/dist/engine/walk-step-instances.d.ts +2 -0
  302. package/dist/engine/walk-step-instances.js +38 -0
  303. package/dist/engine/where-rule-lint.js +1 -1
  304. package/dist/engine/write-handler-qn-extract.js +1 -1
  305. package/dist/entrypoint/index.d.ts +4 -1
  306. package/dist/entrypoint/index.js +10 -4
  307. package/dist/env/index.d.ts +7 -6
  308. package/dist/env/index.js +10 -9
  309. package/dist/errors/docs-url.d.ts +1 -1
  310. package/dist/errors/docs-url.js +22 -2
  311. package/dist/errors/transition-details.js +7 -7
  312. package/dist/errors/write-error-info.d.ts +6 -6
  313. package/dist/errors/write-error-info.js +6 -6
  314. package/dist/es-ops/context.d.ts +9 -9
  315. package/dist/es-ops/context.js +29 -29
  316. package/dist/es-ops/index.js +7 -9
  317. package/dist/es-ops/operations-schema.js +4 -10
  318. package/dist/es-ops/runner.d.ts +11 -11
  319. package/dist/es-ops/runner.js +49 -53
  320. package/dist/es-ops/types.d.ts +55 -55
  321. package/dist/es-ops/types.js +11 -11
  322. package/dist/event-store/admin-api.js +7 -8
  323. package/dist/event-store/errors.js +1 -1
  324. package/dist/event-store/event-store.js +12 -6
  325. package/dist/event-store/rebuild-dead-letter.js +1 -1
  326. package/dist/files/bind-pending-file-refs.js +3 -3
  327. package/dist/files/create-file-ref-from-bytes.js +4 -1
  328. package/dist/files/feature.js +9 -10
  329. package/dist/files/file-handle.js +7 -9
  330. package/dist/files/file-ref-entity.js +15 -15
  331. package/dist/files/file-ref-table.js +7 -8
  332. package/dist/files/file-routes.d.ts +7 -0
  333. package/dist/files/file-routes.js +22 -13
  334. package/dist/files/in-memory-provider.js +8 -9
  335. package/dist/files/local-provider.js +18 -25
  336. package/dist/files/provider-resolver.d.ts +7 -7
  337. package/dist/files/resolve-content-type.js +43 -0
  338. package/dist/files/storage-tracking.js +2 -2
  339. package/dist/files/upload-ledger.js +7 -0
  340. package/dist/files/zip-stream.d.ts +12 -19
  341. package/dist/files/zip-stream.js +44 -63
  342. package/dist/i18n/required-surface-keys.d.ts +1 -1
  343. package/dist/i18n/required-surface-keys.js +188 -212
  344. package/dist/internal/stack-support.d.ts +2 -1
  345. package/dist/internal/stack-support.js +1 -1
  346. package/dist/jobs/job-runner-context.d.ts +32 -0
  347. package/dist/jobs/job-runner-context.js +305 -0
  348. package/dist/jobs/job-runner-execution.d.ts +7 -0
  349. package/dist/jobs/job-runner-execution.js +160 -0
  350. package/dist/jobs/job-runner-gates.d.ts +19 -0
  351. package/dist/jobs/job-runner-gates.js +198 -0
  352. package/dist/jobs/job-runner-helpers.d.ts +17 -3
  353. package/dist/jobs/job-runner-helpers.js +69 -4
  354. package/dist/jobs/job-runner-schedulers.d.ts +1 -1
  355. package/dist/jobs/job-runner-schedulers.js +3 -4
  356. package/dist/jobs/job-runner-scope.d.ts +44 -0
  357. package/dist/jobs/job-runner-scope.js +102 -0
  358. package/dist/jobs/job-runner-types.d.ts +12 -0
  359. package/dist/jobs/job-runner.d.ts +0 -1
  360. package/dist/jobs/job-runner.js +57 -669
  361. package/dist/lifecycle/lifecycle.js +3 -5
  362. package/dist/lifecycle/signal-handlers.js +5 -4
  363. package/dist/logging/utils.d.ts +2 -1
  364. package/dist/logging/utils.js +38 -10
  365. package/dist/migrations/index.js +1 -1
  366. package/dist/migrations/kumiko-drift.js +14 -13
  367. package/dist/migrations/pending-rebuilds.d.ts +21 -20
  368. package/dist/migrations/pending-rebuilds.js +27 -27
  369. package/dist/migrations/projection-table-index.d.ts +3 -3
  370. package/dist/migrations/projection-table-index.js +8 -8
  371. package/dist/observability/escape-hatch-report.d.ts +3 -0
  372. package/dist/observability/escape-hatch-report.js +12 -1
  373. package/dist/observability/metric-validator.js +1 -1
  374. package/dist/observability/metrics-handle.js +3 -3
  375. package/dist/pipeline/append-event-core.js +11 -2
  376. package/dist/pipeline/cascade-handler.d.ts +1 -1
  377. package/dist/pipeline/cascade-handler.js +165 -138
  378. package/dist/pipeline/dispatch-batch.js +2 -2
  379. package/dist/pipeline/dispatch-query.js +20 -13
  380. package/dist/pipeline/dispatch-shared.d.ts +14 -3
  381. package/dist/pipeline/dispatch-shared.js +67 -721
  382. package/dist/pipeline/dispatch-stream.js +1 -1
  383. package/dist/pipeline/dispatch-write.js +297 -190
  384. package/dist/pipeline/dispatcher-utils.d.ts +1 -1
  385. package/dist/pipeline/dispatcher-utils.js +1 -1
  386. package/dist/pipeline/event-consumer-state.js +2 -2
  387. package/dist/pipeline/event-dispatcher-admin.js +2 -1
  388. package/dist/pipeline/event-dispatcher-delivery.js +4 -6
  389. package/dist/pipeline/event-dispatcher.js +17 -28
  390. package/dist/pipeline/handler-context-auth.d.ts +9 -0
  391. package/dist/pipeline/handler-context-auth.js +54 -0
  392. package/dist/pipeline/handler-context-config.d.ts +14 -0
  393. package/dist/pipeline/handler-context-config.js +113 -0
  394. package/dist/pipeline/handler-context-db.d.ts +24 -0
  395. package/dist/pipeline/handler-context-db.js +149 -0
  396. package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
  397. package/dist/pipeline/handler-context-dispatch.js +89 -0
  398. package/dist/pipeline/handler-context-events.d.ts +43 -0
  399. package/dist/pipeline/handler-context-events.js +237 -0
  400. package/dist/pipeline/handler-context-notify.d.ts +12 -0
  401. package/dist/pipeline/handler-context-notify.js +55 -0
  402. package/dist/pipeline/handler-context-observability.d.ts +7 -0
  403. package/dist/pipeline/handler-context-observability.js +21 -0
  404. package/dist/pipeline/handler-context-scope.d.ts +48 -0
  405. package/dist/pipeline/handler-context-scope.js +88 -0
  406. package/dist/pipeline/irreversible-operation-gate.js +1 -1
  407. package/dist/pipeline/lifecycle-pipeline.js +9 -9
  408. package/dist/pipeline/member-read-only-transaction.js +1 -3
  409. package/dist/pipeline/msp-rebuild.js +1 -1
  410. package/dist/pipeline/notify-scope.js +12 -1
  411. package/dist/pipeline/pre-handler-gates.js +20 -4
  412. package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
  413. package/dist/pipeline/pre-transaction-reservations.js +9 -3
  414. package/dist/pipeline/projection-rebuild.js +15 -13
  415. package/dist/pipeline/system-hooks.js +39 -41
  416. package/dist/pipeline/system-identity-switch.js +1 -3
  417. package/dist/pipeline/tenant-timezone-cache.js +1 -1
  418. package/dist/random/generate.d.ts +29 -29
  419. package/dist/random/generate.js +30 -30
  420. package/dist/random/words.js +20 -20
  421. package/dist/rate-limit/middleware.js +9 -5
  422. package/dist/redis/client.js +2 -2
  423. package/dist/redis/pubsub-signal.js +14 -17
  424. package/dist/schema-cli/apply-command.d.ts +2 -0
  425. package/dist/schema-cli/apply-command.js +97 -0
  426. package/dist/schema-cli/baseline-command.d.ts +2 -0
  427. package/dist/schema-cli/baseline-command.js +33 -0
  428. package/dist/schema-cli/generate-command.d.ts +2 -0
  429. package/dist/schema-cli/generate-command.js +90 -0
  430. package/dist/schema-cli/shared.d.ts +39 -0
  431. package/dist/schema-cli/shared.js +95 -0
  432. package/dist/schema-cli/status-command.d.ts +2 -0
  433. package/dist/schema-cli/status-command.js +73 -0
  434. package/dist/schema-cli/validate-boot-input.d.ts +5 -0
  435. package/dist/schema-cli/validate-boot-input.js +130 -0
  436. package/dist/schema-cli/validate-command.d.ts +2 -0
  437. package/dist/schema-cli/validate-command.js +130 -0
  438. package/dist/schema-cli.d.ts +2 -18
  439. package/dist/schema-cli.js +47 -510
  440. package/dist/search/index.js +4 -4
  441. package/dist/search/meilisearch-adapter.js +2 -1
  442. package/dist/search/purge-subject.js +4 -5
  443. package/dist/search/reindex-entity.js +6 -9
  444. package/dist/time/boot-tz-warning.d.ts +2 -2
  445. package/dist/time/boot-tz-warning.js +8 -8
  446. package/dist/time/iana.js +10 -10
  447. package/dist/time/index.js +9 -9
  448. package/dist/time/legacy-date.d.ts +2 -2
  449. package/dist/time/legacy-date.js +6 -8
  450. package/dist/time/polyfill.js +1 -1
  451. package/dist/time/tz-context.d.ts +1 -1
  452. package/dist/time/tz-context.js +1 -1
  453. package/dist/ui-types/app-schema.js +7 -7
  454. package/dist/ui-types/list-row-meta.js +4 -4
  455. package/dist/upgrade-cli.js +159 -130
  456. package/dist/utils/compare.js +1 -1
  457. package/package.json +5 -5
  458. package/src/changes.json +358 -0
  459. package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
  460. package/src/scripts/codemod/feature-layout.ts +10 -2
  461. package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
  462. package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
  463. package/src/scripts/codemod/object-form.ts +41 -4
  464. package/src/scripts/codemod/package-merge.ts +58 -8
  465. package/src/scripts/codemod/pii-personal-migration.ts +192 -153
  466. package/src/scripts/codemod/testing-injection.ts +122 -42
  467. package/src/scripts/codemod/testing-move.ts +29 -7
  468. package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
@@ -6,16 +6,14 @@ import { access as accessPresets } from "../config-helpers.js";
6
6
  import { parseRefTarget } from "../parse-ref-target.js";
7
7
  import { isSelectOptionTone } from "./select-option-tone.js";
8
8
  export const FILE_FIELD_TYPES = new Set(["file", "image", "files", "images"]);
9
- // Field-Namen die typischerweise PII enthalten. Ohne `pii: true` /
10
- // `userOwned` / `tenantOwned` / `allowPlaintext`-Marker → Boot-Warning.
11
- // Lower-case compare für case-insensitive Match (displayName vs displayname).
9
+ // Field names that typically contain PII. Without a `pii: true` /
10
+ // `userOwned` / `tenantOwned` / `allowPlaintext` marker → boot warning.
11
+ // Compare in lower case for case-insensitive matching (displayName vs displayname).
12
12
  //
13
- // Bewusst NICHT in der Liste:
14
- // - `name` allein — zu viele Geschäfts-Kontexte (product.name,
15
- // tenant.name, role.name) sind kein PII. Personen-Namen werden
16
- // ueber displayName / firstName / lastName / fullName erfasst.
17
- //
18
- // Quelle: docs/plans/datenschutz/crypto-shredding.md Boot-Validation-Sektion.
13
+ // Deliberately NOT included:
14
+ // - `name` alone — too many business contexts (product.name,
15
+ // tenant.name, role.name) are not PII. Personal names are detected
16
+ // through displayName / firstName / lastName / fullName.
19
17
  export const PII_DIRECT_NAME_HINTS = new Set([
20
18
  "email",
21
19
  "phone",
@@ -42,8 +40,8 @@ export const PII_DIRECT_NAME_HINTS = new Set([
42
40
  "iban",
43
41
  "bic",
44
42
  ]);
45
- // Field-Namen die typischerweise User-Generated-Content enthalten —
46
- // User-Forget muss diese mit Author-Subject-Key encrypten.
43
+ // Field names that typically contain user-generated content —
44
+ // User-Forget must encrypt these with the author subject key.
47
45
  export const PII_USER_OWNED_NAME_HINTS = new Set([
48
46
  "body",
49
47
  "text",
@@ -141,6 +139,7 @@ export function validateHandlerAccess(feature) {
141
139
  for (const [name, handler] of Object.entries(feature.writeHandlers)) {
142
140
  validateTenantlessAnonymous(feature.name, name, handler);
143
141
  validateAdditionalRateLimits(feature.name, name, handler);
142
+ validateRateLimitCost(feature.name, name, handler);
144
143
  validatePerCommandTransaction(feature.name, name, handler);
145
144
  }
146
145
  }
@@ -237,6 +236,22 @@ export function validateTenantlessAnonymous(featureName, handlerName, handler) {
237
236
  `tenant/user bucket, so one caller could exhaust it for all. Use ${IP_SCOPED_RATE_LIMIT_PER.map((allowed) => `"${allowed}"`).join(" or ")}.`);
238
237
  }
239
238
  }
239
+ export function validateRateLimitCost(featureName, handlerName, handler) {
240
+ // skip: no payload-derived cost declared
241
+ if (!handler.rateLimitCost)
242
+ return;
243
+ if (handler.rateLimit === undefined || isRateLimitDisabled(handler.rateLimit)) {
244
+ throw new Error(`write handler "${featureName}:write:${handlerName}" declares rateLimitCost without an active rateLimit — ` +
245
+ `the cost is booked on the rateLimit buckets, so declare rateLimit or drop rateLimitCost.`);
246
+ }
247
+ const isAnonymous = "roles" in handler.access && handler.access.roles.includes("anonymous");
248
+ const hasIpBucket = declaredRateLimitOptions(handler.rateLimit).some(({ per }) => IP_SCOPED_RATE_LIMIT_PER.includes(per));
249
+ if (isAnonymous || hasIpBucket) {
250
+ throw new Error(`write handler "${featureName}:write:${handlerName}" declares rateLimitCost on an anonymous or ip-limited handler — ` +
251
+ `rateLimitCost books after the parse, so such a handler would lose its pre-parse protection against junk floods. ` +
252
+ `Bulk handlers are authenticated: use a user/tenant bucket and drop "anonymous", or drop rateLimitCost.`);
253
+ }
254
+ }
240
255
  export function validateRateLimitDisabledReason(featureName, kind, handlerName, rateLimit) {
241
256
  // skip: rate limit isn't disabled, so no reason is required
242
257
  if (!isRateLimitDisabled(rateLimit))
@@ -305,18 +320,18 @@ export function validateAnonymousRateLimit(featureName, kind, handlerName, acces
305
320
  `Use rateLimit.per="ip" or "ip+handler" for anonymous endpoints.`);
306
321
  }
307
322
  }
308
- // --- MultiStreamProjection delivery-invariant ---
323
+ // --- MultiStreamProjection delivery invariant ---
309
324
  //
310
- // `delivery: "per-instance"` mit einer `table` ist eine semantische Falle:
311
- // N Dispatcher-Instanzen würden parallel die gleichen INSERT/UPDATE-Zeilen
312
- // schreiben (Race / Duplicates), und ein Rebuild würde nur eine Zeile in
313
- // kumiko_event_consumers anfassen (die SHARED_INSTANCE_SENTINEL-Zeile),
314
- // während Live-Cursor in per-instance-Zeilen liegen → Cursor-Divergenz.
325
+ // `delivery: "per-instance"` with a `table` is a semantic trap:
326
+ // N dispatcher instances would write the same INSERT/UPDATE rows in parallel
327
+ // (races / duplicates), and a rebuild would affect only one row in
328
+ // kumiko_event_consumers (the SHARED_INSTANCE_SENTINEL row),
329
+ // while live cursors reside in per-instance rows → cursor divergence.
315
330
  //
316
- // Die Invariante ist: per-instance-Consumer sind rein side-effect (SSE,
317
- // in-memory cache invalidation). Wer eine Tabelle materialisiert, braucht
318
- // shared delivery — das ist exactly-once globally und gibt dem Rebuild
319
- // einen einzigen Cursor zum zurücksetzen.
331
+ // The invariant is: per-instance consumers are side-effect-only (SSE,
332
+ // in-memory cache invalidation). Materializing a table requires
333
+ // shared delivery — it is exactly-once globally and gives the rebuild
334
+ // a single cursor to reset.
320
335
  export function validateMultiStreamProjections(feature) {
321
336
  for (const [name, msp] of Object.entries(feature.multiStreamProjections)) {
322
337
  if (msp.delivery === "per-instance" && msp.table !== undefined) {
@@ -326,18 +341,19 @@ export function validateMultiStreamProjections(feature) {
326
341
  }
327
342
  }
328
343
  }
329
- // --- Located-Timestamp validation ---
344
+ // --- Located-timestamp validation ---
330
345
  //
331
- // Wenn ein Feld `type: "timestamp"` einen `locatedBy`-Marker trägt, muss das
332
- // referenzierte Feld in derselben Entity existieren UND vom Typ `tz` sein.
333
- // Sonst weiß weder DB-Wrapper noch JSON-Serializer welche TZ zur Wall-Clock
334
- // gehört → silent data loss bei Reads in anderer Server-TZ.
346
+ // When a field with `type: "timestamp"` has a `locatedBy` marker, the
347
+ // referenced field must exist in the same entity AND have type `tz`.
348
+ // Otherwise neither the DB wrapper nor the JSON serializer knows which TZ
349
+ // belongs to the wall clock → silent data loss when reading in another
350
+ // server TZ.
335
351
  //
336
- // Die häufigste Quelle von Konflikten ist Hand-Konstruktion:
352
+ // Manual construction is the most common source of conflicts:
337
353
  // { foo: { type: "timestamp", locatedBy: "fooTz" } }
338
- // ohne das `fooTz`-Feld zu deklarieren. `createLocatedTimestampField()`
339
- // erzeugt stattdessen EIN Feld vom Typ "locatedTimestamp" — wer den nutzt,
340
- // fliegt nicht durch diesen Validator.
354
+ // without declaring the `fooTz` field. `createLocatedTimestampField()`
355
+ // instead creates ONE field of type "locatedTimestamp" — using it avoids
356
+ // this validator.
341
357
  export function validateLocatedTimestamps(feature) {
342
358
  for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
343
359
  const fields = entity.fields;
@@ -359,18 +375,18 @@ export function validateLocatedTimestamps(feature) {
359
375
  }
360
376
  }
361
377
  }
362
- // --- Entity-Index validation ---
378
+ // --- Entity-index validation ---
363
379
  //
364
- // entity.indexes deklariert Composite-/Unique-Indices über mehrere Feld-
365
- // Spalten. Häufige Fehler: Tippfehler im Feld-Namen, leere column-Liste,
366
- // Index auf einem Field das die DB-Spalte gar nicht existiert (file/image
367
- // in der multi-Variante). Catched at boot, lange bevor drizzle-kit beim
368
- // generate-Run zickt.
380
+ // entity.indexes declares composite/unique indexes over multiple field
381
+ // columns. Common errors include misspelled field names, an empty column
382
+ // list, and an index on a field for which the DB has no column (file/image
383
+ // in the multi-value variant). Catch these at boot, long before drizzle-kit
384
+ // fails during the generate run.
369
385
  //
370
- // `tenantId` als einzige Spalte ist redundant — buildEntityTable legt
371
- // den Index sowieso automatisch an. Wir lassen die Composite-Form erlaubt
372
- // (`["tenantId", "key"]` ist sinnvoll), nur die rein-tenantId-Single-
373
- // column-Form blockieren wir.
386
+ // A `tenantId`-only index is redundant because buildEntityTable creates
387
+ // that index automatically. Composite forms remain allowed
388
+ // (`["tenantId", "key"]` is useful); only the tenantId-only single-column
389
+ // form is rejected.
374
390
  export function validateEntityIndexes(feature) {
375
391
  for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
376
392
  if (!entity.indexes)
@@ -397,21 +413,21 @@ export function validateEntityIndexes(feature) {
397
413
  `a jsonb array cannot be indexed as a column. Use a single-value field or remove from the index.`);
398
414
  }
399
415
  if (field && field.type === "longText") {
400
- // longText ist semantisch "potentially-megabytes content" — ein
401
- // BTREE-Index auf einer 1-MB-Spalte ist Performance-Disaster
402
- // (PG würde in TOAST-pages dereferenzieren müssen für jeden
403
- // Index-Lookup). Konsistent mit der type-level-decision dass
404
- // longText kein sortable/searchable/filterable hat. Wer
405
- // wirklich indexieren will, nimmt `text` mit den
406
- // entsprechenden Skalierungs-Trade-offs.
416
+ // longText semantically means "potentially-megabytes content" — a
417
+ // BTREE index on a 1-MB column is a performance disaster
418
+ // (PG would need to dereference TOAST pages for every
419
+ // index lookup). This is consistent with the type-level decision
420
+ // that longText has no sortable/searchable/filterable flags. Anyone
421
+ // who truly needs indexing should use `text` with the
422
+ // corresponding scaling trade-offs.
407
423
  throw new Error(`${where}: column "${col}" is a longText field — these cannot be indexed. ` +
408
424
  `Use \`text\` if you need indexing, or rely on the SearchAdapter (Meilisearch) for full-text search on long content.`);
409
425
  }
410
426
  }
411
- // UNIQUE-constraint auf tenantId ist semantisch (1:1 tenant→entity)
412
- // und NICHT redundant — buildEntityTable's auto-Index ist nur ein
413
- // Performance-Hint, kein constraint. Nur die rein-tenantId-Single-
414
- // column-non-unique-Form blockieren.
427
+ // A UNIQUE constraint on tenantId is semantically meaningful (1:1 tenant→entity)
428
+ // and NOT redundant — buildEntityTable's automatic index is only a
429
+ // performance hint, not a constraint. Only the tenantId-only,
430
+ // single-column, non-unique form is rejected.
415
431
  if (def.columns.length === 1 && def.columns[0] === "tenantId" && !def.unique) {
416
432
  throw new Error(`${where}: single-column index on "tenantId" is redundant — ` +
417
433
  `buildEntityTable always creates one automatically. Remove this entry.`);
@@ -436,9 +452,9 @@ export function validateEncryptedFields(feature) {
436
452
  let found = false;
437
453
  for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
438
454
  for (const [fieldName, field] of Object.entries(entity.fields)) {
439
- // Beide string-typed fields können encrypted sein. Die
440
- // searchable/sortable-Konflikt-Checks gelten nur für `text`
441
- // (longText hat diese flags type-level nicht).
455
+ // Both string-typed fields can be encrypted. The
456
+ // searchable/sortable conflict checks apply only to `text`
457
+ // (`longText` does not have these flags at the type level).
442
458
  if (field.type !== "text" && field.type !== "longText")
443
459
  continue;
444
460
  if (!field.encrypted)
@@ -491,7 +507,7 @@ const NUMERIC_EMBEDDED_SUB_TYPES = new Set(["number", "money", "decimal"]);
491
507
  function isValidEmbeddedDecimalScale(scale) {
492
508
  return Number.isInteger(scale) && scale >= 0 && scale <= 15;
493
509
  }
494
- // Tier 2.7e-3 + Cross-Feature: ReferenceFieldDef validation, shared by
510
+ // ReferenceFieldDef validation, shared by
495
511
  // top-level reference fields and reference sub-fields of an embedded field
496
512
  // (only the field-path in error messages differs, e.g. "accountId" vs
497
513
  // "lines.accountId", so a failure is locatable either way).
@@ -552,7 +568,7 @@ function validateReferenceTarget(entityName, fieldPath, refString, labelField, o
552
568
  // (use-reference-lookup).
553
569
  validateReferenceOptionsQuery(entityName, fieldPath, optionsQuery, feature, queryHandlers);
554
570
  }
555
- // fw#2780: the picker may source its options from a query handler instead of
571
+ // The picker may source its options from a query handler instead of
556
572
  // the referenced entity's table. A typo'd QN would only surface as a 404 on
557
573
  // first Combobox open, so it is pinned at boot — same treatment as
558
574
  // `DashboardFilterDefinition.optionsQuery` (boot-validator/query-refs.ts).
@@ -571,7 +587,7 @@ function validateReferenceOptionsQuery(entityName, fieldPath, optionsQuery, feat
571
587
  `is declared via r.queryHandler(...).`);
572
588
  }
573
589
  }
574
- // Tier 2.7e-3 + Cross-Feature: ReferenceFieldDef validation for top-level
590
+ // ReferenceFieldDef validation for top-level
575
591
  // reference fields (self-reference, entity → entity, is allowed). The actual
576
592
  // checks run in validateReferenceTarget — shared with the reference
577
593
  // sub-fields of an embedded field (validateEmbeddedFields).
@@ -705,10 +721,10 @@ function validateEmbeddedListMetadata(fieldName, entityName, field, entity) {
705
721
  }
706
722
  // --- MultiSelect field validation ---
707
723
  //
708
- // options muss non-empty sein (sonst wäre das Feld nicht benutzbar) und
709
- // default — wenn gesetzt — ist eine Teilmenge der options. Beides würde
710
- // auch im Zod-Schema bei runtime fehlschlagen, der Boot-Catch ist nur
711
- // die früheste Stelle für klare Fehlermeldungen.
724
+ // options must be non-empty (otherwise the field cannot be used), and
725
+ // default — when set — must be a subset of options. Both conditions would
726
+ // also fail at runtime in the Zod schema; boot validation only provides
727
+ // the earliest clear error messages.
712
728
  // Exhaustive over MultiSelectFieldDef["columns"] (the compiler flags a union
713
729
  // change); the TS union is gone at runtime for JS/JSON-sourced definitions.
714
730
  const CHECKBOX_COLUMN_COUNTS = {
@@ -3,15 +3,46 @@ import { parseQn } from "../qualified-name.js";
3
3
  import { formDerivationsOfSchema } from "../with-form-derivations.js";
4
4
  const NUMERIC_FORM_FIELD_TYPES = ["money", "number", "decimal"];
5
5
  const NUMERIC_SUB_FIELD_TYPES = ["money", "number", "decimal"];
6
+ function isMoneyType(type) {
7
+ return type === "money";
8
+ }
9
+ // Money operands are computed in minor units, number/decimal in major units: mixing them
10
+ // silently scales the result by 100 or 10000.
11
+ function validateUnitCompatibility({ prefix, screen, target }, derivation) {
12
+ const targetIsMoney = isMoneyType(screen.fields[target]?.type);
13
+ const moneyFlags = derivation.from.map((source) => isMoneyType(screen.fields[source]?.type));
14
+ const moneyCount = moneyFlags.filter(Boolean).length;
15
+ if (derivation.op === "sum" || derivation.op === "subtract") {
16
+ if (moneyCount !== 0 && moneyCount !== moneyFlags.length) {
17
+ throw new Error(`${prefix}.${target}: "${derivation.op}" sources must be all money or all number/decimal.`);
18
+ }
19
+ if (moneyCount > 0 !== targetIsMoney) {
20
+ throw new Error(`${prefix}.${target}: a "${derivation.op}" over ${moneyCount > 0 ? "money" : "number/decimal"} sources needs a ${moneyCount > 0 ? "money" : "number/decimal"} target.`);
21
+ }
22
+ // skip: sum/subtract fully validated above.
23
+ return;
24
+ }
25
+ if (moneyCount > 1 || (derivation.op === "percent" && moneyFlags[1] === true)) {
26
+ throw new Error(`${prefix}.${target}: "${derivation.op}" takes at most one money source${derivation.op === "percent" ? " (the base, not the rate)" : ""}.`);
27
+ }
28
+ if ((moneyCount === 1) !== targetIsMoney) {
29
+ throw new Error(`${prefix}.${target}: the target must be money exactly when a "${derivation.op}" source is money.`);
30
+ }
31
+ }
6
32
  function validateFromList({ prefix, screen, target }, derivation) {
7
33
  if (derivation.op === "percent" && derivation.from.length !== 2) {
8
34
  throw new Error(`${prefix}.${target}: "percent" needs exactly two sources (base, rate).`);
9
35
  }
10
36
  for (const source of derivation.from) {
11
- if (!(source in screen.fields)) {
37
+ const sourceField = screen.fields[source];
38
+ if (sourceField === undefined) {
12
39
  throw new Error(`${prefix}.${target}: unknown source field "${source}". Name a field of this screen.`);
13
40
  }
41
+ if (!NUMERIC_FORM_FIELD_TYPES.includes(sourceField.type)) {
42
+ throw new Error(`${prefix}.${target}: source "${source}" must be money, number or decimal, not "${sourceField.type}".`);
43
+ }
14
44
  }
45
+ validateUnitCompatibility({ prefix, screen, target }, derivation);
15
46
  }
16
47
  function validateListSum(check, derivation) {
17
48
  const { prefix, screen, target } = check;
@@ -26,6 +57,9 @@ function validateListSum(check, derivation) {
26
57
  if (cellType === undefined || !NUMERIC_SUB_FIELD_TYPES.includes(cellType)) {
27
58
  throw new Error(`${prefix}.${target}: "${derivation.cell}" must be a money, number or decimal sub-field of "${derivation.list}".`);
28
59
  }
60
+ if (isMoneyType(cellType) !== isMoneyType(screen.fields[target]?.type)) {
61
+ throw new Error(`${prefix}.${target}: a listSum of a ${isMoneyType(cellType) ? "money" : "number/decimal"} cell needs a ${isMoneyType(cellType) ? "money" : "number/decimal"} target.`);
62
+ }
29
63
  const { whenEmpty } = derivation;
30
64
  // skip: whenEmpty is optional
31
65
  if (whenEmpty === undefined)
@@ -1,3 +1,5 @@
1
+ import { createFallbackLogger } from "../../logging/utils.js";
2
+ const log = createFallbackLogger("kumiko:boot");
1
3
  // Providers whose bytes do not survive a process restart. Only "inmemory"
2
4
  // today; extend if another ephemeral bundled provider lands.
3
5
  const EPHEMERAL_PROVIDERS = new Set(["inmemory"]);
@@ -30,8 +32,7 @@ export function validateGdprStoragePersistence(features) {
30
32
  }
31
33
  const persistent = [...registeredProviders].filter((p) => !EPHEMERAL_PROVIDERS.has(p));
32
34
  if (persistent.length === 0) {
33
- // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
34
- console.warn("[kumiko:boot] user-data-rights is mounted but no persistent file provider is — GDPR exports use an ephemeral/in-memory store and are LOST on restart (the download then 500s). Mount createFileProviderS3Feature() or createFileProviderS3Feature({ fromEnv: true }) and select it via the file-foundation provider config.");
35
+ log.warn("user-data-rights is mounted but no persistent file provider is — GDPR exports use an ephemeral/in-memory store and are LOST on restart (the download then 500s). Mount createFileProviderS3Feature() or createFileProviderS3Feature({ fromEnv: true }) and select it via the file-foundation provider config.");
35
36
  // skip: ephemeral store already warned; the s3-env env-var check below is moot
36
37
  return;
37
38
  }
@@ -41,8 +42,7 @@ export function validateGdprStoragePersistence(features) {
41
42
  if (persistent.length === 1 && persistent[0] === "s3-env") {
42
43
  const missing = S3_ENV_REQUIRED.filter((v) => !process.env[v]);
43
44
  if (missing.length > 0) {
44
- // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
45
- console.warn(`[kumiko:boot] createFileProviderS3Feature({ fromEnv: true }) (provider "s3-env") is the GDPR file store but these env vars are unset: ${missing.join(", ")}. The provider throws on the first export (inside the cron), not here — set them so GDPR storage works.`);
45
+ log.warn(`createFileProviderS3Feature({ fromEnv: true }) (provider "s3-env") is the GDPR file store but these env vars are unset: ${missing.join(", ")}. The provider throws on the first export (inside the cron), not here — set them so GDPR storage works.`);
46
46
  }
47
47
  }
48
48
  }
@@ -11,7 +11,7 @@ function requiredKeysFromGeneratedConfigHub(schema) {
11
11
  const out = new Set();
12
12
  // The generator's section titles + mask-title field-label overrides are
13
13
  // dot-form i18n references (`${feature}.settings`), never literal display
14
- // text, so treatDotFormAsKey bypasses isI18nKey's colon-only check (fw#2260).
14
+ // text, so treatDotFormAsKey bypasses isI18nKey's colon-only check.
15
15
  for (const screen of schema.screens) {
16
16
  for (const key of requiredKeysFromScreen(SETTINGS_HUB_FEATURE, screen, {
17
17
  treatDotFormAsKey: true,
@@ -94,9 +94,11 @@ export function validateTranslationLocaleConsistency(features) {
94
94
  }
95
95
  }
96
96
  export function validateRequiredLocales(features, requiredLocales) {
97
- const gaps = findRequiredLocaleGaps(features, requiredLocales ?? []);
98
- if (gaps.length > 0) {
97
+ const gaps = requiredLocales === undefined || requiredLocales.length === 0
98
+ ? []
99
+ : findRequiredLocaleGaps(features, requiredLocales);
100
+ if (requiredLocales !== undefined && gaps.length > 0) {
99
101
  const lines = gaps.map((gap) => `[i18n] Feature "${gap.featureName}": translation key "${gap.key}" is missing required locale(s) ${gap.missingLocales.join(", ")}.`);
100
- throw new Error(`${lines.join("\n")}\nThe app requires locales [${(requiredLocales ?? []).join(", ")}] for every translation key; load the matching locale package or add the missing translations.`);
102
+ throw new Error(`${lines.join("\n")}\nThe app requires locales [${requiredLocales.join(", ")}] for every translation key; load the matching locale package or add the missing translations.`);
101
103
  }
102
104
  }
@@ -9,14 +9,14 @@ export type ValidateBootOptions = {
9
9
  /** Warn when an access role is used by exactly one handler/config-key/
10
10
  * field across the whole boot scan — often a typo, but also the normal
11
11
  * shape of a legitimate fine-grained role (one role scoped to one admin
12
- * endpoint on purpose). Opt-in (default false, #1711): a default-on
12
+ * endpoint on purpose). Opt-in (default false): a default-on
13
13
  * prod warning that nobody can silence per-role isn't worth the noise
14
14
  * it generates on every boot. */
15
15
  readonly warnOnUniqueAccessRoles?: boolean;
16
16
  /** QNs the app's sidebar allowlist admits. When set, every declared nav
17
17
  * whose screen isn't reachable from any allowlisted nav entry gets a
18
18
  * boot-time warning — that screen would otherwise be silently
19
- * unreachable via the sidebar (fw#3019). When omitted and the app has
19
+ * unreachable via the sidebar. When omitted and the app has
20
20
  * workspaces, the allowlist is derived from `r.workspace({ nav })` and
21
21
  * `r.nav({ workspaces })` assignments instead. */
22
22
  readonly navAllowlist?: ReadonlySet<string>;
@@ -2,6 +2,7 @@ import { validateEntityFieldEncryptionAvailable } from "../../db/entity-field-en
2
2
  import { dedupeFeatures } from "../dedupe-features.js";
3
3
  import { FILE_STORAGE_PROVIDER_ENV } from "../extension-names.js";
4
4
  import { QnTypes, qualifyEntityName } from "../qualified-name.js";
5
+ import { validateProjectionAllowlist } from "../validate-projection-allowlist.js";
5
6
  import { validateAccessDeclarations } from "./access-declarations.js";
6
7
  import { warnOnUniqueAccessRoles } from "./access-roles.js";
7
8
  import { validateActionWiring, validateFieldWiring } from "./action-wiring.js";
@@ -30,9 +31,9 @@ import { validateExtensionSelectorPanels } from "./selector-owner-panels.js";
30
31
  import { validateTransferGraph } from "./transfer-graph.js";
31
32
  import { validateSubmitPrefilledHiddenFields } from "./url-prefill-fields.js";
32
33
  import { collectWorkspaceQns, resolveNavAllowlist, validateDefaultWorkspaceUniqueness, validateWorkspaces, } from "./workspaces.js";
33
- // Re-export: wird von run-dev-app.ts benötigt um Write-Handler-QNs
34
- // an den Codegen zu übergeben. Nicht Teil von validateBoot, aber
35
- // dieselbe Extraktionslogik.
34
+ // Re-export: run-dev-app.ts needs this to pass write-handler QNs
35
+ // to codegen. It is not part of validateBoot, but uses the same
36
+ // extraction logic.
36
37
  export { collectCardMetaOverflow } from "./card-meta-overflow.js";
37
38
  export { validateAppCustomScreenWriteQns } from "./custom-screen-write-qns.js";
38
39
  export { collectWriteHandlerQns } from "./nav.js";
@@ -101,13 +102,13 @@ export function validateBoot(rawFeatures, options) {
101
102
  }
102
103
  // Collect all config keys across features (for cross-feature reference validation)
103
104
  const allConfigKeys = new Set();
104
- // Qualified config-key set für ConfigEditScreen-Validation. MUSS via
105
- // qualifyEntityName kanonisiert werden (toKebab auf Feature + Key) — exakt
106
- // wie define-feature/registry den QN bildet. Der rohe f.configKeys-Key ist
107
- // der camelCase-Objekt-Key (`brandingTitle`), die echte QN aber
108
- // `…:config:branding-title`; ohne toKebab failt jeder configEdit-Screen mit
109
- // multi-word Config-Key fälschlich. allConfigKeys oben nutzt das ältere
110
- // `feature.short`-Format für validateConfigReads.
105
+ // Qualified config-key set for ConfigEditScreen validation. MUST be
106
+ // canonicalized via qualifyEntityName (toKebab on feature + key), exactly
107
+ // as define-feature/registry builds the QN. The raw f.configKeys key is
108
+ // the camelCase object key (`brandingTitle`), but the actual QN is
109
+ // `…:config:branding-title`; without toKebab, every configEdit screen
110
+ // with a multi-word config key would fail incorrectly. allConfigKeys above
111
+ // uses the older `feature.short` format for validateConfigReads.
111
112
  const configKeyDefsByQn = new Map();
112
113
  for (const f of features) {
113
114
  for (const [key, def] of Object.entries(f.configKeys)) {
@@ -138,15 +139,15 @@ export function validateBoot(rawFeatures, options) {
138
139
  const screensByShortId = collectScreensByShortId(features);
139
140
  validateScreenShortIdCollisions(screensByShortId);
140
141
  // Cross-feature entity → detailFor-screen map — built (+ uniqueness-
141
- // validated) up front so per-feature rowAction entity-targets (fw#2228)
142
+ // validated) up front so per-feature rowAction entity-targets
142
143
  // resolve against it inside the loop below, instead of at the very end
143
144
  // where this used to run.
144
145
  const detailForScreens = collectDetailForScreens(features, featureMap);
145
- // Cross-feature API exposure-map — jedes Feature deklariert Marker via
146
- // r.exposesApi(name). Per-feature validateApiExposureMatching walkt
147
- // usedApis-Set und checkt dass jeder Eintrag hier einen Match findet.
148
- // Verhindert dass typo-getroffene oder gedroppte QN-Aufrufe zu
149
- // Runtime-Crash statt Boot-Fail werden.
146
+ // Cross-feature API exposure map — each feature declares markers via
147
+ // r.exposesApi(name). Per-feature validateApiExposureMatching walks the
148
+ // usedApis set and checks that every entry has a match here.
149
+ // This prevents typoed or removed QN calls from causing a runtime crash
150
+ // instead of a boot failure.
150
151
  const allExposedApis = new Map(); // apiName → providerFeature
151
152
  for (const f of features) {
152
153
  for (const apiName of f.exposedApis) {
@@ -200,10 +201,10 @@ export function validateBoot(rawFeatures, options) {
200
201
  validateParentRefs(feature, featureMap);
201
202
  validateTransferGraph(feature, featureMap);
202
203
  validateMultiStreamProjections(feature);
203
- // Vor validateScreens: dessen visible/entityId-Feldref-Checks werfen für
204
- // einen Function-Wert bereits (mit verwirrender "unknown field undefined"-
205
- // Message, weil `typeof fn !== "boolean"` true ist), bevor der klare
206
- // Function-Check hier überhaupt liefe.
204
+ // Before validateScreens: its visible/entityId field-reference checks throw
205
+ // for a Function value first, with the confusing "unknown field undefined"
206
+ // message (because `typeof fn !== "boolean"` is true), before the clear
207
+ // Function check here can run.
207
208
  validateActionWiring(feature);
208
209
  validateFieldWiring(feature);
209
210
  validateScreens(feature, featureMap, allWriteHandlerQns, allScreenQns, configKeyDefsByQn, screensByShortId, detailForScreens);
@@ -238,6 +239,7 @@ export function validateBoot(rawFeatures, options) {
238
239
  validateGdprStoragePersistence(features);
239
240
  validateGlobalTenancyEntities(features);
240
241
  validateFeatureBootChecks(features);
242
+ validateProjectionAllowlist(features);
241
243
  if (hasEncryptedFields) {
242
244
  // Availability check, not env-presence: eagerly building the keyring
243
245
  // catches malformed keys (wrong length, bad base64) at boot instead of
@@ -1,8 +1,10 @@
1
1
  // Nav validation + collectKnownRoles (role synthesis for ownership
2
2
  // validation in index.ts — thematically adjacent to ownership.ts, kept
3
3
  // here since this is a structural move, not a semantic reorg).
4
+ import { createFallbackLogger } from "../../logging/utils.js";
4
5
  import { qualifyEntityName } from "../qualified-name.js";
5
6
  import { isOpenToAllGranted } from "../types/index.js";
7
+ const log = createFallbackLogger("kumiko:boot");
6
8
  export function collectWriteHandlerQns(features) {
7
9
  const set = new Set();
8
10
  for (const f of features) {
@@ -110,7 +112,7 @@ function navViewerRoles(access) {
110
112
  return isOpenToAllGranted(access) ? undefined : [];
111
113
  return access.roles;
112
114
  }
113
- // fw#2640: warn (never throw — this is a real UX bug, not a broken ref)
115
+ // Warn (never throw — this is a real UX bug, not a broken ref)
114
116
  // when a nav leaf's role gate is disjoint from its parent section's — no
115
117
  // user who can see the section could ever see this child, so the section
116
118
  // renders with zero visible children for every user who reaches it.
@@ -133,8 +135,7 @@ export function warnOnNavAccessInversion(allNavQns) {
133
135
  continue; // parent already visible to nobody — a different problem
134
136
  if (leafRoles.some((role) => parentRoles.includes(role)))
135
137
  continue;
136
- // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
137
- console.warn(`[kumiko:boot] Nav entry "${qn}" requires roles [${leafRoles.join(", ")}] but its parent ` +
138
+ log.warn(`Nav entry "${qn}" requires roles [${leafRoles.join(", ")}] but its parent ` +
138
139
  `"${navDef.parent}" only allows [${parentRoles.join(", ")}] — no user who can see the ` +
139
140
  `parent section could ever see this entry. If this is intentional, ignore this warning; ` +
140
141
  `otherwise align the role sets.`);
@@ -158,8 +159,7 @@ export function warnOnUnreachableNavScreens(allNavQns, allowedNavQns, navAllowli
158
159
  const remedy = allowlistSource === "workspaces"
159
160
  ? "add it to a workspace's `nav` array or set `workspaces` on the nav"
160
161
  : "add it to the allowlist";
161
- // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
162
- console.warn(`[kumiko:boot] Nav entry "${qn}" declared by feature "${navDef.featureName}" points at ` +
162
+ log.warn(`Nav entry "${qn}" declared by feature "${navDef.featureName}" points at ` +
163
163
  `screen "${screenQn}", which no allowlisted nav entry reaches — it is unreachable via ` +
164
164
  `the sidebar. If this is intentional (e.g. reachable through a generated hub), add ` +
165
165
  `"${qn}" to navAllowlistExempt; otherwise ${remedy}.`);
@@ -3,23 +3,23 @@ import { tableNameOf, } from "../ownership.js";
3
3
  import { SYSTEM_USER_ID } from "../system-user.js";
4
4
  import { SYSTEM_TENANT_ID } from "../types/identifiers.js";
5
5
  import { assertQualifiedWhereFragment, tableColumnSqlNames } from "../where-rule-lint.js";
6
- // --- Ownership rule validation (H.2) ---
6
+ // --- Ownership rule validation ---
7
7
  //
8
8
  // Walks every entity.access and every field.access map, resolves each
9
9
  // FromRule against the cross-feature claim registry, and confirms the
10
10
  // referenced column exists on the entity. Catches typos, renames, and
11
11
  // cross-feature-claim-removal scenarios at boot instead of at request time.
12
12
  //
13
- // fw#2639: also probes every `{ kind: "where" }` rule against the where-rule
13
+ // Also probes every `{ kind: "where" }` rule against the where-rule
14
14
  // lint (see ../where-rule-lint.ts) so a fail-open unqualified-column bug
15
15
  // fails the boot instead of waiting for a request from the right role.
16
16
  //
17
17
  // Field-level `access.read` rejects where-rules too: filterFieldsForRead
18
18
  // evaluates it in memory and userCanReadFieldRow fails closed on a where-rule.
19
19
  //
20
- // fw#2626: `access.write` maps reject `{ kind: "where" }` outright. A
21
- // where-rule is raw SQL and the write path never reaches SQL — it evaluates
22
- // rules in memory against the concrete row (userCanCreateFieldRow /
20
+ // `access.write` maps reject `{ kind: "where" }` outright. A where-rule is
21
+ // raw SQL and the write path never reaches SQL — it evaluates rules in memory
22
+ // against the concrete row (userCanCreateFieldRow /
23
23
  // userCanWriteFieldRow), and a create has no row to run a predicate against
24
24
  // at all. Such a rule can therefore only ever deny, so the boot fails instead
25
25
  // of shipping an access map that silently locks the role out.
@@ -49,9 +49,8 @@ export function collectClaimKeys(features) {
49
49
  // corpora go in as undefined — a three-feature stack genuinely does not know
50
50
  // which roles and claims the whole app declares, and answering that question
51
51
  // from a slice produces false "unknown role"/"unknown claim" errors. The
52
- // subset-invariant checks (#2639 where-lint, #2626 where-on-write, unknown
53
- // column) run in full: those hold or fail per feature, whatever else is
54
- // mounted.
52
+ // subset-invariant checks (where-lint, where-on-write, unknown column) run in
53
+ // full: those hold or fail per feature, whatever else is mounted.
55
54
  export function validateOwnershipBoot(features) {
56
55
  for (const feature of features) {
57
56
  validateOwnershipRules(feature, undefined, undefined);
@@ -258,12 +257,16 @@ export function probeWhereRule(rule, roleName, probe, scope, featureName) {
258
257
  return;
259
258
  assertQualifiedWhereFragment(fragment.sqlText, probe.columns, `${scope} (role "${roleName}", feature: "${featureName}")`);
260
259
  }
260
+ // Array.isArray does not narrow a readonly array out of a union.
261
+ function isRoleList(access) {
262
+ return Array.isArray(access);
263
+ }
261
264
  // Embedded sub-fields are filtered in memory too (filterFieldsForRead), so a
262
265
  // where-rule there is just as unevaluable as on a top-level field.
263
266
  function rejectWhereInEmbeddedRead(schema, parentScope, featureName) {
264
267
  for (const [subName, sub] of Object.entries(schema)) {
265
268
  const map = sub.access?.read;
266
- if (!map || Array.isArray(map))
269
+ if (!map || isRoleList(map))
267
270
  continue;
268
271
  for (const [roleName, rule] of Object.entries(map)) {
269
272
  if (rule !== "all" && rule.kind === "where") {
@@ -1,5 +1,5 @@
1
1
  import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
2
- // --- Parent-ref boot validation (fw#2766) ---
2
+ // --- Parent-ref boot validation ---
3
3
  //
4
4
  // Join-row entities (EntityDefinition.parentRef) derive both the write-path
5
5
  // deny and the read-path SQL gate from one declaration. This is the boot-time