@papyruslabsai/seshat-mcp 0.6.0 → 0.8.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/graph.js +89 -37
- package/dist/index.js +82 -1
- package/dist/tools/functors.d.ts +22 -0
- package/dist/tools/functors.js +219 -1
- package/package.json +1 -1
package/dist/graph.js
CHANGED
|
@@ -31,59 +31,111 @@ export function buildCallGraph(entities) {
|
|
|
31
31
|
}
|
|
32
32
|
// Build call edges from dependency data
|
|
33
33
|
for (const caller of entities) {
|
|
34
|
-
if (!caller.id || !caller.edges
|
|
34
|
+
if (!caller.id || !caller.edges)
|
|
35
35
|
continue;
|
|
36
|
+
// Process direct function/method calls
|
|
36
37
|
const callsArray = caller.edges.calls;
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// Strategy 1: Exact match
|
|
45
|
-
if (entityById.has(target)) {
|
|
46
|
-
calleeId = target;
|
|
47
|
-
}
|
|
48
|
-
// Strategy 2: Method match (target = "module.method")
|
|
49
|
-
else if (target.includes('.')) {
|
|
50
|
-
const dotIdx = target.indexOf('.');
|
|
51
|
-
const modulePart = target.substring(0, dotIdx);
|
|
52
|
-
const methodPart = target.substring(dotIdx + 1);
|
|
53
|
-
// First try "module.method" as a full entity ID
|
|
38
|
+
if (Array.isArray(callsArray)) {
|
|
39
|
+
for (const call of callsArray) {
|
|
40
|
+
if (!call.target)
|
|
41
|
+
continue;
|
|
42
|
+
const target = call.target;
|
|
43
|
+
let calleeId = null;
|
|
44
|
+
// Strategy 1: Exact match
|
|
54
45
|
if (entityById.has(target)) {
|
|
55
46
|
calleeId = target;
|
|
56
47
|
}
|
|
57
|
-
//
|
|
48
|
+
// Strategy 2: Method match (target = "module.method")
|
|
49
|
+
else if (target.includes('.')) {
|
|
50
|
+
const dotIdx = target.indexOf('.');
|
|
51
|
+
const modulePart = target.substring(0, dotIdx);
|
|
52
|
+
const methodPart = target.substring(dotIdx + 1);
|
|
53
|
+
// First try "module.method" as a full entity ID
|
|
54
|
+
if (entityById.has(target)) {
|
|
55
|
+
calleeId = target;
|
|
56
|
+
}
|
|
57
|
+
// Search within the module's entities for one named methodPart
|
|
58
|
+
if (!calleeId) {
|
|
59
|
+
const moduleEntities = entityByModule.get(modulePart);
|
|
60
|
+
if (moduleEntities) {
|
|
61
|
+
const match = moduleEntities.find(e => {
|
|
62
|
+
const name = typeof e.struct === 'string' ? e.struct : e.struct?.name;
|
|
63
|
+
return e.id === methodPart || name === methodPart;
|
|
64
|
+
});
|
|
65
|
+
if (match) {
|
|
66
|
+
calleeId = match.id;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Also try "module.method" concatenated as an ID
|
|
71
|
+
if (!calleeId && entityById.has(`${modulePart}.${methodPart}`)) {
|
|
72
|
+
calleeId = `${modulePart}.${methodPart}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Strategy 3: Prefix match
|
|
58
76
|
if (!calleeId) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return e.id === methodPart || name === methodPart;
|
|
64
|
-
});
|
|
65
|
-
if (match) {
|
|
66
|
-
calleeId = match.id;
|
|
77
|
+
for (const [id] of entityById) {
|
|
78
|
+
if (id.endsWith(`.${target}`) || id === target) {
|
|
79
|
+
calleeId = id;
|
|
80
|
+
break;
|
|
67
81
|
}
|
|
68
82
|
}
|
|
69
83
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
calleeId
|
|
84
|
+
if (calleeId && calleeId !== caller.id) {
|
|
85
|
+
callees.get(caller.id)?.add(calleeId);
|
|
86
|
+
callers.get(calleeId)?.add(caller.id);
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
89
|
+
}
|
|
90
|
+
// Process lexical imports (wiring file-level dependencies into the graph)
|
|
91
|
+
const importsArray = caller.edges.imports;
|
|
92
|
+
if (Array.isArray(importsArray)) {
|
|
93
|
+
for (const imp of importsArray) {
|
|
94
|
+
// If it's a module-level entity, we link to the module
|
|
95
|
+
let calleeId = null;
|
|
96
|
+
const source = imp.source || imp.module;
|
|
97
|
+
if (!source)
|
|
98
|
+
continue;
|
|
99
|
+
// Attempt to find the imported module or entity
|
|
100
|
+
// Here we do a basic substring match against source files/modules
|
|
101
|
+
for (const [id, entity] of entityById) {
|
|
102
|
+
const eSource = entity._sourceFile || entity.context?.module || '';
|
|
103
|
+
if (eSource && (eSource.includes(source) || source.includes(eSource))) {
|
|
79
104
|
calleeId = id;
|
|
80
105
|
break;
|
|
81
106
|
}
|
|
82
107
|
}
|
|
108
|
+
if (calleeId && calleeId !== caller.id) {
|
|
109
|
+
callees.get(caller.id)?.add(calleeId);
|
|
110
|
+
callers.get(calleeId)?.add(caller.id);
|
|
111
|
+
}
|
|
83
112
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
113
|
+
}
|
|
114
|
+
// Process IoC / Dynamic Dispatch (calledBy)
|
|
115
|
+
const calledByArray = caller.edges.calledBy;
|
|
116
|
+
if (Array.isArray(calledByArray)) {
|
|
117
|
+
for (const cb of calledByArray) {
|
|
118
|
+
const source = cb.source;
|
|
119
|
+
if (!source)
|
|
120
|
+
continue;
|
|
121
|
+
let callerId = null;
|
|
122
|
+
if (entityById.has(source)) {
|
|
123
|
+
callerId = source;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
for (const [id] of entityById) {
|
|
127
|
+
if (id.endsWith(`.${source}`) || id === source) {
|
|
128
|
+
callerId = id;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// In this relationship, the current entity (`caller` in this loop context, despite the name)
|
|
134
|
+
// is actually the CALLEE, and the `source` is the true CALLER.
|
|
135
|
+
if (callerId && callerId !== caller.id) {
|
|
136
|
+
callees.get(callerId)?.add(caller.id); // true caller calls this entity
|
|
137
|
+
callers.get(caller.id)?.add(callerId); // this entity is called by true caller
|
|
138
|
+
}
|
|
87
139
|
}
|
|
88
140
|
}
|
|
89
141
|
}
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextpro
|
|
|
34
34
|
import { MultiLoader } from './loader.js';
|
|
35
35
|
import { bootstrap } from './bootstrap.js';
|
|
36
36
|
import { initTools, queryEntities, getEntity, getDependencies, getDataFlow, findByConstraint, getBlastRadius, listModules, getTopology, } from './tools/index.js';
|
|
37
|
-
import { findDeadCode, findLayerViolations, getCouplingMetrics, getAuthMatrix, findErrorGaps, getTestCoverage, getOptimalContext, estimateTaskCost, reportActualBurn, find_runtime_violations, find_ownership_violations, query_traits, } from './tools/functors.js';
|
|
37
|
+
import { findDeadCode, findLayerViolations, getCouplingMetrics, getAuthMatrix, findErrorGaps, getTestCoverage, getOptimalContext, estimateTaskCost, reportActualBurn, find_runtime_violations, find_ownership_violations, query_traits, simulate_mutation, query_data_targets, find_exposure_leaks, find_semantic_clones, } from './tools/functors.js';
|
|
38
38
|
import { diffBundle, conflictMatrix, } from './tools/diff.js';
|
|
39
39
|
// ─── Project Discovery ───────────────────────────────────────────
|
|
40
40
|
/**
|
|
@@ -424,6 +424,75 @@ const TOOLS = [
|
|
|
424
424
|
required: ['trait'],
|
|
425
425
|
},
|
|
426
426
|
},
|
|
427
|
+
{
|
|
428
|
+
name: 'simulate_mutation',
|
|
429
|
+
description: 'The Semantic Physics Engine. Proposes a hypothetical change to an entity\'s dimensions (like adding a "fallible" or "auth" trait) and simulates the topological fallout upstream and downstream. Returns a blueprint of exactly which other entities will break and what fixes they require.',
|
|
430
|
+
inputSchema: {
|
|
431
|
+
type: 'object',
|
|
432
|
+
properties: {
|
|
433
|
+
project: projectParam,
|
|
434
|
+
entity_id: {
|
|
435
|
+
type: 'string',
|
|
436
|
+
description: 'The target entity to mutate.',
|
|
437
|
+
},
|
|
438
|
+
mutation: {
|
|
439
|
+
type: 'object',
|
|
440
|
+
description: 'The hypothetical change to apply.',
|
|
441
|
+
properties: {
|
|
442
|
+
dimension: { type: 'string', enum: ['constraints', 'traits'] },
|
|
443
|
+
change: {
|
|
444
|
+
type: 'object',
|
|
445
|
+
properties: {
|
|
446
|
+
add: { type: 'array', items: { type: 'string' } },
|
|
447
|
+
remove: { type: 'array', items: { type: 'string' } },
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
required: ['dimension', 'change'],
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
required: ['entity_id', 'mutation'],
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
name: 'query_data_targets',
|
|
459
|
+
description: 'Search the codebase across the δ (Data) dimension to find all entities that read or write to a specific database table, state object, or data source. This acts as a reverse-index for data flow, essential for planning migrations or state refactors.',
|
|
460
|
+
inputSchema: {
|
|
461
|
+
type: 'object',
|
|
462
|
+
properties: {
|
|
463
|
+
project: projectParam,
|
|
464
|
+
target_name: {
|
|
465
|
+
type: 'string',
|
|
466
|
+
description: 'The name of the database table, state object, or data source to query (e.g., "users", "auth_token").',
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
required: ['target_name'],
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: 'find_exposure_leaks',
|
|
474
|
+
description: 'Analyze the call graph across the χ (Context) dimension to find architectural visibility leaks. Flags paths where a "public" or "api" entity directly accesses a "private" entity, potentially leaking sensitive data or bypassing internal service boundaries.',
|
|
475
|
+
inputSchema: {
|
|
476
|
+
type: 'object',
|
|
477
|
+
properties: {
|
|
478
|
+
project: projectParam,
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
name: 'find_semantic_clones',
|
|
484
|
+
description: 'Analyze the codebase across the Σ (Semantics) dimension to find duplicated logic blocks. Normalizes variables and compares abstract syntax tree shapes to identify identical algorithms written across different files or even different languages.',
|
|
485
|
+
inputSchema: {
|
|
486
|
+
type: 'object',
|
|
487
|
+
properties: {
|
|
488
|
+
project: projectParam,
|
|
489
|
+
min_complexity: {
|
|
490
|
+
type: 'number',
|
|
491
|
+
description: 'Minimum number of logic expressions required to constitute a match (default: 5).',
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
},
|
|
427
496
|
// ─── Diff Tools ─────────────────────────────────────────────────
|
|
428
497
|
{
|
|
429
498
|
name: 'diff_bundle',
|
|
@@ -638,6 +707,18 @@ async function main() {
|
|
|
638
707
|
case 'query_traits':
|
|
639
708
|
result = query_traits(args);
|
|
640
709
|
break;
|
|
710
|
+
case 'simulate_mutation':
|
|
711
|
+
result = simulate_mutation(args);
|
|
712
|
+
break;
|
|
713
|
+
case 'query_data_targets':
|
|
714
|
+
result = query_data_targets(args);
|
|
715
|
+
break;
|
|
716
|
+
case 'find_exposure_leaks':
|
|
717
|
+
result = find_exposure_leaks(args);
|
|
718
|
+
break;
|
|
719
|
+
case 'find_semantic_clones':
|
|
720
|
+
result = find_semantic_clones(args);
|
|
721
|
+
break;
|
|
641
722
|
// Diff Tools
|
|
642
723
|
case 'diff_bundle':
|
|
643
724
|
result = await diffBundle(args);
|
package/dist/tools/functors.d.ts
CHANGED
|
@@ -76,3 +76,25 @@ export declare function query_traits(args: {
|
|
|
76
76
|
trait: string;
|
|
77
77
|
project?: string;
|
|
78
78
|
}): unknown;
|
|
79
|
+
export declare function simulate_mutation(args: {
|
|
80
|
+
entity_id: string;
|
|
81
|
+
mutation: {
|
|
82
|
+
dimension: 'constraints' | 'traits';
|
|
83
|
+
change: {
|
|
84
|
+
add?: string[];
|
|
85
|
+
remove?: string[];
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
project?: string;
|
|
89
|
+
}): unknown;
|
|
90
|
+
export declare function query_data_targets(args: {
|
|
91
|
+
target_name: string;
|
|
92
|
+
project?: string;
|
|
93
|
+
}): unknown;
|
|
94
|
+
export declare function find_exposure_leaks(args?: {
|
|
95
|
+
project?: string;
|
|
96
|
+
}): unknown;
|
|
97
|
+
export declare function find_semantic_clones(args?: {
|
|
98
|
+
project?: string;
|
|
99
|
+
min_complexity?: number;
|
|
100
|
+
}): unknown;
|
package/dist/tools/functors.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* coupling metrics, auth coverage, etc.) from the entity data.
|
|
7
7
|
*/
|
|
8
8
|
import { computeBlastRadius } from '../graph.js';
|
|
9
|
-
import { getLoader, getGraph, validateProject, entityLayer, entitySummary, } from './index.js';
|
|
9
|
+
import { getLoader, getGraph, validateProject, entityName, entityLayer, entitySummary, } from './index.js';
|
|
10
10
|
import { isSupabaseConfigured, insertPrediction, updateActualBurn, abandonPrediction, listPredictions, } from '../supabase.js';
|
|
11
11
|
// ─── Layer ordering for violation detection ──────────────────────
|
|
12
12
|
const LAYER_ORDER = {
|
|
@@ -992,3 +992,221 @@ export function query_traits(args) {
|
|
|
992
992
|
entities: results.slice(0, 100).map(entitySummary),
|
|
993
993
|
};
|
|
994
994
|
}
|
|
995
|
+
// ─── Tool: simulate_mutation (The Physics Engine) ────────────────
|
|
996
|
+
export function simulate_mutation(args) {
|
|
997
|
+
const projErr = validateProject(args.project);
|
|
998
|
+
if (projErr)
|
|
999
|
+
return { error: projErr };
|
|
1000
|
+
const loader = getLoader();
|
|
1001
|
+
const g = getGraph(args.project);
|
|
1002
|
+
const targetEntity = loader.getEntityById(args.entity_id, args.project)
|
|
1003
|
+
|| loader.getEntityByName(args.entity_id, args.project);
|
|
1004
|
+
if (!targetEntity) {
|
|
1005
|
+
return { error: `Entity not found: ${args.entity_id}` };
|
|
1006
|
+
}
|
|
1007
|
+
const targetId = targetEntity.id;
|
|
1008
|
+
const { dimension, change } = args.mutation;
|
|
1009
|
+
const addedTags = (change.add || []).map(t => t.toLowerCase());
|
|
1010
|
+
// We are currently simulating topological fallout for newly ADDED constraints/traits.
|
|
1011
|
+
// Example: If we add "fallible" or "auth", who upstream breaks because they don't handle it?
|
|
1012
|
+
const fallout = [];
|
|
1013
|
+
// Simulate Fallout: Traversal Upstream (Callers)
|
|
1014
|
+
const upQueue = [[targetId, 0]];
|
|
1015
|
+
const upVisited = new Set([targetId]);
|
|
1016
|
+
while (upQueue.length > 0) {
|
|
1017
|
+
const [currentId, depth] = upQueue.shift();
|
|
1018
|
+
if (depth > 5)
|
|
1019
|
+
continue; // bound simulation depth
|
|
1020
|
+
const callerSet = g.callers.get(currentId);
|
|
1021
|
+
if (!callerSet)
|
|
1022
|
+
continue;
|
|
1023
|
+
for (const callerId of callerSet) {
|
|
1024
|
+
if (upVisited.has(callerId))
|
|
1025
|
+
continue;
|
|
1026
|
+
upVisited.add(callerId);
|
|
1027
|
+
const callerEntity = g.entityById.get(callerId);
|
|
1028
|
+
if (!callerEntity)
|
|
1029
|
+
continue;
|
|
1030
|
+
const callerConstraints = callerEntity.constraints;
|
|
1031
|
+
const callerTraits = callerEntity.traits;
|
|
1032
|
+
// Rule 1: Fallible Simulation
|
|
1033
|
+
if (addedTags.includes('fallible') || addedTags.includes('throws')) {
|
|
1034
|
+
const hasErrorHandling = callerConstraints?.errorHandling &&
|
|
1035
|
+
(callerConstraints.errorHandling.tryCatch || callerConstraints.errorHandling.catchClause);
|
|
1036
|
+
if (!hasErrorHandling) {
|
|
1037
|
+
fallout.push({
|
|
1038
|
+
entity: entitySummary(callerEntity),
|
|
1039
|
+
distance: depth + 1,
|
|
1040
|
+
reason: `Calls a newly fallible pipeline but lacks error handling.`,
|
|
1041
|
+
requiredFix: `Wrap call to ${currentId} in try/catch or propagate error.`,
|
|
1042
|
+
});
|
|
1043
|
+
// Error propagates up if not caught, so we continue queueing
|
|
1044
|
+
upQueue.push([callerId, depth + 1]);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
// Rule 2: Auth / Security Simulation
|
|
1048
|
+
else if (addedTags.includes('auth') || addedTags.includes('secure')) {
|
|
1049
|
+
// Does the caller provide Auth? Check constraints.auth or if they are a known auth middleware
|
|
1050
|
+
const hasAuth = callerConstraints?.auth || callerTraits?.self?.authContext;
|
|
1051
|
+
const isMiddleware = entityLayer(callerEntity) === 'middleware';
|
|
1052
|
+
if (!hasAuth && !isMiddleware) {
|
|
1053
|
+
fallout.push({
|
|
1054
|
+
entity: entitySummary(callerEntity),
|
|
1055
|
+
distance: depth + 1,
|
|
1056
|
+
reason: `Upstream path is unauthenticated but downstream requires auth.`,
|
|
1057
|
+
requiredFix: `Add authentication context to ${entityName(callerEntity)} or its router.`,
|
|
1058
|
+
});
|
|
1059
|
+
upQueue.push([callerId, depth + 1]);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
// Generic unhandled propagation
|
|
1063
|
+
else {
|
|
1064
|
+
upQueue.push([callerId, depth + 1]);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return {
|
|
1069
|
+
target: entitySummary(targetEntity),
|
|
1070
|
+
mutation: args.mutation,
|
|
1071
|
+
affectedEntitiesCount: fallout.length,
|
|
1072
|
+
_summary: `Simulating adding [${addedTags.join(', ')}] to ${entityName(targetEntity)}. This structurally breaks ${fallout.length} upstream entities.`,
|
|
1073
|
+
fallout,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
// ─── Tool: query_data_targets (δ Dimension Reverse-Index) ────────
|
|
1077
|
+
export function query_data_targets(args) {
|
|
1078
|
+
const projErr = validateProject(args.project);
|
|
1079
|
+
if (projErr)
|
|
1080
|
+
return { error: projErr };
|
|
1081
|
+
const loader = getLoader();
|
|
1082
|
+
const entities = loader.getEntities(args.project);
|
|
1083
|
+
const target = args.target_name.toLowerCase();
|
|
1084
|
+
const readers = [];
|
|
1085
|
+
const writers = [];
|
|
1086
|
+
for (const e of entities) {
|
|
1087
|
+
if (!e.data)
|
|
1088
|
+
continue;
|
|
1089
|
+
let isReader = false;
|
|
1090
|
+
let isWriter = false;
|
|
1091
|
+
// Check tables
|
|
1092
|
+
if (Array.isArray(e.data.tables) && e.data.tables.some(t => String(t).toLowerCase() === target)) {
|
|
1093
|
+
// By default, if they touch a table but don't specify mutation, we assume read
|
|
1094
|
+
isReader = true;
|
|
1095
|
+
}
|
|
1096
|
+
// Check inputs (sources)
|
|
1097
|
+
if (Array.isArray(e.data.inputs)) {
|
|
1098
|
+
if (e.data.inputs.some(i => i && typeof i === 'object' && String(i.name || i.source).toLowerCase() === target)) {
|
|
1099
|
+
isReader = true;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
else if (Array.isArray(e.data.sources)) {
|
|
1103
|
+
if (e.data.sources.some(s => String(s).toLowerCase() === target)) {
|
|
1104
|
+
isReader = true;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
// Check mutations
|
|
1108
|
+
if (Array.isArray(e.data.mutations)) {
|
|
1109
|
+
if (e.data.mutations.some(m => m && typeof m === 'object' && String(m.target).toLowerCase() === target)) {
|
|
1110
|
+
isWriter = true;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (isWriter) {
|
|
1114
|
+
writers.push(entitySummary(e));
|
|
1115
|
+
}
|
|
1116
|
+
else if (isReader) {
|
|
1117
|
+
readers.push(entitySummary(e));
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return {
|
|
1121
|
+
target: args.target_name,
|
|
1122
|
+
totalInteractions: readers.length + writers.length,
|
|
1123
|
+
writersCount: writers.length,
|
|
1124
|
+
readersCount: readers.length,
|
|
1125
|
+
_summary: `Found ${writers.length} entities mutating and ${readers.length} entities reading data target '${args.target_name}' (δ dimension reverse-index)`,
|
|
1126
|
+
writers: writers.slice(0, 50),
|
|
1127
|
+
readers: readers.slice(0, 50),
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
// ─── Tool: find_exposure_leaks (χ Context Dimension) ─────────────
|
|
1131
|
+
export function find_exposure_leaks(args) {
|
|
1132
|
+
const projErr = validateProject(args?.project);
|
|
1133
|
+
if (projErr)
|
|
1134
|
+
return { error: projErr };
|
|
1135
|
+
const g = getGraph(args?.project);
|
|
1136
|
+
const leaks = [];
|
|
1137
|
+
for (const [callerId, calleeIds] of g.callees) {
|
|
1138
|
+
const callerEntity = g.entityById.get(callerId);
|
|
1139
|
+
if (!callerEntity)
|
|
1140
|
+
continue;
|
|
1141
|
+
const callerVisibility = callerEntity.context?.visibility || callerEntity.context?.exposure;
|
|
1142
|
+
if (callerVisibility !== 'public' && callerVisibility !== 'api')
|
|
1143
|
+
continue;
|
|
1144
|
+
for (const calleeId of calleeIds) {
|
|
1145
|
+
const calleeEntity = g.entityById.get(calleeId);
|
|
1146
|
+
if (!calleeEntity)
|
|
1147
|
+
continue;
|
|
1148
|
+
const calleeVisibility = calleeEntity.context?.visibility;
|
|
1149
|
+
if (calleeVisibility === 'private') {
|
|
1150
|
+
leaks.push({
|
|
1151
|
+
publicCaller: entitySummary(callerEntity),
|
|
1152
|
+
privateCallee: entitySummary(calleeEntity),
|
|
1153
|
+
issue: `Exposure Leak: Public/API entity directly calls deeply private entity.`,
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return {
|
|
1159
|
+
totalLeaks: leaks.length,
|
|
1160
|
+
_summary: `Found ${leaks.length} architectural visibility leaks where public edges bypass internal boundaries to reach private entities (χ dimension)`,
|
|
1161
|
+
leaks: leaks.slice(0, 100),
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
// ─── Tool: find_semantic_clones (Σ Logic Dimension) ──────────────
|
|
1165
|
+
import { createHash } from 'crypto';
|
|
1166
|
+
export function find_semantic_clones(args) {
|
|
1167
|
+
const projErr = validateProject(args?.project);
|
|
1168
|
+
if (projErr)
|
|
1169
|
+
return { error: projErr };
|
|
1170
|
+
const loader = getLoader();
|
|
1171
|
+
const entities = loader.getEntities(args?.project);
|
|
1172
|
+
const { min_complexity = 5 } = args || {};
|
|
1173
|
+
const logicHashes = new Map();
|
|
1174
|
+
for (const e of entities) {
|
|
1175
|
+
if (!e.semantics || !Array.isArray(e.semantics) || e.semantics.length < min_complexity) {
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
// A highly simplified logic stringification that ignores specific variable names
|
|
1179
|
+
// but captures the structural AST shape.
|
|
1180
|
+
// In a production scenario, you would normalize variable identifiers ($1, $2) here.
|
|
1181
|
+
const shapeString = JSON.stringify(e.semantics, (key, value) => {
|
|
1182
|
+
// Omit line numbers and exact variable names to find structural clones
|
|
1183
|
+
if (key === 'loc' || key === 'name' || key === 'value' || key === 'id')
|
|
1184
|
+
return undefined;
|
|
1185
|
+
return value;
|
|
1186
|
+
});
|
|
1187
|
+
const hash = createHash('sha256').update(shapeString).digest('hex');
|
|
1188
|
+
if (!logicHashes.has(hash))
|
|
1189
|
+
logicHashes.set(hash, []);
|
|
1190
|
+
logicHashes.get(hash).push(e);
|
|
1191
|
+
}
|
|
1192
|
+
const clones = [];
|
|
1193
|
+
for (const [hash, group] of logicHashes.entries()) {
|
|
1194
|
+
if (group.length > 1) {
|
|
1195
|
+
clones.push({
|
|
1196
|
+
count: group.length,
|
|
1197
|
+
entities: group.map(e => entitySummary(e)),
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
// Sort by highest duplication count
|
|
1202
|
+
clones.sort((a, b) => b.count - a.count);
|
|
1203
|
+
let totalDuplicatedEntities = 0;
|
|
1204
|
+
for (const c of clones)
|
|
1205
|
+
totalDuplicatedEntities += c.count;
|
|
1206
|
+
return {
|
|
1207
|
+
cloneGroupsFound: clones.length,
|
|
1208
|
+
totalDuplicatedEntities,
|
|
1209
|
+
_summary: `Found ${clones.length} semantic clone groups involving ${totalDuplicatedEntities} total entities (Σ dimension analysis)`,
|
|
1210
|
+
cloneGroups: clones.slice(0, 50),
|
|
1211
|
+
};
|
|
1212
|
+
}
|