@cosmicdrift/kumiko-framework 0.2.2 → 0.3.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 (191) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/package.json +124 -38
  3. package/src/__tests__/full-stack.integration.ts +2 -2
  4. package/src/api/auth-routes.ts +5 -5
  5. package/src/api/jwt.ts +2 -2
  6. package/src/api/route-registrars.ts +1 -1
  7. package/src/api/routes.ts +3 -3
  8. package/src/api/server.ts +6 -7
  9. package/src/auth/__tests__/roles.test.ts +24 -0
  10. package/src/auth/index.ts +7 -0
  11. package/src/auth/roles.ts +42 -0
  12. package/src/compliance/__tests__/duration-spec.test.ts +72 -0
  13. package/src/compliance/__tests__/profiles.test.ts +308 -0
  14. package/src/compliance/__tests__/sub-processors.test.ts +139 -0
  15. package/src/compliance/duration-spec.ts +44 -0
  16. package/src/compliance/index.ts +31 -0
  17. package/src/compliance/override-schema.ts +136 -0
  18. package/src/compliance/profiles.ts +427 -0
  19. package/src/compliance/sub-processors.ts +152 -0
  20. package/src/db/__tests__/big-int-field.test.ts +131 -0
  21. package/src/db/assert-exists-in.ts +2 -2
  22. package/src/db/cursor.ts +3 -3
  23. package/src/db/event-store-executor.ts +19 -13
  24. package/src/db/located-timestamp.ts +1 -1
  25. package/src/db/money.ts +12 -2
  26. package/src/db/pg-error.ts +1 -1
  27. package/src/db/row-helpers.ts +1 -1
  28. package/src/db/table-builder.ts +20 -5
  29. package/src/db/tenant-db.ts +9 -9
  30. package/src/engine/__tests__/_pipeline-test-utils.ts +23 -0
  31. package/src/engine/__tests__/boot-validator-api-exposure.test.ts +142 -0
  32. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +570 -0
  33. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +160 -0
  34. package/src/engine/__tests__/build-target.test.ts +135 -0
  35. package/src/engine/__tests__/codemod-pipeline.test.ts +551 -0
  36. package/src/engine/__tests__/entity-handlers.test.ts +3 -3
  37. package/src/engine/__tests__/event-helpers.test.ts +4 -4
  38. package/src/engine/__tests__/pipeline-engine.test.ts +215 -0
  39. package/src/engine/__tests__/pipeline-handler.integration.ts +894 -0
  40. package/src/engine/__tests__/pipeline-observability.integration.ts +142 -0
  41. package/src/engine/__tests__/pipeline-performance.integration.ts +152 -0
  42. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +288 -0
  43. package/src/engine/__tests__/raw-table.test.ts +2 -2
  44. package/src/engine/__tests__/steps-aggregate-append-event.test.ts +115 -0
  45. package/src/engine/__tests__/steps-aggregate-create.test.ts +92 -0
  46. package/src/engine/__tests__/steps-aggregate-update.test.ts +127 -0
  47. package/src/engine/__tests__/steps-call-feature.test.ts +123 -0
  48. package/src/engine/__tests__/steps-mail-send.test.ts +136 -0
  49. package/src/engine/__tests__/steps-read.test.ts +142 -0
  50. package/src/engine/__tests__/steps-resolver-utils.test.ts +50 -0
  51. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +69 -0
  52. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +117 -0
  53. package/src/engine/__tests__/steps-webhook-send.test.ts +135 -0
  54. package/src/engine/__tests__/steps-workflow.test.ts +198 -0
  55. package/src/engine/__tests__/validate-projection-allowlist.test.ts +491 -0
  56. package/src/engine/__tests__/visual-tree-patterns.test.ts +251 -0
  57. package/src/engine/boot-validator/api-ext.ts +77 -0
  58. package/src/engine/boot-validator/config-deps.ts +163 -0
  59. package/src/engine/boot-validator/entity-handler.ts +466 -0
  60. package/src/engine/boot-validator/index.ts +159 -0
  61. package/src/engine/boot-validator/ownership.ts +198 -0
  62. package/src/engine/boot-validator/pii-retention.ts +155 -0
  63. package/src/engine/boot-validator/screens-nav.ts +624 -0
  64. package/src/engine/boot-validator.ts +1 -1528
  65. package/src/engine/build-app-schema.ts +1 -1
  66. package/src/engine/build-target.ts +99 -0
  67. package/src/engine/codemod/index.ts +15 -0
  68. package/src/engine/codemod/pipeline-codemod.ts +641 -0
  69. package/src/engine/config-helpers.ts +9 -19
  70. package/src/engine/constants.ts +1 -1
  71. package/src/engine/define-feature.ts +127 -9
  72. package/src/engine/define-handler.ts +89 -3
  73. package/src/engine/define-roles.ts +2 -2
  74. package/src/engine/define-step.ts +28 -0
  75. package/src/engine/define-workflow.ts +110 -0
  76. package/src/engine/entity-handlers.ts +10 -9
  77. package/src/engine/event-helpers.ts +4 -4
  78. package/src/engine/extension-names.ts +105 -0
  79. package/src/engine/extensions/user-data.ts +106 -0
  80. package/src/engine/factories.ts +26 -16
  81. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +184 -0
  82. package/src/engine/feature-ast/extractors/index.ts +74 -0
  83. package/src/engine/feature-ast/extractors/round1.ts +110 -0
  84. package/src/engine/feature-ast/extractors/round2.ts +253 -0
  85. package/src/engine/feature-ast/extractors/round3.ts +471 -0
  86. package/src/engine/feature-ast/extractors/round4.ts +1365 -0
  87. package/src/engine/feature-ast/extractors/round5.ts +72 -0
  88. package/src/engine/feature-ast/extractors/round6.ts +66 -0
  89. package/src/engine/feature-ast/extractors/shared.ts +177 -0
  90. package/src/engine/feature-ast/parse.ts +13 -0
  91. package/src/engine/feature-ast/patch.ts +9 -1
  92. package/src/engine/feature-ast/patcher.ts +10 -3
  93. package/src/engine/feature-ast/patterns.ts +71 -1
  94. package/src/engine/feature-ast/render.ts +31 -1
  95. package/src/engine/index.ts +66 -2
  96. package/src/engine/pattern-library/__tests__/library.test.ts +11 -0
  97. package/src/engine/pattern-library/library.ts +78 -2
  98. package/src/engine/pipeline.ts +88 -0
  99. package/src/engine/projection-helpers.ts +1 -1
  100. package/src/engine/read-claim.ts +1 -1
  101. package/src/engine/registry.ts +30 -2
  102. package/src/engine/resolve-config-or-param.ts +4 -0
  103. package/src/engine/run-pipeline.ts +162 -0
  104. package/src/engine/schema-builder.ts +10 -4
  105. package/src/engine/state-machine.ts +1 -1
  106. package/src/engine/steps/_drizzle-boundary.ts +19 -0
  107. package/src/engine/steps/_duration-utils.ts +33 -0
  108. package/src/engine/steps/_no-return-guard.ts +21 -0
  109. package/src/engine/steps/_resolver-utils.ts +42 -0
  110. package/src/engine/steps/_step-dispatch-constants.ts +38 -0
  111. package/src/engine/steps/aggregate-append-event.ts +56 -0
  112. package/src/engine/steps/aggregate-create.ts +56 -0
  113. package/src/engine/steps/aggregate-update.ts +68 -0
  114. package/src/engine/steps/branch.ts +84 -0
  115. package/src/engine/steps/call-feature.ts +49 -0
  116. package/src/engine/steps/compute.ts +41 -0
  117. package/src/engine/steps/for-each.ts +111 -0
  118. package/src/engine/steps/mail-send.ts +44 -0
  119. package/src/engine/steps/read-find-many.ts +51 -0
  120. package/src/engine/steps/read-find-one.ts +58 -0
  121. package/src/engine/steps/retry.ts +87 -0
  122. package/src/engine/steps/return.ts +34 -0
  123. package/src/engine/steps/unsafe-projection-delete.ts +46 -0
  124. package/src/engine/steps/unsafe-projection-upsert.ts +69 -0
  125. package/src/engine/steps/wait-for-event.ts +71 -0
  126. package/src/engine/steps/wait.ts +69 -0
  127. package/src/engine/steps/webhook-send.ts +71 -0
  128. package/src/engine/system-user.ts +1 -1
  129. package/src/engine/types/feature.ts +143 -1
  130. package/src/engine/types/fields.ts +134 -10
  131. package/src/engine/types/handlers.ts +18 -10
  132. package/src/engine/types/identifiers.ts +1 -0
  133. package/src/engine/types/index.ts +15 -1
  134. package/src/engine/types/step.ts +334 -0
  135. package/src/engine/types/target-ref.ts +21 -0
  136. package/src/engine/types/tree-node.ts +130 -0
  137. package/src/engine/types/workspace.ts +7 -0
  138. package/src/engine/validate-projection-allowlist.ts +161 -0
  139. package/src/event-store/snapshot.ts +1 -1
  140. package/src/event-store/upcaster-dead-letter.ts +1 -1
  141. package/src/event-store/upcaster.ts +1 -1
  142. package/src/files/__tests__/read-stream.test.ts +105 -0
  143. package/src/files/__tests__/write-stream.test.ts +233 -0
  144. package/src/files/__tests__/zip-stream.test.ts +357 -0
  145. package/src/files/file-routes.ts +1 -1
  146. package/src/files/in-memory-provider.ts +38 -0
  147. package/src/files/index.ts +3 -0
  148. package/src/files/local-provider.ts +58 -1
  149. package/src/files/types.ts +36 -8
  150. package/src/files/zip-stream.ts +251 -0
  151. package/src/jobs/job-runner.ts +10 -10
  152. package/src/lifecycle/lifecycle.ts +0 -3
  153. package/src/logging/index.ts +1 -0
  154. package/src/logging/pino-logger.ts +11 -7
  155. package/src/logging/utils.ts +24 -0
  156. package/src/observability/prometheus-meter.ts +7 -5
  157. package/src/pipeline/__tests__/archive-stream.integration.ts +1 -1
  158. package/src/pipeline/__tests__/causation-chain.integration.ts +1 -1
  159. package/src/pipeline/__tests__/domain-events-projections.integration.ts +3 -3
  160. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +4 -4
  161. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +1 -1
  162. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +3 -3
  163. package/src/pipeline/__tests__/msp-rebuild.integration.ts +3 -3
  164. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +2 -2
  165. package/src/pipeline/__tests__/query-projection.integration.ts +5 -5
  166. package/src/pipeline/append-event-core.ts +22 -6
  167. package/src/pipeline/dispatcher-utils.ts +188 -0
  168. package/src/pipeline/dispatcher.ts +63 -283
  169. package/src/pipeline/distributed-lock.ts +1 -1
  170. package/src/pipeline/entity-cache.ts +2 -2
  171. package/src/pipeline/event-consumer-state.ts +0 -13
  172. package/src/pipeline/event-dispatcher.ts +4 -4
  173. package/src/pipeline/index.ts +0 -2
  174. package/src/pipeline/lifecycle-pipeline.ts +6 -12
  175. package/src/pipeline/msp-rebuild.ts +5 -5
  176. package/src/pipeline/multi-stream-apply-context.ts +6 -7
  177. package/src/pipeline/projection-rebuild.ts +2 -2
  178. package/src/pipeline/projection-state.ts +0 -12
  179. package/src/rate-limit/__tests__/resolver.integration.ts +8 -4
  180. package/src/rate-limit/resolver.ts +1 -1
  181. package/src/search/in-memory-adapter.ts +1 -1
  182. package/src/search/meilisearch-adapter.ts +3 -3
  183. package/src/search/types.ts +1 -1
  184. package/src/secrets/leak-guard.ts +2 -2
  185. package/src/stack/request-helper.ts +9 -5
  186. package/src/stack/test-stack.ts +1 -1
  187. package/src/testing/handler-context.ts +4 -4
  188. package/src/testing/http-cookies.ts +1 -1
  189. package/src/time/tz-context.ts +1 -2
  190. package/src/ui-types/index.ts +4 -0
  191. package/src/engine/feature-ast/extractors.ts +0 -2562
@@ -0,0 +1,72 @@
1
+ import type { CallExpression, SourceFile } from "ts-morph";
2
+ import { SyntaxKind } from "ts-morph";
3
+ import type { ExposesApiPattern, ExtendsRegistrarPattern, UsesApiPattern } from "../patterns";
4
+ import { sourceLocationFromNode } from "../source-location";
5
+ import { type ExtractOutput, fail, ok } from "./shared";
6
+
7
+ export function extractExtendsRegistrar(
8
+ call: CallExpression,
9
+ sourceFile: SourceFile,
10
+ ): ExtractOutput<ExtendsRegistrarPattern> {
11
+ const args = call.getArguments();
12
+ const nameArg = args[0]?.asKind(SyntaxKind.StringLiteral);
13
+ if (!nameArg) {
14
+ return fail(
15
+ "extendsRegistrar",
16
+ sourceLocationFromNode(call, sourceFile),
17
+ "first argument must be a string literal extension name",
18
+ );
19
+ }
20
+ const defArg = args[1];
21
+ if (!defArg) {
22
+ return fail(
23
+ "extendsRegistrar",
24
+ sourceLocationFromNode(call, sourceFile),
25
+ "expected a definition argument",
26
+ );
27
+ }
28
+ return ok({
29
+ kind: "extendsRegistrar",
30
+ source: sourceLocationFromNode(call, sourceFile),
31
+ extensionName: nameArg.getLiteralValue(),
32
+ defBody: sourceLocationFromNode(defArg, sourceFile),
33
+ });
34
+ }
35
+
36
+ export function extractUsesApi(
37
+ call: CallExpression,
38
+ sourceFile: SourceFile,
39
+ ): ExtractOutput<UsesApiPattern> {
40
+ const arg = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral);
41
+ if (!arg) {
42
+ return fail(
43
+ "usesApi",
44
+ sourceLocationFromNode(call, sourceFile),
45
+ 'expected a single string-literal API name (e.g. "sessions.revokeAllForUser")',
46
+ );
47
+ }
48
+ return ok({
49
+ kind: "usesApi",
50
+ source: sourceLocationFromNode(call, sourceFile),
51
+ apiName: arg.getLiteralValue(),
52
+ });
53
+ }
54
+
55
+ export function extractExposesApi(
56
+ call: CallExpression,
57
+ sourceFile: SourceFile,
58
+ ): ExtractOutput<ExposesApiPattern> {
59
+ const arg = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral);
60
+ if (!arg) {
61
+ return fail(
62
+ "exposesApi",
63
+ sourceLocationFromNode(call, sourceFile),
64
+ 'expected a single string-literal API name (e.g. "sessions.revokeAllForUser")',
65
+ );
66
+ }
67
+ return ok({
68
+ kind: "exposesApi",
69
+ source: sourceLocationFromNode(call, sourceFile),
70
+ apiName: arg.getLiteralValue(),
71
+ });
72
+ }
@@ -0,0 +1,66 @@
1
+ import type { CallExpression, SourceFile } from "ts-morph";
2
+ import type { TreeActionDef } from "../../types/tree-node";
3
+ import type { TreeActionsPattern, TreePattern } from "../patterns";
4
+ import { sourceLocationFromNode } from "../source-location";
5
+ import {
6
+ type ExtractOutput,
7
+ fail,
8
+ findFunctionLiteral,
9
+ isPlainObject,
10
+ ok,
11
+ readDataLiteralNode,
12
+ } from "./shared";
13
+
14
+ export function extractTreeActions(
15
+ call: CallExpression,
16
+ sourceFile: SourceFile,
17
+ ): ExtractOutput<TreeActionsPattern> {
18
+ const arg = call.getArguments()[0];
19
+ if (!arg) {
20
+ return fail(
21
+ "treeActions",
22
+ sourceLocationFromNode(call, sourceFile),
23
+ "expected an action-map object literal as first argument",
24
+ );
25
+ }
26
+ const definitions = readDataLiteralNode(arg);
27
+ if (!isPlainObject(definitions)) {
28
+ return fail(
29
+ "treeActions",
30
+ sourceLocationFromNode(call, sourceFile),
31
+ "action-map could not be read as a plain object",
32
+ );
33
+ }
34
+ return ok({
35
+ kind: "treeActions",
36
+ source: sourceLocationFromNode(call, sourceFile),
37
+ definitions: definitions as Readonly<Record<string, TreeActionDef>>,
38
+ });
39
+ }
40
+
41
+ export function extractTree(
42
+ call: CallExpression,
43
+ sourceFile: SourceFile,
44
+ ): ExtractOutput<TreePattern> {
45
+ const arg = call.getArguments()[0];
46
+ if (!arg) {
47
+ return fail(
48
+ "tree",
49
+ sourceLocationFromNode(call, sourceFile),
50
+ "expected a tree-provider function as first argument",
51
+ );
52
+ }
53
+ const fn = findFunctionLiteral(arg);
54
+ if (!fn) {
55
+ return fail(
56
+ "tree",
57
+ sourceLocationFromNode(call, sourceFile),
58
+ "first argument must be an inline arrow function or function expression",
59
+ );
60
+ }
61
+ return ok({
62
+ kind: "tree",
63
+ source: sourceLocationFromNode(call, sourceFile),
64
+ providerBody: sourceLocationFromNode(fn, sourceFile),
65
+ });
66
+ }
@@ -0,0 +1,177 @@
1
+ import type { CallExpression, Node } from "ts-morph";
2
+ import { SyntaxKind } from "ts-morph";
3
+ import type { ParseError } from "../parse";
4
+
5
+ export type ExtractOutput<TPattern> =
6
+ | { readonly kind: "pattern"; readonly pattern: TPattern }
7
+ | { readonly kind: "error"; readonly error: ParseError };
8
+
9
+ export function ok<TPattern>(pattern: TPattern): ExtractOutput<TPattern> {
10
+ return { kind: "pattern", pattern };
11
+ }
12
+
13
+ export function fail(
14
+ methodName: string,
15
+ source: ParseError["source"],
16
+ reason: string,
17
+ ): { readonly kind: "error"; readonly error: ParseError } {
18
+ return { kind: "error", error: { methodName, source, reason } };
19
+ }
20
+
21
+ export function readStringLiteralArgs(call: CallExpression): readonly string[] | undefined {
22
+ const out: string[] = [];
23
+ for (const arg of call.getArguments()) {
24
+ const literal = arg.asKind(SyntaxKind.StringLiteral);
25
+ if (!literal) return undefined;
26
+ out.push(literal.getLiteralValue());
27
+ }
28
+ return out;
29
+ }
30
+
31
+ export function readBooleanProperty(
32
+ objectLiteral: Node,
33
+ propertyName: string,
34
+ ): boolean | undefined {
35
+ const obj = objectLiteral.asKind(SyntaxKind.ObjectLiteralExpression);
36
+ if (!obj) return undefined;
37
+ const prop = obj.getProperty(propertyName);
38
+ if (!prop) return undefined;
39
+ const assignment = prop.asKind(SyntaxKind.PropertyAssignment);
40
+ if (!assignment) return undefined;
41
+ const initializer = assignment.getInitializer();
42
+ if (!initializer) return undefined;
43
+ const kind = initializer.getKind();
44
+ if (kind === SyntaxKind.TrueKeyword) return true;
45
+ if (kind === SyntaxKind.FalseKeyword) return false;
46
+ return undefined;
47
+ }
48
+
49
+ export function readDataLiteralNode(node: Node): unknown {
50
+ const kind = node.getKind();
51
+ switch (kind) {
52
+ case SyntaxKind.StringLiteral:
53
+ return node.asKindOrThrow(SyntaxKind.StringLiteral).getLiteralValue();
54
+ case SyntaxKind.NoSubstitutionTemplateLiteral:
55
+ return node.asKindOrThrow(SyntaxKind.NoSubstitutionTemplateLiteral).getLiteralValue();
56
+ case SyntaxKind.NumericLiteral:
57
+ return Number(node.asKindOrThrow(SyntaxKind.NumericLiteral).getText());
58
+ case SyntaxKind.TrueKeyword:
59
+ return true;
60
+ case SyntaxKind.FalseKeyword:
61
+ return false;
62
+ case SyntaxKind.NullKeyword:
63
+ return null;
64
+ case SyntaxKind.PrefixUnaryExpression: {
65
+ const expr = node.asKindOrThrow(SyntaxKind.PrefixUnaryExpression);
66
+ if (expr.getOperatorToken() !== SyntaxKind.MinusToken) return undefined;
67
+ const inner = readDataLiteralNode(expr.getOperand());
68
+ if (typeof inner !== "number") return undefined;
69
+ return -inner;
70
+ }
71
+ case SyntaxKind.ArrayLiteralExpression: {
72
+ const arr = node.asKindOrThrow(SyntaxKind.ArrayLiteralExpression);
73
+ const out: unknown[] = [];
74
+ for (const el of arr.getElements()) {
75
+ const value = readDataLiteralNode(el);
76
+ if (value === undefined) return undefined;
77
+ out.push(value);
78
+ }
79
+ return out;
80
+ }
81
+ case SyntaxKind.ObjectLiteralExpression: {
82
+ const obj = node.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
83
+ const out: Record<string, unknown> = {};
84
+ for (const prop of obj.getProperties()) {
85
+ const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
86
+ if (!propAssign) return undefined;
87
+ const initializer = propAssign.getInitializer();
88
+ if (!initializer) return undefined;
89
+ const value = readDataLiteralNode(initializer);
90
+ if (value === undefined) return undefined;
91
+ out[readPropertyKey(propAssign)] = value;
92
+ }
93
+ return out;
94
+ }
95
+ case SyntaxKind.AsExpression:
96
+ return readDataLiteralNode(node.asKindOrThrow(SyntaxKind.AsExpression).getExpression());
97
+ case SyntaxKind.SatisfiesExpression:
98
+ return readDataLiteralNode(
99
+ node.asKindOrThrow(SyntaxKind.SatisfiesExpression).getExpression(),
100
+ );
101
+ case SyntaxKind.ParenthesizedExpression:
102
+ return readDataLiteralNode(
103
+ node.asKindOrThrow(SyntaxKind.ParenthesizedExpression).getExpression(),
104
+ );
105
+ default:
106
+ return undefined;
107
+ }
108
+ }
109
+
110
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
111
+ return typeof value === "object" && value !== null && !Array.isArray(value);
112
+ }
113
+
114
+ export function readPropertyKey(propAssign: import("ts-morph").PropertyAssignment): string {
115
+ const nameNode = propAssign.getNameNode();
116
+ const literal = nameNode.asKind(SyntaxKind.StringLiteral);
117
+ if (literal) return literal.getLiteralValue();
118
+ return propAssign.getName();
119
+ }
120
+
121
+ export function readNameOrRef(node: Node): string | undefined {
122
+ const literal = node.asKind(SyntaxKind.StringLiteral);
123
+ if (literal) return literal.getLiteralValue();
124
+ const obj = readDataLiteralNode(node);
125
+ if (isPlainObject(obj) && typeof obj["name"] === "string") return obj["name"];
126
+ return undefined;
127
+ }
128
+
129
+ export function findFunctionLiteral(node: Node): Node | undefined {
130
+ if (node.getKind() === SyntaxKind.ArrowFunction) return node;
131
+ if (node.getKind() === SyntaxKind.FunctionExpression) return node;
132
+ const paren = node.asKind(SyntaxKind.ParenthesizedExpression);
133
+ if (paren) return findFunctionLiteral(paren.getExpression());
134
+ return undefined;
135
+ }
136
+
137
+ export function readNameOrRefOrList(node: Node): string | readonly string[] | undefined {
138
+ const single = readNameOrRef(node);
139
+ if (single) return single;
140
+ const arr = node.asKind(SyntaxKind.ArrayLiteralExpression);
141
+ if (!arr) return undefined;
142
+ const out: string[] = [];
143
+ for (const el of arr.getElements()) {
144
+ const name = readNameOrRef(el);
145
+ if (!name) return undefined;
146
+ out.push(name);
147
+ }
148
+ return out;
149
+ }
150
+
151
+ export function readVarargsOrArrayProp(
152
+ call: CallExpression,
153
+ arrayPropName: "features" | "keys",
154
+ ): readonly string[] | undefined {
155
+ const args = call.getArguments();
156
+ if (args.length === 1) {
157
+ const obj = args[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
158
+ if (obj) {
159
+ const propInit = obj
160
+ .getProperty(arrayPropName)
161
+ ?.asKind(SyntaxKind.PropertyAssignment)
162
+ ?.getInitializer();
163
+ if (propInit) {
164
+ const arr = propInit.asKind(SyntaxKind.ArrayLiteralExpression);
165
+ if (!arr) return undefined;
166
+ const out: string[] = [];
167
+ for (const el of arr.getElements()) {
168
+ const lit = el.asKind(SyntaxKind.StringLiteral);
169
+ if (!lit) return undefined;
170
+ out.push(lit.getLiteralValue());
171
+ }
172
+ return out;
173
+ }
174
+ }
175
+ }
176
+ return readStringLiteralArgs(call);
177
+ }
@@ -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,
@@ -53,7 +54,10 @@ import {
53
54
  extractSystemScope,
54
55
  extractToggleable,
55
56
  extractTranslations,
57
+ extractTree,
58
+ extractTreeActions,
56
59
  extractUseExtension,
60
+ extractUsesApi,
57
61
  extractWorkspace,
58
62
  extractWriteHandler,
59
63
  } from "./extractors";
@@ -346,6 +350,15 @@ function dispatchExtractor(
346
350
  // Round 5 — opaque patterns
347
351
  case "extendsRegistrar":
348
352
  return extractExtendsRegistrar(call, sourceFile);
353
+ case "usesApi":
354
+ return extractUsesApi(call, sourceFile);
355
+ case "exposesApi":
356
+ return extractExposesApi(call, sourceFile);
357
+ // Round 6 — Visual-Tree patterns
358
+ case "treeActions":
359
+ return extractTreeActions(call, sourceFile);
360
+ case "tree":
361
+ return extractTree(call, sourceFile);
349
362
  // Unknown method — UnknownPattern signal so Designer/AI surface it
350
363
  // as "custom call" without losing the source location.
351
364
  default:
@@ -81,7 +81,9 @@ export type PatternId =
81
81
  | { readonly kind: "toggleable" }
82
82
  | { readonly kind: "config" }
83
83
  | { readonly kind: "translations" }
84
- | { readonly kind: "authClaims" };
84
+ | { readonly kind: "authClaims" }
85
+ | { readonly kind: "treeActions" }
86
+ | { readonly kind: "tree" };
85
87
 
86
88
  // =============================================================================
87
89
  // Change ops — generic apply API
@@ -271,6 +273,10 @@ export const SINGLETON_KINDS: ReadonlySet<PatternId["kind"]> = new Set([
271
273
  "config",
272
274
  "translations",
273
275
  "authClaims",
276
+ // Visual-Tree slots — at-most-one per feature, mirrors the registrar's
277
+ // only-once-guard in define-feature.ts.
278
+ "treeActions",
279
+ "tree",
274
280
  ]);
275
281
 
276
282
  /**
@@ -323,6 +329,8 @@ function callMatchesId(call: CallExpression, id: PatternId): boolean {
323
329
  case "config":
324
330
  case "translations":
325
331
  case "authClaims":
332
+ case "treeActions":
333
+ case "tree":
326
334
  return true;
327
335
 
328
336
  case "entity":
@@ -93,7 +93,7 @@ export type AddWriteHandlerArgs = {
93
93
  readonly handlerSource: string;
94
94
  readonly access?: AccessRule;
95
95
  readonly rateLimit?: RateLimitOption;
96
- readonly skipTransitionGuard?: boolean;
96
+ readonly unsafeSkipTransitionGuard?: boolean;
97
97
  };
98
98
 
99
99
  export type AddQueryHandlerArgs = {
@@ -364,7 +364,14 @@ export function createFeaturePatcher(sourceFile: SourceFile): FeaturePatcher {
364
364
  add({ kind: "screen", source: SYNTHETIC_LOC, definition, opaqueProps });
365
365
  },
366
366
 
367
- addWriteHandler({ name, schemaSource, handlerSource, access, rateLimit, skipTransitionGuard }) {
367
+ addWriteHandler({
368
+ name,
369
+ schemaSource,
370
+ handlerSource,
371
+ access,
372
+ rateLimit,
373
+ unsafeSkipTransitionGuard,
374
+ }) {
368
375
  add({
369
376
  kind: "writeHandler",
370
377
  source: SYNTHETIC_LOC,
@@ -373,7 +380,7 @@ export function createFeaturePatcher(sourceFile: SourceFile): FeaturePatcher {
373
380
  handlerBody: rawLoc(handlerSource),
374
381
  ...(access !== undefined && { access }),
375
382
  ...(rateLimit !== undefined && { rateLimit }),
376
- ...(skipTransitionGuard === true && { skipTransitionGuard: true }),
383
+ ...(unsafeSkipTransitionGuard === true && { unsafeSkipTransitionGuard: true }),
377
384
  });
378
385
  },
379
386
 
@@ -26,6 +26,28 @@
26
26
  // **Naming convention.** Pattern `kind` matches the r.* method name
27
27
  // 1:1 (e.g. `r.writeHandler` → `kind: "writeHandler"`). No kebab/camel
28
28
  // translation layer.
29
+ //
30
+ // **Adding a new FeaturePattern kind — full consumer cascade.** The
31
+ // extension-point is wider than just this file + the parser. Update
32
+ // ALL of these when introducing a new r.* API, otherwise tests/checks
33
+ // catch the drift but the call-site jumps across files:
34
+ // 1. patterns.ts (this file): Pattern type + add to FeaturePattern
35
+ // union + getEditability switch
36
+ // 2. feature-ast/extractors.ts: extract<Kind> function + import in
37
+ // patterns-import-block
38
+ // 3. feature-ast/parse.ts: dispatcher case + import
39
+ // 4. feature-ast/render.ts: render<Kind> function + import + switch
40
+ // case
41
+ // 5. feature-ast/patch.ts: PatternId variant; if singleton-per-feature
42
+ // add to SINGLETON_KINDS; callMatchesId case
43
+ // 6. pattern-library/library.ts: <kind>Schema + entry in
44
+ // PATTERN_LIBRARY map
45
+ // 7. pattern-library/__tests__/library.test.ts: ALL_KINDS array +
46
+ // makePlaceholderPattern case
47
+ // TS-exhaustiveness catches most omissions automatically (1, 3, 4, 5,
48
+ // 7-via-makePlaceholderPattern), but the runtime-checked maps in 6 +
49
+ // the ALL_KINDS array in 7 are silent if forgotten — pin them with the
50
+ // library.test.ts coverage tests.
29
51
 
30
52
  import type { LifecycleHookType } from "../constants";
31
53
  import type {
@@ -45,6 +67,7 @@ import type { NavDefinition } from "../types/nav";
45
67
  import type { MspErrorMode } from "../types/projection";
46
68
  import type { RelationDefinition } from "../types/relations";
47
69
  import type { ScreenDefinition } from "../types/screen";
70
+ import type { TreeActionDef } from "../types/tree-node";
48
71
  import type { WorkspaceDefinition } from "../types/workspace";
49
72
  import type { SourceLocation } from "./source-location";
50
73
 
@@ -158,6 +181,17 @@ export type UseExtensionPattern = {
158
181
  readonly options?: Readonly<Record<string, unknown>>;
159
182
  };
160
183
 
184
+ // r.treeActions({ ... }) — Schema-Map für Visual-Tree-Action-Verben.
185
+ // Static: Args sind Type-Samples (kein Runtime-Validator), Designer
186
+ // rendert das als nested form pro Action. Compile-Time-Validation
187
+ // passiert via setup-export-Handle (TreeActionsHandle), nicht über
188
+ // dieses Pattern — das hier ist reine Runtime-Repräsentation.
189
+ export type TreeActionsPattern = {
190
+ readonly kind: "treeActions";
191
+ readonly source: SourceLocation;
192
+ readonly definitions: Readonly<Record<string, TreeActionDef>>;
193
+ };
194
+
161
195
  // =============================================================================
162
196
  // Mixed patterns — header (name/access/etc.) is declarative, body
163
197
  // (handler/hook/apply/transform fn) is opaque. Designer renders the
@@ -202,7 +236,7 @@ export type WriteHandlerPattern = {
202
236
  readonly handlerBody: SourceLocation;
203
237
  readonly access?: AccessRule;
204
238
  readonly rateLimit?: RateLimitOption;
205
- readonly skipTransitionGuard?: boolean;
239
+ readonly unsafeSkipTransitionGuard?: boolean;
206
240
  };
207
241
 
208
242
  export type QueryHandlerPattern = {
@@ -262,6 +296,16 @@ export type AuthClaimsPattern = {
262
296
  readonly fnBody: SourceLocation;
263
297
  };
264
298
 
299
+ // r.tree(provider) — Top-Level-Tree-Provider-Function. Closure-only,
300
+ // kein Header-Form. Designer rendert als read-only Code-Block, AI-
301
+ // Patcher überschreibt span verbatim. Konsistent mit r.authClaims —
302
+ // auch da ist die Function-Body die einzige Information.
303
+ export type TreePattern = {
304
+ readonly kind: "tree";
305
+ readonly source: SourceLocation;
306
+ readonly providerBody: SourceLocation;
307
+ };
308
+
265
309
  export type HttpRoutePattern = {
266
310
  readonly kind: "httpRoute";
267
311
  readonly source: SourceLocation;
@@ -318,6 +362,24 @@ export type ExtendsRegistrarPattern = {
318
362
  readonly defBody: SourceLocation;
319
363
  };
320
364
 
365
+ // r.usesApi("a.b") — declarative cross-feature handler-ID dependency.
366
+ // Boot-validation throws if no other feature exposes the handler. Single
367
+ // string argument; pattern is purely declarative.
368
+ export type UsesApiPattern = {
369
+ readonly kind: "usesApi";
370
+ readonly source: SourceLocation;
371
+ readonly apiName: string;
372
+ };
373
+
374
+ // r.exposesApi("a.b") — declarative announcement that this feature
375
+ // provides a handler matching the cross-feature contract `a.b`. Single
376
+ // string argument; pattern is purely declarative.
377
+ export type ExposesApiPattern = {
378
+ readonly kind: "exposesApi";
379
+ readonly source: SourceLocation;
380
+ readonly apiName: string;
381
+ };
382
+
321
383
  // =============================================================================
322
384
  // Catch-all — r.* calls the visitor doesn't recognise. Designer renders
323
385
  // "unknown call (cannot edit)", AI patcher leaves them unchanged. When
@@ -354,6 +416,9 @@ export type FeaturePattern =
354
416
  | ReferenceDataPattern
355
417
  | ReadsConfigPattern
356
418
  | UseExtensionPattern
419
+ | UsesApiPattern
420
+ | ExposesApiPattern
421
+ | TreeActionsPattern
357
422
  // Mixed
358
423
  | ScreenPattern
359
424
  | WriteHandlerPattern
@@ -369,6 +434,7 @@ export type FeaturePattern =
369
434
  | DefineEventPattern
370
435
  | EventMigrationPattern
371
436
  | ExtendsRegistrarPattern
437
+ | TreePattern
372
438
  // Catch-all
373
439
  | UnknownPattern;
374
440
 
@@ -406,6 +472,9 @@ export function getEditability(pattern: FeaturePattern): Editability {
406
472
  case "referenceData":
407
473
  case "readsConfig":
408
474
  case "useExtension":
475
+ case "usesApi":
476
+ case "exposesApi":
477
+ case "treeActions":
409
478
  return "static";
410
479
  case "screen":
411
480
  case "writeHandler":
@@ -422,6 +491,7 @@ export function getEditability(pattern: FeaturePattern): Editability {
422
491
  return "mixed";
423
492
  case "authClaims":
424
493
  case "extendsRegistrar":
494
+ case "tree":
425
495
  case "unknown":
426
496
  return "opaque";
427
497
  default: {
@@ -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,
@@ -45,8 +46,11 @@ import type {
45
46
  SystemScopePattern,
46
47
  ToggleablePattern,
47
48
  TranslationsPattern,
49
+ TreeActionsPattern,
50
+ TreePattern,
48
51
  UnknownPattern,
49
52
  UseExtensionPattern,
53
+ UsesApiPattern,
50
54
  WorkspacePattern,
51
55
  WriteHandlerPattern,
52
56
  } from "./patterns";
@@ -122,6 +126,14 @@ export function renderPattern(pattern: FeaturePattern): string {
122
126
  return renderEventMigration(pattern);
123
127
  case "extendsRegistrar":
124
128
  return renderExtendsRegistrar(pattern);
129
+ case "usesApi":
130
+ return renderUsesApi(pattern);
131
+ case "exposesApi":
132
+ return renderExposesApi(pattern);
133
+ case "treeActions":
134
+ return renderTreeActions(pattern);
135
+ case "tree":
136
+ return renderTree(pattern);
125
137
  case "unknown":
126
138
  return renderUnknown(pattern);
127
139
  default: {
@@ -340,7 +352,7 @@ function renderWriteHandler(p: WriteHandlerPattern): string {
340
352
  lines.push(` handler: ${reindentBody(p.handlerBody.raw, PATTERN_INDENT)},`);
341
353
  if (p.access !== undefined) lines.push(` access: ${renderValue(p.access)},`);
342
354
  if (p.rateLimit !== undefined) lines.push(` rateLimit: ${renderValue(p.rateLimit)},`);
343
- if (p.skipTransitionGuard === true) lines.push(" skipTransitionGuard: true,");
355
+ if (p.unsafeSkipTransitionGuard === true) lines.push(" unsafeSkipTransitionGuard: true,");
344
356
  lines.push("});");
345
357
  return lines.join("\n");
346
358
  }
@@ -408,6 +420,16 @@ function renderAuthClaims(p: AuthClaimsPattern): string {
408
420
  return `r.authClaims(${p.fnBody.raw});`;
409
421
  }
410
422
 
423
+ // Visual-Tree patterns. treeActions is a static object-literal (mirrors
424
+ // renderWorkspace), tree is opaque-only (mirrors renderAuthClaims).
425
+ function renderTreeActions(p: TreeActionsPattern): string {
426
+ return `r.treeActions(${renderValue(p.definitions)});`;
427
+ }
428
+
429
+ function renderTree(p: TreePattern): string {
430
+ return `r.tree(${p.providerBody.raw});`;
431
+ }
432
+
411
433
  function renderHttpRoute(p: HttpRoutePattern): string {
412
434
  const lines: string[] = ["r.httpRoute({"];
413
435
  lines.push(` method: ${JSON.stringify(p.method)},`);
@@ -473,6 +495,14 @@ function renderExtendsRegistrar(p: ExtendsRegistrarPattern): string {
473
495
  return `r.extendsRegistrar(${JSON.stringify(p.extensionName)}, ${p.defBody.raw});`;
474
496
  }
475
497
 
498
+ function renderUsesApi(p: UsesApiPattern): string {
499
+ return `r.usesApi(${JSON.stringify(p.apiName)});`;
500
+ }
501
+
502
+ function renderExposesApi(p: ExposesApiPattern): string {
503
+ return `r.exposesApi(${JSON.stringify(p.apiName)});`;
504
+ }
505
+
476
506
  function renderUnknown(p: UnknownPattern): string {
477
507
  // Round-trip preservation only: emit the raw call text from the
478
508
  // SourceLocation so the rendered file stays semantically identical