@cosmicdrift/kumiko-framework 0.385.0 → 0.387.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 (298) hide show
  1. package/LICENSE +21 -57
  2. package/README.md +2 -2
  3. package/agents/widgets.md +39 -39
  4. package/dist/api/api-constants.js +2 -2
  5. package/dist/api/auth-middleware.js +7 -7
  6. package/dist/api/auth-rate-limiters.js +7 -3
  7. package/dist/api/auth-routes-mfa.js +1 -1
  8. package/dist/api/auth-routes-session.js +1 -1
  9. package/dist/api/auth-routes-shared.js +7 -3
  10. package/dist/api/auth-routes-signup.js +1 -1
  11. package/dist/api/client-ip.js +3 -1
  12. package/dist/api/extra-route.js +1 -1
  13. package/dist/api/pii-leak-guard.js +4 -2
  14. package/dist/api/readiness.js +5 -5
  15. package/dist/api/redis-sse-broker.js +8 -6
  16. package/dist/api/request-context.d.ts +2 -0
  17. package/dist/api/request-context.js +16 -7
  18. package/dist/api/routes.js +2 -2
  19. package/dist/api/server.js +34 -33
  20. package/dist/api/sse-broker.js +1 -1
  21. package/dist/api/sse-route.d.ts +13 -13
  22. package/dist/api/sse-route.js +15 -15
  23. package/dist/auth/index.js +4 -4
  24. package/dist/auth/roles.d.ts +4 -4
  25. package/dist/auth/roles.js +24 -24
  26. package/dist/compliance/duration-spec.js +18 -20
  27. package/dist/compliance/index.js +2 -3
  28. package/dist/compliance/override-schema.d.ts +4 -4
  29. package/dist/compliance/override-schema.js +19 -22
  30. package/dist/compliance/profiles.d.ts +26 -29
  31. package/dist/compliance/profiles.js +62 -69
  32. package/dist/compliance/sub-processors.d.ts +28 -30
  33. package/dist/compliance/sub-processors.js +17 -21
  34. package/dist/consumer-cli.js +4 -4
  35. package/dist/crypto/blind-index.js +18 -17
  36. package/dist/crypto/ciphertext-pattern.js +1 -1
  37. package/dist/crypto/event-pii.js +3 -3
  38. package/dist/crypto/kek-source.d.ts +1 -1
  39. package/dist/crypto/kek-source.js +3 -2
  40. package/dist/crypto/kms-adapter.d.ts +1 -1
  41. package/dist/crypto/kms-adapter.js +4 -6
  42. package/dist/crypto/kms-wiring.d.ts +1 -2
  43. package/dist/crypto/kms-wiring.js +2 -2
  44. package/dist/crypto/pg-kms-adapter.js +2 -6
  45. package/dist/crypto/pii-field-encryption.js +4 -4
  46. package/dist/crypto/subject-resolver.d.ts +1 -1
  47. package/dist/crypto/subject-resolver.js +2 -2
  48. package/dist/db/apply-entity-event.d.ts +6 -6
  49. package/dist/db/apply-entity-event.js +55 -55
  50. package/dist/db/blind-index-cleanup.js +7 -7
  51. package/dist/db/collect-table-metas.js +10 -11
  52. package/dist/db/compound-types.d.ts +1 -1
  53. package/dist/db/compound-types.js +12 -12
  54. package/dist/db/cross-tenant-reads.d.ts +8 -0
  55. package/dist/db/cross-tenant-reads.js +23 -0
  56. package/dist/db/cursor.js +3 -4
  57. package/dist/db/dialect.d.ts +2 -2
  58. package/dist/db/dialect.js +2 -2
  59. package/dist/db/eagerload.d.ts +3 -3
  60. package/dist/db/eagerload.js +21 -12
  61. package/dist/db/entity-table-meta.d.ts +2 -2
  62. package/dist/db/entity-table-meta.js +4 -4
  63. package/dist/db/event-store-executor-context.d.ts +1 -1
  64. package/dist/db/event-store-executor-context.js +13 -13
  65. package/dist/db/event-store-executor-read.js +27 -28
  66. package/dist/db/event-store-executor-write.js +65 -70
  67. package/dist/db/located-timestamp.d.ts +11 -11
  68. package/dist/db/located-timestamp.js +29 -30
  69. package/dist/db/migrate-generator.d.ts +1 -1
  70. package/dist/db/migrate-generator.js +26 -19
  71. package/dist/db/migrate-runner.js +25 -25
  72. package/dist/db/money.d.ts +6 -6
  73. package/dist/db/money.js +29 -27
  74. package/dist/db/queries/backfill-pii.js +5 -5
  75. package/dist/db/queries/ddl.js +1 -1
  76. package/dist/db/queries/es-ops.d.ts +1 -1
  77. package/dist/db/queries/es-ops.js +1 -1
  78. package/dist/db/queries/event-consumer.js +3 -3
  79. package/dist/db/queries/event-store.js +4 -2
  80. package/dist/db/queries/projection-rebuild.js +3 -3
  81. package/dist/db/queries/raw-sql.d.ts +2 -2
  82. package/dist/db/queries/raw-sql.js +2 -2
  83. package/dist/db/queries/shadow-swap.js +19 -19
  84. package/dist/db/queries/test-stack.d.ts +4 -1
  85. package/dist/db/queries/test-stack.js +15 -2
  86. package/dist/db/query.d.ts +1 -1
  87. package/dist/db/query.js +6 -6
  88. package/dist/db/rebuild-marker.js +20 -21
  89. package/dist/db/replay-migration-sql.js +2 -2
  90. package/dist/db/schema-inspection.js +4 -5
  91. package/dist/db/table-builder.d.ts +1 -2
  92. package/dist/db/table-builder.js +82 -82
  93. package/dist/db/tenant-db.d.ts +2 -2
  94. package/dist/db/tenant-db.js +9 -2
  95. package/dist/derivatives/delete-stored-file.js +5 -2
  96. package/dist/derivatives/derivatives-context.js +1 -1
  97. package/dist/engine/boot-validator/access-roles.js +4 -3
  98. package/dist/engine/boot-validator/api-ext.js +14 -14
  99. package/dist/engine/boot-validator/config-deps.js +7 -6
  100. package/dist/engine/boot-validator/detail-screens.js +3 -3
  101. package/dist/engine/boot-validator/entity-handler.js +60 -61
  102. package/dist/engine/boot-validator/gdpr-storage.js +4 -4
  103. package/dist/engine/boot-validator/i18n-keys.js +1 -1
  104. package/dist/engine/boot-validator/index.d.ts +2 -2
  105. package/dist/engine/boot-validator/index.js +20 -20
  106. package/dist/engine/boot-validator/nav.js +5 -5
  107. package/dist/engine/boot-validator/ownership.js +12 -9
  108. package/dist/engine/boot-validator/parent-ref.js +1 -1
  109. package/dist/engine/boot-validator/pii-retention.js +50 -57
  110. package/dist/engine/boot-validator/projection-list-screens.js +7 -7
  111. package/dist/engine/boot-validator/query-refs.js +1 -1
  112. package/dist/engine/boot-validator/screens-actions.js +15 -16
  113. package/dist/engine/boot-validator/screens-forms.js +10 -11
  114. package/dist/engine/boot-validator/screens-lists.js +14 -15
  115. package/dist/engine/boot-validator/screens.js +34 -40
  116. package/dist/engine/boot-validator/security-baseline.js +3 -2
  117. package/dist/engine/boot-validator/transfer-graph.js +2 -2
  118. package/dist/engine/boot-validator/workspaces.js +3 -3
  119. package/dist/engine/bridge-stub.js +3 -4
  120. package/dist/engine/build-app-schema.js +75 -75
  121. package/dist/engine/build-config-feature-schema.js +50 -50
  122. package/dist/engine/build-target.js +20 -23
  123. package/dist/engine/codemod/pipeline-codemod.js +20 -13
  124. package/dist/engine/config-helpers.js +4 -4
  125. package/dist/engine/constants.js +2 -2
  126. package/dist/engine/create-app.js +2 -2
  127. package/dist/engine/define-feature.js +12 -11
  128. package/dist/engine/define-handler.js +12 -11
  129. package/dist/engine/define-step.js +1 -1
  130. package/dist/engine/define-workflow.d.ts +5 -5
  131. package/dist/engine/define-workflow.js +6 -6
  132. package/dist/engine/entity-handlers.js +13 -14
  133. package/dist/engine/event-def.js +2 -2
  134. package/dist/engine/extension-names.d.ts +16 -17
  135. package/dist/engine/extension-names.js +37 -38
  136. package/dist/engine/extensions/escape-hatch-usage.js +1 -1
  137. package/dist/engine/extensions/tenant-data.js +2 -2
  138. package/dist/engine/extensions/user-data.d.ts +23 -24
  139. package/dist/engine/factories.d.ts +14 -13
  140. package/dist/engine/factories.js +38 -37
  141. package/dist/engine/feature-ast/entity-field-types.js +1 -1
  142. package/dist/engine/feature-ast/extractors/handlers.js +1 -1
  143. package/dist/engine/feature-ast/extractors/hooks.js +1 -1
  144. package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
  145. package/dist/engine/feature-ast/extractors/shared.js +2 -3
  146. package/dist/engine/feature-ast/parse.js +13 -13
  147. package/dist/engine/feature-ast/patch.js +1 -1
  148. package/dist/engine/feature-ast/pattern-change-schema.d.ts +8 -0
  149. package/dist/engine/feature-ast/pattern-change-schema.js +1 -1
  150. package/dist/engine/feature-ast/patterns.d.ts +2 -2
  151. package/dist/engine/feature-ast/render.js +2 -2
  152. package/dist/engine/feature-config-events-jobs.js +15 -12
  153. package/dist/engine/feature-entity-handlers.js +9 -5
  154. package/dist/engine/feature-manifest.d.ts +9 -9
  155. package/dist/engine/feature-manifest.js +6 -7
  156. package/dist/engine/feature-ui-extensions.d.ts +3 -3
  157. package/dist/engine/feature-ui-extensions.js +30 -22
  158. package/dist/engine/field-access.js +5 -6
  159. package/dist/engine/index.d.ts +1 -0
  160. package/dist/engine/index.js +4 -4
  161. package/dist/engine/object-form.d.ts +1 -0
  162. package/dist/engine/object-form.js +14 -4
  163. package/dist/engine/ownership.js +3 -3
  164. package/dist/engine/parse-ref-target.js +5 -7
  165. package/dist/engine/pattern-library/index.js +2 -2
  166. package/dist/engine/pattern-library/library.js +2 -2
  167. package/dist/engine/pattern-library/types.js +3 -3
  168. package/dist/engine/pipeline.js +2 -2
  169. package/dist/engine/project-app-schema-for-roles.js +1 -1
  170. package/dist/engine/projection-helpers.js +9 -9
  171. package/dist/engine/qualified-name.js +2 -2
  172. package/dist/engine/registry-facade.js +1 -1
  173. package/dist/engine/registry-ingest.js +12 -12
  174. package/dist/engine/registry-state.js +17 -19
  175. package/dist/engine/registry-validate.js +12 -12
  176. package/dist/engine/run-in.js +2 -2
  177. package/dist/engine/run-pipeline.js +7 -0
  178. package/dist/engine/schema-builder.js +40 -38
  179. package/dist/engine/screen-filter-ops.js +17 -20
  180. package/dist/engine/screen-helpers.js +4 -3
  181. package/dist/engine/state-machine.d.ts +12 -12
  182. package/dist/engine/state-machine.js +7 -7
  183. package/dist/engine/steps/_resolver-utils.js +4 -3
  184. package/dist/engine/steps/for-each.js +4 -4
  185. package/dist/engine/steps/unsafe-projection-delete.js +1 -1
  186. package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
  187. package/dist/engine/steps/wait-for-event.js +4 -4
  188. package/dist/engine/steps/workflow-system-events.js +3 -3
  189. package/dist/engine/tier-resolver-extension.d.ts +29 -27
  190. package/dist/engine/tier-resolver-extension.js +26 -26
  191. package/dist/engine/transfer-adjacency.js +2 -2
  192. package/dist/engine/validate-projection-allowlist.js +7 -27
  193. package/dist/engine/walk-step-instances.d.ts +2 -0
  194. package/dist/engine/walk-step-instances.js +38 -0
  195. package/dist/engine/where-rule-lint.js +1 -1
  196. package/dist/engine/write-handler-qn-extract.js +1 -1
  197. package/dist/entrypoint/index.js +3 -3
  198. package/dist/env/index.d.ts +7 -6
  199. package/dist/env/index.js +10 -9
  200. package/dist/errors/transition-details.js +7 -7
  201. package/dist/errors/write-error-info.d.ts +6 -6
  202. package/dist/errors/write-error-info.js +6 -6
  203. package/dist/es-ops/context.d.ts +9 -9
  204. package/dist/es-ops/context.js +28 -28
  205. package/dist/es-ops/index.js +7 -9
  206. package/dist/es-ops/operations-schema.js +4 -10
  207. package/dist/es-ops/runner.d.ts +11 -11
  208. package/dist/es-ops/runner.js +49 -53
  209. package/dist/es-ops/types.d.ts +55 -55
  210. package/dist/es-ops/types.js +11 -11
  211. package/dist/event-store/admin-api.js +6 -7
  212. package/dist/event-store/errors.js +1 -1
  213. package/dist/event-store/event-store.js +2 -2
  214. package/dist/event-store/rebuild-dead-letter.js +1 -1
  215. package/dist/files/feature.js +9 -10
  216. package/dist/files/file-handle.js +5 -6
  217. package/dist/files/file-ref-entity.js +15 -15
  218. package/dist/files/file-ref-table.js +7 -8
  219. package/dist/files/file-routes.d.ts +7 -0
  220. package/dist/files/file-routes.js +11 -5
  221. package/dist/files/in-memory-provider.js +8 -9
  222. package/dist/files/local-provider.js +18 -25
  223. package/dist/files/provider-resolver.d.ts +7 -7
  224. package/dist/files/storage-tracking.js +2 -2
  225. package/dist/files/zip-stream.d.ts +12 -19
  226. package/dist/files/zip-stream.js +44 -63
  227. package/dist/i18n/required-surface-keys.d.ts +1 -1
  228. package/dist/i18n/required-surface-keys.js +8 -8
  229. package/dist/internal/stack-support.d.ts +1 -1
  230. package/dist/internal/stack-support.js +1 -1
  231. package/dist/jobs/job-runner-helpers.d.ts +1 -2
  232. package/dist/jobs/job-runner-helpers.js +8 -18
  233. package/dist/jobs/job-runner-schedulers.d.ts +1 -1
  234. package/dist/jobs/job-runner-schedulers.js +3 -4
  235. package/dist/jobs/job-runner.js +17 -18
  236. package/dist/lifecycle/lifecycle.js +3 -5
  237. package/dist/lifecycle/signal-handlers.js +5 -4
  238. package/dist/logging/utils.d.ts +2 -1
  239. package/dist/logging/utils.js +38 -10
  240. package/dist/migrations/index.js +1 -1
  241. package/dist/migrations/kumiko-drift.js +14 -13
  242. package/dist/migrations/pending-rebuilds.d.ts +21 -20
  243. package/dist/migrations/pending-rebuilds.js +27 -27
  244. package/dist/migrations/projection-table-index.d.ts +3 -3
  245. package/dist/migrations/projection-table-index.js +8 -8
  246. package/dist/observability/metric-validator.js +1 -1
  247. package/dist/observability/metrics-handle.js +3 -3
  248. package/dist/pipeline/append-event-core.js +11 -2
  249. package/dist/pipeline/cascade-handler.d.ts +1 -1
  250. package/dist/pipeline/dispatch-batch.js +2 -2
  251. package/dist/pipeline/dispatch-query.js +10 -10
  252. package/dist/pipeline/dispatch-shared.d.ts +1 -0
  253. package/dist/pipeline/dispatch-shared.js +42 -19
  254. package/dist/pipeline/dispatch-stream.js +1 -1
  255. package/dist/pipeline/dispatch-write.js +3 -3
  256. package/dist/pipeline/dispatcher-utils.d.ts +1 -1
  257. package/dist/pipeline/dispatcher-utils.js +1 -1
  258. package/dist/pipeline/event-consumer-state.js +2 -2
  259. package/dist/pipeline/event-dispatcher-delivery.js +1 -1
  260. package/dist/pipeline/event-dispatcher.js +17 -28
  261. package/dist/pipeline/irreversible-operation-gate.js +1 -1
  262. package/dist/pipeline/lifecycle-pipeline.js +9 -9
  263. package/dist/pipeline/member-read-only-transaction.js +1 -3
  264. package/dist/pipeline/msp-rebuild.js +1 -1
  265. package/dist/pipeline/projection-rebuild.js +13 -12
  266. package/dist/pipeline/system-hooks.js +39 -41
  267. package/dist/pipeline/system-identity-switch.js +1 -3
  268. package/dist/pipeline/tenant-timezone-cache.js +1 -1
  269. package/dist/random/generate.d.ts +29 -29
  270. package/dist/random/generate.js +30 -30
  271. package/dist/random/words.js +20 -20
  272. package/dist/rate-limit/middleware.js +9 -5
  273. package/dist/redis/client.js +2 -2
  274. package/dist/redis/pubsub-signal.js +14 -17
  275. package/dist/schema-cli.js +116 -22
  276. package/dist/search/index.js +4 -4
  277. package/dist/search/purge-subject.js +4 -5
  278. package/dist/search/reindex-entity.js +6 -9
  279. package/dist/time/boot-tz-warning.d.ts +2 -2
  280. package/dist/time/boot-tz-warning.js +8 -8
  281. package/dist/time/iana.js +10 -10
  282. package/dist/time/index.js +9 -9
  283. package/dist/time/legacy-date.d.ts +2 -2
  284. package/dist/time/legacy-date.js +6 -8
  285. package/dist/time/polyfill.js +1 -1
  286. package/dist/time/tz-context.d.ts +1 -1
  287. package/dist/time/tz-context.js +1 -1
  288. package/dist/ui-types/app-schema.js +7 -7
  289. package/dist/ui-types/list-row-meta.js +4 -4
  290. package/dist/upgrade-cli.js +10 -9
  291. package/dist/utils/compare.js +1 -1
  292. package/package.json +4 -4
  293. package/src/changes.json +54 -0
  294. package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
  295. package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
  296. package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
  297. package/src/scripts/codemod/pii-personal-migration.ts +17 -8
  298. package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
@@ -23,7 +23,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
23
23
  // features (a declarative list in the entity's owning feature navigates to
24
24
  // the consumer app's custom editors this way). The validator mirrors that:
25
25
  // same feature OR any feature. redirect/cancelTarget additionally accept a
26
- // fully qualified cross-feature QN (resolveScreenTargetQn, #1946); short IDs
26
+ // fully qualified cross-feature QN (resolveScreenTargetQn); short IDs
27
27
  // stay same-feature as before.
28
28
  const navTargetShortIds = screenShortIdsFrom(allScreenQns);
29
29
  const navigateLookups = {
@@ -91,7 +91,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
91
91
  assertRefTargetRegistered(`[Feature ${feature.name}] Screen "${screenId}" (projectionList)`, `column "${normalizedCol.field}" (refEntity)`, normalizedCol.refEntity, feature.name, featureMap);
92
92
  }
93
93
  }
94
- // Screen filter (fw#2224) — field existence can't be checked without
94
+ // Screen filter — field existence can't be checked without
95
95
  // an entity (columns aren't a complete field inventory of the
96
96
  // underlying query), so only pin the structure: "in" requires an
97
97
  // array. Field validity is documented in the PR body.
@@ -156,7 +156,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
156
156
  `is no row id to send under idParam. Remove idParam.`);
157
157
  }
158
158
  // singleton has no row id in the path, but the auto-generated "Edit"
159
- // action (fw#2166, defaultEditAction in kumiko-screen.tsx) navigates
159
+ // action (defaultEditAction in kumiko-screen.tsx) navigates
160
160
  // to the detailFor entity's entityEdit screen using that path id —
161
161
  // under singleton it would silently open that screen in CREATE mode
162
162
  // instead of editing the singleton row. Reject rather than let that
@@ -285,7 +285,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
285
285
  // extension that persists through the host's Save button has
286
286
  // nothing to save against. Extensions that persist themselves
287
287
  // (e.g. notes-history NotesSection, via their own dispatcher
288
- // writes) are fine (solon#264).
288
+ // writes) are fine.
289
289
  if (section.contributesToFormSubmit === true) {
290
290
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) extension section ` +
291
291
  `"${section.title}" sets contributesToFormSubmit: true, but projectionDetail has no ` +
@@ -420,9 +420,8 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
420
420
  continue;
421
421
  }
422
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.
423
+ // Validate configEdit layout and fields like actionForm, then verify that
424
+ // every qualified config key is registered and its scope matches the key.
426
425
  const fieldNames = new Set(Object.keys(screen.fields));
427
426
  if (fieldNames.size === 0) {
428
427
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) has empty fields map — ` +
@@ -473,8 +472,8 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
473
472
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) secretKeys names unknown field "${fname}".`);
474
473
  }
475
474
  }
476
- // configKeys: jeder fieldName muss einen Mapping-Eintrag haben,
477
- // jeder qualifizierte Key muss in der Registry existieren.
475
+ // configKeys: every fieldName must have a mapping entry, and every
476
+ // qualified key must exist in the registry.
478
477
  for (const fname of fieldNames) {
479
478
  const secretQn = screen.secretKeys?.[fname];
480
479
  if (secretQn !== undefined) {
@@ -593,9 +592,8 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
593
592
  assertRefTargetRegistered(`[Feature ${feature.name}] Screen "${screenId}" (entityList)`, `column "${normalized.field}" (refEntity)`, normalized.refEntity, feature.name, featureMap);
594
593
  }
595
594
  }
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.
595
+ // Validate pagination, sorting, and search at boot so authoring errors
596
+ // fail before runtime debugging of empty or incorrectly sorted lists.
599
597
  if (screen.pageSize !== undefined && screen.pageSize <= 0) {
600
598
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) has pageSize=${screen.pageSize} — ` +
601
599
  `must be a positive integer.`);
@@ -606,15 +604,13 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
606
604
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) defaultSort references unknown ` +
607
605
  `field "${sortField}". Known fields: ${[...fieldNames].sort().join(", ")}`);
608
606
  }
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).
607
+ // `sortable: true` is required so the UI never sorts on a column that has
608
+ // no server-side DB index or that the schema deliberately keeps unsortable
609
+ // (audit fields, computed values). `sortable` currently
610
+ // exists only on TextFieldDef; the `in` narrowing also permits other
611
+ // field types without an explicit flag, which is acceptable because
612
+ // Number/Date fields are naturally sortable. Authors can mark them
613
+ // sortable when their field type supports it.
618
614
  const fieldDef = entityDef.fields[sortField];
619
615
  const isSortable = fieldDef !== undefined && "sortable" in fieldDef && fieldDef.sortable === true;
620
616
  if (!isSortable) {
@@ -628,15 +624,14 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
628
624
  `"${screen.createScreen}" does not resolve to a registered screen in this feature. ` +
629
625
  `Known screens in this feature: ${Object.keys(feature.screens).sort().join(", ") || "(none)"}.`);
630
626
  }
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.
627
+ // Screen filters are checked in three layers:
628
+ // 1) The field exists on the entity; a typo would otherwise produce an
629
+ // empty list instead of a clear boot failure.
630
+ // 2) The field has `filterable: true`, preventing accidental filtering
631
+ // of audit, computed, or encrypted fields.
632
+ // 3) The operation matches the field type, so lt/gt on text fields
633
+ // fails at boot instead of causing surprising runtime behavior.
634
+ // Additionally, "in" requires a readonly array.
640
635
  if (screen.filter !== undefined) {
641
636
  const filterField = screen.filter.field;
642
637
  if (!fieldNames.has(filterField)) {
@@ -668,9 +663,9 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
668
663
  `filter.value to be a readonly array. Pass an array as filter.value.`);
669
664
  }
670
665
  }
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.
666
+ // Pin rowActions at boot: a navigate target must exist in the same feature
667
+ // and a writeHandler QN must be registered. Typos would otherwise only
668
+ // surface on the first click as "Screen not found" or a 404.
674
669
  if (screen.rowActions !== undefined) {
675
670
  for (const action of screen.rowActions) {
676
671
  rejectRedirectOnListRowAction(feature.name, screenId, "entityList", action);
@@ -681,7 +676,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
681
676
  // the id for a screen owned by a different feature. Cross-feature
682
677
  // + entityEdit therefore MUST set an explicit entityId, or the
683
678
  // edit screen silently opens with no entity context at runtime.
684
- // Entity-targets (fw#2228) are exempt: the renderer always
679
+ // Entity-targets are exempt: the renderer always
685
680
  // supplies an id for them (explicit entityId, else row["id"]),
686
681
  // regardless of which feature the resolved detailFor screen
687
682
  // belongs to.
@@ -730,9 +725,8 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
730
725
  }
731
726
  validateAtMostOneRowClick(feature.name, screenId, "entityList", screen.rowActions);
732
727
  }
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.
728
+ // toolbarActions, analogous to rowActions: misspelled navigate targets and
729
+ // unregistered writeHandler QNs fail at boot instead of on the first click.
736
730
  if (screen.toolbarActions !== undefined) {
737
731
  for (const action of screen.toolbarActions) {
738
732
  if (action.kind === "navigate") {
@@ -895,9 +889,9 @@ export function collectScreenQns(features) {
895
889
  }
896
890
  return set;
897
891
  }
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.
892
+ // Extract bare screen IDs (without the `<feature>:screen:` prefix) from
893
+ // qualified QNs for app-wide navigate-target resolution (see validateScreens).
894
+ // This mirrors the runtime router, which matches bare IDs across features.
901
895
  export function screenShortIdsFrom(allScreenQns) {
902
896
  const marker = ":screen:";
903
897
  const set = new Set();
@@ -1,3 +1,5 @@
1
+ import { createFallbackLogger } from "../../logging/utils.js";
2
+ const log = createFallbackLogger("kumiko:boot");
1
3
  export const SECURITY_BASELINE_FEATURE_NAMES = [
2
4
  "sessions",
3
5
  "crypto-shredding",
@@ -13,6 +15,5 @@ export function warnOnMissingSecurityBaseline(features, nodeEnv = process.env["N
13
15
  // skip: every baseline feature is mounted
14
16
  if (missing.length === 0)
15
17
  return;
16
- // biome-ignore lint/suspicious/noConsole: boot-time hint, no logger available yet
17
- console.warn(`[kumiko:boot] NODE_ENV=production but the security baseline is incomplete — missing feature(s): ${missing.join(", ")}. Mount securityBaselineFeatures() from @cosmicdrift/kumiko-bundled-features/presets (or the missing features individually).`);
18
+ log.warn(`NODE_ENV=production but the security baseline is incomplete — missing feature(s): ${missing.join(", ")}. Mount securityBaselineFeatures() from @cosmicdrift/kumiko-bundled-features/presets (or the missing features individually).`);
18
19
  }
@@ -1,12 +1,12 @@
1
1
  import { parseRefTargetEntityName } from "../parse-ref-target.js";
2
2
  import { findOverDeepTransferChain, MAX_TRANSFER_DEPTH, resolveTransferAdjacency, } from "../transfer-adjacency.js";
3
- // --- Transfer-graph boot validation (fw#3088) ---
3
+ // --- Transfer-graph boot validation ---
4
4
  //
5
5
  // tenant-handover walks an entity graph across the tenant boundary along two
6
6
  // declared edge kinds: `parentRef` and plain `reference` fields. Two shapes
7
7
  // cannot be walked, and both have to fail loudly here rather than silently
8
8
  // leaving rows behind in the source tenant — the partial move is exactly what
9
- // #3088 exists to end.
9
+ // this validation exists to end.
10
10
  //
11
11
  // Scoped to entities declaring `transferable: true`, so a consumer that has
12
12
  // one of these shapes but never hands that entity over is unaffected.
@@ -33,9 +33,9 @@ export function validateWorkspaces(feature, allNavQns) {
33
33
  }
34
34
  }
35
35
  const SETTINGS_HUB_AUDIENCE_NAV_QN_SET = new Set(SETTINGS_HUB_AUDIENCE_NAV_QNS);
36
- // fw#3019: apps that assign navs to workspaces via `r.workspace({ nav })`
37
- // or `r.nav({ workspaces })` never pass an explicit navAllowlist (solon#113)
38
- // — without this, warnOnUnreachableNavScreens never runs for them. Both
36
+ // Apps that assign navs to workspaces via `r.workspace({ nav })`
37
+ // or `r.nav({ workspaces })` never pass an explicit navAllowlist — without
38
+ // this, warnOnUnreachableNavScreens never runs for them. Both
39
39
  // `WorkspaceDefinition.nav` and `NavDefinition.workspaces` already hold
40
40
  // fully-qualified QNs (validateWorkspaces/validateNavs compare them
41
41
  // directly against allNavQns/allWorkspaceQns), so no re-qualification is
@@ -12,7 +12,7 @@ import { createAnonymousUser } from "./system-user.js";
12
12
  // anything downstream accidentally reaches for them.
13
13
  //
14
14
  // Use: `{ db, registry, ...bridgeStub() }`
15
- const notAvailable = (what) => async () => {
15
+ const notAvailable = (what) => async (..._args) => {
16
16
  throw new Error(`ctx.${what} not available in this context — use the dispatcher, not a stubbed handler context`);
17
17
  };
18
18
  // Noop observability — hand back the shared fallback tracer so ctx.tracer has
@@ -24,11 +24,10 @@ export function bridgeStub(opts) {
24
24
  // anonymous default values when no user is passed explicitly. Test code
25
25
  // that cares about identity passes its own SessionUser here and gets it
26
26
  // back on ctx.
27
- const stubUser = opts?.user ??
28
- createAnonymousUser("00000000-0000-0000-0000-000000000000"); // @cast-boundary engine-bridge
27
+ const stubUser = opts?.user ?? createAnonymousUser("00000000-0000-0000-0000-000000000000");
29
28
  return {
30
29
  user: stubUser,
31
- query: notAvailable("query"), // @cast-boundary engine-bridge
30
+ query: notAvailable("query"),
32
31
  queryAs: notAvailable("queryAs"),
33
32
  write: notAvailable("write"),
34
33
  writeAs: notAvailable("writeAs"),
@@ -1,22 +1,22 @@
1
- // Erzeugt aus einer Server-Registry das client-safe AppSchema das die
2
- // Browser-Renderer-Pipeline konsumiert. Genauer Zweck: dev-server kann
3
- // die ganze hand-geschriebene `clientSchema`-Spiegelung abschaffen, der
4
- // Server schickt einfach das aufgelöste AppSchema beim Boot mit.
1
+ // Builds the client-safe AppSchema from a server registry for the
2
+ // browser renderer pipeline. The dev server can therefore drop its entire
3
+ // hand-written `clientSchema` mirror and simply send the resolved AppSchema
4
+ // during boot.
5
5
  //
6
- // JSON-Safety: Wir projezieren explizit auf eine Whitelist statt
7
- // JSON.stringify-roundtripping. Functions würden silent gedroppt und
8
- // Zod-Schemas (v4 hat .toJSON, aber emittiert ein _zod-Envelope) würden
9
- // als komische Ghost-Properties auftauchen. Das hier ist ein bewusster
10
- // Vertrag: was die Browser-Renderer-Pipeline liest, taucht hier auf.
11
- // Neue Browser-needed-Fields müssen explizit erweitert werden.
6
+ // JSON safety: project explicitly onto a whitelist instead of round-tripping
7
+ // through JSON.stringify. Functions would be silently dropped, and Zod schemas
8
+ // (v4 has .toJSON, but emits a _zod envelope) would appear as strange ghost
9
+ // properties. This is an intentional contract: everything read by the browser
10
+ // renderer pipeline must appear here. New fields needed by the browser must be
11
+ // added explicitly.
12
12
  //
13
- // Aktuell projeziert (Stand 2026-04-25):
13
+ // Currently projected:
14
14
  // Entity: { table?, fields: { type, required?, sortable?, default? } }
15
- // Screen: verbatim (ScreenDefinition ist von Haus aus JSON-safe —
16
- // custom-screens haben keine functions, layout/columns/etc.
17
- // sind plain literals)
18
- // Nav: verbatim (NavDefinition ist nur strings + literals)
19
- // Workspace: verbatim definition + getWorkspaceNavs() von der Registry
15
+ // Screen: verbatim (ScreenDefinition is JSON-safe by design —
16
+ // custom screens have no functions; layout/columns/etc. are
17
+ // plain literals)
18
+ // Nav: verbatim (NavDefinition contains only strings + literals)
19
+ // Workspace: verbatim definition + getWorkspaceNavs() from the registry
20
20
  // Translations: verbatim feature.translations (raw r.translations keys,
21
21
  // unfiltered — see FeatureSchema.translations doc)
22
22
  //
@@ -24,11 +24,13 @@
24
24
  // The schema route strips features the caller's tenant has switched off
25
25
  // (r.toggleable) via stripDisabledFeaturesFromAppSchema.
26
26
  import { ZodObject } from "zod";
27
+ import { createFallbackLogger } from "../logging/utils.js";
27
28
  import { collectUrlPrefillFieldsByScreenQn } from "./boot-validator/url-prefill-fields.js";
28
29
  import { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE, } from "./build-config-feature-schema.js";
29
30
  import { isEncryptedAtRest } from "./config-helpers.js";
30
31
  import { isListQueryWithoutSearchableFields } from "./entity-list-searchability.js";
31
32
  import { QnTypes, qualifyEntityName } from "./qualified-name.js";
33
+ const log = createFallbackLogger("kumiko");
32
34
  export function buildAppSchema(registry, options = {}) {
33
35
  const features = [];
34
36
  const urlPrefillFieldsByScreenQn = collectUrlPrefillFieldsByScreenQn([
@@ -52,7 +54,7 @@ export function buildAppSchema(registry, options = {}) {
52
54
  screens: projectScreens(featureName, feature.screens, registry, urlPrefillFieldsByScreenQn, encryptedConfigKeyQns),
53
55
  ...(navs.length > 0 && { navs }),
54
56
  ...(contentCollections.length > 0 && { contentCollections }),
55
- // #1059: verbatim r.translations({keys}) — see FeatureSchema.translations
57
+ // Verbatim r.translations({keys}) — see FeatureSchema.translations
56
58
  // doc for why this must NOT go through registry.getAllTranslations()
57
59
  // (double-prefixes features that already qualify their own keys).
58
60
  ...(Object.keys(feature.translations ?? {}).length > 0 && {
@@ -62,12 +64,12 @@ export function buildAppSchema(registry, options = {}) {
62
64
  };
63
65
  features.push(featureSchema);
64
66
  }
65
- // Workspaces: getAllWorkspaces() liefert mit QUALIFIZIERTEN ids (das
66
- // schreibt die Registry beim Store-Overwrite ein). Die Browser-
67
- // Renderer erwartet aber kurze ids (matcht gegen URL-Segment, gegen
68
- // navigate({ workspaceId })). Wir gehen direkt durch `feature.workspaces`
69
- // — dort sind die ids noch in der Autor-Form (short) — und ziehen die
70
- // pre-resolved navMembers aus der Registry.
67
+ // Workspaces: getAllWorkspaces() returns QUALIFIED ids (the Registry writes
68
+ // them during store overwrite). The browser renderer expects short ids
69
+ // (matching the URL segment and navigate({ workspaceId })). We therefore
70
+ // read directly from `feature.workspaces`, where the ids are still in the
71
+ // author's short form, and obtain the pre-resolved navMembers from the
72
+ // registry.
71
73
  let workspaces = [];
72
74
  for (const [featureName, feature] of registry.features) {
73
75
  for (const [shortId, definition] of Object.entries(feature.workspaces)) {
@@ -78,16 +80,16 @@ export function buildAppSchema(registry, options = {}) {
78
80
  });
79
81
  }
80
82
  }
81
- // Self-Populating Settings-Hub: aus den deklarierten Config-Keys mit `mask`
82
- // werden Screens/Navs (+ eine Workspace) abgeleitet und hier eingehängt —
83
- // kein manuelles r.screen/r.nav am App-Author.
83
+ // Self-populating Settings Hub: screens/navs (plus one workspace) are
84
+ // derived from the declared config keys with `mask` and attached here,
85
+ // so the app author does not need manual r.screen/r.nav declarations.
84
86
  const appHadWorkspaces = workspaces.length > 0;
85
87
  const generated = buildConfigFeatureSchema(registry);
86
88
  if (generated.screens.length > 0) {
87
89
  mergeSettingsHubIntoConfigFeature(features, generated);
88
- // Flip-Schutz: nur für Apps die schon Workspaces nutzen. Bei einer
89
- // workspace-losen App bleibt app.workspaces undefined → der Renderer zeigt
90
- // alle Navs ungefiltert, die Hub-Navs inklusive.
90
+ // Flip protection: applies only to apps that already use workspaces. For an
91
+ // app without workspaces, app.workspaces remains undefined, so the renderer
92
+ // shows all navs without filtering, including the Hub navs.
91
93
  if (appHadWorkspaces) {
92
94
  const placed = placeSettingsHub(workspaces, generated);
93
95
  workspaces = placed.workspaces;
@@ -109,16 +111,16 @@ export function buildAppSchema(registry, options = {}) {
109
111
  // instead so a leaked function renderer is actually caught.
110
112
  const offender = findNonJsonSafePath(schema, "schema");
111
113
  if (offender !== null) {
112
- // biome-ignore lint/suspicious/noConsole: dev-only assertion
113
- console.error(`[kumiko] buildAppSchema: Output ist nicht JSON-safe — nicht-serialisierbarer Wert bei "${offender}" (Funktions-Renderer oder Klassen-Instanz im Schema?).`, schema);
114
+ log.error(`buildAppSchema: Output ist nicht JSON-safe — nicht-serialisierbarer Wert bei "${offender}" (Funktions-Renderer oder Klassen-Instanz im Schema?).`, { offender });
114
115
  }
115
116
  }
116
117
  return schema;
117
118
  }
118
- // Hängt die generierten Hub-Screens/Navs an die config-FeatureSchema (qualified
119
- // dann als config:screen:* / config:nav:*). Existiert sie noch nicht (config
120
- // bundled-feature nicht gemountet), wird sie angelegt. find-or-create statt
121
- // fixem Push verhindert eine zweite FeatureSchema mit demselben featureName.
119
+ // Attaches the generated Hub screens/navs to the config FeatureSchema
120
+ // (qualified as config:screen:* / config:nav:*). If it does not yet exist
121
+ // (because the config bundled feature is not mounted), it is created. Using
122
+ // find-or-create instead of an unconditional push prevents a second
123
+ // FeatureSchema with the same featureName.
122
124
  function mergeSettingsHubIntoConfigFeature(features, generated) {
123
125
  const existing = features.find((f) => f.featureName === SETTINGS_HUB_FEATURE);
124
126
  if (existing === undefined) {
@@ -202,8 +204,7 @@ function warnUnplacedAudiences(unplaced) {
202
204
  // skip: every audience placed — nothing to warn about
203
205
  if (unplaced.length === 0)
204
206
  return;
205
- // biome-ignore lint/suspicious/noConsole: dev-only authoring hint
206
- console.warn(`[kumiko] Settings-Hub: ${unplaced.join(", ")} nicht in einer App-Workspace platziert — ` +
207
+ log.warn(`Settings-Hub: ${unplaced.join(", ")} nicht in einer App-Workspace platziert — ` +
207
208
  `erscheint im Standalone-"Einstellungen"-Tab. Referenziere ` +
208
209
  `${unplaced.map((id) => `${SETTINGS_HUB_FEATURE}:nav:${id}`).join(", ")} ` +
209
210
  `in einer r.workspace.nav, um die Gruppe inline zu zeigen.`);
@@ -212,8 +213,7 @@ function warnDanglingAudienceRefs(dangling) {
212
213
  // skip: no dangling refs — nothing to warn about
213
214
  if (dangling.length === 0)
214
215
  return;
215
- // biome-ignore lint/suspicious/noConsole: dev-only authoring hint
216
- console.warn(`[kumiko] Settings-Hub: ${dangling
216
+ log.warn(`Settings-Hub: ${dangling
217
217
  .map((id) => `${SETTINGS_HUB_FEATURE}:nav:${id}`)
218
218
  .join(", ")} in einer Workspace referenziert, aber nie generiert — ` +
219
219
  `keine Config-Keys für diesen Scope registriert. Tippfehler oder ` +
@@ -260,8 +260,8 @@ export function findNonJsonSafePath(value, path) {
260
260
  }
261
261
  // projectionList screens don't declare searchable/sortable/paginated as
262
262
  // authoring intent — they're derived here from the bound query handler's
263
- // Zod schema, the source of truth for what parameters it actually accepts
264
- // (fw#2165). A hand-written `searchable: true` still wins over the derived
263
+ // Zod schema, the source of truth for what parameters it actually accepts.
264
+ // A hand-written `searchable: true` still wins over the derived
265
265
  // default (screen.searchable ?? derived); the boot-validator (3a) rejects
266
266
  // one that contradicts the schema.
267
267
  function projectScreens(featureName, screens, registry, urlPrefillFieldsByScreenQn, encryptedConfigKeyQns) {
@@ -372,30 +372,30 @@ function projectEntities(entities) {
372
372
  }
373
373
  return out;
374
374
  }
375
- // EntityDefinition ist eine Discriminated-Union an Field-Types — wir
376
- // kennen alle JSON-safe Properties pro Field-Type. Statt jede Variante
377
- // einzeln auszuhandeln, walken wir die Field-Map und filtern auf die
378
- // Whitelist. Was nicht durchkommt: Server-only-runtime wie ZodValidate-
379
- // Functions, Computed-Functions, Default-Functions.
375
+ // EntityDefinition is a discriminated union of field types, and we know the
376
+ // JSON-safe properties for each type. Rather than handling every variant
377
+ // separately, walk the field map and filter it through the whitelist. Values
378
+ // that do not pass through include server-only runtime values such as
379
+ // ZodValidate functions, computed functions, and default functions.
380
380
  function projectEntity(entityName, entity) {
381
381
  const fieldsOut = {};
382
382
  for (const [fieldName, fieldDef] of Object.entries(entity.fields)) {
383
383
  assertValidMultilineRows(`Entity "${entityName}"`, fieldName, fieldDef);
384
384
  fieldsOut[fieldName] = projectField(fieldDef);
385
385
  }
386
- // derivedFields MÜSSEN mit ins Client-Schema (nur die Metadaten, nicht die
387
- // derive-fn): computeListViewModel löst eine entityList-Spalte über
388
- // `entity.derivedFields[field].valueType` auf — fehlt der Eintrag, wirft es
389
- // "references unknown field". Der executor hat den Wert server-seitig schon
390
- // an die Row gehängt; der Client braucht nur den valueType für den Renderer.
386
+ // derivedFields MUST be included in the client schema (metadata only, not the
387
+ // derive function): computeListViewModel resolves an entityList column via
388
+ // `entity.derivedFields[field].valueType`; without the entry it throws
389
+ // "references unknown field". The executor has already attached the value
390
+ // server-side to the row; the client only needs valueType for rendering.
391
391
  const derivedOut = {};
392
392
  for (const [name, derivedDef] of Object.entries(entity.derivedFields ?? {})) {
393
393
  derivedOut[name] = projectDerivedField(derivedDef);
394
394
  }
395
- // EntityDefinition akzeptiert idType/access/searchWeight als optional —
396
- // wir lassen die weg weil der Browser-Renderer sie nicht liest. `table`
397
- // schicken wir mit, falls Apps `entity.table` direkt referenzieren.
398
- // Kein Cast nötig: alle weggelassenen Felder sind `?`-optional.
395
+ // EntityDefinition accepts idType/access/searchWeight as optional, but we omit
396
+ // them because the browser renderer does not read them. `table` is included
397
+ // in case apps reference `entity.table` directly.
398
+ // No cast is needed: all omitted fields are optional (`?`).
399
399
  return {
400
400
  fields: fieldsOut,
401
401
  // @cast-boundary schema-walk: EntityDefinition.derivedFields is typed for
@@ -413,10 +413,10 @@ function projectEntity(entityName, entity) {
413
413
  ...(entity.lockedWhen !== undefined && { lockedWhen: entity.lockedWhen }),
414
414
  };
415
415
  }
416
- // Nur valueType durch — die derive-fn ist Server-only und NICHT JSON-safe
417
- // (würde sonst die Output-Walk-Guard triggern). Gibt ehrlich
418
- // ClientDerivedFieldDef zurück statt eine DerivedFieldDef vorzutäuschen,
419
- // die keine derive-fn hat.
416
+ // Only valueType is forwarded — the derive function is server-only and is not
417
+ // JSON-safe (it would otherwise trigger the output-walk guard). Return
418
+ // ClientDerivedFieldDef honestly instead of pretending to return a
419
+ // DerivedFieldDef without its derive function.
420
420
  function projectDerivedField(derivedDef) {
421
421
  return { valueType: derivedDef.valueType };
422
422
  }
@@ -470,28 +470,28 @@ function projectField(fieldDef) {
470
470
  out["required"] = def["required"];
471
471
  if (typeof def["sortable"] === "boolean")
472
472
  out["sortable"] = def["sortable"];
473
- // filterable steuert die Faceted-Filter-Dropdowns im Renderer (select/
474
- // boolean) — muss daher ins Client-Schema.
473
+ // filterable controls the faceted filter dropdowns in the renderer (select/
474
+ // boolean), so it must be included in the client schema.
475
475
  if (typeof def["filterable"] === "boolean")
476
476
  out["filterable"] = def["filterable"];
477
477
  if (typeof def["searchable"] === "boolean")
478
478
  out["searchable"] = def["searchable"];
479
479
  if (isJsonSafeValue(def["default"]))
480
480
  out["default"] = def["default"];
481
- // Select: options-Liste ist plain JSON, durchschicken.
481
+ // Select: the options list is plain JSON, so forward it.
482
482
  if (Array.isArray(def["options"]))
483
483
  out["options"] = def["options"];
484
484
  if (isJsonSafeObject(def["optionTones"]))
485
485
  out["optionTones"] = def["optionTones"];
486
- // Reference: entity-Target + labelField + multiple müssen zum Renderer.
487
- // Der ReferenceInput baut die Options-Query aus refEntity/refFeature und
488
- // resolved das Label über labelField — ohne diese Properties fällt das
489
- // Dropdown leer aus (QN wird `<feature>:query::list` → 404).
486
+ // Reference: entity target, labelField, and multiple are required by the
487
+ // renderer. ReferenceInput builds the options query from refEntity/refFeature
488
+ // and resolves the label through labelField; without these properties, the
489
+ // dropdown is empty (the QN becomes `<feature>:query::list` → 404).
490
490
  if (typeof def["entity"] === "string")
491
491
  out["entity"] = def["entity"];
492
492
  if (typeof def["labelField"] === "string")
493
493
  out["labelField"] = def["labelField"];
494
- // fw#2780: author-supplied picker source — without it the client falls back to the list handler.
494
+ // Author-supplied picker source — without it the client falls back to the list handler.
495
495
  if (typeof def["optionsQuery"] === "string")
496
496
  out["optionsQuery"] = def["optionsQuery"];
497
497
  // Select: tier/tenant availability of the static options, loaded by the renderer.
@@ -504,7 +504,7 @@ function projectField(fieldDef) {
504
504
  out["multiple"] = def["multiple"];
505
505
  // MultiSelect: display picks checkboxes vs. combobox in the renderer,
506
506
  // columns/maxRows size the checkbox grid — without these the renderer
507
- // always falls back to the combobox (fw#2494). Select: display picks
507
+ // always falls back to the combobox. Select: display picks
508
508
  // "radio" | "dropdown" the same way.
509
509
  if (typeof def["display"] === "string")
510
510
  out["display"] = def["display"];
@@ -513,19 +513,19 @@ function projectField(fieldDef) {
513
513
  if (typeof def["maxRows"] === "number")
514
514
  out["maxRows"] = def["maxRows"];
515
515
  // text/longText: textarea row count — DefaultInput renders a single-line
516
- // input otherwise (fw#2497).
516
+ // input otherwise.
517
517
  if (typeof def["multiline"] === "boolean" || isJsonSafeObject(def["multiline"]))
518
518
  out["multiline"] = def["multiline"];
519
519
  // number: Zod write-boundary bounds. date/timestamp/locatedTimestamp:
520
- // ISO-string picker bounds — same keys, different literal type (fw#2497).
520
+ // ISO-string picker bounds — same keys, different literal type.
521
521
  if (typeof def["min"] === "number" || typeof def["min"] === "string")
522
522
  out["min"] = def["min"];
523
523
  if (typeof def["max"] === "number" || typeof def["max"] === "string")
524
524
  out["max"] = def["max"];
525
- // date/timestamp/locatedTimestamp: display/parsing locale override (fw#2497).
525
+ // date/timestamp/locatedTimestamp: display/parsing locale override.
526
526
  if (typeof def["locale"] === "string")
527
527
  out["locale"] = def["locale"];
528
- // image/images: which camera a mobile capture opens (fw#2497).
528
+ // image/images: which camera a mobile capture opens.
529
529
  if (typeof def["capture"] === "string")
530
530
  out["capture"] = def["capture"];
531
531
  // number: display-only suffix, static or read from a sibling field of the row.
@@ -559,13 +559,13 @@ function projectField(fieldDef) {
559
559
  // decimal, incl. embedded sub-fields: rounding of derived embedded-list cells.
560
560
  if (typeof def["scale"] === "number")
561
561
  out["scale"] = def["scale"];
562
- // money: declared currency source (MoneyCurrencySource, fw#2933) — without
562
+ // money: declared currency source (MoneyCurrencySource) — without
563
563
  // it the renderer can't tell a "currency: { kind: 'tenant' }" field apart
564
564
  // from an undeclared one and always falls back to entity.defaultCurrency.
565
565
  if (isJsonSafeObject(def["currency"]))
566
566
  out["currency"] = def["currency"];
567
567
  // embedded lists: row-count bounds, computed cells, totals row, and the
568
- // sibling-money-field totals check (fw#2497).
568
+ // sibling-money-field totals check.
569
569
  if (typeof def["minItems"] === "number")
570
570
  out["minItems"] = def["minItems"];
571
571
  if (typeof def["maxItems"] === "number")
@@ -577,7 +577,7 @@ function projectField(fieldDef) {
577
577
  if (isJsonSafeObject(def["totalsMatch"]))
578
578
  out["totalsMatch"] = def["totalsMatch"];
579
579
  // embedded lists: the sub-field map itself — without it the client-side
580
- // edit view-model iterates `undefined` and the screen crashes (fw#2507).
580
+ // edit view-model iterates `undefined` and the screen crashes.
581
581
  // Recursive per sub-field, NOT a blanket isJsonSafeValue gate: a single
582
582
  // sub-field carrying a validator fn would otherwise drop the whole map,
583
583
  // and a blanket copy would ship server internals.