@cosmicdrift/kumiko-framework 0.362.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 (259) 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 +14 -5
  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/redis-sse-broker.js +10 -8
  80. package/dist/api/routes.js +48 -19
  81. package/dist/api/server.d.ts +2 -1
  82. package/dist/api/server.js +18 -6
  83. package/dist/api/session-cookies.d.ts +1 -0
  84. package/dist/api/session-cookies.js +20 -5
  85. package/dist/api/sse-route.js +16 -1
  86. package/dist/bun-db/index.d.ts +1 -1
  87. package/dist/bun-db/index.js +1 -1
  88. package/dist/bun-db/query.d.ts +12 -5
  89. package/dist/bun-db/query.js +88 -94
  90. package/dist/crypto/kek-source.js +5 -3
  91. package/dist/crypto/pii-field-encryption.d.ts +4 -1
  92. package/dist/crypto/pii-field-encryption.js +2 -2
  93. package/dist/db/apply-entity-event.js +2 -2
  94. package/dist/db/collect-table-metas.js +11 -7
  95. package/dist/db/cursor.js +2 -1
  96. package/dist/db/dialect.d.ts +1 -0
  97. package/dist/db/dialect.js +28 -13
  98. package/dist/db/eagerload.js +2 -2
  99. package/dist/db/entity-table-from-registry.js +1 -1
  100. package/dist/db/entity-table-meta.js +5 -13
  101. package/dist/db/event-store-executor-context.d.ts +5 -6
  102. package/dist/db/event-store-executor-context.js +29 -27
  103. package/dist/db/event-store-executor-read.js +40 -25
  104. package/dist/db/event-store-executor-write.js +18 -20
  105. package/dist/db/index.d.ts +1 -1
  106. package/dist/db/index.js +1 -1
  107. package/dist/db/lock-guard.d.ts +4 -0
  108. package/dist/db/lock-guard.js +14 -0
  109. package/dist/db/migrate-generator.js +6 -4
  110. package/dist/db/migrate-runner.js +32 -35
  111. package/dist/db/parent-ref-clause.js +2 -2
  112. package/dist/db/queries/backfill-pii.js +11 -9
  113. package/dist/db/queries/es-ops.js +1 -1
  114. package/dist/db/queries/event-consumer.js +17 -17
  115. package/dist/db/queries/event-store-admin.js +6 -6
  116. package/dist/db/queries/event-store-transfer.js +2 -2
  117. package/dist/db/queries/event-store.js +2 -2
  118. package/dist/db/queries/pii-backfill-state.js +2 -2
  119. package/dist/db/queries/raw-sql.js +1 -1
  120. package/dist/db/queries/test-stack.js +1 -1
  121. package/dist/db/reference-data.js +3 -5
  122. package/dist/db/table-builder.js +6 -4
  123. package/dist/db/table-introspection.d.ts +16 -0
  124. package/dist/db/table-introspection.js +76 -0
  125. package/dist/db/tenant-db-runner.d.ts +4 -0
  126. package/dist/db/tenant-db-runner.js +17 -0
  127. package/dist/db/tenant-db.d.ts +5 -2
  128. package/dist/db/tenant-db.js +34 -14
  129. package/dist/derivatives/derivatives-context.js +1 -1
  130. package/dist/derivatives/variant-key.js +2 -1
  131. package/dist/engine/boot-validator/access-declarations.js +2 -2
  132. package/dist/engine/boot-validator/api-ext.js +3 -3
  133. package/dist/engine/boot-validator/boot-check.js +1 -1
  134. package/dist/engine/boot-validator/config-deps.js +13 -13
  135. package/dist/engine/boot-validator/detail-screens.js +1 -1
  136. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  137. package/dist/engine/boot-validator/entity-handler.js +84 -35
  138. package/dist/engine/boot-validator/entity-list-screens.js +3 -3
  139. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  140. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  141. package/dist/engine/boot-validator/nav.js +1 -1
  142. package/dist/engine/boot-validator/pii-retention.js +9 -9
  143. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  144. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  145. package/dist/engine/boot-validator/screens.d.ts +2 -1
  146. package/dist/engine/boot-validator/screens.js +155 -96
  147. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  148. package/dist/engine/changeset-changes.js +3 -3
  149. package/dist/engine/define-handler.d.ts +2 -2
  150. package/dist/engine/define-handler.js +11 -7
  151. package/dist/engine/embedded-derived.js +10 -6
  152. package/dist/engine/extensions/escape-hatch-usage.js +2 -1
  153. package/dist/engine/factories.js +5 -6
  154. package/dist/engine/feature-ast/extractors/events.js +43 -33
  155. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  156. package/dist/engine/feature-ast/patcher.d.ts +3 -3
  157. package/dist/engine/feature-ast/patcher.js +3 -3
  158. package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
  159. package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
  160. package/dist/engine/feature-ast/patterns.d.ts +3 -3
  161. package/dist/engine/feature-ast/render.js +16 -5
  162. package/dist/engine/feature-changelog.d.ts +2 -1
  163. package/dist/engine/feature-changelog.js +8 -6
  164. package/dist/engine/feature-config-events-jobs.d.ts +17 -7
  165. package/dist/engine/feature-config-events-jobs.js +20 -5
  166. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  167. package/dist/engine/feature-entity-handlers.js +12 -3
  168. package/dist/engine/feature-ui-extensions.js +2 -3
  169. package/dist/engine/handler-field-references.d.ts +21 -2
  170. package/dist/engine/handler-field-references.js +55 -17
  171. package/dist/engine/index.d.ts +3 -2
  172. package/dist/engine/index.js +1 -0
  173. package/dist/engine/ownership.js +7 -20
  174. package/dist/engine/pattern-library/mixed-schemas.js +0 -1
  175. package/dist/engine/registry-ingest.d.ts +2 -1
  176. package/dist/engine/registry-ingest.js +85 -2
  177. package/dist/engine/registry-validate.d.ts +1 -0
  178. package/dist/engine/registry-validate.js +38 -8
  179. package/dist/engine/registry.js +2 -1
  180. package/dist/engine/schema-builder.js +3 -4
  181. package/dist/engine/soft-delete-cleanup.js +12 -4
  182. package/dist/engine/steps/call-feature.js +1 -3
  183. package/dist/engine/steps/retry.js +2 -1
  184. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  185. package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
  186. package/dist/engine/template-variables.d.ts +32 -0
  187. package/dist/engine/template-variables.js +80 -0
  188. package/dist/engine/tier-resolver-extension.js +2 -3
  189. package/dist/engine/types/index.d.ts +2 -2
  190. package/dist/engine/validate-projection-allowlist.js +12 -13
  191. package/dist/engine/where-rule-lint.js +8 -7
  192. package/dist/env/_zod-introspect.d.ts +1 -0
  193. package/dist/errors/classes.js +1 -0
  194. package/dist/errors/kumiko-error.d.ts +2 -0
  195. package/dist/errors/kumiko-error.js +2 -0
  196. package/dist/errors/reasons.d.ts +1 -0
  197. package/dist/errors/reasons.js +2 -0
  198. package/dist/errors/write-error-info.js +2 -0
  199. package/dist/event-store/tenant-db-append.js +2 -2
  200. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  201. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  202. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  203. package/dist/files/bind-pending-file-refs.js +3 -3
  204. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  205. package/dist/files/bind-referenced-files.write.js +22 -13
  206. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  207. package/dist/files/create-file-ref-from-bytes.js +67 -4
  208. package/dist/files/create-file-ref.d.ts +22 -0
  209. package/dist/files/create-file-ref.js +53 -25
  210. package/dist/files/file-handle.d.ts +1 -1
  211. package/dist/files/file-handle.js +8 -0
  212. package/dist/files/file-routes.js +2 -2
  213. package/dist/files/index.d.ts +2 -2
  214. package/dist/files/index.js +1 -1
  215. package/dist/files/load-save-file-refs.d.ts +10 -0
  216. package/dist/files/load-save-file-refs.js +47 -0
  217. package/dist/files/local-provider.js +2 -1
  218. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  219. package/dist/files/purge-expired-file-ref-bytes.js +45 -19
  220. package/dist/i18n/index.d.ts +1 -1
  221. package/dist/i18n/index.js +1 -1
  222. package/dist/i18n/mail-registry.d.ts +2 -0
  223. package/dist/i18n/mail-registry.js +7 -0
  224. package/dist/jobs/index.d.ts +1 -1
  225. package/dist/jobs/job-runner.d.ts +4 -1
  226. package/dist/jobs/job-runner.js +13 -9
  227. package/dist/observability/escape-hatch-report.d.ts +1 -1
  228. package/dist/observability/escape-hatch-report.js +17 -5
  229. package/dist/pipeline/cascade-handler.js +5 -2
  230. package/dist/pipeline/dispatch-batch.js +79 -31
  231. package/dist/pipeline/dispatch-query.js +2 -3
  232. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  233. package/dist/pipeline/dispatch-shared.js +142 -34
  234. package/dist/pipeline/dispatch-write.js +34 -22
  235. package/dist/pipeline/event-dispatcher-delivery.js +2 -3
  236. package/dist/pipeline/index.d.ts +2 -0
  237. package/dist/pipeline/index.js +2 -0
  238. package/dist/pipeline/member-read-only-transaction.js +1 -1
  239. package/dist/pipeline/notify-scope.d.ts +13 -0
  240. package/dist/pipeline/notify-scope.js +32 -0
  241. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  242. package/dist/pipeline/pre-transaction-reservations.js +28 -9
  243. package/dist/pipeline/system-hooks.js +2 -1
  244. package/dist/pipeline/system-identity-switch.js +2 -2
  245. package/dist/rate-limit/bucket.d.ts +3 -3
  246. package/dist/rate-limit/bucket.js +14 -4
  247. package/dist/redis/cache-sync-bus.js +9 -3
  248. package/dist/search/meilisearch-adapter.js +39 -13
  249. package/dist/secrets/stored-envelope.js +7 -7
  250. package/dist/stack/push-entity-projection-tables.js +2 -3
  251. package/dist/stack/table-helpers.js +10 -26
  252. package/dist/stack/test-stack.js +2 -5
  253. package/dist/testing/multipart-helper.d.ts +0 -6
  254. package/dist/testing/multipart-helper.js +5 -5
  255. package/dist/testing/without-ambient-temporal.js +4 -4
  256. package/dist/utils/is-plain-object.d.ts +1 -2
  257. package/dist/utils/is-plain-object.js +1 -4
  258. package/package.json +7 -5
  259. package/src/changes.json +251 -8
@@ -1,6 +1,6 @@
1
1
  import { qualifyEntityName } from "../qualified-name.js";
2
2
  import { isFieldsEditSection, normalizeEditField, sectionFieldSpecs } from "../screen-helpers.js";
3
- import { collectScreensByShortId, readsNavigateParamsAsFormPrefill, rowFieldExtractorKeys, } from "./screens.js";
3
+ import { collectScreensByShortId, readsNavigateParamsAsFormPrefill, rowFieldExtractorKeys, validateDrawerPrefillKeys, } from "./screens.js";
4
4
  function isNavigateKind(action) {
5
5
  return action.kind === undefined || action.kind === "navigate";
6
6
  }
@@ -59,13 +59,14 @@ function navigateActionsOf(screen) {
59
59
  return [];
60
60
  }
61
61
  }
62
- function toNavigateParamsSource(action, params, sourceScreenEntity) {
62
+ function toNavigateParamsSource(action, params, sourceScreenEntity, sourceLabel) {
63
63
  return {
64
64
  ...(action.screen !== undefined && { screen: action.screen }),
65
65
  ...(action.entity !== undefined && { entity: action.entity }),
66
66
  ...(action.entityId !== undefined && { entityId: action.entityId }),
67
67
  params,
68
68
  sourceScreenEntity,
69
+ sourceLabel,
69
70
  };
70
71
  }
71
72
  // Every declarative navigate that writes params into the target's URL —
@@ -73,17 +74,19 @@ function toNavigateParamsSource(action, params, sourceScreenEntity) {
73
74
  // plus projectionDetail metrics (runMetricNavigate), which reuse that shape —
74
75
  // plus a relatedList toolbarAction with no `params` of its own, which still
75
76
  // writes its implicit parent-id param at runtime (see `implicitParams`).
76
- function navigateParamsSources(screen) {
77
+ function navigateParamsSources(screen, sourceLabel) {
77
78
  return navigateActionsOf(screen).flatMap(({ action, sourceScreenEntity, implicitParams }) => {
78
79
  if (!isNavigateKind(action))
79
80
  return [];
80
81
  const params = action.params ?? implicitParams;
81
- return params === undefined ? [] : [toNavigateParamsSource(action, params, sourceScreenEntity)];
82
+ return params === undefined
83
+ ? []
84
+ : [toNavigateParamsSource(action, params, sourceScreenEntity, sourceLabel)];
82
85
  });
83
86
  }
84
- // Per target screen QN, the union of field names any navigate `params` may
85
- // prefill from the URL. A form screen absent from the map accepts no URL prefill.
86
- export function collectUrlPrefillFieldsByScreenQn(features) {
87
+ // Per target screen QN, every navigate `params` source that writes into the
88
+ // target's URL prefill. A form screen absent from the map accepts no URL prefill.
89
+ function collectUrlPrefillSourcesByScreenQn(features) {
87
90
  const screensByShortId = collectScreensByShortId(features);
88
91
  const detailForByEntity = new Map();
89
92
  for (const feature of features) {
@@ -95,8 +98,9 @@ export function collectUrlPrefillFieldsByScreenQn(features) {
95
98
  }
96
99
  const result = new Map();
97
100
  for (const feature of features) {
98
- for (const screen of Object.values(feature.screens)) {
99
- for (const source of navigateParamsSources(screen)) {
101
+ for (const [screenId, screen] of Object.entries(feature.screens)) {
102
+ const sourceLabel = `[Feature ${feature.name}] Screen "${screenId}" (${screen.type}) navigate action`;
103
+ for (const source of navigateParamsSources(screen, sourceLabel)) {
100
104
  const target = resolveTarget(source, screensByShortId, detailForByEntity);
101
105
  if (target === undefined)
102
106
  continue;
@@ -105,15 +109,21 @@ export function collectUrlPrefillFieldsByScreenQn(features) {
105
109
  continue;
106
110
  }
107
111
  const qn = qualifyEntityName(target.featureName, "screen", target.shortId);
108
- const fields = result.get(qn) ?? new Set();
109
- for (const key of rowFieldExtractorKeys(source.params))
110
- fields.add(key);
111
- result.set(qn, fields);
112
+ result.set(qn, [...(result.get(qn) ?? []), source]);
112
113
  }
113
114
  }
114
115
  }
115
116
  return result;
116
117
  }
118
+ // Per target screen QN, the union of field names any navigate `params` may
119
+ // prefill from the URL.
120
+ export function collectUrlPrefillFieldsByScreenQn(features) {
121
+ const result = new Map();
122
+ for (const [qn, sources] of collectUrlPrefillSourcesByScreenQn(features)) {
123
+ result.set(qn, new Set(sources.flatMap((source) => rowFieldExtractorKeys(source.params))));
124
+ }
125
+ return result;
126
+ }
117
127
  // Runtime resolution: a bare screen id matches the first feature registering
118
128
  // it (create-app's router); an entity target opens its detailFor screen with an id.
119
129
  function resolveTarget(source, screensByShortId, detailForByEntity) {
@@ -124,37 +134,46 @@ function resolveTarget(source, screensByShortId, detailForByEntity) {
124
134
  const match = screensByShortId.get(source.screen)?.[0];
125
135
  return match === undefined ? undefined : { ...match, shortId: source.screen };
126
136
  }
137
+ function assertHiddenRequiredFieldsSeeded(featureName, screenId, screen, seeded) {
138
+ for (const section of screen.layout.sections) {
139
+ if (!isFieldsEditSection(section))
140
+ continue;
141
+ for (const rawSpec of sectionFieldSpecs(section)) {
142
+ const spec = normalizeEditField(rawSpec);
143
+ if (spec.visible !== false)
144
+ continue;
145
+ const fieldDef = screen.fields[spec.field];
146
+ if (fieldDef === undefined || !("required" in fieldDef) || fieldDef.required !== true) {
147
+ continue;
148
+ }
149
+ if (seeded.has(spec.field))
150
+ continue;
151
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screen.type}) sets submitPrefilled but ` +
152
+ `hides required field "${spec.field}" (visible: false) and no navigate action passes it in ` +
153
+ `params — the field could never get a value and the form could never submit. Add ` +
154
+ `"${spec.field}" to the params of a navigate action targeting this screen, make the field ` +
155
+ `visible, or drop required.`);
156
+ }
157
+ }
158
+ }
127
159
  // submitPrefilled lets an unchanged form submit, so a required field the layout
128
- // hides must be seeded by a navigate param — nothing else can ever fill it.
160
+ // hides must be seeded by a navigate param — nothing else can ever fill it. The
161
+ // param keys of every source (incl. metric/subtitle navigates and the implicit
162
+ // relatedList parent id) must also be fields the renderer would actually prefill.
129
163
  export function validateSubmitPrefilledHiddenFields(features) {
130
- const prefillByScreenQn = collectUrlPrefillFieldsByScreenQn(features);
164
+ const sourcesByScreenQn = collectUrlPrefillSourcesByScreenQn(features);
131
165
  for (const feature of features) {
132
166
  for (const [screenId, screen] of Object.entries(feature.screens)) {
133
167
  if (screen.type !== "actionForm" && screen.type !== "secretMint")
134
168
  continue;
135
169
  if (screen.submitPrefilled !== true)
136
170
  continue;
137
- const seeded = prefillByScreenQn.get(qualifyEntityName(feature.name, "screen", screenId));
138
- for (const section of screen.layout.sections) {
139
- if (!isFieldsEditSection(section))
140
- continue;
141
- for (const rawSpec of sectionFieldSpecs(section)) {
142
- const spec = normalizeEditField(rawSpec);
143
- if (spec.visible !== false)
144
- continue;
145
- const fieldDef = screen.fields[spec.field];
146
- if (fieldDef === undefined || !("required" in fieldDef) || fieldDef.required !== true) {
147
- continue;
148
- }
149
- if (seeded?.has(spec.field) === true)
150
- continue;
151
- throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (${screen.type}) sets submitPrefilled but ` +
152
- `hides required field "${spec.field}" (visible: false) and no navigate action passes it in ` +
153
- `params — the field could never get a value and the form could never submit. Add ` +
154
- `"${spec.field}" to the params of a navigate action targeting this screen, make the field ` +
155
- `visible, or drop required.`);
156
- }
171
+ const sources = sourcesByScreenQn.get(qualifyEntityName(feature.name, "screen", screenId));
172
+ for (const source of sources ?? []) {
173
+ validateDrawerPrefillKeys(source.sourceLabel, "navigate-target", screenId, screen, source.params);
157
174
  }
175
+ const seeded = new Set((sources ?? []).flatMap((source) => rowFieldExtractorKeys(source.params)));
176
+ assertHiddenRequiredFieldsSeeded(feature.name, screenId, screen, seeded);
158
177
  }
159
178
  }
160
179
  }
@@ -1,6 +1,6 @@
1
- import { validateChangelog } from "./feature-changelog.js";
1
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
2
+ import { CHANGELOG_TYPES, validateChangelog } from "./feature-changelog.js";
2
3
  const BLOCK_RE = /<!--\s*kumiko-changes\s*\n([\s\S]*?)\n\s*-->/g;
3
- const TYPE_VALUES = new Set(["breaking", "improvement", "fix"]);
4
4
  const KEY_RE = /^(feature|type|title|detail|migration|codemod|manualAfterCodemod):(?:\s(.*))?$/;
5
5
  function requiredField(fields, field, source) {
6
6
  const value = fields.get(field)?.trim();
@@ -95,7 +95,7 @@ function parseManualAfterCodemod(fields, source) {
95
95
  function buildChange(fields, prose, source) {
96
96
  const feature = requiredField(fields, "feature", source);
97
97
  const rawType = requiredField(fields, "type", source);
98
- if (!TYPE_VALUES.has(rawType)) {
98
+ if (!isOneOf(CHANGELOG_TYPES, rawType)) {
99
99
  throw new Error(`${source}: kumiko-changes type must be breaking, improvement, or fix`);
100
100
  }
101
101
  const { title, detail } = resolveTitleAndDetail(fields, prose, source);
@@ -4,9 +4,9 @@ import type { ContainsSecret } from "../secrets/types.js";
4
4
  import type { QueryHandlerDefinition, WriteHandlerDefinition, WriteHandlerInput } from "./types/define-handler.js";
5
5
  import type { KumikoEventTypeMap } from "./types/index.js";
6
6
  export type { QueryHandlerDefinition, StreamHandlerDefinition, WriteHandlerDefinition, WriteHandlerInput, } from "./types/define-handler.js";
7
- export declare function defineWriteHandler<const TName extends string, TSchema extends ZodType, TData = unknown, TMap extends object = KumikoEventTypeMap>(def: WriteHandlerInput<TName, TSchema, TData, TMap>, ..._noSecretInResponse: true extends ContainsSecret<TData> ? [
7
+ export declare function defineWriteHandler<const TName extends string, TSchema extends ZodType, TData = unknown, TMap extends object = KumikoEventTypeMap, TPrepared = undefined>(def: WriteHandlerInput<TName, TSchema, TData, TMap, TPrepared>, ..._noSecretInResponse: true extends ContainsSecret<TData> ? [
8
8
  secretLeak: "A handler response must not contain a Secret<> — call .reveal() and return the plaintext, or drop the field."
9
- ] : []): WriteHandlerDefinition<TName, TSchema, TData, TMap>;
9
+ ] : []): WriteHandlerDefinition<TName, TSchema, TData, TMap, TPrepared>;
10
10
  export declare function defineQueryHandler<const TName extends string, TSchema extends ZodType, TResult = unknown, TMap extends object = KumikoEventTypeMap>(def: QueryHandlerDefinition<TName, TSchema, TResult, TMap>, ..._noSecretInResponse: true extends ContainsSecret<TResult> ? [
11
11
  secretLeak: "A handler response must not contain a Secret<> — call .reveal() and return the plaintext, or drop the field."
12
12
  ] : []): QueryHandlerDefinition<TName, TSchema, TResult, TMap>;
@@ -1,5 +1,12 @@
1
1
  import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
2
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
2
3
  import { runPipeline } from "./run-pipeline.js";
4
+ function pickPreTransactionPhases(def) {
5
+ return {
6
+ ...(def.reserveBeforeTransaction && { reserveBeforeTransaction: def.reserveBeforeTransaction }),
7
+ ...(def.beforeTransaction && { beforeTransaction: def.beforeTransaction }),
8
+ };
9
+ }
3
10
  export function defineWriteHandler(def,
4
11
  // R6: a phantom rest-param. When the inferred response `TData` carries a
5
12
  // Secret<> anywhere, ContainsSecret<TData> is `true` and this resolves to a
@@ -50,8 +57,9 @@ export function defineWriteHandler(def,
50
57
  ...(def.rateLimit && { rateLimit: def.rateLimit }),
51
58
  ...(def.additionalRateLimits && { additionalRateLimits: def.additionalRateLimits }),
52
59
  ...(def.escapeHatch && { escapeHatch: def.escapeHatch }),
53
- ...(def.reserveBeforeTransaction && { reserveBeforeTransaction: def.reserveBeforeTransaction }),
60
+ ...pickPreTransactionPhases(def),
54
61
  ...(def.tenantlessAnonymous && { tenantlessAnonymous: true }),
62
+ ...(def.transaction && { transaction: def.transaction }),
55
63
  };
56
64
  if ("perform" in def && def.perform !== undefined) {
57
65
  const performDef = def.perform;
@@ -88,14 +96,10 @@ export function defineQueryHandler(def,
88
96
  // brand through into the stored registry entry.
89
97
  export const PAGED_QUERY_HANDLER_BRAND = "__kumikoPagedQueryHandler";
90
98
  export function isPagedQueryHandler(def) {
91
- // @cast-boundary brand-probe — reading an internal marker key off an
92
- // otherwise-typed handler definition; the property may legitimately be
93
- // absent, which is exactly the case this function distinguishes.
94
- return def[PAGED_QUERY_HANDLER_BRAND] === true;
99
+ return propertyOf(def, PAGED_QUERY_HANDLER_BRAND) === true;
95
100
  }
96
101
  export function isEntityConventionQueryHandler(def) {
97
- // @cast-boundary brand-probe — same shape as isPagedQueryHandler above.
98
- return def[ENTITY_CONVENTION_QUERY_BRAND] === true;
102
+ return propertyOf(def, ENTITY_CONVENTION_QUERY_BRAND) === true;
99
103
  }
100
104
  // A projectionList screen's query must resolve to { rows, nextCursor, total? }
101
105
  // (CursorResult<T>) — the renderer used to read rowsQuery.data?.rows and
@@ -1,3 +1,4 @@
1
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
1
2
  /** Computes a derived cell from its source values. Missing/non-numeric
2
3
  * sources: "multiply" with any missing source → undefined; "sum"/"subtract"
3
4
  * treat missing as 0 only when at least one source is present — if every
@@ -6,9 +7,13 @@
6
7
  * to the target sub-field's declared precision afterward. */
7
8
  export function computeDerivedCellValue(op, values) {
8
9
  if (op === "multiply") {
9
- if (values.some((value) => value === undefined))
10
- return undefined;
11
- return values.reduce((product, value) => product * value, 1);
10
+ let product = 1;
11
+ for (const value of values) {
12
+ if (value === undefined)
13
+ return undefined;
14
+ product *= value;
15
+ }
16
+ return product;
12
17
  }
13
18
  if (values.every((value) => value === undefined))
14
19
  return undefined;
@@ -58,10 +63,9 @@ function roundHalfAwayFromZero(value, decimals) {
58
63
  * it's written back, so a fractional product lands on a value the target
59
64
  * type can actually represent. */
60
65
  export function withDerivedCells(row, derived, schema) {
61
- if (typeof row !== "object" || row === null || Array.isArray(row))
66
+ if (!isPlainObject(row))
62
67
  return row;
63
- const source = row;
64
- const copy = { ...source };
68
+ const copy = { ...row };
65
69
  for (const [cellName, def] of Object.entries(derived)) {
66
70
  const sourceValues = def.from.map((sourceField) => {
67
71
  const value = copy[sourceField];
@@ -3,9 +3,10 @@
3
3
  // validation (feature-ui-extensions.ts's useExtension) already rejects an
4
4
  // empty reason, but options stays `Record<string, unknown>`, so orchestrators
5
5
  // re-narrow at read time instead of trusting the shape.
6
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
6
7
  export function extensionUsageEscapeHatchReason(usage) {
7
8
  const escapeHatch = usage.options?.["escapeHatch"];
8
- if (typeof escapeHatch !== "object" || escapeHatch === null)
9
+ if (!isPlainObject(escapeHatch))
9
10
  return undefined;
10
11
  const { reason, grants } = escapeHatch;
11
12
  if (Array.isArray(grants) && !grants.includes("unsafeRaw"))
@@ -1,3 +1,4 @@
1
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
1
2
  // `personal: { of: "id" }` means the row itself is the subject, not a
2
3
  // referenced user — resolves to `recordOwned` instead of `userOwned`.
3
4
  const RECORD_OWNER_FIELD = "id";
@@ -75,15 +76,13 @@ function isNonEmptyString(value) {
75
76
  // Fail-closed stance gate (kumiko-framework#2921) for the two text-shaped
76
77
  // factories — runs before expandPersonalAnnotations so an untyped JS caller
77
78
  // (no compiler to stop it) can't slip an unannotated text field past the
78
- // type layer either. `overrides` is `unknown` here on purpose: the typed
79
- // signature above already rejects a missing/malformed shape at compile
80
- // time, this re-checks the same shape at the runtime boundary.
79
+ // type layer either. The typed signature above already rejects a
80
+ // missing/malformed shape at compile time; this re-checks the same shape at
81
+ // runtime, hence the nullable parameter.
81
82
  function assertTextFieldStance(factoryName, overrides) {
82
83
  if (overrides === null || typeof overrides !== "object") {
83
84
  throw missingTextFieldStanceError(factoryName);
84
85
  }
85
- // @cast-boundary runtime-guard — re-validated field by field below before
86
- // any value is trusted.
87
86
  const { personal, find, reason } = overrides;
88
87
  if (personal === undefined || personal === null) {
89
88
  throw missingTextFieldStanceError(factoryName);
@@ -106,7 +105,7 @@ function assertTextFieldStance(factoryName, overrides) {
106
105
  throw missingTextFieldStanceError(factoryName);
107
106
  }
108
107
  const validFinds = TEXT_FIELD_STANCE_FIND_VALUES[factoryName];
109
- if (typeof find !== "string" || !validFinds.includes(find)) {
108
+ if (typeof find !== "string" || !isOneOf(validFinds, find)) {
110
109
  throw missingTextFieldStanceError(factoryName);
111
110
  }
112
111
  }
@@ -200,37 +200,45 @@ export function extractNotification(call, sourceFile) {
200
200
  return fail("notification", sourceLocationFromNode(call, sourceFile), "second argument must be an inline definition object");
201
201
  }
202
202
  }
203
- const triggerObj = defObj
204
- .getProperty("trigger")
205
- ?.asKind(SyntaxKind.PropertyAssignment)
206
- ?.getInitializer()
207
- ?.asKind(SyntaxKind.ObjectLiteralExpression);
208
- if (!triggerObj) {
209
- return fail("notification", sourceLocationFromNode(call, sourceFile), "missing or non-object `trigger` property");
210
- }
211
- const onInit = triggerObj
212
- .getProperty("on")
213
- ?.asKind(SyntaxKind.PropertyAssignment)
214
- ?.getInitializer();
215
- const onName = onInit ? readNameOrRef(onInit) : undefined;
216
- if (!onName) {
217
- return fail("notification", sourceLocationFromNode(call, sourceFile), "trigger.on must be a string literal or inline { name } object");
203
+ const triggerProp = defObj.getProperty("trigger");
204
+ if (!triggerProp && (defObj.getProperty("recipient") || defObj.getProperty("data"))) {
205
+ return fail("notification", sourceLocationFromNode(call, sourceFile), "recipient and data require a `trigger`");
218
206
  }
219
- const recipientInit = defObj
220
- .getProperty("recipient")
221
- ?.asKind(SyntaxKind.PropertyAssignment)
222
- ?.getInitializer();
223
- const recipientFn = recipientInit ? findFunctionLiteral(recipientInit) : undefined;
224
- if (!recipientFn) {
225
- return fail("notification", sourceLocationFromNode(call, sourceFile), "recipient must be an inline arrow function or function expression");
226
- }
227
- const dataInit = defObj
228
- .getProperty("data")
229
- ?.asKind(SyntaxKind.PropertyAssignment)
230
- ?.getInitializer();
231
- const dataFn = dataInit ? findFunctionLiteral(dataInit) : undefined;
232
- if (!dataFn) {
233
- return fail("notification", sourceLocationFromNode(call, sourceFile), "data must be an inline arrow function or function expression");
207
+ let onName;
208
+ let recipientFn;
209
+ let dataFn;
210
+ if (triggerProp) {
211
+ const triggerObj = triggerProp
212
+ .asKind(SyntaxKind.PropertyAssignment)
213
+ ?.getInitializer()
214
+ ?.asKind(SyntaxKind.ObjectLiteralExpression);
215
+ if (!triggerObj) {
216
+ return fail("notification", sourceLocationFromNode(call, sourceFile), "non-object `trigger` property");
217
+ }
218
+ const onInit = triggerObj
219
+ .getProperty("on")
220
+ ?.asKind(SyntaxKind.PropertyAssignment)
221
+ ?.getInitializer();
222
+ onName = onInit ? readNameOrRef(onInit) : undefined;
223
+ if (!onName) {
224
+ return fail("notification", sourceLocationFromNode(call, sourceFile), "trigger.on must be a string literal or inline { name } object");
225
+ }
226
+ const recipientInit = defObj
227
+ .getProperty("recipient")
228
+ ?.asKind(SyntaxKind.PropertyAssignment)
229
+ ?.getInitializer();
230
+ recipientFn = recipientInit ? findFunctionLiteral(recipientInit) : undefined;
231
+ if (!recipientFn) {
232
+ return fail("notification", sourceLocationFromNode(call, sourceFile), "recipient must be an inline arrow function or function expression");
233
+ }
234
+ const dataInit = defObj
235
+ .getProperty("data")
236
+ ?.asKind(SyntaxKind.PropertyAssignment)
237
+ ?.getInitializer();
238
+ dataFn = dataInit ? findFunctionLiteral(dataInit) : undefined;
239
+ if (!dataFn) {
240
+ return fail("notification", sourceLocationFromNode(call, sourceFile), "data must be an inline arrow function or function expression");
241
+ }
234
242
  }
235
243
  let templates;
236
244
  const templatesObj = defObj
@@ -257,9 +265,11 @@ export function extractNotification(call, sourceFile) {
257
265
  kind: "notification",
258
266
  source: sourceLocationFromNode(call, sourceFile),
259
267
  notificationName,
260
- trigger: { on: onName },
261
- recipientBody: sourceLocationFromNode(recipientFn, sourceFile),
262
- dataBody: sourceLocationFromNode(dataFn, sourceFile),
268
+ ...(onName !== undefined && { trigger: { on: onName } }),
269
+ ...(recipientFn !== undefined && {
270
+ recipientBody: sourceLocationFromNode(recipientFn, sourceFile),
271
+ }),
272
+ ...(dataFn !== undefined && { dataBody: sourceLocationFromNode(dataFn, sourceFile) }),
263
273
  ...(templates !== undefined && { templates }),
264
274
  });
265
275
  }
@@ -36,6 +36,8 @@ const WRITE_HANDLER_KEY_KINDS = {
36
36
  escapeHatch: "modeled",
37
37
  tenantlessAnonymous: "opaque",
38
38
  reserveBeforeTransaction: "opaque",
39
+ beforeTransaction: "opaque",
40
+ transaction: "opaque",
39
41
  perform: "opaque",
40
42
  };
41
43
  const QUERY_HANDLER_KEY_KINDS = {
@@ -69,11 +69,11 @@ export type AddJobArgs = {
69
69
  };
70
70
  export type AddNotificationArgs = {
71
71
  readonly name: string;
72
- readonly trigger: {
72
+ readonly trigger?: {
73
73
  readonly on: string;
74
74
  };
75
- readonly recipientSource: string;
76
- readonly dataSource: string;
75
+ readonly recipientSource?: string;
76
+ readonly dataSource?: string;
77
77
  readonly templates?: Readonly<Record<string, string>>;
78
78
  };
79
79
  export type AddHttpRouteArgs = {
@@ -205,9 +205,9 @@ export function createFeaturePatcher(sourceFile) {
205
205
  kind: "notification",
206
206
  source: SYNTHETIC_LOC,
207
207
  notificationName: name,
208
- trigger,
209
- recipientBody: rawLoc(recipientSource),
210
- dataBody: rawLoc(dataSource),
208
+ ...(trigger !== undefined && { trigger }),
209
+ ...(recipientSource !== undefined && { recipientBody: rawLoc(recipientSource) }),
210
+ ...(dataSource !== undefined && { dataBody: rawLoc(dataSource) }),
211
211
  ...(templateLocs !== undefined && { templates: templateLocs }),
212
212
  });
213
213
  },
@@ -202,6 +202,7 @@ export declare const PATTERN_SCHEMAS_BY_KIND: {
202
202
  limit: z.ZodNumber;
203
203
  windowSeconds: z.ZodNumber;
204
204
  cost: z.ZodOptional<z.ZodNumber>;
205
+ normalize: z.ZodOptional<z.ZodLiteral<"email">>;
205
206
  }, z.core.$strict>>, z.ZodObject<{
206
207
  __raw: z.ZodString;
207
208
  }, z.core.$strict>]>>;
@@ -366,11 +367,11 @@ export declare const PATTERN_SCHEMAS_BY_KIND: {
366
367
  kind: z.ZodLiteral<"notification">;
367
368
  source: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>;
368
369
  notificationName: z.ZodString;
369
- trigger: z.ZodObject<{
370
+ trigger: z.ZodOptional<z.ZodObject<{
370
371
  on: z.ZodString;
371
- }, z.core.$strict>;
372
- recipientBody: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>;
373
- dataBody: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>;
372
+ }, z.core.$strict>>;
373
+ recipientBody: z.ZodOptional<z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>;
374
+ dataBody: z.ZodOptional<z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>;
374
375
  templates: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>>;
375
376
  }, z.core.$strict>;
376
377
  authClaims: z.ZodObject<{
@@ -724,6 +725,7 @@ export declare const patternSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
724
725
  limit: z.ZodNumber;
725
726
  windowSeconds: z.ZodNumber;
726
727
  cost: z.ZodOptional<z.ZodNumber>;
728
+ normalize: z.ZodOptional<z.ZodLiteral<"email">>;
727
729
  }, z.core.$strict>>, z.ZodObject<{
728
730
  __raw: z.ZodString;
729
731
  }, z.core.$strict>]>>;
@@ -883,11 +885,11 @@ export declare const patternSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
883
885
  kind: z.ZodLiteral<"notification">;
884
886
  source: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>;
885
887
  notificationName: z.ZodString;
886
- trigger: z.ZodObject<{
888
+ trigger: z.ZodOptional<z.ZodObject<{
887
889
  on: z.ZodString;
888
- }, z.core.$strict>;
889
- recipientBody: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>;
890
- dataBody: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>;
890
+ }, z.core.$strict>>;
891
+ recipientBody: z.ZodOptional<z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>;
892
+ dataBody: z.ZodOptional<z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>;
891
893
  templates: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>>;
892
894
  }, z.core.$strict>, z.ZodObject<{
893
895
  kind: z.ZodLiteral<"authClaims">;
@@ -163,6 +163,7 @@ const additionalRateLimitsSchema = z.array(z
163
163
  limit: z.number(),
164
164
  windowSeconds: z.number(),
165
165
  cost: z.number().optional(),
166
+ normalize: z.literal("email").optional(),
166
167
  })
167
168
  .strict());
168
169
  const agentHandlerHintsSchema = z
@@ -562,9 +563,9 @@ const notificationSchema = z
562
563
  kind: z.literal("notification"),
563
564
  source: sourceFieldSchema,
564
565
  notificationName: z.string(),
565
- trigger: z.object({ on: z.string() }).strict(),
566
- recipientBody: sourceBodySchema,
567
- dataBody: sourceBodySchema,
566
+ trigger: z.object({ on: z.string() }).strict().optional(),
567
+ recipientBody: sourceBodySchema.optional(),
568
+ dataBody: sourceBodySchema.optional(),
568
569
  templates: z.record(z.string(), sourceBodySchema).optional(),
569
570
  })
570
571
  .strict();
@@ -190,11 +190,11 @@ export type NotificationPattern = {
190
190
  readonly kind: "notification";
191
191
  readonly source: SourceLocation;
192
192
  readonly notificationName: string;
193
- readonly trigger: {
193
+ readonly trigger?: {
194
194
  readonly on: string;
195
195
  };
196
- readonly recipientBody: SourceLocation;
197
- readonly dataBody: SourceLocation;
196
+ readonly recipientBody?: SourceLocation;
197
+ readonly dataBody?: SourceLocation;
198
198
  readonly templates?: Readonly<Record<string, SourceLocation>>;
199
199
  };
200
200
  export type AuthClaimsPattern = {
@@ -240,18 +240,26 @@ function renderMetric(p) {
240
240
  const merged = { name: p.shortName, ...p.options };
241
241
  return `r.metric(${renderValue(merged)});`;
242
242
  }
243
+ // r.secret / r.secretNamespace throw on an empty writeRoles list; the designer's
244
+ // string-list input emits [] when cleared, which means "rely on handler access alone".
245
+ function dropEmptyWriteRoles(options) {
246
+ if (options.writeRoles?.length !== 0)
247
+ return options;
248
+ const { writeRoles: _emptyWriteRoles, ...rest } = options;
249
+ return rest;
250
+ }
243
251
  function renderSecret(p) {
244
252
  if (isRawRefSentinel(p.options)) {
245
253
  return `r.secret(${JSON.stringify(p.shortName)}, ${p.options.__raw});`;
246
254
  }
247
- const merged = { name: p.shortName, ...p.options };
255
+ const merged = { name: p.shortName, ...dropEmptyWriteRoles(p.options) };
248
256
  return `r.secret(${renderValue(merged)});`;
249
257
  }
250
258
  function renderSecretNamespace(p) {
251
259
  if (isRawRefSentinel(p.options)) {
252
260
  return `r.secretNamespace(${JSON.stringify(p.shortName)}, ${p.options.__raw});`;
253
261
  }
254
- const merged = { name: p.shortName, ...p.options };
262
+ const merged = { name: p.shortName, ...dropEmptyWriteRoles(p.options) };
255
263
  return `r.secretNamespace(${renderValue(merged)});`;
256
264
  }
257
265
  function renderClaimKey(p) {
@@ -497,9 +505,12 @@ function renderJob(p) {
497
505
  function renderNotification(p) {
498
506
  const lines = ["r.notification({"];
499
507
  lines.push(` name: ${JSON.stringify(p.notificationName)},`);
500
- lines.push(` trigger: { on: ${JSON.stringify(p.trigger.on)} },`);
501
- lines.push(` recipient: ${p.recipientBody.raw},`);
502
- lines.push(` data: ${p.dataBody.raw},`);
508
+ if (p.trigger)
509
+ lines.push(` trigger: { on: ${JSON.stringify(p.trigger.on)} },`);
510
+ if (p.recipientBody)
511
+ lines.push(` recipient: ${p.recipientBody.raw},`);
512
+ if (p.dataBody)
513
+ lines.push(` data: ${p.dataBody.raw},`);
503
514
  if (p.templates && Object.keys(p.templates).length > 0) {
504
515
  lines.push(" templates: {");
505
516
  for (const [k, loc] of Object.entries(p.templates)) {
@@ -1,4 +1,5 @@
1
- export type ChangelogType = "breaking" | "improvement" | "fix";
1
+ export declare const CHANGELOG_TYPES: readonly ["breaking", "improvement", "fix"];
2
+ export type ChangelogType = (typeof CHANGELOG_TYPES)[number];
2
3
  export type ChangelogEntry = {
3
4
  readonly version: string;
4
5
  readonly type: ChangelogType;
@@ -5,6 +5,8 @@
5
5
  //
6
6
  // File I/O stays in the CLI (`bin/commands/upgrade.ts`) — this module is
7
7
  // pure parse/validate so engine stays off the node:fs allowlist.
8
+ import { isOneOf, isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
9
+ export const CHANGELOG_TYPES = ["breaking", "improvement", "fix"];
8
10
  /** Parse a changes.json body. Callers own file I/O. */
9
11
  export function parseFeatureChangelog(raw, featureName) {
10
12
  try {
@@ -24,16 +26,16 @@ export function parseFeatureChangelog(raw, featureName) {
24
26
  }
25
27
  }
26
28
  function isChangelogEntry(value) {
27
- if (typeof value !== "object" || value === null)
29
+ if (!isPlainObject(value))
28
30
  return false;
29
- const obj = value;
30
- if (typeof obj["version"] !== "string")
31
+ if (typeof value["version"] !== "string")
31
32
  return false;
32
- if (!["breaking", "improvement", "fix"].includes(obj["type"]))
33
+ if (!isOneOf(CHANGELOG_TYPES, value["type"]))
33
34
  return false;
34
- if (typeof obj["title"] !== "string")
35
+ if (typeof value["title"] !== "string")
35
36
  return false;
36
- if (obj["manualAfterCodemod"] !== undefined && typeof obj["manualAfterCodemod"] !== "boolean") {
37
+ if (value["manualAfterCodemod"] !== undefined &&
38
+ typeof value["manualAfterCodemod"] !== "boolean") {
37
39
  return false;
38
40
  }
39
41
  return true;