@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
@@ -1,4 +1,4 @@
1
- import { handlerFieldReference } from "../handler-field-references.js";
1
+ import { readHandlerFieldReference } from "../handler-field-references.js";
2
2
  import { parseRefTarget } from "../parse-ref-target.js";
3
3
  import { getZodObjectShape } from "./zod-shape.js";
4
4
  function validateHandlerGroup(feature, featureMap, handlers, handlerNoun) {
@@ -7,15 +7,25 @@ function validateHandlerGroup(feature, featureMap, handlers, handlerNoun) {
7
7
  if (shape === undefined)
8
8
  continue;
9
9
  for (const fieldName of Object.keys(shape)) {
10
- const reference = handlerFieldReference(def.schema, fieldName);
11
- if (reference === undefined)
10
+ const where = `Input field "${fieldName}" of ${handlerNoun} "${handlerName}"`;
11
+ const reading = readHandlerFieldReference(def.schema, fieldName);
12
+ if (reading.kind === "none")
12
13
  continue;
13
- const target = parseRefTarget(reference, feature.name);
14
- const targetEntities = featureMap.get(target.featureName)?.entities;
15
- if (targetEntities?.[target.entityName] !== undefined)
16
- continue;
17
- throw new Error(`[Feature ${feature.name}] Input field "${fieldName}" of ${handlerNoun} "${handlerName}" has meta references: "${reference}", ` +
18
- `which is not a registered entity. Use "<entity>" for this feature or "<feature>:<entity>" for another one.`);
14
+ if (reading.kind === "invalid") {
15
+ throw new Error(`[Feature ${feature.name}] ${where} has an invalid meta references value ${JSON.stringify(reading.raw)}. ` +
16
+ `Use "<entity>", "<feature>:<entity>" or { entity, by? }.`);
17
+ }
18
+ const { entity, by } = reading.reference;
19
+ const target = parseRefTarget(entity, feature.name);
20
+ const targetEntity = featureMap.get(target.featureName)?.entities?.[target.entityName];
21
+ if (targetEntity === undefined) {
22
+ throw new Error(`[Feature ${feature.name}] ${where} has meta references: "${entity}", ` +
23
+ `which is not a registered entity. Use "<entity>" for this feature or "<feature>:<entity>" for another one.`);
24
+ }
25
+ if (by !== undefined && targetEntity.fields[by] === undefined) {
26
+ throw new Error(`[Feature ${feature.name}] ${where} references entity "${entity}" by "${by}", ` +
27
+ `but that entity has no field "${by}". Use an existing field of that entity for "by".`);
28
+ }
19
29
  }
20
30
  }
21
31
  }
@@ -76,13 +76,13 @@ export function validateI18nSurfaceKeys(features) {
76
76
  if (isFrameworkOwnedI18nKey(key))
77
77
  continue;
78
78
  if (!hasDefinedTranslation(defined, key)) {
79
- throw new Error(`[i18n] Feature "${feature.name}": required translation key missing: "${key}"`);
79
+ throw new Error(`[i18n] Feature "${feature.name}": required translation key missing: "${key}". Add it via r.translations(...) in this feature.`);
80
80
  }
81
81
  }
82
82
  }
83
83
  for (const key of requiredKeysFromGeneratedConfigHub(generatedConfigHub)) {
84
84
  if (!hasDefinedTranslation(defined, key)) {
85
- throw new Error(`[i18n] Settings-Hub: required translation key missing: "${key}"`);
85
+ throw new Error(`[i18n] Settings-Hub: required translation key missing: "${key}". Add it via r.translations(...) in the owning feature.`);
86
86
  }
87
87
  }
88
88
  }
@@ -83,7 +83,7 @@ export function validateNavCycles(allNavQns) {
83
83
  const stack = new Set();
84
84
  function visit(qualified, path) {
85
85
  if (stack.has(qualified)) {
86
- throw new Error(`[Kumiko Nav] Nav entry parent cycle detected: ${[...path, qualified].join(" → ")}`);
86
+ throw new Error(`[Kumiko Nav] Nav entry parent cycle detected: ${[...path, qualified].join(" → ")}. Change or remove the parent of one entry in the cycle.`);
87
87
  }
88
88
  // skip: already visited — cycle-detection only needs to traverse each
89
89
  // node once, and the `stack` check above catches any actual cycles
@@ -50,7 +50,7 @@ function validateWriteOnlyField(featureName, entityName, fieldName, field) {
50
50
  for (const [subName, subField] of Object.entries(field.schema)) {
51
51
  if ("writeOnly" in subField && subField.writeOnly === true) {
52
52
  // @cast-boundary schema-walk
53
- throw new Error(`${where} declares writeOnly on embedded sub-field "${subName}" — writeOnly is only supported on top-level text fields.`);
53
+ throw new Error(`${where} declares writeOnly on embedded sub-field "${subName}" — writeOnly is only supported on top-level text fields. Remove writeOnly from the sub-field or declare it on a top-level text field.`);
54
54
  }
55
55
  }
56
56
  // skip: embedded fields carry no writeOnly of their own; sub-fields are checked above
@@ -61,7 +61,7 @@ function validateWriteOnlyField(featureName, entityName, fieldName, field) {
61
61
  if (flags.writeOnly !== true)
62
62
  return;
63
63
  if (field.type !== "text") {
64
- throw new Error(`${where} declares { writeOnly: true } but has type "${field.type}" — writeOnly only applies to text fields.`);
64
+ throw new Error(`${where} declares { writeOnly: true } but has type "${field.type}" — writeOnly only applies to text fields. Use a text field or remove writeOnly.`);
65
65
  }
66
66
  if (field.sensitive !== true) {
67
67
  throw new Error(`${where} declares { writeOnly: true } without { sensitive: true } — add find: "secret" (ciphertext at rest, stripped from event echoes).`);
@@ -69,7 +69,7 @@ function validateWriteOnlyField(featureName, entityName, fieldName, field) {
69
69
  for (const flag of WRITE_ONLY_FORBIDDEN_FLAGS) {
70
70
  const value = flags[flag];
71
71
  if (value !== undefined && value !== false) {
72
- throw new Error(`${where} combines { writeOnly: true } with { ${flag} } — a value that is never returned cannot be defaulted, searched, sorted, filtered or looked up.`);
72
+ throw new Error(`${where} combines { writeOnly: true } with { ${flag} } — a value that is never returned cannot be defaulted, searched, sorted, filtered or looked up. Remove either writeOnly or the conflicting flag.`);
73
73
  }
74
74
  }
75
75
  }
@@ -89,7 +89,7 @@ function validatePersonalJsonbField(featureName, entityName, entity, fieldName,
89
89
  const flags = field; // @cast-boundary schema-walk
90
90
  for (const flag of JSONB_PERSONAL_FORBIDDEN_FLAGS) {
91
91
  if (flags[flag] !== undefined && flags[flag] !== false) {
92
- throw new Error(`${where} combines a personal annotation on a jsonb field with { ${flag} } — ${reason}.`);
92
+ throw new Error(`${where} combines a personal annotation on a jsonb field with { ${flag} } — ${reason}. Remove the ${flag} flag or the personal annotation.`);
93
93
  }
94
94
  }
95
95
  const index = entity.indexes?.find((def) => def.columns.includes(fieldName));
@@ -151,7 +151,7 @@ export function validatePiiAndRetention(feature) {
151
151
  // zweite (deterministische!) Kopie ohne Nutzen.
152
152
  if (annot.lookupable === true) {
153
153
  if (field.type !== "text") {
154
- throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" declares find: "exact" (or "fuzzy") but has type "${field.type}" — blind-index equality lookups only apply to text fields.`);
154
+ throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" declares find: "exact" (or "fuzzy") but has type "${field.type}" — blind-index equality lookups only apply to text fields. Use a text field or set find: "none".`);
155
155
  }
156
156
  if (annotCount === 0) {
157
157
  throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" declares find: "exact" (or "fuzzy") without a subject annotation (personal: "self" / "tenant" / { of: "<field>" }). Plaintext fields don't need a blind index — add personal or set find: "none".`);
@@ -188,7 +188,7 @@ export function validatePiiAndRetention(feature) {
188
188
  if (annot.userOwned) {
189
189
  const ownerName = annot.userOwned.ownerField;
190
190
  if (!ownerName || typeof ownerName !== "string") {
191
- throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" has personal: { of: ... } without an owner field name`);
191
+ throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" has personal: { of: ... } without an owner field name. Pass the name of the owner field: personal: { of: "<ownerField>" }.`);
192
192
  }
193
193
  const ownerField = fieldsByName[ownerName];
194
194
  if (!ownerField) {
@@ -205,7 +205,7 @@ export function validatePiiAndRetention(feature) {
205
205
  }
206
206
  const { whenAbsent } = annot.userOwned;
207
207
  if (whenAbsent !== undefined && whenAbsent !== "tenant") {
208
- throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" declares personal.whenAbsent "${String(whenAbsent)}" — entity fields only support whenAbsent: "tenant" (no plaintext fallback for stored PII).`);
208
+ throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" declares personal.whenAbsent "${String(whenAbsent)}" — entity fields only support whenAbsent: "tenant" (no plaintext fallback for stored PII). Remove whenAbsent or set it to "tenant".`);
209
209
  }
210
210
  if (ownerField.required !== true && whenAbsent === undefined) {
211
211
  throw new Error(`[Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" is personal: { of: "${ownerName}" }, but "${ownerName}" is not required — rows without an owner would fail to encrypt (SubjectResolutionError, HTTP 500) on update, delete and forget. Declare { personal: { of: "${ownerName}", whenAbsent: "tenant" } } to encrypt those rows under the tenant key, or make "${ownerName}" required: true. Entity fields have no plaintext fallback (fw#3654).`);
@@ -259,7 +259,7 @@ export function validatePiiAndRetention(feature) {
259
259
  if (retention) {
260
260
  if (!KEEP_FOR_PATTERN.test(retention.keepFor)) {
261
261
  // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
262
- console.warn(`[kumiko:boot] [Feature ${feature.name}] Entity "${entityName}" retention.keepFor="${retention.keepFor}" hat ungueltiges Format. Erwartet: <Zahl><h|d|w|m|y> (z.B. "30d", "10y", "6m"). Cleanup-Job (Sprint 2) wird das nicht parsen koennen.`);
262
+ console.warn(`[kumiko:boot] [Feature ${feature.name}] Entity "${entityName}" retention.keepFor="${retention.keepFor}" has an invalid format. Expected: <number><h|d|w|m|y> (e.g. "30d", "10y", "6m"). The cleanup job cannot parse anything else; fix keepFor.`);
263
263
  }
264
264
  if (retention.reference !== undefined) {
265
265
  const refName = retention.reference;
@@ -32,7 +32,7 @@ function validateOneProjectionListScreen(feature, screen, queryHandlers) {
32
32
  const prefix = `[projectionList] Feature "${feature.name}" screen "${screen.id}"`;
33
33
  const schema = queryHandlers.get(screen.query)?.schema;
34
34
  if (screen.searchable === true && !schemaAccepts(schema, "search")) {
35
- throw new Error(`${prefix}: searchable: true but query "${screen.query}" has no "search" parameter in its Zod schema`);
35
+ throw new Error(`${prefix}: searchable: true but query "${screen.query}" has no "search" parameter in its Zod schema. Add a "search" field to the query schema or remove searchable: true.`);
36
36
  }
37
37
  if (screen.searchable === false &&
38
38
  schemaAccepts(schema, "search") &&
@@ -52,7 +52,7 @@ function validateOneProjectionListScreen(feature, screen, queryHandlers) {
52
52
  const searchActive = screen.searchable !== false && schemaAccepts(schema, "search");
53
53
  const sortActive = schemaAccepts(schema, "sort");
54
54
  if ((searchActive || sortActive) && screen.defaultSort === undefined) {
55
- throw new Error(`${prefix}: defaultSort required when search or sort is active`);
55
+ throw new Error(`${prefix}: defaultSort required when search or sort is active. Set defaultSort: { field, direction }.`);
56
56
  }
57
57
  validateProjectionListFilterSchemaAcceptance(prefix, screen, schema);
58
58
  validateProjectionListFacetsSchemaAcceptance(prefix, screen, schema);
@@ -97,7 +97,7 @@ function validateProjectionListDateRangeFacets(prefix, screen, schema) {
97
97
  continue;
98
98
  if (facet.params.from === facet.params.to) {
99
99
  throw new Error(`${prefix}: dateRange facet on "${facet.field}" names the same param ` +
100
- `"${facet.params.from}" for both bounds.`);
100
+ `"${facet.params.from}" for both bounds. Use two different param names for from and to.`);
101
101
  }
102
102
  for (const param of [facet.params.from, facet.params.to]) {
103
103
  if (RESERVED_LIST_PAYLOAD_KEYS.has(param)) {
@@ -106,7 +106,7 @@ function validateProjectionListDateRangeFacets(prefix, screen, schema) {
106
106
  }
107
107
  if (usedParams.has(param)) {
108
108
  throw new Error(`${prefix}: dateRange facet on "${facet.field}" names "${param}" which another dateRange ` +
109
- `facet on this screen already uses — the facets would overwrite each other's bound.`);
109
+ `facet on this screen already uses — the facets would overwrite each other's bound. Give each dateRange facet its own param names.`);
110
110
  }
111
111
  usedParams.add(param);
112
112
  if (schemaAccepts(schema, param))
@@ -144,7 +144,7 @@ function validateRelatedListSearchable(prefix, section, schema) {
144
144
  // skip: not searchable, or the schema already accepts search — nothing to reject.
145
145
  if (section.searchable !== true || schemaAccepts(schema, "search"))
146
146
  return;
147
- throw new Error(`${prefix}: searchable: true but query "${section.query}" has no "search" parameter in its Zod schema`);
147
+ throw new Error(`${prefix}: searchable: true but query "${section.query}" has no "search" parameter in its Zod schema. Add a "search" field to the query schema or remove searchable: true.`);
148
148
  }
149
149
  function validateRelatedListFacets(prefix, section, schema) {
150
150
  // skip: no facets declared — nothing to reject.
@@ -73,7 +73,7 @@ function checkProjectionDetailOutputFields(queryHandlers, featureName, screenId,
73
73
  const prefix = `[Feature ${featureName}] Screen "${screenId}" (projectionDetail)`;
74
74
  // @wrapper-known semantic-alias
75
75
  const checkHeaderField = (part, field) => {
76
- checkFieldExists(recordShape, field, () => `${prefix} header.${part} references field "${field}" which is not present in query "${screen.query}"'s outputSchema.`);
76
+ checkFieldExists(recordShape, field, () => `${prefix} header.${part} references field "${field}" which is not present in query "${screen.query}"'s outputSchema. Check the field name for a typo or add the field to the outputSchema.`);
77
77
  };
78
78
  if (screen.header !== undefined) {
79
79
  checkHeaderField("title", screen.header.title);
@@ -103,11 +103,11 @@ function checkProjectionDetailOutputFields(queryHandlers, featureName, screenId,
103
103
  }
104
104
  if (screen.recordTitleField !== undefined) {
105
105
  const field = screen.recordTitleField;
106
- checkFieldExists(recordShape, field, () => `${prefix} recordTitleField references field "${field}" which is not present in query "${screen.query}"'s outputSchema.`);
106
+ checkFieldExists(recordShape, field, () => `${prefix} recordTitleField references field "${field}" which is not present in query "${screen.query}"'s outputSchema. Check the field name for a typo or add the field to the outputSchema.`);
107
107
  }
108
108
  for (const metric of screen.metrics ?? []) {
109
109
  const field = metricField(metric);
110
- checkFieldExists(recordShape, field, () => `${prefix} metrics references field "${field}" which is not present in query "${screen.query}"'s outputSchema.`);
110
+ checkFieldExists(recordShape, field, () => `${prefix} metrics references field "${field}" which is not present in query "${screen.query}"'s outputSchema. Check the field name for a typo or add the field to the outputSchema.`);
111
111
  }
112
112
  checkDetailSectionFields(recordShape, prefix, screen);
113
113
  checkRelatedListToolbarActionFields(recordShape, featureName, screenId, screen);
@@ -119,13 +119,13 @@ function checkDetailSectionFields(recordShape, prefix, screen) {
119
119
  for (const section of screen.layout.sections) {
120
120
  const countField = "countField" in section ? section.countField : undefined;
121
121
  if (countField !== undefined) {
122
- checkFieldExists(recordShape, countField, () => `${prefix} section "${section.title ?? "(untitled)"}" countField references field "${countField}" which is not present in query "${screen.query}"'s outputSchema.`);
122
+ checkFieldExists(recordShape, countField, () => `${prefix} section "${section.title ?? "(untitled)"}" countField references field "${countField}" which is not present in query "${screen.query}"'s outputSchema. Check the field name for a typo or add the field to the outputSchema.`);
123
123
  }
124
124
  if (section.kind !== undefined && section.kind !== "fields")
125
125
  continue;
126
126
  for (const entry of sectionFieldSpecs(section)) {
127
127
  const field = typeof entry === "string" ? entry : entry.field;
128
- checkFieldExists(recordShape, field, () => `${prefix} section "${section.title ?? section.id ?? "(untitled)"}" references field "${field}" which is not present in query "${screen.query}"'s outputSchema.`);
128
+ checkFieldExists(recordShape, field, () => `${prefix} section "${section.title ?? section.id ?? "(untitled)"}" references field "${field}" which is not present in query "${screen.query}"'s outputSchema. Check the field name for a typo or add the field to the outputSchema.`);
129
129
  }
130
130
  }
131
131
  }
@@ -158,7 +158,7 @@ function checkDashboardStatPanelFields(queryHandlers, featureName, screenId, pan
158
158
  // skip: this stat-panel field slot is optional and wasn't declared — nothing to validate
159
159
  if (field === undefined)
160
160
  return;
161
- checkFieldExists(recordShape, field, () => `${prefix} ${part} references field "${field}" which is not present in query "${panel.query}"'s outputSchema.`);
161
+ checkFieldExists(recordShape, field, () => `${prefix} ${part} references field "${field}" which is not present in query "${panel.query}"'s outputSchema. Check the field name for a typo or add the field to the outputSchema.`);
162
162
  };
163
163
  checkPanelField("valueField", panel.valueField);
164
164
  checkPanelField("subField", panel.subField);
@@ -174,7 +174,7 @@ function checkVisibleWhenField(queryHandlers, visibleWhen, owner) {
174
174
  if (visibleWhen === undefined)
175
175
  return;
176
176
  const { query, field } = visibleWhen;
177
- checkFieldExists(getZodObjectShape(queryHandlers.get(query)?.outputSchema), field, () => `${owner} visibleWhen references field "${field}" which is not present in query "${query}"'s outputSchema.`);
177
+ checkFieldExists(getZodObjectShape(queryHandlers.get(query)?.outputSchema), field, () => `${owner} visibleWhen references field "${field}" which is not present in query "${query}"'s outputSchema. Check the field name for a typo or add the field to the outputSchema.`);
178
178
  }
179
179
  function checkDashboardOutputFields(queryHandlers, featureName, screenId, screen) {
180
180
  for (const panel of screen.panels) {
@@ -1,6 +1,6 @@
1
1
  import type { IconKey } from "@cosmicdrift/kumiko-types/nav-icon";
2
2
  import type { ConfigKeyDefinition, FeatureDefinition } from "../types/index.js";
3
- import type { DashboardCustomPanel, DashboardScreenPanel, RelatedListToolbarAction, RowAction, RowFieldExtractor, ScreenDefinition, ToolbarAction } from "../types/screen.js";
3
+ import type { ActionFormScreenDefinition, DashboardCustomPanel, DashboardScreenPanel, RelatedListToolbarAction, RowAction, RowFieldExtractor, ScreenDefinition, SecretMintScreenDefinition, ToolbarAction } from "../types/screen.js";
4
4
  export declare function rowFieldExtractorKeys(params: RowFieldExtractor): readonly string[];
5
5
  export declare function readsNavigateParamsAsFormPrefill(target: ScreenDefinition, explicitEntityId: string | undefined, sourceScreenEntity: string | undefined): boolean;
6
6
  export declare function validateActionHasIcon(featureName: string, screenId: string, screenType: "projectionDetail" | "entityEdit" | "entityList" | "projectionList", context: string, action: {
@@ -13,6 +13,7 @@ export declare function validateScreenShortIdCollisions(screensByShortId: Readon
13
13
  readonly screen: ScreenDefinition;
14
14
  }>>): void;
15
15
  export declare function resolveScreenTargetQn(featureName: string, target: string): string;
16
+ export declare function validateDrawerPrefillKeys(where: string, targetLabel: "drawer-target" | "navigate-target", targetScreenId: string, target: ActionFormScreenDefinition | SecretMintScreenDefinition, params: RowFieldExtractor): void;
16
17
  export declare function validateScreens(feature: FeatureDefinition, featureMap: ReadonlyMap<string, FeatureDefinition>, allWriteHandlerQns: ReadonlySet<string>, allScreenQns: ReadonlySet<string>, configKeyDefsByQn: ReadonlyMap<string, ConfigKeyDefinition>, screensByShortId: ReadonlyMap<string, ReadonlyArray<{
17
18
  readonly featureName: string;
18
19
  readonly screen: ScreenDefinition;