@papyruslabsai/seshat-mcp 0.20.0 → 0.20.1

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.
@@ -1,604 +0,0 @@
1
- /**
2
- * Cross-Bundle Analysis Tools: diff_bundle + conflict_matrix
3
- *
4
- * diff_bundle: Compare entities between a worktree and the loaded project.
5
- * conflict_matrix: Classify conflict tiers for parallel task scheduling.
6
- *
7
- * These tools compare TWO entity sets (base vs branch, or task vs task),
8
- * unlike the single-bundle queries in index.ts and functors.ts.
9
- */
10
- import fs from 'fs';
11
- import path from 'path';
12
- import { bootstrap } from '../bootstrap.js';
13
- import { computeBlastRadius } from '../graph.js';
14
- import { getGraph, validateProject, entityName, entityLayer, } from './index.js';
15
- import { tableContract } from './dbops.js';
16
- // ─── Entity Identity ─────────────────────────────────────────────
17
- // Ported from api-v2/translator/seshat-pipeline/src/incremental/diff-engine.mjs
18
- /**
19
- * Generate a unique key for an entity: <relative_path>::<id>
20
- *
21
- * Uses _sourceFile (already relative in bundles) or derives from
22
- * sourceFile (absolute path) by stripping the repo root.
23
- */
24
- function entityKey(entity, repoRoot) {
25
- let relativePath = entity._sourceFile || null;
26
- // If _sourceFile not set, try deriving from sourceFile (absolute)
27
- if (!relativePath) {
28
- const raw = entity;
29
- const sourceFile = (raw.sourceFile || '').replace(/\\/g, '/');
30
- if (repoRoot && sourceFile) {
31
- const normalizedRoot = repoRoot.replace(/\\/g, '/').replace(/\/+$/, '') + '/';
32
- if (sourceFile.startsWith(normalizedRoot)) {
33
- relativePath = sourceFile.substring(normalizedRoot.length);
34
- }
35
- else {
36
- // Case-insensitive match (Windows paths)
37
- const lowerFile = sourceFile.toLowerCase();
38
- const lowerRoot = normalizedRoot.toLowerCase();
39
- if (lowerFile.startsWith(lowerRoot)) {
40
- relativePath = sourceFile.substring(normalizedRoot.length);
41
- }
42
- }
43
- }
44
- if (!relativePath) {
45
- relativePath = sourceFile || 'unknown';
46
- }
47
- }
48
- // Normalize path separators
49
- relativePath = (relativePath || 'unknown').replace(/\\/g, '/');
50
- const id = entity.id || (typeof entity.struct === 'string' ? entity.struct : entity.struct?.name) || 'anonymous';
51
- return `${relativePath}::${id}`;
52
- }
53
- // ─── Bundle Loading ───────────────────────────────────────────────
54
- /**
55
- * Load a branch bundle from a worktree path.
56
- * Does NOT add to the global MultiLoader — this is a transient comparison target.
57
- *
58
- * 1. Check <worktree_path>/.seshat/_bundle.json — parse if exists
59
- * 2. If not, run bootstrap() to extract on-the-fly
60
- * 3. Apply the same field remapping as BundleLoader (sourceFile → _sourceFile, etc.)
61
- */
62
- async function loadBranchBundle(worktreePath) {
63
- const absPath = path.resolve(worktreePath);
64
- const bundlePath = path.join(absPath, '.seshat', '_bundle.json');
65
- let bundle;
66
- if (fs.existsSync(bundlePath)) {
67
- const raw = fs.readFileSync(bundlePath, 'utf-8');
68
- bundle = JSON.parse(raw);
69
- }
70
- else {
71
- // Auto-extract via bootstrap
72
- const result = await bootstrap(absPath);
73
- if (!result.success) {
74
- throw new Error(`Failed to extract bundle from ${absPath}: ${result.error}`);
75
- }
76
- // Read the freshly generated bundle
77
- if (!fs.existsSync(bundlePath)) {
78
- throw new Error(`Bootstrap succeeded but no bundle found at ${bundlePath}`);
79
- }
80
- const raw = fs.readFileSync(bundlePath, 'utf-8');
81
- bundle = JSON.parse(raw);
82
- }
83
- const entities = bundle.entities || [];
84
- // Apply the same field remapping as BundleLoader.load()
85
- for (const e of entities) {
86
- const raw = e;
87
- if (raw.sourceFile && !e._sourceFile) {
88
- e._sourceFile = raw.sourceFile;
89
- }
90
- if (raw.sourceLanguage && !e._sourceLanguage) {
91
- e._sourceLanguage = raw.sourceLanguage;
92
- }
93
- }
94
- // Try to get commit SHA from manifest
95
- let commitSha = '';
96
- const manifestPath = path.join(absPath, '.seshat', 'manifest.json');
97
- if (fs.existsSync(manifestPath)) {
98
- try {
99
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
100
- commitSha = manifest.commitSha || '';
101
- }
102
- catch { /* ignore */ }
103
- }
104
- return {
105
- entities,
106
- source: bundle.source || absPath,
107
- commitSha,
108
- entityCount: entities.length,
109
- };
110
- }
111
- // ─── Field Comparison ─────────────────────────────────────────────
112
- /** The entity fields to compare, mapped to user-facing names (v0.4.2 convention) */
113
- const FIELD_MAP = [
114
- ['struct', 'structure'],
115
- ['edges', 'call_graph'],
116
- ['data', 'data_flow'],
117
- ['constraints', 'constraints'],
118
- ['context', 'context'],
119
- ['ownership', 'ownership'],
120
- ['traits', 'type_info'],
121
- ['runtime', 'runtime'],
122
- ['semantics', 'logic'],
123
- ];
124
- /**
125
- * Compare two entities field-by-field. Returns which fields changed
126
- * using user-facing names.
127
- */
128
- function compareEntityFields(baseEntity, branchEntity) {
129
- const changed = [];
130
- for (const [field, displayName] of FIELD_MAP) {
131
- const baseVal = baseEntity[field];
132
- const branchVal = branchEntity[field];
133
- // Both undefined/null → no change
134
- if (baseVal == null && branchVal == null)
135
- continue;
136
- // One null, other not → changed
137
- if (baseVal == null || branchVal == null) {
138
- changed.push(displayName);
139
- continue;
140
- }
141
- // Deep compare via JSON
142
- if (JSON.stringify(baseVal) !== JSON.stringify(branchVal)) {
143
- changed.push(displayName);
144
- }
145
- }
146
- return changed;
147
- }
148
- // ─── Tool: diff_bundle ───────────────────────────────────────────
149
- export async function diffBundle(args, loader) {
150
- const projErr = validateProject(args.project, loader);
151
- if (projErr)
152
- return { error: projErr };
153
- const baseEntities = loader.getEntities(args.project);
154
- const baseManifest = loader.getManifest(args.project);
155
- if (baseEntities.length === 0) {
156
- return { error: 'No base entities loaded. Ensure the project has been extracted.' };
157
- }
158
- // Load branch bundle
159
- let branchData;
160
- try {
161
- branchData = await loadBranchBundle(args.worktree_path);
162
- }
163
- catch (err) {
164
- return { error: `Failed to load branch bundle: ${err.message}` };
165
- }
166
- const branchEntities = branchData.entities;
167
- // Build entity maps by key
168
- const baseSource = baseManifest?.commitSha ? '' : ''; // base entities have relative paths already
169
- const baseMap = new Map();
170
- for (const entity of baseEntities) {
171
- const key = entityKey(entity);
172
- baseMap.set(key, entity);
173
- }
174
- const branchMap = new Map();
175
- for (const entity of branchEntities) {
176
- const key = entityKey(entity);
177
- branchMap.set(key, entity);
178
- }
179
- // Classify entities
180
- const added = [];
181
- const removed = [];
182
- const modified = [];
183
- const unchanged = [];
184
- // Added: in branch but not base
185
- for (const [key, entity] of branchMap) {
186
- if (!baseMap.has(key)) {
187
- added.push({
188
- id: entity.id,
189
- name: entityName(entity),
190
- sourceFile: entity._sourceFile || null,
191
- layer: entityLayer(entity),
192
- });
193
- }
194
- }
195
- // Removed: in base but not branch
196
- for (const [key, entity] of baseMap) {
197
- if (!branchMap.has(key)) {
198
- removed.push({
199
- id: entity.id,
200
- name: entityName(entity),
201
- sourceFile: entity._sourceFile || null,
202
- layer: entityLayer(entity),
203
- });
204
- }
205
- }
206
- // Modified / Unchanged: in both
207
- for (const [key, branchEntity] of branchMap) {
208
- const baseEntity = baseMap.get(key);
209
- if (!baseEntity)
210
- continue; // already in added
211
- const changedFields = compareEntityFields(baseEntity, branchEntity);
212
- if (changedFields.length > 0) {
213
- modified.push({
214
- id: branchEntity.id,
215
- name: entityName(branchEntity),
216
- sourceFile: branchEntity._sourceFile || null,
217
- layer: entityLayer(branchEntity),
218
- changedFields,
219
- });
220
- }
221
- else if (args.include_unchanged) {
222
- unchanged.push({
223
- id: branchEntity.id,
224
- name: entityName(branchEntity),
225
- sourceFile: branchEntity._sourceFile || null,
226
- layer: entityLayer(branchEntity),
227
- });
228
- }
229
- }
230
- // Count unique files touched
231
- const touchedFiles = new Set();
232
- for (const e of [...added, ...removed, ...modified]) {
233
- if (e.sourceFile)
234
- touchedFiles.add(e.sourceFile);
235
- }
236
- const totalChanges = added.length + removed.length + modified.length;
237
- const result = {
238
- base: {
239
- project: baseManifest?.projectName || args.project || 'default',
240
- commitSha: baseManifest?.commitSha || '',
241
- entityCount: baseEntities.length,
242
- },
243
- branch: {
244
- path: args.worktree_path,
245
- commitSha: branchData.commitSha,
246
- entityCount: branchData.entityCount,
247
- },
248
- summary: {
249
- added: added.length,
250
- removed: removed.length,
251
- modified: modified.length,
252
- unchanged: baseEntities.length - removed.length - modified.length,
253
- },
254
- added,
255
- removed,
256
- modified,
257
- _summary: `Branch modifies ${totalChanges} entities (${added.length} added, ${removed.length} removed, ${modified.length} modified) across ${touchedFiles.size} files`,
258
- };
259
- if (args.include_unchanged) {
260
- result.unchanged = unchanged;
261
- }
262
- return result;
263
- }
264
- /**
265
- * Entropy weights — measured, not asserted. Instance-weighted Shannon entropy
266
- * per dimension, FUNCTION-LEVEL entities only (conflict tasks target functions;
267
- * import/const entities dilute all-entities numbers). Re-measured 2026-06-11 on
268
- * the CLEAN substrate (honest κ, derived χ, collapsed ε — sniffr-backend snap
269
- * 205 + ptah snap 207 + second-brain snap 210, ~2,059 JS function instances):
270
- * the original 56-project corpus was extracted pre-Testimony-Rule, so part of
271
- * its κ entropy was false-positive variety and its χ was path-substring noise.
272
- * Clean JS vector: ε .225, Σ .146, δ .140, χ .114, κ .107, σ .089; ρ 0
273
- * everywhere. Notable shift vs the corrupted corpus: χ (.087→.114) overtakes
274
- * σ (.107→.089) — topology-derived layers carry real discrimination. See
275
- * ptah/docs/dimension-entropy-2026-06-clean.json (by_language_functions) and
276
- * DIMENSION-ENTROPY-FINDINGS.md (clean re-run section).
277
- */
278
- const CONFLICT_WEIGHTS = {
279
- symbols: 0.09, // σ — direct entity overlap
280
- epsilon: 0.23, // ε — shared call-graph neighborhood
281
- delta: 0.14, // δ — shared data contracts (tables)
282
- files: 0.11, // χ-adjacent — shared files
283
- // Historical co-change from the lineage record (entity_transitions):
284
- // entities that changed together in past commits are coupled IN PRACTICE,
285
- // whatever static analysis says. Weight provisional — revisit once
286
- // corpus-wide lineage allows an entropy-style calibration.
287
- history: 0.10,
288
- };
289
- function jaccard(a, b) {
290
- if (a.size === 0 && b.size === 0)
291
- return 0;
292
- let inter = 0;
293
- for (const x of a)
294
- if (b.has(x))
295
- inter++;
296
- return inter / (a.size + b.size - inter);
297
- }
298
- function intersect(a, b) {
299
- const out = [];
300
- for (const x of a)
301
- if (b.has(x))
302
- out.push(x);
303
- return out;
304
- }
305
- /** Co-change pairs between two tasks, from a pre-fetched pair→count map keyed `${idA}${idB}` (sorted). */
306
- function coChangePairs(taskA, taskB, coChange) {
307
- if (!coChange || coChange.size === 0)
308
- return [];
309
- const out = [];
310
- for (const a of taskA.entityIds) {
311
- for (const b of taskB.entityIds) {
312
- if (a === b)
313
- continue;
314
- const key = a < b ? `${a}${b}` : `${b}${a}`;
315
- const n = coChange.get(key);
316
- if (n && n >= 2)
317
- out.push({ a, b, sharedCommits: n });
318
- }
319
- }
320
- return out.sort((x, y) => y.sharedCommits - x.sharedCommits);
321
- }
322
- // ≥ this many shared commits between a cross-task entity pair = coupled in
323
- // practice, escalate to Tier 2 even when statically disjoint everywhere.
324
- const CO_CHANGE_TIER2_THRESHOLD = 3;
325
- function classifyConflictTier(taskA, taskB, coChange = null) {
326
- const sharedEntities = intersect(taskA.entityIds, taskB.entityIds);
327
- const sharedFiles = intersect(taskA.files, taskB.files);
328
- const sharedNeighbors = intersect(taskA.neighbors, taskB.neighbors);
329
- // δ-contract overlap with read/write direction per side
330
- const sharedTables = [];
331
- const allTables = new Set([
332
- ...taskA.tablesRead, ...taskA.tablesWritten,
333
- ...taskB.tablesRead, ...taskB.tablesWritten,
334
- ]);
335
- for (const table of allTables) {
336
- const aW = taskA.tablesWritten.has(table);
337
- const aR = taskA.tablesRead.has(table) || aW;
338
- const bW = taskB.tablesWritten.has(table);
339
- const bR = taskB.tablesRead.has(table) || bW;
340
- if (aR && bR && (aW || bW)) {
341
- // overlap matters only when at least one side writes
342
- sharedTables.push({ table, taskA: aW ? 'write' : 'read', taskB: bW ? 'write' : 'read' });
343
- }
344
- }
345
- const writeWrite = sharedTables.filter(t => t.taskA === 'write' && t.taskB === 'write');
346
- const readWrite = sharedTables.filter(t => t.taskA !== t.taskB);
347
- // Historical co-change (lineage evidence)
348
- const sharedHistory = coChangePairs(taskA, taskB, coChange);
349
- const maxShared = sharedHistory[0]?.sharedCommits || 0;
350
- // Entropy-weighted evidence score in [0,1]
351
- const tablesA = new Set([...taskA.tablesRead, ...taskA.tablesWritten]);
352
- const tablesB = new Set([...taskB.tablesRead, ...taskB.tablesWritten]);
353
- const w = CONFLICT_WEIGHTS;
354
- const totalW = w.symbols + w.epsilon + w.delta + w.files + w.history;
355
- const conflictScore = Math.round(((w.symbols * jaccard(taskA.entityIds, taskB.entityIds) +
356
- w.epsilon * jaccard(taskA.neighbors, taskB.neighbors) +
357
- w.delta * jaccard(tablesA, tablesB) +
358
- w.files * jaccard(taskA.files, taskB.files) +
359
- w.history * Math.min(1, maxShared / 5)) / totalW) * 1000) / 1000;
360
- const base = { conflictScore, sharedFiles, sharedEntities, sharedTables, sharedNeighbors, sharedHistory };
361
- // Tier 3 — must sequence
362
- if (sharedEntities.length > 0) {
363
- return {
364
- ...base,
365
- tier: 3,
366
- reason: `${sharedEntities.length} shared symbols — MUST sequence to maintain surgical splice integrity`,
367
- };
368
- }
369
- if (writeWrite.length > 0) {
370
- return {
371
- ...base,
372
- tier: 3,
373
- reason: `shared data contract — both tasks WRITE table(s) ${writeWrite.map(t => t.table).join(', ')}; ` +
374
- 'symbols are disjoint but the tasks collide through the data layer — MUST sequence',
375
- };
376
- }
377
- // Tier 2 — parallelize with care
378
- if (readWrite.length > 0) {
379
- return {
380
- ...base,
381
- tier: 2,
382
- reason: `read/write data contract overlap on table(s) ${readWrite.map(t => t.table).join(', ')} — ` +
383
- 'parallel-safe only if the reader tolerates the writer\'s changes',
384
- };
385
- }
386
- if (sharedFiles.length > 0) {
387
- return {
388
- ...base,
389
- tier: 2,
390
- reason: `${sharedFiles.length} shared files but different symbols — safe to parallelize via surgical splicing`,
391
- };
392
- }
393
- if (sharedNeighbors.length > 0) {
394
- return {
395
- ...base,
396
- tier: 2,
397
- reason: `call-graph adjacency — tasks touch ${sharedNeighbors.length} shared neighbor(s) within 1 hop ` +
398
- `(e.g. ${sharedNeighbors.slice(0, 3).join(', ')}) — review the shared interface before parallelizing`,
399
- };
400
- }
401
- if (maxShared >= CO_CHANGE_TIER2_THRESHOLD) {
402
- const top = sharedHistory[0];
403
- return {
404
- ...base,
405
- tier: 2,
406
- reason: `historical co-change — ${top.a.split('#').pop()} and ${top.b.split('#').pop()} changed together ` +
407
- `in ${top.sharedCommits} past commits (${sharedHistory.length} coupled pair${sharedHistory.length === 1 ? '' : 's'} total); ` +
408
- 'statically disjoint but coupled in practice — review for an implicit shared contract before parallelizing',
409
- };
410
- }
411
- if (conflictScore > 0) {
412
- return {
413
- ...base,
414
- tier: 1,
415
- reason: 'No conflicting overlap (weak affinity only — shared read-only tables or nearby code) — safe to parallelize',
416
- };
417
- }
418
- return { ...base, tier: 1, reason: 'No shared symbols, files, data contracts, or call-graph adjacency — safe to parallelize' };
419
- }
420
- // ─── Execution Plan Builder ───────────────────────────────────────
421
- /**
422
- * Build execution plan from tier-3 conflict graph using connected components.
423
- * Tasks with no tier-3 edges to each other run in parallel.
424
- * Tasks in the same tier-3 component run sequentially.
425
- */
426
- function buildExecutionPlan(taskIds, tier3Edges) {
427
- // Build adjacency list for tier-3 conflicts
428
- const adj = new Map();
429
- for (const id of taskIds) {
430
- adj.set(id, new Set());
431
- }
432
- for (const [a, b] of tier3Edges) {
433
- adj.get(a)?.add(b);
434
- adj.get(b)?.add(a);
435
- }
436
- // Find connected components via BFS
437
- const visited = new Set();
438
- const components = [];
439
- for (const id of taskIds) {
440
- if (visited.has(id))
441
- continue;
442
- const component = [];
443
- const queue = [id];
444
- visited.add(id);
445
- while (queue.length > 0) {
446
- const current = queue.shift();
447
- component.push(current);
448
- const neighbors = adj.get(current);
449
- if (neighbors) {
450
- for (const neighbor of neighbors) {
451
- if (!visited.has(neighbor)) {
452
- visited.add(neighbor);
453
- queue.push(neighbor);
454
- }
455
- }
456
- }
457
- }
458
- components.push(component);
459
- }
460
- // Build groups
461
- const groups = components.map(component => {
462
- if (component.length === 1) {
463
- return { tasks: component, sequential: false };
464
- }
465
- return { tasks: component, sequential: true, order: component };
466
- });
467
- // Count parallelizable groups
468
- const parallelGroups = groups.length;
469
- const sequentialCount = groups.filter(g => g.sequential).length;
470
- const parallelCount = groups.filter(g => !g.sequential).length;
471
- let summaryParts = [`${taskIds.length} tasks`];
472
- if (parallelCount > 0) {
473
- summaryParts.push(`${parallelCount === taskIds.length ? 'all' : parallelCount} can run in parallel`);
474
- }
475
- if (sequentialCount > 0) {
476
- const seqTasks = groups.filter(g => g.sequential).reduce((sum, g) => sum + g.tasks.length, 0);
477
- summaryParts.push(`${seqTasks} must be sequenced (${sequentialCount} sequential group${sequentialCount > 1 ? 's' : ''})`);
478
- }
479
- return {
480
- parallelGroups,
481
- groups,
482
- _summary: summaryParts.join(': '),
483
- };
484
- }
485
- // ─── Tool: conflict_matrix ────────────────────────────────────────
486
- export async function conflictMatrix(args, loader, extras = {}) {
487
- const projErr = validateProject(args.project, loader);
488
- if (projErr)
489
- return { error: projErr };
490
- const { tasks } = args;
491
- if (!tasks || tasks.length < 2) {
492
- return { error: 'At least 2 tasks are required to compute a conflict matrix.' };
493
- }
494
- const warnings = [];
495
- // Resolve entity sets for each task
496
- const resolvedTasks = [];
497
- const graph = getGraph(args.project, loader);
498
- for (const task of tasks) {
499
- const entityIds = new Set();
500
- const files = new Set();
501
- const entities = [];
502
- const notFound = [];
503
- const dimensions = new Set(task.dimensions || []);
504
- for (const nameOrId of task.entity_ids) {
505
- const entity = loader.getEntityById(nameOrId, args.project)
506
- || loader.getEntityByName(nameOrId, args.project);
507
- if (entity) {
508
- entityIds.add(entity.id);
509
- entities.push(entity);
510
- if (entity._sourceFile)
511
- files.add(entity._sourceFile);
512
- }
513
- else {
514
- notFound.push(nameOrId);
515
- }
516
- }
517
- if (notFound.length > 0) {
518
- warnings.push(`Task "${task.id}": entities not found: ${notFound.join(', ')}`);
519
- }
520
- // Expand blast radius if requested
521
- if (task.expand_blast_radius && entityIds.size > 0) {
522
- const blastResult = computeBlastRadius(graph, entityIds);
523
- for (const affectedId of blastResult.affected) {
524
- if (!entityIds.has(affectedId)) {
525
- entityIds.add(affectedId);
526
- const affectedEntity = graph.entityById.get(affectedId);
527
- if (affectedEntity) {
528
- entities.push(affectedEntity);
529
- if (affectedEntity._sourceFile)
530
- files.add(affectedEntity._sourceFile);
531
- }
532
- }
533
- }
534
- }
535
- // δ-contract surface: tables read/written by the task's entities
536
- const { reads: tablesRead, writes: tablesWritten } = tableContract(entities);
537
- // ε surface: 1-hop call-graph neighborhood (always computed — cheap, and
538
- // it catches adjacent-task conflicts even without blast-radius expansion)
539
- const neighbors = new Set();
540
- for (const id of entityIds) {
541
- for (const c of graph.callers.get(id) || [])
542
- if (!entityIds.has(c))
543
- neighbors.add(c);
544
- for (const c of graph.callees.get(id) || [])
545
- if (!entityIds.has(c))
546
- neighbors.add(c);
547
- }
548
- resolvedTasks.push({ id: task.id, entityIds, files, entities, dimensions, tablesRead, tablesWritten, neighbors });
549
- }
550
- // Pairwise comparison
551
- const matrix = [];
552
- // Lineage-backed co-change evidence, fetched once for all tasks' entities
553
- let coChange = null;
554
- if (extras.fetchCoChanges) {
555
- try {
556
- const allIds = [...new Set(resolvedTasks.flatMap(t => [...t.entityIds]))];
557
- coChange = await extras.fetchCoChanges(allIds);
558
- }
559
- catch {
560
- warnings.push('co-change history unavailable for this run (lineage lookup failed) — tiers computed from static evidence only');
561
- }
562
- }
563
- const tier3Edges = [];
564
- for (let i = 0; i < resolvedTasks.length; i++) {
565
- for (let j = i + 1; j < resolvedTasks.length; j++) {
566
- const taskA = resolvedTasks[i];
567
- const taskB = resolvedTasks[j];
568
- const result = classifyConflictTier(taskA, taskB, coChange);
569
- const entry = {
570
- taskA: taskA.id,
571
- taskB: taskB.id,
572
- tier: result.tier,
573
- reason: result.reason,
574
- conflictScore: result.conflictScore,
575
- };
576
- if (result.sharedFiles.length > 0)
577
- entry.sharedFiles = result.sharedFiles;
578
- if (result.sharedEntities.length > 0)
579
- entry.sharedEntities = result.sharedEntities;
580
- if (result.sharedTables.length > 0)
581
- entry.sharedTables = result.sharedTables;
582
- if (result.sharedNeighbors.length > 0)
583
- entry.sharedNeighbors = result.sharedNeighbors.slice(0, 10);
584
- if (result.sharedHistory && result.sharedHistory.length > 0)
585
- entry.sharedHistory = result.sharedHistory.slice(0, 10);
586
- matrix.push(entry);
587
- if (result.tier === 3) {
588
- tier3Edges.push([taskA.id, taskB.id]);
589
- }
590
- }
591
- }
592
- // Build execution plan
593
- const taskIds = resolvedTasks.map(t => t.id);
594
- const executionPlan = buildExecutionPlan(taskIds, tier3Edges);
595
- const result = {
596
- taskCount: tasks.length,
597
- matrix,
598
- executionPlan,
599
- };
600
- if (warnings.length > 0) {
601
- result.warnings = warnings;
602
- }
603
- return result;
604
- }