@cosmicdrift/kumiko-framework 0.385.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 (298) 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/request-context.d.ts +2 -0
  17. package/dist/api/request-context.js +16 -7
  18. package/dist/api/routes.js +2 -2
  19. package/dist/api/server.js +34 -33
  20. package/dist/api/sse-broker.js +1 -1
  21. package/dist/api/sse-route.d.ts +13 -13
  22. package/dist/api/sse-route.js +15 -15
  23. package/dist/auth/index.js +4 -4
  24. package/dist/auth/roles.d.ts +4 -4
  25. package/dist/auth/roles.js +24 -24
  26. package/dist/compliance/duration-spec.js +18 -20
  27. package/dist/compliance/index.js +2 -3
  28. package/dist/compliance/override-schema.d.ts +4 -4
  29. package/dist/compliance/override-schema.js +19 -22
  30. package/dist/compliance/profiles.d.ts +26 -29
  31. package/dist/compliance/profiles.js +62 -69
  32. package/dist/compliance/sub-processors.d.ts +28 -30
  33. package/dist/compliance/sub-processors.js +17 -21
  34. package/dist/consumer-cli.js +4 -4
  35. package/dist/crypto/blind-index.js +18 -17
  36. package/dist/crypto/ciphertext-pattern.js +1 -1
  37. package/dist/crypto/event-pii.js +3 -3
  38. package/dist/crypto/kek-source.d.ts +1 -1
  39. package/dist/crypto/kek-source.js +3 -2
  40. package/dist/crypto/kms-adapter.d.ts +1 -1
  41. package/dist/crypto/kms-adapter.js +4 -6
  42. package/dist/crypto/kms-wiring.d.ts +1 -2
  43. package/dist/crypto/kms-wiring.js +2 -2
  44. package/dist/crypto/pg-kms-adapter.js +2 -6
  45. package/dist/crypto/pii-field-encryption.js +4 -4
  46. package/dist/crypto/subject-resolver.d.ts +1 -1
  47. package/dist/crypto/subject-resolver.js +2 -2
  48. package/dist/db/apply-entity-event.d.ts +6 -6
  49. package/dist/db/apply-entity-event.js +55 -55
  50. package/dist/db/blind-index-cleanup.js +7 -7
  51. package/dist/db/collect-table-metas.js +10 -11
  52. package/dist/db/compound-types.d.ts +1 -1
  53. package/dist/db/compound-types.js +12 -12
  54. package/dist/db/cross-tenant-reads.d.ts +8 -0
  55. package/dist/db/cross-tenant-reads.js +23 -0
  56. package/dist/db/cursor.js +3 -4
  57. package/dist/db/dialect.d.ts +2 -2
  58. package/dist/db/dialect.js +2 -2
  59. package/dist/db/eagerload.d.ts +3 -3
  60. package/dist/db/eagerload.js +21 -12
  61. package/dist/db/entity-table-meta.d.ts +2 -2
  62. package/dist/db/entity-table-meta.js +4 -4
  63. package/dist/db/event-store-executor-context.d.ts +1 -1
  64. package/dist/db/event-store-executor-context.js +13 -13
  65. package/dist/db/event-store-executor-read.js +27 -28
  66. package/dist/db/event-store-executor-write.js +65 -70
  67. package/dist/db/located-timestamp.d.ts +11 -11
  68. package/dist/db/located-timestamp.js +29 -30
  69. package/dist/db/migrate-generator.d.ts +1 -1
  70. package/dist/db/migrate-generator.js +26 -19
  71. package/dist/db/migrate-runner.js +25 -25
  72. package/dist/db/money.d.ts +6 -6
  73. package/dist/db/money.js +29 -27
  74. package/dist/db/queries/backfill-pii.js +5 -5
  75. package/dist/db/queries/ddl.js +1 -1
  76. package/dist/db/queries/es-ops.d.ts +1 -1
  77. package/dist/db/queries/es-ops.js +1 -1
  78. package/dist/db/queries/event-consumer.js +3 -3
  79. package/dist/db/queries/event-store.js +4 -2
  80. package/dist/db/queries/projection-rebuild.js +3 -3
  81. package/dist/db/queries/raw-sql.d.ts +2 -2
  82. package/dist/db/queries/raw-sql.js +2 -2
  83. package/dist/db/queries/shadow-swap.js +19 -19
  84. package/dist/db/queries/test-stack.d.ts +4 -1
  85. package/dist/db/queries/test-stack.js +15 -2
  86. package/dist/db/query.d.ts +1 -1
  87. package/dist/db/query.js +6 -6
  88. package/dist/db/rebuild-marker.js +20 -21
  89. package/dist/db/replay-migration-sql.js +2 -2
  90. package/dist/db/schema-inspection.js +4 -5
  91. package/dist/db/table-builder.d.ts +1 -2
  92. package/dist/db/table-builder.js +82 -82
  93. package/dist/db/tenant-db.d.ts +2 -2
  94. package/dist/db/tenant-db.js +9 -2
  95. package/dist/derivatives/delete-stored-file.js +5 -2
  96. package/dist/derivatives/derivatives-context.js +1 -1
  97. package/dist/engine/boot-validator/access-roles.js +4 -3
  98. package/dist/engine/boot-validator/api-ext.js +14 -14
  99. package/dist/engine/boot-validator/config-deps.js +7 -6
  100. package/dist/engine/boot-validator/detail-screens.js +3 -3
  101. package/dist/engine/boot-validator/entity-handler.js +60 -61
  102. package/dist/engine/boot-validator/gdpr-storage.js +4 -4
  103. package/dist/engine/boot-validator/i18n-keys.js +1 -1
  104. package/dist/engine/boot-validator/index.d.ts +2 -2
  105. package/dist/engine/boot-validator/index.js +20 -20
  106. package/dist/engine/boot-validator/nav.js +5 -5
  107. package/dist/engine/boot-validator/ownership.js +12 -9
  108. package/dist/engine/boot-validator/parent-ref.js +1 -1
  109. package/dist/engine/boot-validator/pii-retention.js +50 -57
  110. package/dist/engine/boot-validator/projection-list-screens.js +7 -7
  111. package/dist/engine/boot-validator/query-refs.js +1 -1
  112. package/dist/engine/boot-validator/screens-actions.js +15 -16
  113. package/dist/engine/boot-validator/screens-forms.js +10 -11
  114. package/dist/engine/boot-validator/screens-lists.js +14 -15
  115. package/dist/engine/boot-validator/screens.js +34 -40
  116. package/dist/engine/boot-validator/security-baseline.js +3 -2
  117. package/dist/engine/boot-validator/transfer-graph.js +2 -2
  118. package/dist/engine/boot-validator/workspaces.js +3 -3
  119. package/dist/engine/bridge-stub.js +3 -4
  120. package/dist/engine/build-app-schema.js +75 -75
  121. package/dist/engine/build-config-feature-schema.js +50 -50
  122. package/dist/engine/build-target.js +20 -23
  123. package/dist/engine/codemod/pipeline-codemod.js +20 -13
  124. package/dist/engine/config-helpers.js +4 -4
  125. package/dist/engine/constants.js +2 -2
  126. package/dist/engine/create-app.js +2 -2
  127. package/dist/engine/define-feature.js +12 -11
  128. package/dist/engine/define-handler.js +12 -11
  129. package/dist/engine/define-step.js +1 -1
  130. package/dist/engine/define-workflow.d.ts +5 -5
  131. package/dist/engine/define-workflow.js +6 -6
  132. package/dist/engine/entity-handlers.js +13 -14
  133. package/dist/engine/event-def.js +2 -2
  134. package/dist/engine/extension-names.d.ts +16 -17
  135. package/dist/engine/extension-names.js +37 -38
  136. package/dist/engine/extensions/escape-hatch-usage.js +1 -1
  137. package/dist/engine/extensions/tenant-data.js +2 -2
  138. package/dist/engine/extensions/user-data.d.ts +23 -24
  139. package/dist/engine/factories.d.ts +14 -13
  140. package/dist/engine/factories.js +38 -37
  141. package/dist/engine/feature-ast/entity-field-types.js +1 -1
  142. package/dist/engine/feature-ast/extractors/handlers.js +1 -1
  143. package/dist/engine/feature-ast/extractors/hooks.js +1 -1
  144. package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
  145. package/dist/engine/feature-ast/extractors/shared.js +2 -3
  146. package/dist/engine/feature-ast/parse.js +13 -13
  147. package/dist/engine/feature-ast/patch.js +1 -1
  148. package/dist/engine/feature-ast/pattern-change-schema.d.ts +8 -0
  149. package/dist/engine/feature-ast/pattern-change-schema.js +1 -1
  150. package/dist/engine/feature-ast/patterns.d.ts +2 -2
  151. package/dist/engine/feature-ast/render.js +2 -2
  152. package/dist/engine/feature-config-events-jobs.js +15 -12
  153. package/dist/engine/feature-entity-handlers.js +9 -5
  154. package/dist/engine/feature-manifest.d.ts +9 -9
  155. package/dist/engine/feature-manifest.js +6 -7
  156. package/dist/engine/feature-ui-extensions.d.ts +3 -3
  157. package/dist/engine/feature-ui-extensions.js +30 -22
  158. package/dist/engine/field-access.js +5 -6
  159. package/dist/engine/index.d.ts +1 -0
  160. package/dist/engine/index.js +4 -4
  161. package/dist/engine/object-form.d.ts +1 -0
  162. package/dist/engine/object-form.js +14 -4
  163. package/dist/engine/ownership.js +3 -3
  164. package/dist/engine/parse-ref-target.js +5 -7
  165. package/dist/engine/pattern-library/index.js +2 -2
  166. package/dist/engine/pattern-library/library.js +2 -2
  167. package/dist/engine/pattern-library/types.js +3 -3
  168. package/dist/engine/pipeline.js +2 -2
  169. package/dist/engine/project-app-schema-for-roles.js +1 -1
  170. package/dist/engine/projection-helpers.js +9 -9
  171. package/dist/engine/qualified-name.js +2 -2
  172. package/dist/engine/registry-facade.js +1 -1
  173. package/dist/engine/registry-ingest.js +12 -12
  174. package/dist/engine/registry-state.js +17 -19
  175. package/dist/engine/registry-validate.js +12 -12
  176. package/dist/engine/run-in.js +2 -2
  177. package/dist/engine/run-pipeline.js +7 -0
  178. package/dist/engine/schema-builder.js +40 -38
  179. package/dist/engine/screen-filter-ops.js +17 -20
  180. package/dist/engine/screen-helpers.js +4 -3
  181. package/dist/engine/state-machine.d.ts +12 -12
  182. package/dist/engine/state-machine.js +7 -7
  183. package/dist/engine/steps/_resolver-utils.js +4 -3
  184. package/dist/engine/steps/for-each.js +4 -4
  185. package/dist/engine/steps/unsafe-projection-delete.js +1 -1
  186. package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
  187. package/dist/engine/steps/wait-for-event.js +4 -4
  188. package/dist/engine/steps/workflow-system-events.js +3 -3
  189. package/dist/engine/tier-resolver-extension.d.ts +29 -27
  190. package/dist/engine/tier-resolver-extension.js +26 -26
  191. package/dist/engine/transfer-adjacency.js +2 -2
  192. package/dist/engine/validate-projection-allowlist.js +7 -27
  193. package/dist/engine/walk-step-instances.d.ts +2 -0
  194. package/dist/engine/walk-step-instances.js +38 -0
  195. package/dist/engine/where-rule-lint.js +1 -1
  196. package/dist/engine/write-handler-qn-extract.js +1 -1
  197. package/dist/entrypoint/index.js +3 -3
  198. package/dist/env/index.d.ts +7 -6
  199. package/dist/env/index.js +10 -9
  200. package/dist/errors/transition-details.js +7 -7
  201. package/dist/errors/write-error-info.d.ts +6 -6
  202. package/dist/errors/write-error-info.js +6 -6
  203. package/dist/es-ops/context.d.ts +9 -9
  204. package/dist/es-ops/context.js +28 -28
  205. package/dist/es-ops/index.js +7 -9
  206. package/dist/es-ops/operations-schema.js +4 -10
  207. package/dist/es-ops/runner.d.ts +11 -11
  208. package/dist/es-ops/runner.js +49 -53
  209. package/dist/es-ops/types.d.ts +55 -55
  210. package/dist/es-ops/types.js +11 -11
  211. package/dist/event-store/admin-api.js +6 -7
  212. package/dist/event-store/errors.js +1 -1
  213. package/dist/event-store/event-store.js +2 -2
  214. package/dist/event-store/rebuild-dead-letter.js +1 -1
  215. package/dist/files/feature.js +9 -10
  216. package/dist/files/file-handle.js +5 -6
  217. package/dist/files/file-ref-entity.js +15 -15
  218. package/dist/files/file-ref-table.js +7 -8
  219. package/dist/files/file-routes.d.ts +7 -0
  220. package/dist/files/file-routes.js +11 -5
  221. package/dist/files/in-memory-provider.js +8 -9
  222. package/dist/files/local-provider.js +18 -25
  223. package/dist/files/provider-resolver.d.ts +7 -7
  224. package/dist/files/storage-tracking.js +2 -2
  225. package/dist/files/zip-stream.d.ts +12 -19
  226. package/dist/files/zip-stream.js +44 -63
  227. package/dist/i18n/required-surface-keys.d.ts +1 -1
  228. package/dist/i18n/required-surface-keys.js +8 -8
  229. package/dist/internal/stack-support.d.ts +1 -1
  230. package/dist/internal/stack-support.js +1 -1
  231. package/dist/jobs/job-runner-helpers.d.ts +1 -2
  232. package/dist/jobs/job-runner-helpers.js +8 -18
  233. package/dist/jobs/job-runner-schedulers.d.ts +1 -1
  234. package/dist/jobs/job-runner-schedulers.js +3 -4
  235. package/dist/jobs/job-runner.js +17 -18
  236. package/dist/lifecycle/lifecycle.js +3 -5
  237. package/dist/lifecycle/signal-handlers.js +5 -4
  238. package/dist/logging/utils.d.ts +2 -1
  239. package/dist/logging/utils.js +38 -10
  240. package/dist/migrations/index.js +1 -1
  241. package/dist/migrations/kumiko-drift.js +14 -13
  242. package/dist/migrations/pending-rebuilds.d.ts +21 -20
  243. package/dist/migrations/pending-rebuilds.js +27 -27
  244. package/dist/migrations/projection-table-index.d.ts +3 -3
  245. package/dist/migrations/projection-table-index.js +8 -8
  246. package/dist/observability/metric-validator.js +1 -1
  247. package/dist/observability/metrics-handle.js +3 -3
  248. package/dist/pipeline/append-event-core.js +11 -2
  249. package/dist/pipeline/cascade-handler.d.ts +1 -1
  250. package/dist/pipeline/dispatch-batch.js +2 -2
  251. package/dist/pipeline/dispatch-query.js +10 -10
  252. package/dist/pipeline/dispatch-shared.d.ts +1 -0
  253. package/dist/pipeline/dispatch-shared.js +42 -19
  254. package/dist/pipeline/dispatch-stream.js +1 -1
  255. package/dist/pipeline/dispatch-write.js +3 -3
  256. package/dist/pipeline/dispatcher-utils.d.ts +1 -1
  257. package/dist/pipeline/dispatcher-utils.js +1 -1
  258. package/dist/pipeline/event-consumer-state.js +2 -2
  259. package/dist/pipeline/event-dispatcher-delivery.js +1 -1
  260. package/dist/pipeline/event-dispatcher.js +17 -28
  261. package/dist/pipeline/irreversible-operation-gate.js +1 -1
  262. package/dist/pipeline/lifecycle-pipeline.js +9 -9
  263. package/dist/pipeline/member-read-only-transaction.js +1 -3
  264. package/dist/pipeline/msp-rebuild.js +1 -1
  265. package/dist/pipeline/projection-rebuild.js +13 -12
  266. package/dist/pipeline/system-hooks.js +39 -41
  267. package/dist/pipeline/system-identity-switch.js +1 -3
  268. package/dist/pipeline/tenant-timezone-cache.js +1 -1
  269. package/dist/random/generate.d.ts +29 -29
  270. package/dist/random/generate.js +30 -30
  271. package/dist/random/words.js +20 -20
  272. package/dist/rate-limit/middleware.js +9 -5
  273. package/dist/redis/client.js +2 -2
  274. package/dist/redis/pubsub-signal.js +14 -17
  275. package/dist/schema-cli.js +116 -22
  276. package/dist/search/index.js +4 -4
  277. package/dist/search/purge-subject.js +4 -5
  278. package/dist/search/reindex-entity.js +6 -9
  279. package/dist/time/boot-tz-warning.d.ts +2 -2
  280. package/dist/time/boot-tz-warning.js +8 -8
  281. package/dist/time/iana.js +10 -10
  282. package/dist/time/index.js +9 -9
  283. package/dist/time/legacy-date.d.ts +2 -2
  284. package/dist/time/legacy-date.js +6 -8
  285. package/dist/time/polyfill.js +1 -1
  286. package/dist/time/tz-context.d.ts +1 -1
  287. package/dist/time/tz-context.js +1 -1
  288. package/dist/ui-types/app-schema.js +7 -7
  289. package/dist/ui-types/list-row-meta.js +4 -4
  290. package/dist/upgrade-cli.js +10 -9
  291. package/dist/utils/compare.js +1 -1
  292. package/package.json +4 -4
  293. package/src/changes.json +54 -0
  294. package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
  295. package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
  296. package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
  297. package/src/scripts/codemod/pii-personal-migration.ts +17 -8
  298. package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
@@ -6,16 +6,14 @@ import { access as accessPresets } from "../config-helpers.js";
6
6
  import { parseRefTarget } from "../parse-ref-target.js";
7
7
  import { isSelectOptionTone } from "./select-option-tone.js";
8
8
  export const FILE_FIELD_TYPES = new Set(["file", "image", "files", "images"]);
9
- // Field-Namen die typischerweise PII enthalten. Ohne `pii: true` /
10
- // `userOwned` / `tenantOwned` / `allowPlaintext`-Marker → Boot-Warning.
11
- // Lower-case compare für case-insensitive Match (displayName vs displayname).
9
+ // Field names that typically contain PII. Without a `pii: true` /
10
+ // `userOwned` / `tenantOwned` / `allowPlaintext` marker → boot warning.
11
+ // Compare in lower case for case-insensitive matching (displayName vs displayname).
12
12
  //
13
- // Bewusst NICHT in der Liste:
14
- // - `name` allein — zu viele Geschäfts-Kontexte (product.name,
15
- // tenant.name, role.name) sind kein PII. Personen-Namen werden
16
- // ueber displayName / firstName / lastName / fullName erfasst.
17
- //
18
- // Quelle: docs/plans/datenschutz/crypto-shredding.md Boot-Validation-Sektion.
13
+ // Deliberately NOT included:
14
+ // - `name` alone — too many business contexts (product.name,
15
+ // tenant.name, role.name) are not PII. Personal names are detected
16
+ // through displayName / firstName / lastName / fullName.
19
17
  export const PII_DIRECT_NAME_HINTS = new Set([
20
18
  "email",
21
19
  "phone",
@@ -42,8 +40,8 @@ export const PII_DIRECT_NAME_HINTS = new Set([
42
40
  "iban",
43
41
  "bic",
44
42
  ]);
45
- // Field-Namen die typischerweise User-Generated-Content enthalten —
46
- // User-Forget muss diese mit Author-Subject-Key encrypten.
43
+ // Field names that typically contain user-generated content —
44
+ // User-Forget must encrypt these with the author subject key.
47
45
  export const PII_USER_OWNED_NAME_HINTS = new Set([
48
46
  "body",
49
47
  "text",
@@ -305,18 +303,18 @@ export function validateAnonymousRateLimit(featureName, kind, handlerName, acces
305
303
  `Use rateLimit.per="ip" or "ip+handler" for anonymous endpoints.`);
306
304
  }
307
305
  }
308
- // --- MultiStreamProjection delivery-invariant ---
306
+ // --- MultiStreamProjection delivery invariant ---
309
307
  //
310
- // `delivery: "per-instance"` mit einer `table` ist eine semantische Falle:
311
- // N Dispatcher-Instanzen würden parallel die gleichen INSERT/UPDATE-Zeilen
312
- // schreiben (Race / Duplicates), und ein Rebuild würde nur eine Zeile in
313
- // kumiko_event_consumers anfassen (die SHARED_INSTANCE_SENTINEL-Zeile),
314
- // während Live-Cursor in per-instance-Zeilen liegen → Cursor-Divergenz.
308
+ // `delivery: "per-instance"` with a `table` is a semantic trap:
309
+ // N dispatcher instances would write the same INSERT/UPDATE rows in parallel
310
+ // (races / duplicates), and a rebuild would affect only one row in
311
+ // kumiko_event_consumers (the SHARED_INSTANCE_SENTINEL row),
312
+ // while live cursors reside in per-instance rows → cursor divergence.
315
313
  //
316
- // Die Invariante ist: per-instance-Consumer sind rein side-effect (SSE,
317
- // in-memory cache invalidation). Wer eine Tabelle materialisiert, braucht
318
- // shared delivery — das ist exactly-once globally und gibt dem Rebuild
319
- // einen einzigen Cursor zum zurücksetzen.
314
+ // The invariant is: per-instance consumers are side-effect-only (SSE,
315
+ // in-memory cache invalidation). Materializing a table requires
316
+ // shared delivery — it is exactly-once globally and gives the rebuild
317
+ // a single cursor to reset.
320
318
  export function validateMultiStreamProjections(feature) {
321
319
  for (const [name, msp] of Object.entries(feature.multiStreamProjections)) {
322
320
  if (msp.delivery === "per-instance" && msp.table !== undefined) {
@@ -326,18 +324,19 @@ export function validateMultiStreamProjections(feature) {
326
324
  }
327
325
  }
328
326
  }
329
- // --- Located-Timestamp validation ---
327
+ // --- Located-timestamp validation ---
330
328
  //
331
- // Wenn ein Feld `type: "timestamp"` einen `locatedBy`-Marker trägt, muss das
332
- // referenzierte Feld in derselben Entity existieren UND vom Typ `tz` sein.
333
- // Sonst weiß weder DB-Wrapper noch JSON-Serializer welche TZ zur Wall-Clock
334
- // gehört → silent data loss bei Reads in anderer Server-TZ.
329
+ // When a field with `type: "timestamp"` has a `locatedBy` marker, the
330
+ // referenced field must exist in the same entity AND have type `tz`.
331
+ // Otherwise neither the DB wrapper nor the JSON serializer knows which TZ
332
+ // belongs to the wall clock → silent data loss when reading in another
333
+ // server TZ.
335
334
  //
336
- // Die häufigste Quelle von Konflikten ist Hand-Konstruktion:
335
+ // Manual construction is the most common source of conflicts:
337
336
  // { foo: { type: "timestamp", locatedBy: "fooTz" } }
338
- // ohne das `fooTz`-Feld zu deklarieren. `createLocatedTimestampField()`
339
- // erzeugt stattdessen EIN Feld vom Typ "locatedTimestamp" — wer den nutzt,
340
- // fliegt nicht durch diesen Validator.
337
+ // without declaring the `fooTz` field. `createLocatedTimestampField()`
338
+ // instead creates ONE field of type "locatedTimestamp" — using it avoids
339
+ // this validator.
341
340
  export function validateLocatedTimestamps(feature) {
342
341
  for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
343
342
  const fields = entity.fields;
@@ -359,18 +358,18 @@ export function validateLocatedTimestamps(feature) {
359
358
  }
360
359
  }
361
360
  }
362
- // --- Entity-Index validation ---
361
+ // --- Entity-index validation ---
363
362
  //
364
- // entity.indexes deklariert Composite-/Unique-Indices über mehrere Feld-
365
- // Spalten. Häufige Fehler: Tippfehler im Feld-Namen, leere column-Liste,
366
- // Index auf einem Field das die DB-Spalte gar nicht existiert (file/image
367
- // in der multi-Variante). Catched at boot, lange bevor drizzle-kit beim
368
- // generate-Run zickt.
363
+ // entity.indexes declares composite/unique indexes over multiple field
364
+ // columns. Common errors include misspelled field names, an empty column
365
+ // list, and an index on a field for which the DB has no column (file/image
366
+ // in the multi-value variant). Catch these at boot, long before drizzle-kit
367
+ // fails during the generate run.
369
368
  //
370
- // `tenantId` als einzige Spalte ist redundant — buildEntityTable legt
371
- // den Index sowieso automatisch an. Wir lassen die Composite-Form erlaubt
372
- // (`["tenantId", "key"]` ist sinnvoll), nur die rein-tenantId-Single-
373
- // column-Form blockieren wir.
369
+ // A `tenantId`-only index is redundant because buildEntityTable creates
370
+ // that index automatically. Composite forms remain allowed
371
+ // (`["tenantId", "key"]` is useful); only the tenantId-only single-column
372
+ // form is rejected.
374
373
  export function validateEntityIndexes(feature) {
375
374
  for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
376
375
  if (!entity.indexes)
@@ -397,21 +396,21 @@ export function validateEntityIndexes(feature) {
397
396
  `a jsonb array cannot be indexed as a column. Use a single-value field or remove from the index.`);
398
397
  }
399
398
  if (field && field.type === "longText") {
400
- // longText ist semantisch "potentially-megabytes content" — ein
401
- // BTREE-Index auf einer 1-MB-Spalte ist Performance-Disaster
402
- // (PG würde in TOAST-pages dereferenzieren müssen für jeden
403
- // Index-Lookup). Konsistent mit der type-level-decision dass
404
- // longText kein sortable/searchable/filterable hat. Wer
405
- // wirklich indexieren will, nimmt `text` mit den
406
- // entsprechenden Skalierungs-Trade-offs.
399
+ // longText semantically means "potentially-megabytes content" — a
400
+ // BTREE index on a 1-MB column is a performance disaster
401
+ // (PG would need to dereference TOAST pages for every
402
+ // index lookup). This is consistent with the type-level decision
403
+ // that longText has no sortable/searchable/filterable flags. Anyone
404
+ // who truly needs indexing should use `text` with the
405
+ // corresponding scaling trade-offs.
407
406
  throw new Error(`${where}: column "${col}" is a longText field — these cannot be indexed. ` +
408
407
  `Use \`text\` if you need indexing, or rely on the SearchAdapter (Meilisearch) for full-text search on long content.`);
409
408
  }
410
409
  }
411
- // UNIQUE-constraint auf tenantId ist semantisch (1:1 tenant→entity)
412
- // und NICHT redundant — buildEntityTable's auto-Index ist nur ein
413
- // Performance-Hint, kein constraint. Nur die rein-tenantId-Single-
414
- // column-non-unique-Form blockieren.
410
+ // A UNIQUE constraint on tenantId is semantically meaningful (1:1 tenant→entity)
411
+ // and NOT redundant — buildEntityTable's automatic index is only a
412
+ // performance hint, not a constraint. Only the tenantId-only,
413
+ // single-column, non-unique form is rejected.
415
414
  if (def.columns.length === 1 && def.columns[0] === "tenantId" && !def.unique) {
416
415
  throw new Error(`${where}: single-column index on "tenantId" is redundant — ` +
417
416
  `buildEntityTable always creates one automatically. Remove this entry.`);
@@ -436,9 +435,9 @@ export function validateEncryptedFields(feature) {
436
435
  let found = false;
437
436
  for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
438
437
  for (const [fieldName, field] of Object.entries(entity.fields)) {
439
- // Beide string-typed fields können encrypted sein. Die
440
- // searchable/sortable-Konflikt-Checks gelten nur für `text`
441
- // (longText hat diese flags type-level nicht).
438
+ // Both string-typed fields can be encrypted. The
439
+ // searchable/sortable conflict checks apply only to `text`
440
+ // (`longText` does not have these flags at the type level).
442
441
  if (field.type !== "text" && field.type !== "longText")
443
442
  continue;
444
443
  if (!field.encrypted)
@@ -491,7 +490,7 @@ const NUMERIC_EMBEDDED_SUB_TYPES = new Set(["number", "money", "decimal"]);
491
490
  function isValidEmbeddedDecimalScale(scale) {
492
491
  return Number.isInteger(scale) && scale >= 0 && scale <= 15;
493
492
  }
494
- // Tier 2.7e-3 + Cross-Feature: ReferenceFieldDef validation, shared by
493
+ // ReferenceFieldDef validation, shared by
495
494
  // top-level reference fields and reference sub-fields of an embedded field
496
495
  // (only the field-path in error messages differs, e.g. "accountId" vs
497
496
  // "lines.accountId", so a failure is locatable either way).
@@ -552,7 +551,7 @@ function validateReferenceTarget(entityName, fieldPath, refString, labelField, o
552
551
  // (use-reference-lookup).
553
552
  validateReferenceOptionsQuery(entityName, fieldPath, optionsQuery, feature, queryHandlers);
554
553
  }
555
- // fw#2780: the picker may source its options from a query handler instead of
554
+ // The picker may source its options from a query handler instead of
556
555
  // the referenced entity's table. A typo'd QN would only surface as a 404 on
557
556
  // first Combobox open, so it is pinned at boot — same treatment as
558
557
  // `DashboardFilterDefinition.optionsQuery` (boot-validator/query-refs.ts).
@@ -571,7 +570,7 @@ function validateReferenceOptionsQuery(entityName, fieldPath, optionsQuery, feat
571
570
  `is declared via r.queryHandler(...).`);
572
571
  }
573
572
  }
574
- // Tier 2.7e-3 + Cross-Feature: ReferenceFieldDef validation for top-level
573
+ // ReferenceFieldDef validation for top-level
575
574
  // reference fields (self-reference, entity → entity, is allowed). The actual
576
575
  // checks run in validateReferenceTarget — shared with the reference
577
576
  // sub-fields of an embedded field (validateEmbeddedFields).
@@ -705,10 +704,10 @@ function validateEmbeddedListMetadata(fieldName, entityName, field, entity) {
705
704
  }
706
705
  // --- MultiSelect field validation ---
707
706
  //
708
- // options muss non-empty sein (sonst wäre das Feld nicht benutzbar) und
709
- // default — wenn gesetzt — ist eine Teilmenge der options. Beides würde
710
- // auch im Zod-Schema bei runtime fehlschlagen, der Boot-Catch ist nur
711
- // die früheste Stelle für klare Fehlermeldungen.
707
+ // options must be non-empty (otherwise the field cannot be used), and
708
+ // default — when set — must be a subset of options. Both conditions would
709
+ // also fail at runtime in the Zod schema; boot validation only provides
710
+ // the earliest clear error messages.
712
711
  // Exhaustive over MultiSelectFieldDef["columns"] (the compiler flags a union
713
712
  // change); the TS union is gone at runtime for JS/JSON-sourced definitions.
714
713
  const CHECKBOX_COLUMN_COUNTS = {
@@ -1,3 +1,5 @@
1
+ import { createFallbackLogger } from "../../logging/utils.js";
2
+ const log = createFallbackLogger("kumiko:boot");
1
3
  // Providers whose bytes do not survive a process restart. Only "inmemory"
2
4
  // today; extend if another ephemeral bundled provider lands.
3
5
  const EPHEMERAL_PROVIDERS = new Set(["inmemory"]);
@@ -30,8 +32,7 @@ export function validateGdprStoragePersistence(features) {
30
32
  }
31
33
  const persistent = [...registeredProviders].filter((p) => !EPHEMERAL_PROVIDERS.has(p));
32
34
  if (persistent.length === 0) {
33
- // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
34
- console.warn("[kumiko:boot] user-data-rights is mounted but no persistent file provider is — GDPR exports use an ephemeral/in-memory store and are LOST on restart (the download then 500s). Mount createFileProviderS3Feature() or createFileProviderS3Feature({ fromEnv: true }) and select it via the file-foundation provider config.");
35
+ log.warn("user-data-rights is mounted but no persistent file provider is — GDPR exports use an ephemeral/in-memory store and are LOST on restart (the download then 500s). Mount createFileProviderS3Feature() or createFileProviderS3Feature({ fromEnv: true }) and select it via the file-foundation provider config.");
35
36
  // skip: ephemeral store already warned; the s3-env env-var check below is moot
36
37
  return;
37
38
  }
@@ -41,8 +42,7 @@ export function validateGdprStoragePersistence(features) {
41
42
  if (persistent.length === 1 && persistent[0] === "s3-env") {
42
43
  const missing = S3_ENV_REQUIRED.filter((v) => !process.env[v]);
43
44
  if (missing.length > 0) {
44
- // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
45
- console.warn(`[kumiko:boot] createFileProviderS3Feature({ fromEnv: true }) (provider "s3-env") is the GDPR file store but these env vars are unset: ${missing.join(", ")}. The provider throws on the first export (inside the cron), not here — set them so GDPR storage works.`);
45
+ log.warn(`createFileProviderS3Feature({ fromEnv: true }) (provider "s3-env") is the GDPR file store but these env vars are unset: ${missing.join(", ")}. The provider throws on the first export (inside the cron), not here — set them so GDPR storage works.`);
46
46
  }
47
47
  }
48
48
  }
@@ -11,7 +11,7 @@ function requiredKeysFromGeneratedConfigHub(schema) {
11
11
  const out = new Set();
12
12
  // The generator's section titles + mask-title field-label overrides are
13
13
  // dot-form i18n references (`${feature}.settings`), never literal display
14
- // text, so treatDotFormAsKey bypasses isI18nKey's colon-only check (fw#2260).
14
+ // text, so treatDotFormAsKey bypasses isI18nKey's colon-only check.
15
15
  for (const screen of schema.screens) {
16
16
  for (const key of requiredKeysFromScreen(SETTINGS_HUB_FEATURE, screen, {
17
17
  treatDotFormAsKey: true,
@@ -9,14 +9,14 @@ export type ValidateBootOptions = {
9
9
  /** Warn when an access role is used by exactly one handler/config-key/
10
10
  * field across the whole boot scan — often a typo, but also the normal
11
11
  * shape of a legitimate fine-grained role (one role scoped to one admin
12
- * endpoint on purpose). Opt-in (default false, #1711): a default-on
12
+ * endpoint on purpose). Opt-in (default false): a default-on
13
13
  * prod warning that nobody can silence per-role isn't worth the noise
14
14
  * it generates on every boot. */
15
15
  readonly warnOnUniqueAccessRoles?: boolean;
16
16
  /** QNs the app's sidebar allowlist admits. When set, every declared nav
17
17
  * whose screen isn't reachable from any allowlisted nav entry gets a
18
18
  * boot-time warning — that screen would otherwise be silently
19
- * unreachable via the sidebar (fw#3019). When omitted and the app has
19
+ * unreachable via the sidebar. When omitted and the app has
20
20
  * workspaces, the allowlist is derived from `r.workspace({ nav })` and
21
21
  * `r.nav({ workspaces })` assignments instead. */
22
22
  readonly navAllowlist?: ReadonlySet<string>;
@@ -30,9 +30,9 @@ import { validateExtensionSelectorPanels } from "./selector-owner-panels.js";
30
30
  import { validateTransferGraph } from "./transfer-graph.js";
31
31
  import { validateSubmitPrefilledHiddenFields } from "./url-prefill-fields.js";
32
32
  import { collectWorkspaceQns, resolveNavAllowlist, validateDefaultWorkspaceUniqueness, validateWorkspaces, } from "./workspaces.js";
33
- // Re-export: wird von run-dev-app.ts benötigt um Write-Handler-QNs
34
- // an den Codegen zu übergeben. Nicht Teil von validateBoot, aber
35
- // dieselbe Extraktionslogik.
33
+ // Re-export: run-dev-app.ts needs this to pass write-handler QNs
34
+ // to codegen. It is not part of validateBoot, but uses the same
35
+ // extraction logic.
36
36
  export { collectCardMetaOverflow } from "./card-meta-overflow.js";
37
37
  export { validateAppCustomScreenWriteQns } from "./custom-screen-write-qns.js";
38
38
  export { collectWriteHandlerQns } from "./nav.js";
@@ -101,13 +101,13 @@ export function validateBoot(rawFeatures, options) {
101
101
  }
102
102
  // Collect all config keys across features (for cross-feature reference validation)
103
103
  const allConfigKeys = new Set();
104
- // Qualified config-key set für ConfigEditScreen-Validation. MUSS via
105
- // qualifyEntityName kanonisiert werden (toKebab auf Feature + Key) — exakt
106
- // wie define-feature/registry den QN bildet. Der rohe f.configKeys-Key ist
107
- // der camelCase-Objekt-Key (`brandingTitle`), die echte QN aber
108
- // `…:config:branding-title`; ohne toKebab failt jeder configEdit-Screen mit
109
- // multi-word Config-Key fälschlich. allConfigKeys oben nutzt das ältere
110
- // `feature.short`-Format für validateConfigReads.
104
+ // Qualified config-key set for ConfigEditScreen validation. MUST be
105
+ // canonicalized via qualifyEntityName (toKebab on feature + key), exactly
106
+ // as define-feature/registry builds the QN. The raw f.configKeys key is
107
+ // the camelCase object key (`brandingTitle`), but the actual QN is
108
+ // `…:config:branding-title`; without toKebab, every configEdit screen
109
+ // with a multi-word config key would fail incorrectly. allConfigKeys above
110
+ // uses the older `feature.short` format for validateConfigReads.
111
111
  const configKeyDefsByQn = new Map();
112
112
  for (const f of features) {
113
113
  for (const [key, def] of Object.entries(f.configKeys)) {
@@ -138,15 +138,15 @@ export function validateBoot(rawFeatures, options) {
138
138
  const screensByShortId = collectScreensByShortId(features);
139
139
  validateScreenShortIdCollisions(screensByShortId);
140
140
  // Cross-feature entity → detailFor-screen map — built (+ uniqueness-
141
- // validated) up front so per-feature rowAction entity-targets (fw#2228)
141
+ // validated) up front so per-feature rowAction entity-targets
142
142
  // resolve against it inside the loop below, instead of at the very end
143
143
  // where this used to run.
144
144
  const detailForScreens = collectDetailForScreens(features, featureMap);
145
- // Cross-feature API exposure-map — jedes Feature deklariert Marker via
146
- // r.exposesApi(name). Per-feature validateApiExposureMatching walkt
147
- // usedApis-Set und checkt dass jeder Eintrag hier einen Match findet.
148
- // Verhindert dass typo-getroffene oder gedroppte QN-Aufrufe zu
149
- // Runtime-Crash statt Boot-Fail werden.
145
+ // Cross-feature API exposure map — each feature declares markers via
146
+ // r.exposesApi(name). Per-feature validateApiExposureMatching walks the
147
+ // usedApis set and checks that every entry has a match here.
148
+ // This prevents typoed or removed QN calls from causing a runtime crash
149
+ // instead of a boot failure.
150
150
  const allExposedApis = new Map(); // apiName → providerFeature
151
151
  for (const f of features) {
152
152
  for (const apiName of f.exposedApis) {
@@ -200,10 +200,10 @@ export function validateBoot(rawFeatures, options) {
200
200
  validateParentRefs(feature, featureMap);
201
201
  validateTransferGraph(feature, featureMap);
202
202
  validateMultiStreamProjections(feature);
203
- // Vor validateScreens: dessen visible/entityId-Feldref-Checks werfen für
204
- // einen Function-Wert bereits (mit verwirrender "unknown field undefined"-
205
- // Message, weil `typeof fn !== "boolean"` true ist), bevor der klare
206
- // Function-Check hier überhaupt liefe.
203
+ // Before validateScreens: its visible/entityId field-reference checks throw
204
+ // for a Function value first, with the confusing "unknown field undefined"
205
+ // message (because `typeof fn !== "boolean"` is true), before the clear
206
+ // Function check here can run.
207
207
  validateActionWiring(feature);
208
208
  validateFieldWiring(feature);
209
209
  validateScreens(feature, featureMap, allWriteHandlerQns, allScreenQns, configKeyDefsByQn, screensByShortId, detailForScreens);
@@ -1,8 +1,10 @@
1
1
  // Nav validation + collectKnownRoles (role synthesis for ownership
2
2
  // validation in index.ts — thematically adjacent to ownership.ts, kept
3
3
  // here since this is a structural move, not a semantic reorg).
4
+ import { createFallbackLogger } from "../../logging/utils.js";
4
5
  import { qualifyEntityName } from "../qualified-name.js";
5
6
  import { isOpenToAllGranted } from "../types/index.js";
7
+ const log = createFallbackLogger("kumiko:boot");
6
8
  export function collectWriteHandlerQns(features) {
7
9
  const set = new Set();
8
10
  for (const f of features) {
@@ -110,7 +112,7 @@ function navViewerRoles(access) {
110
112
  return isOpenToAllGranted(access) ? undefined : [];
111
113
  return access.roles;
112
114
  }
113
- // fw#2640: warn (never throw — this is a real UX bug, not a broken ref)
115
+ // Warn (never throw — this is a real UX bug, not a broken ref)
114
116
  // when a nav leaf's role gate is disjoint from its parent section's — no
115
117
  // user who can see the section could ever see this child, so the section
116
118
  // renders with zero visible children for every user who reaches it.
@@ -133,8 +135,7 @@ export function warnOnNavAccessInversion(allNavQns) {
133
135
  continue; // parent already visible to nobody — a different problem
134
136
  if (leafRoles.some((role) => parentRoles.includes(role)))
135
137
  continue;
136
- // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
137
- console.warn(`[kumiko:boot] Nav entry "${qn}" requires roles [${leafRoles.join(", ")}] but its parent ` +
138
+ log.warn(`Nav entry "${qn}" requires roles [${leafRoles.join(", ")}] but its parent ` +
138
139
  `"${navDef.parent}" only allows [${parentRoles.join(", ")}] — no user who can see the ` +
139
140
  `parent section could ever see this entry. If this is intentional, ignore this warning; ` +
140
141
  `otherwise align the role sets.`);
@@ -158,8 +159,7 @@ export function warnOnUnreachableNavScreens(allNavQns, allowedNavQns, navAllowli
158
159
  const remedy = allowlistSource === "workspaces"
159
160
  ? "add it to a workspace's `nav` array or set `workspaces` on the nav"
160
161
  : "add it to the allowlist";
161
- // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
162
- console.warn(`[kumiko:boot] Nav entry "${qn}" declared by feature "${navDef.featureName}" points at ` +
162
+ log.warn(`Nav entry "${qn}" declared by feature "${navDef.featureName}" points at ` +
163
163
  `screen "${screenQn}", which no allowlisted nav entry reaches — it is unreachable via ` +
164
164
  `the sidebar. If this is intentional (e.g. reachable through a generated hub), add ` +
165
165
  `"${qn}" to navAllowlistExempt; otherwise ${remedy}.`);
@@ -3,23 +3,23 @@ import { tableNameOf, } from "../ownership.js";
3
3
  import { SYSTEM_USER_ID } from "../system-user.js";
4
4
  import { SYSTEM_TENANT_ID } from "../types/identifiers.js";
5
5
  import { assertQualifiedWhereFragment, tableColumnSqlNames } from "../where-rule-lint.js";
6
- // --- Ownership rule validation (H.2) ---
6
+ // --- Ownership rule validation ---
7
7
  //
8
8
  // Walks every entity.access and every field.access map, resolves each
9
9
  // FromRule against the cross-feature claim registry, and confirms the
10
10
  // referenced column exists on the entity. Catches typos, renames, and
11
11
  // cross-feature-claim-removal scenarios at boot instead of at request time.
12
12
  //
13
- // fw#2639: also probes every `{ kind: "where" }` rule against the where-rule
13
+ // Also probes every `{ kind: "where" }` rule against the where-rule
14
14
  // lint (see ../where-rule-lint.ts) so a fail-open unqualified-column bug
15
15
  // fails the boot instead of waiting for a request from the right role.
16
16
  //
17
17
  // Field-level `access.read` rejects where-rules too: filterFieldsForRead
18
18
  // evaluates it in memory and userCanReadFieldRow fails closed on a where-rule.
19
19
  //
20
- // fw#2626: `access.write` maps reject `{ kind: "where" }` outright. A
21
- // where-rule is raw SQL and the write path never reaches SQL — it evaluates
22
- // rules in memory against the concrete row (userCanCreateFieldRow /
20
+ // `access.write` maps reject `{ kind: "where" }` outright. A where-rule is
21
+ // raw SQL and the write path never reaches SQL — it evaluates rules in memory
22
+ // against the concrete row (userCanCreateFieldRow /
23
23
  // userCanWriteFieldRow), and a create has no row to run a predicate against
24
24
  // at all. Such a rule can therefore only ever deny, so the boot fails instead
25
25
  // of shipping an access map that silently locks the role out.
@@ -49,9 +49,8 @@ export function collectClaimKeys(features) {
49
49
  // corpora go in as undefined — a three-feature stack genuinely does not know
50
50
  // which roles and claims the whole app declares, and answering that question
51
51
  // from a slice produces false "unknown role"/"unknown claim" errors. The
52
- // subset-invariant checks (#2639 where-lint, #2626 where-on-write, unknown
53
- // column) run in full: those hold or fail per feature, whatever else is
54
- // mounted.
52
+ // subset-invariant checks (where-lint, where-on-write, unknown column) run in
53
+ // full: those hold or fail per feature, whatever else is mounted.
55
54
  export function validateOwnershipBoot(features) {
56
55
  for (const feature of features) {
57
56
  validateOwnershipRules(feature, undefined, undefined);
@@ -258,12 +257,16 @@ export function probeWhereRule(rule, roleName, probe, scope, featureName) {
258
257
  return;
259
258
  assertQualifiedWhereFragment(fragment.sqlText, probe.columns, `${scope} (role "${roleName}", feature: "${featureName}")`);
260
259
  }
260
+ // Array.isArray does not narrow a readonly array out of a union.
261
+ function isRoleList(access) {
262
+ return Array.isArray(access);
263
+ }
261
264
  // Embedded sub-fields are filtered in memory too (filterFieldsForRead), so a
262
265
  // where-rule there is just as unevaluable as on a top-level field.
263
266
  function rejectWhereInEmbeddedRead(schema, parentScope, featureName) {
264
267
  for (const [subName, sub] of Object.entries(schema)) {
265
268
  const map = sub.access?.read;
266
- if (!map || Array.isArray(map))
269
+ if (!map || isRoleList(map))
267
270
  continue;
268
271
  for (const [roleName, rule] of Object.entries(map)) {
269
272
  if (rule !== "all" && rule.kind === "where") {
@@ -1,5 +1,5 @@
1
1
  import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
2
- // --- Parent-ref boot validation (fw#2766) ---
2
+ // --- Parent-ref boot validation ---
3
3
  //
4
4
  // Join-row entities (EntityDefinition.parentRef) derive both the write-path
5
5
  // deny and the read-path SQL gate from one declaration. This is the boot-time