@cosmicdrift/kumiko-framework 0.240.0 → 0.242.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.240.0",
3
+ "version": "0.242.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.240.0",
201
+ "@cosmicdrift/kumiko-types": "0.242.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.240.0",
217
+ "@cosmicdrift/kumiko-dispatcher-live": "0.242.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,28 @@
1
1
  [
2
+ {
3
+ "version": "0.241.0",
4
+ "type": "improvement",
5
+ "title": "ProjectionDetailScreenDefinition gains singleton: boolean for self-service screens bound to a caller-scoped query (fw#2312).",
6
+ "detail": "A `projectionDetail` screen previously always resolved its row from a path id — the only case `ProjectionDetailBody` handled — so binding one to a `me`-style query (e.g. `user:query:user:me`) rendered nothing but a \"missing id\" error banner. With `singleton: true`, the query is called without the `idParam` key (there is no id to send) and any path id is ignored rather than forwarded into the query or into extension sections' entity-id resolution — a singleton row is server-picked, so no client-supplied id can reach it. The boot validator rejects `idParam` or `detailFor` declared together with `singleton` (both are meaningless once the server owns row selection)."
7
+ },
8
+ {
9
+ "version": "0.241.0",
10
+ "type": "improvement",
11
+ "title": "DashboardStatPanel gains an optional params field for static query parameters (fw#2312).",
12
+ "detail": "`DashboardStatPanel.params?: Readonly<Record<string, unknown>>` (`@cosmicdrift/kumiko-types`) is merged under the panel's dynamic `filterParams` by `@cosmicdrift/kumiko-renderer-web`'s dashboard body — lets a panel pin a value (e.g. a status facet) the screen-wide filter doesn't cover, without needing its own query. Additive; existing `dashboard` screens are unaffected."
13
+ },
14
+ {
15
+ "version": "0.241.0",
16
+ "type": "improvement",
17
+ "title": "EditFieldSpec.renderer gains a json format for pretty-printed structured fields (fw#2312).",
18
+ "detail": "`FieldFormatRegistry.json: { indent?: number }` (`@cosmicdrift/kumiko-types`, resolved by `@cosmicdrift/kumiko-headless`) — set `renderer: { format: \"json\", indent: 2 }` on an `EditFieldSpec` to pretty-print a JSON-string field instead of showing the raw escaped string. Pairs with the new `JsonView` primitive below when `@cosmicdrift/kumiko-renderer-web` is in use."
19
+ },
20
+ {
21
+ "version": "0.241.0",
22
+ "type": "improvement",
23
+ "title": "Optional JsonView primitive for structured, syntax-highlighted JSON display (fw#2312).",
24
+ "detail": "`CorePrimitives.JsonView` (`@cosmicdrift/kumiko-renderer`, web impl `DefaultJsonView` in `@cosmicdrift/kumiko-renderer-web`) receives the raw value (not a pre-stringified string) and stringifies + tokenizes itself; never throws on circular references, `BigInt`, or other non-serializable input. Optional on `CorePrimitives`, so an existing partial `CorePrimitives` test mock/provider keeps compiling unchanged — every call site (renderer's `format: \"json\"` fields, the jsonb/embedded/files/images fallback banner, `EditorPanel`'s unresolved-target args display) falls back to the prior `<Text>`/`<pre>` rendering when no `JsonView` is registered. A custom `PrimitivesProvider` that wants the improved rendering needs to add a `JsonView` implementation; nothing breaks if it doesn't."
25
+ },
2
26
  {
3
27
  "version": "0.235.0",
4
28
  "type": "breaking",
@@ -0,0 +1,81 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { z } from "zod";
3
+ import { withBootValidatorFixture } from "../../testing/boot-validator-fixture";
4
+ import { validateBoot as validateBootRaw } from "../boot-validator";
5
+ import { defineFeature } from "../define-feature";
6
+ import { createEntity, createTextField } from "../factories";
7
+
8
+ function validateBoot(features: Parameters<typeof validateBootRaw>[0]): void {
9
+ validateBootRaw(withBootValidatorFixture(features));
10
+ }
11
+
12
+ // fw#2312: `singleton: true` marks a projectionDetail screen whose query
13
+ // determines the shown row from the caller's context (session), not a row
14
+ // id in the path — a self-service screen like "my profile". idParam names
15
+ // the query-payload key for a path row id, which a singleton screen never
16
+ // sends; detailFor drives the auto-generated "Edit" action's navigation via
17
+ // that same path id (fw#2166). Both combinations are rejected at boot
18
+ // instead of silently winning (idParam ignored / edit button opening a
19
+ // blank create form).
20
+ describe("validateBoot — projectionDetail singleton (fw#2312)", () => {
21
+ test("singleton + idParam throws", () => {
22
+ const feature = defineFeature("app", (r) => {
23
+ r.queryHandler("me", z.object({}), async () => ({ id: "u1" }), {
24
+ access: { openToAll: true },
25
+ });
26
+ r.screen({
27
+ id: "profile",
28
+ type: "projectionDetail",
29
+ query: "app:query:me",
30
+ singleton: true,
31
+ idParam: "userId",
32
+ layout: { sections: [{ title: "s", fields: ["id"] }] },
33
+ });
34
+ });
35
+ expect(() => validateBoot([feature])).toThrow(
36
+ /Screen "profile" \(projectionDetail\) sets both singleton: true and idParam/,
37
+ );
38
+ });
39
+
40
+ test("singleton + detailFor throws", () => {
41
+ const feature = defineFeature("app", (r) => {
42
+ r.entity("user", createEntity({ fields: { name: createTextField() } }));
43
+ r.queryHandler("me", z.object({}), async () => ({ id: "u1" }), {
44
+ access: { openToAll: true },
45
+ });
46
+ r.screen({
47
+ id: "profile",
48
+ type: "projectionDetail",
49
+ query: "app:query:me",
50
+ singleton: true,
51
+ detailFor: "user",
52
+ layout: { sections: [{ title: "s", fields: ["id"] }] },
53
+ });
54
+ r.screen({
55
+ id: "user-edit",
56
+ type: "entityEdit",
57
+ entity: "user",
58
+ layout: { sections: [{ columns: 1, fields: ["name"] }] },
59
+ });
60
+ });
61
+ expect(() => validateBoot([feature])).toThrow(
62
+ /Screen "profile" \(projectionDetail\) sets both singleton: true and detailFor/,
63
+ );
64
+ });
65
+
66
+ test("singleton without idParam/detailFor boots cleanly", () => {
67
+ const feature = defineFeature("app", (r) => {
68
+ r.queryHandler("me", z.object({}), async () => ({ id: "u1" }), {
69
+ access: { openToAll: true },
70
+ });
71
+ r.screen({
72
+ id: "profile",
73
+ type: "projectionDetail",
74
+ query: "app:query:me",
75
+ singleton: true,
76
+ layout: { sections: [{ title: "s", fields: ["id"] }] },
77
+ });
78
+ });
79
+ expect(() => validateBoot([feature])).not.toThrow();
80
+ });
81
+ });
@@ -622,6 +622,26 @@ export function validateScreens(
622
622
  `[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) has empty or non-string query.`,
623
623
  );
624
624
  }
625
+ if (screen.singleton === true && screen.idParam !== undefined) {
626
+ throw new Error(
627
+ `[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) sets both singleton: true ` +
628
+ `and idParam — singleton means the server picks the row from the caller's context, so there ` +
629
+ `is no row id to send under idParam. Remove idParam.`,
630
+ );
631
+ }
632
+ // singleton has no row id in the path, but the auto-generated "Edit"
633
+ // action (fw#2166, defaultEditAction in kumiko-screen.tsx) navigates
634
+ // to the detailFor entity's entityEdit screen using that path id —
635
+ // under singleton it would silently open that screen in CREATE mode
636
+ // instead of editing the singleton row. Reject rather than let that
637
+ // win silently, same as the idParam check above.
638
+ if (screen.singleton === true && screen.detailFor !== undefined) {
639
+ throw new Error(
640
+ `[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) sets both singleton: true ` +
641
+ `and detailFor — the auto-generated "Edit" action navigates using the path's row id, which a ` +
642
+ `singleton screen never has. Remove detailFor, or declare an explicit "edit" action instead.`,
643
+ );
644
+ }
625
645
  if (screen.layout.sections.length === 0) {
626
646
  throw new Error(
627
647
  `[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) has an empty sections list — ` +
@@ -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 () => {