@cosmicdrift/kumiko-framework 0.158.2 → 0.160.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 (168) hide show
  1. package/package.json +7 -2
  2. package/src/__tests__/consumer-cli.integration.test.ts +110 -0
  3. package/src/api/__tests__/api.test.ts +65 -0
  4. package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
  5. package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +237 -0
  6. package/src/api/__tests__/auth-routes-mfa-verify.test.ts +1 -0
  7. package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
  8. package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
  9. package/src/api/__tests__/jwt.test.ts +150 -1
  10. package/src/api/__tests__/login-rate-limiter-sweep.test.ts +41 -0
  11. package/src/api/__tests__/server-boot-guards.test.ts +71 -0
  12. package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
  13. package/src/api/api-constants.ts +5 -0
  14. package/src/api/auth-middleware.ts +48 -59
  15. package/src/api/auth-routes.ts +51 -17
  16. package/src/api/index.ts +3 -3
  17. package/src/api/jwt.ts +148 -7
  18. package/src/api/pii-leak-guard.ts +5 -2
  19. package/src/api/routes.ts +57 -0
  20. package/src/api/server.ts +19 -5
  21. package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
  22. package/src/bun-db/query.ts +46 -27
  23. package/src/consumer-cli.ts +87 -0
  24. package/src/crypto/__tests__/pii-field-encryption.test.ts +69 -13
  25. package/src/crypto/blind-index.ts +8 -4
  26. package/src/crypto/event-pii.ts +1 -0
  27. package/src/crypto/kms-adapter.ts +2 -118
  28. package/src/crypto/pii-field-encryption.ts +49 -15
  29. package/src/db/__tests__/build-filter-where.test.ts +34 -0
  30. package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
  31. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +396 -0
  32. package/src/db/__tests__/event-store-executor.integration.test.ts +5 -5
  33. package/src/db/blind-index-cleanup.ts +3 -1
  34. package/src/db/connection.ts +3 -11
  35. package/src/db/cursor.ts +1 -18
  36. package/src/db/dialect.ts +8 -19
  37. package/src/db/encryption.ts +2 -3
  38. package/src/db/entity-table-meta-types.ts +2 -0
  39. package/src/db/entity-table-meta.ts +16 -90
  40. package/src/db/event-store-executor.ts +4 -96
  41. package/src/db/queries/backfill-pii.ts +1 -0
  42. package/src/db/queries/event-consumer.ts +35 -2
  43. package/src/db/table-builder.ts +2 -19
  44. package/src/db/tenant-db.ts +6 -55
  45. package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
  46. package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -233
  47. package/src/engine/__tests__/boot-validator.test.ts +46 -0
  48. package/src/engine/__tests__/codemod-pipeline.test.ts +139 -10
  49. package/src/engine/__tests__/define-roles.test.ts +21 -0
  50. package/src/engine/__tests__/engine.test.ts +28 -0
  51. package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
  52. package/src/engine/__tests__/registry-facade-sweep.test.ts +80 -0
  53. package/src/engine/__tests__/registry.test.ts +40 -0
  54. package/src/engine/__tests__/store-table.test.ts +12 -0
  55. package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
  56. package/src/engine/boot-validator/action-wiring.ts +1 -1
  57. package/src/engine/boot-validator/boot-check.ts +21 -0
  58. package/src/engine/boot-validator/entity-handler.ts +10 -1
  59. package/src/engine/boot-validator/entity-list-screens.ts +1 -1
  60. package/src/engine/boot-validator/gdpr-storage.ts +0 -112
  61. package/src/engine/boot-validator/index.ts +3 -9
  62. package/src/engine/boot-validator/screens.ts +1 -1
  63. package/src/engine/define-feature.ts +2 -0
  64. package/src/engine/define-handler.ts +11 -91
  65. package/src/engine/entity-handlers.ts +15 -27
  66. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +11 -1
  67. package/src/engine/feature-ast/__tests__/parse.test.ts +983 -3
  68. package/src/engine/feature-ast/__tests__/patch.test.ts +168 -0
  69. package/src/engine/feature-ast/__tests__/patcher.test.ts +7 -0
  70. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +9 -0
  71. package/src/engine/feature-ast/extractors/handlers.ts +19 -2
  72. package/src/engine/feature-ast/extractors/index.ts +1 -0
  73. package/src/engine/feature-ast/index.ts +2 -0
  74. package/src/engine/feature-ast/parse.ts +3 -0
  75. package/src/engine/feature-ast/patch.ts +2 -0
  76. package/src/engine/feature-ast/patcher.ts +21 -0
  77. package/src/engine/feature-ast/patterns.ts +16 -0
  78. package/src/engine/feature-ast/render.ts +15 -0
  79. package/src/engine/feature-builder-state.ts +6 -0
  80. package/src/engine/feature-config-events-jobs.ts +1 -1
  81. package/src/engine/feature-entity-handlers.ts +36 -2
  82. package/src/engine/feature-ui-extensions.ts +5 -1
  83. package/src/engine/field-helpers.ts +31 -0
  84. package/src/engine/handler-helpers.ts +26 -0
  85. package/src/engine/hook-helpers.ts +14 -0
  86. package/src/engine/index.ts +5 -2
  87. package/src/engine/ownership.ts +22 -76
  88. package/src/engine/pattern-library/__tests__/library.test.ts +9 -0
  89. package/src/engine/pattern-library/library.ts +2 -0
  90. package/src/engine/pattern-library/mixed-schemas.ts +37 -0
  91. package/src/engine/registry-facade.ts +9 -0
  92. package/src/engine/registry-ingest.ts +10 -0
  93. package/src/engine/registry-state.ts +3 -0
  94. package/src/engine/registry-validate.ts +1 -1
  95. package/src/engine/screen-helpers.ts +54 -0
  96. package/src/engine/tier-resolver-extension.ts +3 -2
  97. package/src/engine/types/config.ts +2 -497
  98. package/src/engine/types/define-handler.ts +2 -0
  99. package/src/engine/types/entity-handlers.ts +2 -0
  100. package/src/engine/types/event-type-map.ts +1 -37
  101. package/src/engine/types/feature.ts +2 -976
  102. package/src/engine/types/fields.ts +2 -697
  103. package/src/engine/types/handlers.ts +2 -839
  104. package/src/engine/types/hooks.ts +2 -184
  105. package/src/engine/types/http-route.ts +1 -72
  106. package/src/engine/types/identifiers.ts +1 -47
  107. package/src/engine/types/index.ts +66 -33
  108. package/src/engine/types/nav.ts +2 -67
  109. package/src/engine/types/ownership.ts +2 -0
  110. package/src/engine/types/projection.ts +2 -165
  111. package/src/engine/types/relations.ts +1 -51
  112. package/src/engine/types/screen.ts +2 -793
  113. package/src/engine/types/step.ts +2 -334
  114. package/src/engine/types/target-ref.ts +1 -21
  115. package/src/engine/types/tree-node.ts +1 -129
  116. package/src/engine/types/workspace.ts +2 -42
  117. package/src/entrypoint/index.ts +2 -2
  118. package/src/errors/write-error-info.ts +6 -22
  119. package/src/event-store/__tests__/event-store.integration.test.ts +31 -0
  120. package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +43 -0
  121. package/src/event-store/errors.ts +2 -35
  122. package/src/event-store/event-store.ts +28 -51
  123. package/src/event-store/events-schema.ts +1 -10
  124. package/src/event-store/index.ts +3 -2
  125. package/src/event-store/snapshot.ts +11 -35
  126. package/src/event-store/types.ts +2 -0
  127. package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
  128. package/src/files/file-handle.ts +2 -19
  129. package/src/files/provider-resolver.ts +3 -5
  130. package/src/files/types.ts +5 -54
  131. package/src/i18n/required-surface-keys.ts +1 -1
  132. package/src/jobs/__tests__/jobs.integration.test.ts +102 -1
  133. package/src/logging/types.ts +1 -7
  134. package/src/observability/types/index.ts +1 -29
  135. package/src/observability/types/metric.ts +1 -56
  136. package/src/observability/types/provider.ts +1 -32
  137. package/src/observability/types/span.ts +1 -58
  138. package/src/pipeline/__tests__/dispatcher.test.ts +134 -1
  139. package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
  140. package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +180 -0
  141. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +208 -0
  142. package/src/pipeline/dispatch-shared.ts +51 -3
  143. package/src/pipeline/dispatch-stream.ts +74 -0
  144. package/src/pipeline/dispatcher-utils.ts +1 -1
  145. package/src/pipeline/dispatcher.ts +7 -0
  146. package/src/pipeline/entity-cache.ts +2 -33
  147. package/src/pipeline/event-consumer-state.ts +28 -3
  148. package/src/pipeline/event-dispatcher-admin.ts +4 -0
  149. package/src/pipeline/event-dispatcher-delivery.ts +29 -3
  150. package/src/pipeline/event-dispatcher.ts +27 -1
  151. package/src/pipeline/multi-stream-apply-context.ts +4 -42
  152. package/src/pipeline/system-hooks.ts +7 -0
  153. package/src/rate-limit/resolver.ts +10 -30
  154. package/src/search/types.ts +1 -39
  155. package/src/secrets/__tests__/envelope-cipher.test.ts +2 -30
  156. package/src/secrets/__tests__/envelope.test.ts +1 -1
  157. package/src/secrets/envelope-cipher.ts +17 -45
  158. package/src/secrets/types.ts +2 -177
  159. package/src/stack/__tests__/event-collector.test.ts +42 -0
  160. package/src/testing/__tests__/late-bound.test.ts +25 -0
  161. package/src/testing/__tests__/wait-for.test.ts +53 -0
  162. package/src/testing/boot-validator-fixture.ts +1 -1
  163. package/src/testing/file-provider-contract.ts +84 -0
  164. package/src/testing/handler-context.ts +1 -1
  165. package/src/testing/index.ts +1 -0
  166. package/src/time/geo-tz.ts +1 -32
  167. package/src/time/tz-context.ts +9 -56
  168. package/src/ui-types/index.ts +7 -7
@@ -42,6 +42,7 @@ export { crossTenantOverrideDenied } from "./cross-tenant";
42
42
  export { defineFeature } from "./define-feature";
43
43
  export type {
44
44
  QueryHandlerDefinition,
45
+ StreamHandlerDefinition,
45
46
  WriteHandlerDefinition,
46
47
  WriteHandlerInput,
47
48
  } from "./define-handler";
@@ -177,6 +178,7 @@ export {
177
178
  checkWriteFieldRoles,
178
179
  filterReadFields,
179
180
  } from "./field-access";
181
+ export { resolveName, withResponseData } from "./handler-helpers";
180
182
  // findForbiddenMembershipRole/isForbiddenMembershipRole/
181
183
  // stripForbiddenMembershipRoles/buildSessionRoles are Public API for host
182
184
  // apps that build their own membership handlers. FORBIDDEN_MEMBERSHIP_ROLES
@@ -203,6 +205,7 @@ export { runsInLane } from "./run-in";
203
205
  export type { StepListOutcome } from "./run-pipeline";
204
206
  export { runPipeline, runStepList } from "./run-pipeline";
205
207
  export { buildInsertSchema, buildUpdateSchema, fieldToZod } from "./schema-builder";
208
+ export { isExtensionEditSection, normalizeEditField, normalizeListColumn } from "./screen-helpers";
206
209
  export type { TransitionGraph } from "./state-machine";
207
210
  export { defineTransitions, guardTransition } from "./state-machine";
208
211
  export {
@@ -362,6 +365,8 @@ export type {
362
365
  SecretKeyHandle,
363
366
  SelectFieldDef,
364
367
  SessionUser,
368
+ StreamHandlerDef,
369
+ StreamHandlerFn,
365
370
  Subscribe,
366
371
  TargetRef,
367
372
  TenantId,
@@ -385,9 +390,7 @@ export type {
385
390
  WriteResult,
386
391
  } from "./types";
387
392
  export { DEFAULT_CURRENCIES, HookPhases } from "./types";
388
- export { resolveName, withResponseData } from "./types/handlers";
389
393
  export { isSystemTenant, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers";
390
- export { isExtensionEditSection, normalizeEditField, normalizeListColumn } from "./types/screen";
391
394
  export type {
392
395
  PipelineBuildCtx,
393
396
  PipelineCtx,
@@ -20,64 +20,28 @@
20
20
  import { toSnakeCase } from "../db/table-builder";
21
21
  import type { SessionUser } from "./types";
22
22
 
23
- // Parameterised SQL fragment produced by buildOwnershipClause + by the
24
- // WhereRule escape-hatch. Caller weaves `sqlText` into a larger statement,
25
- // renumbering placeholders if needed (FragmentBuilder below).
26
- export type SqlFragment = {
27
- readonly sqlText: string;
28
- readonly params: readonly unknown[];
29
- };
30
-
31
- // Reference spec supported by `from()`:
32
- // "user:id" → user.id
33
- // "user:tenantId" → user.tenantId (rarely needed — TenantDb scopes anyway)
34
- // "claim:<featureName>:<key>" → user.claims["<featureName>:<key>"]
35
- //
36
- // The string form is keyed so the framework can look up the referenced
37
- // Registry entry at boot (Claim-QN exists? Column type compatible?). A typed
38
- // object form would force features to import each other's handles — the
39
- // whole point of H.2's unified path is string-based references, no imports.
40
- export type OwnershipRef = string;
41
-
42
- // Resolved during `from()` — the parser eagerly splits the prefix so the
43
- // runtime evaluator avoids string-parsing on every row. `kind` drives the
44
- // evaluator branch; the rest is the resolved metadata.
45
- export type FromRuleKind = "user" | "claim";
46
-
47
- export type FromRule = {
48
- readonly kind: "from";
49
- readonly refKind: FromRuleKind;
50
- // For "user:id" → "id"; for "user:tenantId" → "tenantId".
51
- // For "claim:<featureName>:<key>" → "<featureName>:<key>" (the full QN,
52
- // which is exactly the key under which the JWT stores the value).
53
- readonly refPath: string;
54
- // Row-column to match against. For claim rules defaults to the claim's
55
- // shortName (second segment of the claim QN). For user-rules the column
56
- // is always explicit.
57
- readonly column: string;
58
- };
59
-
60
- // Context passed to a WhereRule escape-hatch. The author returns a SqlFragment
61
- // whose placeholders start at `paramStart` ($N, $N+1, ...); the framework
62
- // concatenates the fragment into the larger query.
63
- export type WhereRuleContext<TTable = unknown> = {
64
- readonly table: TTable;
65
- readonly tableName: string;
66
- readonly paramStart: number;
67
- };
68
-
69
- export type WhereRule<TTable = unknown> = {
70
- readonly kind: "where";
71
- readonly where: (user: SessionUser, ctx: WhereRuleContext<TTable>) => SqlFragment;
72
- };
73
-
74
- // "all" collapses to a primitive so map authors can write `Admin: "all"`
75
- // without importing a helper.
76
- export type OwnershipRule = "all" | FromRule | WhereRule;
77
-
78
- // Per-role map: every key is a role name, value is the rule that role
79
- // satisfies to pass the access check.
80
- export type OwnershipMap = Readonly<Record<string, OwnershipRule>>;
23
+ // Types live in engine/types/ownership.ts (no runtime dependency); re-exported
24
+ // here for backwards compatibility with existing importers of this file.
25
+ export type {
26
+ FromRule,
27
+ FromRuleKind,
28
+ OwnershipClause,
29
+ OwnershipMap,
30
+ OwnershipRef,
31
+ OwnershipRule,
32
+ SqlFragment,
33
+ WhereRule,
34
+ WhereRuleContext,
35
+ } from "./types/ownership";
36
+
37
+ import type {
38
+ FromRule,
39
+ OwnershipClause,
40
+ OwnershipMap,
41
+ OwnershipRef,
42
+ OwnershipRule,
43
+ SqlFragment,
44
+ } from "./types/ownership";
81
45
 
82
46
  // Parse an OwnershipRef into kind + resolved path + default column.
83
47
  // Throws on malformed input so the error surfaces at `from()`-call-site (in
@@ -254,24 +218,6 @@ export function userCanCreateFieldRow(
254
218
  return false;
255
219
  }
256
220
 
257
- // Result of buildOwnershipClause. The discriminant lets the caller handle
258
- // the three outcomes without inspecting SQL internals:
259
- //
260
- // "pass" → user is unrestricted. Run the query as-is.
261
- // "empty" → user has a role mapped but no rule accepts any row (missing
262
- // claim, empty array, role not in map). Skip the DB call entirely
263
- // — returning [] is equivalent and avoids a pointless roundtrip.
264
- // "sql" → apply the parameterised fragment as an AND on the query.
265
- // Caller is responsible for renumbering placeholders when
266
- // concatenating with other fragments (see `shiftParams` below).
267
- //
268
- // "empty" vs. "pass" is the critical distinction for a safe default:
269
- // undefined/pass = allow, empty = deny-by-construction.
270
- export type OwnershipClause =
271
- | { readonly kind: "pass" }
272
- | { readonly kind: "empty" }
273
- | { readonly kind: "sql"; readonly sqlText: string; readonly params: readonly unknown[] };
274
-
275
221
  const PASS_CLAUSE: OwnershipClause = { kind: "pass" };
276
222
  const EMPTY_CLAUSE: OwnershipClause = { kind: "empty" };
277
223
 
@@ -46,6 +46,7 @@ const ALL_KINDS: FeaturePatternKind[] = [
46
46
  "screen",
47
47
  "writeHandler",
48
48
  "queryHandler",
49
+ "streamHandler",
49
50
  "hook",
50
51
  "job",
51
52
  "notification",
@@ -270,6 +271,14 @@ function makePlaceholderPattern(kind: FeaturePatternKind): FeaturePattern {
270
271
  schemaSource: PLACEHOLDER_BODY_LOC,
271
272
  handlerBody: PLACEHOLDER_BODY_LOC,
272
273
  };
274
+ case "streamHandler":
275
+ return {
276
+ kind,
277
+ source: PLACEHOLDER_LOC,
278
+ handlerName: "x",
279
+ schemaSource: PLACEHOLDER_BODY_LOC,
280
+ handlerBody: PLACEHOLDER_BODY_LOC,
281
+ };
273
282
  case "hook":
274
283
  return {
275
284
  kind,
@@ -25,6 +25,7 @@ import {
25
25
  projectionSchema,
26
26
  queryHandlerSchema,
27
27
  screenSchema,
28
+ streamHandlerSchema,
28
29
  writeHandlerSchema,
29
30
  } from "./mixed-schemas";
30
31
  import {
@@ -79,6 +80,7 @@ export const PATTERN_LIBRARY: Readonly<Record<FeaturePatternKind, PatternFormSch
79
80
  screen: screenSchema,
80
81
  writeHandler: writeHandlerSchema,
81
82
  queryHandler: queryHandlerSchema,
83
+ streamHandler: streamHandlerSchema,
82
84
  hook: hookSchema,
83
85
  job: jobSchema,
84
86
  notification: notificationSchema,
@@ -125,6 +125,43 @@ export const queryHandlerSchema: PatternFormSchema = {
125
125
  ],
126
126
  };
127
127
 
128
+ export const streamHandlerSchema: PatternFormSchema = {
129
+ kind: "streamHandler",
130
+ label: { en: "Stream handler", de: "Stream-Handler" },
131
+ summary: { en: "Streaming read endpoint (SSE) with Zod schema + async-generator body." },
132
+ category: "behaviour",
133
+ editability: "mixed",
134
+ fields: [
135
+ {
136
+ path: "handlerName",
137
+ label: { en: "Name", de: "Name" },
138
+ input: "text",
139
+ required: true,
140
+ placeholder: "chat:complete",
141
+ },
142
+ {
143
+ path: "schemaSource",
144
+ label: { en: "Zod schema (source)", de: "Zod-Schema (Source)" },
145
+ input: "code-block",
146
+ language: "zod",
147
+ readOnly: true,
148
+ },
149
+ {
150
+ path: "handlerBody",
151
+ label: { en: "Handler body (source)", de: "Handler-Body (Source)" },
152
+ input: "code-block",
153
+ language: "typescript",
154
+ readOnly: true,
155
+ },
156
+ accessRuleField,
157
+ {
158
+ path: "rateLimit",
159
+ label: { en: "Rate limit", de: "Rate-Limit" },
160
+ input: "json-readonly",
161
+ },
162
+ ],
163
+ };
164
+
128
165
  export const hookSchema: PatternFormSchema = {
129
166
  kind: "hook",
130
167
  label: { en: "Lifecycle hook", de: "Lifecycle-Hook" },
@@ -30,6 +30,7 @@ import type {
30
30
  SearchPayloadContributorFn,
31
31
  SecretKeyDefinition,
32
32
  StoreTableDef,
33
+ StreamHandlerDef,
33
34
  TranslationKeys,
34
35
  TreeActionDef,
35
36
  WorkspaceDefinition,
@@ -72,6 +73,14 @@ export function buildRegistryFacade(state: RegistryState): Registry {
72
73
  return state.queryHandlerMap;
73
74
  },
74
75
 
76
+ getStreamHandler(name: string): StreamHandlerDef | undefined {
77
+ return state.streamHandlerMap.get(name);
78
+ },
79
+
80
+ getAllStreamHandlers(): ReadonlyMap<string, StreamHandlerDef> {
81
+ return state.streamHandlerMap;
82
+ },
83
+
75
84
  getSearchableFields(entityName: string): readonly string[] {
76
85
  return state.searchableFieldsCache.get(entityName) ?? [];
77
86
  },
@@ -80,6 +80,16 @@ export function populateHandlers(state: RegistryState, feature: FeatureDefinitio
80
80
  state.queryHandlerMap.set(qualified, { ...handler, name: qualified });
81
81
  state.handlerFeatureMap.set(qualified, feature.name);
82
82
  }
83
+
84
+ // Stream handlers: scope:stream:name
85
+ for (const [name, handler] of Object.entries(feature.streamHandlers ?? {})) {
86
+ const qualified = qualify(feature.name, "stream", name);
87
+ if (state.streamHandlerMap.has(qualified)) {
88
+ throw new Error(`Duplicate stream handler: "${qualified}" (registered by multiple features)`);
89
+ }
90
+ state.streamHandlerMap.set(qualified, { ...handler, name: qualified });
91
+ state.handlerFeatureMap.set(qualified, feature.name);
92
+ }
83
93
  }
84
94
 
85
95
  // Config keys: scope:config:name, duplicate-guarded.
@@ -37,6 +37,7 @@ import type {
37
37
  SearchPayloadContributorFn,
38
38
  SecretKeyDefinition,
39
39
  StoreTableDef,
40
+ StreamHandlerDef,
40
41
  TreeActionDef,
41
42
  WorkspaceDefinition,
42
43
  WriteHandlerDef,
@@ -165,6 +166,7 @@ export type RegistryState = {
165
166
  relationMap: Map<string, Record<string, RelationDefinition>>;
166
167
  writeHandlerMap: Map<string, WriteHandlerDef>;
167
168
  queryHandlerMap: Map<string, QueryHandlerDef>;
169
+ streamHandlerMap: Map<string, StreamHandlerDef>;
168
170
  preSaveHooks: Map<string, OwnedFn<PreSaveHookFn>[]>;
169
171
  postSaveHooks: Map<string, PhasedHook<PostSaveHookFn>[]>;
170
172
  preDeleteHooks: Map<string, PhasedHook<PreDeleteHookFn>[]>;
@@ -229,6 +231,7 @@ export function createInitialState(): RegistryState {
229
231
  relationMap: new Map(),
230
232
  writeHandlerMap: new Map(),
231
233
  queryHandlerMap: new Map(),
234
+ streamHandlerMap: new Map(),
232
235
  preSaveHooks: new Map(),
233
236
  postSaveHooks: new Map(),
234
237
  preDeleteHooks: new Map(),
@@ -1,4 +1,5 @@
1
1
  import { configureEventPiiCatalog } from "../crypto/event-pii";
2
+ import { resolveName } from "./handler-helpers";
2
3
  import type { RegistryState } from "./registry-state";
3
4
  import { buildImplicitProjection, hasFieldAccessRules, qualify } from "./registry-state";
4
5
  import {
@@ -11,7 +12,6 @@ import {
11
12
  } from "./soft-delete-cleanup";
12
13
  import type { EventPiiFields, EventUpcastFn, FeatureDefinition } from "./types";
13
14
  import { HookPhases } from "./types";
14
- import { resolveName } from "./types/handlers";
15
15
 
16
16
  function allHandlerQns(state: RegistryState): ReadonlySet<string> {
17
17
  return new Set([...state.writeHandlerMap.keys(), ...state.queryHandlerMap.keys()]);
@@ -0,0 +1,54 @@
1
+ import type {
2
+ EditExtensionSection,
3
+ EditFieldSpec,
4
+ EditSectionSpec,
5
+ FieldCondition,
6
+ FormatSpec,
7
+ ListColumnSpec,
8
+ } from "./types/screen";
9
+
10
+ export function isExtensionEditSection(section: EditSectionSpec): section is EditExtensionSection {
11
+ return section.kind === "extension";
12
+ }
13
+
14
+ // Type guard — narrows FieldRenderer to FormatSpec. Useful for renderer
15
+ // authors who branch on the three FieldRenderer variants without manual
16
+ // "format" in renderer checks.
17
+ export function isFormatSpec(r: unknown): r is FormatSpec {
18
+ return typeof r === "object" && r !== null && "format" in r && typeof r.format === "string";
19
+ }
20
+
21
+ // Collapse the string-shorthand into the object form. Both the boot-validator
22
+ // and (later) ui-core's view-model builder iterate over fields/columns — the
23
+ // helper keeps that loop from growing two branches everywhere.
24
+ export function normalizeListColumn(c: ListColumnSpec): Exclude<ListColumnSpec, string> {
25
+ const col = typeof c === "string" ? { field: c } : c;
26
+ if (
27
+ typeof process !== "undefined" &&
28
+ process.env.NODE_ENV !== "production" &&
29
+ col.renderer !== undefined &&
30
+ typeof col.renderer === "function"
31
+ ) {
32
+ // biome-ignore lint/suspicious/noConsole: dev-only warning
33
+ console.warn(
34
+ `[kumiko] normalizeListColumn: Feld "${col.field}" hat einen Funktions-Renderer — dieser wird von JSON.stringify verworfen. Bitte auf FormatSpec ({ format: "..." }) migrieren.`,
35
+ );
36
+ }
37
+ return col;
38
+ }
39
+
40
+ /** Evaluates a declarative FieldCondition against the current row/form
41
+ * values. THE single implementation — renderer (row-action visibility),
42
+ * headless view-model (visible/readOnly/required) and render-edit
43
+ * (form-condition closures) reuse it; three hand-rolled copies had
44
+ * already drifted in shape. */
45
+ export function evalFieldCondition(cond: FieldCondition, values: Record<string, unknown>): boolean {
46
+ if (typeof cond === "boolean") return cond;
47
+ const val = values[cond.field];
48
+ if ("eq" in cond) return val === cond.eq;
49
+ return val !== cond.ne;
50
+ }
51
+
52
+ export function normalizeEditField(f: EditFieldSpec): Exclude<EditFieldSpec, string> {
53
+ return typeof f === "string" ? { field: f } : f;
54
+ }
@@ -48,8 +48,9 @@ export type EffectiveFeaturesResolver = ((tenantId: TenantId) => ReadonlySet<str
48
48
  * Signup-Datum des Tenants live (tenant.inserted_at) und returnt true wenn
49
49
  * der Tenant im Trial-Fenster ist UND das Feature zum Trial-Tier gehört.
50
50
  * Async + nicht im Boot-Cache, weil der Trial zeit-abgeleitet ist (ändert
51
- * sich zwischen Requests). Nur die 2 Gate-Aufrufstellen awaiten ihn; der
52
- * synchrone Hot-Path (ctx.hasFeature, Feature-Set) bleibt unberührt.
51
+ * sich zwischen Requests). Konsultiert an 3 Stellen: den 2 Dispatch-Gate-
52
+ * Aufrufstellen (checkFeatureEnabled) UND ctx.hasFeature im Handler-Body
53
+ * (fällt darauf zurück, wenn der synchrone Feature-Set-Check negativ ist).
53
54
  */
54
55
  readonly trialGate?: TrialGate;
55
56
  };