@papyruslabsai/seshat-mcp 0.20.0 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/graph.js DELETED
@@ -1,272 +0,0 @@
1
- /**
2
- * Call Graph Construction & Blast Radius Computation
3
- *
4
- * Computes the set of entities affected by a change:
5
- * affected(e) = ancestors(e) ∪ descendants(e) ∪ {e}
6
- *
7
- * Ancestors = all transitive callers (upstream).
8
- * Descendants = all transitive callees (downstream).
9
- */
10
- /**
11
- * Local part of a (possibly file-qualified) entity id.
12
- * `src/rooms/participants.js#addParticipant~2` → `addParticipant`.
13
- */
14
- function localIdOf(id) {
15
- const i = id.lastIndexOf('#');
16
- const local = i === -1 ? id : id.slice(i + 1);
17
- return local.replace(/~\d+$/, '');
18
- }
19
- function sourceFileOf(entity) {
20
- return entity.sourceFile || entity._sourceFile || null;
21
- }
22
- /**
23
- * Build a bidirectional call graph from entities' dependency data.
24
- *
25
- * Reference resolution is identity-honest: a bare-name target resolves to the
26
- * unique entity with that local name, or the unique same-file match when the
27
- * name is ambiguous repo-wide. A reference that stays ambiguous produces NO
28
- * edge — first-wins matching used to silently attribute edges to whichever
29
- * same-named entity was indexed first.
30
- */
31
- export function buildCallGraph(entities) {
32
- const callers = new Map();
33
- const callees = new Map();
34
- const entityById = new Map();
35
- const entityByModule = new Map();
36
- const byLocal = new Map(); // local name → entities
37
- const bySuffix = new Map(); // last dotted segment of local → entities
38
- // Index entities
39
- for (const entity of entities) {
40
- if (!entity.id)
41
- continue;
42
- entityById.set(entity.id, entity);
43
- callers.set(entity.id, new Set());
44
- callees.set(entity.id, new Set());
45
- const local = localIdOf(entity.id);
46
- if (!byLocal.has(local))
47
- byLocal.set(local, []);
48
- byLocal.get(local).push(entity);
49
- const suffix = local.includes('.') ? local.split('.').pop() : null;
50
- if (suffix) {
51
- if (!bySuffix.has(suffix))
52
- bySuffix.set(suffix, []);
53
- bySuffix.get(suffix).push(entity);
54
- }
55
- if (entity.context?.module) {
56
- const mod = entity.context.module;
57
- if (!entityByModule.has(mod))
58
- entityByModule.set(mod, []);
59
- entityByModule.get(mod).push(entity);
60
- }
61
- }
62
- /** Unique candidate, or unique same-file candidate, else null. */
63
- function pick(candidates, contextFile) {
64
- if (!candidates || candidates.length === 0)
65
- return null;
66
- if (candidates.length === 1)
67
- return candidates[0];
68
- if (contextFile) {
69
- const same = candidates.filter(e => sourceFileOf(e) === contextFile);
70
- if (same.length === 1)
71
- return same[0];
72
- }
73
- return null;
74
- }
75
- const stats = { resolved: 0, ambiguous: 0, unknown: 0 };
76
- /** Resolve a reference string to an entity id, with file context. */
77
- function resolveRef(target, contextFile) {
78
- const found = (id) => { stats.resolved++; return id; };
79
- if (entityById.has(target))
80
- return found(target);
81
- const direct = pick(byLocal.get(target), contextFile);
82
- if (direct)
83
- return found(direct.id);
84
- if (target.includes('.')) {
85
- const dotIdx = target.indexOf('.');
86
- const modulePart = target.substring(0, dotIdx);
87
- const methodPart = target.substring(dotIdx + 1);
88
- const moduleEntities = entityByModule.get(modulePart);
89
- if (moduleEntities) {
90
- const matches = moduleEntities.filter(e => {
91
- const name = typeof e.struct === 'string' ? e.struct : e.struct?.name;
92
- return localIdOf(e.id) === methodPart || name === methodPart;
93
- });
94
- const m = pick(matches, contextFile);
95
- if (m)
96
- return found(m.id);
97
- }
98
- const method = pick(byLocal.get(methodPart), contextFile);
99
- if (method)
100
- return found(method.id);
101
- }
102
- const suffixed = pick(bySuffix.get(target), contextFile);
103
- if (suffixed)
104
- return found(suffixed.id);
105
- const hadCandidates = (byLocal.get(target)?.length || 0) + (bySuffix.get(target)?.length || 0) > 1;
106
- if (hadCandidates)
107
- stats.ambiguous++;
108
- else
109
- stats.unknown++;
110
- return null;
111
- }
112
- // File index for import resolution: repo-relative path (sans extension) → entities
113
- const byFileBase = new Map();
114
- for (const entity of entities) {
115
- const f = sourceFileOf(entity);
116
- if (!f)
117
- continue;
118
- const base = f.replace(/\.[^./]+$/, '');
119
- if (!byFileBase.has(base))
120
- byFileBase.set(base, []);
121
- byFileBase.get(base).push(entity);
122
- }
123
- /** `src/app.js` + `./rooms/participants.js` → `src/rooms/participants` (extensionless). */
124
- function resolveRelativeImport(importerFile, source) {
125
- const stack = importerFile.split('/').slice(0, -1);
126
- for (const part of source.split('/')) {
127
- if (part === '.' || part === '')
128
- continue;
129
- if (part === '..') {
130
- if (stack.length === 0)
131
- return null;
132
- stack.pop();
133
- continue;
134
- }
135
- stack.push(part);
136
- }
137
- return stack.join('/').replace(/\.[^./]+$/, '');
138
- }
139
- // Build call edges from dependency data
140
- for (const caller of entities) {
141
- if (!caller.id || !caller.edges)
142
- continue;
143
- const callerFile = sourceFileOf(caller);
144
- // Process direct function/method calls
145
- const callsArray = caller.edges.calls;
146
- if (Array.isArray(callsArray)) {
147
- for (const call of callsArray) {
148
- if (!call.target)
149
- continue;
150
- const calleeId = resolveRef(call.target, callerFile);
151
- if (calleeId && calleeId !== caller.id) {
152
- callees.get(caller.id)?.add(calleeId);
153
- callers.get(calleeId)?.add(caller.id);
154
- }
155
- }
156
- }
157
- // Process lexical imports — resolve the import PATH against the importer's
158
- // file, then link each specifier to the same-named entity in the resolved
159
- // file. Basename matching used to link any same-named file first-wins.
160
- const importsArray = caller.edges.imports;
161
- if (Array.isArray(importsArray) && callerFile) {
162
- for (const imp of importsArray) {
163
- const source = imp.source || imp.module;
164
- if (!source || !source.startsWith('.'))
165
- continue; // package import — no repo entity
166
- const resolvedBase = resolveRelativeImport(callerFile, source);
167
- if (!resolvedBase)
168
- continue;
169
- const targetEntities = byFileBase.get(resolvedBase) || byFileBase.get(`${resolvedBase}/index`);
170
- if (!targetEntities)
171
- continue;
172
- const names = (imp.specifiers || [])
173
- .map((s) => s.imported || s.local)
174
- .filter(Boolean);
175
- for (const name of names) {
176
- const match = targetEntities.find(e => localIdOf(e.id) === name);
177
- if (match && match.id !== caller.id) {
178
- callees.get(caller.id)?.add(match.id);
179
- callers.get(match.id)?.add(caller.id);
180
- }
181
- }
182
- }
183
- }
184
- // Process IoC / Dynamic Dispatch (calledBy)
185
- const calledByArray = caller.edges.calledBy;
186
- if (Array.isArray(calledByArray)) {
187
- for (const cb of calledByArray) {
188
- if (!cb.source)
189
- continue;
190
- // In this relationship, the current entity (`caller` in this loop
191
- // context, despite the name) is actually the CALLEE, and the
192
- // `source` is the true CALLER.
193
- const callerId = resolveRef(cb.source, callerFile);
194
- if (callerId && callerId !== caller.id) {
195
- callees.get(callerId)?.add(caller.id); // true caller calls this entity
196
- callers.get(caller.id)?.add(callerId); // this entity is called by true caller
197
- }
198
- }
199
- }
200
- }
201
- return { callers, callees, entityById, resolutionStats: stats };
202
- }
203
- /**
204
- * Compute blast radius: all entities affected by changes to the given IDs.
205
- * Traverses both callers (upstream) and callees (downstream).
206
- */
207
- export function computeBlastRadius(graph, changedIds, maxDepth = 10) {
208
- const ancestors = new Set();
209
- const descendants = new Set();
210
- const depthMap = {};
211
- // BFS upstream (callers)
212
- function bfsUp(startIds) {
213
- const queue = [];
214
- for (const id of startIds) {
215
- queue.push([id, 0]);
216
- }
217
- const visited = new Set(startIds);
218
- while (queue.length > 0) {
219
- const [current, depth] = queue.shift();
220
- if (depth > maxDepth)
221
- continue;
222
- const callerSet = graph.callers.get(current);
223
- if (!callerSet)
224
- continue;
225
- for (const callerId of callerSet) {
226
- if (!visited.has(callerId)) {
227
- visited.add(callerId);
228
- ancestors.add(callerId);
229
- depthMap[callerId] = Math.min(depthMap[callerId] ?? Infinity, -(depth + 1));
230
- queue.push([callerId, depth + 1]);
231
- }
232
- }
233
- }
234
- }
235
- // BFS downstream (callees)
236
- function bfsDown(startIds) {
237
- const queue = [];
238
- for (const id of startIds) {
239
- queue.push([id, 0]);
240
- }
241
- const visited = new Set(startIds);
242
- while (queue.length > 0) {
243
- const [current, depth] = queue.shift();
244
- if (depth > maxDepth)
245
- continue;
246
- const calleeSet = graph.callees.get(current);
247
- if (!calleeSet)
248
- continue;
249
- for (const calleeId of calleeSet) {
250
- if (!visited.has(calleeId)) {
251
- visited.add(calleeId);
252
- descendants.add(calleeId);
253
- depthMap[calleeId] = Math.min(depthMap[calleeId] ?? Infinity, depth + 1);
254
- queue.push([calleeId, depth + 1]);
255
- }
256
- }
257
- }
258
- }
259
- bfsUp(changedIds);
260
- bfsDown(changedIds);
261
- // Mark changed entities at depth 0
262
- for (const id of changedIds) {
263
- depthMap[id] = 0;
264
- }
265
- const allAffected = new Set([...changedIds, ...ancestors, ...descendants]);
266
- return {
267
- affected: [...allAffected],
268
- depthMap,
269
- ancestors: [...ancestors],
270
- descendants: [...descendants],
271
- };
272
- }
package/dist/index.d.ts DELETED
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * @papyruslabs/seshat-mcp — Structural Code Analysis MCP Server (Cloud Proxy)
4
- *
5
- * Declares the Seshat tool suite to Claude/AI clients. All tool calls are
6
- * proxied to the Ptah Cloud API. Tool descriptions are written as triggers —
7
- * they tell the LLM *when* to reach for each tool, not just what it does.
8
- */
9
- export {};
package/dist/loader.d.ts DELETED
@@ -1,62 +0,0 @@
1
- /**
2
- * Bundle Loader
3
- *
4
- * Discovers and loads .seshat/_bundle.json from the current working directory.
5
- * Falls back to checking common alternative locations.
6
- */
7
- import type { JstfEntity, Topology, Manifest, ProjectInfo, ProjectLoader } from './types.js';
8
- export declare class BundleLoader {
9
- private entities;
10
- private topology;
11
- private manifest;
12
- private loaded;
13
- private seshatDir;
14
- private bundleMtime;
15
- constructor(cwd?: string);
16
- /**
17
- * Check if the bundle file on disk has changed since last load.
18
- * If so, reload transparently. Called by all accessors.
19
- */
20
- private ensureFresh;
21
- load(): void;
22
- getEntities(): JstfEntity[];
23
- getTopology(): Topology | null;
24
- getManifest(): Manifest | null;
25
- getEntityById(id: string): JstfEntity | undefined;
26
- getEntityByName(name: string): JstfEntity | undefined;
27
- isLoaded(): boolean;
28
- }
29
- /**
30
- * Wraps N BundleLoader instances for multi-project access.
31
- * In single-project mode (1 dir), behaves identically to BundleLoader.
32
- * In multi-project mode, `project` param is required on all accessors.
33
- */
34
- export declare class MultiLoader implements ProjectLoader {
35
- private projects;
36
- private projectPaths;
37
- private projectEntities;
38
- private loaded;
39
- private dirs;
40
- constructor(projectDirs: string[]);
41
- load(): void;
42
- private loadProject;
43
- /** Get the resolved project name when only one project is loaded. */
44
- private defaultProject;
45
- private resolveProject;
46
- getEntities(project?: string): JstfEntity[];
47
- getTopology(project?: string): Topology | null;
48
- getManifest(project?: string): Manifest | null;
49
- getEntityById(id: string, project?: string): JstfEntity | undefined;
50
- getEntityByName(name: string, project?: string): JstfEntity | undefined;
51
- getProjectNames(): string[];
52
- getProjectInfo(): ProjectInfo[];
53
- isMultiProject(): boolean;
54
- isLoaded(): boolean;
55
- hasProject(name: string): boolean;
56
- /**
57
- * Register a virtual entity in memory.
58
- * Used for Phase 1/2 of the Harness to reason about entities that haven't been written to disk yet.
59
- */
60
- registerVirtualEntity(entity: JstfEntity, project?: string): void;
61
- totalEntities(): number;
62
- }
package/dist/loader.js DELETED
@@ -1,264 +0,0 @@
1
- /**
2
- * Bundle Loader
3
- *
4
- * Discovers and loads .seshat/_bundle.json from the current working directory.
5
- * Falls back to checking common alternative locations.
6
- */
7
- import fs from 'fs';
8
- import path from 'path';
9
- export class BundleLoader {
10
- entities = [];
11
- topology = null;
12
- manifest = null;
13
- loaded = false;
14
- seshatDir;
15
- bundleMtime = 0;
16
- constructor(cwd) {
17
- const root = cwd || process.cwd();
18
- this.seshatDir = path.join(root, '.seshat');
19
- }
20
- /**
21
- * Check if the bundle file on disk has changed since last load.
22
- * If so, reload transparently. Called by all accessors.
23
- */
24
- ensureFresh() {
25
- if (!this.loaded) {
26
- this.load();
27
- return;
28
- }
29
- const bundlePath = path.join(this.seshatDir, '_bundle.json');
30
- try {
31
- const currentMtime = fs.statSync(bundlePath).mtimeMs;
32
- if (currentMtime !== this.bundleMtime) {
33
- process.stderr.write(`[seshat] Bundle changed on disk, reloading…\n`);
34
- this.load();
35
- }
36
- }
37
- catch {
38
- // File may have been deleted — keep stale data rather than crash
39
- }
40
- }
41
- load() {
42
- const bundlePath = path.join(this.seshatDir, '_bundle.json');
43
- if (!fs.existsSync(bundlePath)) {
44
- throw new Error(`No .seshat/_bundle.json found in ${path.dirname(this.seshatDir)}.\n` +
45
- `Run the Seshat extraction pipeline first:\n` +
46
- ` node ci/extract-and-generate.mjs <repo-path> .seshat <project-name>\n` +
47
- `Or add the CI workflow to auto-generate on push to main.`);
48
- }
49
- this.bundleMtime = fs.statSync(bundlePath).mtimeMs;
50
- const raw = fs.readFileSync(bundlePath, 'utf-8');
51
- const bundle = JSON.parse(raw);
52
- this.entities = bundle.entities || [];
53
- // Remap bundle field names to internal _ prefixed names.
54
- // The extraction pipeline outputs `sourceFile`, `sourceLanguage`
55
- // but the entity type expects `_sourceFile`, `_sourceLanguage`.
56
- for (const e of this.entities) {
57
- const raw = e;
58
- if (raw.sourceFile && !e._sourceFile) {
59
- e._sourceFile = raw.sourceFile;
60
- }
61
- if (raw.sourceLanguage && !e._sourceLanguage) {
62
- e._sourceLanguage = raw.sourceLanguage;
63
- }
64
- }
65
- // Load topology if available
66
- const topoPath = path.join(this.seshatDir, '_topology.json');
67
- if (fs.existsSync(topoPath)) {
68
- this.topology = JSON.parse(fs.readFileSync(topoPath, 'utf-8'));
69
- }
70
- // Load manifest if available
71
- const manifestPath = path.join(this.seshatDir, 'manifest.json');
72
- if (fs.existsSync(manifestPath)) {
73
- this.manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
74
- }
75
- this.loaded = true;
76
- }
77
- getEntities() {
78
- this.ensureFresh();
79
- return this.entities;
80
- }
81
- getTopology() {
82
- this.ensureFresh();
83
- return this.topology;
84
- }
85
- getManifest() {
86
- this.ensureFresh();
87
- return this.manifest;
88
- }
89
- getEntityById(id) {
90
- return this.getEntities().find(e => e.id === id);
91
- }
92
- getEntityByName(name) {
93
- return this.getEntities().find(e => {
94
- const structName = typeof e.struct === 'string' ? e.struct : e.struct?.name;
95
- return e.id === name || localIdOfRef(e.id) === name || structName === name;
96
- });
97
- }
98
- isLoaded() {
99
- return this.loaded;
100
- }
101
- }
102
- /** Local part of a (possibly file-qualified) entity id. */
103
- function localIdOfRef(id) {
104
- const i = id.lastIndexOf('#');
105
- const local = i === -1 ? id : id.slice(i + 1);
106
- return local.replace(/~\d+$/, '');
107
- }
108
- // ─── Multi-Project Loader ─────────────────────────────────────────
109
- /**
110
- * Wraps N BundleLoader instances for multi-project access.
111
- * In single-project mode (1 dir), behaves identically to BundleLoader.
112
- * In multi-project mode, `project` param is required on all accessors.
113
- */
114
- export class MultiLoader {
115
- projects = new Map();
116
- projectPaths = new Map();
117
- projectEntities = new Map();
118
- loaded = false;
119
- dirs;
120
- constructor(projectDirs) {
121
- this.dirs = projectDirs.length > 0 ? projectDirs : [process.cwd()];
122
- }
123
- load() {
124
- for (const dir of this.dirs) {
125
- this.loadProject(dir);
126
- }
127
- this.loaded = true;
128
- }
129
- loadProject(dir) {
130
- const loader = new BundleLoader(dir);
131
- try {
132
- loader.load();
133
- const manifest = loader.getManifest();
134
- const name = manifest?.projectName || path.basename(dir);
135
- this.projects.set(name, loader);
136
- this.projectPaths.set(name, dir);
137
- // Stamp entities with _project
138
- const entities = loader.getEntities();
139
- for (const e of entities) {
140
- e._project = name;
141
- }
142
- this.projectEntities.set(name, entities);
143
- }
144
- catch (err) {
145
- process.stderr.write(`Warning: Skipping ${dir}: ${err.message}\n`);
146
- }
147
- }
148
- /** Get the resolved project name when only one project is loaded. */
149
- defaultProject() {
150
- if (this.projects.size === 1)
151
- return [...this.projects.keys()][0];
152
- return undefined;
153
- }
154
- resolveProject(project) {
155
- return project || this.defaultProject();
156
- }
157
- getEntities(project) {
158
- if (!this.loaded)
159
- this.load();
160
- const p = this.resolveProject(project);
161
- if (!p)
162
- return [];
163
- // Delegate to BundleLoader (which checks mtime via ensureFresh),
164
- // then re-stamp and cache if the reference changed.
165
- const loader = this.projects.get(p);
166
- if (!loader)
167
- return [];
168
- const fresh = loader.getEntities();
169
- const cached = this.projectEntities.get(p);
170
- if (fresh !== cached) {
171
- for (const e of fresh) {
172
- e._project = p;
173
- }
174
- this.projectEntities.set(p, fresh);
175
- }
176
- return fresh;
177
- }
178
- getTopology(project) {
179
- if (!this.loaded)
180
- this.load();
181
- const p = this.resolveProject(project);
182
- if (p)
183
- return this.projects.get(p)?.getTopology() || null;
184
- return null;
185
- }
186
- getManifest(project) {
187
- if (!this.loaded)
188
- this.load();
189
- const p = this.resolveProject(project);
190
- if (p)
191
- return this.projects.get(p)?.getManifest() || null;
192
- return null;
193
- }
194
- getEntityById(id, project) {
195
- return this.getEntities(project).find(e => e.id === id);
196
- }
197
- getEntityByName(name, project) {
198
- return this.getEntities(project).find(e => {
199
- const structName = typeof e.struct === 'string' ? e.struct : e.struct?.name;
200
- return e.id === name || localIdOfRef(e.id) === name || structName === name;
201
- });
202
- }
203
- getProjectNames() {
204
- if (!this.loaded)
205
- this.load();
206
- return [...this.projects.keys()];
207
- }
208
- getProjectInfo() {
209
- if (!this.loaded)
210
- this.load();
211
- const result = [];
212
- for (const [name, loader] of this.projects) {
213
- const manifest = loader.getManifest();
214
- result.push({
215
- name,
216
- path: this.projectPaths.get(name) || '',
217
- entityCount: loader.getEntities().length,
218
- languages: manifest?.languages || [],
219
- commitSha: manifest?.commitSha || '',
220
- extractedAt: manifest?.extractedAt || '',
221
- layers: manifest?.layers || {},
222
- });
223
- }
224
- return result;
225
- }
226
- isMultiProject() {
227
- return this.projects.size > 1;
228
- }
229
- isLoaded() {
230
- return this.loaded && this.projects.size > 0;
231
- }
232
- hasProject(name) {
233
- return this.projects.has(name);
234
- }
235
- /**
236
- * Register a virtual entity in memory.
237
- * Used for Phase 1/2 of the Harness to reason about entities that haven't been written to disk yet.
238
- */
239
- registerVirtualEntity(entity, project) {
240
- if (!this.loaded)
241
- this.load();
242
- const p = this.resolveProject(project);
243
- if (!p)
244
- return;
245
- const entities = this.projectEntities.get(p) || [];
246
- entity._project = p;
247
- // If entity already exists, update it, otherwise append
248
- const existingIdx = entities.findIndex(e => e.id === entity.id);
249
- if (existingIdx !== -1) {
250
- entities[existingIdx] = { ...entities[existingIdx], ...entity };
251
- }
252
- else {
253
- entities.push(entity);
254
- }
255
- this.projectEntities.set(p, entities);
256
- }
257
- totalEntities() {
258
- let total = 0;
259
- for (const entities of this.projectEntities.values()) {
260
- total += entities.length;
261
- }
262
- return total;
263
- }
264
- }