@cosmicdrift/kumiko-framework 0.241.0 → 0.243.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-framework",
3
- "version": "0.241.0",
3
+ "version": "0.243.0",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -198,7 +198,7 @@
198
198
  "./package.json": "./package.json"
199
199
  },
200
200
  "dependencies": {
201
- "@cosmicdrift/kumiko-types": "0.241.0",
201
+ "@cosmicdrift/kumiko-types": "0.243.0",
202
202
  "bullmq": "^5.76.7",
203
203
  "bun-types": "^1.3.13",
204
204
  "hono": "^4.13.1",
@@ -214,7 +214,7 @@
214
214
  "zod": "^4.4.3"
215
215
  },
216
216
  "devDependencies": {
217
- "@cosmicdrift/kumiko-dispatcher-live": "0.241.0",
217
+ "@cosmicdrift/kumiko-dispatcher-live": "0.243.0",
218
218
  "bun-types": "^1.3.13",
219
219
  "pino-pretty": "^13.1.3"
220
220
  },
package/src/changes.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.242.0",
4
+ "type": "improvement",
5
+ "title": "ActionFormScreenDefinition.redirect accepts an object naming the success-payload field the navigation id comes from (fw#2670).",
6
+ "detail": "`redirect: { screen: \"lease-detail\", idFrom: \"leaseId\" }` (`ActionFormRedirect`, `@cosmicdrift/kumiko-types`) — the renderer previously always navigated with `data.id`, so an action that creates a child record could only land on that child; redirecting back into the parent's detail screen navigated with the child id and 404'd. Making the handler report the parent id instead would break its contract for every other caller, so the routing decision now sits on the screen. Backwards compatible: the plain string form keeps navigating by `data.id`. The boot validator resolves the object's `screen` exactly like the string form (short id or cross-feature QN) and rejects an empty `idFrom`; the id is still only appended when the target screen carries one (`entityEdit`, `projectionDetail`)."
7
+ },
2
8
  {
3
9
  "version": "0.241.0",
4
10
  "type": "improvement",
@@ -2243,7 +2243,7 @@ describe("boot-validator", () => {
2243
2243
  readonly title: string;
2244
2244
  readonly fields: readonly string[];
2245
2245
  }>;
2246
- readonly redirect?: string;
2246
+ readonly redirect?: string | { readonly screen: string; readonly idFrom: string };
2247
2247
  readonly cancelTarget?: string | false;
2248
2248
  readonly extraScreens?: readonly string[];
2249
2249
  readonly mode?: "single" | "wizard";
@@ -2352,6 +2352,35 @@ describe("boot-validator", () => {
2352
2352
  );
2353
2353
  });
2354
2354
 
2355
+ // --- redirect object form with idFrom (fw#2670) ---
2356
+ test("redirect object → existing screen-id → kein Throw", () => {
2357
+ expect(() =>
2358
+ validateBoot([
2359
+ makeFeature({
2360
+ redirect: { screen: "after-form", idFrom: "leaseId" },
2361
+ extraScreens: ["after-form"],
2362
+ }),
2363
+ ]),
2364
+ ).not.toThrow();
2365
+ });
2366
+
2367
+ test("redirect object → unknown screen-id → Throw (unwrap umgeht die Validierung nicht)", () => {
2368
+ expect(() =>
2369
+ validateBoot([makeFeature({ redirect: { screen: "ghost-screen", idFrom: "leaseId" } })]),
2370
+ ).toThrow(/redirect "ghost-screen" does not resolve to a registered screen/);
2371
+ });
2372
+
2373
+ test("redirect object mit leerem idFrom → Throw", () => {
2374
+ expect(() =>
2375
+ validateBoot([
2376
+ makeFeature({
2377
+ redirect: { screen: "after-form", idFrom: " " },
2378
+ extraScreens: ["after-form"],
2379
+ }),
2380
+ ]),
2381
+ ).toThrow(/redirect\.idFrom is empty/);
2382
+ });
2383
+
2355
2384
  test("cancelTarget → existing screen-id → kein Throw", () => {
2356
2385
  expect(() =>
2357
2386
  validateBoot([makeFeature({ cancelTarget: "after-form", extraScreens: ["after-form"] })]),
@@ -1094,17 +1094,30 @@ export function validateScreens(
1094
1094
  }
1095
1095
  validateWizardLayout(feature.name, screenId, "actionForm", screen.layout, featureMap);
1096
1096
  if (screen.redirect !== undefined) {
1097
- // redirect ist entweder die kurze Screen-ID (same-feature, z.B.
1098
- // "item-list") oder eine voll-qualifizierte Cross-Feature-QN
1099
- // (`<feature>:screen:<id>`) — der Renderer strippt letztere beim
1100
- // Navigieren auf die kurze ID (lastSegment), die der nav-Router
1101
- // app-weit auflöst (#1946).
1097
+ // redirect is either a short screen id (same-feature, e.g.
1098
+ // "item-list") or a fully-qualified cross-feature QN
1099
+ // (`<feature>:screen:<id>`) — the renderer strips the latter to the
1100
+ // short id (lastSegment) when navigating, which the nav-router
1101
+ // resolves app-wide (#1946). The object form (fw#2670) carries the
1102
+ // same target under `screen` plus the payload field `idFrom`.
1103
+ const redirectTarget =
1104
+ typeof screen.redirect === "string" ? screen.redirect : screen.redirect.screen;
1105
+ if (
1106
+ typeof screen.redirect !== "string" &&
1107
+ (typeof screen.redirect.idFrom !== "string" || screen.redirect.idFrom.trim() === "")
1108
+ ) {
1109
+ throw new Error(
1110
+ `[Feature ${feature.name}] Screen "${screenId}" (actionForm) redirect.idFrom is empty or not a string — ` +
1111
+ `name the success-payload field carrying the navigation id, or use the plain string ` +
1112
+ `redirect form to navigate with the handler's own "id".`,
1113
+ );
1114
+ }
1102
1115
  validateScreenNavTarget(
1103
1116
  feature.name,
1104
1117
  screenId,
1105
1118
  "actionForm",
1106
1119
  "redirect",
1107
- screen.redirect,
1120
+ redirectTarget,
1108
1121
  allScreenQns,
1109
1122
  feature.screens,
1110
1123
  );
@@ -473,7 +473,13 @@ export type {
473
473
  WriteHandlerFn,
474
474
  WriteResult,
475
475
  } from "./types";
476
- export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, HookPhases, resolveAgentExposure } from "./types";
476
+ export {
477
+ DEFAULT_CURRENCIES,
478
+ DEFAULT_LOCALES,
479
+ HookPhases,
480
+ isAgentVisibleScreen,
481
+ resolveAgentExposure,
482
+ } from "./types";
477
483
  export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers";
478
484
  export type {
479
485
  AwaitedEventType,
@@ -1,7 +1,10 @@
1
1
  // Barrel: re-exports all types from @cosmicdrift/kumiko-types, plus the
2
2
  // runtime helpers below that stay framework-side.
3
3
 
4
- export { resolveAgentExposure } from "@cosmicdrift/kumiko-types/agent-exposure";
4
+ export {
5
+ isAgentVisibleScreen,
6
+ resolveAgentExposure,
7
+ } from "@cosmicdrift/kumiko-types/agent-exposure";
5
8
  export type {
6
9
  ConfigAccessor,
7
10
  ConfigAccessorFactory,
@@ -103,8 +103,8 @@ describe("event-store performance — Gate A", () => {
103
103
  );
104
104
 
105
105
  expect(p95).toBeLessThan(30);
106
- // Tail budget: cold-checkpoint/fsync spikes after warm-up, not connection warm-up.
107
- expect(p99).toBeLessThan(100);
106
+ // Tail budget: absorbs cold-checkpoint/fsync spikes (observed up to 102ms at p50=1.2ms) while still catching order-of-magnitude regressions.
107
+ expect(p99).toBeLessThan(250);
108
108
  });
109
109
 
110
110
  test("read-latency p95 < 25ms for loadAggregate detail reads", async () => {
@@ -145,7 +145,7 @@ describe("event-store performance — Gate A", () => {
145
145
  // 25ms budget kept from the original spike doc's 10ms — an
146
146
  // order-of-magnitude gate, not an idle-best-case one. Tracking: #325.
147
147
  expect(p95).toBeLessThan(25);
148
- expect(p99).toBeLessThan(100);
148
+ expect(p99).toBeLessThan(250);
149
149
  });
150
150
 
151
151
  test("update-latency p95 < 30ms — exercises predecessor-check WHERE EXISTS path", async () => {
@@ -205,7 +205,7 @@ describe("event-store performance — Gate A", () => {
205
205
  );
206
206
 
207
207
  expect(p95).toBeLessThan(30);
208
- expect(p99).toBeLessThan(100);
208
+ expect(p99).toBeLessThan(250);
209
209
  });
210
210
 
211
211
  test("snapshot-load < 50ms for 1000-event aggregate (Gate A)", async () => {
@@ -55,6 +55,7 @@ export type {
55
55
  export type { AccessRule } from "../engine/types/handlers";
56
56
  export type { IconKey, NavDefinition, NavIconKey } from "../engine/types/nav";
57
57
  export type {
58
+ ActionFormRedirect,
58
59
  ActionFormScreenDefinition,
59
60
  ConfigEditScreenDefinition,
60
61
  CustomScreenDefinition,