clearctx 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/LICENSE +21 -0
- package/README.md +1006 -0
- package/STRATEGY.md +485 -0
- package/bin/cli.js +1756 -0
- package/bin/continuity-hook.js +118 -0
- package/bin/mcp.js +27 -0
- package/bin/setup.js +929 -0
- package/package.json +56 -0
- package/src/artifact-store.js +710 -0
- package/src/atomic-io.js +99 -0
- package/src/briefing-generator.js +451 -0
- package/src/continuity-hooks.js +253 -0
- package/src/contract-store.js +525 -0
- package/src/decision-journal.js +229 -0
- package/src/delegate.js +348 -0
- package/src/dependency-resolver.js +453 -0
- package/src/diff-engine.js +473 -0
- package/src/file-lock.js +161 -0
- package/src/index.js +61 -0
- package/src/lineage-graph.js +402 -0
- package/src/manager.js +510 -0
- package/src/mcp-server.js +3501 -0
- package/src/pattern-registry.js +221 -0
- package/src/pipeline-engine.js +618 -0
- package/src/prompts.js +1217 -0
- package/src/safety-net.js +170 -0
- package/src/session-snapshot.js +508 -0
- package/src/snapshot-engine.js +490 -0
- package/src/stale-detector.js +169 -0
- package/src/store.js +131 -0
- package/src/stream-session.js +463 -0
- package/src/team-hub.js +615 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline Engine - Layer 3 Reactive Automation System
|
|
3
|
+
*
|
|
4
|
+
* This class manages reactive pipelines: "when X happens, automatically do Y."
|
|
5
|
+
* It listens for events (artifact published, contract completed, etc.) and
|
|
6
|
+
* fires rules that match the trigger conditions.
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* - Pipelines contain rules
|
|
10
|
+
* - Each rule has: trigger (when to fire), condition (filter), action (what to do)
|
|
11
|
+
* - Triggers match against events from the system
|
|
12
|
+
* - Actions can notify sessions, broadcast, or return actions for caller to execute
|
|
13
|
+
*
|
|
14
|
+
* @class PipelineEngine
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
|
|
21
|
+
// Import our atomic file operations and locking utilities
|
|
22
|
+
const { atomicWriteJson, readJsonSafe, appendJsonl } = require('./atomic-io');
|
|
23
|
+
const { acquireLock, releaseLock } = require('./file-lock');
|
|
24
|
+
const TeamHub = require('./team-hub');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* PipelineEngine class - manages reactive automation pipelines
|
|
28
|
+
*/
|
|
29
|
+
class PipelineEngine {
|
|
30
|
+
/**
|
|
31
|
+
* Creates a new PipelineEngine instance
|
|
32
|
+
*
|
|
33
|
+
* @param {string} teamName - Name of the team (default: 'default')
|
|
34
|
+
*/
|
|
35
|
+
constructor(teamName = 'default') {
|
|
36
|
+
// Set the team name
|
|
37
|
+
this.teamName = teamName;
|
|
38
|
+
|
|
39
|
+
// Create the base directory path: ~/.clearctx
|
|
40
|
+
const baseDir = path.join(os.homedir(), '.clearctx');
|
|
41
|
+
|
|
42
|
+
// Create the pipelines directory path: ~/.clearctx/team/{teamName}/pipelines
|
|
43
|
+
this.pipelinesDir = path.join(baseDir, 'team', teamName, 'pipelines');
|
|
44
|
+
|
|
45
|
+
// Set paths for pipeline data
|
|
46
|
+
this.indexPath = path.join(this.pipelinesDir, 'index.json');
|
|
47
|
+
this.logPath = path.join(this.pipelinesDir, 'log.jsonl');
|
|
48
|
+
this.locksDir = path.join(baseDir, 'locks');
|
|
49
|
+
|
|
50
|
+
// Make sure all directories exist
|
|
51
|
+
fs.mkdirSync(this.pipelinesDir, { recursive: true });
|
|
52
|
+
fs.mkdirSync(this.locksDir, { recursive: true });
|
|
53
|
+
|
|
54
|
+
// Initialize TeamHub for messaging capabilities
|
|
55
|
+
this.teamHub = new TeamHub(teamName);
|
|
56
|
+
|
|
57
|
+
// Safety limit: max number of rule evaluations per event
|
|
58
|
+
// This prevents infinite loops or runaway pipeline execution
|
|
59
|
+
this.maxEvaluationsPerCycle = 50;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a new pipeline
|
|
64
|
+
*
|
|
65
|
+
* @param {string} pipelineId - Unique identifier for the pipeline
|
|
66
|
+
* @param {Object} options - Pipeline configuration
|
|
67
|
+
* @param {Array} options.rules - Array of rule objects
|
|
68
|
+
* @param {string} options.owner - Who created this pipeline
|
|
69
|
+
* @param {boolean} [options.enabled=true] - Whether pipeline is active
|
|
70
|
+
* @returns {Object} The created pipeline object
|
|
71
|
+
* @throws {Error} If pipelineId already exists or validation fails
|
|
72
|
+
*/
|
|
73
|
+
create(pipelineId, { rules, owner, enabled = true }) {
|
|
74
|
+
// Acquire lock to prevent concurrent modifications
|
|
75
|
+
acquireLock(this.locksDir, 'pipelines-index');
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// Read current pipeline index
|
|
79
|
+
const index = readJsonSafe(this.indexPath, {});
|
|
80
|
+
|
|
81
|
+
// Check if pipeline already exists
|
|
82
|
+
if (index[pipelineId]) {
|
|
83
|
+
throw new Error(`Pipeline ${pipelineId} already exists`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validate rules - each must have required fields
|
|
87
|
+
for (const rule of rules) {
|
|
88
|
+
// Every rule must have a unique ruleId
|
|
89
|
+
if (!rule.ruleId) {
|
|
90
|
+
throw new Error('Rule missing ruleId');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Every rule must have a trigger with a type
|
|
94
|
+
if (!rule.trigger || !rule.trigger.type) {
|
|
95
|
+
throw new Error(`Rule ${rule.ruleId} missing trigger or trigger.type`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Every rule must have an action with a type
|
|
99
|
+
if (!rule.action || !rule.action.type) {
|
|
100
|
+
throw new Error(`Rule ${rule.ruleId} missing action or action.type`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Create the new pipeline object
|
|
105
|
+
const pipeline = {
|
|
106
|
+
pipelineId,
|
|
107
|
+
owner,
|
|
108
|
+
enabled,
|
|
109
|
+
createdAt: new Date().toISOString(),
|
|
110
|
+
rules,
|
|
111
|
+
executionCount: 0,
|
|
112
|
+
lastExecutedAt: null
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Add to index
|
|
116
|
+
index[pipelineId] = pipeline;
|
|
117
|
+
|
|
118
|
+
// Save the updated index
|
|
119
|
+
atomicWriteJson(this.indexPath, index);
|
|
120
|
+
|
|
121
|
+
return pipeline;
|
|
122
|
+
} finally {
|
|
123
|
+
// Always release the lock, even if an error occurred
|
|
124
|
+
releaseLock(this.locksDir, 'pipelines-index');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Evaluate an event against all enabled pipelines
|
|
130
|
+
*
|
|
131
|
+
* @param {Object} event - The event to evaluate
|
|
132
|
+
* @param {string} event.type - Event type (e.g., "artifact_published")
|
|
133
|
+
* @param {string} [event.artifactId] - Artifact identifier (if applicable)
|
|
134
|
+
* @param {string} [event.artifactType] - Artifact type (if applicable)
|
|
135
|
+
* @param {string} [event.contractId] - Contract identifier (if applicable)
|
|
136
|
+
* @param {number} [event.version] - Version number (if applicable)
|
|
137
|
+
* @param {Object} [event.data] - Additional event data
|
|
138
|
+
* @returns {Object} Summary of evaluation results
|
|
139
|
+
*/
|
|
140
|
+
evaluate(event) {
|
|
141
|
+
// Read pipeline index (no lock needed for reads - they're atomic)
|
|
142
|
+
const index = readJsonSafe(this.indexPath, {});
|
|
143
|
+
|
|
144
|
+
// Track evaluation statistics
|
|
145
|
+
let evaluationCount = 0;
|
|
146
|
+
const firedActions = [];
|
|
147
|
+
|
|
148
|
+
// Get all pipelines as an array
|
|
149
|
+
const pipelines = Object.values(index);
|
|
150
|
+
|
|
151
|
+
// Process each enabled pipeline
|
|
152
|
+
for (const pipeline of pipelines) {
|
|
153
|
+
// Skip disabled pipelines
|
|
154
|
+
if (!pipeline.enabled) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Evaluate each rule in the pipeline
|
|
159
|
+
for (const rule of pipeline.rules) {
|
|
160
|
+
// Check safety limit to prevent runaway execution
|
|
161
|
+
if (evaluationCount >= this.maxEvaluationsPerCycle) {
|
|
162
|
+
console.warn(
|
|
163
|
+
`Pipeline evaluation limit reached (${this.maxEvaluationsPerCycle}). ` +
|
|
164
|
+
`Stopping to prevent infinite loops.`
|
|
165
|
+
);
|
|
166
|
+
// Stop processing more rules
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Increment evaluation counter
|
|
171
|
+
evaluationCount++;
|
|
172
|
+
|
|
173
|
+
// Check if trigger matches this event
|
|
174
|
+
const triggerMatches = this._matchTrigger(rule.trigger, event);
|
|
175
|
+
if (!triggerMatches) {
|
|
176
|
+
continue; // This rule doesn't apply to this event
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check if condition passes (if there is one)
|
|
180
|
+
const conditionPasses = this._evaluateCondition(rule.condition, event);
|
|
181
|
+
if (!conditionPasses) {
|
|
182
|
+
continue; // Condition didn't pass, skip this rule
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Both trigger and condition passed - execute the action!
|
|
186
|
+
const actionResult = this._executeAction(rule.action, event);
|
|
187
|
+
|
|
188
|
+
// Log this execution to the append-only log
|
|
189
|
+
const logEntry = {
|
|
190
|
+
pipelineId: pipeline.pipelineId,
|
|
191
|
+
ruleId: rule.ruleId,
|
|
192
|
+
trigger: rule.trigger,
|
|
193
|
+
eventType: event.type,
|
|
194
|
+
eventSummary: {
|
|
195
|
+
artifactId: event.artifactId,
|
|
196
|
+
artifactType: event.artifactType,
|
|
197
|
+
contractId: event.contractId,
|
|
198
|
+
version: event.version
|
|
199
|
+
},
|
|
200
|
+
action: rule.action,
|
|
201
|
+
timestamp: new Date().toISOString()
|
|
202
|
+
};
|
|
203
|
+
appendJsonl(this.logPath, logEntry);
|
|
204
|
+
|
|
205
|
+
// Add to fired actions
|
|
206
|
+
firedActions.push({
|
|
207
|
+
pipelineId: pipeline.pipelineId,
|
|
208
|
+
ruleId: rule.ruleId,
|
|
209
|
+
result: actionResult
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Update pipeline execution stats
|
|
213
|
+
pipeline.executionCount++;
|
|
214
|
+
pipeline.lastExecutedAt = new Date().toISOString();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Update pipeline stats in index (need lock for writes)
|
|
219
|
+
if (firedActions.length > 0) {
|
|
220
|
+
acquireLock(this.locksDir, 'pipelines-index');
|
|
221
|
+
try {
|
|
222
|
+
const currentIndex = readJsonSafe(this.indexPath, {});
|
|
223
|
+
// Update stats for pipelines that fired
|
|
224
|
+
for (const fired of firedActions) {
|
|
225
|
+
if (currentIndex[fired.pipelineId]) {
|
|
226
|
+
currentIndex[fired.pipelineId].executionCount =
|
|
227
|
+
(currentIndex[fired.pipelineId].executionCount || 0) + 1;
|
|
228
|
+
currentIndex[fired.pipelineId].lastExecutedAt = new Date().toISOString();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
atomicWriteJson(this.indexPath, currentIndex);
|
|
232
|
+
} finally {
|
|
233
|
+
releaseLock(this.locksDir, 'pipelines-index');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Return summary of what happened
|
|
238
|
+
return {
|
|
239
|
+
evaluated: evaluationCount,
|
|
240
|
+
fired: firedActions
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* List all pipelines, optionally filtered by owner
|
|
246
|
+
*
|
|
247
|
+
* @param {Object} [options={}] - Filter options
|
|
248
|
+
* @param {string} [options.owner] - Filter by owner name
|
|
249
|
+
* @returns {Array} Array of pipeline objects
|
|
250
|
+
*/
|
|
251
|
+
list({ owner } = {}) {
|
|
252
|
+
// Read pipeline index
|
|
253
|
+
const index = readJsonSafe(this.indexPath, {});
|
|
254
|
+
|
|
255
|
+
// Get all pipelines as an array
|
|
256
|
+
let pipelines = Object.values(index);
|
|
257
|
+
|
|
258
|
+
// Filter by owner if specified
|
|
259
|
+
if (owner) {
|
|
260
|
+
pipelines = pipelines.filter(p => p.owner === owner);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return pipelines;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Pause (disable) a pipeline
|
|
268
|
+
*
|
|
269
|
+
* @param {string} pipelineId - Pipeline to pause
|
|
270
|
+
* @throws {Error} If pipeline not found
|
|
271
|
+
*/
|
|
272
|
+
pause(pipelineId) {
|
|
273
|
+
// Acquire lock for modification
|
|
274
|
+
acquireLock(this.locksDir, 'pipelines-index');
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
// Read current index
|
|
278
|
+
const index = readJsonSafe(this.indexPath, {});
|
|
279
|
+
|
|
280
|
+
// Check if pipeline exists
|
|
281
|
+
if (!index[pipelineId]) {
|
|
282
|
+
throw new Error(`Pipeline ${pipelineId} not found`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Set enabled to false
|
|
286
|
+
index[pipelineId].enabled = false;
|
|
287
|
+
|
|
288
|
+
// Save updated index
|
|
289
|
+
atomicWriteJson(this.indexPath, index);
|
|
290
|
+
} finally {
|
|
291
|
+
releaseLock(this.locksDir, 'pipelines-index');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Resume (enable) a pipeline
|
|
297
|
+
*
|
|
298
|
+
* @param {string} pipelineId - Pipeline to resume
|
|
299
|
+
* @throws {Error} If pipeline not found
|
|
300
|
+
*/
|
|
301
|
+
resume(pipelineId) {
|
|
302
|
+
// Acquire lock for modification
|
|
303
|
+
acquireLock(this.locksDir, 'pipelines-index');
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
// Read current index
|
|
307
|
+
const index = readJsonSafe(this.indexPath, {});
|
|
308
|
+
|
|
309
|
+
// Check if pipeline exists
|
|
310
|
+
if (!index[pipelineId]) {
|
|
311
|
+
throw new Error(`Pipeline ${pipelineId} not found`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Set enabled to true
|
|
315
|
+
index[pipelineId].enabled = true;
|
|
316
|
+
|
|
317
|
+
// Save updated index
|
|
318
|
+
atomicWriteJson(this.indexPath, index);
|
|
319
|
+
} finally {
|
|
320
|
+
releaseLock(this.locksDir, 'pipelines-index');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get execution log entries
|
|
326
|
+
*
|
|
327
|
+
* @param {string|null} [pipelineId=null] - Filter by pipeline (null = all)
|
|
328
|
+
* @param {number} [limit=50] - Max number of entries to return
|
|
329
|
+
* @returns {Array} Array of log entries (most recent first)
|
|
330
|
+
*/
|
|
331
|
+
getLog(pipelineId = null, limit = 50) {
|
|
332
|
+
// Check if log file exists
|
|
333
|
+
if (!fs.existsSync(this.logPath)) {
|
|
334
|
+
return []; // No log file yet
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Read the log file
|
|
338
|
+
const content = fs.readFileSync(this.logPath, 'utf-8');
|
|
339
|
+
const lines = content.trim().split('\n').filter(line => line.length > 0);
|
|
340
|
+
|
|
341
|
+
// Parse each line as JSON
|
|
342
|
+
let entries = lines.map(line => JSON.parse(line));
|
|
343
|
+
|
|
344
|
+
// Filter by pipelineId if specified
|
|
345
|
+
if (pipelineId) {
|
|
346
|
+
entries = entries.filter(e => e.pipelineId === pipelineId);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Return last N entries in reverse order (most recent first)
|
|
350
|
+
return entries.slice(-limit).reverse();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ========== HELPER METHODS ==========
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Check if a trigger matches an event
|
|
357
|
+
*
|
|
358
|
+
* @private
|
|
359
|
+
* @param {Object} trigger - Rule trigger configuration
|
|
360
|
+
* @param {Object} event - Event to match against
|
|
361
|
+
* @returns {boolean} True if trigger matches event
|
|
362
|
+
*/
|
|
363
|
+
_matchTrigger(trigger, event) {
|
|
364
|
+
// First check: trigger type must match event type
|
|
365
|
+
if (trigger.type !== event.type) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Check artifact type if specified in trigger
|
|
370
|
+
if (trigger.artifactType && event.artifactType) {
|
|
371
|
+
if (trigger.artifactType !== event.artifactType) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Check artifact ID if specified in trigger (supports wildcards)
|
|
377
|
+
if (trigger.artifactId && event.artifactId) {
|
|
378
|
+
// Convert wildcard pattern to regex
|
|
379
|
+
// Example: "api-contract-*" becomes /^api-contract-.*$/
|
|
380
|
+
const pattern = trigger.artifactId.replace(/\*/g, '.*');
|
|
381
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
382
|
+
|
|
383
|
+
if (!regex.test(event.artifactId)) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Check contract ID if specified in trigger
|
|
389
|
+
if (trigger.contractId && event.contractId) {
|
|
390
|
+
if (trigger.contractId !== event.contractId) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// All checks passed!
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Evaluate a condition expression
|
|
401
|
+
*
|
|
402
|
+
* @private
|
|
403
|
+
* @param {string|null} condition - Condition expression or null
|
|
404
|
+
* @param {Object} event - Event data to evaluate against
|
|
405
|
+
* @returns {boolean} True if condition passes (or is null)
|
|
406
|
+
*/
|
|
407
|
+
_evaluateCondition(condition, event) {
|
|
408
|
+
// If no condition, always pass
|
|
409
|
+
if (!condition || condition === null || condition === undefined) {
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Parse simple expressions like "data.failed > 0" or "version >= 3"
|
|
414
|
+
// Supported operators: >, <, >=, <=, ==, !=
|
|
415
|
+
const operators = ['>=', '<=', '==', '!=', '>', '<'];
|
|
416
|
+
let operator = null;
|
|
417
|
+
let parts = null;
|
|
418
|
+
|
|
419
|
+
// Find which operator is used
|
|
420
|
+
for (const op of operators) {
|
|
421
|
+
if (condition.includes(op)) {
|
|
422
|
+
operator = op;
|
|
423
|
+
parts = condition.split(op).map(s => s.trim());
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// If no operator found or invalid split, fail safely
|
|
429
|
+
if (!operator || !parts || parts.length !== 2) {
|
|
430
|
+
console.warn(`Invalid condition expression: ${condition}`);
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const leftSide = parts[0];
|
|
435
|
+
const rightSide = parts[1];
|
|
436
|
+
|
|
437
|
+
// Evaluate left side (get value from event)
|
|
438
|
+
let leftValue;
|
|
439
|
+
if (leftSide.startsWith('data.')) {
|
|
440
|
+
// Extract from event.data
|
|
441
|
+
const fieldName = leftSide.substring(5); // Remove "data."
|
|
442
|
+
leftValue = event.data ? event.data[fieldName] : undefined;
|
|
443
|
+
} else if (leftSide === 'version') {
|
|
444
|
+
leftValue = event.version;
|
|
445
|
+
} else {
|
|
446
|
+
// Unknown left side
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// If left value is undefined, condition fails
|
|
451
|
+
if (leftValue === undefined) {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Parse right side as number
|
|
456
|
+
const rightValue = parseFloat(rightSide);
|
|
457
|
+
if (isNaN(rightValue)) {
|
|
458
|
+
console.warn(`Right side is not a number: ${rightSide}`);
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Convert left value to number
|
|
463
|
+
const leftNum = parseFloat(leftValue);
|
|
464
|
+
if (isNaN(leftNum)) {
|
|
465
|
+
console.warn(`Left side is not a number: ${leftValue}`);
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Evaluate based on operator
|
|
470
|
+
switch (operator) {
|
|
471
|
+
case '>':
|
|
472
|
+
return leftNum > rightValue;
|
|
473
|
+
case '<':
|
|
474
|
+
return leftNum < rightValue;
|
|
475
|
+
case '>=':
|
|
476
|
+
return leftNum >= rightValue;
|
|
477
|
+
case '<=':
|
|
478
|
+
return leftNum <= rightValue;
|
|
479
|
+
case '==':
|
|
480
|
+
return leftNum === rightValue;
|
|
481
|
+
case '!=':
|
|
482
|
+
return leftNum !== rightValue;
|
|
483
|
+
default:
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Execute an action or return it for caller to handle
|
|
490
|
+
*
|
|
491
|
+
* @private
|
|
492
|
+
* @param {Object} action - Action configuration
|
|
493
|
+
* @param {Object} event - Event that triggered this action
|
|
494
|
+
* @returns {Object} Action result
|
|
495
|
+
*/
|
|
496
|
+
_executeAction(action, event) {
|
|
497
|
+
// Get the action type
|
|
498
|
+
const actionType = action.type;
|
|
499
|
+
|
|
500
|
+
// Handle "notify_session" - send direct message via TeamHub
|
|
501
|
+
if (actionType === 'notify_session') {
|
|
502
|
+
// Interpolate message template with event data
|
|
503
|
+
const message = this._interpolate(action.message, event);
|
|
504
|
+
|
|
505
|
+
// Send direct message to target session
|
|
506
|
+
this.teamHub.sendDirect(
|
|
507
|
+
action.pipelineId || 'pipeline', // From: pipeline system
|
|
508
|
+
action.target, // To: target session
|
|
509
|
+
message
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
executed: true,
|
|
514
|
+
type: 'notify_session'
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Handle "broadcast" - send message to all team members
|
|
519
|
+
if (actionType === 'broadcast') {
|
|
520
|
+
// Interpolate message template with event data
|
|
521
|
+
const message = this._interpolate(action.message, event);
|
|
522
|
+
|
|
523
|
+
// Broadcast to all team members
|
|
524
|
+
this.teamHub.sendBroadcast(
|
|
525
|
+
'pipeline', // From: pipeline system
|
|
526
|
+
message
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
executed: true,
|
|
531
|
+
type: 'broadcast'
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Handle "reopen_contract" - return for caller to execute
|
|
536
|
+
// We don't import ContractStore here to avoid circular dependencies
|
|
537
|
+
if (actionType === 'reopen_contract') {
|
|
538
|
+
return {
|
|
539
|
+
executed: false,
|
|
540
|
+
type: 'reopen_contract',
|
|
541
|
+
params: action
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Handle "create_contract" - return for caller to execute
|
|
546
|
+
if (actionType === 'create_contract') {
|
|
547
|
+
return {
|
|
548
|
+
executed: false,
|
|
549
|
+
type: 'create_contract',
|
|
550
|
+
params: action
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Handle "invalidate_downstream" - return for caller to execute
|
|
555
|
+
if (actionType === 'invalidate_downstream') {
|
|
556
|
+
return {
|
|
557
|
+
executed: false,
|
|
558
|
+
type: 'invalidate_downstream',
|
|
559
|
+
params: action
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Unknown action type
|
|
564
|
+
console.warn(`Unknown action type: ${actionType}`);
|
|
565
|
+
return {
|
|
566
|
+
executed: false,
|
|
567
|
+
type: 'unknown',
|
|
568
|
+
error: `Unknown action type: ${actionType}`
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Interpolate template variables in a message
|
|
574
|
+
*
|
|
575
|
+
* @private
|
|
576
|
+
* @param {string} template - Message template with ${variable} placeholders
|
|
577
|
+
* @param {Object} event - Event data to interpolate from
|
|
578
|
+
* @returns {string} Interpolated message
|
|
579
|
+
*/
|
|
580
|
+
_interpolate(template, event) {
|
|
581
|
+
// Replace ${data.fieldName} with values from event.data
|
|
582
|
+
let result = template.replace(/\$\{data\.(\w+)\}/g, (match, fieldName) => {
|
|
583
|
+
if (event.data && event.data[fieldName] !== undefined) {
|
|
584
|
+
return event.data[fieldName];
|
|
585
|
+
}
|
|
586
|
+
return match; // Keep placeholder if no value found
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// Replace ${version} with event.version
|
|
590
|
+
result = result.replace(/\$\{version\}/g, () => {
|
|
591
|
+
if (event.version !== undefined) {
|
|
592
|
+
return event.version;
|
|
593
|
+
}
|
|
594
|
+
return '${version}'; // Keep placeholder if no value
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Replace ${artifactId} with event.artifactId
|
|
598
|
+
result = result.replace(/\$\{artifactId\}/g, () => {
|
|
599
|
+
if (event.artifactId !== undefined) {
|
|
600
|
+
return event.artifactId;
|
|
601
|
+
}
|
|
602
|
+
return '${artifactId}'; // Keep placeholder if no value
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// Replace ${contractId} with event.contractId
|
|
606
|
+
result = result.replace(/\$\{contractId\}/g, () => {
|
|
607
|
+
if (event.contractId !== undefined) {
|
|
608
|
+
return event.contractId;
|
|
609
|
+
}
|
|
610
|
+
return '${contractId}'; // Keep placeholder if no value
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
return result;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Export the PipelineEngine class
|
|
618
|
+
module.exports = PipelineEngine;
|