@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
@@ -1,890 +1,123 @@
1
- // Screen validation entry point. The sub-validators live in the screens-*.ts
2
- // siblings; dependencies only point one way (screens.ts -> dashboard/forms/lists
3
- // -> actions), so there is no same-folder require cycle.
4
- import { SELECT_OPTION_TONES } from "@cosmicdrift/kumiko-types/fields";
5
- import { isListFilterAllowed } from "../../db/entity-field-encryption.js";
6
- import { rowMetaFieldNames } from "../../db/table-builder.js";
7
- import { isEncryptedAtRest } from "../config-helpers.js";
1
+ // Screen validation entry point. The per-kind validators live in the
2
+ // screens-kind-*.ts siblings and the shared sub-validators in screens-dashboard/
3
+ // forms/lists/actions; dependencies only point one way (screens.ts ->
4
+ // screens-kind-*.ts -> screens-kind-shared.ts -> dashboard/forms/lists -> actions),
5
+ // so there is no same-folder require cycle.
8
6
  import { qualifyEntityName } from "../qualified-name.js";
9
- import { getAllowedFilterOps, isFieldFilterable } from "../screen-filter-ops.js";
10
- import { isExtensionEditSection, isFieldsEditSection, isWriteFormEditSection, normalizeEditField, normalizeListColumn, sectionFieldSpecs, } from "../screen-helpers.js";
11
- import { LIST_ROW_META_COLUMN_NAMES } from "./entity-list-screens.js";
12
- import { isRowClickAction, rejectRedirectOnListRowAction, resolveRowActionNavigateTarget, validateActionFieldRefs, validateActionHasIcon, validateAtMostOneRowClick, validateClientActionName, validateDrawerTargetAction, validateNavigateActionTab, validatePrimaryActionTarget, validateRecordActionRedirect, validateRedirectTarget, validateRowActionNavigateParams, validateScreenVisibleWhen, } from "./screens-actions.js";
7
+ import { validateActionHasIcon, validateScreenVisibleWhen } from "./screens-actions.js";
13
8
  import { validateDashboardActions, validateDashboardRecordBinding, validateDashboardScreen, } from "./screens-dashboard.js";
14
- import { isWriteOnlyField, rejectWriteOnlyFormField, validateConfigEditSecretField, validateEditFieldExtras, validateFieldFormRefs, validateFieldsXorGroups, validateFillActionTarget, validateFormSelectOptions, validateInlineFormScreen, validateNoWidgetRequiredField, validateSecretMintConfirm, validateTabSections, validateWizardLayout, } from "./screens-forms.js";
15
- import { assertRefTargetRegistered, rejectInertColumnSortable, validateColumnRendererForm, validateExplicitListScreenExists, validateListActionTargets, validateListFacets, validateRelatedListSection, validateScreenHasNavArea, } from "./screens-lists.js";
16
- import { isSelectOptionTone } from "./select-option-tone.js";
9
+ import { validateInlineFormScreen } from "./screens-forms.js";
10
+ import { validateConfigEditScreen } from "./screens-kind-config-edit.js";
11
+ import { validateEntityEditScreen } from "./screens-kind-entity-edit.js";
12
+ import { validateEntityListScreen } from "./screens-kind-entity-list.js";
13
+ import { validateProjectionDetailScreen } from "./screens-kind-projection-detail.js";
14
+ import { validateProjectionListScreen } from "./screens-kind-projection-list.js";
15
+ import { validateSecretMintScreen } from "./screens-kind-secret-mint.js";
16
+ import { listActionContext } from "./screens-kind-shared.js";
17
+ import { validateExplicitListScreenExists, validateListActionTargets, validateScreenHasNavArea, } from "./screens-lists.js";
17
18
  export { readsNavigateParamsAsFormPrefill, resolveScreenTargetQn, rowFieldExtractorKeys, validateActionFieldRefs, validateActionHasIcon, validateDrawerPrefillKeys, validateScreenShortIdCollisions, } from "./screens-actions.js";
18
19
  export { validateDashboardCustomPanel, validateDashboardScreenPanel } from "./screens-dashboard.js";
19
20
  export { validateColumnRendererForm } from "./screens-lists.js";
20
- export function validateScreens(feature, featureMap, allWriteHandlerQns, allScreenQns, configKeyDefsByQn, screensByShortId, detailForScreens) {
21
+ function validateCustomScreen(ctx, screenId, screen) {
22
+ if (!screen.renderer.react && !screen.renderer.native) {
23
+ throw new Error(`[Feature ${ctx.feature.name}] Screen "${screenId}" has type="custom" but the renderer ` +
24
+ `declares neither a react nor a native component — at least one platform must be set.`);
25
+ }
26
+ }
27
+ function validateDashboardKindScreen(ctx, screenId, screen) {
28
+ validateDashboardScreen(ctx.feature.name, screenId, screen, ctx.featureMap);
29
+ validateDashboardRecordBinding(ctx.feature.name, screenId, screen);
30
+ validateDashboardActions(ctx.feature.name, screenId, screen, ctx.allWriteHandlerQns, ctx);
31
+ }
32
+ function validateActionFormScreen(ctx, screenId, screen) {
33
+ validateInlineFormScreen(ctx.feature, screenId, screen, "actionForm", ctx.allWriteHandlerQns, ctx, ctx.featureMap);
34
+ }
35
+ const SCREEN_KIND_VALIDATORS = {
36
+ custom: validateCustomScreen,
37
+ projectionList: validateProjectionListScreen,
38
+ projectionDetail: validateProjectionDetailScreen,
39
+ dashboard: validateDashboardKindScreen,
40
+ configEdit: validateConfigEditScreen,
41
+ actionForm: validateActionFormScreen,
42
+ secretMint: validateSecretMintScreen,
43
+ // secretsEdit is generator-only (buildConfigFeatureSchema) — never authored
44
+ // via r.screen, so feature.screens never actually carries one.
45
+ secretsEdit: () => { },
46
+ entityList: validateEntityListScreen,
47
+ entityEdit: validateEntityEditScreen,
48
+ };
49
+ function validateScreenOfKind(ctx, screenId, kind, screen) {
50
+ SCREEN_KIND_VALIDATORS[kind](ctx, screenId, screen);
51
+ }
52
+ // Only reachable by casting past ScreenDefinition; without this the table
53
+ // lookup would surface as a TypeError instead of a boot error.
54
+ function rejectUnknownScreenKind(featureName, screenId, screen) {
55
+ if (!Object.hasOwn(SCREEN_KIND_VALIDATORS, screen.type)) {
56
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" has unknown type "${String(screen.type)}".`);
57
+ }
58
+ }
59
+ function buildScreenValidationContext(feature, featureMap, allWriteHandlerQns, allScreenQns, configKeyDefsByQn, screensByShortId, detailForScreens) {
21
60
  // navigate targets (rowAction/toolbarAction) may point cross-feature: the
22
61
  // runtime router (create-app) resolves a bare screenId app-wide across ALL
23
62
  // features (a declarative list in the entity's owning feature navigates to
24
63
  // the consumer app's custom editors this way). The validator mirrors that:
25
64
  // same feature OR any feature. redirect/cancelTarget additionally accept a
26
- // fully qualified cross-feature QN (resolveScreenTargetQn, #1946); short IDs
65
+ // fully qualified cross-feature QN (resolveScreenTargetQn); short IDs
27
66
  // stay same-feature as before.
28
67
  const navTargetShortIds = screenShortIdsFrom(allScreenQns);
29
- const navigateLookups = {
68
+ return {
69
+ feature,
70
+ featureMap,
71
+ allWriteHandlerQns,
72
+ configKeyDefsByQn,
30
73
  allScreenQns,
31
74
  navTargetShortIds,
32
75
  screensByShortId,
33
76
  detailForScreens,
34
77
  };
35
- for (const [screenId, screen] of Object.entries(feature.screens)) {
36
- validateExplicitListScreenExists(feature.name, screenId, screen, screensByShortId);
37
- if ((screen.type === "entityList" || screen.type === "projectionList") &&
38
- screen.emptyState?.action !== undefined) {
39
- validateActionHasIcon(feature.name, screenId, screen.type, "emptyState action", screen.emptyState.action);
40
- validateListActionTargets({
41
- featureName: feature.name,
42
- screenId,
43
- screenType: screen.type,
44
- screenEntity: undefined,
45
- where: `Screen "${screenId}" (${screen.type})`,
46
- screens: feature.screens,
47
- allWriteHandlerQns,
48
- ...navigateLookups,
49
- }, "emptyState action", screen.emptyState.action);
50
- }
51
- // Output of buildAppSchema, which overwrites it — an authored value would be a silent no-op.
52
- if ((screen.type === "entityEdit" ||
53
- screen.type === "actionForm" ||
54
- screen.type === "secretMint") &&
55
- screen.urlPrefillFields !== undefined) {
56
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${screen.type}) sets urlPrefillFields — ` +
57
- `it is derived from navigate params by buildAppSchema; declare params on the navigating action instead.`);
58
- }
59
- if (screen.type !== "actionForm" &&
60
- screen.type !== "secretMint" &&
61
- "submitPrefilled" in screen &&
62
- screen.submitPrefilled !== undefined) {
63
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${screen.type}) sets submitPrefilled — ` +
64
- `only actionForm and secretMint screens support it. Remove it.`);
65
- }
66
- validateScreenHasNavArea(feature, screenId, screen, featureMap);
67
- validateScreenVisibleWhen(feature, screenId, screen, allScreenQns, featureMap);
68
- if (screen.type === "custom") {
69
- if (!screen.renderer.react && !screen.renderer.native) {
70
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" has type="custom" but the renderer ` +
71
- `declares neither a react nor a native component — at least one platform must be set.`);
72
- }
73
- continue;
74
- }
75
- if (screen.type === "projectionList") {
76
- // Query-driven, no entity, so only query + columns are checked (the
77
- // column fields cannot be checked against an entity; they resolve
78
- // against the projection rows at render time).
79
- if (!screen.query || typeof screen.query !== "string") {
80
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) has empty or non-string query. Set query to a registered query name.`);
81
- }
82
- if (screen.columns.length === 0) {
83
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) has an empty columns list — ` +
84
- `declare at least one column.`);
85
- }
86
- for (const col of screen.columns) {
87
- const normalizedCol = normalizeListColumn(col);
88
- validateColumnRendererForm(feature.name, screenId, normalizedCol);
89
- rejectInertColumnSortable(feature.name, screenId, "projectionList", normalizedCol);
90
- if (normalizedCol.refEntity !== undefined) {
91
- assertRefTargetRegistered(`[Feature ${feature.name}] Screen "${screenId}" (projectionList)`, `column "${normalizedCol.field}" (refEntity)`, normalizedCol.refEntity, feature.name, featureMap);
92
- }
93
- }
94
- // Screen filter (fw#2224) — field existence can't be checked without
95
- // an entity (columns aren't a complete field inventory of the
96
- // underlying query), so only pin the structure: "in" requires an
97
- // array. Field validity is documented in the PR body.
98
- if (screen.filter !== undefined &&
99
- screen.filter.op === "in" &&
100
- !Array.isArray(screen.filter.value)) {
101
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) filter.op "in" requires ` +
102
- `filter.value to be a readonly array. Pass an array as filter.value.`);
103
- }
104
- if (screen.facets !== undefined) {
105
- validateListFacets(`[Feature ${feature.name}] Screen "${screenId}" (projectionList)`, screen.facets, screen.columns, feature.name, featureMap);
106
- }
107
- if (screen.rowActions !== undefined) {
108
- for (const action of screen.rowActions) {
109
- rejectRedirectOnListRowAction(feature.name, screenId, "projectionList", action);
110
- if (action.kind === "navigate") {
111
- const target = resolveRowActionNavigateTarget(feature.name, screenId, "projectionList", "rowAction", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
112
- validateRowActionNavigateParams(feature.name, screenId, "projectionList", undefined, action, target);
113
- }
114
- else if (action.kind === "drawer") {
115
- validateDrawerTargetAction(feature.name, screenId, "projectionList", "rowAction", action, feature.screens);
116
- }
117
- else if (action.kind === "clientAction") {
118
- validateClientActionName(feature.name, screenId, "projectionList", "rowAction", action);
119
- }
120
- else if (!allWriteHandlerQns.has(action.handler)) {
121
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) rowAction "${action.id}" ` +
122
- `handler "${action.handler}" is not a registered write-handler. Check the QN spelling ` +
123
- `(expected "<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
124
- }
125
- }
126
- validateAtMostOneRowClick(feature.name, screenId, "projectionList", screen.rowActions);
127
- }
128
- // Plain navigate toolbarActions (no tab) have no boot check yet.
129
- if (screen.toolbarActions !== undefined) {
130
- for (const action of screen.toolbarActions) {
131
- if (action.kind === "drawer") {
132
- validateDrawerTargetAction(feature.name, screenId, "projectionList", "toolbarAction", action, feature.screens);
133
- }
134
- else if (action.kind === "writeHandler" && !allWriteHandlerQns.has(action.handler)) {
135
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) toolbarAction "${action.id}" ` +
136
- `handler "${action.handler}" is not a registered write-handler. Check the QN spelling ` +
137
- `(expected "<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
138
- }
139
- if (action.kind === "navigate" && action.tab !== undefined) {
140
- resolveRowActionNavigateTarget(feature.name, screenId, "projectionList", "toolbarAction", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
141
- }
142
- }
143
- }
144
- continue;
145
- }
146
- if (screen.type === "projectionDetail") {
147
- // Query-driven like projectionList, but a single row plus layout instead
148
- // of columns. No entity check is possible: fields resolve against the
149
- // query response at render time, not against an entity.
150
- if (!screen.query || typeof screen.query !== "string") {
151
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) has empty or non-string query. Set query to a registered query name.`);
152
- }
153
- if (screen.singleton === true && screen.idParam !== undefined) {
154
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) sets both singleton: true ` +
155
- `and idParam — singleton means the server picks the row from the caller's context, so there ` +
156
- `is no row id to send under idParam. Remove idParam.`);
157
- }
158
- // singleton has no row id in the path, but the auto-generated "Edit"
159
- // action (fw#2166, defaultEditAction in kumiko-screen.tsx) navigates
160
- // to the detailFor entity's entityEdit screen using that path id —
161
- // under singleton it would silently open that screen in CREATE mode
162
- // instead of editing the singleton row. Reject rather than let that
163
- // win silently, same as the idParam check above.
164
- if (screen.singleton === true && screen.detailFor !== undefined) {
165
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) sets both singleton: true ` +
166
- `and detailFor — the auto-generated "Edit" action navigates using the path's row id, which a ` +
167
- `singleton screen never has. Remove detailFor, or declare an explicit "edit" action instead.`);
168
- }
169
- if (screen.recordTitleField !== undefined && screen.header !== undefined) {
170
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) sets both header and ` +
171
- `recordTitleField — header.title already names the record, so drop recordTitleField.`);
172
- }
173
- if (screen.layout.sections.length === 0) {
174
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) has an empty sections list — ` +
175
- `declare at least one section.`);
176
- }
177
- if (screen.layout.mode === "tabs") {
178
- validateTabSections(feature.name, screenId, "projectionDetail", screen.layout.sections);
179
- for (const section of screen.layout.sections) {
180
- if (isFieldsEditSection(section) &&
181
- "groups" in section &&
182
- section.actions !== undefined) {
183
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.id}" ` +
184
- `declares both groups and actions in tabs mode — actions has no single group's Card to sit ` +
185
- `in, and an extra title-less Card around the group cards would nest a Card inside a Card. ` +
186
- `Move the actions onto one group's title, or drop groups for a flat fields grid.`);
187
- }
188
- }
189
- }
190
- if (screen.metrics !== undefined) {
191
- for (const metric of screen.metrics) {
192
- const field = metric.field;
193
- if (typeof metric.label !== "string" || metric.label.length === 0) {
194
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) metric "${field}" has ` +
195
- `no "label" — every metric needs an i18n label key, ` +
196
- `there is no fallback to the raw column name.`);
197
- }
198
- const navigate = metric.navigate;
199
- if (navigate !== undefined) {
200
- if (navigate.screen === undefined && navigate.entity === undefined) {
201
- if (navigate.tab !== undefined &&
202
- !screen.layout.sections.some((section) => section.id === navigate.tab)) {
203
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) metric "${field}" ` +
204
- `navigates to tab "${navigate.tab}", which is not a section id on this screen. Navigate to an existing section id or remove navigate.tab.`);
205
- }
206
- }
207
- else {
208
- // Also runs without `tab`: a typo'd screen/entity or an entity
209
- // target without entityId would otherwise render a clickable
210
- // metric that silently does nothing.
211
- resolveRowActionNavigateTarget(feature.name, screenId, "projectionDetail", "metric", {
212
- kind: "navigate",
213
- id: field,
214
- label: field,
215
- ...(navigate.screen !== undefined ? { screen: navigate.screen } : {}),
216
- ...(navigate.entity !== undefined ? { entity: navigate.entity } : {}),
217
- ...(navigate.entityId !== undefined ? { entityId: navigate.entityId } : {}),
218
- ...(navigate.tab !== undefined ? { tab: navigate.tab } : {}),
219
- }, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
220
- }
221
- }
222
- }
223
- }
224
- for (const [statusValue, tone] of Object.entries(screen.header?.statusTones ?? {})) {
225
- if (!isSelectOptionTone(tone)) {
226
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) header.statusTones["${statusValue}"] ` +
227
- `is "${String(tone)}" — expected one of ${SELECT_OPTION_TONES.join(", ")}.`);
228
- }
229
- }
230
- if (Array.isArray(screen.header?.subtitle)) {
231
- if (screen.header.subtitleHref !== undefined) {
232
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) sets header.subtitleHref ` +
233
- `together with a multi-part header.subtitle — subtitleHref only works with the string form. Use a single-field subtitle or remove subtitleHref.`);
234
- }
235
- const seenPartFields = new Set();
236
- for (const part of screen.header.subtitle) {
237
- const partField = typeof part === "string" ? part : part.field;
238
- if (seenPartFields.has(partField)) {
239
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) header.subtitle ` +
240
- `lists field "${partField}" twice. List each field once.`);
241
- }
242
- seenPartFields.add(partField);
243
- const navigate = typeof part === "string" ? undefined : part.navigate;
244
- if (navigate === undefined)
245
- continue;
246
- if (navigate.screen === undefined && navigate.entity === undefined) {
247
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) header.subtitle part ` +
248
- `"${partField}" navigates with neither "screen" nor "entity" — a tab-only navigate has no link target.`);
249
- }
250
- resolveRowActionNavigateTarget(feature.name, screenId, "projectionDetail", "subtitle part", {
251
- kind: "navigate",
252
- id: partField,
253
- label: partField,
254
- ...(navigate.screen !== undefined ? { screen: navigate.screen } : {}),
255
- ...(navigate.entity !== undefined ? { entity: navigate.entity } : {}),
256
- ...(navigate.entityId !== undefined ? { entityId: navigate.entityId } : {}),
257
- ...(navigate.tab !== undefined ? { tab: navigate.tab } : {}),
258
- }, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
259
- }
260
- }
261
- for (const section of screen.layout.sections) {
262
- const sectionLabel = section.title ?? section.id ?? "(untitled)";
263
- if (section.actions !== undefined) {
264
- for (const action of section.actions) {
265
- validateActionHasIcon(feature.name, screenId, "projectionDetail", `section "${sectionLabel}" action`, action);
266
- validateRecordActionRedirect(feature, screenId, "projectionDetail", action, navigateLookups);
267
- validateNavigateActionTab(feature.name, screenId, "projectionDetail", `section "${sectionLabel}" action`, action, navigateLookups);
268
- }
269
- }
270
- if (section.kind === "relatedList" && section.emptyState?.action !== undefined) {
271
- validateActionHasIcon(feature.name, screenId, "projectionDetail", `section "${sectionLabel}" emptyState action`, section.emptyState.action);
272
- validateListActionTargets({
273
- featureName: feature.name,
274
- screenId,
275
- screenType: "projectionDetail",
276
- screenEntity: undefined,
277
- where: `Screen "${screenId}" (projectionDetail) section "${sectionLabel}"`,
278
- screens: feature.screens,
279
- allWriteHandlerQns,
280
- ...navigateLookups,
281
- }, "emptyState action", section.emptyState.action);
282
- }
283
- if (isExtensionEditSection(section)) {
284
- // projectionDetail is read-only (no composed form submit) — an
285
- // extension that persists through the host's Save button has
286
- // nothing to save against. Extensions that persist themselves
287
- // (e.g. notes-history NotesSection, via their own dispatcher
288
- // writes) are fine (solon#264).
289
- if (section.contributesToFormSubmit === true) {
290
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) extension section ` +
291
- `"${section.title}" sets contributesToFormSubmit: true, but projectionDetail has no ` +
292
- `form submit to contribute to (read-only screen). Omit contributesToFormSubmit and ` +
293
- `persist through the extension's own dispatcher writes instead.`);
294
- }
295
- if (section.component?.react === undefined && section.component?.native === undefined) {
296
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) extension section ` +
297
- `"${section.title}" has no component — declare a react/native component marker.`);
298
- }
299
- // The host-derived entity name on projectionDetail is an internal
300
- // placeholder (no real entity backs this screen) — a self-
301
- // persisting extension needs the real domain entity name declared
302
- // explicitly, or it silently reads/writes against the wrong type.
303
- if (section.entityName === undefined || section.entityName.trim().length === 0) {
304
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) extension section ` +
305
- `"${section.title}" has no entityName — projectionDetail has no real entity, so the ` +
306
- `extension must declare entityName explicitly (the domain entity it persists against).`);
307
- }
308
- continue;
309
- }
310
- if (section.kind === "relatedList") {
311
- if (screen.layout.mode === "wizard") {
312
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
313
- `is kind "relatedList" in a wizard layout — a read-only list is not supported in a ` +
314
- `stepped form. Remove mode: "wizard" or drop the relatedList section.`);
315
- }
316
- validateRelatedListSection({
317
- featureName: feature.name,
318
- featureMap,
319
- screenId,
320
- screenType: "projectionDetail",
321
- where: `Screen "${screenId}" (projectionDetail) section "${section.title}" (relatedList)`,
322
- section,
323
- screens: feature.screens,
324
- allWriteHandlerQns,
325
- allScreenQns,
326
- navTargetShortIds,
327
- screensByShortId,
328
- detailForScreens,
329
- });
330
- continue;
331
- }
332
- if (isWriteFormEditSection(section)) {
333
- if (Object.keys(section.fieldDefs).length === 0) {
334
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
335
- `(writeForm) has an empty fieldDefs map — declare at least one field type.`);
336
- }
337
- if (section.fields.length === 0) {
338
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
339
- `(writeForm) has zero fields — drop the section or add fields to it.`);
340
- }
341
- // A ref to a fieldDef the form never renders would never resolve.
342
- const writeFormFieldNames = new Set(section.fields.map((f) => normalizeEditField(f).field));
343
- for (const [defName, fdef] of Object.entries(section.fieldDefs)) {
344
- validateFormSelectOptions(feature.name, screenId, `projectionDetail section "${section.title}" writeForm`, defName, fdef, writeFormFieldNames);
345
- rejectWriteOnlyFormField(feature.name, screenId, `projectionDetail section "${section.title}" writeForm`, defName, fdef);
346
- }
347
- for (const f of section.fields) {
348
- const fieldName = normalizeEditField(f).field;
349
- if (section.fieldDefs[fieldName] === undefined) {
350
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
351
- `(writeForm) field "${fieldName}" has no entry in fieldDefs — every rendered field needs ` +
352
- `a type declared there. Add the field to fieldDefs.`);
353
- }
354
- }
355
- if (screen.layout.mode === "wizard") {
356
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
357
- `is kind "writeForm" in a wizard layout — a self-persisting form is not supported in a ` +
358
- `stepped form. Remove mode: "wizard" or drop the writeForm section.`);
359
- }
360
- if (!allWriteHandlerQns.has(section.handler)) {
361
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
362
- `(writeForm) handler "${section.handler}" is not a registered write-handler. Check the QN ` +
363
- `spelling (expected "<feature>:write:<short>") and that the handler is declared via ` +
364
- `r.writeHandler(...).`);
365
- }
366
- continue;
367
- }
368
- validateFieldsXorGroups(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail)`, section);
369
- for (const fieldSpec of sectionFieldSpecs(section)) {
370
- const normalizedField = normalizeEditField(fieldSpec);
371
- if (normalizedField.refEntity !== undefined) {
372
- assertRefTargetRegistered(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail)`, `field "${normalizedField.field}" (refEntity)`, normalizedField.refEntity, feature.name, featureMap);
373
- }
374
- }
375
- }
376
- // Header actions reuse RowAction (the displayed record stands in for
377
- // the row), so the same navigate/writeHandler existence checks as
378
- // entityList/projectionList apply. `rowClick` is rejected outright —
379
- // a detail screen has no row to click.
380
- validatePrimaryActionTarget(feature.name, screenId, screen);
381
- if (screen.actions !== undefined) {
382
- for (const action of screen.actions) {
383
- validateActionHasIcon(feature.name, screenId, "projectionDetail", "action", action);
384
- validateRecordActionRedirect(feature, screenId, "projectionDetail", action, navigateLookups);
385
- if (isRowClickAction(action)) {
386
- throw new Error(`[Feature ${feature.name}] Screen "${qualifyEntityName(feature.name, "screen", screenId)}" ` +
387
- `(projectionDetail) action "${action.id}" sets rowClick: true — there is no row to ` +
388
- `click on a detail screen. Remove rowClick.`);
389
- }
390
- if (action.kind === "navigate") {
391
- const target = resolveRowActionNavigateTarget(feature.name, screenId, "projectionDetail", "action", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
392
- validateRowActionNavigateParams(feature.name, screenId, "projectionDetail", screen.detailFor, action, target);
393
- }
394
- else if (action.kind === "drawer") {
395
- validateDrawerTargetAction(feature.name, screenId, "projectionDetail", "action", action, feature.screens);
396
- }
397
- else if (action.kind === "clientAction") {
398
- validateClientActionName(feature.name, screenId, "projectionDetail", "action", action);
399
- }
400
- else {
401
- if (!allWriteHandlerQns.has(action.handler)) {
402
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) action "${action.id}" ` +
403
- `handler "${action.handler}" is not a registered write-handler. Check the QN spelling ` +
404
- `(expected "<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
405
- }
406
- }
407
- }
408
- }
409
- continue;
410
- }
411
- if (screen.type === "dashboard") {
412
- validateDashboardScreen(feature.name, screenId, screen, featureMap);
413
- validateDashboardRecordBinding(feature.name, screenId, screen);
414
- validateDashboardActions(feature.name, screenId, screen, allWriteHandlerQns, {
415
- allScreenQns,
416
- navTargetShortIds,
417
- screensByShortId,
418
- detailForScreens,
419
- });
420
- continue;
421
- }
422
- if (screen.type === "configEdit") {
423
- // configEdit: layout/fields wie actionForm validieren, plus
424
- // Cross-Check dass jeder qualifizierte Config-Key registriert
425
- // ist und der scope mit dem Key matcht.
426
- const fieldNames = new Set(Object.keys(screen.fields));
427
- if (fieldNames.size === 0) {
428
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) has empty fields map — ` +
429
- `declare at least one field.`);
430
- }
431
- for (const [fname, fdef] of Object.entries(screen.fields)) {
432
- // @cast-boundary schema-walk — feature-config inspection
433
- const ftype = fdef.type;
434
- if (typeof ftype !== "string" || ftype.length === 0) {
435
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}" has no ` +
436
- `\`type\` set. Each field must declare a type (e.g. "text", "number", "select").`);
437
- }
438
- validateFormSelectOptions(feature.name, screenId, "configEdit", fname, fdef, fieldNames);
439
- }
440
- if (screen.layout.sections.length === 0) {
441
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) has an empty sections list — ` +
442
- `declare at least one section.`);
443
- }
444
- for (const section of screen.layout.sections) {
445
- if (isExtensionEditSection(section)) {
446
- if (section.component?.react === undefined && section.component?.native === undefined) {
447
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) extension section ` +
448
- `"${section.title}" has no component — declare a react/native component marker.`);
449
- }
450
- continue;
451
- }
452
- if (section.kind === "relatedList") {
453
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) relatedList section ` +
454
- `"${section.title}" is not supported — relatedList is a projectionDetail-only ` +
455
- `primitive (fw#2166). Move the section to a projectionDetail screen or remove it.`);
456
- }
457
- if (isWriteFormEditSection(section)) {
458
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) writeForm section ` +
459
- `"${section.title}" is not supported — writeForm is a projectionDetail-only primitive. Move the section to a projectionDetail screen or remove it.`);
460
- }
461
- validateFieldsXorGroups(`[Feature ${feature.name}] Screen "${screenId}" (configEdit)`, section);
462
- for (const fieldSpec of sectionFieldSpecs(section)) {
463
- const normalized = normalizeEditField(fieldSpec);
464
- if (!fieldNames.has(normalized.field)) {
465
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) layout references unknown ` +
466
- `field "${normalized.field}". Known fields: ${[...fieldNames].sort().join(", ")}`);
467
- }
468
- }
469
- }
470
- validateWizardLayout(feature.name, screenId, "configEdit", screen.layout, featureMap);
471
- for (const fname of Object.keys(screen.secretKeys ?? {})) {
472
- if (!fieldNames.has(fname)) {
473
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) secretKeys names unknown field "${fname}".`);
474
- }
475
- }
476
- // configKeys: jeder fieldName muss einen Mapping-Eintrag haben,
477
- // jeder qualifizierte Key muss in der Registry existieren.
478
- for (const fname of fieldNames) {
479
- const secretQn = screen.secretKeys?.[fname];
480
- if (secretQn !== undefined) {
481
- validateConfigEditSecretField(feature.name, screen, fname, secretQn, featureMap);
482
- continue;
483
- }
484
- const qualified = screen.configKeys[fname];
485
- if (qualified === undefined) {
486
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}" has ` +
487
- `no entry in the configKeys map. Every declared field needs a mapping to a ` +
488
- `qualified config key (\`<feature>:config:<short>\`). Add one to configKeys.`);
489
- }
490
- const configKeyDef = configKeyDefsByQn.get(qualified);
491
- if (configKeyDef === undefined) {
492
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}" → ` +
493
- `Config key "${qualified}" is not declared in any feature registry. Typo? ` +
494
- `Expected format: "<feature>:config:<short>". Known keys: ${[...configKeyDefsByQn.keys()].sort().join(", ") || "(none)"}`);
495
- }
496
- if (isWriteOnlyField(screen.fields[fname]) && !isEncryptedAtRest(configKeyDef)) {
497
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}" declares writeOnly ` +
498
- `but config key "${qualified}" is not encrypted at rest — writeOnly on a configEdit field ` +
499
- `only hides a stored secret (declare the key with encrypted: true or backing: "secrets").`);
500
- }
501
- }
502
- continue;
503
- }
504
- if (screen.type === "actionForm") {
505
- validateInlineFormScreen(feature, screenId, screen, "actionForm", allWriteHandlerQns, navigateLookups, featureMap);
506
- continue;
507
- }
508
- if (screen.type === "secretMint") {
509
- validateInlineFormScreen(feature, screenId, screen, "secretMint", allWriteHandlerQns, navigateLookups, featureMap);
510
- if (screen.reveal.fields.length === 0) {
511
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (secretMint) has an empty reveal.fields ` +
512
- `list — declare at least one field to reveal.`);
513
- }
514
- const revealFieldNames = new Set();
515
- for (const revealField of screen.reveal.fields) {
516
- if (typeof revealField.field !== "string" || revealField.field.trim() === "") {
517
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (secretMint) has a reveal.fields entry ` +
518
- `with an empty or non-string "field". Set field to a non-empty string.`);
519
- }
520
- if (revealFieldNames.has(revealField.field)) {
521
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (secretMint) reveal.fields has a ` +
522
- `duplicate field "${revealField.field}" — each revealed field must be unique.`);
523
- }
524
- revealFieldNames.add(revealField.field);
525
- if (typeof revealField.label !== "string" || revealField.label.trim() === "") {
526
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (secretMint) reveal.fields entry ` +
527
- `"${revealField.field}" has an empty or non-string "label". Set label to a non-empty string.`);
528
- }
529
- }
530
- validateSecretMintConfirm(feature, screenId, screen, allWriteHandlerQns);
531
- continue;
532
- }
533
- // secretsEdit is generator-only (buildConfigFeatureSchema) — never
534
- // authored via r.screen, so feature.screens never actually carries one.
535
- // Kept here only so this loop's type narrowing stays exhaustive.
536
- if (screen.type === "secretsEdit")
537
- continue;
538
- // entityList / entityEdit: entity-refs are feature-local.
539
- const entityDef = feature.entities?.[screen.entity];
540
- if (!entityDef) {
541
- const known = Object.keys(feature.entities ?? {})
542
- .sort()
543
- .join(", ") || "(none)";
544
- const crossFeature = findEntityFeature(screen.entity, featureMap);
545
- const hint = crossFeature
546
- ? ` Entity "${screen.entity}" is owned by feature "${crossFeature}" — cross-feature screen ownership is not supported.`
547
- : "";
548
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" references entity "${screen.entity}" ` +
549
- `which is not declared in this feature (known: ${known}).${hint}`);
550
- }
551
- const fieldNames = new Set(Object.keys(entityDef.fields));
552
- // List columns may also name a read-time derived field (not a stored
553
- // column). Allowed for display; deliberately NOT added to `fieldNames`, so
554
- // defaultSort/filter on a derived field still fails — server-side sort over
555
- // a non-column is a silent no-op (see DerivedFieldDef).
556
- const columnFieldNames = entityDef.derivedFields !== undefined
557
- ? new Set([...fieldNames, ...Object.keys(entityDef.derivedFields)])
558
- : fieldNames;
559
- const rowMeta = rowMetaFieldNames(entityDef.softDelete ?? false);
560
- if (screen.type === "entityList") {
561
- // Empty column list would render as a blank table — almost always the
562
- // sign of an in-progress screen the author forgot to fill in. Fail
563
- // loud: ui-core's computeListViewModel can't do anything useful with
564
- // zero columns either.
565
- if (screen.columns.length === 0) {
566
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) has an empty columns list — ` +
567
- `declare at least one column.`);
568
- }
569
- for (const col of screen.columns) {
570
- const normalized = normalizeListColumn(col);
571
- // A row-meta column (id/tenantId/version/insertedAt/modifiedAt/...) is a
572
- // real DB column without being a declared entity field — e.g. a
573
- // SystemAdmin cross-tenant list exposing tenantId. Accept it like a
574
- // regular field name — but only the columns computeListViewModel can
575
- // actually render (LIST_ROW_META_COLUMN_NAMES), not the wider
576
- // softDelete-aware rowMeta set used below for payload/visible refs.
577
- //
578
- // A virtual presentational column (drawn by a columnRenderer component
579
- // from the row, e.g. tag chips) needs BOTH a label AND a renderer —
580
- // renderer is what actually makes it "virtual" (label alone still
581
- // needs list.ts's virtual-branch to have something to draw; without
582
- // a renderer the column would render nothing). A column with neither
583
- // matching a real field/row-meta column NOR a renderer is a typo worth
584
- // failing the boot.
585
- if (!columnFieldNames.has(normalized.field) &&
586
- !LIST_ROW_META_COLUMN_NAMES.has(normalized.field) &&
587
- !(normalized.label !== undefined && normalized.renderer !== undefined)) {
588
- throw new Error(buildUnknownFieldMessage(feature.name, screenId, normalized.field, screen.entity, columnFieldNames));
589
- }
590
- validateColumnRendererForm(feature.name, screenId, normalized);
591
- rejectInertColumnSortable(feature.name, screenId, "entityList", normalized);
592
- if (normalized.refEntity !== undefined) {
593
- assertRefTargetRegistered(`[Feature ${feature.name}] Screen "${screenId}" (entityList)`, `column "${normalized.field}" (refEntity)`, normalized.refEntity, feature.name, featureMap);
594
- }
595
- }
596
- // Pagination/Sort/Search-Validierung: Author-Fehler beim Boot
597
- // fangen, damit kein "warum kommt die Liste leer / falsch
598
- // sortiert"-Debug-Cycle zur Laufzeit losgeht.
599
- if (screen.pageSize !== undefined && screen.pageSize <= 0) {
600
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) has pageSize=${screen.pageSize} — ` +
601
- `must be a positive integer.`);
602
- }
603
- if (screen.defaultSort !== undefined) {
604
- const sortField = screen.defaultSort.field;
605
- if (!fieldNames.has(sortField)) {
606
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) defaultSort references unknown ` +
607
- `field "${sortField}". Known fields: ${[...fieldNames].sort().join(", ")}`);
608
- }
609
- // sortable: true Pflicht — verhindert dass das UI auf einer
610
- // Spalte sortiert, die Server-Side gar keinen DB-Index hat
611
- // oder im Schema absichtlich nicht sortiert werden soll
612
- // (Audit-Felder, Computed-Werte). `sortable` lebt heute nur
613
- // auf TextFieldDef; "in"-narrow lässt das auch für andere
614
- // Field-Types ohne explizites Flag durchfallen, was ok ist:
615
- // Number/Date sind natürlich sortierbar, der Author kann sie
616
- // im Author-Code als sortable markieren wenn das Field-Type
617
- // es trägt (Erweiterung folgt).
618
- const fieldDef = entityDef.fields[sortField];
619
- const isSortable = fieldDef !== undefined && "sortable" in fieldDef && fieldDef.sortable === true;
620
- if (!isSortable) {
621
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) defaultSort.field "${sortField}" ` +
622
- `is not sortable. Set sortable: true on the field definition or pick another field.`);
623
- }
624
- }
625
- if (screen.createScreen !== undefined &&
626
- !Object.hasOwn(feature.screens, screen.createScreen)) {
627
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) createScreen ` +
628
- `"${screen.createScreen}" does not resolve to a registered screen in this feature. ` +
629
- `Known screens in this feature: ${Object.keys(feature.screens).sort().join(", ") || "(none)"}.`);
630
- }
631
- // Screen-Filter (Tier 2.7c) — drei Layer Author-Code-Check:
632
- // 1) Field existiert auf der Entity (Tippfehler = leere Liste
633
- // statt Crash; Boot-Fail ist deutlich besser).
634
- // 2) Field hat `filterable: true` (Author opt-in, analog zu
635
- // `sortable`). Verhindert dass Audit-/Computed-/encrypted-
636
- // Felder unbeabsichtigt filterbar werden.
637
- // 3) Op passt zum Field-Type. Lt/gt auf text-Feldern → Boot-
638
- // Fail mit Hinweis statt String-Sort-Surprise zur Laufzeit.
639
- // Außerdem: "in" verlangt readonly Array.
640
- if (screen.filter !== undefined) {
641
- const filterField = screen.filter.field;
642
- if (!fieldNames.has(filterField)) {
643
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter references unknown ` +
644
- `field "${filterField}". Known fields: ${[...fieldNames].sort().join(", ")}`);
645
- }
646
- if (!isListFilterAllowed(entityDef, filterField, screen.filter)) {
647
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter references field ` +
648
- `"${filterField}" which is encrypted, personal or secret, so the list handler rejects ` +
649
- `this filter at runtime. Only op "eq" works on a personal field with find: "exact" or ` +
650
- `"fuzzy"; otherwise drop the filter or filter on another field.`);
651
- }
652
- const fieldDef = entityDef.fields[filterField];
653
- if (fieldDef !== undefined && !isFieldFilterable(fieldDef)) {
654
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter references field ` +
655
- `"${filterField}" which is not filterable. Set filterable: true on the field ` +
656
- `definition or pick another field.`);
657
- }
658
- if (fieldDef !== undefined) {
659
- const allowedOps = getAllowedFilterOps(fieldDef);
660
- if (!allowedOps.includes(screen.filter.op)) {
661
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter.op ` +
662
- `"${screen.filter.op}" is not allowed on field "${filterField}" ` +
663
- `(type "${fieldDef.type}"). Allowed ops: ${allowedOps.join(", ") || "(none)"}.`);
664
- }
665
- }
666
- if (screen.filter.op === "in" && !Array.isArray(screen.filter.value)) {
667
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter.op "in" requires ` +
668
- `filter.value to be a readonly array. Pass an array as filter.value.`);
669
- }
670
- }
671
- // Tier 2.7e-1: rowActions pinnen — navigate-target existiert (selbes
672
- // Feature), writeHandler-QN ist registriert. Tippfehler fallen sonst
673
- // erst beim ersten Klick als "Screen not found" / 404 auf.
674
- if (screen.rowActions !== undefined) {
675
- for (const action of screen.rowActions) {
676
- rejectRedirectOnListRowAction(feature.name, screenId, "entityList", action);
677
- if (action.kind === "navigate") {
678
- const target = resolveRowActionNavigateTarget(feature.name, screenId, "entityList", "rowAction", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
679
- // The renderer's default-entityId fallback (row["id"]) only fires
680
- // for a same-feature entityEdit target — it can't safely guess
681
- // the id for a screen owned by a different feature. Cross-feature
682
- // + entityEdit therefore MUST set an explicit entityId, or the
683
- // edit screen silently opens with no entity context at runtime.
684
- // Entity-targets (fw#2228) are exempt: the renderer always
685
- // supplies an id for them (explicit entityId, else row["id"]),
686
- // regardless of which feature the resolved detailFor screen
687
- // belongs to.
688
- if (action.screen !== undefined &&
689
- target !== undefined &&
690
- target.featureName !== feature.name &&
691
- target.screen.type === "entityEdit" &&
692
- action.entityId === undefined) {
693
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) rowAction "${action.id}" ` +
694
- `navigates cross-feature to entityEdit screen "${action.screen}" (feature ` +
695
- `"${target.featureName}") without an explicit entityId field — the renderer's ` +
696
- `same-feature row["id"] fallback does not apply across features. Set entityId to ` +
697
- `the row field that names the target entity's id.`);
698
- }
699
- // params only have a reader in actionForm and entityEdit-CREATE;
700
- // on projectionDetail/dashboard/configEdit/entityEdit-update they
701
- // are silently ignored at runtime. `custom` is deliberately
702
- // exempt: it renders an app-registered component the framework
703
- // has no visibility into — the author may read nav.searchParams
704
- // directly (real example: publicstatus's MonitorDetailScreen
705
- // does exactly that), so flagging it would be a false positive
706
- // on working code, not a caught bug.
707
- //
708
- // Whether an entityEdit target lands in create or update mode is
709
- // decided by the same rule the renderer's runNavigate() uses: an
710
- // explicit entityId always forces update mode, and (absent an
711
- // explicit entityId) a same-entity target gets row["id"] auto-
712
- // injected — only a cross-entity target with no explicit
713
- // entityId reaches create.
714
- validateRowActionNavigateParams(feature.name, screenId, "entityList", screen.entity, action, target);
715
- }
716
- else if (action.kind === "drawer") {
717
- validateDrawerTargetAction(feature.name, screenId, "entityList", "rowAction", action, feature.screens);
718
- }
719
- else if (action.kind === "clientAction") {
720
- validateClientActionName(feature.name, screenId, "entityList", "rowAction", action);
721
- }
722
- else {
723
- if (!allWriteHandlerQns.has(action.handler)) {
724
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) rowAction "${action.id}" ` +
725
- `handler "${action.handler}" is not a registered write-handler. Check the QN spelling ` +
726
- `(expected "<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
727
- }
728
- }
729
- validateActionFieldRefs(feature.name, screenId, "rowAction", action.id, action, fieldNames, rowMeta);
730
- }
731
- validateAtMostOneRowClick(feature.name, screenId, "entityList", screen.rowActions);
732
- }
733
- // Tier 2.7e-2: toolbarActions — analog zu rowActions, aber bisher
734
- // ohne Validator. Typo'd navigate-targets und unregistrierte
735
- // writeHandler-QNs fallen bis hierhin erst beim Klick auf.
736
- if (screen.toolbarActions !== undefined) {
737
- for (const action of screen.toolbarActions) {
738
- if (action.kind === "navigate") {
739
- const candidateQn = qualifyEntityName(feature.name, "screen", action.screen);
740
- if (!allScreenQns.has(candidateQn) && !navTargetShortIds.has(action.screen)) {
741
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) toolbarAction "${action.id}" ` +
742
- `navigate-target "${action.screen}" does not resolve to a registered screen in any feature. Use "<feature>:screen:<id>" of an existing screen.`);
743
- }
744
- if (action.tab !== undefined) {
745
- resolveRowActionNavigateTarget(feature.name, screenId, "entityList", "toolbarAction", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
746
- }
747
- }
748
- else if (action.kind === "drawer") {
749
- validateDrawerTargetAction(feature.name, screenId, "entityList", "toolbarAction", action, feature.screens);
750
- }
751
- else {
752
- if (!allWriteHandlerQns.has(action.handler)) {
753
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) toolbarAction "${action.id}" ` +
754
- `handler "${action.handler}" is not a registered write-handler. Check the QN spelling ` +
755
- `(expected "<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
756
- }
757
- }
758
- validateActionFieldRefs(feature.name, screenId, "toolbarAction", action.id, action, fieldNames, rowMeta);
759
- }
760
- }
761
- if (screen.expandableRow !== undefined) {
762
- const expandableRow = screen.expandableRow;
763
- validateRelatedListSection({
764
- featureName: feature.name,
765
- featureMap,
766
- screenId,
767
- screenType: "entityList expandableRow",
768
- where: `Screen "${screenId}" (entityList) expandableRow "${expandableRow.title}"`,
769
- section: expandableRow,
770
- screens: feature.screens,
771
- allWriteHandlerQns,
772
- allScreenQns,
773
- navTargetShortIds,
774
- screensByShortId,
775
- detailForScreens,
776
- });
777
- // Toolbar actions of the expansion evaluate against the host row.
778
- for (const action of expandableRow.toolbarActions ?? []) {
779
- validateActionFieldRefs(feature.name, screenId, "toolbarAction", action.id, action, fieldNames, rowMeta);
780
- }
781
- }
782
- }
783
- else {
784
- // Same rationale as the columns check: an entityEdit layout with zero
785
- // sections (or sections without any fields) renders as nothing — reject
786
- // at boot so the author sees it before the blank form surprises them.
787
- if (screen.layout.sections.length === 0) {
788
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) has an empty sections list — ` +
789
- `declare at least one section.`);
790
- }
791
- for (const section of screen.layout.sections) {
792
- if (section.actions !== undefined) {
793
- const sectionLabel = section.title ?? section.id ?? "(untitled)";
794
- for (const action of section.actions) {
795
- validateActionHasIcon(feature.name, screenId, "entityEdit", `section "${sectionLabel}" action`, action);
796
- validateRecordActionRedirect(feature, screenId, "entityEdit", action, navigateLookups);
797
- validateNavigateActionTab(feature.name, screenId, "entityEdit", `section "${sectionLabel}" action`, action, navigateLookups);
798
- }
799
- }
800
- if (isExtensionEditSection(section)) {
801
- if (section.component?.react === undefined && section.component?.native === undefined) {
802
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) extension section ` +
803
- `"${section.title}" has no component — declare a react/native component marker.`);
804
- }
805
- continue;
806
- }
807
- if (section.kind === "relatedList") {
808
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) relatedList section ` +
809
- `"${section.title}" is not supported — relatedList is a projectionDetail-only ` +
810
- `primitive (fw#2166). Move the section to a projectionDetail screen or remove it.`);
811
- }
812
- if (isWriteFormEditSection(section)) {
813
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) writeForm section ` +
814
- `"${section.title}" is not supported — writeForm is a projectionDetail-only primitive. Move the section to a projectionDetail screen or remove it.`);
815
- }
816
- validateFieldsXorGroups(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit)`, section);
817
- for (const fieldSpec of sectionFieldSpecs(section)) {
818
- const normalized = normalizeEditField(fieldSpec);
819
- if (!fieldNames.has(normalized.field)) {
820
- throw new Error(buildUnknownFieldMessage(feature.name, screenId, normalized.field, screen.entity, fieldNames));
821
- }
822
- validateNoWidgetRequiredField(feature.name, screenId, entityDef, normalized);
823
- validateEditFieldExtras(feature.name, screenId, entityDef, normalized);
824
- const fieldWhere = `[Feature ${feature.name}] Screen "${screenId}" (entityEdit) field "${normalized.field}"`;
825
- validateFieldFormRefs(fieldWhere, normalized, fieldNames);
826
- validateFillActionTarget(fieldWhere, normalized, entityDef.fields[normalized.field]);
827
- }
828
- }
829
- validateWizardLayout(feature.name, screenId, "entityEdit", screen.layout, featureMap);
830
- // Header actions reuse RowAction — same shape and same checks as
831
- // projectionDetail's `actions` above (the loaded record stands in
832
- // for the row). `rowClick` is rejected outright — entityEdit has no
833
- // row to click either.
834
- if (screen.actions !== undefined) {
835
- for (const action of screen.actions) {
836
- validateRecordActionRedirect(feature, screenId, "entityEdit", action, navigateLookups);
837
- if (isRowClickAction(action)) {
838
- throw new Error(`[Feature ${feature.name}] Screen "${qualifyEntityName(feature.name, "screen", screenId)}" ` +
839
- `(entityEdit) action "${action.id}" sets rowClick: true — there is no row to click ` +
840
- `on an edit screen. Remove rowClick.`);
841
- }
842
- if (action.kind === "navigate") {
843
- const target = resolveRowActionNavigateTarget(feature.name, screenId, "entityEdit", "action", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
844
- validateRowActionNavigateParams(feature.name, screenId, "entityEdit", screen.entity, action, target);
845
- }
846
- else if (action.kind === "drawer") {
847
- validateDrawerTargetAction(feature.name, screenId, "entityEdit", "action", action, feature.screens);
848
- }
849
- else if (action.kind === "clientAction") {
850
- validateClientActionName(feature.name, screenId, "entityEdit", "action", action);
851
- }
852
- else {
853
- if (!allWriteHandlerQns.has(action.handler)) {
854
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) action "${action.id}" ` +
855
- `handler "${action.handler}" is not a registered write-handler. Check the QN spelling ` +
856
- `(expected "<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
857
- }
858
- }
859
- }
860
- }
861
- if (screen.recordTitleField !== undefined && !fieldNames.has(screen.recordTitleField)) {
862
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) recordTitleField ` +
863
- `"${screen.recordTitleField}" is not a field of entity "${screen.entity}" ` +
864
- `(known: ${[...fieldNames].sort().join(", ")}).`);
865
- }
866
- if (typeof screen.readOnly === "object" && !fieldNames.has(screen.readOnly.field)) {
867
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) readOnly condition field ` +
868
- `"${screen.readOnly.field}" is not a field of entity "${screen.entity}" ` +
869
- `(known: ${[...fieldNames].sort().join(", ")}).`);
870
- }
871
- if (screen.redirect !== undefined) {
872
- validateRedirectTarget(feature, screenId, "entityEdit", screen.redirect, navigateLookups);
873
- }
874
- }
78
+ }
79
+ function validateListEmptyStateAction(ctx, screenId, screen) {
80
+ if ((screen.type === "entityList" || screen.type === "projectionList") &&
81
+ screen.emptyState?.action !== undefined) {
82
+ validateActionHasIcon(ctx.feature.name, screenId, screen.type, "emptyState action", screen.emptyState.action);
83
+ validateListActionTargets(listActionContext(ctx, screenId, screen.type, undefined, `Screen "${screenId}" (${screen.type})`), "emptyState action", screen.emptyState.action);
84
+ }
85
+ }
86
+ // Output of buildAppSchema, which overwrites it — an authored value would be a silent no-op.
87
+ function rejectAuthoredUrlPrefillFields(featureName, screenId, screen) {
88
+ if ((screen.type === "entityEdit" ||
89
+ screen.type === "actionForm" ||
90
+ screen.type === "secretMint") &&
91
+ screen.urlPrefillFields !== undefined) {
92
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screen.type}) sets urlPrefillFields — ` +
93
+ `it is derived from navigate params by buildAppSchema; declare params on the navigating action instead.`);
875
94
  }
876
95
  }
877
- export function findEntityFeature(entityName, featureMap) {
878
- for (const [name, feature] of featureMap) {
879
- if (feature.entities?.[entityName])
880
- return name;
96
+ function rejectSubmitPrefilledOutsideForms(featureName, screenId, screen) {
97
+ if (screen.type !== "actionForm" &&
98
+ screen.type !== "secretMint" &&
99
+ "submitPrefilled" in screen &&
100
+ screen.submitPrefilled !== undefined) {
101
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screen.type}) sets submitPrefilled — ` +
102
+ `only actionForm and secretMint screens support it. Remove it.`);
881
103
  }
882
- return undefined;
883
104
  }
884
- export function buildUnknownFieldMessage(featureName, screenId, fieldName, entityName, knownFields) {
885
- const known = [...knownFields].sort().join(", ");
886
- return (`[Feature ${featureName}] Screen "${screenId}" references field "${fieldName}" ` +
887
- `which does not exist on entity "${entityName}" (known: ${known}).`);
105
+ function validateScreenPrelude(ctx, screenId, screen) {
106
+ const featureName = ctx.feature.name;
107
+ validateExplicitListScreenExists(featureName, screenId, screen, ctx.screensByShortId);
108
+ validateListEmptyStateAction(ctx, screenId, screen);
109
+ rejectAuthoredUrlPrefillFields(featureName, screenId, screen);
110
+ rejectSubmitPrefilledOutsideForms(featureName, screenId, screen);
111
+ validateScreenHasNavArea(ctx.feature, screenId, screen, ctx.featureMap);
112
+ validateScreenVisibleWhen(ctx.feature, screenId, screen, ctx.allScreenQns, ctx.featureMap);
113
+ }
114
+ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScreenQns, configKeyDefsByQn, screensByShortId, detailForScreens) {
115
+ const ctx = buildScreenValidationContext(feature, featureMap, allWriteHandlerQns, allScreenQns, configKeyDefsByQn, screensByShortId, detailForScreens);
116
+ for (const [screenId, screen] of Object.entries(feature.screens)) {
117
+ validateScreenPrelude(ctx, screenId, screen);
118
+ rejectUnknownScreenKind(feature.name, screenId, screen);
119
+ validateScreenOfKind(ctx, screenId, screen.type, screen);
120
+ }
888
121
  }
889
122
  export function collectScreenQns(features) {
890
123
  const set = new Set();
@@ -895,9 +128,9 @@ export function collectScreenQns(features) {
895
128
  }
896
129
  return set;
897
130
  }
898
- // Bare Screen-ids (ohne `<feature>:screen:`-Prefix) aus den qualifizierten
899
- // QNs — für die app-weite Auflösung von navigate-Targets (s. validateScreens).
900
- // Spiegelt den Runtime-Router, der bare ids feature-übergreifend matcht.
131
+ // Extract bare screen IDs (without the `<feature>:screen:` prefix) from
132
+ // qualified QNs for app-wide navigate-target resolution (see buildScreenValidationContext).
133
+ // This mirrors the runtime router, which matches bare IDs across features.
901
134
  export function screenShortIdsFrom(allScreenQns) {
902
135
  const marker = ":screen:";
903
136
  const set = new Set();