@ductape/sdk 0.1.25 → 0.1.27

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 (152) hide show
  1. package/README.md +83 -52
  2. package/dist/agents/agent-context.d.ts +1 -1
  3. package/dist/agents/agent-context.js +1 -1
  4. package/dist/agents/agent-context.js.map +1 -1
  5. package/dist/agents/agents.service.js +4 -4
  6. package/dist/agents/agents.service.js.map +1 -1
  7. package/dist/agents/types/agents.types.d.ts +6 -6
  8. package/dist/api/services/featureApi.service.d.ts +199 -0
  9. package/dist/api/services/featureApi.service.js +201 -0
  10. package/dist/api/services/featureApi.service.js.map +1 -0
  11. package/dist/api/services/processorApi.service.d.ts +2 -2
  12. package/dist/api/services/processorApi.service.js +1 -1
  13. package/dist/api/services/productsApi.service.d.ts +6 -6
  14. package/dist/api/services/productsApi.service.js +3 -3
  15. package/dist/api/services/productsApi.service.js.map +1 -1
  16. package/dist/api/urls.d.ts +12 -12
  17. package/dist/api/urls.js +22 -22
  18. package/dist/api/urls.js.map +1 -1
  19. package/dist/apps/validators/index.d.ts +5 -1
  20. package/dist/apps/validators/index.js +9 -1
  21. package/dist/apps/validators/index.js.map +1 -1
  22. package/dist/apps/validators/joi-validators/create.appWebhook.validator.d.ts +1 -0
  23. package/dist/apps/validators/joi-validators/create.appWebhook.validator.js +1 -0
  24. package/dist/apps/validators/joi-validators/create.appWebhook.validator.js.map +1 -1
  25. package/dist/apps/validators/joi-validators/create.appWebhookEvent.validator.d.ts +1 -0
  26. package/dist/apps/validators/joi-validators/create.appWebhookEvent.validator.js +1 -0
  27. package/dist/apps/validators/joi-validators/create.appWebhookEvent.validator.js.map +1 -1
  28. package/dist/apps/validators/joi-validators/update.appWebhook.validator.d.ts +1 -0
  29. package/dist/apps/validators/joi-validators/update.appWebhook.validator.js +1 -0
  30. package/dist/apps/validators/joi-validators/update.appWebhook.validator.js.map +1 -1
  31. package/dist/apps/validators/joi-validators/update.appWebhookEvent.validator.d.ts +1 -0
  32. package/dist/apps/validators/joi-validators/update.appWebhookEvent.validator.js +1 -0
  33. package/dist/apps/validators/joi-validators/update.appWebhookEvent.validator.js.map +1 -1
  34. package/dist/brokers/brokers.service.d.ts +2 -2
  35. package/dist/brokers/brokers.service.js +2 -2
  36. package/dist/brokers/brokers.service.js.map +1 -1
  37. package/dist/brokers/types/index.d.ts +1 -1
  38. package/dist/cloud/cloud-database-link.util.js +22 -3
  39. package/dist/cloud/cloud-database-link.util.js.map +1 -1
  40. package/dist/cloud/cloud-service-map.util.js +6 -5
  41. package/dist/cloud/cloud-service-map.util.js.map +1 -1
  42. package/dist/cloud/index.d.ts +1 -1
  43. package/dist/cloud/index.js +3 -1
  44. package/dist/cloud/index.js.map +1 -1
  45. package/dist/cloud/types/cloud.types.d.ts +1 -1
  46. package/dist/cloud/vpc-connector-connect.util.d.ts +11 -2
  47. package/dist/cloud/vpc-connector-connect.util.js +7 -0
  48. package/dist/cloud/vpc-connector-connect.util.js.map +1 -1
  49. package/dist/cloud/vpc-connector-tunnel.client.d.ts +26 -0
  50. package/dist/cloud/vpc-connector-tunnel.client.js +70 -0
  51. package/dist/cloud/vpc-connector-tunnel.client.js.map +1 -1
  52. package/dist/database/adapters/mysql.adapter.js +15 -0
  53. package/dist/database/adapters/mysql.adapter.js.map +1 -1
  54. package/dist/database/adapters/postgresql.adapter.js +11 -55
  55. package/dist/database/adapters/postgresql.adapter.js.map +1 -1
  56. package/dist/database/databases.service.d.ts +3 -3
  57. package/dist/database/databases.service.js +18 -18
  58. package/dist/database/databases.service.js.map +1 -1
  59. package/dist/database/index.d.ts +1 -1
  60. package/dist/database/triggers/trigger-processor.d.ts +7 -7
  61. package/dist/database/triggers/trigger-processor.js +17 -17
  62. package/dist/database/triggers/trigger-processor.js.map +1 -1
  63. package/dist/database/types/connection.interface.d.ts +0 -2
  64. package/dist/database/types/index.d.ts +1 -1
  65. package/dist/database/types/trigger.interface.d.ts +9 -9
  66. package/dist/database/types/trigger.interface.js +3 -3
  67. package/dist/database/types/trigger.interface.js.map +1 -1
  68. package/dist/features/feature-builder.d.ts +70 -0
  69. package/dist/features/feature-builder.js +348 -0
  70. package/dist/features/feature-builder.js.map +1 -0
  71. package/dist/features/feature-executor.d.ts +289 -0
  72. package/dist/features/feature-executor.js +2444 -0
  73. package/dist/features/feature-executor.js.map +1 -0
  74. package/dist/features/features.service.d.ts +412 -0
  75. package/dist/features/features.service.js +2222 -0
  76. package/dist/features/features.service.js.map +1 -0
  77. package/dist/features/index.d.ts +30 -0
  78. package/dist/features/index.js +64 -0
  79. package/dist/features/index.js.map +1 -0
  80. package/dist/features/types/features.types.d.ts +1081 -0
  81. package/dist/features/types/features.types.js +13 -0
  82. package/dist/features/types/features.types.js.map +1 -0
  83. package/dist/features/types/index.d.ts +6 -0
  84. package/dist/features/types/index.js +23 -0
  85. package/dist/features/types/index.js.map +1 -0
  86. package/dist/index.d.ts +35 -48
  87. package/dist/index.js +48 -63
  88. package/dist/index.js.map +1 -1
  89. package/dist/init.interface.d.ts +1 -11
  90. package/dist/inputs/inputs.service.d.ts +4 -4
  91. package/dist/inputs/inputs.service.js.map +1 -1
  92. package/dist/introspect/index.d.ts +31 -0
  93. package/dist/introspect/index.js +168 -0
  94. package/dist/introspect/index.js.map +1 -0
  95. package/dist/jobs/index.d.ts +1 -1
  96. package/dist/jobs/types.d.ts +3 -3
  97. package/dist/logs/logs.service.d.ts +0 -3
  98. package/dist/logs/logs.service.js +1 -4
  99. package/dist/logs/logs.service.js.map +1 -1
  100. package/dist/logs/logs.types.d.ts +4 -7
  101. package/dist/logs/logs.types.js +2 -3
  102. package/dist/logs/logs.types.js.map +1 -1
  103. package/dist/processor/services/processor.service.d.ts +27 -26
  104. package/dist/processor/services/processor.service.js +71 -72
  105. package/dist/processor/services/processor.service.js.map +1 -1
  106. package/dist/processor/utils/processor.utils.d.ts +2 -2
  107. package/dist/processor/utils/processor.utils.js.map +1 -1
  108. package/dist/products/bootstrap-cache.d.ts +2 -2
  109. package/dist/products/bootstrap-cache.js +3 -3
  110. package/dist/products/bootstrap-cache.js.map +1 -1
  111. package/dist/products/services/products.service.d.ts +32 -57
  112. package/dist/products/services/products.service.js +44 -397
  113. package/dist/products/services/products.service.js.map +1 -1
  114. package/dist/products/validators/index.d.ts +1 -3
  115. package/dist/products/validators/index.js +1 -5
  116. package/dist/products/validators/index.js.map +1 -1
  117. package/dist/products/validators/joi-validators/create.productDatabase.validator.js +1 -1
  118. package/dist/products/validators/joi-validators/create.productDatabase.validator.js.map +1 -1
  119. package/dist/products/validators/joi-validators/create.productFeature.validator.d.ts +1 -7
  120. package/dist/products/validators/joi-validators/create.productFeature.validator.js +6 -61
  121. package/dist/products/validators/joi-validators/create.productFeature.validator.js.map +1 -1
  122. package/dist/products/validators/joi-validators/update.productDatabase.validator.js +23 -27
  123. package/dist/products/validators/joi-validators/update.productDatabase.validator.js.map +1 -1
  124. package/dist/resilience/fallback.service.js +19 -41
  125. package/dist/resilience/fallback.service.js.map +1 -1
  126. package/dist/resilience/healthcheck.service.d.ts +5 -5
  127. package/dist/resilience/healthcheck.service.js +26 -26
  128. package/dist/resilience/healthcheck.service.js.map +1 -1
  129. package/dist/resilience/quota.service.js +17 -27
  130. package/dist/resilience/quota.service.js.map +1 -1
  131. package/dist/resilience/resilience-execution.utils.d.ts +2 -2
  132. package/dist/resilience/resilience-execution.utils.js +7 -7
  133. package/dist/resilience/resilience-execution.utils.js.map +1 -1
  134. package/dist/resilience/resilience.service.js +14 -30
  135. package/dist/resilience/resilience.service.js.map +1 -1
  136. package/dist/resilience/types/index.d.ts +20 -23
  137. package/dist/resilience/types/index.js +2 -3
  138. package/dist/resilience/types/index.js.map +1 -1
  139. package/dist/types/enums.d.ts +1 -2
  140. package/dist/types/enums.js +1 -2
  141. package/dist/types/enums.js.map +1 -1
  142. package/dist/types/index.types.d.ts +3 -4
  143. package/dist/types/index.types.js +2 -2
  144. package/dist/types/index.types.js.map +1 -1
  145. package/dist/types/logs.types.d.ts +0 -1
  146. package/dist/types/logs.types.js.map +1 -1
  147. package/dist/types/processor.types.d.ts +11 -13
  148. package/dist/types/processor.types.js.map +1 -1
  149. package/dist/types/productsBuilder.types.d.ts +52 -58
  150. package/dist/types/productsBuilder.types.js +57 -55
  151. package/dist/types/productsBuilder.types.js.map +1 -1
  152. package/package.json +5 -2
@@ -0,0 +1,2444 @@
1
+ "use strict";
2
+ /**
3
+ * FeatureExecutor - Executes feature steps and manages rollbacks
4
+ *
5
+ * Handles the actual execution of feature steps, including:
6
+ * - Processing each step type (action, database, notification, etc.)
7
+ * - Managing rollback on failure
8
+ * - Tracking feature state and completed steps
9
+ * - Handling signals and checkpoints
10
+ */
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ var _a, _b;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.FeatureExecutor = void 0;
17
+ const crypto_1 = require("crypto");
18
+ const processor_service_1 = __importDefault(require("../processor/services/processor.service"));
19
+ const products_service_1 = __importDefault(require("../products/services/products.service"));
20
+ const processorApi_service_1 = require("../api/services/processorApi.service");
21
+ const featureApi_service_1 = require("../api/services/featureApi.service");
22
+ const graph_1 = require("../graph");
23
+ const database_1 = require("../database");
24
+ const brokers_1 = require("../brokers");
25
+ const storage_1 = require("../storage");
26
+ const vector_1 = require("../vector");
27
+ const resilience_1 = require("../resilience");
28
+ const logs_service_1 = __importDefault(require("../logs/logs.service"));
29
+ const logs_types_1 = require("../logs/logs.types");
30
+ const types_1 = require("../types");
31
+ const inputs_types_1 = require("../types/inputs.types");
32
+ const processor_utils_1 = require("../processor/utils/processor.utils");
33
+ const productsBuilder_types_1 = require("../types/productsBuilder.types");
34
+ const date_fns_1 = require("date-fns");
35
+ /** Only log when DUCTAPE_DEBUG is set to avoid sync I/O and serialization cost in hot path */
36
+ const debugLog = typeof process !== 'undefined' && (((_a = process.env) === null || _a === void 0 ? void 0 : _a.DUCTAPE_DEBUG) === 'true' || ((_b = process.env) === null || _b === void 0 ? void 0 : _b.DUCTAPE_DEBUG) === '1')
37
+ ? (...args) => console.log(...args)
38
+ : () => { };
39
+ /** Always-on step/summary logging for visibility in tests and support */
40
+ const stepLog = typeof process !== 'undefined'
41
+ ? (msg, data) => {
42
+ if (data != null && Object.keys(data).length > 0) {
43
+ console.log(`[FeatureExecutor] ${msg}`, data);
44
+ }
45
+ else {
46
+ console.log(`[FeatureExecutor] ${msg}`);
47
+ }
48
+ }
49
+ : () => { };
50
+ /** Normalize error to a string and extract HTTP details when present (e.g. Axios) */
51
+ function formatErrorDetails(error) {
52
+ var _a, _b, _c, _d;
53
+ const err = error;
54
+ const message = (_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(error);
55
+ const out = { message };
56
+ if (err === null || err === void 0 ? void 0 : err.stack)
57
+ out.stack = err.stack;
58
+ if (err === null || err === void 0 ? void 0 : err.response) {
59
+ out.status = (_b = err.response) === null || _b === void 0 ? void 0 : _b.status;
60
+ const data = (_c = err.response) === null || _c === void 0 ? void 0 : _c.data;
61
+ out.responseBody = data;
62
+ // Prefer API error reason over generic "Request failed with status code N"
63
+ const apiErrors = data === null || data === void 0 ? void 0 : data.errors;
64
+ if (apiErrors != null) {
65
+ out.reason = typeof apiErrors === 'string' ? apiErrors : ((_d = apiErrors === null || apiErrors === void 0 ? void 0 : apiErrors.message) !== null && _d !== void 0 ? _d : JSON.stringify(apiErrors));
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+ /**
71
+ * FeatureExecutor handles the execution of feature steps
72
+ */
73
+ class FeatureExecutor {
74
+ constructor(config, feature, options, private_key, sessionLogFields, preInitializedBuilder) {
75
+ var _a, _b;
76
+ this._processorService = null;
77
+ this._graphService = null;
78
+ this._databaseService = null;
79
+ this._brokersService = null;
80
+ this._storageService = null;
81
+ this._vectorService = null;
82
+ this._quotaService = null;
83
+ this._fallbackService = null;
84
+ this.productId = null;
85
+ /** Pending signals waiting to be resolved */
86
+ this.pendingSignals = new Map();
87
+ /** Signal polling interval (ms) */
88
+ this.signalPollInterval = 2000;
89
+ /** Signal polling timer */
90
+ this.signalPollTimer = null;
91
+ /** Session log fields for logging */
92
+ this.sessionLogFields = {};
93
+ /** LogService instance for logging operations */
94
+ this.logService = null;
95
+ /** Base log fields inherited by all step logs */
96
+ this.baseLogs = null;
97
+ /** Set when a graph step runs; we disconnect once at end of execute() instead of per-step. */
98
+ this.graphConnectionUsed = false;
99
+ /** Pre-fetched bootstrap data per step tag (feature batch prefetch). */
100
+ this.stepBootstrapCache = new Map();
101
+ debugLog('[FeatureExecutor] constructor ENTRY', {
102
+ feature_tag: feature.tag,
103
+ product: options.product,
104
+ env: options.env,
105
+ hasSessionFields: !!sessionLogFields && Object.keys(sessionLogFields).length > 0,
106
+ });
107
+ this.config = config;
108
+ this.feature = feature;
109
+ this.sessionLogFields = sessionLogFields || {};
110
+ this._privateKey = private_key || '';
111
+ this.preInitializedProduct = Boolean(preInitializedBuilder);
112
+ // Use pre-initialized builder from FeatureService when provided to avoid duplicate initProduct API call
113
+ this.productBuilder = preInitializedBuilder !== null && preInitializedBuilder !== void 0 ? preInitializedBuilder : new products_service_1.default({
114
+ workspace_id: config.workspace_id,
115
+ public_key: config.public_key,
116
+ user_id: config.user_id,
117
+ token: config.token,
118
+ env_type: config.env_type,
119
+ access_key: config.access_key,
120
+ workspace_private_key: this._privateKey || undefined,
121
+ });
122
+ // Lightweight API services (created up front)
123
+ this.processorApiService = new processorApi_service_1.ProcessorApiService(config.env_type);
124
+ this.featureApiService = new featureApi_service_1.FeatureApiService(config.env_type);
125
+ // Heavy services are lazy-initialized on first use (getProcessorService, getGraphService, etc.)
126
+ // Initialize feature state
127
+ this.state = {
128
+ feature_id: (0, crypto_1.randomUUID)(),
129
+ feature_tag: feature.tag,
130
+ product: options.product,
131
+ env: options.env,
132
+ input: options.input,
133
+ steps: {},
134
+ completed_steps: [],
135
+ step_timings: [],
136
+ state: {},
137
+ status: types_1.FeatureStatus.PENDING,
138
+ started_at: Date.now(),
139
+ };
140
+ debugLog('[FeatureExecutor] constructor DONE', {
141
+ feature_id: this.state.feature_id,
142
+ feature_tag: this.state.feature_tag,
143
+ steps_count: (_b = (_a = feature.steps) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0,
144
+ });
145
+ }
146
+ /**
147
+ * Get auth payload for API calls
148
+ */
149
+ getAuthPayload() {
150
+ return {
151
+ user_id: this.config.user_id,
152
+ workspace_id: this.config.workspace_id,
153
+ public_key: this.config.public_key,
154
+ token: this.config.token,
155
+ };
156
+ }
157
+ /** Product private key for processor result encryption; must always match backend decryption key. No fallback to workspace key. */
158
+ getProcessorResultEncryptionKey() {
159
+ var _a, _b, _c;
160
+ return (_c = (_b = (_a = this.productBuilder) === null || _a === void 0 ? void 0 : _a.fetchPrivateKey) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : '';
161
+ }
162
+ /** Lazy-initialized processor (shares productBuilder to avoid duplicate product init per step). */
163
+ getProcessorService() {
164
+ if (!this._processorService) {
165
+ this._processorService = new processor_service_1.default({
166
+ workspace_id: this.config.workspace_id,
167
+ public_key: this.config.public_key,
168
+ user_id: this.config.user_id,
169
+ token: this.config.token,
170
+ env_type: this.config.env_type,
171
+ private_key: this._privateKey,
172
+ access_key: this.config.access_key,
173
+ preInitializedProductBuilder: this.productBuilder,
174
+ });
175
+ }
176
+ return this._processorService;
177
+ }
178
+ /** Lazy-initialized graph service. */
179
+ getGraphService() {
180
+ if (!this._graphService) {
181
+ this._graphService = new graph_1.GraphService({
182
+ workspace_id: this.config.workspace_id,
183
+ public_key: this.config.public_key,
184
+ user_id: this.config.user_id,
185
+ token: this.config.token,
186
+ env_type: this.config.env_type,
187
+ private_key: this._privateKey,
188
+ access_key: this.config.access_key,
189
+ });
190
+ }
191
+ return this._graphService;
192
+ }
193
+ /** Lazy-initialized database service. */
194
+ getDatabaseService() {
195
+ if (!this._databaseService) {
196
+ this._databaseService = new database_1.DatabaseService({
197
+ workspace_id: this.config.workspace_id,
198
+ public_key: this.config.public_key,
199
+ user_id: this.config.user_id,
200
+ token: this.config.token,
201
+ env_type: this.config.env_type,
202
+ private_key: this._privateKey,
203
+ access_key: this.config.access_key,
204
+ });
205
+ }
206
+ return this._databaseService;
207
+ }
208
+ /** Lazy-initialized brokers service. Used for produce steps so ctx.messaging.produce uses the same path as ductape.messaging.produce (pool, tracking, cache). */
209
+ getBrokersService() {
210
+ if (!this._brokersService) {
211
+ this._brokersService = new brokers_1.BrokersService({
212
+ workspace_id: this.config.workspace_id,
213
+ public_key: this.config.public_key,
214
+ user_id: this.config.user_id,
215
+ token: this.config.token,
216
+ env_type: this.config.env_type,
217
+ access_key: this.config.access_key,
218
+ redis_client: this.config.redis_client,
219
+ preInitializedProductBuilder: this.productBuilder,
220
+ });
221
+ }
222
+ return this._brokersService;
223
+ }
224
+ /** Lazy-initialized storage service. Used for storage steps (upload/download/delete) instead of processor. */
225
+ getStorageService() {
226
+ if (!this._storageService) {
227
+ this._storageService = new storage_1.StorageService({
228
+ workspace_id: this.config.workspace_id,
229
+ public_key: this.config.public_key,
230
+ user_id: this.config.user_id,
231
+ token: this.config.token,
232
+ env_type: this.config.env_type,
233
+ redis_client: this.config.redis_client,
234
+ private_key: this._privateKey,
235
+ });
236
+ }
237
+ return this._storageService;
238
+ }
239
+ /** Lazy-initialized vector service. Used for vector steps (query/upsert/delete/execute action). */
240
+ getVectorService() {
241
+ if (!this._vectorService) {
242
+ this._vectorService = new vector_1.VectorDatabaseService({
243
+ workspace_id: this.config.workspace_id,
244
+ public_key: this.config.public_key,
245
+ user_id: this.config.user_id,
246
+ token: this.config.token,
247
+ env_type: this.config.env_type,
248
+ redis_client: this.config.redis_client,
249
+ private_key: this._privateKey,
250
+ access_key: this.config.access_key,
251
+ });
252
+ }
253
+ return this._vectorService;
254
+ }
255
+ /** Lazy-initialized quota service. */
256
+ getQuotaService() {
257
+ if (!this._quotaService) {
258
+ this._quotaService = new resilience_1.QuotaService({
259
+ workspace_id: this.config.workspace_id,
260
+ public_key: this.config.public_key,
261
+ user_id: this.config.user_id,
262
+ token: this.config.token,
263
+ env_type: this.config.env_type,
264
+ private_key: this._privateKey,
265
+ });
266
+ }
267
+ return this._quotaService;
268
+ }
269
+ /** Lazy-initialized fallback service. */
270
+ getFallbackService() {
271
+ if (!this._fallbackService) {
272
+ this._fallbackService = new resilience_1.FallbackService({
273
+ workspace_id: this.config.workspace_id,
274
+ public_key: this.config.public_key,
275
+ user_id: this.config.user_id,
276
+ token: this.config.token,
277
+ env_type: this.config.env_type,
278
+ private_key: this._privateKey,
279
+ });
280
+ }
281
+ return this._fallbackService;
282
+ }
283
+ /**
284
+ * Persist feature execution result to backend
285
+ */
286
+ async persistExecutionResult(status, output, error) {
287
+ debugLog('[FeatureExecutor] persistExecutionResult ENTRY', {
288
+ feature_id: this.state.feature_id,
289
+ feature_tag: this.state.feature_tag,
290
+ status,
291
+ hasError: !!error,
292
+ });
293
+ try {
294
+ const resultData = {
295
+ feature_tag: this.state.feature_tag,
296
+ input: this.state.input,
297
+ output,
298
+ completed_steps: this.state.completed_steps,
299
+ failed_step: this.state.failed_step,
300
+ step_outputs: this.state.steps,
301
+ error,
302
+ };
303
+ const inputPayload = { product: this.state.product, env: this.state.env, event: this.state.feature_tag, input: this.state.input };
304
+ const resultStr = JSON.stringify(resultData);
305
+ const inputStr = JSON.stringify(inputPayload);
306
+ const encKey = this.getProcessorResultEncryptionKey();
307
+ const processorResult = {
308
+ process_id: this.state.feature_id,
309
+ product_id: this.productId || null,
310
+ env: this.state.env,
311
+ component: types_1.LogEventTypes.FEATURE,
312
+ status,
313
+ start: this.state.started_at,
314
+ end: Date.now(),
315
+ retryable: true,
316
+ result: encKey ? (0, processor_utils_1.encrypt)(resultStr, encKey) : resultStr,
317
+ input: encKey ? (0, processor_utils_1.encrypt)(inputStr, encKey) : inputStr,
318
+ feature_id: this.state.feature_id,
319
+ feature_tag: this.state.feature_tag,
320
+ product_tag: this.state.product,
321
+ workspace_id: this.config.workspace_id,
322
+ };
323
+ await this.processorApiService.saveResult(processorResult, this.getAuthPayload());
324
+ debugLog('[FeatureExecutor] persistExecutionResult SUCCESS', { feature_id: this.state.feature_id, status });
325
+ }
326
+ catch (err) {
327
+ // Log error but don't throw - persistence failure shouldn't fail the feature
328
+ debugLog('[FeatureExecutor] persistExecutionResult FAILED', { feature_id: this.state.feature_id, status, error: err });
329
+ }
330
+ }
331
+ /**
332
+ * Step processor results must always use component: 'feature_step'.
333
+ * step_type is the step kind: action, notification, storage, produce, database_action, graph, vector, quota, fallback, child_workflow, sleep, wait_for_signal, checkpoint, feature.
334
+ */
335
+ getStepTypeForResult(step) {
336
+ var _a;
337
+ if (step.type === types_1.FeatureStepType.PUBLISH)
338
+ return 'produce';
339
+ return (_a = step.type) !== null && _a !== void 0 ? _a : 'action';
340
+ }
341
+ /**
342
+ * Persist individual step execution to backend (input/output per step in processor result).
343
+ * Processor results for steps always have component: 'feature_step' and step_type set to the step kind.
344
+ */
345
+ async persistStepResult(step, status, output, error, duration, input) {
346
+ const resolvedInput = input !== null && input !== void 0 ? input : step.input;
347
+ const stepType = this.getStepTypeForResult(step);
348
+ debugLog('[FeatureExecutor] persistStepResult ENTRY', {
349
+ feature_id: this.state.feature_id,
350
+ step_tag: step.tag,
351
+ step_type: stepType,
352
+ status,
353
+ durationMs: duration,
354
+ });
355
+ try {
356
+ // Caller passes the step return value (resolved shape) on success; we persist it as-is
357
+ const stepProcessId = `${this.state.feature_id}:${step.tag}`;
358
+ const value = status === types_1.LogEventStatus.SUCCESS
359
+ ? output !== undefined && output !== null
360
+ ? output
361
+ : {}
362
+ : { error: error !== null && error !== void 0 ? error : 'Step failed' };
363
+ const resultStr = JSON.stringify(value);
364
+ const inputStr = JSON.stringify(resolvedInput);
365
+ const encKey = this.getProcessorResultEncryptionKey();
366
+ const stepResult = {
367
+ process_id: stepProcessId,
368
+ product_id: this.productId || null,
369
+ env: this.state.env,
370
+ component: types_1.LogEventTypes.FEATURE_STEP, // step results always feature_step
371
+ status,
372
+ start: Date.now() - (duration || 0),
373
+ end: Date.now(),
374
+ retryable: true,
375
+ result: encKey ? (0, processor_utils_1.encrypt)(resultStr, encKey) : resultStr,
376
+ input: encKey ? (0, processor_utils_1.encrypt)(inputStr, encKey) : inputStr,
377
+ feature_id: this.state.feature_id,
378
+ feature_tag: this.state.feature_tag,
379
+ product_tag: this.state.product,
380
+ workspace_id: this.config.workspace_id,
381
+ step_tag: step.tag,
382
+ step_type: stepType, // action | notification | storage | produce | database_action | graph | vector | quota | fallback | child_feature | sleep | wait_for_signal | checkpoint
383
+ step_error: error,
384
+ step_duration_ms: duration,
385
+ };
386
+ this.processorApiService.saveResult(stepResult, this.getAuthPayload()).then(() => debugLog('[FeatureExecutor] persistStepResult SUCCESS', { feature_id: this.state.feature_id, step_tag: step.tag }), (err) => debugLog('[FeatureExecutor] persistStepResult FAILED', { feature_id: this.state.feature_id, step_tag: step.tag, error: err }));
387
+ }
388
+ catch (err) {
389
+ debugLog('[FeatureExecutor] persistStepResult ENTRY FAILED', { feature_id: this.state.feature_id, step_tag: step.tag, error: err });
390
+ }
391
+ }
392
+ /**
393
+ * Initialize logging service and base log fields
394
+ */
395
+ initializeLogging() {
396
+ var _a;
397
+ if (!this.logService && this.productId) {
398
+ this.logService = new logs_service_1.default({
399
+ product_id: this.productId,
400
+ workspace_id: this.config.workspace_id,
401
+ public_key: this.config.public_key,
402
+ user_id: this.config.user_id,
403
+ token: this.config.token,
404
+ env_type: this.config.env_type,
405
+ workspace_private_key: (_a = this._privateKey) !== null && _a !== void 0 ? _a : undefined,
406
+ });
407
+ // Set up base logs that all step logs inherit
408
+ this.baseLogs = Object.assign({ product_tag: this.state.product, workspace_id: this.config.workspace_id, env: this.state.env, process_id: this.state.feature_id, type: logs_types_1.LogEventTypes.FEATURE, parent_tag: this.state.feature_tag }, this.sessionLogFields);
409
+ }
410
+ }
411
+ /**
412
+ * Log a step execution event
413
+ */
414
+ logStepEvent(step, message, status, additionalData) {
415
+ if (!this.logService || !this.baseLogs)
416
+ return;
417
+ const stepTypeDisplay = step.type === types_1.FeatureStepType.PUBLISH ? 'produce' : step.type;
418
+ const stepProcessId = `${this.state.feature_id}:${step.tag}`;
419
+ this.logService.add(Object.assign(Object.assign({}, this.baseLogs), { process_id: stepProcessId, type: logs_types_1.LogEventTypes.FEATURE_STEP, child_tag: step.tag, message,
420
+ status, data: Object.assign({ feature: this.state.feature_tag, feature_id: this.state.feature_id, step: step.tag, step_type: stepTypeDisplay }, additionalData) }));
421
+ }
422
+ /**
423
+ * Execute the feature
424
+ */
425
+ async execute() {
426
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
427
+ const startTime = Date.now();
428
+ debugLog('[FeatureExecutor] execute ENTRY', {
429
+ feature_id: this.state.feature_id,
430
+ feature_tag: this.state.feature_tag,
431
+ product: this.state.product,
432
+ env: this.state.env,
433
+ });
434
+ try {
435
+ // Track phase so failures before first step are reported as (initialization)
436
+ this.state.current_step = '(initialization)';
437
+ // Skip product init when builder was pre-initialized by FeatureService (avoids duplicate initProduct API call)
438
+ if (this.preInitializedProduct) {
439
+ this.productId = this.productBuilder.fetchProductId();
440
+ }
441
+ else {
442
+ await this.productBuilder.initializeProductByTag(this.state.product);
443
+ this.productId = this.productBuilder.fetchProductId();
444
+ }
445
+ debugLog('[FeatureExecutor] execute product initialized', { feature_id: this.state.feature_id, product_id: this.productId });
446
+ // Initialize logging after we have product ID
447
+ this.initializeLogging();
448
+ this.state.status = types_1.FeatureStatus.RUNNING;
449
+ // Get steps in execution order (respecting dependencies)
450
+ const orderedSteps = this.getOrderedSteps();
451
+ stepLog('Ordered steps', {
452
+ count: orderedSteps.length,
453
+ steps: orderedSteps.map((s) => ({ tag: s.tag, type: s.type === types_1.FeatureStepType.PUBLISH ? 'produce' : s.type })),
454
+ });
455
+ debugLog('[FeatureExecutor] execute ordered steps', {
456
+ feature_id: this.state.feature_id,
457
+ steps_count: orderedSteps.length,
458
+ step_tags: orderedSteps.map((s) => s.tag),
459
+ });
460
+ // Pre-fetch all action/notification/storage bootstrap data in one backend call (cached for step executors)
461
+ try {
462
+ await this.prefetchStepBootstrap(orderedSteps);
463
+ }
464
+ catch (prefetchErr) {
465
+ stepLog('Bootstrap prefetch failed (continuing without prefetch)', {
466
+ error: (_a = prefetchErr === null || prefetchErr === void 0 ? void 0 : prefetchErr.message) !== null && _a !== void 0 ? _a : String(prefetchErr),
467
+ });
468
+ this.stepBootstrapCache.clear();
469
+ }
470
+ // Execute each step
471
+ for (const step of orderedSteps) {
472
+ // Check step condition if present
473
+ if (step.condition && !(await this.evaluateCondition(step.condition))) {
474
+ debugLog('[FeatureExecutor] execute step SKIPPED (condition)', { feature_id: this.state.feature_id, step_tag: step.tag });
475
+ this.logStepEvent(step, `Step ${step.tag} - skipped (condition not met)`, logs_types_1.LogEventStatus.SUCCESS, { skipped: true });
476
+ continue; // Skip step if condition not met
477
+ }
478
+ this.state.current_step = step.tag;
479
+ stepLog('Step start', { tag: step.tag, type: step.type === types_1.FeatureStepType.PUBLISH ? 'produce' : step.type });
480
+ debugLog('[FeatureExecutor] execute step START', { feature_id: this.state.feature_id, step_tag: step.tag, step_type: step.type });
481
+ // Log step start
482
+ this.logStepEvent(step, `Step ${step.tag} - started`, logs_types_1.LogEventStatus.PROCESSING);
483
+ const result = await this.executeStep(step);
484
+ stepLog('Step end', Object.assign({ tag: step.tag, type: step.type === types_1.FeatureStepType.PUBLISH ? 'produce' : step.type, success: result.success, duration_ms: result.duration }, (result.error && { error: result.error })));
485
+ if (result.error) {
486
+ console.warn(`[FeatureExecutor] Step "${step.tag}" error:`, result.error);
487
+ }
488
+ debugLog('[FeatureExecutor] execute step END', {
489
+ feature_id: this.state.feature_id,
490
+ step_tag: step.tag,
491
+ success: result.success,
492
+ durationMs: result.duration,
493
+ hasError: !!result.error,
494
+ input: result.input,
495
+ output: result.output,
496
+ });
497
+ // Log step completion
498
+ this.logStepEvent(step, result.success ? `Step ${step.tag} - completed` : `Step ${step.tag} - failed`, result.success ? logs_types_1.LogEventStatus.SUCCESS : logs_types_1.LogEventStatus.FAIL, { duration: result.duration, error: result.error });
499
+ // Step return value = resolved step.output schema with primitive result, or raw when no schema
500
+ const outputSchema = step.output;
501
+ const hasOutputSchema = outputSchema && typeof outputSchema === 'object' && !Array.isArray(outputSchema) && Object.keys(outputSchema).length > 0;
502
+ let stepReturnValue = result.output;
503
+ if (result.success && hasOutputSchema) {
504
+ try {
505
+ stepReturnValue = await this.resolveInput(outputSchema, result.output);
506
+ }
507
+ catch (_p) {
508
+ stepReturnValue = result.output;
509
+ }
510
+ }
511
+ else if (!result.success) {
512
+ stepReturnValue = undefined;
513
+ }
514
+ console.warn(`[FeatureExecutor] Step "${step.tag}" output:`, stepReturnValue);
515
+ // Persist step result to backend (use step return shape, not raw primitive)
516
+ await this.persistStepResult(step, result.success ? types_1.LogEventStatus.SUCCESS : types_1.LogEventStatus.FAIL, stepReturnValue, result.error, result.duration, result.input);
517
+ this.state.step_timings.push({
518
+ tag: step.tag,
519
+ duration_ms: result.duration,
520
+ success: result.success,
521
+ });
522
+ if (!result.success) {
523
+ this.state.failed_step = step.tag;
524
+ stepLog('Step failed', { tag: step.tag, error: result.error, allow_fail: (_b = step.options) === null || _b === void 0 ? void 0 : _b.allow_fail, optional: (_c = step.options) === null || _c === void 0 ? void 0 : _c.optional });
525
+ debugLog('[FeatureExecutor] execute step FAILED', {
526
+ feature_id: this.state.feature_id,
527
+ step_tag: step.tag,
528
+ error: result.error,
529
+ allow_fail: (_d = step.options) === null || _d === void 0 ? void 0 : _d.allow_fail,
530
+ optional: (_e = step.options) === null || _e === void 0 ? void 0 : _e.optional,
531
+ });
532
+ // Check if step allows failure
533
+ if ((_f = step.options) === null || _f === void 0 ? void 0 : _f.allow_fail) {
534
+ // Store error but continue
535
+ this.state.steps[step.tag] = { error: result.error };
536
+ continue;
537
+ }
538
+ // Check if step is optional
539
+ if ((_g = step.options) === null || _g === void 0 ? void 0 : _g.optional) {
540
+ // Don't trigger rollback, just continue
541
+ continue;
542
+ }
543
+ // Trigger rollback
544
+ this.state.status = types_1.FeatureStatus.ROLLING_BACK;
545
+ debugLog('[FeatureExecutor] execute ROLLBACK START', { feature_id: this.state.feature_id, failed_step: step.tag, reason: result.error });
546
+ const rollbackResult = await this.executeRollback(step.tag, result.error || 'Step failed');
547
+ debugLog('[FeatureExecutor] execute ROLLBACK END', {
548
+ feature_id: this.state.feature_id,
549
+ success: rollbackResult.success,
550
+ rolled_back_steps: rollbackResult.rolled_back_steps,
551
+ failed_rollbacks: (_j = (_h = rollbackResult.failed_steps) === null || _h === void 0 ? void 0 : _h.length) !== null && _j !== void 0 ? _j : 0,
552
+ });
553
+ // Persist failed feature result
554
+ await this.persistExecutionResult(types_1.LogEventStatus.FAIL, undefined, result.error);
555
+ stepLog('Run finished (rollback)', {
556
+ status: types_1.FeatureStatus.ROLLED_BACK,
557
+ failed_step: step.tag,
558
+ error: result.error,
559
+ completed_steps: this.state.completed_steps,
560
+ step_timings: this.state.step_timings,
561
+ });
562
+ await ((_k = this.logService) === null || _k === void 0 ? void 0 : _k.publish());
563
+ if (this.graphConnectionUsed) {
564
+ await this.getGraphService().disconnect().catch(() => { });
565
+ await this.getProcessorService().disconnectBrokerConnections().catch(() => { });
566
+ if (this._brokersService)
567
+ await this._brokersService.disconnectAll().catch(() => { });
568
+ await this.getProcessorService().disconnectMailTransporters().catch(() => { });
569
+ await this.getProcessorService().disconnectFirebaseApps().catch(() => { });
570
+ await this.getProcessorService().disconnectSmsClients().catch(() => { });
571
+ }
572
+ return {
573
+ status: types_1.FeatureStatus.ROLLED_BACK,
574
+ feature_id: this.state.feature_id,
575
+ error: result.error,
576
+ execution_time: Date.now() - startTime,
577
+ completed_steps: this.state.completed_steps,
578
+ step_timings: this.state.step_timings,
579
+ failed_step: step.tag,
580
+ rollback_info: {
581
+ triggered_by: step.tag,
582
+ reason: result.error || 'Step failed',
583
+ rolled_back_steps: rollbackResult.rolled_back_steps,
584
+ failed_rollbacks: rollbackResult.failed_steps,
585
+ },
586
+ };
587
+ }
588
+ // Store step return shape in state (same value we persisted)
589
+ this.state.steps[step.tag] = stepReturnValue;
590
+ this.state.completed_steps.push(step.tag);
591
+ }
592
+ this.state.status = types_1.FeatureStatus.COMPLETED;
593
+ this.state.ended_at = Date.now();
594
+ // Determine output - use last step output or aggregate (resolves $ operators)
595
+ const output = await this.determineWorkflowOutput();
596
+ stepLog('Run finished (completed)', {
597
+ status: types_1.FeatureStatus.COMPLETED,
598
+ completed_steps: this.state.completed_steps,
599
+ step_timings: this.state.step_timings,
600
+ execution_time_ms: Date.now() - startTime,
601
+ });
602
+ debugLog('[FeatureExecutor] execute COMPLETED', {
603
+ feature_id: this.state.feature_id,
604
+ feature_tag: this.state.feature_tag,
605
+ completed_steps: this.state.completed_steps.length,
606
+ execution_time_ms: Date.now() - startTime,
607
+ });
608
+ // Persist successful feature result
609
+ await this.persistExecutionResult(types_1.LogEventStatus.SUCCESS, output);
610
+ // Fire-and-forget log publish on success so we don't block on log upload
611
+ (_l = this.logService) === null || _l === void 0 ? void 0 : _l.publish().catch(() => { });
612
+ if (this.graphConnectionUsed) {
613
+ await this.getGraphService().disconnect().catch(() => { });
614
+ await this.getProcessorService().disconnectBrokerConnections().catch(() => { });
615
+ if (this._brokersService)
616
+ await this._brokersService.disconnectAll().catch(() => { });
617
+ await this.getProcessorService().disconnectMailTransporters().catch(() => { });
618
+ await this.getProcessorService().disconnectFirebaseApps().catch(() => { });
619
+ await this.getProcessorService().disconnectSmsClients().catch(() => { });
620
+ }
621
+ return {
622
+ status: types_1.FeatureStatus.COMPLETED,
623
+ feature_id: this.state.feature_id,
624
+ output: output,
625
+ execution_time: Date.now() - startTime,
626
+ completed_steps: this.state.completed_steps,
627
+ step_timings: this.state.step_timings,
628
+ };
629
+ }
630
+ catch (error) {
631
+ this.state.status = types_1.FeatureStatus.FAILED;
632
+ this.state.ended_at = Date.now();
633
+ const details = formatErrorDetails(error);
634
+ const failedAt = (_m = this.state.current_step) !== null && _m !== void 0 ? _m : '(unknown)';
635
+ debugLog('[FeatureExecutor] execute FAILED (exception)', Object.assign(Object.assign({ feature_id: this.state.feature_id, feature_tag: this.state.feature_tag, failed_at: failedAt, completed_steps: this.state.completed_steps, error: details.message }, (details.status != null && { http_status: details.status })), (details.responseBody != null && { response_body: details.responseBody })));
636
+ if (details.stack) {
637
+ debugLog('[FeatureExecutor] execute FAILED stack', details.stack);
638
+ }
639
+ // Persist failed feature result with clear failure reason
640
+ const errorSummary = details.status != null
641
+ ? `Failed at ${failedAt}: ${details.message} (HTTP ${details.status})`
642
+ : `Failed at ${failedAt}: ${details.message}`;
643
+ stepLog('Run finished (failed - exception)', Object.assign({ status: types_1.FeatureStatus.FAILED, failed_step: failedAt, error: errorSummary, completed_steps: this.state.completed_steps, step_timings: this.state.step_timings }, (details.reason && { reason: details.reason })));
644
+ await this.persistExecutionResult(types_1.LogEventStatus.FAIL, undefined, errorSummary);
645
+ await ((_o = this.logService) === null || _o === void 0 ? void 0 : _o.publish());
646
+ if (this.graphConnectionUsed) {
647
+ await this.getGraphService().disconnect().catch(() => { });
648
+ await this.getProcessorService().disconnectBrokerConnections().catch(() => { });
649
+ if (this._brokersService)
650
+ await this._brokersService.disconnectAll().catch(() => { });
651
+ await this.getProcessorService().disconnectMailTransporters().catch(() => { });
652
+ await this.getProcessorService().disconnectFirebaseApps().catch(() => { });
653
+ await this.getProcessorService().disconnectSmsClients().catch(() => { });
654
+ }
655
+ return {
656
+ status: types_1.FeatureStatus.FAILED,
657
+ feature_id: this.state.feature_id,
658
+ error: errorSummary,
659
+ execution_time: Date.now() - startTime,
660
+ completed_steps: this.state.completed_steps,
661
+ step_timings: this.state.step_timings,
662
+ failed_step: this.state.current_step,
663
+ };
664
+ }
665
+ }
666
+ /**
667
+ * Execute a single step
668
+ */
669
+ async executeStep(step) {
670
+ const startTime = Date.now();
671
+ debugLog('[FeatureExecutor] executeStep ENTRY', {
672
+ feature_id: this.state.feature_id,
673
+ step_tag: step.tag,
674
+ step_type: step.type,
675
+ });
676
+ let resolvedInput = {};
677
+ try {
678
+ // Resolve input with data references and all $ operators
679
+ resolvedInput = (await this.resolveInput(step.input || {}));
680
+ debugLog('[FeatureExecutor] executeStep input', {
681
+ feature_id: this.state.feature_id,
682
+ step_tag: step.tag,
683
+ input: resolvedInput,
684
+ });
685
+ let output;
686
+ // Route steps to dedicated services; only action and notification use ProcessorService.
687
+ switch (step.type) {
688
+ case types_1.FeatureStepType.ACTION:
689
+ output = await this.executeActionStep(step, resolvedInput);
690
+ break;
691
+ case types_1.FeatureStepType.DATABASE_ACTION:
692
+ output = await this.executeDatabaseStep(step, resolvedInput);
693
+ break;
694
+ case types_1.FeatureStepType.NOTIFICATION:
695
+ output = await this.executeNotificationStep(step, resolvedInput);
696
+ break;
697
+ case types_1.FeatureStepType.STORAGE:
698
+ output = await this.executeStorageStep(step, resolvedInput);
699
+ break;
700
+ case types_1.FeatureStepType.PUBLISH:
701
+ case types_1.FeatureStepType.PRODUCE:
702
+ output = await this.executeProduceStep(step, resolvedInput);
703
+ break;
704
+ case types_1.FeatureStepType.GRAPH:
705
+ output = await this.executeGraphStep(step, resolvedInput);
706
+ break;
707
+ case types_1.FeatureStepType.VECTOR:
708
+ output = await this.executeVectorStep(step, resolvedInput);
709
+ break;
710
+ case types_1.FeatureStepType.QUOTA:
711
+ output = await this.executeQuotaStep(step, resolvedInput);
712
+ break;
713
+ case types_1.FeatureStepType.FALLBACK:
714
+ output = await this.executeFallbackStep(step, resolvedInput);
715
+ break;
716
+ case types_1.FeatureStepType.CHILD_FEATURE:
717
+ output = await this.executeChildWorkflowStep(step, resolvedInput);
718
+ break;
719
+ case types_1.FeatureStepType.SLEEP:
720
+ await this.executeSleepStep(step, resolvedInput);
721
+ output = { slept: true };
722
+ break;
723
+ case types_1.FeatureStepType.WAIT_FOR_SIGNAL:
724
+ output = await this.executeWaitForSignalStep(step, resolvedInput);
725
+ break;
726
+ case types_1.FeatureStepType.CHECKPOINT:
727
+ await this.executeCheckpointStep(step, resolvedInput);
728
+ output = { checkpoint: step.tag };
729
+ break;
730
+ default:
731
+ stepLog('Step type not implemented (passthrough)', { tag: step.tag, type: step.type });
732
+ debugLog('[FeatureExecutor] executeStep unknown step type (passthrough)', { step_tag: step.tag, step_type: step.type });
733
+ output = resolvedInput;
734
+ }
735
+ // For action steps with no app: if step.output is a template with $ refs (e.g. $Sequence{main}{step-a}{value}-b),
736
+ // resolve it so downstream steps get the correct value (checkpoint / closure-style steps).
737
+ if (step.type === types_1.FeatureStepType.ACTION &&
738
+ !step.app &&
739
+ step.output &&
740
+ typeof step.output === 'object' &&
741
+ !Array.isArray(step.output) &&
742
+ this.hasOperatorRefsInOutput(step.output)) {
743
+ try {
744
+ output = (await this.resolveInput(step.output));
745
+ }
746
+ catch (_a) {
747
+ // keep output from executeActionStep if resolve fails
748
+ }
749
+ }
750
+ const duration = Date.now() - startTime;
751
+ debugLog('[FeatureExecutor] executeStep output', {
752
+ feature_id: this.state.feature_id,
753
+ step_tag: step.tag,
754
+ output,
755
+ durationMs: duration,
756
+ });
757
+ return {
758
+ success: true,
759
+ input: resolvedInput,
760
+ output,
761
+ duration,
762
+ };
763
+ }
764
+ catch (error) {
765
+ const duration = Date.now() - startTime;
766
+ const details = formatErrorDetails(error);
767
+ // Use API reason when present so we log why it failed (e.g. "Notification X not found") not just "Request failed with status code 500"
768
+ const stepErrorMsg = details.reason != null
769
+ ? (details.status != null ? `${details.reason} (HTTP ${details.status})` : details.reason)
770
+ : (details.status != null ? `${details.message} (HTTP ${details.status})` : details.message);
771
+ debugLog('[FeatureExecutor] executeStep FAILED', Object.assign(Object.assign({ feature_id: this.state.feature_id, step_tag: step.tag, step_type: step.type, durationMs: duration, input: resolvedInput, error: stepErrorMsg }, (details.status != null && { http_status: details.status })), (details.responseBody != null && { response_body: details.responseBody })));
772
+ return {
773
+ success: false,
774
+ input: resolvedInput,
775
+ error: stepErrorMsg,
776
+ duration,
777
+ };
778
+ }
779
+ }
780
+ /**
781
+ * Execute an action step
782
+ */
783
+ async executeActionStep(step, input) {
784
+ var _a, _b;
785
+ debugLog('[FeatureExecutor] executeActionStep', { feature_id: this.state.feature_id, step_tag: step.tag, app: step.app });
786
+ // Step that only returns (no component call): no app → passthrough, output = resolved input
787
+ if (!step.app) {
788
+ return input;
789
+ }
790
+ return this.getProcessorService().processAction({
791
+ product: this.state.product,
792
+ env: this.state.env,
793
+ app: step.app,
794
+ action: step.event,
795
+ input: input,
796
+ retries: ((_a = step.options) === null || _a === void 0 ? void 0 : _a.retries) || 0,
797
+ cache: (_b = step.options) === null || _b === void 0 ? void 0 : _b.cache,
798
+ preloadedBootstrap: this.stepBootstrapCache.get(step.tag),
799
+ });
800
+ }
801
+ /**
802
+ * Execute a database step: use query/insert/update/delete/upsert for DB operations, execute() for named actions.
803
+ */
804
+ async executeDatabaseStep(step, input) {
805
+ const database = step.database;
806
+ const product = this.state.product;
807
+ const env = this.state.env;
808
+ const event = (step.event || '').toLowerCase();
809
+ const db = this.getDatabaseService();
810
+ const opts = Object.assign({ product, env, database }, input);
811
+ debugLog('[FeatureExecutor] executeDatabaseStep', {
812
+ feature_id: this.state.feature_id,
813
+ step_tag: step.tag,
814
+ database,
815
+ event,
816
+ });
817
+ switch (event) {
818
+ case 'query':
819
+ return db.query(opts);
820
+ case 'insert':
821
+ return db.insert(opts);
822
+ case 'update':
823
+ return db.update(opts);
824
+ case 'delete':
825
+ return db.delete(opts);
826
+ case 'upsert':
827
+ return db.upsert(opts);
828
+ default:
829
+ return db.execute({
830
+ product,
831
+ env,
832
+ database,
833
+ action: step.event,
834
+ input: input,
835
+ });
836
+ }
837
+ }
838
+ /**
839
+ * Execute a notification step
840
+ */
841
+ async executeNotificationStep(step, input) {
842
+ var _a;
843
+ const event = `${step.notification}:${step.event}`;
844
+ debugLog('[FeatureExecutor] executeNotificationStep', { feature_id: this.state.feature_id, step_tag: step.tag, event });
845
+ return this.getProcessorService().processNotification({
846
+ product: this.state.product,
847
+ env: this.state.env,
848
+ event,
849
+ input: input,
850
+ retries: ((_a = step.options) === null || _a === void 0 ? void 0 : _a.retries) || 0,
851
+ preloadedBootstrap: this.stepBootstrapCache.get(step.tag),
852
+ });
853
+ }
854
+ /**
855
+ * Execute a storage step using StorageService (upload, download, or delete by input shape).
856
+ */
857
+ async executeStorageStep(step, input) {
858
+ var _a, _b;
859
+ const product = this.state.product;
860
+ const env = this.state.env;
861
+ const storage = step.storage;
862
+ const opts = { product, env, storage };
863
+ debugLog('[FeatureExecutor] executeStorageStep', { feature_id: this.state.feature_id, step_tag: step.tag, storage, event: step.event });
864
+ const inp = input;
865
+ if (inp.buffer != null && inp.fileName != null) {
866
+ return this.getStorageService().upload(Object.assign(Object.assign({}, opts), { fileName: String(inp.fileName), buffer: inp.buffer, mimeType: inp.mimeType != null ? String(inp.mimeType) : undefined }));
867
+ }
868
+ if (inp.file_key != null) {
869
+ return this.getStorageService().delete(Object.assign(Object.assign({}, opts), { fileName: String(inp.file_key) }));
870
+ }
871
+ return this.getStorageService().download(Object.assign(Object.assign({}, opts), { fileName: String((_b = (_a = inp.fileName) !== null && _a !== void 0 ? _a : inp.file_key) !== null && _b !== void 0 ? _b : '') }));
872
+ }
873
+ /**
874
+ * Execute a produce step (message broker produce to topic).
875
+ * Uses BrokersService so ctx.messaging.produce has the same behavior as ductape.messaging.produce (pool, tracking, cache).
876
+ */
877
+ async executeProduceStep(step, input) {
878
+ const event = `${step.broker}:${step.event}`;
879
+ const message = (input && input.message != null) ? input.message : (input || {});
880
+ debugLog('[FeatureExecutor] executeProduceStep', { feature_id: this.state.feature_id, step_tag: step.tag, event });
881
+ return this.getBrokersService().publish({
882
+ product: this.state.product,
883
+ env: this.state.env,
884
+ event,
885
+ message: message,
886
+ });
887
+ }
888
+ /**
889
+ * Execute a graph step
890
+ * Supports: execute (custom action), createNode, updateNode, deleteNode,
891
+ * createRelationship, deleteRelationship, query
892
+ */
893
+ async executeGraphStep(step, input) {
894
+ const graphTag = step.graph;
895
+ const action = step.event;
896
+ debugLog('[FeatureExecutor] executeGraphStep', { feature_id: this.state.feature_id, step_tag: step.tag, graph: graphTag, action });
897
+ this.graphConnectionUsed = true;
898
+ await this.getGraphService().connect({
899
+ env: this.state.env,
900
+ product: this.state.product,
901
+ graph: graphTag,
902
+ });
903
+ try {
904
+ // Handle different graph operations based on action
905
+ switch (action) {
906
+ case 'createNode': {
907
+ const result = await this.getGraphService().createNode({
908
+ labels: input.labels,
909
+ properties: input.properties,
910
+ });
911
+ return result;
912
+ }
913
+ case 'updateNode': {
914
+ const result = await this.getGraphService().updateNode({
915
+ id: input.id,
916
+ properties: input.properties,
917
+ });
918
+ return result;
919
+ }
920
+ case 'deleteNode': {
921
+ const result = await this.getGraphService().deleteNode({
922
+ id: input.id,
923
+ detach: input.detach,
924
+ });
925
+ return result;
926
+ }
927
+ case 'createRelationship': {
928
+ const result = await this.getGraphService().createRelationship({
929
+ startNodeId: input.startNodeId || input.from,
930
+ endNodeId: input.endNodeId || input.to,
931
+ type: input.type,
932
+ properties: input.properties,
933
+ });
934
+ return result;
935
+ }
936
+ case 'deleteRelationship': {
937
+ const result = await this.getGraphService().deleteRelationship({
938
+ id: input.id,
939
+ });
940
+ return result;
941
+ }
942
+ case 'query': {
943
+ const result = await this.getGraphService().query(input.query, input.params);
944
+ return result;
945
+ }
946
+ case 'findNodes': {
947
+ const result = await this.getGraphService().findNodes({
948
+ labels: input.labels,
949
+ where: input.where,
950
+ limit: input.limit,
951
+ skip: input.skip,
952
+ });
953
+ return result;
954
+ }
955
+ case 'traverse': {
956
+ const result = await this.getGraphService().traverse({
957
+ startNodeId: input.startNodeId,
958
+ relationshipTypes: input.relationshipTypes,
959
+ direction: input.direction,
960
+ maxDepth: input.maxDepth,
961
+ });
962
+ return result;
963
+ }
964
+ default: {
965
+ // Execute a custom graph action
966
+ const result = await this.getGraphService().execute({
967
+ product: this.state.product,
968
+ env: this.state.env,
969
+ graph: graphTag,
970
+ action,
971
+ input,
972
+ });
973
+ return result;
974
+ }
975
+ }
976
+ }
977
+ finally {
978
+ // Disconnect is done once at end of execute() when graphConnectionUsed (see 2.8 optimization)
979
+ }
980
+ }
981
+ /**
982
+ * Execute a vector step (query, upsert, deleteVectors, or custom action via execute).
983
+ */
984
+ async executeVectorStep(step, input) {
985
+ var _a;
986
+ const vectorTag = step.vector;
987
+ if (!vectorTag) {
988
+ throw new Error(`Vector step "${step.tag}" has no vector tag`);
989
+ }
990
+ const product = this.state.product;
991
+ const env = this.state.env;
992
+ const event = (step.event || '').toLowerCase();
993
+ const svc = this.getVectorService();
994
+ debugLog('[FeatureExecutor] executeVectorStep', {
995
+ feature_id: this.state.feature_id,
996
+ step_tag: step.tag,
997
+ vector: vectorTag,
998
+ event,
999
+ });
1000
+ switch (event) {
1001
+ case 'query': {
1002
+ const result = await svc.query(Object.assign({ product,
1003
+ env, tag: vectorTag }, input));
1004
+ return result;
1005
+ }
1006
+ case 'upsert': {
1007
+ const result = await svc.upsert(Object.assign({ product,
1008
+ env, tag: vectorTag }, input));
1009
+ return result;
1010
+ }
1011
+ case 'delete':
1012
+ case 'deletevectors': {
1013
+ const result = await svc.deleteVectors(Object.assign({ product,
1014
+ env, tag: vectorTag }, input));
1015
+ return result;
1016
+ }
1017
+ default: {
1018
+ // Custom vector action: resolve template then execute (resolve returns options for query/upsert/delete)
1019
+ const resolved = await svc.actions.resolve({
1020
+ product,
1021
+ env,
1022
+ vector: vectorTag,
1023
+ action: step.event,
1024
+ input: input,
1025
+ });
1026
+ const op = (_a = resolved === null || resolved === void 0 ? void 0 : resolved._operation) !== null && _a !== void 0 ? _a : resolved === null || resolved === void 0 ? void 0 : resolved.operation;
1027
+ const opts = Object.assign({ product, env, tag: vectorTag }, resolved);
1028
+ if (op === 'query' || !op) {
1029
+ return svc.query(opts);
1030
+ }
1031
+ if (op === 'upsert') {
1032
+ return svc.upsert(opts);
1033
+ }
1034
+ if (op === 'delete' || op === 'deleteVectors') {
1035
+ return svc.deleteVectors(opts);
1036
+ }
1037
+ return svc.query(opts);
1038
+ }
1039
+ }
1040
+ }
1041
+ /**
1042
+ * Execute a quota step
1043
+ */
1044
+ async executeQuotaStep(step, input) {
1045
+ debugLog('[FeatureExecutor] executeQuotaStep', { feature_id: this.state.feature_id, step_tag: step.tag, quota: step.quota });
1046
+ return this.getQuotaService().run({
1047
+ product: this.state.product,
1048
+ env: this.state.env,
1049
+ tag: step.quota,
1050
+ input,
1051
+ });
1052
+ }
1053
+ /**
1054
+ * Execute a fallback step
1055
+ */
1056
+ async executeFallbackStep(step, input) {
1057
+ debugLog('[FeatureExecutor] executeFallbackStep', { feature_id: this.state.feature_id, step_tag: step.tag, fallback: step.fallback });
1058
+ return this.getFallbackService().run({
1059
+ product: this.state.product,
1060
+ env: this.state.env,
1061
+ tag: step.fallback,
1062
+ input,
1063
+ });
1064
+ }
1065
+ /**
1066
+ * Execute a child feature step
1067
+ */
1068
+ async executeChildWorkflowStep(step, input) {
1069
+ let childTag = step.feature;
1070
+ if (childTag == null) {
1071
+ const fromResolved = input === null || input === void 0 ? void 0 : input.feature;
1072
+ const fromStepInput = step.input && typeof step.input === 'object' ? step.input.feature : undefined;
1073
+ childTag = typeof fromResolved === 'string' ? fromResolved : typeof fromStepInput === 'string' ? fromStepInput : undefined;
1074
+ }
1075
+ debugLog('[FeatureExecutor] executeChildWorkflowStep', { feature_id: this.state.feature_id, step_tag: step.tag, child_feature: childTag });
1076
+ if (!childTag) {
1077
+ throw new Error('Child feature step has no feature tag (missing feature or step.input.feature)');
1078
+ }
1079
+ const childFeature = await this.productBuilder.fetchFeature(childTag);
1080
+ if (!childFeature) {
1081
+ debugLog('[FeatureExecutor] executeChildWorkflowStep child not found', { step_tag: step.tag, child_feature: childTag });
1082
+ throw new Error(`Child feature ${childTag} not found`);
1083
+ }
1084
+ const childExecutor = new FeatureExecutor(this.config, childFeature, {
1085
+ product: this.state.product,
1086
+ env: this.state.env,
1087
+ tag: childTag,
1088
+ input,
1089
+ }, this._privateKey);
1090
+ const result = await childExecutor.execute();
1091
+ debugLog('[FeatureExecutor] executeChildWorkflowStep result', {
1092
+ feature_id: this.state.feature_id,
1093
+ step_tag: step.tag,
1094
+ child_status: result.status,
1095
+ });
1096
+ if (result.status !== types_1.FeatureStatus.COMPLETED) {
1097
+ throw new Error(`Child feature failed: ${result.error}`);
1098
+ }
1099
+ return result.output;
1100
+ }
1101
+ /**
1102
+ * Execute a sleep step
1103
+ */
1104
+ async executeSleepStep(step, input) {
1105
+ var _a;
1106
+ const duration = input.duration || ((_a = step.options) === null || _a === void 0 ? void 0 : _a.timeout) || 1000;
1107
+ debugLog('[FeatureExecutor] executeSleepStep', { feature_id: this.state.feature_id, step_tag: step.tag, durationMs: duration });
1108
+ await this.sleep(duration);
1109
+ }
1110
+ /**
1111
+ * Execute a wait for signal step
1112
+ * Polls the backend API for signal delivery until received or timeout
1113
+ */
1114
+ async executeWaitForSignalStep(step, input) {
1115
+ var _a;
1116
+ const signalName = step.event;
1117
+ const signals = input.signals || [signalName];
1118
+ const timeout = ((_a = step.options) === null || _a === void 0 ? void 0 : _a.timeout) || 300000; // 5 min default
1119
+ const startTime = Date.now();
1120
+ debugLog('[FeatureExecutor] executeWaitForSignalStep', {
1121
+ feature_id: this.state.feature_id,
1122
+ step_tag: step.tag,
1123
+ signals,
1124
+ timeoutMs: timeout,
1125
+ });
1126
+ // Store that we're waiting for these signals in state
1127
+ this.state.state[`waiting_for_signal:${step.tag}`] = {
1128
+ signals,
1129
+ started_at: startTime,
1130
+ timeout,
1131
+ };
1132
+ // Persist the waiting state to backend
1133
+ await this.persistExecutionResult(types_1.LogEventStatus.WAITING);
1134
+ return new Promise((resolve, reject) => {
1135
+ // Create pending signal entry
1136
+ const pendingSignal = {
1137
+ signal: signalName,
1138
+ resolve,
1139
+ reject,
1140
+ };
1141
+ // Store for each signal we're waiting for
1142
+ for (const sig of signals) {
1143
+ this.pendingSignals.set(sig, pendingSignal);
1144
+ }
1145
+ // Set up timeout
1146
+ if (timeout > 0) {
1147
+ pendingSignal.timeout = setTimeout(() => {
1148
+ // Clean up pending signals
1149
+ for (const sig of signals) {
1150
+ this.pendingSignals.delete(sig);
1151
+ }
1152
+ this.stopSignalPolling();
1153
+ // Update state
1154
+ delete this.state.state[`waiting_for_signal:${step.tag}`];
1155
+ this.state.state[`signal_timeout:${step.tag}`] = {
1156
+ signals,
1157
+ timed_out_at: Date.now(),
1158
+ };
1159
+ reject(new Error(`Signal wait timed out after ${timeout}ms waiting for: ${signals.join(', ')}`));
1160
+ }, timeout);
1161
+ }
1162
+ // Start polling for signals
1163
+ this.startSignalPolling(signals, step.tag);
1164
+ });
1165
+ }
1166
+ /**
1167
+ * Start polling the backend for signal delivery
1168
+ */
1169
+ startSignalPolling(signals, stepTag) {
1170
+ // Don't start multiple pollers
1171
+ if (this.signalPollTimer)
1172
+ return;
1173
+ debugLog('[FeatureExecutor] startSignalPolling', { feature_id: this.state.feature_id, stepTag, signals });
1174
+ const poll = async () => {
1175
+ var _a;
1176
+ try {
1177
+ // Get feature status from backend to check for received signals
1178
+ const status = await this.featureApiService.getStatus(this.state.feature_id, this.state.product, this.state.env, this.getAuthPayload());
1179
+ if (status) {
1180
+ // Check if any of the signals we're waiting for have been received
1181
+ const receivedSignals = ((_a = status.state) === null || _a === void 0 ? void 0 : _a.received_signals) || {};
1182
+ for (const signalName of signals) {
1183
+ const signalKey = `signal:${signalName}`;
1184
+ if (receivedSignals[signalKey]) {
1185
+ const signalData = receivedSignals[signalKey];
1186
+ const pendingSignal = this.pendingSignals.get(signalName);
1187
+ if (pendingSignal) {
1188
+ // Clear timeout
1189
+ if (pendingSignal.timeout) {
1190
+ clearTimeout(pendingSignal.timeout);
1191
+ }
1192
+ // Clean up
1193
+ for (const sig of signals) {
1194
+ this.pendingSignals.delete(sig);
1195
+ }
1196
+ this.stopSignalPolling();
1197
+ // Update state
1198
+ delete this.state.state[`waiting_for_signal:${stepTag}`];
1199
+ this.state.state[`signal_received:${stepTag}`] = {
1200
+ signal: signalName,
1201
+ payload: signalData,
1202
+ received_at: Date.now(),
1203
+ };
1204
+ // Resolve the promise
1205
+ pendingSignal.resolve({
1206
+ signal: signalName,
1207
+ payload: signalData,
1208
+ received_at: Date.now(),
1209
+ });
1210
+ return;
1211
+ }
1212
+ }
1213
+ }
1214
+ }
1215
+ }
1216
+ catch (error) {
1217
+ // Log but continue polling - transient errors shouldn't stop us
1218
+ debugLog('[FeatureExecutor] Signal polling error', { feature_id: this.state.feature_id, error });
1219
+ }
1220
+ // Schedule next poll if we still have pending signals
1221
+ if (this.pendingSignals.size > 0) {
1222
+ this.signalPollTimer = setTimeout(poll, this.signalPollInterval);
1223
+ }
1224
+ };
1225
+ // Start polling
1226
+ poll();
1227
+ }
1228
+ /**
1229
+ * Stop the signal polling timer
1230
+ */
1231
+ stopSignalPolling() {
1232
+ if (this.signalPollTimer) {
1233
+ clearTimeout(this.signalPollTimer);
1234
+ this.signalPollTimer = null;
1235
+ }
1236
+ }
1237
+ /**
1238
+ * Deliver a signal to this feature (called by external signal handler)
1239
+ */
1240
+ deliverSignal(signalName, payload) {
1241
+ debugLog('[FeatureExecutor] deliverSignal', { feature_id: this.state.feature_id, signalName, hasPayload: payload !== undefined });
1242
+ const pendingSignal = this.pendingSignals.get(signalName);
1243
+ if (!pendingSignal) {
1244
+ debugLog('[FeatureExecutor] deliverSignal no pending signal', { feature_id: this.state.feature_id, signalName });
1245
+ return false;
1246
+ }
1247
+ // Clear timeout
1248
+ if (pendingSignal.timeout) {
1249
+ clearTimeout(pendingSignal.timeout);
1250
+ }
1251
+ // Clean up all related pending signals
1252
+ // (in case we were waiting for multiple signals)
1253
+ for (const [key, value] of this.pendingSignals.entries()) {
1254
+ if (value === pendingSignal) {
1255
+ this.pendingSignals.delete(key);
1256
+ }
1257
+ }
1258
+ this.stopSignalPolling();
1259
+ // Resolve with the signal data
1260
+ pendingSignal.resolve({
1261
+ signal: signalName,
1262
+ payload,
1263
+ received_at: Date.now(),
1264
+ });
1265
+ return true;
1266
+ }
1267
+ /**
1268
+ * Execute a checkpoint step
1269
+ */
1270
+ async executeCheckpointStep(step, input) {
1271
+ debugLog('[FeatureExecutor] executeCheckpointStep', { feature_id: this.state.feature_id, step_tag: step.tag });
1272
+ // Store checkpoint data in feature state
1273
+ this.state.state[`checkpoint:${step.tag}`] = {
1274
+ timestamp: Date.now(),
1275
+ completed_steps: [...this.state.completed_steps],
1276
+ data: input,
1277
+ };
1278
+ }
1279
+ /**
1280
+ * Execute rollback for completed steps
1281
+ */
1282
+ async executeRollback(failedStep, reason) {
1283
+ var _a;
1284
+ const rolledBackSteps = [];
1285
+ const failedRollbacks = [];
1286
+ // Get rollback strategy
1287
+ const strategy = ((_a = this.feature.options) === null || _a === void 0 ? void 0 : _a.rollback_strategy) || types_1.FeatureRollbackStrategy.REVERSE_ALL;
1288
+ debugLog('[FeatureExecutor] executeRollback ENTRY', {
1289
+ feature_id: this.state.feature_id,
1290
+ failedStep,
1291
+ reason,
1292
+ strategy,
1293
+ completed_steps: this.state.completed_steps,
1294
+ });
1295
+ // Determine which steps to rollback
1296
+ let stepsToRollback = [];
1297
+ switch (strategy) {
1298
+ case types_1.FeatureRollbackStrategy.REVERSE_ALL:
1299
+ // Rollback all completed steps in reverse order
1300
+ stepsToRollback = this.state.completed_steps
1301
+ .slice()
1302
+ .reverse()
1303
+ .map((tag) => this.findStep(tag))
1304
+ .filter((step) => step !== undefined);
1305
+ break;
1306
+ case types_1.FeatureRollbackStrategy.REVERSE_CRITICAL:
1307
+ // Only rollback steps marked as critical
1308
+ stepsToRollback = this.state.completed_steps
1309
+ .slice()
1310
+ .reverse()
1311
+ .map((tag) => this.findStep(tag))
1312
+ .filter((step) => { var _a; return step !== undefined && ((_a = step.options) === null || _a === void 0 ? void 0 : _a.critical) === true; });
1313
+ break;
1314
+ case types_1.FeatureRollbackStrategy.NONE:
1315
+ // No rollback
1316
+ debugLog('[FeatureExecutor] executeRollback NONE strategy', { feature_id: this.state.feature_id });
1317
+ return { success: true, rolled_back_steps: [], reason };
1318
+ }
1319
+ debugLog('[FeatureExecutor] executeRollback steps to rollback', {
1320
+ feature_id: this.state.feature_id,
1321
+ count: stepsToRollback.length,
1322
+ tags: stepsToRollback.map((s) => s.tag),
1323
+ });
1324
+ // Execute rollback for each step
1325
+ for (const step of stepsToRollback) {
1326
+ if (!step.rollback) {
1327
+ debugLog('[FeatureExecutor] executeRollback step has no rollback', { step_tag: step.tag });
1328
+ continue; // No rollback defined for this step
1329
+ }
1330
+ try {
1331
+ const stepOutput = this.state.steps[step.tag];
1332
+ await this.executeRollbackHandler(step.rollback, stepOutput);
1333
+ rolledBackSteps.push(step.tag);
1334
+ debugLog('[FeatureExecutor] executeRollback step rolled back', { feature_id: this.state.feature_id, step_tag: step.tag });
1335
+ }
1336
+ catch (error) {
1337
+ debugLog('[FeatureExecutor] executeRollback step failed', { feature_id: this.state.feature_id, step_tag: step.tag, error: error === null || error === void 0 ? void 0 : error.message });
1338
+ if (!step.rollback.continue_on_failure) {
1339
+ failedRollbacks.push({ tag: step.tag, error: error.message });
1340
+ // Stop rollback if continue_on_failure is not set
1341
+ break;
1342
+ }
1343
+ failedRollbacks.push({ tag: step.tag, error: error.message });
1344
+ }
1345
+ }
1346
+ const rollbackResult = {
1347
+ success: failedRollbacks.length === 0,
1348
+ rolled_back_steps: rolledBackSteps,
1349
+ failed_steps: failedRollbacks.length > 0 ? failedRollbacks : undefined,
1350
+ reason,
1351
+ };
1352
+ debugLog('[FeatureExecutor] executeRollback DONE', {
1353
+ feature_id: this.state.feature_id,
1354
+ success: rollbackResult.success,
1355
+ rolled_back_count: rolledBackSteps.length,
1356
+ failed_count: failedRollbacks.length,
1357
+ });
1358
+ return rollbackResult;
1359
+ }
1360
+ /**
1361
+ * Execute a rollback handler
1362
+ */
1363
+ async executeRollbackHandler(rollback, stepOutput) {
1364
+ var _a, _b, _c, _d, _e, _f, _g;
1365
+ if (!rollback)
1366
+ return;
1367
+ debugLog('[FeatureExecutor] executeRollbackHandler', { feature_id: this.state.feature_id, rollback_type: rollback.type });
1368
+ // Resolve rollback input, including step output references
1369
+ const resolvedInput = await this.resolveInput(rollback.input || {}, stepOutput);
1370
+ switch (rollback.type) {
1371
+ case types_1.FeatureStepType.ACTION:
1372
+ await this.getProcessorService().processAction({
1373
+ product: this.state.product,
1374
+ env: this.state.env,
1375
+ app: rollback.app,
1376
+ action: rollback.event,
1377
+ input: resolvedInput,
1378
+ retries: (_a = rollback.retries) !== null && _a !== void 0 ? _a : 0,
1379
+ });
1380
+ break;
1381
+ case types_1.FeatureStepType.DATABASE_ACTION: {
1382
+ const db = this.getDatabaseService();
1383
+ const product = this.state.product;
1384
+ const env = this.state.env;
1385
+ const database = rollback.database;
1386
+ const event = ((_b = rollback.event) !== null && _b !== void 0 ? _b : '').toLowerCase();
1387
+ const opts = Object.assign({ product, env, database }, resolvedInput);
1388
+ if (event === 'query')
1389
+ await db.query(opts);
1390
+ else if (event === 'insert')
1391
+ await db.insert(opts);
1392
+ else if (event === 'update')
1393
+ await db.update(opts);
1394
+ else if (event === 'delete')
1395
+ await db.delete(opts);
1396
+ else if (event === 'upsert')
1397
+ await db.upsert(opts);
1398
+ else
1399
+ await db.execute({
1400
+ product,
1401
+ env,
1402
+ database,
1403
+ action: rollback.event,
1404
+ input: resolvedInput,
1405
+ });
1406
+ break;
1407
+ }
1408
+ case types_1.FeatureStepType.NOTIFICATION:
1409
+ const notifEvent = `${rollback.notification}:${rollback.event}`;
1410
+ await this.getProcessorService().processNotification({
1411
+ product: this.state.product,
1412
+ env: this.state.env,
1413
+ event: notifEvent,
1414
+ input: resolvedInput,
1415
+ retries: (_c = rollback.retries) !== null && _c !== void 0 ? _c : 0,
1416
+ });
1417
+ break;
1418
+ case types_1.FeatureStepType.STORAGE: {
1419
+ const product = this.state.product;
1420
+ const env = this.state.env;
1421
+ const storage = rollback.storage;
1422
+ const opts = { product, env, storage };
1423
+ const inp = resolvedInput;
1424
+ if (inp.buffer != null && inp.fileName != null) {
1425
+ await this.getStorageService().upload(Object.assign(Object.assign({}, opts), { fileName: String(inp.fileName), buffer: inp.buffer, mimeType: inp.mimeType != null ? String(inp.mimeType) : undefined }));
1426
+ }
1427
+ else if (inp.file_key != null) {
1428
+ await this.getStorageService().delete(Object.assign(Object.assign({}, opts), { fileName: String(inp.file_key) }));
1429
+ }
1430
+ else {
1431
+ await this.getStorageService().download(Object.assign(Object.assign({}, opts), { fileName: String((_e = (_d = inp.fileName) !== null && _d !== void 0 ? _d : inp.file_key) !== null && _e !== void 0 ? _e : '') }));
1432
+ }
1433
+ break;
1434
+ }
1435
+ case types_1.FeatureStepType.VECTOR: {
1436
+ const product = this.state.product;
1437
+ const env = this.state.env;
1438
+ const vectorTag = rollback.vector;
1439
+ const inp = resolvedInput;
1440
+ const event = ((_f = rollback.event) !== null && _f !== void 0 ? _f : '').toLowerCase();
1441
+ const svc = this.getVectorService();
1442
+ if (event === 'query') {
1443
+ await svc.query(Object.assign({ product, env, tag: vectorTag }, inp));
1444
+ }
1445
+ else if (event === 'upsert') {
1446
+ await svc.upsert(Object.assign({ product, env, tag: vectorTag }, inp));
1447
+ }
1448
+ else if (event === 'delete' || event === 'deletevectors') {
1449
+ await svc.deleteVectors(Object.assign({ product, env, tag: vectorTag }, inp));
1450
+ }
1451
+ else {
1452
+ const resolved = await svc.actions.resolve({
1453
+ product,
1454
+ env,
1455
+ vector: vectorTag,
1456
+ action: rollback.event,
1457
+ input: inp,
1458
+ });
1459
+ const op = (_g = resolved === null || resolved === void 0 ? void 0 : resolved._operation) !== null && _g !== void 0 ? _g : resolved === null || resolved === void 0 ? void 0 : resolved.operation;
1460
+ const opts = Object.assign({ product, env, tag: vectorTag }, resolved);
1461
+ if (op === 'upsert')
1462
+ await svc.upsert(opts);
1463
+ else if (op === 'delete' || op === 'deleteVectors')
1464
+ await svc.deleteVectors(opts);
1465
+ else
1466
+ await svc.query(opts);
1467
+ }
1468
+ break;
1469
+ }
1470
+ case types_1.FeatureStepType.PUBLISH:
1471
+ case types_1.FeatureStepType.PRODUCE:
1472
+ const pubEvent = `${rollback.broker}:${rollback.event}`;
1473
+ await this.getProcessorService().processMessageBrokerPublish({
1474
+ product: this.state.product,
1475
+ env: this.state.env,
1476
+ event: pubEvent,
1477
+ input: resolvedInput,
1478
+ });
1479
+ break;
1480
+ }
1481
+ }
1482
+ /**
1483
+ * Resolve input by replacing data references and all $ operators (per docs/operators).
1484
+ * Top-level keys are resolved in parallel for better performance.
1485
+ */
1486
+ async resolveInput(input, stepOutput) {
1487
+ const entries = Object.entries(input);
1488
+ const resolvedValues = await Promise.all(entries.map(([, value]) => this.resolveValue(value, stepOutput)));
1489
+ const resolved = {};
1490
+ entries.forEach(([key], i) => {
1491
+ resolved[key] = resolvedValues[i];
1492
+ });
1493
+ return resolved;
1494
+ }
1495
+ /**
1496
+ * Resolve a single value: refs ($Input{}, $Step{}, etc.) and operators ($Add, $Concat, etc.)
1497
+ */
1498
+ async resolveValue(value, stepOutput) {
1499
+ if (typeof value === 'string') {
1500
+ return this.resolveStringValue(value, stepOutput);
1501
+ }
1502
+ if (Array.isArray(value)) {
1503
+ return Promise.all(value.map((item) => this.resolveValue(item, stepOutput)));
1504
+ }
1505
+ if (value && typeof value === 'object') {
1506
+ return this.resolveInput(value, stepOutput);
1507
+ }
1508
+ return value;
1509
+ }
1510
+ /** Strip surrounding quotes from a string literal (e.g. '"foo"' -> 'foo') */
1511
+ unquote(part) {
1512
+ const t = part.trim();
1513
+ if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
1514
+ return t.slice(1, -1);
1515
+ }
1516
+ return t;
1517
+ }
1518
+ /**
1519
+ * Resolve string value: data references and all $ operators from docs/operators
1520
+ * Supports: $Input{}, $Step{}, $Sequence{}, $State{}, $StepOutput{}, $Now,
1521
+ * $Add, $Subtract, $Concat, $Substring, $Trim, $Split, $Pick, $Join,
1522
+ * $Uppercase, $Lowercase, $Dateformat, $Replace, $Filter, $Find, $Size, $Length
1523
+ */
1524
+ async resolveStringValue(value, stepOutput) {
1525
+ var _a;
1526
+ const v = value.trim();
1527
+ // --- Data references (exact match) ---
1528
+ if (v === '$Now' || v === '$Now()')
1529
+ return Date.now();
1530
+ const inputMatch = v.match(/^\$Input\{(.+)\}$/);
1531
+ if (inputMatch)
1532
+ return this.getNestedValue(this.state.input, inputMatch[1]);
1533
+ const sequenceMatch = v.match(/^\$Sequence\{main\}\{([^}]+)\}\{(.+)\}$/);
1534
+ if (sequenceMatch) {
1535
+ const stepData = this.state.steps[sequenceMatch[1]];
1536
+ if (stepData)
1537
+ return this.getNestedValue(stepData, sequenceMatch[2]);
1538
+ return undefined;
1539
+ }
1540
+ const stepMatch = v.match(/^\$Step\{(.+)\}\{(.+)\}$/);
1541
+ if (stepMatch) {
1542
+ const stepData = this.state.steps[stepMatch[1]];
1543
+ if (stepData)
1544
+ return this.getNestedValue(stepData, stepMatch[2]);
1545
+ return undefined;
1546
+ }
1547
+ const stateMatch = v.match(/^\$State\{(.+)\}$/);
1548
+ if (stateMatch)
1549
+ return this.state.state[stateMatch[1]];
1550
+ const outputMatch = v.match(/^\$StepOutput\{(.+)\}$/);
1551
+ if (outputMatch && stepOutput)
1552
+ return this.getNestedValue(stepOutput, outputMatch[1]);
1553
+ // --- $Size{ref} / $Length{ref} ---
1554
+ const sizeMatch = v.match(/^\$Size\{(.+)\}$/);
1555
+ if (sizeMatch) {
1556
+ const inner = await this.resolveValue(sizeMatch[1].trim(), stepOutput);
1557
+ if (inner != null && typeof inner === 'object' && !Array.isArray(inner))
1558
+ return Object.keys(inner).length;
1559
+ return 0;
1560
+ }
1561
+ const lengthMatch = v.match(/^\$Length\{(.+)\}$/);
1562
+ if (lengthMatch) {
1563
+ const inner = await this.resolveValue(lengthMatch[1].trim(), stepOutput);
1564
+ if (Array.isArray(inner))
1565
+ return inner.length;
1566
+ if (typeof inner === 'string')
1567
+ return inner.length;
1568
+ return 0;
1569
+ }
1570
+ // --- Operators (whole-string forms) ---
1571
+ // $Add(...) or $Add{...}
1572
+ const addMatch = v.match(/^\$Add\(([^)]*)\)$/) || v.match(/^\$Add\{([^}]*)\}$/);
1573
+ if (addMatch) {
1574
+ const parts = addMatch[1].split(',').map((s) => this.unquote(s.trim()));
1575
+ const nums = await Promise.all(parts.map((p) => this.resolveValue(p, stepOutput)));
1576
+ return nums.reduce((sum, n) => sum + Number(n), 0);
1577
+ }
1578
+ // $Subtract(...) / $Substract(...) (typo) and $Subtract{...}
1579
+ const subMatch = v.match(/^\$Subtract?\(([^)]*)\)$/) || v.match(/^\$Subtract?\{([^}]*)\}$/);
1580
+ if (subMatch) {
1581
+ const parts = subMatch[1].split(',').map((s) => this.unquote(s.trim()));
1582
+ const nums = await Promise.all(parts.map((p) => this.resolveValue(p, stepOutput)));
1583
+ const n = nums.map((x) => Number(x));
1584
+ return n.length ? n.reduce((a, b) => a - b) : 0;
1585
+ }
1586
+ // $Concat([...], "sep")
1587
+ const concatMatch = v.match(inputs_types_1.concatRegex);
1588
+ if (concatMatch) {
1589
+ const arrContent = concatMatch[1];
1590
+ const separator = concatMatch[2];
1591
+ const parts = arrContent.split(',').map((s) => this.unquote(s.trim()));
1592
+ const resolvedParts = await Promise.all(parts.map((p) => this.resolveValue(p, stepOutput)));
1593
+ return resolvedParts.map((x) => (x == null ? '' : String(x))).join(separator);
1594
+ }
1595
+ // $Substring(str, start, end)
1596
+ const substrMatch = v.match(inputs_types_1.substringRegex);
1597
+ if (substrMatch) {
1598
+ const str = await this.resolveValue(this.unquote(substrMatch[1]), stepOutput);
1599
+ const start = Number(substrMatch[2]);
1600
+ const end = Number(substrMatch[3]);
1601
+ if (typeof str !== 'string')
1602
+ return '';
1603
+ return str.substring(start, end);
1604
+ }
1605
+ // $Trim(str)
1606
+ const trimMatch = v.match(inputs_types_1.trimRegex);
1607
+ if (trimMatch) {
1608
+ const str = await this.resolveValue(this.unquote(trimMatch[1]), stepOutput);
1609
+ return typeof str === 'string' ? str.trim() : String(str !== null && str !== void 0 ? str : '').trim();
1610
+ }
1611
+ // $Uppercase(str)
1612
+ const upperMatch = v.match(inputs_types_1.uppercaseRegex);
1613
+ if (upperMatch) {
1614
+ const str = await this.resolveValue(this.unquote(upperMatch[1]), stepOutput);
1615
+ return typeof str === 'string' ? str.toUpperCase() : String(str !== null && str !== void 0 ? str : '').toUpperCase();
1616
+ }
1617
+ // $Lowercase(str)
1618
+ const lowerMatch = v.match(inputs_types_1.lowercaseRegex);
1619
+ if (lowerMatch) {
1620
+ const str = await this.resolveValue(this.unquote(lowerMatch[1]), stepOutput);
1621
+ return typeof str === 'string' ? str.toLowerCase() : String(str !== null && str !== void 0 ? str : '').toLowerCase();
1622
+ }
1623
+ // $Dateformat(date, "format")
1624
+ const dateMatch = v.match(inputs_types_1.dateFormatRegex);
1625
+ if (dateMatch) {
1626
+ const dateVal = await this.resolveValue(this.unquote(dateMatch[1]), stepOutput);
1627
+ const formatStr = dateMatch[3];
1628
+ const d = typeof dateVal === 'string' ? (0, date_fns_1.parseISO)(dateVal) : new Date(dateVal);
1629
+ if (isNaN(d.getTime()))
1630
+ return '';
1631
+ return (0, date_fns_1.format)(d, formatStr);
1632
+ }
1633
+ // $Replace(str, search, replace)
1634
+ const replaceMatch = v.match(inputs_types_1.replaceRegex);
1635
+ if (replaceMatch) {
1636
+ const str = await this.resolveValue(this.unquote(replaceMatch[1]), stepOutput);
1637
+ const search = replaceMatch[3];
1638
+ const repl = replaceMatch[5];
1639
+ return typeof str === 'string' ? str.split(search).join(repl) : String(str !== null && str !== void 0 ? str : '');
1640
+ }
1641
+ // $Pick(value, index)
1642
+ const pickMatch = v.match(inputs_types_1.pickRegex);
1643
+ if (pickMatch) {
1644
+ const val = await this.resolveValue(this.unquote(pickMatch[1]), stepOutput);
1645
+ const idx = Number(pickMatch[2]);
1646
+ if (typeof val === 'string' || Array.isArray(val))
1647
+ return (_a = val[idx]) !== null && _a !== void 0 ? _a : '';
1648
+ return '';
1649
+ }
1650
+ // $Filter(array, "op", value)
1651
+ const filterMatch = v.match(inputs_types_1.filterRegex);
1652
+ if (filterMatch) {
1653
+ const arr = await this.resolveValue(this.unquote(filterMatch[1]), stepOutput);
1654
+ const op = filterMatch[2].trim();
1655
+ const compareVal = await this.resolveValue(this.unquote(filterMatch[3]), stepOutput);
1656
+ if (!productsBuilder_types_1.ValidOperators.includes(op) || !Array.isArray(arr))
1657
+ return [];
1658
+ return arr.filter((item) => (0, processor_utils_1.compareValues)(item, op, compareVal));
1659
+ }
1660
+ // $Find(array, "op", value)
1661
+ const findMatch = v.match(inputs_types_1.findRegex);
1662
+ if (findMatch) {
1663
+ const arr = await this.resolveValue(this.unquote(findMatch[1]), stepOutput);
1664
+ const op = findMatch[2].trim();
1665
+ const compareVal = await this.resolveValue(this.unquote(findMatch[3]), stepOutput);
1666
+ if (!productsBuilder_types_1.ValidOperators.includes(op) || !Array.isArray(arr))
1667
+ return undefined;
1668
+ return arr.find((item) => (0, processor_utils_1.compareValues)(item, op, compareVal));
1669
+ }
1670
+ // $Split(str, "sep")
1671
+ const splitMatch = v.match(inputs_types_1.splitRegex);
1672
+ if (splitMatch) {
1673
+ const str = await this.resolveValue(this.unquote(splitMatch[1]), stepOutput);
1674
+ const sep = splitMatch[2];
1675
+ return typeof str === 'string' ? str.split(sep) : [];
1676
+ }
1677
+ // $Join([[...], [...]]) - inner can be refs like $Sequence{main}{a}{data}
1678
+ const joinMatch = v.match(inputs_types_1.joinRegex);
1679
+ if (joinMatch) {
1680
+ const innerStr = joinMatch[1].trim();
1681
+ const parts = innerStr.slice(1, -1).split(',').map((s) => s.trim());
1682
+ const resolved = await Promise.all(parts.map((p) => this.resolveValue(p, stepOutput)));
1683
+ return resolved.reduce((acc, current) => {
1684
+ if (Array.isArray(current))
1685
+ return acc.concat(current);
1686
+ return acc;
1687
+ }, []);
1688
+ }
1689
+ // --- Inline references (string contains $Input{}, $Step{}, $Now, etc.) ---
1690
+ let out = value;
1691
+ out = out.replace(/\$Now(?:\(\))?/g, () => String(Date.now()));
1692
+ out = out.replace(/\$Input\{([^}]+)\}/g, (_, field) => {
1693
+ const val = this.getNestedValue(this.state.input, field);
1694
+ return String(val !== null && val !== void 0 ? val : '');
1695
+ });
1696
+ out = out.replace(/\$Sequence\{main\}\{([^}]+)\}\{([^}]+)\}/g, (_, stepTag, field) => {
1697
+ const stepData = this.state.steps[stepTag];
1698
+ if (stepData) {
1699
+ const val = this.getNestedValue(stepData, field);
1700
+ return String(val !== null && val !== void 0 ? val : '');
1701
+ }
1702
+ return '';
1703
+ });
1704
+ out = out.replace(/\$Step\{([^}]+)\}\{([^}]+)\}/g, (_, stepTag, field) => {
1705
+ const stepData = this.state.steps[stepTag];
1706
+ if (stepData) {
1707
+ const val = this.getNestedValue(stepData, field);
1708
+ return String(val !== null && val !== void 0 ? val : '');
1709
+ }
1710
+ return '';
1711
+ });
1712
+ return out;
1713
+ }
1714
+ /**
1715
+ * Get nested value from object using dot notation
1716
+ */
1717
+ getNestedValue(obj, path) {
1718
+ const keys = path.split('.');
1719
+ let current = obj;
1720
+ for (const key of keys) {
1721
+ if (current === null || current === undefined) {
1722
+ return undefined;
1723
+ }
1724
+ current = current[key];
1725
+ }
1726
+ return current;
1727
+ }
1728
+ /**
1729
+ * Get ordered steps respecting dependencies
1730
+ */
1731
+ getOrderedSteps() {
1732
+ const steps = this.feature.steps;
1733
+ const ordered = [];
1734
+ const visited = new Set();
1735
+ const visiting = new Set();
1736
+ const visit = (step) => {
1737
+ if (visited.has(step.tag))
1738
+ return;
1739
+ if (visiting.has(step.tag)) {
1740
+ throw new Error(`Circular dependency detected at step ${step.tag}`);
1741
+ }
1742
+ visiting.add(step.tag);
1743
+ // Visit dependencies first
1744
+ if (step.depends_on) {
1745
+ for (const depTag of step.depends_on) {
1746
+ const depStep = steps.find((s) => s.tag === depTag);
1747
+ if (depStep) {
1748
+ visit(depStep);
1749
+ }
1750
+ }
1751
+ }
1752
+ visiting.delete(step.tag);
1753
+ visited.add(step.tag);
1754
+ ordered.push(step);
1755
+ };
1756
+ for (const step of steps) {
1757
+ visit(step);
1758
+ }
1759
+ return ordered;
1760
+ }
1761
+ /**
1762
+ * Pre-fetch all action/notification/storage bootstrap data for the given steps in one backend call.
1763
+ * Results are cached in stepBootstrapCache by step tag so step executors can skip their own bootstrap calls.
1764
+ */
1765
+ async prefetchStepBootstrap(steps) {
1766
+ this.stepBootstrapCache.clear();
1767
+ const product = this.state.product;
1768
+ const env = this.state.env;
1769
+ const workflowSteps = [];
1770
+ for (const step of steps) {
1771
+ if (step.type === types_1.FeatureStepType.ACTION && step.app) {
1772
+ workflowSteps.push({
1773
+ step_tag: step.tag,
1774
+ type: 'action',
1775
+ access_tag: step.app,
1776
+ action_tag: step.event,
1777
+ });
1778
+ }
1779
+ else if (step.type === types_1.FeatureStepType.NOTIFICATION && step.notification && step.event) {
1780
+ workflowSteps.push({
1781
+ step_tag: step.tag,
1782
+ type: 'notification',
1783
+ notification_tag: step.notification,
1784
+ message_tag: step.event,
1785
+ });
1786
+ }
1787
+ else if (step.type === types_1.FeatureStepType.STORAGE && step.storage && step.event) {
1788
+ workflowSteps.push({
1789
+ step_tag: step.tag,
1790
+ type: 'storage',
1791
+ storage_tag: step.storage,
1792
+ });
1793
+ }
1794
+ }
1795
+ if (workflowSteps.length > 0) {
1796
+ stepLog('Bootstrap feature API call (single request)', {
1797
+ product_tag: product,
1798
+ env_slug: env,
1799
+ steps_count: workflowSteps.length,
1800
+ step_tags: workflowSteps.map((s) => s.step_tag),
1801
+ });
1802
+ const bootstraps = await this.productBuilder.bootstrapFeature({
1803
+ product_tag: product,
1804
+ env_slug: env,
1805
+ steps: workflowSteps,
1806
+ });
1807
+ stepLog('Bootstrap feature API response', {
1808
+ received_keys: Object.keys(bootstraps),
1809
+ keys_count: Object.keys(bootstraps).length,
1810
+ });
1811
+ for (const [stepTag, data] of Object.entries(bootstraps)) {
1812
+ if (data != null) {
1813
+ this.stepBootstrapCache.set(stepTag, data);
1814
+ }
1815
+ }
1816
+ }
1817
+ // Pre-fetch message brokers for produce steps so runBrokerPublish has broker/topic in cache
1818
+ const brokerTags = new Set();
1819
+ for (const step of steps) {
1820
+ if ((step.type === types_1.FeatureStepType.PUBLISH || step.type === types_1.FeatureStepType.PRODUCE) && step.broker) {
1821
+ brokerTags.add(step.broker);
1822
+ }
1823
+ }
1824
+ if (brokerTags.size > 0) {
1825
+ stepLog('Fetching message brokers for produce steps', { broker_tags: [...brokerTags] });
1826
+ await Promise.all([...brokerTags].map(async (tag) => {
1827
+ var _a;
1828
+ try {
1829
+ const broker = await this.productBuilder.fetchMessageBroker(tag);
1830
+ return { tag, found: !!broker };
1831
+ }
1832
+ catch (e) {
1833
+ stepLog('Broker fetch failed (step may fail later)', { broker_tag: tag, error: (_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e) });
1834
+ return { tag, found: false };
1835
+ }
1836
+ }));
1837
+ }
1838
+ // Pre-fetch databases for DATABASE_ACTION steps
1839
+ const databaseTags = new Set();
1840
+ for (const step of steps) {
1841
+ if (step.type === types_1.FeatureStepType.DATABASE_ACTION && step.database) {
1842
+ databaseTags.add(step.database);
1843
+ }
1844
+ }
1845
+ if (databaseTags.size > 0) {
1846
+ stepLog('Fetching databases for database steps', { database_tags: [...databaseTags] });
1847
+ await Promise.all([...databaseTags].map(async (tag) => {
1848
+ var _a;
1849
+ try {
1850
+ const db = await this.productBuilder.fetchDatabase(tag);
1851
+ return { tag, found: !!db };
1852
+ }
1853
+ catch (e) {
1854
+ stepLog('Database fetch failed (step may fail later)', { database_tag: tag, error: (_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e) });
1855
+ return { tag, found: false };
1856
+ }
1857
+ }));
1858
+ }
1859
+ // Pre-fetch graphs for GRAPH steps
1860
+ const graphTags = new Set();
1861
+ for (const step of steps) {
1862
+ if (step.type === types_1.FeatureStepType.GRAPH && step.graph) {
1863
+ graphTags.add(step.graph);
1864
+ }
1865
+ }
1866
+ if (graphTags.size > 0) {
1867
+ stepLog('Fetching graphs for graph steps', { graph_tags: [...graphTags] });
1868
+ await Promise.all([...graphTags].map(async (tag) => {
1869
+ var _a;
1870
+ try {
1871
+ const graph = await this.productBuilder.fetchGraph(tag);
1872
+ return { tag, found: !!graph };
1873
+ }
1874
+ catch (e) {
1875
+ stepLog('Graph fetch failed (step may fail later)', { graph_tag: tag, error: (_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e) });
1876
+ return { tag, found: false };
1877
+ }
1878
+ }));
1879
+ }
1880
+ const vectorTags = new Set();
1881
+ for (const step of steps) {
1882
+ if (step.type === types_1.FeatureStepType.VECTOR && step.vector) {
1883
+ vectorTags.add(step.vector);
1884
+ }
1885
+ }
1886
+ if (vectorTags.size > 0) {
1887
+ stepLog('Fetching vectors for vector steps', { vector_tags: [...vectorTags] });
1888
+ await Promise.all([...vectorTags].map(async (tag) => {
1889
+ var _a;
1890
+ try {
1891
+ const vec = await this.productBuilder.fetchVector(tag);
1892
+ return { tag, found: !!vec };
1893
+ }
1894
+ catch (e) {
1895
+ stepLog('Vector fetch failed (step may fail later)', { vector_tag: tag, error: (_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e) });
1896
+ return { tag, found: false };
1897
+ }
1898
+ }));
1899
+ }
1900
+ // Pre-fetch quotas for QUOTA steps
1901
+ const quotaTags = new Set();
1902
+ for (const step of steps) {
1903
+ if (step.type === types_1.FeatureStepType.QUOTA && step.quota) {
1904
+ quotaTags.add(step.quota);
1905
+ }
1906
+ }
1907
+ if (quotaTags.size > 0) {
1908
+ stepLog('Fetching quotas for quota steps', { quota_tags: [...quotaTags] });
1909
+ await Promise.all([...quotaTags].map(async (tag) => {
1910
+ var _a;
1911
+ try {
1912
+ const quota = await this.productBuilder.fetchQuota(tag);
1913
+ return { tag, found: !!quota };
1914
+ }
1915
+ catch (e) {
1916
+ stepLog('Quota fetch failed (step may fail later)', { quota_tag: tag, error: (_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e) });
1917
+ return { tag, found: false };
1918
+ }
1919
+ }));
1920
+ }
1921
+ // Pre-fetch fallbacks for FALLBACK steps
1922
+ const fallbackTags = new Set();
1923
+ for (const step of steps) {
1924
+ if (step.type === types_1.FeatureStepType.FALLBACK && step.fallback) {
1925
+ fallbackTags.add(step.fallback);
1926
+ }
1927
+ }
1928
+ if (fallbackTags.size > 0) {
1929
+ stepLog('Fetching fallbacks for fallback steps', { fallback_tags: [...fallbackTags] });
1930
+ await Promise.all([...fallbackTags].map(async (tag) => {
1931
+ var _a;
1932
+ try {
1933
+ const fallback = await this.productBuilder.fetchFallback(tag);
1934
+ return { tag, found: !!fallback };
1935
+ }
1936
+ catch (e) {
1937
+ stepLog('Fallback fetch failed (step may fail later)', { fallback_tag: tag, error: (_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e) });
1938
+ return { tag, found: false };
1939
+ }
1940
+ }));
1941
+ }
1942
+ // Pre-fetch child workflows for CHILD_FEATURE steps
1943
+ const workflowTags = new Set();
1944
+ for (const step of steps) {
1945
+ if (step.type === types_1.FeatureStepType.CHILD_FEATURE && step.feature) {
1946
+ workflowTags.add(step.feature);
1947
+ }
1948
+ }
1949
+ if (workflowTags.size > 0) {
1950
+ stepLog('Fetching child features for child_feature steps', { feature_tags: [...workflowTags] });
1951
+ await Promise.all([...workflowTags].map(async (tag) => {
1952
+ var _a;
1953
+ try {
1954
+ const feature = await this.productBuilder.fetchFeature(tag);
1955
+ return { tag, found: !!feature };
1956
+ }
1957
+ catch (e) {
1958
+ stepLog('Child feature fetch failed (step may fail later)', { feature_tag: tag, error: (_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e) });
1959
+ return { tag, found: false };
1960
+ }
1961
+ }));
1962
+ }
1963
+ // Pre-connect brokers, graphs, and databases so step execution reuses connections.
1964
+ // Security: product/env from this.state (same execution context). No connection URLs, config, or
1965
+ // credentials are ever logged. On pre-connect failure we log only tags and a generic message.
1966
+ if (brokerTags.size > 0) {
1967
+ stepLog('Pre-warming broker connections', { broker_tags: [...brokerTags] });
1968
+ try {
1969
+ await this.getBrokersService().warmBrokerConnections({ product, env, brokerTags: [...brokerTags] });
1970
+ }
1971
+ catch (_a) {
1972
+ stepLog('Broker pre-warm failed (steps may connect on first use)', { broker_tags: [...brokerTags] });
1973
+ }
1974
+ }
1975
+ if (graphTags.size > 0) {
1976
+ stepLog('Pre-connecting graph connections', { graph_tags: [...graphTags] });
1977
+ await Promise.all([...graphTags].map((tag) => this.getGraphService()
1978
+ .connect({ product, env, graph: tag })
1979
+ .catch(() => {
1980
+ stepLog('Graph pre-connect failed (step may connect on first use)', { graph_tag: tag });
1981
+ })));
1982
+ }
1983
+ if (databaseTags.size > 0) {
1984
+ stepLog('Pre-connecting database connections', { database_tags: [...databaseTags] });
1985
+ await Promise.all([...databaseTags].map((tag) => this.getDatabaseService()
1986
+ .connect({ product, env, database: tag })
1987
+ .catch(() => {
1988
+ stepLog('Database pre-connect failed (step may connect on first use)', { database_tag: tag });
1989
+ })));
1990
+ }
1991
+ if (vectorTags.size > 0) {
1992
+ stepLog('Pre-connecting vector connections', { vector_tags: [...vectorTags] });
1993
+ await Promise.all([...vectorTags].map((tag) => this.getVectorService()
1994
+ .connect({ product, env, vector: tag })
1995
+ .catch(() => {
1996
+ stepLog('Vector pre-connect failed (step may connect on first use)', { vector_tag: tag });
1997
+ })));
1998
+ }
1999
+ }
2000
+ /**
2001
+ * Find a step by tag
2002
+ */
2003
+ findStep(tag) {
2004
+ return this.feature.steps.find((s) => s.tag === tag);
2005
+ }
2006
+ /**
2007
+ * Evaluate a condition string.
2008
+ * Supports: (cond1) || (cond2), (cond1) && (cond2), and in each part: ==, ===, !=, !==, >, <, >=, <=.
2009
+ * Left/right can be $Step{}, $Input{}, etc. (resolved before comparison).
2010
+ */
2011
+ async evaluateCondition(condition) {
2012
+ try {
2013
+ const orParts = condition.split(/\s*\|\|\s*/).map((p) => p.trim());
2014
+ for (const orPart of orParts) {
2015
+ const trimmed = orPart.replace(/^\(|\)$/g, '').trim();
2016
+ const andParts = trimmed.split(/\s*&&\s*/).map((p) => p.trim());
2017
+ let andResult = true;
2018
+ for (const andPart of andParts) {
2019
+ if (!(await this.evaluateConditionPart(andPart))) {
2020
+ andResult = false;
2021
+ break;
2022
+ }
2023
+ }
2024
+ if (andResult)
2025
+ return true;
2026
+ }
2027
+ return false;
2028
+ }
2029
+ catch (_a) {
2030
+ return false;
2031
+ }
2032
+ }
2033
+ /**
2034
+ * Coerce a trimmed condition literal (after resolve) to a comparable value: boolean, number, or string.
2035
+ */
2036
+ conditionLiteral(val) {
2037
+ const s = val.replace(/^["']|["']$/g, '').trim();
2038
+ if (s === 'true')
2039
+ return true;
2040
+ if (s === 'false')
2041
+ return false;
2042
+ if (s === 'null')
2043
+ return null;
2044
+ const n = Number(s);
2045
+ if (s !== '' && !Number.isNaN(n))
2046
+ return n;
2047
+ return s;
2048
+ }
2049
+ async evaluateConditionPart(condition) {
2050
+ try {
2051
+ const resolved = await this.resolveStringValue(condition);
2052
+ if (typeof resolved !== 'string') {
2053
+ return Boolean(resolved);
2054
+ }
2055
+ const match = resolved.match(FeatureExecutor.CONDITION_OP_REGEX);
2056
+ if (!match) {
2057
+ return Boolean(this.conditionLiteral(resolved.trim()));
2058
+ }
2059
+ const [, leftStr, op, rightStr] = match;
2060
+ const left = this.conditionLiteral(leftStr.trim());
2061
+ const right = this.conditionLiteral(rightStr.trim());
2062
+ switch (op) {
2063
+ case '==':
2064
+ case '===':
2065
+ return this.compareLooseEqual(left, right);
2066
+ case '!=':
2067
+ case '!==':
2068
+ return !this.compareLooseEqual(left, right);
2069
+ case '>':
2070
+ return this.compareOrdered(left, right, (a, b) => a > b);
2071
+ case '<':
2072
+ return this.compareOrdered(left, right, (a, b) => a < b);
2073
+ case '>=':
2074
+ return this.compareOrdered(left, right, (a, b) => a >= b);
2075
+ case '<=':
2076
+ return this.compareOrdered(left, right, (a, b) => a <= b);
2077
+ default:
2078
+ return false;
2079
+ }
2080
+ }
2081
+ catch (_a) {
2082
+ return false;
2083
+ }
2084
+ }
2085
+ compareLooseEqual(a, b) {
2086
+ if (a === b)
2087
+ return true;
2088
+ if (a == null && b == null)
2089
+ return true;
2090
+ if (typeof a === 'boolean' && typeof b === 'boolean')
2091
+ return a === b;
2092
+ if (typeof a === 'number' && typeof b === 'number')
2093
+ return a === b;
2094
+ return String(a) === String(b);
2095
+ }
2096
+ /**
2097
+ * Compare for ordering (>, <, >=, <=). Uses numeric comparison when both sides are numeric, else string.
2098
+ */
2099
+ compareOrdered(left, right, cmp) {
2100
+ const l = typeof left === 'number' ? left : Number(left);
2101
+ const r = typeof right === 'number' ? right : Number(right);
2102
+ const bothNumeric = !Number.isNaN(l) && !Number.isNaN(r);
2103
+ if (bothNumeric)
2104
+ return cmp(l, r);
2105
+ return cmp(String(left !== null && left !== void 0 ? left : ''), String(right !== null && right !== void 0 ? right : ''));
2106
+ }
2107
+ /**
2108
+ * Returns true if the value tree contains any $ operator reference ($Input{}, $Step{}, $Sequence{}, etc.).
2109
+ * Used to distinguish output templates (to resolve) from type-only schemas (to ignore for persistence).
2110
+ */
2111
+ hasOperatorRefsInOutput(value) {
2112
+ if (typeof value === 'string') {
2113
+ const v = value.trim();
2114
+ return /^\$((Input|Step|Sequence|State|StepOutput|Now|Add|Subtract|Concat|Size|Length|Pick|Filter|Find|Join|Split|Replace|Substring|Trim|Uppercase|Lowercase|Dateformat)\{|[A-Za-z]+\()/.test(v);
2115
+ }
2116
+ if (Array.isArray(value)) {
2117
+ return value.some((item) => this.hasOperatorRefsInOutput(item));
2118
+ }
2119
+ if (value && typeof value === 'object') {
2120
+ return Object.values(value).some((v) => this.hasOperatorRefsInOutput(v));
2121
+ }
2122
+ return false;
2123
+ }
2124
+ /**
2125
+ * Returns true if the object looks like a feature output type schema (e.g. { orderId: { type: 'string' }, ... })
2126
+ * rather than a template with $ operators or literal values.
2127
+ */
2128
+ isOutputTypeSchema(output) {
2129
+ if (Object.keys(output).length === 0)
2130
+ return false;
2131
+ return Object.values(output).every((v) => {
2132
+ if (v != null && typeof v === 'object' && !Array.isArray(v)) {
2133
+ const o = v;
2134
+ return 'type' in o && typeof o.type === 'string';
2135
+ }
2136
+ return false;
2137
+ });
2138
+ }
2139
+ /**
2140
+ * Determine feature output for persistence and return value.
2141
+ * - If feature.output has $ operator refs: resolve template to actual values (matches definition).
2142
+ * - If feature.output is a type-only schema: use execution result (last step or step_outputs) so stored result matches runtime output, not the schema.
2143
+ * - If no feature.output: use last step result or {}.
2144
+ */
2145
+ async determineWorkflowOutput() {
2146
+ const outputDef = this.feature.output;
2147
+ if (!outputDef || Object.keys(outputDef).length === 0) {
2148
+ const lastStep = this.state.completed_steps[this.state.completed_steps.length - 1];
2149
+ return lastStep ? this.state.steps[lastStep] : {};
2150
+ }
2151
+ const hasRefs = this.hasOperatorRefsInOutput(outputDef);
2152
+ let result;
2153
+ if (hasRefs) {
2154
+ result = this.resolveInput(outputDef);
2155
+ }
2156
+ else if (this.isOutputTypeSchema(outputDef)) {
2157
+ const out = this.buildOutputFromStepsToMatchDefinition(outputDef);
2158
+ result = Object.keys(out).length > 0 ? out : {};
2159
+ }
2160
+ else {
2161
+ result = this.resolveInput(outputDef);
2162
+ }
2163
+ // recordScenarios: feature.output is from last scenario; override outcome from which branch actually ran
2164
+ if (result != null && typeof result === 'object' && 'outcome' in result) {
2165
+ const res = result;
2166
+ if (this.state.completed_steps.includes('approve-request'))
2167
+ res.outcome = 'approved';
2168
+ else if (this.state.completed_steps.includes('reject-request'))
2169
+ res.outcome = 'rejected';
2170
+ }
2171
+ return result;
2172
+ }
2173
+ /**
2174
+ * Build output with same keys as feature definition, values from step_outputs (last step first, then others).
2175
+ * Ensures persisted result shape matches the feature definition output shape.
2176
+ */
2177
+ buildOutputFromStepsToMatchDefinition(definitionKeys) {
2178
+ var _a;
2179
+ const keys = Object.keys(definitionKeys);
2180
+ if (keys.length === 0)
2181
+ return {};
2182
+ const out = {};
2183
+ const stepsReversed = [...this.state.completed_steps].reverse();
2184
+ for (const key of keys) {
2185
+ let found = false;
2186
+ for (const stepTag of stepsReversed) {
2187
+ const stepData = this.state.steps[stepTag];
2188
+ if (stepData == null || typeof stepData !== 'object')
2189
+ continue;
2190
+ const rec = stepData;
2191
+ if (key in rec) {
2192
+ out[key] = rec[key];
2193
+ found = true;
2194
+ break;
2195
+ }
2196
+ const nested = (_a = rec.output) !== null && _a !== void 0 ? _a : rec.result;
2197
+ if (nested != null && typeof nested === 'object' && key in nested) {
2198
+ out[key] = nested[key];
2199
+ found = true;
2200
+ break;
2201
+ }
2202
+ }
2203
+ // Handler return shape { a, b } often maps to steps step-a, step-b; use step output so out.a.value / out.b.value exist
2204
+ if (!found && this.state.steps[`step-${key}`] != null) {
2205
+ const stepResult = this.state.steps[`step-${key}`];
2206
+ out[key] = ((stepResult === null || stepResult === void 0 ? void 0 : stepResult.output) != null ? stepResult.output : stepResult);
2207
+ }
2208
+ }
2209
+ if (Object.keys(out).length === 0 && this.state.completed_steps.length > 0) {
2210
+ const lastStep = this.state.completed_steps[this.state.completed_steps.length - 1];
2211
+ const lastOutput = this.state.steps[lastStep];
2212
+ return lastOutput != null && typeof lastOutput === 'object' ? lastOutput : {};
2213
+ }
2214
+ return out;
2215
+ }
2216
+ /**
2217
+ * Sleep for specified duration
2218
+ */
2219
+ sleep(ms) {
2220
+ return new Promise((resolve) => setTimeout(resolve, ms));
2221
+ }
2222
+ /**
2223
+ * Resume a feature from a previous state
2224
+ *
2225
+ * @param options - Resume options including completed steps, state, and starting point
2226
+ */
2227
+ async resume(options) {
2228
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
2229
+ const startTime = Date.now();
2230
+ debugLog('[FeatureExecutor] resume ENTRY', {
2231
+ feature_id: this.state.feature_id,
2232
+ feature_tag: this.state.feature_tag,
2233
+ completed_steps_count: (_b = (_a = options.completed_steps) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0,
2234
+ from_checkpoint: options.from_checkpoint,
2235
+ from_step: options.from_step,
2236
+ skip_steps: (_d = (_c = options.skip_steps) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0,
2237
+ });
2238
+ try {
2239
+ // Track phase so failures before first step are reported as (initialization)
2240
+ this.state.current_step = '(initialization)';
2241
+ // Initialize product builder with the product
2242
+ await this.productBuilder.initializeProductByTag(this.state.product);
2243
+ this.productId = this.productBuilder.fetchProductId();
2244
+ // Initialize logging after we have product ID
2245
+ this.initializeLogging();
2246
+ // Restore previous state
2247
+ this.state.completed_steps = options.completed_steps || [];
2248
+ this.state.state = options.state || {};
2249
+ // Restore step outputs if provided
2250
+ if (options.step_outputs) {
2251
+ this.state.steps = Object.assign(Object.assign({}, this.state.steps), options.step_outputs);
2252
+ }
2253
+ this.state.status = types_1.FeatureStatus.RUNNING;
2254
+ // Get steps in execution order
2255
+ const orderedSteps = this.getOrderedSteps();
2256
+ // Find starting point
2257
+ let startIndex = 0;
2258
+ if (options.from_checkpoint) {
2259
+ // Find the step after the checkpoint
2260
+ const checkpointIndex = orderedSteps.findIndex((s) => s.type === types_1.FeatureStepType.CHECKPOINT && s.tag === options.from_checkpoint);
2261
+ if (checkpointIndex >= 0) {
2262
+ startIndex = checkpointIndex + 1;
2263
+ }
2264
+ }
2265
+ else if (options.from_step) {
2266
+ // Start from the specified step
2267
+ startIndex = orderedSteps.findIndex((s) => s.tag === options.from_step);
2268
+ if (startIndex === -1) {
2269
+ throw new Error(`Step ${options.from_step} not found`);
2270
+ }
2271
+ }
2272
+ else {
2273
+ // Start after last completed step
2274
+ const lastCompleted = this.state.completed_steps[this.state.completed_steps.length - 1];
2275
+ if (lastCompleted) {
2276
+ const lastIndex = orderedSteps.findIndex((s) => s.tag === lastCompleted);
2277
+ startIndex = lastIndex + 1;
2278
+ }
2279
+ }
2280
+ debugLog('[FeatureExecutor] resume starting from step index', {
2281
+ feature_id: this.state.feature_id,
2282
+ startIndex,
2283
+ total_steps: orderedSteps.length,
2284
+ steps_to_run: orderedSteps.slice(startIndex).map((s) => s.tag),
2285
+ });
2286
+ // Execute remaining steps
2287
+ for (let i = startIndex; i < orderedSteps.length; i++) {
2288
+ const step = orderedSteps[i];
2289
+ // Skip if already completed or in skip list
2290
+ if (this.state.completed_steps.includes(step.tag)) {
2291
+ this.logStepEvent(step, `Step ${step.tag} - skipped (already completed)`, logs_types_1.LogEventStatus.SUCCESS, { skipped: true });
2292
+ continue;
2293
+ }
2294
+ if ((_e = options.skip_steps) === null || _e === void 0 ? void 0 : _e.includes(step.tag)) {
2295
+ this.logStepEvent(step, `Step ${step.tag} - skipped (in skip list)`, logs_types_1.LogEventStatus.SUCCESS, { skipped: true });
2296
+ continue;
2297
+ }
2298
+ // Check step condition if present
2299
+ if (step.condition && !(await this.evaluateCondition(step.condition))) {
2300
+ this.logStepEvent(step, `Step ${step.tag} - skipped (condition not met)`, logs_types_1.LogEventStatus.SUCCESS, { skipped: true });
2301
+ continue;
2302
+ }
2303
+ this.state.current_step = step.tag;
2304
+ // Log step start
2305
+ this.logStepEvent(step, `Step ${step.tag} - started`, logs_types_1.LogEventStatus.PROCESSING);
2306
+ const result = await this.executeStep(step);
2307
+ // Log step completion
2308
+ this.logStepEvent(step, result.success ? `Step ${step.tag} - completed` : `Step ${step.tag} - failed`, result.success ? logs_types_1.LogEventStatus.SUCCESS : logs_types_1.LogEventStatus.FAIL, { duration: result.duration, error: result.error });
2309
+ // Step return value = resolved step.output schema with primitive result, or raw when no schema
2310
+ const outputSchemaResume = step.output;
2311
+ const hasOutputSchemaResume = outputSchemaResume && typeof outputSchemaResume === 'object' && !Array.isArray(outputSchemaResume) && Object.keys(outputSchemaResume).length > 0;
2312
+ let stepReturnValueResume = result.output;
2313
+ if (result.success && hasOutputSchemaResume) {
2314
+ try {
2315
+ stepReturnValueResume = await this.resolveInput(outputSchemaResume, result.output);
2316
+ }
2317
+ catch (_m) {
2318
+ stepReturnValueResume = result.output;
2319
+ }
2320
+ }
2321
+ else if (!result.success) {
2322
+ stepReturnValueResume = undefined;
2323
+ }
2324
+ // Persist step result to backend (use step return shape, not raw primitive)
2325
+ await this.persistStepResult(step, result.success ? types_1.LogEventStatus.SUCCESS : types_1.LogEventStatus.FAIL, stepReturnValueResume, result.error, result.duration, result.input);
2326
+ this.state.step_timings.push({
2327
+ tag: step.tag,
2328
+ duration_ms: result.duration,
2329
+ success: result.success,
2330
+ });
2331
+ if (!result.success) {
2332
+ this.state.failed_step = step.tag;
2333
+ // Check if step allows failure
2334
+ if ((_f = step.options) === null || _f === void 0 ? void 0 : _f.allow_fail) {
2335
+ this.state.steps[step.tag] = { error: result.error };
2336
+ continue;
2337
+ }
2338
+ // Check if step is optional
2339
+ if ((_g = step.options) === null || _g === void 0 ? void 0 : _g.optional) {
2340
+ continue;
2341
+ }
2342
+ // Trigger rollback
2343
+ this.state.status = types_1.FeatureStatus.ROLLING_BACK;
2344
+ const rollbackResult = await this.executeRollback(step.tag, result.error || 'Step failed');
2345
+ await this.persistExecutionResult(types_1.LogEventStatus.FAIL, undefined, result.error);
2346
+ await ((_h = this.logService) === null || _h === void 0 ? void 0 : _h.publish());
2347
+ if (this.graphConnectionUsed) {
2348
+ await this.getGraphService().disconnect().catch(() => { });
2349
+ await this.getProcessorService().disconnectBrokerConnections().catch(() => { });
2350
+ if (this._brokersService)
2351
+ await this._brokersService.disconnectAll().catch(() => { });
2352
+ await this.getProcessorService().disconnectMailTransporters().catch(() => { });
2353
+ await this.getProcessorService().disconnectFirebaseApps().catch(() => { });
2354
+ await this.getProcessorService().disconnectSmsClients().catch(() => { });
2355
+ }
2356
+ return {
2357
+ status: types_1.FeatureStatus.ROLLED_BACK,
2358
+ feature_id: this.state.feature_id,
2359
+ error: result.error,
2360
+ execution_time: Date.now() - startTime,
2361
+ completed_steps: this.state.completed_steps,
2362
+ step_timings: this.state.step_timings,
2363
+ failed_step: step.tag,
2364
+ rollback_info: {
2365
+ triggered_by: step.tag,
2366
+ reason: result.error || 'Step failed',
2367
+ rolled_back_steps: rollbackResult.rolled_back_steps,
2368
+ failed_rollbacks: rollbackResult.failed_steps,
2369
+ },
2370
+ };
2371
+ }
2372
+ // Store step return shape in state (same value we persisted)
2373
+ this.state.steps[step.tag] = stepReturnValueResume;
2374
+ this.state.completed_steps.push(step.tag);
2375
+ }
2376
+ this.state.status = types_1.FeatureStatus.COMPLETED;
2377
+ this.state.ended_at = Date.now();
2378
+ const output = await this.determineWorkflowOutput();
2379
+ debugLog('[FeatureExecutor] resume COMPLETED', {
2380
+ feature_id: this.state.feature_id,
2381
+ feature_tag: this.state.feature_tag,
2382
+ completed_steps: this.state.completed_steps.length,
2383
+ execution_time_ms: Date.now() - startTime,
2384
+ });
2385
+ await this.persistExecutionResult(types_1.LogEventStatus.SUCCESS, output);
2386
+ await ((_j = this.logService) === null || _j === void 0 ? void 0 : _j.publish());
2387
+ if (this.graphConnectionUsed) {
2388
+ await this.getGraphService().disconnect().catch(() => { });
2389
+ await this.getProcessorService().disconnectBrokerConnections().catch(() => { });
2390
+ if (this._brokersService)
2391
+ await this._brokersService.disconnectAll().catch(() => { });
2392
+ await this.getProcessorService().disconnectMailTransporters().catch(() => { });
2393
+ await this.getProcessorService().disconnectFirebaseApps().catch(() => { });
2394
+ await this.getProcessorService().disconnectSmsClients().catch(() => { });
2395
+ }
2396
+ return {
2397
+ status: types_1.FeatureStatus.COMPLETED,
2398
+ feature_id: this.state.feature_id,
2399
+ output: output,
2400
+ execution_time: Date.now() - startTime,
2401
+ completed_steps: this.state.completed_steps,
2402
+ step_timings: this.state.step_timings,
2403
+ };
2404
+ }
2405
+ catch (error) {
2406
+ this.state.status = types_1.FeatureStatus.FAILED;
2407
+ this.state.ended_at = Date.now();
2408
+ const details = formatErrorDetails(error);
2409
+ const failedAt = (_k = this.state.current_step) !== null && _k !== void 0 ? _k : '(unknown)';
2410
+ debugLog('[FeatureExecutor] resume FAILED (exception)', Object.assign(Object.assign({ feature_id: this.state.feature_id, feature_tag: this.state.feature_tag, failed_at: failedAt, completed_steps: this.state.completed_steps, error: details.message }, (details.status != null && { http_status: details.status })), (details.responseBody != null && { response_body: details.responseBody })));
2411
+ if (details.stack) {
2412
+ debugLog('[FeatureExecutor] resume FAILED stack', details.stack);
2413
+ }
2414
+ const errorSummary = details.status != null
2415
+ ? `Failed at ${failedAt}: ${details.message} (HTTP ${details.status})`
2416
+ : `Failed at ${failedAt}: ${details.message}`;
2417
+ await this.persistExecutionResult(types_1.LogEventStatus.FAIL, undefined, errorSummary);
2418
+ await ((_l = this.logService) === null || _l === void 0 ? void 0 : _l.publish());
2419
+ if (this.graphConnectionUsed) {
2420
+ await this.getGraphService().disconnect().catch(() => { });
2421
+ await this.getProcessorService().disconnectBrokerConnections().catch(() => { });
2422
+ if (this._brokersService)
2423
+ await this._brokersService.disconnectAll().catch(() => { });
2424
+ await this.getProcessorService().disconnectMailTransporters().catch(() => { });
2425
+ await this.getProcessorService().disconnectFirebaseApps().catch(() => { });
2426
+ await this.getProcessorService().disconnectSmsClients().catch(() => { });
2427
+ }
2428
+ return {
2429
+ status: types_1.FeatureStatus.FAILED,
2430
+ feature_id: this.state.feature_id,
2431
+ error: errorSummary,
2432
+ execution_time: Date.now() - startTime,
2433
+ completed_steps: this.state.completed_steps,
2434
+ step_timings: this.state.step_timings,
2435
+ failed_step: this.state.current_step,
2436
+ };
2437
+ }
2438
+ }
2439
+ }
2440
+ exports.FeatureExecutor = FeatureExecutor;
2441
+ /** Comparison operators in parse order (multi-char before single-char). */
2442
+ FeatureExecutor.CONDITION_OP_REGEX = /^\s*(.+?)\s*(!==|===|==|>=|<=|!=|>|<)\s*(.+)\s*$/;
2443
+ exports.default = FeatureExecutor;
2444
+ //# sourceMappingURL=feature-executor.js.map