@git.zone/tsdoc 1.5.1 → 1.6.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.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@git.zone/tsdoc',
6
- version: '1.5.1',
6
+ version: '1.6.0',
7
7
  description: 'A comprehensive TypeScript documentation tool that leverages AI to generate and enhance project documentation, including dynamic README creation, API docs via TypeDoc, and smart commit message generation.'
8
8
  }
@@ -77,8 +77,8 @@ interface {
77
77
  For the recommendedNextVersionDetails, please only add a detail entries to the array if it has an obvious value to the reader.
78
78
 
79
79
  You are being given the files of the project. You should use them to create the commit message.
80
- Also you are given a diff
81
-
80
+ Also you are given a diff.
81
+ Never mention CLAUDE code, or codex.
82
82
  `,
83
83
  messageHistory: [],
84
84
  userMessage: contextString,
@@ -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,28 @@ 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
+ enabled: true,
89
+ useAIRefinement: false, // Disabled by default for now
90
+ aiModel: 'haiku'
91
+ },
92
+ prioritization: {
93
+ dependencyWeight: 0.3,
94
+ relevanceWeight: 0.4,
95
+ efficiencyWeight: 0.2,
96
+ recencyWeight: 0.1
97
+ },
98
+ tiers: {
99
+ essential: { minScore: 0.8, trimLevel: 'none' },
100
+ important: { minScore: 0.5, trimLevel: 'light' },
101
+ optional: { minScore: 0.2, trimLevel: 'aggressive' }
68
102
  }
69
103
  };
70
104
  }
@@ -77,21 +111,40 @@ export class ConfigManager {
77
111
  if (!this.projectDir) {
78
112
  return;
79
113
  }
80
-
81
- // Create KeyValueStore for this project
82
- // We'll just use smartfile directly instead of KeyValueStore
83
-
114
+
115
+ const npmextraJsonPath = plugins.path.join(this.projectDir, 'npmextra.json');
116
+
117
+ // Check if file exists
118
+ const fileExists = await plugins.smartfile.fs.fileExists(npmextraJsonPath);
119
+ if (!fileExists) {
120
+ return;
121
+ }
122
+
123
+ // Check cache
124
+ const stats = await fs.promises.stat(npmextraJsonPath);
125
+ const currentMtime = Math.floor(stats.mtimeMs);
126
+
127
+ if (this.configCache && this.configCache.mtime === currentMtime) {
128
+ // Use cached config
129
+ this.config = this.configCache.config;
130
+ return;
131
+ }
132
+
84
133
  // Read the npmextra.json file
85
- const npmextraJsonFile = await plugins.smartfile.SmartFile.fromFilePath(
86
- plugins.path.join(this.projectDir, 'npmextra.json')
87
- );
134
+ const npmextraJsonFile = await plugins.smartfile.SmartFile.fromFilePath(npmextraJsonPath);
88
135
  const npmextraContent = JSON.parse(npmextraJsonFile.contents.toString());
89
-
136
+
90
137
  // Check for tsdoc context configuration
91
138
  if (npmextraContent?.tsdoc?.context) {
92
139
  // Merge with default config
93
140
  this.config = this.mergeConfigs(this.config, npmextraContent.tsdoc.context);
94
141
  }
142
+
143
+ // Cache the config
144
+ this.configCache = {
145
+ mtime: currentMtime,
146
+ config: { ...this.config }
147
+ };
95
148
  } catch (error) {
96
149
  console.error('Error loading context configuration:', error);
97
150
  }
@@ -131,7 +184,39 @@ export class ConfigManager {
131
184
  ...userConfig.trimming
132
185
  };
133
186
  }
134
-
187
+
188
+ // Merge cache configuration
189
+ if (userConfig.cache) {
190
+ result.cache = {
191
+ ...result.cache,
192
+ ...userConfig.cache
193
+ };
194
+ }
195
+
196
+ // Merge analyzer configuration
197
+ if (userConfig.analyzer) {
198
+ result.analyzer = {
199
+ ...result.analyzer,
200
+ ...userConfig.analyzer
201
+ };
202
+ }
203
+
204
+ // Merge prioritization weights
205
+ if (userConfig.prioritization) {
206
+ result.prioritization = {
207
+ ...result.prioritization,
208
+ ...userConfig.prioritization
209
+ };
210
+ }
211
+
212
+ // Merge tier configuration
213
+ if (userConfig.tiers) {
214
+ result.tiers = {
215
+ ...result.tiers,
216
+ ...userConfig.tiers
217
+ };
218
+ }
219
+
135
220
  return result;
136
221
  }
137
222
 
@@ -179,26 +264,29 @@ export class ConfigManager {
179
264
  public async updateConfig(config: Partial<IContextConfig>): Promise<void> {
180
265
  // Merge with existing config
181
266
  this.config = this.mergeConfigs(this.config, config);
182
-
267
+
268
+ // Invalidate cache
269
+ this.configCache = null;
270
+
183
271
  try {
184
272
  if (!this.projectDir) {
185
273
  return;
186
274
  }
187
-
275
+
188
276
  // Read the existing npmextra.json file
189
277
  const npmextraJsonPath = plugins.path.join(this.projectDir, 'npmextra.json');
190
278
  let npmextraContent = {};
191
-
279
+
192
280
  if (await plugins.smartfile.fs.fileExists(npmextraJsonPath)) {
193
281
  const npmextraJsonFile = await plugins.smartfile.SmartFile.fromFilePath(npmextraJsonPath);
194
282
  npmextraContent = JSON.parse(npmextraJsonFile.contents.toString()) || {};
195
283
  }
196
-
284
+
197
285
  // Update the tsdoc context configuration
198
286
  const typedContent = npmextraContent as any;
199
287
  if (!typedContent.tsdoc) typedContent.tsdoc = {};
200
288
  typedContent.tsdoc.context = this.config;
201
-
289
+
202
290
  // Write back to npmextra.json
203
291
  const updatedContent = JSON.stringify(npmextraContent, null, 2);
204
292
  await plugins.smartfile.memory.toFs(updatedContent, npmextraJsonPath);
@@ -206,4 +294,48 @@ export class ConfigManager {
206
294
  console.error('Error updating context configuration:', error);
207
295
  }
208
296
  }
297
+
298
+ /**
299
+ * Get cache configuration
300
+ */
301
+ public getCacheConfig(): ICacheConfig {
302
+ return this.config.cache || { enabled: true, ttl: 3600, maxSize: 100 };
303
+ }
304
+
305
+ /**
306
+ * Get analyzer configuration
307
+ */
308
+ public getAnalyzerConfig(): IAnalyzerConfig {
309
+ return this.config.analyzer || { enabled: true, useAIRefinement: false, aiModel: 'haiku' };
310
+ }
311
+
312
+ /**
313
+ * Get prioritization weights
314
+ */
315
+ public getPrioritizationWeights(): IPrioritizationWeights {
316
+ return this.config.prioritization || {
317
+ dependencyWeight: 0.3,
318
+ relevanceWeight: 0.4,
319
+ efficiencyWeight: 0.2,
320
+ recencyWeight: 0.1
321
+ };
322
+ }
323
+
324
+ /**
325
+ * Get tier configuration
326
+ */
327
+ public getTierConfig(): ITierConfig {
328
+ return this.config.tiers || {
329
+ essential: { minScore: 0.8, trimLevel: 'none' },
330
+ important: { minScore: 0.5, trimLevel: 'light' },
331
+ optional: { minScore: 0.2, trimLevel: 'aggressive' }
332
+ };
333
+ }
334
+
335
+ /**
336
+ * Clear the config cache (force reload on next access)
337
+ */
338
+ public clearCache(): void {
339
+ this.configCache = null;
340
+ }
209
341
  }
@@ -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
+ }