@cosmicdrift/kumiko-framework 0.388.0 → 0.389.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 (194) hide show
  1. package/AGENTS.md +3 -0
  2. package/agents/examples/golden-path.md +13 -1
  3. package/agents/examples/lifecycle-hooks.md +55 -2
  4. package/agents/examples/list-table-options.md +466 -0
  5. package/agents/examples/pdf-audit-export.md +4 -2
  6. package/agents/examples/state-machine.md +3 -2
  7. package/agents/examples/tenant-isolation.md +73 -1
  8. package/agents/examples.md +1 -0
  9. package/agents/widgets.md +1 -0
  10. package/dist/api/api-constants.d.ts +1 -0
  11. package/dist/api/api-constants.js +2 -0
  12. package/dist/api/auth-routes-shared.d.ts +2 -0
  13. package/dist/api/auth-routes-shared.js +5 -0
  14. package/dist/api/client-error-route.js +2 -3
  15. package/dist/api/entity-read-access.d.ts +2 -1
  16. package/dist/api/entity-read-access.js +14 -0
  17. package/dist/api/export/export-cells-reference.d.ts +9 -0
  18. package/dist/api/export/export-cells-reference.js +21 -0
  19. package/dist/api/export/export-cells.d.ts +11 -0
  20. package/dist/api/export/export-cells.js +136 -0
  21. package/dist/api/export/export-ciphertext-guard.d.ts +2 -0
  22. package/dist/api/export/export-ciphertext-guard.js +27 -0
  23. package/dist/api/export/export-columns.d.ts +29 -0
  24. package/dist/api/export/export-columns.js +85 -0
  25. package/dist/api/export/export-errors.d.ts +4 -0
  26. package/dist/api/export/export-errors.js +14 -0
  27. package/dist/api/export/export-route.d.ts +4 -0
  28. package/dist/api/export/export-route.js +74 -0
  29. package/dist/api/export/export-rows.d.ts +13 -0
  30. package/dist/api/export/export-rows.js +62 -0
  31. package/dist/api/export/export-screen.d.ts +12 -0
  32. package/dist/api/export/export-screen.js +47 -0
  33. package/dist/api/export/export-xlsx-writer.d.ts +4 -0
  34. package/dist/api/export/export-xlsx-writer.js +21 -0
  35. package/dist/api/extra-route.d.ts +4 -0
  36. package/dist/api/pii-leak-guard.d.ts +3 -0
  37. package/dist/api/pii-leak-guard.js +3 -2
  38. package/dist/api/routes.d.ts +16 -0
  39. package/dist/api/routes.js +2 -2
  40. package/dist/api/server-extra-routes.js +3 -0
  41. package/dist/api/server-routes.d.ts +1 -1
  42. package/dist/api/server-routes.js +8 -2
  43. package/dist/api/server-wiring.js +2 -0
  44. package/dist/api/sse-route.d.ts +2 -1
  45. package/dist/api/sse-route.js +11 -1
  46. package/dist/config/index.d.ts +3 -1
  47. package/dist/crypto/pg-kms-adapter.d.ts +1 -0
  48. package/dist/crypto/pg-kms-adapter.js +12 -18
  49. package/dist/db/aggregate-sql.d.ts +2 -0
  50. package/dist/db/aggregate-sql.js +10 -0
  51. package/dist/db/closed-connection.js +12 -19
  52. package/dist/db/connection.d.ts +4 -5
  53. package/dist/db/connection.js +29 -20
  54. package/dist/db/driver-row-coercion.js +4 -4
  55. package/dist/db/driver-value-preparation.js +6 -4
  56. package/dist/db/event-store-executor-read-aggregate-currencies.d.ts +17 -0
  57. package/dist/db/event-store-executor-read-aggregate-currencies.js +40 -0
  58. package/dist/db/event-store-executor-read-aggregate.js +36 -5
  59. package/dist/db/event-store-executor-read-filter.js +6 -3
  60. package/dist/db/event-store-executor-write-atomic.d.ts +4 -0
  61. package/dist/db/event-store-executor-write-atomic.js +41 -0
  62. package/dist/db/event-store-executor-write-steps.d.ts +0 -3
  63. package/dist/db/event-store-executor-write-steps.js +0 -5
  64. package/dist/db/event-store-executor-write.js +15 -15
  65. package/dist/db/event-store-executor.js +2 -1
  66. package/dist/db/executor-managed-tables.js +1 -1
  67. package/dist/db/index.d.ts +1 -1
  68. package/dist/db/index.js +1 -1
  69. package/dist/db/migrate-runner.js +3 -3
  70. package/dist/db/pg-error.js +26 -21
  71. package/dist/db/queries/backfill-pii.js +3 -6
  72. package/dist/db/queries/database-now.js +2 -1
  73. package/dist/db/queries/shadow-swap.js +2 -3
  74. package/dist/db/query.d.ts +14 -0
  75. package/dist/db/query.js +68 -43
  76. package/dist/db/reference-data.js +14 -2
  77. package/dist/db/statement-timeout.js +2 -1
  78. package/dist/db/tenant-db-upsert.d.ts +5 -0
  79. package/dist/db/tenant-db-upsert.js +91 -0
  80. package/dist/db/tenant-db.js +11 -1
  81. package/dist/engine/boot-validator/bundled-lists-table.d.ts +2 -0
  82. package/dist/engine/boot-validator/bundled-lists-table.js +36 -0
  83. package/dist/engine/boot-validator/entity-list-aggregate-options.d.ts +3 -0
  84. package/dist/engine/boot-validator/entity-list-aggregate-options.js +23 -0
  85. package/dist/engine/boot-validator/list-bulk-actions.d.ts +3 -0
  86. package/dist/engine/boot-validator/list-bulk-actions.js +49 -0
  87. package/dist/engine/boot-validator/list-column-layout.d.ts +8 -0
  88. package/dist/engine/boot-validator/list-column-layout.js +41 -0
  89. package/dist/engine/boot-validator/list-table-aggregate-options.d.ts +24 -0
  90. package/dist/engine/boot-validator/list-table-aggregate-options.js +145 -0
  91. package/dist/engine/boot-validator/list-table-aggregate-query.d.ts +10 -0
  92. package/dist/engine/boot-validator/list-table-aggregate-query.js +31 -0
  93. package/dist/engine/boot-validator/list-table-export-options.d.ts +1 -0
  94. package/dist/engine/boot-validator/list-table-export-options.js +27 -0
  95. package/dist/engine/boot-validator/list-table-options.d.ts +3 -0
  96. package/dist/engine/boot-validator/list-table-options.js +77 -0
  97. package/dist/engine/boot-validator/list-table-reject-unknown-keys.d.ts +1 -0
  98. package/dist/engine/boot-validator/list-table-reject-unknown-keys.js +7 -0
  99. package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
  100. package/dist/engine/boot-validator/projection-list-screens.js +3 -13
  101. package/dist/engine/boot-validator/related-list-table-rules.d.ts +4 -0
  102. package/dist/engine/boot-validator/related-list-table-rules.js +42 -0
  103. package/dist/engine/boot-validator/screens-kind-entity-list.js +7 -0
  104. package/dist/engine/boot-validator/screens-kind-projection-list.js +4 -0
  105. package/dist/engine/boot-validator/screens-lists.js +12 -2
  106. package/dist/engine/build-app-schema.d.ts +2 -1
  107. package/dist/engine/build-app-schema.js +9 -1
  108. package/dist/engine/build-config-feature-schema.js +6 -1
  109. package/dist/engine/define-feature.js +4 -0
  110. package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
  111. package/dist/engine/feature-ast/extractors/index.js +1 -1
  112. package/dist/engine/feature-ast/extractors/round1.d.ts +2 -1
  113. package/dist/engine/feature-ast/extractors/round1.js +6 -0
  114. package/dist/engine/feature-ast/index.d.ts +1 -1
  115. package/dist/engine/feature-ast/parse.js +3 -1
  116. package/dist/engine/feature-ast/patch.d.ts +2 -0
  117. package/dist/engine/feature-ast/patch.js +1 -0
  118. package/dist/engine/feature-ast/pattern-change-schema.d.ts +7 -245
  119. package/dist/engine/feature-ast/pattern-change-schema.js +9 -163
  120. package/dist/engine/feature-ast/pattern-id-matchers.js +1 -0
  121. package/dist/engine/feature-ast/pattern-id-schema.d.ts +251 -0
  122. package/dist/engine/feature-ast/pattern-id-schema.js +166 -0
  123. package/dist/engine/feature-ast/patterns.d.ts +5 -1
  124. package/dist/engine/feature-ast/patterns.js +1 -0
  125. package/dist/engine/feature-ast/render.js +4 -0
  126. package/dist/engine/feature-builder-state.d.ts +1 -0
  127. package/dist/engine/feature-builder-state.js +1 -0
  128. package/dist/engine/feature-manifest.d.ts +1 -0
  129. package/dist/engine/feature-manifest.js +1 -0
  130. package/dist/engine/field-access.d.ts +1 -0
  131. package/dist/engine/field-access.js +12 -0
  132. package/dist/engine/index.d.ts +2 -2
  133. package/dist/engine/index.js +1 -1
  134. package/dist/engine/pattern-library/library.js +2 -1
  135. package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
  136. package/dist/engine/pattern-library/static-schemas.js +9 -0
  137. package/dist/engine/screen-filter-ops.js +2 -0
  138. package/dist/engine/strip-disabled-features-from-app-schema.js +7 -3
  139. package/dist/engine/types/feature.d.ts +1 -0
  140. package/dist/engine/types/index.d.ts +4 -3
  141. package/dist/engine/types/screen.d.ts +1 -1
  142. package/dist/engine/types/screen.js +1 -1
  143. package/dist/entrypoint/index.d.ts +2 -0
  144. package/dist/entrypoint/index.js +3 -0
  145. package/dist/event-store/event-store.d.ts +1 -0
  146. package/dist/event-store/event-store.js +7 -4
  147. package/dist/event-store/provenance-append.js +1 -1
  148. package/dist/files/create-file-ref-from-bytes.js +3 -0
  149. package/dist/files/create-file-ref.d.ts +3 -0
  150. package/dist/files/create-file-ref.js +11 -2
  151. package/dist/files/file-routes.d.ts +2 -0
  152. package/dist/files/file-routes.js +29 -2
  153. package/dist/files/provider-resolver.d.ts +1 -0
  154. package/dist/files/provider-resolver.js +1 -1
  155. package/dist/files/record-write-check.d.ts +6 -0
  156. package/dist/files/record-write-check.js +13 -0
  157. package/dist/files/upload-ledger.d.ts +2 -2
  158. package/dist/files/upload-ledger.js +4 -4
  159. package/dist/i18n/required-surface-keys.js +7 -2
  160. package/dist/internal/testing.js +1 -1
  161. package/dist/jobs/job-runner-scope.js +6 -6
  162. package/dist/jobs/job-runner.js +3 -0
  163. package/dist/observability/escape-hatch-report.js +9 -2
  164. package/dist/pipeline/after-rollback-sink.d.ts +6 -0
  165. package/dist/pipeline/after-rollback-sink.js +32 -0
  166. package/dist/pipeline/dispatch-batch.js +8 -1
  167. package/dist/pipeline/dispatch-shared.js +4 -0
  168. package/dist/pipeline/event-dispatcher-delivery.js +3 -1
  169. package/dist/pipeline/event-dispatcher.d.ts +1 -0
  170. package/dist/pipeline/event-dispatcher.js +6 -9
  171. package/dist/pipeline/handler-context-dispatch.d.ts +2 -0
  172. package/dist/pipeline/handler-context-dispatch.js +13 -1
  173. package/dist/pipeline/handler-context-events.d.ts +2 -4
  174. package/dist/pipeline/handler-context-events.js +6 -4
  175. package/dist/pipeline/system-hooks.d.ts +3 -1
  176. package/dist/pipeline/system-hooks.js +55 -6
  177. package/dist/pipeline/write-result-masking.js +13 -10
  178. package/dist/schema-cli/baseline-command.js +1 -1
  179. package/dist/schema-cli/status-command.js +1 -1
  180. package/dist/search/reindex-entity.js +15 -5
  181. package/dist/ui-types/index.d.ts +2 -2
  182. package/dist/ui-types/index.js +1 -1
  183. package/dist/upgrade-changelog.d.ts +2 -0
  184. package/dist/upgrade-changelog.js +14 -0
  185. package/dist/upgrade-cli.d.ts +3 -12
  186. package/dist/upgrade-cli.js +22 -76
  187. package/dist/upgrade-marker-parse.d.ts +32 -0
  188. package/dist/upgrade-marker-parse.js +73 -0
  189. package/package.json +9 -6
  190. package/src/changes.json +245 -0
  191. package/src/scripts/codemod/codemod-support.ts +1 -1
  192. package/src/scripts/codemod/db-factory.ts +1 -1
  193. package/dist/engine/provider-plugin-context.d.ts +0 -3
  194. package/dist/engine/provider-plugin-context.js +0 -28
@@ -199,8 +199,9 @@ function pushRelatedListSectionKeys(out, section) {
199
199
  }
200
200
  for (const action of section.rowActions ?? [])
201
201
  pushRowActionKeys(out, action);
202
- pushKey(out, section.groupBy?.label);
203
- for (const label of Object.values(section.groupBy?.labels ?? {}))
202
+ const grouping = section.table?.grouping;
203
+ pushKey(out, grouping?.label);
204
+ for (const label of Object.values(grouping?.labels ?? {}))
204
205
  pushKey(out, label);
205
206
  }
206
207
  function pushListEmptyStateKeys(out, emptyState) {
@@ -237,6 +238,8 @@ function pushListChromeKeys(out, list) {
237
238
  pushRowActionKeys(out, action);
238
239
  for (const action of list.toolbarActions ?? [])
239
240
  pushToolbarActionKeys(out, action);
241
+ for (const bulk of list.bulkActions ?? [])
242
+ pushKey(out, bulk.confirm);
240
243
  }
241
244
  function collectEntityListKeys(ctx, list) {
242
245
  for (const col of list.columns) {
@@ -372,6 +375,8 @@ function collectProjectionDetailKeys(ctx, detail) {
372
375
  }
373
376
  if (section.kind === "relatedList") {
374
377
  pushRelatedListSectionKeys(ctx.out, section);
378
+ for (const bulk of section.bulkActions ?? [])
379
+ pushKey(ctx.out, bulk.confirm);
375
380
  continue;
376
381
  }
377
382
  if (isWriteFormEditSection(section)) {
@@ -14,5 +14,5 @@ export function withExecutorManagedWritesForTests(db) {
14
14
  }
15
15
  if (isDbRunner(db))
16
16
  return withExecutorManagedWrites(db);
17
- throw new Error("withExecutorManagedWritesForTests: expected a TenantDb or a postgres-js runner");
17
+ throw new Error("withExecutorManagedWritesForTests: expected a TenantDb or a Bun.SQL runner");
18
18
  }
@@ -13,21 +13,21 @@ export function laneForJob(def) {
13
13
  // lane (jobDef.runIn is static), but walking both is cheap and avoids
14
14
  // a subtle bug if someone ever reassigns a job to a different lane
15
15
  // between deploys while old queue contents are still draining.
16
+ // One getJobs per queue reads the requested lists in a single Lua script, so a job
17
+ // moving from waiting to active between two separate reads can no longer vanish
18
+ // from the count; that gap let a third dispatch pass the cap in CI.
16
19
  // `activeOnly` + `self` serve the defer check, which runs inside the job being
17
20
  // counted: it must not see waiting jobs or itself, and only counts jobs that
18
21
  // became active before it. Without that order, workers that fetch a burst of
19
22
  // jobs together all see each other as active and all defer, again and again.
20
23
  export async function isOverPerTenantLimit(scope, jobName, tenantId, max, countFilter = {}) {
21
24
  const { queues } = scope;
22
- const results = await Promise.all([
23
- queues.api.getActive(),
24
- queues.worker.getActive(),
25
- ...(countFilter.activeOnly ? [] : [queues.api.getWaiting(), queues.worker.getWaiting()]),
26
- ]);
25
+ const types = countFilter.activeOnly ? ["active"] : ["active", "waiting"];
26
+ const results = await Promise.all([queues.api.getJobs(types), queues.worker.getJobs(types)]);
27
27
  let count = 0;
28
28
  for (const list of results) {
29
29
  for (const j of list) {
30
- if (j.name !== jobName)
30
+ if (j === undefined || j.name !== jobName)
31
31
  continue;
32
32
  if (countFilter.self && !isActiveBefore(j, countFilter.self))
33
33
  continue;
@@ -264,6 +264,9 @@ export function createJobRunner(options) {
264
264
  // a hanging process instead of a non-zero exit.
265
265
  await worker?.close();
266
266
  worker = null;
267
+ // A failed start() may never be followed by stop(); the abandoned gate must not keep
268
+ // writing to DB/Redis while the process shuts down.
269
+ await settleAbandonedBootGates(Math.min(bootRedisTimeoutMs, STOP_QUEUE_READY_TIMEOUT_MS));
267
270
  throw err;
268
271
  }
269
272
  finally {
@@ -40,6 +40,12 @@ const READ_WINDOW_KINDS = new Set([
40
40
  // A long-lived worker must leave an audit event inside every retention window
41
41
  // (90 days), or its crons vanish from the log once the first event is pruned.
42
42
  const PROCESS_REAUDIT_INTERVAL_MS = 24 * 60 * 60 * 1000;
43
+ // Declared escape hatches of system crons are audited once per day and process for each
44
+ // (handler, kind, tenant, target): the declaration, the system actor and the reason are
45
+ // identical on every run, so repeats carry no information. The target is part of the
46
+ // key only where it names a table (one run can write several global tables); a row target
47
+ // would add a key and an audit event per touched foreign row.
48
+ const TABLE_TARGET_KINDS = new Set(["global-write"]);
43
49
  // Row-level targets add a key per touched row; without a sweep a long-lived worker keeps them all.
44
50
  function sweepExpiredProcessDedup(dedup, nowMs) {
45
51
  if (nowMs - dedup.lastSweep.atMs >= PROCESS_REAUDIT_INTERVAL_MS) {
@@ -157,12 +163,13 @@ export function createEscapeHatchReporter(opts) {
157
163
  emitEscapeHatchUse(opts.meter, opts.handler, kind);
158
164
  const { processDedup } = opts;
159
165
  if (processDedup?.kinds.has(kind)) {
166
+ const keyTarget = TABLE_TARGET_KINDS.has(kind) ? target : undefined;
160
167
  const processKey = JSON.stringify([
161
168
  opts.handler,
162
169
  kind,
163
170
  opts.tenantId,
164
- target?.id ?? null,
165
- target?.tenantId ?? null,
171
+ keyTarget?.id ?? null,
172
+ keyTarget?.tenantId ?? null,
166
173
  ]);
167
174
  const nowMs = now();
168
175
  const lastAuditedAt = processDedup.lastAuditedAt.get(processKey);
@@ -0,0 +1,6 @@
1
+ import type { createFallbackLogger } from "../logging/utils.ts";
2
+ import type { AfterCommitHook } from "./dispatcher-utils.ts";
3
+ export type AfterRollbackHook = () => Promise<void>;
4
+ export declare function afterRollbackHooksOf(commitSink: AfterCommitHook[]): AfterRollbackHook[];
5
+ export declare function carrySavepointRollbackHooks(innerCommitSink: AfterCommitHook[], outerCommitSink: AfterCommitHook[], savepointRolledBack: boolean): void;
6
+ export declare function flushAfterRollback(commitSink: AfterCommitHook[], logError: Pick<ReturnType<typeof createFallbackLogger>, "error">): Promise<void>;
@@ -0,0 +1,32 @@
1
+ // Side channel keyed by the afterCommit sink array, so executeWrite and friends keep their signatures.
2
+ // Invariant: every registration runs at most once, and only if the work it compensates was discarded.
3
+ const rollbackSinks = new WeakMap();
4
+ export function afterRollbackHooksOf(commitSink) {
5
+ const existing = rollbackSinks.get(commitSink);
6
+ if (existing)
7
+ return existing;
8
+ const created = [];
9
+ rollbackSinks.set(commitSink, created);
10
+ return created;
11
+ }
12
+ // A rolled-back savepoint is not compensated immediately: the compensation could wait on locks of
13
+ // the still-open outer transaction. It runs once after that transaction ends, via the commit sink
14
+ // on commit or the rollback sink on rollback.
15
+ export function carrySavepointRollbackHooks(innerCommitSink, outerCommitSink, savepointRolledBack) {
16
+ const innerHooks = rollbackSinks.get(innerCommitSink) ?? [];
17
+ if (innerHooks.length > 0) {
18
+ if (savepointRolledBack)
19
+ outerCommitSink.push(...innerHooks);
20
+ afterRollbackHooksOf(outerCommitSink).push(...innerHooks);
21
+ }
22
+ }
23
+ export async function flushAfterRollback(commitSink, logError) {
24
+ const hooks = rollbackSinks.get(commitSink) ?? [];
25
+ const outcomes = await Promise.allSettled(hooks.map((hook) => hook()));
26
+ for (const outcome of outcomes) {
27
+ if (outcome.status === "rejected") {
28
+ const detail = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
29
+ logError.error("afterRollback hook failed", { error: detail });
30
+ }
31
+ }
32
+ }
@@ -3,6 +3,7 @@ import { transaction } from "../db/query.js";
3
3
  import { InternalError, toWriteErrorInfo, UnprocessableError, writeFailure, } from "../errors/index.js";
4
4
  import { createFallbackLogger } from "../logging/utils.js";
5
5
  import { parseJsonSafe } from "../utils/safe-json.js";
6
+ import { flushAfterRollback } from "./after-rollback-sink.js";
6
7
  import { resolveAppPoolDb } from "./dispatch-shared.js";
7
8
  import { executeNestedWrite, executeWrite } from "./dispatch-write.js";
8
9
  import { BatchRollback, isLifecycleResult, wrapToKumiko, } from "./dispatcher-utils.js";
@@ -36,6 +37,9 @@ async function runReservedCommand(scope, cmd, index, tx) {
36
37
  rewrapHooksWithOrigin(afterCommitHooks, hookStart, origin);
37
38
  return res;
38
39
  }
40
+ function rollbackLogger(scope) {
41
+ return createFallbackLogger("dispatcher", scope.ctx.appContext.log);
42
+ }
39
43
  // Sequential fallback without an outer transaction. A failure result and a throw both end the
40
44
  // batch, so both give the reserved capacity back.
41
45
  async function runCommandsWithoutTransaction(scope, commands, results) {
@@ -50,12 +54,14 @@ async function runCommandsWithoutTransaction(scope, commands, results) {
50
54
  // No tx means no rollback — but we still drop afterCommit hooks,
51
55
  // matching the semantic "failure = side-effects don't fire".
52
56
  await scope.reservations.releaseAll();
57
+ await flushAfterRollback(scope.afterCommitHooks, rollbackLogger(scope));
53
58
  return { index: i, error: res.error };
54
59
  }
55
60
  }
56
61
  }
57
62
  catch (e) {
58
63
  await scope.reservations.releaseAll();
64
+ await flushAfterRollback(scope.afterCommitHooks, rollbackLogger(scope));
59
65
  throw e;
60
66
  }
61
67
  return undefined;
@@ -208,7 +214,7 @@ async function runBatchBody(ctx, commands, user, requestId, inheritedOrigin) {
208
214
  }, reservations.error.httpStatus >= 500);
209
215
  }
210
216
  // batch() opens its own outer transaction — needs the top-level
211
- // connection's `.begin()` (TransactionSql exposes only `.savepoint()`).
217
+ // connection's `.begin()` (a transaction handle cannot begin another one).
212
218
  const db = resolveAppPoolDb(ctx);
213
219
  if (!db || runsWithoutTransaction) {
214
220
  // Without a DB connection (or for a perCommand handler, whose writes open their own transactions)
@@ -250,6 +256,7 @@ async function runBatchBody(ctx, commands, user, requestId, inheritedOrigin) {
250
256
  // Rollback or failed COMMIT: the transaction is over, so the reserved capacity goes back.
251
257
  // The release is outcome-safe: it only gives back a reservation row the handler tx did not delete.
252
258
  await reservations.releaseAll();
259
+ await flushAfterRollback(afterCommitHooks, createFallbackLogger("dispatcher", context.log));
253
260
  if (e instanceof BatchRollback) {
254
261
  // Thrown inside the callback, so the tx rolled back. A 4xx is
255
262
  // deterministic and stays cached; a 5xx may be transient.
@@ -140,6 +140,9 @@ export function applyMemberResolutionReadOnly(handlerContext) {
140
140
  scheduleAfterCommit: () => {
141
141
  throw memberResolutionReadOnlyDenied();
142
142
  },
143
+ scheduleAfterRollback: () => {
144
+ throw memberResolutionReadOnlyDenied();
145
+ },
143
146
  // A read as a member gets no system-scope DB, preSave pipeline or file storage.
144
147
  runPreSave: undefined,
145
148
  systemDb: undefined,
@@ -199,6 +202,7 @@ export function denyTransactionBoundSurfaces(handlerContext) {
199
202
  write: denySurface(beforeTransactionDenied, "write"),
200
203
  writeAs: denySurface(beforeTransactionDenied, "writeAs"),
201
204
  scheduleAfterCommit: denySurface(beforeTransactionDenied, "scheduleAfterCommit"),
205
+ scheduleAfterRollback: denySurface(beforeTransactionDenied, "scheduleAfterRollback"),
202
206
  ...(handlerContext.notify && { notify: denySurface(beforeTransactionDenied, "notify") }),
203
207
  ...(handlerContext.jobRunner && {
204
208
  jobRunner: denyingJobRunnerProxy(() => beforeTransactionDenied("jobRunner")),
@@ -212,9 +212,11 @@ export async function deliverEvents(consumer, events, context, maxAttempts, stat
212
212
  resolve(row.id);
213
213
  continue;
214
214
  }
215
- attempts += 1;
216
215
  lastError = errMessage;
217
216
  failedEventId = row.id;
217
+ if (consumer.errorPolicy?.isTransientError?.(e))
218
+ break;
219
+ attempts += 1;
218
220
  if (attempts >= effectiveMaxAttempts)
219
221
  deadLettered = true;
220
222
  break;
@@ -7,6 +7,7 @@ export type EventConsumerBatchHandler = (events: readonly StoredEvent[], ctx: Ap
7
7
  export type EventConsumerErrorPolicy = {
8
8
  readonly skipApplyErrors?: boolean;
9
9
  readonly maxAttempts?: number;
10
+ readonly isTransientError?: (error: unknown) => boolean;
10
11
  };
11
12
  export type EventConsumer = {
12
13
  readonly name: string;
@@ -16,12 +16,10 @@ const PENDING_GAPS_WARN_THRESHOLD = 1000;
16
16
  // Only these say the database socket itself is gone; timeouts, schema or pool errors do not
17
17
  // imply the dedicated LISTEN connection dropped.
18
18
  const CONNECTION_LOSS_CODES = new Set([
19
- "CONNECTION_CLOSED",
20
- "CONNECTION_DESTROYED",
21
- "CONNECTION_ENDED",
22
- "CONNECT_TIMEOUT",
23
- "ECONNREFUSED",
24
- "ECONNRESET",
19
+ "ERR_POSTGRES_CONNECTION_CLOSED",
20
+ "ERR_POSTGRES_CONNECTION_REFUSED",
21
+ "ERR_POSTGRES_CONNECTION_TIMEOUT",
22
+ "ERR_POSTGRES_CONNECTION_FAILED",
25
23
  "57P01",
26
24
  ]);
27
25
  function isConnectionLossError(e) {
@@ -111,7 +109,7 @@ export function createEventDispatcher(options) {
111
109
  let pgSubscription = null;
112
110
  // Bounds how many consumer turns hold a DB transaction at once. Each turn's
113
111
  // db.begin() checks out one pool connection, plus any writes the handler
114
- // itself makes via context.db — the app db pool defaults to postgres-js's
112
+ // itself makes via context.db — the app db pool defaults to Bun.SQL's
115
113
  // max=10 (db/connection.ts, no DATABASE_POOL_MAX override) and is shared
116
114
  // with HTTP request handlers, so turns can't be allowed to claim it all.
117
115
  // A pool no larger than this limit can deadlock: every connection held by
@@ -493,8 +491,7 @@ export function createEventDispatcher(options) {
493
491
  const sub = await options.pgClient.listen(EVENTS_PUBSUB_CHANNEL, runBackgroundPass, () => {
494
492
  // Fires on initial connect and again when the driver's own
495
493
  // re-LISTEN lands after a drop; during an outage that re-LISTEN
496
- // waits in its connection backoff (postgres-js: measured 10-30s
497
- // after the DB returns), polling covers
494
+ // waits in the driver's reconnect backoff, polling covers
498
495
  // delivery meanwhile. Never unlisten()
499
496
  // + listen() by hand: that leaves duplicate listeners.
500
497
  // ponytail: no own LISTEN reconnect, the backoff gap is polled;
@@ -1,4 +1,5 @@
1
1
  import type { WriteResult } from "../engine/types/index.ts";
2
+ import { type AfterRollbackHook } from "./after-rollback-sink.ts";
2
3
  import { type HandlerTransactionScope } from "./dispatch-shared.ts";
3
4
  import { type AfterCommitHook } from "./dispatcher-utils.ts";
4
5
  import type { HandlerContextScope } from "./handler-context-scope.ts";
@@ -17,5 +18,6 @@ export declare function buildDispatchFacet(scope: HandlerContextScope, sinks: Co
17
18
  };
18
19
  scheduleFragment: {
19
20
  scheduleAfterCommit?: ((hook: AfterCommitHook) => void) | undefined;
21
+ scheduleAfterRollback?: ((hook: AfterRollbackHook) => void) | undefined;
20
22
  };
21
23
  };
@@ -1,5 +1,7 @@
1
+ import { runWithWriteOrigin } from "../api/request-context.js";
1
2
  import { runInSavepoint } from "../db/query.js";
2
3
  import { writeFailure } from "../errors/index.js";
4
+ import { afterRollbackHooksOf, carrySavepointRollbackHooks, } from "./after-rollback-sink.js";
3
5
  import { runBatch, unwrapSingle } from "./dispatch-batch.js";
4
6
  import { executeQuery } from "./dispatch-query.js";
5
7
  import { perCommandNestedDenied } from "./dispatch-shared.js";
@@ -40,9 +42,11 @@ async function runNestedWriteInSavepoint(scope, sinks, asUser, targetType, paylo
40
42
  return innerResult;
41
43
  });
42
44
  bridgeSink.push(...innerAfterCommitHooks);
45
+ carrySavepointRollbackHooks(innerAfterCommitHooks, bridgeSink, false);
43
46
  return result;
44
47
  }
45
48
  catch (e) {
49
+ carrySavepointRollbackHooks(innerAfterCommitHooks, bridgeSink, true);
46
50
  if (e instanceof NestedWriteRollback)
47
51
  return e.failure;
48
52
  throw e;
@@ -64,6 +68,9 @@ export function buildDispatchFacet(scope, sinks) {
64
68
  const scheduleAfterCommit = (hook) => {
65
69
  sinks.bridgeSink.push(hook);
66
70
  };
71
+ const scheduleAfterRollback = (hook) => {
72
+ afterRollbackHooksOf(sinks.bridgeSink).push(() => runWithWriteOrigin(origin, hook));
73
+ };
67
74
  const executeNestedWriteInSavepoint = createSerializedNestedWriter(scope, sinks);
68
75
  // Without an open transaction (post-commit hooks, stream/query handlers, perCommand handlers) a bare
69
76
  // executeWrite would auto-commit event append, projections and inTransaction hooks separately and its
@@ -84,6 +91,11 @@ export function buildDispatchFacet(scope, sinks) {
84
91
  write: (targetType, payload) => writeAsUser(user, targetType, payload),
85
92
  writeAs: identitySwitch.writeAs,
86
93
  },
87
- scheduleFragment: { ...(sinks.hasLiveCommitSink && { scheduleAfterCommit }) },
94
+ scheduleFragment: {
95
+ ...(sinks.hasLiveCommitSink && { scheduleAfterCommit }),
96
+ // perCommand and no-tx paths have earlier commands already committed: nothing to compensate on rollback.
97
+ ...(sinks.hasLiveCommitSink &&
98
+ transactionScope === "inTransaction" && { scheduleAfterRollback }),
99
+ },
88
100
  };
89
101
  }
@@ -1,4 +1,4 @@
1
- import type { AggregateStreamHandle, AppendEventArgs, AppendEventFn, FetchForWritingArgs } from "../engine/types/index.ts";
1
+ import type { AggregateStreamHandle, AppendEventArgs, AppendEventFn, FetchForWritingArgs, HandlerContext } from "../engine/types/index.ts";
2
2
  import { IdempotentAppendConflictError as EventStoreIdempotentAppendConflictError, VersionConflictError as EventStoreVersionConflictError } from "../event-store/errors.ts";
3
3
  import { type StoredEvent } from "../event-store/event-store.ts";
4
4
  import { type LoadAggregateWithSnapshotOptions, type LoadAggregateWithSnapshotResult, type SnapshotReducer } from "../event-store/snapshot.ts";
@@ -20,9 +20,7 @@ export declare function buildEventStoreFacet(scope: HandlerContextScope): {
20
20
  conflict: EventStoreIdempotentAppendConflictError;
21
21
  }>;
22
22
  fetchForWriting: (args: FetchForWritingArgs) => Promise<AggregateStreamHandle>;
23
- loadAggregate: (aggregateId: string, loadOptions?: {
24
- readonly asOf?: Temporal.Instant;
25
- }) => Promise<readonly StoredEvent[]>;
23
+ loadAggregate: (aggregateId: string, loadOptions?: Parameters<HandlerContext["loadAggregate"]>[1]) => Promise<readonly StoredEvent[]>;
26
24
  archiveStream: (aggregateId: string, archiveArgs: {
27
25
  readonly aggregateType: string;
28
26
  readonly reason?: string;
@@ -38,9 +38,9 @@ async function appendDomainEvent(ctx, args, user, tx, callerFeature) {
38
38
  }, args);
39
39
  }
40
40
  // Savepoint-scoped append: catches a losing writer's VersionConflictError
41
- // without poisoning the rest of the handler's transaction. postgres.js
42
- // aborts the WHOLE begin() on an uncaught statement error
43
- // (SQLSTATE 25P02) even if the JS error is caught — runInSavepoint
41
+ // without poisoning the rest of the handler's transaction. Postgres
42
+ // aborts the WHOLE transaction on a statement error
43
+ // (SQLSTATE 25P02 until rollback) even if the JS error is caught — runInSavepoint
44
44
  // wraps the append in a real SAVEPOINT so only that nested scope rolls
45
45
  // back on conflict, and subsequent statements in the outer tx (e.g. a
46
46
  // dedup-anchor insert) still succeed. Use when a handler must react to
@@ -127,7 +127,9 @@ function buildLoadAggregate(scope) {
127
127
  const dbSource = requireDbSource(ctx, tx, `ctx.loadAggregate("${aggregateId}")`);
128
128
  const events = loadOptions?.asOf
129
129
  ? await loadAggregateAsOf(dbSource, aggregateId, user.tenantId, loadOptions.asOf)
130
- : await loadAggregate(dbSource, aggregateId, user.tenantId);
130
+ : await loadAggregate(dbSource, aggregateId, user.tenantId, {
131
+ ...(loadOptions?.atVersion !== undefined && { atVersion: loadOptions.atVersion }),
132
+ });
131
133
  return upcastStoredEvents(events, registry.getEventUpcasters(), {
132
134
  db: dbSource,
133
135
  tenantId: user.tenantId,
@@ -22,4 +22,6 @@ export declare function createSseBroadcastEventConsumer(sseBroker: SseBroker): E
22
22
  export declare const JOB_TRIGGER_CONSUMER_NAME = "system:consumer:job-trigger";
23
23
  export declare function createJobTriggerEventConsumer(jobRunner: JobRunnerRef, registry: Registry): EventConsumer;
24
24
  export declare const ACCESS_INVALIDATION_CONSUMER_NAME = "system:consumer:access-invalidation";
25
- export declare function createAccessInvalidationEventConsumer(sseBroker: SseBroker): EventConsumer;
25
+ export declare function createAccessInvalidationEventConsumer(sseBroker: SseBroker, opts?: {
26
+ readonly now?: () => number;
27
+ }): EventConsumer;
@@ -552,9 +552,58 @@ function readPatRevokedScope(payload) {
552
552
  const tokenIds = readNonEmptyStringArray(payload["tokenIds"]);
553
553
  return tokenIds ? { kind: "pat-tokens", tokenIds } : { kind: "user" };
554
554
  }
555
- export function createAccessInvalidationEventConsumer(sseBroker) {
555
+ const ACCESS_INVALIDATION_BACKOFF_INITIAL_MS = 250;
556
+ const ACCESS_INVALIDATION_BACKOFF_MAX_MS = 10_000;
557
+ // Only the publish can fail because Redis is unreachable. The consumer must keep retrying those
558
+ // until Redis is back (dead-lettering would silently stop every later revocation reaching open
559
+ // streams), while any other failure keeps the dispatcher's attempt limit so one poison event
560
+ // cannot hold back every later revocation for good.
561
+ class AccessInvalidationTransportError extends Error {
562
+ constructor(cause) {
563
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
564
+ this.name = "AccessInvalidationTransportError";
565
+ }
566
+ }
567
+ function isAccessInvalidationTransportError(error) {
568
+ return error instanceof AccessInvalidationTransportError;
569
+ }
570
+ // The gate rethrows without touching Redis until the backoff elapsed.
571
+ function withTransportBackoff(handle, now) {
572
+ let delayMs = ACCESS_INVALIDATION_BACKOFF_INITIAL_MS;
573
+ let retryNotBefore = 0;
574
+ let transportError = null;
575
+ return async (event) => {
576
+ if (transportError !== null && now() < retryNotBefore)
577
+ throw transportError;
578
+ try {
579
+ await handle(event);
580
+ delayMs = ACCESS_INVALIDATION_BACKOFF_INITIAL_MS;
581
+ transportError = null;
582
+ }
583
+ catch (err) {
584
+ if (isAccessInvalidationTransportError(err)) {
585
+ transportError = err;
586
+ retryNotBefore = now() + delayMs;
587
+ delayMs = Math.min(delayMs * 2, ACCESS_INVALIDATION_BACKOFF_MAX_MS);
588
+ }
589
+ throw err;
590
+ }
591
+ };
592
+ }
593
+ export function createAccessInvalidationEventConsumer(sseBroker, opts = {}) {
594
+ const now = opts.now ?? Date.now;
595
+ const publish = async (userId, scope) => {
596
+ try {
597
+ await sseBroker.publishAccessInvalidation(userId, scope);
598
+ }
599
+ catch (cause) {
600
+ throw new AccessInvalidationTransportError(cause);
601
+ }
602
+ };
556
603
  return {
557
604
  name: ACCESS_INVALIDATION_CONSUMER_NAME,
605
+ // Count-based dead-lettering would end after a few seconds of Redis downtime.
606
+ errorPolicy: { isTransientError: isAccessInvalidationTransportError },
558
607
  // Shared delivery, same reasoning as SSE broadcast: one
559
608
  // cursor reads every event once and calls sseBroker.publishAccessInvalidation
560
609
  // below, which a Redis-backed broker republishes to every replica's
@@ -565,7 +614,7 @@ export function createAccessInvalidationEventConsumer(sseBroker) {
565
614
  // existed has nothing to invalidate — replaying it would be a no-op at
566
615
  // best and a spurious stream-close at worst.
567
616
  startFrom: "now",
568
- handler: async (event) => {
617
+ handler: withTransportBackoff(async (event) => {
569
618
  if (event.type === SESSION_REVOKED_EVENT_TYPE) {
570
619
  const userId = event.payload["userId"];
571
620
  // skip: malformed session-revoked payload — fail open on this one
@@ -574,7 +623,7 @@ export function createAccessInvalidationEventConsumer(sseBroker) {
574
623
  // every user behind one bad row).
575
624
  if (typeof userId !== "string" || userId.length === 0)
576
625
  return;
577
- await sseBroker.publishAccessInvalidation(userId, readSessionRevokedScope(event.payload));
626
+ await publish(userId, readSessionRevokedScope(event.payload));
578
627
  }
579
628
  if (event.type === PAT_REVOKED_EVENT_TYPE) {
580
629
  const userId = event.payload["userId"];
@@ -582,7 +631,7 @@ export function createAccessInvalidationEventConsumer(sseBroker) {
582
631
  // session-revoked above.
583
632
  if (typeof userId !== "string" || userId.length === 0)
584
633
  return;
585
- await sseBroker.publishAccessInvalidation(userId, readPatRevokedScope(event.payload));
634
+ await publish(userId, readPatRevokedScope(event.payload));
586
635
  }
587
636
  if (event.type === TENANT_MEMBERSHIP_UPDATED_EVENT_TYPE ||
588
637
  event.type === TENANT_MEMBERSHIP_DELETED_EVENT_TYPE) {
@@ -591,8 +640,8 @@ export function createAccessInvalidationEventConsumer(sseBroker) {
591
640
  // reasoning as above.
592
641
  if (userId === undefined)
593
642
  return;
594
- await sseBroker.publishAccessInvalidation(userId);
643
+ await publish(userId);
595
644
  }
596
- },
645
+ }, now),
597
646
  };
598
647
  }
@@ -1,19 +1,24 @@
1
1
  import { maskWriteOnlyFields } from "../engine/field-access.js";
2
2
  import { isLifecycleResult } from "./dispatcher-utils.js";
3
+ // The echoed event carries plaintext entity data; only hooks and projections may read it.
3
4
  function maskLifecycleOrRow(entity, data) {
4
5
  if (typeof data !== "object" || data === null || Array.isArray(data))
5
6
  return data;
6
- if (!isLifecycleResult(data))
7
- return maskWriteOnlyFields(entity, data); // @cast-boundary engine-payload
8
- if (data.kind === "save") {
7
+ if (!isLifecycleResult(data)) {
8
+ return entity ? maskWriteOnlyFields(entity, data) : data; // @cast-boundary engine-payload
9
+ }
10
+ const { event: _event, ...lifecycle } = data;
11
+ if (!entity)
12
+ return lifecycle;
13
+ if (lifecycle.kind === "save") {
9
14
  return {
10
- ...data,
11
- data: maskWriteOnlyFields(entity, data.data),
12
- changes: maskWriteOnlyFields(entity, data.changes),
13
- previous: maskWriteOnlyFields(entity, data.previous),
15
+ ...lifecycle,
16
+ data: maskWriteOnlyFields(entity, lifecycle.data),
17
+ changes: maskWriteOnlyFields(entity, lifecycle.changes),
18
+ previous: maskWriteOnlyFields(entity, lifecycle.previous),
14
19
  };
15
20
  }
16
- return { ...data, data: maskWriteOnlyFields(entity, data.data) };
21
+ return { ...lifecycle, data: maskWriteOnlyFields(entity, lifecycle.data) };
17
22
  }
18
23
  // Copy-on-mask: the original results still feed postSaveBatch hooks, which
19
24
  // need the plaintext. Only what leaves the dispatcher is masked.
@@ -22,8 +27,6 @@ export function maskWriteResultForClient(registry, handlerType, result) {
22
27
  return result;
23
28
  const entityName = registry.getHandlerEntity(handlerType);
24
29
  const entity = entityName ? registry.getEntity(entityName) : undefined;
25
- if (!entity)
26
- return result;
27
30
  const data = maskLifecycleOrRow(entity, result.data);
28
31
  return data === result.data ? result : { ...result, data };
29
32
  }
@@ -10,7 +10,7 @@ export async function runBaselineCommand(ctx) {
10
10
  return 1;
11
11
  if (!migrationsDirExistsOrReport(ctx))
12
12
  return 1;
13
- const { db, close } = createDbConnection({ url: dbUrl });
13
+ const { db, close } = createDbConnection({ url: dbUrl, maxConnections: 1 });
14
14
  try {
15
15
  const result = await baselineMigrations(db, loadMigrationsFromDir(migrationsDir));
16
16
  out.log("");
@@ -42,7 +42,7 @@ export async function runStatusCommand(ctx) {
42
42
  return 0;
43
43
  }
44
44
  const local = loadMigrationsFromDir(migrationsDir);
45
- const { db, close } = createDbConnection({ url: dbUrl });
45
+ const { db, close } = createDbConnection({ url: dbUrl, maxConnections: 1 });
46
46
  try {
47
47
  const applied = await fetchAppliedMigrationIds(db);
48
48
  out.log("");
@@ -6,7 +6,7 @@
6
6
  // existing row and indexes it, same as a live write would.
7
7
  import { resolveTableName } from "../db/entity-table-meta.js";
8
8
  import { executeRawQueryRead } from "../db/queries/raw-sql.js";
9
- import { withoutStatementTimeout } from "../db/statement-timeout.js";
9
+ import { runInNewTransaction, runnerCanBeginTransaction } from "../db/query.js";
10
10
  import { buildSearchDocument, decryptSearchableSubjectFields, hasErasedSearchableSubjectField, } from "../pipeline/system-hooks.js";
11
11
  import { toSnakeCase } from "../utils/case.js";
12
12
  function quoteIdent(name) {
@@ -26,10 +26,20 @@ function rowToState(row, fieldNames) {
26
26
  }
27
27
  return state;
28
28
  }
29
- export function reindexEntity(db, registry, searchAdapter, entityName, tenantId, options = {}) {
30
- return withoutStatementTimeout(db, (unboundedDb) => scanAndIndexEntity(unboundedDb, registry, searchAdapter, entityName, tenantId, options));
29
+ // Server pools cap statement_timeout, and a batch scan over a large table can outlast it. The caller's
30
+ // runner (often a gated proxy around the pool) is used as is, so the cap is lifted per batch
31
+ // with a transaction-local set_config instead of moving the scan to an ungated sibling pool. A runner already inside a
32
+ // transaction keeps the caller's setting rather than leaking ours into its remaining statements.
33
+ async function readBatchWithoutStatementTimeout(db, sqlText, params) {
34
+ if (!runnerCanBeginTransaction(db)) {
35
+ return executeRawQueryRead(db, sqlText, params);
36
+ }
37
+ return runInNewTransaction(db, async (tx) => {
38
+ await executeRawQueryRead(tx, "SELECT set_config('statement_timeout', '0', true)", []);
39
+ return executeRawQueryRead(tx, sqlText, params);
40
+ });
31
41
  }
32
- async function scanAndIndexEntity(db, registry, searchAdapter, entityName, tenantId, options) {
42
+ export async function reindexEntity(db, registry, searchAdapter, entityName, tenantId, options = {}) {
33
43
  const entity = registry.getEntity(entityName);
34
44
  if (!entity) {
35
45
  throw new Error(`reindexEntity: unknown entity "${entityName}"`);
@@ -61,7 +71,7 @@ async function scanAndIndexEntity(db, registry, searchAdapter, entityName, tenan
61
71
  for (;;) {
62
72
  // Keyset, not OFFSET: rows deleted or changed while the loop runs must not shift the window.
63
73
  const afterLastId = lastId === undefined ? "" : `AND ${quoteIdent("id")} > $3`;
64
- const rows = await executeRawQueryRead(db, `SELECT * FROM ${quoteIdent(tableName)}
74
+ const rows = await readBatchWithoutStatementTimeout(db, `SELECT * FROM ${quoteIdent(tableName)}
65
75
  WHERE ${quoteIdent("tenant_id")} = $1 ${deletedFilter} ${afterLastId}
66
76
  ORDER BY ${quoteIdent("id")} ASC
67
77
  LIMIT $2`, lastId === undefined ? [tenantId, batchSize] : [tenantId, batchSize, lastId]);
@@ -11,8 +11,8 @@ export type { BooleanFieldDef, DateFieldDef, EmbeddedDerivedCellDef, EntityDefin
11
11
  export type { AccessRule, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, RoleAccessPersonalData, RoleAccessRule, } from "../engine/types/handlers.ts";
12
12
  export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.ts";
13
13
  export type { IconKey, NavDefinition, NavIconKey } from "../engine/types/nav.ts";
14
- export type { ActionFormFooterAction, ActionFormLoad, ActionFormRedirect, ActionFormScreenDefinition, ActionFormSuccessFact, ActionFormSuccessLink, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartMarkerKind, DashboardChartPanel, DashboardChartRangeOption, DashboardChartRanges, DashboardChartTone, DashboardCurrencyFormat, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelGate, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardProgressTone, DashboardRecordBinding, DashboardRefresh, DashboardRelativeTimeFormat, DashboardRelativeTimeParam, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextBlock, DashboardTextPanel, DashboardTextParam, DashboardTimeRangeDefinition, DashboardValueFormat, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EmbeddedListEditSpec, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityListExpandableRow, EntityListFacetConfig, EntityListFacetExtraOption, EntityListScreenDefinition, EntityWriteExcludedFields, FieldAffix, FieldCondition, FieldFillAction, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListEmptyStateSpec, ListFacetSpec, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSubtitlePart, RelatedListGroupBy, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionClientAction, RowActionDisplay, RowActionDrawer, RowActionMode, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.ts";
15
- export { relatedListGroupHeaderLabel, relatedListGroupKey, } from "../engine/types/screen.ts";
14
+ export type { ActionFormFooterAction, ActionFormLoad, ActionFormRedirect, ActionFormScreenDefinition, ActionFormSuccessFact, ActionFormSuccessLink, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartMarkerKind, DashboardChartPanel, DashboardChartRangeOption, DashboardChartRanges, DashboardChartTone, DashboardCurrencyFormat, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelGate, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardProgressTone, DashboardRecordBinding, DashboardRefresh, DashboardRelativeTimeFormat, DashboardRelativeTimeParam, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextBlock, DashboardTextPanel, DashboardTextParam, DashboardTimeRangeDefinition, DashboardValueFormat, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EmbeddedListEditSpec, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityListExpandableRow, EntityListFacetConfig, EntityListFacetExtraOption, EntityListScreenDefinition, EntityWriteExcludedFields, ExpandableRowGrouping, FieldAffix, FieldCondition, FieldFillAction, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListBulkAction, ListColumnDisplay, ListColumnSpec, ListEmptyStateSpec, ListFacetSpec, ListSortSpec, ListTableDensity, ListTableOptions, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSubtitlePart, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionClientAction, RowActionDisplay, RowActionDrawer, RowActionMode, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.ts";
15
+ export { MAX_BULK_SELECTION, relatedListGroupHeaderLabel, relatedListGroupKey, } from "../engine/types/screen.ts";
16
16
  export type { TargetRef } from "../engine/types/target-ref.ts";
17
17
  export type { TreeAction, TreeNode, TreeNodeState } from "../engine/types/tree-node.ts";
18
18
  export type { WorkspaceDefinition } from "../engine/types/workspace.ts";
@@ -25,7 +25,7 @@ export { applyFormDerivations, isFormDerivationInputMode, isUnusedFallbackOperan
25
25
  export { parseRefTarget } from "../engine/parse-ref-target.js";
26
26
  export { ACTION_FORM_PSEUDO_ENTITY, availableSelectOptions, evalFieldCondition, explicitListScreenId, findUnavailableSelectOptions, isExtensionEditSection, isFieldsEditSection, isFormatSpec, isRecordLocked, isUnchangedValue, isWriteFormEditSection, normalizeEditField, normalizeListColumn, resolveNavParentScreen, sectionFieldSpecs, } from "../engine/screen-helpers.js";
27
27
  export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
28
- export { relatedListGroupHeaderLabel, relatedListGroupKey, } from "../engine/types/screen.js";
28
+ export { MAX_BULK_SELECTION, relatedListGroupHeaderLabel, relatedListGroupKey, } from "../engine/types/screen.js";
29
29
  export { ACTION_FORM_ENTITY, handlerFieldLabelKey, handlerTitleKey, PROJECTION_DETAIL_ENTITY, WRITE_FORM_SECTION_ENTITY, } from "../i18n/required-surface-keys.js";
30
30
  export { CARD_META_MAX, cardColumnRoles, cardFileDisplayKind } from "./card-column-roles.js";
31
31
  export { formalLocaleTag } from "./formality.js";
@@ -23,3 +23,5 @@ export type UpgradeChangelogSection = {
23
23
  export declare function renderUpgradeSection(section: UpgradeChangelogSection): string;
24
24
  /** Puts the section on top; a section for the same target version is replaced, so a re-run stays idempotent. */
25
25
  export declare function mergeUpgradeSection(existing: string | undefined, section: UpgradeChangelogSection): string;
26
+ /** Pure string match on the rendered `- [open] <id> · ` prefix, in every section. */
27
+ export declare function setManualStatusInChangelog(content: string, ids: ReadonlySet<string>, resolution: "migrated" | "not-applicable"): string;