@cosmicdrift/kumiko-framework 0.387.0 → 0.389.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 (375) hide show
  1. package/AGENTS.md +9 -1
  2. package/agents/examples/anonymous-access.md +1 -1
  3. package/agents/examples/apex-landing.md +204 -0
  4. package/agents/examples/api-worker-split.md +64 -0
  5. package/agents/examples/app-config.md +14 -0
  6. package/agents/examples/auth-mfa.md +115 -0
  7. package/agents/examples/env-composition.md +28 -1
  8. package/agents/examples/golden-path.md +13 -1
  9. package/agents/examples/legal-pages.md +1 -1
  10. package/agents/examples/lifecycle-hooks.md +55 -2
  11. package/agents/examples/list-table-options.md +466 -0
  12. package/agents/examples/pdf-audit-export.md +4 -2
  13. package/agents/examples/prod-boot-options.md +16 -0
  14. package/agents/examples/rate-limiting.md +48 -0
  15. package/agents/examples/record-detail-layout.md +692 -0
  16. package/agents/examples/seed-migration.md +212 -0
  17. package/agents/examples/state-machine.md +3 -2
  18. package/agents/examples/tenant-isolation.md +73 -1
  19. package/agents/examples/wizard-form.md +941 -0
  20. package/agents/examples/writeform-section.md +548 -0
  21. package/agents/examples.md +1 -0
  22. package/agents/features.md +19 -2
  23. package/agents/widgets.md +2 -0
  24. package/dist/api/api-constants.d.ts +1 -0
  25. package/dist/api/api-constants.js +2 -0
  26. package/dist/api/auth-routes-shared.d.ts +2 -0
  27. package/dist/api/auth-routes-shared.js +5 -0
  28. package/dist/api/client-error-route.js +10 -5
  29. package/dist/api/entity-read-access.d.ts +2 -1
  30. package/dist/api/entity-read-access.js +14 -0
  31. package/dist/api/export/export-cells-reference.d.ts +9 -0
  32. package/dist/api/export/export-cells-reference.js +21 -0
  33. package/dist/api/export/export-cells.d.ts +11 -0
  34. package/dist/api/export/export-cells.js +136 -0
  35. package/dist/api/export/export-ciphertext-guard.d.ts +2 -0
  36. package/dist/api/export/export-ciphertext-guard.js +27 -0
  37. package/dist/api/export/export-columns.d.ts +29 -0
  38. package/dist/api/export/export-columns.js +85 -0
  39. package/dist/api/export/export-errors.d.ts +4 -0
  40. package/dist/api/export/export-errors.js +14 -0
  41. package/dist/api/export/export-route.d.ts +4 -0
  42. package/dist/api/export/export-route.js +74 -0
  43. package/dist/api/export/export-rows.d.ts +13 -0
  44. package/dist/api/export/export-rows.js +62 -0
  45. package/dist/api/export/export-screen.d.ts +12 -0
  46. package/dist/api/export/export-screen.js +47 -0
  47. package/dist/api/export/export-xlsx-writer.d.ts +4 -0
  48. package/dist/api/export/export-xlsx-writer.js +21 -0
  49. package/dist/api/extra-route.d.ts +4 -0
  50. package/dist/api/index.d.ts +3 -1
  51. package/dist/api/index.js +3 -1
  52. package/dist/api/pii-leak-guard.d.ts +3 -0
  53. package/dist/api/pii-leak-guard.js +3 -2
  54. package/dist/api/request-context.d.ts +1 -1
  55. package/dist/api/request-context.js +1 -1
  56. package/dist/api/routes.d.ts +16 -0
  57. package/dist/api/routes.js +2 -2
  58. package/dist/api/server-boot-checks.d.ts +10 -0
  59. package/dist/api/server-boot-checks.js +157 -0
  60. package/dist/api/server-event-dispatcher.d.ts +26 -0
  61. package/dist/api/server-event-dispatcher.js +208 -0
  62. package/dist/api/server-extra-routes.d.ts +11 -0
  63. package/dist/api/server-extra-routes.js +206 -0
  64. package/dist/api/server-middleware.d.ts +20 -0
  65. package/dist/api/server-middleware.js +211 -0
  66. package/dist/api/server-routes.d.ts +19 -0
  67. package/dist/api/server-routes.js +299 -0
  68. package/dist/api/server-websocket-routes.d.ts +4 -0
  69. package/dist/api/server-websocket-routes.js +108 -0
  70. package/dist/api/server-wiring.d.ts +32 -0
  71. package/dist/api/server-wiring.js +238 -0
  72. package/dist/api/server.d.ts +10 -15
  73. package/dist/api/server.js +48 -1251
  74. package/dist/api/session-cookies.js +4 -4
  75. package/dist/api/sse-route.d.ts +2 -1
  76. package/dist/api/sse-route.js +27 -13
  77. package/dist/config/index.d.ts +3 -1
  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.d.ts +1 -0
  81. package/dist/crypto/pg-kms-adapter.js +12 -18
  82. package/dist/crypto/pii-field-encryption.d.ts +5 -1
  83. package/dist/crypto/pii-field-encryption.js +3 -1
  84. package/dist/db/aggregate-sql.d.ts +23 -0
  85. package/dist/db/aggregate-sql.js +165 -0
  86. package/dist/db/closed-connection.d.ts +9 -0
  87. package/dist/db/closed-connection.js +88 -0
  88. package/dist/db/connection.d.ts +4 -5
  89. package/dist/db/connection.js +29 -20
  90. package/dist/db/dialect.js +1 -1
  91. package/dist/db/distinct-on.d.ts +1 -1
  92. package/dist/db/driver-row-coercion.d.ts +2 -0
  93. package/dist/db/driver-row-coercion.js +186 -0
  94. package/dist/db/driver-value-preparation.d.ts +16 -0
  95. package/dist/db/driver-value-preparation.js +101 -0
  96. package/dist/db/entity-field-encryption.d.ts +1 -0
  97. package/dist/db/entity-field-encryption.js +1 -1
  98. package/dist/db/entity-table-meta.js +103 -110
  99. package/dist/db/event-store-executor-context.d.ts +2 -1
  100. package/dist/db/event-store-executor-read-aggregate-currencies.d.ts +17 -0
  101. package/dist/db/event-store-executor-read-aggregate-currencies.js +40 -0
  102. package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
  103. package/dist/db/event-store-executor-read-aggregate.js +143 -0
  104. package/dist/db/event-store-executor-read-filter.d.ts +31 -0
  105. package/dist/db/event-store-executor-read-filter.js +206 -0
  106. package/dist/db/event-store-executor-read-paging.d.ts +14 -0
  107. package/dist/db/event-store-executor-read-paging.js +99 -0
  108. package/dist/db/event-store-executor-read-rows.d.ts +6 -0
  109. package/dist/db/event-store-executor-read-rows.js +32 -0
  110. package/dist/db/event-store-executor-read-search.d.ts +28 -0
  111. package/dist/db/event-store-executor-read-search.js +221 -0
  112. package/dist/db/event-store-executor-read-sort.d.ts +41 -0
  113. package/dist/db/event-store-executor-read-sort.js +130 -0
  114. package/dist/db/event-store-executor-read-where.d.ts +24 -0
  115. package/dist/db/event-store-executor-read-where.js +81 -0
  116. package/dist/db/event-store-executor-read.d.ts +2 -9
  117. package/dist/db/event-store-executor-read.js +48 -701
  118. package/dist/db/event-store-executor-write-atomic.d.ts +4 -0
  119. package/dist/db/event-store-executor-write-atomic.js +41 -0
  120. package/dist/db/event-store-executor-write-steps.d.ts +17 -0
  121. package/dist/db/event-store-executor-write-steps.js +95 -0
  122. package/dist/db/event-store-executor-write.js +405 -509
  123. package/dist/db/event-store-executor.js +2 -1
  124. package/dist/db/executor-managed-tables.js +1 -1
  125. package/dist/db/index.d.ts +7 -3
  126. package/dist/db/index.js +6 -2
  127. package/dist/db/list-aggregate-labels.d.ts +14 -0
  128. package/dist/db/list-aggregate-labels.js +26 -0
  129. package/dist/db/migrate-generator.js +71 -86
  130. package/dist/db/migrate-runner.js +7 -4
  131. package/dist/db/pg-error.js +26 -21
  132. package/dist/db/queries/backfill-pii.js +3 -6
  133. package/dist/db/queries/database-now.js +2 -1
  134. package/dist/db/queries/shadow-swap.js +2 -3
  135. package/dist/db/query.d.ts +16 -15
  136. package/dist/db/query.js +76 -804
  137. package/dist/db/reference-data.js +19 -2
  138. package/dist/db/statement-timeout.js +2 -1
  139. package/dist/db/table-builder.js +113 -166
  140. package/dist/db/table-info.d.ts +10 -0
  141. package/dist/db/table-info.js +45 -0
  142. package/dist/db/temporal-value-guards.d.ts +6 -0
  143. package/dist/db/temporal-value-guards.js +15 -0
  144. package/dist/db/tenant-db-runner.d.ts +1 -0
  145. package/dist/db/tenant-db-runner.js +3 -0
  146. package/dist/db/tenant-db-upsert.d.ts +5 -0
  147. package/dist/db/tenant-db-upsert.js +91 -0
  148. package/dist/db/tenant-db.js +50 -10
  149. package/dist/db/where-clause.d.ts +6 -0
  150. package/dist/db/where-clause.js +236 -0
  151. package/dist/derivatives/delete-stored-file.js +2 -1
  152. package/dist/engine/boot-validator/boot-check.js +1 -1
  153. package/dist/engine/boot-validator/bundled-lists-table.d.ts +2 -0
  154. package/dist/engine/boot-validator/bundled-lists-table.js +36 -0
  155. package/dist/engine/boot-validator/detail-screens.js +2 -2
  156. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  157. package/dist/engine/boot-validator/entity-handler.js +17 -0
  158. package/dist/engine/boot-validator/entity-list-aggregate-options.d.ts +3 -0
  159. package/dist/engine/boot-validator/entity-list-aggregate-options.js +23 -0
  160. package/dist/engine/boot-validator/form-derivations.js +35 -1
  161. package/dist/engine/boot-validator/i18n-keys.js +5 -3
  162. package/dist/engine/boot-validator/index.js +2 -0
  163. package/dist/engine/boot-validator/list-bulk-actions.d.ts +3 -0
  164. package/dist/engine/boot-validator/list-bulk-actions.js +49 -0
  165. package/dist/engine/boot-validator/list-column-layout.d.ts +8 -0
  166. package/dist/engine/boot-validator/list-column-layout.js +41 -0
  167. package/dist/engine/boot-validator/list-table-aggregate-options.d.ts +24 -0
  168. package/dist/engine/boot-validator/list-table-aggregate-options.js +145 -0
  169. package/dist/engine/boot-validator/list-table-aggregate-query.d.ts +10 -0
  170. package/dist/engine/boot-validator/list-table-aggregate-query.js +31 -0
  171. package/dist/engine/boot-validator/list-table-export-options.d.ts +1 -0
  172. package/dist/engine/boot-validator/list-table-export-options.js +27 -0
  173. package/dist/engine/boot-validator/list-table-options.d.ts +3 -0
  174. package/dist/engine/boot-validator/list-table-options.js +77 -0
  175. package/dist/engine/boot-validator/list-table-reject-unknown-keys.d.ts +1 -0
  176. package/dist/engine/boot-validator/list-table-reject-unknown-keys.js +7 -0
  177. package/dist/engine/boot-validator/pii-retention.js +233 -155
  178. package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
  179. package/dist/engine/boot-validator/projection-list-screens.js +3 -13
  180. package/dist/engine/boot-validator/related-list-table-rules.d.ts +4 -0
  181. package/dist/engine/boot-validator/related-list-table-rules.js +42 -0
  182. package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
  183. package/dist/engine/boot-validator/screens-actions.js +8 -1
  184. package/dist/engine/boot-validator/screens-dashboard.js +2 -5
  185. package/dist/engine/boot-validator/screens-forms.js +2 -6
  186. package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
  187. package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
  188. package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
  189. package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
  190. package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
  191. package/dist/engine/boot-validator/screens-kind-entity-list.js +235 -0
  192. package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
  193. package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
  194. package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
  195. package/dist/engine/boot-validator/screens-kind-projection-list.js +56 -0
  196. package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
  197. package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
  198. package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
  199. package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
  200. package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
  201. package/dist/engine/boot-validator/screens-lists.js +24 -11
  202. package/dist/engine/boot-validator/screens.d.ts +0 -2
  203. package/dist/engine/boot-validator/screens.js +99 -860
  204. package/dist/engine/build-app-schema.d.ts +2 -1
  205. package/dist/engine/build-app-schema.js +65 -97
  206. package/dist/engine/build-config-feature-schema.js +6 -1
  207. package/dist/engine/define-feature.js +13 -2
  208. package/dist/engine/define-handler.js +1 -0
  209. package/dist/engine/entity-handlers.d.ts +13 -0
  210. package/dist/engine/entity-handlers.js +10 -21
  211. package/dist/engine/entity-list-runtime-options.d.ts +8 -0
  212. package/dist/engine/entity-list-runtime-options.js +18 -0
  213. package/dist/engine/feature-ast/extractors/handlers.js +1 -0
  214. package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
  215. package/dist/engine/feature-ast/extractors/index.js +1 -1
  216. package/dist/engine/feature-ast/extractors/round1.d.ts +2 -1
  217. package/dist/engine/feature-ast/extractors/round1.js +6 -0
  218. package/dist/engine/feature-ast/extractors/round2.js +25 -6
  219. package/dist/engine/feature-ast/index.d.ts +1 -1
  220. package/dist/engine/feature-ast/parse.js +3 -1
  221. package/dist/engine/feature-ast/patch.d.ts +2 -0
  222. package/dist/engine/feature-ast/patch.js +5 -182
  223. package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -245
  224. package/dist/engine/feature-ast/pattern-change-schema.js +10 -163
  225. package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
  226. package/dist/engine/feature-ast/pattern-id-matchers.js +211 -0
  227. package/dist/engine/feature-ast/pattern-id-schema.d.ts +251 -0
  228. package/dist/engine/feature-ast/pattern-id-schema.js +166 -0
  229. package/dist/engine/feature-ast/patterns.d.ts +14 -1
  230. package/dist/engine/feature-ast/patterns.js +44 -51
  231. package/dist/engine/feature-ast/render.js +52 -87
  232. package/dist/engine/feature-builder-state.d.ts +1 -0
  233. package/dist/engine/feature-builder-state.js +1 -0
  234. package/dist/engine/feature-entity-handlers.js +3 -0
  235. package/dist/engine/feature-manifest.d.ts +1 -0
  236. package/dist/engine/feature-manifest.js +1 -0
  237. package/dist/engine/field-access.d.ts +1 -0
  238. package/dist/engine/field-access.js +12 -0
  239. package/dist/engine/form-derivations.js +18 -7
  240. package/dist/engine/handler-field-references.js +1 -1
  241. package/dist/engine/index.d.ts +2 -2
  242. package/dist/engine/index.js +1 -1
  243. package/dist/engine/pattern-library/library.js +2 -1
  244. package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
  245. package/dist/engine/pattern-library/static-schemas.js +9 -0
  246. package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
  247. package/dist/engine/project-app-schema-for-roles.js +6 -4
  248. package/dist/engine/registry-ingest.js +1 -1
  249. package/dist/engine/schema-builder.js +228 -212
  250. package/dist/engine/screen-filter-ops.js +2 -0
  251. package/dist/engine/screen-helpers.js +1 -1
  252. package/dist/engine/screen-kind.d.ts +7 -0
  253. package/dist/engine/screen-kind.js +1 -0
  254. package/dist/engine/strip-disabled-features-from-app-schema.js +58 -17
  255. package/dist/engine/types/feature.d.ts +1 -0
  256. package/dist/engine/types/index.d.ts +4 -3
  257. package/dist/engine/types/screen.d.ts +1 -1
  258. package/dist/engine/types/screen.js +1 -1
  259. package/dist/engine/validate-projection-allowlist.js +2 -1
  260. package/dist/entrypoint/index.d.ts +6 -1
  261. package/dist/entrypoint/index.js +10 -1
  262. package/dist/errors/docs-url.d.ts +1 -1
  263. package/dist/errors/docs-url.js +22 -2
  264. package/dist/es-ops/context.js +1 -1
  265. package/dist/event-store/admin-api.js +1 -1
  266. package/dist/event-store/event-store.d.ts +1 -0
  267. package/dist/event-store/event-store.js +17 -8
  268. package/dist/event-store/provenance-append.js +1 -1
  269. package/dist/files/bind-pending-file-refs.js +3 -3
  270. package/dist/files/create-file-ref-from-bytes.js +7 -1
  271. package/dist/files/create-file-ref.d.ts +3 -0
  272. package/dist/files/create-file-ref.js +11 -2
  273. package/dist/files/file-handle.js +2 -3
  274. package/dist/files/file-routes.d.ts +2 -0
  275. package/dist/files/file-routes.js +40 -10
  276. package/dist/files/provider-resolver.d.ts +1 -0
  277. package/dist/files/provider-resolver.js +1 -1
  278. package/dist/files/record-write-check.d.ts +6 -0
  279. package/dist/files/record-write-check.js +13 -0
  280. package/dist/files/resolve-content-type.js +43 -0
  281. package/dist/files/upload-ledger.d.ts +2 -2
  282. package/dist/files/upload-ledger.js +11 -4
  283. package/dist/i18n/required-surface-keys.js +189 -208
  284. package/dist/internal/stack-support.d.ts +2 -1
  285. package/dist/internal/stack-support.js +1 -1
  286. package/dist/internal/testing.js +1 -1
  287. package/dist/jobs/job-runner-context.d.ts +32 -0
  288. package/dist/jobs/job-runner-context.js +305 -0
  289. package/dist/jobs/job-runner-execution.d.ts +7 -0
  290. package/dist/jobs/job-runner-execution.js +160 -0
  291. package/dist/jobs/job-runner-gates.d.ts +19 -0
  292. package/dist/jobs/job-runner-gates.js +198 -0
  293. package/dist/jobs/job-runner-helpers.d.ts +17 -3
  294. package/dist/jobs/job-runner-helpers.js +61 -2
  295. package/dist/jobs/job-runner-scope.d.ts +44 -0
  296. package/dist/jobs/job-runner-scope.js +102 -0
  297. package/dist/jobs/job-runner-types.d.ts +12 -0
  298. package/dist/jobs/job-runner.d.ts +0 -1
  299. package/dist/jobs/job-runner.js +50 -657
  300. package/dist/observability/escape-hatch-report.d.ts +3 -0
  301. package/dist/observability/escape-hatch-report.js +21 -3
  302. package/dist/pipeline/after-rollback-sink.d.ts +6 -0
  303. package/dist/pipeline/after-rollback-sink.js +32 -0
  304. package/dist/pipeline/cascade-handler.js +165 -138
  305. package/dist/pipeline/dispatch-batch.js +8 -1
  306. package/dist/pipeline/dispatch-query.js +10 -3
  307. package/dist/pipeline/dispatch-shared.d.ts +13 -3
  308. package/dist/pipeline/dispatch-shared.js +62 -710
  309. package/dist/pipeline/dispatch-write.js +296 -189
  310. package/dist/pipeline/event-dispatcher-admin.js +2 -1
  311. package/dist/pipeline/event-dispatcher-delivery.js +6 -6
  312. package/dist/pipeline/event-dispatcher.d.ts +1 -0
  313. package/dist/pipeline/event-dispatcher.js +6 -9
  314. package/dist/pipeline/handler-context-auth.d.ts +9 -0
  315. package/dist/pipeline/handler-context-auth.js +54 -0
  316. package/dist/pipeline/handler-context-config.d.ts +14 -0
  317. package/dist/pipeline/handler-context-config.js +113 -0
  318. package/dist/pipeline/handler-context-db.d.ts +24 -0
  319. package/dist/pipeline/handler-context-db.js +149 -0
  320. package/dist/pipeline/handler-context-dispatch.d.ts +23 -0
  321. package/dist/pipeline/handler-context-dispatch.js +101 -0
  322. package/dist/pipeline/handler-context-events.d.ts +41 -0
  323. package/dist/pipeline/handler-context-events.js +239 -0
  324. package/dist/pipeline/handler-context-notify.d.ts +12 -0
  325. package/dist/pipeline/handler-context-notify.js +55 -0
  326. package/dist/pipeline/handler-context-observability.d.ts +7 -0
  327. package/dist/pipeline/handler-context-observability.js +21 -0
  328. package/dist/pipeline/handler-context-scope.d.ts +48 -0
  329. package/dist/pipeline/handler-context-scope.js +88 -0
  330. package/dist/pipeline/notify-scope.js +12 -1
  331. package/dist/pipeline/pre-handler-gates.js +20 -4
  332. package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
  333. package/dist/pipeline/pre-transaction-reservations.js +9 -3
  334. package/dist/pipeline/projection-rebuild.js +2 -1
  335. package/dist/pipeline/system-hooks.d.ts +3 -1
  336. package/dist/pipeline/system-hooks.js +55 -6
  337. package/dist/pipeline/write-result-masking.js +13 -10
  338. package/dist/schema-cli/apply-command.d.ts +2 -0
  339. package/dist/schema-cli/apply-command.js +97 -0
  340. package/dist/schema-cli/baseline-command.d.ts +2 -0
  341. package/dist/schema-cli/baseline-command.js +33 -0
  342. package/dist/schema-cli/generate-command.d.ts +2 -0
  343. package/dist/schema-cli/generate-command.js +90 -0
  344. package/dist/schema-cli/shared.d.ts +39 -0
  345. package/dist/schema-cli/shared.js +95 -0
  346. package/dist/schema-cli/status-command.d.ts +2 -0
  347. package/dist/schema-cli/status-command.js +73 -0
  348. package/dist/schema-cli/validate-boot-input.d.ts +5 -0
  349. package/dist/schema-cli/validate-boot-input.js +130 -0
  350. package/dist/schema-cli/validate-command.d.ts +2 -0
  351. package/dist/schema-cli/validate-command.js +130 -0
  352. package/dist/schema-cli.d.ts +2 -18
  353. package/dist/schema-cli.js +47 -604
  354. package/dist/search/meilisearch-adapter.js +2 -1
  355. package/dist/search/reindex-entity.js +15 -5
  356. package/dist/ui-types/index.d.ts +2 -2
  357. package/dist/ui-types/index.js +1 -1
  358. package/dist/upgrade-changelog.d.ts +2 -0
  359. package/dist/upgrade-changelog.js +14 -0
  360. package/dist/upgrade-cli.d.ts +3 -12
  361. package/dist/upgrade-cli.js +171 -197
  362. package/dist/upgrade-marker-parse.d.ts +32 -0
  363. package/dist/upgrade-marker-parse.js +73 -0
  364. package/package.json +9 -6
  365. package/src/changes.json +555 -0
  366. package/src/scripts/codemod/codemod-support.ts +1 -1
  367. package/src/scripts/codemod/db-factory.ts +1 -1
  368. package/src/scripts/codemod/feature-layout.ts +10 -2
  369. package/src/scripts/codemod/object-form.ts +41 -4
  370. package/src/scripts/codemod/package-merge.ts +58 -8
  371. package/src/scripts/codemod/pii-personal-migration.ts +175 -145
  372. package/src/scripts/codemod/testing-injection.ts +122 -42
  373. package/src/scripts/codemod/testing-move.ts +29 -7
  374. package/dist/engine/provider-plugin-context.d.ts +0 -3
  375. package/dist/engine/provider-plugin-context.js +0 -28
@@ -0,0 +1,88 @@
1
+ import { isEncryptedAtRest } from "../config-helpers.js";
2
+ import { isExtensionEditSection, normalizeEditField, sectionFieldSpecs, } from "../screen-helpers.js";
3
+ import { isWriteOnlyField, validateConfigEditSecretField, validateFieldsXorGroups, validateFormSelectOptions, validateWizardLayout, } from "./screens-forms.js";
4
+ import { assertExtensionSectionHasComponent, rejectEmptySections, rejectProjectionDetailOnlySection, } from "./screens-kind-shared.js";
5
+ function validateConfigEditFields(featureName, screenId, screen, fieldNames) {
6
+ if (fieldNames.size === 0) {
7
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (configEdit) has empty fields map — ` +
8
+ `declare at least one field.`);
9
+ }
10
+ for (const [fname, fdef] of Object.entries(screen.fields)) {
11
+ const fieldType = fdef.type;
12
+ if (typeof fieldType !== "string" || fieldType.length === 0) {
13
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (configEdit) field "${fname}" has no ` +
14
+ `\`type\` set. Each field must declare a type (e.g. "text", "number", "select").`);
15
+ }
16
+ validateFormSelectOptions(featureName, screenId, "configEdit", fname, fdef, fieldNames);
17
+ }
18
+ }
19
+ function validateConfigEditFieldsSection(featureName, screenId, fieldNames, section) {
20
+ rejectProjectionDetailOnlySection(featureName, screenId, "configEdit", section);
21
+ validateFieldsXorGroups(`[Feature ${featureName}] Screen "${screenId}" (configEdit)`, section);
22
+ for (const fieldSpec of sectionFieldSpecs(section)) {
23
+ const normalized = normalizeEditField(fieldSpec);
24
+ if (!fieldNames.has(normalized.field)) {
25
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (configEdit) layout references unknown ` +
26
+ `field "${normalized.field}". Known fields: ${[...fieldNames].sort().join(", ")}`);
27
+ }
28
+ }
29
+ }
30
+ function validateConfigEditSection(featureName, screenId, fieldNames, section) {
31
+ if (isExtensionEditSection(section)) {
32
+ assertExtensionSectionHasComponent(featureName, screenId, "configEdit", section);
33
+ }
34
+ else {
35
+ validateConfigEditFieldsSection(featureName, screenId, fieldNames, section);
36
+ }
37
+ }
38
+ // configKeys: every fieldName must have a mapping entry, and every qualified
39
+ // key must exist in the registry.
40
+ function validateConfigEditFieldKey(ctx, screenId, screen, fname) {
41
+ const secretQn = screen.secretKeys?.[fname];
42
+ if (secretQn !== undefined) {
43
+ validateConfigEditSecretField(ctx.feature.name, screen, fname, secretQn, ctx.featureMap);
44
+ }
45
+ else {
46
+ validateConfigKeyMapping(ctx, screenId, screen, fname);
47
+ }
48
+ }
49
+ function validateConfigKeyMapping(ctx, screenId, screen, fname) {
50
+ const featureName = ctx.feature.name;
51
+ const qualified = screen.configKeys[fname];
52
+ if (qualified === undefined) {
53
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (configEdit) field "${fname}" has ` +
54
+ `no entry in the configKeys map. Every declared field needs a mapping to a ` +
55
+ `qualified config key (\`<feature>:config:<short>\`). Add one to configKeys.`);
56
+ }
57
+ const configKeyDef = ctx.configKeyDefsByQn.get(qualified);
58
+ if (configKeyDef === undefined) {
59
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (configEdit) field "${fname}" → ` +
60
+ `Config key "${qualified}" is not declared in any feature registry. Typo? ` +
61
+ `Expected format: "<feature>:config:<short>". Known keys: ${[...ctx.configKeyDefsByQn.keys()].sort().join(", ") || "(none)"}`);
62
+ }
63
+ if (isWriteOnlyField(screen.fields[fname]) && !isEncryptedAtRest(configKeyDef)) {
64
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (configEdit) field "${fname}" declares writeOnly ` +
65
+ `but config key "${qualified}" is not encrypted at rest — writeOnly on a configEdit field ` +
66
+ `only hides a stored secret (declare the key with encrypted: true or backing: "secrets").`);
67
+ }
68
+ }
69
+ // Validate configEdit layout and fields like actionForm, then verify that
70
+ // every qualified config key is registered and its scope matches the key.
71
+ export function validateConfigEditScreen(ctx, screenId, screen) {
72
+ const featureName = ctx.feature.name;
73
+ const fieldNames = new Set(Object.keys(screen.fields));
74
+ validateConfigEditFields(featureName, screenId, screen, fieldNames);
75
+ rejectEmptySections(featureName, screenId, "configEdit", screen.layout.sections.length);
76
+ for (const section of screen.layout.sections) {
77
+ validateConfigEditSection(featureName, screenId, fieldNames, section);
78
+ }
79
+ validateWizardLayout(featureName, screenId, "configEdit", screen.layout, ctx.featureMap);
80
+ for (const fname of Object.keys(screen.secretKeys ?? {})) {
81
+ if (!fieldNames.has(fname)) {
82
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (configEdit) secretKeys names unknown field "${fname}".`);
83
+ }
84
+ }
85
+ for (const fname of fieldNames) {
86
+ validateConfigEditFieldKey(ctx, screenId, screen, fname);
87
+ }
88
+ }
@@ -0,0 +1,3 @@
1
+ import type { EntityEditScreenDefinition } from "../types/screen.ts";
2
+ import { type ScreenValidationContext } from "./screens-kind-shared.ts";
3
+ export declare function validateEntityEditScreen(ctx: ScreenValidationContext, screenId: string, screen: EntityEditScreenDefinition): void;
@@ -0,0 +1,55 @@
1
+ import { isExtensionEditSection, normalizeEditField, sectionFieldSpecs, } from "../screen-helpers.js";
2
+ import { validateRedirectTarget } from "./screens-actions.js";
3
+ import { validateEditFieldExtras, validateFieldFormRefs, validateFieldsXorGroups, validateFillActionTarget, validateNoWidgetRequiredField, validateWizardLayout, } from "./screens-forms.js";
4
+ import { assertExtensionSectionHasComponent, buildUnknownFieldMessage, rejectEmptySections, rejectProjectionDetailOnlySection, resolveScreenEntity, validateRecordHeaderAction, validateRecordSectionActions, } from "./screens-kind-shared.js";
5
+ function validateEntityEditField(featureName, screenId, entityName, entityDef, fieldNames, fieldSpec) {
6
+ const normalized = normalizeEditField(fieldSpec);
7
+ if (!fieldNames.has(normalized.field)) {
8
+ throw new Error(buildUnknownFieldMessage(featureName, screenId, normalized.field, entityName, fieldNames));
9
+ }
10
+ validateNoWidgetRequiredField(featureName, screenId, entityDef, normalized);
11
+ validateEditFieldExtras(featureName, screenId, entityDef, normalized);
12
+ const fieldWhere = `[Feature ${featureName}] Screen "${screenId}" (entityEdit) field "${normalized.field}"`;
13
+ validateFieldFormRefs(fieldWhere, normalized, fieldNames);
14
+ validateFillActionTarget(fieldWhere, normalized, entityDef.fields[normalized.field]);
15
+ }
16
+ function validateEntityEditSection(ctx, screenId, screen, entityDef, fieldNames, section) {
17
+ const featureName = ctx.feature.name;
18
+ validateRecordSectionActions(ctx, screenId, "entityEdit", section);
19
+ if (isExtensionEditSection(section)) {
20
+ assertExtensionSectionHasComponent(featureName, screenId, "entityEdit", section);
21
+ }
22
+ else {
23
+ rejectProjectionDetailOnlySection(featureName, screenId, "entityEdit", section);
24
+ validateFieldsXorGroups(`[Feature ${featureName}] Screen "${screenId}" (entityEdit)`, section);
25
+ for (const fieldSpec of sectionFieldSpecs(section)) {
26
+ validateEntityEditField(featureName, screenId, screen.entity, entityDef, fieldNames, fieldSpec);
27
+ }
28
+ }
29
+ }
30
+ export function validateEntityEditScreen(ctx, screenId, screen) {
31
+ const featureName = ctx.feature.name;
32
+ const entityDef = resolveScreenEntity(ctx, screenId, screen.entity);
33
+ const fieldNames = new Set(Object.keys(entityDef.fields));
34
+ rejectEmptySections(featureName, screenId, "entityEdit", screen.layout.sections.length);
35
+ for (const section of screen.layout.sections) {
36
+ validateEntityEditSection(ctx, screenId, screen, entityDef, fieldNames, section);
37
+ }
38
+ validateWizardLayout(featureName, screenId, "entityEdit", screen.layout, ctx.featureMap);
39
+ for (const action of screen.actions ?? []) {
40
+ validateRecordHeaderAction(ctx, screenId, "entityEdit", screen.entity, action);
41
+ }
42
+ if (screen.recordTitleField !== undefined && !fieldNames.has(screen.recordTitleField)) {
43
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityEdit) recordTitleField ` +
44
+ `"${screen.recordTitleField}" is not a field of entity "${screen.entity}" ` +
45
+ `(known: ${[...fieldNames].sort().join(", ")}).`);
46
+ }
47
+ if (typeof screen.readOnly === "object" && !fieldNames.has(screen.readOnly.field)) {
48
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityEdit) readOnly condition field ` +
49
+ `"${screen.readOnly.field}" is not a field of entity "${screen.entity}" ` +
50
+ `(known: ${[...fieldNames].sort().join(", ")}).`);
51
+ }
52
+ if (screen.redirect !== undefined) {
53
+ validateRedirectTarget(ctx.feature, screenId, "entityEdit", screen.redirect, ctx);
54
+ }
55
+ }
@@ -0,0 +1,3 @@
1
+ import type { EntityListScreenDefinition } from "../types/screen.ts";
2
+ import { type ScreenValidationContext } from "./screens-kind-shared.ts";
3
+ export declare function validateEntityListScreen(ctx: ScreenValidationContext, screenId: string, screen: EntityListScreenDefinition): void;
@@ -0,0 +1,235 @@
1
+ import { isListFilterAllowed } from "../../db/entity-field-encryption.js";
2
+ import { rowMetaFieldNames } from "../../db/table-builder.js";
3
+ import { qualifyEntityName } from "../qualified-name.js";
4
+ import { getAllowedFilterOps, isFieldFilterable } from "../screen-filter-ops.js";
5
+ import { normalizeListColumn } from "../screen-helpers.js";
6
+ import { validateEntityListAggregateOptions } from "./entity-list-aggregate-options.js";
7
+ import { LIST_ROW_META_COLUMN_NAMES } from "./entity-list-screens.js";
8
+ import { validateListBulkActions } from "./list-bulk-actions.js";
9
+ import { validateListTableOptions } from "./list-table-options.js";
10
+ import { assertWriteHandlerRegistered, rejectRedirectOnListRowAction, resolveRowActionNavigateTarget, validateActionFieldRefs, validateAtMostOneRowClick, validateDrawerTargetAction, validateRowActionNavigateParams, } from "./screens-actions.js";
11
+ import { buildUnknownFieldMessage, listActionContext, rejectEmptyColumns, rejectNonArrayInFilter, resolveScreenEntity, validateListColumnRendering, } from "./screens-kind-shared.js";
12
+ import { validateRelatedListSection, validateRowActionTarget } from "./screens-lists.js";
13
+ function resolveEntityListFields(ctx, screenId, entityName) {
14
+ const entityDef = resolveScreenEntity(ctx, screenId, entityName);
15
+ const fieldNames = new Set(Object.keys(entityDef.fields));
16
+ // List columns may also name a read-time derived field (not a stored
17
+ // column). Allowed for display; deliberately NOT added to `fieldNames`, so
18
+ // defaultSort/filter on a derived field still fails — server-side sort over
19
+ // a non-column is a silent no-op (see DerivedFieldDef).
20
+ const columnFieldNames = entityDef.derivedFields !== undefined
21
+ ? new Set([...fieldNames, ...Object.keys(entityDef.derivedFields)])
22
+ : fieldNames;
23
+ const rowMeta = rowMetaFieldNames(entityDef.softDelete ?? false);
24
+ return { entityDef, fieldNames, columnFieldNames, rowMeta };
25
+ }
26
+ function validateEntityListColumns(ctx, screenId, screen, fields) {
27
+ const featureName = ctx.feature.name;
28
+ rejectEmptyColumns(featureName, screenId, "entityList", screen.columns.length);
29
+ for (const col of screen.columns) {
30
+ const normalized = normalizeListColumn(col);
31
+ // A row-meta column (id/tenantId/version/insertedAt/modifiedAt/...) is a
32
+ // real DB column without being a declared entity field — e.g. a
33
+ // SystemAdmin cross-tenant list exposing tenantId. Accept it like a
34
+ // regular field name — but only the columns computeListViewModel can
35
+ // actually render (LIST_ROW_META_COLUMN_NAMES), not the wider
36
+ // softDelete-aware rowMeta set used for payload/visible refs.
37
+ //
38
+ // A virtual presentational column (drawn by a columnRenderer component
39
+ // from the row, e.g. tag chips) needs BOTH a label AND a renderer —
40
+ // renderer is what actually makes it "virtual" (label alone still
41
+ // needs list.ts's virtual-branch to have something to draw). A column
42
+ // with neither matching a real field/row-meta column NOR a renderer is
43
+ // a typo worth failing the boot.
44
+ if (!fields.columnFieldNames.has(normalized.field) &&
45
+ !LIST_ROW_META_COLUMN_NAMES.has(normalized.field) &&
46
+ !(normalized.label !== undefined && normalized.renderer !== undefined)) {
47
+ throw new Error(buildUnknownFieldMessage(featureName, screenId, normalized.field, screen.entity, fields.columnFieldNames));
48
+ }
49
+ validateListColumnRendering(ctx, screenId, "entityList", normalized);
50
+ }
51
+ }
52
+ function validateEntityListDefaultSort(featureName, screenId, defaultSort, fields) {
53
+ const sortField = defaultSort.field;
54
+ if (!fields.fieldNames.has(sortField)) {
55
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) defaultSort references unknown ` +
56
+ `field "${sortField}". Known fields: ${[...fields.fieldNames].sort().join(", ")}`);
57
+ }
58
+ // `sortable: true` is required so the UI never sorts on a column that has
59
+ // no server-side DB index or that the schema deliberately keeps unsortable
60
+ // (audit fields, computed values). `sortable` currently
61
+ // exists only on TextFieldDef; the `in` narrowing also permits other
62
+ // field types without an explicit flag, which is acceptable because
63
+ // Number/Date fields are naturally sortable. Authors can mark them
64
+ // sortable when their field type supports it.
65
+ const fieldDef = fields.entityDef.fields[sortField];
66
+ const isSortable = fieldDef !== undefined && "sortable" in fieldDef && fieldDef.sortable === true;
67
+ if (!isSortable) {
68
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) defaultSort.field "${sortField}" ` +
69
+ `is not sortable. Set sortable: true on the field definition or pick another field.`);
70
+ }
71
+ }
72
+ // Screen filters are checked in three layers:
73
+ // 1) The field exists on the entity; a typo would otherwise produce an
74
+ // empty list instead of a clear boot failure.
75
+ // 2) The field has `filterable: true`, preventing accidental filtering
76
+ // of audit, computed, or encrypted fields.
77
+ // 3) The operation matches the field type, so lt/gt on text fields
78
+ // fails at boot instead of causing surprising runtime behavior.
79
+ // Additionally, "in" requires a readonly array.
80
+ function validateEntityListFilter(featureName, screenId, filter, fields) {
81
+ const filterField = filter.field;
82
+ if (!fields.fieldNames.has(filterField)) {
83
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) filter references unknown ` +
84
+ `field "${filterField}". Known fields: ${[...fields.fieldNames].sort().join(", ")}`);
85
+ }
86
+ if (!isListFilterAllowed(fields.entityDef, filterField, filter)) {
87
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) filter references field ` +
88
+ `"${filterField}" which is encrypted, personal or secret, so the list handler rejects ` +
89
+ `this filter at runtime. Only op "eq" works on a personal field with find: "exact" or ` +
90
+ `"fuzzy"; otherwise drop the filter or filter on another field.`);
91
+ }
92
+ const fieldDef = fields.entityDef.fields[filterField];
93
+ if (fieldDef !== undefined && !isFieldFilterable(fieldDef)) {
94
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) filter references field ` +
95
+ `"${filterField}" which is not filterable. Set filterable: true on the field ` +
96
+ `definition or pick another field.`);
97
+ }
98
+ if (fieldDef !== undefined) {
99
+ const allowedOps = getAllowedFilterOps(fieldDef);
100
+ if (!allowedOps.includes(filter.op)) {
101
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) filter.op ` +
102
+ `"${filter.op}" is not allowed on field "${filterField}" ` +
103
+ `(type "${fieldDef.type}"). Allowed ops: ${allowedOps.join(", ") || "(none)"}.`);
104
+ }
105
+ }
106
+ rejectNonArrayInFilter(featureName, screenId, "entityList", filter);
107
+ }
108
+ // The renderer's default-entityId fallback (row["id"]) only fires for a
109
+ // same-feature entityEdit target — it can't safely guess the id for a screen
110
+ // owned by a different feature. Cross-feature + entityEdit therefore MUST set
111
+ // an explicit entityId, or the edit screen silently opens with no entity
112
+ // context at runtime. Entity-targets are exempt: the renderer always supplies
113
+ // an id for them (explicit entityId, else row["id"]), regardless of which
114
+ // feature the resolved detailFor screen belongs to.
115
+ function rejectCrossFeatureEditTargetWithoutEntityId(featureName, screenId, action, target) {
116
+ if (action.screen !== undefined &&
117
+ target !== undefined &&
118
+ target.featureName !== featureName &&
119
+ target.screen.type === "entityEdit" &&
120
+ action.entityId === undefined) {
121
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) rowAction "${action.id}" ` +
122
+ `navigates cross-feature to entityEdit screen "${action.screen}" (feature ` +
123
+ `"${target.featureName}") without an explicit entityId field — the renderer's ` +
124
+ `same-feature row["id"] fallback does not apply across features. Set entityId to ` +
125
+ `the row field that names the target entity's id.`);
126
+ }
127
+ }
128
+ // Pin rowActions at boot: a navigate target must exist and a writeHandler QN
129
+ // must be registered. Typos would otherwise only surface on the first click as
130
+ // "Screen not found" or a 404.
131
+ function validateEntityListRowAction(ctx, screenId, screen, fields, action) {
132
+ const featureName = ctx.feature.name;
133
+ rejectRedirectOnListRowAction(featureName, screenId, "entityList", action);
134
+ if (action.kind === "navigate") {
135
+ const target = resolveRowActionNavigateTarget(featureName, screenId, "entityList", "rowAction", action, ctx.allScreenQns, ctx.navTargetShortIds, ctx.screensByShortId, ctx.detailForScreens);
136
+ rejectCrossFeatureEditTargetWithoutEntityId(featureName, screenId, action, target);
137
+ // params only have a reader in actionForm and entityEdit-CREATE; on
138
+ // projectionDetail/dashboard/configEdit/entityEdit-update they are
139
+ // silently ignored at runtime. `custom` is deliberately exempt: it renders
140
+ // an app-registered component the framework has no visibility into, so
141
+ // flagging it would be a false positive on working code.
142
+ //
143
+ // Whether an entityEdit target lands in create or update mode is decided
144
+ // by the same rule the renderer's runNavigate() uses: an explicit
145
+ // entityId always forces update mode, and (absent an explicit entityId) a
146
+ // same-entity target gets row["id"] auto-injected — only a cross-entity
147
+ // target with no explicit entityId reaches create.
148
+ validateRowActionNavigateParams(featureName, screenId, "entityList", screen.entity, action, target);
149
+ }
150
+ else {
151
+ validateRowActionTarget(listActionContext(ctx, screenId, "entityList", screen.entity, `Screen "${screenId}" (entityList)`), "rowAction", "rowAction", action);
152
+ }
153
+ validateActionFieldRefs(featureName, screenId, "rowAction", action.id, action, fields.fieldNames, fields.rowMeta);
154
+ }
155
+ // toolbarActions, analogous to rowActions: misspelled navigate targets and
156
+ // unregistered writeHandler QNs fail at boot instead of on the first click.
157
+ function validateEntityListToolbarAction(ctx, screenId, fields, action) {
158
+ const featureName = ctx.feature.name;
159
+ if (action.kind === "navigate") {
160
+ const candidateQn = qualifyEntityName(featureName, "screen", action.screen);
161
+ if (!ctx.allScreenQns.has(candidateQn) && !ctx.navTargetShortIds.has(action.screen)) {
162
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) toolbarAction "${action.id}" ` +
163
+ `navigate-target "${action.screen}" does not resolve to a registered screen in any feature. Use "<feature>:screen:<id>" of an existing screen.`);
164
+ }
165
+ if (action.tab !== undefined) {
166
+ resolveRowActionNavigateTarget(featureName, screenId, "entityList", "toolbarAction", action, ctx.allScreenQns, ctx.navTargetShortIds, ctx.screensByShortId, ctx.detailForScreens);
167
+ }
168
+ }
169
+ else if (action.kind === "drawer") {
170
+ validateDrawerTargetAction(featureName, screenId, "entityList", "toolbarAction", action, ctx.feature.screens);
171
+ }
172
+ else {
173
+ assertWriteHandlerRegistered(featureName, `Screen "${screenId}" (entityList) toolbarAction "${action.id}"`, action.handler, ctx.allWriteHandlerQns);
174
+ }
175
+ validateActionFieldRefs(featureName, screenId, "toolbarAction", action.id, action, fields.fieldNames, fields.rowMeta);
176
+ }
177
+ function validateEntityListExpandableRow(ctx, screenId, expandableRow, fields) {
178
+ validateRelatedListSection({
179
+ featureName: ctx.feature.name,
180
+ featureMap: ctx.featureMap,
181
+ screenId,
182
+ screenType: "entityList expandableRow",
183
+ where: `Screen "${screenId}" (entityList) expandableRow "${expandableRow.title}"`,
184
+ section: expandableRow,
185
+ screens: ctx.feature.screens,
186
+ allWriteHandlerQns: ctx.allWriteHandlerQns,
187
+ allScreenQns: ctx.allScreenQns,
188
+ navTargetShortIds: ctx.navTargetShortIds,
189
+ screensByShortId: ctx.screensByShortId,
190
+ detailForScreens: ctx.detailForScreens,
191
+ });
192
+ // Toolbar actions of the expansion evaluate against the host row.
193
+ for (const action of expandableRow.toolbarActions ?? []) {
194
+ validateActionFieldRefs(ctx.feature.name, screenId, "toolbarAction", action.id, action, fields.fieldNames, fields.rowMeta);
195
+ }
196
+ }
197
+ export function validateEntityListScreen(ctx, screenId, screen) {
198
+ const featureName = ctx.feature.name;
199
+ const fields = resolveEntityListFields(ctx, screenId, screen.entity);
200
+ validateEntityListColumns(ctx, screenId, screen, fields);
201
+ // Validate pagination, sorting, and search at boot so authoring errors
202
+ // fail before runtime debugging of empty or incorrectly sorted lists.
203
+ if (screen.pageSize !== undefined && screen.pageSize <= 0) {
204
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) has pageSize=${screen.pageSize} — ` +
205
+ `must be a positive integer.`);
206
+ }
207
+ const where = `[Feature ${featureName}] Screen "${screenId}" (entityList)`;
208
+ validateListTableOptions(where, screen.table, screen.columns, true);
209
+ validateEntityListAggregateOptions(where, screen, fields.entityDef);
210
+ if (screen.defaultSort !== undefined) {
211
+ validateEntityListDefaultSort(featureName, screenId, screen.defaultSort, fields);
212
+ }
213
+ if (screen.createScreen !== undefined &&
214
+ !Object.hasOwn(ctx.feature.screens, screen.createScreen)) {
215
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (entityList) createScreen ` +
216
+ `"${screen.createScreen}" does not resolve to a registered screen in this feature. ` +
217
+ `Known screens in this feature: ${Object.keys(ctx.feature.screens).sort().join(", ") || "(none)"}.`);
218
+ }
219
+ if (screen.filter !== undefined) {
220
+ validateEntityListFilter(featureName, screenId, screen.filter, fields);
221
+ }
222
+ if (screen.rowActions !== undefined) {
223
+ for (const action of screen.rowActions) {
224
+ validateEntityListRowAction(ctx, screenId, screen, fields, action);
225
+ }
226
+ validateAtMostOneRowClick(featureName, screenId, "entityList", screen.rowActions);
227
+ }
228
+ validateListBulkActions(`[Feature ${featureName}] Screen "${screenId}" (entityList)`, screen.bulkActions, screen.rowActions);
229
+ for (const action of screen.toolbarActions ?? []) {
230
+ validateEntityListToolbarAction(ctx, screenId, fields, action);
231
+ }
232
+ if (screen.expandableRow !== undefined) {
233
+ validateEntityListExpandableRow(ctx, screenId, screen.expandableRow, fields);
234
+ }
235
+ }
@@ -0,0 +1,3 @@
1
+ import type { ProjectionDetailScreenDefinition } from "../types/screen.ts";
2
+ import { type ScreenValidationContext } from "./screens-kind-shared.ts";
3
+ export declare function validateProjectionDetailScreen(ctx: ScreenValidationContext, screenId: string, screen: ProjectionDetailScreenDefinition): void;
@@ -0,0 +1,235 @@
1
+ import { SELECT_OPTION_TONES } from "@cosmicdrift/kumiko-types/fields";
2
+ import { isExtensionEditSection, isFieldsEditSection, isWriteFormEditSection, normalizeEditField, sectionFieldSpecs, } from "../screen-helpers.js";
3
+ import { assertWriteHandlerRegistered, resolveRowActionNavigateTarget, validateActionHasIcon, validatePrimaryActionTarget, } from "./screens-actions.js";
4
+ import { rejectWriteOnlyFormField, validateFieldsXorGroups, validateFormSelectOptions, validateTabSections, } from "./screens-forms.js";
5
+ import { assertExtensionSectionHasComponent, editSectionLabel, listActionContext, navigateActionFromLink, rejectEmptySections, rejectMissingQuery, validateRecordHeaderAction, validateRecordSectionActions, } from "./screens-kind-shared.js";
6
+ import { assertRefTargetRegistered, validateListActionTargets, validateRelatedListSection, } from "./screens-lists.js";
7
+ import { isSelectOptionTone } from "./select-option-tone.js";
8
+ function validateProjectionDetailFlags(featureName, screenId, screen) {
9
+ if (screen.singleton === true && screen.idParam !== undefined) {
10
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) sets both singleton: true ` +
11
+ `and idParam — singleton means the server picks the row from the caller's context, so there ` +
12
+ `is no row id to send under idParam. Remove idParam.`);
13
+ }
14
+ // singleton has no row id in the path, but the auto-generated "Edit"
15
+ // action (defaultEditAction in kumiko-screen.tsx) navigates
16
+ // to the detailFor entity's entityEdit screen using that path id —
17
+ // under singleton it would silently open that screen in CREATE mode
18
+ // instead of editing the singleton row. Reject rather than let that
19
+ // win silently, same as the idParam check above.
20
+ if (screen.singleton === true && screen.detailFor !== undefined) {
21
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) sets both singleton: true ` +
22
+ `and detailFor — the auto-generated "Edit" action navigates using the path's row id, which a ` +
23
+ `singleton screen never has. Remove detailFor, or declare an explicit "edit" action instead.`);
24
+ }
25
+ if (screen.recordTitleField !== undefined && screen.header !== undefined) {
26
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) sets both header and ` +
27
+ `recordTitleField — header.title already names the record, so drop recordTitleField.`);
28
+ }
29
+ }
30
+ function validateProjectionDetailTabs(featureName, screenId, screen) {
31
+ if (screen.layout.mode === "tabs") {
32
+ validateTabSections(featureName, screenId, "projectionDetail", screen.layout.sections);
33
+ for (const section of screen.layout.sections) {
34
+ if (isFieldsEditSection(section) && "groups" in section && section.actions !== undefined) {
35
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) section "${section.id}" ` +
36
+ `declares both groups and actions in tabs mode — actions has no single group's Card to sit ` +
37
+ `in, and an extra title-less Card around the group cards would nest a Card inside a Card. ` +
38
+ `Move the actions onto one group's title, or drop groups for a flat fields grid.`);
39
+ }
40
+ }
41
+ }
42
+ }
43
+ function validateProjectionDetailMetricNavigate(ctx, screenId, screen, field, navigate) {
44
+ const featureName = ctx.feature.name;
45
+ if (navigate.screen === undefined && navigate.entity === undefined) {
46
+ if (navigate.tab !== undefined &&
47
+ !screen.layout.sections.some((section) => section.id === navigate.tab)) {
48
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) metric "${field}" ` +
49
+ `navigates to tab "${navigate.tab}", which is not a section id on this screen. Navigate to an existing section id or remove navigate.tab.`);
50
+ }
51
+ }
52
+ else {
53
+ // Also runs without `tab`: a typo'd screen/entity or an entity
54
+ // target without entityId would otherwise render a clickable
55
+ // metric that silently does nothing.
56
+ resolveRowActionNavigateTarget(featureName, screenId, "projectionDetail", "metric", navigateActionFromLink(field, navigate), ctx.allScreenQns, ctx.navTargetShortIds, ctx.screensByShortId, ctx.detailForScreens);
57
+ }
58
+ }
59
+ function validateProjectionDetailMetric(ctx, screenId, screen, metric) {
60
+ if (typeof metric.label !== "string" || metric.label.length === 0) {
61
+ throw new Error(`[Feature ${ctx.feature.name}] Screen "${screenId}" (projectionDetail) metric "${metric.field}" has ` +
62
+ `no "label" — every metric needs an i18n label key, ` +
63
+ `there is no fallback to the raw column name.`);
64
+ }
65
+ if (metric.navigate !== undefined) {
66
+ validateProjectionDetailMetricNavigate(ctx, screenId, screen, metric.field, metric.navigate);
67
+ }
68
+ }
69
+ function validateProjectionDetailStatusTones(featureName, screenId, screen) {
70
+ for (const [statusValue, tone] of Object.entries(screen.header?.statusTones ?? {})) {
71
+ if (!isSelectOptionTone(tone)) {
72
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) header.statusTones["${statusValue}"] ` +
73
+ `is "${String(tone)}" — expected one of ${SELECT_OPTION_TONES.join(", ")}.`);
74
+ }
75
+ }
76
+ }
77
+ function validateProjectionDetailSubtitleParts(ctx, screenId, subtitleHref, parts) {
78
+ const featureName = ctx.feature.name;
79
+ if (subtitleHref !== undefined) {
80
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) sets header.subtitleHref ` +
81
+ `together with a multi-part header.subtitle — subtitleHref only works with the string form. Use a single-field subtitle or remove subtitleHref.`);
82
+ }
83
+ const seenPartFields = new Set();
84
+ for (const part of parts) {
85
+ const partField = typeof part === "string" ? part : part.field;
86
+ if (seenPartFields.has(partField)) {
87
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) header.subtitle ` +
88
+ `lists field "${partField}" twice. List each field once.`);
89
+ }
90
+ seenPartFields.add(partField);
91
+ const navigate = typeof part === "string" ? undefined : part.navigate;
92
+ if (navigate === undefined)
93
+ continue;
94
+ if (navigate.screen === undefined && navigate.entity === undefined) {
95
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) header.subtitle part ` +
96
+ `"${partField}" navigates with neither "screen" nor "entity" — a tab-only navigate has no link target.`);
97
+ }
98
+ resolveRowActionNavigateTarget(featureName, screenId, "projectionDetail", "subtitle part", navigateActionFromLink(partField, navigate), ctx.allScreenQns, ctx.navTargetShortIds, ctx.screensByShortId, ctx.detailForScreens);
99
+ }
100
+ }
101
+ function validateProjectionDetailSubtitle(ctx, screenId, screen) {
102
+ const header = screen.header;
103
+ if (header !== undefined && Array.isArray(header.subtitle)) {
104
+ validateProjectionDetailSubtitleParts(ctx, screenId, header.subtitleHref, header.subtitle);
105
+ }
106
+ }
107
+ function validateProjectionDetailExtensionSection(featureName, screenId, section) {
108
+ // projectionDetail is read-only (no composed form submit) — an
109
+ // extension that persists through the host's Save button has
110
+ // nothing to save against. Extensions that persist themselves
111
+ // (e.g. notes-history NotesSection, via their own dispatcher
112
+ // writes) are fine.
113
+ if (section.contributesToFormSubmit === true) {
114
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) extension section ` +
115
+ `"${section.title}" sets contributesToFormSubmit: true, but projectionDetail has no ` +
116
+ `form submit to contribute to (read-only screen). Omit contributesToFormSubmit and ` +
117
+ `persist through the extension's own dispatcher writes instead.`);
118
+ }
119
+ assertExtensionSectionHasComponent(featureName, screenId, "projectionDetail", section);
120
+ // The host-derived entity name on projectionDetail is an internal
121
+ // placeholder (no real entity backs this screen) — a self-
122
+ // persisting extension needs the real domain entity name declared
123
+ // explicitly, or it silently reads/writes against the wrong type.
124
+ if (section.entityName === undefined || section.entityName.trim().length === 0) {
125
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) extension section ` +
126
+ `"${section.title}" has no entityName — projectionDetail has no real entity, so the ` +
127
+ `extension must declare entityName explicitly (the domain entity it persists against).`);
128
+ }
129
+ }
130
+ function validateProjectionDetailRelatedList(ctx, screenId, screen, section) {
131
+ if (screen.layout.mode === "wizard") {
132
+ throw new Error(`[Feature ${ctx.feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
133
+ `is kind "relatedList" in a wizard layout — a read-only list is not supported in a ` +
134
+ `stepped form. Remove mode: "wizard" or drop the relatedList section.`);
135
+ }
136
+ validateRelatedListSection({
137
+ featureName: ctx.feature.name,
138
+ featureMap: ctx.featureMap,
139
+ screenId,
140
+ screenType: "projectionDetail",
141
+ where: `Screen "${screenId}" (projectionDetail) section "${section.title}" (relatedList)`,
142
+ section,
143
+ screens: ctx.feature.screens,
144
+ allWriteHandlerQns: ctx.allWriteHandlerQns,
145
+ allScreenQns: ctx.allScreenQns,
146
+ navTargetShortIds: ctx.navTargetShortIds,
147
+ screensByShortId: ctx.screensByShortId,
148
+ detailForScreens: ctx.detailForScreens,
149
+ });
150
+ }
151
+ function validateProjectionDetailWriteForm(ctx, screenId, screen, section) {
152
+ const featureName = ctx.feature.name;
153
+ if (Object.keys(section.fieldDefs).length === 0) {
154
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
155
+ `(writeForm) has an empty fieldDefs map — declare at least one field type.`);
156
+ }
157
+ if (section.fields.length === 0) {
158
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
159
+ `(writeForm) has zero fields — drop the section or add fields to it.`);
160
+ }
161
+ // A ref to a fieldDef the form never renders would never resolve.
162
+ const writeFormFieldNames = new Set(section.fields.map((f) => normalizeEditField(f).field));
163
+ for (const [defName, fdef] of Object.entries(section.fieldDefs)) {
164
+ validateFormSelectOptions(featureName, screenId, `projectionDetail section "${section.title}" writeForm`, defName, fdef, writeFormFieldNames);
165
+ rejectWriteOnlyFormField(featureName, screenId, `projectionDetail section "${section.title}" writeForm`, defName, fdef);
166
+ }
167
+ for (const f of section.fields) {
168
+ const fieldName = normalizeEditField(f).field;
169
+ if (section.fieldDefs[fieldName] === undefined) {
170
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
171
+ `(writeForm) field "${fieldName}" has no entry in fieldDefs — every rendered field needs ` +
172
+ `a type declared there. Add the field to fieldDefs.`);
173
+ }
174
+ }
175
+ if (screen.layout.mode === "wizard") {
176
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
177
+ `is kind "writeForm" in a wizard layout — a self-persisting form is not supported in a ` +
178
+ `stepped form. Remove mode: "wizard" or drop the writeForm section.`);
179
+ }
180
+ assertWriteHandlerRegistered(featureName, `Screen "${screenId}" (projectionDetail) section "${section.title}" (writeForm)`, section.handler, ctx.allWriteHandlerQns);
181
+ }
182
+ function validateProjectionDetailFieldsSection(ctx, screenId, section) {
183
+ const featureName = ctx.feature.name;
184
+ validateFieldsXorGroups(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail)`, section);
185
+ for (const fieldSpec of sectionFieldSpecs(section)) {
186
+ const normalizedField = normalizeEditField(fieldSpec);
187
+ if (normalizedField.refEntity !== undefined) {
188
+ assertRefTargetRegistered(`[Feature ${featureName}] Screen "${screenId}" (projectionDetail)`, `field "${normalizedField.field}" (refEntity)`, normalizedField.refEntity, featureName, ctx.featureMap);
189
+ }
190
+ }
191
+ }
192
+ function validateProjectionDetailSection(ctx, screenId, screen, section) {
193
+ const featureName = ctx.feature.name;
194
+ validateRecordSectionActions(ctx, screenId, "projectionDetail", section);
195
+ if (section.kind === "relatedList" && section.emptyState?.action !== undefined) {
196
+ const sectionLabel = editSectionLabel(section);
197
+ validateActionHasIcon(featureName, screenId, "projectionDetail", `section "${sectionLabel}" emptyState action`, section.emptyState.action);
198
+ validateListActionTargets(listActionContext(ctx, screenId, "projectionDetail", undefined, `Screen "${screenId}" (projectionDetail) section "${sectionLabel}"`), "emptyState action", section.emptyState.action);
199
+ }
200
+ if (isExtensionEditSection(section)) {
201
+ validateProjectionDetailExtensionSection(featureName, screenId, section);
202
+ }
203
+ else if (section.kind === "relatedList") {
204
+ validateProjectionDetailRelatedList(ctx, screenId, screen, section);
205
+ }
206
+ else if (isWriteFormEditSection(section)) {
207
+ validateProjectionDetailWriteForm(ctx, screenId, screen, section);
208
+ }
209
+ else {
210
+ validateProjectionDetailFieldsSection(ctx, screenId, section);
211
+ }
212
+ }
213
+ // Query-driven like projectionList, but a single row plus layout instead of
214
+ // columns. No entity check is possible: fields resolve against the query
215
+ // response at render time, not against an entity.
216
+ export function validateProjectionDetailScreen(ctx, screenId, screen) {
217
+ const featureName = ctx.feature.name;
218
+ rejectMissingQuery(featureName, screenId, "projectionDetail", screen.query);
219
+ validateProjectionDetailFlags(featureName, screenId, screen);
220
+ rejectEmptySections(featureName, screenId, "projectionDetail", screen.layout.sections.length);
221
+ validateProjectionDetailTabs(featureName, screenId, screen);
222
+ for (const metric of screen.metrics ?? []) {
223
+ validateProjectionDetailMetric(ctx, screenId, screen, metric);
224
+ }
225
+ validateProjectionDetailStatusTones(featureName, screenId, screen);
226
+ validateProjectionDetailSubtitle(ctx, screenId, screen);
227
+ for (const section of screen.layout.sections) {
228
+ validateProjectionDetailSection(ctx, screenId, screen, section);
229
+ }
230
+ validatePrimaryActionTarget(featureName, screenId, screen);
231
+ for (const action of screen.actions ?? []) {
232
+ validateActionHasIcon(featureName, screenId, "projectionDetail", "action", action);
233
+ validateRecordHeaderAction(ctx, screenId, "projectionDetail", screen.detailFor, action);
234
+ }
235
+ }
@@ -0,0 +1,3 @@
1
+ import type { ProjectionListScreenDefinition } from "../types/screen.ts";
2
+ import { type ScreenValidationContext } from "./screens-kind-shared.ts";
3
+ export declare function validateProjectionListScreen(ctx: ScreenValidationContext, screenId: string, screen: ProjectionListScreenDefinition): void;