@cosmicdrift/kumiko-framework 0.387.0 → 0.388.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/AGENTS.md +6 -1
  2. package/agents/examples/anonymous-access.md +1 -1
  3. package/agents/examples/apex-landing.md +204 -0
  4. package/agents/examples/api-worker-split.md +64 -0
  5. package/agents/examples/app-config.md +14 -0
  6. package/agents/examples/auth-mfa.md +115 -0
  7. package/agents/examples/env-composition.md +28 -1
  8. package/agents/examples/legal-pages.md +1 -1
  9. package/agents/examples/prod-boot-options.md +16 -0
  10. package/agents/examples/rate-limiting.md +48 -0
  11. package/agents/examples/record-detail-layout.md +692 -0
  12. package/agents/examples/seed-migration.md +212 -0
  13. package/agents/examples/wizard-form.md +941 -0
  14. package/agents/examples/writeform-section.md +548 -0
  15. package/agents/features.md +19 -2
  16. package/agents/widgets.md +1 -0
  17. package/dist/api/client-error-route.js +8 -2
  18. package/dist/api/index.d.ts +3 -1
  19. package/dist/api/index.js +3 -1
  20. package/dist/api/request-context.d.ts +1 -1
  21. package/dist/api/request-context.js +1 -1
  22. package/dist/api/server-boot-checks.d.ts +10 -0
  23. package/dist/api/server-boot-checks.js +157 -0
  24. package/dist/api/server-event-dispatcher.d.ts +26 -0
  25. package/dist/api/server-event-dispatcher.js +208 -0
  26. package/dist/api/server-extra-routes.d.ts +11 -0
  27. package/dist/api/server-extra-routes.js +203 -0
  28. package/dist/api/server-middleware.d.ts +20 -0
  29. package/dist/api/server-middleware.js +211 -0
  30. package/dist/api/server-routes.d.ts +19 -0
  31. package/dist/api/server-routes.js +293 -0
  32. package/dist/api/server-websocket-routes.d.ts +4 -0
  33. package/dist/api/server-websocket-routes.js +108 -0
  34. package/dist/api/server-wiring.d.ts +32 -0
  35. package/dist/api/server-wiring.js +236 -0
  36. package/dist/api/server.d.ts +10 -15
  37. package/dist/api/server.js +48 -1251
  38. package/dist/api/session-cookies.js +4 -4
  39. package/dist/api/sse-route.js +16 -12
  40. package/dist/crypto/owner-transition-rekey.d.ts +5 -1
  41. package/dist/crypto/owner-transition-rekey.js +1 -1
  42. package/dist/crypto/pii-field-encryption.d.ts +5 -1
  43. package/dist/crypto/pii-field-encryption.js +3 -1
  44. package/dist/db/aggregate-sql.d.ts +21 -0
  45. package/dist/db/aggregate-sql.js +155 -0
  46. package/dist/db/closed-connection.d.ts +9 -0
  47. package/dist/db/closed-connection.js +95 -0
  48. package/dist/db/dialect.js +1 -1
  49. package/dist/db/distinct-on.d.ts +1 -1
  50. package/dist/db/driver-row-coercion.d.ts +2 -0
  51. package/dist/db/driver-row-coercion.js +186 -0
  52. package/dist/db/driver-value-preparation.d.ts +16 -0
  53. package/dist/db/driver-value-preparation.js +99 -0
  54. package/dist/db/entity-field-encryption.d.ts +1 -0
  55. package/dist/db/entity-field-encryption.js +1 -1
  56. package/dist/db/entity-table-meta.js +103 -110
  57. package/dist/db/event-store-executor-context.d.ts +2 -1
  58. package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
  59. package/dist/db/event-store-executor-read-aggregate.js +112 -0
  60. package/dist/db/event-store-executor-read-filter.d.ts +31 -0
  61. package/dist/db/event-store-executor-read-filter.js +203 -0
  62. package/dist/db/event-store-executor-read-paging.d.ts +14 -0
  63. package/dist/db/event-store-executor-read-paging.js +99 -0
  64. package/dist/db/event-store-executor-read-rows.d.ts +6 -0
  65. package/dist/db/event-store-executor-read-rows.js +32 -0
  66. package/dist/db/event-store-executor-read-search.d.ts +28 -0
  67. package/dist/db/event-store-executor-read-search.js +221 -0
  68. package/dist/db/event-store-executor-read-sort.d.ts +41 -0
  69. package/dist/db/event-store-executor-read-sort.js +130 -0
  70. package/dist/db/event-store-executor-read-where.d.ts +24 -0
  71. package/dist/db/event-store-executor-read-where.js +81 -0
  72. package/dist/db/event-store-executor-read.d.ts +2 -9
  73. package/dist/db/event-store-executor-read.js +48 -701
  74. package/dist/db/event-store-executor-write-steps.d.ts +20 -0
  75. package/dist/db/event-store-executor-write-steps.js +100 -0
  76. package/dist/db/event-store-executor-write.js +395 -499
  77. package/dist/db/index.d.ts +6 -2
  78. package/dist/db/index.js +5 -1
  79. package/dist/db/list-aggregate-labels.d.ts +14 -0
  80. package/dist/db/list-aggregate-labels.js +26 -0
  81. package/dist/db/migrate-generator.js +71 -86
  82. package/dist/db/migrate-runner.js +4 -1
  83. package/dist/db/query.d.ts +2 -15
  84. package/dist/db/query.js +8 -761
  85. package/dist/db/reference-data.js +6 -1
  86. package/dist/db/table-builder.js +113 -166
  87. package/dist/db/table-info.d.ts +10 -0
  88. package/dist/db/table-info.js +45 -0
  89. package/dist/db/temporal-value-guards.d.ts +6 -0
  90. package/dist/db/temporal-value-guards.js +15 -0
  91. package/dist/db/tenant-db-runner.d.ts +1 -0
  92. package/dist/db/tenant-db-runner.js +3 -0
  93. package/dist/db/tenant-db.js +39 -9
  94. package/dist/db/where-clause.d.ts +6 -0
  95. package/dist/db/where-clause.js +236 -0
  96. package/dist/derivatives/delete-stored-file.js +2 -1
  97. package/dist/engine/boot-validator/boot-check.js +1 -1
  98. package/dist/engine/boot-validator/detail-screens.js +2 -2
  99. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  100. package/dist/engine/boot-validator/entity-handler.js +17 -0
  101. package/dist/engine/boot-validator/form-derivations.js +35 -1
  102. package/dist/engine/boot-validator/i18n-keys.js +5 -3
  103. package/dist/engine/boot-validator/index.js +2 -0
  104. package/dist/engine/boot-validator/pii-retention.js +233 -155
  105. package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
  106. package/dist/engine/boot-validator/screens-actions.js +8 -1
  107. package/dist/engine/boot-validator/screens-dashboard.js +2 -5
  108. package/dist/engine/boot-validator/screens-forms.js +2 -6
  109. package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
  110. package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
  111. package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
  112. package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
  113. package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
  114. package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
  115. package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
  116. package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
  117. package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
  118. package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
  119. package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
  120. package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
  121. package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
  122. package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
  123. package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
  124. package/dist/engine/boot-validator/screens-lists.js +12 -9
  125. package/dist/engine/boot-validator/screens.d.ts +0 -2
  126. package/dist/engine/boot-validator/screens.js +99 -860
  127. package/dist/engine/build-app-schema.js +56 -96
  128. package/dist/engine/define-feature.js +9 -2
  129. package/dist/engine/define-handler.js +1 -0
  130. package/dist/engine/entity-handlers.d.ts +13 -0
  131. package/dist/engine/entity-handlers.js +10 -21
  132. package/dist/engine/entity-list-runtime-options.d.ts +8 -0
  133. package/dist/engine/entity-list-runtime-options.js +18 -0
  134. package/dist/engine/feature-ast/extractors/handlers.js +1 -0
  135. package/dist/engine/feature-ast/extractors/round2.js +25 -6
  136. package/dist/engine/feature-ast/patch.js +4 -182
  137. package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
  138. package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
  139. package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
  140. package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
  141. package/dist/engine/feature-ast/patterns.d.ts +9 -0
  142. package/dist/engine/feature-ast/patterns.js +43 -51
  143. package/dist/engine/feature-ast/render.js +48 -87
  144. package/dist/engine/feature-entity-handlers.js +3 -0
  145. package/dist/engine/form-derivations.js +18 -7
  146. package/dist/engine/handler-field-references.js +1 -1
  147. package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
  148. package/dist/engine/project-app-schema-for-roles.js +6 -4
  149. package/dist/engine/registry-ingest.js +1 -1
  150. package/dist/engine/schema-builder.js +228 -212
  151. package/dist/engine/screen-helpers.js +1 -1
  152. package/dist/engine/screen-kind.d.ts +7 -0
  153. package/dist/engine/screen-kind.js +1 -0
  154. package/dist/engine/strip-disabled-features-from-app-schema.js +51 -14
  155. package/dist/engine/validate-projection-allowlist.js +2 -1
  156. package/dist/entrypoint/index.d.ts +4 -1
  157. package/dist/entrypoint/index.js +7 -1
  158. package/dist/errors/docs-url.d.ts +1 -1
  159. package/dist/errors/docs-url.js +22 -2
  160. package/dist/es-ops/context.js +1 -1
  161. package/dist/event-store/admin-api.js +1 -1
  162. package/dist/event-store/event-store.js +10 -4
  163. package/dist/files/bind-pending-file-refs.js +3 -3
  164. package/dist/files/create-file-ref-from-bytes.js +4 -1
  165. package/dist/files/file-handle.js +2 -3
  166. package/dist/files/file-routes.js +11 -8
  167. package/dist/files/resolve-content-type.js +43 -0
  168. package/dist/files/upload-ledger.js +7 -0
  169. package/dist/i18n/required-surface-keys.js +182 -206
  170. package/dist/internal/stack-support.d.ts +2 -1
  171. package/dist/internal/stack-support.js +1 -1
  172. package/dist/jobs/job-runner-context.d.ts +32 -0
  173. package/dist/jobs/job-runner-context.js +305 -0
  174. package/dist/jobs/job-runner-execution.d.ts +7 -0
  175. package/dist/jobs/job-runner-execution.js +160 -0
  176. package/dist/jobs/job-runner-gates.d.ts +19 -0
  177. package/dist/jobs/job-runner-gates.js +198 -0
  178. package/dist/jobs/job-runner-helpers.d.ts +17 -3
  179. package/dist/jobs/job-runner-helpers.js +61 -2
  180. package/dist/jobs/job-runner-scope.d.ts +44 -0
  181. package/dist/jobs/job-runner-scope.js +102 -0
  182. package/dist/jobs/job-runner-types.d.ts +12 -0
  183. package/dist/jobs/job-runner.d.ts +0 -1
  184. package/dist/jobs/job-runner.js +47 -657
  185. package/dist/observability/escape-hatch-report.d.ts +3 -0
  186. package/dist/observability/escape-hatch-report.js +12 -1
  187. package/dist/pipeline/cascade-handler.js +165 -138
  188. package/dist/pipeline/dispatch-query.js +10 -3
  189. package/dist/pipeline/dispatch-shared.d.ts +13 -3
  190. package/dist/pipeline/dispatch-shared.js +58 -710
  191. package/dist/pipeline/dispatch-write.js +296 -189
  192. package/dist/pipeline/event-dispatcher-admin.js +2 -1
  193. package/dist/pipeline/event-dispatcher-delivery.js +3 -5
  194. package/dist/pipeline/handler-context-auth.d.ts +9 -0
  195. package/dist/pipeline/handler-context-auth.js +54 -0
  196. package/dist/pipeline/handler-context-config.d.ts +14 -0
  197. package/dist/pipeline/handler-context-config.js +113 -0
  198. package/dist/pipeline/handler-context-db.d.ts +24 -0
  199. package/dist/pipeline/handler-context-db.js +149 -0
  200. package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
  201. package/dist/pipeline/handler-context-dispatch.js +89 -0
  202. package/dist/pipeline/handler-context-events.d.ts +43 -0
  203. package/dist/pipeline/handler-context-events.js +237 -0
  204. package/dist/pipeline/handler-context-notify.d.ts +12 -0
  205. package/dist/pipeline/handler-context-notify.js +55 -0
  206. package/dist/pipeline/handler-context-observability.d.ts +7 -0
  207. package/dist/pipeline/handler-context-observability.js +21 -0
  208. package/dist/pipeline/handler-context-scope.d.ts +48 -0
  209. package/dist/pipeline/handler-context-scope.js +88 -0
  210. package/dist/pipeline/notify-scope.js +12 -1
  211. package/dist/pipeline/pre-handler-gates.js +20 -4
  212. package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
  213. package/dist/pipeline/pre-transaction-reservations.js +9 -3
  214. package/dist/pipeline/projection-rebuild.js +2 -1
  215. package/dist/schema-cli/apply-command.d.ts +2 -0
  216. package/dist/schema-cli/apply-command.js +97 -0
  217. package/dist/schema-cli/baseline-command.d.ts +2 -0
  218. package/dist/schema-cli/baseline-command.js +33 -0
  219. package/dist/schema-cli/generate-command.d.ts +2 -0
  220. package/dist/schema-cli/generate-command.js +90 -0
  221. package/dist/schema-cli/shared.d.ts +39 -0
  222. package/dist/schema-cli/shared.js +95 -0
  223. package/dist/schema-cli/status-command.d.ts +2 -0
  224. package/dist/schema-cli/status-command.js +73 -0
  225. package/dist/schema-cli/validate-boot-input.d.ts +5 -0
  226. package/dist/schema-cli/validate-boot-input.js +130 -0
  227. package/dist/schema-cli/validate-command.d.ts +2 -0
  228. package/dist/schema-cli/validate-command.js +130 -0
  229. package/dist/schema-cli.d.ts +2 -18
  230. package/dist/schema-cli.js +47 -604
  231. package/dist/search/meilisearch-adapter.js +2 -1
  232. package/dist/upgrade-cli.js +149 -121
  233. package/package.json +4 -4
  234. package/src/changes.json +310 -0
  235. package/src/scripts/codemod/feature-layout.ts +10 -2
  236. package/src/scripts/codemod/object-form.ts +41 -4
  237. package/src/scripts/codemod/package-merge.ts +58 -8
  238. package/src/scripts/codemod/pii-personal-migration.ts +175 -145
  239. package/src/scripts/codemod/testing-injection.ts +122 -42
  240. package/src/scripts/codemod/testing-move.ts +29 -7
@@ -0,0 +1,37 @@
1
+ import { type normalizeListColumn } from "../screen-helpers.ts";
2
+ import type { ConfigKeyDefinition, EntityDefinition, FeatureDefinition } from "../types/index.ts";
3
+ import type { EditExtensionSection, EditFieldsSection, EditSectionSpec, RowAction } from "../types/screen.ts";
4
+ import type { NavigateSourceScreenType, NavigateTargetLookups } from "./screens-actions.ts";
5
+ import { type resolveRowActionNavigateTarget } from "./screens-actions.ts";
6
+ import type { ListActionTargetContext } from "./screens-lists.ts";
7
+ export type ScreenValidationContext = NavigateTargetLookups & {
8
+ readonly feature: FeatureDefinition;
9
+ readonly featureMap: ReadonlyMap<string, FeatureDefinition>;
10
+ readonly allWriteHandlerQns: ReadonlySet<string>;
11
+ readonly configKeyDefsByQn: ReadonlyMap<string, ConfigKeyDefinition>;
12
+ };
13
+ export declare function findEntityFeature(entityName: string, featureMap: ReadonlyMap<string, FeatureDefinition>): string | undefined;
14
+ export declare function buildUnknownFieldMessage(featureName: string, screenId: string, fieldName: string, entityName: string, knownFields: ReadonlySet<string>): string;
15
+ export declare function listActionContext(ctx: ScreenValidationContext, screenId: string, screenType: NavigateSourceScreenType, screenEntity: string | undefined, where: string): ListActionTargetContext;
16
+ export declare function resolveScreenEntity(ctx: ScreenValidationContext, screenId: string, entityName: string): EntityDefinition;
17
+ export declare function rejectMissingQuery(featureName: string, screenId: string, kind: "projectionList" | "projectionDetail", query: unknown): void;
18
+ export declare function rejectEmptyColumns(featureName: string, screenId: string, kind: "projectionList" | "entityList", columnCount: number): void;
19
+ export declare function validateListColumnRendering(ctx: ScreenValidationContext, screenId: string, kind: "projectionList" | "entityList", column: ReturnType<typeof normalizeListColumn>): void;
20
+ export declare function rejectNonArrayInFilter(featureName: string, screenId: string, kind: "projectionList" | "entityList", filter: {
21
+ readonly op: string;
22
+ readonly value?: unknown;
23
+ }): void;
24
+ export declare function rejectEmptySections(featureName: string, screenId: string, kind: "projectionDetail" | "configEdit" | "entityEdit", sectionCount: number): void;
25
+ export declare function assertExtensionSectionHasComponent(featureName: string, screenId: string, kind: "projectionDetail" | "configEdit" | "entityEdit", section: EditExtensionSection): void;
26
+ export declare function rejectProjectionDetailOnlySection(featureName: string, screenId: string, kind: "configEdit" | "entityEdit", section: Exclude<EditSectionSpec, EditExtensionSection>): asserts section is EditFieldsSection;
27
+ export declare function editSectionLabel(section: EditSectionSpec): string;
28
+ export declare function validateRecordSectionActions(ctx: ScreenValidationContext, screenId: string, kind: "projectionDetail" | "entityEdit", section: EditSectionSpec): void;
29
+ export declare function validateRecordHeaderAction(ctx: ScreenValidationContext, screenId: string, kind: "projectionDetail" | "entityEdit", screenEntity: string | undefined, action: RowAction): void;
30
+ type NavigateLink = {
31
+ readonly screen?: string;
32
+ readonly entity?: string;
33
+ readonly entityId?: string;
34
+ readonly tab?: string;
35
+ };
36
+ export declare function navigateActionFromLink(id: string, navigate: NavigateLink): Parameters<typeof resolveRowActionNavigateTarget>[4];
37
+ export {};
@@ -0,0 +1,142 @@
1
+ // Checks shared by several screen kinds; each helper takes the kind it reports under.
2
+ // Dependencies point one way: screens-kind-*.ts -> here ->
3
+ // screens-dashboard/forms/lists -> screens-actions.
4
+ import { qualifyEntityName } from "../qualified-name.js";
5
+ import { isWriteFormEditSection } from "../screen-helpers.js";
6
+ import { isRowClickAction, validateActionHasIcon, validateNavigateActionTab, validateRecordActionRedirect, } from "./screens-actions.js";
7
+ import { assertRefTargetRegistered, rejectInertColumnSortable, validateColumnRendererForm, validateRowActionTarget, } from "./screens-lists.js";
8
+ export function findEntityFeature(entityName, featureMap) {
9
+ for (const [name, feature] of featureMap) {
10
+ if (feature.entities?.[entityName])
11
+ return name;
12
+ }
13
+ return undefined;
14
+ }
15
+ export function buildUnknownFieldMessage(featureName, screenId, fieldName, entityName, knownFields) {
16
+ const known = [...knownFields].sort().join(", ");
17
+ return (`[Feature ${featureName}] Screen "${screenId}" references field "${fieldName}" ` +
18
+ `which does not exist on entity "${entityName}" (known: ${known}).`);
19
+ }
20
+ export function listActionContext(ctx, screenId, screenType, screenEntity, where) {
21
+ return {
22
+ featureName: ctx.feature.name,
23
+ screenId,
24
+ screenType,
25
+ screenEntity,
26
+ where,
27
+ screens: ctx.feature.screens,
28
+ allWriteHandlerQns: ctx.allWriteHandlerQns,
29
+ allScreenQns: ctx.allScreenQns,
30
+ navTargetShortIds: ctx.navTargetShortIds,
31
+ screensByShortId: ctx.screensByShortId,
32
+ detailForScreens: ctx.detailForScreens,
33
+ };
34
+ }
35
+ // entityList / entityEdit: entity-refs are feature-local.
36
+ export function resolveScreenEntity(ctx, screenId, entityName) {
37
+ const entityDef = ctx.feature.entities?.[entityName];
38
+ if (entityDef)
39
+ return entityDef;
40
+ const known = Object.keys(ctx.feature.entities ?? {})
41
+ .sort()
42
+ .join(", ") || "(none)";
43
+ const crossFeature = findEntityFeature(entityName, ctx.featureMap);
44
+ const hint = crossFeature
45
+ ? ` Entity "${entityName}" is owned by feature "${crossFeature}" — cross-feature screen ownership is not supported.`
46
+ : "";
47
+ throw new Error(`[Feature ${ctx.feature.name}] Screen "${screenId}" references entity "${entityName}" ` +
48
+ `which is not declared in this feature (known: ${known}).${hint}`);
49
+ }
50
+ export function rejectMissingQuery(featureName, screenId, kind, query) {
51
+ if (!query || typeof query !== "string") {
52
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${kind}) has empty or non-string query. Set query to a registered query name.`);
53
+ }
54
+ }
55
+ // An empty column list would render a blank table: almost always an
56
+ // in-progress screen the author forgot to fill in.
57
+ export function rejectEmptyColumns(featureName, screenId, kind, columnCount) {
58
+ if (columnCount === 0) {
59
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${kind}) has an empty columns list — ` +
60
+ `declare at least one column.`);
61
+ }
62
+ }
63
+ export function validateListColumnRendering(ctx, screenId, kind, column) {
64
+ const featureName = ctx.feature.name;
65
+ validateColumnRendererForm(featureName, screenId, column);
66
+ rejectInertColumnSortable(featureName, screenId, kind, column);
67
+ if (column.refEntity !== undefined) {
68
+ assertRefTargetRegistered(`[Feature ${featureName}] Screen "${screenId}" (${kind})`, `column "${column.field}" (refEntity)`, column.refEntity, featureName, ctx.featureMap);
69
+ }
70
+ }
71
+ export function rejectNonArrayInFilter(featureName, screenId, kind, filter) {
72
+ if (filter.op === "in" && !Array.isArray(filter.value)) {
73
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${kind}) filter.op "in" requires ` +
74
+ `filter.value to be a readonly array. Pass an array as filter.value.`);
75
+ }
76
+ }
77
+ // A layout with zero sections renders as nothing: reject at boot so the
78
+ // author sees it before the blank form surprises them.
79
+ export function rejectEmptySections(featureName, screenId, kind, sectionCount) {
80
+ if (sectionCount === 0) {
81
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${kind}) has an empty sections list — ` +
82
+ `declare at least one section.`);
83
+ }
84
+ }
85
+ export function assertExtensionSectionHasComponent(featureName, screenId, kind, section) {
86
+ if (section.component?.react === undefined && section.component?.native === undefined) {
87
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${kind}) extension section ` +
88
+ `"${section.title}" has no component — declare a react/native component marker.`);
89
+ }
90
+ }
91
+ // relatedList and writeForm sections exist on projectionDetail only; what
92
+ // remains afterwards is a plain fields/groups section.
93
+ export function rejectProjectionDetailOnlySection(featureName, screenId, kind, section) {
94
+ if (section.kind === "relatedList") {
95
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${kind}) relatedList section ` +
96
+ `"${section.title}" is not supported — relatedList is a projectionDetail-only ` +
97
+ `primitive (fw#2166). Move the section to a projectionDetail screen or remove it.`);
98
+ }
99
+ if (isWriteFormEditSection(section)) {
100
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${kind}) writeForm section ` +
101
+ `"${section.title}" is not supported — writeForm is a projectionDetail-only primitive. Move the section to a projectionDetail screen or remove it.`);
102
+ }
103
+ }
104
+ export function editSectionLabel(section) {
105
+ return section.title ?? section.id ?? "(untitled)";
106
+ }
107
+ export function validateRecordSectionActions(ctx, screenId, kind, section) {
108
+ const actionContext = `section "${editSectionLabel(section)}" action`;
109
+ for (const action of section.actions ?? []) {
110
+ validateActionHasIcon(ctx.feature.name, screenId, kind, actionContext, action);
111
+ validateRecordActionRedirect(ctx.feature, screenId, kind, action, ctx);
112
+ validateNavigateActionTab(ctx.feature.name, screenId, kind, actionContext, action, ctx);
113
+ }
114
+ }
115
+ const NO_ROW_SUBJECT = {
116
+ projectionDetail: "a detail screen",
117
+ entityEdit: "an edit screen",
118
+ };
119
+ // Header actions reuse RowAction (the displayed record stands in for the row),
120
+ // so the same navigate/writeHandler existence checks as list rowActions apply.
121
+ // `rowClick` is rejected outright: there is no row to click.
122
+ export function validateRecordHeaderAction(ctx, screenId, kind, screenEntity, action) {
123
+ const featureName = ctx.feature.name;
124
+ validateRecordActionRedirect(ctx.feature, screenId, kind, action, ctx);
125
+ if (isRowClickAction(action)) {
126
+ throw new Error(`[Feature ${featureName}] Screen "${qualifyEntityName(featureName, "screen", screenId)}" ` +
127
+ `(${kind}) action "${action.id}" sets rowClick: true — there is no row to ` +
128
+ `click on ${NO_ROW_SUBJECT[kind]}. Remove rowClick.`);
129
+ }
130
+ validateRowActionTarget(listActionContext(ctx, screenId, kind, screenEntity, `Screen "${screenId}" (${kind})`), "action", "rowAction", action);
131
+ }
132
+ export function navigateActionFromLink(id, navigate) {
133
+ return {
134
+ kind: "navigate",
135
+ id,
136
+ label: id,
137
+ ...(navigate.screen !== undefined ? { screen: navigate.screen } : {}),
138
+ ...(navigate.entity !== undefined ? { entity: navigate.entity } : {}),
139
+ ...(navigate.entityId !== undefined ? { entityId: navigate.entityId } : {}),
140
+ ...(navigate.tab !== undefined ? { tab: navigate.tab } : {}),
141
+ };
142
+ }
@@ -24,7 +24,7 @@ type RelatedListSectionValidation = {
24
24
  }>;
25
25
  };
26
26
  export declare function validateRelatedListSection(args: RelatedListSectionValidation): void;
27
- type ListActionTargetContext = NavigateTargetLookups & {
27
+ export type ListActionTargetContext = NavigateTargetLookups & {
28
28
  readonly featureName: string;
29
29
  readonly screenId: string;
30
30
  readonly screenType: NavigateSourceScreenType;
@@ -34,6 +34,7 @@ type ListActionTargetContext = NavigateTargetLookups & {
34
34
  readonly allWriteHandlerQns: ReadonlySet<string>;
35
35
  };
36
36
  export declare function validateListActionTargets(ctx: ListActionTargetContext, actionLabel: "rowAction" | "emptyState action", action: RowAction): void;
37
+ export declare function validateRowActionTarget(ctx: ListActionTargetContext, actionLabel: "rowAction" | "emptyState action" | "action", paramsLabel: "rowAction" | "emptyState action", action: RowAction): void;
37
38
  export declare function rejectInertColumnSortable(featureName: string, screenId: string, screenType: "entityList" | "projectionList", column: {
38
39
  readonly field: string;
39
40
  readonly sortable?: boolean;
@@ -2,7 +2,7 @@ import { REFERENCE_LOOKUP_SOURCES } from "../../ui-types/list-row-meta.js";
2
2
  import { parseRefTarget } from "../parse-ref-target.js";
3
3
  import { qualifyEntityName } from "../qualified-name.js";
4
4
  import { explicitListScreenId, normalizeListColumn, resolveNavParentScreen, } from "../screen-helpers.js";
5
- import { isRowClickAction, rejectRedirectOnListRowAction, resolveRowActionNavigateTarget, validateClientActionName, validateDrawerTargetAction, validateRowActionNavigateParams, } from "./screens-actions.js";
5
+ import { assertWriteHandlerRegistered, isRowClickAction, rejectRedirectOnListRowAction, resolveRowActionNavigateTarget, validateClientActionName, validateDrawerTargetAction, validateRowActionNavigateParams, } from "./screens-actions.js";
6
6
  // validateScreenHasNavArea runs once per screen; the index is built once per
7
7
  // featureMap (immutable during boot) instead of re-scanning every feature each call.
8
8
  const navAreaIndexByFeatureMap = new WeakMap();
@@ -115,13 +115,19 @@ function rejectEmptyStateActionParams(ctx, actionLabel, action) {
115
115
  // Target checks shared by list/related-list rowActions and emptyState.action:
116
116
  // navigate target + params, drawer target, write-handler registration.
117
117
  export function validateListActionTargets(ctx, actionLabel, action) {
118
- const { featureName, screenId, screenType } = ctx;
119
- rejectRedirectOnListRowAction(featureName, screenId, screenType, action);
118
+ rejectRedirectOnListRowAction(ctx.featureName, ctx.screenId, ctx.screenType, action);
120
119
  if (actionLabel === "emptyState action")
121
120
  rejectEmptyStateActionParams(ctx, actionLabel, action);
121
+ validateRowActionTarget(ctx, actionLabel, actionLabel, action);
122
+ }
123
+ // Header actions have always reported navigate-params errors under the
124
+ // "rowAction" label, which validateRowActionNavigateParams only knows in list
125
+ // labels — hence paramsLabel apart from actionLabel.
126
+ export function validateRowActionTarget(ctx, actionLabel, paramsLabel, action) {
127
+ const { featureName, screenId, screenType } = ctx;
122
128
  if (action.kind === "navigate") {
123
129
  const target = resolveRowActionNavigateTarget(featureName, screenId, screenType, actionLabel, action, ctx.allScreenQns, ctx.navTargetShortIds, ctx.screensByShortId, ctx.detailForScreens);
124
- validateRowActionNavigateParams(featureName, screenId, screenType, ctx.screenEntity, action, target, actionLabel);
130
+ validateRowActionNavigateParams(featureName, screenId, screenType, ctx.screenEntity, action, target, paramsLabel);
125
131
  }
126
132
  else if (action.kind === "drawer") {
127
133
  validateDrawerTargetAction(featureName, screenId, screenType, actionLabel, action, ctx.screens);
@@ -129,11 +135,8 @@ export function validateListActionTargets(ctx, actionLabel, action) {
129
135
  else if (action.kind === "clientAction") {
130
136
  validateClientActionName(featureName, screenId, screenType, actionLabel, action);
131
137
  }
132
- else if (!ctx.allWriteHandlerQns.has(action.handler)) {
133
- throw new Error(`[Feature ${featureName}] ${ctx.where} ` +
134
- `${actionLabel} "${action.id}" handler "${action.handler}" ` +
135
- `is not a registered write-handler. Check the QN spelling (expected ` +
136
- `"<feature>:write:<short>") and that the handler is declared via r.writeHandler(...).`);
138
+ else {
139
+ assertWriteHandlerRegistered(featureName, `${ctx.where} ${actionLabel} "${action.id}"`, action.handler, ctx.allWriteHandlerQns);
137
140
  }
138
141
  }
139
142
  function validateRelatedListRowActions(args, rowActions) {
@@ -10,8 +10,6 @@ export declare function validateScreens(feature: FeatureDefinition, featureMap:
10
10
  readonly featureName: string;
11
11
  readonly screen: ScreenDefinition;
12
12
  }>): void;
13
- export declare function findEntityFeature(entityName: string, featureMap: ReadonlyMap<string, FeatureDefinition>): string | undefined;
14
- export declare function buildUnknownFieldMessage(featureName: string, screenId: string, fieldName: string, entityName: string, knownFields: ReadonlySet<string>): string;
15
13
  export declare function collectScreenQns(features: readonly FeatureDefinition[]): Set<string>;
16
14
  export declare function screenShortIdsFrom(allScreenQns: ReadonlySet<string>): Set<string>;
17
15
  export declare function collectScreensByShortId(features: readonly FeatureDefinition[]): Map<string, ReadonlyArray<{