@cosmicdrift/kumiko-framework 0.146.4 → 0.147.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 (68) hide show
  1. package/package.json +6 -2
  2. package/src/api/auth-routes.ts +32 -67
  3. package/src/api/routes.ts +1 -3
  4. package/src/bun-db/__tests__/PATTERN.md +0 -1
  5. package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +29 -3
  6. package/src/db/__tests__/schema-inspection.test.ts +7 -0
  7. package/src/db/event-store-executor-context.ts +398 -0
  8. package/src/db/event-store-executor-read.ts +276 -0
  9. package/src/db/event-store-executor-write.ts +615 -0
  10. package/src/db/event-store-executor.ts +29 -1166
  11. package/src/db/queries/shadow-swap.ts +3 -1
  12. package/src/db/schema-inspection.ts +1 -1
  13. package/src/engine/__tests__/boot-validator-dashboard.test.ts +10 -0
  14. package/src/engine/__tests__/engine.test.ts +45 -1
  15. package/src/engine/__tests__/event-migration-declarative.test.ts +6 -0
  16. package/src/engine/__tests__/membership-roles.test.ts +4 -10
  17. package/src/engine/__tests__/screen.test.ts +26 -0
  18. package/src/engine/boot-validator/entity-list-screens.ts +1 -1
  19. package/src/engine/boot-validator/gdpr-storage.ts +1 -1
  20. package/src/engine/boot-validator/index.ts +12 -8
  21. package/src/engine/boot-validator/nav.ts +120 -0
  22. package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +12 -190
  23. package/src/engine/boot-validator/workspaces.ts +68 -0
  24. package/src/engine/define-feature.ts +77 -1011
  25. package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
  26. package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
  27. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
  28. package/src/engine/feature-ast/__tests__/parse.test.ts +291 -2
  29. package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -1
  30. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +73 -0
  31. package/src/engine/feature-ast/extractors/round1.ts +3 -3
  32. package/src/engine/feature-ast/extractors/round4.ts +45 -10
  33. package/src/engine/feature-ast/extractors/shared.ts +64 -6
  34. package/src/engine/feature-ast/parse.ts +123 -10
  35. package/src/engine/feature-ast/patterns.ts +14 -7
  36. package/src/engine/feature-ast/render.ts +28 -7
  37. package/src/engine/feature-builder-state.ts +165 -0
  38. package/src/engine/feature-config-events-jobs.ts +303 -0
  39. package/src/engine/feature-entity-handlers.ts +161 -0
  40. package/src/engine/feature-ui-extensions.ts +413 -0
  41. package/src/engine/index.ts +0 -2
  42. package/src/engine/pattern-library/library.ts +44 -1131
  43. package/src/engine/pattern-library/mixed-schemas.ts +484 -0
  44. package/src/engine/pattern-library/opaque-schemas.ts +124 -0
  45. package/src/engine/pattern-library/shared-fields.ts +80 -0
  46. package/src/engine/pattern-library/static-schemas.ts +456 -0
  47. package/src/engine/registry-facade.ts +349 -0
  48. package/src/engine/registry-ingest.ts +473 -0
  49. package/src/engine/registry-state.ts +388 -0
  50. package/src/engine/registry-validate.ts +660 -0
  51. package/src/engine/registry.ts +79 -1695
  52. package/src/engine/types/screen.ts +4 -2
  53. package/src/engine/types/tree-node.ts +2 -5
  54. package/src/event-store/snapshot.ts +7 -7
  55. package/src/i18n/required-surface-keys.ts +2 -0
  56. package/src/jobs/job-runner.ts +1 -1
  57. package/src/observability/standard-metrics.ts +4 -3
  58. package/src/pipeline/dispatch-batch.ts +3 -3
  59. package/src/pipeline/dispatch-shared.ts +17 -10
  60. package/src/pipeline/event-dispatcher-admin.ts +289 -0
  61. package/src/pipeline/event-dispatcher-delivery.ts +264 -0
  62. package/src/pipeline/event-dispatcher.ts +28 -540
  63. package/src/pipeline/projection-rebuild.ts +1 -1
  64. package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +95 -0
  65. package/src/stack/test-stack.ts +46 -1
  66. package/src/testing/boot-validator-fixture.ts +1 -2
  67. package/src/engine/__tests__/deep-link.test.ts +0 -35
  68. package/src/engine/deep-link.ts +0 -23
@@ -0,0 +1,413 @@
1
+ import type { EntityTableMeta } from "../db/entity-table-meta";
2
+ import { LifecycleHookTypes } from "./constants";
3
+ import type { FeatureBuilderState } from "./feature-builder-state";
4
+ import { isKebabSegment, toKebab } from "./qualified-name";
5
+ import type {
6
+ EntityProjectionExtension,
7
+ HookPhase,
8
+ LifecycleHookFn,
9
+ LifecycleHookType,
10
+ MultiStreamProjectionDefinition,
11
+ NameOrRef,
12
+ PostDeleteHookFn,
13
+ PostQueryHookFn,
14
+ PostSaveHookFn,
15
+ PreDeleteHookFn,
16
+ ProjectionDefinition,
17
+ RawTableOptions,
18
+ RegistrarExtensionDef,
19
+ SearchPayloadContributorFn,
20
+ TreeActionDef,
21
+ TreeActionsHandle,
22
+ UnmanagedTableOptions,
23
+ ValidationHookFn,
24
+ } from "./types";
25
+ import { HookPhases } from "./types";
26
+ import { resolveName } from "./types/handlers";
27
+ import type { HttpRouteDefinition } from "./types/http-route";
28
+ import type { NavDefinition } from "./types/nav";
29
+ import type { ScreenDefinition } from "./types/screen";
30
+ import type { WorkspaceDefinition } from "./types/workspace";
31
+
32
+ // Builds hooks/extensions/projections/screens/nav/workspace/tables/tree-actions
33
+ // registrar methods.
34
+ export function buildUiExtensionsMethods<TName extends string>(
35
+ state: FeatureBuilderState,
36
+ name: TName,
37
+ ) {
38
+ return {
39
+ hook(
40
+ type: LifecycleHookType | "validation",
41
+ target: NameOrRef | readonly NameOrRef[],
42
+ fn: LifecycleHookFn | ValidationHookFn,
43
+ options?: { phase?: HookPhase },
44
+ ): void {
45
+ const targets = Array.isArray(target) ? target : [target];
46
+ const names = targets.map(resolveName);
47
+
48
+ // Hook-fn casts unten alle: @cast-boundary engine-bridge
49
+ // — typed Dev-API (LifecycleHookFn|ValidationHookFn) → erased Map<name, fn>.
50
+ if (type === "validation") {
51
+ for (const n of names) {
52
+ state.validationHooks[n] = fn as ValidationHookFn; // @cast-boundary engine-bridge
53
+ }
54
+ // skip: validation hooks have no phase, stored and done
55
+ return;
56
+ }
57
+
58
+ if (
59
+ type === LifecycleHookTypes.preSave ||
60
+ type === LifecycleHookTypes.preQuery ||
61
+ type === LifecycleHookTypes.postQuery
62
+ ) {
63
+ if (!state.lifecycleHooks[type]) state.lifecycleHooks[type] = {};
64
+ for (const n of names) {
65
+ if (!state.lifecycleHooks[type][n]) state.lifecycleHooks[type][n] = [];
66
+ state.lifecycleHooks[type][n].push({ fn: fn as LifecycleHookFn, featureName: name }); // @cast-boundary engine-bridge
67
+ }
68
+ // skip: pre/post-hooks without phase semantics, stored and done
69
+ return;
70
+ }
71
+
72
+ // Phased storage. preDelete has no phase option (always inTransaction);
73
+ // postSave/postDelete default to afterCommit.
74
+ const phase =
75
+ type === LifecycleHookTypes.preDelete
76
+ ? HookPhases.inTransaction
77
+ : (options?.phase ?? HookPhases.afterCommit);
78
+ const bucket = state.phasedLifecycleHooks[type];
79
+ for (const n of names) {
80
+ if (!bucket[n]) bucket[n] = [];
81
+ bucket[n].push({ fn: fn as LifecycleHookFn, phase, featureName: name }); // @cast-boundary engine-bridge
82
+ }
83
+ },
84
+ entityHook(
85
+ type: "postSave" | "preDelete" | "postDelete" | "postQuery",
86
+ entityRef: NameOrRef,
87
+ fn: LifecycleHookFn,
88
+ options?: { phase?: HookPhase },
89
+ ): void {
90
+ const entityName = resolveName(entityRef);
91
+ if (type === LifecycleHookTypes.postSave) {
92
+ const phase = options?.phase ?? HookPhases.afterCommit;
93
+ if (!state.entityPostSave[entityName]) state.entityPostSave[entityName] = [];
94
+ state.entityPostSave[entityName].push({
95
+ fn: fn as PostSaveHookFn,
96
+ phase,
97
+ featureName: name,
98
+ }); // @cast-boundary engine-bridge
99
+ } else if (type === LifecycleHookTypes.preDelete) {
100
+ if (!state.entityPreDelete[entityName]) state.entityPreDelete[entityName] = [];
101
+ state.entityPreDelete[entityName].push({
102
+ fn: fn as PreDeleteHookFn, // @cast-boundary engine-bridge
103
+ phase: HookPhases.inTransaction,
104
+ featureName: name,
105
+ });
106
+ } else if (type === LifecycleHookTypes.postDelete) {
107
+ const phase = options?.phase ?? HookPhases.afterCommit;
108
+ if (!state.entityPostDelete[entityName]) state.entityPostDelete[entityName] = [];
109
+ state.entityPostDelete[entityName].push({
110
+ fn: fn as PostDeleteHookFn,
111
+ phase,
112
+ featureName: name,
113
+ }); // @cast-boundary engine-bridge
114
+ } else if (type === LifecycleHookTypes.postQuery) {
115
+ // postQuery is unphased (no inTransaction/afterCommit semantics — fires
116
+ // synchronously after query-handler-execute, before field-access-filter)
117
+ if (!state.entityPostQuery[entityName]) state.entityPostQuery[entityName] = [];
118
+ state.entityPostQuery[entityName].push({ fn: fn as PostQueryHookFn, featureName: name }); // @cast-boundary engine-bridge
119
+ }
120
+ },
121
+ searchPayloadExtension(entityRef: NameOrRef, fn: SearchPayloadContributorFn): void {
122
+ const entityName = resolveName(entityRef);
123
+ if (!state.searchPayloadExtensions[entityName])
124
+ state.searchPayloadExtensions[entityName] = [];
125
+ state.searchPayloadExtensions[entityName].push({ fn, featureName: name });
126
+ },
127
+ extendsRegistrar(extensionName: string, def: RegistrarExtensionDef): void {
128
+ state.registrarExtensions[extensionName] = def;
129
+ },
130
+ useExtension(
131
+ extensionName: string,
132
+ entityRef: NameOrRef,
133
+ options?: Record<string, unknown>,
134
+ ): void {
135
+ state.extensionUsages.push({ extensionName, entityName: resolveName(entityRef), options });
136
+ },
137
+ extensionSelector(extensionName: string, key: { readonly name: string } | string): void {
138
+ if (state.extensionSelectors.some((s) => s.extensionName === extensionName)) {
139
+ throw new Error(
140
+ `[Feature ${name}] extensionSelector("${extensionName}") declared twice — ` +
141
+ `one selector key per extension point.`,
142
+ );
143
+ }
144
+ const qualifiedKey = typeof key === "string" ? key : key.name;
145
+ state.extensionSelectors.push({ extensionName, qualifiedKey });
146
+ },
147
+ /**
148
+ * Marker-Deklaration: dieses Feature stellt eine Cross-Feature-API
149
+ * unter dem genannten Namen bereit. Die eigentliche Implementation
150
+ * wird separat als Query- oder Write-Handler unter dem QN-Pattern
151
+ * registriert; r.exposesApi ist reine Boot-Check-Surface.
152
+ *
153
+ * Beispiel:
154
+ * defineFeature("compliance-profiles", (r) => {
155
+ * r.exposesApi("compliance.forTenant");
156
+ * r.queryHandler({ name: "compliance:query:for-tenant", ... });
157
+ * });
158
+ * defineFeature("user-data-rights", (r) => {
159
+ * r.requires("compliance-profiles");
160
+ * r.usesApi("compliance.forTenant");
161
+ * // ruft im Handler: ctx.callQuery("compliance:query:for-tenant", ...)
162
+ * });
163
+ */
164
+ exposesApi(apiName: string): void {
165
+ if (state.exposedApis.has(apiName)) {
166
+ throw new Error(
167
+ `[Feature ${name}] r.exposesApi("${apiName}") called twice — API names must be unique within a feature.`,
168
+ );
169
+ }
170
+ state.exposedApis.add(apiName);
171
+ },
172
+ /**
173
+ * Declares that this feature calls a cross-feature API. Boot-Validator
174
+ * checkt dass irgendein anderes Feature `r.exposesApi(name)` macht und
175
+ * dass dieses Feature `r.requires` darauf hat.
176
+ */
177
+ usesApi(apiName: string): void {
178
+ state.usedApis.add(apiName);
179
+ },
180
+ projection(definition: ProjectionDefinition): void {
181
+ // Reject names that would blow up at registry-boot when we qualify them.
182
+ // Catch it at the registration site so the stack trace points at the
183
+ // feature file, not at framework internals.
184
+ if (!isKebabSegment(definition.name)) {
185
+ throw new Error(
186
+ `[Feature ${name}] Projection name "${definition.name}" must be kebab-case ` +
187
+ `(lowercase letters, digits, dashes; start with a letter). ` +
188
+ `Got "${definition.name}" — try "${toKebab(definition.name).replace(/_/g, "-")}".`,
189
+ );
190
+ }
191
+ if (state.projections[definition.name]) {
192
+ throw new Error(
193
+ `[Feature ${name}] Projection "${definition.name}" already registered. ` +
194
+ `Projection names must be unique per feature.`,
195
+ );
196
+ }
197
+ state.projections[definition.name] = definition;
198
+ },
199
+ multiStreamProjection(definition: MultiStreamProjectionDefinition): void {
200
+ if (!isKebabSegment(definition.name)) {
201
+ throw new Error(
202
+ `[Feature ${name}] MultiStreamProjection name "${definition.name}" must be kebab-case ` +
203
+ `(lowercase letters, digits, dashes; start with a letter). ` +
204
+ `Got "${definition.name}" — try "${toKebab(definition.name).replace(/_/g, "-")}".`,
205
+ );
206
+ }
207
+ if (state.multiStreamProjections[definition.name] || state.projections[definition.name]) {
208
+ throw new Error(
209
+ `[Feature ${name}] Projection name "${definition.name}" already registered. ` +
210
+ `r.projection and r.multiStreamProjection share a namespace — pick a unique short name.`,
211
+ );
212
+ }
213
+ if (Object.keys(definition.apply).length === 0) {
214
+ throw new Error(
215
+ `[Feature ${name}] MultiStreamProjection "${definition.name}" has no apply handlers. ` +
216
+ `Declare at least one event type it reacts to, otherwise the dispatcher has nothing to route.`,
217
+ );
218
+ }
219
+ state.multiStreamProjections[definition.name] = definition;
220
+ },
221
+ extendEntityProjection(entityName: string, extension: EntityProjectionExtension): void {
222
+ if (Object.keys(extension.apply).length === 0) {
223
+ throw new Error(
224
+ `[Feature ${name}] extendEntityProjection("${entityName}") has no apply handlers. ` +
225
+ `Declare at least one event type, otherwise the rebuild replay has nothing to do.`,
226
+ );
227
+ }
228
+ // Entity existence + apply-key collisions are validated at registry
229
+ // build — r.entity may legally be called after this in the same feature.
230
+ const list = state.entityProjectionExtensions[entityName] ?? [];
231
+ list.push(extension);
232
+ state.entityProjectionExtensions[entityName] = list;
233
+ },
234
+ referenceData(
235
+ entityRef: NameOrRef,
236
+ data: readonly Record<string, unknown>[],
237
+ options?: { upsertKey?: string },
238
+ ): void {
239
+ state.referenceData.push({
240
+ entityName: resolveName(entityRef),
241
+ data,
242
+ upsertKey: options?.upsertKey,
243
+ });
244
+ },
245
+ screen(definition: ScreenDefinition): void {
246
+ // Reject kebab-drift at registration-time so the stack trace points at
247
+ // the feature file, not at registry-boot. Same guard pattern as
248
+ // r.projection / r.multiStreamProjection.
249
+ if (!isKebabSegment(definition.id)) {
250
+ throw new Error(
251
+ `[Feature ${name}] Screen id "${definition.id}" must be kebab-case ` +
252
+ `(lowercase letters, digits, dashes; start with a letter). ` +
253
+ `Got "${definition.id}" — try "${toKebab(definition.id).replace(/_/g, "-")}".`,
254
+ );
255
+ }
256
+ if (state.screens[definition.id]) {
257
+ throw new Error(
258
+ `[Feature ${name}] Screen "${definition.id}" already registered. ` +
259
+ `Screen ids must be unique per feature.`,
260
+ );
261
+ }
262
+ state.screens[definition.id] = definition;
263
+ },
264
+ nav(definition: NavDefinition): void {
265
+ // Reject kebab-drift at registration-time so the stack trace points at
266
+ // the feature file, not at registry-boot. Same guard pattern as
267
+ // r.projection / r.multiStreamProjection / r.screen.
268
+ if (!isKebabSegment(definition.id)) {
269
+ throw new Error(
270
+ `[Feature ${name}] Nav id "${definition.id}" must be kebab-case ` +
271
+ `(lowercase letters, digits, dashes; start with a letter). ` +
272
+ `Got "${definition.id}" — try "${toKebab(definition.id).replace(/_/g, "-")}".`,
273
+ );
274
+ }
275
+ if (state.navs[definition.id]) {
276
+ throw new Error(
277
+ `[Feature ${name}] Nav entry "${definition.id}" already registered. ` +
278
+ `Nav ids must be unique per feature.`,
279
+ );
280
+ }
281
+ state.navs[definition.id] = definition;
282
+ },
283
+ workspace(definition: WorkspaceDefinition): void {
284
+ // Same kebab guard as r.screen / r.nav so authoring-time mistakes
285
+ // surface at the feature file, not deep in registry boot.
286
+ if (!isKebabSegment(definition.id)) {
287
+ throw new Error(
288
+ `[Feature ${name}] Workspace id "${definition.id}" must be kebab-case ` +
289
+ `(lowercase letters, digits, dashes; start with a letter). ` +
290
+ `Got "${definition.id}" — try "${toKebab(definition.id).replace(/_/g, "-")}".`,
291
+ );
292
+ }
293
+ if (state.workspaces[definition.id]) {
294
+ throw new Error(
295
+ `[Feature ${name}] Workspace "${definition.id}" already registered. ` +
296
+ `Workspace ids must be unique per feature.`,
297
+ );
298
+ }
299
+ state.workspaces[definition.id] = definition;
300
+ },
301
+ httpRoute(definition: HttpRouteDefinition): void {
302
+ // Path-Validation: muss mit "/" beginnen, keine /api/-Routes (die
303
+ // sind dem Dispatcher reserviert; eine HTTP-Route die /api/foo
304
+ // belegt, würde die Auth-Middleware umgehen ohne dass der Author
305
+ // das ausgesprochen hat — bewusster Block).
306
+ if (!definition.path.startsWith("/")) {
307
+ throw new Error(
308
+ `[Feature ${name}] httpRoute path "${definition.path}" must start with "/". ` +
309
+ `Got "${definition.path}".`,
310
+ );
311
+ }
312
+ if (definition.path === "/api" || definition.path.startsWith("/api/")) {
313
+ throw new Error(
314
+ `[Feature ${name}] httpRoute path "${definition.path}" is in the /api/* namespace ` +
315
+ `which is reserved for the dispatcher (write/query/batch/auth/sse). ` +
316
+ `Pick a different path or use r.queryHandler / r.writeHandler.`,
317
+ );
318
+ }
319
+ const key = `${definition.method} ${definition.path}`;
320
+ if (state.httpRoutes[key]) {
321
+ throw new Error(
322
+ `[Feature ${name}] HTTP-Route "${key}" already registered. ` +
323
+ `method + path must be unique per feature.`,
324
+ );
325
+ }
326
+ state.httpRoutes[key] = definition;
327
+ },
328
+ rawTable(rawTableName: string, table: unknown, options: RawTableOptions): void {
329
+ // Same kebab guard as r.projection / r.screen / r.nav so authoring-time
330
+ // mistakes surface at the feature file, not deep in registry boot.
331
+ if (!isKebabSegment(rawTableName)) {
332
+ throw new Error(
333
+ `[Feature ${name}] Raw-table name "${rawTableName}" must be kebab-case ` +
334
+ `(lowercase letters, digits, dashes; start with a letter). ` +
335
+ `Got "${rawTableName}" — try "${toKebab(rawTableName).replace(/_/g, "-")}".`,
336
+ );
337
+ }
338
+ if (state.rawTables[rawTableName]) {
339
+ throw new Error(
340
+ `[Feature ${name}] r.rawTable("${rawTableName}") already registered. ` +
341
+ `Raw-table names must be unique per feature.`,
342
+ );
343
+ }
344
+ // The `reason` is the marker that justifies the bypass — empty
345
+ // strings would defeat the audit trail. Reject early so the
346
+ // failure points at the feature file.
347
+ if (typeof options.reason !== "string" || options.reason.trim().length === 0) {
348
+ throw new Error(
349
+ `[Feature ${name}] r.rawTable("${rawTableName}"): options.reason must be a ` +
350
+ `non-empty string. The reason is the marker that justifies the bypass — ` +
351
+ `if you can't write one, declare data via r.entity() instead.`,
352
+ );
353
+ }
354
+ state.rawTables[rawTableName] = {
355
+ name: rawTableName,
356
+ table,
357
+ reason: options.reason,
358
+ };
359
+ },
360
+ unmanagedTable(meta: EntityTableMeta, options: UnmanagedTableOptions): void {
361
+ // Name comes from the meta itself — apps already give the table a
362
+ // name when calling defineUnmanagedTable, no need to repeat it.
363
+ const tableName = meta.tableName;
364
+ if (!isKebabSegment(tableName.replace(/_/g, "-"))) {
365
+ // EntityTableMeta uses snake_case for tableName (matches Postgres
366
+ // convention); we just guard against truly broken input.
367
+ throw new Error(
368
+ `[Feature ${name}] Unmanaged-table name "${tableName}" must be a ` +
369
+ `valid identifier (lowercase letters, digits, underscores; start with a letter).`,
370
+ );
371
+ }
372
+ if (state.unmanagedTables[tableName]) {
373
+ throw new Error(
374
+ `[Feature ${name}] r.unmanagedTable("${tableName}") already registered. ` +
375
+ `Unmanaged-table names must be unique per feature.`,
376
+ );
377
+ }
378
+ if (typeof options.reason !== "string" || options.reason.trim().length === 0) {
379
+ throw new Error(
380
+ `[Feature ${name}] r.unmanagedTable("${tableName}"): options.reason must be a ` +
381
+ `non-empty string. The reason justifies the audit-trail bypass — ` +
382
+ `if you can't write one, declare data via r.entity() instead.`,
383
+ );
384
+ }
385
+ state.unmanagedTables[tableName] = {
386
+ name: tableName,
387
+ meta,
388
+ reason: options.reason,
389
+ ...(options.piiEncryptedOnWrite && { piiEncryptedOnWrite: true }),
390
+ };
391
+ },
392
+ treeActions<const TActions extends Record<string, TreeActionDef>>(
393
+ actions: TActions,
394
+ ): TreeActionsHandle<TName, TActions> {
395
+ // Only-once-guard: zweiter Aufruf ist Author-Bug, soll am
396
+ // Feature-File aufschlagen (gleicher Stil wie r.toggleable).
397
+ if (state.treeActions !== undefined) {
398
+ throw new Error(
399
+ `[Feature ${name}] r.treeActions() already called. ` +
400
+ `Each feature may declare a single tree-actions schema.`,
401
+ );
402
+ }
403
+ state.treeActions = actions;
404
+ // Return typed handle für setup-export. Frozen damit Caller die
405
+ // Map nicht nachträglich mutieren (würde Pattern-AST + Runtime-
406
+ // Lookup divergieren lassen).
407
+ return Object.freeze({
408
+ id: name,
409
+ treeActions: actions,
410
+ });
411
+ },
412
+ };
413
+ }
@@ -38,8 +38,6 @@ export {
38
38
  export type { App, AppConfig } from "./create-app";
39
39
  export { createApp } from "./create-app";
40
40
  export { crossTenantOverrideDenied } from "./cross-tenant";
41
- export type { DeepLinkTarget } from "./deep-link";
42
- export { buildDeepLinkUrl } from "./deep-link";
43
41
  export { defineFeature } from "./define-feature";
44
42
  export type {
45
43
  QueryHandlerDefinition,