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.
@@ -0,0 +1,402 @@
1
+ /**
2
+ * lineage-graph.js
3
+ * Layer 3: Artifact Lineage Graph - Track data provenance and impact analysis
4
+ *
5
+ * This module provides methods to traverse the lineage graph embedded in artifact version files.
6
+ * The lineage graph is NOT a separate index - it's stored directly in each version file's
7
+ * lineage field (producedBy and derivedFrom).
8
+ *
9
+ * Key concepts:
10
+ * - Upstream: What artifacts was this derived from? (follow derivedFrom)
11
+ * - Downstream: What artifacts depend on this? (scan all artifacts for references)
12
+ * - Impact: What would be affected if this artifact changes?
13
+ * - Stale: Which artifacts reference outdated versions?
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ // Import Layer 2 ArtifactStore
20
+ const ArtifactStore = require('./artifact-store');
21
+
22
+ /**
23
+ * LineageGraph provides methods to traverse artifact dependencies and analyze impact
24
+ *
25
+ * The lineage graph is stored in each artifact version file:
26
+ * {
27
+ * lineage: {
28
+ * producedBy: { contractId, session } | null,
29
+ * derivedFrom: ["other-artifact@v1", ...] // format: "artifactId@vN"
30
+ * }
31
+ * }
32
+ */
33
+ class LineageGraph {
34
+ /**
35
+ * Create a new LineageGraph instance
36
+ * @param {string} teamName - Name of the team (default: 'default')
37
+ */
38
+ constructor(teamName = 'default') {
39
+ // Create an ArtifactStore instance to access artifact data
40
+ this.artifacts = new ArtifactStore(teamName);
41
+
42
+ // Store team name for reference
43
+ this.teamName = teamName;
44
+
45
+ // Store the data directory path for scanning operations
46
+ this.dataDir = this.artifacts.dataDir;
47
+ }
48
+
49
+ /**
50
+ * Parse a lineage reference like "artifactId@vN" into components
51
+ * @param {string} ref - Reference string (format: "artifactId@vN" or just "artifactId")
52
+ * @returns {Object} { artifactId, version } where version is number or null for "latest"
53
+ * @private
54
+ */
55
+ _parseRef(ref) {
56
+ // Check if the reference includes a version suffix
57
+ const atIndex = ref.lastIndexOf('@v');
58
+
59
+ if (atIndex === -1) {
60
+ // No version specified - means "latest"
61
+ return { artifactId: ref, version: null };
62
+ }
63
+
64
+ // Split into artifact ID and version
65
+ const artifactId = ref.substring(0, atIndex);
66
+ const versionStr = ref.substring(atIndex + 2); // Skip "@v"
67
+ const version = parseInt(versionStr, 10);
68
+
69
+ return { artifactId, version };
70
+ }
71
+
72
+ /**
73
+ * Scan all artifact directories and collect latest version files
74
+ * @returns {Map<string, Object>} Map of artifactId -> latest version data
75
+ * @private
76
+ */
77
+ _scanAllVersionFiles() {
78
+ const results = new Map();
79
+
80
+ // Check if data directory exists
81
+ if (!fs.existsSync(this.dataDir)) {
82
+ return results; // Empty map
83
+ }
84
+
85
+ // Get all artifact directories
86
+ const artifactDirs = fs.readdirSync(this.dataDir);
87
+
88
+ // Process each artifact directory
89
+ for (const artifactId of artifactDirs) {
90
+ const artifactDir = path.join(this.dataDir, artifactId);
91
+
92
+ // Skip if not a directory
93
+ try {
94
+ if (!fs.statSync(artifactDir).isDirectory()) {
95
+ continue;
96
+ }
97
+ } catch (err) {
98
+ continue; // Skip if can't access
99
+ }
100
+
101
+ // Get the latest version from the artifact store
102
+ const latestVersion = this.artifacts.get(artifactId);
103
+
104
+ if (latestVersion) {
105
+ results.set(artifactId, latestVersion);
106
+ }
107
+ }
108
+
109
+ return results;
110
+ }
111
+
112
+ /**
113
+ * Get the upstream dependency tree for an artifact
114
+ * Shows what artifacts this one was derived from, recursively
115
+ * @param {string} artifactId - The artifact identifier
116
+ * @param {number|null} [version=null] - Version number (null = latest)
117
+ * @returns {Object|null} Tree: { artifactId, version, parents: [...] } or null if not found
118
+ */
119
+ getUpstream(artifactId, version = null) {
120
+ // Use a Set to track visited nodes and prevent cycles
121
+ const visited = new Set();
122
+
123
+ /**
124
+ * Recursive helper to build upstream tree
125
+ * @param {string} id - Artifact ID
126
+ * @param {number|null} ver - Version number
127
+ * @returns {Object|null} Tree node
128
+ */
129
+ const buildUpstreamTree = (id, ver) => {
130
+ // Get the artifact version
131
+ const versionData = this.artifacts.get(id, ver);
132
+
133
+ if (!versionData) {
134
+ return null; // Artifact not found
135
+ }
136
+
137
+ // Create a unique key for cycle detection
138
+ const nodeKey = `${id}@v${versionData.version}`;
139
+
140
+ // Check if we've already visited this node (cycle detection)
141
+ if (visited.has(nodeKey)) {
142
+ return { artifactId: id, version: versionData.version, parents: [], cycleDetected: true };
143
+ }
144
+
145
+ // Mark this node as visited
146
+ visited.add(nodeKey);
147
+
148
+ // Get the derivedFrom array from lineage
149
+ const derivedFrom = versionData.lineage?.derivedFrom || [];
150
+
151
+ // Build parent nodes recursively
152
+ const parents = [];
153
+ for (const ref of derivedFrom) {
154
+ const { artifactId: parentId, version: parentVer } = this._parseRef(ref);
155
+ const parentTree = buildUpstreamTree(parentId, parentVer);
156
+
157
+ if (parentTree) {
158
+ parents.push(parentTree);
159
+ }
160
+ }
161
+
162
+ // Return the tree node
163
+ return {
164
+ artifactId: id,
165
+ version: versionData.version,
166
+ parents
167
+ };
168
+ };
169
+
170
+ // Build and return the tree
171
+ return buildUpstreamTree(artifactId, version);
172
+ }
173
+
174
+ /**
175
+ * Get the downstream dependency tree for an artifact
176
+ * Shows what artifacts depend on this one, recursively
177
+ * @param {string} artifactId - The artifact identifier
178
+ * @param {number|null} [version=null] - Version number (null = latest)
179
+ * @returns {Object|null} Tree: { artifactId, version, dependents: [...] } or null if not found
180
+ */
181
+ getDownstream(artifactId, version = null) {
182
+ // Get the artifact version to find its actual version number
183
+ const versionData = this.artifacts.get(artifactId, version);
184
+
185
+ if (!versionData) {
186
+ return null; // Artifact not found
187
+ }
188
+
189
+ const targetVersion = versionData.version;
190
+
191
+ // Use a Set to track visited nodes and prevent cycles
192
+ const visited = new Set();
193
+
194
+ /**
195
+ * Recursive helper to build downstream tree
196
+ * @param {string} id - Artifact ID
197
+ * @param {number} ver - Version number
198
+ * @returns {Object} Tree node
199
+ */
200
+ const buildDownstreamTree = (id, ver) => {
201
+ // Create a unique key for cycle detection
202
+ const nodeKey = `${id}@v${ver}`;
203
+
204
+ // Check if we've already visited this node (cycle detection)
205
+ if (visited.has(nodeKey)) {
206
+ return { artifactId: id, version: ver, dependents: [], cycleDetected: true };
207
+ }
208
+
209
+ // Mark this node as visited
210
+ visited.add(nodeKey);
211
+
212
+ // Scan all artifacts to find which ones reference this artifact
213
+ const allArtifacts = this._scanAllVersionFiles();
214
+ const dependents = [];
215
+
216
+ for (const [depId, depData] of allArtifacts.entries()) {
217
+ // Get the derivedFrom array
218
+ const derivedFrom = depData.lineage?.derivedFrom || [];
219
+
220
+ // Check if this artifact is in the derivedFrom list
221
+ for (const ref of derivedFrom) {
222
+ const { artifactId: refId, version: refVer } = this._parseRef(ref);
223
+
224
+ // Check if this reference matches our target artifact
225
+ // Match if: same artifactId AND (version matches OR ref has no version specified)
226
+ if (refId === id && (refVer === ver || refVer === null)) {
227
+ // This artifact depends on our target
228
+ const depTree = buildDownstreamTree(depId, depData.version);
229
+ dependents.push(depTree);
230
+ break; // Don't check other refs in this artifact's derivedFrom
231
+ }
232
+ }
233
+ }
234
+
235
+ // Return the tree node
236
+ return {
237
+ artifactId: id,
238
+ version: ver,
239
+ dependents
240
+ };
241
+ };
242
+
243
+ // Build and return the tree
244
+ return buildDownstreamTree(artifactId, targetVersion);
245
+ }
246
+
247
+ /**
248
+ * Calculate the impact radius of an artifact
249
+ * Shows how many artifacts would be affected if this artifact changes
250
+ * @param {string} artifactId - The artifact identifier
251
+ * @returns {Object} { artifactId, impactRadius, affectedArtifacts, affectedContracts }
252
+ */
253
+ getImpact(artifactId) {
254
+ // Get the downstream tree
255
+ const downstreamTree = this.getDownstream(artifactId);
256
+
257
+ if (!downstreamTree) {
258
+ return {
259
+ artifactId,
260
+ impactRadius: 0,
261
+ affectedArtifacts: [],
262
+ affectedContracts: []
263
+ };
264
+ }
265
+
266
+ // Flatten the tree to get all affected artifact IDs
267
+ const affectedArtifacts = new Set();
268
+ let maxDepth = 0;
269
+
270
+ /**
271
+ * Recursive helper to traverse the tree
272
+ * @param {Object} node - Tree node
273
+ * @param {number} depth - Current depth
274
+ */
275
+ const traverseTree = (node, depth) => {
276
+ // Add this artifact to the affected set
277
+ if (node.artifactId !== artifactId) {
278
+ affectedArtifacts.add(node.artifactId);
279
+ }
280
+
281
+ // Update max depth
282
+ maxDepth = Math.max(maxDepth, depth);
283
+
284
+ // Traverse dependents
285
+ for (const dependent of node.dependents || []) {
286
+ // Skip cycles
287
+ if (!dependent.cycleDetected) {
288
+ traverseTree(dependent, depth + 1);
289
+ }
290
+ }
291
+ };
292
+
293
+ // Traverse the downstream tree
294
+ traverseTree(downstreamTree, 0);
295
+
296
+ // TODO: Check contracts for references to this artifact
297
+ // This requires the ContractStore to be implemented
298
+ // For now, return an empty array
299
+ const affectedContracts = [];
300
+
301
+ // Return the impact analysis
302
+ return {
303
+ artifactId,
304
+ impactRadius: maxDepth,
305
+ affectedArtifacts: Array.from(affectedArtifacts),
306
+ affectedContracts
307
+ };
308
+ }
309
+
310
+ /**
311
+ * Find artifacts that reference outdated versions of their dependencies
312
+ * @returns {Array} Array of { artifactId, version, staleReason, staleSourceId, referencedVersion, latestVersion }
313
+ */
314
+ findStale() {
315
+ const staleArtifacts = [];
316
+
317
+ // Scan all artifacts
318
+ const allArtifacts = this._scanAllVersionFiles();
319
+
320
+ for (const [artifactId, versionData] of allArtifacts.entries()) {
321
+ // Get the derivedFrom array
322
+ const derivedFrom = versionData.lineage?.derivedFrom || [];
323
+
324
+ // Check each dependency
325
+ for (const ref of derivedFrom) {
326
+ const { artifactId: sourceId, version: referencedVersion } = this._parseRef(ref);
327
+
328
+ // Get the source artifact's latest version
329
+ const sourceLatest = this.artifacts.get(sourceId);
330
+
331
+ if (!sourceLatest) {
332
+ // Source artifact doesn't exist anymore
333
+ staleArtifacts.push({
334
+ artifactId,
335
+ version: versionData.version,
336
+ staleReason: 'Source artifact not found',
337
+ staleSourceId: sourceId,
338
+ referencedVersion,
339
+ latestVersion: null
340
+ });
341
+ continue;
342
+ }
343
+
344
+ // If referencedVersion is null, it means "latest" - not stale
345
+ if (referencedVersion === null) {
346
+ continue;
347
+ }
348
+
349
+ // Check if the referenced version is outdated
350
+ if (sourceLatest.version > referencedVersion) {
351
+ staleArtifacts.push({
352
+ artifactId,
353
+ version: versionData.version,
354
+ staleReason: 'References outdated version',
355
+ staleSourceId: sourceId,
356
+ referencedVersion,
357
+ latestVersion: sourceLatest.version
358
+ });
359
+ }
360
+ }
361
+ }
362
+
363
+ return staleArtifacts;
364
+ }
365
+
366
+ /**
367
+ * Get the complete audit trail for an artifact
368
+ * Shows full provenance: who published it, what contract produced it, what inputs it used
369
+ * @param {string} artifactId - The artifact identifier
370
+ * @param {number|null} [version=null] - Version number (null = latest)
371
+ * @returns {Object|null} Audit trail with full provenance, or null if not found
372
+ */
373
+ getAuditTrail(artifactId, version = null) {
374
+ // Get the artifact version
375
+ const versionData = this.artifacts.get(artifactId, version);
376
+
377
+ if (!versionData) {
378
+ return null; // Artifact not found
379
+ }
380
+
381
+ // Get upstream chain
382
+ const upstreamChain = this.getUpstream(artifactId, versionData.version);
383
+
384
+ // Build the audit trail
385
+ const auditTrail = {
386
+ artifactId,
387
+ version: versionData.version,
388
+ type: versionData.type,
389
+ publisher: versionData.publisher,
390
+ publishedAt: versionData.publishedAt,
391
+ summary: versionData.summary,
392
+ contract: versionData.lineage?.producedBy || null,
393
+ inputs: versionData.lineage?.derivedFrom || [],
394
+ upstreamChain
395
+ };
396
+
397
+ return auditTrail;
398
+ }
399
+ }
400
+
401
+ // Export the LineageGraph class
402
+ module.exports = LineageGraph;