@papyruslabsai/seshat-mcp 0.11.0 → 0.12.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 +0 -28
- package/dist/index.js +21 -4
- package/dist/loader.d.ts +9 -2
- package/dist/loader.js +62 -26
- package/dist/supabase.d.ts +27 -1
- package/dist/supabase.js +19 -11
- package/dist/tools/diff.d.ts +2 -2
- package/dist/tools/diff.js +6 -8
- package/dist/tools/functors.d.ts +10 -10
- package/dist/tools/functors.js +52 -104
- package/dist/tools/index.d.ts +11 -14
- package/dist/tools/index.js +48 -64
- package/dist/types.d.ts +14 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,29 +1 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* @papyruslabs/seshat-mcp — Semantic Code Analysis MCP Server
|
|
4
|
-
*
|
|
5
|
-
* Exposes a codebase's structure, dependencies, and constraints as queryable
|
|
6
|
-
* MCP tools. Reads pre-extracted analysis data from .seshat/_bundle.json.
|
|
7
|
-
*
|
|
8
|
-
* Multi-project mode:
|
|
9
|
-
* Set SESHAT_PROJECTS env var to comma-separated paths or a glob pattern.
|
|
10
|
-
* In multi-project mode, all tools require a `project` parameter.
|
|
11
|
-
* Use `list_projects` to see available projects.
|
|
12
|
-
*
|
|
13
|
-
* Single-project mode (default):
|
|
14
|
-
* When SESHAT_PROJECTS is not set, loads from CWD. No `project` param needed.
|
|
15
|
-
*
|
|
16
|
-
* 20 tools across 4 categories:
|
|
17
|
-
* Discovery: list_projects, query_entities, get_entity, list_modules, get_topology
|
|
18
|
-
* Graph: get_dependencies, get_data_flow, find_by_constraint, get_blast_radius
|
|
19
|
-
* Analysis: find_dead_code, find_layer_violations, get_coupling_metrics,
|
|
20
|
-
* get_auth_matrix, find_error_gaps, get_test_coverage,
|
|
21
|
-
* get_optimal_context, estimate_task_cost, report_actual_burn
|
|
22
|
-
* Diff: diff_bundle, conflict_matrix
|
|
23
|
-
*
|
|
24
|
-
* Usage:
|
|
25
|
-
* npx @papyruslabs/seshat-mcp # single project (CWD)
|
|
26
|
-
* SESHAT_PROJECTS=/path/a,/path/b npx @papyruslabs/seshat-mcp # multi-project
|
|
27
|
-
* SESHAT_PROJECTS="/home/user/projects/*" npx @papyruslabs/seshat-mcp # glob
|
|
28
|
-
*/
|
|
29
1
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
!/usr/bin / env;
|
|
2
|
+
node;
|
|
2
3
|
/**
|
|
3
4
|
* @papyruslabs/seshat-mcp — Semantic Code Analysis MCP Server
|
|
4
5
|
*
|
|
@@ -33,7 +34,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
33
34
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
34
35
|
import { MultiLoader } from './loader.js';
|
|
35
36
|
import { bootstrap } from './bootstrap.js';
|
|
36
|
-
import {
|
|
37
|
+
import { logTelemetry, isSupabaseConfigured } from './supabase.js';
|
|
38
|
+
import { queryEntities, getEntity, getDependencies, getDataFlow, findByConstraint, getBlastRadius, listModules, getTopology, } from './tools/index.js';
|
|
37
39
|
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, create_symbol, } from './tools/functors.js';
|
|
38
40
|
import { diffBundle, conflictMatrix, } from './tools/diff.js';
|
|
39
41
|
// ─── Project Discovery ───────────────────────────────────────────
|
|
@@ -647,6 +649,7 @@ async function main() {
|
|
|
647
649
|
// Handle tool calls
|
|
648
650
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
649
651
|
const { name, arguments: args } = request.params;
|
|
652
|
+
const startTime = Date.now();
|
|
650
653
|
try {
|
|
651
654
|
if (!loader.isLoaded()) {
|
|
652
655
|
return {
|
|
@@ -659,8 +662,7 @@ async function main() {
|
|
|
659
662
|
};
|
|
660
663
|
}
|
|
661
664
|
let result;
|
|
662
|
-
switch (name) {
|
|
663
|
-
// Meta
|
|
665
|
+
switch (name) { // Meta
|
|
664
666
|
case 'list_projects':
|
|
665
667
|
result = {
|
|
666
668
|
multiProject: isMulti,
|
|
@@ -759,6 +761,21 @@ async function main() {
|
|
|
759
761
|
default:
|
|
760
762
|
result = { error: `Unknown tool: ${name}` };
|
|
761
763
|
}
|
|
764
|
+
// Log telemetry for the tool execution
|
|
765
|
+
if (isSupabaseConfigured() && name !== 'list_projects') {
|
|
766
|
+
const executionMs = Date.now() - startTime;
|
|
767
|
+
const projectHash = args && typeof args === 'object' && 'project' in args
|
|
768
|
+
? String(args.project)
|
|
769
|
+
: loader.getProjectNames()[0] || 'unknown';
|
|
770
|
+
// Fire-and-forget telemetry log
|
|
771
|
+
logTelemetry({
|
|
772
|
+
tool_name: name,
|
|
773
|
+
project_hash: projectHash,
|
|
774
|
+
execution_ms: executionMs,
|
|
775
|
+
// user_id will be added by the Ptah API gateway if passing through there,
|
|
776
|
+
// or stay null if running strictly local CLI without an injected token.
|
|
777
|
+
}).catch(() => { });
|
|
778
|
+
}
|
|
762
779
|
return {
|
|
763
780
|
content: [{
|
|
764
781
|
type: 'text',
|
package/dist/loader.d.ts
CHANGED
|
@@ -4,14 +4,20 @@
|
|
|
4
4
|
* Discovers and loads .seshat/_bundle.json from the current working directory.
|
|
5
5
|
* Falls back to checking common alternative locations.
|
|
6
6
|
*/
|
|
7
|
-
import type { JstfEntity, Topology, Manifest, ProjectInfo } from './types.js';
|
|
7
|
+
import type { JstfEntity, Topology, Manifest, ProjectInfo, ProjectLoader } from './types.js';
|
|
8
8
|
export declare class BundleLoader {
|
|
9
9
|
private entities;
|
|
10
10
|
private topology;
|
|
11
11
|
private manifest;
|
|
12
12
|
private loaded;
|
|
13
13
|
private seshatDir;
|
|
14
|
+
private bundleMtime;
|
|
14
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;
|
|
15
21
|
load(): void;
|
|
16
22
|
getEntities(): JstfEntity[];
|
|
17
23
|
getTopology(): Topology | null;
|
|
@@ -25,7 +31,7 @@ export declare class BundleLoader {
|
|
|
25
31
|
* In single-project mode (1 dir), behaves identically to BundleLoader.
|
|
26
32
|
* In multi-project mode, `project` param is required on all accessors.
|
|
27
33
|
*/
|
|
28
|
-
export declare class MultiLoader {
|
|
34
|
+
export declare class MultiLoader implements ProjectLoader {
|
|
29
35
|
private projects;
|
|
30
36
|
private projectPaths;
|
|
31
37
|
private projectEntities;
|
|
@@ -33,6 +39,7 @@ export declare class MultiLoader {
|
|
|
33
39
|
private dirs;
|
|
34
40
|
constructor(projectDirs: string[]);
|
|
35
41
|
load(): void;
|
|
42
|
+
private loadProject;
|
|
36
43
|
/** Get the resolved project name when only one project is loaded. */
|
|
37
44
|
private defaultProject;
|
|
38
45
|
private resolveProject;
|
package/dist/loader.js
CHANGED
|
@@ -12,10 +12,32 @@ export class BundleLoader {
|
|
|
12
12
|
manifest = null;
|
|
13
13
|
loaded = false;
|
|
14
14
|
seshatDir;
|
|
15
|
+
bundleMtime = 0;
|
|
15
16
|
constructor(cwd) {
|
|
16
17
|
const root = cwd || process.cwd();
|
|
17
18
|
this.seshatDir = path.join(root, '.seshat');
|
|
18
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
|
+
}
|
|
19
41
|
load() {
|
|
20
42
|
const bundlePath = path.join(this.seshatDir, '_bundle.json');
|
|
21
43
|
if (!fs.existsSync(bundlePath)) {
|
|
@@ -24,6 +46,7 @@ export class BundleLoader {
|
|
|
24
46
|
` node ci/extract-and-generate.mjs <repo-path> .seshat <project-name>\n` +
|
|
25
47
|
`Or add the CI workflow to auto-generate on push to main.`);
|
|
26
48
|
}
|
|
49
|
+
this.bundleMtime = fs.statSync(bundlePath).mtimeMs;
|
|
27
50
|
const raw = fs.readFileSync(bundlePath, 'utf-8');
|
|
28
51
|
const bundle = JSON.parse(raw);
|
|
29
52
|
this.entities = bundle.entities || [];
|
|
@@ -52,18 +75,15 @@ export class BundleLoader {
|
|
|
52
75
|
this.loaded = true;
|
|
53
76
|
}
|
|
54
77
|
getEntities() {
|
|
55
|
-
|
|
56
|
-
this.load();
|
|
78
|
+
this.ensureFresh();
|
|
57
79
|
return this.entities;
|
|
58
80
|
}
|
|
59
81
|
getTopology() {
|
|
60
|
-
|
|
61
|
-
this.load();
|
|
82
|
+
this.ensureFresh();
|
|
62
83
|
return this.topology;
|
|
63
84
|
}
|
|
64
85
|
getManifest() {
|
|
65
|
-
|
|
66
|
-
this.load();
|
|
86
|
+
this.ensureFresh();
|
|
67
87
|
return this.manifest;
|
|
68
88
|
}
|
|
69
89
|
getEntityById(id) {
|
|
@@ -96,26 +116,29 @@ export class MultiLoader {
|
|
|
96
116
|
}
|
|
97
117
|
load() {
|
|
98
118
|
for (const dir of this.dirs) {
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
loader.load();
|
|
102
|
-
const manifest = loader.getManifest();
|
|
103
|
-
const name = manifest?.projectName || path.basename(dir);
|
|
104
|
-
this.projects.set(name, loader);
|
|
105
|
-
this.projectPaths.set(name, dir);
|
|
106
|
-
// Stamp entities with _project
|
|
107
|
-
const entities = loader.getEntities();
|
|
108
|
-
for (const e of entities) {
|
|
109
|
-
e._project = name;
|
|
110
|
-
}
|
|
111
|
-
this.projectEntities.set(name, entities);
|
|
112
|
-
}
|
|
113
|
-
catch (err) {
|
|
114
|
-
process.stderr.write(`Warning: Skipping ${dir}: ${err.message}\n`);
|
|
115
|
-
}
|
|
119
|
+
this.loadProject(dir);
|
|
116
120
|
}
|
|
117
121
|
this.loaded = true;
|
|
118
122
|
}
|
|
123
|
+
loadProject(dir) {
|
|
124
|
+
const loader = new BundleLoader(dir);
|
|
125
|
+
try {
|
|
126
|
+
loader.load();
|
|
127
|
+
const manifest = loader.getManifest();
|
|
128
|
+
const name = manifest?.projectName || path.basename(dir);
|
|
129
|
+
this.projects.set(name, loader);
|
|
130
|
+
this.projectPaths.set(name, dir);
|
|
131
|
+
// Stamp entities with _project
|
|
132
|
+
const entities = loader.getEntities();
|
|
133
|
+
for (const e of entities) {
|
|
134
|
+
e._project = name;
|
|
135
|
+
}
|
|
136
|
+
this.projectEntities.set(name, entities);
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
process.stderr.write(`Warning: Skipping ${dir}: ${err.message}\n`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
119
142
|
/** Get the resolved project name when only one project is loaded. */
|
|
120
143
|
defaultProject() {
|
|
121
144
|
if (this.projects.size === 1)
|
|
@@ -129,9 +152,22 @@ export class MultiLoader {
|
|
|
129
152
|
if (!this.loaded)
|
|
130
153
|
this.load();
|
|
131
154
|
const p = this.resolveProject(project);
|
|
132
|
-
if (p)
|
|
133
|
-
return
|
|
134
|
-
|
|
155
|
+
if (!p)
|
|
156
|
+
return [];
|
|
157
|
+
// Delegate to BundleLoader (which checks mtime via ensureFresh),
|
|
158
|
+
// then re-stamp and cache if the reference changed.
|
|
159
|
+
const loader = this.projects.get(p);
|
|
160
|
+
if (!loader)
|
|
161
|
+
return [];
|
|
162
|
+
const fresh = loader.getEntities();
|
|
163
|
+
const cached = this.projectEntities.get(p);
|
|
164
|
+
if (fresh !== cached) {
|
|
165
|
+
for (const e of fresh) {
|
|
166
|
+
e._project = p;
|
|
167
|
+
}
|
|
168
|
+
this.projectEntities.set(p, fresh);
|
|
169
|
+
}
|
|
170
|
+
return fresh;
|
|
135
171
|
}
|
|
136
172
|
getTopology(project) {
|
|
137
173
|
if (!this.loaded)
|
package/dist/supabase.d.ts
CHANGED
|
@@ -55,6 +55,31 @@ export interface TokenPredictionRow {
|
|
|
55
55
|
session_id: string | null;
|
|
56
56
|
notes: string | null;
|
|
57
57
|
}
|
|
58
|
+
export interface McpTelemetryLog {
|
|
59
|
+
user_id?: string;
|
|
60
|
+
tool_name: string;
|
|
61
|
+
project_hash: string;
|
|
62
|
+
execution_ms: number;
|
|
63
|
+
}
|
|
64
|
+
export interface JstfSnapshotInsert {
|
|
65
|
+
user_id?: string;
|
|
66
|
+
project_hash: string;
|
|
67
|
+
commit_sha: string;
|
|
68
|
+
language_primary: string;
|
|
69
|
+
total_entities: number;
|
|
70
|
+
metric_typeless_interfaces: number;
|
|
71
|
+
metric_max_in_degree: number;
|
|
72
|
+
metric_circular_dependencies: number;
|
|
73
|
+
metric_untracked_data_transformations: number;
|
|
74
|
+
metric_exposure_leaks: number;
|
|
75
|
+
metric_layer_violations: number;
|
|
76
|
+
metric_ownership_violations: number;
|
|
77
|
+
metric_impure_ratio: number;
|
|
78
|
+
metric_runtime_violations: number;
|
|
79
|
+
metric_semantic_clones: number;
|
|
80
|
+
metric_core_fragility_score: number;
|
|
81
|
+
bundle_payload: unknown;
|
|
82
|
+
}
|
|
58
83
|
/**
|
|
59
84
|
* Insert a prediction row. Returns the row ID or null on failure.
|
|
60
85
|
*/
|
|
@@ -70,4 +95,5 @@ export declare function abandonPrediction(predictionId: string): Promise<boolean
|
|
|
70
95
|
/**
|
|
71
96
|
* List recent predictions for a project (for calibration analysis).
|
|
72
97
|
*/
|
|
73
|
-
export declare function
|
|
98
|
+
export declare function logTelemetry(log: McpTelemetryLog): Promise<void>;
|
|
99
|
+
export declare function insertJstfSnapshot(snapshot: JstfSnapshotInsert): Promise<boolean>;
|
package/dist/supabase.js
CHANGED
|
@@ -18,7 +18,9 @@ export function isSupabaseConfigured() {
|
|
|
18
18
|
return SUPABASE_URL.length > 0 && SUPABASE_KEY.length > 0;
|
|
19
19
|
}
|
|
20
20
|
// ─── REST helpers ────────────────────────────────────────────────
|
|
21
|
-
const
|
|
21
|
+
const TABLE_PREDICTIONS = 'mcp_token_predictions';
|
|
22
|
+
const TABLE_TELEMETRY = 'mcp_telemetry_logs';
|
|
23
|
+
const TABLE_SNAPSHOTS = 'jstf_snapshots';
|
|
22
24
|
async function supabaseRequest(method, path, body, headers) {
|
|
23
25
|
if (!isSupabaseConfigured()) {
|
|
24
26
|
return { ok: false, status: 0, error: 'Supabase not configured' };
|
|
@@ -59,7 +61,7 @@ async function supabaseRequest(method, path, body, headers) {
|
|
|
59
61
|
* Insert a prediction row. Returns the row ID or null on failure.
|
|
60
62
|
*/
|
|
61
63
|
export async function insertPrediction(row) {
|
|
62
|
-
const result = await supabaseRequest('POST',
|
|
64
|
+
const result = await supabaseRequest('POST', TABLE_PREDICTIONS, row);
|
|
63
65
|
if (!result.ok) {
|
|
64
66
|
process.stderr.write(`[seshat] Prediction log failed: ${result.error}\n`);
|
|
65
67
|
return null;
|
|
@@ -72,7 +74,7 @@ export async function insertPrediction(row) {
|
|
|
72
74
|
*/
|
|
73
75
|
export async function updateActualBurn(predictionId, actual) {
|
|
74
76
|
// First fetch the prediction to compute drift
|
|
75
|
-
const fetchResult = await supabaseRequest('GET', `${
|
|
77
|
+
const fetchResult = await supabaseRequest('GET', `${TABLE_PREDICTIONS}?id=eq.${predictionId}&select=predicted_total,status`);
|
|
76
78
|
if (!fetchResult.ok) {
|
|
77
79
|
process.stderr.write(`[seshat] Fetch prediction failed: ${fetchResult.error}\n`);
|
|
78
80
|
return null;
|
|
@@ -98,7 +100,7 @@ export async function updateActualBurn(predictionId, actual) {
|
|
|
98
100
|
status: 'completed',
|
|
99
101
|
...(actual.notes ? { notes: actual.notes } : {}),
|
|
100
102
|
};
|
|
101
|
-
const updateResult = await supabaseRequest('PATCH', `${
|
|
103
|
+
const updateResult = await supabaseRequest('PATCH', `${TABLE_PREDICTIONS}?id=eq.${predictionId}`, updateBody);
|
|
102
104
|
if (!updateResult.ok) {
|
|
103
105
|
process.stderr.write(`[seshat] Update actual burn failed: ${updateResult.error}\n`);
|
|
104
106
|
return null;
|
|
@@ -109,16 +111,22 @@ export async function updateActualBurn(predictionId, actual) {
|
|
|
109
111
|
* Abandon a prediction (task was cancelled or not completed).
|
|
110
112
|
*/
|
|
111
113
|
export async function abandonPrediction(predictionId) {
|
|
112
|
-
const result = await supabaseRequest('PATCH', `${
|
|
114
|
+
const result = await supabaseRequest('PATCH', `${TABLE_PREDICTIONS}?id=eq.${predictionId}&status=eq.predicted`, { status: 'abandoned' });
|
|
113
115
|
return result.ok;
|
|
114
116
|
}
|
|
115
117
|
/**
|
|
116
118
|
* List recent predictions for a project (for calibration analysis).
|
|
117
119
|
*/
|
|
118
|
-
export async function
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
export async function logTelemetry(log) {
|
|
121
|
+
const result = await supabaseRequest('POST', TABLE_TELEMETRY, log);
|
|
122
|
+
if (!result.ok) {
|
|
123
|
+
process.stderr.write(`[seshat] Telemetry log failed: ${result.error}\n`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export async function insertJstfSnapshot(snapshot) {
|
|
127
|
+
const result = await supabaseRequest('POST', TABLE_SNAPSHOTS, snapshot);
|
|
128
|
+
if (!result.ok) {
|
|
129
|
+
process.stderr.write(`[seshat] JSTF snapshot failed: ${result.error}\n`);
|
|
130
|
+
}
|
|
131
|
+
return result.ok;
|
|
124
132
|
}
|
package/dist/tools/diff.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export declare function diffBundle(args: {
|
|
|
11
11
|
worktree_path: string;
|
|
12
12
|
project?: string;
|
|
13
13
|
include_unchanged?: boolean;
|
|
14
|
-
}): Promise<unknown>;
|
|
14
|
+
}, loader: ProjectLoader): Promise<unknown>;
|
|
15
15
|
export declare function conflictMatrix(args: {
|
|
16
16
|
tasks: Array<{
|
|
17
17
|
id: string;
|
|
@@ -20,4 +20,4 @@ export declare function conflictMatrix(args: {
|
|
|
20
20
|
expand_blast_radius?: boolean;
|
|
21
21
|
}>;
|
|
22
22
|
project?: string;
|
|
23
|
-
}): unknown;
|
|
23
|
+
}, loader: ProjectLoader): unknown;
|
package/dist/tools/diff.js
CHANGED
|
@@ -11,7 +11,7 @@ import fs from 'fs';
|
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import { bootstrap } from '../bootstrap.js';
|
|
13
13
|
import { computeBlastRadius } from '../graph.js';
|
|
14
|
-
import {
|
|
14
|
+
import { getGraph, validateProject, entityName, entityLayer, } from './index.js';
|
|
15
15
|
// ─── Entity Identity ─────────────────────────────────────────────
|
|
16
16
|
// Ported from api-v2/translator/seshat-pipeline/src/incremental/diff-engine.mjs
|
|
17
17
|
/**
|
|
@@ -145,11 +145,10 @@ function compareEntityFields(baseEntity, branchEntity) {
|
|
|
145
145
|
return changed;
|
|
146
146
|
}
|
|
147
147
|
// ─── Tool: diff_bundle ───────────────────────────────────────────
|
|
148
|
-
export async function diffBundle(args) {
|
|
149
|
-
const projErr = validateProject(args.project);
|
|
148
|
+
export async function diffBundle(args, loader) {
|
|
149
|
+
const projErr = validateProject(args.project, loader);
|
|
150
150
|
if (projErr)
|
|
151
151
|
return { error: projErr };
|
|
152
|
-
const loader = getLoader();
|
|
153
152
|
const baseEntities = loader.getEntities(args.project);
|
|
154
153
|
const baseManifest = loader.getManifest(args.project);
|
|
155
154
|
if (baseEntities.length === 0) {
|
|
@@ -366,11 +365,10 @@ function buildExecutionPlan(taskIds, tier3Edges) {
|
|
|
366
365
|
};
|
|
367
366
|
}
|
|
368
367
|
// ─── Tool: conflict_matrix ────────────────────────────────────────
|
|
369
|
-
export function conflictMatrix(args) {
|
|
370
|
-
const projErr = validateProject(args.project);
|
|
368
|
+
export function conflictMatrix(args, loader) {
|
|
369
|
+
const projErr = validateProject(args.project, loader);
|
|
371
370
|
if (projErr)
|
|
372
371
|
return { error: projErr };
|
|
373
|
-
const loader = getLoader();
|
|
374
372
|
const { tasks } = args;
|
|
375
373
|
if (!tasks || tasks.length < 2) {
|
|
376
374
|
return { error: 'At least 2 tasks are required to compute a conflict matrix.' };
|
|
@@ -402,7 +400,7 @@ export function conflictMatrix(args) {
|
|
|
402
400
|
}
|
|
403
401
|
// Expand blast radius if requested
|
|
404
402
|
if (task.expand_blast_radius && entityIds.size > 0) {
|
|
405
|
-
const g = getGraph(args.project);
|
|
403
|
+
const g = getGraph(args.project, loader);
|
|
406
404
|
const blastResult = computeBlastRadius(g, entityIds);
|
|
407
405
|
for (const affectedId of blastResult.affected) {
|
|
408
406
|
if (!entityIds.has(affectedId)) {
|
package/dist/tools/functors.d.ts
CHANGED
|
@@ -15,14 +15,14 @@ export declare function estimateTokens(e: JstfEntity): number;
|
|
|
15
15
|
export declare function findDeadCode(args: {
|
|
16
16
|
include_tests?: boolean;
|
|
17
17
|
project?: string;
|
|
18
|
-
}): unknown;
|
|
18
|
+
}, loader: ProjectLoader): unknown;
|
|
19
19
|
export declare function findLayerViolations(args?: {
|
|
20
20
|
project?: string;
|
|
21
21
|
}): unknown;
|
|
22
22
|
export declare function getCouplingMetrics(args: {
|
|
23
23
|
group_by?: 'module' | 'layer';
|
|
24
24
|
project?: string;
|
|
25
|
-
}): unknown;
|
|
25
|
+
}, loader: ProjectLoader): unknown;
|
|
26
26
|
export declare function getAuthMatrix(args?: {
|
|
27
27
|
project?: string;
|
|
28
28
|
}): unknown;
|
|
@@ -32,13 +32,13 @@ export declare function findErrorGaps(args?: {
|
|
|
32
32
|
export declare function getTestCoverage(args: {
|
|
33
33
|
weight_by_blast_radius?: boolean;
|
|
34
34
|
project?: string;
|
|
35
|
-
}): unknown;
|
|
35
|
+
}, loader: ProjectLoader): unknown;
|
|
36
36
|
export declare function getOptimalContext(args: {
|
|
37
37
|
target_entity: string;
|
|
38
38
|
max_tokens?: number;
|
|
39
39
|
strategy?: 'bfs' | 'blast_radius';
|
|
40
40
|
project?: string;
|
|
41
|
-
}): unknown;
|
|
41
|
+
}, loader: ProjectLoader): unknown;
|
|
42
42
|
/**
|
|
43
43
|
* Estimate token cost of a code change BEFORE starting work.
|
|
44
44
|
* Computes blast radius, sums source token counts, and projects total burn.
|
|
@@ -48,7 +48,7 @@ export declare function estimateTaskCost(args: {
|
|
|
48
48
|
target_entities: string[];
|
|
49
49
|
context_budget?: number;
|
|
50
50
|
project?: string;
|
|
51
|
-
}): Promise<unknown>;
|
|
51
|
+
}, loader: ProjectLoader): Promise<unknown>;
|
|
52
52
|
/**
|
|
53
53
|
* Close the calibration feedback loop by reporting actual token usage
|
|
54
54
|
* against a prior prediction from estimate_task_cost.
|
|
@@ -65,7 +65,7 @@ export declare function reportActualBurn(args: {
|
|
|
65
65
|
action?: 'complete' | 'abandon' | 'list';
|
|
66
66
|
project?: string;
|
|
67
67
|
notes?: string;
|
|
68
|
-
}): Promise<unknown>;
|
|
68
|
+
}, loader: ProjectLoader): Promise<unknown>;
|
|
69
69
|
export declare function find_runtime_violations(args?: {
|
|
70
70
|
project?: string;
|
|
71
71
|
}): unknown;
|
|
@@ -75,7 +75,7 @@ export declare function find_ownership_violations(args?: {
|
|
|
75
75
|
export declare function query_traits(args: {
|
|
76
76
|
trait: string;
|
|
77
77
|
project?: string;
|
|
78
|
-
}): unknown;
|
|
78
|
+
}, loader: ProjectLoader): unknown;
|
|
79
79
|
export declare function simulate_mutation(args: {
|
|
80
80
|
entity_id: string;
|
|
81
81
|
mutation: {
|
|
@@ -86,11 +86,11 @@ export declare function simulate_mutation(args: {
|
|
|
86
86
|
};
|
|
87
87
|
};
|
|
88
88
|
project?: string;
|
|
89
|
-
}): unknown;
|
|
89
|
+
}, loader: ProjectLoader): unknown;
|
|
90
90
|
export declare function query_data_targets(args: {
|
|
91
91
|
target_name: string;
|
|
92
92
|
project?: string;
|
|
93
|
-
}): unknown;
|
|
93
|
+
}, loader: ProjectLoader): unknown;
|
|
94
94
|
export declare function find_exposure_leaks(args?: {
|
|
95
95
|
project?: string;
|
|
96
96
|
}): unknown;
|
|
@@ -104,4 +104,4 @@ export declare function create_symbol(args: {
|
|
|
104
104
|
layer?: string;
|
|
105
105
|
project?: string;
|
|
106
106
|
description?: string;
|
|
107
|
-
}): unknown;
|
|
107
|
+
}, loader: ProjectLoader): unknown;
|
package/dist/tools/functors.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { computeBlastRadius } from '../graph.js';
|
|
10
|
-
import {
|
|
11
|
-
import { isSupabaseConfigured, insertPrediction, updateActualBurn, abandonPrediction,
|
|
10
|
+
import { getGraph, validateProject, entityName, entityLayer, entitySummary, } from './index.js';
|
|
11
|
+
import { isSupabaseConfigured, insertPrediction, updateActualBurn, abandonPrediction, } from '../supabase.js';
|
|
12
12
|
// ─── Layer ordering for violation detection ──────────────────────
|
|
13
13
|
const LAYER_ORDER = {
|
|
14
14
|
route: 0,
|
|
@@ -82,13 +82,12 @@ function dominantEstimator(entities) {
|
|
|
82
82
|
return 'heuristic';
|
|
83
83
|
}
|
|
84
84
|
// ─── Tool: find_dead_code ────────────────────────────────────────
|
|
85
|
-
export function findDeadCode(args) {
|
|
86
|
-
const projErr = validateProject(args.project);
|
|
85
|
+
export function findDeadCode(args, loader) {
|
|
86
|
+
const projErr = validateProject(args.project, loader);
|
|
87
87
|
if (projErr)
|
|
88
88
|
return { error: projErr };
|
|
89
89
|
const { include_tests = false } = args;
|
|
90
|
-
const
|
|
91
|
-
const g = getGraph(args.project);
|
|
90
|
+
const g = getGraph(args.project, loader);
|
|
92
91
|
const entities = loader.getEntities(args.project);
|
|
93
92
|
// Entry points: routes, exported functions, test files, plugin registrations
|
|
94
93
|
const entryPointIds = new Set();
|
|
@@ -132,9 +131,9 @@ export function findDeadCode(args) {
|
|
|
132
131
|
}
|
|
133
132
|
}
|
|
134
133
|
// Unreachable = dead code candidates
|
|
135
|
-
let deadEntities = entities.filter(e => e.id && !reachable.has(e.id));
|
|
134
|
+
let deadEntities = entities.filter((e) => e.id && !reachable.has(e.id));
|
|
136
135
|
if (!include_tests) {
|
|
137
|
-
deadEntities = deadEntities.filter(e => entityLayer(e) !== 'test');
|
|
136
|
+
deadEntities = deadEntities.filter((e) => entityLayer(e) !== 'test');
|
|
138
137
|
}
|
|
139
138
|
// Group by layer for overview
|
|
140
139
|
const byLayer = new Map();
|
|
@@ -153,10 +152,10 @@ export function findDeadCode(args) {
|
|
|
153
152
|
}
|
|
154
153
|
// ─── Tool: find_layer_violations ─────────────────────────────────
|
|
155
154
|
export function findLayerViolations(args) {
|
|
156
|
-
const projErr = validateProject(args?.project);
|
|
155
|
+
const projErr = validateProject(args?.project, loader);
|
|
157
156
|
if (projErr)
|
|
158
157
|
return { error: projErr };
|
|
159
|
-
const g = getGraph(args?.project);
|
|
158
|
+
const g = getGraph(args?.project, loader);
|
|
160
159
|
const violations = [];
|
|
161
160
|
for (const [callerId, calleeIds] of g.callees) {
|
|
162
161
|
const callerEntity = g.entityById.get(callerId);
|
|
@@ -208,13 +207,12 @@ export function findLayerViolations(args) {
|
|
|
208
207
|
};
|
|
209
208
|
}
|
|
210
209
|
// ─── Tool: get_coupling_metrics ──────────────────────────────────
|
|
211
|
-
export function getCouplingMetrics(args) {
|
|
212
|
-
const projErr = validateProject(args.project);
|
|
210
|
+
export function getCouplingMetrics(args, loader) {
|
|
211
|
+
const projErr = validateProject(args.project, loader);
|
|
213
212
|
if (projErr)
|
|
214
213
|
return { error: projErr };
|
|
215
214
|
const { group_by = 'module' } = args;
|
|
216
|
-
const
|
|
217
|
-
const g = getGraph(args.project);
|
|
215
|
+
const g = getGraph(args.project, loader);
|
|
218
216
|
const entities = loader.getEntities(args.project);
|
|
219
217
|
// Group entities
|
|
220
218
|
const groups = new Map();
|
|
@@ -281,12 +279,11 @@ export function getCouplingMetrics(args) {
|
|
|
281
279
|
}
|
|
282
280
|
// ─── Tool: get_auth_matrix ───────────────────────────────────────
|
|
283
281
|
export function getAuthMatrix(args) {
|
|
284
|
-
const projErr = validateProject(args?.project);
|
|
282
|
+
const projErr = validateProject(args?.project, loader);
|
|
285
283
|
if (projErr)
|
|
286
284
|
return { error: projErr };
|
|
287
|
-
const loader = getLoader();
|
|
288
285
|
const entities = loader.getEntities(args?.project);
|
|
289
|
-
const apiEntities = entities.filter(e => {
|
|
286
|
+
const apiEntities = entities.filter((e) => {
|
|
290
287
|
const layer = entityLayer(e);
|
|
291
288
|
return layer === 'route' || layer === 'controller' ||
|
|
292
289
|
e.context?.exposure === 'api';
|
|
@@ -339,11 +336,10 @@ export function getAuthMatrix(args) {
|
|
|
339
336
|
}
|
|
340
337
|
// ─── Tool: find_error_gaps ───────────────────────────────────────
|
|
341
338
|
export function findErrorGaps(args) {
|
|
342
|
-
const projErr = validateProject(args?.project);
|
|
339
|
+
const projErr = validateProject(args?.project, loader);
|
|
343
340
|
if (projErr)
|
|
344
341
|
return { error: projErr };
|
|
345
|
-
const
|
|
346
|
-
const g = getGraph(args?.project);
|
|
342
|
+
const g = getGraph(args?.project, loader);
|
|
347
343
|
const entities = loader.getEntities(args?.project);
|
|
348
344
|
// Find all fallible entities (throws === true or has THROWS tag)
|
|
349
345
|
const fallibleIds = new Set();
|
|
@@ -405,13 +401,12 @@ export function findErrorGaps(args) {
|
|
|
405
401
|
};
|
|
406
402
|
}
|
|
407
403
|
// ─── Tool: get_test_coverage ─────────────────────────────────────
|
|
408
|
-
export function getTestCoverage(args) {
|
|
409
|
-
const projErr = validateProject(args.project);
|
|
404
|
+
export function getTestCoverage(args, loader) {
|
|
405
|
+
const projErr = validateProject(args.project, loader);
|
|
410
406
|
if (projErr)
|
|
411
407
|
return { error: projErr };
|
|
412
408
|
const { weight_by_blast_radius = false } = args;
|
|
413
|
-
const
|
|
414
|
-
const g = getGraph(args.project);
|
|
409
|
+
const g = getGraph(args.project, loader);
|
|
415
410
|
const entities = loader.getEntities(args.project);
|
|
416
411
|
// Partition into test and non-test entities
|
|
417
412
|
const testIds = new Set();
|
|
@@ -445,8 +440,8 @@ export function getTestCoverage(args) {
|
|
|
445
440
|
}
|
|
446
441
|
}
|
|
447
442
|
}
|
|
448
|
-
const covered = productionEntities.filter(e => exercised.has(e.id));
|
|
449
|
-
const uncovered = productionEntities.filter(e => !exercised.has(e.id));
|
|
443
|
+
const covered = productionEntities.filter((e) => exercised.has(e.id));
|
|
444
|
+
const uncovered = productionEntities.filter((e) => !exercised.has(e.id));
|
|
450
445
|
const coveragePercent = productionEntities.length > 0
|
|
451
446
|
? Math.round((covered.length / productionEntities.length) * 1000) / 10
|
|
452
447
|
: 0;
|
|
@@ -484,13 +479,12 @@ export function getTestCoverage(args) {
|
|
|
484
479
|
return result;
|
|
485
480
|
}
|
|
486
481
|
// ─── Tool: get_optimal_context ───────────────────────────────────
|
|
487
|
-
export function getOptimalContext(args) {
|
|
488
|
-
const projErr = validateProject(args.project);
|
|
482
|
+
export function getOptimalContext(args, loader) {
|
|
483
|
+
const projErr = validateProject(args.project, loader);
|
|
489
484
|
if (projErr)
|
|
490
485
|
return { error: projErr };
|
|
491
486
|
const { target_entity, max_tokens = 8000, strategy = 'bfs' } = args;
|
|
492
|
-
const
|
|
493
|
-
const g = getGraph(args.project);
|
|
487
|
+
const g = getGraph(args.project, loader);
|
|
494
488
|
const entity = loader.getEntityById(target_entity, args.project) || loader.getEntityByName(target_entity, args.project);
|
|
495
489
|
if (!entity) {
|
|
496
490
|
return { error: `Entity not found: ${target_entity}` };
|
|
@@ -605,13 +599,12 @@ export function getOptimalContext(args) {
|
|
|
605
599
|
* Computes blast radius, sums source token counts, and projects total burn.
|
|
606
600
|
* Logs prediction to Supabase when configured (for calibration feedback loop).
|
|
607
601
|
*/
|
|
608
|
-
export async function estimateTaskCost(args) {
|
|
609
|
-
const projErr = validateProject(args.project);
|
|
602
|
+
export async function estimateTaskCost(args, loader) {
|
|
603
|
+
const projErr = validateProject(args.project, loader);
|
|
610
604
|
if (projErr)
|
|
611
605
|
return { error: projErr };
|
|
612
606
|
const { target_entities, context_budget = 200000 } = args;
|
|
613
|
-
const
|
|
614
|
-
const g = getGraph(args.project);
|
|
607
|
+
const g = getGraph(args.project, loader);
|
|
615
608
|
// Resolve target entities
|
|
616
609
|
const resolvedTargets = [];
|
|
617
610
|
const unresolvedNames = [];
|
|
@@ -674,7 +667,7 @@ export async function estimateTaskCost(args) {
|
|
|
674
667
|
let outgoing = 0;
|
|
675
668
|
let incoming = 0;
|
|
676
669
|
const entities = loader.getEntities(args.project);
|
|
677
|
-
const modIds = new Set(entities.filter(e => e.context?.module === mod).map(e => e.id));
|
|
670
|
+
const modIds = new Set(entities.filter((e) => e.context?.module === mod).map(e => e.id));
|
|
678
671
|
for (const id of modIds) {
|
|
679
672
|
const calleeSet = g.callees.get(id);
|
|
680
673
|
if (calleeSet) {
|
|
@@ -772,7 +765,7 @@ export async function estimateTaskCost(args) {
|
|
|
772
765
|
* Can also abandon a prediction (task was cancelled/not completed),
|
|
773
766
|
* or list recent predictions for calibration analysis.
|
|
774
767
|
*/
|
|
775
|
-
export async function reportActualBurn(args) {
|
|
768
|
+
export async function reportActualBurn(args, loader) {
|
|
776
769
|
if (!isSupabaseConfigured()) {
|
|
777
770
|
return {
|
|
778
771
|
error: 'Supabase not configured. Set SESHAT_SUPABASE_URL and SESHAT_SUPABASE_KEY env vars.',
|
|
@@ -782,48 +775,9 @@ export async function reportActualBurn(args) {
|
|
|
782
775
|
const { action = 'complete' } = args;
|
|
783
776
|
// List mode: show recent predictions for calibration analysis
|
|
784
777
|
if (action === 'list') {
|
|
785
|
-
const rows = await listPredictions(args.project);
|
|
786
|
-
if (rows.length === 0) {
|
|
787
|
-
return { message: 'No predictions found.', predictions: [] };
|
|
788
|
-
}
|
|
789
|
-
const summary = rows.map((r) => ({
|
|
790
|
-
id: r.id,
|
|
791
|
-
project: r.project,
|
|
792
|
-
targets: r.target_entities,
|
|
793
|
-
predicted: r.predicted_total,
|
|
794
|
-
actual: r.actual_total_tokens,
|
|
795
|
-
drift: r.drift_ratio,
|
|
796
|
-
estimator: r.estimator_used,
|
|
797
|
-
status: r.status,
|
|
798
|
-
createdAt: r.created_at,
|
|
799
|
-
}));
|
|
800
|
-
// Compute aggregate calibration stats for completed predictions
|
|
801
|
-
const completed = rows.filter((r) => r.status === 'completed' && r.drift_ratio != null);
|
|
802
|
-
let calibration;
|
|
803
|
-
if (completed.length >= 3) {
|
|
804
|
-
const drifts = completed.map((r) => r.drift_ratio);
|
|
805
|
-
const meanDrift = drifts.reduce((a, b) => a + b, 0) / drifts.length;
|
|
806
|
-
const sortedDrifts = [...drifts].sort((a, b) => a - b);
|
|
807
|
-
const medianDrift = sortedDrifts[Math.floor(sortedDrifts.length / 2)];
|
|
808
|
-
const maxOvershoot = Math.max(...drifts);
|
|
809
|
-
const maxUndershoot = Math.min(...drifts);
|
|
810
|
-
calibration = {
|
|
811
|
-
completedSamples: completed.length,
|
|
812
|
-
meanDrift: Math.round(meanDrift * 1000) / 1000,
|
|
813
|
-
medianDrift: Math.round(medianDrift * 1000) / 1000,
|
|
814
|
-
maxOvershoot: Math.round(maxOvershoot * 1000) / 1000,
|
|
815
|
-
maxUndershoot: Math.round(maxUndershoot * 1000) / 1000,
|
|
816
|
-
_interpretation: meanDrift > 0.2
|
|
817
|
-
? 'Predictions underestimate — consider increasing iteration multiplier.'
|
|
818
|
-
: meanDrift < -0.2
|
|
819
|
-
? 'Predictions overestimate — consider decreasing iteration multiplier.'
|
|
820
|
-
: 'Predictions are well-calibrated (within 20% mean drift).',
|
|
821
|
-
};
|
|
822
|
-
}
|
|
823
778
|
return {
|
|
824
|
-
|
|
825
|
-
predictions:
|
|
826
|
-
...(calibration ? { calibration } : {}),
|
|
779
|
+
message: 'List predictions has been migrated to the Ptah Dashboard.',
|
|
780
|
+
predictions: []
|
|
827
781
|
};
|
|
828
782
|
}
|
|
829
783
|
// Complete or abandon requires prediction_id
|
|
@@ -869,10 +823,10 @@ export async function reportActualBurn(args) {
|
|
|
869
823
|
}
|
|
870
824
|
// ─── Tool: find_runtime_violations (Runtime Context) ────────────
|
|
871
825
|
export function find_runtime_violations(args) {
|
|
872
|
-
const projErr = validateProject(args?.project);
|
|
826
|
+
const projErr = validateProject(args?.project, loader);
|
|
873
827
|
if (projErr)
|
|
874
828
|
return { error: projErr };
|
|
875
|
-
const g = getGraph(args?.project);
|
|
829
|
+
const g = getGraph(args?.project, loader);
|
|
876
830
|
const violations = [];
|
|
877
831
|
for (const [callerId, calleeIds] of g.callees) {
|
|
878
832
|
const callerEntity = g.entityById.get(callerId);
|
|
@@ -910,10 +864,9 @@ export function find_runtime_violations(args) {
|
|
|
910
864
|
}
|
|
911
865
|
// ─── Tool: find_ownership_violations (Ownership & Lifetimes) ─────
|
|
912
866
|
export function find_ownership_violations(args) {
|
|
913
|
-
const projErr = validateProject(args?.project);
|
|
867
|
+
const projErr = validateProject(args?.project, loader);
|
|
914
868
|
if (projErr)
|
|
915
869
|
return { error: projErr };
|
|
916
|
-
const loader = getLoader();
|
|
917
870
|
const entities = loader.getEntities(args?.project);
|
|
918
871
|
const violations = [];
|
|
919
872
|
for (const e of entities) {
|
|
@@ -950,19 +903,18 @@ export function find_ownership_violations(args) {
|
|
|
950
903
|
};
|
|
951
904
|
}
|
|
952
905
|
// ─── Tool: query_traits (Trait Search) ───────────────────────────
|
|
953
|
-
export function query_traits(args) {
|
|
954
|
-
const projErr = validateProject(args.project);
|
|
906
|
+
export function query_traits(args, loader) {
|
|
907
|
+
const projErr = validateProject(args.project, loader);
|
|
955
908
|
if (projErr)
|
|
956
909
|
return { error: projErr };
|
|
957
910
|
const target = args.trait.toLowerCase();
|
|
958
|
-
const loader = getLoader();
|
|
959
911
|
const entities = loader.getEntities(args.project);
|
|
960
|
-
const results = entities.filter(e => {
|
|
912
|
+
const results = entities.filter((e) => {
|
|
961
913
|
if (!e.traits)
|
|
962
914
|
return false;
|
|
963
915
|
// Handle array of strings [ 'asyncContext', 'generator' ]
|
|
964
916
|
if (Array.isArray(e.traits)) {
|
|
965
|
-
return e.traits.some(t => t.toLowerCase().includes(target));
|
|
917
|
+
return e.traits.some((t) => t.toLowerCase().includes(target));
|
|
966
918
|
}
|
|
967
919
|
// Handle structured traits { self: { fallible: true } }
|
|
968
920
|
if (typeof e.traits === 'object') {
|
|
@@ -994,12 +946,11 @@ export function query_traits(args) {
|
|
|
994
946
|
};
|
|
995
947
|
}
|
|
996
948
|
// ─── Tool: simulate_mutation (Impact Simulator) ──────────────────
|
|
997
|
-
export function simulate_mutation(args) {
|
|
998
|
-
const projErr = validateProject(args.project);
|
|
949
|
+
export function simulate_mutation(args, loader) {
|
|
950
|
+
const projErr = validateProject(args.project, loader);
|
|
999
951
|
if (projErr)
|
|
1000
952
|
return { error: projErr };
|
|
1001
|
-
const
|
|
1002
|
-
const g = getGraph(args.project);
|
|
953
|
+
const g = getGraph(args.project, loader);
|
|
1003
954
|
const targetEntity = loader.getEntityById(args.entity_id, args.project)
|
|
1004
955
|
|| loader.getEntityByName(args.entity_id, args.project);
|
|
1005
956
|
if (!targetEntity) {
|
|
@@ -1075,11 +1026,10 @@ export function simulate_mutation(args) {
|
|
|
1075
1026
|
};
|
|
1076
1027
|
}
|
|
1077
1028
|
// ─── Tool: query_data_targets (Data Flow Analysis) ──────────────
|
|
1078
|
-
export function query_data_targets(args) {
|
|
1079
|
-
const projErr = validateProject(args.project);
|
|
1029
|
+
export function query_data_targets(args, loader) {
|
|
1030
|
+
const projErr = validateProject(args.project, loader);
|
|
1080
1031
|
if (projErr)
|
|
1081
1032
|
return { error: projErr };
|
|
1082
|
-
const loader = getLoader();
|
|
1083
1033
|
const entities = loader.getEntities(args.project);
|
|
1084
1034
|
const target = args.target_name.toLowerCase();
|
|
1085
1035
|
const readers = [];
|
|
@@ -1090,24 +1040,24 @@ export function query_data_targets(args) {
|
|
|
1090
1040
|
let isReader = false;
|
|
1091
1041
|
let isWriter = false;
|
|
1092
1042
|
// Check tables
|
|
1093
|
-
if (Array.isArray(e.data.tables) && e.data.tables.some(t => String(t).toLowerCase() === target)) {
|
|
1043
|
+
if (Array.isArray(e.data.tables) && e.data.tables.some((t) => String(t).toLowerCase() === target)) {
|
|
1094
1044
|
// By default, if they touch a table but don't specify mutation, we assume read
|
|
1095
1045
|
isReader = true;
|
|
1096
1046
|
}
|
|
1097
1047
|
// Check inputs (sources)
|
|
1098
1048
|
if (Array.isArray(e.data.inputs)) {
|
|
1099
|
-
if (e.data.inputs.some(i => i && typeof i === 'object' && String(i.name || i.source).toLowerCase() === target)) {
|
|
1049
|
+
if (e.data.inputs.some((i) => i && typeof i === 'object' && String(i.name || i.source).toLowerCase() === target)) {
|
|
1100
1050
|
isReader = true;
|
|
1101
1051
|
}
|
|
1102
1052
|
}
|
|
1103
1053
|
else if (Array.isArray(e.data.sources)) {
|
|
1104
|
-
if (e.data.sources.some(s => String(s).toLowerCase() === target)) {
|
|
1054
|
+
if (e.data.sources.some((s) => String(s).toLowerCase() === target)) {
|
|
1105
1055
|
isReader = true;
|
|
1106
1056
|
}
|
|
1107
1057
|
}
|
|
1108
1058
|
// Check mutations
|
|
1109
1059
|
if (Array.isArray(e.data.mutations)) {
|
|
1110
|
-
if (e.data.mutations.some(m => m && typeof m === 'object' && String(m.target).toLowerCase() === target)) {
|
|
1060
|
+
if (e.data.mutations.some((m) => m && typeof m === 'object' && String(m.target).toLowerCase() === target)) {
|
|
1111
1061
|
isWriter = true;
|
|
1112
1062
|
}
|
|
1113
1063
|
}
|
|
@@ -1130,10 +1080,10 @@ export function query_data_targets(args) {
|
|
|
1130
1080
|
}
|
|
1131
1081
|
// ─── Tool: find_exposure_leaks (Architectural Visibility) ───────
|
|
1132
1082
|
export function find_exposure_leaks(args) {
|
|
1133
|
-
const projErr = validateProject(args?.project);
|
|
1083
|
+
const projErr = validateProject(args?.project, loader);
|
|
1134
1084
|
if (projErr)
|
|
1135
1085
|
return { error: projErr };
|
|
1136
|
-
const g = getGraph(args?.project);
|
|
1086
|
+
const g = getGraph(args?.project, loader);
|
|
1137
1087
|
const leaks = [];
|
|
1138
1088
|
for (const [callerId, calleeIds] of g.callees) {
|
|
1139
1089
|
const callerEntity = g.entityById.get(callerId);
|
|
@@ -1165,10 +1115,9 @@ export function find_exposure_leaks(args) {
|
|
|
1165
1115
|
// ─── Tool: find_semantic_clones (Logic Analysis) ────────────────
|
|
1166
1116
|
import { createHash } from 'crypto';
|
|
1167
1117
|
export function find_semantic_clones(args) {
|
|
1168
|
-
const projErr = validateProject(args?.project);
|
|
1118
|
+
const projErr = validateProject(args?.project, loader);
|
|
1169
1119
|
if (projErr)
|
|
1170
1120
|
return { error: projErr };
|
|
1171
|
-
const loader = getLoader();
|
|
1172
1121
|
const entities = loader.getEntities(args?.project);
|
|
1173
1122
|
const { min_complexity = 5 } = args || {};
|
|
1174
1123
|
const logicHashes = new Map();
|
|
@@ -1212,11 +1161,10 @@ export function find_semantic_clones(args) {
|
|
|
1212
1161
|
};
|
|
1213
1162
|
}
|
|
1214
1163
|
// ─── Tool: create_symbol (Virtual Entry) ─────────────────────────
|
|
1215
|
-
export function create_symbol(args) {
|
|
1216
|
-
const projErr = validateProject(args.project);
|
|
1164
|
+
export function create_symbol(args, loader) {
|
|
1165
|
+
const projErr = validateProject(args.project, loader);
|
|
1217
1166
|
if (projErr)
|
|
1218
1167
|
return { error: projErr };
|
|
1219
|
-
const loader = getLoader();
|
|
1220
1168
|
// Create a skeleton JSTF-T entity
|
|
1221
1169
|
const virtualEntity = {
|
|
1222
1170
|
id: args.id,
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -8,17 +8,14 @@
|
|
|
8
8
|
* `project` parameter. In multi-project mode, `project` is required.
|
|
9
9
|
*/
|
|
10
10
|
import type { JstfEntity } from '../types.js';
|
|
11
|
-
import { MultiLoader } from '../loader.js';
|
|
12
11
|
import { type CallGraph } from '../graph.js';
|
|
13
|
-
export declare function
|
|
14
|
-
export declare function getLoader(): MultiLoader;
|
|
15
|
-
export declare function getGraph(project?: string): CallGraph;
|
|
12
|
+
export declare function getGraph(project: string | undefined, loader: ProjectLoader): CallGraph;
|
|
16
13
|
/**
|
|
17
14
|
* Validate project param. Returns error string if invalid, null if OK.
|
|
18
15
|
* In single-project mode, project is optional (defaults to the only project).
|
|
19
16
|
* In multi-project mode, project is required.
|
|
20
17
|
*/
|
|
21
|
-
export declare function validateProject(project
|
|
18
|
+
export declare function validateProject(project: string | undefined, loader: ProjectLoader): string | null;
|
|
22
19
|
export declare function entityName(e: JstfEntity): string;
|
|
23
20
|
export declare function entityLayer(e: JstfEntity): string;
|
|
24
21
|
export declare function entitySummary(e: JstfEntity): Record<string, unknown>;
|
|
@@ -35,34 +32,34 @@ export declare function queryEntities(args: {
|
|
|
35
32
|
module?: string;
|
|
36
33
|
language?: string;
|
|
37
34
|
limit?: number;
|
|
38
|
-
}): unknown
|
|
35
|
+
}, loader: ProjectLoader): Promise<unknown>;
|
|
39
36
|
export declare function getEntity(args: {
|
|
40
37
|
id: string;
|
|
41
38
|
project?: string;
|
|
42
|
-
}): unknown;
|
|
39
|
+
}, loader: ProjectLoader): unknown;
|
|
43
40
|
export declare function getDependencies(args: {
|
|
44
41
|
entity_id: string;
|
|
45
42
|
direction?: 'callers' | 'callees' | 'both';
|
|
46
43
|
depth?: number;
|
|
47
44
|
project?: string;
|
|
48
|
-
}): unknown;
|
|
45
|
+
}, loader: ProjectLoader): unknown;
|
|
49
46
|
export declare function collectTransitive(adjacency: Map<string, Set<string>>, startId: string, maxDepth: number): string[];
|
|
50
47
|
export declare function getDataFlow(args: {
|
|
51
48
|
entity_id: string;
|
|
52
49
|
project?: string;
|
|
53
|
-
}): unknown;
|
|
50
|
+
}, loader: ProjectLoader): unknown;
|
|
54
51
|
export declare function findByConstraint(args: {
|
|
55
52
|
constraint: string;
|
|
56
53
|
project?: string;
|
|
57
|
-
}): unknown;
|
|
54
|
+
}, loader: ProjectLoader): unknown;
|
|
58
55
|
export declare function getBlastRadius(args: {
|
|
59
56
|
entity_ids: string[];
|
|
60
57
|
project?: string;
|
|
61
|
-
}): unknown;
|
|
58
|
+
}, loader: ProjectLoader): unknown;
|
|
62
59
|
export declare function listModules(args: {
|
|
63
60
|
group_by?: 'layer' | 'module' | 'file' | 'language';
|
|
64
61
|
project?: string;
|
|
65
|
-
}): unknown;
|
|
66
|
-
export declare function getTopology(args
|
|
62
|
+
}, loader: ProjectLoader): unknown;
|
|
63
|
+
export declare function getTopology(args: {
|
|
67
64
|
project?: string;
|
|
68
|
-
}): unknown;
|
|
65
|
+
} | undefined, loader: ProjectLoader): unknown;
|
package/dist/tools/index.js
CHANGED
|
@@ -8,29 +8,15 @@
|
|
|
8
8
|
* `project` parameter. In multi-project mode, `project` is required.
|
|
9
9
|
*/
|
|
10
10
|
import { buildCallGraph, computeBlastRadius } from '../graph.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
let graphCache = new Map();
|
|
14
|
-
export function initTools(multiLoader) {
|
|
15
|
-
loader = multiLoader;
|
|
16
|
-
graphCache = new Map();
|
|
17
|
-
}
|
|
18
|
-
export function getLoader() {
|
|
19
|
-
return loader;
|
|
20
|
-
}
|
|
21
|
-
export function getGraph(project) {
|
|
22
|
-
const key = project || '__default__';
|
|
23
|
-
if (!graphCache.has(key)) {
|
|
24
|
-
graphCache.set(key, buildCallGraph(loader.getEntities(project)));
|
|
25
|
-
}
|
|
26
|
-
return graphCache.get(key);
|
|
11
|
+
export function getGraph(project, loader) {
|
|
12
|
+
return buildCallGraph(loader.getEntities(project));
|
|
27
13
|
}
|
|
28
14
|
/**
|
|
29
15
|
* Validate project param. Returns error string if invalid, null if OK.
|
|
30
16
|
* In single-project mode, project is optional (defaults to the only project).
|
|
31
17
|
* In multi-project mode, project is required.
|
|
32
18
|
*/
|
|
33
|
-
export function validateProject(project) {
|
|
19
|
+
export function validateProject(project, loader) {
|
|
34
20
|
if (!loader.isMultiProject())
|
|
35
21
|
return null; // single project, always OK
|
|
36
22
|
if (!project) {
|
|
@@ -155,7 +141,7 @@ export function normalizeConstraints(constraints) {
|
|
|
155
141
|
export function constraintMatches(constraints, target) {
|
|
156
142
|
// First check normalized tags
|
|
157
143
|
const tags = normalizeConstraints(constraints);
|
|
158
|
-
if (tags.some(t => t.toUpperCase().includes(target.toUpperCase())))
|
|
144
|
+
if (tags.some((t) => t.toUpperCase().includes(target.toUpperCase())))
|
|
159
145
|
return true;
|
|
160
146
|
// Also search raw constraint values for substring match
|
|
161
147
|
if (constraints && typeof constraints === 'object' && !Array.isArray(constraints)) {
|
|
@@ -166,42 +152,40 @@ export function constraintMatches(constraints, target) {
|
|
|
166
152
|
return false;
|
|
167
153
|
}
|
|
168
154
|
// ─── Tool: query_entities ─────────────────────────────────────────
|
|
169
|
-
export function queryEntities(args) {
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
return { error:
|
|
173
|
-
const { query, layer, module, language, limit = 50 } = args;
|
|
174
|
-
let results = loader.getEntities(args.project);
|
|
175
|
-
if (layer) {
|
|
176
|
-
results = results.filter(e => entityLayer(e).toLowerCase() === layer.toLowerCase());
|
|
177
|
-
}
|
|
178
|
-
if (module) {
|
|
179
|
-
results = results.filter(e => e.context?.module?.toLowerCase().includes(module.toLowerCase()));
|
|
155
|
+
export async function queryEntities(args, loader) {
|
|
156
|
+
const apiKey = process.env.SESHAT_API_KEY;
|
|
157
|
+
if (!apiKey) {
|
|
158
|
+
return { error: 'SESHAT_API_KEY environment variable is missing. You must provide an API key to use Ptah Cloud tools.' };
|
|
180
159
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
160
|
+
// We are hardcoding the project_hash to 'seshat-fabric' for this local test,
|
|
161
|
+
// but in production the MCP would pass args.project or read from the manifest.
|
|
162
|
+
const project_hash = args.project || 'seshat-fabric';
|
|
163
|
+
try {
|
|
164
|
+
const res = await fetch('http://localhost:3001/api/mcp/execute', {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: {
|
|
167
|
+
'Content-Type': 'application/json',
|
|
168
|
+
'x-api-key': apiKey
|
|
169
|
+
},
|
|
170
|
+
body: JSON.stringify({
|
|
171
|
+
tool: 'query_entities',
|
|
172
|
+
project_hash: project_hash,
|
|
173
|
+
args
|
|
174
|
+
})
|
|
192
175
|
});
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
const errorText = await res.text();
|
|
178
|
+
return { error: `Cloud API Error (${res.status}): ${errorText}` };
|
|
179
|
+
}
|
|
180
|
+
return await res.json();
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return { error: `Failed to connect to Ptah Cloud: ${error.message}` };
|
|
193
184
|
}
|
|
194
|
-
const total = results.length;
|
|
195
|
-
const entities = results.slice(0, limit).map(entitySummary);
|
|
196
|
-
return {
|
|
197
|
-
total,
|
|
198
|
-
showing: entities.length,
|
|
199
|
-
entities,
|
|
200
|
-
};
|
|
201
185
|
}
|
|
202
186
|
// ─── Tool: get_entity ─────────────────────────────────────────────
|
|
203
|
-
export function getEntity(args) {
|
|
204
|
-
const projErr = validateProject(args.project);
|
|
187
|
+
export function getEntity(args, loader) {
|
|
188
|
+
const projErr = validateProject(args.project, loader);
|
|
205
189
|
if (projErr)
|
|
206
190
|
return { error: projErr };
|
|
207
191
|
const entity = loader.getEntityById(args.id, args.project) || loader.getEntityByName(args.id, args.project);
|
|
@@ -226,12 +210,12 @@ export function getEntity(args) {
|
|
|
226
210
|
};
|
|
227
211
|
}
|
|
228
212
|
// ─── Tool: get_dependencies ───────────────────────────────────────
|
|
229
|
-
export function getDependencies(args) {
|
|
230
|
-
const projErr = validateProject(args.project);
|
|
213
|
+
export function getDependencies(args, loader) {
|
|
214
|
+
const projErr = validateProject(args.project, loader);
|
|
231
215
|
if (projErr)
|
|
232
216
|
return { error: projErr };
|
|
233
217
|
const { entity_id, direction = 'both', depth = 2 } = args;
|
|
234
|
-
const g = getGraph(args.project);
|
|
218
|
+
const g = getGraph(args.project, loader);
|
|
235
219
|
const entity = loader.getEntityById(entity_id, args.project) || loader.getEntityByName(entity_id, args.project);
|
|
236
220
|
if (!entity) {
|
|
237
221
|
return { error: `Entity not found: ${entity_id}` };
|
|
@@ -282,8 +266,8 @@ export function collectTransitive(adjacency, startId, maxDepth) {
|
|
|
282
266
|
return [...visited];
|
|
283
267
|
}
|
|
284
268
|
// ─── Tool: get_data_flow ──────────────────────────────────────────
|
|
285
|
-
export function getDataFlow(args) {
|
|
286
|
-
const projErr = validateProject(args.project);
|
|
269
|
+
export function getDataFlow(args, loader) {
|
|
270
|
+
const projErr = validateProject(args.project, loader);
|
|
287
271
|
if (projErr)
|
|
288
272
|
return { error: projErr };
|
|
289
273
|
const entity = loader.getEntityById(args.entity_id, args.project) || loader.getEntityByName(args.entity_id, args.project);
|
|
@@ -298,12 +282,12 @@ export function getDataFlow(args) {
|
|
|
298
282
|
};
|
|
299
283
|
}
|
|
300
284
|
// ─── Tool: find_by_constraint ─────────────────────────────────────
|
|
301
|
-
export function findByConstraint(args) {
|
|
302
|
-
const projErr = validateProject(args.project);
|
|
285
|
+
export function findByConstraint(args, loader) {
|
|
286
|
+
const projErr = validateProject(args.project, loader);
|
|
303
287
|
if (projErr)
|
|
304
288
|
return { error: projErr };
|
|
305
289
|
const target = args.constraint;
|
|
306
|
-
const results = loader.getEntities(args.project).filter(e => constraintMatches(e.constraints, target));
|
|
290
|
+
const results = loader.getEntities(args.project).filter((e) => constraintMatches(e.constraints, target));
|
|
307
291
|
return {
|
|
308
292
|
constraint: args.constraint,
|
|
309
293
|
total: results.length,
|
|
@@ -311,11 +295,11 @@ export function findByConstraint(args) {
|
|
|
311
295
|
};
|
|
312
296
|
}
|
|
313
297
|
// ─── Tool: get_blast_radius ───────────────────────────────────────
|
|
314
|
-
export function getBlastRadius(args) {
|
|
315
|
-
const projErr = validateProject(args.project);
|
|
298
|
+
export function getBlastRadius(args, loader) {
|
|
299
|
+
const projErr = validateProject(args.project, loader);
|
|
316
300
|
if (projErr)
|
|
317
301
|
return { error: projErr };
|
|
318
|
-
const g = getGraph(args.project);
|
|
302
|
+
const g = getGraph(args.project, loader);
|
|
319
303
|
// Resolve names to IDs
|
|
320
304
|
const resolvedIds = new Set();
|
|
321
305
|
const notFound = [];
|
|
@@ -348,8 +332,8 @@ export function getBlastRadius(args) {
|
|
|
348
332
|
};
|
|
349
333
|
}
|
|
350
334
|
// ─── Tool: list_modules ───────────────────────────────────────────
|
|
351
|
-
export function listModules(args) {
|
|
352
|
-
const projErr = validateProject(args.project);
|
|
335
|
+
export function listModules(args, loader) {
|
|
336
|
+
const projErr = validateProject(args.project, loader);
|
|
353
337
|
if (projErr)
|
|
354
338
|
return { error: projErr };
|
|
355
339
|
const { group_by = 'layer' } = args;
|
|
@@ -385,8 +369,8 @@ export function listModules(args) {
|
|
|
385
369
|
};
|
|
386
370
|
}
|
|
387
371
|
// ─── Tool: get_topology ───────────────────────────────────────────
|
|
388
|
-
export function getTopology(args) {
|
|
389
|
-
const projErr = validateProject(args?.project);
|
|
372
|
+
export function getTopology(args, loader) {
|
|
373
|
+
const projErr = validateProject(args?.project, loader);
|
|
390
374
|
if (projErr)
|
|
391
375
|
return { error: projErr };
|
|
392
376
|
const topology = loader.getTopology(args?.project);
|
package/dist/types.d.ts
CHANGED
|
@@ -158,3 +158,17 @@ export interface ProjectInfo {
|
|
|
158
158
|
extractedAt: string;
|
|
159
159
|
layers: Record<string, number>;
|
|
160
160
|
}
|
|
161
|
+
export interface ProjectLoader {
|
|
162
|
+
getEntities(project?: string): JstfEntity[];
|
|
163
|
+
getTopology(project?: string): Topology | null;
|
|
164
|
+
getManifest(project?: string): Manifest | null;
|
|
165
|
+
getEntityById(id: string, project?: string): JstfEntity | undefined;
|
|
166
|
+
getEntityByName(name: string, project?: string): JstfEntity | undefined;
|
|
167
|
+
getProjectNames(): string[];
|
|
168
|
+
getProjectInfo(): ProjectInfo[];
|
|
169
|
+
isMultiProject(): boolean;
|
|
170
|
+
isLoaded(): boolean;
|
|
171
|
+
hasProject(name: string): boolean;
|
|
172
|
+
registerVirtualEntity(entity: JstfEntity, project?: string): void;
|
|
173
|
+
totalEntities(): number;
|
|
174
|
+
}
|
package/package.json
CHANGED