@papyruslabsai/seshat-mcp 0.17.0 → 0.18.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 CHANGED
@@ -278,6 +278,18 @@ 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
+ },
281
293
  {
282
294
  name: 'list_modules',
283
295
  title: 'List Modules',
@@ -21,4 +21,8 @@ export declare function conflictMatrix(args: {
21
21
  expand_blast_radius?: boolean;
22
22
  }>;
23
23
  project?: string;
24
- }, loader: ProjectLoader): unknown;
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>;
@@ -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
- function classifyConflictTier(taskA, taskB) {
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)) / totalW) * 1000) / 1000;
331
- const base = { conflictScore, sharedFiles, sharedEntities, sharedTables, sharedNeighbors };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papyruslabsai/seshat-mcp",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Semantic MCP server — exposes a codebase's structure, dependencies, and constraints as queryable tools",
5
5
  "type": "module",
6
6
  "bin": {