@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
package/src/changes.json CHANGED
@@ -1,4 +1,314 @@
1
1
  [
2
+ {
3
+ "version": "0.388.0",
4
+ "type": "fix",
5
+ "title": "runProdApp passes errorDocs and instanceId to the server",
6
+ "detail": "ApiEntrypointOptions now carries errorDocs and instanceId and createApiEntrypoint forwards them to the server. Before, runProdApp dropped its errorDocs option, and a KUMIKO_INSTANCE_ID set only in a custom envSource never reached the server, which fell back to process.env. The boot-abort cleanup of runProdApp also moved into a helper that swallows synchronously throwing cleanups."
7
+ },
8
+ {
9
+ "version": "0.388.0",
10
+ "type": "fix",
11
+ "title": "Physical-table collisions use the DDL table name",
12
+ "detail": "createRegistry compared entity tables under a feature-prefixed name that no DDL path creates, so an entity whose explicit table equals another feature's derived table name, or two entities with the same explicit table in different features, registered fine and then emitted the same CREATE TABLE twice. The check now uses the unprefixed physical name, the same as the store-table check and the migrate runner.",
13
+ "migration": "Apps where two entities share a physical table name fail at boot with \"has physical table ... which collides with entity ...\". Give one of them a different `table`."
14
+ },
15
+ {
16
+ "version": "0.388.0",
17
+ "type": "fix",
18
+ "title": "Worker entrypoint takes instanceId",
19
+ "detail": "WorkerEntrypointOptions now carries instanceId (moved to BaseEntrypointOptions, so the API and all-in-one options keep it) and createWorkerEntrypoint forwards it to the server. runWorkerApp reads KUMIKO_INSTANCE_ID from its envSource like runProdApp, so with KUMIKO_INSTANCE_ID set, per-instance consumers in the worker and its app.started event use the same id. Before, the worker server only saw process.env."
20
+ },
21
+ {
22
+ "version": "0.388.0",
23
+ "type": "improvement",
24
+ "title": "PII, retention and cascade-delete rules as tables, English dev messages",
25
+ "detail": "validatePiiAndRetention runs one checker per rule in a fixed order, and the cascade-delete hook dispatches each relation through a table of onDelete strategies. Error and warning texts, their order, transaction boundaries and event order are unchanged. buildAppSchema forwards field properties through an ordered table, and kumiko-upgrade and the pii-personal-migration codemod run one function per step; their output is byte-identical. The dev warnings of normalizeListColumn and buildAppSchema are now English."
26
+ },
27
+ {
28
+ "version": "0.388.0",
29
+ "type": "improvement",
30
+ "title": "Named steps for list reads, field columns, table metadata and table diffs",
31
+ "detail": "The list read verb runs search, sort, cursor boundary, filters, stream-version projection, row rehydration and the next cursor as named steps in their own files next to event-store-executor-read.ts, and detail() shares the ownership and row rehydration steps. Field columns come from one emitter per field type in a typed table, so a new field type fails the typecheck until it has an emitter, and an unknown type from untyped input still throws \"unhandled field type\". deriveEntityTableMeta collects columns, foreign-key, blind-index and declared indexes in separate steps with the same overloads and error texts. diffOneTable compares columns and indexes through one comparator per kind and keeps the order of added, dropped and changed entries."
32
+ },
33
+ {
34
+ "version": "0.388.0",
35
+ "type": "improvement",
36
+ "title": "Per-kind tables for feature-AST id matching, editability and rendering",
37
+ "detail": "callMatchesId() looks up one matcher per PatternId kind in a typed table in feature-ast/pattern-id-matchers.ts next to patch.ts. getEditability() reads a table keyed by pattern kind, and renderPattern() dispatches through one renderer per pattern kind. All three tables are typed over their kind union, so a new kind does not compile until it has an entry. The feature-ast barrel exports the same names as before."
38
+ },
39
+ {
40
+ "version": "0.388.0",
41
+ "type": "fix",
42
+ "title": "Boot enforces the projection allowlist",
43
+ "detail": "The projection allowlist validator existed but was never called from validateBoot. It now runs as the last cross-feature check. It also keys aggregate tables by the physical table name (entity.table or the derived read_* name) instead of the entity name, so unsafeProjection steps on the table of an entity without an explicit table, and duplicate tables between a derived and an explicit name, are detected.",
44
+ "migration": "Apps with such violations fail at boot. Add the missing r.requires.projection or r.requires.step declaration, or use r.step.aggregate.* for aggregate tables."
45
+ },
46
+ {
47
+ "version": "0.388.0",
48
+ "type": "improvement",
49
+ "title": "Recipe for Input kind \"code\" in a confirmation step",
50
+ "detail": "samples/recipes/auth-mfa has a ConfirmationCodeStep for e-mail or invite codes that uses Input kind \"code\" with length, autoFocus and onComplete, clears and refocuses after a rejected code, and has a DOM test. The agent example for auth-mfa includes it."
51
+ },
52
+ {
53
+ "version": "0.388.0",
54
+ "type": "improvement",
55
+ "title": "Groups and sums on the entity list query",
56
+ "detail": "Pass aggregate with an optional groupBy field and up to 20 measures (sum, avg, count, min, max per field) to a list query. The answer is groups with key, count, one value per measure in request order and, for a reference groupBy, the label of the referenced row. Screen filters, search, ownership and field read access apply exactly as for rows; a field the caller may not read yields null or no groups, and encrypted, personal or secret fields and unsuitable field types are rejected with a validation error. At most 200 groups are returned, with truncated set beyond that. Custom query handlers use executor.aggregate or answerListAggregate."
57
+ },
58
+ {
59
+ "version": "0.388.0",
60
+ "type": "improvement",
61
+ "title": "buildServer split into middleware installers and route mounters",
62
+ "detail": "buildServer() keeps its signature and behavior. Boot checks, pre-app wiring, the event dispatcher, middleware installers and route mounters now live in server-*.ts files next to server.ts. withFileProviderResolver and makeDispatchSystemWrite/makeDispatchWriteAs/makeDispatchSystemQuery are still exported from the api barrel under the same names."
63
+ },
64
+ {
65
+ "version": "0.388.0",
66
+ "type": "improvement",
67
+ "title": "Where-clause emitters, coercer table and named write steps",
68
+ "detail": "buildWhereClause() now dispatches through one emitter per WhereOperator key in a typed table, and coerceDriverRow() looks up one coercer per pgType. Table introspection, value preparation, row coercion, closed-connection handling and aggregate SQL live in their own files next to db/query.ts. The create and update write verbs run preSave, field and ownership checks, the record-lock guard, select-option checks, the version check, the append and the projection as named steps. The db barrel exports the same names as before."
69
+ },
70
+ {
71
+ "version": "0.388.0",
72
+ "type": "improvement",
73
+ "title": "Job runner run path split into named steps",
74
+ "detail": "handleJob() in jobs/job-runner.ts now composes the perTenant fan-out, the unknown-job check, the maxPerTenant defer gate and the sequential lock (job-runner-gates.ts), the run context with its db, service and dispatch facets (job-runner-context.ts), the job.execute span with success and failure recording (job-runner-execution.ts) and the handler deferral. The runner's shared state travels in one scope object (job-runner-scope.ts). The jobs barrel exports the same names as before. New integration tests cover unknown jobs taken from the queue, a perTenant wrapper for an unknown job, a perTenant job without a tenant resolver and ctx.defer with a non-positive delay."
75
+ },
76
+ {
77
+ "version": "0.388.0",
78
+ "type": "improvement",
79
+ "title": "Handler context and write pipeline split into named steps",
80
+ "detail": "buildHandlerContext() in pipeline/dispatch-shared.ts now composes one builder per facet of the flat HandlerContext: the shared scope (handler-context-scope.ts), the tenant-scoped db handles, forTenant and file contexts (handler-context-db.ts), the commit sinks with ctx.query/write/queryAs/writeAs and nested writes in savepoints (handler-context-dispatch.ts), ctx.notify (handler-context-notify.ts), config, configFor, tz and locale (handler-context-config.ts), the event store surfaces and ctx.queryProjection (handler-context-events.ts), the identity-gated auth surfaces and ctx.hasFeature (handler-context-auth.ts) and log, tracer and metrics (handler-context-observability.ts). executeWriteInner() in pipeline/dispatch-write.ts runs the entry checks, the pre-handler gates with the reservation, the transition guard, the handler call and the settle step with reservation confirm, lifecycle and after-commit hooks as separate functions; executeNestedWrite() checks the nested foreign keys, verifies the parent row and writes the children in their own steps. Public exports are unchanged. New tests cover the ctx.snapshotAggregate/ctx.loadAggregateWithSnapshot roundtrip with tenant isolation and the no-database error text of the event store surfaces."
81
+ },
82
+ {
83
+ "version": "0.388.0",
84
+ "type": "improvement",
85
+ "title": "kumiko-schema output in English, schema CLI and screen key collection split per kind",
86
+ "detail": "Visible change: kumiko-schema (and the dev `kumiko schema` command) now prints \"is missing\", \"App convention\", \"No schema changes detected — no new migration file written.\", \"Review, hand-edit if needed, then git add + commit.\", \"is missing — run schema generate <name> first.\", \"No kumiko/migrations/ — the app is still on the legacy drizzle path.\", \"Unknown subcommand\" / \"Allowed\", \"must declare `export const ENTITY_METAS: EntityTableMeta[]`\" and an English help text instead of the German lines. The standalone `kumiko schema apply` of production bundles prints \"Creating framework infrastructure tables (idempotent)…\", \"Applying kumiko migrations (…)…\", \"… is missing — kumiko/migrations/ must be part of the image.\" and \"Unknown command: kumiko … / The standalone bundle only supports 'kumiko schema apply'.\". Scripts that matched the German text need the English one. Exit codes, the order of the output lines and which lines go to stderr are unchanged. runSchemaCli() keeps its signature; each subcommand lives in its own file under schema-cli/. requiredKeysFromScreen() returns the same keys in the same order; a screen with an unknown type (only reachable through a cast) yields just its title key."
87
+ },
88
+ {
89
+ "version": "0.388.0",
90
+ "type": "improvement",
91
+ "title": "validateScreens split into one validator per screen kind",
92
+ "detail": "validateScreens() keeps its signature, its error messages and the order in which it reports the first error. The per-kind validators live in screens-kind-*.ts next to screens.ts, and checks shared by several kinds (empty columns or sections, header actions, write-handler registration) live in screens-kind-shared.ts and screens-actions.ts. A screen whose type is not a known kind now fails boot with an explicit \"unknown type\" error."
93
+ },
94
+ {
95
+ "version": "0.388.0",
96
+ "type": "fix",
97
+ "title": "Sequential job lock released when run setup fails",
98
+ "detail": "handleJob() claimed the sequential lock before prepareJobRun() and the onJobStart hook, but only released it in the finally block that started after them. A throwing onJobStart left the lock until its TTL (305 s) and every further invocation of that job was requeued behind it. Both calls now run inside the try and the lock is released by job name. A failure there is logged and, on the final attempt, reported through onJobFailed. The seed-migration failure message starts with systemWriteAs(...) instead of the duplicated [es-ops/seed-migration] prefix."
99
+ },
100
+ {
101
+ "version": "0.388.0",
102
+ "type": "fix",
103
+ "title": "Reconnect after an outage no longer waits out connect_timeout",
104
+ "detail": "The postgres.js fork left the write buffer of a dead socket in place, so the first query after a database outage queued its startup message behind stale bytes and failed with CONNECT_TIMEOUT after 30 seconds. The fork now resets the buffer when the socket closes, and the next query opens a fresh connection immediately."
105
+ },
106
+ {
107
+ "version": "0.388.0",
108
+ "type": "improvement",
109
+ "title": "rateLimitCost for write handlers",
110
+ "detail": "A write handler can declare `rateLimitCost: (payload) => number`. The result is booked on every declared `rateLimit` bucket instead of the options' static `cost`; values below 1 count as 1 and non-finite values fail the request. The bucket is charged after schema validation, so an invalid payload costs nothing, and a rejected request consumes no tokens. Boot fails when `rateLimitCost` is set without an active `rateLimit`, or on an anonymous or ip-limited handler, because the cost books after the parse and such a handler would lose its pre-parse protection.",
111
+ "migration": "Optional. Bulk handlers should add `rateLimitCost: (p) => p.files.length` so a request with many items is not counted as one."
112
+ },
113
+ {
114
+ "version": "0.388.0",
115
+ "type": "fix",
116
+ "title": "Codemods no longer drop side-effect imports or miss moved index files",
117
+ "detail": "The testing-move codemod repoints side-effect and empty imports instead of deleting them. The feature-layout codemod rewrites directory imports such as \"./components\" when the moved file is components/index.ts."
118
+ },
119
+ {
120
+ "version": "0.388.0",
121
+ "type": "fix",
122
+ "title": "Review fixes for notify, form derivations, i18n keys, feature AST, nav stripping and escape-hatch audit",
123
+ "detail": "A deferred ctx.notify with Temporal values in data no longer fails the write. Form derivations take the literal currency or the source currency before the client value, reject mixed-currency operands, and the boot validator now rejects derive definitions that mix money with number/decimal units or use non-numeric sources. Metric labels of projectionDetail screens are required i18n keys. The feature AST keeps backingTable on spread entity definitions. Pure grouping navs emptied by disabled features are removed. db.global() writes are audited per table."
124
+ },
125
+ {
126
+ "version": "0.388.0",
127
+ "type": "fix",
128
+ "title": "Client-error rate limit survives address rotation",
129
+ "detail": "Without Redis, the per-process limiter now drops the least recently seen IPs when more than 10000 are tracked, so a flood of fresh addresses no longer resets the window of every other IP."
130
+ },
131
+ {
132
+ "version": "0.388.0",
133
+ "type": "fix",
134
+ "title": "Stalled SSE clients are dropped without further broadcasts",
135
+ "detail": "The heartbeat ping is written and tracked like entity frames instead of awaited, so a client that stopped reading is aborted at the next heartbeat after the stall timeout and releases its broker slot."
136
+ },
137
+ {
138
+ "version": "0.388.0",
139
+ "type": "fix",
140
+ "title": "Leftover __Host- cookies are expired with secureCookies:false too",
141
+ "detail": "Clearing the session cookies now always sends the Secure delete for the __Host- auth and csrf variants when the configured names are unprefixed, so an app switched to secureCookies:false behind a TLS proxy no longer keeps a stale __Host- csrf cookie."
142
+ },
143
+ {
144
+ "version": "0.388.0",
145
+ "type": "breaking",
146
+ "title": "runAsDirectCallEntry renamed to runAsAgentToolCallEntry",
147
+ "detail": "The internal request-context helper also marks its scope as an agent tool call, so the name now says so; it stays reserved for dispatchToolCall.",
148
+ "migration": "replace imports of runAsDirectCallEntry from @cosmicdrift/kumiko-framework/internal/request-context with runAsAgentToolCallEntry; the signature is unchanged"
149
+ },
150
+ {
151
+ "version": "0.388.0",
152
+ "type": "fix",
153
+ "title": "Version conflicts skip the extra stream-version lookup",
154
+ "detail": "The CRUD executor reports the current version carried by the event store conflict and queries the stream again only when the conflict carries none."
155
+ },
156
+ {
157
+ "version": "0.388.0",
158
+ "type": "fix",
159
+ "title": "distinctOn documents that system reference rows compete for newest",
160
+ "detail": "The tenant-scoped read set includes system reference rows, so they take part in the newest-row pick; the docs name the tenantId filter that restricts it to own rows, and tests pin both behaviours. Tests also cover the createDbConnection ssl and onNotice options, the version-conflict race over HTTP, and the advisory-lock waiter probe is scoped to its own lock."
161
+ },
162
+ {
163
+ "version": "0.388.0",
164
+ "type": "fix",
165
+ "title": "Empty and exclusion tenant filters no longer widen a read",
166
+ "detail": "A where.tenantId of { in: [] } now returns no rows, and { ne } or { notIn } keep excluding the named tenants instead of falling back to the caller's full scope. scopeTenantDbToTenant keeps the write denial of its source TenantDb."
167
+ },
168
+ {
169
+ "version": "0.388.0",
170
+ "type": "fix",
171
+ "title": "Boot seeding re-creates soft-deleted reference rows",
172
+ "detail": "The seed lookup only considers live system-tenant rows, so a soft-deleted reference row is created anew instead of aborting the boot with a not-found error."
173
+ },
174
+ {
175
+ "version": "0.388.0",
176
+ "type": "fix",
177
+ "title": "Version conflict on update reports the appended-against version",
178
+ "detail": "The racing update path now reports expectedVersion as the stream version it appended against, also with skipOptimisticLock or an expect precondition. A failed advisory unlock after a dropped session no longer hides the migration error."
179
+ },
180
+ {
181
+ "version": "0.388.0",
182
+ "type": "fix",
183
+ "title": "Unknown keys in a handler field references meta fail the boot",
184
+ "detail": "A references meta such as `{ entity: \"channel\", bY: \"key\" }` is reported as invalid instead of silently reading as an id reference, so a misspelled `by` can no longer turn a business-key field into an id field."
185
+ },
186
+ {
187
+ "version": "0.388.0",
188
+ "type": "fix",
189
+ "title": "r.requires rejects malformed arguments",
190
+ "detail": "`r.requires` now throws when called with more than one argument or without a `features` string array, instead of dropping extra arguments or registering the object as a dependency name."
191
+ },
192
+ {
193
+ "version": "0.388.0",
194
+ "type": "fix",
195
+ "title": "r.bootCheck failures keep the check's own message",
196
+ "detail": "The boot error no longer appends a generic sentence, so messages ending in a period are not doubled and the check's text is the whole explanation."
197
+ },
198
+ {
199
+ "version": "0.388.0",
200
+ "type": "fix",
201
+ "title": "Embedded list minimum stays in the JSON schema",
202
+ "detail": "A required or `minItems` embedded list again reports `minItems` in `z.toJSONSchema` output, so agent manifests tell callers that at least one row is needed."
203
+ },
204
+ {
205
+ "version": "0.388.0",
206
+ "type": "fix",
207
+ "title": "Failing trial gate no longer breaks GET /api/schema",
208
+ "detail": "The trial gates of all disabled toggleable features run in parallel; a gate that throws counts as not unlocked, hides the add-on and is logged, so the client still boots."
209
+ },
210
+ {
211
+ "version": "0.388.0",
212
+ "type": "fix",
213
+ "title": "No docsUrl for app error codes without a framework page",
214
+ "detail": "An app subclass that overrides the error code with an uncovered reason no longer links to a framework page that does not exist; the field is absent unless errorDocs covers the reason."
215
+ },
216
+ {
217
+ "version": "0.388.0",
218
+ "type": "fix",
219
+ "title": "Anonymous sessions cannot bind pending uploads on save",
220
+ "detail": "The save pipeline now applies the same anonymous-session exclusion as the pending file ref helpers, so one visitor cannot attach another visitor's pending upload to their record."
221
+ },
222
+ {
223
+ "version": "0.388.0",
224
+ "type": "fix",
225
+ "title": "Failed uploads drop their upload ledger row",
226
+ "detail": "When the fileRef create fails after the bytes were written, POST /files and createFileRefFromBytes now delete the bytes and forget the ledger row instead of leaving it for the next sweep."
227
+ },
228
+ {
229
+ "version": "0.388.0",
230
+ "type": "fix",
231
+ "title": "More HTML roots are kept out of the XML content type",
232
+ "detail": "Uploads without a declared content type whose root element is a common HTML element (form, iframe, input, table parts, media and others) are no longer labelled application/xml."
233
+ },
234
+ {
235
+ "version": "0.388.0",
236
+ "type": "fix",
237
+ "title": "POST /files uses the tenant file context and a transaction-safe upload ledger",
238
+ "detail": "The upload route records and forgets upload bytes through the resolver's ledger like every other file context, and createFileUploadLedger throws when given a connection already inside a transaction."
239
+ },
240
+ {
241
+ "version": "0.388.0",
242
+ "type": "fix",
243
+ "title": "A failed runner start no longer leaves a scheduler refresh timer behind",
244
+ "detail": "The scheduler-seen refresh timer starts only after the rest of start() succeeded, so a failed start does not keep stamping for a runner that is not running."
245
+ },
246
+ {
247
+ "version": "0.388.0",
248
+ "type": "fix",
249
+ "title": "Job runs survive a failing tenant timezone config read",
250
+ "detail": "A throwing tenant timezone config read now logs a warning and falls back to UTC for ctx.tz instead of failing a job that never uses ctx.tz."
251
+ },
252
+ {
253
+ "version": "0.388.0",
254
+ "type": "fix",
255
+ "title": "Escape-hatch process dedup no longer grows without bound",
256
+ "detail": "Entries older than the daily re-audit interval are swept, so row-level cross-tenant read targets in long-lived workers are not kept forever."
257
+ },
258
+ {
259
+ "version": "0.388.0",
260
+ "type": "fix",
261
+ "title": "Handler cross-tenant reads count in the escape-hatch metric",
262
+ "detail": "Handlers now report to the escape-hatch meter like jobs do, so every ctx.crossTenantReads call is counted while the audit event stays deduplicated per report window."
263
+ },
264
+ {
265
+ "version": "0.388.0",
266
+ "type": "fix",
267
+ "title": "beforeTransaction phase and handler share one parsed payload",
268
+ "detail": "The payload parsed for the pre-transaction phases is handed to the handler as is, so schema defaults or transforms that generate values no longer differ between the phase and the handler."
269
+ },
270
+ {
271
+ "version": "0.388.0",
272
+ "type": "fix",
273
+ "title": "Cascade delete and set-null load only child ids",
274
+ "detail": "The cascade handler selects the id column instead of full child rows before calling the executor per child."
275
+ },
276
+ {
277
+ "version": "0.388.0",
278
+ "type": "fix",
279
+ "title": "Codemods stop miscompiling or skipping ambiguous sites",
280
+ "detail": "testing-injection rewrites named re-exports of moved names and reports export-star, testing-move reports namespace destructuring and qualified type references of split names, and object-form no longer rewrites r.requires(identifier) but reports it for a manual check."
281
+ },
282
+ {
283
+ "version": "0.388.0",
284
+ "type": "fix",
285
+ "title": "schema validate names the violated FeatureDefinition key",
286
+ "detail": "A malformed FEATURES entry now reports the first key that is missing or has the wrong type instead of always pointing at the name."
287
+ },
288
+ {
289
+ "version": "0.388.0",
290
+ "type": "fix",
291
+ "title": "Upgrade check names the codemod an improvement runs on apply",
292
+ "detail": "kumiko-upgrade without --apply now prints the codemod of each improvement entry, the same way it prints the migration of breaking entries."
293
+ },
294
+ {
295
+ "version": "0.388.0",
296
+ "type": "fix",
297
+ "title": "Package-merge codemod keeps one entry per target package",
298
+ "detail": "When the merge target is already listed in dependencies, the old devDependencies entry is dropped; when the old entry is in dependencies and the target only in devDependencies, the target moves to dependencies so the runtime entry wins."
299
+ },
300
+ {
301
+ "version": "0.388.0",
302
+ "type": "fix",
303
+ "title": "NotificationTemplateMeta declares senderName and replyTo",
304
+ "detail": "The type now matches the mail template meta schema of the template resolver, which accepts senderName and replyTo."
305
+ },
306
+ {
307
+ "version": "0.388.0",
308
+ "type": "fix",
309
+ "title": "Seed writes run the save lifecycle, so seeded files bind to their records",
310
+ "detail": "runProdApp built the boot seed dispatcher without lifecycle hooks, so seed writes and seed migrations skipped file binding (entity_id stayed null, other users got 404), the pre/post save and delete hooks, the tenant check of file references and the disposal of removed file references. The seed dispatcher now reuses the lifecycle hooks of the API dispatcher, exposed as lifecycleHooks on the entrypoints and on KumikoServer. A hook that throws inside the transaction now fails the seed and the boot."
311
+ },
2
312
  {
3
313
  "version": "0.387.0",
4
314
  "type": "improvement",
@@ -122,16 +122,24 @@ function movedTargetOf(
122
122
  moved: ReadonlyMap<string, string>,
123
123
  ): string | undefined {
124
124
  const { base, extension } = stripScriptExtension(absoluteSpecifier);
125
- const candidates =
125
+ const candidates: string[] =
126
126
  extension === ".js" || extension === ".ts" || extension === ".tsx" || extension === ""
127
127
  ? [absoluteSpecifier, `${base}.ts`, `${base}.tsx`]
128
128
  : [absoluteSpecifier];
129
+ if (extension === "")
130
+ candidates.push(`${absoluteSpecifier}/index.ts`, `${absoluteSpecifier}/index.tsx`);
129
131
  for (const candidate of candidates) {
130
132
  if (moved.has(candidate)) return candidate;
131
133
  }
132
134
  return undefined;
133
135
  }
134
136
 
137
+ // A moved `<dir>/index.ts` is imported as `<dir>`, so the file name alone never appears in the importer.
138
+ function importableNameOf(file: string): string {
139
+ const { base } = stripScriptExtension(baseName(file));
140
+ return base === "index" ? baseName(dirname(file)) : base;
141
+ }
142
+
135
143
  function relativeSpecifierFrom(fromDir: string, target: string): string {
136
144
  const path = relative(fromDir, target).split("\\").join("/");
137
145
  return path.startsWith(".") ? path : `./${path}`;
@@ -258,7 +266,7 @@ function buildWrites(
258
266
  importRewrites: CodemodSite[],
259
267
  ): Map<string, string> {
260
268
  const writes = new Map<string, string>();
261
- const basenames = [...moved.keys()].map((file) => stripScriptExtension(baseName(file)).base);
269
+ const basenames = [...moved.keys()].map(importableNameOf);
262
270
  for (const [file, original] of files) {
263
271
  const content = stripped.get(file) ?? original;
264
272
  const newFile = moved.get(file) ?? file;
@@ -139,8 +139,27 @@ function isAssumedRegistrar(receiver: Node, assumeRegistrarNames: ReadonlySet<st
139
139
  );
140
140
  }
141
141
 
142
+ function isFeatureNameLiteral(arg: Node): boolean {
143
+ return (
144
+ Node.isStringLiteral(arg) ||
145
+ Node.isNoSubstitutionTemplateLiteral(arg) ||
146
+ Node.isTemplateExpression(arg) ||
147
+ Node.isSpreadElement(arg)
148
+ );
149
+ }
150
+
151
+ // An identifier or call argument could be a features object (already the object form) or a name,
152
+ // so it is neither rewritten nor skipped silently.
153
+ function isAmbiguousRequires(method: RegistrarMethod, args: readonly Node[]): boolean {
154
+ return (
155
+ method === "requires" &&
156
+ !args.some(isFeatureNameLiteral) &&
157
+ args.some((arg) => !Node.isObjectLiteralExpression(arg))
158
+ );
159
+ }
160
+
142
161
  export function isPositionalShape(method: RegistrarMethod, args: readonly Node[]): boolean {
143
- if (method === "requires") return args.some((arg) => !Node.isObjectLiteralExpression(arg));
162
+ if (method === "requires") return args.some(isFeatureNameLiteral);
144
163
  // The object form takes exactly one argument, so a spread can only be a positional call.
145
164
  if (args.some((arg) => Node.isSpreadElement(arg))) return true;
146
165
  return args.length >= (method === "hook" ? 3 : 2);
@@ -477,13 +496,31 @@ function visitRegistrarCall(
477
496
  if (
478
497
  !Node.isPropertyAccessExpression(callee) ||
479
498
  method === undefined ||
480
- !isRegistrarMethod(method) ||
481
- !isPositionalShape(method, call.getArguments())
499
+ !isRegistrarMethod(method)
482
500
  ) {
483
- // skip: not a positional registrar call, nothing to rewrite
501
+ // skip: not a registrar method call, nothing to rewrite
484
502
  return;
485
503
  }
504
+ const args = call.getArguments();
486
505
  const receiver = callee.getExpression();
506
+ if (isAmbiguousRequires(method, args)) {
507
+ if (
508
+ isFeatureRegistrarReceiver(receiver) ||
509
+ isAssumedRegistrar(receiver, assumeRegistrarNames)
510
+ ) {
511
+ rewriter.recordManual(
512
+ call,
513
+ method,
514
+ "argument is not a string literal, so it may already be the object form — check by hand",
515
+ );
516
+ }
517
+ // skip: an ambiguous argument is never rewritten; registrar receivers were reported above
518
+ return;
519
+ }
520
+ if (!isPositionalShape(method, args)) {
521
+ // skip: not a positional registrar call, nothing to rewrite
522
+ return;
523
+ }
487
524
  if (isFeatureRegistrarReceiver(receiver) || isAssumedRegistrar(receiver, assumeRegistrarNames)) {
488
525
  rewriter.recordPlan(call, method, planRegistrarCall(rewriter, call, method));
489
526
  } else if (isUnresolvedRegistrarLookalike(receiver)) {
@@ -162,6 +162,10 @@ function mergeDependencyBlock(
162
162
  moved: ReadonlyMap<string, string>,
163
163
  ownName: unknown,
164
164
  describe: (description: string) => void,
165
+ hooks: {
166
+ readonly isDuplicatedElsewhere: (target: string) => boolean;
167
+ readonly onAdded: (target: string) => void;
168
+ },
165
169
  ): Record<string, string> | undefined {
166
170
  const next: Record<string, string> = {};
167
171
  let changed = false;
@@ -173,7 +177,10 @@ function mergeDependencyBlock(
173
177
  }
174
178
  changed = true;
175
179
  describe(`${name} -> ${target}`);
176
- if (target !== ownName && !(target in block)) next[target] = range;
180
+ if (target !== ownName && !(target in block) && !hooks.isDuplicatedElsewhere(target)) {
181
+ next[target] = range;
182
+ hooks.onAdded(target);
183
+ }
177
184
  }
178
185
  if (!changed) return undefined;
179
186
  return isSortedKeys(block)
@@ -181,6 +188,31 @@ function mergeDependencyBlock(
181
188
  : next;
182
189
  }
183
190
 
191
+ function dropAddedFromDevDependencies(
192
+ record: Record<string, unknown>,
193
+ addedToDependencies: ReadonlySet<string>,
194
+ filePath: string,
195
+ result: SiteLists,
196
+ ): boolean {
197
+ const devBlock = record["devDependencies"];
198
+ if (addedToDependencies.size === 0 || !isStringRecord(devBlock)) return false;
199
+ const kept = Object.fromEntries(
200
+ Object.entries(devBlock).filter(([name]) => !addedToDependencies.has(name)),
201
+ );
202
+ if (Object.keys(kept).length === Object.keys(devBlock).length) return false;
203
+ record["devDependencies"] = kept;
204
+ for (const target of addedToDependencies) {
205
+ if (target in devBlock) {
206
+ result.rewrites.push({
207
+ file: filePath,
208
+ line: 1,
209
+ description: `devDependencies: ${target} dropped, dependencies already lists it`,
210
+ });
211
+ }
212
+ }
213
+ return true;
214
+ }
215
+
184
216
  function transformPackageJson(
185
217
  source: string,
186
218
  filePath: string,
@@ -198,20 +230,38 @@ function transformPackageJson(
198
230
  const moved = movedPackages(mappings);
199
231
  const record: Record<string, unknown> = { ...manifest };
200
232
  let changed = false;
233
+ const addedToDependencies = new Set<string>();
201
234
  for (const blockName of DEPENDENCY_BLOCKS) {
202
235
  const block = record[blockName];
203
236
  if (!isStringRecord(block)) continue;
204
- const merged = mergeDependencyBlock(block, moved, record["name"], (description) => {
205
- result.rewrites.push({
206
- file: filePath,
207
- line: 1,
208
- description: `${blockName}: ${description}`,
209
- });
210
- });
237
+ const merged = mergeDependencyBlock(
238
+ block,
239
+ moved,
240
+ record["name"],
241
+ (description) => {
242
+ result.rewrites.push({
243
+ file: filePath,
244
+ line: 1,
245
+ description: `${blockName}: ${description}`,
246
+ });
247
+ },
248
+ {
249
+ // npm/bun resolve a package listed in both dependencies and devDependencies by
250
+ // block order, so the runtime entry wins and the dev copy is dropped below.
251
+ isDuplicatedElsewhere: (target) =>
252
+ blockName === "devDependencies" &&
253
+ isStringRecord(record["dependencies"]) &&
254
+ target in record["dependencies"],
255
+ onAdded: (target) => {
256
+ if (blockName === "dependencies") addedToDependencies.add(target);
257
+ },
258
+ },
259
+ );
211
260
  if (merged === undefined) continue;
212
261
  record[blockName] = merged;
213
262
  changed = true;
214
263
  }
264
+ if (dropAddedFromDevDependencies(record, addedToDependencies, filePath, result)) changed = true;
215
265
  if (!changed) return source;
216
266
  const trailingNewline = source.endsWith("\n") ? "\n" : "";
217
267
  return `${JSON.stringify(record, null, detectIndent(source))}${trailingNewline}`;