@cosmicdrift/kumiko-framework 0.386.0 → 0.387.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 (288) hide show
  1. package/LICENSE +21 -57
  2. package/README.md +2 -2
  3. package/agents/widgets.md +39 -39
  4. package/dist/api/api-constants.js +2 -2
  5. package/dist/api/auth-middleware.js +7 -7
  6. package/dist/api/auth-rate-limiters.js +7 -3
  7. package/dist/api/auth-routes-mfa.js +1 -1
  8. package/dist/api/auth-routes-session.js +1 -1
  9. package/dist/api/auth-routes-shared.js +7 -3
  10. package/dist/api/auth-routes-signup.js +1 -1
  11. package/dist/api/client-ip.js +3 -1
  12. package/dist/api/extra-route.js +1 -1
  13. package/dist/api/pii-leak-guard.js +4 -2
  14. package/dist/api/readiness.js +5 -5
  15. package/dist/api/redis-sse-broker.js +8 -6
  16. package/dist/api/routes.js +2 -2
  17. package/dist/api/server.js +34 -33
  18. package/dist/api/sse-broker.js +1 -1
  19. package/dist/api/sse-route.d.ts +13 -13
  20. package/dist/api/sse-route.js +15 -15
  21. package/dist/auth/index.js +4 -4
  22. package/dist/auth/roles.d.ts +4 -4
  23. package/dist/auth/roles.js +24 -24
  24. package/dist/compliance/duration-spec.js +18 -20
  25. package/dist/compliance/index.js +2 -3
  26. package/dist/compliance/override-schema.d.ts +4 -4
  27. package/dist/compliance/override-schema.js +19 -22
  28. package/dist/compliance/profiles.d.ts +26 -29
  29. package/dist/compliance/profiles.js +62 -69
  30. package/dist/compliance/sub-processors.d.ts +28 -30
  31. package/dist/compliance/sub-processors.js +17 -21
  32. package/dist/consumer-cli.js +4 -4
  33. package/dist/crypto/blind-index.js +18 -17
  34. package/dist/crypto/ciphertext-pattern.js +1 -1
  35. package/dist/crypto/event-pii.js +3 -3
  36. package/dist/crypto/kek-source.d.ts +1 -1
  37. package/dist/crypto/kek-source.js +3 -2
  38. package/dist/crypto/kms-adapter.d.ts +1 -1
  39. package/dist/crypto/kms-adapter.js +4 -6
  40. package/dist/crypto/kms-wiring.d.ts +1 -2
  41. package/dist/crypto/kms-wiring.js +2 -2
  42. package/dist/crypto/pg-kms-adapter.js +2 -6
  43. package/dist/crypto/pii-field-encryption.js +4 -4
  44. package/dist/crypto/subject-resolver.d.ts +1 -1
  45. package/dist/crypto/subject-resolver.js +2 -2
  46. package/dist/db/apply-entity-event.d.ts +6 -6
  47. package/dist/db/apply-entity-event.js +55 -55
  48. package/dist/db/blind-index-cleanup.js +7 -7
  49. package/dist/db/collect-table-metas.js +10 -11
  50. package/dist/db/compound-types.d.ts +1 -1
  51. package/dist/db/compound-types.js +12 -12
  52. package/dist/db/cursor.js +3 -4
  53. package/dist/db/dialect.d.ts +2 -2
  54. package/dist/db/dialect.js +2 -2
  55. package/dist/db/eagerload.d.ts +3 -3
  56. package/dist/db/eagerload.js +21 -12
  57. package/dist/db/entity-table-meta.d.ts +2 -2
  58. package/dist/db/entity-table-meta.js +4 -4
  59. package/dist/db/event-store-executor-context.d.ts +1 -1
  60. package/dist/db/event-store-executor-context.js +13 -13
  61. package/dist/db/event-store-executor-read.js +27 -28
  62. package/dist/db/event-store-executor-write.js +65 -70
  63. package/dist/db/located-timestamp.d.ts +11 -11
  64. package/dist/db/located-timestamp.js +29 -30
  65. package/dist/db/migrate-generator.d.ts +1 -1
  66. package/dist/db/migrate-generator.js +26 -19
  67. package/dist/db/migrate-runner.js +25 -25
  68. package/dist/db/money.d.ts +6 -6
  69. package/dist/db/money.js +29 -27
  70. package/dist/db/queries/backfill-pii.js +5 -5
  71. package/dist/db/queries/ddl.js +1 -1
  72. package/dist/db/queries/es-ops.d.ts +1 -1
  73. package/dist/db/queries/es-ops.js +1 -1
  74. package/dist/db/queries/event-consumer.js +3 -3
  75. package/dist/db/queries/event-store.js +4 -2
  76. package/dist/db/queries/projection-rebuild.js +3 -3
  77. package/dist/db/queries/raw-sql.d.ts +2 -2
  78. package/dist/db/queries/raw-sql.js +2 -2
  79. package/dist/db/queries/shadow-swap.js +19 -19
  80. package/dist/db/query.d.ts +1 -1
  81. package/dist/db/query.js +6 -6
  82. package/dist/db/rebuild-marker.js +20 -21
  83. package/dist/db/replay-migration-sql.js +2 -2
  84. package/dist/db/schema-inspection.js +4 -5
  85. package/dist/db/table-builder.d.ts +1 -2
  86. package/dist/db/table-builder.js +82 -82
  87. package/dist/db/tenant-db.d.ts +2 -2
  88. package/dist/db/tenant-db.js +9 -2
  89. package/dist/derivatives/delete-stored-file.js +5 -2
  90. package/dist/derivatives/derivatives-context.js +1 -1
  91. package/dist/engine/boot-validator/access-roles.js +4 -3
  92. package/dist/engine/boot-validator/api-ext.js +14 -14
  93. package/dist/engine/boot-validator/config-deps.js +7 -6
  94. package/dist/engine/boot-validator/detail-screens.js +3 -3
  95. package/dist/engine/boot-validator/entity-handler.js +60 -61
  96. package/dist/engine/boot-validator/gdpr-storage.js +4 -4
  97. package/dist/engine/boot-validator/i18n-keys.js +1 -1
  98. package/dist/engine/boot-validator/index.d.ts +2 -2
  99. package/dist/engine/boot-validator/index.js +20 -20
  100. package/dist/engine/boot-validator/nav.js +5 -5
  101. package/dist/engine/boot-validator/ownership.js +12 -9
  102. package/dist/engine/boot-validator/parent-ref.js +1 -1
  103. package/dist/engine/boot-validator/pii-retention.js +50 -57
  104. package/dist/engine/boot-validator/projection-list-screens.js +7 -7
  105. package/dist/engine/boot-validator/query-refs.js +1 -1
  106. package/dist/engine/boot-validator/screens-actions.js +15 -16
  107. package/dist/engine/boot-validator/screens-forms.js +10 -11
  108. package/dist/engine/boot-validator/screens-lists.js +14 -15
  109. package/dist/engine/boot-validator/screens.js +34 -40
  110. package/dist/engine/boot-validator/security-baseline.js +3 -2
  111. package/dist/engine/boot-validator/transfer-graph.js +2 -2
  112. package/dist/engine/boot-validator/workspaces.js +3 -3
  113. package/dist/engine/bridge-stub.js +3 -4
  114. package/dist/engine/build-app-schema.js +75 -75
  115. package/dist/engine/build-config-feature-schema.js +50 -50
  116. package/dist/engine/build-target.js +20 -23
  117. package/dist/engine/codemod/pipeline-codemod.js +20 -13
  118. package/dist/engine/config-helpers.js +4 -4
  119. package/dist/engine/constants.js +2 -2
  120. package/dist/engine/create-app.js +2 -2
  121. package/dist/engine/define-feature.js +12 -11
  122. package/dist/engine/define-handler.js +12 -11
  123. package/dist/engine/define-step.js +1 -1
  124. package/dist/engine/define-workflow.d.ts +5 -5
  125. package/dist/engine/define-workflow.js +6 -6
  126. package/dist/engine/entity-handlers.js +13 -14
  127. package/dist/engine/event-def.js +2 -2
  128. package/dist/engine/extension-names.d.ts +16 -17
  129. package/dist/engine/extension-names.js +37 -38
  130. package/dist/engine/extensions/escape-hatch-usage.js +1 -1
  131. package/dist/engine/extensions/tenant-data.js +2 -2
  132. package/dist/engine/extensions/user-data.d.ts +23 -24
  133. package/dist/engine/factories.d.ts +14 -13
  134. package/dist/engine/factories.js +38 -37
  135. package/dist/engine/feature-ast/entity-field-types.js +1 -1
  136. package/dist/engine/feature-ast/extractors/handlers.js +1 -1
  137. package/dist/engine/feature-ast/extractors/hooks.js +1 -1
  138. package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
  139. package/dist/engine/feature-ast/extractors/shared.js +2 -3
  140. package/dist/engine/feature-ast/parse.js +13 -13
  141. package/dist/engine/feature-ast/patch.js +1 -1
  142. package/dist/engine/feature-ast/pattern-change-schema.js +1 -1
  143. package/dist/engine/feature-ast/patterns.d.ts +2 -2
  144. package/dist/engine/feature-ast/render.js +2 -2
  145. package/dist/engine/feature-config-events-jobs.js +15 -12
  146. package/dist/engine/feature-entity-handlers.js +9 -5
  147. package/dist/engine/feature-manifest.d.ts +9 -9
  148. package/dist/engine/feature-manifest.js +6 -7
  149. package/dist/engine/feature-ui-extensions.d.ts +3 -3
  150. package/dist/engine/feature-ui-extensions.js +30 -22
  151. package/dist/engine/field-access.js +5 -6
  152. package/dist/engine/index.d.ts +1 -0
  153. package/dist/engine/index.js +4 -4
  154. package/dist/engine/object-form.d.ts +1 -0
  155. package/dist/engine/object-form.js +14 -4
  156. package/dist/engine/ownership.js +3 -3
  157. package/dist/engine/parse-ref-target.js +5 -7
  158. package/dist/engine/pattern-library/index.js +2 -2
  159. package/dist/engine/pattern-library/library.js +2 -2
  160. package/dist/engine/pattern-library/types.js +3 -3
  161. package/dist/engine/pipeline.js +2 -2
  162. package/dist/engine/project-app-schema-for-roles.js +1 -1
  163. package/dist/engine/projection-helpers.js +9 -9
  164. package/dist/engine/qualified-name.js +2 -2
  165. package/dist/engine/registry-facade.js +1 -1
  166. package/dist/engine/registry-ingest.js +12 -12
  167. package/dist/engine/registry-state.js +17 -19
  168. package/dist/engine/registry-validate.js +12 -12
  169. package/dist/engine/run-in.js +2 -2
  170. package/dist/engine/run-pipeline.js +7 -0
  171. package/dist/engine/schema-builder.js +40 -38
  172. package/dist/engine/screen-filter-ops.js +17 -20
  173. package/dist/engine/screen-helpers.js +4 -3
  174. package/dist/engine/state-machine.d.ts +12 -12
  175. package/dist/engine/state-machine.js +7 -7
  176. package/dist/engine/steps/_resolver-utils.js +4 -3
  177. package/dist/engine/steps/for-each.js +4 -4
  178. package/dist/engine/steps/unsafe-projection-delete.js +1 -1
  179. package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
  180. package/dist/engine/steps/wait-for-event.js +4 -4
  181. package/dist/engine/steps/workflow-system-events.js +3 -3
  182. package/dist/engine/tier-resolver-extension.d.ts +29 -27
  183. package/dist/engine/tier-resolver-extension.js +26 -26
  184. package/dist/engine/transfer-adjacency.js +2 -2
  185. package/dist/engine/validate-projection-allowlist.js +7 -27
  186. package/dist/engine/walk-step-instances.d.ts +2 -0
  187. package/dist/engine/walk-step-instances.js +38 -0
  188. package/dist/engine/where-rule-lint.js +1 -1
  189. package/dist/engine/write-handler-qn-extract.js +1 -1
  190. package/dist/entrypoint/index.js +3 -3
  191. package/dist/env/index.d.ts +7 -6
  192. package/dist/env/index.js +10 -9
  193. package/dist/errors/transition-details.js +7 -7
  194. package/dist/errors/write-error-info.d.ts +6 -6
  195. package/dist/errors/write-error-info.js +6 -6
  196. package/dist/es-ops/context.d.ts +9 -9
  197. package/dist/es-ops/context.js +28 -28
  198. package/dist/es-ops/index.js +7 -9
  199. package/dist/es-ops/operations-schema.js +4 -10
  200. package/dist/es-ops/runner.d.ts +11 -11
  201. package/dist/es-ops/runner.js +49 -53
  202. package/dist/es-ops/types.d.ts +55 -55
  203. package/dist/es-ops/types.js +11 -11
  204. package/dist/event-store/admin-api.js +6 -7
  205. package/dist/event-store/errors.js +1 -1
  206. package/dist/event-store/event-store.js +2 -2
  207. package/dist/event-store/rebuild-dead-letter.js +1 -1
  208. package/dist/files/feature.js +9 -10
  209. package/dist/files/file-handle.js +5 -6
  210. package/dist/files/file-ref-entity.js +15 -15
  211. package/dist/files/file-ref-table.js +7 -8
  212. package/dist/files/file-routes.d.ts +7 -0
  213. package/dist/files/file-routes.js +11 -5
  214. package/dist/files/in-memory-provider.js +8 -9
  215. package/dist/files/local-provider.js +18 -25
  216. package/dist/files/provider-resolver.d.ts +7 -7
  217. package/dist/files/storage-tracking.js +2 -2
  218. package/dist/files/zip-stream.d.ts +12 -19
  219. package/dist/files/zip-stream.js +44 -63
  220. package/dist/i18n/required-surface-keys.d.ts +1 -1
  221. package/dist/i18n/required-surface-keys.js +8 -8
  222. package/dist/jobs/job-runner-helpers.js +8 -2
  223. package/dist/jobs/job-runner-schedulers.d.ts +1 -1
  224. package/dist/jobs/job-runner-schedulers.js +3 -4
  225. package/dist/jobs/job-runner.js +13 -15
  226. package/dist/lifecycle/lifecycle.js +3 -5
  227. package/dist/lifecycle/signal-handlers.js +5 -4
  228. package/dist/logging/utils.d.ts +2 -1
  229. package/dist/logging/utils.js +38 -10
  230. package/dist/migrations/index.js +1 -1
  231. package/dist/migrations/kumiko-drift.js +14 -13
  232. package/dist/migrations/pending-rebuilds.d.ts +21 -20
  233. package/dist/migrations/pending-rebuilds.js +27 -27
  234. package/dist/migrations/projection-table-index.d.ts +3 -3
  235. package/dist/migrations/projection-table-index.js +8 -8
  236. package/dist/observability/metric-validator.js +1 -1
  237. package/dist/observability/metrics-handle.js +3 -3
  238. package/dist/pipeline/append-event-core.js +11 -2
  239. package/dist/pipeline/cascade-handler.d.ts +1 -1
  240. package/dist/pipeline/dispatch-batch.js +2 -2
  241. package/dist/pipeline/dispatch-query.js +10 -10
  242. package/dist/pipeline/dispatch-shared.d.ts +1 -0
  243. package/dist/pipeline/dispatch-shared.js +15 -17
  244. package/dist/pipeline/dispatch-stream.js +1 -1
  245. package/dist/pipeline/dispatch-write.js +3 -3
  246. package/dist/pipeline/dispatcher-utils.d.ts +1 -1
  247. package/dist/pipeline/dispatcher-utils.js +1 -1
  248. package/dist/pipeline/event-consumer-state.js +2 -2
  249. package/dist/pipeline/event-dispatcher-delivery.js +1 -1
  250. package/dist/pipeline/event-dispatcher.js +17 -28
  251. package/dist/pipeline/irreversible-operation-gate.js +1 -1
  252. package/dist/pipeline/lifecycle-pipeline.js +9 -9
  253. package/dist/pipeline/member-read-only-transaction.js +1 -3
  254. package/dist/pipeline/msp-rebuild.js +1 -1
  255. package/dist/pipeline/projection-rebuild.js +13 -12
  256. package/dist/pipeline/system-hooks.js +39 -41
  257. package/dist/pipeline/system-identity-switch.js +1 -3
  258. package/dist/pipeline/tenant-timezone-cache.js +1 -1
  259. package/dist/random/generate.d.ts +29 -29
  260. package/dist/random/generate.js +30 -30
  261. package/dist/random/words.js +20 -20
  262. package/dist/rate-limit/middleware.js +9 -5
  263. package/dist/redis/client.js +2 -2
  264. package/dist/redis/pubsub-signal.js +14 -17
  265. package/dist/schema-cli.js +116 -22
  266. package/dist/search/index.js +4 -4
  267. package/dist/search/purge-subject.js +4 -5
  268. package/dist/search/reindex-entity.js +6 -9
  269. package/dist/time/boot-tz-warning.d.ts +2 -2
  270. package/dist/time/boot-tz-warning.js +8 -8
  271. package/dist/time/iana.js +10 -10
  272. package/dist/time/index.js +9 -9
  273. package/dist/time/legacy-date.d.ts +2 -2
  274. package/dist/time/legacy-date.js +6 -8
  275. package/dist/time/polyfill.js +1 -1
  276. package/dist/time/tz-context.d.ts +1 -1
  277. package/dist/time/tz-context.js +1 -1
  278. package/dist/ui-types/app-schema.js +7 -7
  279. package/dist/ui-types/list-row-meta.js +4 -4
  280. package/dist/upgrade-cli.js +10 -9
  281. package/dist/utils/compare.js +1 -1
  282. package/package.json +4 -4
  283. package/src/changes.json +48 -0
  284. package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
  285. package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
  286. package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
  287. package/src/scripts/codemod/pii-personal-migration.ts +17 -8
  288. package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
@@ -1,3 +1,3 @@
1
- // Public API of the pattern-library — Designer (C5/C6), AI-Builder (L2),
2
- // MCP-Server (L9) consume from here.
1
+ // Public API of the pattern-library — Designer, AI-Builder,
2
+ // MCP-Server consume from here.
3
3
  export { getPatternSchema, groupByCategory, PATTERN_LIBRARY } from "./library.js";
@@ -1,6 +1,6 @@
1
1
  // Pattern-Library — concrete FormSchema entries for every FeaturePattern
2
- // kind. Centralised here so the Designer (C5/C6), the AI-Builder (L2),
3
- // and the MCP-Server (L9) share one source-of-truth for "how does this
2
+ // kind. Centralised here so the Designer, the AI-Builder,
3
+ // and the MCP-Server share one source-of-truth for "how does this
4
4
  // pattern look as a form?".
5
5
  //
6
6
  // **Updating contract:** when a new pattern-kind gets a parser/renderer
@@ -1,7 +1,7 @@
1
1
  // Forms-Schema metadata — describes how a FeaturePattern is rendered as
2
2
  // a Designer form, fed to an LLM as a JSON-Schema, or surfaced in the
3
3
  // MCP-Server tool list. One schema, three consumers — that's the whole
4
- // point of factoring this out of the hand-rolled C5 skeleton.
4
+ // point of factoring this out of the hand-rolled skeleton.
5
5
  //
6
6
  // **NOT a DSL.** The Source-of-Truth for a feature stays
7
7
  // `defineFeature.ts` — the AST-visitor (feature-ast/parse.ts) reads it
@@ -10,7 +10,7 @@
10
10
  // already-parsed FeaturePattern in a form. No alternative parser, no
11
11
  // alternative syntax, no second canonical representation.
12
12
  //
13
- // **Vokabular** (12 input-types):
13
+ // **Vocabulary** (12 input-types):
14
14
  //
15
15
  // text — single-line string (name / label / id)
16
16
  // textarea — multi-line string (description / SQL / readme)
@@ -31,6 +31,6 @@
31
31
  // FeaturePattern object using dot-separated keys (`definition.fields`,
32
32
  // `access.roles`, `templates.email`). Numeric indices use the bracket
33
33
  // form (`columns.0`). The Designer/LLM uses paths for read & write —
34
- // the patcher (C2) consumes pattern-level updates only, so paths are
34
+ // the patcher consumes pattern-level updates only, so paths are
35
35
  // purely a UI concern at this stage.
36
36
  export {};
@@ -8,7 +8,7 @@
8
8
  // the resolver-side PipelineCtx (run-pipeline.ts). Resolvers that need
9
9
  // prior step results destructure them from the resolver's ctx.
10
10
  //
11
- // Renamed from `pipeline()` (Followup #1) — it shared its name with the
11
+ // Renamed from `pipeline()` — it shared its name with the
12
12
  // internal `packages/framework/src/pipeline/` directory (dispatcher,
13
13
  // lifecycle, outbox-poller), which made repo-wide grep for `pipeline`
14
14
  // return mixed results.
@@ -76,7 +76,7 @@ export function stepsPipeline(closure) {
76
76
  // awaits map wired into the closure. The cast below is the one place a
77
77
  // workflow's raw declared event-type strings become AwaitedEventType — safe
78
78
  // because pipelineDef.awaits only ever holds what defineWorkflow's `awaits`
79
- // carried in (D4, workflow-resume-loop.md), never an unchecked external
79
+ // carried in (workflow-resume-loop.md), never an unchecked external
80
80
  // value.
81
81
  export function buildPipelineSteps(pipelineDef, event) {
82
82
  const steps = pipelineDef.build({
@@ -1,4 +1,4 @@
1
- // Per-role projection of a fully-built AppSchema (fw#3314). GET /api/schema
1
+ // Per-role projection of a fully-built AppSchema. GET /api/schema
2
2
  // used to ship the same full schema — screens, navs, workspaces, content
3
3
  // collections — to every authenticated user regardless of role, relying on
4
4
  // client-side UI checks (isUiAccessGranted) alone to hide what a role
@@ -1,27 +1,27 @@
1
1
  import { updateMany } from "../db/query.js";
2
- // Typed-Apply-Helper für r.projection.apply: erlaubt per-event-type
3
- // typed event.payload-Access ohne SingleStreamApplyFn-Generic durch die
4
- // ganze ProjectionDefinition propagieren zu müssen.
2
+ // Typed apply helper for r.projection.apply: enables typed event.payload
3
+ // access per event type without propagating a SingleStreamApplyFn generic
4
+ // through the entire ProjectionDefinition.
5
5
  //
6
- // Der Helper ist ein purer Type-Vehikel — zur Laufzeit identitäts-fn:
6
+ // The helper is purely a type vehicle — an identity function at runtime:
7
7
  //
8
8
  // apply: {
9
9
  // "user.created": defineApply<UserCreatedPayload>(async (event, tx, table) => {
10
- // // event.payload ist UserCreatedPayload, nicht Record<string, unknown>.
10
+ // // event.payload is UserCreatedPayload, not Record<string, unknown>.
11
11
  // // Write through the passed `table` (ES-blessed inside apply), not a
12
12
  // // closed-over branded constant.
13
13
  // await insertOne(tx, table, { id: event.aggregateId, ...event.payload });
14
14
  // }),
15
15
  // }
16
16
  //
17
- // Default-Generic = Record<string, unknown> behält rückwärtskompatibles
18
- // Verhalten für Apply-Handler die ohne Type-Argument geschrieben sind. Der
19
- // `table`-Param ist optional zu nutzen — 2-arg-Applies bleiben gültig.
17
+ // The default generic, Record<string, unknown>, preserves backward-compatible
18
+ // behavior for apply handlers written without a type argument. The `table`
19
+ // parameter is optional to use, so 2-argument applies remain valid.
20
20
  export function defineApply(fn) {
21
21
  // @cast-boundary engine-bridge — typed user-fn → erased internal storage
22
22
  return fn;
23
23
  }
24
- // Pendant für r.multiStreamProjection.apply — bekommt zusätzlich ctx.
24
+ // Counterpart for r.multiStreamProjection.apply — also receives ctx.
25
25
  export function defineMspApply(fn) {
26
26
  // @cast-boundary engine-bridge — typed user-fn → erased internal storage
27
27
  return fn;
@@ -32,7 +32,7 @@ function validateSegment(value, label, context) {
32
32
  }
33
33
  // registry-ingest.ts also qualifies handlers/projections with these two —
34
34
  // not in the public QnTypes enum, but reachable as a type segment the same
35
- // way (#1991).
35
+ // way.
36
36
  const NON_ENUM_QN_TYPES = ["stream", "projection", "workspace"];
37
37
  // Every segment word that a fully-qualified QN can carry as its type.
38
38
  const RESERVED_QN_SEGMENTS = new Set([
@@ -44,7 +44,7 @@ const RESERVED_QN_SEGMENTS = new Set([
44
44
  // the trailing part. Both signals must hold together: the scope alone
45
45
  // repeating (e.g. entity "user:create" on feature "user") or a reserved word
46
46
  // alone appearing (e.g. "compliance:query:for-tenant") are both common,
47
- // legitimate sub-structure on their own (#1991).
47
+ // legitimate sub-structure on their own.
48
48
  function isDoubleQualified(scope, segments) {
49
49
  const [first, second] = segments;
50
50
  return first === scope && second !== undefined && RESERVED_QN_SEGMENTS.has(second);
@@ -85,7 +85,7 @@ export function buildRegistryFacade(state) {
85
85
  getEntityPostQueryHooks(entityName, effectiveFeatures) {
86
86
  return filterOwned(state.entityPostQueryHooks.get(entityName), effectiveFeatures);
87
87
  },
88
- // F3 — Search-Payload-Extension contributors for an entity. Used by
88
+ // Search-Payload-Extension contributors for an entity. Used by
89
89
  // `buildSearchDocument` in system-hooks.ts to enrich the indexed payload.
90
90
  // `effectiveFeatures` filters out contributors owned by feature-toggle-
91
91
  // disabled features (parallel to getEntityPostQueryHooks etc.).
@@ -23,9 +23,9 @@ export function populateFeatureCore(state, feature) {
23
23
  `collides with r.storeTable("${physical}") (feature "${clash.featureName}"). ` +
24
24
  `Pick a different tableName — both would emit CREATE TABLE "${physical}".`);
25
25
  }
26
- // Entity-vs-entity ist genauso fatal: zwei Entities mit explizitem,
27
- // identischem tableName überschrieben sich hier vorher still —
28
- // doppeltes CREATE TABLE bzw. eine Projektion frisst die andere.
26
+ // Entity-vs-entity collisions are equally fatal: two entities with an explicit,
27
+ // identical tableName previously overwrote each other silently here—
28
+ // causing duplicate CREATE TABLE statements or one projection to replace the other.
29
29
  if (clash?.kind === "entity") {
30
30
  throw new Error(`Entity "${name}" (feature "${feature.name}") has physical table "${physical}" which ` +
31
31
  `collides with entity "${clash.owner}" (feature "${clash.featureName}"). ` +
@@ -232,7 +232,7 @@ export function populateTranslations(state, feature) {
232
232
  // Translations prefixed with featureName: (i18next namespace convention).
233
233
  // Keys that already carry the feature's own namespace prefix (e.g. a nav
234
234
  // label referencing "cap-counter:nav.cap-list" verbatim) must NOT be
235
- // re-prefixed, else server-side t() can never resolve them (#1105). Both
235
+ // re-prefixed, else server-side t() can never resolve them. Both
236
236
  // bare and qualified spellings for the same key are accepted; if a
237
237
  // feature defines both, whichever is declared last wins (Object.entries
238
238
  // = insertion order, not "qualified always wins").
@@ -263,7 +263,7 @@ export function populateHooks(state, feature) {
263
263
  mergeHookList(state.entityPreDeleteHooks, feature.entityHooks?.preDelete);
264
264
  mergeHookList(state.entityPostDeleteHooks, feature.entityHooks?.postDelete);
265
265
  mergeHookList(state.entityPostQueryHooks, feature.entityHooks?.postQuery);
266
- // F3 search-payload-extensions: per-entity contributors merged additively
266
+ // Search-payload-extensions: per-entity contributors merged additively
267
267
  for (const [entityName, contributors] of Object.entries(feature.searchPayloadExtensions ?? {})) {
268
268
  const existing = state.searchPayloadExtensions.get(entityName) ?? [];
269
269
  for (const c of contributors)
@@ -460,10 +460,10 @@ export function populateScreensNavWorkspaces(state, feature) {
460
460
  const stored = { ...screenDef, id: qualified };
461
461
  state.screenMap.set(qualified, stored);
462
462
  state.screenFeatureMap.set(qualified, feature.name);
463
- // entity-Index nur für Screens die direkt an einer Entity hängen.
464
- // entityList/entityEdit haben `entity`; custom + actionForm haben
465
- // keinen entity-Bezug (custom ist opaque, actionForm hat inline
466
- // fields ohne Entity-Reference).
463
+ // Index only screens that are directly attached to an entity.
464
+ // entityList/entityEdit have `entity`; custom + actionForm do not reference
465
+ // an entity (custom is opaque, and actionForm has inline fields without
466
+ // an entity reference).
467
467
  if (stored.type === "entityList" || stored.type === "entityEdit") {
468
468
  const existing = state.screensByEntity.get(stored.entity) ?? [];
469
469
  existing.push(stored);
@@ -515,9 +515,9 @@ export function populateScreensNavWorkspaces(state, feature) {
515
515
  }
516
516
  }
517
517
  }
518
- // Tree-Actions slot — at-most-one per feature (only-once-guard im
519
- // registrar). Erased Map für Runtime-Lookup; compile-time-typed
520
- // Surface läuft über FeatureDefinition.exports (TreeActionsHandle).
518
+ // Tree-Actions slot—at most one per feature (the only-once guard runs in
519
+ // the registrar). The Map is erased for runtime lookup; the compile-time-typed
520
+ // surface is exposed through FeatureDefinition.exports (TreeActionsHandle).
521
521
  if (feature.treeActions !== undefined) {
522
522
  state.treeActionsMap.set(feature.name, feature.treeActions);
523
523
  }
@@ -5,25 +5,25 @@ import { asEntityTableMeta } from "../db/query.js";
5
5
  import { buildEntityTable } from "../db/table-builder.js";
6
6
  import { qualifyEntityName } from "./qualified-name.js";
7
7
  const IMPLICIT_PROJECTION_SUFFIX = "-entity";
8
- // Pro r.entity-Registration eine ImplicitProjection mit auto-generierten
9
- // apply-Handlern für die 4 Auto-Verben. Live-Pfad geht durch
10
- // EventStoreExecutor und schreibt direkt in die Tabelle; rebuildProjection
11
- // nutzt diese Definition um aus Events zu replayen. Beide rufen dieselbe
12
- // applyEntityEvent-Funktion → Live==Rebuild by-construction (verstärkt
13
- // durch implicit-projection-equivalence.integration.ts).
8
+ // Each `r.entity` registration gets an implicit projection with generated
9
+ // apply handlers for the four automatic verbs. The live path writes directly
10
+ // to the table through EventStoreExecutor, while rebuildProjection replays
11
+ // events through this definition. Both call applyEntityEvent, ensuring live
12
+ // and rebuild use the same logic (also covered by
13
+ // implicit-projection-equivalence.integration.ts).
14
14
  export function buildImplicitProjection(featureName, entityName, entity, qualify, backingTable, extensions = []) {
15
15
  const name = qualify(featureName, "projection", `${entityName}${IMPLICIT_PROJECTION_SUFFIX}`);
16
16
  // Backing table (r.entity({ name, ...def, backingTable })) is the one physical table
17
17
  // object shared by executor-writes, rebuild-replay, test-push and
18
- // collectTableMetas — restoring the #255 invariant (test-push == generate).
18
+ // collectTableMetas — restoring the invariant (test-push == generate).
19
19
  // Validated as a superset of the field-derived columns so a field/table
20
20
  // disagreement fails at boot, not as a silent thin-vs-rich row.
21
21
  const drizzleTable = backingTable !== undefined
22
22
  ? resolveBackingTable(entityName, entity, backingTable)
23
23
  : buildEntityTable(entityName, entity);
24
24
  markExecutorManagedTable(drizzleTable);
25
- // applyEntityEvent gibt ApplyResult zurück; SingleStreamApplyFn erwartet
26
- // Promise<void>. Im rebuild-Pfad ist die Row irrelevant — wir discarden.
25
+ // applyEntityEvent returns ApplyResult, but SingleStreamApplyFn expects
26
+ // Promise<void>. The row is irrelevant during rebuild, so we discard it.
27
27
  const handler = async (event, tx) => {
28
28
  await applyEntityEvent(event, drizzleTable, entity, tx);
29
29
  };
@@ -35,10 +35,9 @@ export function buildImplicitProjection(featureName, entityName, entity, qualify
35
35
  // Registered for every entity so the erasure replays on rebuild.
36
36
  [`${entityName}.forgotten`]: handler,
37
37
  };
38
- // Restore-Verb existiert nur für softDelete-Entities. Hard-Delete-
39
- // Entities sollten keine restored-Events produzieren — würden sie es
40
- // doch, würde applyEntityEvent intern als no-op laufen, aber wir
41
- // registrieren den Handler gar nicht erst.
38
+ // The restore verb exists only for softDelete entities. Hard-delete entities
39
+ // should not produce restored events; if they did, applyEntityEvent would
40
+ // treat them as a no-op, so the handler is not registered.
42
41
  if (entity.softDelete) {
43
42
  apply[`${entityName}.restored`] = handler;
44
43
  }
@@ -230,8 +229,8 @@ function ownerEnabled(owner, effectiveFeatures) {
230
229
  // every hook entry (set by defineFeature), so there's no parallel
231
230
  // bookkeeping — just append.
232
231
  export function mergeHookList(map, source) {
233
- // skip: optionaler entityHook-slot — features ohne postSave/preDelete/
234
- // postDelete/postQuery lassen das slot undefined.
232
+ // skip: the optional entity-hook slot may be undefined when a feature has no
233
+ // postSave, preDelete, postDelete, or postQuery hooks.
235
234
  if (!source)
236
235
  return;
237
236
  for (const [name, fns] of Object.entries(source)) {
@@ -244,10 +243,9 @@ export function mergeHookList(map, source) {
244
243
  // Hook keys are handler QNs — hooks don't get their own QN, they're keyed by the handler they target.
245
244
  // The hookQnType indicates whether the targeted handler is a write or query handler.
246
245
  export function mergeHookListQualified(map, source, featureName, hookQnType) {
247
- // skip: optionaler hook-slot — defineFeature materialisiert zwar alle
248
- // Slots, aber hand-gebaute Definitionen an System-Grenzen (Fixtures,
249
- // Partial-Boots, s. registry.test.ts) lassen sie weg. Leeres Record
250
- // statt Object.entries(undefined)-Crash.
246
+ // skip: the optional hook slot may be omitted by hand-built definitions at
247
+ // system boundaries (fixtures and partial boots), even though defineFeature
248
+ // materializes every slot. This avoids Object.entries(undefined) crashing.
251
249
  if (!source)
252
250
  return;
253
251
  for (const [name, fns] of Object.entries(source)) {
@@ -144,7 +144,7 @@ export function applyExtensionUsages(state) {
144
144
  }
145
145
  }
146
146
  }
147
- // fw#2660: labelField defaults to "id" (a UUID column) elsewhere, but a
147
+ // labelField defaults to "id" (a UUID column) elsewhere, but a
148
148
  // searchable reference ILIKEs the label column — an implicit or explicit
149
149
  // "id" default would 500 on every search request. Fail at boot.
150
150
  function buildSearchableReferenceField(entityName, fieldName, field, entityMap) {
@@ -176,7 +176,7 @@ function buildSearchableReferenceField(entityName, fieldName, field, entityMap)
176
176
  labelField: field.labelField,
177
177
  };
178
178
  }
179
- // fw#2741: ORDER BY runs against the referenced row's label column, so the
179
+ // ORDER BY runs against the referenced row's label column, so the
180
180
  // same explicit-labelField requirement as searchable applies — ordering by the
181
181
  // "id" default would be a UUID order that looks deliberate to the user.
182
182
  function buildSortableReferenceField(entityName, fieldName, field) {
@@ -230,12 +230,12 @@ export function buildSearchableSortableCaches(state) {
230
230
  }
231
231
  // Implicit-Projection pro r.entity — see buildImplicitProjection doc above.
232
232
  export function buildImplicitProjections(state, features) {
233
- // Implicit-Projection pro r.entity. Macht die Entity-Tabelle rebaubar
234
- // ohne dass Apps eine explizite r.projection schreiben müssen.
235
- // Naming-Convention: `<feature>:projection:<entityName>-entity` — der
236
- // "-entity"-Suffix unterscheidet implicit von explicit-Projections und
237
- // vermeidet Kollisionen wenn jemand z.B. eine Cross-Aggregate-Projection
238
- // mit Entity-Name registriert.
233
+ // Implicit projection for r.entity. Makes the entity table rebuildable
234
+ // without requiring apps to declare an explicit r.projection.
235
+ // Naming convention: `<feature>:projection:<entityName>-entity` — the
236
+ // "-entity" suffix distinguishes implicit from explicit projections and
237
+ // prevents collisions when someone registers, for example, a
238
+ // cross-aggregate projection using the entity name.
239
239
  for (const feature of features) {
240
240
  // extendEntityProjection targets must exist as r.entity in the SAME
241
241
  // feature — a typo'd entity name would otherwise vanish silently and the
@@ -651,7 +651,7 @@ export function validateLifecycleHookTargets(state) {
651
651
  }
652
652
  export function validateEntityHookTargets(state, features) {
653
653
  // Same logic for entity-keyed hooks — targets must reference existing entities.
654
- // Memory `feedback_dead_hook_needs_second_consumer`: a typo silently registers
654
+ // A hook whose target has a typo silently registers
655
655
  // and never fires. Validates all four entity-hook types (postSave/preDelete/
656
656
  // postDelete/postQuery) — net cleanup of an existing antipattern, not a
657
657
  // postQuery-special.
@@ -810,7 +810,7 @@ export function computeHasRateLimitedHandler(state) {
810
810
  // Lua-script registration on Redis) when nobody opted in. Deliberately
811
811
  // stays limited to explicit, non-disabled `rateLimit` declarations —
812
812
  // `{ disabled: true }` must not auto-wire a resolver for an app that only
813
- // opted OUT, and fw#2861's systemScope default only fires once a resolver
813
+ // opted OUT, and the systemScope default only fires once a resolver
814
814
  // already exists, so it must not force wantsL3 either.
815
815
  state.hasRateLimitedHandlerCached = (() => {
816
816
  for (const h of state.writeHandlerMap.values()) {
@@ -827,12 +827,12 @@ export function computeHasRateLimitedHandler(state) {
827
827
  })();
828
828
  }
829
829
  export function publishEventPiiCatalog(state) {
830
- // Publish the event-PII catalog (#799): append() — the single write funnel
830
+ // Publish the event-PII catalog: append() — the single write funnel
831
831
  // into kumiko_events — encrypts catalogued payload fields regardless of
832
832
  // which path produced the event (ctx.appendEvent, MSP-apply, low-level
833
833
  // append in delivery/jobs loggers).
834
834
  //
835
- // Defense-in-depth (fw#2558): defineEvent is the only producer of
835
+ // Defense-in-depth: defineEvent is the only producer of
836
836
  // EventDef.piiFields and already fails closed without a stance, but this
837
837
  // boot gate re-checks every registered event — a future producer that
838
838
  // forgets to run through defineEvent would otherwise leave a hole where
@@ -1,6 +1,6 @@
1
1
  // runIn lane-routing helpers. Tiny module on purpose: both buildServer
2
- // (MSP-consumer filter) and the boot-validator (Welle 2.6.c coverage
3
- // check) need the same resolution rule, and duplicating it in two places
2
+ // (MSP-consumer filter) and the boot-validator (coverage check)
3
+ // need the same resolution rule, and duplicating it in two places
4
4
  // invites drift — when the default changes, one site gets updated and
5
5
  // the other silently doesn't.
6
6
  //
@@ -21,6 +21,8 @@
21
21
  // errors propagate to the dispatcher's catch which maps them to
22
22
  // WriteFailure / HTTP. "return" / "skip" / fallback strategies land in
23
23
  // later slices together with their own integration tests.
24
+ import { InternalError } from "../errors/index.js";
25
+ import { describeShape, isWriteResultShape } from "../pipeline/dispatcher-utils.js";
24
26
  import { getStep } from "./define-step.js";
25
27
  import { buildPipelineSteps } from "./pipeline.js";
26
28
  import { SUSPEND_SENTINEL } from "./steps/_step-dispatch-constants.js";
@@ -112,6 +114,11 @@ export async function runStepList(steps, event, handlerCtx, stepsAcc, scopeAcc,
112
114
  }
113
115
  const key = stepDef.resultKey?.(instance.args);
114
116
  if (key === RETURN_RESULT_KEY) {
117
+ if (!isWriteResultShape(value)) {
118
+ throw new InternalError({
119
+ message: `Step "${instance.kind}" at index ${i} must return a WriteResult, got ${describeShape(value)}`,
120
+ });
121
+ }
115
122
  return { kind: "return", result: value };
116
123
  }
117
124
  if (key !== undefined) {
@@ -16,10 +16,11 @@ export function isRepresentableAtScale(n, scale) {
16
16
  const tolerance = Math.abs(scaled) * 8 * Number.EPSILON + Number.EPSILON;
17
17
  return Math.abs(scaled - Math.round(scaled)) <= tolerance;
18
18
  }
19
- // Lexikografischer ISO-Vergleich — exakt für `yyyy-mm-dd` (date) und korrekt
20
- // für ISO-Datetime in konsistenter Repräsentation (gleiche Offset-/Präzisions-
21
- // Form). Bewusst ohne Date-API (no-date-api-Guard); die Tag-genaue Grenze
22
- // reicht für min/max-Use-Cases (z.B. Geburtsdatum nicht in der Zukunft).
19
+ // Lexicographic ISO comparison — exact for `yyyy-mm-dd` (date) and correct
20
+ // for ISO datetimes in a consistent representation (same offset/precision
21
+ // form). Deliberately avoids the Date API (no-date-api-Guard); day-level
22
+ // comparison is sufficient for min/max use cases (e.g. a birth date not in
23
+ // the future).
23
24
  function withDateBounds(schema, min, max) {
24
25
  if (min === undefined && max === undefined)
25
26
  return schema;
@@ -127,8 +128,9 @@ export function fieldToZod(field, currencies, opts = {}) {
127
128
  return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
128
129
  }
129
130
  case "longText": {
130
- // longText hat keine `format`-Variante (per type-design). Nur
131
- // optional maxLength + required, sonst ein offener z.string().
131
+ // longText has no `format` variant (by type design). Only optional
132
+ // maxLength and required are supported; otherwise it is an unrestricted
133
+ // z.string().
132
134
  let schema = z.string();
133
135
  if (field.maxLength)
134
136
  schema = schema.max(field.maxLength);
@@ -148,13 +150,13 @@ export function fieldToZod(field, currencies, opts = {}) {
148
150
  if (field.default !== undefined) {
149
151
  // Untouched <select> sends "" too; with a default that maps to the
150
152
  // default (same semantics as undefined) instead of the invalid-value
151
- // rejection from #1702. A field with a default is never "unset" —
153
+ // rejection. A field with a default is never "unset" —
152
154
  // true on both insert AND update, so this branch (and its "" → default
153
155
  // mapping) fires regardless of applyDefaults; only the `.default(...)`
154
156
  // schema-level fallback for OMITTED input is update-gated below.
155
157
  // `null` maps the same way: the no-default branch below normalizes
156
158
  // an untouched select to null, and a client that reuses that value
157
- // against a since-defaulted field must not get rejected either.
159
+ // against a defaulted field must not get rejected either.
158
160
  const mapped = z.preprocess((value) => (value === "" || value === null ? field.default : value), enumSchema);
159
161
  return applyDefaults ? mapped.default(field.default) : mapped;
160
162
  }
@@ -171,9 +173,9 @@ export function fieldToZod(field, currencies, opts = {}) {
171
173
  const [first, ...rest] = field.options;
172
174
  if (!first)
173
175
  return z.array(z.string());
174
- // `required: true` heißt non-empty — Analogie zu `text`-Field.
175
- // Leeres Array wird rejected; das globale `.optional()`-Wrapping
176
- // in buildInsertSchema kümmert sich um „darf fehlen".
176
+ // `required: true` means non-empty, as with a `text` field.
177
+ // An empty array is rejected; the global `.optional()` wrapping
178
+ // in buildInsertSchema determines whether the field may be omitted.
177
179
  let schema = z.array(z.enum([first, ...rest]));
178
180
  if (field.required)
179
181
  schema = schema.min(1);
@@ -209,10 +211,10 @@ export function fieldToZod(field, currencies, opts = {}) {
209
211
  return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
210
212
  }
211
213
  case "bigInt": {
212
- // JS-`number`-Round-trip via mode:"number"; sicher bis 2^53.
213
- // safe-integer-Cap ist explizit damit ein Caller, der einen
214
- // Float reinwirft (z.B. parseFloat-Bug), beim Insert sofort
215
- // failed statt silent-Truncation zu kassieren.
214
+ // JS-`number` round-trip via mode:"number"; exact up to 2^53.
215
+ // The safe-integer cap ensures that a caller passing a float (for example,
216
+ // due to a parseFloat bug) fails immediately at insert instead of being
217
+ // silently truncated.
216
218
  const schema = z.number().int().safe();
217
219
  return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
218
220
  }
@@ -267,8 +269,8 @@ export function fieldToZod(field, currencies, opts = {}) {
267
269
  });
268
270
  }
269
271
  case "jsonb": {
270
- // Free-form jsonb — keys sind tenant-/runtime-defined. Validation
271
- // passthrough: any plain object passt durch.
272
+ // Free-form jsonb — keys are tenant-/runtime-defined. Validation
273
+ // passes through any plain object.
272
274
  return z.record(z.string(), z.unknown());
273
275
  }
274
276
  case "date": {
@@ -276,26 +278,26 @@ export function fieldToZod(field, currencies, opts = {}) {
276
278
  return withDateBounds(schema, field.min, field.max);
277
279
  }
278
280
  case "timestamp": {
279
- // Wenn locatedBy gesetzt: Wall-Clock OHNE Offset (ISO-Datetime ohne `Z`).
280
- // Sonst: ISO-UTC-Datetime (mit `Z`). Beide werden über z.iso.datetime
281
- // gegen das ISO-8601-Schema validiert; die Präzision (mit/ohne Offset)
282
- // hängt von locatedBy ab.
281
+ // When locatedBy is set: wall-clock time without an offset (ISO datetime
282
+ // without `Z`). Otherwise: ISO UTC datetime (with `Z`). Both are validated
283
+ // against the ISO-8601 schema through z.iso.datetime; precision (with or
284
+ // without an offset) depends on locatedBy.
283
285
  const schema = field.locatedBy !== undefined ? z.iso.datetime({ local: true }) : z.iso.datetime();
284
286
  return withDateBounds(schema, field.min, field.max);
285
287
  }
286
288
  case "tz": {
287
- // IANA-Zonenname, validiert gegen die Runtime-Zonenliste
288
- // (isValidIanaTimeZone). Ein ungültiger Name failt hier am
289
- // Write-Boundary statt erst später in ctx.tz.parse / Temporal.
289
+ // IANA time-zone name, validated against the runtime's zone list
290
+ // (isValidIanaTimeZone). An invalid name fails at the write boundary
291
+ // instead of later in ctx.tz.parse / Temporal.
290
292
  return z.string().refine(isValidIanaTimeZone, { message: "invalid IANA time zone" });
291
293
  }
292
294
  case "locatedTimestamp": {
293
- // Combined Wall-Clock+TZ Object. Beim Write akzeptieren wir entweder
294
- // { at, tz } (typisch UI-Form, utc wird berechnet) oder { utc, tz }
295
- // (typisch Server-zu-Server, at wird berechnet). Beim Read liefert
296
- // der Read-Wrapper alle drei Felder (siehe Phase D in MIGRATION.md).
295
+ // Combined wall-clock and time-zone object. On write, either
296
+ // { at, tz } (typical for a UI form, with utc calculated) or { utc, tz }
297
+ // (typical for server-to-server communication, with at calculated) is
298
+ // accepted. On read, the read wrapper provides all three fields.
297
299
  //
298
- // Hier nur die Schema-Garantie: mindestens tz + (at ODER utc).
300
+ // The schema guarantees only tz plus at least one of at or utc.
299
301
  const at = z.iso.datetime({ local: true });
300
302
  const tz = z.string().refine(isValidIanaTimeZone, { message: "invalid IANA time zone" });
301
303
  const utc = z.iso.datetime();
@@ -322,11 +324,12 @@ export function fieldToZod(field, currencies, opts = {}) {
322
324
  return field.maxCount === undefined ? ids : ids.max(field.maxCount);
323
325
  }
324
326
  case "reference":
325
- // Tier 2.7e-3: Validiert UUID-shape. Existenz-Check der Reference
326
- // (Row im referenced Table existiert + Tenant-Scope) ist Server-
327
- // side-Verantwortung im Handler / Foreign-Key-Constraint, nicht
328
- // im Schema-Validator (würde sonst Round-trip zur DB beim Parse).
329
- // Multi-Mode (Tier 2.7e-Multi): Array von UUIDs.
327
+ // Validates the UUID shape. Checking that the reference exists
328
+ // (the row exists in the referenced table and belongs to the tenant) is
329
+ // the handler / foreign-key constraint's responsibility, not the schema
330
+ // validator's; checking it here would require a database round trip during
331
+ // parsing.
332
+ // Multi-mode accepts an array of UUIDs.
330
333
  return field.multiple === true ? z.array(z.uuid()) : z.uuid();
331
334
  default:
332
335
  assertUnreachable(field, "field type");
@@ -342,8 +345,7 @@ export function fieldToZod(field, currencies, opts = {}) {
342
345
  // fails the check even if the raw minor-unit amounts happen to match.
343
346
  //
344
347
  // Known limitation: compares against rounded `derived` cells, i.e.
345
- // "sum-of-rounded" not "round-of-sum" (kumiko-framework#1866). Follow-up
346
- // for a computed, read-only sibling total: kumiko-framework#1873.
348
+ // "sum-of-rounded" not "round-of-sum".
347
349
  // kumiko-lint-ignore complexity-budget currency-equality check on sibling money payloads
348
350
  function applyTotalsMatchRefinements(entity, schema) {
349
351
  let result = schema;
@@ -360,7 +362,7 @@ function applyTotalsMatchRefinements(entity, schema) {
360
362
  continue;
361
363
  // One side of a totalsMatch pair without the other → reject. Update
362
364
  // payloads only carry `changes`, so omitting the sibling would otherwise
363
- // silently leave sum ≠ total (fw#1841).
365
+ // silently leave sum ≠ total.
364
366
  if (rawRows === undefined || siblingRaw === undefined) {
365
367
  ctx.addIssue({
366
368
  code: "custom",
@@ -478,7 +480,7 @@ export function buildUpdateSchema(entity, currencies = [...DEFAULT_CURRENCIES],
478
480
  // sends only `{ title }` means "only change title"; zod defaults would
479
481
  // silently inject default values for every omitted field and clobber
480
482
  // existing data via the event-store-executor's `changes` payload.
481
- // The field is passed through un-stripped (unlike before fw#1703) so
483
+ // The field is passed through un-stripped so
482
484
  // fieldToZod still knows the default for its "" → default mapping
483
485
  // (e.g. select) — applyDefaults: false only suppresses the schema-level
484
486
  // `.default(...)` fallback for a genuinely omitted key.
@@ -1,18 +1,16 @@
1
- // Screen-Filter (Tier 2.7c) — Op-vs-Field-Type-Compatibility.
1
+ // Screen-Filter — operator-to-field-type compatibility.
2
2
  //
3
- // Wer darf was filtern?
4
- // - text/select/multiSelect: nur equality-Ops (eq, ne, in). lt/gt
5
- // auf Strings ist semantisch fast immer Tippfehler (lexicographic
6
- // compare nutzt der Author selten bewusst); Author kann den
7
- // Filter dann einfach nicht setzen.
8
- // - boolean: eq, ne (in/lt/gt sinnlos für 2-Werte-Type).
9
- // - number/money/decimal/date/timestamp/locatedTimestamp: alle 7
10
- // Ops — die Felder sind natürlich vergleichbar.
3
+ // Allowed operators depend on the field type:
4
+ // - text/select/multiSelect: equality operators only (eq, ne, in). lt/gt
5
+ // on strings are usually accidental; authors can simply omit the filter.
6
+ // - boolean: eq, ne (in/lt/gt are meaningless for a two-value type).
7
+ // - number/money/decimal/date/timestamp/locatedTimestamp: all 7 operators,
8
+ // because these fields are naturally comparable.
11
9
  //
12
- // Boot-Validator nutzt diese Map um Author-Fehler früh zu fangen
13
- // ("filter mit op:lt auf einem text-Feld" → Boot-Fail). Erweitert sich
14
- // transparent: neuer Field-Type → hier eintragen + sortable/filterable-
15
- // Flag im Type-Def, sonst lehnt der Validator das Field generell ab.
10
+ // The boot validator uses this map to catch authoring errors early
11
+ // ("filter with op:lt on a text field" → boot failure). When adding a field
12
+ // type, add it here and set its sortable/filterable flag in the type definition;
13
+ // otherwise the validator rejects the field entirely.
16
14
  const EQUALITY_ONLY = ["eq", "ne", "in"];
17
15
  const COMPARABLE = [
18
16
  "eq",
@@ -39,17 +37,16 @@ export function getAllowedFilterOps(field) {
39
37
  case "timestamp":
40
38
  case "locatedTimestamp":
41
39
  return COMPARABLE;
42
- // tz/embedded/file/image/files/images: nicht filterbar — Author
43
- // kann das Feld nicht als filterable: true markieren (Boot-
44
- // Validator weist `filterable: true` auf den Types ohnehin schon
45
- // ab, weil das Flag dort gar nicht im Type-Def steht).
40
+ // tz/embedded/file/image/files/images: not filterable — authors cannot
41
+ // mark the field as filterable: true because the boot validator already
42
+ // rejects `filterable: true` for these types; the flag is not present in
43
+ // their type definitions.
46
44
  default:
47
45
  return [];
48
46
  }
49
47
  }
50
- // Author hat das Feld als filterable markiert? Tz/Embedded/File-Types
51
- // haben das Flag gar nicht im Type-Def, also fangen wir das per
52
- // `"filterable" in field`-Narrow ab.
48
+ // If an author marked the field as filterable, catch types without that
49
+ // flag in their type definition with the `"filterable" in field` narrowing.
53
50
  export function isFieldFilterable(field) {
54
51
  return "filterable" in field && field.filterable === true;
55
52
  }