@git.zone/tsdoc 1.5.2 → 1.6.1
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_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/context/config-manager.d.ts +22 -1
- package/dist_ts/context/config-manager.js +113 -4
- package/dist_ts/context/context-analyzer.d.ts +73 -0
- package/dist_ts/context/context-analyzer.js +311 -0
- package/dist_ts/context/context-cache.d.ts +73 -0
- package/dist_ts/context/context-cache.js +238 -0
- package/dist_ts/context/context-trimmer.d.ts +8 -0
- package/dist_ts/context/context-trimmer.js +60 -1
- package/dist_ts/context/enhanced-context.d.ts +12 -14
- package/dist_ts/context/enhanced-context.js +130 -127
- package/dist_ts/context/index.d.ts +6 -3
- package/dist_ts/context/index.js +5 -2
- package/dist_ts/context/lazy-file-loader.d.ts +60 -0
- package/dist_ts/context/lazy-file-loader.js +164 -0
- package/dist_ts/context/types.d.ts +142 -0
- package/package.json +1 -1
- package/readme.md +348 -53
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/context/config-manager.ts +146 -15
- package/ts/context/context-analyzer.ts +391 -0
- package/ts/context/context-cache.ts +285 -0
- package/ts/context/context-trimmer.ts +64 -0
- package/ts/context/enhanced-context.ts +169 -180
- package/ts/context/index.ts +31 -5
- package/ts/context/lazy-file-loader.ts +191 -0
- package/ts/context/types.ts +152 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
|
-
import type { ContextMode, IContextResult, IFileInfo, TaskType } from './types.js';
|
|
2
|
+
import type { ContextMode, IContextResult, IFileInfo, TaskType, IFileMetadata } from './types.js';
|
|
3
3
|
import { ContextTrimmer } from './context-trimmer.js';
|
|
4
4
|
import { ConfigManager } from './config-manager.js';
|
|
5
|
+
import { LazyFileLoader } from './lazy-file-loader.js';
|
|
6
|
+
import { ContextCache } from './context-cache.js';
|
|
7
|
+
import { ContextAnalyzer } from './context-analyzer.js';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* Enhanced ProjectContext that supports context optimization strategies
|
|
@@ -10,6 +13,9 @@ export class EnhancedContext {
|
|
|
10
13
|
private projectDir: string;
|
|
11
14
|
private trimmer: ContextTrimmer;
|
|
12
15
|
private configManager: ConfigManager;
|
|
16
|
+
private lazyLoader: LazyFileLoader;
|
|
17
|
+
private cache: ContextCache;
|
|
18
|
+
private analyzer: ContextAnalyzer;
|
|
13
19
|
private contextMode: ContextMode = 'trimmed';
|
|
14
20
|
private tokenBudget: number = 190000; // Default for o4-mini
|
|
15
21
|
private contextResult: IContextResult = {
|
|
@@ -29,6 +35,13 @@ export class EnhancedContext {
|
|
|
29
35
|
this.projectDir = projectDirArg;
|
|
30
36
|
this.configManager = ConfigManager.getInstance();
|
|
31
37
|
this.trimmer = new ContextTrimmer(this.configManager.getTrimConfig());
|
|
38
|
+
this.lazyLoader = new LazyFileLoader(projectDirArg);
|
|
39
|
+
this.cache = new ContextCache(projectDirArg, this.configManager.getCacheConfig());
|
|
40
|
+
this.analyzer = new ContextAnalyzer(
|
|
41
|
+
projectDirArg,
|
|
42
|
+
this.configManager.getPrioritizationWeights(),
|
|
43
|
+
this.configManager.getTierConfig()
|
|
44
|
+
);
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
/**
|
|
@@ -38,6 +51,7 @@ export class EnhancedContext {
|
|
|
38
51
|
await this.configManager.initialize(this.projectDir);
|
|
39
52
|
this.tokenBudget = this.configManager.getMaxTokens();
|
|
40
53
|
this.trimmer.updateConfig(this.configManager.getTrimConfig());
|
|
54
|
+
await this.cache.init();
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
/**
|
|
@@ -55,75 +69,17 @@ export class EnhancedContext {
|
|
|
55
69
|
public setTokenBudget(maxTokens: number): void {
|
|
56
70
|
this.tokenBudget = maxTokens;
|
|
57
71
|
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Gather files from the project
|
|
61
|
-
* @param includePaths Optional paths to include
|
|
62
|
-
* @param excludePaths Optional paths to exclude
|
|
63
|
-
*/
|
|
64
|
-
public async gatherFiles(includePaths?: string[], excludePaths?: string[]): Promise<Record<string, plugins.smartfile.SmartFile | plugins.smartfile.SmartFile[]>> {
|
|
65
|
-
const smartfilePackageJSON = await plugins.smartfile.SmartFile.fromFilePath(
|
|
66
|
-
plugins.path.join(this.projectDir, 'package.json'),
|
|
67
|
-
this.projectDir,
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
const smartfilesReadme = await plugins.smartfile.SmartFile.fromFilePath(
|
|
71
|
-
plugins.path.join(this.projectDir, 'readme.md'),
|
|
72
|
-
this.projectDir,
|
|
73
|
-
);
|
|
74
72
|
|
|
75
|
-
const smartfilesReadmeHints = await plugins.smartfile.SmartFile.fromFilePath(
|
|
76
|
-
plugins.path.join(this.projectDir, 'readme.hints.md'),
|
|
77
|
-
this.projectDir,
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
const smartfilesNpmextraJSON = await plugins.smartfile.SmartFile.fromFilePath(
|
|
81
|
-
plugins.path.join(this.projectDir, 'npmextra.json'),
|
|
82
|
-
this.projectDir,
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
// Use provided include paths or default to all TypeScript files
|
|
86
|
-
const includeGlobs = includePaths?.map(path => `${path}/**/*.ts`) || ['ts*/**/*.ts'];
|
|
87
|
-
|
|
88
|
-
// Get TypeScript files
|
|
89
|
-
const smartfilesModPromises = includeGlobs.map(glob =>
|
|
90
|
-
plugins.smartfile.fs.fileTreeToObject(this.projectDir, glob)
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
const smartfilesModArrays = await Promise.all(smartfilesModPromises);
|
|
94
|
-
|
|
95
|
-
// Flatten the arrays
|
|
96
|
-
const smartfilesMod: plugins.smartfile.SmartFile[] = [];
|
|
97
|
-
smartfilesModArrays.forEach(array => {
|
|
98
|
-
smartfilesMod.push(...array);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// Get test files if not excluded
|
|
102
|
-
let smartfilesTest: plugins.smartfile.SmartFile[] = [];
|
|
103
|
-
if (!excludePaths?.includes('test/')) {
|
|
104
|
-
smartfilesTest = await plugins.smartfile.fs.fileTreeToObject(
|
|
105
|
-
this.projectDir,
|
|
106
|
-
'test/**/*.ts',
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
smartfilePackageJSON,
|
|
112
|
-
smartfilesReadme,
|
|
113
|
-
smartfilesReadmeHints,
|
|
114
|
-
smartfilesNpmextraJSON,
|
|
115
|
-
smartfilesMod,
|
|
116
|
-
smartfilesTest,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
73
|
/**
|
|
121
|
-
* Convert files to context
|
|
122
|
-
* @param
|
|
123
|
-
* @param
|
|
74
|
+
* Convert files to context with smart analysis and prioritization
|
|
75
|
+
* @param metadata - File metadata to analyze
|
|
76
|
+
* @param taskType - Task type for context-aware prioritization
|
|
77
|
+
* @param mode - Context mode to use
|
|
78
|
+
* @returns Context string
|
|
124
79
|
*/
|
|
125
|
-
public async
|
|
126
|
-
|
|
80
|
+
public async convertFilesToContextWithAnalysis(
|
|
81
|
+
metadata: IFileMetadata[],
|
|
82
|
+
taskType: TaskType,
|
|
127
83
|
mode: ContextMode = this.contextMode
|
|
128
84
|
): Promise<string> {
|
|
129
85
|
// Reset context result
|
|
@@ -135,140 +91,173 @@ export class EnhancedContext {
|
|
|
135
91
|
excludedFiles: [],
|
|
136
92
|
tokenSavings: 0
|
|
137
93
|
};
|
|
138
|
-
|
|
94
|
+
|
|
95
|
+
// Analyze files for smart prioritization
|
|
96
|
+
const analysis = await this.analyzer.analyze(metadata, taskType, []);
|
|
97
|
+
|
|
98
|
+
// Sort files by importance score (highest first)
|
|
99
|
+
const sortedAnalysis = [...analysis.files].sort(
|
|
100
|
+
(a, b) => b.importanceScore - a.importanceScore
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Filter out excluded tier
|
|
104
|
+
const relevantFiles = sortedAnalysis.filter(f => f.tier !== 'excluded');
|
|
105
|
+
|
|
139
106
|
let totalTokenCount = 0;
|
|
140
107
|
let totalOriginalTokens = 0;
|
|
141
|
-
|
|
142
|
-
// Sort files by importance (for now just a simple alphabetical sort)
|
|
143
|
-
// Later this could be enhanced with more sophisticated prioritization
|
|
144
|
-
const sortedFiles = [...files].sort((a, b) => a.relative.localeCompare(b.relative));
|
|
145
|
-
|
|
146
108
|
const processedFiles: string[] = [];
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
109
|
+
|
|
110
|
+
// Load files with cache support
|
|
111
|
+
for (const fileAnalysis of relevantFiles) {
|
|
112
|
+
try {
|
|
113
|
+
// Check cache first
|
|
114
|
+
let contents: string;
|
|
115
|
+
let originalTokenCount: number;
|
|
116
|
+
|
|
117
|
+
const cached = await this.cache.get(fileAnalysis.path);
|
|
118
|
+
if (cached) {
|
|
119
|
+
contents = cached.contents;
|
|
120
|
+
originalTokenCount = cached.tokenCount;
|
|
121
|
+
} else {
|
|
122
|
+
// Load file
|
|
123
|
+
const fileData = await plugins.smartfile.fs.toStringSync(fileAnalysis.path);
|
|
124
|
+
contents = fileData;
|
|
125
|
+
originalTokenCount = this.countTokens(contents);
|
|
126
|
+
|
|
127
|
+
// Cache it
|
|
128
|
+
await this.cache.set({
|
|
129
|
+
path: fileAnalysis.path,
|
|
130
|
+
contents,
|
|
131
|
+
tokenCount: originalTokenCount,
|
|
132
|
+
mtime: Date.now(),
|
|
133
|
+
cachedAt: Date.now()
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
totalOriginalTokens += originalTokenCount;
|
|
138
|
+
|
|
139
|
+
// Apply tier-based trimming
|
|
140
|
+
let processedContent = contents;
|
|
141
|
+
let trimLevel: 'none' | 'light' | 'aggressive' = 'light';
|
|
142
|
+
|
|
143
|
+
if (fileAnalysis.tier === 'essential') {
|
|
144
|
+
trimLevel = 'none';
|
|
145
|
+
} else if (fileAnalysis.tier === 'important') {
|
|
146
|
+
trimLevel = 'light';
|
|
147
|
+
} else if (fileAnalysis.tier === 'optional') {
|
|
148
|
+
trimLevel = 'aggressive';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Apply trimming based on mode and tier
|
|
152
|
+
if (mode !== 'full' && trimLevel !== 'none') {
|
|
153
|
+
const relativePath = plugins.path.relative(this.projectDir, fileAnalysis.path);
|
|
154
|
+
processedContent = this.trimmer.trimFileWithLevel(
|
|
155
|
+
relativePath,
|
|
156
|
+
contents,
|
|
157
|
+
trimLevel
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Calculate token count
|
|
162
|
+
const processedTokenCount = this.countTokens(processedContent);
|
|
163
|
+
|
|
164
|
+
// Check token budget
|
|
165
|
+
if (totalTokenCount + processedTokenCount > this.tokenBudget) {
|
|
166
|
+
// We don't have budget for this file
|
|
167
|
+
const relativePath = plugins.path.relative(this.projectDir, fileAnalysis.path);
|
|
168
|
+
this.contextResult.excludedFiles.push({
|
|
169
|
+
path: fileAnalysis.path,
|
|
170
|
+
contents,
|
|
171
|
+
relativePath,
|
|
172
|
+
tokenCount: originalTokenCount,
|
|
173
|
+
importanceScore: fileAnalysis.importanceScore
|
|
174
|
+
});
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Format the file for context
|
|
179
|
+
const relativePath = plugins.path.relative(this.projectDir, fileAnalysis.path);
|
|
180
|
+
const formattedContent = `
|
|
181
|
+
====== START OF FILE ${relativePath} ======
|
|
182
|
+
|
|
184
183
|
${processedContent}
|
|
185
|
-
|
|
186
|
-
====== END OF FILE ${
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
184
|
+
|
|
185
|
+
====== END OF FILE ${relativePath} ======
|
|
186
|
+
`;
|
|
187
|
+
|
|
188
|
+
processedFiles.push(formattedContent);
|
|
189
|
+
totalTokenCount += processedTokenCount;
|
|
190
|
+
|
|
191
|
+
// Track file in appropriate list
|
|
192
|
+
const fileInfo: IFileInfo = {
|
|
193
|
+
path: fileAnalysis.path,
|
|
194
|
+
contents: processedContent,
|
|
195
|
+
relativePath,
|
|
196
|
+
tokenCount: processedTokenCount,
|
|
197
|
+
importanceScore: fileAnalysis.importanceScore
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (trimLevel === 'none' || processedContent === contents) {
|
|
201
|
+
this.contextResult.includedFiles.push(fileInfo);
|
|
202
|
+
} else {
|
|
203
|
+
this.contextResult.trimmedFiles.push(fileInfo);
|
|
204
|
+
this.contextResult.tokenSavings += (originalTokenCount - processedTokenCount);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.warn(`Failed to process file ${fileAnalysis.path}:`, error.message);
|
|
205
208
|
}
|
|
206
209
|
}
|
|
207
|
-
|
|
210
|
+
|
|
208
211
|
// Join all processed files
|
|
209
212
|
const context = processedFiles.join('\n');
|
|
210
|
-
|
|
213
|
+
|
|
211
214
|
// Update context result
|
|
212
215
|
this.contextResult.context = context;
|
|
213
216
|
this.contextResult.tokenCount = totalTokenCount;
|
|
214
|
-
|
|
217
|
+
|
|
215
218
|
return context;
|
|
216
219
|
}
|
|
217
|
-
|
|
220
|
+
|
|
218
221
|
/**
|
|
219
|
-
* Build context for the project
|
|
220
|
-
* @param taskType
|
|
222
|
+
* Build context for the project using smart analysis
|
|
223
|
+
* @param taskType Task type for context-aware prioritization (defaults to 'description')
|
|
221
224
|
*/
|
|
222
225
|
public async buildContext(taskType?: TaskType): Promise<IContextResult> {
|
|
223
226
|
// Initialize if needed
|
|
224
227
|
if (this.tokenBudget === 0) {
|
|
225
228
|
await this.initialize();
|
|
226
229
|
}
|
|
227
|
-
|
|
228
|
-
//
|
|
229
|
-
if
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
// Gather files
|
|
237
|
-
const taskConfig = taskType ? this.configManager.getTaskConfig(taskType) : undefined;
|
|
238
|
-
const files = await this.gatherFiles(
|
|
239
|
-
taskConfig?.includePaths,
|
|
240
|
-
taskConfig?.excludePaths
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
// Convert files to context
|
|
244
|
-
// Create an array of all files to process
|
|
245
|
-
const allFiles: plugins.smartfile.SmartFile[] = [];
|
|
246
|
-
|
|
247
|
-
// Add individual files
|
|
248
|
-
if (files.smartfilePackageJSON) allFiles.push(files.smartfilePackageJSON as plugins.smartfile.SmartFile);
|
|
249
|
-
if (files.smartfilesReadme) allFiles.push(files.smartfilesReadme as plugins.smartfile.SmartFile);
|
|
250
|
-
if (files.smartfilesReadmeHints) allFiles.push(files.smartfilesReadmeHints as plugins.smartfile.SmartFile);
|
|
251
|
-
if (files.smartfilesNpmextraJSON) allFiles.push(files.smartfilesNpmextraJSON as plugins.smartfile.SmartFile);
|
|
252
|
-
|
|
253
|
-
// Add arrays of files
|
|
254
|
-
if (files.smartfilesMod) {
|
|
255
|
-
if (Array.isArray(files.smartfilesMod)) {
|
|
256
|
-
allFiles.push(...files.smartfilesMod);
|
|
257
|
-
} else {
|
|
258
|
-
allFiles.push(files.smartfilesMod);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (files.smartfilesTest) {
|
|
263
|
-
if (Array.isArray(files.smartfilesTest)) {
|
|
264
|
-
allFiles.push(...files.smartfilesTest);
|
|
265
|
-
} else {
|
|
266
|
-
allFiles.push(files.smartfilesTest);
|
|
267
|
-
}
|
|
230
|
+
|
|
231
|
+
// Smart context building always requires a task type for optimal prioritization
|
|
232
|
+
// Default to 'description' if not provided
|
|
233
|
+
const effectiveTaskType = taskType || 'description';
|
|
234
|
+
|
|
235
|
+
// Get task-specific configuration
|
|
236
|
+
const taskConfig = this.configManager.getTaskConfig(effectiveTaskType);
|
|
237
|
+
if (taskConfig.mode) {
|
|
238
|
+
this.setContextMode(taskConfig.mode);
|
|
268
239
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
240
|
+
|
|
241
|
+
// Build globs for scanning
|
|
242
|
+
const includeGlobs = taskConfig?.includePaths?.map(p => `${p}/**/*.ts`) || [
|
|
243
|
+
'ts/**/*.ts',
|
|
244
|
+
'ts*/**/*.ts'
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
// Add config files
|
|
248
|
+
const configGlobs = [
|
|
249
|
+
'package.json',
|
|
250
|
+
'readme.md',
|
|
251
|
+
'readme.hints.md',
|
|
252
|
+
'npmextra.json'
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
// Scan files for metadata (fast, doesn't load contents)
|
|
256
|
+
const metadata = await this.lazyLoader.scanFiles([...configGlobs, ...includeGlobs]);
|
|
257
|
+
|
|
258
|
+
// Use smart analyzer to build context with intelligent prioritization
|
|
259
|
+
await this.convertFilesToContextWithAnalysis(metadata, effectiveTaskType, this.contextMode);
|
|
260
|
+
|
|
272
261
|
return this.contextResult;
|
|
273
262
|
}
|
|
274
263
|
|
package/ts/context/index.ts
CHANGED
|
@@ -2,14 +2,27 @@ import { EnhancedContext } from './enhanced-context.js';
|
|
|
2
2
|
import { TaskContextFactory } from './task-context-factory.js';
|
|
3
3
|
import { ConfigManager } from './config-manager.js';
|
|
4
4
|
import { ContextTrimmer } from './context-trimmer.js';
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { LazyFileLoader } from './lazy-file-loader.js';
|
|
6
|
+
import { ContextCache } from './context-cache.js';
|
|
7
|
+
import { ContextAnalyzer } from './context-analyzer.js';
|
|
8
|
+
import type {
|
|
9
|
+
ContextMode,
|
|
10
|
+
IContextConfig,
|
|
8
11
|
IContextResult,
|
|
9
12
|
IFileInfo,
|
|
10
13
|
ITrimConfig,
|
|
11
14
|
ITaskConfig,
|
|
12
|
-
TaskType
|
|
15
|
+
TaskType,
|
|
16
|
+
ICacheConfig,
|
|
17
|
+
IAnalyzerConfig,
|
|
18
|
+
IPrioritizationWeights,
|
|
19
|
+
ITierConfig,
|
|
20
|
+
ITierSettings,
|
|
21
|
+
IFileMetadata,
|
|
22
|
+
ICacheEntry,
|
|
23
|
+
IFileDependencies,
|
|
24
|
+
IFileAnalysis,
|
|
25
|
+
IAnalysisResult
|
|
13
26
|
} from './types.js';
|
|
14
27
|
|
|
15
28
|
export {
|
|
@@ -18,6 +31,9 @@ export {
|
|
|
18
31
|
TaskContextFactory,
|
|
19
32
|
ConfigManager,
|
|
20
33
|
ContextTrimmer,
|
|
34
|
+
LazyFileLoader,
|
|
35
|
+
ContextCache,
|
|
36
|
+
ContextAnalyzer,
|
|
21
37
|
};
|
|
22
38
|
|
|
23
39
|
// Types
|
|
@@ -28,5 +44,15 @@ export type {
|
|
|
28
44
|
IFileInfo,
|
|
29
45
|
ITrimConfig,
|
|
30
46
|
ITaskConfig,
|
|
31
|
-
TaskType
|
|
47
|
+
TaskType,
|
|
48
|
+
ICacheConfig,
|
|
49
|
+
IAnalyzerConfig,
|
|
50
|
+
IPrioritizationWeights,
|
|
51
|
+
ITierConfig,
|
|
52
|
+
ITierSettings,
|
|
53
|
+
IFileMetadata,
|
|
54
|
+
ICacheEntry,
|
|
55
|
+
IFileDependencies,
|
|
56
|
+
IFileAnalysis,
|
|
57
|
+
IAnalysisResult
|
|
32
58
|
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import type { IFileMetadata, IFileInfo } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* LazyFileLoader handles efficient file loading by:
|
|
7
|
+
* - Scanning files for metadata without loading contents
|
|
8
|
+
* - Providing fast file size and token estimates
|
|
9
|
+
* - Loading contents only when requested
|
|
10
|
+
* - Parallel loading of selected files
|
|
11
|
+
*/
|
|
12
|
+
export class LazyFileLoader {
|
|
13
|
+
private projectRoot: string;
|
|
14
|
+
private metadataCache: Map<string, IFileMetadata> = new Map();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a new LazyFileLoader
|
|
18
|
+
* @param projectRoot - Root directory of the project
|
|
19
|
+
*/
|
|
20
|
+
constructor(projectRoot: string) {
|
|
21
|
+
this.projectRoot = projectRoot;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Scans files in given globs and creates metadata without loading contents
|
|
26
|
+
* @param globs - File patterns to scan (e.g., ['ts/**\/*.ts', 'test/**\/*.ts'])
|
|
27
|
+
* @returns Array of file metadata
|
|
28
|
+
*/
|
|
29
|
+
public async scanFiles(globs: string[]): Promise<IFileMetadata[]> {
|
|
30
|
+
const metadata: IFileMetadata[] = [];
|
|
31
|
+
|
|
32
|
+
for (const globPattern of globs) {
|
|
33
|
+
try {
|
|
34
|
+
const smartFiles = await plugins.smartfile.fs.fileTreeToObject(this.projectRoot, globPattern);
|
|
35
|
+
const fileArray = Array.isArray(smartFiles) ? smartFiles : [smartFiles];
|
|
36
|
+
|
|
37
|
+
for (const smartFile of fileArray) {
|
|
38
|
+
try {
|
|
39
|
+
const meta = await this.getMetadata(smartFile.path);
|
|
40
|
+
metadata.push(meta);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// Skip files that can't be read
|
|
43
|
+
console.warn(`Failed to get metadata for ${smartFile.path}:`, error.message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// Skip patterns that don't match any files
|
|
48
|
+
console.warn(`No files found for pattern ${globPattern}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return metadata;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Gets metadata for a single file without loading contents
|
|
57
|
+
* @param filePath - Absolute path to the file
|
|
58
|
+
* @returns File metadata
|
|
59
|
+
*/
|
|
60
|
+
public async getMetadata(filePath: string): Promise<IFileMetadata> {
|
|
61
|
+
// Check cache first
|
|
62
|
+
if (this.metadataCache.has(filePath)) {
|
|
63
|
+
const cached = this.metadataCache.get(filePath)!;
|
|
64
|
+
const currentStats = await fs.promises.stat(filePath);
|
|
65
|
+
|
|
66
|
+
// Return cached if file hasn't changed
|
|
67
|
+
if (cached.mtime === Math.floor(currentStats.mtimeMs)) {
|
|
68
|
+
return cached;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Get file stats
|
|
73
|
+
const stats = await fs.promises.stat(filePath);
|
|
74
|
+
const relativePath = plugins.path.relative(this.projectRoot, filePath);
|
|
75
|
+
|
|
76
|
+
// Estimate tokens: rough estimate of ~4 characters per token
|
|
77
|
+
// This is faster than reading and tokenizing the entire file
|
|
78
|
+
const estimatedTokens = Math.ceil(stats.size / 4);
|
|
79
|
+
|
|
80
|
+
const metadata: IFileMetadata = {
|
|
81
|
+
path: filePath,
|
|
82
|
+
relativePath,
|
|
83
|
+
size: stats.size,
|
|
84
|
+
mtime: Math.floor(stats.mtimeMs),
|
|
85
|
+
estimatedTokens,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Cache the metadata
|
|
89
|
+
this.metadataCache.set(filePath, metadata);
|
|
90
|
+
|
|
91
|
+
return metadata;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Loads file contents for selected files in parallel
|
|
96
|
+
* @param metadata - Array of file metadata to load
|
|
97
|
+
* @param tokenizer - Function to calculate accurate token count
|
|
98
|
+
* @returns Array of complete file info with contents
|
|
99
|
+
*/
|
|
100
|
+
public async loadFiles(
|
|
101
|
+
metadata: IFileMetadata[],
|
|
102
|
+
tokenizer: (content: string) => number
|
|
103
|
+
): Promise<IFileInfo[]> {
|
|
104
|
+
// Load files in parallel
|
|
105
|
+
const loadPromises = metadata.map(async (meta) => {
|
|
106
|
+
try {
|
|
107
|
+
const contents = await plugins.smartfile.fs.toStringSync(meta.path);
|
|
108
|
+
const tokenCount = tokenizer(contents);
|
|
109
|
+
|
|
110
|
+
const fileInfo: IFileInfo = {
|
|
111
|
+
path: meta.path,
|
|
112
|
+
relativePath: meta.relativePath,
|
|
113
|
+
contents,
|
|
114
|
+
tokenCount,
|
|
115
|
+
importanceScore: meta.importanceScore,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return fileInfo;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn(`Failed to load file ${meta.path}:`, error.message);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Wait for all loads to complete and filter out failures
|
|
126
|
+
const results = await Promise.all(loadPromises);
|
|
127
|
+
return results.filter((r): r is IFileInfo => r !== null);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Loads a single file with contents
|
|
132
|
+
* @param filePath - Absolute path to the file
|
|
133
|
+
* @param tokenizer - Function to calculate accurate token count
|
|
134
|
+
* @returns Complete file info with contents
|
|
135
|
+
*/
|
|
136
|
+
public async loadFile(
|
|
137
|
+
filePath: string,
|
|
138
|
+
tokenizer: (content: string) => number
|
|
139
|
+
): Promise<IFileInfo> {
|
|
140
|
+
const meta = await this.getMetadata(filePath);
|
|
141
|
+
const contents = await plugins.smartfile.fs.toStringSync(filePath);
|
|
142
|
+
const tokenCount = tokenizer(contents);
|
|
143
|
+
const relativePath = plugins.path.relative(this.projectRoot, filePath);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
path: filePath,
|
|
147
|
+
relativePath,
|
|
148
|
+
contents,
|
|
149
|
+
tokenCount,
|
|
150
|
+
importanceScore: meta.importanceScore,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Updates importance scores for metadata entries
|
|
156
|
+
* @param scores - Map of file paths to importance scores
|
|
157
|
+
*/
|
|
158
|
+
public updateImportanceScores(scores: Map<string, number>): void {
|
|
159
|
+
for (const [path, score] of scores) {
|
|
160
|
+
const meta = this.metadataCache.get(path);
|
|
161
|
+
if (meta) {
|
|
162
|
+
meta.importanceScore = score;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Clears the metadata cache
|
|
169
|
+
*/
|
|
170
|
+
public clearCache(): void {
|
|
171
|
+
this.metadataCache.clear();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Gets total estimated tokens for all cached metadata
|
|
176
|
+
*/
|
|
177
|
+
public getTotalEstimatedTokens(): number {
|
|
178
|
+
let total = 0;
|
|
179
|
+
for (const meta of this.metadataCache.values()) {
|
|
180
|
+
total += meta.estimatedTokens;
|
|
181
|
+
}
|
|
182
|
+
return total;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Gets cached metadata entries
|
|
187
|
+
*/
|
|
188
|
+
public getCachedMetadata(): IFileMetadata[] {
|
|
189
|
+
return Array.from(this.metadataCache.values());
|
|
190
|
+
}
|
|
191
|
+
}
|