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,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dependency-resolver.js
|
|
3
|
+
* Layer 2: Dependency Resolution Engine for clearctx
|
|
4
|
+
*
|
|
5
|
+
* This is the CORE algorithm that automatically advances contracts when their
|
|
6
|
+
* dependencies are satisfied. It runs after every artifact publish and contract
|
|
7
|
+
* status change to check if new work can be started or completed.
|
|
8
|
+
*
|
|
9
|
+
* Think of it like a project manager who:
|
|
10
|
+
* 1. Checks if blocked tasks can now start (dependencies satisfied)
|
|
11
|
+
* 2. Checks if running tasks are taking too long (timeouts)
|
|
12
|
+
* 3. Checks if completed work meets acceptance criteria (auto-completion)
|
|
13
|
+
* 4. Notifies team members when their tasks are ready
|
|
14
|
+
*
|
|
15
|
+
* @class DependencyResolver
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const ArtifactStore = require('./artifact-store');
|
|
19
|
+
const ContractStore = require('./contract-store');
|
|
20
|
+
const TeamHub = require('./team-hub');
|
|
21
|
+
const { acquireLock, releaseLock } = require('./file-lock');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* DependencyResolver - Automatically advances contracts when dependencies are met
|
|
27
|
+
*
|
|
28
|
+
* This class is called after:
|
|
29
|
+
* - An artifact is published (might satisfy artifact dependencies)
|
|
30
|
+
* - A contract changes status (might satisfy contract dependencies)
|
|
31
|
+
*
|
|
32
|
+
* It performs a cascade of checks and transitions:
|
|
33
|
+
* - Pending contracts → Ready (when dependencies satisfied)
|
|
34
|
+
* - In-progress contracts → Failed (when timeout exceeded)
|
|
35
|
+
* - In-progress contracts → Completed (when acceptance criteria met)
|
|
36
|
+
*/
|
|
37
|
+
class DependencyResolver {
|
|
38
|
+
/**
|
|
39
|
+
* Create a new DependencyResolver
|
|
40
|
+
*
|
|
41
|
+
* @param {string} teamName - The team name (default: 'default')
|
|
42
|
+
*/
|
|
43
|
+
constructor(teamName = 'default') {
|
|
44
|
+
// Store the team name for later use
|
|
45
|
+
this.teamName = teamName;
|
|
46
|
+
|
|
47
|
+
// Create instances of the three core stores
|
|
48
|
+
// These let us read artifacts, contracts, and send messages
|
|
49
|
+
this.artifacts = new ArtifactStore(teamName);
|
|
50
|
+
this.contracts = new ContractStore(teamName);
|
|
51
|
+
this.teamHub = new TeamHub(teamName);
|
|
52
|
+
|
|
53
|
+
// Maximum cascade depth - prevents infinite loops
|
|
54
|
+
// If resolve() calls itself too many times, we stop
|
|
55
|
+
this.maxCascadeDepth = Number(process.env.CMS_MAX_CASCADE_DEPTH) || 10;
|
|
56
|
+
|
|
57
|
+
// Pipeline engine reference (set later to avoid circular require)
|
|
58
|
+
this.pipelineEngine = null;
|
|
59
|
+
|
|
60
|
+
// Set up the locks directory path
|
|
61
|
+
const baseDir = path.join(os.homedir(), '.clearctx');
|
|
62
|
+
this.locksDir = path.join(baseDir, 'team', teamName, 'locks');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Set the pipeline engine reference
|
|
67
|
+
* This is called after the pipeline engine is created to avoid circular dependencies
|
|
68
|
+
*
|
|
69
|
+
* @param {Object} engine - The pipeline engine instance
|
|
70
|
+
*/
|
|
71
|
+
setPipelineEngine(engine) {
|
|
72
|
+
this.pipelineEngine = engine;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Main resolution method - checks all contracts and advances them if possible
|
|
77
|
+
*
|
|
78
|
+
* This is the heart of the dependency resolver. It:
|
|
79
|
+
* 1. Checks for timeouts
|
|
80
|
+
* 2. Advances pending contracts to ready
|
|
81
|
+
* 3. Auto-completes contracts that meet criteria
|
|
82
|
+
* 4. Cascades to handle newly completed contracts
|
|
83
|
+
*
|
|
84
|
+
* @param {Object} trigger - What triggered this resolution (for debugging)
|
|
85
|
+
* @param {number} depth - Current cascade depth (prevents infinite loops)
|
|
86
|
+
* @returns {Promise<Object>} Object with transitions array and depth
|
|
87
|
+
*/
|
|
88
|
+
async resolve(trigger = {}, depth = 0) {
|
|
89
|
+
// Step 1: Check if we've cascaded too deep (safety check)
|
|
90
|
+
if (depth >= this.maxCascadeDepth) {
|
|
91
|
+
// Send a warning message to the orchestrator
|
|
92
|
+
const orchestratorInbox = this.teamHub.getInbox('orchestrator');
|
|
93
|
+
if (orchestratorInbox) {
|
|
94
|
+
this.teamHub.sendDirect(
|
|
95
|
+
'dependency-resolver',
|
|
96
|
+
'orchestrator',
|
|
97
|
+
`Cascade depth limit reached (${this.maxCascadeDepth}). Some contracts may need manual intervention.`,
|
|
98
|
+
{ priority: 'urgent' }
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { cascadeLimited: true, depth };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Step 2: Track all transitions that happen in this cycle
|
|
106
|
+
// A transition is when a contract moves from one status to another
|
|
107
|
+
const transitions = [];
|
|
108
|
+
|
|
109
|
+
// Step 3: Check for timed-out contracts
|
|
110
|
+
// Get all contracts that are currently in progress
|
|
111
|
+
const inProgressContracts = this.contracts.list({ status: 'in_progress' });
|
|
112
|
+
|
|
113
|
+
for (const contract of inProgressContracts) {
|
|
114
|
+
// Only check timeout if timeoutMs is set
|
|
115
|
+
if (contract.timeoutMs && contract.startedAt) {
|
|
116
|
+
// Calculate how long the contract has been running
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
const startedTime = new Date(contract.startedAt).getTime();
|
|
119
|
+
const elapsedTime = now - startedTime;
|
|
120
|
+
|
|
121
|
+
// If elapsed time exceeds timeout, fail the contract
|
|
122
|
+
if (elapsedTime > contract.timeoutMs) {
|
|
123
|
+
try {
|
|
124
|
+
this.contracts.fail(contract.contractId, 'Timeout exceeded');
|
|
125
|
+
|
|
126
|
+
// Record this transition
|
|
127
|
+
transitions.push({
|
|
128
|
+
contractId: contract.contractId,
|
|
129
|
+
from: 'in_progress',
|
|
130
|
+
to: 'failed',
|
|
131
|
+
reason: 'timeout'
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Notify the assignee that their contract timed out
|
|
135
|
+
this.teamHub.sendDirect(
|
|
136
|
+
'dependency-resolver',
|
|
137
|
+
contract.assignee,
|
|
138
|
+
`Contract "${contract.contractId}" timed out after ${contract.timeoutMs}ms`,
|
|
139
|
+
{ priority: 'urgent' }
|
|
140
|
+
);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
// If failing the contract throws an error, log it but continue
|
|
143
|
+
// (contract might have been modified by another process)
|
|
144
|
+
console.error(`Failed to timeout contract ${contract.contractId}:`, err.message);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Step 4: Check pending contracts for satisfied dependencies
|
|
151
|
+
// Get all contracts that are waiting for dependencies
|
|
152
|
+
const pendingContracts = this.contracts.list({ status: 'pending' });
|
|
153
|
+
|
|
154
|
+
for (const contract of pendingContracts) {
|
|
155
|
+
// Check if ALL dependencies are satisfied
|
|
156
|
+
let allSatisfied = true;
|
|
157
|
+
|
|
158
|
+
// Loop through each dependency
|
|
159
|
+
for (const dep of contract.dependencies) {
|
|
160
|
+
const satisfied = this._checkDependency(dep);
|
|
161
|
+
if (!satisfied) {
|
|
162
|
+
allSatisfied = false;
|
|
163
|
+
break; // No need to check more if one is not satisfied
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// If all dependencies are satisfied, transition to ready
|
|
168
|
+
if (allSatisfied) {
|
|
169
|
+
try {
|
|
170
|
+
// Use the helper method to transition pending → ready
|
|
171
|
+
this._transitionToReady(contract.contractId);
|
|
172
|
+
|
|
173
|
+
// Record this transition
|
|
174
|
+
transitions.push({
|
|
175
|
+
contractId: contract.contractId,
|
|
176
|
+
from: 'pending',
|
|
177
|
+
to: 'ready',
|
|
178
|
+
reason: 'dependencies_satisfied'
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Notify the assignee that their contract is ready to start
|
|
182
|
+
this.teamHub.sendDirect(
|
|
183
|
+
'dependency-resolver',
|
|
184
|
+
contract.assignee,
|
|
185
|
+
`Contract "${contract.contractId}" is ready to start (all dependencies satisfied)`,
|
|
186
|
+
{ priority: 'normal' }
|
|
187
|
+
);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
// If transition fails, log but continue
|
|
190
|
+
console.error(`Failed to transition contract ${contract.contractId} to ready:`, err.message);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Step 5: Evaluate acceptance criteria for in_progress contracts
|
|
196
|
+
// Re-fetch in-progress contracts (some might have timed out in step 3)
|
|
197
|
+
const currentInProgress = this.contracts.list({ status: 'in_progress' });
|
|
198
|
+
|
|
199
|
+
for (const contract of currentInProgress) {
|
|
200
|
+
// Skip if autoComplete is disabled
|
|
201
|
+
if (!contract.autoComplete) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check if ALL acceptance criteria are met
|
|
206
|
+
let allCriteriaMet = true;
|
|
207
|
+
|
|
208
|
+
// If there are no criteria, we can't auto-complete
|
|
209
|
+
if (contract.acceptanceCriteria.length === 0) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Loop through each criterion
|
|
214
|
+
for (const criterion of contract.acceptanceCriteria) {
|
|
215
|
+
const met = this._evaluateCriterion(criterion, contract);
|
|
216
|
+
if (!met) {
|
|
217
|
+
allCriteriaMet = false;
|
|
218
|
+
break; // No need to check more if one is not met
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// If all criteria met, auto-complete the contract
|
|
223
|
+
if (allCriteriaMet) {
|
|
224
|
+
try {
|
|
225
|
+
this.contracts.complete(contract.contractId, {
|
|
226
|
+
summary: 'Auto-completed: all acceptance criteria met'
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Record this transition
|
|
230
|
+
transitions.push({
|
|
231
|
+
contractId: contract.contractId,
|
|
232
|
+
from: 'in_progress',
|
|
233
|
+
to: 'completed',
|
|
234
|
+
reason: 'auto_complete'
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Notify both assignee and assigner
|
|
238
|
+
this.teamHub.sendDirect(
|
|
239
|
+
'dependency-resolver',
|
|
240
|
+
contract.assignee,
|
|
241
|
+
`Contract "${contract.contractId}" auto-completed (all criteria met)`,
|
|
242
|
+
{ priority: 'normal' }
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
this.teamHub.sendDirect(
|
|
246
|
+
'dependency-resolver',
|
|
247
|
+
contract.assigner,
|
|
248
|
+
`Contract "${contract.contractId}" completed by ${contract.assignee}`,
|
|
249
|
+
{ priority: 'normal' }
|
|
250
|
+
);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.error(`Failed to auto-complete contract ${contract.contractId}:`, err.message);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Step 6: Handle cascading and notifications
|
|
258
|
+
if (transitions.length > 0) {
|
|
259
|
+
// Send summary notification to orchestrator if it exists
|
|
260
|
+
try {
|
|
261
|
+
this.teamHub.sendDirect(
|
|
262
|
+
'dependency-resolver',
|
|
263
|
+
'orchestrator',
|
|
264
|
+
`Resolution cycle complete: ${transitions.length} transition(s) at depth ${depth}`,
|
|
265
|
+
{ priority: 'normal' }
|
|
266
|
+
);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
// Orchestrator might not exist, that's okay
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// If pipeline engine is configured, evaluate it for each transition
|
|
272
|
+
if (this.pipelineEngine) {
|
|
273
|
+
for (const transition of transitions) {
|
|
274
|
+
try {
|
|
275
|
+
await this.pipelineEngine.evaluate(transition);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.error('Pipeline evaluation failed:', err.message);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Cascade: recursively call resolve for any completed contracts
|
|
283
|
+
// This allows newly completed contracts to unlock other pending ones
|
|
284
|
+
const completedTransitions = transitions.filter(t => t.to === 'completed');
|
|
285
|
+
|
|
286
|
+
if (completedTransitions.length > 0) {
|
|
287
|
+
// Recursively resolve with increased depth
|
|
288
|
+
await this.resolve({ cascadeFrom: completedTransitions }, depth + 1);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Step 7: Return results
|
|
293
|
+
return { transitions, depth };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Check if a single dependency is satisfied
|
|
298
|
+
*
|
|
299
|
+
* @private
|
|
300
|
+
* @param {Object} dep - The dependency object
|
|
301
|
+
* @param {string} dep.type - Type of dependency ('artifact' or 'contract')
|
|
302
|
+
* @param {string} dep.artifactId - Artifact ID (if type is 'artifact')
|
|
303
|
+
* @param {string} dep.contractId - Contract ID (if type is 'contract')
|
|
304
|
+
* @returns {boolean} True if dependency is satisfied, false otherwise
|
|
305
|
+
*/
|
|
306
|
+
_checkDependency(dep) {
|
|
307
|
+
// Handle artifact dependency
|
|
308
|
+
if (dep.type === 'artifact') {
|
|
309
|
+
// Check if the artifact exists
|
|
310
|
+
const artifact = this.artifacts.get(dep.artifactId);
|
|
311
|
+
return artifact !== null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Handle contract dependency
|
|
315
|
+
if (dep.type === 'contract') {
|
|
316
|
+
// Check if the referenced contract is completed
|
|
317
|
+
const contract = this.contracts.get(dep.contractId);
|
|
318
|
+
return contract !== null && contract.status === 'completed';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Unknown dependency type - consider it unsatisfied
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Evaluate a single acceptance criterion
|
|
327
|
+
*
|
|
328
|
+
* @private
|
|
329
|
+
* @param {Object} criterion - The acceptance criterion object
|
|
330
|
+
* @param {string} criterion.type - Type of criterion
|
|
331
|
+
* @param {Object} contract - The contract being evaluated
|
|
332
|
+
* @returns {boolean} True if criterion is met, false otherwise
|
|
333
|
+
*/
|
|
334
|
+
_evaluateCriterion(criterion, contract) {
|
|
335
|
+
// Handle 'artifact_published' criterion
|
|
336
|
+
// Checks if a specific artifact exists
|
|
337
|
+
if (criterion.type === 'artifact_published') {
|
|
338
|
+
const artifact = this.artifacts.get(criterion.artifactId);
|
|
339
|
+
return artifact !== null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Handle 'tests_passing' criterion
|
|
343
|
+
// Checks if test-results artifact shows no failures
|
|
344
|
+
if (criterion.type === 'tests_passing') {
|
|
345
|
+
// Look for a test-results artifact
|
|
346
|
+
const testArtifacts = this.artifacts.list({ type: 'test-results' });
|
|
347
|
+
|
|
348
|
+
// If no test results, criterion not met
|
|
349
|
+
if (testArtifacts.length === 0) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Get the latest test results
|
|
354
|
+
const latestTest = testArtifacts[testArtifacts.length - 1];
|
|
355
|
+
const testData = this.artifacts.get(latestTest.artifactId);
|
|
356
|
+
|
|
357
|
+
if (!testData || !testData.data) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Check if failures are zero (or within threshold if specified)
|
|
362
|
+
const threshold = criterion.maxFailures || 0;
|
|
363
|
+
return testData.data.failed <= threshold;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Handle 'contract_completed' criterion
|
|
367
|
+
// Checks if another contract is completed
|
|
368
|
+
if (criterion.type === 'contract_completed') {
|
|
369
|
+
const referencedContract = this.contracts.get(criterion.contractId);
|
|
370
|
+
return referencedContract !== null && referencedContract.status === 'completed';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Handle 'all_outputs_published' criterion
|
|
374
|
+
// Checks if all expected outputs have matching artifacts
|
|
375
|
+
if (criterion.type === 'all_outputs_published') {
|
|
376
|
+
// Get the expected outputs from the contract
|
|
377
|
+
const expectedOutputs = contract.expectedOutputs || [];
|
|
378
|
+
|
|
379
|
+
if (expectedOutputs.length === 0) {
|
|
380
|
+
// No outputs expected - criterion met
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Check each expected output
|
|
385
|
+
for (const output of expectedOutputs) {
|
|
386
|
+
// Each output should have an artifactId
|
|
387
|
+
if (output.artifactId) {
|
|
388
|
+
const artifact = this.artifacts.get(output.artifactId);
|
|
389
|
+
if (!artifact) {
|
|
390
|
+
return false; // Missing artifact
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return true; // All outputs published
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Unknown criterion type - consider it not met
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Transition a contract from pending to ready
|
|
404
|
+
* Uses locking to ensure thread-safety
|
|
405
|
+
*
|
|
406
|
+
* @private
|
|
407
|
+
* @param {string} contractId - The contract to transition
|
|
408
|
+
*/
|
|
409
|
+
_transitionToReady(contractId) {
|
|
410
|
+
// Acquire lock on contracts index
|
|
411
|
+
acquireLock(this.locksDir, 'contracts-index');
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
// Get the contract
|
|
415
|
+
const contract = this.contracts.get(contractId);
|
|
416
|
+
|
|
417
|
+
if (!contract) {
|
|
418
|
+
throw new Error(`Contract ${contractId} not found`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Verify it's in pending status
|
|
422
|
+
if (contract.status !== 'pending') {
|
|
423
|
+
throw new Error(`Contract ${contractId} is not pending (status: ${contract.status})`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Read the entire index
|
|
427
|
+
const { readJsonSafe, atomicWriteJson } = require('./atomic-io');
|
|
428
|
+
const indexPath = path.join(
|
|
429
|
+
os.homedir(),
|
|
430
|
+
'.clearctx',
|
|
431
|
+
'team',
|
|
432
|
+
this.teamName,
|
|
433
|
+
'contracts',
|
|
434
|
+
'index.json'
|
|
435
|
+
);
|
|
436
|
+
const index = readJsonSafe(indexPath, {});
|
|
437
|
+
|
|
438
|
+
// Update the contract status
|
|
439
|
+
index[contractId].status = 'ready';
|
|
440
|
+
index[contractId].updatedAt = new Date().toISOString();
|
|
441
|
+
|
|
442
|
+
// Save the index
|
|
443
|
+
atomicWriteJson(indexPath, index);
|
|
444
|
+
|
|
445
|
+
} finally {
|
|
446
|
+
// Always release the lock
|
|
447
|
+
releaseLock(this.locksDir, 'contracts-index');
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Export the DependencyResolver class
|
|
453
|
+
module.exports = DependencyResolver;
|