@papyruslabsai/seshat-mcp 0.1.0 → 0.2.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.d.ts CHANGED
@@ -15,6 +15,15 @@
15
15
  * list_modules — Group entities by layer, module, file, or language
16
16
  * get_topology — API topology (routes, plugins, auth, tables)
17
17
  *
18
+ * Interpretation Functors (composite analysis):
19
+ * find_dead_code — Unreachable entities via ε-graph BFS from entry points
20
+ * find_layer_violations — ε edges violating architectural layer ordering
21
+ * get_coupling_metrics — Module coupling/cohesion/instability from ε-graph
22
+ * get_auth_matrix — Auth coverage across API-facing entities from κ
23
+ * find_error_gaps — Fallible callees whose callers lack try/catch
24
+ * get_test_coverage — Entities exercised by tests vs uncovered
25
+ * get_optimal_context — Greedy knapsack: max relevance per token for LLM context
26
+ *
18
27
  * Usage:
19
28
  * npx @papyruslabs/seshat-mcp
20
29
  *
package/dist/index.js CHANGED
@@ -15,6 +15,15 @@
15
15
  * list_modules — Group entities by layer, module, file, or language
16
16
  * get_topology — API topology (routes, plugins, auth, tables)
17
17
  *
18
+ * Interpretation Functors (composite analysis):
19
+ * find_dead_code — Unreachable entities via ε-graph BFS from entry points
20
+ * find_layer_violations — ε edges violating architectural layer ordering
21
+ * get_coupling_metrics — Module coupling/cohesion/instability from ε-graph
22
+ * get_auth_matrix — Auth coverage across API-facing entities from κ
23
+ * find_error_gaps — Fallible callees whose callers lack try/catch
24
+ * get_test_coverage — Entities exercised by tests vs uncovered
25
+ * get_optimal_context — Greedy knapsack: max relevance per token for LLM context
26
+ *
18
27
  * Usage:
19
28
  * npx @papyruslabs/seshat-mcp
20
29
  *
@@ -26,6 +35,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
26
35
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
27
36
  import { BundleLoader } from './loader.js';
28
37
  import { initTools, queryEntities, getEntity, getDependencies, getDataFlow, findByConstraint, getBlastRadius, listModules, getTopology, } from './tools/index.js';
38
+ import { findDeadCode, findLayerViolations, getCouplingMetrics, getAuthMatrix, findErrorGaps, getTestCoverage, getOptimalContext, } from './tools/functors.js';
29
39
  // ─── Tool Definitions ─────────────────────────────────────────────
30
40
  const TOOLS = [
31
41
  {
@@ -159,6 +169,94 @@ const TOOLS = [
159
169
  properties: {},
160
170
  },
161
171
  },
172
+ // ─── Interpretation Functor Tools ────────────────────────────────
173
+ {
174
+ name: 'find_dead_code',
175
+ description: 'Find unreachable entities (dead code candidates). BFS from entry points (routes, exported functions, tests, plugins) through the ε call graph. Entities not reachable from any entry point are flagged.',
176
+ inputSchema: {
177
+ type: 'object',
178
+ properties: {
179
+ include_tests: {
180
+ type: 'boolean',
181
+ description: 'Include test entities in dead code results (default: false)',
182
+ },
183
+ },
184
+ },
185
+ },
186
+ {
187
+ name: 'find_layer_violations',
188
+ description: 'Detect architectural layer violations in the ε call graph. Finds backward calls (lower layer calling higher layer, e.g. repository → route) and skip-layer calls (jumping over multiple layers). Uses χ.layer for classification.',
189
+ inputSchema: {
190
+ type: 'object',
191
+ properties: {},
192
+ },
193
+ },
194
+ {
195
+ name: 'get_coupling_metrics',
196
+ description: 'Compute coupling, cohesion, and instability metrics for modules or layers. Analyzes ε edges between and within groups. High coupling + low cohesion = candidates for refactoring.',
197
+ inputSchema: {
198
+ type: 'object',
199
+ properties: {
200
+ group_by: {
201
+ type: 'string',
202
+ enum: ['module', 'layer'],
203
+ description: 'Group entities by module or layer (default: module)',
204
+ },
205
+ },
206
+ },
207
+ },
208
+ {
209
+ name: 'get_auth_matrix',
210
+ description: 'Analyze authentication coverage across all API-facing entities. Shows which routes/controllers have auth requirements (from κ.auth) and which don\'t. Detects inconsistencies like DB access without auth.',
211
+ inputSchema: {
212
+ type: 'object',
213
+ properties: {},
214
+ },
215
+ },
216
+ {
217
+ name: 'find_error_gaps',
218
+ description: 'Find error handling gaps: fallible entities (κ.throws, network/db side effects) whose callers lack try/catch (κ.errorHandling). These are crash risk points where exceptions can propagate unhandled.',
219
+ inputSchema: {
220
+ type: 'object',
221
+ properties: {},
222
+ },
223
+ },
224
+ {
225
+ name: 'get_test_coverage',
226
+ description: 'Compute semantic test coverage: which production entities are exercised by test entities via the ε call graph. Optionally weight uncovered entities by blast radius to prioritize what to test first.',
227
+ inputSchema: {
228
+ type: 'object',
229
+ properties: {
230
+ weight_by_blast_radius: {
231
+ type: 'boolean',
232
+ description: 'Rank uncovered entities by blast radius to prioritize testing (default: false, slower)',
233
+ },
234
+ },
235
+ },
236
+ },
237
+ {
238
+ name: 'get_optimal_context',
239
+ description: 'Compute the optimal set of related entities to include in an LLM context window for a target entity. Uses greedy knapsack: maximizes relevance per token. Returns entities ranked by relevance with token estimates.',
240
+ inputSchema: {
241
+ type: 'object',
242
+ properties: {
243
+ target_entity: {
244
+ type: 'string',
245
+ description: 'Entity ID or name to build context around',
246
+ },
247
+ max_tokens: {
248
+ type: 'number',
249
+ description: 'Token budget for the context window (default: 8000)',
250
+ },
251
+ strategy: {
252
+ type: 'string',
253
+ enum: ['bfs', 'blast_radius'],
254
+ description: 'Traversal strategy: bfs (faster, local neighborhood) or blast_radius (full affected set)',
255
+ },
256
+ },
257
+ required: ['target_entity'],
258
+ },
259
+ },
162
260
  ];
163
261
  // ─── Server Setup ─────────────────────────────────────────────────
164
262
  async function main() {
@@ -177,7 +275,7 @@ async function main() {
177
275
  const entityCount = manifest?.entityCount || 0;
178
276
  const server = new Server({
179
277
  name: `seshat-mcp (${projectName})`,
180
- version: '0.1.0',
278
+ version: '0.2.0',
181
279
  }, {
182
280
  capabilities: {
183
281
  tools: {},
@@ -227,6 +325,28 @@ async function main() {
227
325
  case 'get_topology':
228
326
  result = getTopology();
229
327
  break;
328
+ // Interpretation Functors
329
+ case 'find_dead_code':
330
+ result = findDeadCode(args);
331
+ break;
332
+ case 'find_layer_violations':
333
+ result = findLayerViolations();
334
+ break;
335
+ case 'get_coupling_metrics':
336
+ result = getCouplingMetrics(args);
337
+ break;
338
+ case 'get_auth_matrix':
339
+ result = getAuthMatrix();
340
+ break;
341
+ case 'find_error_gaps':
342
+ result = findErrorGaps();
343
+ break;
344
+ case 'get_test_coverage':
345
+ result = getTestCoverage(args);
346
+ break;
347
+ case 'get_optimal_context':
348
+ result = getOptimalContext(args);
349
+ break;
230
350
  default:
231
351
  result = { error: `Unknown tool: ${name}` };
232
352
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Interpretation Functor Tools
3
+ *
4
+ * Each functor is I: J -> D — projecting the 9D JSTF-T coordinate space
5
+ * onto a domain-specific judgment. These are composite analyses built
6
+ * from the primitive dimensions (sigma, epsilon, delta, kappa, chi, tau, rho).
7
+ */
8
+ export declare function findDeadCode(args: {
9
+ include_tests?: boolean;
10
+ }): unknown;
11
+ export declare function findLayerViolations(): unknown;
12
+ export declare function getCouplingMetrics(args: {
13
+ group_by?: 'module' | 'layer';
14
+ }): unknown;
15
+ export declare function getAuthMatrix(): unknown;
16
+ export declare function findErrorGaps(): unknown;
17
+ export declare function getTestCoverage(args: {
18
+ weight_by_blast_radius?: boolean;
19
+ }): unknown;
20
+ export declare function getOptimalContext(args: {
21
+ target_entity: string;
22
+ max_tokens?: number;
23
+ strategy?: 'bfs' | 'blast_radius';
24
+ }): unknown;
@@ -0,0 +1,533 @@
1
+ /**
2
+ * Interpretation Functor Tools
3
+ *
4
+ * Each functor is I: J -> D — projecting the 9D JSTF-T coordinate space
5
+ * onto a domain-specific judgment. These are composite analyses built
6
+ * from the primitive dimensions (sigma, epsilon, delta, kappa, chi, tau, rho).
7
+ */
8
+ import { computeBlastRadius } from '../graph.js';
9
+ import { getLoader, getGraph, entityLayer, entitySummary, } from './index.js';
10
+ // ─── Layer ordering for violation detection ──────────────────────
11
+ const LAYER_ORDER = {
12
+ route: 0,
13
+ controller: 1,
14
+ middleware: 2,
15
+ service: 3,
16
+ hook: 4,
17
+ repository: 5,
18
+ model: 6,
19
+ schema: 7,
20
+ utility: 8,
21
+ component: 1, // UI components are peers to controllers
22
+ };
23
+ // ─── Functor 1: find_dead_code ───────────────────────────────────
24
+ export function findDeadCode(args) {
25
+ const { include_tests = false } = args;
26
+ const loader = getLoader();
27
+ const g = getGraph();
28
+ const entities = loader.getEntities();
29
+ // Entry points: routes, exported functions, test files, plugin registrations
30
+ const entryPointIds = new Set();
31
+ for (const e of entities) {
32
+ if (!e.id)
33
+ continue;
34
+ const layer = entityLayer(e);
35
+ // Routes and controllers are entry points
36
+ if (layer === 'route' || layer === 'controller') {
37
+ entryPointIds.add(e.id);
38
+ continue;
39
+ }
40
+ // Test entities are entry points
41
+ if (layer === 'test') {
42
+ entryPointIds.add(e.id);
43
+ continue;
44
+ }
45
+ // Plugin registrations
46
+ if (e.context?.exposure === 'framework') {
47
+ entryPointIds.add(e.id);
48
+ continue;
49
+ }
50
+ // Exported functions at the top level are entry points
51
+ if (typeof e.struct !== 'string' && e.struct?.exported) {
52
+ entryPointIds.add(e.id);
53
+ }
54
+ }
55
+ // BFS from all entry points through callees
56
+ const reachable = new Set(entryPointIds);
57
+ const queue = [...entryPointIds];
58
+ while (queue.length > 0) {
59
+ const current = queue.shift();
60
+ const calleeSet = g.callees.get(current);
61
+ if (!calleeSet)
62
+ continue;
63
+ for (const calleeId of calleeSet) {
64
+ if (!reachable.has(calleeId)) {
65
+ reachable.add(calleeId);
66
+ queue.push(calleeId);
67
+ }
68
+ }
69
+ }
70
+ // Unreachable = dead code candidates
71
+ let deadEntities = entities.filter(e => e.id && !reachable.has(e.id));
72
+ if (!include_tests) {
73
+ deadEntities = deadEntities.filter(e => entityLayer(e) !== 'test');
74
+ }
75
+ // Group by layer for overview
76
+ const byLayer = new Map();
77
+ for (const e of deadEntities) {
78
+ const l = entityLayer(e);
79
+ byLayer.set(l, (byLayer.get(l) || 0) + 1);
80
+ }
81
+ return {
82
+ totalEntities: entities.length,
83
+ entryPoints: entryPointIds.size,
84
+ reachable: reachable.size,
85
+ deadCount: deadEntities.length,
86
+ deadByLayer: Object.fromEntries([...byLayer.entries()].sort((a, b) => b[1] - a[1])),
87
+ dead: deadEntities.slice(0, 100).map(entitySummary),
88
+ };
89
+ }
90
+ // ─── Functor 2: find_layer_violations ────────────────────────────
91
+ export function findLayerViolations() {
92
+ const g = getGraph();
93
+ const violations = [];
94
+ for (const [callerId, calleeIds] of g.callees) {
95
+ const callerEntity = g.entityById.get(callerId);
96
+ if (!callerEntity)
97
+ continue;
98
+ const callerLayer = entityLayer(callerEntity);
99
+ const callerOrder = LAYER_ORDER[callerLayer];
100
+ if (callerOrder === undefined)
101
+ continue;
102
+ for (const calleeId of calleeIds) {
103
+ const calleeEntity = g.entityById.get(calleeId);
104
+ if (!calleeEntity)
105
+ continue;
106
+ const calleeLayer = entityLayer(calleeEntity);
107
+ const calleeOrder = LAYER_ORDER[calleeLayer];
108
+ if (calleeOrder === undefined)
109
+ continue;
110
+ // Skip same-layer calls
111
+ if (callerLayer === calleeLayer)
112
+ continue;
113
+ // Backward call: lower layer calling higher layer
114
+ if (callerOrder > calleeOrder) {
115
+ violations.push({
116
+ from: entitySummary(callerEntity),
117
+ to: entitySummary(calleeEntity),
118
+ type: `backward: ${callerLayer}(${callerOrder}) -> ${calleeLayer}(${calleeOrder})`,
119
+ });
120
+ }
121
+ // Skip-layer: jumping over more than 1 layer
122
+ else if (calleeOrder - callerOrder > 2) {
123
+ violations.push({
124
+ from: entitySummary(callerEntity),
125
+ to: entitySummary(calleeEntity),
126
+ type: `skip-layer: ${callerLayer}(${callerOrder}) -> ${calleeLayer}(${calleeOrder})`,
127
+ });
128
+ }
129
+ }
130
+ }
131
+ // Group violations by type
132
+ const byType = new Map();
133
+ for (const v of violations) {
134
+ const typePrefix = v.type.split(':')[0];
135
+ byType.set(typePrefix, (byType.get(typePrefix) || 0) + 1);
136
+ }
137
+ return {
138
+ totalViolations: violations.length,
139
+ byType: Object.fromEntries(byType),
140
+ violations: violations.slice(0, 100),
141
+ };
142
+ }
143
+ // ─── Functor 3: get_coupling_metrics ─────────────────────────────
144
+ export function getCouplingMetrics(args) {
145
+ const { group_by = 'module' } = args;
146
+ const loader = getLoader();
147
+ const g = getGraph();
148
+ const entities = loader.getEntities();
149
+ // Group entities
150
+ const groups = new Map();
151
+ for (const e of entities) {
152
+ if (!e.id)
153
+ continue;
154
+ const key = group_by === 'module'
155
+ ? (e.context?.module || 'unknown')
156
+ : entityLayer(e);
157
+ if (!groups.has(key))
158
+ groups.set(key, new Set());
159
+ groups.get(key).add(e.id);
160
+ }
161
+ const metrics = [];
162
+ for (const [groupName, memberIds] of groups) {
163
+ let internalEdges = 0;
164
+ let outgoingEdges = 0;
165
+ let incomingEdges = 0;
166
+ for (const memberId of memberIds) {
167
+ const calleeSet = g.callees.get(memberId);
168
+ if (calleeSet) {
169
+ for (const calleeId of calleeSet) {
170
+ if (memberIds.has(calleeId)) {
171
+ internalEdges++;
172
+ }
173
+ else {
174
+ outgoingEdges++;
175
+ }
176
+ }
177
+ }
178
+ const callerSet = g.callers.get(memberId);
179
+ if (callerSet) {
180
+ for (const callerId of callerSet) {
181
+ if (!memberIds.has(callerId)) {
182
+ incomingEdges++;
183
+ }
184
+ }
185
+ }
186
+ }
187
+ const size = memberIds.size;
188
+ const maxInternalEdges = size * (size - 1); // directed
189
+ const cohesion = maxInternalEdges > 0 ? internalEdges / maxInternalEdges : 0;
190
+ const totalExternal = outgoingEdges + incomingEdges;
191
+ const coupling = totalExternal;
192
+ const instability = totalExternal > 0 ? outgoingEdges / totalExternal : 0;
193
+ metrics.push({
194
+ group: groupName,
195
+ size,
196
+ internalEdges,
197
+ externalEdges: totalExternal,
198
+ cohesion: Math.round(cohesion * 1000) / 1000,
199
+ coupling,
200
+ instability: Math.round(instability * 1000) / 1000,
201
+ });
202
+ }
203
+ // Sort by coupling (most coupled first)
204
+ metrics.sort((a, b) => b.coupling - a.coupling);
205
+ return {
206
+ groupBy: group_by,
207
+ groupCount: metrics.length,
208
+ metrics: metrics.slice(0, 50),
209
+ };
210
+ }
211
+ // ─── Functor 4: get_auth_matrix ──────────────────────────────────
212
+ export function getAuthMatrix() {
213
+ const loader = getLoader();
214
+ const entities = loader.getEntities();
215
+ const apiEntities = entities.filter(e => {
216
+ const layer = entityLayer(e);
217
+ return layer === 'route' || layer === 'controller' ||
218
+ e.context?.exposure === 'api';
219
+ });
220
+ const withAuth = [];
221
+ const withoutAuth = [];
222
+ const inconsistencies = [];
223
+ for (const e of apiEntities) {
224
+ const constraints = e.constraints;
225
+ const hasAuth = constraints && !Array.isArray(constraints) &&
226
+ constraints.auth && constraints.auth !== 'none';
227
+ const summary = entitySummary(e);
228
+ if (hasAuth) {
229
+ withAuth.push({
230
+ ...summary,
231
+ auth: constraints.auth,
232
+ });
233
+ }
234
+ else {
235
+ withoutAuth.push(summary);
236
+ }
237
+ // Check for inconsistencies: has auth decorator but marked as public
238
+ if (hasAuth && e.context?.visibility === 'public') {
239
+ inconsistencies.push({
240
+ entity: summary,
241
+ issue: 'Has auth requirements but visibility is public',
242
+ });
243
+ }
244
+ // Has DB access but no auth
245
+ if (!hasAuth) {
246
+ const sideEffects = constraints && !Array.isArray(constraints)
247
+ ? constraints.sideEffects : undefined;
248
+ if (Array.isArray(sideEffects) && sideEffects.some((s) => s === 'db' || s === 'database')) {
249
+ inconsistencies.push({
250
+ entity: summary,
251
+ issue: 'DB access without auth — potential security gap',
252
+ });
253
+ }
254
+ }
255
+ }
256
+ return {
257
+ totalApiEntities: apiEntities.length,
258
+ withAuth: withAuth.length,
259
+ withoutAuth: withoutAuth.length,
260
+ inconsistencies: inconsistencies.length,
261
+ authenticated: withAuth.slice(0, 50),
262
+ unauthenticated: withoutAuth.slice(0, 50),
263
+ issues: inconsistencies.slice(0, 50),
264
+ };
265
+ }
266
+ // ─── Functor 5: find_error_gaps ──────────────────────────────────
267
+ export function findErrorGaps() {
268
+ const loader = getLoader();
269
+ const g = getGraph();
270
+ const entities = loader.getEntities();
271
+ // Find all fallible entities (throws === true or has THROWS tag)
272
+ const fallibleIds = new Set();
273
+ for (const e of entities) {
274
+ if (!e.id)
275
+ continue;
276
+ // Check kappa.throws
277
+ const constraints = e.constraints;
278
+ if (constraints && !Array.isArray(constraints) && constraints.throws) {
279
+ fallibleIds.add(e.id);
280
+ continue;
281
+ }
282
+ // Check tau.self.fallible
283
+ const traits = e.traits;
284
+ if (traits && !Array.isArray(traits) && traits.self?.fallible) {
285
+ fallibleIds.add(e.id);
286
+ continue;
287
+ }
288
+ // Check for network/db side effects (implicitly fallible)
289
+ if (constraints && !Array.isArray(constraints) && Array.isArray(constraints.sideEffects)) {
290
+ const effects = constraints.sideEffects;
291
+ if (effects.some((s) => s === 'network' || s === 'db' || s === 'database' || s === 'filesystem' || s === 'fs')) {
292
+ fallibleIds.add(e.id);
293
+ }
294
+ }
295
+ }
296
+ // Find callers of fallible entities that lack error handling
297
+ const gaps = [];
298
+ for (const fallibleId of fallibleIds) {
299
+ const callerSet = g.callers.get(fallibleId);
300
+ if (!callerSet)
301
+ continue;
302
+ const fallibleEntity = g.entityById.get(fallibleId);
303
+ if (!fallibleEntity)
304
+ continue;
305
+ for (const callerId of callerSet) {
306
+ const callerEntity = g.entityById.get(callerId);
307
+ if (!callerEntity)
308
+ continue;
309
+ // Check if caller has error handling
310
+ const callerConstraints = callerEntity.constraints;
311
+ const hasErrorHandling = callerConstraints && !Array.isArray(callerConstraints) &&
312
+ callerConstraints.errorHandling &&
313
+ (callerConstraints.errorHandling.tryCatch || callerConstraints.errorHandling.catchClause);
314
+ if (!hasErrorHandling) {
315
+ gaps.push({
316
+ caller: entitySummary(callerEntity),
317
+ fallibleCallee: entitySummary(fallibleEntity),
318
+ issue: 'Calls fallible function without try/catch',
319
+ });
320
+ }
321
+ }
322
+ }
323
+ return {
324
+ totalFallible: fallibleIds.size,
325
+ errorGaps: gaps.length,
326
+ gaps: gaps.slice(0, 100),
327
+ };
328
+ }
329
+ // ─── Functor 6: get_test_coverage ────────────────────────────────
330
+ export function getTestCoverage(args) {
331
+ const { weight_by_blast_radius = false } = args;
332
+ const loader = getLoader();
333
+ const g = getGraph();
334
+ const entities = loader.getEntities();
335
+ // Partition into test and non-test entities
336
+ const testIds = new Set();
337
+ const productionEntities = [];
338
+ for (const e of entities) {
339
+ if (!e.id)
340
+ continue;
341
+ if (entityLayer(e) === 'test') {
342
+ testIds.add(e.id);
343
+ }
344
+ else {
345
+ productionEntities.push(e);
346
+ }
347
+ }
348
+ // BFS from test entities through callees to find what they exercise
349
+ const exercised = new Set();
350
+ const queue = [...testIds];
351
+ const visited = new Set(testIds);
352
+ while (queue.length > 0) {
353
+ const current = queue.shift();
354
+ const calleeSet = g.callees.get(current);
355
+ if (!calleeSet)
356
+ continue;
357
+ for (const calleeId of calleeSet) {
358
+ if (!visited.has(calleeId)) {
359
+ visited.add(calleeId);
360
+ if (!testIds.has(calleeId)) {
361
+ exercised.add(calleeId);
362
+ }
363
+ queue.push(calleeId);
364
+ }
365
+ }
366
+ }
367
+ const covered = productionEntities.filter(e => exercised.has(e.id));
368
+ const uncovered = productionEntities.filter(e => !exercised.has(e.id));
369
+ const result = {
370
+ totalProduction: productionEntities.length,
371
+ totalTests: testIds.size,
372
+ coveredCount: covered.length,
373
+ uncoveredCount: uncovered.length,
374
+ coveragePercent: productionEntities.length > 0
375
+ ? Math.round((covered.length / productionEntities.length) * 1000) / 10
376
+ : 0,
377
+ };
378
+ if (weight_by_blast_radius && uncovered.length > 0) {
379
+ // Compute blast radius for each uncovered entity to prioritize what to test
380
+ const prioritized = uncovered
381
+ .map(e => {
382
+ const br = computeBlastRadius(g, new Set([e.id]));
383
+ return {
384
+ ...entitySummary(e),
385
+ blastRadius: br.affected.length,
386
+ };
387
+ })
388
+ .sort((a, b) => b.blastRadius - a.blastRadius);
389
+ result.uncoveredByPriority = prioritized.slice(0, 50);
390
+ }
391
+ else {
392
+ // Group uncovered by layer
393
+ const byLayer = new Map();
394
+ for (const e of uncovered) {
395
+ const l = entityLayer(e);
396
+ byLayer.set(l, (byLayer.get(l) || 0) + 1);
397
+ }
398
+ result.uncoveredByLayer = Object.fromEntries([...byLayer.entries()].sort((a, b) => b[1] - a[1]));
399
+ result.uncovered = uncovered.slice(0, 50).map(entitySummary);
400
+ }
401
+ return result;
402
+ }
403
+ // ─── Functor 7: get_optimal_context ──────────────────────────────
404
+ export function getOptimalContext(args) {
405
+ const { target_entity, max_tokens = 8000, strategy = 'bfs' } = args;
406
+ const loader = getLoader();
407
+ const g = getGraph();
408
+ const entity = loader.getEntityById(target_entity) || loader.getEntityByName(target_entity);
409
+ if (!entity) {
410
+ return { error: `Entity not found: ${target_entity}` };
411
+ }
412
+ const targetId = entity.id;
413
+ // Estimate tokens for an entity based on its dimensions
414
+ function estimateTokens(e) {
415
+ let tokens = 50; // Base: name, id, layer
416
+ if (e.struct && typeof e.struct !== 'string') {
417
+ tokens += 20; // signature
418
+ tokens += (e.struct.params?.length || 0) * 10;
419
+ }
420
+ if (e.edges?.calls)
421
+ tokens += e.edges.calls.length * 8;
422
+ if (e.edges?.imports)
423
+ tokens += e.edges.imports.length * 6;
424
+ if (e.data?.inputs)
425
+ tokens += e.data.inputs.length * 10;
426
+ if (e.constraints && typeof e.constraints === 'object' && !Array.isArray(e.constraints)) {
427
+ tokens += 30;
428
+ }
429
+ return tokens;
430
+ }
431
+ const candidates = [];
432
+ if (strategy === 'blast_radius') {
433
+ // Use blast radius to get all related entities with depth
434
+ const br = computeBlastRadius(g, new Set([targetId]));
435
+ for (const id of br.affected) {
436
+ if (id === targetId)
437
+ continue;
438
+ const e = g.entityById.get(id);
439
+ if (!e)
440
+ continue;
441
+ const depth = Math.abs(br.depthMap[id] || 99);
442
+ const tokens = estimateTokens(e);
443
+ // Relevance decays with distance; direct callers/callees are most valuable
444
+ const relevance = 1 / (1 + depth);
445
+ candidates.push({
446
+ entity: entitySummary(e),
447
+ relevance: Math.round(relevance * 1000) / 1000,
448
+ distance: depth,
449
+ direction: (br.depthMap[id] || 0) < 0 ? 'upstream' : 'downstream',
450
+ tokens,
451
+ });
452
+ }
453
+ }
454
+ else {
455
+ // BFS from target in both directions with distance tracking
456
+ const distances = new Map();
457
+ // BFS callees (downstream)
458
+ const downQueue = [[targetId, 0]];
459
+ const downVisited = new Set([targetId]);
460
+ while (downQueue.length > 0) {
461
+ const [current, d] = downQueue.shift();
462
+ if (d > 5)
463
+ continue;
464
+ const calleeSet = g.callees.get(current);
465
+ if (!calleeSet)
466
+ continue;
467
+ for (const id of calleeSet) {
468
+ if (!downVisited.has(id)) {
469
+ downVisited.add(id);
470
+ distances.set(id, { dist: d + 1, dir: 'downstream' });
471
+ downQueue.push([id, d + 1]);
472
+ }
473
+ }
474
+ }
475
+ // BFS callers (upstream)
476
+ const upQueue = [[targetId, 0]];
477
+ const upVisited = new Set([targetId]);
478
+ while (upQueue.length > 0) {
479
+ const [current, d] = upQueue.shift();
480
+ if (d > 5)
481
+ continue;
482
+ const callerSet = g.callers.get(current);
483
+ if (!callerSet)
484
+ continue;
485
+ for (const id of callerSet) {
486
+ if (!upVisited.has(id)) {
487
+ upVisited.add(id);
488
+ // Only override if not already found with shorter distance
489
+ if (!distances.has(id) || distances.get(id).dist > d + 1) {
490
+ distances.set(id, { dist: d + 1, dir: 'upstream' });
491
+ }
492
+ upQueue.push([id, d + 1]);
493
+ }
494
+ }
495
+ }
496
+ for (const [id, { dist, dir }] of distances) {
497
+ const e = g.entityById.get(id);
498
+ if (!e)
499
+ continue;
500
+ const tokens = estimateTokens(e);
501
+ const relevance = 1 / (1 + dist);
502
+ candidates.push({
503
+ entity: entitySummary(e),
504
+ relevance: Math.round(relevance * 1000) / 1000,
505
+ distance: dist,
506
+ direction: dir,
507
+ tokens,
508
+ });
509
+ }
510
+ }
511
+ // Greedy knapsack: sort by relevance/token ratio, fill until budget
512
+ candidates.sort((a, b) => (b.relevance / b.tokens) - (a.relevance / a.tokens));
513
+ const selected = [];
514
+ const targetTokens = estimateTokens(entity);
515
+ let usedTokens = targetTokens; // Reserve space for the target itself
516
+ for (const candidate of candidates) {
517
+ if (usedTokens + candidate.tokens > max_tokens)
518
+ continue;
519
+ selected.push(candidate);
520
+ usedTokens += candidate.tokens;
521
+ }
522
+ return {
523
+ target: {
524
+ ...entitySummary(entity),
525
+ tokens: targetTokens,
526
+ },
527
+ maxTokens: max_tokens,
528
+ usedTokens,
529
+ contextEntities: selected.length,
530
+ totalCandidates: candidates.length,
531
+ context: selected,
532
+ };
533
+ }
@@ -4,8 +4,21 @@
4
4
  * Each tool exposes a dimension or computation over the 9D JSTF-T coordinate space.
5
5
  * Tools operate on the in-memory entity bundle loaded from .seshat/_bundle.json.
6
6
  */
7
+ import type { JstfEntity } from '../types.js';
7
8
  import { BundleLoader } from '../loader.js';
9
+ import { type CallGraph } from '../graph.js';
8
10
  export declare function initTools(bundleLoader: BundleLoader): void;
11
+ export declare function getLoader(): BundleLoader;
12
+ export declare function getGraph(): CallGraph;
13
+ export declare function entityName(e: JstfEntity): string;
14
+ export declare function entityLayer(e: JstfEntity): string;
15
+ export declare function entitySummary(e: JstfEntity): Record<string, unknown>;
16
+ export declare function normalizeConstraints(constraints: JstfEntity['constraints']): string[];
17
+ /**
18
+ * Deep search constraints — matches against raw constraint object fields too,
19
+ * not just normalized tags.
20
+ */
21
+ export declare function constraintMatches(constraints: JstfEntity['constraints'], target: string): boolean;
9
22
  export declare function queryEntities(args: {
10
23
  query?: string;
11
24
  layer?: string;
@@ -21,6 +34,7 @@ export declare function getDependencies(args: {
21
34
  direction?: 'callers' | 'callees' | 'both';
22
35
  depth?: number;
23
36
  }): unknown;
37
+ export declare function collectTransitive(adjacency: Map<string, Set<string>>, startId: string, maxDepth: number): string[];
24
38
  export declare function getDataFlow(args: {
25
39
  entity_id: string;
26
40
  }): unknown;
@@ -12,19 +12,22 @@ export function initTools(bundleLoader) {
12
12
  loader = bundleLoader;
13
13
  graph = null; // Reset graph cache when loader changes
14
14
  }
15
- function getGraph() {
15
+ export function getLoader() {
16
+ return loader;
17
+ }
18
+ export function getGraph() {
16
19
  if (!graph) {
17
20
  graph = buildCallGraph(loader.getEntities());
18
21
  }
19
22
  return graph;
20
23
  }
21
24
  // ─── Helper: Extract entity name from struct ─────────────────────
22
- function entityName(e) {
25
+ export function entityName(e) {
23
26
  if (typeof e.struct === 'string')
24
27
  return e.struct;
25
28
  return e.struct?.name || e.id || 'anonymous';
26
29
  }
27
- function entityLayer(e) {
30
+ export function entityLayer(e) {
28
31
  // Use explicit layer if specific enough
29
32
  const explicit = e.context?.layer?.toLowerCase();
30
33
  if (explicit && explicit !== 'module' && explicit !== 'unknown')
@@ -55,7 +58,7 @@ function entityLayer(e) {
55
58
  return 'test';
56
59
  return explicit || 'other';
57
60
  }
58
- function entitySummary(e) {
61
+ export function entitySummary(e) {
59
62
  const constraintTags = normalizeConstraints(e.constraints);
60
63
  return {
61
64
  id: e.id,
@@ -70,7 +73,7 @@ function entitySummary(e) {
70
73
  callCount: Array.isArray(e.edges?.calls) ? e.edges.calls.length : 0,
71
74
  };
72
75
  }
73
- function normalizeConstraints(constraints) {
76
+ export function normalizeConstraints(constraints) {
74
77
  if (!constraints)
75
78
  return [];
76
79
  if (Array.isArray(constraints))
@@ -124,7 +127,7 @@ function normalizeConstraints(constraints) {
124
127
  * Deep search constraints — matches against raw constraint object fields too,
125
128
  * not just normalized tags.
126
129
  */
127
- function constraintMatches(constraints, target) {
130
+ export function constraintMatches(constraints, target) {
128
131
  // First check normalized tags
129
132
  const tags = normalizeConstraints(constraints);
130
133
  if (tags.some(t => t.toUpperCase().includes(target.toUpperCase())))
@@ -226,7 +229,7 @@ export function getDependencies(args) {
226
229
  }
227
230
  return result;
228
231
  }
229
- function collectTransitive(adjacency, startId, maxDepth) {
232
+ export function collectTransitive(adjacency, startId, maxDepth) {
230
233
  const visited = new Set();
231
234
  const queue = [[startId, 0]];
232
235
  visited.add(startId);
package/dist/types.d.ts CHANGED
@@ -55,6 +55,7 @@ export interface JstfEntity {
55
55
  target: string;
56
56
  operation?: string;
57
57
  }>;
58
+ tables?: string[];
58
59
  sources?: unknown[];
59
60
  returns?: unknown[];
60
61
  [key: string]: unknown;
@@ -67,14 +68,27 @@ export interface JstfEntity {
67
68
  field: string;
68
69
  rule: string;
69
70
  }>;
71
+ auth?: string[] | 'none';
72
+ authRequired?: boolean;
73
+ purity?: string;
74
+ throws?: boolean;
75
+ sideEffects?: string[];
76
+ errorHandling?: {
77
+ tryCatch?: boolean;
78
+ catchClause?: boolean;
79
+ finally?: boolean;
80
+ errorBoundary?: boolean;
81
+ };
70
82
  [key: string]: unknown;
71
83
  } | string[];
72
84
  /** χ — Context: architectural position, visibility, layer */
73
85
  context?: {
74
86
  layer?: string;
87
+ layerSource?: string;
75
88
  module?: string;
76
89
  path?: string;
77
90
  exposure?: string;
91
+ visibility?: string;
78
92
  traffic?: string;
79
93
  criticality?: string;
80
94
  [key: string]: unknown;
@@ -82,7 +96,18 @@ export interface JstfEntity {
82
96
  /** λ — Ownership: memory ownership, lifetimes, borrowing */
83
97
  ownership?: Record<string, unknown>;
84
98
  /** τ — Traits: type capabilities, bounds, markers */
85
- traits?: string[] | Record<string, unknown>;
99
+ traits?: string[] | {
100
+ self?: {
101
+ asyncContext?: boolean;
102
+ fallible?: boolean;
103
+ [key: string]: unknown;
104
+ };
105
+ params?: Record<string, {
106
+ bounds?: string[];
107
+ markers?: string[];
108
+ }>;
109
+ [key: string]: unknown;
110
+ };
86
111
  /** ρ — Runtime: reactive model, async platform, framework */
87
112
  runtime?: {
88
113
  async?: string;
package/package.json CHANGED
@@ -1,35 +1,35 @@
1
- {
2
- "name": "@papyruslabsai/seshat-mcp",
3
- "version": "0.1.0",
4
- "description": "Semantic MCP server — exposes a codebase's 9D JSTF-T coordinate space as queryable tools",
5
- "type": "module",
6
- "bin": {
7
- "seshat-mcp": "./dist/index.js"
8
- },
9
- "main": "./dist/index.js",
10
- "scripts": {
11
- "build": "tsc",
12
- "dev": "tsc --watch",
13
- "start": "node dist/index.js"
14
- },
15
- "dependencies": {
16
- "@modelcontextprotocol/sdk": "^1.12.1"
17
- },
18
- "devDependencies": {
19
- "typescript": "^5.5.0",
20
- "@types/node": "^20.0.0"
21
- },
22
- "engines": {
23
- "node": ">=20"
24
- },
25
- "files": [
26
- "dist/"
27
- ],
28
- "repository": {
29
- "type": "git",
30
- "url": "https://github.com/papyruslabs-ai/seshat.git",
31
- "directory": "packages/seshat-mcp"
32
- },
33
- "keywords": ["mcp", "jstf", "semantic", "code-analysis", "seshat"],
34
- "license": "MIT"
35
- }
1
+ {
2
+ "name": "@papyruslabsai/seshat-mcp",
3
+ "version": "0.2.0",
4
+ "description": "Semantic MCP server — exposes a codebase's 9D JSTF-T coordinate space as queryable tools",
5
+ "type": "module",
6
+ "bin": {
7
+ "seshat-mcp": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/index.js"
14
+ },
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.12.1"
17
+ },
18
+ "devDependencies": {
19
+ "typescript": "^5.5.0",
20
+ "@types/node": "^20.0.0"
21
+ },
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
25
+ "files": [
26
+ "dist/"
27
+ ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/papyruslabs-ai/seshat.git",
31
+ "directory": "packages/seshat-mcp"
32
+ },
33
+ "keywords": ["mcp", "jstf", "semantic", "code-analysis", "seshat"],
34
+ "license": "MIT"
35
+ }