@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
@@ -16,29 +16,28 @@ export function physicalColumnName(table, field) {
16
16
  // Returns column(s) for a field. Most fields return a single entry,
17
17
  // money returns two (amount + currency).
18
18
  //
19
- // `required: true` auf einem FieldDefinition mappt **immer** auf .notNull()
20
- // in der DB-Spalte. Die alte Implementation hat das nur für reference-
21
- // fields gemacht — text/select/number/etc. waren stillschweigend nullable
22
- // in der DB, auch wenn der API-Validator required erzwungen hat. Folge:
23
- // hand-written PgTable-Definitionen mussten daneben mit .notNull() pflegen,
24
- // was zu Doppel-Definitionen + Schema-Drift führte. Jetzt ist r.entity
25
- // die einzige Wahrheit.
19
+ // `required: true` on a FieldDefinition always maps to .notNull() in the
20
+ // database column. Previously this applied only to reference fields, while
21
+ // text/select/number/etc. remained silently nullable even when the API
22
+ // validator enforced required. That forced hand-written PgTable definitions
23
+ // to add .notNull(), causing duplicate definitions and schema drift. Now
24
+ // r.entity is the single source of truth.
26
25
  function fieldToColumns(name, field, entity) {
27
26
  const snakeName = toSnakeCase(name);
28
27
  switch (field.type) {
29
28
  case "text":
30
29
  case "longText": {
31
- // Beide mappen auf PG `text` (unbounded). Unterschied lebt nur
32
- // im Type-Layer: longText hat kein sortable/searchable/filterable
33
- // (Sprint 5b vorab). Reihenfolge default() VOR notNull(): drizzle's
34
- // column-builder chained beides; ohne default() hat die generierte
35
- // SQL keinen DEFAULT-clause (bricht ALTER TABLE ADD COLUMN auf
36
- // existing rows). longText hat heute kein default-feld im type,
37
- // aber der check `field.default !== undefined` ist defensive.
30
+ // Both map to PG `text` (unbounded). The difference exists only in the
31
+ // type layer: longText has no sortable/searchable/filterable support.
32
+ // The order default() before notNull() matters: the column builder
33
+ // chains both; without default(), the generated SQL has no DEFAULT
34
+ // clause, which breaks ALTER TABLE ADD COLUMN on existing rows.
35
+ // longText currently has no default field in its type, but checking
36
+ // `field.default !== undefined` is defensive.
38
37
  const base = text(snakeName);
39
38
  const withDefault = "default" in field && field.default !== undefined ? base.default(field.default) : base;
40
39
  const mainCol = { [name]: field.required ? withDefault.notNull() : withDefault };
41
- // lookupable → nullable HMAC-Blind-Index-Spalte (lock-step mit
40
+ // lookupable → nullable HMAC blind-index column (kept in lockstep with
42
41
  // fieldToColumnMeta in entity-table-meta.ts).
43
42
  if (field.type === "text" && field.lookupable === true) {
44
43
  return { ...mainCol, [`${name}Bidx`]: text(`${snakeName}_bidx`) };
@@ -54,23 +53,23 @@ function fieldToColumns(name, field, entity) {
54
53
  : boolean(snakeName),
55
54
  };
56
55
  case "select": {
57
- // default() durchreichen — entity-table-meta rendert deklarierte
58
- // defaults, dieser Builder MUSS lock-step bleiben (sonst trägt das
59
- // Meta am buildEntityTable-Objekt eine andere Spalte als die
60
- // Migration, #255-Follow-up-Befund).
56
+ // Pass default() through because entity-table-meta renders declared
57
+ // defaults. This builder must stay in lockstep; otherwise the metadata
58
+ // on the buildEntityTable object describes a different column than the
59
+ // migration.
61
60
  const base = text(snakeName);
62
61
  const col = field.default !== undefined ? base.default(field.default) : base;
63
62
  return { [name]: field.required ? col.notNull() : col };
64
63
  }
65
64
  case "multiSelect":
66
- // jsonb-Array<string> mit Default `[]` und immer NOT NULL.
65
+ // jsonb string array with default `[]`, always NOT NULL.
67
66
  //
68
- // Der `required`-Flag auf MultiSelectFieldDef wird hier bewusst
69
- // ignoriert: Mit Default `[]` ist das Feld strukturell never-null
70
- // (Insert ohne Wert → leeres Array, nicht NULL). Read-Side-Code
71
- // braucht keinen null-check, das ist API-Garantie. Wer "wirklich
72
- // null" will (= "Feld noch nie gesetzt") nutzt einen separaten
73
- // Status-Field oder ein optional-typed reference statt eines
67
+ // The `required` flag on MultiSelectFieldDef is intentionally ignored:
68
+ // with default `[]`, the field is structurally never null (an insert
69
+ // without a value produces an empty array, not NULL). Read-side code
70
+ // therefore needs no null check; this is an API guarantee. Code that
71
+ // needs actual null (meaning the field has never been set) should use
72
+ // a separate status field or an optional-typed reference instead of a
74
73
  // multi-select.
75
74
  return { [name]: jsonb(snakeName).default([]).notNull() };
76
75
  case "number": {
@@ -84,19 +83,18 @@ function fieldToColumns(name, field, entity) {
84
83
  return { [name]: field.required ? col.notNull() : col };
85
84
  }
86
85
  case "bigInt": {
87
- // 64-bit-Integer fuer Audit-Counter, Byte-Sizes, Cumulative-Sums.
88
- // mode:"number" liefert JS-`number` (sicher bis 2^53 ≈ 9 PB) statt
89
- // JS-`bigint` — JSON-serialisierbar, Frontend-tauglich. Wer >2^53
90
- // braucht (Astronomie-Astronomie), nutzt einen Text-Field mit
91
- // eigenem Codec.
86
+ // 64-bit integer for audit counters, byte sizes, and cumulative sums.
87
+ // mode:"number" produces a JS `number` (safe up to 2^53 ≈ 9 PB) rather
88
+ // than JS `bigint`, making it JSON-serializable and frontend-friendly.
89
+ // Values above 2^53 should use a text field with a custom codec.
92
90
  const base = bigint(snakeName, { mode: "number" });
93
91
  const col = field.default !== undefined ? base.default(field.default) : base;
94
92
  return { [name]: field.required ? col.notNull() : col };
95
93
  }
96
94
  case "reference":
97
- // Tier 2.7e-3: FK-Style UUID-Spalte. Multi-Mode (Tier 2.7e-Multi)
98
- // speichert UUIDs als jsonb-Array<string>. Single-Mode bleibt
99
- // klassische UUID-Spalte (NOT NULL nur bei required).
95
+ // FK-style UUID column. Multi-mode stores UUIDs as a jsonb string array;
96
+ // single-mode remains a conventional UUID column (NOT NULL only when
97
+ // required).
100
98
  if (field.multiple === true) {
101
99
  return { [name]: jsonb(snakeName).default([]).notNull() };
102
100
  }
@@ -104,11 +102,12 @@ function fieldToColumns(name, field, entity) {
104
102
  [name]: field.required ? uuid(snakeName).notNull() : uuid(snakeName),
105
103
  };
106
104
  case "money":
107
- // BIGINT storing the integer minor unit (cents for EUR, yen for JPY —
108
- // the currency column tells you which). INTEGER would cap at ~21 M EUR
109
- // which is too tight for B2B invoices, property values or balance
110
- // aggregates. BIGINT handles up to ~90 trillion EUR safely in JS.
111
- // Currency hat immer einen Default, ist also strukturell .notNull().
105
+ // BIGINT stores the integer minor unit (cents for EUR, yen for JPY —
106
+ // the currency column identifies which). INTEGER would cap at about
107
+ // 21 M EUR, which is too low for B2B invoices, property values, or
108
+ // balance aggregates. BIGINT safely handles up to about 90 trillion
109
+ // EUR in JS.
110
+ // Currency always has a default, so it is structurally .notNull().
112
111
  return {
113
112
  [name]: field.required ? moneyAmount(snakeName).notNull() : moneyAmount(snakeName),
114
113
  [`${name}Currency`]: text(`${snakeName}_currency`)
@@ -116,47 +115,47 @@ function fieldToColumns(name, field, entity) {
116
115
  .notNull(),
117
116
  };
118
117
  case "embedded":
119
- // jsonb mit default `{}` und immer NOT NULL — analog zu multiSelect.
120
- // `required` wird bewusst ignoriert weil der Default das Feld
121
- // strukturell never-null macht. Wer optional-embedded möchte (=
122
- // "Feld komplett weglassen können") modelliert das über ein
123
- // wrapper-feld mit boolean-flag oder discriminierte-union.
118
+ // jsonb with default `{}`, always NOT NULL, analogous to multiSelect.
119
+ // `required` is intentionally ignored because the default makes the
120
+ // field structurally never null. To make embedded data optional (so
121
+ // the field can be omitted entirely), model it with a wrapper field
122
+ // and boolean flag or a discriminated union.
124
123
  // multiple → the value is a list of rows, so the empty default is `[]`.
125
124
  return field.multiple === true
126
125
  ? { [name]: jsonb(snakeName).default([]).notNull() }
127
126
  : { [name]: jsonb(snakeName).default({}).notNull() };
128
127
  case "jsonb":
129
- // Free-form jsonb — keys nicht schema-validated. Default `{}`, NOT NULL
130
- // (analog zu embedded). Use-case: custom-fields-Bundle's host-entity-
131
- // `customFields`-Spalte (tenant-definierte dynamische keys).
128
+ // Free-form jsonb — keys are not schema-validated. Default `{}`, NOT NULL
129
+ // (analogous to embedded). Use case: the host entity's custom-fields
130
+ // bundle in the `customFields` column, whose keys are tenant-defined.
132
131
  return { [name]: jsonb(snakeName).default({}).notNull() };
133
132
  case "date": {
134
133
  // Real PG `date` — no time-of-day, no timezone. Temporal.PlainDate
135
134
  // end-to-end (see plainDate() in dialect.ts). Was aliased on instant()
136
- // = TIMESTAMPTZ until kumiko-framework#1924 (TZ-dependent round-trip).
135
+ // = TIMESTAMPTZ (TZ-dependent round-trip).
137
136
  const col = plainDate(snakeName);
138
137
  return { [name]: field.required ? col.notNull() : col };
139
138
  }
140
139
  case "timestamp": {
141
- // UTC-Instant — gespeichert als TIMESTAMPTZ in PG, gelesen/geschrieben
142
- // als Temporal.Instant via instant() customType (siehe dialect.ts).
143
- // Sprint F: Single-Mode-Welt — Caller-Code kennt nur Temporal.Instant,
144
- // nie JS-Date. Auch Vergleiche (lte/gt/orderBy) akzeptieren Instants
145
- // direkt, kein .toString()-Cast nötig.
140
+ // UTC instant — stored as TIMESTAMPTZ in PG and read/written as
141
+ // Temporal.Instant through the instant() customType (see dialect.ts).
142
+ // Callers use only Temporal.Instant, never JS Date. Comparisons
143
+ // (lte/gt/orderBy) also accept Instants directly, so no .toString()
144
+ // cast is needed.
146
145
  const col = instant(snakeName);
147
146
  return { [name]: field.required ? col.notNull() : col };
148
147
  }
149
148
  case "tz": {
150
- // IANA-Zonenname als TEXT — Validierung über Zod-Schema (kommt im
151
- // Validator-Schritt). Snake-Convention: `pickup_tz`.
149
+ // IANA time-zone name as TEXT — validated through the Zod schema in the
150
+ // validator step. Snake-case convention: `pickup_tz`.
152
151
  const col = text(snakeName);
153
152
  return { [name]: field.required ? col.notNull() : col };
154
153
  }
155
154
  case "locatedTimestamp": {
156
- // ZWEI Spalten als atomares Pair: <name>_utc TIMESTAMPTZ + <name>_tz TEXT.
157
- // _utc ist instant() (Temporal.Instant), _tz ist text (IANA-Name).
158
- // Auto-Convert (at+tz → utc beim Insert; utc+tz → at beim Read) wird
159
- // im Executor verdrahtet (Phase C). required propagiert auf beide.
155
+ // Two columns form an atomic pair: <name>_utc TIMESTAMPTZ + <name>_tz TEXT.
156
+ // _utc is instant() (Temporal.Instant), and _tz is text (IANA name).
157
+ // The executor wires automatic conversion (at+tz → utc on insert;
158
+ // utc+tz → at on read). required propagates to both columns.
160
159
  const utc = instant(`${snakeName}_utc`);
161
160
  const tz = text(`${snakeName}_tz`);
162
161
  return {
@@ -188,12 +187,12 @@ function fieldToColumns(name, field, entity) {
188
187
  * Derives a table name from an entity name:
189
188
  * 1. camelCase → snake_case (e.g. "memberTask" → "member_task")
190
189
  * 2. Simple English pluralization (category→categories, status→statuses, task→tasks)
191
- * 3. `read_` prefix — markiert die Tabelle als Event-Sourced-Read-Model,
192
- * damit im DB-Tool sofort erkennbar ist dass App-Code nicht direkt
193
- * reinschreibt. Event-Store (kumiko_events) + Framework-State
194
- * (kumiko_*) haben ihren eigenen Prefix, normale App-Side-Tables
195
- * (ohne ES-Anbindung) haben keinen — die drei Kategorien sind damit
196
- * im Tabellenbrowser unterscheidbar.
190
+ * 3. `read_` prefix — marks the table as an event-sourced read model so
191
+ * it is immediately distinguishable in database tools and application
192
+ * code is not expected to write to it directly. The event store
193
+ * (`kumiko_events`) and framework state (`kumiko_*`) use their own
194
+ * prefixes; ordinary app-side tables without event-sourcing have none.
195
+ * The three categories are therefore distinguishable in the table browser.
197
196
  */
198
197
  const ES_PLURAL_SUFFIXES = ["s", "sh", "ch", "x"];
199
198
  export const READ_MODEL_PREFIX = "read_";
@@ -219,12 +218,13 @@ export function buildBaseColumns(softDelete, idType = "uuid") {
219
218
  id: idColumn,
220
219
  tenantId: uuid("tenant_id").notNull(),
221
220
  version: integer("version").default(1).notNull(),
222
- // Sprint F: Temporal.Instant durchgängig (siehe instant() in dialect.ts).
223
- // Vorher mode default "date" → Inkonsistenz mit user-defined timestamp
224
- // Felder (mode "string"). Jetzt: ein Mode für alle Timestamps.
221
+ // Temporal.Instant is used throughout (see instant() in dialect.ts).
222
+ // Previously the default mode was "date", which was inconsistent with
223
+ // user-defined timestamp fields (mode "string"). A single mode is now
224
+ // used for all timestamps.
225
225
  // customType doesn't expose Drizzle's `defaultNow()` shortcut — use raw
226
- // SQL so PG sets the value on insert and we don't need to pass an
227
- // Instant from JS for every row create.
226
+ // SQL so PG sets the value on insert and callers need not pass an
227
+ // Instant for every row creation.
228
228
  insertedAt: instant("inserted_at").default(sql `now()`).notNull(),
229
229
  modifiedAt: instant("modified_at"),
230
230
  // User-IDs are stringified UUIDs post-ES migration. Text (not uuid) so the
@@ -295,9 +295,9 @@ export function buildEntityTable(entityName, entity, options) {
295
295
  }
296
296
  // Default table name derived from entityName (e.g. "memberTask" → "read_member_tasks")
297
297
  const baseTableName = entity.table ?? toTableName(entityName);
298
- // featureName-prefix wird zwischen read_ und den base-Namen geschoben,
299
- // damit alle read-models einheitlich mit `read_` starten — egal ob
300
- // featureName gesetzt ist oder nicht. Beispiel:
298
+ // The featureName prefix is inserted between read_ and the base name so
299
+ // all read models consistently start with `read_`, whether or not
300
+ // featureName is set. Examples:
301
301
  // featureName="shop", base="read_orders" → "read_shop_orders"
302
302
  // featureName=undef, base="read_orders" → "read_orders"
303
303
  // featureName="shop", base="orders" (no read_) → "shop_orders"
@@ -349,7 +349,7 @@ export function buildEntityTable(entityName, entity, options) {
349
349
  indexes[`${tableName}_${toSnakeCase(fieldName)}_idx`] = index(`${tableName}_${toSnakeCase(fieldName)}_idx`).on(column);
350
350
  }
351
351
  }
352
- // lookupable-Felder: Index auf der bidx-Spalte (lock-step mit
352
+ // lookupable fields: index the bidx column (kept in lockstep with
353
353
  // deriveEntityTableMeta).
354
354
  const bidxFieldByField = new Map();
355
355
  for (const [name, field] of Object.entries(entity.fields)) {
@@ -362,10 +362,10 @@ export function buildEntityTable(entityName, entity, options) {
362
362
  indexes[idxName] = index(idxName).on(column);
363
363
  }
364
364
  }
365
- // entity.indexes = composite/unique-Indices die der Author explizit
366
- // deklariert hat. Spalten werden via field-name (camelCase) angesprochen,
367
- // der Index-Name folgt der Convention <table>_<col1>_<col2>_<unique|idx>
368
- // — Override via index.name möglich.
365
+ // entity.indexes contains composite/unique indexes explicitly declared by
366
+ // the author. Columns are addressed by field name (camelCase), and the
367
+ // index name follows <table>_<col1>_<col2>_<unique|idx>; index.name can
368
+ // override the convention.
369
369
  for (const def of entity.indexes ?? []) {
370
370
  const cols = def.columns
371
371
  .map((fieldName) => tHandle[fieldName])
@@ -388,12 +388,12 @@ export function buildEntityTable(entityName, entity, options) {
388
388
  }
389
389
  else if (def.unique === true && entity.softDelete === true) {
390
390
  // Soft-deleted rows keep their value, so a full unique index would
391
- // permanently block reuse; kept in lock-step with entity-table-meta.ts (framework#2593).
391
+ // permanently block reuse; kept in lock-step with entity-table-meta.ts.
392
392
  chain = chain.where(sql.raw(SOFT_DELETE_LIVE_ROW_PREDICATE));
393
393
  }
394
394
  indexes[indexName] = chain;
395
- // Partielles bidx-Pendant für unique-Indices über lookupable-Spalten
396
- // (lock-step mit deriveEntityTableMeta).
395
+ // Partial bidx counterpart for unique indexes over lookupable fields
396
+ // (kept in lockstep with deriveEntityTableMeta).
397
397
  if (def.unique === true && def.where === undefined) {
398
398
  const bidxFieldNames = def.columns.map((c) => bidxFieldByField.get(c) ?? c);
399
399
  if (bidxFieldNames.some((c, i) => c !== def.columns[i])) {
@@ -406,7 +406,7 @@ export function buildEntityTable(entityName, entity, options) {
406
406
  .map((c) => `"${toSnakeCase(c)}" IS NOT NULL`);
407
407
  // Soft-deleted rows keep their bidx hash — without this, a
408
408
  // restored row can't reuse a value a soft-deleted sibling still
409
- // holds (framework#2464).
409
+ // holds.
410
410
  const whereText = (entity.softDelete === true
411
411
  ? [...notNullParts, SOFT_DELETE_LIVE_ROW_PREDICATE]
412
412
  : notNullParts).join(" AND ");
@@ -16,7 +16,7 @@ export declare function withExecutorManagedWrites(runner: DbRunner): ExecutorMan
16
16
  export declare function withoutExecutorManagedWrites(runner: DbRunner): DbRunner;
17
17
  export declare function assertPersonalDataWrite(db: TenantDb, tableName: string, keys: readonly string[], entity: EntityDefinition): void;
18
18
  export declare function unsafeRawForDeclaredStep(holder: TenantDb | UncheckedSystemDb, reason: string): DbRunner;
19
- export declare function withSystemDbUnsafeRawGrant(systemDb: UncheckedSystemDb, grant: EscapeHatchDeclaration | undefined, deniedCallerLabel: string): UncheckedSystemDb;
19
+ export declare function withSystemDbUnsafeRawGrant<T extends object>(systemDb: T, grant: EscapeHatchDeclaration | undefined, deniedCallerLabel: string): T | UncheckedSystemDb;
20
20
  export declare function createUncheckedSystemDb(db: TenantDb, dbOutsideTransaction?: TenantDb, report?: EscapeHatchReporter): UncheckedSystemDb;
21
21
  export declare function createSystemDbView(db: TenantDb, dbOutsideTransaction?: TenantDb, report?: EscapeHatchReporter): UncheckedSystemDb;
22
22
  export declare function scopeTenantDbToTenant(db: TenantDb, tenantId: TenantId): TenantDb;
@@ -34,7 +34,7 @@ export type TenantDbGrants = {
34
34
  readonly writesDeniedReason?: string;
35
35
  };
36
36
  export type PersonalDataGate = (tableName: string, keys: readonly string[], entity?: EntityDefinition) => void;
37
- export declare function withUnsafeRawGrant(tenantDb: TenantDb, grant: EscapeHatchDeclaration | undefined): TenantDb;
37
+ export declare function withUnsafeRawGrant<T extends object>(tenantDb: T, grant: EscapeHatchDeclaration | undefined): T | TenantDb;
38
38
  export declare function acknowledgeConventionCrossTenant(tenantDb: TenantDb, reason: string, options?: {
39
39
  readonly deferReport?: boolean;
40
40
  }): TenantDb;
@@ -26,6 +26,9 @@ function boundRunner(runner, gate, projectionRegistry, executorManagedWrites) {
26
26
  const proxy = new Proxy(runner, {
27
27
  // Tagged-template calls need the real driver object as `this`.
28
28
  apply(target, _thisArg, args) {
29
+ if (typeof target !== "function") {
30
+ throw new TypeError("tenant-db runner proxy applied on a non-callable runner");
31
+ }
29
32
  return Reflect.apply(target, target, args);
30
33
  },
31
34
  get(target, prop, _receiver) {
@@ -51,7 +54,11 @@ function boundRunner(runner, gate, projectionRegistry, executorManagedWrites) {
51
54
  }
52
55
  const gatedArgs = [
53
56
  ...args.slice(0, -1),
54
- (tx) => callback(boundRunner(tx, gate, projectionRegistry, executorManagedWrites)),
57
+ (tx) => {
58
+ // @cast-boundary db-runner — driver callback hands over the tx handle untyped
59
+ const txRunner = tx;
60
+ return callback(boundRunner(txRunner, gate, projectionRegistry, executorManagedWrites));
61
+ },
55
62
  ];
56
63
  return Reflect.apply(value, target, gatedArgs);
57
64
  };
@@ -179,7 +186,7 @@ function buildUncheckedSystemDb(db, dbOutsideTransaction, report, gate) {
179
186
  // Reference rows (tenantId === SYSTEM_TENANT_ID) are allowed, mirroring
180
187
  // "tenant"-mode readWhere's own [tenantId, SYSTEM_TENANT_ID] allowlist.
181
188
  assertRowsTenant(rows, tenantField) {
182
- const hasOffender = rows.some((row) => !allowedTenantIds.includes(row[tenantField]));
189
+ const hasOffender = rows.some((row) => !allowedTenantIds.some((allowed) => allowed === row[tenantField]));
183
190
  if (hasOffender) {
184
191
  throw new AccessDeniedError({
185
192
  message: `systemScope() row tenant self-check failed on field "${String(tenantField)}"`,
@@ -12,6 +12,7 @@
12
12
  // Used by both the user-data-rights-defaults forget hook and the
13
13
  // data-retention hardDelete cleanup — moved here so neither copies the logic.
14
14
  import { assertSafeStorageKey } from "../files/types.js";
15
+ import { createFallbackLogger } from "../logging/utils.js";
15
16
  import { derivativeListPrefix, isDerivativeKeyOf } from "./variant-key.js";
16
17
  // Wrap rather than let a raw provider error (e.g. S3 AccessDenied) surface
17
18
  // unexplained — list() is a requirement on top of delete(); a bucket policy
@@ -62,8 +63,10 @@ export async function deleteStoredFileAndDerivatives(originalKey, store, errorCo
62
63
  }
63
64
  catch (err) {
64
65
  failedKeys.push(key);
65
- // biome-ignore lint/suspicious/noConsole: operator-visibility for binary-cleanup-failure
66
- console.warn(`[${errorContext}] storage delete failed key=${key} err=${err instanceof Error ? err.message : String(err)}`);
66
+ createFallbackLogger(errorContext).warn("storage delete failed", {
67
+ key,
68
+ error: err instanceof Error ? err.message : String(err),
69
+ });
67
70
  }
68
71
  }
69
72
  return failedKeys;
@@ -116,7 +116,7 @@ async function resolveOverlaySpec(spec, row, registry, tenantId) {
116
116
  // allowlisted before it becomes a response Content-Type or storage-provider
117
117
  // metadata. Otherwise an app whose renderer wildcard is broad enough (e.g.
118
118
  // `image/*`) would let a client-declared `image/svg+xml` ride straight
119
- // through as an honestly-declared (non-sniffed) active-content type (#2021).
119
+ // through as an honestly-declared (non-sniffed) active-content type.
120
120
  const KNOWN_SAFE_VARIANT_MIME_TYPES = new Set([
121
121
  "image/jpeg",
122
122
  "image/png",
@@ -1,4 +1,6 @@
1
+ import { createFallbackLogger } from "../../logging/utils.js";
1
2
  import { normalizeAccessEntry } from "../ownership.js";
3
+ const log = createFallbackLogger("kumiko:boot");
2
4
  const BUILTIN_ROLES = new Set(["all", "system"]);
3
5
  function addRole(roleHandlers, role, identifier) {
4
6
  let handlers = roleHandlers.get(role);
@@ -50,7 +52,7 @@ function addEntityFieldRoles(roleHandlers, f) {
50
52
  // purpose) are the normal case, not a typo — and until every access
51
53
  // surface is scanned, a role can look unique here while it's really used
52
54
  // elsewhere (configKeys / entity+field access), a false positive in the
53
- // other direction. Both is why this stays opt-in (#1711) rather than a
55
+ // other direction. This is why this stays opt-in rather than a
54
56
  // default-on prod warning.
55
57
  export function warnOnUniqueAccessRoles(features) {
56
58
  // role → set of distinct identifiers using it, across every access
@@ -68,8 +70,7 @@ export function warnOnUniqueAccessRoles(features) {
68
70
  if (handlers.size !== 1)
69
71
  continue;
70
72
  const [identifier] = handlers;
71
- // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
72
- console.warn(`[kumiko:boot] Access role "${role}" is only used by one handler (${identifier}). ` +
73
+ log.warn(`Access role "${role}" is only used by one handler (${identifier}). ` +
73
74
  `This is often a typo — the role is unknown to every other handler in the boot scan. ` +
74
75
  `If this is intentional, ignore this warning.`);
75
76
  }
@@ -1,16 +1,17 @@
1
+ import { createFallbackLogger } from "../../logging/utils.js";
2
+ const log = createFallbackLogger("kumiko:boot");
1
3
  // --- Cross-feature API exposure / usage matching ---
2
4
  //
3
5
  // `r.exposesApi(name, impl)` registers a callable; `r.usesApi(name)`
4
- // declares a caller. Boot-Validator prüft drei Invarianten:
5
- // 1. Jeder usesApi(name) findet einen exposesApi(name) in irgendeinem
6
- // Feature.
7
- // 2. Das exposing-Feature ist in requires/optionalRequires des callers
8
- // gelisted (sonst klappt die Cross-Feature-Aufruf-Reihenfolge nicht).
9
- // 3. Self-exposure ist erlaubt (Feature ruft eigene API), wird aber
10
- // mit Warning markiert weil es typisch ein Refactor-Restbestand ist.
6
+ // declares a caller. Boot validation checks three invariants:
7
+ // 1. Every usesApi(name) finds an exposesApi(name) in some feature.
8
+ // 2. The exposing feature is listed in the caller's
9
+ // requires/optionalRequires so cross-feature call ordering works.
10
+ // 3. Self-exposure is allowed (a feature calling its own API), but is
11
+ // warned about because it is typically a refactoring leftover.
11
12
  //
12
- // Globale Eindeutigkeit der apiNames (kein Dublicate über Features)
13
- // wird in validateBoot() vor dem Per-Feature-Walk geprüft.
13
+ // apiNames must be globally unique across features; validateBoot() checks
14
+ // this before the per-feature walk.
14
15
  export function validateApiExposureMatching(feature, allExposedApis, featureMap) {
15
16
  for (const apiName of feature.usedApis) {
16
17
  const providerFeature = allExposedApis.get(apiName);
@@ -19,17 +20,16 @@ export function validateApiExposureMatching(feature, allExposedApis, featureMap)
19
20
  throw new Error(`[Feature ${feature.name}] r.usesApi("${apiName}") but no feature exposes that API. Known exposed APIs: ${known}`);
20
21
  }
21
22
  if (providerFeature === feature.name) {
22
- // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
23
- console.warn(`[kumiko:boot] [Feature ${feature.name}] r.usesApi("${apiName}") on its own r.exposesApi — typically a refactor leftover. Call the impl directly instead.`);
23
+ log.warn(`[Feature ${feature.name}] r.usesApi("${apiName}") on its own r.exposesApi — typically a refactor leftover. Call the impl directly instead.`);
24
24
  continue;
25
25
  }
26
26
  const allDeps = [...feature.requires, ...feature.optionalRequires];
27
27
  if (!allDeps.includes(providerFeature)) {
28
28
  throw new Error(`[Feature ${feature.name}] r.usesApi("${apiName}") is exposed by "${providerFeature}" but feature is not in requires/optionalRequires. Add r.requires({ features: ["${providerFeature}"] }).`);
29
29
  }
30
- // Sanity: provider feature actually exists in this app's feature set.
31
- // Should always be true if allExposedApis was built from `features`,
32
- // aber defensiv für unklare Constructor-Pfade.
30
+ // Sanity: the provider feature must exist in this app's feature set.
31
+ // This should always hold when allExposedApis was built from `features`,
32
+ // but remains defensive for unclear constructor paths.
33
33
  if (!featureMap.has(providerFeature)) {
34
34
  throw new Error(`[Feature ${feature.name}] internal: r.usesApi("${apiName}") points to provider "${providerFeature}" which is not in feature map. Register the provider feature in the app's feature list.`);
35
35
  }
@@ -1,5 +1,7 @@
1
1
  import { optionsQueryFieldRefs } from "@cosmicdrift/kumiko-types/fields";
2
+ import { createFallbackLogger } from "../../logging/utils.js";
2
3
  import { isEncryptedAtRest } from "../config-helpers.js";
4
+ const log = createFallbackLogger("kumiko:boot");
3
5
  // --- Toggleable-dependency warnings ---
4
6
  //
5
7
  // When feature A declares r.requires({ features: ["B"] }) and B is toggleable with
@@ -15,8 +17,7 @@ export function warnOnToggleableDependencies(features, featureMap) {
15
17
  if (!depFeature)
16
18
  continue; // requires-target-missing is handled elsewhere
17
19
  if (depFeature.toggleableDefault === false) {
18
- // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
19
- console.warn(`[kumiko:boot] Feature "${f.name}" requires "${dep}", which is toggleable(default=false). ` +
20
+ log.warn(`Feature "${f.name}" requires "${dep}", which is toggleable(default=false). ` +
20
21
  `"${f.name}" will be effectively disabled until "${dep}" is enabled globally via the feature-toggles feature. ` +
21
22
  `If this is intentional, ignore this warning; otherwise consider r.optionalRequires() or default=true.`);
22
23
  }
@@ -70,9 +71,9 @@ export function validateConfigKeyRequired(feature) {
70
71
  for (const [keyName, keyDef] of Object.entries(feature.configKeys ?? {})) {
71
72
  if (keyDef.required !== true)
72
73
  continue;
73
- // required heißt "Tenant MUSS konfigurieren" — ein non-empty default
74
- // oder ein computed-Resolver macht den Key nie unset, readiness könnte
75
- // die Lücke nie melden: der required-Flag wäre eine stille Lüge.
74
+ // `required` means the tenant must configure the key. A non-empty default
75
+ // or computed resolver means the key can never be unset, so readiness could
76
+ // never report the omission and the flag would be misleading.
76
77
  if (keyDef.computed !== undefined) {
77
78
  throw new Error(`[Feature ${feature.name}] Config key "${keyName}" has required=true AND a computed resolver — a computed key can never be missing; drop one of the two`);
78
79
  }
@@ -160,7 +161,7 @@ export function validateConfigKeyBacking(feature) {
160
161
  // read/write without it throws loud at request time, not silently.
161
162
  }
162
163
  }
163
- // --- Config key piiEncrypted compatibility (kumiko-platform#231/#459) ---
164
+ // --- Config key piiEncrypted compatibility ---
164
165
  export function validateConfigKeyPiiEncrypted(feature) {
165
166
  for (const [keyName, keyDef] of Object.entries(feature.configKeys)) {
166
167
  if (!keyDef.piiEncrypted)
@@ -2,9 +2,9 @@ import { qualifyEntityName } from "../qualified-name.js";
2
2
  import { findEntityFeature } from "./screens.js";
3
3
  // Entity name → the one screen that declares detailFor: "<entity>". Built
4
4
  // once, up front (before the per-feature validateScreens loop) so both this
5
- // module's own uniqueness check AND rowAction entity-targets (fw#2228,
6
- // screens.ts) resolve against the same map instead of re-walking every
7
- // feature's screens twice.
5
+ // module's own uniqueness check AND rowAction entity-targets (in screens.ts)
6
+ // resolve against the same map instead of re-walking every feature's
7
+ // screens twice.
8
8
  export function collectDetailForScreens(features, featureMap) {
9
9
  const screenQnByEntity = new Map();
10
10
  const result = new Map();