@cosmicdrift/kumiko-framework 0.386.0 → 0.387.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/LICENSE +21 -57
  2. package/README.md +2 -2
  3. package/agents/widgets.md +39 -39
  4. package/dist/api/api-constants.js +2 -2
  5. package/dist/api/auth-middleware.js +7 -7
  6. package/dist/api/auth-rate-limiters.js +7 -3
  7. package/dist/api/auth-routes-mfa.js +1 -1
  8. package/dist/api/auth-routes-session.js +1 -1
  9. package/dist/api/auth-routes-shared.js +7 -3
  10. package/dist/api/auth-routes-signup.js +1 -1
  11. package/dist/api/client-ip.js +3 -1
  12. package/dist/api/extra-route.js +1 -1
  13. package/dist/api/pii-leak-guard.js +4 -2
  14. package/dist/api/readiness.js +5 -5
  15. package/dist/api/redis-sse-broker.js +8 -6
  16. package/dist/api/routes.js +2 -2
  17. package/dist/api/server.js +34 -33
  18. package/dist/api/sse-broker.js +1 -1
  19. package/dist/api/sse-route.d.ts +13 -13
  20. package/dist/api/sse-route.js +15 -15
  21. package/dist/auth/index.js +4 -4
  22. package/dist/auth/roles.d.ts +4 -4
  23. package/dist/auth/roles.js +24 -24
  24. package/dist/compliance/duration-spec.js +18 -20
  25. package/dist/compliance/index.js +2 -3
  26. package/dist/compliance/override-schema.d.ts +4 -4
  27. package/dist/compliance/override-schema.js +19 -22
  28. package/dist/compliance/profiles.d.ts +26 -29
  29. package/dist/compliance/profiles.js +62 -69
  30. package/dist/compliance/sub-processors.d.ts +28 -30
  31. package/dist/compliance/sub-processors.js +17 -21
  32. package/dist/consumer-cli.js +4 -4
  33. package/dist/crypto/blind-index.js +18 -17
  34. package/dist/crypto/ciphertext-pattern.js +1 -1
  35. package/dist/crypto/event-pii.js +3 -3
  36. package/dist/crypto/kek-source.d.ts +1 -1
  37. package/dist/crypto/kek-source.js +3 -2
  38. package/dist/crypto/kms-adapter.d.ts +1 -1
  39. package/dist/crypto/kms-adapter.js +4 -6
  40. package/dist/crypto/kms-wiring.d.ts +1 -2
  41. package/dist/crypto/kms-wiring.js +2 -2
  42. package/dist/crypto/pg-kms-adapter.js +2 -6
  43. package/dist/crypto/pii-field-encryption.js +4 -4
  44. package/dist/crypto/subject-resolver.d.ts +1 -1
  45. package/dist/crypto/subject-resolver.js +2 -2
  46. package/dist/db/apply-entity-event.d.ts +6 -6
  47. package/dist/db/apply-entity-event.js +55 -55
  48. package/dist/db/blind-index-cleanup.js +7 -7
  49. package/dist/db/collect-table-metas.js +10 -11
  50. package/dist/db/compound-types.d.ts +1 -1
  51. package/dist/db/compound-types.js +12 -12
  52. package/dist/db/cursor.js +3 -4
  53. package/dist/db/dialect.d.ts +2 -2
  54. package/dist/db/dialect.js +2 -2
  55. package/dist/db/eagerload.d.ts +3 -3
  56. package/dist/db/eagerload.js +21 -12
  57. package/dist/db/entity-table-meta.d.ts +2 -2
  58. package/dist/db/entity-table-meta.js +4 -4
  59. package/dist/db/event-store-executor-context.d.ts +1 -1
  60. package/dist/db/event-store-executor-context.js +13 -13
  61. package/dist/db/event-store-executor-read.js +27 -28
  62. package/dist/db/event-store-executor-write.js +65 -70
  63. package/dist/db/located-timestamp.d.ts +11 -11
  64. package/dist/db/located-timestamp.js +29 -30
  65. package/dist/db/migrate-generator.d.ts +1 -1
  66. package/dist/db/migrate-generator.js +26 -19
  67. package/dist/db/migrate-runner.js +25 -25
  68. package/dist/db/money.d.ts +6 -6
  69. package/dist/db/money.js +29 -27
  70. package/dist/db/queries/backfill-pii.js +5 -5
  71. package/dist/db/queries/ddl.js +1 -1
  72. package/dist/db/queries/es-ops.d.ts +1 -1
  73. package/dist/db/queries/es-ops.js +1 -1
  74. package/dist/db/queries/event-consumer.js +3 -3
  75. package/dist/db/queries/event-store.js +4 -2
  76. package/dist/db/queries/projection-rebuild.js +3 -3
  77. package/dist/db/queries/raw-sql.d.ts +2 -2
  78. package/dist/db/queries/raw-sql.js +2 -2
  79. package/dist/db/queries/shadow-swap.js +19 -19
  80. package/dist/db/query.d.ts +1 -1
  81. package/dist/db/query.js +6 -6
  82. package/dist/db/rebuild-marker.js +20 -21
  83. package/dist/db/replay-migration-sql.js +2 -2
  84. package/dist/db/schema-inspection.js +4 -5
  85. package/dist/db/table-builder.d.ts +1 -2
  86. package/dist/db/table-builder.js +82 -82
  87. package/dist/db/tenant-db.d.ts +2 -2
  88. package/dist/db/tenant-db.js +9 -2
  89. package/dist/derivatives/delete-stored-file.js +5 -2
  90. package/dist/derivatives/derivatives-context.js +1 -1
  91. package/dist/engine/boot-validator/access-roles.js +4 -3
  92. package/dist/engine/boot-validator/api-ext.js +14 -14
  93. package/dist/engine/boot-validator/config-deps.js +7 -6
  94. package/dist/engine/boot-validator/detail-screens.js +3 -3
  95. package/dist/engine/boot-validator/entity-handler.js +60 -61
  96. package/dist/engine/boot-validator/gdpr-storage.js +4 -4
  97. package/dist/engine/boot-validator/i18n-keys.js +1 -1
  98. package/dist/engine/boot-validator/index.d.ts +2 -2
  99. package/dist/engine/boot-validator/index.js +20 -20
  100. package/dist/engine/boot-validator/nav.js +5 -5
  101. package/dist/engine/boot-validator/ownership.js +12 -9
  102. package/dist/engine/boot-validator/parent-ref.js +1 -1
  103. package/dist/engine/boot-validator/pii-retention.js +50 -57
  104. package/dist/engine/boot-validator/projection-list-screens.js +7 -7
  105. package/dist/engine/boot-validator/query-refs.js +1 -1
  106. package/dist/engine/boot-validator/screens-actions.js +15 -16
  107. package/dist/engine/boot-validator/screens-forms.js +10 -11
  108. package/dist/engine/boot-validator/screens-lists.js +14 -15
  109. package/dist/engine/boot-validator/screens.js +34 -40
  110. package/dist/engine/boot-validator/security-baseline.js +3 -2
  111. package/dist/engine/boot-validator/transfer-graph.js +2 -2
  112. package/dist/engine/boot-validator/workspaces.js +3 -3
  113. package/dist/engine/bridge-stub.js +3 -4
  114. package/dist/engine/build-app-schema.js +75 -75
  115. package/dist/engine/build-config-feature-schema.js +50 -50
  116. package/dist/engine/build-target.js +20 -23
  117. package/dist/engine/codemod/pipeline-codemod.js +20 -13
  118. package/dist/engine/config-helpers.js +4 -4
  119. package/dist/engine/constants.js +2 -2
  120. package/dist/engine/create-app.js +2 -2
  121. package/dist/engine/define-feature.js +12 -11
  122. package/dist/engine/define-handler.js +12 -11
  123. package/dist/engine/define-step.js +1 -1
  124. package/dist/engine/define-workflow.d.ts +5 -5
  125. package/dist/engine/define-workflow.js +6 -6
  126. package/dist/engine/entity-handlers.js +13 -14
  127. package/dist/engine/event-def.js +2 -2
  128. package/dist/engine/extension-names.d.ts +16 -17
  129. package/dist/engine/extension-names.js +37 -38
  130. package/dist/engine/extensions/escape-hatch-usage.js +1 -1
  131. package/dist/engine/extensions/tenant-data.js +2 -2
  132. package/dist/engine/extensions/user-data.d.ts +23 -24
  133. package/dist/engine/factories.d.ts +14 -13
  134. package/dist/engine/factories.js +38 -37
  135. package/dist/engine/feature-ast/entity-field-types.js +1 -1
  136. package/dist/engine/feature-ast/extractors/handlers.js +1 -1
  137. package/dist/engine/feature-ast/extractors/hooks.js +1 -1
  138. package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
  139. package/dist/engine/feature-ast/extractors/shared.js +2 -3
  140. package/dist/engine/feature-ast/parse.js +13 -13
  141. package/dist/engine/feature-ast/patch.js +1 -1
  142. package/dist/engine/feature-ast/pattern-change-schema.js +1 -1
  143. package/dist/engine/feature-ast/patterns.d.ts +2 -2
  144. package/dist/engine/feature-ast/render.js +2 -2
  145. package/dist/engine/feature-config-events-jobs.js +15 -12
  146. package/dist/engine/feature-entity-handlers.js +9 -5
  147. package/dist/engine/feature-manifest.d.ts +9 -9
  148. package/dist/engine/feature-manifest.js +6 -7
  149. package/dist/engine/feature-ui-extensions.d.ts +3 -3
  150. package/dist/engine/feature-ui-extensions.js +30 -22
  151. package/dist/engine/field-access.js +5 -6
  152. package/dist/engine/index.d.ts +1 -0
  153. package/dist/engine/index.js +4 -4
  154. package/dist/engine/object-form.d.ts +1 -0
  155. package/dist/engine/object-form.js +14 -4
  156. package/dist/engine/ownership.js +3 -3
  157. package/dist/engine/parse-ref-target.js +5 -7
  158. package/dist/engine/pattern-library/index.js +2 -2
  159. package/dist/engine/pattern-library/library.js +2 -2
  160. package/dist/engine/pattern-library/types.js +3 -3
  161. package/dist/engine/pipeline.js +2 -2
  162. package/dist/engine/project-app-schema-for-roles.js +1 -1
  163. package/dist/engine/projection-helpers.js +9 -9
  164. package/dist/engine/qualified-name.js +2 -2
  165. package/dist/engine/registry-facade.js +1 -1
  166. package/dist/engine/registry-ingest.js +12 -12
  167. package/dist/engine/registry-state.js +17 -19
  168. package/dist/engine/registry-validate.js +12 -12
  169. package/dist/engine/run-in.js +2 -2
  170. package/dist/engine/run-pipeline.js +7 -0
  171. package/dist/engine/schema-builder.js +40 -38
  172. package/dist/engine/screen-filter-ops.js +17 -20
  173. package/dist/engine/screen-helpers.js +4 -3
  174. package/dist/engine/state-machine.d.ts +12 -12
  175. package/dist/engine/state-machine.js +7 -7
  176. package/dist/engine/steps/_resolver-utils.js +4 -3
  177. package/dist/engine/steps/for-each.js +4 -4
  178. package/dist/engine/steps/unsafe-projection-delete.js +1 -1
  179. package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
  180. package/dist/engine/steps/wait-for-event.js +4 -4
  181. package/dist/engine/steps/workflow-system-events.js +3 -3
  182. package/dist/engine/tier-resolver-extension.d.ts +29 -27
  183. package/dist/engine/tier-resolver-extension.js +26 -26
  184. package/dist/engine/transfer-adjacency.js +2 -2
  185. package/dist/engine/validate-projection-allowlist.js +7 -27
  186. package/dist/engine/walk-step-instances.d.ts +2 -0
  187. package/dist/engine/walk-step-instances.js +38 -0
  188. package/dist/engine/where-rule-lint.js +1 -1
  189. package/dist/engine/write-handler-qn-extract.js +1 -1
  190. package/dist/entrypoint/index.js +3 -3
  191. package/dist/env/index.d.ts +7 -6
  192. package/dist/env/index.js +10 -9
  193. package/dist/errors/transition-details.js +7 -7
  194. package/dist/errors/write-error-info.d.ts +6 -6
  195. package/dist/errors/write-error-info.js +6 -6
  196. package/dist/es-ops/context.d.ts +9 -9
  197. package/dist/es-ops/context.js +28 -28
  198. package/dist/es-ops/index.js +7 -9
  199. package/dist/es-ops/operations-schema.js +4 -10
  200. package/dist/es-ops/runner.d.ts +11 -11
  201. package/dist/es-ops/runner.js +49 -53
  202. package/dist/es-ops/types.d.ts +55 -55
  203. package/dist/es-ops/types.js +11 -11
  204. package/dist/event-store/admin-api.js +6 -7
  205. package/dist/event-store/errors.js +1 -1
  206. package/dist/event-store/event-store.js +2 -2
  207. package/dist/event-store/rebuild-dead-letter.js +1 -1
  208. package/dist/files/feature.js +9 -10
  209. package/dist/files/file-handle.js +5 -6
  210. package/dist/files/file-ref-entity.js +15 -15
  211. package/dist/files/file-ref-table.js +7 -8
  212. package/dist/files/file-routes.d.ts +7 -0
  213. package/dist/files/file-routes.js +11 -5
  214. package/dist/files/in-memory-provider.js +8 -9
  215. package/dist/files/local-provider.js +18 -25
  216. package/dist/files/provider-resolver.d.ts +7 -7
  217. package/dist/files/storage-tracking.js +2 -2
  218. package/dist/files/zip-stream.d.ts +12 -19
  219. package/dist/files/zip-stream.js +44 -63
  220. package/dist/i18n/required-surface-keys.d.ts +1 -1
  221. package/dist/i18n/required-surface-keys.js +8 -8
  222. package/dist/jobs/job-runner-helpers.js +8 -2
  223. package/dist/jobs/job-runner-schedulers.d.ts +1 -1
  224. package/dist/jobs/job-runner-schedulers.js +3 -4
  225. package/dist/jobs/job-runner.js +13 -15
  226. package/dist/lifecycle/lifecycle.js +3 -5
  227. package/dist/lifecycle/signal-handlers.js +5 -4
  228. package/dist/logging/utils.d.ts +2 -1
  229. package/dist/logging/utils.js +38 -10
  230. package/dist/migrations/index.js +1 -1
  231. package/dist/migrations/kumiko-drift.js +14 -13
  232. package/dist/migrations/pending-rebuilds.d.ts +21 -20
  233. package/dist/migrations/pending-rebuilds.js +27 -27
  234. package/dist/migrations/projection-table-index.d.ts +3 -3
  235. package/dist/migrations/projection-table-index.js +8 -8
  236. package/dist/observability/metric-validator.js +1 -1
  237. package/dist/observability/metrics-handle.js +3 -3
  238. package/dist/pipeline/append-event-core.js +11 -2
  239. package/dist/pipeline/cascade-handler.d.ts +1 -1
  240. package/dist/pipeline/dispatch-batch.js +2 -2
  241. package/dist/pipeline/dispatch-query.js +10 -10
  242. package/dist/pipeline/dispatch-shared.d.ts +1 -0
  243. package/dist/pipeline/dispatch-shared.js +15 -17
  244. package/dist/pipeline/dispatch-stream.js +1 -1
  245. package/dist/pipeline/dispatch-write.js +3 -3
  246. package/dist/pipeline/dispatcher-utils.d.ts +1 -1
  247. package/dist/pipeline/dispatcher-utils.js +1 -1
  248. package/dist/pipeline/event-consumer-state.js +2 -2
  249. package/dist/pipeline/event-dispatcher-delivery.js +1 -1
  250. package/dist/pipeline/event-dispatcher.js +17 -28
  251. package/dist/pipeline/irreversible-operation-gate.js +1 -1
  252. package/dist/pipeline/lifecycle-pipeline.js +9 -9
  253. package/dist/pipeline/member-read-only-transaction.js +1 -3
  254. package/dist/pipeline/msp-rebuild.js +1 -1
  255. package/dist/pipeline/projection-rebuild.js +13 -12
  256. package/dist/pipeline/system-hooks.js +39 -41
  257. package/dist/pipeline/system-identity-switch.js +1 -3
  258. package/dist/pipeline/tenant-timezone-cache.js +1 -1
  259. package/dist/random/generate.d.ts +29 -29
  260. package/dist/random/generate.js +30 -30
  261. package/dist/random/words.js +20 -20
  262. package/dist/rate-limit/middleware.js +9 -5
  263. package/dist/redis/client.js +2 -2
  264. package/dist/redis/pubsub-signal.js +14 -17
  265. package/dist/schema-cli.js +116 -22
  266. package/dist/search/index.js +4 -4
  267. package/dist/search/purge-subject.js +4 -5
  268. package/dist/search/reindex-entity.js +6 -9
  269. package/dist/time/boot-tz-warning.d.ts +2 -2
  270. package/dist/time/boot-tz-warning.js +8 -8
  271. package/dist/time/iana.js +10 -10
  272. package/dist/time/index.js +9 -9
  273. package/dist/time/legacy-date.d.ts +2 -2
  274. package/dist/time/legacy-date.js +6 -8
  275. package/dist/time/polyfill.js +1 -1
  276. package/dist/time/tz-context.d.ts +1 -1
  277. package/dist/time/tz-context.js +1 -1
  278. package/dist/ui-types/app-schema.js +7 -7
  279. package/dist/ui-types/list-row-meta.js +4 -4
  280. package/dist/upgrade-cli.js +10 -9
  281. package/dist/utils/compare.js +1 -1
  282. package/package.json +4 -4
  283. package/src/changes.json +48 -0
  284. package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
  285. package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
  286. package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
  287. package/src/scripts/codemod/pii-personal-migration.ts +17 -8
  288. package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
@@ -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();
@@ -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}.`);