@papyruslabsai/seshat-mcp 0.1.0 → 0.3.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 +29 -14
- package/dist/index.js +257 -27
- package/dist/loader.d.ts +29 -1
- package/dist/loader.js +119 -0
- package/dist/tools/functors.d.ts +34 -0
- package/dist/tools/functors.js +554 -0
- package/dist/tools/index.d.ts +35 -3
- package/dist/tools/index.js +74 -25
- package/dist/types.d.ts +36 -1
- package/package.json +35 -35
|
@@ -0,0 +1,554 @@
|
|
|
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, validateProject, 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 projErr = validateProject(args.project);
|
|
26
|
+
if (projErr)
|
|
27
|
+
return { error: projErr };
|
|
28
|
+
const { include_tests = false } = args;
|
|
29
|
+
const loader = getLoader();
|
|
30
|
+
const g = getGraph(args.project);
|
|
31
|
+
const entities = loader.getEntities(args.project);
|
|
32
|
+
// Entry points: routes, exported functions, test files, plugin registrations
|
|
33
|
+
const entryPointIds = new Set();
|
|
34
|
+
for (const e of entities) {
|
|
35
|
+
if (!e.id)
|
|
36
|
+
continue;
|
|
37
|
+
const layer = entityLayer(e);
|
|
38
|
+
// Routes and controllers are entry points
|
|
39
|
+
if (layer === 'route' || layer === 'controller') {
|
|
40
|
+
entryPointIds.add(e.id);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// Test entities are entry points
|
|
44
|
+
if (layer === 'test') {
|
|
45
|
+
entryPointIds.add(e.id);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// Plugin registrations
|
|
49
|
+
if (e.context?.exposure === 'framework') {
|
|
50
|
+
entryPointIds.add(e.id);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
// Exported functions at the top level are entry points
|
|
54
|
+
if (typeof e.struct !== 'string' && e.struct?.exported) {
|
|
55
|
+
entryPointIds.add(e.id);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// BFS from all entry points through callees
|
|
59
|
+
const reachable = new Set(entryPointIds);
|
|
60
|
+
const queue = [...entryPointIds];
|
|
61
|
+
while (queue.length > 0) {
|
|
62
|
+
const current = queue.shift();
|
|
63
|
+
const calleeSet = g.callees.get(current);
|
|
64
|
+
if (!calleeSet)
|
|
65
|
+
continue;
|
|
66
|
+
for (const calleeId of calleeSet) {
|
|
67
|
+
if (!reachable.has(calleeId)) {
|
|
68
|
+
reachable.add(calleeId);
|
|
69
|
+
queue.push(calleeId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Unreachable = dead code candidates
|
|
74
|
+
let deadEntities = entities.filter(e => e.id && !reachable.has(e.id));
|
|
75
|
+
if (!include_tests) {
|
|
76
|
+
deadEntities = deadEntities.filter(e => entityLayer(e) !== 'test');
|
|
77
|
+
}
|
|
78
|
+
// Group by layer for overview
|
|
79
|
+
const byLayer = new Map();
|
|
80
|
+
for (const e of deadEntities) {
|
|
81
|
+
const l = entityLayer(e);
|
|
82
|
+
byLayer.set(l, (byLayer.get(l) || 0) + 1);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
totalEntities: entities.length,
|
|
86
|
+
entryPoints: entryPointIds.size,
|
|
87
|
+
reachable: reachable.size,
|
|
88
|
+
deadCount: deadEntities.length,
|
|
89
|
+
deadByLayer: Object.fromEntries([...byLayer.entries()].sort((a, b) => b[1] - a[1])),
|
|
90
|
+
dead: deadEntities.slice(0, 100).map(entitySummary),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// ─── Functor 2: find_layer_violations ────────────────────────────
|
|
94
|
+
export function findLayerViolations(args) {
|
|
95
|
+
const projErr = validateProject(args?.project);
|
|
96
|
+
if (projErr)
|
|
97
|
+
return { error: projErr };
|
|
98
|
+
const g = getGraph(args?.project);
|
|
99
|
+
const violations = [];
|
|
100
|
+
for (const [callerId, calleeIds] of g.callees) {
|
|
101
|
+
const callerEntity = g.entityById.get(callerId);
|
|
102
|
+
if (!callerEntity)
|
|
103
|
+
continue;
|
|
104
|
+
const callerLayer = entityLayer(callerEntity);
|
|
105
|
+
const callerOrder = LAYER_ORDER[callerLayer];
|
|
106
|
+
if (callerOrder === undefined)
|
|
107
|
+
continue;
|
|
108
|
+
for (const calleeId of calleeIds) {
|
|
109
|
+
const calleeEntity = g.entityById.get(calleeId);
|
|
110
|
+
if (!calleeEntity)
|
|
111
|
+
continue;
|
|
112
|
+
const calleeLayer = entityLayer(calleeEntity);
|
|
113
|
+
const calleeOrder = LAYER_ORDER[calleeLayer];
|
|
114
|
+
if (calleeOrder === undefined)
|
|
115
|
+
continue;
|
|
116
|
+
// Skip same-layer calls
|
|
117
|
+
if (callerLayer === calleeLayer)
|
|
118
|
+
continue;
|
|
119
|
+
// Backward call: lower layer calling higher layer
|
|
120
|
+
if (callerOrder > calleeOrder) {
|
|
121
|
+
violations.push({
|
|
122
|
+
from: entitySummary(callerEntity),
|
|
123
|
+
to: entitySummary(calleeEntity),
|
|
124
|
+
type: `backward: ${callerLayer}(${callerOrder}) -> ${calleeLayer}(${calleeOrder})`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// Skip-layer: jumping over more than 1 layer
|
|
128
|
+
else if (calleeOrder - callerOrder > 2) {
|
|
129
|
+
violations.push({
|
|
130
|
+
from: entitySummary(callerEntity),
|
|
131
|
+
to: entitySummary(calleeEntity),
|
|
132
|
+
type: `skip-layer: ${callerLayer}(${callerOrder}) -> ${calleeLayer}(${calleeOrder})`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Group violations by type
|
|
138
|
+
const byType = new Map();
|
|
139
|
+
for (const v of violations) {
|
|
140
|
+
const typePrefix = v.type.split(':')[0];
|
|
141
|
+
byType.set(typePrefix, (byType.get(typePrefix) || 0) + 1);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
totalViolations: violations.length,
|
|
145
|
+
byType: Object.fromEntries(byType),
|
|
146
|
+
violations: violations.slice(0, 100),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// ─── Functor 3: get_coupling_metrics ─────────────────────────────
|
|
150
|
+
export function getCouplingMetrics(args) {
|
|
151
|
+
const projErr = validateProject(args.project);
|
|
152
|
+
if (projErr)
|
|
153
|
+
return { error: projErr };
|
|
154
|
+
const { group_by = 'module' } = args;
|
|
155
|
+
const loader = getLoader();
|
|
156
|
+
const g = getGraph(args.project);
|
|
157
|
+
const entities = loader.getEntities(args.project);
|
|
158
|
+
// Group entities
|
|
159
|
+
const groups = new Map();
|
|
160
|
+
for (const e of entities) {
|
|
161
|
+
if (!e.id)
|
|
162
|
+
continue;
|
|
163
|
+
const key = group_by === 'module'
|
|
164
|
+
? (e.context?.module || 'unknown')
|
|
165
|
+
: entityLayer(e);
|
|
166
|
+
if (!groups.has(key))
|
|
167
|
+
groups.set(key, new Set());
|
|
168
|
+
groups.get(key).add(e.id);
|
|
169
|
+
}
|
|
170
|
+
const metrics = [];
|
|
171
|
+
for (const [groupName, memberIds] of groups) {
|
|
172
|
+
let internalEdges = 0;
|
|
173
|
+
let outgoingEdges = 0;
|
|
174
|
+
let incomingEdges = 0;
|
|
175
|
+
for (const memberId of memberIds) {
|
|
176
|
+
const calleeSet = g.callees.get(memberId);
|
|
177
|
+
if (calleeSet) {
|
|
178
|
+
for (const calleeId of calleeSet) {
|
|
179
|
+
if (memberIds.has(calleeId)) {
|
|
180
|
+
internalEdges++;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
outgoingEdges++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const callerSet = g.callers.get(memberId);
|
|
188
|
+
if (callerSet) {
|
|
189
|
+
for (const callerId of callerSet) {
|
|
190
|
+
if (!memberIds.has(callerId)) {
|
|
191
|
+
incomingEdges++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const size = memberIds.size;
|
|
197
|
+
const maxInternalEdges = size * (size - 1); // directed
|
|
198
|
+
const cohesion = maxInternalEdges > 0 ? internalEdges / maxInternalEdges : 0;
|
|
199
|
+
const totalExternal = outgoingEdges + incomingEdges;
|
|
200
|
+
const coupling = totalExternal;
|
|
201
|
+
const instability = totalExternal > 0 ? outgoingEdges / totalExternal : 0;
|
|
202
|
+
metrics.push({
|
|
203
|
+
group: groupName,
|
|
204
|
+
size,
|
|
205
|
+
internalEdges,
|
|
206
|
+
externalEdges: totalExternal,
|
|
207
|
+
cohesion: Math.round(cohesion * 1000) / 1000,
|
|
208
|
+
coupling,
|
|
209
|
+
instability: Math.round(instability * 1000) / 1000,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// Sort by coupling (most coupled first)
|
|
213
|
+
metrics.sort((a, b) => b.coupling - a.coupling);
|
|
214
|
+
return {
|
|
215
|
+
groupBy: group_by,
|
|
216
|
+
groupCount: metrics.length,
|
|
217
|
+
metrics: metrics.slice(0, 50),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
// ─── Functor 4: get_auth_matrix ──────────────────────────────────
|
|
221
|
+
export function getAuthMatrix(args) {
|
|
222
|
+
const projErr = validateProject(args?.project);
|
|
223
|
+
if (projErr)
|
|
224
|
+
return { error: projErr };
|
|
225
|
+
const loader = getLoader();
|
|
226
|
+
const entities = loader.getEntities(args?.project);
|
|
227
|
+
const apiEntities = entities.filter(e => {
|
|
228
|
+
const layer = entityLayer(e);
|
|
229
|
+
return layer === 'route' || layer === 'controller' ||
|
|
230
|
+
e.context?.exposure === 'api';
|
|
231
|
+
});
|
|
232
|
+
const withAuth = [];
|
|
233
|
+
const withoutAuth = [];
|
|
234
|
+
const inconsistencies = [];
|
|
235
|
+
for (const e of apiEntities) {
|
|
236
|
+
const constraints = e.constraints;
|
|
237
|
+
const hasAuth = constraints && !Array.isArray(constraints) &&
|
|
238
|
+
constraints.auth && constraints.auth !== 'none';
|
|
239
|
+
const summary = entitySummary(e);
|
|
240
|
+
if (hasAuth) {
|
|
241
|
+
withAuth.push({
|
|
242
|
+
...summary,
|
|
243
|
+
auth: constraints.auth,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
withoutAuth.push(summary);
|
|
248
|
+
}
|
|
249
|
+
// Check for inconsistencies: has auth decorator but marked as public
|
|
250
|
+
if (hasAuth && e.context?.visibility === 'public') {
|
|
251
|
+
inconsistencies.push({
|
|
252
|
+
entity: summary,
|
|
253
|
+
issue: 'Has auth requirements but visibility is public',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
// Has DB access but no auth
|
|
257
|
+
if (!hasAuth) {
|
|
258
|
+
const sideEffects = constraints && !Array.isArray(constraints)
|
|
259
|
+
? constraints.sideEffects : undefined;
|
|
260
|
+
if (Array.isArray(sideEffects) && sideEffects.some((s) => s === 'db' || s === 'database')) {
|
|
261
|
+
inconsistencies.push({
|
|
262
|
+
entity: summary,
|
|
263
|
+
issue: 'DB access without auth — potential security gap',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
totalApiEntities: apiEntities.length,
|
|
270
|
+
withAuth: withAuth.length,
|
|
271
|
+
withoutAuth: withoutAuth.length,
|
|
272
|
+
inconsistencies: inconsistencies.length,
|
|
273
|
+
authenticated: withAuth.slice(0, 50),
|
|
274
|
+
unauthenticated: withoutAuth.slice(0, 50),
|
|
275
|
+
issues: inconsistencies.slice(0, 50),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
// ─── Functor 5: find_error_gaps ──────────────────────────────────
|
|
279
|
+
export function findErrorGaps(args) {
|
|
280
|
+
const projErr = validateProject(args?.project);
|
|
281
|
+
if (projErr)
|
|
282
|
+
return { error: projErr };
|
|
283
|
+
const loader = getLoader();
|
|
284
|
+
const g = getGraph(args?.project);
|
|
285
|
+
const entities = loader.getEntities(args?.project);
|
|
286
|
+
// Find all fallible entities (throws === true or has THROWS tag)
|
|
287
|
+
const fallibleIds = new Set();
|
|
288
|
+
for (const e of entities) {
|
|
289
|
+
if (!e.id)
|
|
290
|
+
continue;
|
|
291
|
+
// Check kappa.throws
|
|
292
|
+
const constraints = e.constraints;
|
|
293
|
+
if (constraints && !Array.isArray(constraints) && constraints.throws) {
|
|
294
|
+
fallibleIds.add(e.id);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
// Check tau.self.fallible
|
|
298
|
+
const traits = e.traits;
|
|
299
|
+
if (traits && !Array.isArray(traits) && traits.self?.fallible) {
|
|
300
|
+
fallibleIds.add(e.id);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
// Check for network/db side effects (implicitly fallible)
|
|
304
|
+
if (constraints && !Array.isArray(constraints) && Array.isArray(constraints.sideEffects)) {
|
|
305
|
+
const effects = constraints.sideEffects;
|
|
306
|
+
if (effects.some((s) => s === 'network' || s === 'db' || s === 'database' || s === 'filesystem' || s === 'fs')) {
|
|
307
|
+
fallibleIds.add(e.id);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Find callers of fallible entities that lack error handling
|
|
312
|
+
const gaps = [];
|
|
313
|
+
for (const fallibleId of fallibleIds) {
|
|
314
|
+
const callerSet = g.callers.get(fallibleId);
|
|
315
|
+
if (!callerSet)
|
|
316
|
+
continue;
|
|
317
|
+
const fallibleEntity = g.entityById.get(fallibleId);
|
|
318
|
+
if (!fallibleEntity)
|
|
319
|
+
continue;
|
|
320
|
+
for (const callerId of callerSet) {
|
|
321
|
+
const callerEntity = g.entityById.get(callerId);
|
|
322
|
+
if (!callerEntity)
|
|
323
|
+
continue;
|
|
324
|
+
// Check if caller has error handling
|
|
325
|
+
const callerConstraints = callerEntity.constraints;
|
|
326
|
+
const hasErrorHandling = callerConstraints && !Array.isArray(callerConstraints) &&
|
|
327
|
+
callerConstraints.errorHandling &&
|
|
328
|
+
(callerConstraints.errorHandling.tryCatch || callerConstraints.errorHandling.catchClause);
|
|
329
|
+
if (!hasErrorHandling) {
|
|
330
|
+
gaps.push({
|
|
331
|
+
caller: entitySummary(callerEntity),
|
|
332
|
+
fallibleCallee: entitySummary(fallibleEntity),
|
|
333
|
+
issue: 'Calls fallible function without try/catch',
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
totalFallible: fallibleIds.size,
|
|
340
|
+
errorGaps: gaps.length,
|
|
341
|
+
gaps: gaps.slice(0, 100),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
// ─── Functor 6: get_test_coverage ────────────────────────────────
|
|
345
|
+
export function getTestCoverage(args) {
|
|
346
|
+
const projErr = validateProject(args.project);
|
|
347
|
+
if (projErr)
|
|
348
|
+
return { error: projErr };
|
|
349
|
+
const { weight_by_blast_radius = false } = args;
|
|
350
|
+
const loader = getLoader();
|
|
351
|
+
const g = getGraph(args.project);
|
|
352
|
+
const entities = loader.getEntities(args.project);
|
|
353
|
+
// Partition into test and non-test entities
|
|
354
|
+
const testIds = new Set();
|
|
355
|
+
const productionEntities = [];
|
|
356
|
+
for (const e of entities) {
|
|
357
|
+
if (!e.id)
|
|
358
|
+
continue;
|
|
359
|
+
if (entityLayer(e) === 'test') {
|
|
360
|
+
testIds.add(e.id);
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
productionEntities.push(e);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// BFS from test entities through callees to find what they exercise
|
|
367
|
+
const exercised = new Set();
|
|
368
|
+
const queue = [...testIds];
|
|
369
|
+
const visited = new Set(testIds);
|
|
370
|
+
while (queue.length > 0) {
|
|
371
|
+
const current = queue.shift();
|
|
372
|
+
const calleeSet = g.callees.get(current);
|
|
373
|
+
if (!calleeSet)
|
|
374
|
+
continue;
|
|
375
|
+
for (const calleeId of calleeSet) {
|
|
376
|
+
if (!visited.has(calleeId)) {
|
|
377
|
+
visited.add(calleeId);
|
|
378
|
+
if (!testIds.has(calleeId)) {
|
|
379
|
+
exercised.add(calleeId);
|
|
380
|
+
}
|
|
381
|
+
queue.push(calleeId);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const covered = productionEntities.filter(e => exercised.has(e.id));
|
|
386
|
+
const uncovered = productionEntities.filter(e => !exercised.has(e.id));
|
|
387
|
+
const result = {
|
|
388
|
+
totalProduction: productionEntities.length,
|
|
389
|
+
totalTests: testIds.size,
|
|
390
|
+
coveredCount: covered.length,
|
|
391
|
+
uncoveredCount: uncovered.length,
|
|
392
|
+
coveragePercent: productionEntities.length > 0
|
|
393
|
+
? Math.round((covered.length / productionEntities.length) * 1000) / 10
|
|
394
|
+
: 0,
|
|
395
|
+
};
|
|
396
|
+
if (weight_by_blast_radius && uncovered.length > 0) {
|
|
397
|
+
// Compute blast radius for each uncovered entity to prioritize what to test
|
|
398
|
+
const prioritized = uncovered
|
|
399
|
+
.map(e => {
|
|
400
|
+
const br = computeBlastRadius(g, new Set([e.id]));
|
|
401
|
+
return {
|
|
402
|
+
...entitySummary(e),
|
|
403
|
+
blastRadius: br.affected.length,
|
|
404
|
+
};
|
|
405
|
+
})
|
|
406
|
+
.sort((a, b) => b.blastRadius - a.blastRadius);
|
|
407
|
+
result.uncoveredByPriority = prioritized.slice(0, 50);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
// Group uncovered by layer
|
|
411
|
+
const byLayer = new Map();
|
|
412
|
+
for (const e of uncovered) {
|
|
413
|
+
const l = entityLayer(e);
|
|
414
|
+
byLayer.set(l, (byLayer.get(l) || 0) + 1);
|
|
415
|
+
}
|
|
416
|
+
result.uncoveredByLayer = Object.fromEntries([...byLayer.entries()].sort((a, b) => b[1] - a[1]));
|
|
417
|
+
result.uncovered = uncovered.slice(0, 50).map(entitySummary);
|
|
418
|
+
}
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
// ─── Functor 7: get_optimal_context ──────────────────────────────
|
|
422
|
+
export function getOptimalContext(args) {
|
|
423
|
+
const projErr = validateProject(args.project);
|
|
424
|
+
if (projErr)
|
|
425
|
+
return { error: projErr };
|
|
426
|
+
const { target_entity, max_tokens = 8000, strategy = 'bfs' } = args;
|
|
427
|
+
const loader = getLoader();
|
|
428
|
+
const g = getGraph(args.project);
|
|
429
|
+
const entity = loader.getEntityById(target_entity, args.project) || loader.getEntityByName(target_entity, args.project);
|
|
430
|
+
if (!entity) {
|
|
431
|
+
return { error: `Entity not found: ${target_entity}` };
|
|
432
|
+
}
|
|
433
|
+
const targetId = entity.id;
|
|
434
|
+
// Estimate tokens for an entity based on its dimensions
|
|
435
|
+
function estimateTokens(e) {
|
|
436
|
+
let tokens = 50; // Base: name, id, layer
|
|
437
|
+
if (e.struct && typeof e.struct !== 'string') {
|
|
438
|
+
tokens += 20; // signature
|
|
439
|
+
tokens += (e.struct.params?.length || 0) * 10;
|
|
440
|
+
}
|
|
441
|
+
if (e.edges?.calls)
|
|
442
|
+
tokens += e.edges.calls.length * 8;
|
|
443
|
+
if (e.edges?.imports)
|
|
444
|
+
tokens += e.edges.imports.length * 6;
|
|
445
|
+
if (e.data?.inputs)
|
|
446
|
+
tokens += e.data.inputs.length * 10;
|
|
447
|
+
if (e.constraints && typeof e.constraints === 'object' && !Array.isArray(e.constraints)) {
|
|
448
|
+
tokens += 30;
|
|
449
|
+
}
|
|
450
|
+
return tokens;
|
|
451
|
+
}
|
|
452
|
+
const candidates = [];
|
|
453
|
+
if (strategy === 'blast_radius') {
|
|
454
|
+
// Use blast radius to get all related entities with depth
|
|
455
|
+
const br = computeBlastRadius(g, new Set([targetId]));
|
|
456
|
+
for (const id of br.affected) {
|
|
457
|
+
if (id === targetId)
|
|
458
|
+
continue;
|
|
459
|
+
const e = g.entityById.get(id);
|
|
460
|
+
if (!e)
|
|
461
|
+
continue;
|
|
462
|
+
const depth = Math.abs(br.depthMap[id] || 99);
|
|
463
|
+
const tokens = estimateTokens(e);
|
|
464
|
+
// Relevance decays with distance; direct callers/callees are most valuable
|
|
465
|
+
const relevance = 1 / (1 + depth);
|
|
466
|
+
candidates.push({
|
|
467
|
+
entity: entitySummary(e),
|
|
468
|
+
relevance: Math.round(relevance * 1000) / 1000,
|
|
469
|
+
distance: depth,
|
|
470
|
+
direction: (br.depthMap[id] || 0) < 0 ? 'upstream' : 'downstream',
|
|
471
|
+
tokens,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
// BFS from target in both directions with distance tracking
|
|
477
|
+
const distances = new Map();
|
|
478
|
+
// BFS callees (downstream)
|
|
479
|
+
const downQueue = [[targetId, 0]];
|
|
480
|
+
const downVisited = new Set([targetId]);
|
|
481
|
+
while (downQueue.length > 0) {
|
|
482
|
+
const [current, d] = downQueue.shift();
|
|
483
|
+
if (d > 5)
|
|
484
|
+
continue;
|
|
485
|
+
const calleeSet = g.callees.get(current);
|
|
486
|
+
if (!calleeSet)
|
|
487
|
+
continue;
|
|
488
|
+
for (const id of calleeSet) {
|
|
489
|
+
if (!downVisited.has(id)) {
|
|
490
|
+
downVisited.add(id);
|
|
491
|
+
distances.set(id, { dist: d + 1, dir: 'downstream' });
|
|
492
|
+
downQueue.push([id, d + 1]);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
// BFS callers (upstream)
|
|
497
|
+
const upQueue = [[targetId, 0]];
|
|
498
|
+
const upVisited = new Set([targetId]);
|
|
499
|
+
while (upQueue.length > 0) {
|
|
500
|
+
const [current, d] = upQueue.shift();
|
|
501
|
+
if (d > 5)
|
|
502
|
+
continue;
|
|
503
|
+
const callerSet = g.callers.get(current);
|
|
504
|
+
if (!callerSet)
|
|
505
|
+
continue;
|
|
506
|
+
for (const id of callerSet) {
|
|
507
|
+
if (!upVisited.has(id)) {
|
|
508
|
+
upVisited.add(id);
|
|
509
|
+
// Only override if not already found with shorter distance
|
|
510
|
+
if (!distances.has(id) || distances.get(id).dist > d + 1) {
|
|
511
|
+
distances.set(id, { dist: d + 1, dir: 'upstream' });
|
|
512
|
+
}
|
|
513
|
+
upQueue.push([id, d + 1]);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
for (const [id, { dist, dir }] of distances) {
|
|
518
|
+
const e = g.entityById.get(id);
|
|
519
|
+
if (!e)
|
|
520
|
+
continue;
|
|
521
|
+
const tokens = estimateTokens(e);
|
|
522
|
+
const relevance = 1 / (1 + dist);
|
|
523
|
+
candidates.push({
|
|
524
|
+
entity: entitySummary(e),
|
|
525
|
+
relevance: Math.round(relevance * 1000) / 1000,
|
|
526
|
+
distance: dist,
|
|
527
|
+
direction: dir,
|
|
528
|
+
tokens,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// Greedy knapsack: sort by relevance/token ratio, fill until budget
|
|
533
|
+
candidates.sort((a, b) => (b.relevance / b.tokens) - (a.relevance / a.tokens));
|
|
534
|
+
const selected = [];
|
|
535
|
+
const targetTokens = estimateTokens(entity);
|
|
536
|
+
let usedTokens = targetTokens; // Reserve space for the target itself
|
|
537
|
+
for (const candidate of candidates) {
|
|
538
|
+
if (usedTokens + candidate.tokens > max_tokens)
|
|
539
|
+
continue;
|
|
540
|
+
selected.push(candidate);
|
|
541
|
+
usedTokens += candidate.tokens;
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
target: {
|
|
545
|
+
...entitySummary(entity),
|
|
546
|
+
tokens: targetTokens,
|
|
547
|
+
},
|
|
548
|
+
maxTokens: max_tokens,
|
|
549
|
+
usedTokens,
|
|
550
|
+
contextEntities: selected.length,
|
|
551
|
+
totalCandidates: candidates.length,
|
|
552
|
+
context: selected,
|
|
553
|
+
};
|
|
554
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -3,10 +3,33 @@
|
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
import type { JstfEntity } from '../types.js';
|
|
11
|
+
import { MultiLoader } from '../loader.js';
|
|
12
|
+
import { type CallGraph } from '../graph.js';
|
|
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.
|
|
6
20
|
*/
|
|
7
|
-
|
|
8
|
-
export declare function
|
|
21
|
+
export declare function validateProject(project?: string): string | null;
|
|
22
|
+
export declare function entityName(e: JstfEntity): string;
|
|
23
|
+
export declare function entityLayer(e: JstfEntity): string;
|
|
24
|
+
export declare function entitySummary(e: JstfEntity): Record<string, unknown>;
|
|
25
|
+
export declare function normalizeConstraints(constraints: JstfEntity['constraints']): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Deep search constraints — matches against raw constraint object fields too,
|
|
28
|
+
* not just normalized tags.
|
|
29
|
+
*/
|
|
30
|
+
export declare function constraintMatches(constraints: JstfEntity['constraints'], target: string): boolean;
|
|
9
31
|
export declare function queryEntities(args: {
|
|
32
|
+
project?: string;
|
|
10
33
|
query?: string;
|
|
11
34
|
layer?: string;
|
|
12
35
|
module?: string;
|
|
@@ -15,22 +38,31 @@ export declare function queryEntities(args: {
|
|
|
15
38
|
}): unknown;
|
|
16
39
|
export declare function getEntity(args: {
|
|
17
40
|
id: string;
|
|
41
|
+
project?: string;
|
|
18
42
|
}): unknown;
|
|
19
43
|
export declare function getDependencies(args: {
|
|
20
44
|
entity_id: string;
|
|
21
45
|
direction?: 'callers' | 'callees' | 'both';
|
|
22
46
|
depth?: number;
|
|
47
|
+
project?: string;
|
|
23
48
|
}): unknown;
|
|
49
|
+
export declare function collectTransitive(adjacency: Map<string, Set<string>>, startId: string, maxDepth: number): string[];
|
|
24
50
|
export declare function getDataFlow(args: {
|
|
25
51
|
entity_id: string;
|
|
52
|
+
project?: string;
|
|
26
53
|
}): unknown;
|
|
27
54
|
export declare function findByConstraint(args: {
|
|
28
55
|
constraint: string;
|
|
56
|
+
project?: string;
|
|
29
57
|
}): unknown;
|
|
30
58
|
export declare function getBlastRadius(args: {
|
|
31
59
|
entity_ids: string[];
|
|
60
|
+
project?: string;
|
|
32
61
|
}): unknown;
|
|
33
62
|
export declare function listModules(args: {
|
|
34
63
|
group_by?: 'layer' | 'module' | 'file' | 'language';
|
|
64
|
+
project?: string;
|
|
65
|
+
}): unknown;
|
|
66
|
+
export declare function getTopology(args?: {
|
|
67
|
+
project?: string;
|
|
35
68
|
}): unknown;
|
|
36
|
-
export declare function getTopology(): unknown;
|