@cosmicdrift/kumiko-framework 0.2.1 → 0.2.3

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 (42) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/package.json +4 -3
  3. package/src/auth/__tests__/roles.test.ts +24 -0
  4. package/src/auth/index.ts +7 -0
  5. package/src/auth/roles.ts +42 -0
  6. package/src/compliance/__tests__/duration-spec.test.ts +72 -0
  7. package/src/compliance/__tests__/profiles.test.ts +308 -0
  8. package/src/compliance/__tests__/sub-processors.test.ts +139 -0
  9. package/src/compliance/duration-spec.ts +44 -0
  10. package/src/compliance/index.ts +31 -0
  11. package/src/compliance/override-schema.ts +136 -0
  12. package/src/compliance/profiles.ts +427 -0
  13. package/src/compliance/sub-processors.ts +152 -0
  14. package/src/db/__tests__/big-int-field.test.ts +131 -0
  15. package/src/db/table-builder.ts +18 -1
  16. package/src/engine/__tests__/boot-validator-api-exposure.test.ts +142 -0
  17. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +570 -0
  18. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +160 -0
  19. package/src/engine/boot-validator.ts +276 -0
  20. package/src/engine/define-feature.ts +39 -0
  21. package/src/engine/extension-names.ts +105 -0
  22. package/src/engine/extensions/user-data.ts +106 -0
  23. package/src/engine/factories.ts +15 -5
  24. package/src/engine/feature-ast/extractors.ts +40 -0
  25. package/src/engine/feature-ast/parse.ts +6 -0
  26. package/src/engine/feature-ast/patterns.ts +22 -0
  27. package/src/engine/feature-ast/render.ts +14 -0
  28. package/src/engine/index.ts +21 -0
  29. package/src/engine/pattern-library/__tests__/library.test.ts +5 -0
  30. package/src/engine/pattern-library/library.ts +36 -0
  31. package/src/engine/schema-builder.ts +8 -0
  32. package/src/engine/types/feature.ts +51 -0
  33. package/src/engine/types/fields.ts +134 -10
  34. package/src/engine/types/index.ts +3 -0
  35. package/src/files/__tests__/read-stream.test.ts +105 -0
  36. package/src/files/__tests__/write-stream.test.ts +233 -0
  37. package/src/files/__tests__/zip-stream.test.ts +357 -0
  38. package/src/files/in-memory-provider.ts +38 -0
  39. package/src/files/index.ts +3 -0
  40. package/src/files/local-provider.ts +58 -1
  41. package/src/files/types.ts +34 -6
  42. package/src/files/zip-stream.ts +251 -0
@@ -0,0 +1,106 @@
1
+ // Hook-Signatur-Types für die EXT_USER_DATA-Extension (DSGVO Art. 15+17+20).
2
+ //
3
+ // Sprint 1.9 Z1: Bisher gibt der Boot-Validator JEDES Hook-Shape durch
4
+ // — useExtension(EXT_USER_DATA, "X", { export: ... }) wird nicht gegen
5
+ // eine erwartete Signatur geprüft. Diese Types sind die canonical
6
+ // Schema-Sicht; Sprint 2 user-data-rights wird sie via
7
+ // `r.extendsRegistrar(EXT_USER_DATA, { hooks: ... })`-Doku exposen.
8
+ //
9
+ // Boot-Time-Schape-Check (Runtime) ist orthogonal und kommt in Sprint
10
+ // 2 wenn die exportRunner-/forgetRunner-Pipelines stehen — bis dahin
11
+ // sind diese Types Compile-Time-Hints für App-Authors, keine Runtime-
12
+ // Validation.
13
+ //
14
+ // Siehe docs/plans/datenschutz/user-data-rights.md.
15
+
16
+ import type { DbRunner } from "../../db/connection";
17
+ import type { TenantId } from "../types";
18
+
19
+ // SessionUser.id ist plattformweit `string` (kein Brand-Type). Wenn
20
+ // jemals ein UserId-Brand eingefuehrt wird, ersetzt man hier den
21
+ // inline-Type — andere Codebase-Stellen nutzen denselben Pfad.
22
+ type UserId = string;
23
+
24
+ /**
25
+ * Strategie für den Forget-Pfad pro Entity:
26
+ * - "delete": Row physisch entfernen (Profil, Eigene Notizen, Sessions).
27
+ * - "anonymize": User-Reference auf null + Display-Felder auf
28
+ * "[Geloescht]" — typisch für geteilte Daten (Tasks,
29
+ * Comments) damit andere User die History nicht verlieren.
30
+ *
31
+ * Cleanup-Job (Sprint 2 data-retention) entscheidet pro Entity über
32
+ * Retention-Policy, welche Strategie greift. blockDelete-Entries lösen
33
+ * IMMER `anonymize` aus damit Aufbewahrungs-Pflicht respektiert wird.
34
+ */
35
+ export type UserDataDeleteStrategy = "delete" | "anonymize";
36
+
37
+ /**
38
+ * Context-Snapshot der dem Hook übergeben wird. Sprint 2 erweitert
39
+ * das ggf. um cancel-/timeout-Marker; aktuell minimaler Schnitt.
40
+ *
41
+ * `db` ist `DbRunner` (DbConnection | DbTx) damit der Cleanup-Runner
42
+ * (S2.U5b) den Hook in einer Per-User-Sub-Tx callen kann. Hooks die
43
+ * raw-DB-Operationen machen funktionieren auf beiden Shapes via
44
+ * Drizzle's polymorphem select/insert/update/delete-Chain.
45
+ */
46
+ export interface UserDataHookCtx {
47
+ readonly db: DbRunner;
48
+ readonly tenantId: TenantId;
49
+ readonly userId: UserId;
50
+ }
51
+
52
+ /**
53
+ * Pro Feature/Entity-Snippet das im Export-Bundle landet. Sprint 2
54
+ * orchestriert die JSON-Serialisierung; Hooks geben Plain-Records.
55
+ */
56
+ export interface UserDataExportSnippet {
57
+ readonly entity: string;
58
+ readonly rows: ReadonlyArray<Record<string, unknown>>;
59
+ /**
60
+ * Optional: signed-URLs für File-Refs. user-data-rights packt sie
61
+ * separat ins ZIP unter `files/`. Andere Hooks lassen das leer.
62
+ */
63
+ readonly fileRefs?: ReadonlyArray<{
64
+ readonly fileRefId: string;
65
+ readonly storageKey: string;
66
+ readonly fileName: string;
67
+ }>;
68
+ }
69
+
70
+ /**
71
+ * Export-Hook: Sammelt alle Daten einer Entity die zu einem User
72
+ * gehören. Wird im Daten-Export-Job pro registrierter Entity einmal
73
+ * aufgerufen. Idempotent — kann mehrfach aufgerufen werden ohne
74
+ * Side-Effects.
75
+ *
76
+ * Sprint 2 user-data-rights ruft das via Iteration über alle
77
+ * `r.useExtension(EXT_USER_DATA, ...)`-Registrierungen.
78
+ */
79
+ export type UserDataExportHook = (ctx: UserDataHookCtx) => Promise<UserDataExportSnippet | null>;
80
+
81
+ /**
82
+ * Forget-Hook: Löscht oder anonymisiert die Entity-Rows die zu einem
83
+ * User gehören. Strategy kommt vom Cleanup-Job (kann per Entity
84
+ * unterschiedlich sein wegen Retention-Policy).
85
+ *
86
+ * Idempotent — wenn der Job zweimal läuft (Crash-Recovery), darf der
87
+ * Hook nicht crashen.
88
+ */
89
+ export type UserDataDeleteHook = (
90
+ ctx: UserDataHookCtx,
91
+ strategy: UserDataDeleteStrategy,
92
+ ) => Promise<void>;
93
+
94
+ /**
95
+ * Komplette Hook-Tafel für EXT_USER_DATA. Sprint 2 user-data-rights
96
+ * deklariert das via `r.extendsRegistrar(EXT_USER_DATA, { hooks: ... })`,
97
+ * konsumierende Features liefern beide Hooks via
98
+ * `r.useExtension(EXT_USER_DATA, "<entity>", { export, delete })`.
99
+ *
100
+ * Kein Hook ist optional — beide MÜSSEN registriert sein. Boot-Check
101
+ * (Sprint 2) prüft das.
102
+ */
103
+ export interface UserDataExtensionHooks {
104
+ readonly export: UserDataExportHook;
105
+ readonly delete: UserDataDeleteHook;
106
+ }
@@ -1,8 +1,10 @@
1
1
  import type {
2
+ BigIntFieldDef,
2
3
  BooleanFieldDef,
3
4
  DateFieldDef,
4
5
  EmbeddedFieldDef,
5
6
  EntityDefinition,
7
+ EntityIndexDef,
6
8
  FieldsMap,
7
9
  FileFieldDef,
8
10
  FilesFieldDef,
@@ -13,6 +15,7 @@ import type {
13
15
  MoneyFieldDef,
14
16
  MultiSelectFieldDef,
15
17
  NumberFieldDef,
18
+ RetentionDef,
16
19
  SelectFieldDef,
17
20
  TextFieldDef,
18
21
  TimestampFieldDef,
@@ -126,6 +129,16 @@ export function createNumberField<R extends true | false = false>(
126
129
  } as NumberFieldDef & { required: R };
127
130
  }
128
131
 
132
+ export function createBigIntField<R extends true | false = false>(
133
+ overrides?: Partial<Omit<BigIntFieldDef, "type" | "required">> & { required?: R },
134
+ ): BigIntFieldDef & { required: R } {
135
+ return {
136
+ type: "bigInt",
137
+ required: false,
138
+ ...overrides,
139
+ } as BigIntFieldDef & { required: R };
140
+ }
141
+
129
142
  export function createMoneyField<R extends true | false = false>(
130
143
  overrides?: Partial<Omit<MoneyFieldDef, "type" | "required">> & { required?: R },
131
144
  ): MoneyFieldDef & { required: R } {
@@ -309,13 +322,10 @@ export function createEntity<F>(def: {
309
322
  readonly searchWeight?: number;
310
323
  readonly defaultCurrency?: string;
311
324
  readonly transitions?: Readonly<Record<string, Readonly<Record<string, readonly string[]>>>>;
312
- readonly indexes?: readonly {
313
- readonly columns: readonly [string, ...string[]];
314
- readonly unique?: boolean;
315
- readonly name?: string;
316
- }[];
325
+ readonly indexes?: readonly EntityIndexDef[];
317
326
  readonly idType?: "serial" | "uuid";
318
327
  readonly access?: EntityDefinition["access"];
328
+ readonly retention?: RetentionDef;
319
329
  }): F extends FieldsMap ? EntityDefinition<F> : never {
320
330
  return {
321
331
  softDelete: false,
@@ -50,6 +50,7 @@ import type {
50
50
  EntityHookPattern,
51
51
  EntityPattern,
52
52
  EventMigrationPattern,
53
+ ExposesApiPattern,
53
54
  ExtendsRegistrarPattern,
54
55
  HookPattern,
55
56
  HttpRoutePattern,
@@ -72,6 +73,7 @@ import type {
72
73
  ToggleablePattern,
73
74
  TranslationsPattern,
74
75
  UseExtensionPattern,
76
+ UsesApiPattern,
75
77
  WorkspacePattern,
76
78
  WriteHandlerPattern,
77
79
  } from "./patterns";
@@ -2560,3 +2562,41 @@ export function extractExtendsRegistrar(
2560
2562
  defBody: sourceLocationFromNode(defArg, sourceFile),
2561
2563
  });
2562
2564
  }
2565
+
2566
+ export function extractUsesApi(
2567
+ call: CallExpression,
2568
+ sourceFile: SourceFile,
2569
+ ): ExtractOutput<UsesApiPattern> {
2570
+ const arg = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral);
2571
+ if (!arg) {
2572
+ return fail(
2573
+ "usesApi",
2574
+ sourceLocationFromNode(call, sourceFile),
2575
+ 'expected a single string-literal API name (e.g. "sessions.revokeAllForUser")',
2576
+ );
2577
+ }
2578
+ return ok({
2579
+ kind: "usesApi",
2580
+ source: sourceLocationFromNode(call, sourceFile),
2581
+ apiName: arg.getLiteralValue(),
2582
+ });
2583
+ }
2584
+
2585
+ export function extractExposesApi(
2586
+ call: CallExpression,
2587
+ sourceFile: SourceFile,
2588
+ ): ExtractOutput<ExposesApiPattern> {
2589
+ const arg = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral);
2590
+ if (!arg) {
2591
+ return fail(
2592
+ "exposesApi",
2593
+ sourceLocationFromNode(call, sourceFile),
2594
+ 'expected a single string-literal API name (e.g. "sessions.revokeAllForUser")',
2595
+ );
2596
+ }
2597
+ return ok({
2598
+ kind: "exposesApi",
2599
+ source: sourceLocationFromNode(call, sourceFile),
2600
+ apiName: arg.getLiteralValue(),
2601
+ });
2602
+ }
@@ -33,6 +33,7 @@ import {
33
33
  extractEntity,
34
34
  extractEntityHook,
35
35
  extractEventMigration,
36
+ extractExposesApi,
36
37
  extractExtendsRegistrar,
37
38
  extractHook,
38
39
  extractHttpRoute,
@@ -54,6 +55,7 @@ import {
54
55
  extractToggleable,
55
56
  extractTranslations,
56
57
  extractUseExtension,
58
+ extractUsesApi,
57
59
  extractWorkspace,
58
60
  extractWriteHandler,
59
61
  } from "./extractors";
@@ -346,6 +348,10 @@ function dispatchExtractor(
346
348
  // Round 5 — opaque patterns
347
349
  case "extendsRegistrar":
348
350
  return extractExtendsRegistrar(call, sourceFile);
351
+ case "usesApi":
352
+ return extractUsesApi(call, sourceFile);
353
+ case "exposesApi":
354
+ return extractExposesApi(call, sourceFile);
349
355
  // Unknown method — UnknownPattern signal so Designer/AI surface it
350
356
  // as "custom call" without losing the source location.
351
357
  default:
@@ -318,6 +318,24 @@ export type ExtendsRegistrarPattern = {
318
318
  readonly defBody: SourceLocation;
319
319
  };
320
320
 
321
+ // r.usesApi("a.b") — declarative cross-feature handler-ID dependency.
322
+ // Boot-validation throws if no other feature exposes the handler. Single
323
+ // string argument; pattern is purely declarative.
324
+ export type UsesApiPattern = {
325
+ readonly kind: "usesApi";
326
+ readonly source: SourceLocation;
327
+ readonly apiName: string;
328
+ };
329
+
330
+ // r.exposesApi("a.b") — declarative announcement that this feature
331
+ // provides a handler matching the cross-feature contract `a.b`. Single
332
+ // string argument; pattern is purely declarative.
333
+ export type ExposesApiPattern = {
334
+ readonly kind: "exposesApi";
335
+ readonly source: SourceLocation;
336
+ readonly apiName: string;
337
+ };
338
+
321
339
  // =============================================================================
322
340
  // Catch-all — r.* calls the visitor doesn't recognise. Designer renders
323
341
  // "unknown call (cannot edit)", AI patcher leaves them unchanged. When
@@ -354,6 +372,8 @@ export type FeaturePattern =
354
372
  | ReferenceDataPattern
355
373
  | ReadsConfigPattern
356
374
  | UseExtensionPattern
375
+ | UsesApiPattern
376
+ | ExposesApiPattern
357
377
  // Mixed
358
378
  | ScreenPattern
359
379
  | WriteHandlerPattern
@@ -406,6 +426,8 @@ export function getEditability(pattern: FeaturePattern): Editability {
406
426
  case "referenceData":
407
427
  case "readsConfig":
408
428
  case "useExtension":
429
+ case "usesApi":
430
+ case "exposesApi":
409
431
  return "static";
410
432
  case "screen":
411
433
  case "writeHandler":
@@ -24,6 +24,7 @@ import type {
24
24
  EntityHookPattern,
25
25
  EntityPattern,
26
26
  EventMigrationPattern,
27
+ ExposesApiPattern,
27
28
  ExtendsRegistrarPattern,
28
29
  FeaturePattern,
29
30
  HookPattern,
@@ -47,6 +48,7 @@ import type {
47
48
  TranslationsPattern,
48
49
  UnknownPattern,
49
50
  UseExtensionPattern,
51
+ UsesApiPattern,
50
52
  WorkspacePattern,
51
53
  WriteHandlerPattern,
52
54
  } from "./patterns";
@@ -122,6 +124,10 @@ export function renderPattern(pattern: FeaturePattern): string {
122
124
  return renderEventMigration(pattern);
123
125
  case "extendsRegistrar":
124
126
  return renderExtendsRegistrar(pattern);
127
+ case "usesApi":
128
+ return renderUsesApi(pattern);
129
+ case "exposesApi":
130
+ return renderExposesApi(pattern);
125
131
  case "unknown":
126
132
  return renderUnknown(pattern);
127
133
  default: {
@@ -473,6 +479,14 @@ function renderExtendsRegistrar(p: ExtendsRegistrarPattern): string {
473
479
  return `r.extendsRegistrar(${JSON.stringify(p.extensionName)}, ${p.defBody.raw});`;
474
480
  }
475
481
 
482
+ function renderUsesApi(p: UsesApiPattern): string {
483
+ return `r.usesApi(${JSON.stringify(p.apiName)});`;
484
+ }
485
+
486
+ function renderExposesApi(p: ExposesApiPattern): string {
487
+ return `r.exposesApi(${JSON.stringify(p.apiName)});`;
488
+ }
489
+
476
490
  function renderUnknown(p: UnknownPattern): string {
477
491
  // Round-trip preservation only: emit the raw call text from the
478
492
  // SourceLocation so the rendered file stays semantically identical
@@ -41,7 +41,25 @@ export {
41
41
  } from "./entity-handlers";
42
42
  export type { EmitCtx } from "./event-helpers";
43
43
  export { emitEvent, typedPayload } from "./event-helpers";
44
+ export type { KumikoExtensionName } from "./extension-names";
44
45
  export {
46
+ EXT_EXTERNAL_RESOURCE,
47
+ EXT_INFRA_RESOURCE,
48
+ EXT_SEARCH_ADAPTER,
49
+ EXT_STORAGE_PROVIDER,
50
+ EXT_TENANT_DATA,
51
+ EXT_USER_DATA,
52
+ } from "./extension-names";
53
+ export type {
54
+ UserDataDeleteHook,
55
+ UserDataDeleteStrategy,
56
+ UserDataExportHook,
57
+ UserDataExportSnippet,
58
+ UserDataExtensionHooks,
59
+ UserDataHookCtx,
60
+ } from "./extensions/user-data";
61
+ export {
62
+ createBigIntField,
45
63
  createBooleanField,
46
64
  createDateField,
47
65
  createEmbeddedField,
@@ -144,6 +162,7 @@ export type {
144
162
  AuthClaimsFn,
145
163
  AuthClaimsHookDef,
146
164
  BelongsToRelation,
165
+ BigIntFieldDef,
147
166
  BooleanFieldDef,
148
167
  CamelToKebab,
149
168
  ClaimKeyDefinition,
@@ -220,6 +239,7 @@ export type {
220
239
  NotifyPriority,
221
240
  NumberFieldDef,
222
241
  OnDeleteStrategy,
242
+ PiiAnnotations,
223
243
  PlatformComponent,
224
244
  PostDeleteHookFn,
225
245
  PostSaveHookFn,
@@ -234,6 +254,7 @@ export type {
234
254
  QueryHandlerFn,
235
255
  Registry,
236
256
  RelationDefinition,
257
+ RetentionDef,
237
258
  RowAction,
238
259
  SaveContext,
239
260
  ScreenDefinition,
@@ -55,6 +55,8 @@ const ALL_KINDS: readonly FeaturePatternKind[] = [
55
55
  "defineEvent",
56
56
  "eventMigration",
57
57
  "extendsRegistrar",
58
+ "usesApi",
59
+ "exposesApi",
58
60
  "unknown",
59
61
  ];
60
62
 
@@ -340,6 +342,9 @@ function makePlaceholderPattern(kind: FeaturePatternKind): FeaturePattern {
340
342
  };
341
343
  case "unknown":
342
344
  return { kind, source: PLACEHOLDER_LOC, methodName: "x" };
345
+ case "usesApi":
346
+ case "exposesApi":
347
+ return { kind, source: PLACEHOLDER_LOC, apiName: "demo.api" };
343
348
  default: {
344
349
  const _exhaustive: never = kind;
345
350
  return _exhaustive;
@@ -1020,6 +1020,40 @@ const extendsRegistrarSchema: PatternFormSchema = {
1020
1020
  ],
1021
1021
  };
1022
1022
 
1023
+ const usesApiSchema: PatternFormSchema = {
1024
+ kind: "usesApi",
1025
+ label: { en: "Uses API", de: "Nutzt API" },
1026
+ summary: {
1027
+ en: "Cross-feature handler-ID dependency. Boot fails if no other feature exposes it.",
1028
+ },
1029
+ category: "advanced",
1030
+ editability: "static",
1031
+ fields: [
1032
+ {
1033
+ path: "apiName",
1034
+ label: { en: "API name", de: "API-Name" },
1035
+ input: "text",
1036
+ required: true,
1037
+ },
1038
+ ],
1039
+ };
1040
+
1041
+ const exposesApiSchema: PatternFormSchema = {
1042
+ kind: "exposesApi",
1043
+ label: { en: "Exposes API", de: "Stellt API bereit" },
1044
+ summary: { en: "Declares this feature provides a handler matching the cross-feature contract." },
1045
+ category: "advanced",
1046
+ editability: "static",
1047
+ fields: [
1048
+ {
1049
+ path: "apiName",
1050
+ label: { en: "API name", de: "API-Name" },
1051
+ input: "text",
1052
+ required: true,
1053
+ },
1054
+ ],
1055
+ };
1056
+
1023
1057
  const unknownSchema: PatternFormSchema = {
1024
1058
  kind: "unknown",
1025
1059
  label: { en: "Unknown call", de: "Unbekannter Call" },
@@ -1077,6 +1111,8 @@ export const PATTERN_LIBRARY: Readonly<Record<FeaturePatternKind, PatternFormSch
1077
1111
  defineEvent: defineEventSchema,
1078
1112
  eventMigration: eventMigrationSchema,
1079
1113
  extendsRegistrar: extendsRegistrarSchema,
1114
+ usesApi: usesApiSchema,
1115
+ exposesApi: exposesApiSchema,
1080
1116
  unknown: unknownSchema,
1081
1117
  };
1082
1118
 
@@ -60,6 +60,14 @@ function fieldToZod(field: FieldDefinition, currencies: readonly string[]): z.Zo
60
60
  const schema = z.number();
61
61
  return field.default !== undefined ? schema.default(field.default) : schema;
62
62
  }
63
+ case "bigInt": {
64
+ // JS-`number`-Round-trip via mode:"number"; sicher bis 2^53.
65
+ // safe-integer-Cap ist explizit damit ein Caller, der einen
66
+ // Float reinwirft (z.B. parseFloat-Bug), beim Insert sofort
67
+ // failed statt silent-Truncation zu kassieren.
68
+ const schema = z.number().int().safe();
69
+ return field.default !== undefined ? schema.default(field.default) : schema;
70
+ }
63
71
  case "money": {
64
72
  const [first, ...rest] = currencies;
65
73
  if (!first) throw new Error("No currencies configured");
@@ -164,6 +164,21 @@ export type FeatureDefinition = {
164
164
  readonly jobs: Readonly<Record<string, JobDefinition>>;
165
165
  readonly registrarExtensions: Readonly<Record<string, RegistrarExtensionDef>>;
166
166
  readonly extensionUsages: readonly RegistrarExtensionRegistration[];
167
+ /**
168
+ * Cross-feature API names this feature exposes via `r.exposesApi(name)`.
169
+ * Pure Marker-Deklaration — die echte Implementation wird als
170
+ * Query-/Write-Handler unter dem QN-Pattern registriert (z.B.
171
+ * `compliance-profiles:query:effective-profile`). Boot-Validator prüft
172
+ * dass jedes `r.usesApi(name)` einen passenden Exposer hier findet —
173
+ * Tippfehler oder Drop-Refactorings werden zu Boot-Fail statt Runtime-Crash.
174
+ */
175
+ readonly exposedApis: ReadonlySet<string>;
176
+ /**
177
+ * Cross-feature API names this feature calls. Pflicht-Boot-Check:
178
+ * jeder Eintrag muss in `exposedApis` irgendeines Features auftauchen
179
+ * UND das Provider-Feature muss in requires/optionalRequires sein.
180
+ */
181
+ readonly usedApis: ReadonlySet<string>;
167
182
  readonly referenceData: readonly ReferenceDataDef[];
168
183
  readonly notifications: Readonly<Record<string, NotificationDefinition>>;
169
184
  readonly events: Readonly<Record<string, EventDef>>;
@@ -360,6 +375,42 @@ export type FeatureRegistrar<TFeature extends string = string> = {
360
375
 
361
376
  useExtension(extensionName: string, entity: NameOrRef, options?: Record<string, unknown>): void;
362
377
 
378
+ /**
379
+ * Marker-Deklaration: dieses Feature stellt eine Cross-Feature-API
380
+ * unter dem genannten Namen bereit. Die eigentliche Implementation
381
+ * wird separat als Query- oder Write-Handler unter dem QN-Pattern
382
+ * registriert; `r.exposesApi` ist reine Boot-Check-Surface.
383
+ *
384
+ * Boot-Validator prüft, dass jedes `r.usesApi(name)` einen passenden
385
+ * Exposer findet, dass das Exposer-Feature in requires/optionalRequires
386
+ * gelisted ist und dass kein API-Name doppelt exposed wird.
387
+ *
388
+ * ```ts
389
+ * defineFeature("compliance-profiles", (r) => {
390
+ * r.exposesApi("compliance.forTenant");
391
+ * r.queryHandler({
392
+ * name: "compliance:query:for-tenant",
393
+ * // ... echte Implementation
394
+ * });
395
+ * });
396
+ * ```
397
+ */
398
+ exposesApi(apiName: string): void;
399
+
400
+ /**
401
+ * Declares that this feature calls a cross-feature API. Boot-Validator
402
+ * checkt dass irgendein anderes Feature `r.exposesApi(apiName)` macht
403
+ * und dass dieses Feature `r.requires/optionalRequires` darauf hat.
404
+ *
405
+ * ```ts
406
+ * defineFeature("user-data-rights", (r) => {
407
+ * r.requires("compliance-profiles");
408
+ * r.usesApi("compliance.forTenant");
409
+ * });
410
+ * ```
411
+ */
412
+ usesApi(apiName: string): void;
413
+
363
414
  // Declare a metric. Short name (without kumiko_<feature>_ prefix) — Framework
364
415
  // qualifies it on boot. Validation (snake_case + typ-suffix) runs at boot.
365
416
  // Usage at runtime: ctx.metrics.inc("created_total", { status: "new" }).