@papyruslabsai/seshat-mcp 0.17.0 → 0.19.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/dist/index.js +24 -0
- package/dist/tools/diff.d.ts +5 -1
- package/dist/tools/diff.js +58 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -278,6 +278,30 @@ const TOOLS = [
|
|
|
278
278
|
},
|
|
279
279
|
annotations: READ_ONLY_OPEN,
|
|
280
280
|
},
|
|
281
|
+
{
|
|
282
|
+
name: 'get_hotspots',
|
|
283
|
+
title: 'Get Hotspots',
|
|
284
|
+
description: 'Project-level change-history orientation: the most-changed entities (and what kind of change dominates each), low-survival thrash spots where changes don\'t stick, heavily-depended-on entities that haven\'t changed all window (interface freeze), and directories with no recent changes. Call it after list_modules when orienting in a codebase — it answers "where does development actually happen, and where does it fail?" from commit history rather than current structure. Complements get_lineage (one entity\'s story) with the project-wide map.',
|
|
285
|
+
inputSchema: {
|
|
286
|
+
type: 'object',
|
|
287
|
+
properties: {
|
|
288
|
+
project: projectParam,
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
annotations: READ_ONLY_OPEN,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: 'get_co_change_clusters',
|
|
295
|
+
title: 'Get Co-Change Clusters',
|
|
296
|
+
description: 'The codebase\'s hidden modules: groups of symbols that historically change together (≥3 shared commits, sweep commits excluded), computed from real commit history. Clusters that span multiple directories reveal coupling the file tree doesn\'t show. Call it when planning a change or splitting work across agents — touching one member of a cluster usually means touching the rest, even when no static dependency connects them. Correlation evidence, honestly framed; complements get_blast_radius (static reach) with empirical reach.',
|
|
297
|
+
inputSchema: {
|
|
298
|
+
type: 'object',
|
|
299
|
+
properties: {
|
|
300
|
+
project: projectParam,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
annotations: READ_ONLY_OPEN,
|
|
304
|
+
},
|
|
281
305
|
{
|
|
282
306
|
name: 'list_modules',
|
|
283
307
|
title: 'List Modules',
|
package/dist/tools/diff.d.ts
CHANGED
|
@@ -21,4 +21,8 @@ export declare function conflictMatrix(args: {
|
|
|
21
21
|
expand_blast_radius?: boolean;
|
|
22
22
|
}>;
|
|
23
23
|
project?: string;
|
|
24
|
-
}, loader: ProjectLoader
|
|
24
|
+
}, loader: ProjectLoader, extras?: {
|
|
25
|
+
/** Lineage-backed co-change provider: given entity ids, returns a map of
|
|
26
|
+
* sorted-pair keys (`${idA}${idB}`) → shared commit counts. */
|
|
27
|
+
fetchCoChanges?: (entityIds: string[]) => Promise<Map<string, number>>;
|
|
28
|
+
}): Promise<unknown>;
|
package/dist/tools/diff.js
CHANGED
|
@@ -280,6 +280,11 @@ const CONFLICT_WEIGHTS = {
|
|
|
280
280
|
epsilon: 0.23, // ε — shared call-graph neighborhood
|
|
281
281
|
delta: 0.14, // δ — shared data contracts (tables)
|
|
282
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,
|
|
283
288
|
};
|
|
284
289
|
function jaccard(a, b) {
|
|
285
290
|
if (a.size === 0 && b.size === 0)
|
|
@@ -297,7 +302,27 @@ function intersect(a, b) {
|
|
|
297
302
|
out.push(x);
|
|
298
303
|
return out;
|
|
299
304
|
}
|
|
300
|
-
|
|
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) {
|
|
301
326
|
const sharedEntities = intersect(taskA.entityIds, taskB.entityIds);
|
|
302
327
|
const sharedFiles = intersect(taskA.files, taskB.files);
|
|
303
328
|
const sharedNeighbors = intersect(taskA.neighbors, taskB.neighbors);
|
|
@@ -319,16 +344,20 @@ function classifyConflictTier(taskA, taskB) {
|
|
|
319
344
|
}
|
|
320
345
|
const writeWrite = sharedTables.filter(t => t.taskA === 'write' && t.taskB === 'write');
|
|
321
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;
|
|
322
350
|
// Entropy-weighted evidence score in [0,1]
|
|
323
351
|
const tablesA = new Set([...taskA.tablesRead, ...taskA.tablesWritten]);
|
|
324
352
|
const tablesB = new Set([...taskB.tablesRead, ...taskB.tablesWritten]);
|
|
325
353
|
const w = CONFLICT_WEIGHTS;
|
|
326
|
-
const totalW = w.symbols + w.epsilon + w.delta + w.files;
|
|
354
|
+
const totalW = w.symbols + w.epsilon + w.delta + w.files + w.history;
|
|
327
355
|
const conflictScore = Math.round(((w.symbols * jaccard(taskA.entityIds, taskB.entityIds) +
|
|
328
356
|
w.epsilon * jaccard(taskA.neighbors, taskB.neighbors) +
|
|
329
357
|
w.delta * jaccard(tablesA, tablesB) +
|
|
330
|
-
w.files * jaccard(taskA.files, taskB.files)
|
|
331
|
-
|
|
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 };
|
|
332
361
|
// Tier 3 — must sequence
|
|
333
362
|
if (sharedEntities.length > 0) {
|
|
334
363
|
return {
|
|
@@ -369,6 +398,16 @@ function classifyConflictTier(taskA, taskB) {
|
|
|
369
398
|
`(e.g. ${sharedNeighbors.slice(0, 3).join(', ')}) — review the shared interface before parallelizing`,
|
|
370
399
|
};
|
|
371
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
|
+
}
|
|
372
411
|
if (conflictScore > 0) {
|
|
373
412
|
return {
|
|
374
413
|
...base,
|
|
@@ -444,7 +483,7 @@ function buildExecutionPlan(taskIds, tier3Edges) {
|
|
|
444
483
|
};
|
|
445
484
|
}
|
|
446
485
|
// ─── Tool: conflict_matrix ────────────────────────────────────────
|
|
447
|
-
export function conflictMatrix(args, loader) {
|
|
486
|
+
export async function conflictMatrix(args, loader, extras = {}) {
|
|
448
487
|
const projErr = validateProject(args.project, loader);
|
|
449
488
|
if (projErr)
|
|
450
489
|
return { error: projErr };
|
|
@@ -510,12 +549,23 @@ export function conflictMatrix(args, loader) {
|
|
|
510
549
|
}
|
|
511
550
|
// Pairwise comparison
|
|
512
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
|
+
}
|
|
513
563
|
const tier3Edges = [];
|
|
514
564
|
for (let i = 0; i < resolvedTasks.length; i++) {
|
|
515
565
|
for (let j = i + 1; j < resolvedTasks.length; j++) {
|
|
516
566
|
const taskA = resolvedTasks[i];
|
|
517
567
|
const taskB = resolvedTasks[j];
|
|
518
|
-
const result = classifyConflictTier(taskA, taskB);
|
|
568
|
+
const result = classifyConflictTier(taskA, taskB, coChange);
|
|
519
569
|
const entry = {
|
|
520
570
|
taskA: taskA.id,
|
|
521
571
|
taskB: taskB.id,
|
|
@@ -531,6 +581,8 @@ export function conflictMatrix(args, loader) {
|
|
|
531
581
|
entry.sharedTables = result.sharedTables;
|
|
532
582
|
if (result.sharedNeighbors.length > 0)
|
|
533
583
|
entry.sharedNeighbors = result.sharedNeighbors.slice(0, 10);
|
|
584
|
+
if (result.sharedHistory && result.sharedHistory.length > 0)
|
|
585
|
+
entry.sharedHistory = result.sharedHistory.slice(0, 10);
|
|
534
586
|
matrix.push(entry);
|
|
535
587
|
if (result.tier === 3) {
|
|
536
588
|
tier3Edges.push([taskA.id, taskB.id]);
|
package/package.json
CHANGED