@papyruslabsai/seshat-mcp 0.3.0 → 0.3.2
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 +24 -6
- package/dist/index.d.ts +2 -1
- package/dist/index.js +27 -3
- package/dist/loader.js +12 -0
- package/dist/tools/functors.d.ts +16 -0
- package/dist/tools/functors.js +171 -21
- package/dist/tools/index.js +6 -2
- package/package.json +1 -1
package/dist/graph.js
CHANGED
|
@@ -21,7 +21,10 @@ export function buildCallGraph(entities) {
|
|
|
21
21
|
callers.set(entity.id, new Set());
|
|
22
22
|
callees.set(entity.id, new Set());
|
|
23
23
|
if (entity.context?.module) {
|
|
24
|
-
|
|
24
|
+
const mod = entity.context.module;
|
|
25
|
+
if (!entityByModule.has(mod))
|
|
26
|
+
entityByModule.set(mod, []);
|
|
27
|
+
entityByModule.get(mod).push(entity);
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
// Build call edges from ε dimension
|
|
@@ -42,12 +45,27 @@ export function buildCallGraph(entities) {
|
|
|
42
45
|
}
|
|
43
46
|
// Strategy 2: Method match (target = "module.method")
|
|
44
47
|
else if (target.includes('.')) {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
const dotIdx = target.indexOf('.');
|
|
49
|
+
const modulePart = target.substring(0, dotIdx);
|
|
50
|
+
const methodPart = target.substring(dotIdx + 1);
|
|
51
|
+
// First try "module.method" as a full entity ID
|
|
52
|
+
if (entityById.has(target)) {
|
|
53
|
+
calleeId = target;
|
|
49
54
|
}
|
|
50
|
-
//
|
|
55
|
+
// Search within the module's entities for one named methodPart
|
|
56
|
+
if (!calleeId) {
|
|
57
|
+
const moduleEntities = entityByModule.get(modulePart);
|
|
58
|
+
if (moduleEntities) {
|
|
59
|
+
const match = moduleEntities.find(e => {
|
|
60
|
+
const name = typeof e.struct === 'string' ? e.struct : e.struct?.name;
|
|
61
|
+
return e.id === methodPart || name === methodPart;
|
|
62
|
+
});
|
|
63
|
+
if (match) {
|
|
64
|
+
calleeId = match.id;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Also try "module.method" concatenated as an ID
|
|
51
69
|
if (!calleeId && entityById.has(`${modulePart}.${methodPart}`)) {
|
|
52
70
|
calleeId = `${modulePart}.${methodPart}`;
|
|
53
71
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Single-project mode (default):
|
|
14
14
|
* When SESHAT_PROJECTS is not set, loads from CWD. No `project` param needed.
|
|
15
15
|
*
|
|
16
|
-
* Tools (8 core +
|
|
16
|
+
* Tools (8 core + 8 interpretation functors + 1 meta):
|
|
17
17
|
* list_projects — Show loaded projects with entity counts
|
|
18
18
|
* query_entities — Search entities by name, layer, module, language
|
|
19
19
|
* get_entity — Full 9D coordinate dump for one entity
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* find_error_gaps — Fallible callees whose callers lack try/catch
|
|
31
31
|
* get_test_coverage — Entities exercised by tests vs uncovered
|
|
32
32
|
* get_optimal_context — Greedy knapsack: max relevance per token for LLM context
|
|
33
|
+
* estimate_task_cost — Pre-work token burn projection from blast radius + source tokens
|
|
33
34
|
*
|
|
34
35
|
* Usage:
|
|
35
36
|
* npx @papyruslabs/seshat-mcp # single project (CWD)
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Single-project mode (default):
|
|
14
14
|
* When SESHAT_PROJECTS is not set, loads from CWD. No `project` param needed.
|
|
15
15
|
*
|
|
16
|
-
* Tools (8 core +
|
|
16
|
+
* Tools (8 core + 8 interpretation functors + 1 meta):
|
|
17
17
|
* list_projects — Show loaded projects with entity counts
|
|
18
18
|
* query_entities — Search entities by name, layer, module, language
|
|
19
19
|
* get_entity — Full 9D coordinate dump for one entity
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* find_error_gaps — Fallible callees whose callers lack try/catch
|
|
31
31
|
* get_test_coverage — Entities exercised by tests vs uncovered
|
|
32
32
|
* get_optimal_context — Greedy knapsack: max relevance per token for LLM context
|
|
33
|
+
* estimate_task_cost — Pre-work token burn projection from blast radius + source tokens
|
|
33
34
|
*
|
|
34
35
|
* Usage:
|
|
35
36
|
* npx @papyruslabs/seshat-mcp # single project (CWD)
|
|
@@ -43,7 +44,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
43
44
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
44
45
|
import { MultiLoader } from './loader.js';
|
|
45
46
|
import { initTools, queryEntities, getEntity, getDependencies, getDataFlow, findByConstraint, getBlastRadius, listModules, getTopology, } from './tools/index.js';
|
|
46
|
-
import { findDeadCode, findLayerViolations, getCouplingMetrics, getAuthMatrix, findErrorGaps, getTestCoverage, getOptimalContext, } from './tools/functors.js';
|
|
47
|
+
import { findDeadCode, findLayerViolations, getCouplingMetrics, getAuthMatrix, findErrorGaps, getTestCoverage, getOptimalContext, estimateTaskCost, } from './tools/functors.js';
|
|
47
48
|
// ─── Project Discovery ───────────────────────────────────────────
|
|
48
49
|
/**
|
|
49
50
|
* Discover project directories from SESHAT_PROJECTS env var.
|
|
@@ -337,6 +338,26 @@ const TOOLS = [
|
|
|
337
338
|
required: ['target_entity'],
|
|
338
339
|
},
|
|
339
340
|
},
|
|
341
|
+
{
|
|
342
|
+
name: 'estimate_task_cost',
|
|
343
|
+
description: 'Estimate token cost of a code change BEFORE starting work. Computes blast radius, sums source token counts across affected entities, and projects total token burn including iteration cycles. Call this before planning to check if a task fits within your token budget.',
|
|
344
|
+
inputSchema: {
|
|
345
|
+
type: 'object',
|
|
346
|
+
properties: {
|
|
347
|
+
project: projectParam,
|
|
348
|
+
target_entities: {
|
|
349
|
+
type: 'array',
|
|
350
|
+
items: { type: 'string' },
|
|
351
|
+
description: 'Entity IDs or names that will be modified',
|
|
352
|
+
},
|
|
353
|
+
context_budget: {
|
|
354
|
+
type: 'number',
|
|
355
|
+
description: 'LLM context window token budget (default: 200000)',
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
required: ['target_entities'],
|
|
359
|
+
},
|
|
360
|
+
},
|
|
340
361
|
];
|
|
341
362
|
// ─── Server Setup ─────────────────────────────────────────────────
|
|
342
363
|
async function main() {
|
|
@@ -367,7 +388,7 @@ async function main() {
|
|
|
367
388
|
}
|
|
368
389
|
const server = new Server({
|
|
369
390
|
name: serverLabel,
|
|
370
|
-
version: '0.3.
|
|
391
|
+
version: '0.3.2',
|
|
371
392
|
}, {
|
|
372
393
|
capabilities: {
|
|
373
394
|
tools: {},
|
|
@@ -451,6 +472,9 @@ async function main() {
|
|
|
451
472
|
case 'get_optimal_context':
|
|
452
473
|
result = getOptimalContext(args);
|
|
453
474
|
break;
|
|
475
|
+
case 'estimate_task_cost':
|
|
476
|
+
result = estimateTaskCost(args);
|
|
477
|
+
break;
|
|
454
478
|
default:
|
|
455
479
|
result = { error: `Unknown tool: ${name}` };
|
|
456
480
|
}
|
package/dist/loader.js
CHANGED
|
@@ -27,6 +27,18 @@ export class BundleLoader {
|
|
|
27
27
|
const raw = fs.readFileSync(bundlePath, 'utf-8');
|
|
28
28
|
const bundle = JSON.parse(raw);
|
|
29
29
|
this.entities = bundle.entities || [];
|
|
30
|
+
// Remap bundle field names to internal _ prefixed names.
|
|
31
|
+
// The extraction pipeline outputs `sourceFile`, `sourceLanguage`, `_jstfFilename`
|
|
32
|
+
// but our JstfEntity type expects `_sourceFile`, `_sourceLanguage`.
|
|
33
|
+
for (const e of this.entities) {
|
|
34
|
+
const raw = e;
|
|
35
|
+
if (raw.sourceFile && !e._sourceFile) {
|
|
36
|
+
e._sourceFile = raw.sourceFile;
|
|
37
|
+
}
|
|
38
|
+
if (raw.sourceLanguage && !e._sourceLanguage) {
|
|
39
|
+
e._sourceLanguage = raw.sourceLanguage;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
30
42
|
// Load topology if available
|
|
31
43
|
const topoPath = path.join(this.seshatDir, '_topology.json');
|
|
32
44
|
if (fs.existsSync(topoPath)) {
|
package/dist/tools/functors.d.ts
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* onto a domain-specific judgment. These are composite analyses built
|
|
6
6
|
* from the primitive dimensions (sigma, epsilon, delta, kappa, chi, tau, rho).
|
|
7
7
|
*/
|
|
8
|
+
import type { JstfEntity } from '../types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Estimate the token cost of loading an entity's source code into an LLM context.
|
|
11
|
+
* Uses real sourceTokens from the extraction pipeline when available (v0.3.2+),
|
|
12
|
+
* falls back to heuristic estimation from 9D coordinates for older bundles.
|
|
13
|
+
*/
|
|
14
|
+
export declare function estimateTokens(e: JstfEntity): number;
|
|
8
15
|
export declare function findDeadCode(args: {
|
|
9
16
|
include_tests?: boolean;
|
|
10
17
|
project?: string;
|
|
@@ -32,3 +39,12 @@ export declare function getOptimalContext(args: {
|
|
|
32
39
|
strategy?: 'bfs' | 'blast_radius';
|
|
33
40
|
project?: string;
|
|
34
41
|
}): unknown;
|
|
42
|
+
/**
|
|
43
|
+
* Estimate token cost of a code change BEFORE starting work.
|
|
44
|
+
* Computes blast radius, sums source token counts, and projects total burn.
|
|
45
|
+
*/
|
|
46
|
+
export declare function estimateTaskCost(args: {
|
|
47
|
+
target_entities: string[];
|
|
48
|
+
context_budget?: number;
|
|
49
|
+
project?: string;
|
|
50
|
+
}): unknown;
|
package/dist/tools/functors.js
CHANGED
|
@@ -20,6 +20,35 @@ const LAYER_ORDER = {
|
|
|
20
20
|
utility: 8,
|
|
21
21
|
component: 1, // UI components are peers to controllers
|
|
22
22
|
};
|
|
23
|
+
// ─── Shared: Token estimation ────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Estimate the token cost of loading an entity's source code into an LLM context.
|
|
26
|
+
* Uses real sourceTokens from the extraction pipeline when available (v0.3.2+),
|
|
27
|
+
* falls back to heuristic estimation from 9D coordinates for older bundles.
|
|
28
|
+
*/
|
|
29
|
+
export function estimateTokens(e) {
|
|
30
|
+
// Use real source token count from extraction pipeline
|
|
31
|
+
const raw = e;
|
|
32
|
+
const st = raw.sourceTokens;
|
|
33
|
+
if (st?.estimated)
|
|
34
|
+
return st.estimated;
|
|
35
|
+
// Fallback: heuristic from 9D coordinates (pre-v0.3.2 bundles)
|
|
36
|
+
let tokens = 50; // Base: name, id, layer
|
|
37
|
+
if (e.struct && typeof e.struct !== 'string') {
|
|
38
|
+
tokens += 20; // signature
|
|
39
|
+
tokens += (e.struct.params?.length || 0) * 10;
|
|
40
|
+
}
|
|
41
|
+
if (e.edges?.calls)
|
|
42
|
+
tokens += e.edges.calls.length * 8;
|
|
43
|
+
if (e.edges?.imports)
|
|
44
|
+
tokens += e.edges.imports.length * 6;
|
|
45
|
+
if (e.data?.inputs)
|
|
46
|
+
tokens += e.data.inputs.length * 10;
|
|
47
|
+
if (e.constraints && typeof e.constraints === 'object' && !Array.isArray(e.constraints)) {
|
|
48
|
+
tokens += 30;
|
|
49
|
+
}
|
|
50
|
+
return tokens;
|
|
51
|
+
}
|
|
23
52
|
// ─── Functor 1: find_dead_code ───────────────────────────────────
|
|
24
53
|
export function findDeadCode(args) {
|
|
25
54
|
const projErr = validateProject(args.project);
|
|
@@ -207,6 +236,7 @@ export function getCouplingMetrics(args) {
|
|
|
207
236
|
cohesion: Math.round(cohesion * 1000) / 1000,
|
|
208
237
|
coupling,
|
|
209
238
|
instability: Math.round(instability * 1000) / 1000,
|
|
239
|
+
_summary: `${groupName}: ${internalEdges} internal / ${totalExternal} external edges, cohesion ${Math.round(cohesion * 1000) / 1000} (${size} entities)`,
|
|
210
240
|
});
|
|
211
241
|
}
|
|
212
242
|
// Sort by coupling (most coupled first)
|
|
@@ -338,6 +368,7 @@ export function findErrorGaps(args) {
|
|
|
338
368
|
return {
|
|
339
369
|
totalFallible: fallibleIds.size,
|
|
340
370
|
errorGaps: gaps.length,
|
|
371
|
+
_summary: `${gaps.length} error handling gaps across ${fallibleIds.size} fallible entities`,
|
|
341
372
|
gaps: gaps.slice(0, 100),
|
|
342
373
|
};
|
|
343
374
|
}
|
|
@@ -384,14 +415,16 @@ export function getTestCoverage(args) {
|
|
|
384
415
|
}
|
|
385
416
|
const covered = productionEntities.filter(e => exercised.has(e.id));
|
|
386
417
|
const uncovered = productionEntities.filter(e => !exercised.has(e.id));
|
|
418
|
+
const coveragePercent = productionEntities.length > 0
|
|
419
|
+
? Math.round((covered.length / productionEntities.length) * 1000) / 10
|
|
420
|
+
: 0;
|
|
387
421
|
const result = {
|
|
388
422
|
totalProduction: productionEntities.length,
|
|
389
423
|
totalTests: testIds.size,
|
|
390
424
|
coveredCount: covered.length,
|
|
391
425
|
uncoveredCount: uncovered.length,
|
|
392
|
-
coveragePercent
|
|
393
|
-
|
|
394
|
-
: 0,
|
|
426
|
+
coveragePercent,
|
|
427
|
+
_summary: `${testIds.size} test entities exercise ${covered.length} of ${productionEntities.length} production entities (${coveragePercent}% coverage)`,
|
|
395
428
|
};
|
|
396
429
|
if (weight_by_blast_radius && uncovered.length > 0) {
|
|
397
430
|
// Compute blast radius for each uncovered entity to prioritize what to test
|
|
@@ -431,24 +464,6 @@ export function getOptimalContext(args) {
|
|
|
431
464
|
return { error: `Entity not found: ${target_entity}` };
|
|
432
465
|
}
|
|
433
466
|
const targetId = entity.id;
|
|
434
|
-
// Estimate tokens for an entity based on its dimensions
|
|
435
|
-
function estimateTokens(e) {
|
|
436
|
-
let tokens = 50; // Base: name, id, layer
|
|
437
|
-
if (e.struct && typeof e.struct !== 'string') {
|
|
438
|
-
tokens += 20; // signature
|
|
439
|
-
tokens += (e.struct.params?.length || 0) * 10;
|
|
440
|
-
}
|
|
441
|
-
if (e.edges?.calls)
|
|
442
|
-
tokens += e.edges.calls.length * 8;
|
|
443
|
-
if (e.edges?.imports)
|
|
444
|
-
tokens += e.edges.imports.length * 6;
|
|
445
|
-
if (e.data?.inputs)
|
|
446
|
-
tokens += e.data.inputs.length * 10;
|
|
447
|
-
if (e.constraints && typeof e.constraints === 'object' && !Array.isArray(e.constraints)) {
|
|
448
|
-
tokens += 30;
|
|
449
|
-
}
|
|
450
|
-
return tokens;
|
|
451
|
-
}
|
|
452
467
|
const candidates = [];
|
|
453
468
|
if (strategy === 'blast_radius') {
|
|
454
469
|
// Use blast radius to get all related entities with depth
|
|
@@ -552,3 +567,138 @@ export function getOptimalContext(args) {
|
|
|
552
567
|
context: selected,
|
|
553
568
|
};
|
|
554
569
|
}
|
|
570
|
+
// ─── Functor 8: estimate_task_cost ───────────────────────────────
|
|
571
|
+
/**
|
|
572
|
+
* Estimate token cost of a code change BEFORE starting work.
|
|
573
|
+
* Computes blast radius, sums source token counts, and projects total burn.
|
|
574
|
+
*/
|
|
575
|
+
export function estimateTaskCost(args) {
|
|
576
|
+
const projErr = validateProject(args.project);
|
|
577
|
+
if (projErr)
|
|
578
|
+
return { error: projErr };
|
|
579
|
+
const { target_entities, context_budget = 200000 } = args;
|
|
580
|
+
const loader = getLoader();
|
|
581
|
+
const g = getGraph(args.project);
|
|
582
|
+
// Resolve target entities
|
|
583
|
+
const resolvedTargets = [];
|
|
584
|
+
const unresolvedNames = [];
|
|
585
|
+
const changedIds = new Set();
|
|
586
|
+
for (const name of target_entities) {
|
|
587
|
+
const entity = loader.getEntityById(name, args.project)
|
|
588
|
+
|| loader.getEntityByName(name, args.project);
|
|
589
|
+
if (entity) {
|
|
590
|
+
resolvedTargets.push(entity);
|
|
591
|
+
changedIds.add(entity.id);
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
unresolvedNames.push(name);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (resolvedTargets.length === 0) {
|
|
598
|
+
return {
|
|
599
|
+
error: `No entities resolved. Unresolved: ${unresolvedNames.join(', ')}`,
|
|
600
|
+
hint: 'Use query_entities to find the correct entity IDs.',
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
// Compute blast radius across all targets
|
|
604
|
+
const br = computeBlastRadius(g, changedIds);
|
|
605
|
+
// Collect all affected entities (targets + blast radius)
|
|
606
|
+
const allAffectedIds = new Set([...changedIds, ...br.affected]);
|
|
607
|
+
const affectedEntities = [];
|
|
608
|
+
for (const id of allAffectedIds) {
|
|
609
|
+
const e = g.entityById.get(id);
|
|
610
|
+
if (e)
|
|
611
|
+
affectedEntities.push(e);
|
|
612
|
+
}
|
|
613
|
+
// Sum token estimates
|
|
614
|
+
let contextLoad = 0;
|
|
615
|
+
const fileTokens = new Map();
|
|
616
|
+
const layerSet = new Set();
|
|
617
|
+
for (const e of affectedEntities) {
|
|
618
|
+
const tokens = estimateTokens(e);
|
|
619
|
+
contextLoad += tokens;
|
|
620
|
+
const file = e._sourceFile || 'unknown';
|
|
621
|
+
if (!fileTokens.has(file))
|
|
622
|
+
fileTokens.set(file, { entities: 0, tokens: 0 });
|
|
623
|
+
const ft = fileTokens.get(file);
|
|
624
|
+
ft.entities++;
|
|
625
|
+
ft.tokens += tokens;
|
|
626
|
+
layerSet.add(entityLayer(e));
|
|
627
|
+
}
|
|
628
|
+
// Compute iteration multiplier
|
|
629
|
+
let iterationMultiplier = 1.5; // base: read + write pass
|
|
630
|
+
// +0.5 if cross-cutting (3+ layers)
|
|
631
|
+
if (layerSet.size >= 3)
|
|
632
|
+
iterationMultiplier += 0.5;
|
|
633
|
+
// +0.5 if any affected module has high instability
|
|
634
|
+
const affectedModules = new Set();
|
|
635
|
+
for (const e of affectedEntities) {
|
|
636
|
+
if (e.context?.module)
|
|
637
|
+
affectedModules.add(e.context.module);
|
|
638
|
+
}
|
|
639
|
+
// Quick instability check: compute outgoing / (outgoing + incoming) for affected modules
|
|
640
|
+
for (const mod of affectedModules) {
|
|
641
|
+
let outgoing = 0;
|
|
642
|
+
let incoming = 0;
|
|
643
|
+
const entities = loader.getEntities(args.project);
|
|
644
|
+
const modIds = new Set(entities.filter(e => e.context?.module === mod).map(e => e.id));
|
|
645
|
+
for (const id of modIds) {
|
|
646
|
+
const calleeSet = g.callees.get(id);
|
|
647
|
+
if (calleeSet) {
|
|
648
|
+
for (const cid of calleeSet) {
|
|
649
|
+
if (!modIds.has(cid))
|
|
650
|
+
outgoing++;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const callerSet = g.callers.get(id);
|
|
654
|
+
if (callerSet) {
|
|
655
|
+
for (const cid of callerSet) {
|
|
656
|
+
if (!modIds.has(cid))
|
|
657
|
+
incoming++;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const total = outgoing + incoming;
|
|
662
|
+
if (total > 0 && outgoing / total > 0.8) {
|
|
663
|
+
iterationMultiplier += 0.5;
|
|
664
|
+
break; // only add once
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// +0.5 if large blast radius
|
|
668
|
+
if (allAffectedIds.size > 50)
|
|
669
|
+
iterationMultiplier += 0.5;
|
|
670
|
+
// Cap at 4.0
|
|
671
|
+
iterationMultiplier = Math.min(4.0, iterationMultiplier);
|
|
672
|
+
const projectedTotal = Math.round(contextLoad * iterationMultiplier);
|
|
673
|
+
const fitsInSinglePass = contextLoad <= context_budget;
|
|
674
|
+
const passesRequired = fitsInSinglePass ? 1 : Math.ceil(contextLoad / context_budget);
|
|
675
|
+
// Build file breakdown sorted by token count
|
|
676
|
+
const breakdown = [...fileTokens.entries()]
|
|
677
|
+
.map(([file, data]) => ({ file, entities: data.entities, tokens: data.tokens }))
|
|
678
|
+
.sort((a, b) => b.tokens - a.tokens);
|
|
679
|
+
// Build target summaries with token estimates
|
|
680
|
+
const targetSummaries = resolvedTargets.map(e => ({
|
|
681
|
+
...entitySummary(e),
|
|
682
|
+
sourceTokens: estimateTokens(e),
|
|
683
|
+
}));
|
|
684
|
+
const affectedLayers = [...layerSet].sort();
|
|
685
|
+
return {
|
|
686
|
+
targets: targetSummaries,
|
|
687
|
+
...(unresolvedNames.length > 0 ? { unresolved: unresolvedNames } : {}),
|
|
688
|
+
affectedEntities: allAffectedIds.size,
|
|
689
|
+
affectedFiles: fileTokens.size,
|
|
690
|
+
affectedLayers,
|
|
691
|
+
tokenEstimate: {
|
|
692
|
+
contextLoad,
|
|
693
|
+
iterationMultiplier: Math.round(iterationMultiplier * 10) / 10,
|
|
694
|
+
projectedTotal,
|
|
695
|
+
},
|
|
696
|
+
feasibility: {
|
|
697
|
+
contextBudget: context_budget,
|
|
698
|
+
fitsInSinglePass,
|
|
699
|
+
passesRequired,
|
|
700
|
+
},
|
|
701
|
+
breakdown: breakdown.slice(0, 30),
|
|
702
|
+
_summary: `Changing ${target_entities.join(', ')} affects ${allAffectedIds.size} entities across ${fileTokens.size} files. Context load: ~${Math.round(contextLoad / 1000)}K tokens. Projected total with ${Math.round(iterationMultiplier * 10) / 10}x iteration: ~${Math.round(projectedTotal / 1000)}K tokens. ${fitsInSinglePass ? `Fits in ${Math.round(context_budget / 1000)}K budget (${passesRequired} pass).` : `Exceeds ${Math.round(context_budget / 1000)}K budget — needs ${passesRequired} passes.`}`,
|
|
703
|
+
};
|
|
704
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -74,7 +74,8 @@ export function entityLayer(e) {
|
|
|
74
74
|
return 'model';
|
|
75
75
|
if (src.includes('/schema') || explicit === 'schema')
|
|
76
76
|
return 'schema';
|
|
77
|
-
if (src.includes('/test') || src.includes('
|
|
77
|
+
if (src.includes('/test/') || src.includes('/tests/') || src.includes('/__tests__/') ||
|
|
78
|
+
src.includes('.test.') || src.includes('.spec.'))
|
|
78
79
|
return 'test';
|
|
79
80
|
return explicit || 'other';
|
|
80
81
|
}
|
|
@@ -91,7 +92,10 @@ export function entitySummary(e) {
|
|
|
91
92
|
async: typeof e.struct !== 'string' ? e.struct?.async : undefined,
|
|
92
93
|
exported: typeof e.struct !== 'string' ? e.struct?.exported : undefined,
|
|
93
94
|
constraints: constraintTags.length > 0 ? constraintTags : undefined,
|
|
94
|
-
|
|
95
|
+
callExpressions: Array.isArray(e.edges?.calls) ? e.edges.calls.length : 0,
|
|
96
|
+
uniqueCallees: Array.isArray(e.edges?.calls)
|
|
97
|
+
? new Set(e.edges.calls.map(c => c.target)).size
|
|
98
|
+
: 0,
|
|
95
99
|
};
|
|
96
100
|
}
|
|
97
101
|
export function normalizeConstraints(constraints) {
|