@cosmicdrift/kumiko-renderer 0.14.0 → 0.16.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -42,4 +42,4 @@
42
42
  "README.md",
43
43
  "LICENSE"
44
44
  ]
45
- }
45
+ }
@@ -1,8 +1,7 @@
1
- // @vitest-environment jsdom
1
+ import { describe, expect, test } from "bun:test";
2
2
  import type { LocaleResolver } from "@cosmicdrift/kumiko-headless";
3
3
  import { act, render, renderHook } from "@testing-library/react";
4
4
  import type { ReactNode } from "react";
5
- import { describe, expect, test } from "vitest";
6
5
  import {
7
6
  createStaticLocaleResolver,
8
7
  LocaleProvider,
@@ -32,7 +31,6 @@ function makeStatefulResolver(initial: string): LocaleResolver {
32
31
  },
33
32
  };
34
33
  }
35
-
36
34
  const wrap =
37
35
  (resolver: LocaleResolver, fallbackBundles?: TranslationsByLocale[]) =>
38
36
  ({ children }: { readonly children: ReactNode }): ReactNode => (
@@ -40,7 +38,6 @@ const wrap =
40
38
  {children}
41
39
  </LocaleProvider>
42
40
  );
43
-
44
41
  describe("useTranslation — lookup order", () => {
45
42
  test("App-Resolver wins when it returns a non-key value", () => {
46
43
  const resolver: LocaleResolver = {
@@ -50,7 +47,6 @@ describe("useTranslation — lookup order", () => {
50
47
  const { result } = renderHook(() => useTranslation(), { wrapper: wrap(resolver) });
51
48
  expect(result.current("hello")).toBe("Resolved by app");
52
49
  });
53
-
54
50
  test("falls back to plugin-bundle for current locale", () => {
55
51
  const resolver = createStaticLocaleResolver({ locale: "de" });
56
52
  const bundles: TranslationsByLocale[] = [{ de: { greet: "Hallo" }, en: { greet: "Hello" } }];
@@ -59,7 +55,6 @@ describe("useTranslation — lookup order", () => {
59
55
  });
60
56
  expect(result.current("greet")).toBe("Hallo");
61
57
  });
62
-
63
58
  test("strips region for bundle lookup (de-AT → de)", () => {
64
59
  const resolver = createStaticLocaleResolver({ locale: "de-AT" });
65
60
  const bundles: TranslationsByLocale[] = [{ de: { greet: "Hallo" } }];
@@ -68,7 +63,6 @@ describe("useTranslation — lookup order", () => {
68
63
  });
69
64
  expect(result.current("greet")).toBe("Hallo");
70
65
  });
71
-
72
66
  test("falls back to fallbackLocale (en) when current locale missing", () => {
73
67
  const resolver = createStaticLocaleResolver({ locale: "fr" });
74
68
  const bundles: TranslationsByLocale[] = [{ de: { greet: "Hallo" }, en: { greet: "Hello" } }];
@@ -77,13 +71,11 @@ describe("useTranslation — lookup order", () => {
77
71
  });
78
72
  expect(result.current("greet")).toBe("Hello");
79
73
  });
80
-
81
74
  test("returns key as-is when nothing resolves", () => {
82
75
  const resolver = createStaticLocaleResolver({ locale: "de" });
83
76
  const { result } = renderHook(() => useTranslation(), { wrapper: wrap(resolver) });
84
77
  expect(result.current("missing.key")).toBe("missing.key");
85
78
  });
86
-
87
79
  test("interpolates {param}-placeholders from params arg", () => {
88
80
  const resolver = createStaticLocaleResolver({ locale: "de" });
89
81
  const bundles: TranslationsByLocale[] = [{ de: { greet: "Hallo {name}!" } }];
@@ -93,31 +85,26 @@ describe("useTranslation — lookup order", () => {
93
85
  expect(result.current("greet", { name: "Marc" })).toBe("Hallo Marc!");
94
86
  });
95
87
  });
96
-
97
88
  describe("useTranslation — re-render on locale change", () => {
98
89
  test("subscribe fires when setLocale runs, hook returns new value", () => {
99
90
  const resolver = makeStatefulResolver("de");
100
91
  const bundles: TranslationsByLocale[] = [{ de: { greet: "Hallo" }, en: { greet: "Hello" } }];
101
-
102
92
  function Probe(): ReactNode {
103
93
  const t = useTranslation();
104
94
  return <span data-testid="msg">{t("greet")}</span>;
105
95
  }
106
-
107
96
  const { getByTestId } = render(
108
97
  <LocaleProvider resolver={resolver} fallbackBundles={bundles}>
109
98
  <Probe />
110
99
  </LocaleProvider>,
111
100
  );
112
101
  expect(getByTestId("msg").textContent).toBe("Hallo");
113
-
114
102
  act(() => {
115
103
  resolver.setLocale?.("en");
116
104
  });
117
105
  expect(getByTestId("msg").textContent).toBe("Hello");
118
106
  });
119
107
  });
120
-
121
108
  describe("useLocale", () => {
122
109
  test("returns the resolver", () => {
123
110
  const resolver = createStaticLocaleResolver({ locale: "de" });
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "vitest";
1
+ import { describe, expect, test } from "bun:test";
2
2
  import { lastSegment } from "../app/qn";
3
3
 
4
4
  describe("lastSegment", () => {
@@ -1,13 +1,12 @@
1
- // @vitest-environment jsdom
2
1
  //
3
2
  // useListUrlState pinnt den URL-State-Vertrag pro Screen-ID-Namespace:
4
3
  // `?<screenId>.sort=…&<screenId>.dir=…&<screenId>.q=…&<screenId>.page=…`.
5
4
  // Zwei Listen auf der Route teilen sich die URL ohne Param-Konflikt.
6
5
  // Page wird bei Sort/Filter-Wechsel reseted; Sort bleibt bei Search.
7
6
 
7
+ import { describe, expect, mock, test } from "bun:test";
8
8
  import { act, renderHook } from "@testing-library/react";
9
9
  import type { ReactNode } from "react";
10
- import { describe, expect, test, vi } from "vitest";
11
10
  import type { NavApi } from "../app/nav";
12
11
  import { NavProvider } from "../app/nav";
13
12
  import { useListUrlState } from "../hooks/use-list-url-state";
@@ -23,8 +22,8 @@ function makeNav(initial: Record<string, string> = {}): NavApi & {
23
22
  captures: Array<Record<string, string | null>>;
24
23
  } = {
25
24
  route: undefined,
26
- navigate: vi.fn(),
27
- replace: vi.fn(),
25
+ navigate: mock(),
26
+ replace: mock(),
28
27
  hrefFor: () => "",
29
28
  get searchParams() {
30
29
  return params;
@@ -41,11 +40,9 @@ function makeNav(initial: Record<string, string> = {}): NavApi & {
41
40
  };
42
41
  return api;
43
42
  }
44
-
45
43
  function wrapper(nav: NavApi): (props: { children: ReactNode }) => ReactNode {
46
44
  return ({ children }) => <NavProvider value={nav}>{children}</NavProvider>;
47
45
  }
48
-
49
46
  describe("useListUrlState", () => {
50
47
  test("Default-State: kein URL-Param → sort=null, q='', page=1", () => {
51
48
  const nav = makeNav();
@@ -54,25 +51,21 @@ describe("useListUrlState", () => {
54
51
  expect(result.current.q).toBe("");
55
52
  expect(result.current.page).toBe(1);
56
53
  });
57
-
58
54
  test("liest sort+dir aus URL-Params (mit screenId-Prefix)", () => {
59
55
  const nav = makeNav({ "orders.sort": "createdAt", "orders.dir": "desc" });
60
56
  const { result } = renderHook(() => useListUrlState("orders"), { wrapper: wrapper(nav) });
61
57
  expect(result.current.sort).toEqual({ field: "createdAt", dir: "desc" });
62
58
  });
63
-
64
59
  test("ignoriert sort einer ANDEREN Liste (Namespacing)", () => {
65
60
  const nav = makeNav({ "incidents.sort": "severity", "incidents.dir": "asc" });
66
61
  const { result } = renderHook(() => useListUrlState("orders"), { wrapper: wrapper(nav) });
67
62
  expect(result.current.sort).toBeNull();
68
63
  });
69
-
70
64
  test("invalid dir (z.B. 'foo') → sort=null (defensive parse)", () => {
71
65
  const nav = makeNav({ "orders.sort": "name", "orders.dir": "foo" });
72
66
  const { result } = renderHook(() => useListUrlState("orders"), { wrapper: wrapper(nav) });
73
67
  expect(result.current.sort).toBeNull();
74
68
  });
75
-
76
69
  test("setSort schreibt sort+dir, resettet page (atomic update)", () => {
77
70
  const nav = makeNav({ "orders.page": "5" });
78
71
  const { result } = renderHook(() => useListUrlState("orders"), { wrapper: wrapper(nav) });
@@ -86,7 +79,6 @@ describe("useListUrlState", () => {
86
79
  "orders.page": null, // page-reset bei sort-change
87
80
  });
88
81
  });
89
-
90
82
  test("setSort(null) löscht sort+dir+page", () => {
91
83
  const nav = makeNav({
92
84
  "orders.sort": "name",
@@ -103,7 +95,6 @@ describe("useListUrlState", () => {
103
95
  "orders.page": null,
104
96
  });
105
97
  });
106
-
107
98
  test("setQ schreibt q + resettet page (Sort bleibt unangetastet)", () => {
108
99
  const nav = makeNav({
109
100
  "orders.sort": "name",
@@ -122,7 +113,6 @@ describe("useListUrlState", () => {
122
113
  // die Sortierung nicht.
123
114
  expect(nav.captures[0]).not.toHaveProperty("orders.sort");
124
115
  });
125
-
126
116
  test("setQ('') löscht den q-Key", () => {
127
117
  const nav = makeNav({ "orders.q": "acme" });
128
118
  const { result } = renderHook(() => useListUrlState("orders"), { wrapper: wrapper(nav) });
@@ -134,7 +124,6 @@ describe("useListUrlState", () => {
134
124
  "orders.page": null,
135
125
  });
136
126
  });
137
-
138
127
  test("setPage(1) löscht den Key (Default-Page = unprefixed URL)", () => {
139
128
  const nav = makeNav({ "orders.page": "5" });
140
129
  const { result } = renderHook(() => useListUrlState("orders"), { wrapper: wrapper(nav) });
@@ -143,7 +132,6 @@ describe("useListUrlState", () => {
143
132
  });
144
133
  expect(nav.captures[0]).toEqual({ "orders.page": null });
145
134
  });
146
-
147
135
  test("setPage(N>1) speichert die Page als String", () => {
148
136
  const nav = makeNav();
149
137
  const { result } = renderHook(() => useListUrlState("orders"), { wrapper: wrapper(nav) });
@@ -152,7 +140,6 @@ describe("useListUrlState", () => {
152
140
  });
153
141
  expect(nav.captures[0]).toEqual({ "orders.page": "7" });
154
142
  });
155
-
156
143
  test("invalid page (negativ, NaN, 0) → fallback auf 1", () => {
157
144
  const nav = makeNav({ "orders.page": "-3" });
158
145
  const { result } = renderHook(() => useListUrlState("orders"), { wrapper: wrapper(nav) });
@@ -0,0 +1,26 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { synthesizeActionFormEntity, synthesizeActionFormScreen } from "../action-form-shim";
3
+
4
+ describe("synthesizeActionFormEntity", () => {
5
+ test("wraps inline fields as minimal EntityDefinition", () => {
6
+ const entity = synthesizeActionFormEntity({
7
+ title: { type: "text" },
8
+ });
9
+ expect(entity.fields["title"]).toEqual({ type: "text" });
10
+ });
11
+ });
12
+
13
+ describe("synthesizeActionFormScreen", () => {
14
+ test("maps actionForm screen to entityEdit shape", () => {
15
+ const screen = synthesizeActionFormScreen({
16
+ id: "invite-user",
17
+ type: "actionForm",
18
+ handler: "users:write:invite-user",
19
+ layout: { sections: [{ title: "Invite", fields: ["email"] }] },
20
+ fields: { email: { type: "text" } },
21
+ });
22
+ expect(screen.type).toBe("entityEdit");
23
+ expect(screen.entity).toBe("__action-form__");
24
+ expect(screen.layout).toEqual({ sections: [{ title: "Invite", fields: ["email"] }] });
25
+ });
26
+ });
package/CHANGELOG.md DELETED
@@ -1,472 +0,0 @@
1
- # @cosmicdrift/kumiko-renderer
2
-
3
- ## 0.14.0
4
-
5
- ### Patch Changes
6
-
7
- - @cosmicdrift/kumiko-framework@0.14.0
8
- - @cosmicdrift/kumiko-headless@0.14.0
9
-
10
- ## 0.13.0
11
-
12
- ### Patch Changes
13
-
14
- - Updated dependencies [7f56b2f]
15
- - @cosmicdrift/kumiko-framework@0.13.0
16
- - @cosmicdrift/kumiko-headless@0.13.0
17
-
18
- ## 0.12.2
19
-
20
- ### Patch Changes
21
-
22
- - Updated dependencies [597de52]
23
- - @cosmicdrift/kumiko-framework@0.12.2
24
- - @cosmicdrift/kumiko-headless@0.12.2
25
-
26
- ## 0.12.1
27
-
28
- ### Patch Changes
29
-
30
- - Updated dependencies [f2ad7c4]
31
- - @cosmicdrift/kumiko-framework@0.12.1
32
- - @cosmicdrift/kumiko-headless@0.12.1
33
-
34
- ## 0.12.0
35
-
36
- ### Patch Changes
37
-
38
- - @cosmicdrift/kumiko-framework@0.12.0
39
- - @cosmicdrift/kumiko-headless@0.12.0
40
-
41
- ## 0.11.2
42
-
43
- ### Patch Changes
44
-
45
- - Updated dependencies [92a84f0]
46
- - @cosmicdrift/kumiko-framework@0.11.2
47
- - @cosmicdrift/kumiko-headless@0.11.2
48
-
49
- ## 0.11.1
50
-
51
- ### Patch Changes
52
-
53
- - @cosmicdrift/kumiko-framework@0.11.1
54
- - @cosmicdrift/kumiko-headless@0.11.1
55
-
56
- ## 0.11.0
57
-
58
- ### Patch Changes
59
-
60
- - Updated dependencies [30ea981]
61
- - Updated dependencies [9347212]
62
- - @cosmicdrift/kumiko-framework@0.11.0
63
- - @cosmicdrift/kumiko-headless@0.11.0
64
-
65
- ## 0.10.0
66
-
67
- ### Patch Changes
68
-
69
- - Updated dependencies [d06f029]
70
- - Updated dependencies [753d392]
71
- - @cosmicdrift/kumiko-framework@0.10.0
72
- - @cosmicdrift/kumiko-headless@0.10.0
73
-
74
- ## 0.9.0
75
-
76
- ### Patch Changes
77
-
78
- - Updated dependencies [51e22f5]
79
- - @cosmicdrift/kumiko-framework@0.9.0
80
- - @cosmicdrift/kumiko-headless@0.9.0
81
-
82
- ## 0.8.1
83
-
84
- ### Patch Changes
85
-
86
- - Updated dependencies [4b5f91e]
87
- - @cosmicdrift/kumiko-framework@0.8.1
88
- - @cosmicdrift/kumiko-headless@0.8.1
89
-
90
- ## 0.8.0
91
-
92
- ### Patch Changes
93
-
94
- - Updated dependencies [f34af9a]
95
- - Updated dependencies [dff4123]
96
- - @cosmicdrift/kumiko-framework@0.8.0
97
- - @cosmicdrift/kumiko-headless@0.8.0
98
-
99
- ## 0.7.0
100
-
101
- ### Minor Changes
102
-
103
- - bcf43b6: es-ops: `SeedMembershipRow` exposes `streamTenantId` (stream-tenant aus `kumiko_events.v1`) neben dem payload-`tenantId`. Seed-Authors müssen den `kumiko_events`-JOIN nicht mehr selbst bauen — `m.streamTenantId` ist der korrekte Wert für `systemWriteAs`'s `tenantIdOverride` wenn das Aggregate von einem fremden Executor angelegt wurde (typisches `seedTenantMembership(by=systemAdmin)`-Pattern).
104
-
105
- ### Patch Changes
106
-
107
- - Updated dependencies [bcf43b6]
108
- - @cosmicdrift/kumiko-framework@0.7.0
109
- - @cosmicdrift/kumiko-headless@0.7.0
110
-
111
- ## 0.6.0
112
-
113
- ### Minor Changes
114
-
115
- - 8489d18: feat(es-ops): Phase 1.5 — tenantIdOverride + dry-run-validator + E2E-Test + Doku
116
-
117
- Phase 1.5 schließt die Lücken aus Phase 1 die den ersten Driver-Use-Case
118
- (publicstatus admin-roles) blockten. Siehe Retro:
119
- `kumiko-platform/docs/plans/features/es-ops-phase1-retro.md` (PR #9).
120
-
121
- **A1 — tenantIdOverride:**
122
- `SeedMigrationContext.systemWriteAs(qn, payload, tenantIdOverride?)`.
123
- Default SYSTEM_TENANT_ID (unverändert für System-scope-Aggregates wie
124
- config-values). Mit override: `createSystemUser(tenantIdOverride)` als
125
- Executor, damit der Event-Store-Executor den Aggregate-Stream im
126
- richtigen Tenant findet. Fix für die `version_conflict`-Klasse-Bug
127
- (Memory `feedback_event_store_tenant_consistency.md`).
128
-
129
- **A2 — dry-run-validator:**
130
- Runner parsed seed-files vor `migration.run()` per regex
131
- `systemWriteAs\(["']([^"']+)["']`, sammelt handler-QNs, validiert
132
- gegen `registry.getWriteHandler(qn)`. Fail-fast mit klarer Message
133
-
134
- - Datei + QN statt zur Runtime "handler not found". Catched camelCase-
135
- typos (kebab-case-vs-camelCase Drift) + andere QN-Drift zur Boot-Zeit.
136
- runProdApp reicht den richtigen Registry rein (`registry` neu in
137
- RunPendingSeedMigrationsArgs).
138
-
139
- **A3 — E2E-Test:**
140
- `packages/bundled-features/src/__tests__/es-ops-e2e.integration.ts`
141
- mit `setupTestStack`-Pattern: tenant+config Features echt geladen,
142
- echtes Membership-Aggregate via TenantHandlers.addMember im Demo-Tenant,
143
- seed-migration ruft update-member-roles mit tenantIdOverride → write
144
- geht durch, Marker landed, Event in Store, Read-Model aktualisiert.
145
- Plus typo-Test: seed mit camelCase fail-t Dry-Run mit
146
- `/dry-run found.*unknown handler-QN/`. **TDD-First**: ohne A1+A2 wäre
147
- der test rot.
148
-
149
- **A4 — Doku:**
150
- `framework/src/es-ops/README.md` erweitert um „Wann brauche ich
151
- tenantIdOverride?" + „Deployment-Anforderungen" (Docker COPY, Idempotenz,
152
- Multi-Replica) + „Lokaler Smoke vor Push". Recipe-README + seed-files
153
- auf neue API aktualisiert.
154
-
155
- **A5 — Smoke-Skript-Template:**
156
- `samples/recipes/seed-migration/scripts/smoke.ts` als copy-paste-Template
157
- für App-Authors: Bun-runnable, offline (read-only, kein DB-Write),
158
- validiert Module-Load + QN-Resolution + System-User-Access. Recipe-
159
- README dokumentiert Pflicht-Pattern.
160
-
161
- **Bonus-Fix:**
162
- `tenant:write:create`-access auf `["system", "SystemAdmin"]` erweitert
163
- (symmetrisch zu update-member-roles). Aufgedeckt durch Recipe-Smoke +
164
- initial-tenants-Seed. Pinning-Test in `tenant.integration.ts` updated.
165
-
166
- **Test-State:** 45/45 grün (Pre-Push). Typecheck clean. Biome clean.
167
- as-cast-Audit clean. Guard-silent-skip clean. Recipe-Smoke clean.
168
-
169
- **Folge-Step (separater PR):** publicstatus driver-sample reaktivieren
170
- mit lokalem Pre-Push-Smoke gegen publicstatus' echtes Feature-Set.
171
-
172
- ### Patch Changes
173
-
174
- - Updated dependencies [8489d18]
175
- - @cosmicdrift/kumiko-framework@0.6.0
176
- - @cosmicdrift/kumiko-headless@0.6.0
177
-
178
- ## 0.5.2
179
-
180
- ### Patch Changes
181
-
182
- - 4f0d781: fix(tenant): updateMemberRoles erlaubt "system"-Rolle (symmetrisch zu create)
183
-
184
- Drift innerhalb des tenant-Features: `tenant:write:create` akzeptierte
185
- `["system", "SystemAdmin"]`, `tenant:write:update-member-roles` aber
186
- nur `["SystemAdmin"]`. Konsequenz: ops-tooling und seed-migrations
187
- (`createSystemUser` mit `roles: ["system"]`) konnten den Handler nicht
188
- aufrufen — `access_denied`.
189
-
190
- Live entdeckt beim ersten Driver-Sample der es-ops Phase 1: publicstatus
191
- seed `2026-05-20-fix-admin-roles.ts` rief `update-member-roles` via
192
- `systemWriteAs` → access_denied → Pod CrashLoopBackOff.
193
-
194
- Plus access-rule-Pinning-Test in `tenant.integration.ts`-scenario-7.
195
-
196
- - Updated dependencies [4f0d781]
197
- - @cosmicdrift/kumiko-framework@0.5.2
198
- - @cosmicdrift/kumiko-headless@0.5.2
199
-
200
- ## 0.5.1
201
-
202
- ### Patch Changes
203
-
204
- - 0e00015: fix(es-ops): path.resolve statt path.join für seedsDir → seed-files
205
-
206
- Bun's `await import()` braucht absolute Pfade. Wenn der App-Author
207
- `runProdApp({ seedsDir: "./seeds" })` setzt (relativ), würde
208
- `path.join("./seeds", "foo.ts")` einen relativen Pfad liefern → Bun's
209
- Import-Resolver such relativ zum `runner.ts`-Modul (nicht zum
210
- `process.cwd()`) → `Cannot find module 'seeds/...' from '<runner-path>'`.
211
-
212
- `path.resolve` löst gegen `process.cwd()` auf → absolute Pfade →
213
- Import funktioniert. Aufgedeckt beim ersten Live-Boot der publicstatus-
214
- Driver-Migration (Pod CrashLoopBackOff).
215
-
216
- - Updated dependencies [0e00015]
217
- - @cosmicdrift/kumiko-framework@0.5.1
218
- - @cosmicdrift/kumiko-headless@0.5.1
219
-
220
- ## 0.5.0
221
-
222
- ### Minor Changes
223
-
224
- - 7ff69ab: feat(es-ops): Phase 1 — file-based seed-migrations
225
-
226
- Neues first-class Operations-Pattern fürs Framework. Liefert `seed-migrations`
227
- als drizzle-migrate-equivalent für Event-Sourcing-Aggregate-Updates die
228
- idempotent-Seeder nicht erfassen können (z.B. „Member hat schon eine
229
- Rolle, aber jetzt soll noch eine dazukommen").
230
-
231
- Public-API:
232
-
233
- - `runProdApp({ seedsDir })` — Auto-apply pending Migrations beim Boot
234
- - `SeedMigration`-Interface (default-Export einer `seeds/<id>.ts`-File)
235
- - `SeedMigrationContext` mit `systemWriteAs` (ruft existing write-handler
236
- als System-User) + Read-Helpers (`findUserByEmail`,
237
- `findMembershipsOfUser`, `findTenants`)
238
- - CLI: `bunx kumiko ops seed:new|status|apply`
239
- - Tracking-Table `kumiko_es_operations` mit `operation_type`-Discriminator
240
- (vorbereitet auf Phase 2+ Operations: projection-rebuild, event-replay,
241
- stream-migration, ...)
242
- - Env-Flags: `KUMIKO_SKIP_ES_OPS=1` (alle skippen für Recovery),
243
- `KUMIKO_SKIP_ES_OPS_<ID>=1` (einzelne kaputte skippen)
244
-
245
- Garantien: single-run via tracking, atomic via per-migration-Tx,
246
- chronological order via filename-prefix, fail-stop bei Failure (kein
247
- Partial-Apply), ES-konform via Handler-Dispatch.
248
-
249
- Sub-path-Export: `@cosmicdrift/kumiko-framework/es-ops`
250
-
251
- Plan-Doc: `kumiko-platform/docs/plans/features/es-ops.md`
252
- Recipe: `samples/recipes/seed-migration/`
253
- Driver-Use-Case: publicstatus admin-roles-drift (parallel-Branch
254
- `feat/es-ops-driver-admin-roles`).
255
-
256
- Phase 2+ skizziert + offen markiert — Implementation pro Use-Case.
257
-
258
- ### Patch Changes
259
-
260
- - Updated dependencies [7ff69ab]
261
- - @cosmicdrift/kumiko-framework@0.5.0
262
- - @cosmicdrift/kumiko-headless@0.5.0
263
-
264
- ## 0.4.1
265
-
266
- ### Patch Changes
267
-
268
- - 010b410: feat(auth-email-password): "Bestätigungs-Mail erneut senden" im LoginScreen
269
-
270
- LoginScreen bietet bei reason=email_not_verified jetzt einen Resend-Link
271
- im Fehler-Banner — der existierende `requestEmailVerification`-Endpoint
272
- wird direkt aufgerufen, der Banner wechselt nach Erfolg zum Info-Variant
273
- ("Wir haben dir eine neue Bestätigungs-Mail geschickt.").
274
-
275
- UX-Details:
276
-
277
- - Bei 429 → inline-Hint "Bitte warte kurz und versuche es erneut."
278
- - Bei Netzwerk/sonstigen Fehlern → inline-Hint "Konnte nicht senden."
279
- - Anti-Typo-Gate: ändert der User die Email-Eingabe nach dem Login-Fail,
280
- verschwindet der Resend-Link — sonst würde Resend silent-success an die
281
- geänderte (potentiell typoed) Adresse gehen ohne User-Feedback.
282
- - Andere Failure-Codes (invalid_credentials etc.) zeigen weiterhin keinen
283
- Resend-Link.
284
-
285
- i18n: 4 neue Keys (DE+EN) im `auth.login.resend*`-Namespace, additive.
286
- Apps die ihre Translations override-en müssen nichts ändern.
287
-
288
- Additive UI-Feature — keine API-Breaks, keine Schema-Migration.
289
-
290
- - Updated dependencies [010b410]
291
- - @cosmicdrift/kumiko-framework@0.4.1
292
- - @cosmicdrift/kumiko-headless@0.4.1
293
-
294
- ## 0.4.0
295
-
296
- ### Minor Changes
297
-
298
- - 825e7d2: Visual-Tree V.1.4 → V.1.6 — Feature-complete Editor + Folder-Hierarchy + Roving-tabindex.
299
-
300
- **V.1.4** — explicit `folder?: string` Schema-Field auf text-block-entity. Slug bleibt
301
- kebab-only validiert, Folder explizit gesetzt. Tree gruppiert via `groupBlocksByFolder`
302
- (ersetzt `groupBlocksBySlugPrefix`). `Subscribe<T>` Signature um optional `emitError`
303
- erweitert für explicit async-error-Pfade. ProviderBranch zeigt Error-Banner mit
304
- Retry-Button. Drift-Test pinnt seedTextBlock-vs-set.write Slug-Validation.
305
-
306
- **V.1.4b** — URL-State-Routing für Editor-Target via `nav.searchParams`. F5 + Back-Button
307
- stellen den Editor-State wieder her. Format: `?t=text-content:edit&a_slug=...&a_lang=...`.
308
- Plus `useDispatchTarget` hook ersetzt globalen `dispatchTarget` als empfohlenen Production-
309
- Pfad (legacy bleibt für Test-Hooks).
310
-
311
- **V.1.5** — Arrow-Key-Navigation (`<aside role="tree">`, ARIA-tree-Pattern) + SSE-driven
312
- Tree-Refresh. `ClientFeatureDefinition.treeEntities?: string[]` listet Entity-Namen pro
313
- Provider; live-events triggern provider-re-mount → Stale-Tree-state="stub"→"filled"
314
- flippt nach save automatisch.
315
-
316
- **V.1.5c+d** — Active-Node-Highlight (explicit blue + 2px border-l + scrollIntoView),
317
- VS-Code-Polish (compact spacing, focus-visible, folder-icon-color text-amber, indent-
318
- guides per ancestor-depth), Folder-Wrapper für legal-pages ("📁 Legal" + slug-first
319
- Verschachtelung) und text-content ("📁 Content").
320
-
321
- **V.1.6** — Multi-level Folder-Splitting (`folder="page/marketing"` → nested folders,
322
- walk-or-create-pattern, folder/leaf-collision-tolerant). Roving-tabindex (nur focused-
323
- treeitem hat tabIndex=0, Tab cyclt aus dem Tree raus).
324
-
325
- 35/35 kumiko check PASS, 13/13 group-blocks + 22/22 text-content integration tests grün.
326
- Browser + Keyboard lokal validated.
327
-
328
- **Breaking**: `TreeContext` Type entfernt (V.1.2 SR2-Rip — war nie genutzt). Provider sind
329
- session-bound: `TreeChildrenSubscribe = () => Subscribe<T>` statt `(ctx) => Subscribe<T>`.
330
-
331
- **V.1.7-Followups**: useEffect-deps in VisualTree-focus-init (Performance), Cancellation-
332
- Token in TreeProvider's fetch (emit-after-unmount-warning), inline-rename, drag-drop,
333
- file-icons per slug-extension, parent-jump bei ArrowLeft auf collapsed-item.
334
-
335
- ### Patch Changes
336
-
337
- - Updated dependencies [825e7d2]
338
- - @cosmicdrift/kumiko-framework@0.4.0
339
- - @cosmicdrift/kumiko-headless@0.4.0
340
-
341
- ## 0.3.0
342
-
343
- ### Minor Changes
344
-
345
- - 0.3.0 bringt zwei neue Subsysteme (Step-Engine Tier-3 + Visual-Tree) plus
346
- eine AST-Codemod-Pipeline als Vorarbeit für den L2-AI-Layer.
347
-
348
- ### Breaking Changes
349
-
350
- - `skipTransitionGuard` → `unsafeSkipTransitionGuard` (Rename in
351
- feature-ast + engine). Der `unsafe`-Prefix macht die Tragweite des
352
- Casts sichtbar und ist konsistent zur `unsafeProjectionUpsert`- und
353
- `r.rawTable`-Konvention. Migration: 1:1-Ersetzung, keine Verhaltens-Änderung.
354
-
355
- ### Features
356
-
357
- - **Step-Engine M.4 — Tier-3 Workflow-Engine.** Neue Step-Vocabulary
358
- `wait`, `waitForEvent`, `retry` ermöglicht persistierte Long-Running-Flows
359
- über Job-Boundaries hinweg. Q7 Snapshot-at-Start hängt jedem Step-Run
360
- einen SHA-256-Fingerprint des Aggregat-Zustands an, sodass Replays
361
- deterministisch gegen den ursprünglichen Eingangszustand laufen.
362
- - **Visual-Tree V.1.x — Tree-API + Editor-Panel.** Neue `VisualTree`-
363
- Component plus TreeProvider-Pattern; erste TreeProviders für
364
- `text-content` und `legal-pages` (CMS-light + Impressum/Privacy).
365
- Fundament für den späteren No-Code-Designer (~3000 LOC, 98 Tests).
366
- - **Codemod-Pipeline.** AST-basierte Patcher-Module für strukturelle
367
- Feature-Edits — wird vom kommenden L2-AI-Layer als Tool-Surface
368
- verwendet, ist aber eigenständig nutzbar für ts-morph-style Migrationen.
369
- - **user-data-rights Sample-Recipe.** DSGVO Art. 15/17/18/20 vollständig
370
- als Sample-Recipe (`samples/recipes/`) inklusive README — zeigt die
371
- Export- und Forget-Pipeline gegen den `compliance-profiles`-Default
372
- (`eu-dsgvo`).
373
-
374
- ### Fixes
375
-
376
- - `tier-engine`: auto-default-tier-Hook benutzt jetzt `ctx.db.raw` für
377
- Event-Store-Operationen (#37, vorher: stiller Bug, 22 Tage live).
378
- - `engine`: unsafe-projection-upsert nutzt `as never` statt `as any` —
379
- schmaler Cast-Surface, weniger Compiler-Knebel.
380
- - `visual-tree`: runtime-isolation marker für client-konsumierte Files,
381
- damit der Multi-Entry-Build den richtigen Bundle-Split bekommt.
382
- - `feature-ast`: vollständiger `unsafeSkipTransitionGuard`-Rename (war
383
- in zwei Modulen noch der alte Name).
384
- - `framework`: Error-Reasons + `noConsole`-Lint + No-Date-API-Guard
385
- wieder push-ready.
386
-
387
- ### Library-Updates
388
-
389
- hono 4.12, jose 6.2, stripe 22.1, meilisearch 0.58, marked 18,
390
- bun-types 1.3.13, lucide-react 1.14, bullmq 5.76, ioredis 5.10,
391
- i18next 26.0, react + radix-ui-primitives auf aktuelle Minors.
392
-
393
- ### Patch Changes
394
-
395
- - Updated dependencies
396
- - @cosmicdrift/kumiko-framework@0.3.0
397
- - @cosmicdrift/kumiko-headless@0.3.0
398
-
399
- ## 0.2.3
400
-
401
- ### Patch Changes
402
-
403
- - @cosmicdrift/kumiko-framework@0.2.3
404
- - @cosmicdrift/kumiko-headless@0.2.3
405
-
406
- ## 0.2.2
407
-
408
- ### Patch Changes
409
-
410
- - 7a7da3e: Re-publish 0.2.1 → 0.2.2 mit korrekt aufgelösten cross-package-Versionen.
411
- 0.2.1 hatte `workspace:*` als Wert in den dependencies (npm publish ohne
412
- yarn-pack rewrite), Konsumenten bekamen "Workspace not found".
413
-
414
- publish-with-oidc.sh nutzt jetzt `yarn pack` (rewrited workspace:\*) +
415
- `npm publish <tarball>` (OIDC + provenance).
416
-
417
- - Updated dependencies [7a7da3e]
418
- - @cosmicdrift/kumiko-framework@0.2.2
419
- - @cosmicdrift/kumiko-headless@0.2.2
420
-
421
- ## 0.2.1
422
-
423
- ### Patch Changes
424
-
425
- - 48b7f6a: CI: switch publish to npm-CLI with OIDC Trusted Publishing + provenance.
426
- No source changes — verifies the new publish path produces a verified-
427
- provenance attestation on npmjs.com instead of token-based publish.
428
- - Updated dependencies [48b7f6a]
429
- - @cosmicdrift/kumiko-framework@0.2.1
430
- - @cosmicdrift/kumiko-headless@0.2.1
431
-
432
- ## 0.2.0
433
-
434
- ### Minor Changes
435
-
436
- - 6c70b6f: fix(tenant): seedTenant idempotent gegen Event-Store-Projection-Drift.
437
-
438
- Verhindert version_conflict beim App-Boot wenn Aggregat existiert aber
439
- Projection-Row fehlt (rebuild-drift, async-lag, manueller DB-Eingriff).
440
-
441
- ### Patch Changes
442
-
443
- - Updated dependencies [6c70b6f]
444
- - @cosmicdrift/kumiko-framework@0.2.0
445
- - @cosmicdrift/kumiko-headless@0.2.0
446
-
447
- ## 0.1.0
448
-
449
- ### Minor Changes
450
-
451
- - 59ba6d7: Initial public release of Kumiko — AI-native backend builder.
452
-
453
- What ships in 0.1.0:
454
-
455
- - **Engine** (`@cosmicdrift/kumiko-framework`): `defineFeature`, `r.entity`, `r.writeHandler`, `r.queryHandler`, `r.projection`, `r.multiStreamProjection`, `r.hook`, `r.translations`, `r.crud`, `r.referenceData`, `r.screen`, `r.nav`, `r.authClaims`, full lifecycle pipeline with field-level access checks
456
- - **Pipeline** (`@cosmicdrift/kumiko-framework`): `createDispatcher`, JWT auth via jose, Zod schema validation, role-based access checks, command/write/query split
457
- - **DB** (`@cosmicdrift/kumiko-framework`): Drizzle helpers (`buildDrizzleTable`, `applyCursorQuery`), CRUD executor, Postgres dialect, optimistic locking, soft delete, multi-tenant scoping
458
- - **Event sourcing** (`@cosmicdrift/kumiko-framework`): aggregate streams, single + multi-stream projections, event upcasters, asOf queries, archive support, AsyncDaemon-pattern dispatcher
459
- - **Bundled features** (`@cosmicdrift/kumiko-bundled-features`): auth-email-password, sessions, tenants, users, jobs, secrets, file-provider-s3, mail-transport-smtp/inmemory, billing-foundation, cap-counter, channel-in-app, delivery, feature-toggles, legal-pages
460
- - **Renderer** (`@cosmicdrift/kumiko-renderer`, `@cosmicdrift/kumiko-renderer-web`): schema-driven CRUD UI for React + Expo Web, override paths, list debounce, theme tokens
461
- - **Headless** (`@cosmicdrift/kumiko-headless`): view-models for list/edit screens, locale-aware
462
- - **Dev server** (`@cosmicdrift/kumiko-dev-server`): `runDevApp`, `runProdApp`, `kumiko-build` for production bundles (client + server), Docker-ready
463
- - **Realtime** (`@cosmicdrift/kumiko-dispatcher-live`): SSE broadcast across tenants, Redis Pub/Sub backend
464
- - **CLI** (`bin/kumiko.ts`): interactive dev menu, test runners, check pipeline (Biome + TypeScript + 18 guards + Vitest)
465
-
466
- This is a pre-1.0 release — APIs may change between minor versions. Breaking changes will be documented per release.
467
-
468
- ### Patch Changes
469
-
470
- - Updated dependencies [59ba6d7]
471
- - @cosmicdrift/kumiko-framework@0.1.0
472
- - @cosmicdrift/kumiko-headless@0.1.0