@cosmicdrift/kumiko-framework 0.2.3 → 0.4.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 (166) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/package.json +124 -39
  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/compliance/profiles.ts +8 -8
  10. package/src/db/assert-exists-in.ts +2 -2
  11. package/src/db/cursor.ts +3 -3
  12. package/src/db/event-store-executor.ts +19 -13
  13. package/src/db/located-timestamp.ts +1 -1
  14. package/src/db/money.ts +12 -2
  15. package/src/db/pg-error.ts +1 -1
  16. package/src/db/row-helpers.ts +1 -1
  17. package/src/db/table-builder.ts +3 -5
  18. package/src/db/tenant-db.ts +9 -9
  19. package/src/engine/__tests__/_pipeline-test-utils.ts +23 -0
  20. package/src/engine/__tests__/build-target.test.ts +135 -0
  21. package/src/engine/__tests__/codemod-pipeline.test.ts +551 -0
  22. package/src/engine/__tests__/entity-handlers.test.ts +3 -3
  23. package/src/engine/__tests__/event-helpers.test.ts +4 -4
  24. package/src/engine/__tests__/pipeline-engine.test.ts +215 -0
  25. package/src/engine/__tests__/pipeline-handler.integration.ts +894 -0
  26. package/src/engine/__tests__/pipeline-observability.integration.ts +142 -0
  27. package/src/engine/__tests__/pipeline-performance.integration.ts +152 -0
  28. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +288 -0
  29. package/src/engine/__tests__/raw-table.test.ts +2 -2
  30. package/src/engine/__tests__/steps-aggregate-append-event.test.ts +115 -0
  31. package/src/engine/__tests__/steps-aggregate-create.test.ts +92 -0
  32. package/src/engine/__tests__/steps-aggregate-update.test.ts +127 -0
  33. package/src/engine/__tests__/steps-call-feature.test.ts +123 -0
  34. package/src/engine/__tests__/steps-mail-send.test.ts +136 -0
  35. package/src/engine/__tests__/steps-read.test.ts +142 -0
  36. package/src/engine/__tests__/steps-resolver-utils.test.ts +50 -0
  37. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +69 -0
  38. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +117 -0
  39. package/src/engine/__tests__/steps-webhook-send.test.ts +135 -0
  40. package/src/engine/__tests__/steps-workflow.test.ts +198 -0
  41. package/src/engine/__tests__/validate-projection-allowlist.test.ts +491 -0
  42. package/src/engine/__tests__/visual-tree-patterns.test.ts +251 -0
  43. package/src/engine/boot-validator/api-ext.ts +77 -0
  44. package/src/engine/boot-validator/config-deps.ts +163 -0
  45. package/src/engine/boot-validator/entity-handler.ts +466 -0
  46. package/src/engine/boot-validator/index.ts +159 -0
  47. package/src/engine/boot-validator/ownership.ts +198 -0
  48. package/src/engine/boot-validator/pii-retention.ts +155 -0
  49. package/src/engine/boot-validator/screens-nav.ts +624 -0
  50. package/src/engine/boot-validator.ts +1 -1804
  51. package/src/engine/build-app-schema.ts +1 -1
  52. package/src/engine/build-target.ts +99 -0
  53. package/src/engine/codemod/index.ts +15 -0
  54. package/src/engine/codemod/pipeline-codemod.ts +641 -0
  55. package/src/engine/config-helpers.ts +9 -19
  56. package/src/engine/constants.ts +1 -1
  57. package/src/engine/define-feature.ts +88 -9
  58. package/src/engine/define-handler.ts +89 -3
  59. package/src/engine/define-roles.ts +2 -2
  60. package/src/engine/define-step.ts +28 -0
  61. package/src/engine/define-workflow.ts +110 -0
  62. package/src/engine/entity-handlers.ts +10 -9
  63. package/src/engine/event-helpers.ts +4 -4
  64. package/src/engine/factories.ts +12 -12
  65. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +184 -0
  66. package/src/engine/feature-ast/extractors/index.ts +74 -0
  67. package/src/engine/feature-ast/extractors/round1.ts +110 -0
  68. package/src/engine/feature-ast/extractors/round2.ts +253 -0
  69. package/src/engine/feature-ast/extractors/round3.ts +471 -0
  70. package/src/engine/feature-ast/extractors/round4.ts +1365 -0
  71. package/src/engine/feature-ast/extractors/round5.ts +72 -0
  72. package/src/engine/feature-ast/extractors/round6.ts +66 -0
  73. package/src/engine/feature-ast/extractors/shared.ts +177 -0
  74. package/src/engine/feature-ast/parse.ts +7 -0
  75. package/src/engine/feature-ast/patch.ts +9 -1
  76. package/src/engine/feature-ast/patcher.ts +10 -3
  77. package/src/engine/feature-ast/patterns.ts +49 -1
  78. package/src/engine/feature-ast/render.ts +17 -1
  79. package/src/engine/index.ts +44 -2
  80. package/src/engine/pattern-library/__tests__/library.test.ts +6 -0
  81. package/src/engine/pattern-library/library.ts +42 -2
  82. package/src/engine/pipeline.ts +88 -0
  83. package/src/engine/projection-helpers.ts +1 -1
  84. package/src/engine/read-claim.ts +1 -1
  85. package/src/engine/registry.ts +30 -2
  86. package/src/engine/resolve-config-or-param.ts +4 -0
  87. package/src/engine/run-pipeline.ts +162 -0
  88. package/src/engine/schema-builder.ts +2 -4
  89. package/src/engine/state-machine.ts +1 -1
  90. package/src/engine/steps/_drizzle-boundary.ts +19 -0
  91. package/src/engine/steps/_duration-utils.ts +33 -0
  92. package/src/engine/steps/_no-return-guard.ts +21 -0
  93. package/src/engine/steps/_resolver-utils.ts +42 -0
  94. package/src/engine/steps/_step-dispatch-constants.ts +38 -0
  95. package/src/engine/steps/aggregate-append-event.ts +56 -0
  96. package/src/engine/steps/aggregate-create.ts +56 -0
  97. package/src/engine/steps/aggregate-update.ts +68 -0
  98. package/src/engine/steps/branch.ts +84 -0
  99. package/src/engine/steps/call-feature.ts +49 -0
  100. package/src/engine/steps/compute.ts +41 -0
  101. package/src/engine/steps/for-each.ts +111 -0
  102. package/src/engine/steps/mail-send.ts +44 -0
  103. package/src/engine/steps/read-find-many.ts +51 -0
  104. package/src/engine/steps/read-find-one.ts +58 -0
  105. package/src/engine/steps/retry.ts +87 -0
  106. package/src/engine/steps/return.ts +34 -0
  107. package/src/engine/steps/unsafe-projection-delete.ts +46 -0
  108. package/src/engine/steps/unsafe-projection-upsert.ts +69 -0
  109. package/src/engine/steps/wait-for-event.ts +71 -0
  110. package/src/engine/steps/wait.ts +69 -0
  111. package/src/engine/steps/webhook-send.ts +71 -0
  112. package/src/engine/system-user.ts +1 -1
  113. package/src/engine/types/feature.ts +93 -1
  114. package/src/engine/types/handlers.ts +18 -10
  115. package/src/engine/types/index.ts +11 -1
  116. package/src/engine/types/step.ts +334 -0
  117. package/src/engine/types/target-ref.ts +21 -0
  118. package/src/engine/types/tree-node.ts +132 -0
  119. package/src/engine/types/workspace.ts +7 -0
  120. package/src/engine/validate-projection-allowlist.ts +161 -0
  121. package/src/event-store/snapshot.ts +1 -1
  122. package/src/event-store/upcaster-dead-letter.ts +1 -1
  123. package/src/event-store/upcaster.ts +1 -1
  124. package/src/files/file-routes.ts +1 -1
  125. package/src/files/types.ts +2 -2
  126. package/src/jobs/job-runner.ts +10 -10
  127. package/src/lifecycle/lifecycle.ts +0 -3
  128. package/src/logging/index.ts +1 -0
  129. package/src/logging/pino-logger.ts +11 -7
  130. package/src/logging/utils.ts +24 -0
  131. package/src/observability/prometheus-meter.ts +7 -5
  132. package/src/pipeline/__tests__/archive-stream.integration.ts +1 -1
  133. package/src/pipeline/__tests__/causation-chain.integration.ts +1 -1
  134. package/src/pipeline/__tests__/domain-events-projections.integration.ts +3 -3
  135. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +4 -4
  136. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +1 -1
  137. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +3 -3
  138. package/src/pipeline/__tests__/msp-rebuild.integration.ts +3 -3
  139. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +2 -2
  140. package/src/pipeline/__tests__/query-projection.integration.ts +5 -5
  141. package/src/pipeline/append-event-core.ts +22 -6
  142. package/src/pipeline/dispatcher-utils.ts +188 -0
  143. package/src/pipeline/dispatcher.ts +63 -283
  144. package/src/pipeline/distributed-lock.ts +1 -1
  145. package/src/pipeline/entity-cache.ts +2 -2
  146. package/src/pipeline/event-consumer-state.ts +0 -13
  147. package/src/pipeline/event-dispatcher.ts +4 -4
  148. package/src/pipeline/index.ts +0 -2
  149. package/src/pipeline/lifecycle-pipeline.ts +6 -12
  150. package/src/pipeline/msp-rebuild.ts +5 -5
  151. package/src/pipeline/multi-stream-apply-context.ts +6 -7
  152. package/src/pipeline/projection-rebuild.ts +2 -2
  153. package/src/pipeline/projection-state.ts +0 -12
  154. package/src/rate-limit/__tests__/resolver.integration.ts +8 -4
  155. package/src/rate-limit/resolver.ts +1 -1
  156. package/src/search/in-memory-adapter.ts +1 -1
  157. package/src/search/meilisearch-adapter.ts +3 -3
  158. package/src/search/types.ts +1 -1
  159. package/src/secrets/leak-guard.ts +2 -2
  160. package/src/stack/request-helper.ts +9 -5
  161. package/src/stack/test-stack.ts +1 -1
  162. package/src/testing/handler-context.ts +4 -4
  163. package/src/testing/http-cookies.ts +1 -1
  164. package/src/time/tz-context.ts +1 -2
  165. package/src/ui-types/index.ts +4 -0
  166. package/src/engine/feature-ast/extractors.ts +0 -2602
@@ -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
+ }
@@ -54,6 +54,8 @@ import {
54
54
  extractSystemScope,
55
55
  extractToggleable,
56
56
  extractTranslations,
57
+ extractTree,
58
+ extractTreeActions,
57
59
  extractUseExtension,
58
60
  extractUsesApi,
59
61
  extractWorkspace,
@@ -352,6 +354,11 @@ function dispatchExtractor(
352
354
  return extractUsesApi(call, sourceFile);
353
355
  case "exposesApi":
354
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);
355
362
  // Unknown method — UnknownPattern signal so Designer/AI surface it
356
363
  // as "custom call" without losing the source location.
357
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;
@@ -374,6 +418,7 @@ export type FeaturePattern =
374
418
  | UseExtensionPattern
375
419
  | UsesApiPattern
376
420
  | ExposesApiPattern
421
+ | TreeActionsPattern
377
422
  // Mixed
378
423
  | ScreenPattern
379
424
  | WriteHandlerPattern
@@ -389,6 +434,7 @@ export type FeaturePattern =
389
434
  | DefineEventPattern
390
435
  | EventMigrationPattern
391
436
  | ExtendsRegistrarPattern
437
+ | TreePattern
392
438
  // Catch-all
393
439
  | UnknownPattern;
394
440
 
@@ -428,6 +474,7 @@ export function getEditability(pattern: FeaturePattern): Editability {
428
474
  case "useExtension":
429
475
  case "usesApi":
430
476
  case "exposesApi":
477
+ case "treeActions":
431
478
  return "static";
432
479
  case "screen":
433
480
  case "writeHandler":
@@ -444,6 +491,7 @@ export function getEditability(pattern: FeaturePattern): Editability {
444
491
  return "mixed";
445
492
  case "authClaims":
446
493
  case "extendsRegistrar":
494
+ case "tree":
447
495
  case "unknown":
448
496
  return "opaque";
449
497
  default: {
@@ -46,6 +46,8 @@ import type {
46
46
  SystemScopePattern,
47
47
  ToggleablePattern,
48
48
  TranslationsPattern,
49
+ TreeActionsPattern,
50
+ TreePattern,
49
51
  UnknownPattern,
50
52
  UseExtensionPattern,
51
53
  UsesApiPattern,
@@ -128,6 +130,10 @@ export function renderPattern(pattern: FeaturePattern): string {
128
130
  return renderUsesApi(pattern);
129
131
  case "exposesApi":
130
132
  return renderExposesApi(pattern);
133
+ case "treeActions":
134
+ return renderTreeActions(pattern);
135
+ case "tree":
136
+ return renderTree(pattern);
131
137
  case "unknown":
132
138
  return renderUnknown(pattern);
133
139
  default: {
@@ -346,7 +352,7 @@ function renderWriteHandler(p: WriteHandlerPattern): string {
346
352
  lines.push(` handler: ${reindentBody(p.handlerBody.raw, PATTERN_INDENT)},`);
347
353
  if (p.access !== undefined) lines.push(` access: ${renderValue(p.access)},`);
348
354
  if (p.rateLimit !== undefined) lines.push(` rateLimit: ${renderValue(p.rateLimit)},`);
349
- if (p.skipTransitionGuard === true) lines.push(" skipTransitionGuard: true,");
355
+ if (p.unsafeSkipTransitionGuard === true) lines.push(" unsafeSkipTransitionGuard: true,");
350
356
  lines.push("});");
351
357
  return lines.join("\n");
352
358
  }
@@ -414,6 +420,16 @@ function renderAuthClaims(p: AuthClaimsPattern): string {
414
420
  return `r.authClaims(${p.fnBody.raw});`;
415
421
  }
416
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
+
417
433
  function renderHttpRoute(p: HttpRoutePattern): string {
418
434
  const lines: string[] = ["r.httpRoute({"];
419
435
  lines.push(` method: ${JSON.stringify(p.method)},`);
@@ -3,6 +3,7 @@
3
3
  export { hasAccess } from "./access";
4
4
  export { validateBoot } from "./boot-validator";
5
5
  export { buildAppSchema } from "./build-app-schema";
6
+ export { buildTarget } from "./build-target";
6
7
  export { access, createSystemConfig, createTenantConfig, createUserConfig } from "./config-helpers";
7
8
  export type { SystemHookName } from "./constants";
8
9
  export {
@@ -18,9 +19,16 @@ export {
18
19
  export type { App, AppConfig } from "./create-app";
19
20
  export { createApp } from "./create-app";
20
21
  export { defineFeature } from "./define-feature";
21
- export type { QueryHandlerDefinition, WriteHandlerDefinition } from "./define-handler";
22
+ export type {
23
+ QueryHandlerDefinition,
24
+ WriteHandlerDefinition,
25
+ WriteHandlerInput,
26
+ } from "./define-handler";
22
27
  export { defineQueryHandler, defineWriteHandler } from "./define-handler";
23
28
  export { defineRoles } from "./define-roles";
29
+ export { defineStep, getStep, listStepKinds } from "./define-step";
30
+ export type { WorkflowDefinition, WorkflowInput, WorkflowTrigger } from "./define-workflow";
31
+ export { computeDefinitionFingerprint, defineWorkflow } from "./define-workflow";
24
32
  export type { ToggleReader } from "./effective-features";
25
33
  export { computeEffectiveFeatures } from "./effective-features";
26
34
  export {
@@ -125,6 +133,7 @@ export {
125
133
  } from "./field-access";
126
134
  export type { OwnershipClause, OwnershipMap, OwnershipRef, OwnershipRule } from "./ownership";
127
135
  export { from } from "./ownership";
136
+ export { buildPipelineSteps, pipeline } from "./pipeline";
128
137
  export { defineApply, defineMspApply, setFields } from "./projection-helpers";
129
138
  export type { BuiltinQnType, ParsedQn, QnType } from "./qualified-name";
130
139
  export { isValidQn, parseQn, QnTypes, qn, toKebab } from "./qualified-name";
@@ -133,9 +142,22 @@ export { createRegistry } from "./registry";
133
142
  export type { ClampInfo, ResolveOptions } from "./resolve-config-or-param";
134
143
  export { resolveConfigOrParam } from "./resolve-config-or-param";
135
144
  export { runsInLane } from "./run-in";
145
+ export type { StepListOutcome } from "./run-pipeline";
146
+ export { runPipeline, runStepList } from "./run-pipeline";
136
147
  export { buildInsertSchema, buildUpdateSchema } from "./schema-builder";
137
148
  export type { TransitionGraph } from "./state-machine";
138
149
  export { defineTransitions, guardTransition } from "./state-machine";
150
+ export {
151
+ SUSPEND_SENTINEL,
152
+ WORKFLOW_AGGREGATE_TYPE,
153
+ WORKFLOW_RESUMED_TYPE,
154
+ WORKFLOW_RETRY_SCHEDULED_TYPE,
155
+ WORKFLOW_RUN_COMPLETED_TYPE,
156
+ WORKFLOW_RUN_FAILED_TYPE,
157
+ WORKFLOW_RUN_STARTED_TYPE,
158
+ WORKFLOW_WAITING_FOR_EVENT_TYPE,
159
+ WORKFLOW_WAITING_TYPE,
160
+ } from "./steps/_step-dispatch-constants";
139
161
  export {
140
162
  ANONYMOUS_ROLE,
141
163
  ANONYMOUS_USER_ID,
@@ -157,7 +179,6 @@ export type {
157
179
  AppContext,
158
180
  AppendEventArgs,
159
181
  AppendEventFn,
160
- AppendEventUnsafeFn,
161
182
  AuthClaimsContext,
162
183
  AuthClaimsFn,
163
184
  AuthClaimsHookDef,
@@ -261,11 +282,20 @@ export type {
261
282
  ScreenSlots,
262
283
  SelectFieldDef,
263
284
  SessionUser,
285
+ Subscribe,
286
+ TargetRef,
264
287
  TenantId,
265
288
  TextFieldDef,
266
289
  ToolbarAction,
267
290
  TranslationKeys,
268
291
  TranslationsDef,
292
+ TreeAction,
293
+ TreeActionDef,
294
+ TreeActionsHandle,
295
+ TreeChildrenSubscribe,
296
+ TreeNode,
297
+ TreeNodeState,
298
+ UnsafeAppendEventFn,
269
299
  ValidationError,
270
300
  ValidationHookFn,
271
301
  WorkspaceDefinition,
@@ -278,4 +308,16 @@ export { DEFAULT_CURRENCIES, HookPhases } from "./types";
278
308
  export { resolveName, withResponseData } from "./types/handlers";
279
309
  export { isSystemTenant, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers";
280
310
  export { normalizeEditField, normalizeListColumn } from "./types/screen";
311
+ export type {
312
+ PipelineBuildCtx,
313
+ PipelineCtx,
314
+ PipelineDef,
315
+ StepBuilder,
316
+ StepDef,
317
+ StepFailureStrategy,
318
+ StepInstance,
319
+ StepKind,
320
+ StepNamespace,
321
+ StepResolver,
322
+ } from "./types/step";
281
323
  export { runValidation } from "./validation";
@@ -57,6 +57,8 @@ const ALL_KINDS: readonly FeaturePatternKind[] = [
57
57
  "extendsRegistrar",
58
58
  "usesApi",
59
59
  "exposesApi",
60
+ "treeActions",
61
+ "tree",
60
62
  "unknown",
61
63
  ];
62
64
 
@@ -340,6 +342,10 @@ function makePlaceholderPattern(kind: FeaturePatternKind): FeaturePattern {
340
342
  extensionName: "x",
341
343
  defBody: PLACEHOLDER_BODY_LOC,
342
344
  };
345
+ case "treeActions":
346
+ return { kind, source: PLACEHOLDER_LOC, definitions: {} };
347
+ case "tree":
348
+ return { kind, source: PLACEHOLDER_LOC, providerBody: PLACEHOLDER_BODY_LOC };
343
349
  case "unknown":
344
350
  return { kind, source: PLACEHOLDER_LOC, methodName: "x" };
345
351
  case "usesApi":