@papyruslabsai/seshat-mcp 0.2.0 → 0.3.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.
- package/dist/graph.js +24 -6
- package/dist/index.d.ts +22 -16
- package/dist/index.js +145 -35
- package/dist/loader.d.ts +29 -1
- package/dist/loader.js +131 -0
- package/dist/tools/functors.d.ts +13 -3
- package/dist/tools/functors.js +37 -16
- package/dist/tools/index.d.ts +23 -5
- package/dist/tools/index.js +71 -21
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
package/dist/graph.js
CHANGED
|
@@ -21,7 +21,10 @@ export function buildCallGraph(entities) {
|
|
|
21
21
|
callers.set(entity.id, new Set());
|
|
22
22
|
callees.set(entity.id, new Set());
|
|
23
23
|
if (entity.context?.module) {
|
|
24
|
-
|
|
24
|
+
const mod = entity.context.module;
|
|
25
|
+
if (!entityByModule.has(mod))
|
|
26
|
+
entityByModule.set(mod, []);
|
|
27
|
+
entityByModule.get(mod).push(entity);
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
// Build call edges from ε dimension
|
|
@@ -42,12 +45,27 @@ export function buildCallGraph(entities) {
|
|
|
42
45
|
}
|
|
43
46
|
// Strategy 2: Method match (target = "module.method")
|
|
44
47
|
else if (target.includes('.')) {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
const dotIdx = target.indexOf('.');
|
|
49
|
+
const modulePart = target.substring(0, dotIdx);
|
|
50
|
+
const methodPart = target.substring(dotIdx + 1);
|
|
51
|
+
// First try "module.method" as a full entity ID
|
|
52
|
+
if (entityById.has(target)) {
|
|
53
|
+
calleeId = target;
|
|
49
54
|
}
|
|
50
|
-
//
|
|
55
|
+
// Search within the module's entities for one named methodPart
|
|
56
|
+
if (!calleeId) {
|
|
57
|
+
const moduleEntities = entityByModule.get(modulePart);
|
|
58
|
+
if (moduleEntities) {
|
|
59
|
+
const match = moduleEntities.find(e => {
|
|
60
|
+
const name = typeof e.struct === 'string' ? e.struct : e.struct?.name;
|
|
61
|
+
return e.id === methodPart || name === methodPart;
|
|
62
|
+
});
|
|
63
|
+
if (match) {
|
|
64
|
+
calleeId = match.id;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Also try "module.method" concatenated as an ID
|
|
51
69
|
if (!calleeId && entityById.has(`${modulePart}.${methodPart}`)) {
|
|
52
70
|
calleeId = `${modulePart}.${methodPart}`;
|
|
53
71
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,20 +3,27 @@
|
|
|
3
3
|
* @papyruslabs/seshat-mcp — Semantic MCP Server
|
|
4
4
|
*
|
|
5
5
|
* Exposes a codebase's 9D JSTF-T coordinate space as queryable MCP tools.
|
|
6
|
-
* Reads .seshat/_bundle.json from
|
|
6
|
+
* Reads .seshat/_bundle.json from project directories.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* get_data_flow — δ dimension: inputs, outputs, mutations
|
|
13
|
-
* find_by_constraint — κ dimension search (AUTH, THROWS, DB_ACCESS, etc.)
|
|
14
|
-
* get_blast_radius — Theorem 9.4: affected set for given entity IDs
|
|
15
|
-
* list_modules — Group entities by layer, module, file, or language
|
|
16
|
-
* get_topology — API topology (routes, plugins, auth, tables)
|
|
8
|
+
* Multi-project mode:
|
|
9
|
+
* Set SESHAT_PROJECTS env var to comma-separated paths or a glob pattern.
|
|
10
|
+
* In multi-project mode, all tools require a `project` parameter.
|
|
11
|
+
* Use `list_projects` to see available projects.
|
|
17
12
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
13
|
+
* Single-project mode (default):
|
|
14
|
+
* When SESHAT_PROJECTS is not set, loads from CWD. No `project` param needed.
|
|
15
|
+
*
|
|
16
|
+
* Tools (8 core + 7 interpretation functors + 1 meta):
|
|
17
|
+
* list_projects — Show loaded projects with entity counts
|
|
18
|
+
* query_entities — Search entities by name, layer, module, language
|
|
19
|
+
* get_entity — Full 9D coordinate dump for one entity
|
|
20
|
+
* get_dependencies — ε edge traversal (callers/callees with depth control)
|
|
21
|
+
* get_data_flow — δ dimension: inputs, outputs, mutations
|
|
22
|
+
* find_by_constraint — κ dimension search (AUTH, THROWS, DB_ACCESS, etc.)
|
|
23
|
+
* get_blast_radius — Theorem 9.4: affected set for given entity IDs
|
|
24
|
+
* list_modules — Group entities by layer, module, file, or language
|
|
25
|
+
* get_topology — API topology (routes, plugins, auth, tables)
|
|
26
|
+
* find_dead_code — Unreachable entities via ε-graph BFS
|
|
20
27
|
* find_layer_violations — ε edges violating architectural layer ordering
|
|
21
28
|
* get_coupling_metrics — Module coupling/cohesion/instability from ε-graph
|
|
22
29
|
* get_auth_matrix — Auth coverage across API-facing entities from κ
|
|
@@ -25,9 +32,8 @@
|
|
|
25
32
|
* get_optimal_context — Greedy knapsack: max relevance per token for LLM context
|
|
26
33
|
*
|
|
27
34
|
* Usage:
|
|
28
|
-
* npx @papyruslabs/seshat-mcp
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* { "mcpServers": { "seshat": { "command": "npx", "args": ["@papyruslabs/seshat-mcp"] } } }
|
|
35
|
+
* npx @papyruslabs/seshat-mcp # single project (CWD)
|
|
36
|
+
* SESHAT_PROJECTS=/path/a,/path/b npx @papyruslabs/seshat-mcp # multi-project
|
|
37
|
+
* SESHAT_PROJECTS="/home/user/projects/*" npx @papyruslabs/seshat-mcp # glob
|
|
32
38
|
*/
|
|
33
39
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -3,20 +3,27 @@
|
|
|
3
3
|
* @papyruslabs/seshat-mcp — Semantic MCP Server
|
|
4
4
|
*
|
|
5
5
|
* Exposes a codebase's 9D JSTF-T coordinate space as queryable MCP tools.
|
|
6
|
-
* Reads .seshat/_bundle.json from
|
|
6
|
+
* Reads .seshat/_bundle.json from project directories.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* get_data_flow — δ dimension: inputs, outputs, mutations
|
|
13
|
-
* find_by_constraint — κ dimension search (AUTH, THROWS, DB_ACCESS, etc.)
|
|
14
|
-
* get_blast_radius — Theorem 9.4: affected set for given entity IDs
|
|
15
|
-
* list_modules — Group entities by layer, module, file, or language
|
|
16
|
-
* get_topology — API topology (routes, plugins, auth, tables)
|
|
8
|
+
* Multi-project mode:
|
|
9
|
+
* Set SESHAT_PROJECTS env var to comma-separated paths or a glob pattern.
|
|
10
|
+
* In multi-project mode, all tools require a `project` parameter.
|
|
11
|
+
* Use `list_projects` to see available projects.
|
|
17
12
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
13
|
+
* Single-project mode (default):
|
|
14
|
+
* When SESHAT_PROJECTS is not set, loads from CWD. No `project` param needed.
|
|
15
|
+
*
|
|
16
|
+
* Tools (8 core + 7 interpretation functors + 1 meta):
|
|
17
|
+
* list_projects — Show loaded projects with entity counts
|
|
18
|
+
* query_entities — Search entities by name, layer, module, language
|
|
19
|
+
* get_entity — Full 9D coordinate dump for one entity
|
|
20
|
+
* get_dependencies — ε edge traversal (callers/callees with depth control)
|
|
21
|
+
* get_data_flow — δ dimension: inputs, outputs, mutations
|
|
22
|
+
* find_by_constraint — κ dimension search (AUTH, THROWS, DB_ACCESS, etc.)
|
|
23
|
+
* get_blast_radius — Theorem 9.4: affected set for given entity IDs
|
|
24
|
+
* list_modules — Group entities by layer, module, file, or language
|
|
25
|
+
* get_topology — API topology (routes, plugins, auth, tables)
|
|
26
|
+
* find_dead_code — Unreachable entities via ε-graph BFS
|
|
20
27
|
* find_layer_violations — ε edges violating architectural layer ordering
|
|
21
28
|
* get_coupling_metrics — Module coupling/cohesion/instability from ε-graph
|
|
22
29
|
* get_auth_matrix — Auth coverage across API-facing entities from κ
|
|
@@ -25,25 +32,80 @@
|
|
|
25
32
|
* get_optimal_context — Greedy knapsack: max relevance per token for LLM context
|
|
26
33
|
*
|
|
27
34
|
* Usage:
|
|
28
|
-
* npx @papyruslabs/seshat-mcp
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* { "mcpServers": { "seshat": { "command": "npx", "args": ["@papyruslabs/seshat-mcp"] } } }
|
|
35
|
+
* npx @papyruslabs/seshat-mcp # single project (CWD)
|
|
36
|
+
* SESHAT_PROJECTS=/path/a,/path/b npx @papyruslabs/seshat-mcp # multi-project
|
|
37
|
+
* SESHAT_PROJECTS="/home/user/projects/*" npx @papyruslabs/seshat-mcp # glob
|
|
32
38
|
*/
|
|
39
|
+
import fs from 'fs';
|
|
40
|
+
import path from 'path';
|
|
33
41
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
34
42
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
35
43
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
36
|
-
import {
|
|
44
|
+
import { MultiLoader } from './loader.js';
|
|
37
45
|
import { initTools, queryEntities, getEntity, getDependencies, getDataFlow, findByConstraint, getBlastRadius, listModules, getTopology, } from './tools/index.js';
|
|
38
46
|
import { findDeadCode, findLayerViolations, getCouplingMetrics, getAuthMatrix, findErrorGaps, getTestCoverage, getOptimalContext, } from './tools/functors.js';
|
|
47
|
+
// ─── Project Discovery ───────────────────────────────────────────
|
|
48
|
+
/**
|
|
49
|
+
* Discover project directories from SESHAT_PROJECTS env var.
|
|
50
|
+
*
|
|
51
|
+
* - Not set → [process.cwd()] (backward compat)
|
|
52
|
+
* - Comma-separated paths → resolve each
|
|
53
|
+
* - Glob pattern (contains *) → scan parent dir for subdirs with .seshat/_bundle.json
|
|
54
|
+
*/
|
|
55
|
+
function discoverProjects() {
|
|
56
|
+
const env = process.env.SESHAT_PROJECTS;
|
|
57
|
+
if (!env)
|
|
58
|
+
return [process.cwd()];
|
|
59
|
+
const trimmed = env.trim();
|
|
60
|
+
// Glob pattern: /path/to/projects/*
|
|
61
|
+
if (trimmed.includes('*')) {
|
|
62
|
+
const globDir = trimmed.replace(/\/?\*$/, '');
|
|
63
|
+
const resolved = path.resolve(globDir);
|
|
64
|
+
if (!fs.existsSync(resolved)) {
|
|
65
|
+
process.stderr.write(`SESHAT_PROJECTS glob dir not found: ${resolved}\n`);
|
|
66
|
+
return [process.cwd()];
|
|
67
|
+
}
|
|
68
|
+
const dirs = [];
|
|
69
|
+
for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
|
|
70
|
+
if (!entry.isDirectory())
|
|
71
|
+
continue;
|
|
72
|
+
const candidate = path.join(resolved, entry.name);
|
|
73
|
+
const bundlePath = path.join(candidate, '.seshat', '_bundle.json');
|
|
74
|
+
if (fs.existsSync(bundlePath)) {
|
|
75
|
+
dirs.push(candidate);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (dirs.length === 0) {
|
|
79
|
+
process.stderr.write(`SESHAT_PROJECTS: no dirs with .seshat/_bundle.json found in ${resolved}\n`);
|
|
80
|
+
return [process.cwd()];
|
|
81
|
+
}
|
|
82
|
+
return dirs;
|
|
83
|
+
}
|
|
84
|
+
// Comma-separated paths
|
|
85
|
+
return trimmed.split(',').map(p => path.resolve(p.trim())).filter(p => p.length > 0);
|
|
86
|
+
}
|
|
87
|
+
// ─── Project param definition (injected into all tool schemas) ───
|
|
88
|
+
const projectParam = {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'Project name (required in multi-project mode). Use list_projects to see available projects.',
|
|
91
|
+
};
|
|
39
92
|
// ─── Tool Definitions ─────────────────────────────────────────────
|
|
40
93
|
const TOOLS = [
|
|
94
|
+
{
|
|
95
|
+
name: 'list_projects',
|
|
96
|
+
description: 'List all loaded projects with entity counts, languages, and metadata. Call this first to see what projects are available, then pass the project name to other tools.',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
41
102
|
{
|
|
42
103
|
name: 'query_entities',
|
|
43
104
|
description: 'Search entities in the 9D semantic coordinate space. Filter by name, architectural layer (route/service/repository/component), module, or source language. Returns entity summaries with constraint tags.',
|
|
44
105
|
inputSchema: {
|
|
45
106
|
type: 'object',
|
|
46
107
|
properties: {
|
|
108
|
+
project: projectParam,
|
|
47
109
|
query: {
|
|
48
110
|
type: 'string',
|
|
49
111
|
description: 'Search term — matches against entity name, ID, source file, and module',
|
|
@@ -73,6 +135,7 @@ const TOOLS = [
|
|
|
73
135
|
inputSchema: {
|
|
74
136
|
type: 'object',
|
|
75
137
|
properties: {
|
|
138
|
+
project: projectParam,
|
|
76
139
|
id: {
|
|
77
140
|
type: 'string',
|
|
78
141
|
description: 'Entity ID or name',
|
|
@@ -87,6 +150,7 @@ const TOOLS = [
|
|
|
87
150
|
inputSchema: {
|
|
88
151
|
type: 'object',
|
|
89
152
|
properties: {
|
|
153
|
+
project: projectParam,
|
|
90
154
|
entity_id: {
|
|
91
155
|
type: 'string',
|
|
92
156
|
description: 'Entity ID or name',
|
|
@@ -110,6 +174,7 @@ const TOOLS = [
|
|
|
110
174
|
inputSchema: {
|
|
111
175
|
type: 'object',
|
|
112
176
|
properties: {
|
|
177
|
+
project: projectParam,
|
|
113
178
|
entity_id: {
|
|
114
179
|
type: 'string',
|
|
115
180
|
description: 'Entity ID or name',
|
|
@@ -124,6 +189,7 @@ const TOOLS = [
|
|
|
124
189
|
inputSchema: {
|
|
125
190
|
type: 'object',
|
|
126
191
|
properties: {
|
|
192
|
+
project: projectParam,
|
|
127
193
|
constraint: {
|
|
128
194
|
type: 'string',
|
|
129
195
|
description: 'Constraint tag to search for: AUTH, VALIDATED, PURE, THROWS, DB_ACCESS, NETWORK_IO, IMP, etc.',
|
|
@@ -138,6 +204,7 @@ const TOOLS = [
|
|
|
138
204
|
inputSchema: {
|
|
139
205
|
type: 'object',
|
|
140
206
|
properties: {
|
|
207
|
+
project: projectParam,
|
|
141
208
|
entity_ids: {
|
|
142
209
|
type: 'array',
|
|
143
210
|
items: { type: 'string' },
|
|
@@ -153,6 +220,7 @@ const TOOLS = [
|
|
|
153
220
|
inputSchema: {
|
|
154
221
|
type: 'object',
|
|
155
222
|
properties: {
|
|
223
|
+
project: projectParam,
|
|
156
224
|
group_by: {
|
|
157
225
|
type: 'string',
|
|
158
226
|
enum: ['layer', 'module', 'file', 'language'],
|
|
@@ -166,7 +234,9 @@ const TOOLS = [
|
|
|
166
234
|
description: 'Get the API topology: routes, plugins, auth patterns, and database tables. Built from the χ (context) and ε (edges) dimensions across all entities.',
|
|
167
235
|
inputSchema: {
|
|
168
236
|
type: 'object',
|
|
169
|
-
properties: {
|
|
237
|
+
properties: {
|
|
238
|
+
project: projectParam,
|
|
239
|
+
},
|
|
170
240
|
},
|
|
171
241
|
},
|
|
172
242
|
// ─── Interpretation Functor Tools ────────────────────────────────
|
|
@@ -176,6 +246,7 @@ const TOOLS = [
|
|
|
176
246
|
inputSchema: {
|
|
177
247
|
type: 'object',
|
|
178
248
|
properties: {
|
|
249
|
+
project: projectParam,
|
|
179
250
|
include_tests: {
|
|
180
251
|
type: 'boolean',
|
|
181
252
|
description: 'Include test entities in dead code results (default: false)',
|
|
@@ -188,7 +259,9 @@ const TOOLS = [
|
|
|
188
259
|
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
260
|
inputSchema: {
|
|
190
261
|
type: 'object',
|
|
191
|
-
properties: {
|
|
262
|
+
properties: {
|
|
263
|
+
project: projectParam,
|
|
264
|
+
},
|
|
192
265
|
},
|
|
193
266
|
},
|
|
194
267
|
{
|
|
@@ -197,6 +270,7 @@ const TOOLS = [
|
|
|
197
270
|
inputSchema: {
|
|
198
271
|
type: 'object',
|
|
199
272
|
properties: {
|
|
273
|
+
project: projectParam,
|
|
200
274
|
group_by: {
|
|
201
275
|
type: 'string',
|
|
202
276
|
enum: ['module', 'layer'],
|
|
@@ -210,7 +284,9 @@ const TOOLS = [
|
|
|
210
284
|
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
285
|
inputSchema: {
|
|
212
286
|
type: 'object',
|
|
213
|
-
properties: {
|
|
287
|
+
properties: {
|
|
288
|
+
project: projectParam,
|
|
289
|
+
},
|
|
214
290
|
},
|
|
215
291
|
},
|
|
216
292
|
{
|
|
@@ -218,7 +294,9 @@ const TOOLS = [
|
|
|
218
294
|
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
295
|
inputSchema: {
|
|
220
296
|
type: 'object',
|
|
221
|
-
properties: {
|
|
297
|
+
properties: {
|
|
298
|
+
project: projectParam,
|
|
299
|
+
},
|
|
222
300
|
},
|
|
223
301
|
},
|
|
224
302
|
{
|
|
@@ -227,6 +305,7 @@ const TOOLS = [
|
|
|
227
305
|
inputSchema: {
|
|
228
306
|
type: 'object',
|
|
229
307
|
properties: {
|
|
308
|
+
project: projectParam,
|
|
230
309
|
weight_by_blast_radius: {
|
|
231
310
|
type: 'boolean',
|
|
232
311
|
description: 'Rank uncovered entities by blast radius to prioritize testing (default: false, slower)',
|
|
@@ -240,6 +319,7 @@ const TOOLS = [
|
|
|
240
319
|
inputSchema: {
|
|
241
320
|
type: 'object',
|
|
242
321
|
properties: {
|
|
322
|
+
project: projectParam,
|
|
243
323
|
target_entity: {
|
|
244
324
|
type: 'string',
|
|
245
325
|
description: 'Entity ID or name to build context around',
|
|
@@ -260,22 +340,34 @@ const TOOLS = [
|
|
|
260
340
|
];
|
|
261
341
|
// ─── Server Setup ─────────────────────────────────────────────────
|
|
262
342
|
async function main() {
|
|
263
|
-
//
|
|
264
|
-
const
|
|
343
|
+
// Discover and load projects
|
|
344
|
+
const projectDirs = discoverProjects();
|
|
345
|
+
const loader = new MultiLoader(projectDirs);
|
|
265
346
|
try {
|
|
266
347
|
loader.load();
|
|
267
348
|
}
|
|
268
349
|
catch (err) {
|
|
269
|
-
// Server will start but tools will return helpful errors
|
|
270
350
|
process.stderr.write(`Warning: ${err.message}\n`);
|
|
271
351
|
}
|
|
272
352
|
initTools(loader);
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
const
|
|
353
|
+
// Build server name
|
|
354
|
+
const projectNames = loader.getProjectNames();
|
|
355
|
+
const totalEntities = loader.totalEntities();
|
|
356
|
+
const isMulti = loader.isMultiProject();
|
|
357
|
+
let serverLabel;
|
|
358
|
+
if (!loader.isLoaded()) {
|
|
359
|
+
serverLabel = 'seshat-mcp (no projects loaded)';
|
|
360
|
+
}
|
|
361
|
+
else if (isMulti) {
|
|
362
|
+
serverLabel = `seshat-mcp (${projectNames.length} projects, ${totalEntities} entities)`;
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
const manifest = loader.getManifest();
|
|
366
|
+
serverLabel = `seshat-mcp (${manifest?.projectName || projectNames[0] || 'unknown'})`;
|
|
367
|
+
}
|
|
276
368
|
const server = new Server({
|
|
277
|
-
name:
|
|
278
|
-
version: '0.
|
|
369
|
+
name: serverLabel,
|
|
370
|
+
version: '0.3.1',
|
|
279
371
|
}, {
|
|
280
372
|
capabilities: {
|
|
281
373
|
tools: {},
|
|
@@ -294,13 +386,25 @@ async function main() {
|
|
|
294
386
|
content: [{
|
|
295
387
|
type: 'text',
|
|
296
388
|
text: JSON.stringify({
|
|
297
|
-
error: 'No .seshat/_bundle.json
|
|
389
|
+
error: 'No projects loaded. Ensure .seshat/_bundle.json exists or set SESHAT_PROJECTS env var.',
|
|
298
390
|
}, null, 2),
|
|
299
391
|
}],
|
|
300
392
|
};
|
|
301
393
|
}
|
|
302
394
|
let result;
|
|
303
395
|
switch (name) {
|
|
396
|
+
// Meta
|
|
397
|
+
case 'list_projects':
|
|
398
|
+
result = {
|
|
399
|
+
multiProject: isMulti,
|
|
400
|
+
projects: loader.getProjectInfo(),
|
|
401
|
+
totalEntities,
|
|
402
|
+
hint: isMulti
|
|
403
|
+
? 'Pass the project name as the "project" parameter to all other tools.'
|
|
404
|
+
: 'Single project mode — the "project" parameter is optional.',
|
|
405
|
+
};
|
|
406
|
+
break;
|
|
407
|
+
// Core tools
|
|
304
408
|
case 'query_entities':
|
|
305
409
|
result = queryEntities(args);
|
|
306
410
|
break;
|
|
@@ -323,23 +427,23 @@ async function main() {
|
|
|
323
427
|
result = listModules(args);
|
|
324
428
|
break;
|
|
325
429
|
case 'get_topology':
|
|
326
|
-
result = getTopology();
|
|
430
|
+
result = getTopology(args);
|
|
327
431
|
break;
|
|
328
432
|
// Interpretation Functors
|
|
329
433
|
case 'find_dead_code':
|
|
330
434
|
result = findDeadCode(args);
|
|
331
435
|
break;
|
|
332
436
|
case 'find_layer_violations':
|
|
333
|
-
result = findLayerViolations();
|
|
437
|
+
result = findLayerViolations(args);
|
|
334
438
|
break;
|
|
335
439
|
case 'get_coupling_metrics':
|
|
336
440
|
result = getCouplingMetrics(args);
|
|
337
441
|
break;
|
|
338
442
|
case 'get_auth_matrix':
|
|
339
|
-
result = getAuthMatrix();
|
|
443
|
+
result = getAuthMatrix(args);
|
|
340
444
|
break;
|
|
341
445
|
case 'find_error_gaps':
|
|
342
|
-
result = findErrorGaps();
|
|
446
|
+
result = findErrorGaps(args);
|
|
343
447
|
break;
|
|
344
448
|
case 'get_test_coverage':
|
|
345
449
|
result = getTestCoverage(args);
|
|
@@ -372,7 +476,13 @@ async function main() {
|
|
|
372
476
|
// Start stdio transport
|
|
373
477
|
const transport = new StdioServerTransport();
|
|
374
478
|
await server.connect(transport);
|
|
375
|
-
|
|
479
|
+
if (isMulti) {
|
|
480
|
+
process.stderr.write(`Seshat MCP server started: ${projectNames.length} projects (${totalEntities} entities) — ${projectNames.join(', ')}\n`);
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
const manifest = loader.getManifest();
|
|
484
|
+
process.stderr.write(`Seshat MCP server started: ${manifest?.projectName || 'unknown'} (${totalEntities} entities)\n`);
|
|
485
|
+
}
|
|
376
486
|
}
|
|
377
487
|
main().catch((err) => {
|
|
378
488
|
process.stderr.write(`Fatal: ${err.message}\n`);
|
package/dist/loader.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Discovers and loads .seshat/_bundle.json from the current working directory.
|
|
5
5
|
* Falls back to checking common alternative locations.
|
|
6
6
|
*/
|
|
7
|
-
import type { JstfEntity, Topology, Manifest } from './types.js';
|
|
7
|
+
import type { JstfEntity, Topology, Manifest, ProjectInfo } from './types.js';
|
|
8
8
|
export declare class BundleLoader {
|
|
9
9
|
private entities;
|
|
10
10
|
private topology;
|
|
@@ -20,3 +20,31 @@ export declare class BundleLoader {
|
|
|
20
20
|
getEntityByName(name: string): JstfEntity | undefined;
|
|
21
21
|
isLoaded(): boolean;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Wraps N BundleLoader instances for multi-project access.
|
|
25
|
+
* In single-project mode (1 dir), behaves identically to BundleLoader.
|
|
26
|
+
* In multi-project mode, `project` param is required on all accessors.
|
|
27
|
+
*/
|
|
28
|
+
export declare class MultiLoader {
|
|
29
|
+
private projects;
|
|
30
|
+
private projectPaths;
|
|
31
|
+
private projectEntities;
|
|
32
|
+
private loaded;
|
|
33
|
+
private dirs;
|
|
34
|
+
constructor(projectDirs: string[]);
|
|
35
|
+
load(): void;
|
|
36
|
+
/** Get the resolved project name when only one project is loaded. */
|
|
37
|
+
private defaultProject;
|
|
38
|
+
private resolveProject;
|
|
39
|
+
getEntities(project?: string): JstfEntity[];
|
|
40
|
+
getTopology(project?: string): Topology | null;
|
|
41
|
+
getManifest(project?: string): Manifest | null;
|
|
42
|
+
getEntityById(id: string, project?: string): JstfEntity | undefined;
|
|
43
|
+
getEntityByName(name: string, project?: string): JstfEntity | undefined;
|
|
44
|
+
getProjectNames(): string[];
|
|
45
|
+
getProjectInfo(): ProjectInfo[];
|
|
46
|
+
isMultiProject(): boolean;
|
|
47
|
+
isLoaded(): boolean;
|
|
48
|
+
hasProject(name: string): boolean;
|
|
49
|
+
totalEntities(): number;
|
|
50
|
+
}
|
package/dist/loader.js
CHANGED
|
@@ -27,6 +27,18 @@ export class BundleLoader {
|
|
|
27
27
|
const raw = fs.readFileSync(bundlePath, 'utf-8');
|
|
28
28
|
const bundle = JSON.parse(raw);
|
|
29
29
|
this.entities = bundle.entities || [];
|
|
30
|
+
// Remap bundle field names to internal _ prefixed names.
|
|
31
|
+
// The extraction pipeline outputs `sourceFile`, `sourceLanguage`, `_jstfFilename`
|
|
32
|
+
// but our JstfEntity type expects `_sourceFile`, `_sourceLanguage`.
|
|
33
|
+
for (const e of this.entities) {
|
|
34
|
+
const raw = e;
|
|
35
|
+
if (raw.sourceFile && !e._sourceFile) {
|
|
36
|
+
e._sourceFile = raw.sourceFile;
|
|
37
|
+
}
|
|
38
|
+
if (raw.sourceLanguage && !e._sourceLanguage) {
|
|
39
|
+
e._sourceLanguage = raw.sourceLanguage;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
30
42
|
// Load topology if available
|
|
31
43
|
const topoPath = path.join(this.seshatDir, '_topology.json');
|
|
32
44
|
if (fs.existsSync(topoPath)) {
|
|
@@ -67,3 +79,122 @@ export class BundleLoader {
|
|
|
67
79
|
return this.loaded;
|
|
68
80
|
}
|
|
69
81
|
}
|
|
82
|
+
// ─── Multi-Project Loader ─────────────────────────────────────────
|
|
83
|
+
/**
|
|
84
|
+
* Wraps N BundleLoader instances for multi-project access.
|
|
85
|
+
* In single-project mode (1 dir), behaves identically to BundleLoader.
|
|
86
|
+
* In multi-project mode, `project` param is required on all accessors.
|
|
87
|
+
*/
|
|
88
|
+
export class MultiLoader {
|
|
89
|
+
projects = new Map();
|
|
90
|
+
projectPaths = new Map();
|
|
91
|
+
projectEntities = new Map();
|
|
92
|
+
loaded = false;
|
|
93
|
+
dirs;
|
|
94
|
+
constructor(projectDirs) {
|
|
95
|
+
this.dirs = projectDirs.length > 0 ? projectDirs : [process.cwd()];
|
|
96
|
+
}
|
|
97
|
+
load() {
|
|
98
|
+
for (const dir of this.dirs) {
|
|
99
|
+
const loader = new BundleLoader(dir);
|
|
100
|
+
try {
|
|
101
|
+
loader.load();
|
|
102
|
+
const manifest = loader.getManifest();
|
|
103
|
+
const name = manifest?.projectName || path.basename(dir);
|
|
104
|
+
this.projects.set(name, loader);
|
|
105
|
+
this.projectPaths.set(name, dir);
|
|
106
|
+
// Stamp entities with _project
|
|
107
|
+
const entities = loader.getEntities();
|
|
108
|
+
for (const e of entities) {
|
|
109
|
+
e._project = name;
|
|
110
|
+
}
|
|
111
|
+
this.projectEntities.set(name, entities);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
process.stderr.write(`Warning: Skipping ${dir}: ${err.message}\n`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
this.loaded = true;
|
|
118
|
+
}
|
|
119
|
+
/** Get the resolved project name when only one project is loaded. */
|
|
120
|
+
defaultProject() {
|
|
121
|
+
if (this.projects.size === 1)
|
|
122
|
+
return [...this.projects.keys()][0];
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
resolveProject(project) {
|
|
126
|
+
return project || this.defaultProject();
|
|
127
|
+
}
|
|
128
|
+
getEntities(project) {
|
|
129
|
+
if (!this.loaded)
|
|
130
|
+
this.load();
|
|
131
|
+
const p = this.resolveProject(project);
|
|
132
|
+
if (p)
|
|
133
|
+
return this.projectEntities.get(p) || [];
|
|
134
|
+
return []; // multi-project with no project specified
|
|
135
|
+
}
|
|
136
|
+
getTopology(project) {
|
|
137
|
+
if (!this.loaded)
|
|
138
|
+
this.load();
|
|
139
|
+
const p = this.resolveProject(project);
|
|
140
|
+
if (p)
|
|
141
|
+
return this.projects.get(p)?.getTopology() || null;
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
getManifest(project) {
|
|
145
|
+
if (!this.loaded)
|
|
146
|
+
this.load();
|
|
147
|
+
const p = this.resolveProject(project);
|
|
148
|
+
if (p)
|
|
149
|
+
return this.projects.get(p)?.getManifest() || null;
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
getEntityById(id, project) {
|
|
153
|
+
return this.getEntities(project).find(e => e.id === id);
|
|
154
|
+
}
|
|
155
|
+
getEntityByName(name, project) {
|
|
156
|
+
return this.getEntities(project).find(e => {
|
|
157
|
+
const structName = typeof e.struct === 'string' ? e.struct : e.struct?.name;
|
|
158
|
+
return e.id === name || structName === name;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
getProjectNames() {
|
|
162
|
+
if (!this.loaded)
|
|
163
|
+
this.load();
|
|
164
|
+
return [...this.projects.keys()];
|
|
165
|
+
}
|
|
166
|
+
getProjectInfo() {
|
|
167
|
+
if (!this.loaded)
|
|
168
|
+
this.load();
|
|
169
|
+
const result = [];
|
|
170
|
+
for (const [name, loader] of this.projects) {
|
|
171
|
+
const manifest = loader.getManifest();
|
|
172
|
+
result.push({
|
|
173
|
+
name,
|
|
174
|
+
path: this.projectPaths.get(name) || '',
|
|
175
|
+
entityCount: loader.getEntities().length,
|
|
176
|
+
languages: manifest?.languages || [],
|
|
177
|
+
commitSha: manifest?.commitSha || '',
|
|
178
|
+
extractedAt: manifest?.extractedAt || '',
|
|
179
|
+
layers: manifest?.layers || {},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
isMultiProject() {
|
|
185
|
+
return this.projects.size > 1;
|
|
186
|
+
}
|
|
187
|
+
isLoaded() {
|
|
188
|
+
return this.loaded && this.projects.size > 0;
|
|
189
|
+
}
|
|
190
|
+
hasProject(name) {
|
|
191
|
+
return this.projects.has(name);
|
|
192
|
+
}
|
|
193
|
+
totalEntities() {
|
|
194
|
+
let total = 0;
|
|
195
|
+
for (const entities of this.projectEntities.values()) {
|
|
196
|
+
total += entities.length;
|
|
197
|
+
}
|
|
198
|
+
return total;
|
|
199
|
+
}
|
|
200
|
+
}
|
package/dist/tools/functors.d.ts
CHANGED
|
@@ -7,18 +7,28 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function findDeadCode(args: {
|
|
9
9
|
include_tests?: boolean;
|
|
10
|
+
project?: string;
|
|
11
|
+
}): unknown;
|
|
12
|
+
export declare function findLayerViolations(args?: {
|
|
13
|
+
project?: string;
|
|
10
14
|
}): unknown;
|
|
11
|
-
export declare function findLayerViolations(): unknown;
|
|
12
15
|
export declare function getCouplingMetrics(args: {
|
|
13
16
|
group_by?: 'module' | 'layer';
|
|
17
|
+
project?: string;
|
|
18
|
+
}): unknown;
|
|
19
|
+
export declare function getAuthMatrix(args?: {
|
|
20
|
+
project?: string;
|
|
21
|
+
}): unknown;
|
|
22
|
+
export declare function findErrorGaps(args?: {
|
|
23
|
+
project?: string;
|
|
14
24
|
}): unknown;
|
|
15
|
-
export declare function getAuthMatrix(): unknown;
|
|
16
|
-
export declare function findErrorGaps(): unknown;
|
|
17
25
|
export declare function getTestCoverage(args: {
|
|
18
26
|
weight_by_blast_radius?: boolean;
|
|
27
|
+
project?: string;
|
|
19
28
|
}): unknown;
|
|
20
29
|
export declare function getOptimalContext(args: {
|
|
21
30
|
target_entity: string;
|
|
22
31
|
max_tokens?: number;
|
|
23
32
|
strategy?: 'bfs' | 'blast_radius';
|
|
33
|
+
project?: string;
|
|
24
34
|
}): unknown;
|
package/dist/tools/functors.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* from the primitive dimensions (sigma, epsilon, delta, kappa, chi, tau, rho).
|
|
7
7
|
*/
|
|
8
8
|
import { computeBlastRadius } from '../graph.js';
|
|
9
|
-
import { getLoader, getGraph, entityLayer, entitySummary, } from './index.js';
|
|
9
|
+
import { getLoader, getGraph, validateProject, entityLayer, entitySummary, } from './index.js';
|
|
10
10
|
// ─── Layer ordering for violation detection ──────────────────────
|
|
11
11
|
const LAYER_ORDER = {
|
|
12
12
|
route: 0,
|
|
@@ -22,10 +22,13 @@ const LAYER_ORDER = {
|
|
|
22
22
|
};
|
|
23
23
|
// ─── Functor 1: find_dead_code ───────────────────────────────────
|
|
24
24
|
export function findDeadCode(args) {
|
|
25
|
+
const projErr = validateProject(args.project);
|
|
26
|
+
if (projErr)
|
|
27
|
+
return { error: projErr };
|
|
25
28
|
const { include_tests = false } = args;
|
|
26
29
|
const loader = getLoader();
|
|
27
|
-
const g = getGraph();
|
|
28
|
-
const entities = loader.getEntities();
|
|
30
|
+
const g = getGraph(args.project);
|
|
31
|
+
const entities = loader.getEntities(args.project);
|
|
29
32
|
// Entry points: routes, exported functions, test files, plugin registrations
|
|
30
33
|
const entryPointIds = new Set();
|
|
31
34
|
for (const e of entities) {
|
|
@@ -88,8 +91,11 @@ export function findDeadCode(args) {
|
|
|
88
91
|
};
|
|
89
92
|
}
|
|
90
93
|
// ─── Functor 2: find_layer_violations ────────────────────────────
|
|
91
|
-
export function findLayerViolations() {
|
|
92
|
-
const
|
|
94
|
+
export function findLayerViolations(args) {
|
|
95
|
+
const projErr = validateProject(args?.project);
|
|
96
|
+
if (projErr)
|
|
97
|
+
return { error: projErr };
|
|
98
|
+
const g = getGraph(args?.project);
|
|
93
99
|
const violations = [];
|
|
94
100
|
for (const [callerId, calleeIds] of g.callees) {
|
|
95
101
|
const callerEntity = g.entityById.get(callerId);
|
|
@@ -142,10 +148,13 @@ export function findLayerViolations() {
|
|
|
142
148
|
}
|
|
143
149
|
// ─── Functor 3: get_coupling_metrics ─────────────────────────────
|
|
144
150
|
export function getCouplingMetrics(args) {
|
|
151
|
+
const projErr = validateProject(args.project);
|
|
152
|
+
if (projErr)
|
|
153
|
+
return { error: projErr };
|
|
145
154
|
const { group_by = 'module' } = args;
|
|
146
155
|
const loader = getLoader();
|
|
147
|
-
const g = getGraph();
|
|
148
|
-
const entities = loader.getEntities();
|
|
156
|
+
const g = getGraph(args.project);
|
|
157
|
+
const entities = loader.getEntities(args.project);
|
|
149
158
|
// Group entities
|
|
150
159
|
const groups = new Map();
|
|
151
160
|
for (const e of entities) {
|
|
@@ -209,9 +218,12 @@ export function getCouplingMetrics(args) {
|
|
|
209
218
|
};
|
|
210
219
|
}
|
|
211
220
|
// ─── Functor 4: get_auth_matrix ──────────────────────────────────
|
|
212
|
-
export function getAuthMatrix() {
|
|
221
|
+
export function getAuthMatrix(args) {
|
|
222
|
+
const projErr = validateProject(args?.project);
|
|
223
|
+
if (projErr)
|
|
224
|
+
return { error: projErr };
|
|
213
225
|
const loader = getLoader();
|
|
214
|
-
const entities = loader.getEntities();
|
|
226
|
+
const entities = loader.getEntities(args?.project);
|
|
215
227
|
const apiEntities = entities.filter(e => {
|
|
216
228
|
const layer = entityLayer(e);
|
|
217
229
|
return layer === 'route' || layer === 'controller' ||
|
|
@@ -264,10 +276,13 @@ export function getAuthMatrix() {
|
|
|
264
276
|
};
|
|
265
277
|
}
|
|
266
278
|
// ─── Functor 5: find_error_gaps ──────────────────────────────────
|
|
267
|
-
export function findErrorGaps() {
|
|
279
|
+
export function findErrorGaps(args) {
|
|
280
|
+
const projErr = validateProject(args?.project);
|
|
281
|
+
if (projErr)
|
|
282
|
+
return { error: projErr };
|
|
268
283
|
const loader = getLoader();
|
|
269
|
-
const g = getGraph();
|
|
270
|
-
const entities = loader.getEntities();
|
|
284
|
+
const g = getGraph(args?.project);
|
|
285
|
+
const entities = loader.getEntities(args?.project);
|
|
271
286
|
// Find all fallible entities (throws === true or has THROWS tag)
|
|
272
287
|
const fallibleIds = new Set();
|
|
273
288
|
for (const e of entities) {
|
|
@@ -328,10 +343,13 @@ export function findErrorGaps() {
|
|
|
328
343
|
}
|
|
329
344
|
// ─── Functor 6: get_test_coverage ────────────────────────────────
|
|
330
345
|
export function getTestCoverage(args) {
|
|
346
|
+
const projErr = validateProject(args.project);
|
|
347
|
+
if (projErr)
|
|
348
|
+
return { error: projErr };
|
|
331
349
|
const { weight_by_blast_radius = false } = args;
|
|
332
350
|
const loader = getLoader();
|
|
333
|
-
const g = getGraph();
|
|
334
|
-
const entities = loader.getEntities();
|
|
351
|
+
const g = getGraph(args.project);
|
|
352
|
+
const entities = loader.getEntities(args.project);
|
|
335
353
|
// Partition into test and non-test entities
|
|
336
354
|
const testIds = new Set();
|
|
337
355
|
const productionEntities = [];
|
|
@@ -402,10 +420,13 @@ export function getTestCoverage(args) {
|
|
|
402
420
|
}
|
|
403
421
|
// ─── Functor 7: get_optimal_context ──────────────────────────────
|
|
404
422
|
export function getOptimalContext(args) {
|
|
423
|
+
const projErr = validateProject(args.project);
|
|
424
|
+
if (projErr)
|
|
425
|
+
return { error: projErr };
|
|
405
426
|
const { target_entity, max_tokens = 8000, strategy = 'bfs' } = args;
|
|
406
427
|
const loader = getLoader();
|
|
407
|
-
const g = getGraph();
|
|
408
|
-
const entity = loader.getEntityById(target_entity) || loader.getEntityByName(target_entity);
|
|
428
|
+
const g = getGraph(args.project);
|
|
429
|
+
const entity = loader.getEntityById(target_entity, args.project) || loader.getEntityByName(target_entity, args.project);
|
|
409
430
|
if (!entity) {
|
|
410
431
|
return { error: `Entity not found: ${target_entity}` };
|
|
411
432
|
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -3,13 +3,22 @@
|
|
|
3
3
|
*
|
|
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
|
+
*
|
|
7
|
+
* Multi-project: when multiple projects are loaded, each tool accepts an optional
|
|
8
|
+
* `project` parameter. In multi-project mode, `project` is required.
|
|
6
9
|
*/
|
|
7
10
|
import type { JstfEntity } from '../types.js';
|
|
8
|
-
import {
|
|
11
|
+
import { MultiLoader } from '../loader.js';
|
|
9
12
|
import { type CallGraph } from '../graph.js';
|
|
10
|
-
export declare function initTools(
|
|
11
|
-
export declare function getLoader():
|
|
12
|
-
export declare function getGraph(): CallGraph;
|
|
13
|
+
export declare function initTools(multiLoader: MultiLoader): void;
|
|
14
|
+
export declare function getLoader(): MultiLoader;
|
|
15
|
+
export declare function getGraph(project?: string): CallGraph;
|
|
16
|
+
/**
|
|
17
|
+
* Validate project param. Returns error string if invalid, null if OK.
|
|
18
|
+
* In single-project mode, project is optional (defaults to the only project).
|
|
19
|
+
* In multi-project mode, project is required.
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateProject(project?: string): string | null;
|
|
13
22
|
export declare function entityName(e: JstfEntity): string;
|
|
14
23
|
export declare function entityLayer(e: JstfEntity): string;
|
|
15
24
|
export declare function entitySummary(e: JstfEntity): Record<string, unknown>;
|
|
@@ -20,6 +29,7 @@ export declare function normalizeConstraints(constraints: JstfEntity['constraint
|
|
|
20
29
|
*/
|
|
21
30
|
export declare function constraintMatches(constraints: JstfEntity['constraints'], target: string): boolean;
|
|
22
31
|
export declare function queryEntities(args: {
|
|
32
|
+
project?: string;
|
|
23
33
|
query?: string;
|
|
24
34
|
layer?: string;
|
|
25
35
|
module?: string;
|
|
@@ -28,23 +38,31 @@ export declare function queryEntities(args: {
|
|
|
28
38
|
}): unknown;
|
|
29
39
|
export declare function getEntity(args: {
|
|
30
40
|
id: string;
|
|
41
|
+
project?: string;
|
|
31
42
|
}): unknown;
|
|
32
43
|
export declare function getDependencies(args: {
|
|
33
44
|
entity_id: string;
|
|
34
45
|
direction?: 'callers' | 'callees' | 'both';
|
|
35
46
|
depth?: number;
|
|
47
|
+
project?: string;
|
|
36
48
|
}): unknown;
|
|
37
49
|
export declare function collectTransitive(adjacency: Map<string, Set<string>>, startId: string, maxDepth: number): string[];
|
|
38
50
|
export declare function getDataFlow(args: {
|
|
39
51
|
entity_id: string;
|
|
52
|
+
project?: string;
|
|
40
53
|
}): unknown;
|
|
41
54
|
export declare function findByConstraint(args: {
|
|
42
55
|
constraint: string;
|
|
56
|
+
project?: string;
|
|
43
57
|
}): unknown;
|
|
44
58
|
export declare function getBlastRadius(args: {
|
|
45
59
|
entity_ids: string[];
|
|
60
|
+
project?: string;
|
|
46
61
|
}): unknown;
|
|
47
62
|
export declare function listModules(args: {
|
|
48
63
|
group_by?: 'layer' | 'module' | 'file' | 'language';
|
|
64
|
+
project?: string;
|
|
65
|
+
}): unknown;
|
|
66
|
+
export declare function getTopology(args?: {
|
|
67
|
+
project?: string;
|
|
49
68
|
}): unknown;
|
|
50
|
-
export declare function getTopology(): unknown;
|
package/dist/tools/index.js
CHANGED
|
@@ -3,23 +3,43 @@
|
|
|
3
3
|
*
|
|
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
|
+
*
|
|
7
|
+
* Multi-project: when multiple projects are loaded, each tool accepts an optional
|
|
8
|
+
* `project` parameter. In multi-project mode, `project` is required.
|
|
6
9
|
*/
|
|
7
10
|
import { buildCallGraph, computeBlastRadius } from '../graph.js';
|
|
8
11
|
// Lazy-initialized shared state
|
|
9
12
|
let loader;
|
|
10
|
-
let
|
|
11
|
-
export function initTools(
|
|
12
|
-
loader =
|
|
13
|
-
|
|
13
|
+
let graphCache = new Map();
|
|
14
|
+
export function initTools(multiLoader) {
|
|
15
|
+
loader = multiLoader;
|
|
16
|
+
graphCache = new Map();
|
|
14
17
|
}
|
|
15
18
|
export function getLoader() {
|
|
16
19
|
return loader;
|
|
17
20
|
}
|
|
18
|
-
export function getGraph() {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
export function getGraph(project) {
|
|
22
|
+
const key = project || '__default__';
|
|
23
|
+
if (!graphCache.has(key)) {
|
|
24
|
+
graphCache.set(key, buildCallGraph(loader.getEntities(project)));
|
|
25
|
+
}
|
|
26
|
+
return graphCache.get(key);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Validate project param. Returns error string if invalid, null if OK.
|
|
30
|
+
* In single-project mode, project is optional (defaults to the only project).
|
|
31
|
+
* In multi-project mode, project is required.
|
|
32
|
+
*/
|
|
33
|
+
export function validateProject(project) {
|
|
34
|
+
if (!loader.isMultiProject())
|
|
35
|
+
return null; // single project, always OK
|
|
36
|
+
if (!project) {
|
|
37
|
+
return `Multiple projects loaded. You must specify a "project" parameter. Available: ${loader.getProjectNames().join(', ')}`;
|
|
38
|
+
}
|
|
39
|
+
if (!loader.hasProject(project)) {
|
|
40
|
+
return `Project "${project}" not found. Available: ${loader.getProjectNames().join(', ')}`;
|
|
21
41
|
}
|
|
22
|
-
return
|
|
42
|
+
return null;
|
|
23
43
|
}
|
|
24
44
|
// ─── Helper: Extract entity name from struct ─────────────────────
|
|
25
45
|
export function entityName(e) {
|
|
@@ -54,7 +74,8 @@ export function entityLayer(e) {
|
|
|
54
74
|
return 'model';
|
|
55
75
|
if (src.includes('/schema') || explicit === 'schema')
|
|
56
76
|
return 'schema';
|
|
57
|
-
if (src.includes('/test') || src.includes('
|
|
77
|
+
if (src.includes('/test/') || src.includes('/tests/') || src.includes('/__tests__/') ||
|
|
78
|
+
src.includes('.test.') || src.includes('.spec.'))
|
|
58
79
|
return 'test';
|
|
59
80
|
return explicit || 'other';
|
|
60
81
|
}
|
|
@@ -63,6 +84,7 @@ export function entitySummary(e) {
|
|
|
63
84
|
return {
|
|
64
85
|
id: e.id,
|
|
65
86
|
name: entityName(e),
|
|
87
|
+
...(e._project ? { project: e._project } : {}),
|
|
66
88
|
layer: entityLayer(e),
|
|
67
89
|
module: e.context?.module || null,
|
|
68
90
|
sourceFile: e._sourceFile || null,
|
|
@@ -70,7 +92,10 @@ export function entitySummary(e) {
|
|
|
70
92
|
async: typeof e.struct !== 'string' ? e.struct?.async : undefined,
|
|
71
93
|
exported: typeof e.struct !== 'string' ? e.struct?.exported : undefined,
|
|
72
94
|
constraints: constraintTags.length > 0 ? constraintTags : undefined,
|
|
73
|
-
|
|
95
|
+
callExpressions: Array.isArray(e.edges?.calls) ? e.edges.calls.length : 0,
|
|
96
|
+
uniqueCallees: Array.isArray(e.edges?.calls)
|
|
97
|
+
? new Set(e.edges.calls.map(c => c.target)).size
|
|
98
|
+
: 0,
|
|
74
99
|
};
|
|
75
100
|
}
|
|
76
101
|
export function normalizeConstraints(constraints) {
|
|
@@ -142,8 +167,11 @@ export function constraintMatches(constraints, target) {
|
|
|
142
167
|
}
|
|
143
168
|
// ─── Tool: query_entities ─────────────────────────────────────────
|
|
144
169
|
export function queryEntities(args) {
|
|
170
|
+
const projErr = validateProject(args.project);
|
|
171
|
+
if (projErr)
|
|
172
|
+
return { error: projErr };
|
|
145
173
|
const { query, layer, module, language, limit = 50 } = args;
|
|
146
|
-
let results = loader.getEntities();
|
|
174
|
+
let results = loader.getEntities(args.project);
|
|
147
175
|
if (layer) {
|
|
148
176
|
results = results.filter(e => entityLayer(e).toLowerCase() === layer.toLowerCase());
|
|
149
177
|
}
|
|
@@ -173,7 +201,10 @@ export function queryEntities(args) {
|
|
|
173
201
|
}
|
|
174
202
|
// ─── Tool: get_entity ─────────────────────────────────────────────
|
|
175
203
|
export function getEntity(args) {
|
|
176
|
-
const
|
|
204
|
+
const projErr = validateProject(args.project);
|
|
205
|
+
if (projErr)
|
|
206
|
+
return { error: projErr };
|
|
207
|
+
const entity = loader.getEntityById(args.id, args.project) || loader.getEntityByName(args.id, args.project);
|
|
177
208
|
if (!entity) {
|
|
178
209
|
return { error: `Entity not found: ${args.id}` };
|
|
179
210
|
}
|
|
@@ -181,6 +212,7 @@ export function getEntity(args) {
|
|
|
181
212
|
return {
|
|
182
213
|
id: entity.id,
|
|
183
214
|
name: entityName(entity),
|
|
215
|
+
...(entity._project ? { project: entity._project } : {}),
|
|
184
216
|
sourceFile: entity._sourceFile,
|
|
185
217
|
sourceLanguage: entity._sourceLanguage,
|
|
186
218
|
jstfFilename: entity._jstfFilename,
|
|
@@ -199,9 +231,12 @@ export function getEntity(args) {
|
|
|
199
231
|
}
|
|
200
232
|
// ─── Tool: get_dependencies ───────────────────────────────────────
|
|
201
233
|
export function getDependencies(args) {
|
|
234
|
+
const projErr = validateProject(args.project);
|
|
235
|
+
if (projErr)
|
|
236
|
+
return { error: projErr };
|
|
202
237
|
const { entity_id, direction = 'both', depth = 2 } = args;
|
|
203
|
-
const g = getGraph();
|
|
204
|
-
const entity = loader.getEntityById(entity_id) || loader.getEntityByName(entity_id);
|
|
238
|
+
const g = getGraph(args.project);
|
|
239
|
+
const entity = loader.getEntityById(entity_id, args.project) || loader.getEntityByName(entity_id, args.project);
|
|
205
240
|
if (!entity) {
|
|
206
241
|
return { error: `Entity not found: ${entity_id}` };
|
|
207
242
|
}
|
|
@@ -252,7 +287,10 @@ export function collectTransitive(adjacency, startId, maxDepth) {
|
|
|
252
287
|
}
|
|
253
288
|
// ─── Tool: get_data_flow ──────────────────────────────────────────
|
|
254
289
|
export function getDataFlow(args) {
|
|
255
|
-
const
|
|
290
|
+
const projErr = validateProject(args.project);
|
|
291
|
+
if (projErr)
|
|
292
|
+
return { error: projErr };
|
|
293
|
+
const entity = loader.getEntityById(args.entity_id, args.project) || loader.getEntityByName(args.entity_id, args.project);
|
|
256
294
|
if (!entity) {
|
|
257
295
|
return { error: `Entity not found: ${args.entity_id}` };
|
|
258
296
|
}
|
|
@@ -265,8 +303,11 @@ export function getDataFlow(args) {
|
|
|
265
303
|
}
|
|
266
304
|
// ─── Tool: find_by_constraint ─────────────────────────────────────
|
|
267
305
|
export function findByConstraint(args) {
|
|
306
|
+
const projErr = validateProject(args.project);
|
|
307
|
+
if (projErr)
|
|
308
|
+
return { error: projErr };
|
|
268
309
|
const target = args.constraint;
|
|
269
|
-
const results = loader.getEntities().filter(e => constraintMatches(e.constraints, target));
|
|
310
|
+
const results = loader.getEntities(args.project).filter(e => constraintMatches(e.constraints, target));
|
|
270
311
|
return {
|
|
271
312
|
constraint: args.constraint,
|
|
272
313
|
total: results.length,
|
|
@@ -275,12 +316,15 @@ export function findByConstraint(args) {
|
|
|
275
316
|
}
|
|
276
317
|
// ─── Tool: get_blast_radius ───────────────────────────────────────
|
|
277
318
|
export function getBlastRadius(args) {
|
|
278
|
-
const
|
|
319
|
+
const projErr = validateProject(args.project);
|
|
320
|
+
if (projErr)
|
|
321
|
+
return { error: projErr };
|
|
322
|
+
const g = getGraph(args.project);
|
|
279
323
|
// Resolve names to IDs
|
|
280
324
|
const resolvedIds = new Set();
|
|
281
325
|
const notFound = [];
|
|
282
326
|
for (const nameOrId of args.entity_ids) {
|
|
283
|
-
const entity = loader.getEntityById(nameOrId) || loader.getEntityByName(nameOrId);
|
|
327
|
+
const entity = loader.getEntityById(nameOrId, args.project) || loader.getEntityByName(nameOrId, args.project);
|
|
284
328
|
if (entity) {
|
|
285
329
|
resolvedIds.add(entity.id);
|
|
286
330
|
}
|
|
@@ -309,8 +353,11 @@ export function getBlastRadius(args) {
|
|
|
309
353
|
}
|
|
310
354
|
// ─── Tool: list_modules ───────────────────────────────────────────
|
|
311
355
|
export function listModules(args) {
|
|
356
|
+
const projErr = validateProject(args.project);
|
|
357
|
+
if (projErr)
|
|
358
|
+
return { error: projErr };
|
|
312
359
|
const { group_by = 'layer' } = args;
|
|
313
|
-
const entities = loader.getEntities();
|
|
360
|
+
const entities = loader.getEntities(args.project);
|
|
314
361
|
const groups = new Map();
|
|
315
362
|
for (const e of entities) {
|
|
316
363
|
let key;
|
|
@@ -342,8 +389,11 @@ export function listModules(args) {
|
|
|
342
389
|
};
|
|
343
390
|
}
|
|
344
391
|
// ─── Tool: get_topology ───────────────────────────────────────────
|
|
345
|
-
export function getTopology() {
|
|
346
|
-
const
|
|
392
|
+
export function getTopology(args) {
|
|
393
|
+
const projErr = validateProject(args?.project);
|
|
394
|
+
if (projErr)
|
|
395
|
+
return { error: projErr };
|
|
396
|
+
const topology = loader.getTopology(args?.project);
|
|
347
397
|
if (!topology) {
|
|
348
398
|
return { error: 'No topology data found. The _topology.json file may not have been generated.' };
|
|
349
399
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface JstfEntity {
|
|
|
11
11
|
_jstfFilename?: string;
|
|
12
12
|
_sourceFile?: string | null;
|
|
13
13
|
_sourceLanguage?: string;
|
|
14
|
+
_project?: string;
|
|
14
15
|
/** σ — Structure: function shape, signature, modifiers */
|
|
15
16
|
struct?: {
|
|
16
17
|
name?: string;
|
|
@@ -145,3 +146,12 @@ export interface Manifest {
|
|
|
145
146
|
languages: string[];
|
|
146
147
|
layers: Record<string, number>;
|
|
147
148
|
}
|
|
149
|
+
export interface ProjectInfo {
|
|
150
|
+
name: string;
|
|
151
|
+
path: string;
|
|
152
|
+
entityCount: number;
|
|
153
|
+
languages: string[];
|
|
154
|
+
commitSha: string;
|
|
155
|
+
extractedAt: string;
|
|
156
|
+
layers: Record<string, number>;
|
|
157
|
+
}
|