@papyruslabsai/seshat-mcp 0.16.2 → 0.16.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +60 -7
- package/dist/tools/functors.d.ts +4 -0
- package/dist/tools/functors.js +160 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -126,6 +126,7 @@ const TOOLS = [
|
|
|
126
126
|
// ─── Always Visible ───────────────────────────────────────────────
|
|
127
127
|
{
|
|
128
128
|
name: 'get_account_status',
|
|
129
|
+
title: 'Account Status',
|
|
129
130
|
description: 'See your current plan, available tools, and credit balance. Call this if a tool returns a tier error or you want to know what tools are available.',
|
|
130
131
|
inputSchema: { type: 'object', properties: {} },
|
|
131
132
|
annotations: READ_ONLY_OPEN,
|
|
@@ -133,23 +134,27 @@ const TOOLS = [
|
|
|
133
134
|
// ─── Cartographer (Free Tier) ─────────────────────────────────────
|
|
134
135
|
{
|
|
135
136
|
name: 'list_projects',
|
|
137
|
+
title: 'List Projects',
|
|
136
138
|
description: 'Start here. Returns all synced codebases with their size, language, and project name. You need the project name for every other tool. If this returns empty, use sync_project to import the current repo.',
|
|
137
139
|
inputSchema: { type: 'object', properties: {} },
|
|
138
140
|
annotations: READ_ONLY_OPEN,
|
|
139
141
|
},
|
|
140
142
|
{
|
|
141
143
|
name: 'sync_project',
|
|
142
|
-
|
|
144
|
+
title: 'Sync Project',
|
|
145
|
+
description: 'Import a public GitHub repo into Seshat for structural analysis. Call this when list_projects returns empty or when the user wants to analyze a new repo. Detects the git remote automatically if no URL is provided. Extraction typically takes 5-30 seconds. After syncing, call list_projects to confirm the project is available. Use force: true to re-extract even if a cached snapshot exists (useful after code changes or when Seshat extraction has been updated).',
|
|
143
146
|
inputSchema: {
|
|
144
147
|
type: 'object',
|
|
145
148
|
properties: {
|
|
146
149
|
repo_url: { type: 'string', description: 'Public GitHub repo URL (e.g., https://github.com/org/repo). If omitted, tries to detect from the current git remote.' },
|
|
150
|
+
force: { type: 'boolean', description: 'Force re-extraction even if a cached snapshot exists. Default: false.' },
|
|
147
151
|
},
|
|
148
152
|
},
|
|
149
153
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
150
154
|
},
|
|
151
155
|
{
|
|
152
156
|
name: 'query_entities',
|
|
157
|
+
title: 'Query Entities',
|
|
153
158
|
description: 'Like grep but for code structure. Find functions, classes, and routes by name, architectural layer (route/service/component), or module. Returns matching symbols with their type, file, and layer — use this instead of grep when you need to find code by what it does, not by text content.',
|
|
154
159
|
inputSchema: {
|
|
155
160
|
type: 'object',
|
|
@@ -166,6 +171,7 @@ const TOOLS = [
|
|
|
166
171
|
},
|
|
167
172
|
{
|
|
168
173
|
name: 'get_entity',
|
|
174
|
+
title: 'Get Entity Details',
|
|
169
175
|
description: 'Get everything about one function or class — its signature, callers, callees, data flow, constraints, source location, and database operations (which tables it reads/writes). Use this when you need to deeply understand a single symbol before modifying it. Returns more than reading the source file because it includes the dependency context.',
|
|
170
176
|
inputSchema: {
|
|
171
177
|
type: 'object',
|
|
@@ -179,6 +185,7 @@ const TOOLS = [
|
|
|
179
185
|
},
|
|
180
186
|
{
|
|
181
187
|
name: 'get_dependencies',
|
|
188
|
+
title: 'Get Dependencies',
|
|
182
189
|
description: 'Trace who calls a function and what it calls, up to N levels deep. Use this instead of grep-for-function-name when you need the actual call chain — returns the dependency graph, not text matches. Covers callers (upstream), callees (downstream), or both.',
|
|
183
190
|
inputSchema: {
|
|
184
191
|
type: 'object',
|
|
@@ -194,6 +201,7 @@ const TOOLS = [
|
|
|
194
201
|
},
|
|
195
202
|
{
|
|
196
203
|
name: 'get_data_flow',
|
|
204
|
+
title: 'Get Data Flow',
|
|
197
205
|
description: 'See what data a function reads, returns, and mutates (DB writes, state changes). Use this when debugging data bugs or when you need to verify whether a function has side effects before refactoring it.',
|
|
198
206
|
inputSchema: {
|
|
199
207
|
type: 'object',
|
|
@@ -207,6 +215,7 @@ const TOOLS = [
|
|
|
207
215
|
},
|
|
208
216
|
{
|
|
209
217
|
name: 'find_by_constraint',
|
|
218
|
+
title: 'Find by Constraint',
|
|
210
219
|
description: 'Find every function with a specific syntactic constraint tag — AUTH (requires authentication), DB_ACCESS (touches database), THROWS (explicit throw statement), PURE (no side effects), NETWORK_IO (makes HTTP calls), VALIDATED (has input validation). Also supports table-level queries: pass table="walks" to find every function that reads or writes the walks table (answers "what touches this table?" for schema migrations). Constraints are extracted from source syntax, not inferred. For semantic/behavioral properties (e.g., "can fail transitively"), use query_traits instead.',
|
|
211
220
|
inputSchema: {
|
|
212
221
|
type: 'object',
|
|
@@ -221,6 +230,7 @@ const TOOLS = [
|
|
|
221
230
|
},
|
|
222
231
|
{
|
|
223
232
|
name: 'get_blast_radius',
|
|
233
|
+
title: 'Get Blast Radius',
|
|
224
234
|
description: 'Before modifying a function, call this to see everything that could break. Returns all transitively affected symbols — both upstream callers and downstream callees — with distance from the change point. Like git log --follow but for runtime impact. Designed for repeated use: as you discover new symbols with other tools, call this again on them to expand your understanding of the affected surface.',
|
|
225
235
|
inputSchema: {
|
|
226
236
|
type: 'object',
|
|
@@ -234,6 +244,7 @@ const TOOLS = [
|
|
|
234
244
|
},
|
|
235
245
|
{
|
|
236
246
|
name: 'list_modules',
|
|
247
|
+
title: 'List Modules',
|
|
237
248
|
description: 'Get a bird\'s-eye view of how the codebase is organized. Groups all symbols by architectural layer (route/service/component), module, file, or language with counts. Use this to orient yourself in an unfamiliar codebase before diving into specifics.',
|
|
238
249
|
inputSchema: {
|
|
239
250
|
type: 'object',
|
|
@@ -246,6 +257,7 @@ const TOOLS = [
|
|
|
246
257
|
},
|
|
247
258
|
{
|
|
248
259
|
name: 'get_topology',
|
|
260
|
+
title: 'Get Topology',
|
|
249
261
|
description: 'Get the full API surface map in one call — all routes, middleware, auth patterns, and database tables. Use this when you need to understand the overall architecture without reading every file. Returns the information you\'d normally piece together from dozens of file reads.',
|
|
250
262
|
inputSchema: {
|
|
251
263
|
type: 'object',
|
|
@@ -255,6 +267,7 @@ const TOOLS = [
|
|
|
255
267
|
},
|
|
256
268
|
{
|
|
257
269
|
name: 'get_optimal_context',
|
|
270
|
+
title: 'Get Optimal Context',
|
|
258
271
|
description: 'Before working on a function, call this to get the most relevant related code ranked by importance and fitted to a token budget. Returns a prioritized reading list of symbols you should understand — better than guessing which files to open. Designed for iterative use: call it on your target, read the top results, then call it again on any surprising dependencies to build a complete picture.',
|
|
259
272
|
inputSchema: {
|
|
260
273
|
type: 'object',
|
|
@@ -271,7 +284,8 @@ const TOOLS = [
|
|
|
271
284
|
// ─── Analyst Tools (Tier 2) ───────────────────────────────────────
|
|
272
285
|
{
|
|
273
286
|
name: 'find_dead_code',
|
|
274
|
-
|
|
287
|
+
title: 'Find Dead Code',
|
|
288
|
+
description: 'Find functions that nothing calls. Walks the call graph from all entry points (routes, exports, tests) and flags symbols that are unreachable. Use this during cleanup or before a release to find safe deletion candidates. Note: JSX rendering (<Component />) is not currently tracked as a call edge. Sub-components rendered via JSX may appear as dead code in React/Vue codebases.',
|
|
275
289
|
inputSchema: {
|
|
276
290
|
type: 'object',
|
|
277
291
|
properties: {
|
|
@@ -283,6 +297,7 @@ const TOOLS = [
|
|
|
283
297
|
},
|
|
284
298
|
{
|
|
285
299
|
name: 'find_layer_violations',
|
|
300
|
+
title: 'Find Layer Violations',
|
|
286
301
|
description: 'Find places where the architecture is broken — a database repository calling a route handler, a utility importing a component. Returns every backward or skip-layer dependency that violates clean architecture.',
|
|
287
302
|
inputSchema: {
|
|
288
303
|
type: 'object',
|
|
@@ -292,6 +307,7 @@ const TOOLS = [
|
|
|
292
307
|
},
|
|
293
308
|
{
|
|
294
309
|
name: 'get_coupling_metrics',
|
|
310
|
+
title: 'Get Coupling Metrics',
|
|
295
311
|
description: 'Measure how tangled your code is. Returns coupling (cross-boundary dependencies), cohesion (within-group dependencies), and instability scores. High coupling + low cohesion = refactoring candidates. Start with group_by: "layer" for the architectural health view ("are my controllers more coupled than my services?"), then drill into group_by: "module" for specific hotspots.',
|
|
296
312
|
inputSchema: {
|
|
297
313
|
type: 'object',
|
|
@@ -304,7 +320,8 @@ const TOOLS = [
|
|
|
304
320
|
},
|
|
305
321
|
{
|
|
306
322
|
name: 'get_auth_matrix',
|
|
307
|
-
|
|
323
|
+
title: 'Get Auth Matrix',
|
|
324
|
+
description: 'Audit authentication coverage. Shows which API routes and controllers require auth and which don\'t, plus inconsistencies like database access without auth checks. For large codebases, use the module parameter to drill into a specific module/directory. Most useful for backend codebases with middleware-based auth. Frontend frameworks (React, Vue) handle auth via component wrappers, which this tool won\'t detect as AUTH constraints.',
|
|
308
325
|
inputSchema: {
|
|
309
326
|
type: 'object',
|
|
310
327
|
properties: {
|
|
@@ -317,6 +334,7 @@ const TOOLS = [
|
|
|
317
334
|
},
|
|
318
335
|
{
|
|
319
336
|
name: 'find_error_gaps',
|
|
337
|
+
title: 'Find Error Gaps',
|
|
320
338
|
description: 'Find crash risks: functions that throw or have network/DB side effects whose callers don\'t catch errors. Returns the specific caller→callee pairs where exceptions can propagate unhandled. Use this before shipping to find missing error handling.',
|
|
321
339
|
inputSchema: {
|
|
322
340
|
type: 'object',
|
|
@@ -326,6 +344,7 @@ const TOOLS = [
|
|
|
326
344
|
},
|
|
327
345
|
{
|
|
328
346
|
name: 'get_test_coverage',
|
|
347
|
+
title: 'Get Test Coverage',
|
|
329
348
|
description: 'See which production functions are actually exercised by tests via the call graph — semantic coverage, not line coverage. Optionally ranks uncovered functions by blast radius so you know which missing tests are riskiest.',
|
|
330
349
|
inputSchema: {
|
|
331
350
|
type: 'object',
|
|
@@ -338,18 +357,21 @@ const TOOLS = [
|
|
|
338
357
|
},
|
|
339
358
|
{
|
|
340
359
|
name: 'find_runtime_violations',
|
|
360
|
+
title: 'Find Runtime Violations',
|
|
341
361
|
description: 'Find architectural boundary leaks where framework-agnostic code imports framework-specific code. Use this when separating core logic from framework dependencies. Returns 0 for most JS/Python codebases — a non-zero result indicates a serious boundary violation worth investigating.',
|
|
342
362
|
inputSchema: { type: 'object', properties: { project: projectParam } },
|
|
343
363
|
annotations: READ_ONLY_OPEN,
|
|
344
364
|
},
|
|
345
365
|
{
|
|
346
366
|
name: 'find_ownership_violations',
|
|
367
|
+
title: 'Find Ownership Violations',
|
|
347
368
|
description: 'Find memory and lifecycle issues — entities with complex ownership, unsafe blocks, escaping references, or illegal mutability on borrowed data. Returns 0 for most JS/Python codebases — a non-zero result in those languages indicates a serious boundary violation worth investigating. Most detailed results for Rust and C++.',
|
|
348
369
|
inputSchema: { type: 'object', properties: { project: projectParam } },
|
|
349
370
|
annotations: READ_ONLY_OPEN,
|
|
350
371
|
},
|
|
351
372
|
{
|
|
352
373
|
name: 'query_traits',
|
|
374
|
+
title: 'Query Traits',
|
|
353
375
|
description: 'Find functions by inferred behavioral trait — "fallible" (can fail, including transitively via callees that throw), "asyncContext" (carries async state), "generator" (yields values). Traits are semantic properties inferred from the call graph, not just syntax. Use this when you need to find all code with a specific capability. For syntactic tags (explicit throw statements, DB access), use find_by_constraint instead.',
|
|
354
376
|
inputSchema: {
|
|
355
377
|
type: 'object',
|
|
@@ -363,12 +385,14 @@ const TOOLS = [
|
|
|
363
385
|
},
|
|
364
386
|
{
|
|
365
387
|
name: 'find_exposure_leaks',
|
|
388
|
+
title: 'Find Exposure Leaks',
|
|
366
389
|
description: 'Find places where public/API code directly accesses private internals, bypassing the intended abstraction boundary. Use this during API design reviews or before extracting a module.',
|
|
367
390
|
inputSchema: { type: 'object', properties: { project: projectParam } },
|
|
368
391
|
annotations: READ_ONLY_OPEN,
|
|
369
392
|
},
|
|
370
393
|
{
|
|
371
394
|
name: 'find_semantic_clones',
|
|
395
|
+
title: 'Find Semantic Clones',
|
|
372
396
|
description: 'Find duplicated logic across the codebase. Normalizes variable names and compares code structure to catch identical algorithms in different files — even across different languages. Use this before a DRY refactor.',
|
|
373
397
|
inputSchema: {
|
|
374
398
|
type: 'object',
|
|
@@ -382,6 +406,7 @@ const TOOLS = [
|
|
|
382
406
|
// ─── Architect Tools (Tier 3) ─────────────────────────────────────
|
|
383
407
|
{
|
|
384
408
|
name: 'estimate_task_cost',
|
|
409
|
+
title: 'Estimate Task Cost',
|
|
385
410
|
description: 'Before starting a code change, estimate how many tokens it will consume. Computes the blast radius, sums source tokens across all affected symbols, and projects total burn including iteration cycles. Use this to check if a task fits your context budget before committing to it.',
|
|
386
411
|
inputSchema: {
|
|
387
412
|
type: 'object',
|
|
@@ -396,6 +421,7 @@ const TOOLS = [
|
|
|
396
421
|
},
|
|
397
422
|
{
|
|
398
423
|
name: 'simulate_mutation',
|
|
424
|
+
title: 'Simulate Mutation',
|
|
399
425
|
description: 'Simulate a hypothetical code change without writing anything. Propose adding or removing constraints/traits on a symbol and see which other symbols would break and what fixes they\'d need. Use this to plan a change before making it.',
|
|
400
426
|
inputSchema: {
|
|
401
427
|
type: 'object',
|
|
@@ -424,6 +450,7 @@ const TOOLS = [
|
|
|
424
450
|
},
|
|
425
451
|
{
|
|
426
452
|
name: 'create_symbol',
|
|
453
|
+
title: 'Create Symbol',
|
|
427
454
|
description: 'Register a planned new symbol in the graph before it exists on disk. This lets simulate_mutation and conflict_matrix reason about code you haven\'t written yet. Nothing is written to disk.',
|
|
428
455
|
inputSchema: {
|
|
429
456
|
type: 'object',
|
|
@@ -440,6 +467,7 @@ const TOOLS = [
|
|
|
440
467
|
},
|
|
441
468
|
{
|
|
442
469
|
name: 'diff_bundle',
|
|
470
|
+
title: 'Diff Bundle',
|
|
443
471
|
description: 'Compare a worktree against the loaded project to see what changed structurally — not a line diff, but which symbols were added, removed, or had their signatures/dependencies changed. Use this after making changes to verify the structural impact.',
|
|
444
472
|
inputSchema: {
|
|
445
473
|
type: 'object',
|
|
@@ -454,6 +482,7 @@ const TOOLS = [
|
|
|
454
482
|
},
|
|
455
483
|
{
|
|
456
484
|
name: 'conflict_matrix',
|
|
485
|
+
title: 'Conflict Matrix',
|
|
457
486
|
description: 'Given multiple planned tasks, check which ones can run in parallel safely. Classifies every task pair: Tier 1 (different files, safe), Tier 2 (same file different symbols, careful), Tier 3 (same symbol, must sequence). Returns a matrix and suggested execution order.',
|
|
458
487
|
inputSchema: {
|
|
459
488
|
type: 'object',
|
|
@@ -478,8 +507,23 @@ const TOOLS = [
|
|
|
478
507
|
},
|
|
479
508
|
annotations: READ_ONLY_OPEN,
|
|
480
509
|
},
|
|
510
|
+
{
|
|
511
|
+
name: 'trace_boundaries',
|
|
512
|
+
title: 'Trace Boundaries',
|
|
513
|
+
description: 'Identify the boundary surface of one or two codebases — entities that face outward (expose an API, accept network input, make network calls). When given two projects, surfaces both boundary sets so you can trace the handshake between a backend and frontend, detect orphaned routes with no consumer, or find frontend calls with no matching backend handler. Works across any language — boundaries are detected from coordinates (exposure, layer, side effects, data sources), not framework-specific patterns.',
|
|
514
|
+
inputSchema: {
|
|
515
|
+
type: 'object',
|
|
516
|
+
properties: {
|
|
517
|
+
project_a: { type: 'string', description: 'First project name (e.g., the backend). Required.' },
|
|
518
|
+
project_b: { type: 'string', description: 'Second project name (e.g., the frontend). Optional — if omitted, shows only project_a\'s boundary surface.' },
|
|
519
|
+
},
|
|
520
|
+
required: ['project_a'],
|
|
521
|
+
},
|
|
522
|
+
annotations: READ_ONLY_OPEN,
|
|
523
|
+
},
|
|
481
524
|
{
|
|
482
525
|
name: 'query_data_targets',
|
|
526
|
+
title: 'Query Data Targets',
|
|
483
527
|
description: 'Find every function that reads from or writes to a specific database table, state object, or data source. Use this when you need to understand all the code paths that touch a particular data store.',
|
|
484
528
|
inputSchema: {
|
|
485
529
|
type: 'object',
|
|
@@ -522,7 +566,7 @@ function getCloudUrl(path) {
|
|
|
522
566
|
async function main() {
|
|
523
567
|
const server = new Server({
|
|
524
568
|
name: 'seshat',
|
|
525
|
-
version: '0.16.
|
|
569
|
+
version: '0.16.4',
|
|
526
570
|
}, {
|
|
527
571
|
capabilities: { tools: {} },
|
|
528
572
|
instructions: SERVER_INSTRUCTIONS,
|
|
@@ -590,6 +634,7 @@ async function main() {
|
|
|
590
634
|
if (name === 'sync_project') {
|
|
591
635
|
let repoUrl = args?.repo_url;
|
|
592
636
|
// If no URL provided, try to detect git remote
|
|
637
|
+
let headSha;
|
|
593
638
|
if (!repoUrl) {
|
|
594
639
|
try {
|
|
595
640
|
const { execSync } = await import('child_process');
|
|
@@ -597,6 +642,11 @@ async function main() {
|
|
|
597
642
|
// Normalize SSH URLs to HTTPS
|
|
598
643
|
const sshMatch = remote.match(/^git@github\.com:(.+)\.git$/);
|
|
599
644
|
repoUrl = sshMatch ? `https://github.com/${sshMatch[1]}` : remote.replace(/\.git$/, '');
|
|
645
|
+
// Capture current HEAD SHA for cache comparison
|
|
646
|
+
try {
|
|
647
|
+
headSha = execSync('git rev-parse HEAD', { timeout: 5000 }).toString().trim().slice(0, 12);
|
|
648
|
+
}
|
|
649
|
+
catch { /* shallow clone or no commits */ }
|
|
600
650
|
}
|
|
601
651
|
catch {
|
|
602
652
|
return {
|
|
@@ -615,7 +665,7 @@ async function main() {
|
|
|
615
665
|
'Content-Type': 'application/json',
|
|
616
666
|
'x-api-key': apiKey,
|
|
617
667
|
},
|
|
618
|
-
body: JSON.stringify({ repo_url: repoUrl }),
|
|
668
|
+
body: JSON.stringify({ repo_url: repoUrl, force: args?.force === true, head_sha: headSha }),
|
|
619
669
|
});
|
|
620
670
|
if (!res.ok) {
|
|
621
671
|
const errorText = await res.text();
|
|
@@ -715,11 +765,14 @@ async function main() {
|
|
|
715
765
|
}
|
|
716
766
|
// Determine the project hash for this workspace
|
|
717
767
|
// list_projects is unscoped — it returns ALL projects for the user
|
|
768
|
+
// trace_boundaries uses project_a instead of project for its primary project
|
|
718
769
|
const project_hash = name === 'list_projects'
|
|
719
770
|
? undefined
|
|
720
771
|
: (args && typeof args === 'object' && 'project' in args)
|
|
721
772
|
? String(args.project)
|
|
722
|
-
:
|
|
773
|
+
: (args && typeof args === 'object' && 'project_a' in args)
|
|
774
|
+
? String(args.project_a)
|
|
775
|
+
: resolveProjectName();
|
|
723
776
|
// If no project could be resolved and this isn't list_projects, tell the LLM how to fix it
|
|
724
777
|
if (!project_hash && name !== 'list_projects') {
|
|
725
778
|
return {
|
|
@@ -784,7 +837,7 @@ async function main() {
|
|
|
784
837
|
});
|
|
785
838
|
const transport = new StdioServerTransport();
|
|
786
839
|
await server.connect(transport);
|
|
787
|
-
process.stderr.write(`Seshat MCP v0.16.
|
|
840
|
+
process.stderr.write(`Seshat MCP v0.16.4 connected. Structural intelligence ready.\n`);
|
|
788
841
|
}
|
|
789
842
|
main().catch((err) => {
|
|
790
843
|
process.stderr.write(`Fatal: ${err.message}\n`);
|
package/dist/tools/functors.d.ts
CHANGED
|
@@ -107,3 +107,7 @@ export declare function create_symbol(args: {
|
|
|
107
107
|
project?: string;
|
|
108
108
|
description?: string;
|
|
109
109
|
}, loader: ProjectLoader): unknown;
|
|
110
|
+
export declare function trace_boundaries(args: {
|
|
111
|
+
project_a: string;
|
|
112
|
+
project_b?: string;
|
|
113
|
+
}, loaderA: ProjectLoader, loaderB?: ProjectLoader): unknown;
|
package/dist/tools/functors.js
CHANGED
|
@@ -1221,3 +1221,163 @@ export function create_symbol(args, loader) {
|
|
|
1221
1221
|
_summary: `Registered virtual symbol '${args.id}' in ${args.source_file}. Other tools can now reason about this symbol prior to its physical creation.`,
|
|
1222
1222
|
};
|
|
1223
1223
|
}
|
|
1224
|
+
// ─── Tool: trace_boundaries (Cross-Project Boundary Surface Analysis) ────
|
|
1225
|
+
/**
|
|
1226
|
+
* Identify the boundary surface of a codebase — entities that face
|
|
1227
|
+
* outward (expose an API, accept network input, make network calls).
|
|
1228
|
+
*
|
|
1229
|
+
* When two loaders are provided, surfaces both boundary sets and
|
|
1230
|
+
* lets the LLM reason about the handshake between them.
|
|
1231
|
+
*/
|
|
1232
|
+
const IMPORT_STRUCT_TYPES = new Set([
|
|
1233
|
+
'import', 'importspecifier', 'importdeclaration', 'reexport',
|
|
1234
|
+
]);
|
|
1235
|
+
function isBoundaryEntity(e) {
|
|
1236
|
+
const structType = (typeof e.struct === 'string' ? e.struct : e.struct?.type || '').toLowerCase();
|
|
1237
|
+
if (IMPORT_STRUCT_TYPES.has(structType))
|
|
1238
|
+
return null;
|
|
1239
|
+
const ctx = e.context || {};
|
|
1240
|
+
const constraints = e.constraints;
|
|
1241
|
+
const sideEffects = (typeof constraints === 'object' && !Array.isArray(constraints))
|
|
1242
|
+
? constraints.sideEffects
|
|
1243
|
+
: undefined;
|
|
1244
|
+
// Inbound boundary: entities that receive external requests
|
|
1245
|
+
// Coordinates: exposure=api, layer=route/controller, or has auth constraints
|
|
1246
|
+
const isApiExposed = ctx.exposure === 'api' || ctx.exposure === 'public';
|
|
1247
|
+
const isRouteLayer = ctx.layer === 'route' || ctx.layer === 'controller';
|
|
1248
|
+
if (isApiExposed || isRouteLayer)
|
|
1249
|
+
return 'inbound';
|
|
1250
|
+
// Outbound boundary: entities that make external calls
|
|
1251
|
+
// Coordinates: sideEffects includes 'network', or data sources include 'api'/'fetch'
|
|
1252
|
+
const hasNetworkIO = Array.isArray(sideEffects) && sideEffects.includes('network');
|
|
1253
|
+
const dataSources = e.data?.sources || [];
|
|
1254
|
+
const hasApiSource = dataSources.some(s => typeof s === 'string' && ['api', 'fetch', 'network'].includes(s));
|
|
1255
|
+
if (hasNetworkIO || hasApiSource)
|
|
1256
|
+
return 'outbound';
|
|
1257
|
+
return null;
|
|
1258
|
+
}
|
|
1259
|
+
function boundaryEntitySummary(e) {
|
|
1260
|
+
const base = entitySummary(e);
|
|
1261
|
+
const ctx = e.context || {};
|
|
1262
|
+
const constraints = e.constraints;
|
|
1263
|
+
const sideEffects = (typeof constraints === 'object' && !Array.isArray(constraints))
|
|
1264
|
+
? constraints.sideEffects
|
|
1265
|
+
: undefined;
|
|
1266
|
+
const dataSources = e.data?.sources || [];
|
|
1267
|
+
// Add boundary-relevant coordinate data
|
|
1268
|
+
if (ctx.exposure)
|
|
1269
|
+
base.exposure = ctx.exposure;
|
|
1270
|
+
if (sideEffects && sideEffects.length > 0)
|
|
1271
|
+
base.sideEffects = sideEffects;
|
|
1272
|
+
if (dataSources.length > 0)
|
|
1273
|
+
base.dataSources = dataSources;
|
|
1274
|
+
// Error handling info (relevant for boundary robustness)
|
|
1275
|
+
const errorHandling = (typeof constraints === 'object' && !Array.isArray(constraints))
|
|
1276
|
+
? constraints.errorHandling
|
|
1277
|
+
: undefined;
|
|
1278
|
+
if (errorHandling)
|
|
1279
|
+
base.errorHandling = errorHandling;
|
|
1280
|
+
return base;
|
|
1281
|
+
}
|
|
1282
|
+
export function trace_boundaries(args, loaderA, loaderB) {
|
|
1283
|
+
const entitiesA = loaderA.getEntities(args.project_a);
|
|
1284
|
+
const topologyA = loaderA.getTopology(args.project_a);
|
|
1285
|
+
const surfaceA = {
|
|
1286
|
+
inbound: [], outbound: [],
|
|
1287
|
+
};
|
|
1288
|
+
for (const e of entitiesA) {
|
|
1289
|
+
const direction = isBoundaryEntity(e);
|
|
1290
|
+
if (direction) {
|
|
1291
|
+
surfaceA[direction].push(boundaryEntitySummary(e));
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
const result = {
|
|
1295
|
+
project_a: {
|
|
1296
|
+
name: args.project_a,
|
|
1297
|
+
totalEntities: entitiesA.length,
|
|
1298
|
+
boundary: {
|
|
1299
|
+
inbound: surfaceA.inbound.length,
|
|
1300
|
+
outbound: surfaceA.outbound.length,
|
|
1301
|
+
},
|
|
1302
|
+
inbound_surface: surfaceA.inbound.slice(0, 100),
|
|
1303
|
+
outbound_surface: surfaceA.outbound.slice(0, 100),
|
|
1304
|
+
},
|
|
1305
|
+
};
|
|
1306
|
+
// Include topology routes if available (structured route data)
|
|
1307
|
+
if (topologyA) {
|
|
1308
|
+
const routes = [];
|
|
1309
|
+
const prefixes = topologyA.prefixes;
|
|
1310
|
+
if (prefixes) {
|
|
1311
|
+
for (const [prefix, info] of Object.entries(prefixes)) {
|
|
1312
|
+
const routeList = info.routes;
|
|
1313
|
+
if (Array.isArray(routeList)) {
|
|
1314
|
+
for (const r of routeList) {
|
|
1315
|
+
routes.push({ ...r, prefix });
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
const unregistered = topologyA.unregistered;
|
|
1321
|
+
if (Array.isArray(unregistered)) {
|
|
1322
|
+
routes.push(...unregistered);
|
|
1323
|
+
}
|
|
1324
|
+
if (routes.length > 0) {
|
|
1325
|
+
result.project_a.topology_routes = routes.slice(0, 100);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
// If second project provided, surface its boundaries too
|
|
1329
|
+
if (loaderB && args.project_b) {
|
|
1330
|
+
const entitiesB = loaderB.getEntities(args.project_b);
|
|
1331
|
+
const topologyB = loaderB.getTopology(args.project_b);
|
|
1332
|
+
const surfaceB = {
|
|
1333
|
+
inbound: [], outbound: [],
|
|
1334
|
+
};
|
|
1335
|
+
for (const e of entitiesB) {
|
|
1336
|
+
const direction = isBoundaryEntity(e);
|
|
1337
|
+
if (direction) {
|
|
1338
|
+
surfaceB[direction].push(boundaryEntitySummary(e));
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
result.project_b = {
|
|
1342
|
+
name: args.project_b,
|
|
1343
|
+
totalEntities: entitiesB.length,
|
|
1344
|
+
boundary: {
|
|
1345
|
+
inbound: surfaceB.inbound.length,
|
|
1346
|
+
outbound: surfaceB.outbound.length,
|
|
1347
|
+
},
|
|
1348
|
+
inbound_surface: surfaceB.inbound.slice(0, 100),
|
|
1349
|
+
outbound_surface: surfaceB.outbound.slice(0, 100),
|
|
1350
|
+
};
|
|
1351
|
+
if (topologyB) {
|
|
1352
|
+
const routes = [];
|
|
1353
|
+
const prefixes = topologyB.prefixes;
|
|
1354
|
+
if (prefixes) {
|
|
1355
|
+
for (const [prefix, info] of Object.entries(prefixes)) {
|
|
1356
|
+
const routeList = info.routes;
|
|
1357
|
+
if (Array.isArray(routeList)) {
|
|
1358
|
+
for (const r of routeList) {
|
|
1359
|
+
routes.push({ ...r, prefix });
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
const unregistered = topologyB.unregistered;
|
|
1365
|
+
if (Array.isArray(unregistered)) {
|
|
1366
|
+
routes.push(...unregistered);
|
|
1367
|
+
}
|
|
1368
|
+
if (routes.length > 0) {
|
|
1369
|
+
result.project_b.topology_routes = routes.slice(0, 100);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
// Summary for cross-project analysis
|
|
1373
|
+
const aIn = surfaceA.inbound.length;
|
|
1374
|
+
const aOut = surfaceA.outbound.length;
|
|
1375
|
+
const bIn = surfaceB.inbound.length;
|
|
1376
|
+
const bOut = surfaceB.outbound.length;
|
|
1377
|
+
result._summary = `${args.project_a}: ${aIn} inbound + ${aOut} outbound boundary entities. ${args.project_b}: ${bIn} inbound + ${bOut} outbound boundary entities. Examine inbound surfaces (API handlers) against outbound surfaces (network callers) to trace the handshake.`;
|
|
1378
|
+
}
|
|
1379
|
+
else {
|
|
1380
|
+
result._summary = `${args.project_a}: ${surfaceA.inbound.length} inbound + ${surfaceA.outbound.length} outbound boundary entities.`;
|
|
1381
|
+
}
|
|
1382
|
+
return result;
|
|
1383
|
+
}
|
package/package.json
CHANGED