@cosmicdrift/kumiko-samples 0.352.0 → 0.354.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 (186) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +18 -2
  3. package/packages/bundled-features/src/agent-tools/agent-doc-lint.ts +66 -0
  4. package/packages/bundled-features/src/agent-tools/agent-manifest.ts +26 -7
  5. package/packages/bundled-features/src/agent-tools/changes.json +12 -0
  6. package/packages/bundled-features/src/agent-tools/feature.ts +13 -2
  7. package/packages/bundled-features/src/agent-tools/index.ts +6 -1
  8. package/packages/bundled-features/src/agent-tools/manifest-deny-qns.ts +25 -0
  9. package/packages/bundled-features/src/agent-tools/tool-catalog.ts +4 -1
  10. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +1 -1
  11. package/packages/bundled-features/src/audit/changes.json +6 -0
  12. package/packages/bundled-features/src/audit/handlers/list.query.ts +20 -10
  13. package/packages/bundled-features/src/auth-email-password/changes.json +24 -0
  14. package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +1 -0
  15. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +3 -2
  16. package/packages/bundled-features/src/auth-email-password/handlers/invite-accept.write.ts +3 -2
  17. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +21 -0
  18. package/packages/bundled-features/src/auth-email-password/handlers/invite-signup-complete.write.ts +4 -1
  19. package/packages/bundled-features/src/auth-email-password/handlers/login.write.ts +1 -0
  20. package/packages/bundled-features/src/auth-email-password/handlers/reset-password.write.ts +1 -0
  21. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +7 -3
  22. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +53 -96
  23. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +1 -0
  24. package/packages/bundled-features/src/auth-email-password/handlers/verify-email.write.ts +1 -0
  25. package/packages/bundled-features/src/auth-email-password/index.ts +12 -0
  26. package/packages/bundled-features/src/auth-email-password/seeding.ts +38 -7
  27. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +83 -0
  28. package/packages/bundled-features/src/auth-email-password/signup-token-store.ts +9 -3
  29. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +7 -7
  30. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +15 -0
  31. package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +2 -2
  32. package/packages/bundled-features/src/auth-email-password/web/user-menu.tsx +1 -1
  33. package/packages/bundled-features/src/auth-mfa/changes.json +7 -0
  34. package/packages/bundled-features/src/auth-mfa/feature.ts +5 -0
  35. package/packages/bundled-features/src/auth-mfa/handlers/enable-confirm-preauth.write.ts +1 -0
  36. package/packages/bundled-features/src/auth-mfa/handlers/verify.write.ts +1 -0
  37. package/packages/bundled-features/src/auth-mfa/screens.ts +2 -2
  38. package/packages/bundled-features/src/auth-mfa/web/mfa-recovery-codes-reveal.tsx +6 -8
  39. package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +18 -21
  40. package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +2 -2
  41. package/packages/bundled-features/src/billing-foundation/billing-info-query.ts +7 -8
  42. package/packages/bundled-features/src/billing-foundation/changes.json +6 -0
  43. package/packages/bundled-features/src/billing-foundation/checkout-core.ts +14 -2
  44. package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +7 -1
  45. package/packages/bundled-features/src/cap-overview/access.ts +2 -9
  46. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +5 -2
  47. package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +6 -9
  48. package/packages/bundled-features/src/channel-in-app/changes.json +8 -1
  49. package/packages/bundled-features/src/channel-in-app/in-app-channel.ts +1 -1
  50. package/packages/bundled-features/src/channel-in-app/tables.ts +18 -12
  51. package/packages/bundled-features/src/compliance-profiles-ops/handlers/tenants-missing-profile.query.ts +5 -4
  52. package/packages/bundled-features/src/config/handlers/values.query.ts +7 -2
  53. package/packages/bundled-features/src/crypto-shredding/changes.json +12 -0
  54. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +154 -69
  55. package/packages/bundled-features/src/delivery/changes.json +6 -0
  56. package/packages/bundled-features/src/delivery/db/queries/address-opt-outs.ts +18 -12
  57. package/packages/bundled-features/src/delivery/delivery-service.ts +103 -16
  58. package/packages/bundled-features/src/delivery/public-names.ts +1 -1
  59. package/packages/bundled-features/src/delivery/unsubscribe.ts +81 -13
  60. package/packages/bundled-features/src/feature-toggles/feature.ts +2 -3
  61. package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +34 -5
  62. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +31 -8
  63. package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +10 -1
  64. package/packages/bundled-features/src/files/changes.json +24 -0
  65. package/packages/bundled-features/src/files-tenant-data/changes.json +5 -0
  66. package/packages/bundled-features/src/files-tenant-data/handlers/sweep-orphaned-derivatives.job.ts +4 -0
  67. package/packages/bundled-features/src/files-tenant-data/hooks.ts +75 -25
  68. package/packages/bundled-features/src/folders/changes.json +6 -0
  69. package/packages/bundled-features/src/folders/feature.ts +2 -12
  70. package/packages/bundled-features/src/folders/handlers/delete-folder.write.ts +41 -9
  71. package/packages/bundled-features/src/form-draft/changes.json +7 -1
  72. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +4 -2
  73. package/packages/bundled-features/src/form-draft/handlers/cleanup.job.ts +18 -6
  74. package/packages/bundled-features/src/foundation-shared/index.ts +1 -0
  75. package/packages/bundled-features/src/foundation-shared/mail-host-policy.ts +16 -4
  76. package/packages/bundled-features/src/inbound-provider-imap/changes.json +6 -0
  77. package/packages/bundled-features/src/inbound-provider-imap/feature.ts +2 -0
  78. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +3 -4
  79. package/packages/bundled-features/src/jobs/changes.json +30 -0
  80. package/packages/bundled-features/src/jobs/constants.ts +1 -0
  81. package/packages/bundled-features/src/jobs/db/queries/retention.ts +12 -0
  82. package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +26 -2
  83. package/packages/bundled-features/src/jobs/feature.ts +7 -1
  84. package/packages/bundled-features/src/jobs/handlers/list.query.ts +45 -12
  85. package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +1 -1
  86. package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +94 -0
  87. package/packages/bundled-features/src/jobs/i18n.ts +1 -0
  88. package/packages/bundled-features/src/jobs/index.ts +2 -0
  89. package/packages/bundled-features/src/jobs/job-run-logger.ts +177 -10
  90. package/packages/bundled-features/src/jobs/tenant-job-failure-table.ts +27 -8
  91. package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +55 -0
  92. package/packages/bundled-features/src/ledger/changes.json +23 -0
  93. package/packages/bundled-features/src/ledger/entity.ts +17 -5
  94. package/packages/bundled-features/src/ledger/feature.ts +3 -0
  95. package/packages/bundled-features/src/ledger/handlers/confirm-schedule-period.write.ts +15 -3
  96. package/packages/bundled-features/src/ledger/handlers/create-transaction.write.ts +1 -0
  97. package/packages/bundled-features/src/ledger/handlers/reverse-transaction.write.ts +1 -0
  98. package/packages/bundled-features/src/ledger/schedule-subject-pair.ts +27 -0
  99. package/packages/bundled-features/src/ledger/schemas.ts +8 -2
  100. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +4 -15
  101. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  102. package/packages/bundled-features/src/managed-pages/feature.ts +24 -11
  103. package/packages/bundled-features/src/notes-history/changes.json +13 -0
  104. package/packages/bundled-features/src/notes-history/entity.ts +4 -0
  105. package/packages/bundled-features/src/notes-history/feature.ts +2 -12
  106. package/packages/bundled-features/src/personal-access-tokens/handlers/create.write.ts +1 -0
  107. package/packages/bundled-features/src/personal-access-tokens/screens.ts +24 -3
  108. package/packages/bundled-features/src/seo/changes.json +5 -0
  109. package/packages/bundled-features/src/seo/feature.ts +23 -7
  110. package/packages/bundled-features/src/sessions/feature.ts +21 -8
  111. package/packages/bundled-features/src/sessions/handlers/detail.query.ts +34 -33
  112. package/packages/bundled-features/src/sessions/handlers/list.query.ts +68 -65
  113. package/packages/bundled-features/src/sessions/screens.ts +10 -8
  114. package/packages/bundled-features/src/shared/assert-no-where-rule-in-ownership-write.ts +25 -0
  115. package/packages/bundled-features/src/shared/encrypt-for-direct-write.ts +15 -8
  116. package/packages/bundled-features/src/shared/index.ts +4 -2
  117. package/packages/bundled-features/src/shared/parent-visibility.ts +31 -1
  118. package/packages/bundled-features/src/shared/rate-limited-text-response.ts +11 -0
  119. package/packages/bundled-features/src/shared/row-bound-grant.ts +18 -2
  120. package/packages/bundled-features/src/shared/session-locale-field.ts +7 -0
  121. package/packages/bundled-features/src/tags/feature.ts +2 -12
  122. package/packages/bundled-features/src/template-resolver/api.ts +3 -3
  123. package/packages/bundled-features/src/template-resolver/changes.json +5 -0
  124. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +2 -2
  125. package/packages/bundled-features/src/tenant/changes.json +19 -0
  126. package/packages/bundled-features/src/tenant/decrypt-tenant-names.ts +17 -0
  127. package/packages/bundled-features/src/tenant/feature.ts +8 -1
  128. package/packages/bundled-features/src/tenant/handlers/me.query.ts +4 -1
  129. package/packages/bundled-features/src/tenant/handlers/memberships.query.ts +7 -3
  130. package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +11 -5
  131. package/packages/bundled-features/src/tenant/handlers/toggle-enabled.write.ts +2 -0
  132. package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +1 -0
  133. package/packages/bundled-features/src/tenant/index.ts +1 -0
  134. package/packages/bundled-features/src/tenant/invitation-table.ts +6 -0
  135. package/packages/bundled-features/src/tenant/invited-membership.ts +45 -12
  136. package/packages/bundled-features/src/tenant/schema/tenant.ts +4 -6
  137. package/packages/bundled-features/src/tenant/screens.ts +1 -1
  138. package/packages/bundled-features/src/tenant-handover/changes.json +12 -0
  139. package/packages/bundled-features/src/tenant-handover/move-entity-graph.ts +12 -19
  140. package/packages/bundled-features/src/tenant-handover/transfer-graph.ts +14 -96
  141. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +16 -0
  142. package/packages/bundled-features/src/tenant-lifecycle/changes.json +22 -0
  143. package/packages/bundled-features/src/tenant-lifecycle/constants.ts +8 -0
  144. package/packages/bundled-features/src/tenant-lifecycle/events.ts +6 -0
  145. package/packages/bundled-features/src/tenant-lifecycle/feature.ts +14 -1
  146. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +105 -29
  147. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +64 -10
  148. package/packages/bundled-features/src/tenant-lifecycle/testing.ts +58 -0
  149. package/packages/bundled-features/src/tier-engine/changes.json +5 -0
  150. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +1 -0
  151. package/packages/bundled-features/src/user/changes.json +15 -0
  152. package/packages/bundled-features/src/user/handlers/detail.query.ts +1 -0
  153. package/packages/bundled-features/src/user/handlers/list.query.ts +8 -2
  154. package/packages/bundled-features/src/user/handlers/update.write.ts +1 -0
  155. package/packages/bundled-features/src/user/seeding.ts +34 -0
  156. package/packages/bundled-features/src/user-data-rights/boot-checks.ts +7 -0
  157. package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
  158. package/packages/bundled-features/src/user-data-rights/deletion-token.ts +1 -1
  159. package/packages/bundled-features/src/user-data-rights/handlers/confirm-deletion-by-token.write.ts +2 -2
  160. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +1 -0
  161. package/packages/bundled-features/src/user-data-rights/index.ts +1 -0
  162. package/packages/bundled-features/src/user-data-rights/schema/download-attempt.ts +17 -9
  163. package/packages/bundled-features/src/user-profile/handlers/change-email.write.ts +8 -2
  164. package/packages/bundled-features/src/workflow-runner/changes.json +5 -0
  165. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +80 -14
  166. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +11 -1
  167. package/packages/bundled-features/src/workflow-runner/runner.ts +8 -2
  168. package/samples/apps/showcase/src/features/items/feature.ts +2 -1
  169. package/samples/apps/showcase/src/features/items/schema/item.ts +0 -1
  170. package/samples/apps/styleguide/src/features/widgets/demo-locale.ts +47 -0
  171. package/samples/apps/styleguide/src/features/widgets/feature.ts +131 -15
  172. package/samples/apps/styleguide/src/features/widgets/i18n.ts +40 -0
  173. package/samples/apps/styleguide/src/features/widgets/web/DashboardFilterEcho.tsx +5 -4
  174. package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +4 -0
  175. package/samples/apps/ui-walkthrough/package.json +3 -0
  176. package/samples/apps/use-all-bundled/README.md +1 -2
  177. package/samples/apps/use-all-bundled/feature-manifest.json +1 -1
  178. package/samples/apps/user-data-rights-demo/README.md +1 -3
  179. package/samples/recipes/legal-pages/README.md +3 -4
  180. package/samples/recipes/managed-pages/README.md +2 -2
  181. package/samples/recipes/public-share-token/README.md +4 -5
  182. package/samples/recipes/public-share-token/src/feature.ts +1 -1
  183. package/samples/recipes/security-baseline/README.md +6 -0
  184. package/samples/recipes/seed-migration/README.md +0 -2
  185. package/samples/recipes/tenant-settings/README.md +1 -1
  186. package/packages/bundled-features/src/shared/has-where-rule.ts +0 -11
@@ -48,6 +48,7 @@ import {
48
48
  WORKFLOW_RUN_STARTED_TYPE,
49
49
  WORKFLOW_WAITING_FOR_EVENT_TYPE,
50
50
  type WorkflowDefinition,
51
+ WorkflowStepError,
51
52
  type WriteEvent,
52
53
  type WriteHandlerDef,
53
54
  } from "@cosmicdrift/kumiko-framework/engine";
@@ -92,6 +93,11 @@ function checkQ7Fingerprint(
92
93
  storedFingerprint: string | null,
93
94
  ): WorkflowRunFailedPayload | null {
94
95
  const currentFingerprint = computeDefinitionFingerprint(workflow);
96
+ if (storedFingerprint === null) {
97
+ log.warn(
98
+ `Run ${runId} of workflow "${workflowName}" has no stored definition fingerprint — resuming against the current definition unchecked.`,
99
+ );
100
+ }
95
101
  const fingerprintChanged = storedFingerprint !== null && currentFingerprint !== storedFingerprint;
96
102
  if (!fingerprintChanged) {
97
103
  return null;
@@ -387,9 +393,13 @@ async function runResumedPipeline(ctx: HandlerContext, input: ResumedPipelineInp
387
393
  return { isSuccess: true as const, data: { outcome: "completed" as const } };
388
394
  } catch (error) {
389
395
  log.warn("workflow run failed", { runId, workflowName, stepIndex, error: String(error) });
396
+ // The input stepIndex is the suspended step; the failure happened at the step the resumed pass reached.
390
397
  const failedPayload: WorkflowRunFailedPayload = {
391
398
  workflowName,
392
- stepIndex,
399
+ stepIndex:
400
+ error instanceof WorkflowStepError || error instanceof WorkflowSuspensionUnsupportedError
401
+ ? error.stepIndex
402
+ : stepIndex,
393
403
  error: describeWorkflowStepError(error),
394
404
  };
395
405
  await appendRunFailed(ctx, runId, failedPayload);
@@ -75,6 +75,8 @@ export type WorkflowRunFailedPayload = {
75
75
  readonly reason?: string;
76
76
  };
77
77
 
78
+ export type WorkflowRunContext = Pick<HandlerContext, "unsafeAppendEvent">;
79
+
78
80
  export class WorkflowSuspensionUnsupportedError extends Error {
79
81
  constructor(
80
82
  readonly workflowName: string,
@@ -102,7 +104,7 @@ export async function startAndRunWorkflow(args: {
102
104
  readonly triggerEvent: WriteEvent;
103
105
  readonly triggerEventRef: TriggerEventRef;
104
106
  readonly idempotencyKey?: string;
105
- readonly handlerCtx: HandlerContext;
107
+ readonly handlerCtx: WorkflowRunContext;
106
108
  }): Promise<{ readonly outcome: "completed" | "suspended" }> {
107
109
  const fingerprint = computeDefinitionFingerprint(args.workflow);
108
110
 
@@ -123,10 +125,14 @@ export async function startAndRunWorkflow(args: {
123
125
 
124
126
  const steps = buildPipelineSteps(args.workflow.pipelineDef, args.triggerEvent);
125
127
 
128
+ // @cast-boundary msp-to-handler-ctx — event-trigger hands over the narrower
129
+ // MultiStreamApplyContext (no query/write/writeAs). The step vocabulary this
130
+ // runner drives (wait, compute, return) only ever calls unsafeAppendEvent.
131
+ const stepCtx = args.handlerCtx as unknown as HandlerContext;
126
132
  const outcome = await runStepList(
127
133
  steps,
128
134
  args.triggerEvent,
129
- args.handlerCtx,
135
+ stepCtx,
130
136
  {},
131
137
  {},
132
138
  {
@@ -8,6 +8,7 @@
8
8
  // hängt hier die Handler dran und ruft r.screen/r.nav.
9
9
 
10
10
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
11
+ import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
11
12
  import { itemsTranslations } from "./i18n";
12
13
  import {
13
14
  itemActiveScreen,
@@ -29,7 +30,7 @@ const open = {
29
30
 
30
31
  // The boot validator checks server-registered keys, while the client bundle is
31
32
  // locale-first — flip it rather than maintaining the same strings twice.
32
- function keyFirst(byLocale: typeof itemsTranslations): Record<string, Record<string, string>> {
33
+ function keyFirst(byLocale: TranslationsByLocale): Record<string, Record<string, string>> {
33
34
  const out: Record<string, Record<string, string>> = {};
34
35
  for (const [locale, entries] of Object.entries(byLocale)) {
35
36
  for (const [key, value] of Object.entries(entries)) {
@@ -53,7 +53,6 @@ export const itemEditScreen: EntityEditScreenDefinition = {
53
53
  type: "entityEdit",
54
54
  entity: "item",
55
55
  layout: {
56
- width: "3xl",
57
56
  sections: [
58
57
  {
59
58
  title: "Basics",
@@ -0,0 +1,47 @@
1
+ // Server-side locale helpers for the dashboard demo queries. Not shared with
2
+ // renderer-web's format.ts: server code must not import renderer-web.
3
+
4
+ import { WIDGETS_I18N } from "./i18n";
5
+
6
+ type WidgetsI18nKey = keyof typeof WIDGETS_I18N;
7
+ type DemoCurrency = "EUR" | "USD";
8
+
9
+ const FALLBACK_LOCALE = "en";
10
+
11
+ // Intl throws RangeError on malformed tags.
12
+ function usableLocale(locale: string): string {
13
+ try {
14
+ return Intl.getCanonicalLocales(locale)[0] ?? FALLBACK_LOCALE;
15
+ } catch {
16
+ return FALLBACK_LOCALE;
17
+ }
18
+ }
19
+
20
+ // The bundle only carries de + en: any "de" tag ("de-AT") picks de, everything else en.
21
+ export function demoText(key: WidgetsI18nKey, locale: string): string {
22
+ const entry = WIDGETS_I18N[key];
23
+ const primarySubtag = usableLocale(locale).split("-")[0];
24
+ return primarySubtag === "de" ? entry.de : entry.en;
25
+ }
26
+
27
+ export function demoMoney(amount: number, currency: DemoCurrency, locale: string): string {
28
+ return new Intl.NumberFormat(usableLocale(locale), {
29
+ style: "currency",
30
+ currency,
31
+ maximumFractionDigits: 0,
32
+ }).format(amount);
33
+ }
34
+
35
+ export function demoPercent(fraction: number, locale: string): string {
36
+ return new Intl.NumberFormat(usableLocale(locale), {
37
+ style: "percent",
38
+ maximumFractionDigits: 0,
39
+ }).format(fraction);
40
+ }
41
+
42
+ export function demoMonthYear(year: number, month: number, locale: string): string {
43
+ return Temporal.PlainDate.from({ year, month, day: 1 }).toLocaleString(usableLocale(locale), {
44
+ month: "short",
45
+ year: "numeric",
46
+ });
47
+ }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
7
7
  import { z } from "zod";
8
+ import { demoMoney, demoMonthYear, demoPercent, demoText } from "./demo-locale";
8
9
  import { WIDGETS_I18N } from "./i18n";
9
10
 
10
11
  // Statische Demo-Zeitreihe (48 Punkte à 30 Minuten) — kein Date-API,
@@ -14,6 +15,44 @@ const RESPONSE_POINTS = Array.from({ length: 48 }, (_, i) => ({
14
15
  value: i === 20 ? null : 120 + Math.round(80 * Math.abs(Math.sin(i / 5))),
15
16
  }));
16
17
 
18
+ const LOAN_START_MS = Date.UTC(2020, 0, 1);
19
+ const LOAN_TODAY_MS = Date.UTC(2026, 5, 15);
20
+ const LOAN_MONTHS = 240;
21
+ const LOAN_PRINCIPAL_MINOR = 25_000_000;
22
+ const LOAN_MONTHLY_RATE = 0.035 / 12;
23
+
24
+ const loanMonthMs = (monthIndex: number): number => Date.UTC(2020, monthIndex, 1);
25
+
26
+ // Annuity loan over 20 years: remaining principal falls, cumulative interest
27
+ // grows. "Today" is a fixed date, not the clock, so the window and the
28
+ // screenshots stay stable.
29
+ const growth = (1 + LOAN_MONTHLY_RATE) ** LOAN_MONTHS;
30
+ const annuityMinor = (LOAN_PRINCIPAL_MINOR * LOAN_MONTHLY_RATE * growth) / (growth - 1);
31
+ let balanceMinor: number = LOAN_PRINCIPAL_MINOR;
32
+ let interestPaidMinor = 0;
33
+ const LOAN_REMAINING_POINTS = [{ atMs: loanMonthMs(0), value: LOAN_PRINCIPAL_MINOR }];
34
+ const LOAN_INTEREST_POINTS = [{ atMs: loanMonthMs(0), value: 0 }];
35
+ for (let i = 1; i <= LOAN_MONTHS; i++) {
36
+ const interest = balanceMinor * LOAN_MONTHLY_RATE;
37
+ balanceMinor = Math.max(0, balanceMinor - (annuityMinor - interest));
38
+ interestPaidMinor += interest;
39
+ LOAN_REMAINING_POINTS.push({ atMs: loanMonthMs(i), value: Math.round(balanceMinor) });
40
+ LOAN_INTEREST_POINTS.push({ atMs: loanMonthMs(i), value: Math.round(interestPaidMinor) });
41
+ }
42
+ const LOAN_SERIES = [
43
+ { key: "remaining", label: "widgets:dashboard:loan-remaining", points: LOAN_REMAINING_POINTS },
44
+ { key: "interest", label: "widgets:dashboard:loan-interest", points: LOAN_INTEREST_POINTS },
45
+ ];
46
+ const LOAN_MARKERS = [
47
+ { atMs: loanMonthMs(84), label: { i18nKey: "widgets:dashboard:marker-extra-1" }, kind: "extra" },
48
+ { atMs: loanMonthMs(150), label: { i18nKey: "widgets:dashboard:marker-extra-2" }, kind: "extra" },
49
+ {
50
+ atMs: loanMonthMs(LOAN_MONTHS),
51
+ label: { i18nKey: "widgets:dashboard:marker-payoff" },
52
+ kind: "payoff",
53
+ },
54
+ ];
55
+
17
56
  const SENDERS = ["William Smith", "Alice Smith", "Bob Johnson", "Emily Davis"] as const;
18
57
  const SUBJECTS = ["Meeting Tomorrow", "Re: Project Update", "Weekend Plans", "Re: Budget"] as const;
19
58
 
@@ -33,6 +72,13 @@ const INBOX_MESSAGES = Array.from({ length: 18 }, (_, i) => ({
33
72
  unread: i % 4 === 0,
34
73
  }));
35
74
 
75
+ function openAmount(amount: number, locale: string): string {
76
+ return demoText("widgets:dashboard:demo-amount-open", locale).replace(
77
+ "{amount}",
78
+ demoMoney(amount, "EUR", locale),
79
+ );
80
+ }
81
+
36
82
  export const widgetsFeature = defineFeature("widgets", (r) => {
37
83
  r.screen({ id: "widgets", type: "custom", renderer: { react: { __component: "widgets" } } });
38
84
  r.screen({
@@ -96,6 +142,31 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
96
142
  chart: "timeseries",
97
143
  query: "widgets:query:metrics:response-times",
98
144
  },
145
+ {
146
+ kind: "chart",
147
+ id: "loan",
148
+ label: "widgets:dashboard:loan",
149
+ chart: "stacked-area",
150
+ query: "widgets:query:metrics:loan",
151
+ valueFormat: { kind: "currency", currency: "EUR", fractionDigits: 0 },
152
+ brush: true,
153
+ legendTotals: false,
154
+ initialWindow: "from-today",
155
+ markerLegend: "legend",
156
+ markerKinds: {
157
+ extra: { tone: "active", label: "widgets:dashboard:kind-extra" },
158
+ payoff: { tone: "positive", label: "widgets:dashboard:kind-payoff" },
159
+ },
160
+ ranges: {
161
+ default: "max",
162
+ options: [
163
+ { value: "y1", label: "widgets:dashboard:range-y1", months: 12 },
164
+ { value: "y3", label: "widgets:dashboard:range-y3", months: 36 },
165
+ { value: "y5", label: "widgets:dashboard:range-y5", months: 60 },
166
+ { value: "max", label: "widgets:dashboard:range-all" },
167
+ ],
168
+ },
169
+ },
99
170
  {
100
171
  kind: "list",
101
172
  id: "latest",
@@ -129,11 +200,14 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
129
200
  r.queryHandler(
130
201
  "metrics:portfolio-stat",
131
202
  z.object({ region: z.string().optional() }),
132
- async ({ payload: { region } }) => ({
133
- value: region === "us" ? "38.120 $" : region === "eu" ? "54.630 €" : "92.753 €",
134
- sub: "über 4 Konten",
203
+ async ({ payload: { region } }, ctx) => ({
204
+ value:
205
+ region === "us"
206
+ ? demoMoney(38120, "USD", ctx.locale)
207
+ : demoMoney(region === "eu" ? 54630 : 92753, "EUR", ctx.locale),
208
+ sub: demoText("widgets:catalog:portfolio-sub", ctx.locale),
135
209
  tone: "positive",
136
- delta: "12 %",
210
+ delta: demoPercent(0.12, ctx.locale),
137
211
  deltaDirection: "up",
138
212
  deltaTone: "positive",
139
213
  }),
@@ -150,7 +224,7 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
150
224
  r.queryHandler(
151
225
  "metrics:net-worth-assets",
152
226
  z.object({ region: z.string().optional() }),
153
- async () => ({ value: "120.000 €" }),
227
+ async (_query, ctx) => ({ value: demoMoney(120000, "EUR", ctx.locale) }),
154
228
  {
155
229
  access: {
156
230
  openToAll: {
@@ -164,7 +238,7 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
164
238
  r.queryHandler(
165
239
  "metrics:net-worth-debts",
166
240
  z.object({ region: z.string().optional() }),
167
- async () => ({ value: "65.370 €" }),
241
+ async (_query, ctx) => ({ value: demoMoney(65370, "EUR", ctx.locale) }),
168
242
  {
169
243
  access: {
170
244
  openToAll: {
@@ -194,12 +268,40 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
194
268
  },
195
269
  );
196
270
  r.queryHandler(
197
- "metrics:latest-items",
271
+ "metrics:loan",
198
272
  z.object({}),
199
273
  async () => ({
274
+ series: LOAN_SERIES,
275
+ markers: LOAN_MARKERS,
276
+ windowStartMs: LOAN_START_MS,
277
+ windowEndMs: loanMonthMs(LOAN_MONTHS),
278
+ todayMs: LOAN_TODAY_MS,
279
+ }),
280
+ {
281
+ access: {
282
+ openToAll: {
283
+ reason:
284
+ "demo dashboard widget: returns static canned data with no per-user or " +
285
+ "tenant scoping; any signed-in user may query it",
286
+ },
287
+ },
288
+ },
289
+ );
290
+ r.queryHandler(
291
+ "metrics:latest-items",
292
+ z.object({}),
293
+ async (_query, ctx) => ({
200
294
  rows: [
201
- { id: "i1", name: "API-Timeout eu-central", status: "resolved" },
202
- { id: "i2", name: "Zertifikat erneuert", status: "done" },
295
+ {
296
+ id: "i1",
297
+ name: demoText("widgets:dashboard:demo-incident-timeout", ctx.locale),
298
+ status: "resolved",
299
+ },
300
+ {
301
+ id: "i2",
302
+ name: demoText("widgets:dashboard:demo-incident-certificate", ctx.locale),
303
+ status: "done",
304
+ },
203
305
  ],
204
306
  nextCursor: null,
205
307
  }),
@@ -249,10 +351,16 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
249
351
  r.queryHandler(
250
352
  "metrics:upcoming-events",
251
353
  z.object({}),
252
- async () => ({
354
+ async (_query, ctx) => ({
253
355
  rows: [
254
- { primary: "Zinsanpassung Baudarlehen", trailing: "Aug 2026" },
255
- { primary: "Bausparvertrag zuteilungsreif", trailing: "Okt 2026" },
356
+ {
357
+ primary: demoText("widgets:dashboard:demo-event-rate-adjustment", ctx.locale),
358
+ trailing: demoMonthYear(2026, 8, ctx.locale),
359
+ },
360
+ {
361
+ primary: demoText("widgets:dashboard:demo-event-savings-contract", ctx.locale),
362
+ trailing: demoMonthYear(2026, 10, ctx.locale),
363
+ },
256
364
  ],
257
365
  }),
258
366
  {
@@ -268,10 +376,18 @@ export const widgetsFeature = defineFeature("widgets", (r) => {
268
376
  r.queryHandler(
269
377
  "metrics:goal-progress",
270
378
  z.object({}),
271
- async () => ({
379
+ async (_query, ctx) => ({
272
380
  rows: [
273
- { label: "Baudarlehen", value: "42.000 € offen", fraction: 0.71 },
274
- { label: "Autokredit", value: "3.200 € offen", fraction: 0.92 },
381
+ {
382
+ label: demoText("widgets:dashboard:demo-goal-mortgage", ctx.locale),
383
+ value: openAmount(42000, ctx.locale),
384
+ fraction: 0.71,
385
+ },
386
+ {
387
+ label: demoText("widgets:dashboard:demo-goal-car-loan", ctx.locale),
388
+ value: openAmount(3200, ctx.locale),
389
+ fraction: 0.92,
390
+ },
275
391
  ],
276
392
  }),
277
393
  {
@@ -19,6 +19,18 @@ export const WIDGETS_I18N = {
19
19
  "widgets:dashboard:net-worth-assets": { de: "Vermögen", en: "Assets" },
20
20
  "widgets:dashboard:net-worth-debts": { de: "Schulden", en: "Debts" },
21
21
  "widgets:dashboard:response-times": { de: "Antwortzeit", en: "Response time" },
22
+ "widgets:dashboard:loan": { de: "Kreditverlauf", en: "Loan balance" },
23
+ "widgets:dashboard:loan-remaining": { de: "Restschuld", en: "Remaining balance" },
24
+ "widgets:dashboard:loan-interest": { de: "Gezahlte Zinsen", en: "Interest paid" },
25
+ "widgets:dashboard:kind-extra": { de: "Sondertilgung", en: "Extra repayment" },
26
+ "widgets:dashboard:kind-payoff": { de: "Ablösung", en: "Payoff" },
27
+ "widgets:dashboard:marker-extra-1": { de: "Sondertilgung 2027", en: "Extra repayment 2027" },
28
+ "widgets:dashboard:marker-extra-2": { de: "Sondertilgung 2032", en: "Extra repayment 2032" },
29
+ "widgets:dashboard:marker-payoff": { de: "Kredit getilgt", en: "Loan repaid" },
30
+ "widgets:dashboard:range-y1": { de: "1 Jahr", en: "1 year" },
31
+ "widgets:dashboard:range-y3": { de: "3 Jahre", en: "3 years" },
32
+ "widgets:dashboard:range-y5": { de: "5 Jahre", en: "5 years" },
33
+ "widgets:dashboard:range-all": { de: "Alles", en: "All" },
22
34
  "widgets:dashboard:latest": { de: "Neueste Ereignisse", en: "Latest events" },
23
35
  "widgets:dashboard:col-name": { de: "Name", en: "Name" },
24
36
  "widgets:dashboard:col-status": { de: "Status", en: "Status" },
@@ -27,6 +39,34 @@ export const WIDGETS_I18N = {
27
39
  "widgets:dashboard:filter-region": { de: "Region", en: "Region" },
28
40
  "widgets:dashboard:filter-region-eu": { de: "Europa", en: "Europe" },
29
41
  "widgets:dashboard:filter-region-us": { de: "USA", en: "USA" },
42
+ "widgets:dashboard:filter-echo-title": { de: "Custom-Panel", en: "Custom panel" },
43
+ "widgets:dashboard:filter-echo-filtered": {
44
+ de: "Gefiltert nach Region: {region}",
45
+ en: "Filtered by region: {region}",
46
+ },
47
+ "widgets:dashboard:filter-echo-unfiltered": {
48
+ de: "Ungefiltert (alle Regionen)",
49
+ en: "Unfiltered (all regions)",
50
+ },
51
+ "widgets:dashboard:demo-incident-timeout": {
52
+ de: "API-Timeout eu-central",
53
+ en: "API timeout eu-central",
54
+ },
55
+ "widgets:dashboard:demo-incident-certificate": {
56
+ de: "Zertifikat erneuert",
57
+ en: "Certificate renewed",
58
+ },
59
+ "widgets:dashboard:demo-event-rate-adjustment": {
60
+ de: "Zinsanpassung Baudarlehen",
61
+ en: "Mortgage rate adjustment",
62
+ },
63
+ "widgets:dashboard:demo-event-savings-contract": {
64
+ de: "Bausparvertrag zuteilungsreif",
65
+ en: "Building savings contract ready for allocation",
66
+ },
67
+ "widgets:dashboard:demo-goal-mortgage": { de: "Baudarlehen", en: "Mortgage" },
68
+ "widgets:dashboard:demo-goal-car-loan": { de: "Autokredit", en: "Car loan" },
69
+ "widgets:dashboard:demo-amount-open": { de: "{amount} offen", en: "{amount} outstanding" },
30
70
  "widgets:catalog:portfolio": { de: "Portfolio", en: "Portfolio" },
31
71
  "widgets:catalog:portfolio-sub": { de: "über 4 Konten", en: "across 4 accounts" },
32
72
  "widgets:catalog:remaining-debt": { de: "Restschuld", en: "Remaining debt" },
@@ -3,18 +3,19 @@
3
3
  // gewählten Screen-Filter-Wert über `filterParams` sieht (siehe
4
4
  // DashboardFilterDefinition in feature.ts).
5
5
 
6
- import type { ExtensionSectionProps } from "@cosmicdrift/kumiko-renderer";
6
+ import { type ExtensionSectionProps, useTranslation } from "@cosmicdrift/kumiko-renderer";
7
7
  import { SectionCard } from "@cosmicdrift/kumiko-renderer-web";
8
8
  import type { ReactNode } from "react";
9
9
 
10
10
  export function DashboardFilterEcho({ filterParams }: ExtensionSectionProps): ReactNode {
11
+ const t = useTranslation();
11
12
  const region = filterParams?.["region"];
12
13
  return (
13
- <SectionCard title="Custom-Panel">
14
+ <SectionCard title={t("widgets:dashboard:filter-echo-title")}>
14
15
  <p className="text-sm text-muted-foreground">
15
16
  {typeof region === "string"
16
- ? `Gefiltert nach Region: ${region}`
17
- : "Ungefiltert (alle Regionen)"}
17
+ ? t("widgets:dashboard:filter-echo-filtered", { region })
18
+ : t("widgets:dashboard:filter-echo-unfiltered")}
18
19
  </p>
19
20
  </SectionCard>
20
21
  );
@@ -146,6 +146,10 @@ export function Widgets(): ReactNode {
146
146
  </div>
147
147
  </SectionCard>
148
148
 
149
+ <div className="flex h-12 w-64 items-stretch" data-testid="progress-stretch-host">
150
+ <ProgressBar value={0.5} testId="progress-stretch-bar" />
151
+ </div>
152
+
149
153
  <SectionCard
150
154
  title={t("widgets:catalog:repayment-model")}
151
155
  action={
@@ -19,5 +19,8 @@
19
19
  "react-dom": "^19.2.6",
20
20
  "zod": "^4.4.3",
21
21
  "@cosmicdrift/kumiko-locale-de": "workspace:*"
22
+ },
23
+ "devDependencies": {
24
+ "@cosmicdrift/kumiko-testing": "workspace:*"
22
25
  }
23
26
  }
@@ -6,8 +6,7 @@ Canonical smoke-sample. Mounts every bundled-feature so framework-CI catches fea
6
6
 
7
7
  Sprint 9.8 found 12 framework bugs in a Studio deployment that all could have
8
8
  been caught earlier: 27 of 30 bundled features had zero integration coverage
9
- from any real app. This sample is the gate; deployment-specific rollout notes
10
- belong to your private operations documentation.
9
+ from any real app. This sample is the gate.
11
10
 
12
11
  ## CI-Gate
13
12
 
@@ -1399,7 +1399,7 @@
1399
1399
  },
1400
1400
  {
1401
1401
  "name": "jobs",
1402
- "description": "Persistence and operator tooling for background jobs registered via `r.job(...)`. Every job execution writes directly into `store_job_runs` (current status + duration) and `store_job_run_logs` (per-line log rows) from the BullMQ callbacks — no event stream in between (#2243). A daily `retention-cleanup` job deletes runs (and their logs) older than `retentionDays`; an hourly `stale-run-sweep` job marks runs stuck at status `running` past `staleRunTimeoutHours` as `failed` (#2246 — a crashed worker never fires the completion callback, so nothing else ever revisits the row). Exposes `jobs:write:trigger` (manual run) and `jobs:write:retry` (operator retry of a failed run), plus `jobs:query:list`, `jobs:query:details`, and `jobs:query:catalog` (manual jobs) for the operator UI. A job that declares `tenantVisibleFailure` also records its last failed attempt per tenant and subject in `store_tenant_job_failures`, which the tenant itself reads through `jobs:query:failures` — a translation key only, never the provider's message (fw#3079).",
1402
+ "description": "Persistence and operator tooling for background jobs registered via `r.job(...)`. Every job execution writes directly into `store_job_runs` (current status + duration) and `store_job_run_logs` (per-line log rows) from the BullMQ callbacks — no event stream in between (#2243). A daily `retention-cleanup` job deletes runs (and their logs) older than `retentionDays`; an hourly `stale-run-sweep` job marks runs stuck at status `running` past `staleRunTimeoutHours` as `failed` (#2246 — a crashed worker never fires the completion callback, so nothing else ever revisits the row). Exposes `jobs:write:trigger` (manual run) and `jobs:write:retry` (operator retry of a failed run), plus `jobs:query:list`, `jobs:query:details`, and `jobs:query:catalog` (manual jobs) for the operator UI. A job that declares `tenantVisibleFailure` also records its last failed attempt per tenant and subject in `store_tenant_job_failures`, which the tenant itself reads through `jobs:query:failures` — a translation key only, never the provider's message (fw#3079). A job that declares `tenantVisibleRun` also keeps its run state per tenant and subject in `store_tenant_job_runs` (queued, running, plus the latest completed or failed run, no payload, error text or logs), which the tenant reads through `jobs:query:tenant-runs` (fw#3616); the stale-run sweep fails its stuck `running` rows and drops its stuck `queued` rows.",
1403
1403
  "toggleableDefault": null,
1404
1404
  "requires": [],
1405
1405
  "optionalRequires": [],
@@ -115,9 +115,7 @@ then 500s). For a real deploy, swap to the persistent provider in
115
115
 
116
116
  Then set `S3_BUCKET` / `S3_REGION` / `S3_ACCESS_KEY` / `S3_SECRET_KEY` (plus
117
117
  optional `S3_ENDPOINT` for Hetzner/S3-compatible stores) and the app override
118
- `file-foundation:config:provider = "s3-env"`. For full wiring and production
119
- verification, follow your deployment team's private operations documentation.
120
-
118
+ `file-foundation:config:provider = "s3-env"`.
121
119
  ## Adding your own domain
122
120
 
123
121
  To add another DSGVO-compliant entity to the demo:
@@ -87,8 +87,8 @@ imprint" decision.
87
87
 
88
88
  ```bash
89
89
  # In the app workspace:
90
- bun kumiko migrate generate # detects text-block entity → SQL migration
91
- bun kumiko migrate apply # one-time (pre-deploy step in prod)
90
+ bun kumiko-schema generate <name> # detects text-block entity → SQL migration
91
+ bun kumiko-schema apply # one-time (pre-deploy step in prod)
92
92
  ```
93
93
 
94
94
  ### 3. Initial seed of the required blocks
@@ -141,8 +141,7 @@ await seedTextBlock(db, {
141
141
  ```
142
142
 
143
143
  → Templates for full legally-sound texts: e-recht24.de
144
- or datenschutz-generator.de by Dr. Schwenke. Follow your deployment team's
145
- private legal-template documentation for the approved source process.
144
+ or datenschutz-generator.de by Dr. Schwenke.
146
145
 
147
146
  ### 4. Visit the pages
148
147
 
@@ -81,8 +81,8 @@ the recipe lists `createConfigFeature()` explicitly (see `src/feature.ts`).
81
81
  ### 2. Create the table
82
82
 
83
83
  ```bash
84
- bun kumiko migrate generate # detects the `page` entity → SQL migration
85
- bun kumiko migrate apply
84
+ bun kumiko-schema generate <name> # detects the `page` entity → SQL migration
85
+ bun kumiko-schema apply
86
86
  ```
87
87
 
88
88
  ### 3. Authoring + branding
@@ -3,9 +3,8 @@
3
3
  Minimal pattern for **tokenized anonymous read surfaces**: authenticated users
4
4
  mint a link; visitors resolve it without login.
5
5
 
6
- Domain-agnostic — no credit/folder logic. Full consumer:
7
- [Cashcolt money-horse](https://github.com/CosmicDriftGameStudio/money-horse)
8
- (`share-by-token` + 5 layout templates).
6
+ Domain-agnostic — no credit/folder logic. A full consumer adds snapshots,
7
+ layout templates and tier gates on top.
9
8
 
10
9
  ## What it shows
11
10
 
@@ -16,8 +15,8 @@ Domain-agnostic — no credit/folder logic. Full consumer:
16
15
  - **`anonymousAccess`** on test stack (see integration test)
17
16
 
18
17
  Client wiring: [`recipes/apex-surface-auth`](../apex-surface-auth/) or a gate
19
- before `createKumikoApp` (money-horse uses `public-share-gate.tsx` like the public
20
- calculator).
18
+ before `createKumikoApp` (a small gate component that renders the public view when
19
+ the URL carries a share token).
21
20
 
22
21
  ## Handlers
23
22
 
@@ -3,7 +3,7 @@
3
3
  // Pattern: authenticated create → plain token show-once → anonymous
4
4
  // read-by-token with SHA-256 lookup. Revoke sets revokedAt via executor.
5
5
  //
6
- // See money-horse for credit/folder snapshots, layouts, tier gates.
6
+ // A full consumer adds domain snapshots, layouts and tier gates on top.
7
7
 
8
8
  import { generateToken } from "@cosmicdrift/kumiko-framework/api";
9
9
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
@@ -54,6 +54,12 @@ export const APP_FEATURES = [
54
54
  For an app that does not mount `dsgvoSelfServiceFeatures()`, use
55
55
  `securityBaselineFeatures()` with its default options, as this recipe does.
56
56
 
57
+ Dedupe keeps the first mounted instance. Late-bound state such as
58
+ `bindAutoRevokeOnPasswordChange` must be bound on the instance returned by
59
+ `composeFeatures`/`createApp`, not on a locally held handle to the dropped one:
60
+ the dropped instance's hooks never run and password changes silently stop
61
+ revoking sessions.
62
+
57
63
  A `createSessionsFeature({ ... })` mounted with different options than the
58
64
  preset's own no-arg instance still produces a clear boot error — dedupe only
59
65
  collapses provably interchangeable instances, never silently picks one:
@@ -88,6 +88,4 @@ Wenn eine kaputte Migration den Boot blockiert: setze `skippable: true` im seed-
88
88
 
89
89
  ## See also
90
90
 
91
- - **Plan-Doc:** your deployment team's private operations documentation —
92
- Phase-2+ Operations (projection-rebuild, event-replay, stream-migration, ...)
93
91
  - **Driver-Use-Case:** `seeds/2026-02-01-fix-admin-roles.ts` in this recipe
@@ -20,7 +20,7 @@ import { defineCreateWithTenantDefaults } from "@cosmicdrift/kumiko-bundled-feat
20
20
  export const invoiceEntity = createEntity({
21
21
  table: "read_invoices",
22
22
  fields: {
23
- title: createTextField({ required: true }),
23
+ title: createTextField({ personal: false, reason: "is_business_data", required: true }),
24
24
  amount: createMoneyField({ required: true }),
25
25
  language: createSelectField({ options: ["en", "de", "fr"] as const }),
26
26
  },
@@ -1,11 +0,0 @@
1
- import type { OwnershipMap } from "@cosmicdrift/kumiko-framework/engine";
2
-
3
- // `where`-rules are read-path only (buildOwnershipClause). The write path
4
- // decides in memory against the concrete row, so a where-rule there can only
5
- // ever deny — boot validation rejects the shape (fw#2626). Feature factories
6
- // that accept an `ownership` option reject it earlier, at build time, so the
7
- // author gets the option name in the message instead of an entity scope.
8
- export function hasWhereRule(map: OwnershipMap | undefined): boolean {
9
- if (!map) return false;
10
- return Object.values(map).some((rule) => rule !== "all" && rule.kind === "where");
11
- }