@yuaone/core 0.1.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/LICENSE +663 -0
- package/README.md +15 -0
- package/dist/__tests__/context-manager.test.d.ts +6 -0
- package/dist/__tests__/context-manager.test.d.ts.map +1 -0
- package/dist/__tests__/context-manager.test.js +220 -0
- package/dist/__tests__/context-manager.test.js.map +1 -0
- package/dist/__tests__/governor.test.d.ts +6 -0
- package/dist/__tests__/governor.test.d.ts.map +1 -0
- package/dist/__tests__/governor.test.js +210 -0
- package/dist/__tests__/governor.test.js.map +1 -0
- package/dist/__tests__/model-router.test.d.ts +6 -0
- package/dist/__tests__/model-router.test.d.ts.map +1 -0
- package/dist/__tests__/model-router.test.js +329 -0
- package/dist/__tests__/model-router.test.js.map +1 -0
- package/dist/agent-logger.d.ts +384 -0
- package/dist/agent-logger.d.ts.map +1 -0
- package/dist/agent-logger.js +820 -0
- package/dist/agent-logger.js.map +1 -0
- package/dist/agent-loop.d.ts +163 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +609 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/agent-modes.d.ts +85 -0
- package/dist/agent-modes.d.ts.map +1 -0
- package/dist/agent-modes.js +418 -0
- package/dist/agent-modes.js.map +1 -0
- package/dist/approval.d.ts +137 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +299 -0
- package/dist/approval.js.map +1 -0
- package/dist/async-completion-queue.d.ts +56 -0
- package/dist/async-completion-queue.d.ts.map +1 -0
- package/dist/async-completion-queue.js +77 -0
- package/dist/async-completion-queue.js.map +1 -0
- package/dist/auto-fix.d.ts +174 -0
- package/dist/auto-fix.d.ts.map +1 -0
- package/dist/auto-fix.js +319 -0
- package/dist/auto-fix.js.map +1 -0
- package/dist/codebase-context.d.ts +396 -0
- package/dist/codebase-context.d.ts.map +1 -0
- package/dist/codebase-context.js +1260 -0
- package/dist/codebase-context.js.map +1 -0
- package/dist/conflict-resolver.d.ts +191 -0
- package/dist/conflict-resolver.d.ts.map +1 -0
- package/dist/conflict-resolver.js +524 -0
- package/dist/conflict-resolver.js.map +1 -0
- package/dist/constants.d.ts +52 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +141 -0
- package/dist/constants.js.map +1 -0
- package/dist/context-budget.d.ts +435 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +903 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/context-compressor.d.ts +143 -0
- package/dist/context-compressor.d.ts.map +1 -0
- package/dist/context-compressor.js +511 -0
- package/dist/context-compressor.js.map +1 -0
- package/dist/context-manager.d.ts +112 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +247 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/continuous-reflection.d.ts +267 -0
- package/dist/continuous-reflection.d.ts.map +1 -0
- package/dist/continuous-reflection.js +338 -0
- package/dist/continuous-reflection.js.map +1 -0
- package/dist/cross-file-refactor.d.ts +352 -0
- package/dist/cross-file-refactor.d.ts.map +1 -0
- package/dist/cross-file-refactor.js +1544 -0
- package/dist/cross-file-refactor.js.map +1 -0
- package/dist/dag-orchestrator.d.ts +138 -0
- package/dist/dag-orchestrator.d.ts.map +1 -0
- package/dist/dag-orchestrator.js +379 -0
- package/dist/dag-orchestrator.js.map +1 -0
- package/dist/debate-orchestrator.d.ts +301 -0
- package/dist/debate-orchestrator.d.ts.map +1 -0
- package/dist/debate-orchestrator.js +719 -0
- package/dist/debate-orchestrator.js.map +1 -0
- package/dist/dependency-analyzer.d.ts +113 -0
- package/dist/dependency-analyzer.d.ts.map +1 -0
- package/dist/dependency-analyzer.js +444 -0
- package/dist/dependency-analyzer.js.map +1 -0
- package/dist/design-loop.d.ts +59 -0
- package/dist/design-loop.d.ts.map +1 -0
- package/dist/design-loop.js +344 -0
- package/dist/design-loop.js.map +1 -0
- package/dist/doc-intelligence.d.ts +383 -0
- package/dist/doc-intelligence.d.ts.map +1 -0
- package/dist/doc-intelligence.js +1307 -0
- package/dist/doc-intelligence.js.map +1 -0
- package/dist/dynamic-role-generator.d.ts +76 -0
- package/dist/dynamic-role-generator.d.ts.map +1 -0
- package/dist/dynamic-role-generator.js +194 -0
- package/dist/dynamic-role-generator.js.map +1 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +102 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-bus.d.ts +159 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +305 -0
- package/dist/event-bus.js.map +1 -0
- package/dist/execution-engine.d.ts +425 -0
- package/dist/execution-engine.d.ts.map +1 -0
- package/dist/execution-engine.js +1555 -0
- package/dist/execution-engine.js.map +1 -0
- package/dist/git-intelligence.d.ts +306 -0
- package/dist/git-intelligence.d.ts.map +1 -0
- package/dist/git-intelligence.js +1099 -0
- package/dist/git-intelligence.js.map +1 -0
- package/dist/governor.d.ts +77 -0
- package/dist/governor.d.ts.map +1 -0
- package/dist/governor.js +161 -0
- package/dist/governor.js.map +1 -0
- package/dist/hierarchical-planner.d.ts +313 -0
- package/dist/hierarchical-planner.d.ts.map +1 -0
- package/dist/hierarchical-planner.js +981 -0
- package/dist/hierarchical-planner.js.map +1 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/intent-inference.d.ts +103 -0
- package/dist/intent-inference.d.ts.map +1 -0
- package/dist/intent-inference.js +605 -0
- package/dist/intent-inference.js.map +1 -0
- package/dist/interrupt-manager.d.ts +143 -0
- package/dist/interrupt-manager.d.ts.map +1 -0
- package/dist/interrupt-manager.js +196 -0
- package/dist/interrupt-manager.js.map +1 -0
- package/dist/kernel.d.ts +564 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +1419 -0
- package/dist/kernel.js.map +1 -0
- package/dist/language-support.d.ts +232 -0
- package/dist/language-support.d.ts.map +1 -0
- package/dist/language-support.js +1134 -0
- package/dist/language-support.js.map +1 -0
- package/dist/llm-client.d.ts +82 -0
- package/dist/llm-client.d.ts.map +1 -0
- package/dist/llm-client.js +475 -0
- package/dist/llm-client.js.map +1 -0
- package/dist/mcp-client.d.ts +232 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +718 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/memory-manager.d.ts +200 -0
- package/dist/memory-manager.d.ts.map +1 -0
- package/dist/memory-manager.js +568 -0
- package/dist/memory-manager.js.map +1 -0
- package/dist/memory.d.ts +87 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +341 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-router.d.ts +245 -0
- package/dist/model-router.d.ts.map +1 -0
- package/dist/model-router.js +632 -0
- package/dist/model-router.js.map +1 -0
- package/dist/parallel-executor.d.ts +125 -0
- package/dist/parallel-executor.d.ts.map +1 -0
- package/dist/parallel-executor.js +201 -0
- package/dist/parallel-executor.js.map +1 -0
- package/dist/perf-optimizer.d.ts +212 -0
- package/dist/perf-optimizer.d.ts.map +1 -0
- package/dist/perf-optimizer.js +721 -0
- package/dist/perf-optimizer.js.map +1 -0
- package/dist/persona.d.ts +305 -0
- package/dist/persona.d.ts.map +1 -0
- package/dist/persona.js +887 -0
- package/dist/persona.js.map +1 -0
- package/dist/planner.d.ts +70 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +264 -0
- package/dist/planner.js.map +1 -0
- package/dist/qa-pipeline.d.ts +365 -0
- package/dist/qa-pipeline.d.ts.map +1 -0
- package/dist/qa-pipeline.js +1352 -0
- package/dist/qa-pipeline.js.map +1 -0
- package/dist/reasoning-adapter.d.ts +116 -0
- package/dist/reasoning-adapter.d.ts.map +1 -0
- package/dist/reasoning-adapter.js +187 -0
- package/dist/reasoning-adapter.js.map +1 -0
- package/dist/role-registry.d.ts +55 -0
- package/dist/role-registry.d.ts.map +1 -0
- package/dist/role-registry.js +192 -0
- package/dist/role-registry.js.map +1 -0
- package/dist/sandbox-tiers.d.ts +327 -0
- package/dist/sandbox-tiers.d.ts.map +1 -0
- package/dist/sandbox-tiers.js +928 -0
- package/dist/sandbox-tiers.js.map +1 -0
- package/dist/security-scanner.d.ts +222 -0
- package/dist/security-scanner.d.ts.map +1 -0
- package/dist/security-scanner.js +1129 -0
- package/dist/security-scanner.js.map +1 -0
- package/dist/security.d.ts +93 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +393 -0
- package/dist/security.js.map +1 -0
- package/dist/self-reflection.d.ts +397 -0
- package/dist/self-reflection.d.ts.map +1 -0
- package/dist/self-reflection.js +908 -0
- package/dist/self-reflection.js.map +1 -0
- package/dist/session-persistence.d.ts +191 -0
- package/dist/session-persistence.d.ts.map +1 -0
- package/dist/session-persistence.js +395 -0
- package/dist/session-persistence.js.map +1 -0
- package/dist/speculative-executor.d.ts +210 -0
- package/dist/speculative-executor.d.ts.map +1 -0
- package/dist/speculative-executor.js +618 -0
- package/dist/speculative-executor.js.map +1 -0
- package/dist/state-machine.d.ts +289 -0
- package/dist/state-machine.d.ts.map +1 -0
- package/dist/state-machine.js +695 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/sub-agent.d.ts +177 -0
- package/dist/sub-agent.d.ts.map +1 -0
- package/dist/sub-agent.js +303 -0
- package/dist/sub-agent.js.map +1 -0
- package/dist/system-prompt.d.ts +26 -0
- package/dist/system-prompt.d.ts.map +1 -0
- package/dist/system-prompt.js +84 -0
- package/dist/system-prompt.js.map +1 -0
- package/dist/test-intelligence.d.ts +439 -0
- package/dist/test-intelligence.d.ts.map +1 -0
- package/dist/test-intelligence.js +1165 -0
- package/dist/test-intelligence.js.map +1 -0
- package/dist/types.d.ts +632 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/vector-index.d.ts +314 -0
- package/dist/vector-index.d.ts.map +1 -0
- package/dist/vector-index.js +618 -0
- package/dist/vector-index.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,1307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module doc-intelligence
|
|
3
|
+
* @description Documentation Intelligence — analyzes code to generate, validate,
|
|
4
|
+
* and maintain documentation automatically.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* 1. **JSDoc/TSDoc Generation** — parse function signatures, generate doc templates
|
|
8
|
+
* 2. **README Generation** — analyze project structure, generate sections
|
|
9
|
+
* 3. **Changelog Generation** — parse conventional commits, group by type
|
|
10
|
+
* 4. **API Documentation** — extract exports, generate API reference
|
|
11
|
+
* 5. **Documentation Quality Analysis** — coverage, freshness, completeness, grading
|
|
12
|
+
*
|
|
13
|
+
* Uses regex-based parsing (no AST libraries). Designed for the YUAN coding agent
|
|
14
|
+
* to automatically maintain documentation alongside code changes.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const di = new DocIntelligence({
|
|
19
|
+
* projectPath: "/path/to/project",
|
|
20
|
+
* srcDirs: ["src"],
|
|
21
|
+
* templateStyle: "jsdoc",
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Analyze doc coverage
|
|
25
|
+
* const files = new Map<string, string>();
|
|
26
|
+
* files.set("src/index.ts", sourceCode);
|
|
27
|
+
* const coverage = di.analyzeCoverage(files);
|
|
28
|
+
* console.log(`Coverage: ${coverage.coveragePercent}% (Grade: ${coverage.grade})`);
|
|
29
|
+
*
|
|
30
|
+
* // Generate JSDoc for a function
|
|
31
|
+
* const jsdoc = di.generateJSDoc("function add(a: number, b: number): number { return a + b; }");
|
|
32
|
+
*
|
|
33
|
+
* // Generate changelog from commits
|
|
34
|
+
* const changelog = di.generateChangelog(commits);
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
import { basename, extname, join } from "node:path";
|
|
38
|
+
// ─── Regex Patterns ──────────────────────────────────────────────
|
|
39
|
+
/** Matches export function declarations */
|
|
40
|
+
const RE_EXPORT_FUNCTION = /^(?:export\s+)?(?:async\s+)?function\s*(\*?)\s*(\w+)\s*(?:<([^>]+)>)?\s*\(([^)]*)\)(?:\s*:\s*([^\n{]+))?\s*\{/gm;
|
|
41
|
+
/** Matches export class declarations */
|
|
42
|
+
const RE_EXPORT_CLASS = /^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s*<([^>]+)>)?(?:\s+extends\s+(\w+(?:<[^>]+>)?))?(?:\s+implements\s+([^\n{]+))?\s*\{/gm;
|
|
43
|
+
/** Matches export interface declarations */
|
|
44
|
+
const RE_EXPORT_INTERFACE = /^(?:export\s+)?interface\s+(\w+)(?:\s*<([^>]+)>)?(?:\s+extends\s+([^\n{]+))?\s*\{/gm;
|
|
45
|
+
/** Matches export type alias declarations */
|
|
46
|
+
const RE_EXPORT_TYPE = /^(?:export\s+)?type\s+(\w+)(?:\s*<([^>]+)>)?\s*=\s*([^\n;]+)/gm;
|
|
47
|
+
/** Matches export enum declarations */
|
|
48
|
+
const RE_EXPORT_ENUM = /^(?:export\s+)?(?:const\s+)?enum\s+(\w+)\s*\{/gm;
|
|
49
|
+
/** Matches export const/let/var declarations */
|
|
50
|
+
const RE_EXPORT_CONST = /^(?:export\s+)(?:const|let|var)\s+(\w+)(?:\s*:\s*([^\n=]+))?\s*=/gm;
|
|
51
|
+
/** Matches JSDoc comment blocks */
|
|
52
|
+
const RE_JSDOC = /\/\*\*\s*([\s\S]*?)\s*\*\//g;
|
|
53
|
+
/** Matches conventional commit messages */
|
|
54
|
+
const RE_CONVENTIONAL_COMMIT = /^(\w+)(?:\(([^)]+)\))?(!?):\s*(.+)$/;
|
|
55
|
+
/** Matches arrow function exports */
|
|
56
|
+
const RE_EXPORT_ARROW = /^(?:export\s+)(?:const|let)\s+(\w+)(?:\s*:\s*([^\n=]+))?\s*=\s*(?:async\s+)?(?:\([^)]*\)|[^=])\s*=>/gm;
|
|
57
|
+
/** Matches method declarations inside classes */
|
|
58
|
+
const RE_METHOD = /^\s+(?:(?:public|private|protected|static|async|readonly)\s+)*(\w+)\s*(?:<([^>]+)>)?\s*\(([^)]*)\)(?:\s*:\s*([^\n{]+))?\s*\{/gm;
|
|
59
|
+
// ─── Constants ───────────────────────────────────────────────────
|
|
60
|
+
/** Grade thresholds (percentage) */
|
|
61
|
+
const GRADE_THRESHOLDS = {
|
|
62
|
+
A: 90,
|
|
63
|
+
B: 75,
|
|
64
|
+
C: 60,
|
|
65
|
+
D: 40,
|
|
66
|
+
F: 0,
|
|
67
|
+
};
|
|
68
|
+
/** Common JSDoc tags */
|
|
69
|
+
const JSDOC_TAGS = {
|
|
70
|
+
param: "@param",
|
|
71
|
+
returns: "@returns",
|
|
72
|
+
throws: "@throws",
|
|
73
|
+
example: "@example",
|
|
74
|
+
deprecated: "@deprecated",
|
|
75
|
+
see: "@see",
|
|
76
|
+
since: "@since",
|
|
77
|
+
};
|
|
78
|
+
/** TSDoc equivalents */
|
|
79
|
+
const TSDOC_TAGS = {
|
|
80
|
+
param: "@param",
|
|
81
|
+
returns: "@returns",
|
|
82
|
+
throws: "@throws",
|
|
83
|
+
example: "@example",
|
|
84
|
+
deprecated: "@deprecated",
|
|
85
|
+
see: "@see",
|
|
86
|
+
since: "@since",
|
|
87
|
+
};
|
|
88
|
+
// ─── DocIntelligence ─────────────────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* DocIntelligence — analyzes code to generate, validate, and maintain
|
|
91
|
+
* documentation automatically.
|
|
92
|
+
*
|
|
93
|
+
* Provides:
|
|
94
|
+
* - Coverage analysis (% of exports with docs, grading A–F)
|
|
95
|
+
* - JSDoc/TSDoc generation from function signatures
|
|
96
|
+
* - README generation from project structure
|
|
97
|
+
* - Changelog generation from conventional commits
|
|
98
|
+
* - API reference generation from exported symbols
|
|
99
|
+
* - Staleness detection (code changed but docs did not)
|
|
100
|
+
*/
|
|
101
|
+
export class DocIntelligence {
|
|
102
|
+
config;
|
|
103
|
+
/**
|
|
104
|
+
* Create a new DocIntelligence instance.
|
|
105
|
+
*
|
|
106
|
+
* @param config - Configuration options
|
|
107
|
+
*/
|
|
108
|
+
constructor(config) {
|
|
109
|
+
this.config = {
|
|
110
|
+
projectPath: config.projectPath,
|
|
111
|
+
srcDirs: config.srcDirs ?? ["src"],
|
|
112
|
+
includePrivate: config.includePrivate ?? false,
|
|
113
|
+
templateStyle: config.templateStyle ?? "jsdoc",
|
|
114
|
+
changelogFormat: config.changelogFormat ?? "keepachangelog",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// ─── Analysis ────────────────────────────────────────────────
|
|
118
|
+
/**
|
|
119
|
+
* Analyze documentation coverage across all provided files.
|
|
120
|
+
*
|
|
121
|
+
* Scans each file for exported symbols and checks whether they have
|
|
122
|
+
* JSDoc comments. Returns coverage stats and a grade (A–F).
|
|
123
|
+
*
|
|
124
|
+
* @param files - Map of file path to file content
|
|
125
|
+
* @returns Documentation coverage analysis
|
|
126
|
+
*/
|
|
127
|
+
analyzeCoverage(files) {
|
|
128
|
+
const allMissing = [];
|
|
129
|
+
const allStale = [];
|
|
130
|
+
let totalExports = 0;
|
|
131
|
+
let documentedExports = 0;
|
|
132
|
+
for (const [filePath, content] of files) {
|
|
133
|
+
const exports = this.parseExports(content, filePath);
|
|
134
|
+
totalExports += exports.length;
|
|
135
|
+
for (const exp of exports) {
|
|
136
|
+
if (exp.hasDoc) {
|
|
137
|
+
documentedExports++;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
allMissing.push({
|
|
141
|
+
symbolName: exp.name,
|
|
142
|
+
symbolType: exp.type,
|
|
143
|
+
filePath: exp.filePath,
|
|
144
|
+
line: exp.line,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Detect stale docs
|
|
149
|
+
const stale = this.detectStaleDoc(content, filePath);
|
|
150
|
+
allStale.push(...stale);
|
|
151
|
+
}
|
|
152
|
+
const coveragePercent = totalExports > 0
|
|
153
|
+
? Math.round((documentedExports / totalExports) * 100)
|
|
154
|
+
: 100;
|
|
155
|
+
const coverage = {
|
|
156
|
+
totalExports,
|
|
157
|
+
documentedExports,
|
|
158
|
+
coveragePercent,
|
|
159
|
+
missing: allMissing,
|
|
160
|
+
stale: allStale,
|
|
161
|
+
grade: "F", // calculated below
|
|
162
|
+
};
|
|
163
|
+
coverage.grade = this.gradeDocumentation(coverage);
|
|
164
|
+
return coverage;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Find undocumented exported symbols in a single file.
|
|
168
|
+
*
|
|
169
|
+
* @param content - File content
|
|
170
|
+
* @param filePath - File path (for reporting)
|
|
171
|
+
* @returns List of missing documentation entries
|
|
172
|
+
*/
|
|
173
|
+
findUndocumented(content, filePath) {
|
|
174
|
+
const exports = this.parseExports(content, filePath);
|
|
175
|
+
return exports
|
|
176
|
+
.filter((e) => !e.hasDoc)
|
|
177
|
+
.map((e) => ({
|
|
178
|
+
symbolName: e.name,
|
|
179
|
+
symbolType: e.type,
|
|
180
|
+
filePath: e.filePath,
|
|
181
|
+
line: e.line,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Detect stale documentation in a file.
|
|
186
|
+
*
|
|
187
|
+
* Checks for common staleness indicators:
|
|
188
|
+
* - @param tags that don't match actual parameters
|
|
189
|
+
* - @returns tag present but function returns void
|
|
190
|
+
* - @deprecated tag on active code
|
|
191
|
+
* - JSDoc description mentions types/names that no longer exist
|
|
192
|
+
*
|
|
193
|
+
* @param content - File content
|
|
194
|
+
* @param filePath - File path (for reporting)
|
|
195
|
+
* @param gitLog - Optional git log output for change detection
|
|
196
|
+
* @returns List of stale documentation entries
|
|
197
|
+
*/
|
|
198
|
+
detectStaleDoc(content, filePath, gitLog) {
|
|
199
|
+
const stale = [];
|
|
200
|
+
const lines = content.split("\n");
|
|
201
|
+
// Find all JSDoc blocks and the symbol they precede
|
|
202
|
+
const jsdocBlocks = this.findAllJSDocBlocks(content);
|
|
203
|
+
for (const block of jsdocBlocks) {
|
|
204
|
+
const symbolLine = block.endLine < lines.length ? lines[block.endLine] : "";
|
|
205
|
+
// Check for param mismatch
|
|
206
|
+
const funcMatch = symbolLine.match(/(?:function\s+(\w+)|(\w+)\s*(?:=\s*(?:async\s+)?\(|:\s*\([^)]*\)\s*=>))\s*(?:<[^>]+>)?\s*\(([^)]*)\)/);
|
|
207
|
+
if (funcMatch) {
|
|
208
|
+
const funcName = funcMatch[1] ?? funcMatch[2] ?? "unknown";
|
|
209
|
+
const paramsStr = funcMatch[3] ?? "";
|
|
210
|
+
const actualParams = this.parseParamNames(paramsStr);
|
|
211
|
+
const docParams = [...block.params.keys()];
|
|
212
|
+
// Params in doc but not in code
|
|
213
|
+
for (const docParam of docParams) {
|
|
214
|
+
if (!actualParams.includes(docParam)) {
|
|
215
|
+
stale.push({
|
|
216
|
+
symbolName: funcName,
|
|
217
|
+
filePath,
|
|
218
|
+
docLine: block.startLine + 1,
|
|
219
|
+
lastCodeChange: `@param ${docParam} documented but not in function signature`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Params in code but not in doc (only flag if doc has ANY @param)
|
|
224
|
+
if (docParams.length > 0) {
|
|
225
|
+
for (const actual of actualParams) {
|
|
226
|
+
if (!block.params.has(actual)) {
|
|
227
|
+
stale.push({
|
|
228
|
+
symbolName: funcName,
|
|
229
|
+
filePath,
|
|
230
|
+
docLine: block.startLine + 1,
|
|
231
|
+
lastCodeChange: `Parameter '${actual}' exists in code but missing from JSDoc`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Check @returns on void function
|
|
238
|
+
if (block.returns) {
|
|
239
|
+
const returnTypeMatch = symbolLine.match(/\):\s*void\s/);
|
|
240
|
+
if (returnTypeMatch) {
|
|
241
|
+
const name = this.extractSymbolName(symbolLine) ?? "unknown";
|
|
242
|
+
stale.push({
|
|
243
|
+
symbolName: name,
|
|
244
|
+
filePath,
|
|
245
|
+
docLine: block.startLine + 1,
|
|
246
|
+
lastCodeChange: "@returns documented but function returns void",
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Git log based staleness
|
|
252
|
+
if (gitLog) {
|
|
253
|
+
const gitStale = this.detectGitBasedStaleness(content, filePath, gitLog);
|
|
254
|
+
stale.push(...gitStale);
|
|
255
|
+
}
|
|
256
|
+
return stale;
|
|
257
|
+
}
|
|
258
|
+
// ─── Generation ──────────────────────────────────────────────
|
|
259
|
+
/**
|
|
260
|
+
* Generate a JSDoc comment for a function/method code snippet.
|
|
261
|
+
*
|
|
262
|
+
* Parses the function signature and produces a template with
|
|
263
|
+
* @param, @returns, @throws, and @example tags.
|
|
264
|
+
*
|
|
265
|
+
* @param functionCode - The function source code
|
|
266
|
+
* @param context - Optional surrounding context for better descriptions
|
|
267
|
+
* @returns Generated JSDoc string
|
|
268
|
+
*/
|
|
269
|
+
generateJSDoc(functionCode, context) {
|
|
270
|
+
const parsed = this.parseFunctionSignature(functionCode);
|
|
271
|
+
if (!parsed) {
|
|
272
|
+
// Try class/interface/type
|
|
273
|
+
return this.generateGenericDoc(functionCode);
|
|
274
|
+
}
|
|
275
|
+
const tags = this.config.templateStyle === "tsdoc" ? TSDOC_TAGS : JSDOC_TAGS;
|
|
276
|
+
const lines = ["/**"];
|
|
277
|
+
// Description placeholder
|
|
278
|
+
const description = this.inferDescription(parsed, context);
|
|
279
|
+
lines.push(` * ${description}`);
|
|
280
|
+
// Generic params
|
|
281
|
+
if (parsed.genericParams) {
|
|
282
|
+
lines.push(` *`);
|
|
283
|
+
const generics = this.parseGenericParams(parsed.genericParams);
|
|
284
|
+
for (const g of generics) {
|
|
285
|
+
lines.push(` * @typeParam ${g} - TODO: describe type parameter`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Params
|
|
289
|
+
if (parsed.params.length > 0) {
|
|
290
|
+
lines.push(` *`);
|
|
291
|
+
for (const param of parsed.params) {
|
|
292
|
+
const typeStr = param.type ? ` {${param.type}}` : "";
|
|
293
|
+
const optStr = param.optional ? " (optional)" : "";
|
|
294
|
+
const defaultStr = param.defaultValue
|
|
295
|
+
? ` (default: ${param.defaultValue})`
|
|
296
|
+
: "";
|
|
297
|
+
const restStr = param.rest ? "..." : "";
|
|
298
|
+
lines.push(` * ${tags.param}${typeStr} ${restStr}${param.name} - TODO: describe${optStr}${defaultStr}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Returns
|
|
302
|
+
if (parsed.returnType && parsed.returnType.trim() !== "void") {
|
|
303
|
+
lines.push(` * ${tags.returns} {${parsed.returnType.trim()}} TODO: describe return value`);
|
|
304
|
+
}
|
|
305
|
+
// Throws placeholder
|
|
306
|
+
if (this.mightThrow(functionCode)) {
|
|
307
|
+
lines.push(` * ${tags.throws} {Error} TODO: describe when this throws`);
|
|
308
|
+
}
|
|
309
|
+
// Example
|
|
310
|
+
lines.push(` *`);
|
|
311
|
+
lines.push(` * ${tags.example}`);
|
|
312
|
+
lines.push(` * \`\`\`typescript`);
|
|
313
|
+
lines.push(` * ${this.generateExampleCall(parsed)}`);
|
|
314
|
+
lines.push(` * \`\`\``);
|
|
315
|
+
lines.push(` */`);
|
|
316
|
+
return lines.join("\n");
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Generate a README document from project information.
|
|
320
|
+
*
|
|
321
|
+
* Creates sections: Overview, Installation, Usage, API Reference,
|
|
322
|
+
* Configuration, and License.
|
|
323
|
+
*
|
|
324
|
+
* @param projectInfo - Project metadata and structure
|
|
325
|
+
* @returns Generated README document
|
|
326
|
+
*/
|
|
327
|
+
generateReadme(projectInfo) {
|
|
328
|
+
const sections = [];
|
|
329
|
+
const pkg = projectInfo.packageJson;
|
|
330
|
+
// Title
|
|
331
|
+
sections.push(`# ${projectInfo.name}`);
|
|
332
|
+
sections.push("");
|
|
333
|
+
if (projectInfo.description) {
|
|
334
|
+
sections.push(projectInfo.description);
|
|
335
|
+
sections.push("");
|
|
336
|
+
}
|
|
337
|
+
// Badges
|
|
338
|
+
const version = pkg.version;
|
|
339
|
+
if (version) {
|
|
340
|
+
sections.push(``);
|
|
341
|
+
sections.push("");
|
|
342
|
+
}
|
|
343
|
+
// Overview
|
|
344
|
+
sections.push("## Overview");
|
|
345
|
+
sections.push("");
|
|
346
|
+
sections.push(projectInfo.description || "TODO: Add project overview.");
|
|
347
|
+
sections.push("");
|
|
348
|
+
// Installation
|
|
349
|
+
sections.push("## Installation");
|
|
350
|
+
sections.push("");
|
|
351
|
+
const pkgManager = pkg.packageManager
|
|
352
|
+
? String(pkg.packageManager).split("@")[0]
|
|
353
|
+
: "npm";
|
|
354
|
+
sections.push("```bash");
|
|
355
|
+
if (pkgManager === "pnpm") {
|
|
356
|
+
sections.push(`pnpm add ${projectInfo.name}`);
|
|
357
|
+
}
|
|
358
|
+
else if (pkgManager === "yarn") {
|
|
359
|
+
sections.push(`yarn add ${projectInfo.name}`);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
sections.push(`npm install ${projectInfo.name}`);
|
|
363
|
+
}
|
|
364
|
+
sections.push("```");
|
|
365
|
+
sections.push("");
|
|
366
|
+
// Usage
|
|
367
|
+
sections.push("## Usage");
|
|
368
|
+
sections.push("");
|
|
369
|
+
sections.push("```typescript");
|
|
370
|
+
sections.push(`import { /* ... */ } from "${projectInfo.name}";`);
|
|
371
|
+
sections.push("");
|
|
372
|
+
sections.push("// TODO: Add usage examples");
|
|
373
|
+
sections.push("```");
|
|
374
|
+
sections.push("");
|
|
375
|
+
// API Reference
|
|
376
|
+
sections.push("## API Reference");
|
|
377
|
+
sections.push("");
|
|
378
|
+
const srcExts = [".ts", ".tsx", ".js", ".jsx"];
|
|
379
|
+
const mainFiles = projectInfo.srcFiles.filter((f) => srcExts.includes(extname(f)) &&
|
|
380
|
+
!f.includes(".test.") &&
|
|
381
|
+
!f.includes(".spec.") &&
|
|
382
|
+
!f.includes("__tests__"));
|
|
383
|
+
if (mainFiles.length > 0) {
|
|
384
|
+
sections.push("### Modules");
|
|
385
|
+
sections.push("");
|
|
386
|
+
for (const f of mainFiles.slice(0, 20)) {
|
|
387
|
+
const name = basename(f, extname(f));
|
|
388
|
+
sections.push(`- \`${name}\` — TODO: describe`);
|
|
389
|
+
}
|
|
390
|
+
sections.push("");
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
sections.push("TODO: Add API reference.");
|
|
394
|
+
sections.push("");
|
|
395
|
+
}
|
|
396
|
+
// CLI section
|
|
397
|
+
if (projectInfo.hasCLI) {
|
|
398
|
+
sections.push("## CLI");
|
|
399
|
+
sections.push("");
|
|
400
|
+
const binEntry = pkg.bin;
|
|
401
|
+
if (binEntry && typeof binEntry === "object") {
|
|
402
|
+
for (const [cmd] of Object.entries(binEntry)) {
|
|
403
|
+
sections.push(`### \`${cmd}\``);
|
|
404
|
+
sections.push("");
|
|
405
|
+
sections.push("```bash");
|
|
406
|
+
sections.push(`${cmd} --help`);
|
|
407
|
+
sections.push("```");
|
|
408
|
+
sections.push("");
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
sections.push("```bash");
|
|
413
|
+
sections.push(`npx ${projectInfo.name} --help`);
|
|
414
|
+
sections.push("```");
|
|
415
|
+
sections.push("");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Configuration
|
|
419
|
+
sections.push("## Configuration");
|
|
420
|
+
sections.push("");
|
|
421
|
+
sections.push("TODO: Add configuration documentation.");
|
|
422
|
+
sections.push("");
|
|
423
|
+
// Scripts
|
|
424
|
+
const scripts = pkg.scripts;
|
|
425
|
+
if (scripts && Object.keys(scripts).length > 0) {
|
|
426
|
+
sections.push("## Development");
|
|
427
|
+
sections.push("");
|
|
428
|
+
sections.push("```bash");
|
|
429
|
+
for (const [name, cmd] of Object.entries(scripts).slice(0, 10)) {
|
|
430
|
+
sections.push(`# ${name}`);
|
|
431
|
+
sections.push(`${pkgManager} run ${name}`);
|
|
432
|
+
sections.push("");
|
|
433
|
+
}
|
|
434
|
+
sections.push("```");
|
|
435
|
+
sections.push("");
|
|
436
|
+
}
|
|
437
|
+
// License
|
|
438
|
+
const license = pkg.license;
|
|
439
|
+
if (license) {
|
|
440
|
+
sections.push("## License");
|
|
441
|
+
sections.push("");
|
|
442
|
+
sections.push(`${license}`);
|
|
443
|
+
sections.push("");
|
|
444
|
+
}
|
|
445
|
+
const readmePath = join(this.config.projectPath, "README.md");
|
|
446
|
+
return {
|
|
447
|
+
type: "readme",
|
|
448
|
+
content: sections.join("\n"),
|
|
449
|
+
targetFile: readmePath,
|
|
450
|
+
isNew: true, // Caller should check if file exists
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Generate a changelog entry from a list of commits.
|
|
455
|
+
*
|
|
456
|
+
* Parses conventional commit messages and groups them by type.
|
|
457
|
+
* Determines the recommended semver bump based on commit types.
|
|
458
|
+
*
|
|
459
|
+
* @param commits - List of commit information
|
|
460
|
+
* @returns Structured changelog entry
|
|
461
|
+
*/
|
|
462
|
+
generateChangelog(commits) {
|
|
463
|
+
const sections = {
|
|
464
|
+
breaking: [],
|
|
465
|
+
features: [],
|
|
466
|
+
fixes: [],
|
|
467
|
+
refactors: [],
|
|
468
|
+
docs: [],
|
|
469
|
+
other: [],
|
|
470
|
+
};
|
|
471
|
+
let hasBreaking = false;
|
|
472
|
+
let hasFeature = false;
|
|
473
|
+
for (const commit of commits) {
|
|
474
|
+
const parsed = this.parseConventionalCommit(commit.message);
|
|
475
|
+
if (!parsed) {
|
|
476
|
+
// Non-conventional commit
|
|
477
|
+
sections.other.push(commit.message.split("\n")[0]);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
const scope = parsed.scope ? `**${parsed.scope}**: ` : "";
|
|
481
|
+
const entry = `${scope}${parsed.description} (${commit.hash.slice(0, 7)})`;
|
|
482
|
+
if (parsed.breaking) {
|
|
483
|
+
hasBreaking = true;
|
|
484
|
+
sections.breaking.push(entry);
|
|
485
|
+
}
|
|
486
|
+
switch (parsed.type) {
|
|
487
|
+
case "feat":
|
|
488
|
+
hasFeature = true;
|
|
489
|
+
sections.features.push(entry);
|
|
490
|
+
break;
|
|
491
|
+
case "fix":
|
|
492
|
+
sections.fixes.push(entry);
|
|
493
|
+
break;
|
|
494
|
+
case "refactor":
|
|
495
|
+
case "perf":
|
|
496
|
+
sections.refactors.push(entry);
|
|
497
|
+
break;
|
|
498
|
+
case "docs":
|
|
499
|
+
sections.docs.push(entry);
|
|
500
|
+
break;
|
|
501
|
+
default:
|
|
502
|
+
sections.other.push(entry);
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const semverBump = hasBreaking
|
|
507
|
+
? "major"
|
|
508
|
+
: hasFeature
|
|
509
|
+
? "minor"
|
|
510
|
+
: "patch";
|
|
511
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
512
|
+
return {
|
|
513
|
+
version: "Unreleased",
|
|
514
|
+
date: today,
|
|
515
|
+
sections,
|
|
516
|
+
semverBump,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Generate an API reference document from exported symbols.
|
|
521
|
+
*
|
|
522
|
+
* Creates a markdown document with sections for each export type
|
|
523
|
+
* (functions, classes, interfaces, types, enums, constants).
|
|
524
|
+
*
|
|
525
|
+
* @param exports - List of exported symbol information
|
|
526
|
+
* @returns Generated API reference document
|
|
527
|
+
*/
|
|
528
|
+
generateAPIReference(exports) {
|
|
529
|
+
const sections = [];
|
|
530
|
+
sections.push("# API Reference");
|
|
531
|
+
sections.push("");
|
|
532
|
+
// Group by type
|
|
533
|
+
const grouped = new Map();
|
|
534
|
+
for (const exp of exports) {
|
|
535
|
+
const list = grouped.get(exp.type) ?? [];
|
|
536
|
+
list.push(exp);
|
|
537
|
+
grouped.set(exp.type, list);
|
|
538
|
+
}
|
|
539
|
+
// Render order
|
|
540
|
+
const order = [
|
|
541
|
+
"class",
|
|
542
|
+
"function",
|
|
543
|
+
"interface",
|
|
544
|
+
"type",
|
|
545
|
+
"enum",
|
|
546
|
+
"const",
|
|
547
|
+
];
|
|
548
|
+
const typeLabels = {
|
|
549
|
+
class: "Classes",
|
|
550
|
+
function: "Functions",
|
|
551
|
+
interface: "Interfaces",
|
|
552
|
+
type: "Type Aliases",
|
|
553
|
+
enum: "Enums",
|
|
554
|
+
const: "Constants",
|
|
555
|
+
};
|
|
556
|
+
for (const type of order) {
|
|
557
|
+
const items = grouped.get(type);
|
|
558
|
+
if (!items || items.length === 0)
|
|
559
|
+
continue;
|
|
560
|
+
sections.push(`## ${typeLabels[type]}`);
|
|
561
|
+
sections.push("");
|
|
562
|
+
for (const item of items) {
|
|
563
|
+
sections.push(`### \`${item.name}\``);
|
|
564
|
+
sections.push("");
|
|
565
|
+
// File location
|
|
566
|
+
sections.push(`> Defined in \`${item.filePath}\` (line ${item.line})`);
|
|
567
|
+
sections.push("");
|
|
568
|
+
// Signature
|
|
569
|
+
sections.push("```typescript");
|
|
570
|
+
sections.push(item.signature);
|
|
571
|
+
sections.push("```");
|
|
572
|
+
sections.push("");
|
|
573
|
+
// Existing doc
|
|
574
|
+
if (item.docContent) {
|
|
575
|
+
// Extract description from JSDoc
|
|
576
|
+
const desc = item.docContent
|
|
577
|
+
.replace(/\/\*\*|\*\//g, "")
|
|
578
|
+
.replace(/^\s*\*\s?/gm, "")
|
|
579
|
+
.replace(/@\w+.*$/gm, "")
|
|
580
|
+
.trim();
|
|
581
|
+
if (desc) {
|
|
582
|
+
sections.push(desc);
|
|
583
|
+
sections.push("");
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
sections.push("---");
|
|
587
|
+
sections.push("");
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const refPath = join(this.config.projectPath, "docs", "API.md");
|
|
591
|
+
return {
|
|
592
|
+
type: "api-reference",
|
|
593
|
+
content: sections.join("\n"),
|
|
594
|
+
targetFile: refPath,
|
|
595
|
+
isNew: true,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
// ─── Helpers ─────────────────────────────────────────────────
|
|
599
|
+
/**
|
|
600
|
+
* Parse all exported symbols from a file's content.
|
|
601
|
+
*
|
|
602
|
+
* Uses regex patterns to detect functions, classes, interfaces,
|
|
603
|
+
* type aliases, enums, and const exports. Checks whether each
|
|
604
|
+
* symbol has a preceding JSDoc comment.
|
|
605
|
+
*
|
|
606
|
+
* @param content - File content
|
|
607
|
+
* @param filePath - File path (for reporting)
|
|
608
|
+
* @returns List of exported symbols
|
|
609
|
+
*/
|
|
610
|
+
parseExports(content, filePath) {
|
|
611
|
+
const exports = [];
|
|
612
|
+
const lines = content.split("\n");
|
|
613
|
+
// Helper: check if line has preceding JSDoc
|
|
614
|
+
const hasJSDoc = (lineIdx) => {
|
|
615
|
+
if (lineIdx <= 0)
|
|
616
|
+
return { has: false };
|
|
617
|
+
// Walk backwards to find JSDoc end (*/)
|
|
618
|
+
let idx = lineIdx - 1;
|
|
619
|
+
while (idx >= 0 && lines[idx].trim() === "") {
|
|
620
|
+
idx--;
|
|
621
|
+
}
|
|
622
|
+
if (idx < 0)
|
|
623
|
+
return { has: false };
|
|
624
|
+
const endLine = lines[idx].trim();
|
|
625
|
+
if (!endLine.endsWith("*/"))
|
|
626
|
+
return { has: false };
|
|
627
|
+
// Find JSDoc start (/**)
|
|
628
|
+
let startIdx = idx;
|
|
629
|
+
while (startIdx >= 0 && !lines[startIdx].includes("/**")) {
|
|
630
|
+
startIdx--;
|
|
631
|
+
}
|
|
632
|
+
if (startIdx < 0)
|
|
633
|
+
return { has: false };
|
|
634
|
+
const docLines = lines.slice(startIdx, idx + 1);
|
|
635
|
+
return { has: true, doc: docLines.join("\n") };
|
|
636
|
+
};
|
|
637
|
+
// Extract exports of each type
|
|
638
|
+
this.extractWithRegex(content, RE_EXPORT_FUNCTION, lines, "function", (match, lineNum) => {
|
|
639
|
+
const name = match[2];
|
|
640
|
+
const generics = match[3] ? `<${match[3]}>` : "";
|
|
641
|
+
const params = match[4];
|
|
642
|
+
const returnType = match[5] ?? "";
|
|
643
|
+
const isAsync = match[0].includes("async ");
|
|
644
|
+
const generator = match[1] === "*" ? "*" : "";
|
|
645
|
+
const asyncStr = isAsync ? "async " : "";
|
|
646
|
+
const retStr = returnType ? `: ${returnType.trim()}` : "";
|
|
647
|
+
const sig = `${asyncStr}function ${generator}${name}${generics}(${params})${retStr}`;
|
|
648
|
+
const doc = hasJSDoc(lineNum);
|
|
649
|
+
exports.push({
|
|
650
|
+
name,
|
|
651
|
+
type: "function",
|
|
652
|
+
filePath,
|
|
653
|
+
line: lineNum + 1,
|
|
654
|
+
signature: sig,
|
|
655
|
+
hasDoc: doc.has,
|
|
656
|
+
docContent: doc.doc,
|
|
657
|
+
});
|
|
658
|
+
}, this.config.includePrivate);
|
|
659
|
+
this.extractWithRegex(content, RE_EXPORT_CLASS, lines, "class", (match, lineNum) => {
|
|
660
|
+
const name = match[1];
|
|
661
|
+
const generics = match[2] ? `<${match[2]}>` : "";
|
|
662
|
+
const ext = match[3] ? ` extends ${match[3]}` : "";
|
|
663
|
+
const impl = match[4] ? ` implements ${match[4].trim()}` : "";
|
|
664
|
+
const abstract = match[0].includes("abstract ") ? "abstract " : "";
|
|
665
|
+
const sig = `${abstract}class ${name}${generics}${ext}${impl}`;
|
|
666
|
+
const doc = hasJSDoc(lineNum);
|
|
667
|
+
exports.push({
|
|
668
|
+
name,
|
|
669
|
+
type: "class",
|
|
670
|
+
filePath,
|
|
671
|
+
line: lineNum + 1,
|
|
672
|
+
signature: sig,
|
|
673
|
+
hasDoc: doc.has,
|
|
674
|
+
docContent: doc.doc,
|
|
675
|
+
});
|
|
676
|
+
}, this.config.includePrivate);
|
|
677
|
+
this.extractWithRegex(content, RE_EXPORT_INTERFACE, lines, "interface", (match, lineNum) => {
|
|
678
|
+
const name = match[1];
|
|
679
|
+
const generics = match[2] ? `<${match[2]}>` : "";
|
|
680
|
+
const ext = match[3] ? ` extends ${match[3].trim()}` : "";
|
|
681
|
+
const sig = `interface ${name}${generics}${ext}`;
|
|
682
|
+
const doc = hasJSDoc(lineNum);
|
|
683
|
+
exports.push({
|
|
684
|
+
name,
|
|
685
|
+
type: "interface",
|
|
686
|
+
filePath,
|
|
687
|
+
line: lineNum + 1,
|
|
688
|
+
signature: sig,
|
|
689
|
+
hasDoc: doc.has,
|
|
690
|
+
docContent: doc.doc,
|
|
691
|
+
});
|
|
692
|
+
}, this.config.includePrivate);
|
|
693
|
+
this.extractWithRegex(content, RE_EXPORT_TYPE, lines, "type", (match, lineNum) => {
|
|
694
|
+
const name = match[1];
|
|
695
|
+
const generics = match[2] ? `<${match[2]}>` : "";
|
|
696
|
+
const value = match[3].trim();
|
|
697
|
+
const sig = `type ${name}${generics} = ${value}`;
|
|
698
|
+
const doc = hasJSDoc(lineNum);
|
|
699
|
+
exports.push({
|
|
700
|
+
name,
|
|
701
|
+
type: "type",
|
|
702
|
+
filePath,
|
|
703
|
+
line: lineNum + 1,
|
|
704
|
+
signature: sig,
|
|
705
|
+
hasDoc: doc.has,
|
|
706
|
+
docContent: doc.doc,
|
|
707
|
+
});
|
|
708
|
+
}, this.config.includePrivate);
|
|
709
|
+
this.extractWithRegex(content, RE_EXPORT_ENUM, lines, "enum", (match, lineNum) => {
|
|
710
|
+
const name = match[1];
|
|
711
|
+
const isConst = match[0].includes("const ");
|
|
712
|
+
const sig = `${isConst ? "const " : ""}enum ${name}`;
|
|
713
|
+
const doc = hasJSDoc(lineNum);
|
|
714
|
+
exports.push({
|
|
715
|
+
name,
|
|
716
|
+
type: "enum",
|
|
717
|
+
filePath,
|
|
718
|
+
line: lineNum + 1,
|
|
719
|
+
signature: sig,
|
|
720
|
+
hasDoc: doc.has,
|
|
721
|
+
docContent: doc.doc,
|
|
722
|
+
});
|
|
723
|
+
}, this.config.includePrivate);
|
|
724
|
+
this.extractWithRegex(content, RE_EXPORT_CONST, lines, "const", (match, lineNum) => {
|
|
725
|
+
const name = match[1];
|
|
726
|
+
const typeAnnotation = match[2] ? `: ${match[2].trim()}` : "";
|
|
727
|
+
const sig = `const ${name}${typeAnnotation}`;
|
|
728
|
+
const doc = hasJSDoc(lineNum);
|
|
729
|
+
exports.push({
|
|
730
|
+
name,
|
|
731
|
+
type: "const",
|
|
732
|
+
filePath,
|
|
733
|
+
line: lineNum + 1,
|
|
734
|
+
signature: sig,
|
|
735
|
+
hasDoc: doc.has,
|
|
736
|
+
docContent: doc.doc,
|
|
737
|
+
});
|
|
738
|
+
}, this.config.includePrivate);
|
|
739
|
+
return exports;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Parse a conventional commit message.
|
|
743
|
+
*
|
|
744
|
+
* Supports the format: `type(scope)!: description`
|
|
745
|
+
* where scope and `!` (breaking) are optional.
|
|
746
|
+
*
|
|
747
|
+
* @param message - Commit message (first line)
|
|
748
|
+
* @returns Parsed commit or null if not conventional format
|
|
749
|
+
*/
|
|
750
|
+
parseConventionalCommit(message) {
|
|
751
|
+
const firstLine = message.split("\n")[0].trim();
|
|
752
|
+
const match = firstLine.match(RE_CONVENTIONAL_COMMIT);
|
|
753
|
+
if (!match)
|
|
754
|
+
return null;
|
|
755
|
+
const type = match[1];
|
|
756
|
+
const scope = match[2] || undefined;
|
|
757
|
+
const bangBreaking = match[3] === "!";
|
|
758
|
+
const description = match[4];
|
|
759
|
+
// Check body for BREAKING CHANGE footer
|
|
760
|
+
const bodyLines = message.split("\n").slice(1).join("\n").trim();
|
|
761
|
+
const footerBreaking = bodyLines.includes("BREAKING CHANGE:");
|
|
762
|
+
return {
|
|
763
|
+
type,
|
|
764
|
+
scope,
|
|
765
|
+
description,
|
|
766
|
+
breaking: bangBreaking || footerBreaking,
|
|
767
|
+
body: bodyLines || undefined,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Grade documentation quality based on coverage metrics.
|
|
772
|
+
*
|
|
773
|
+
* Grading scale:
|
|
774
|
+
* - A: >= 90% coverage
|
|
775
|
+
* - B: >= 75% coverage
|
|
776
|
+
* - C: >= 60% coverage
|
|
777
|
+
* - D: >= 40% coverage
|
|
778
|
+
* - F: < 40% coverage
|
|
779
|
+
*
|
|
780
|
+
* Stale docs reduce the grade by one level per 10 stale entries.
|
|
781
|
+
*
|
|
782
|
+
* @param coverage - Documentation coverage data
|
|
783
|
+
* @returns Documentation grade
|
|
784
|
+
*/
|
|
785
|
+
gradeDocumentation(coverage) {
|
|
786
|
+
let effectivePercent = coverage.coveragePercent;
|
|
787
|
+
// Penalize for stale docs (each stale entry reduces effective coverage by 2%)
|
|
788
|
+
const stalePenalty = Math.min(coverage.stale.length * 2, 20);
|
|
789
|
+
effectivePercent = Math.max(0, effectivePercent - stalePenalty);
|
|
790
|
+
if (effectivePercent >= GRADE_THRESHOLDS.A)
|
|
791
|
+
return "A";
|
|
792
|
+
if (effectivePercent >= GRADE_THRESHOLDS.B)
|
|
793
|
+
return "B";
|
|
794
|
+
if (effectivePercent >= GRADE_THRESHOLDS.C)
|
|
795
|
+
return "C";
|
|
796
|
+
if (effectivePercent >= GRADE_THRESHOLDS.D)
|
|
797
|
+
return "D";
|
|
798
|
+
return "F";
|
|
799
|
+
}
|
|
800
|
+
// ─── Private: Regex Extraction ─────────────────────────────
|
|
801
|
+
/**
|
|
802
|
+
* Execute a regex against content and invoke callback for each match.
|
|
803
|
+
* If includePrivate is false, only matches that start with `export` are included.
|
|
804
|
+
*/
|
|
805
|
+
extractWithRegex(content, regex, lines, _type, callback, includePrivate) {
|
|
806
|
+
// Reset regex state
|
|
807
|
+
const re = new RegExp(regex.source, regex.flags);
|
|
808
|
+
let match;
|
|
809
|
+
while ((match = re.exec(content)) !== null) {
|
|
810
|
+
// Check export keyword
|
|
811
|
+
if (!includePrivate && !match[0].startsWith("export")) {
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
// Calculate line number
|
|
815
|
+
const lineNum = content.slice(0, match.index).split("\n").length - 1;
|
|
816
|
+
callback(match, lineNum);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
// ─── Private: Function Parsing ─────────────────────────────
|
|
820
|
+
/**
|
|
821
|
+
* Parse a function/method signature from code.
|
|
822
|
+
*/
|
|
823
|
+
parseFunctionSignature(code) {
|
|
824
|
+
// Named function
|
|
825
|
+
const funcMatch = code.match(/(?:export\s+)?(?:async\s+)?function\s*(\*?)\s*(\w+)\s*(?:<([^>]+)>)?\s*\(([^)]*)\)(?:\s*:\s*([^\n{]+))?/);
|
|
826
|
+
if (funcMatch) {
|
|
827
|
+
return {
|
|
828
|
+
name: funcMatch[2],
|
|
829
|
+
params: this.parseParams(funcMatch[4]),
|
|
830
|
+
returnType: funcMatch[5]?.trim() ?? "void",
|
|
831
|
+
isAsync: code.trimStart().startsWith("async") || /\basync\s+function\b/.test(code),
|
|
832
|
+
isGenerator: funcMatch[1] === "*",
|
|
833
|
+
genericParams: funcMatch[3],
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
// Arrow function
|
|
837
|
+
const arrowMatch = code.match(/(?:export\s+)?(?:const|let)\s+(\w+)(?:\s*:\s*([^\n=]+))?\s*=\s*(async\s+)?\(([^)]*)\)(?:\s*:\s*([^\n=>{]+))?\s*=>/);
|
|
838
|
+
if (arrowMatch) {
|
|
839
|
+
return {
|
|
840
|
+
name: arrowMatch[1],
|
|
841
|
+
params: this.parseParams(arrowMatch[4]),
|
|
842
|
+
returnType: arrowMatch[5]?.trim() ?? "void",
|
|
843
|
+
isAsync: !!arrowMatch[3],
|
|
844
|
+
isGenerator: false,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
// Method
|
|
848
|
+
const methodMatch = code.match(/^\s*(?:(?:public|private|protected|static|async|readonly)\s+)*(\w+)\s*(?:<([^>]+)>)?\s*\(([^)]*)\)(?:\s*:\s*([^\n{]+))?/m);
|
|
849
|
+
if (methodMatch) {
|
|
850
|
+
return {
|
|
851
|
+
name: methodMatch[1],
|
|
852
|
+
params: this.parseParams(methodMatch[3]),
|
|
853
|
+
returnType: methodMatch[4]?.trim() ?? "void",
|
|
854
|
+
isAsync: /\basync\b/.test(code.slice(0, code.indexOf(methodMatch[1]))),
|
|
855
|
+
isGenerator: false,
|
|
856
|
+
genericParams: methodMatch[2],
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Parse function parameters from a parameter string.
|
|
863
|
+
*/
|
|
864
|
+
parseParams(paramsStr) {
|
|
865
|
+
if (!paramsStr.trim())
|
|
866
|
+
return [];
|
|
867
|
+
const params = [];
|
|
868
|
+
// Split respecting nested generics and destructuring
|
|
869
|
+
const parts = this.splitParams(paramsStr);
|
|
870
|
+
for (const part of parts) {
|
|
871
|
+
const trimmed = part.trim();
|
|
872
|
+
if (!trimmed)
|
|
873
|
+
continue;
|
|
874
|
+
const rest = trimmed.startsWith("...");
|
|
875
|
+
const cleaned = rest ? trimmed.slice(3) : trimmed;
|
|
876
|
+
// Check for default value
|
|
877
|
+
const eqIdx = this.findTopLevelChar(cleaned, "=");
|
|
878
|
+
let nameAndType = cleaned;
|
|
879
|
+
let defaultValue;
|
|
880
|
+
if (eqIdx !== -1) {
|
|
881
|
+
nameAndType = cleaned.slice(0, eqIdx).trim();
|
|
882
|
+
defaultValue = cleaned.slice(eqIdx + 1).trim();
|
|
883
|
+
}
|
|
884
|
+
// Check for optional marker and type annotation
|
|
885
|
+
const colonIdx = this.findTopLevelChar(nameAndType, ":");
|
|
886
|
+
let name;
|
|
887
|
+
let type = "";
|
|
888
|
+
let optional = false;
|
|
889
|
+
if (colonIdx !== -1) {
|
|
890
|
+
name = nameAndType.slice(0, colonIdx).trim();
|
|
891
|
+
type = nameAndType.slice(colonIdx + 1).trim();
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
name = nameAndType.trim();
|
|
895
|
+
}
|
|
896
|
+
if (name.endsWith("?")) {
|
|
897
|
+
optional = true;
|
|
898
|
+
name = name.slice(0, -1);
|
|
899
|
+
}
|
|
900
|
+
if (defaultValue !== undefined) {
|
|
901
|
+
optional = true;
|
|
902
|
+
}
|
|
903
|
+
params.push({ name, type, optional, defaultValue, rest });
|
|
904
|
+
}
|
|
905
|
+
return params;
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Split parameter string respecting nested brackets/generics.
|
|
909
|
+
*/
|
|
910
|
+
splitParams(str) {
|
|
911
|
+
const parts = [];
|
|
912
|
+
let depth = 0;
|
|
913
|
+
let current = "";
|
|
914
|
+
for (const ch of str) {
|
|
915
|
+
if (ch === "(" || ch === "<" || ch === "{" || ch === "[") {
|
|
916
|
+
depth++;
|
|
917
|
+
current += ch;
|
|
918
|
+
}
|
|
919
|
+
else if (ch === ")" || ch === ">" || ch === "}" || ch === "]") {
|
|
920
|
+
depth--;
|
|
921
|
+
current += ch;
|
|
922
|
+
}
|
|
923
|
+
else if (ch === "," && depth === 0) {
|
|
924
|
+
parts.push(current);
|
|
925
|
+
current = "";
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
current += ch;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (current.trim()) {
|
|
932
|
+
parts.push(current);
|
|
933
|
+
}
|
|
934
|
+
return parts;
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Find a top-level character (not nested in brackets).
|
|
938
|
+
*/
|
|
939
|
+
findTopLevelChar(str, char) {
|
|
940
|
+
let depth = 0;
|
|
941
|
+
for (let i = 0; i < str.length; i++) {
|
|
942
|
+
const ch = str[i];
|
|
943
|
+
if (ch === "(" || ch === "<" || ch === "{" || ch === "[")
|
|
944
|
+
depth++;
|
|
945
|
+
else if (ch === ")" || ch === ">" || ch === "}" || ch === "]")
|
|
946
|
+
depth--;
|
|
947
|
+
else if (ch === char && depth === 0)
|
|
948
|
+
return i;
|
|
949
|
+
}
|
|
950
|
+
return -1;
|
|
951
|
+
}
|
|
952
|
+
// ─── Private: JSDoc Parsing ────────────────────────────────
|
|
953
|
+
/**
|
|
954
|
+
* Find all JSDoc blocks in content, returning their position and parsed tags.
|
|
955
|
+
*/
|
|
956
|
+
findAllJSDocBlocks(content) {
|
|
957
|
+
const blocks = [];
|
|
958
|
+
const lines = content.split("\n");
|
|
959
|
+
let i = 0;
|
|
960
|
+
while (i < lines.length) {
|
|
961
|
+
const line = lines[i];
|
|
962
|
+
if (line.trim().startsWith("/**")) {
|
|
963
|
+
const startLine = i;
|
|
964
|
+
let endLine = i;
|
|
965
|
+
// Find end of JSDoc
|
|
966
|
+
while (endLine < lines.length && !lines[endLine].includes("*/")) {
|
|
967
|
+
endLine++;
|
|
968
|
+
}
|
|
969
|
+
const docLines = lines.slice(startLine, endLine + 1);
|
|
970
|
+
const docContent = docLines.join("\n");
|
|
971
|
+
const analysis = this.analyzeJSDoc(docContent, startLine);
|
|
972
|
+
// endLine for the symbol = the line after the closing */
|
|
973
|
+
analysis.endLine = endLine + 1;
|
|
974
|
+
blocks.push(analysis);
|
|
975
|
+
i = endLine + 1;
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
i++;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return blocks;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Analyze a JSDoc comment block.
|
|
985
|
+
*/
|
|
986
|
+
analyzeJSDoc(docContent, startLine) {
|
|
987
|
+
const params = new Map();
|
|
988
|
+
const throws = [];
|
|
989
|
+
const examples = [];
|
|
990
|
+
let returns;
|
|
991
|
+
let description = "";
|
|
992
|
+
// Strip comment markers
|
|
993
|
+
const cleaned = docContent
|
|
994
|
+
.replace(/\/\*\*|\*\//g, "")
|
|
995
|
+
.replace(/^\s*\*\s?/gm, "");
|
|
996
|
+
// Extract description (text before first tag)
|
|
997
|
+
const firstTagIdx = cleaned.search(/^@/m);
|
|
998
|
+
if (firstTagIdx === -1) {
|
|
999
|
+
description = cleaned.trim();
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
description = cleaned.slice(0, firstTagIdx).trim();
|
|
1003
|
+
}
|
|
1004
|
+
// Extract @param tags
|
|
1005
|
+
const paramMatches = cleaned.matchAll(/@param\s+(?:\{[^}]*\}\s+)?(\w+)\s*-?\s*(.*)/g);
|
|
1006
|
+
for (const m of paramMatches) {
|
|
1007
|
+
params.set(m[1], m[2].trim());
|
|
1008
|
+
}
|
|
1009
|
+
// Extract @returns
|
|
1010
|
+
const returnsMatch = cleaned.match(/@returns?\s+(?:\{[^}]*\}\s+)?(.*)/);
|
|
1011
|
+
if (returnsMatch) {
|
|
1012
|
+
returns = returnsMatch[1].trim();
|
|
1013
|
+
}
|
|
1014
|
+
// Extract @throws
|
|
1015
|
+
const throwsMatches = cleaned.matchAll(/@throws?\s+(?:\{[^}]*\}\s+)?(.*)/g);
|
|
1016
|
+
for (const m of throwsMatches) {
|
|
1017
|
+
throws.push(m[1].trim());
|
|
1018
|
+
}
|
|
1019
|
+
// Extract @example
|
|
1020
|
+
const exampleMatches = cleaned.matchAll(/@example\s*([\s\S]*?)(?=@\w|$)/g);
|
|
1021
|
+
for (const m of exampleMatches) {
|
|
1022
|
+
examples.push(m[1].trim());
|
|
1023
|
+
}
|
|
1024
|
+
return {
|
|
1025
|
+
exists: true,
|
|
1026
|
+
startLine,
|
|
1027
|
+
endLine: startLine, // Will be overridden by caller
|
|
1028
|
+
content: docContent,
|
|
1029
|
+
params,
|
|
1030
|
+
returns,
|
|
1031
|
+
throws,
|
|
1032
|
+
examples,
|
|
1033
|
+
description,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
// ─── Private: Generation Helpers ───────────────────────────
|
|
1037
|
+
/**
|
|
1038
|
+
* Infer a description from function name and context.
|
|
1039
|
+
*/
|
|
1040
|
+
inferDescription(parsed, context) {
|
|
1041
|
+
const name = parsed.name;
|
|
1042
|
+
// Common verb prefixes
|
|
1043
|
+
const verbMap = {
|
|
1044
|
+
get: "Get",
|
|
1045
|
+
set: "Set",
|
|
1046
|
+
is: "Check whether",
|
|
1047
|
+
has: "Check if there is",
|
|
1048
|
+
can: "Determine if",
|
|
1049
|
+
should: "Determine whether to",
|
|
1050
|
+
create: "Create",
|
|
1051
|
+
build: "Build",
|
|
1052
|
+
make: "Create",
|
|
1053
|
+
parse: "Parse",
|
|
1054
|
+
format: "Format",
|
|
1055
|
+
validate: "Validate",
|
|
1056
|
+
check: "Check",
|
|
1057
|
+
find: "Find",
|
|
1058
|
+
search: "Search for",
|
|
1059
|
+
filter: "Filter",
|
|
1060
|
+
map: "Transform",
|
|
1061
|
+
reduce: "Reduce",
|
|
1062
|
+
handle: "Handle",
|
|
1063
|
+
process: "Process",
|
|
1064
|
+
init: "Initialize",
|
|
1065
|
+
setup: "Set up",
|
|
1066
|
+
load: "Load",
|
|
1067
|
+
save: "Save",
|
|
1068
|
+
update: "Update",
|
|
1069
|
+
delete: "Delete",
|
|
1070
|
+
remove: "Remove",
|
|
1071
|
+
add: "Add",
|
|
1072
|
+
insert: "Insert",
|
|
1073
|
+
emit: "Emit",
|
|
1074
|
+
on: "Handle",
|
|
1075
|
+
render: "Render",
|
|
1076
|
+
fetch: "Fetch",
|
|
1077
|
+
send: "Send",
|
|
1078
|
+
receive: "Receive",
|
|
1079
|
+
convert: "Convert",
|
|
1080
|
+
transform: "Transform",
|
|
1081
|
+
extract: "Extract",
|
|
1082
|
+
merge: "Merge",
|
|
1083
|
+
split: "Split",
|
|
1084
|
+
sort: "Sort",
|
|
1085
|
+
compare: "Compare",
|
|
1086
|
+
calculate: "Calculate",
|
|
1087
|
+
compute: "Compute",
|
|
1088
|
+
};
|
|
1089
|
+
// Try to match a verb prefix
|
|
1090
|
+
for (const [prefix, verb] of Object.entries(verbMap)) {
|
|
1091
|
+
if (name.startsWith(prefix) &&
|
|
1092
|
+
name.length > prefix.length &&
|
|
1093
|
+
name[prefix.length] === name[prefix.length].toUpperCase()) {
|
|
1094
|
+
const rest = name.slice(prefix.length);
|
|
1095
|
+
const words = this.camelToWords(rest);
|
|
1096
|
+
return `${verb} ${words.toLowerCase()}.`;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
// Fallback: split camelCase
|
|
1100
|
+
const words = this.camelToWords(name);
|
|
1101
|
+
return `TODO: describe ${words.toLowerCase()}.`;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Convert camelCase/PascalCase to space-separated words.
|
|
1105
|
+
*/
|
|
1106
|
+
camelToWords(name) {
|
|
1107
|
+
return name
|
|
1108
|
+
.replace(/([A-Z])/g, " $1")
|
|
1109
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
1110
|
+
.trim();
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Parse generic type parameters.
|
|
1114
|
+
*/
|
|
1115
|
+
parseGenericParams(str) {
|
|
1116
|
+
return str.split(",").map((s) => {
|
|
1117
|
+
const trimmed = s.trim();
|
|
1118
|
+
// Extract just the name (before extends/=)
|
|
1119
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
1120
|
+
return spaceIdx !== -1 ? trimmed.slice(0, spaceIdx) : trimmed;
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Check if function code might throw errors.
|
|
1125
|
+
*/
|
|
1126
|
+
mightThrow(code) {
|
|
1127
|
+
return (/\bthrow\b/.test(code) ||
|
|
1128
|
+
/\bawait\b/.test(code) ||
|
|
1129
|
+
/\.catch\b/.test(code) ||
|
|
1130
|
+
/try\s*\{/.test(code));
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Generate an example function call.
|
|
1134
|
+
*/
|
|
1135
|
+
generateExampleCall(parsed) {
|
|
1136
|
+
const args = parsed.params
|
|
1137
|
+
.filter((p) => !p.optional)
|
|
1138
|
+
.map((p) => this.generateExampleValue(p))
|
|
1139
|
+
.join(", ");
|
|
1140
|
+
const asyncPrefix = parsed.isAsync ? "await " : "";
|
|
1141
|
+
return `const result = ${asyncPrefix}${parsed.name}(${args});`;
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Generate an example value for a parameter.
|
|
1145
|
+
*/
|
|
1146
|
+
generateExampleValue(param) {
|
|
1147
|
+
if (param.defaultValue)
|
|
1148
|
+
return param.defaultValue;
|
|
1149
|
+
const type = param.type.toLowerCase().trim();
|
|
1150
|
+
if (type === "string")
|
|
1151
|
+
return `"example"`;
|
|
1152
|
+
if (type === "number")
|
|
1153
|
+
return "42";
|
|
1154
|
+
if (type === "boolean")
|
|
1155
|
+
return "true";
|
|
1156
|
+
if (type.startsWith("map"))
|
|
1157
|
+
return "new Map()";
|
|
1158
|
+
if (type.startsWith("set"))
|
|
1159
|
+
return "new Set()";
|
|
1160
|
+
if (type.endsWith("[]") || type.startsWith("array"))
|
|
1161
|
+
return "[]";
|
|
1162
|
+
if (type.startsWith("record") || type === "object")
|
|
1163
|
+
return "{}";
|
|
1164
|
+
if (type === "function" || type.includes("=>"))
|
|
1165
|
+
return "() => {}";
|
|
1166
|
+
if (type === "null")
|
|
1167
|
+
return "null";
|
|
1168
|
+
if (type === "undefined")
|
|
1169
|
+
return "undefined";
|
|
1170
|
+
// Complex types: use placeholder
|
|
1171
|
+
return `/* ${param.name} */`;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Generate a generic JSDoc for non-function symbols.
|
|
1175
|
+
*/
|
|
1176
|
+
generateGenericDoc(code) {
|
|
1177
|
+
const lines = ["/**"];
|
|
1178
|
+
// Try to detect what kind of symbol this is
|
|
1179
|
+
if (/\bclass\s+(\w+)/.test(code)) {
|
|
1180
|
+
const name = code.match(/\bclass\s+(\w+)/)?.[1] ?? "Unknown";
|
|
1181
|
+
lines.push(` * ${name} — TODO: describe class.`);
|
|
1182
|
+
}
|
|
1183
|
+
else if (/\binterface\s+(\w+)/.test(code)) {
|
|
1184
|
+
const name = code.match(/\binterface\s+(\w+)/)?.[1] ?? "Unknown";
|
|
1185
|
+
lines.push(` * ${name} — TODO: describe interface.`);
|
|
1186
|
+
}
|
|
1187
|
+
else if (/\btype\s+(\w+)/.test(code)) {
|
|
1188
|
+
const name = code.match(/\btype\s+(\w+)/)?.[1] ?? "Unknown";
|
|
1189
|
+
lines.push(` * ${name} — TODO: describe type.`);
|
|
1190
|
+
}
|
|
1191
|
+
else if (/\benum\s+(\w+)/.test(code)) {
|
|
1192
|
+
const name = code.match(/\benum\s+(\w+)/)?.[1] ?? "Unknown";
|
|
1193
|
+
lines.push(` * ${name} — TODO: describe enum.`);
|
|
1194
|
+
}
|
|
1195
|
+
else {
|
|
1196
|
+
lines.push(` * TODO: add description.`);
|
|
1197
|
+
}
|
|
1198
|
+
lines.push(` */`);
|
|
1199
|
+
return lines.join("\n");
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Parse parameter names from a parameter string (simple extraction).
|
|
1203
|
+
*/
|
|
1204
|
+
parseParamNames(paramsStr) {
|
|
1205
|
+
if (!paramsStr.trim())
|
|
1206
|
+
return [];
|
|
1207
|
+
const parts = this.splitParams(paramsStr);
|
|
1208
|
+
return parts
|
|
1209
|
+
.map((p) => {
|
|
1210
|
+
const trimmed = p.trim().replace(/^\.\.\./, "");
|
|
1211
|
+
// Extract just the name (before : or =)
|
|
1212
|
+
const colonIdx = this.findTopLevelChar(trimmed, ":");
|
|
1213
|
+
const eqIdx = this.findTopLevelChar(trimmed, "=");
|
|
1214
|
+
let end = trimmed.length;
|
|
1215
|
+
if (colonIdx !== -1 && colonIdx < end)
|
|
1216
|
+
end = colonIdx;
|
|
1217
|
+
if (eqIdx !== -1 && eqIdx < end)
|
|
1218
|
+
end = eqIdx;
|
|
1219
|
+
return trimmed.slice(0, end).trim().replace(/\?$/, "");
|
|
1220
|
+
})
|
|
1221
|
+
.filter((n) => n.length > 0);
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Extract symbol name from a line of code.
|
|
1225
|
+
*/
|
|
1226
|
+
extractSymbolName(line) {
|
|
1227
|
+
const match = line.match(/(?:function\s+\*?\s*|class\s+|interface\s+|type\s+|enum\s+|const\s+|let\s+|var\s+)(\w+)/);
|
|
1228
|
+
return match?.[1] ?? null;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Detect staleness using git log information.
|
|
1232
|
+
*/
|
|
1233
|
+
detectGitBasedStaleness(content, filePath, gitLog) {
|
|
1234
|
+
const stale = [];
|
|
1235
|
+
// Parse git log for function names that changed
|
|
1236
|
+
// Expected format: lines like "M src/file.ts:functionName"
|
|
1237
|
+
// or standard git log --name-status output
|
|
1238
|
+
const changedFunctions = new Set();
|
|
1239
|
+
const logLines = gitLog.split("\n");
|
|
1240
|
+
for (const logLine of logLines) {
|
|
1241
|
+
// Look for function names mentioned in commit messages
|
|
1242
|
+
const funcRefs = logLine.match(/\b([a-z][a-zA-Z]+)\s*\(/g);
|
|
1243
|
+
if (funcRefs) {
|
|
1244
|
+
for (const ref of funcRefs) {
|
|
1245
|
+
const name = ref.replace(/\s*\($/, "");
|
|
1246
|
+
changedFunctions.add(name);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
// Check if any documented functions were mentioned in git log
|
|
1251
|
+
if (changedFunctions.size > 0) {
|
|
1252
|
+
const jsdocBlocks = this.findAllJSDocBlocks(content);
|
|
1253
|
+
const lines = content.split("\n");
|
|
1254
|
+
for (const block of jsdocBlocks) {
|
|
1255
|
+
if (block.endLine < lines.length) {
|
|
1256
|
+
const symbolName = this.extractSymbolName(lines[block.endLine]);
|
|
1257
|
+
if (symbolName && changedFunctions.has(symbolName)) {
|
|
1258
|
+
stale.push({
|
|
1259
|
+
symbolName,
|
|
1260
|
+
filePath,
|
|
1261
|
+
docLine: block.startLine + 1,
|
|
1262
|
+
lastCodeChange: `Function '${symbolName}' appears in recent git changes — doc may need update`,
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return stale;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Format a changelog entry as markdown text.
|
|
1272
|
+
*
|
|
1273
|
+
* @param entry - Changelog entry to format
|
|
1274
|
+
* @returns Formatted markdown string
|
|
1275
|
+
*/
|
|
1276
|
+
formatChangelog(entry) {
|
|
1277
|
+
const lines = [];
|
|
1278
|
+
if (this.config.changelogFormat === "keepachangelog") {
|
|
1279
|
+
lines.push(`## [${entry.version}] - ${entry.date}`);
|
|
1280
|
+
}
|
|
1281
|
+
else {
|
|
1282
|
+
lines.push(`# ${entry.version} (${entry.date})`);
|
|
1283
|
+
}
|
|
1284
|
+
lines.push("");
|
|
1285
|
+
const sectionMap = [
|
|
1286
|
+
["breaking", "BREAKING CHANGES"],
|
|
1287
|
+
["features", "Features"],
|
|
1288
|
+
["fixes", "Bug Fixes"],
|
|
1289
|
+
["refactors", "Refactoring"],
|
|
1290
|
+
["docs", "Documentation"],
|
|
1291
|
+
["other", "Other"],
|
|
1292
|
+
];
|
|
1293
|
+
for (const [key, title] of sectionMap) {
|
|
1294
|
+
const items = entry.sections[key];
|
|
1295
|
+
if (items.length === 0)
|
|
1296
|
+
continue;
|
|
1297
|
+
lines.push(`### ${title}`);
|
|
1298
|
+
lines.push("");
|
|
1299
|
+
for (const item of items) {
|
|
1300
|
+
lines.push(`- ${item}`);
|
|
1301
|
+
}
|
|
1302
|
+
lines.push("");
|
|
1303
|
+
}
|
|
1304
|
+
return lines.join("\n");
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
//# sourceMappingURL=doc-intelligence.js.map
|