@cosmicdrift/kumiko-framework 0.363.0 → 0.364.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 (189) hide show
  1. package/AGENTS.md +307 -0
  2. package/agents/examples/access-control.md +355 -0
  3. package/agents/examples/anonymous-access-multitenant.md +443 -0
  4. package/agents/examples/anonymous-access.md +300 -0
  5. package/agents/examples/apex-landing.md +528 -0
  6. package/agents/examples/apex-surface-auth.md +355 -0
  7. package/agents/examples/api-worker-split.md +434 -0
  8. package/agents/examples/auth-claims.md +351 -0
  9. package/agents/examples/auth-foundation-providers.md +155 -0
  10. package/agents/examples/auth-mfa.md +323 -0
  11. package/agents/examples/basic-entity.md +420 -0
  12. package/agents/examples/boot-check.md +122 -0
  13. package/agents/examples/chat-channels.md +254 -0
  14. package/agents/examples/compliance-profiles-demo.md +307 -0
  15. package/agents/examples/cross-feature-events.md +317 -0
  16. package/agents/examples/crypto-shredding-hr.md +314 -0
  17. package/agents/examples/currencies-global.md +609 -0
  18. package/agents/examples/currencies-per-tenant.md +453 -0
  19. package/agents/examples/custom-fields-basic.md +314 -0
  20. package/agents/examples/custom-handlers.md +397 -0
  21. package/agents/examples/delivery-notifications.md +432 -0
  22. package/agents/examples/designer-demo.md +86 -0
  23. package/agents/examples/e2e-generator.md +211 -0
  24. package/agents/examples/embedded-entity-form.md +598 -0
  25. package/agents/examples/embedded.md +649 -0
  26. package/agents/examples/encrypted-tenant-config.md +405 -0
  27. package/agents/examples/error-contract.md +539 -0
  28. package/agents/examples/event-sourcing.md +1287 -0
  29. package/agents/examples/feature-toggles.md +263 -0
  30. package/agents/examples/field-access.md +271 -0
  31. package/agents/examples/files-post-processing.md +433 -0
  32. package/agents/examples/i18n.md +166 -0
  33. package/agents/examples/idempotency.md +179 -0
  34. package/agents/examples/inbound-mail-triage.md +315 -0
  35. package/agents/examples/lane-routing.md +246 -0
  36. package/agents/examples/ledger-banking.md +214 -0
  37. package/agents/examples/ledger-invoicing.md +235 -0
  38. package/agents/examples/legal-pages.md +393 -0
  39. package/agents/examples/lifecycle-hooks.md +368 -0
  40. package/agents/examples/managed-config.md +428 -0
  41. package/agents/examples/managed-pages.md +353 -0
  42. package/agents/examples/notes-history-basic.md +345 -0
  43. package/agents/examples/ownership.md +396 -0
  44. package/agents/examples/pdf-audit-export.md +270 -0
  45. package/agents/examples/pipeline-basics.md +792 -0
  46. package/agents/examples/public-share-token.md +351 -0
  47. package/agents/examples/rate-limiting.md +211 -0
  48. package/agents/examples/realtime-sse.md +187 -0
  49. package/agents/examples/record-detail-layout.md +297 -0
  50. package/agents/examples/reference-data.md +151 -0
  51. package/agents/examples/relations.md +280 -0
  52. package/agents/examples/screens-nav.md +353 -0
  53. package/agents/examples/search.md +233 -0
  54. package/agents/examples/secrets-demo.md +417 -0
  55. package/agents/examples/security-baseline.md +187 -0
  56. package/agents/examples/seed-migration.md +99 -0
  57. package/agents/examples/session-revocation.md +259 -0
  58. package/agents/examples/state-machine.md +920 -0
  59. package/agents/examples/tags-basic.md +401 -0
  60. package/agents/examples/tenant-isolation.md +173 -0
  61. package/agents/examples/tenant-settings.md +275 -0
  62. package/agents/examples/tier-admin.md +347 -0
  63. package/agents/examples/timezones.md +366 -0
  64. package/agents/examples/user-data-rights.md +410 -0
  65. package/agents/examples/user-profile.md +226 -0
  66. package/agents/examples/webhook-step.md +597 -0
  67. package/agents/examples/wizard-form.md +379 -0
  68. package/agents/examples/workflow-engine.md +1731 -0
  69. package/agents/examples/writeform-section.md +300 -0
  70. package/agents/examples.md +74 -0
  71. package/agents/features.md +768 -0
  72. package/agents/widgets.md +181 -0
  73. package/dist/api/auth-middleware.d.ts +2 -0
  74. package/dist/api/auth-middleware.js +7 -0
  75. package/dist/api/auth-routes.js +5 -3
  76. package/dist/api/extra-route.d.ts +3 -0
  77. package/dist/api/index.d.ts +2 -2
  78. package/dist/api/index.js +1 -1
  79. package/dist/api/routes.js +46 -17
  80. package/dist/api/server.d.ts +2 -1
  81. package/dist/api/server.js +18 -6
  82. package/dist/api/session-cookies.d.ts +1 -0
  83. package/dist/api/session-cookies.js +20 -5
  84. package/dist/api/sse-route.js +16 -1
  85. package/dist/bun-db/index.d.ts +1 -1
  86. package/dist/bun-db/index.js +1 -1
  87. package/dist/bun-db/query.d.ts +8 -3
  88. package/dist/bun-db/query.js +27 -7
  89. package/dist/db/collect-table-metas.js +11 -7
  90. package/dist/db/eagerload.js +2 -2
  91. package/dist/db/event-store-executor-context.d.ts +5 -6
  92. package/dist/db/event-store-executor-context.js +24 -22
  93. package/dist/db/event-store-executor-read.js +31 -16
  94. package/dist/db/event-store-executor-write.js +18 -20
  95. package/dist/db/index.d.ts +1 -1
  96. package/dist/db/index.js +1 -1
  97. package/dist/db/lock-guard.d.ts +4 -0
  98. package/dist/db/lock-guard.js +14 -0
  99. package/dist/db/migrate-generator.js +1 -1
  100. package/dist/db/migrate-runner.js +9 -8
  101. package/dist/db/queries/event-consumer.d.ts +6 -6
  102. package/dist/db/queries/event-store.d.ts +1 -1
  103. package/dist/db/queries/projection-rebuild.d.ts +1 -1
  104. package/dist/db/tenant-db-runner.d.ts +4 -0
  105. package/dist/db/tenant-db-runner.js +17 -0
  106. package/dist/db/tenant-db.d.ts +2 -0
  107. package/dist/db/tenant-db.js +30 -4
  108. package/dist/derivatives/derivatives-context.js +1 -1
  109. package/dist/engine/boot-validator/access-declarations.js +2 -2
  110. package/dist/engine/boot-validator/api-ext.js +3 -3
  111. package/dist/engine/boot-validator/boot-check.js +1 -1
  112. package/dist/engine/boot-validator/config-deps.js +13 -13
  113. package/dist/engine/boot-validator/detail-screens.js +1 -1
  114. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  115. package/dist/engine/boot-validator/entity-handler.js +84 -35
  116. package/dist/engine/boot-validator/entity-list-screens.js +2 -2
  117. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  118. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  119. package/dist/engine/boot-validator/nav.js +1 -1
  120. package/dist/engine/boot-validator/pii-retention.js +8 -8
  121. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  122. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  123. package/dist/engine/boot-validator/screens.d.ts +2 -1
  124. package/dist/engine/boot-validator/screens.js +153 -95
  125. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  126. package/dist/engine/define-handler.d.ts +2 -2
  127. package/dist/engine/define-handler.js +8 -1
  128. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  129. package/dist/engine/feature-ast/pattern-change-schema.d.ts +2 -0
  130. package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
  131. package/dist/engine/feature-ast/render.js +10 -2
  132. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  133. package/dist/engine/feature-entity-handlers.js +12 -3
  134. package/dist/engine/handler-field-references.d.ts +21 -2
  135. package/dist/engine/handler-field-references.js +55 -17
  136. package/dist/engine/index.d.ts +2 -2
  137. package/dist/engine/schema-builder.js +2 -1
  138. package/dist/engine/soft-delete-cleanup.js +10 -3
  139. package/dist/engine/steps/retry.js +2 -1
  140. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  141. package/dist/engine/steps/unsafe-projection-upsert.js +2 -2
  142. package/dist/engine/template-variables.d.ts +3 -0
  143. package/dist/engine/template-variables.js +3 -0
  144. package/dist/engine/types/index.d.ts +1 -1
  145. package/dist/env/_zod-introspect.d.ts +1 -0
  146. package/dist/errors/classes.js +1 -0
  147. package/dist/errors/kumiko-error.d.ts +2 -0
  148. package/dist/errors/kumiko-error.js +2 -0
  149. package/dist/errors/write-error-info.js +2 -0
  150. package/dist/event-store/tenant-db-append.js +2 -2
  151. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  152. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  153. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  154. package/dist/files/bind-pending-file-refs.js +3 -3
  155. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  156. package/dist/files/bind-referenced-files.write.js +22 -13
  157. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  158. package/dist/files/create-file-ref-from-bytes.js +67 -4
  159. package/dist/files/create-file-ref.d.ts +22 -0
  160. package/dist/files/create-file-ref.js +53 -25
  161. package/dist/files/file-handle.d.ts +1 -1
  162. package/dist/files/file-handle.js +8 -0
  163. package/dist/files/file-routes.js +1 -1
  164. package/dist/files/index.d.ts +2 -2
  165. package/dist/files/index.js +1 -1
  166. package/dist/files/load-save-file-refs.d.ts +10 -0
  167. package/dist/files/load-save-file-refs.js +47 -0
  168. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  169. package/dist/files/purge-expired-file-ref-bytes.js +45 -19
  170. package/dist/jobs/index.d.ts +1 -1
  171. package/dist/jobs/job-runner.d.ts +4 -1
  172. package/dist/jobs/job-runner.js +8 -4
  173. package/dist/observability/escape-hatch-report.d.ts +1 -1
  174. package/dist/observability/escape-hatch-report.js +17 -5
  175. package/dist/pipeline/cascade-handler.js +5 -2
  176. package/dist/pipeline/dispatch-batch.js +79 -31
  177. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  178. package/dist/pipeline/dispatch-shared.js +99 -22
  179. package/dist/pipeline/dispatch-write.js +34 -22
  180. package/dist/pipeline/index.d.ts +1 -0
  181. package/dist/pipeline/index.js +1 -0
  182. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  183. package/dist/pipeline/pre-transaction-reservations.js +28 -9
  184. package/dist/rate-limit/bucket.d.ts +3 -3
  185. package/dist/rate-limit/bucket.js +14 -4
  186. package/dist/redis/cache-sync-bus.js +9 -3
  187. package/dist/search/meilisearch-adapter.js +36 -9
  188. package/package.json +7 -5
  189. package/src/changes.json +207 -8
@@ -24,7 +24,7 @@ function validateAtMostOneRowClick(featureName, screenId, screenType, rowActions
24
24
  const rowClickActions = rowActions.filter((a) => a.kind === "navigate" && a.rowClick === true);
25
25
  if (rowClickActions.length > 1) {
26
26
  throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) has ${rowClickActions.length} ` +
27
- "rowActions marked rowClick:true — at most one may fire on a row-body click.");
27
+ "rowActions marked rowClick:true — at most one may fire on a row-body click. Mark at most one rowAction with rowClick: true.");
28
28
  }
29
29
  }
30
30
  // A field type in NO_WIDGET_FIELD_TYPES renders read-only on the auto-wired
@@ -45,7 +45,7 @@ function validateEditFieldExtras(featureName, screenId, entityDef, fieldSpec) {
45
45
  if (listSpec !== undefined) {
46
46
  if (fieldDef.type !== "embedded" || fieldDef.multiple !== true) {
47
47
  throw new Error(`${where} sets embeddedList, which only applies to an embedded list ` +
48
- `(createEmbeddedListField); the field is type "${fieldDef.type}".`);
48
+ `(createEmbeddedListField); the field is type "${fieldDef.type}". Remove embeddedList or point it at an embedded list field.`);
49
49
  }
50
50
  for (const cell of listSpec.readOnlyCells ?? []) {
51
51
  if (fieldDef.schema[cell] === undefined) {
@@ -60,7 +60,7 @@ function validateEditFieldExtras(featureName, screenId, entityDef, fieldSpec) {
60
60
  return;
61
61
  if (prefill.kind === "today") {
62
62
  if (fieldDef.type !== "date") {
63
- throw new Error(`${where} prefill "today" needs a date field; the field is type "${fieldDef.type}".`);
63
+ throw new Error(`${where} prefill "today" needs a date field; the field is type "${fieldDef.type}". Use a date field or a different prefill.`);
64
64
  }
65
65
  // skip: today carries no property, nothing further to check
66
66
  return;
@@ -68,7 +68,7 @@ function validateEditFieldExtras(featureName, screenId, entityDef, fieldSpec) {
68
68
  const allowsUserId = fieldDef.type === "text" || fieldDef.type === "reference";
69
69
  if (prefill.property === "id" ? !allowsUserId : fieldDef.type !== "text") {
70
70
  throw new Error(`${where} prefill currentUser.${prefill.property} does not fit a field of type ` +
71
- `"${fieldDef.type}" (displayName and email need text; id needs text or a reference).`);
71
+ `"${fieldDef.type}" (displayName and email need text; id needs text or a reference). Use a fitting field type or a different prefill property.`);
72
72
  }
73
73
  }
74
74
  function validateNoWidgetRequiredField(featureName, screenId, entityDef, fieldSpec) {
@@ -121,7 +121,7 @@ export function readsNavigateParamsAsFormPrefill(target, explicitEntityId, sourc
121
121
  // for the value to land, and a params extractor whose keys don't include
122
122
  // the filter's id would silently miss it — both are boot errors instead of
123
123
  // a silently-empty dashboard.
124
- function validateRowActionNavigateParams(featureName, screenId, screenType, screenEntity, action, target) {
124
+ function validateRowActionNavigateParams(featureName, screenId, screenType, screenEntity, action, target, actionLabel = "rowAction") {
125
125
  // skip: not a navigate-with-params action — nothing to validate here.
126
126
  if (action.kind !== "navigate" || action.params === undefined)
127
127
  return;
@@ -137,14 +137,14 @@ function validateRowActionNavigateParams(featureName, screenId, screenType, scre
137
137
  const targetDescriptor = action.screen !== undefined ? `"${action.screen}"` : `entity "${action.entity}"`;
138
138
  const filter = target.screen.filter;
139
139
  if (filter === undefined) {
140
- throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) rowAction "${action.id}" sets ` +
140
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" sets ` +
141
141
  `params on navigate-target ${targetDescriptor} (dashboard) — target dashboard declares no ` +
142
142
  `filter — params would be a no-op. Remove the params extractor, or add a \`filter\` to the ` +
143
143
  `target dashboard so it has somewhere to read the value from.`);
144
144
  }
145
145
  const extractedKeys = rowFieldExtractorKeys(action.params);
146
146
  if (!extractedKeys.includes(filter.id)) {
147
- throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) rowAction "${action.id}" sets ` +
147
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" sets ` +
148
148
  `params [${extractedKeys.join(", ")}] on navigate-target ${targetDescriptor} (dashboard) whose ` +
149
149
  `filter id is "${filter.id}" — none of the extracted keys match, so the filter would stay ` +
150
150
  `unset. Fix the params extractor to produce a "${filter.id}" key, or remove it.`);
@@ -160,7 +160,7 @@ function validateRowActionNavigateParams(featureName, screenId, screenType, scre
160
160
  : `same entity "${screenEntity}" auto-fills row["id"]`})`
161
161
  : `screen type "${target.screen.type}"`;
162
162
  const targetDescriptor = action.screen !== undefined ? `"${action.screen}"` : `entity "${action.entity}"`;
163
- throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) rowAction "${action.id}" ` +
163
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" ` +
164
164
  `sets params on navigate-target ${targetDescriptor} which ${reason} — only actionForm, ` +
165
165
  `secretMint and entityEdit-create targets read URL search params as initial values. Remove ` +
166
166
  `the params extractor or retarget to an actionForm / secretMint / cross-entity ` +
@@ -169,7 +169,7 @@ function validateRowActionNavigateParams(featureName, screenId, screenType, scre
169
169
  const formTarget = target.screen;
170
170
  if ((formTarget.type === "actionForm" || formTarget.type === "secretMint") &&
171
171
  formTarget.submitPrefilled === true) {
172
- validateDrawerPrefillKeys(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) rowAction "${action.id}"`, "navigate-target", action.screen ?? `entity "${action.entity}"`, formTarget, action.params);
172
+ validateDrawerPrefillKeys(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}"`, "navigate-target", action.screen ?? `entity "${action.entity}"`, formTarget, action.params);
173
173
  }
174
174
  }
175
175
  // Shared by projectionDetail and entityEdit: a tab strip needs something to
@@ -197,7 +197,7 @@ function validateTabSections(featureName, screenId, screenType, sections) {
197
197
  }
198
198
  if (tabIds.has(section.id)) {
199
199
  throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) has duplicate tab id ` +
200
- `"${section.id}" (sections[${index}]).`);
200
+ `"${section.id}" (sections[${index}]). Give each tab section a unique id.`);
201
201
  }
202
202
  tabIds.add(section.id);
203
203
  });
@@ -399,7 +399,7 @@ function validateDrawerTargetAction(featureName, screenId, screenKind, actionLab
399
399
  return;
400
400
  validateDrawerPrefillKeys(`[Feature ${featureName}] Screen "${screenId}" (${screenKind}) ${actionLabel} "${action.id}"`, "drawer-target", action.screen, target, action.params);
401
401
  }
402
- function validateDrawerPrefillKeys(where, targetLabel, targetScreenId, target, params) {
402
+ export function validateDrawerPrefillKeys(where, targetLabel, targetScreenId, target, params) {
403
403
  const renderedFieldNames = new Set();
404
404
  for (const section of target.layout.sections) {
405
405
  if (!isFieldsEditSection(section))
@@ -419,7 +419,7 @@ function validateDrawerPrefillKeys(where, targetLabel, targetScreenId, target, p
419
419
  ("format" in targetField && targetField.format === "password"));
420
420
  if (isSensitiveOrPassword) {
421
421
  throw new Error(`${where} params prefills "${fieldName}", which ${targetLabel} "${targetScreenId}" marks as ` +
422
- `sensitive or password — the renderer never prefills those fields and would leave the form empty.`);
422
+ `sensitive or password — the renderer never prefills those fields and would leave the form empty. Remove that param or prefill a non-sensitive field.`);
423
423
  }
424
424
  if (!renderedFieldNames.has(fieldName)) {
425
425
  throw new Error(`${where} params prefills "${fieldName}", which ${targetLabel} "${targetScreenId}" does not render in ` +
@@ -435,12 +435,12 @@ function validateRowActionNavigateTab(featureName, screenId, screenType, actionL
435
435
  return;
436
436
  if (target === undefined) {
437
437
  throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" ` +
438
- `sets tab "${action.tab}", but its navigate-target could not be resolved to a screen.`);
438
+ `sets tab "${action.tab}", but its navigate-target could not be resolved to a screen. Point the navigate-target at a registered screen.`);
439
439
  }
440
440
  if (target.screen.type !== "projectionDetail" || target.screen.layout.mode !== "tabs") {
441
441
  throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" ` +
442
442
  `sets tab "${action.tab}", but navigate-target screen "${target.screen.id}" is not a ` +
443
- `projectionDetail with layout.mode "tabs" — only those read the tab param.`);
443
+ `projectionDetail with layout.mode "tabs" — only those read the tab param. Remove the tab or navigate to a projectionDetail screen with layout.mode "tabs".`);
444
444
  }
445
445
  const sectionIds = target.screen.layout.sections
446
446
  .map((section) => section.id)
@@ -471,7 +471,7 @@ function resolveRowActionNavigateTarget(featureName, screenId, screenType, actio
471
471
  if (detail === undefined) {
472
472
  throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" ` +
473
473
  `navigate-target entity "${action.entity}" has no screen declaring ` +
474
- `detailFor: "${action.entity}".`);
474
+ `detailFor: "${action.entity}". Add detailFor: "${action.entity}" to a screen or navigate to a screen directly.`);
475
475
  }
476
476
  validateRowActionNavigateTab(featureName, screenId, screenType, actionLabel, action, detail);
477
477
  return detail;
@@ -483,7 +483,7 @@ function resolveRowActionNavigateTarget(featureName, screenId, screenType, actio
483
483
  const candidateQn = qualifyEntityName(featureName, "screen", action.screen);
484
484
  if (!allScreenQns.has(candidateQn) && !navTargetShortIds.has(action.screen)) {
485
485
  throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${actionLabel} "${action.id}" ` +
486
- `navigate-target "${action.screen}" does not resolve to a registered screen in any feature.`);
486
+ `navigate-target "${action.screen}" does not resolve to a registered screen in any feature. Use "<feature>:screen:<id>" of an existing screen.`);
487
487
  }
488
488
  const target = screensByShortId.get(action.screen)?.[0];
489
489
  validateRowActionNavigateTab(featureName, screenId, screenType, actionLabel, action, target);
@@ -494,7 +494,7 @@ function resolveRowActionNavigateTarget(featureName, screenId, screenType, actio
494
494
  // same registration requirement.
495
495
  function validateWriteHandlerRegistered(featureName, screenId, context, handler, allWriteHandlerQns) {
496
496
  if (!handler || typeof handler !== "string") {
497
- throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) has empty or non-string handler.`);
497
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) has empty or non-string handler. Set handler to a non-empty handler name.`);
498
498
  }
499
499
  if (!allWriteHandlerQns.has(handler)) {
500
500
  throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) handler "${handler}" ` +
@@ -540,7 +540,7 @@ function validateFormFieldsMap(featureName, screenId, context, fields, allowEmpt
540
540
  function validateOptionsQueryFieldRefs(where, fieldName, payload, siblingFieldNames) {
541
541
  for (const ref of optionsQueryFieldRefs(payload)) {
542
542
  if (ref === fieldName) {
543
- throw new Error(`${where} optionsQueryPayload references itself ({ field: "${ref}" })`);
543
+ throw new Error(`${where} optionsQueryPayload references itself ({ field: "${ref}" }). Reference a different field of the form.`);
544
544
  }
545
545
  if (!siblingFieldNames.has(ref)) {
546
546
  throw new Error(`${where} optionsQueryPayload references unknown field "${ref}" — it must be another field of the same form`);
@@ -568,30 +568,33 @@ function validateFormSelectOptions(featureName, screenId, context, fieldName, fd
568
568
  if (fdef.type === "select") {
569
569
  const where = `[Feature ${featureName}] Screen "${screenId}" (${context}) select field "${fieldName}"`;
570
570
  if (fdef.conditionalOptions !== undefined) {
571
- throw new Error(`${where} declares conditionalOptions — ${CONDITIONAL_OPTIONS_ENTITY_ONLY}`);
571
+ throw new Error(`${where} declares conditionalOptions — ${CONDITIONAL_OPTIONS_ENTITY_ONLY}. Remove conditionalOptions from this field.`);
572
572
  }
573
573
  if (fdef.optionsQuery !== undefined) {
574
574
  if (fdef.optionsQuery.length === 0) {
575
- throw new Error(`${where} has an empty optionsQuery`);
575
+ throw new Error(`${where} has an empty optionsQuery. Set it to a query name or remove it.`);
576
576
  }
577
577
  if (fdef.options.length > 0) {
578
578
  throw new Error(`${where} declares both options and optionsQuery — pick one`);
579
579
  }
580
+ if (fdef.optionsAvailabilityQuery !== undefined) {
581
+ throw new Error(`${where} declares both optionsQuery and optionsAvailabilityQuery — availability only applies to static options`);
582
+ }
580
583
  validateOptionsQueryFieldRefs(where, fieldName, fdef.optionsQueryPayload, siblingFieldNames);
581
584
  }
582
585
  else if (fdef.optionsAvailabilityQuery !== undefined) {
583
586
  if (fdef.optionsAvailabilityQuery.length === 0) {
584
- throw new Error(`${where} has an empty optionsAvailabilityQuery`);
587
+ throw new Error(`${where} has an empty optionsAvailabilityQuery. Set it to a query name or remove it.`);
585
588
  }
586
589
  validateOptionsQueryFieldRefs(where, fieldName, fdef.optionsQueryPayload, siblingFieldNames);
587
590
  }
588
591
  else if (fdef.optionsQueryPayload !== undefined) {
589
- throw new Error(`${where} has optionsQueryPayload without optionsQuery`);
592
+ throw new Error(`${where} has optionsQueryPayload without optionsQuery. Set optionsQuery or remove optionsQueryPayload.`);
590
593
  }
591
594
  }
592
595
  else if (fdef.type === "multiSelect" && fdef.optionsAvailabilityQuery !== undefined) {
593
596
  if (fdef.optionsAvailabilityQuery.length === 0) {
594
- throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) multiSelect field "${fieldName}" has an empty optionsAvailabilityQuery`);
597
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) multiSelect field "${fieldName}" has an empty optionsAvailabilityQuery. Set it to a query name or remove it.`);
595
598
  }
596
599
  }
597
600
  }
@@ -664,11 +667,11 @@ function validateFormLayoutSections(featureName, screenId, context, layout, fiel
664
667
  if (section.kind === "relatedList") {
665
668
  throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) relatedList section ` +
666
669
  `"${section.title}" is not supported — relatedList is a projectionDetail-only ` +
667
- `primitive (fw#2166).`);
670
+ `primitive (fw#2166). Move the section to a projectionDetail screen or remove it.`);
668
671
  }
669
672
  if (isWriteFormEditSection(section)) {
670
673
  throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${context}) writeForm section ` +
671
- `"${section.title}" is not supported — writeForm is a projectionDetail-only primitive.`);
674
+ `"${section.title}" is not supported — writeForm is a projectionDetail-only primitive. Move the section to a projectionDetail screen or remove it.`);
672
675
  }
673
676
  validateFieldsXorGroups(`[Feature ${featureName}] Screen "${screenId}" (${context})`, section);
674
677
  for (const fieldSpec of sectionFieldSpecs(section)) {
@@ -700,7 +703,7 @@ function validateRedirectTarget(feature, screenId, screenKind, redirect, lookups
700
703
  // never matches and the navigation silently lands without an entityId.
701
704
  if (typeof redirect !== "string" && redirect.idFrom !== redirect.idFrom.trim()) {
702
705
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${screenKind}) redirect.idFrom "${redirect.idFrom}" ` +
703
- `has leading/trailing whitespace — it would never match a payload field.`);
706
+ `has leading/trailing whitespace — it would never match a payload field. Remove the whitespace from idFrom.`);
704
707
  }
705
708
  validateScreenNavTarget(feature.name, screenId, screenKind, "redirect", redirectTarget, allScreenQns, feature.screens);
706
709
  // idFrom only means something to a target that carries a record id; the
@@ -758,10 +761,10 @@ function validateScreenVisibleWhen(feature, screenId, screen, allScreenQns, feat
758
761
  const { visibleWhen, fallback } = screen;
759
762
  const context = `[Feature ${feature.name}] Screen "${screenId}" (${screen.type})`;
760
763
  if (visibleWhen === undefined && fallback !== undefined) {
761
- throw new Error(`${context} declares fallback without visibleWhen.`);
764
+ throw new Error(`${context} declares fallback without visibleWhen. Add visibleWhen or remove fallback.`);
762
765
  }
763
766
  if (visibleWhen !== undefined && (visibleWhen.query === "" || visibleWhen.field === "")) {
764
- throw new Error(`${context} visibleWhen needs a non-empty query and field.`);
767
+ throw new Error(`${context} visibleWhen needs a non-empty query and field. Set both visibleWhen.query and visibleWhen.field.`);
765
768
  }
766
769
  if (fallback !== undefined) {
767
770
  validateScreenFallback(feature, screenId, screen.type, fallback, allScreenQns, featureMap);
@@ -772,7 +775,7 @@ function validateScreenFallback(feature, screenId, screenType, fallback, allScre
772
775
  validateScreenNavTarget(feature.name, screenId, screenType, "fallback", fallback, allScreenQns, feature.screens);
773
776
  const fallbackQn = resolveScreenTargetQn(feature.name, fallback);
774
777
  if (fallbackQn === qualifyEntityName(feature.name, "screen", screenId)) {
775
- throw new Error(`${context} fallback points at the screen itself.`);
778
+ throw new Error(`${context} fallback points at the screen itself. Point fallback at a different screen.`);
776
779
  }
777
780
  // A gated fallback could hide in turn and chain or cycle (A→B→A); one hop keeps the
778
781
  // runtime gate trivially terminating.
@@ -836,7 +839,7 @@ function validateSecretMintConfirm(feature, screenId, screen, allWriteHandlerQns
836
839
  for (const carryField of confirm.carry) {
837
840
  if (typeof carryField !== "string" || carryField.trim() === "") {
838
841
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${context}) has an empty or non-string ` +
839
- `entry in "carry".`);
842
+ `entry in "carry". Use non-empty strings in carry.`);
840
843
  }
841
844
  if (fieldNames.has(carryField)) {
842
845
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${context}) carry field "${carryField}" ` +
@@ -915,7 +918,7 @@ function validateScreenHasNavArea(feature, screenId, screen, featureMap) {
915
918
  function validateRelatedListSection(args) {
916
919
  const { featureName, featureMap, where, section } = args;
917
920
  if (!section.query || typeof section.query !== "string") {
918
- throw new Error(`[Feature ${featureName}] ${where} has empty or non-string query.`);
921
+ throw new Error(`[Feature ${featureName}] ${where} has empty or non-string query. Set query to a registered query name.`);
919
922
  }
920
923
  if (section.entity !== undefined) {
921
924
  assertRefTargetRegistered(`[Feature ${featureName}] ${where}`, "entity", section.entity, featureName, featureMap);
@@ -946,23 +949,51 @@ function validateRelatedListSection(args) {
946
949
  validateRelatedListToolbarActions(args, section.toolbarActions);
947
950
  }
948
951
  }
952
+ // An empty list has no row, so params resolve against an empty record.
953
+ function rejectEmptyStateActionParams(ctx, actionLabel, action) {
954
+ const params = action.kind === "navigate" || action.kind === "drawer" ? action.params : undefined;
955
+ if (params !== undefined) {
956
+ throw new Error(`[Feature ${ctx.featureName}] ${ctx.where} ${actionLabel} "${action.id}" sets params — an empty list ` +
957
+ `has no row to read them from, so they would resolve against an empty record. Remove the params.`);
958
+ }
959
+ }
960
+ // Target checks shared by list/related-list rowActions and emptyState.action:
961
+ // navigate target + params, drawer target, write-handler registration.
962
+ function validateListActionTargets(ctx, actionLabel, action) {
963
+ const { featureName, screenId, screenType } = ctx;
964
+ rejectRedirectOnListRowAction(featureName, screenId, screenType, action);
965
+ if (actionLabel === "emptyState action")
966
+ rejectEmptyStateActionParams(ctx, actionLabel, action);
967
+ if (action.kind === "navigate") {
968
+ const target = resolveRowActionNavigateTarget(featureName, screenId, screenType, actionLabel, action, ctx.allScreenQns, ctx.navTargetShortIds, ctx.screensByShortId, ctx.detailForScreens);
969
+ validateRowActionNavigateParams(featureName, screenId, screenType, ctx.screenEntity, action, target, actionLabel);
970
+ }
971
+ else if (action.kind === "drawer") {
972
+ validateDrawerTargetAction(featureName, screenId, screenType, actionLabel, action, ctx.screens);
973
+ }
974
+ else if (!ctx.allWriteHandlerQns.has(action.handler)) {
975
+ throw new Error(`[Feature ${featureName}] ${ctx.where} ` +
976
+ `${actionLabel} "${action.id}" handler "${action.handler}" ` +
977
+ `is not a registered write-handler. Check the QN spelling (expected ` +
978
+ `"<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
979
+ }
980
+ }
949
981
  function validateRelatedListRowActions(args, rowActions) {
950
982
  const { featureName, screenId, screenType, where, section, screens, allWriteHandlerQns, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens, } = args;
951
983
  for (const action of rowActions) {
952
- rejectRedirectOnListRowAction(featureName, screenId, screenType, action);
953
- if (action.kind === "navigate") {
954
- const target = resolveRowActionNavigateTarget(featureName, screenId, screenType, "rowAction", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
955
- validateRowActionNavigateParams(featureName, screenId, screenType, undefined, action, target);
956
- }
957
- else if (action.kind === "drawer") {
958
- validateDrawerTargetAction(featureName, screenId, screenType, "rowAction", action, screens);
959
- }
960
- else if (!allWriteHandlerQns.has(action.handler)) {
961
- throw new Error(`[Feature ${featureName}] ${where} ` +
962
- `rowAction "${action.id}" handler "${action.handler}" ` +
963
- `is not a registered write-handler. Check the QN spelling (expected ` +
964
- `"<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
965
- }
984
+ validateListActionTargets({
985
+ featureName,
986
+ screenId,
987
+ screenType,
988
+ screenEntity: undefined,
989
+ where,
990
+ screens,
991
+ allWriteHandlerQns,
992
+ allScreenQns,
993
+ navTargetShortIds,
994
+ screensByShortId,
995
+ detailForScreens,
996
+ }, "rowAction", action);
966
997
  }
967
998
  // section.rowClick (legacy, navigate-only) and a rowActions entry
968
999
  // marked rowClick:true both claim the row-body click — same
@@ -982,7 +1013,7 @@ function validateRelatedListDefaultSort(args, defaultSort) {
982
1013
  const col = section.columns.find((c) => normalizeListColumn(c).field === sortField);
983
1014
  if (col === undefined) {
984
1015
  throw new Error(`[Feature ${featureName}] ${where} ` +
985
- `defaultSort.field "${sortField}" is not a listed column.`);
1016
+ `defaultSort.field "${sortField}" is not a listed column. Use a listed column as defaultSort.field.`);
986
1017
  }
987
1018
  if (normalizeListColumn(col).sortable !== true) {
988
1019
  throw new Error(`[Feature ${featureName}] ${where} ` +
@@ -1032,7 +1063,16 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1032
1063
  if ((screen.type === "entityList" || screen.type === "projectionList") &&
1033
1064
  screen.emptyState?.action !== undefined) {
1034
1065
  validateActionHasIcon(feature.name, screenId, screen.type, "emptyState action", screen.emptyState.action);
1035
- validateNavigateActionTab(feature.name, screenId, screen.type, "emptyState action", screen.emptyState.action, navigateLookups);
1066
+ validateListActionTargets({
1067
+ featureName: feature.name,
1068
+ screenId,
1069
+ screenType: screen.type,
1070
+ screenEntity: undefined,
1071
+ where: `Screen "${screenId}" (${screen.type})`,
1072
+ screens: feature.screens,
1073
+ allWriteHandlerQns,
1074
+ ...navigateLookups,
1075
+ }, "emptyState action", screen.emptyState.action);
1036
1076
  }
1037
1077
  // Output of buildAppSchema, which overwrites it — an authored value would be a silent no-op.
1038
1078
  if ((screen.type === "entityEdit" ||
@@ -1063,7 +1103,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1063
1103
  // Column-Felder können nicht gegen eine Entity gecheckt werden; sie
1064
1104
  // werden zur Render-Zeit gegen die Projection-Rows aufgelöst).
1065
1105
  if (!screen.query || typeof screen.query !== "string") {
1066
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) has empty or non-string query.`);
1106
+ throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) has empty or non-string query. Set query to a registered query name.`);
1067
1107
  }
1068
1108
  if (screen.columns.length === 0) {
1069
1109
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) has an empty columns list — ` +
@@ -1085,7 +1125,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1085
1125
  screen.filter.op === "in" &&
1086
1126
  !Array.isArray(screen.filter.value)) {
1087
1127
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionList) filter.op "in" requires ` +
1088
- `filter.value to be a readonly array.`);
1128
+ `filter.value to be a readonly array. Pass an array as filter.value.`);
1089
1129
  }
1090
1130
  if (screen.facets !== undefined) {
1091
1131
  validateListFacets(`[Feature ${feature.name}] Screen "${screenId}" (projectionList)`, screen.facets, screen.columns, feature.name, featureMap);
@@ -1123,7 +1163,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1123
1163
  // Columns. Kein Entity-Check möglich — Felder werden render-seitig
1124
1164
  // gegen die Query-Response aufgelöst, nicht gegen eine Entity.
1125
1165
  if (!screen.query || typeof screen.query !== "string") {
1126
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) has empty or non-string query.`);
1166
+ throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) has empty or non-string query. Set query to a registered query name.`);
1127
1167
  }
1128
1168
  if (screen.singleton === true && screen.idParam !== undefined) {
1129
1169
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) sets both singleton: true ` +
@@ -1176,7 +1216,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1176
1216
  if (navigate.tab !== undefined &&
1177
1217
  !screen.layout.sections.some((section) => section.id === navigate.tab)) {
1178
1218
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) metric "${field}" ` +
1179
- `navigates to tab "${navigate.tab}", which is not a section id on this screen.`);
1219
+ `navigates to tab "${navigate.tab}", which is not a section id on this screen. Navigate to an existing section id or remove navigate.tab.`);
1180
1220
  }
1181
1221
  }
1182
1222
  else {
@@ -1205,14 +1245,14 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1205
1245
  if (Array.isArray(screen.header?.subtitle)) {
1206
1246
  if (screen.header.subtitleHref !== undefined) {
1207
1247
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) sets header.subtitleHref ` +
1208
- `together with a multi-part header.subtitle — subtitleHref only works with the string form.`);
1248
+ `together with a multi-part header.subtitle — subtitleHref only works with the string form. Use a single-field subtitle or remove subtitleHref.`);
1209
1249
  }
1210
1250
  const seenPartFields = new Set();
1211
1251
  for (const part of screen.header.subtitle) {
1212
1252
  const partField = typeof part === "string" ? part : part.field;
1213
1253
  if (seenPartFields.has(partField)) {
1214
1254
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) header.subtitle ` +
1215
- `lists field "${partField}" twice.`);
1255
+ `lists field "${partField}" twice. List each field once.`);
1216
1256
  }
1217
1257
  seenPartFields.add(partField);
1218
1258
  const navigate = typeof part === "string" ? undefined : part.navigate;
@@ -1244,7 +1284,16 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1244
1284
  }
1245
1285
  if (section.kind === "relatedList" && section.emptyState?.action !== undefined) {
1246
1286
  validateActionHasIcon(feature.name, screenId, "projectionDetail", `section "${sectionLabel}" emptyState action`, section.emptyState.action);
1247
- validateNavigateActionTab(feature.name, screenId, "projectionDetail", `section "${sectionLabel}" emptyState action`, section.emptyState.action, navigateLookups);
1287
+ validateListActionTargets({
1288
+ featureName: feature.name,
1289
+ screenId,
1290
+ screenType: "projectionDetail",
1291
+ screenEntity: undefined,
1292
+ where: `Screen "${screenId}" (projectionDetail) section "${sectionLabel}"`,
1293
+ screens: feature.screens,
1294
+ allWriteHandlerQns,
1295
+ ...navigateLookups,
1296
+ }, "emptyState action", section.emptyState.action);
1248
1297
  }
1249
1298
  if (isExtensionEditSection(section)) {
1250
1299
  // projectionDetail is read-only (no composed form submit) — an
@@ -1318,7 +1367,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1318
1367
  if (section.fieldDefs[fieldName] === undefined) {
1319
1368
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.title}" ` +
1320
1369
  `(writeForm) field "${fieldName}" has no entry in fieldDefs — every rendered field needs ` +
1321
- `a type declared there.`);
1370
+ `a type declared there. Add the field to fieldDefs.`);
1322
1371
  }
1323
1372
  }
1324
1373
  if (screen.layout.mode === "wizard") {
@@ -1411,11 +1460,11 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1411
1460
  if (section.kind === "relatedList") {
1412
1461
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) relatedList section ` +
1413
1462
  `"${section.title}" is not supported — relatedList is a projectionDetail-only ` +
1414
- `primitive (fw#2166).`);
1463
+ `primitive (fw#2166). Move the section to a projectionDetail screen or remove it.`);
1415
1464
  }
1416
1465
  if (isWriteFormEditSection(section)) {
1417
1466
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) writeForm section ` +
1418
- `"${section.title}" is not supported — writeForm is a projectionDetail-only primitive.`);
1467
+ `"${section.title}" is not supported — writeForm is a projectionDetail-only primitive. Move the section to a projectionDetail screen or remove it.`);
1419
1468
  }
1420
1469
  validateFieldsXorGroups(`[Feature ${feature.name}] Screen "${screenId}" (configEdit)`, section);
1421
1470
  for (const fieldSpec of sectionFieldSpecs(section)) {
@@ -1432,15 +1481,15 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1432
1481
  for (const fname of fieldNames) {
1433
1482
  const qualified = screen.configKeys[fname];
1434
1483
  if (qualified === undefined) {
1435
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}" hat ` +
1436
- `keinen Eintrag in configKeys-Map. Jedes deklarierte Field braucht ein Mapping zu ` +
1437
- `einem qualifizierten Config-Key (\`<feature>:config:<short>\`).`);
1484
+ throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}" has ` +
1485
+ `no entry in the configKeys map. Every declared field needs a mapping to a ` +
1486
+ `qualified config key (\`<feature>:config:<short>\`). Add one to configKeys.`);
1438
1487
  }
1439
1488
  const configKeyDef = configKeyDefsByQn.get(qualified);
1440
1489
  if (configKeyDef === undefined) {
1441
1490
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}" → ` +
1442
- `Config-Key "${qualified}" ist in keiner Feature-Registry deklariert. Tippfehler? ` +
1443
- `Erwartetes Format: "<feature>:config:<short>". Bekannte Keys: ${[...configKeyDefsByQn.keys()].sort().join(", ") || "(keine)"}`);
1491
+ `Config key "${qualified}" is not declared in any feature registry. Typo? ` +
1492
+ `Expected format: "<feature>:config:<short>". Known keys: ${[...configKeyDefsByQn.keys()].sort().join(", ") || "(none)"}`);
1444
1493
  }
1445
1494
  if (isWriteOnlyField(screen.fields[fname]) && !isEncryptedAtRest(configKeyDef)) {
1446
1495
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (configEdit) field "${fname}" declares writeOnly ` +
@@ -1464,7 +1513,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1464
1513
  for (const revealField of screen.reveal.fields) {
1465
1514
  if (typeof revealField.field !== "string" || revealField.field.trim() === "") {
1466
1515
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (secretMint) has a reveal.fields entry ` +
1467
- `with an empty or non-string "field".`);
1516
+ `with an empty or non-string "field". Set field to a non-empty string.`);
1468
1517
  }
1469
1518
  if (revealFieldNames.has(revealField.field)) {
1470
1519
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (secretMint) reveal.fields has a ` +
@@ -1473,7 +1522,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1473
1522
  revealFieldNames.add(revealField.field);
1474
1523
  if (typeof revealField.label !== "string" || revealField.label.trim() === "") {
1475
1524
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (secretMint) reveal.fields entry ` +
1476
- `"${revealField.field}" has an empty or non-string "label".`);
1525
+ `"${revealField.field}" has an empty or non-string "label". Set label to a non-empty string.`);
1477
1526
  }
1478
1527
  }
1479
1528
  validateSecretMintConfirm(feature, screenId, screen, allWriteHandlerQns);
@@ -1608,7 +1657,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1608
1657
  }
1609
1658
  if (screen.filter.op === "in" && !Array.isArray(screen.filter.value)) {
1610
1659
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) filter.op "in" requires ` +
1611
- `filter.value to be a readonly array.`);
1660
+ `filter.value to be a readonly array. Pass an array as filter.value.`);
1612
1661
  }
1613
1662
  }
1614
1663
  // Tier 2.7e-1: rowActions pinnen — navigate-target existiert (selbes
@@ -1679,7 +1728,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1679
1728
  const candidateQn = qualifyEntityName(feature.name, "screen", action.screen);
1680
1729
  if (!allScreenQns.has(candidateQn) && !navTargetShortIds.has(action.screen)) {
1681
1730
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) toolbarAction "${action.id}" ` +
1682
- `navigate-target "${action.screen}" does not resolve to a registered screen in any feature.`);
1731
+ `navigate-target "${action.screen}" does not resolve to a registered screen in any feature. Use "<feature>:screen:<id>" of an existing screen.`);
1683
1732
  }
1684
1733
  if (action.tab !== undefined) {
1685
1734
  resolveRowActionNavigateTarget(feature.name, screenId, "entityList", "toolbarAction", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
@@ -1747,11 +1796,11 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1747
1796
  if (section.kind === "relatedList") {
1748
1797
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) relatedList section ` +
1749
1798
  `"${section.title}" is not supported — relatedList is a projectionDetail-only ` +
1750
- `primitive (fw#2166).`);
1799
+ `primitive (fw#2166). Move the section to a projectionDetail screen or remove it.`);
1751
1800
  }
1752
1801
  if (isWriteFormEditSection(section)) {
1753
1802
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) writeForm section ` +
1754
- `"${section.title}" is not supported — writeForm is a projectionDetail-only primitive.`);
1803
+ `"${section.title}" is not supported — writeForm is a projectionDetail-only primitive. Move the section to a projectionDetail screen or remove it.`);
1755
1804
  }
1756
1805
  validateFieldsXorGroups(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit)`, section);
1757
1806
  for (const fieldSpec of sectionFieldSpecs(section)) {
@@ -1819,7 +1868,7 @@ function validateDashboardScreen(featureName, screenId, screen, featureMap) {
1819
1868
  const panelIds = new Set();
1820
1869
  const addPanelId = (id, context) => {
1821
1870
  if (panelIds.has(id)) {
1822
- throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) has duplicate panel id "${id}" (${context}).`);
1871
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) has duplicate panel id "${id}" (${context}). Give each panel a unique id.`);
1823
1872
  }
1824
1873
  panelIds.add(id);
1825
1874
  };
@@ -1848,9 +1897,9 @@ function validateDashboardScreen(featureName, screenId, screen, featureMap) {
1848
1897
  function validateDashboardTimeRange(featureName, screenId, timeRange, filterId) {
1849
1898
  const prefix = `[Feature ${featureName}] Screen "${screenId}" (dashboard) timeRange`;
1850
1899
  if (timeRange.id.length === 0)
1851
- throw new Error(`${prefix} has an empty id.`);
1900
+ throw new Error(`${prefix} has an empty id. Set a non-empty id.`);
1852
1901
  if (timeRange.id === filterId) {
1853
- throw new Error(`${prefix} id "${timeRange.id}" collides with the filter id — both live in the URL search params.`);
1902
+ throw new Error(`${prefix} id "${timeRange.id}" collides with the filter id — both live in the URL search params. Use an id different from the filter id.`);
1854
1903
  }
1855
1904
  if (timeRange.options.length === 0) {
1856
1905
  throw new Error(`${prefix}.options is empty — declare at least one option.`);
@@ -1858,15 +1907,15 @@ function validateDashboardTimeRange(featureName, screenId, timeRange, filterId)
1858
1907
  const values = new Set();
1859
1908
  for (const option of timeRange.options) {
1860
1909
  if (option.value.length === 0 || option.label.length === 0) {
1861
- throw new Error(`${prefix} option needs a non-empty value and label.`);
1910
+ throw new Error(`${prefix} option needs a non-empty value and label. Give every option both.`);
1862
1911
  }
1863
1912
  if (values.has(option.value)) {
1864
- throw new Error(`${prefix}.options has duplicate value "${option.value}".`);
1913
+ throw new Error(`${prefix}.options has duplicate value "${option.value}". Use each option value once.`);
1865
1914
  }
1866
1915
  values.add(option.value);
1867
1916
  }
1868
1917
  if (!values.has(timeRange.default)) {
1869
- throw new Error(`${prefix}.default "${timeRange.default}" is not among the options (${[...values].join(", ")}).`);
1918
+ throw new Error(`${prefix}.default "${timeRange.default}" is not among the options (${[...values].join(", ")}). Set default to one of the options.`);
1870
1919
  }
1871
1920
  }
1872
1921
  const EMBEDDABLE_SCREEN_TYPES = new Set([
@@ -1902,7 +1951,7 @@ export function validateDashboardScreenPanel(featureName, screenId, panel, featu
1902
1951
  }
1903
1952
  function validateDashboardPanelVisibleWhen(context, visibleWhen) {
1904
1953
  if (visibleWhen !== undefined && (visibleWhen.query === "" || visibleWhen.field === "")) {
1905
- throw new Error(`${context} visibleWhen needs a non-empty query and field.`);
1954
+ throw new Error(`${context} visibleWhen needs a non-empty query and field. Set both visibleWhen.query and visibleWhen.field.`);
1906
1955
  }
1907
1956
  }
1908
1957
  function validateDashboardPanelSpan(featureName, screenId, panel) {
@@ -1913,10 +1962,10 @@ function validateDashboardPanelSpan(featureName, screenId, panel) {
1913
1962
  function validateDashboardStatGroupPanel(featureName, screenId, panel, addPanelId) {
1914
1963
  const context = `[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}"`;
1915
1964
  if (panel.stats.length === 0) {
1916
- throw new Error(`${context} has an empty stats list.`);
1965
+ throw new Error(`${context} has an empty stats list. Add at least one stat.`);
1917
1966
  }
1918
1967
  if (panel.subtitle !== undefined && panel.label === undefined) {
1919
- throw new Error(`${context} sets subtitle without label — an unlabeled group renders as a KPI strip without a header.`);
1968
+ throw new Error(`${context} sets subtitle without label — an unlabeled group renders as a KPI strip without a header. Set a label or remove subtitle.`);
1920
1969
  }
1921
1970
  validateDashboardPanelSpan(featureName, screenId, panel);
1922
1971
  validateDashboardPanelVisibleWhen(context, panel.visibleWhen);
@@ -1926,22 +1975,27 @@ function validateDashboardStatGroupPanel(featureName, screenId, panel, addPanelI
1926
1975
  throw new Error(`${context} child "${stat.id}" sets visibleWhen — gate the whole group instead; a hidden cell would leave a gap in the strip.`);
1927
1976
  }
1928
1977
  if (!stat.query || typeof stat.query !== "string") {
1929
- throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}" has empty or non-string query.`);
1978
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}" has empty or non-string query. Set query to a registered query name.`);
1930
1979
  }
1931
1980
  if (stat.valueField.length === 0) {
1932
- throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}" has empty valueField.`);
1981
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}" has empty valueField. Set valueField to a field of the query result.`);
1933
1982
  }
1934
1983
  validateDashboardValueFormat(featureName, screenId, stat.id, stat.valueFormat);
1935
1984
  }
1936
1985
  }
1986
+ function validateFromTodayWindowReach(where, panel) {
1987
+ if (panel.initialWindow === "from-today" && panel.ranges === undefined && panel.brush !== true) {
1988
+ throw new Error(`${where}.initialWindow "from-today" needs ranges or brush, otherwise data before today is unreachable.`);
1989
+ }
1990
+ }
1937
1991
  function validateDashboardChartDisplay(featureName, screenId, panel) {
1938
1992
  validateDashboardValueFormat(featureName, screenId, panel.id, panel.valueFormat);
1939
1993
  const where = `[Feature ${featureName}] Screen "${screenId}" (dashboard) chart-panel "${panel.id}"`;
1940
1994
  if (panel.scrollable !== undefined && typeof panel.scrollable !== "boolean") {
1941
- throw new Error(`${where} has a non-boolean scrollable.`);
1995
+ throw new Error(`${where} has a non-boolean scrollable. Use true or false.`);
1942
1996
  }
1943
1997
  if (panel.scrollable === true && panel.chart !== "stacked-area") {
1944
- throw new Error(`${where} sets scrollable on chart "${panel.chart}" — only "stacked-area" supports it.`);
1998
+ throw new Error(`${where} sets scrollable on chart "${panel.chart}" — only "stacked-area" supports it. Remove scrollable or use chart "stacked-area".`);
1945
1999
  }
1946
2000
  validateStackedAreaOnlyProps(where, panel);
1947
2001
  validateStackedAreaEnumProp(where, "dateFormat", panel.dateFormat, ["day", "month"]);
@@ -1952,8 +2006,9 @@ function validateDashboardChartDisplay(featureName, screenId, panel) {
1952
2006
  validateStackedAreaEnumProp(where, "markerLegend", panel.markerLegend, ["list", "legend"]);
1953
2007
  const nonBooleanProp = ["brush", "legendTotals", "markerLabels"].find((prop) => panel[prop] !== undefined && typeof panel[prop] !== "boolean");
1954
2008
  if (nonBooleanProp !== undefined) {
1955
- throw new Error(`${where} has a non-boolean ${nonBooleanProp}.`);
2009
+ throw new Error(`${where} has a non-boolean ${nonBooleanProp}. Use true or false.`);
1956
2010
  }
2011
+ validateFromTodayWindowReach(where, panel);
1957
2012
  for (const [key, color] of Object.entries(panel.seriesColors ?? {})) {
1958
2013
  if (typeof color !== "string" || color.trim() === "") {
1959
2014
  throw new Error(`${where} seriesColors["${key}"] must be a non-empty CSS color.`);
@@ -1988,7 +2043,7 @@ function validateStackedAreaOnlyProps(where, panel) {
1988
2043
  "markerLabels",
1989
2044
  ].filter((prop) => panel[prop] !== undefined);
1990
2045
  if (stackedAreaOnly.length > 0) {
1991
- throw new Error(`${where} sets ${stackedAreaOnly.join(", ")} on chart "${panel.chart}" — only "stacked-area" supports it.`);
2046
+ throw new Error(`${where} sets ${stackedAreaOnly.join(", ")} on chart "${panel.chart}" — only "stacked-area" supports it. Remove these props or use chart "stacked-area".`);
1992
2047
  }
1993
2048
  }
1994
2049
  const DASHBOARD_CHART_TONES = new Set([
@@ -1999,7 +2054,7 @@ const DASHBOARD_CHART_TONES = new Set([
1999
2054
  ]);
2000
2055
  function validateDashboardChartMarkerKind(where, look) {
2001
2056
  if (look.tone === undefined && look.color === undefined) {
2002
- throw new Error(`${where} needs a tone or a color.`);
2057
+ throw new Error(`${where} needs a tone or a color. Set tone or color.`);
2003
2058
  }
2004
2059
  if (look.tone !== undefined && !DASHBOARD_CHART_TONES.has(look.tone)) {
2005
2060
  throw new Error(`${where}.tone "${String(look.tone)}" is not one of ${[...DASHBOARD_CHART_TONES].join(", ")}.`);
@@ -2018,10 +2073,10 @@ function validateDashboardChartRanges(where, ranges) {
2018
2073
  const values = new Set();
2019
2074
  for (const option of ranges.options) {
2020
2075
  if (option.value.length === 0 || option.label.length === 0) {
2021
- throw new Error(`${where} option needs a non-empty value and label.`);
2076
+ throw new Error(`${where} option needs a non-empty value and label. Give every option both.`);
2022
2077
  }
2023
2078
  if (values.has(option.value)) {
2024
- throw new Error(`${where}.options has duplicate value "${option.value}".`);
2079
+ throw new Error(`${where}.options has duplicate value "${option.value}". Use each option value once.`);
2025
2080
  }
2026
2081
  if (option.months !== undefined && (!Number.isInteger(option.months) || option.months <= 0)) {
2027
2082
  throw new Error(`${where} option "${option.value}" months "${String(option.months)}" must be a positive integer.`);
@@ -2029,7 +2084,7 @@ function validateDashboardChartRanges(where, ranges) {
2029
2084
  values.add(option.value);
2030
2085
  }
2031
2086
  if (!values.has(ranges.default)) {
2032
- throw new Error(`${where}.default "${ranges.default}" is not among the options (${[...values].join(", ")}).`);
2087
+ throw new Error(`${where}.default "${ranges.default}" is not among the options (${[...values].join(", ")}). Set default to one of the options.`);
2033
2088
  }
2034
2089
  }
2035
2090
  const CURRENCY_CODE_RE = /^[A-Z]{3}$/;
@@ -2039,6 +2094,9 @@ function validateDashboardValueFormat(featureName, screenId, panelId, valueForma
2039
2094
  if (valueFormat === undefined)
2040
2095
  return;
2041
2096
  const where = `[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panelId}" valueFormat`;
2097
+ if (valueFormat.kind !== "currency") {
2098
+ throw new Error(`${where}.kind "${String(valueFormat.kind)}" must be "currency".`);
2099
+ }
2042
2100
  if (!CURRENCY_CODE_RE.test(valueFormat.currency)) {
2043
2101
  throw new Error(`${where}.currency "${String(valueFormat.currency)}" must be a 3-letter uppercase ISO currency code.`);
2044
2102
  }
@@ -2056,12 +2114,12 @@ export function validateDashboardCustomPanel(featureName, screenId, panel) {
2056
2114
  }
2057
2115
  function validateDashboardQueryPanel(featureName, screenId, panel, featureMap) {
2058
2116
  if (!panel.query || typeof panel.query !== "string") {
2059
- throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}" has empty or non-string query.`);
2117
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}" has empty or non-string query. Set query to a registered query name.`);
2060
2118
  }
2061
2119
  validateDashboardPanelSpan(featureName, screenId, panel);
2062
2120
  if (panel.kind === "stat") {
2063
2121
  if (panel.valueField.length === 0) {
2064
- throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-panel "${panel.id}" has empty valueField.`);
2122
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-panel "${panel.id}" has empty valueField. Set valueField to a field of the query result.`);
2065
2123
  }
2066
2124
  validateDashboardValueFormat(featureName, screenId, panel.id, panel.valueFormat);
2067
2125
  }
@@ -2070,7 +2128,7 @@ function validateDashboardQueryPanel(featureName, screenId, panel, featureMap) {
2070
2128
  validateDashboardChartDisplay(featureName, screenId, panel);
2071
2129
  if (panel.kind === "list") {
2072
2130
  if (panel.columns.length === 0) {
2073
- throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) list-panel "${panel.id}" has an empty columns list.`);
2131
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) list-panel "${panel.id}" has an empty columns list. Add at least one column.`);
2074
2132
  }
2075
2133
  for (const col of panel.columns) {
2076
2134
  const normalized = normalizeListColumn(col);
@@ -2083,10 +2141,10 @@ function validateDashboardQueryPanel(featureName, screenId, panel, featureMap) {
2083
2141
  }
2084
2142
  function validateDashboardFilterDefinition(featureName, screenId, filter) {
2085
2143
  if (filter.id.length === 0) {
2086
- throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter has an empty id.`);
2144
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter has an empty id. Set a non-empty id.`);
2087
2145
  }
2088
2146
  if (filter.label.length === 0) {
2089
- throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter has an empty label.`);
2147
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter has an empty label. Set a non-empty label.`);
2090
2148
  }
2091
2149
  const hasOptions = filter.options !== undefined;
2092
2150
  const hasOptionsQuery = filter.optionsQuery !== undefined;
@@ -2099,7 +2157,7 @@ function validateDashboardFilterDefinition(featureName, screenId, filter) {
2099
2157
  `declare at least one option or use optionsQuery instead.`);
2100
2158
  }
2101
2159
  if (hasOptionsQuery && filter.optionsQuery?.length === 0) {
2102
- throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter.optionsQuery is empty.`);
2160
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter.optionsQuery is empty. Set optionsQuery to a query name or remove it.`);
2103
2161
  }
2104
2162
  }
2105
2163
  // Form-check für ListColumn-Renderer in der PlatformComponent-Form
@@ -2120,7 +2178,7 @@ function rejectInertColumnSortable(featureName, screenId, screenType, column) {
2120
2178
  ? "set sortable: true on the entity field instead"
2121
2179
  : "sorting is derived from the query's Zod schema; only sortable: false is honored";
2122
2180
  throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenType}) column "${column.field}" sets sortable — ` +
2123
- `it is ignored here; ${source}.`);
2181
+ `it is ignored here; ${source}. Remove sortable from this column.`);
2124
2182
  }
2125
2183
  export function validateColumnRendererForm(featureName, screenId, column) {
2126
2184
  const renderer = column.renderer;
@@ -2191,7 +2249,7 @@ function validateListFacets(prefix, facets, columns, currentFeatureName, feature
2191
2249
  const seenFacetFields = new Set();
2192
2250
  for (const facet of facets) {
2193
2251
  if (seenFacetFields.has(facet.field)) {
2194
- throw new Error(`${prefix} declares facet "${facet.field}" more than once.`);
2252
+ throw new Error(`${prefix} declares facet "${facet.field}" more than once. Declare each facet field once.`);
2195
2253
  }
2196
2254
  seenFacetFields.add(facet.field);
2197
2255
  if (facet.type !== "reference" && !columnFieldNames.has(facet.field)) {