@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.
@@ -1,5 +1,16 @@
1
1
  import * as plugins from '../plugins.js';
2
- import type { IContextConfig, ITrimConfig, ITaskConfig, TaskType, ContextMode } from './types.js';
2
+ import * as fs from 'fs';
3
+ import type {
4
+ IContextConfig,
5
+ ITrimConfig,
6
+ ITaskConfig,
7
+ TaskType,
8
+ ContextMode,
9
+ ICacheConfig,
10
+ IAnalyzerConfig,
11
+ IPrioritizationWeights,
12
+ ITierConfig
13
+ } from './types.js';
3
14
 
4
15
  /**
5
16
  * Manages configuration for context building
@@ -8,6 +19,7 @@ export class ConfigManager {
8
19
  private static instance: ConfigManager;
9
20
  private config: IContextConfig;
10
21
  private projectDir: string = '';
22
+ private configCache: { mtime: number; config: IContextConfig } | null = null;
11
23
 
12
24
  /**
13
25
  * Get the singleton instance of ConfigManager
@@ -65,6 +77,27 @@ export class ConfigManager {
65
77
  maxFunctionLines: 5,
66
78
  removeComments: true,
67
79
  removeBlankLines: true
80
+ },
81
+ cache: {
82
+ enabled: true,
83
+ ttl: 3600, // 1 hour
84
+ maxSize: 100, // 100MB
85
+ directory: undefined // Will be set to .nogit/context-cache by ContextCache
86
+ },
87
+ analyzer: {
88
+ useAIRefinement: false, // Disabled by default for now
89
+ aiModel: 'haiku'
90
+ },
91
+ prioritization: {
92
+ dependencyWeight: 0.3,
93
+ relevanceWeight: 0.4,
94
+ efficiencyWeight: 0.2,
95
+ recencyWeight: 0.1
96
+ },
97
+ tiers: {
98
+ essential: { minScore: 0.8, trimLevel: 'none' },
99
+ important: { minScore: 0.5, trimLevel: 'light' },
100
+ optional: { minScore: 0.2, trimLevel: 'aggressive' }
68
101
  }
69
102
  };
70
103
  }
@@ -77,21 +110,40 @@ export class ConfigManager {
77
110
  if (!this.projectDir) {
78
111
  return;
79
112
  }
80
-
81
- // Create KeyValueStore for this project
82
- // We'll just use smartfile directly instead of KeyValueStore
83
-
113
+
114
+ const npmextraJsonPath = plugins.path.join(this.projectDir, 'npmextra.json');
115
+
116
+ // Check if file exists
117
+ const fileExists = await plugins.smartfile.fs.fileExists(npmextraJsonPath);
118
+ if (!fileExists) {
119
+ return;
120
+ }
121
+
122
+ // Check cache
123
+ const stats = await fs.promises.stat(npmextraJsonPath);
124
+ const currentMtime = Math.floor(stats.mtimeMs);
125
+
126
+ if (this.configCache && this.configCache.mtime === currentMtime) {
127
+ // Use cached config
128
+ this.config = this.configCache.config;
129
+ return;
130
+ }
131
+
84
132
  // Read the npmextra.json file
85
- const npmextraJsonFile = await plugins.smartfile.SmartFile.fromFilePath(
86
- plugins.path.join(this.projectDir, 'npmextra.json')
87
- );
133
+ const npmextraJsonFile = await plugins.smartfile.SmartFile.fromFilePath(npmextraJsonPath);
88
134
  const npmextraContent = JSON.parse(npmextraJsonFile.contents.toString());
89
-
135
+
90
136
  // Check for tsdoc context configuration
91
137
  if (npmextraContent?.tsdoc?.context) {
92
138
  // Merge with default config
93
139
  this.config = this.mergeConfigs(this.config, npmextraContent.tsdoc.context);
94
140
  }
141
+
142
+ // Cache the config
143
+ this.configCache = {
144
+ mtime: currentMtime,
145
+ config: { ...this.config }
146
+ };
95
147
  } catch (error) {
96
148
  console.error('Error loading context configuration:', error);
97
149
  }
@@ -131,7 +183,39 @@ export class ConfigManager {
131
183
  ...userConfig.trimming
132
184
  };
133
185
  }
134
-
186
+
187
+ // Merge cache configuration
188
+ if (userConfig.cache) {
189
+ result.cache = {
190
+ ...result.cache,
191
+ ...userConfig.cache
192
+ };
193
+ }
194
+
195
+ // Merge analyzer configuration
196
+ if (userConfig.analyzer) {
197
+ result.analyzer = {
198
+ ...result.analyzer,
199
+ ...userConfig.analyzer
200
+ };
201
+ }
202
+
203
+ // Merge prioritization weights
204
+ if (userConfig.prioritization) {
205
+ result.prioritization = {
206
+ ...result.prioritization,
207
+ ...userConfig.prioritization
208
+ };
209
+ }
210
+
211
+ // Merge tier configuration
212
+ if (userConfig.tiers) {
213
+ result.tiers = {
214
+ ...result.tiers,
215
+ ...userConfig.tiers
216
+ };
217
+ }
218
+
135
219
  return result;
136
220
  }
137
221
 
@@ -179,26 +263,29 @@ export class ConfigManager {
179
263
  public async updateConfig(config: Partial<IContextConfig>): Promise<void> {
180
264
  // Merge with existing config
181
265
  this.config = this.mergeConfigs(this.config, config);
182
-
266
+
267
+ // Invalidate cache
268
+ this.configCache = null;
269
+
183
270
  try {
184
271
  if (!this.projectDir) {
185
272
  return;
186
273
  }
187
-
274
+
188
275
  // Read the existing npmextra.json file
189
276
  const npmextraJsonPath = plugins.path.join(this.projectDir, 'npmextra.json');
190
277
  let npmextraContent = {};
191
-
278
+
192
279
  if (await plugins.smartfile.fs.fileExists(npmextraJsonPath)) {
193
280
  const npmextraJsonFile = await plugins.smartfile.SmartFile.fromFilePath(npmextraJsonPath);
194
281
  npmextraContent = JSON.parse(npmextraJsonFile.contents.toString()) || {};
195
282
  }
196
-
283
+
197
284
  // Update the tsdoc context configuration
198
285
  const typedContent = npmextraContent as any;
199
286
  if (!typedContent.tsdoc) typedContent.tsdoc = {};
200
287
  typedContent.tsdoc.context = this.config;
201
-
288
+
202
289
  // Write back to npmextra.json
203
290
  const updatedContent = JSON.stringify(npmextraContent, null, 2);
204
291
  await plugins.smartfile.memory.toFs(updatedContent, npmextraJsonPath);
@@ -206,4 +293,48 @@ export class ConfigManager {
206
293
  console.error('Error updating context configuration:', error);
207
294
  }
208
295
  }
296
+
297
+ /**
298
+ * Get cache configuration
299
+ */
300
+ public getCacheConfig(): ICacheConfig {
301
+ return this.config.cache || { enabled: true, ttl: 3600, maxSize: 100 };
302
+ }
303
+
304
+ /**
305
+ * Get analyzer configuration
306
+ */
307
+ public getAnalyzerConfig(): IAnalyzerConfig {
308
+ return this.config.analyzer || { useAIRefinement: false, aiModel: 'haiku' };
309
+ }
310
+
311
+ /**
312
+ * Get prioritization weights
313
+ */
314
+ public getPrioritizationWeights(): IPrioritizationWeights {
315
+ return this.config.prioritization || {
316
+ dependencyWeight: 0.3,
317
+ relevanceWeight: 0.4,
318
+ efficiencyWeight: 0.2,
319
+ recencyWeight: 0.1
320
+ };
321
+ }
322
+
323
+ /**
324
+ * Get tier configuration
325
+ */
326
+ public getTierConfig(): ITierConfig {
327
+ return this.config.tiers || {
328
+ essential: { minScore: 0.8, trimLevel: 'none' },
329
+ important: { minScore: 0.5, trimLevel: 'light' },
330
+ optional: { minScore: 0.2, trimLevel: 'aggressive' }
331
+ };
332
+ }
333
+
334
+ /**
335
+ * Clear the config cache (force reload on next access)
336
+ */
337
+ public clearCache(): void {
338
+ this.configCache = null;
339
+ }
209
340
  }
@@ -0,0 +1,391 @@
1
+ import * as plugins from '../plugins.js';
2
+ import type {
3
+ IFileMetadata,
4
+ IFileDependencies,
5
+ IFileAnalysis,
6
+ IAnalysisResult,
7
+ TaskType,
8
+ IPrioritizationWeights,
9
+ ITierConfig,
10
+ } from './types.js';
11
+
12
+ /**
13
+ * ContextAnalyzer provides intelligent file selection and prioritization
14
+ * based on dependency analysis, task relevance, and configurable weights
15
+ */
16
+ export class ContextAnalyzer {
17
+ private projectRoot: string;
18
+ private weights: Required<IPrioritizationWeights>;
19
+ private tiers: Required<ITierConfig>;
20
+
21
+ /**
22
+ * Creates a new ContextAnalyzer
23
+ * @param projectRoot - Root directory of the project
24
+ * @param weights - Prioritization weights
25
+ * @param tiers - Tier configuration
26
+ */
27
+ constructor(
28
+ projectRoot: string,
29
+ weights: Partial<IPrioritizationWeights> = {},
30
+ tiers: Partial<ITierConfig> = {}
31
+ ) {
32
+ this.projectRoot = projectRoot;
33
+
34
+ // Default weights
35
+ this.weights = {
36
+ dependencyWeight: weights.dependencyWeight ?? 0.3,
37
+ relevanceWeight: weights.relevanceWeight ?? 0.4,
38
+ efficiencyWeight: weights.efficiencyWeight ?? 0.2,
39
+ recencyWeight: weights.recencyWeight ?? 0.1,
40
+ };
41
+
42
+ // Default tiers
43
+ this.tiers = {
44
+ essential: tiers.essential ?? { minScore: 0.8, trimLevel: 'none' },
45
+ important: tiers.important ?? { minScore: 0.5, trimLevel: 'light' },
46
+ optional: tiers.optional ?? { minScore: 0.2, trimLevel: 'aggressive' },
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Analyzes files for a specific task type
52
+ * @param metadata - Array of file metadata to analyze
53
+ * @param taskType - Type of task being performed
54
+ * @param changedFiles - Optional list of recently changed files (for commits)
55
+ * @returns Analysis result with scored files
56
+ */
57
+ public async analyze(
58
+ metadata: IFileMetadata[],
59
+ taskType: TaskType,
60
+ changedFiles: string[] = []
61
+ ): Promise<IAnalysisResult> {
62
+ const startTime = Date.now();
63
+
64
+ // Build dependency graph
65
+ const dependencyGraph = await this.buildDependencyGraph(metadata);
66
+
67
+ // Calculate centrality scores
68
+ this.calculateCentrality(dependencyGraph);
69
+
70
+ // Analyze each file
71
+ const files: IFileAnalysis[] = [];
72
+ for (const meta of metadata) {
73
+ const analysis = await this.analyzeFile(
74
+ meta,
75
+ taskType,
76
+ dependencyGraph,
77
+ changedFiles
78
+ );
79
+ files.push(analysis);
80
+ }
81
+
82
+ // Sort by importance score (highest first)
83
+ files.sort((a, b) => b.importanceScore - a.importanceScore);
84
+
85
+ const analysisDuration = Date.now() - startTime;
86
+
87
+ return {
88
+ taskType,
89
+ files,
90
+ dependencyGraph,
91
+ totalFiles: metadata.length,
92
+ analysisDuration,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Builds a dependency graph from file metadata
98
+ * @param metadata - Array of file metadata
99
+ * @returns Dependency graph as a map
100
+ */
101
+ private async buildDependencyGraph(
102
+ metadata: IFileMetadata[]
103
+ ): Promise<Map<string, IFileDependencies>> {
104
+ const graph = new Map<string, IFileDependencies>();
105
+
106
+ // Initialize graph entries
107
+ for (const meta of metadata) {
108
+ graph.set(meta.path, {
109
+ path: meta.path,
110
+ imports: [],
111
+ importedBy: [],
112
+ centrality: 0,
113
+ });
114
+ }
115
+
116
+ // Parse imports from each file
117
+ for (const meta of metadata) {
118
+ try {
119
+ const contents = await plugins.smartfile.fs.toStringSync(meta.path);
120
+ const imports = this.extractImports(contents, meta.path);
121
+
122
+ const deps = graph.get(meta.path)!;
123
+ deps.imports = imports;
124
+
125
+ // Update importedBy for imported files
126
+ for (const importPath of imports) {
127
+ const importedDeps = graph.get(importPath);
128
+ if (importedDeps) {
129
+ importedDeps.importedBy.push(meta.path);
130
+ }
131
+ }
132
+ } catch (error) {
133
+ console.warn(`Failed to parse imports from ${meta.path}:`, error.message);
134
+ }
135
+ }
136
+
137
+ return graph;
138
+ }
139
+
140
+ /**
141
+ * Extracts import statements from file contents
142
+ * @param contents - File contents
143
+ * @param filePath - Path of the file being analyzed
144
+ * @returns Array of absolute paths to imported files
145
+ */
146
+ private extractImports(contents: string, filePath: string): string[] {
147
+ const imports: string[] = [];
148
+ const fileDir = plugins.path.dirname(filePath);
149
+
150
+ // Match various import patterns
151
+ const importRegex = /(?:import|export).*?from\s+['"](.+?)['"]/g;
152
+ let match;
153
+
154
+ while ((match = importRegex.exec(contents)) !== null) {
155
+ const importPath = match[1];
156
+
157
+ // Skip external modules
158
+ if (!importPath.startsWith('.')) {
159
+ continue;
160
+ }
161
+
162
+ // Resolve relative import to absolute path
163
+ let resolvedPath = plugins.path.resolve(fileDir, importPath);
164
+
165
+ // Handle various file extensions
166
+ const extensions = ['.ts', '.js', '.tsx', '.jsx', '/index.ts', '/index.js'];
167
+ let found = false;
168
+
169
+ for (const ext of extensions) {
170
+ const testPath = resolvedPath.endsWith(ext) ? resolvedPath : resolvedPath + ext;
171
+ try {
172
+ // Use synchronous file check to avoid async in this context
173
+ const fs = require('fs');
174
+ const exists = fs.existsSync(testPath);
175
+ if (exists) {
176
+ imports.push(testPath);
177
+ found = true;
178
+ break;
179
+ }
180
+ } catch (error) {
181
+ // Continue trying other extensions
182
+ }
183
+ }
184
+
185
+ if (!found && !resolvedPath.includes('.')) {
186
+ // Try with .ts extension as default
187
+ imports.push(resolvedPath + '.ts');
188
+ }
189
+ }
190
+
191
+ return imports;
192
+ }
193
+
194
+ /**
195
+ * Calculates centrality scores for all nodes in the dependency graph
196
+ * Uses a simplified PageRank-like algorithm
197
+ * @param graph - Dependency graph
198
+ */
199
+ private calculateCentrality(graph: Map<string, IFileDependencies>): void {
200
+ const damping = 0.85;
201
+ const iterations = 10;
202
+ const nodeCount = graph.size;
203
+
204
+ // Initialize scores
205
+ const scores = new Map<string, number>();
206
+ for (const path of graph.keys()) {
207
+ scores.set(path, 1.0 / nodeCount);
208
+ }
209
+
210
+ // Iterative calculation
211
+ for (let i = 0; i < iterations; i++) {
212
+ const newScores = new Map<string, number>();
213
+
214
+ for (const [path, deps] of graph.entries()) {
215
+ let score = (1 - damping) / nodeCount;
216
+
217
+ // Add contributions from nodes that import this file
218
+ for (const importerPath of deps.importedBy) {
219
+ const importerDeps = graph.get(importerPath);
220
+ if (importerDeps) {
221
+ const importerScore = scores.get(importerPath) ?? 0;
222
+ const outgoingCount = importerDeps.imports.length || 1;
223
+ score += damping * (importerScore / outgoingCount);
224
+ }
225
+ }
226
+
227
+ newScores.set(path, score);
228
+ }
229
+
230
+ // Update scores
231
+ for (const [path, score] of newScores) {
232
+ scores.set(path, score);
233
+ }
234
+ }
235
+
236
+ // Normalize scores to 0-1 range
237
+ const maxScore = Math.max(...scores.values());
238
+ if (maxScore > 0) {
239
+ for (const deps of graph.values()) {
240
+ const score = scores.get(deps.path) ?? 0;
241
+ deps.centrality = score / maxScore;
242
+ }
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Analyzes a single file
248
+ * @param meta - File metadata
249
+ * @param taskType - Task being performed
250
+ * @param graph - Dependency graph
251
+ * @param changedFiles - Recently changed files
252
+ * @returns File analysis
253
+ */
254
+ private async analyzeFile(
255
+ meta: IFileMetadata,
256
+ taskType: TaskType,
257
+ graph: Map<string, IFileDependencies>,
258
+ changedFiles: string[]
259
+ ): Promise<IFileAnalysis> {
260
+ const deps = graph.get(meta.path);
261
+ const centralityScore = deps?.centrality ?? 0;
262
+
263
+ // Calculate task-specific relevance
264
+ const relevanceScore = this.calculateRelevance(meta, taskType);
265
+
266
+ // Calculate efficiency (information per token)
267
+ const efficiencyScore = this.calculateEfficiency(meta);
268
+
269
+ // Calculate recency (for commit tasks)
270
+ const recencyScore = this.calculateRecency(meta, changedFiles);
271
+
272
+ // Calculate combined importance score
273
+ const importanceScore =
274
+ relevanceScore * this.weights.relevanceWeight +
275
+ centralityScore * this.weights.dependencyWeight +
276
+ efficiencyScore * this.weights.efficiencyWeight +
277
+ recencyScore * this.weights.recencyWeight;
278
+
279
+ // Assign tier
280
+ const tier = this.assignTier(importanceScore);
281
+
282
+ return {
283
+ path: meta.path,
284
+ relevanceScore,
285
+ centralityScore,
286
+ efficiencyScore,
287
+ recencyScore,
288
+ importanceScore,
289
+ tier,
290
+ reason: this.generateReason(meta, taskType, importanceScore, tier),
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Calculates task-specific relevance score
296
+ */
297
+ private calculateRelevance(meta: IFileMetadata, taskType: TaskType): number {
298
+ const relativePath = meta.relativePath.toLowerCase();
299
+ let score = 0.5; // Base score
300
+
301
+ // README generation - prioritize public APIs and main exports
302
+ if (taskType === 'readme') {
303
+ if (relativePath.includes('index.ts')) score += 0.3;
304
+ if (relativePath.match(/^ts\/[^\/]+\.ts$/)) score += 0.2; // Root level exports
305
+ if (relativePath.includes('test/')) score -= 0.3;
306
+ if (relativePath.includes('classes/')) score += 0.1;
307
+ if (relativePath.includes('interfaces/')) score += 0.1;
308
+ }
309
+
310
+ // Commit messages - prioritize changed files and their dependencies
311
+ if (taskType === 'commit') {
312
+ if (relativePath.includes('test/')) score -= 0.2;
313
+ // Recency will handle changed files
314
+ }
315
+
316
+ // Description generation - prioritize main exports and core interfaces
317
+ if (taskType === 'description') {
318
+ if (relativePath.includes('index.ts')) score += 0.4;
319
+ if (relativePath.match(/^ts\/[^\/]+\.ts$/)) score += 0.3;
320
+ if (relativePath.includes('test/')) score -= 0.4;
321
+ if (relativePath.includes('interfaces/')) score += 0.2;
322
+ }
323
+
324
+ return Math.max(0, Math.min(1, score));
325
+ }
326
+
327
+ /**
328
+ * Calculates efficiency score (information density)
329
+ */
330
+ private calculateEfficiency(meta: IFileMetadata): number {
331
+ // Prefer files that are not too large (good signal-to-noise ratio)
332
+ const optimalSize = 5000; // ~1250 tokens
333
+ const distance = Math.abs(meta.estimatedTokens - optimalSize);
334
+ const normalized = Math.max(0, 1 - distance / optimalSize);
335
+
336
+ return normalized;
337
+ }
338
+
339
+ /**
340
+ * Calculates recency score for changed files
341
+ */
342
+ private calculateRecency(meta: IFileMetadata, changedFiles: string[]): number {
343
+ if (changedFiles.length === 0) {
344
+ return 0;
345
+ }
346
+
347
+ // Check if this file was changed
348
+ const isChanged = changedFiles.some((changed) => changed === meta.path);
349
+
350
+ return isChanged ? 1.0 : 0.0;
351
+ }
352
+
353
+ /**
354
+ * Assigns a tier based on importance score
355
+ */
356
+ private assignTier(score: number): 'essential' | 'important' | 'optional' | 'excluded' {
357
+ if (score >= this.tiers.essential.minScore) return 'essential';
358
+ if (score >= this.tiers.important.minScore) return 'important';
359
+ if (score >= this.tiers.optional.minScore) return 'optional';
360
+ return 'excluded';
361
+ }
362
+
363
+ /**
364
+ * Generates a human-readable reason for the score
365
+ */
366
+ private generateReason(
367
+ meta: IFileMetadata,
368
+ taskType: TaskType,
369
+ score: number,
370
+ tier: string
371
+ ): string {
372
+ const reasons: string[] = [];
373
+
374
+ if (meta.relativePath.includes('index.ts')) {
375
+ reasons.push('main export file');
376
+ }
377
+
378
+ if (meta.relativePath.includes('test/')) {
379
+ reasons.push('test file (lower priority)');
380
+ }
381
+
382
+ if (taskType === 'readme' && meta.relativePath.match(/^ts\/[^\/]+\.ts$/)) {
383
+ reasons.push('root-level module');
384
+ }
385
+
386
+ reasons.push(`score: ${score.toFixed(2)}`);
387
+ reasons.push(`tier: ${tier}`);
388
+
389
+ return reasons.join(', ');
390
+ }
391
+ }