@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.
Files changed (235) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +15 -0
  3. package/dist/__tests__/context-manager.test.d.ts +6 -0
  4. package/dist/__tests__/context-manager.test.d.ts.map +1 -0
  5. package/dist/__tests__/context-manager.test.js +220 -0
  6. package/dist/__tests__/context-manager.test.js.map +1 -0
  7. package/dist/__tests__/governor.test.d.ts +6 -0
  8. package/dist/__tests__/governor.test.d.ts.map +1 -0
  9. package/dist/__tests__/governor.test.js +210 -0
  10. package/dist/__tests__/governor.test.js.map +1 -0
  11. package/dist/__tests__/model-router.test.d.ts +6 -0
  12. package/dist/__tests__/model-router.test.d.ts.map +1 -0
  13. package/dist/__tests__/model-router.test.js +329 -0
  14. package/dist/__tests__/model-router.test.js.map +1 -0
  15. package/dist/agent-logger.d.ts +384 -0
  16. package/dist/agent-logger.d.ts.map +1 -0
  17. package/dist/agent-logger.js +820 -0
  18. package/dist/agent-logger.js.map +1 -0
  19. package/dist/agent-loop.d.ts +163 -0
  20. package/dist/agent-loop.d.ts.map +1 -0
  21. package/dist/agent-loop.js +609 -0
  22. package/dist/agent-loop.js.map +1 -0
  23. package/dist/agent-modes.d.ts +85 -0
  24. package/dist/agent-modes.d.ts.map +1 -0
  25. package/dist/agent-modes.js +418 -0
  26. package/dist/agent-modes.js.map +1 -0
  27. package/dist/approval.d.ts +137 -0
  28. package/dist/approval.d.ts.map +1 -0
  29. package/dist/approval.js +299 -0
  30. package/dist/approval.js.map +1 -0
  31. package/dist/async-completion-queue.d.ts +56 -0
  32. package/dist/async-completion-queue.d.ts.map +1 -0
  33. package/dist/async-completion-queue.js +77 -0
  34. package/dist/async-completion-queue.js.map +1 -0
  35. package/dist/auto-fix.d.ts +174 -0
  36. package/dist/auto-fix.d.ts.map +1 -0
  37. package/dist/auto-fix.js +319 -0
  38. package/dist/auto-fix.js.map +1 -0
  39. package/dist/codebase-context.d.ts +396 -0
  40. package/dist/codebase-context.d.ts.map +1 -0
  41. package/dist/codebase-context.js +1260 -0
  42. package/dist/codebase-context.js.map +1 -0
  43. package/dist/conflict-resolver.d.ts +191 -0
  44. package/dist/conflict-resolver.d.ts.map +1 -0
  45. package/dist/conflict-resolver.js +524 -0
  46. package/dist/conflict-resolver.js.map +1 -0
  47. package/dist/constants.d.ts +52 -0
  48. package/dist/constants.d.ts.map +1 -0
  49. package/dist/constants.js +141 -0
  50. package/dist/constants.js.map +1 -0
  51. package/dist/context-budget.d.ts +435 -0
  52. package/dist/context-budget.d.ts.map +1 -0
  53. package/dist/context-budget.js +903 -0
  54. package/dist/context-budget.js.map +1 -0
  55. package/dist/context-compressor.d.ts +143 -0
  56. package/dist/context-compressor.d.ts.map +1 -0
  57. package/dist/context-compressor.js +511 -0
  58. package/dist/context-compressor.js.map +1 -0
  59. package/dist/context-manager.d.ts +112 -0
  60. package/dist/context-manager.d.ts.map +1 -0
  61. package/dist/context-manager.js +247 -0
  62. package/dist/context-manager.js.map +1 -0
  63. package/dist/continuous-reflection.d.ts +267 -0
  64. package/dist/continuous-reflection.d.ts.map +1 -0
  65. package/dist/continuous-reflection.js +338 -0
  66. package/dist/continuous-reflection.js.map +1 -0
  67. package/dist/cross-file-refactor.d.ts +352 -0
  68. package/dist/cross-file-refactor.d.ts.map +1 -0
  69. package/dist/cross-file-refactor.js +1544 -0
  70. package/dist/cross-file-refactor.js.map +1 -0
  71. package/dist/dag-orchestrator.d.ts +138 -0
  72. package/dist/dag-orchestrator.d.ts.map +1 -0
  73. package/dist/dag-orchestrator.js +379 -0
  74. package/dist/dag-orchestrator.js.map +1 -0
  75. package/dist/debate-orchestrator.d.ts +301 -0
  76. package/dist/debate-orchestrator.d.ts.map +1 -0
  77. package/dist/debate-orchestrator.js +719 -0
  78. package/dist/debate-orchestrator.js.map +1 -0
  79. package/dist/dependency-analyzer.d.ts +113 -0
  80. package/dist/dependency-analyzer.d.ts.map +1 -0
  81. package/dist/dependency-analyzer.js +444 -0
  82. package/dist/dependency-analyzer.js.map +1 -0
  83. package/dist/design-loop.d.ts +59 -0
  84. package/dist/design-loop.d.ts.map +1 -0
  85. package/dist/design-loop.js +344 -0
  86. package/dist/design-loop.js.map +1 -0
  87. package/dist/doc-intelligence.d.ts +383 -0
  88. package/dist/doc-intelligence.d.ts.map +1 -0
  89. package/dist/doc-intelligence.js +1307 -0
  90. package/dist/doc-intelligence.js.map +1 -0
  91. package/dist/dynamic-role-generator.d.ts +76 -0
  92. package/dist/dynamic-role-generator.d.ts.map +1 -0
  93. package/dist/dynamic-role-generator.js +194 -0
  94. package/dist/dynamic-role-generator.js.map +1 -0
  95. package/dist/errors.d.ts +69 -0
  96. package/dist/errors.d.ts.map +1 -0
  97. package/dist/errors.js +102 -0
  98. package/dist/errors.js.map +1 -0
  99. package/dist/event-bus.d.ts +159 -0
  100. package/dist/event-bus.d.ts.map +1 -0
  101. package/dist/event-bus.js +305 -0
  102. package/dist/event-bus.js.map +1 -0
  103. package/dist/execution-engine.d.ts +425 -0
  104. package/dist/execution-engine.d.ts.map +1 -0
  105. package/dist/execution-engine.js +1555 -0
  106. package/dist/execution-engine.js.map +1 -0
  107. package/dist/git-intelligence.d.ts +306 -0
  108. package/dist/git-intelligence.d.ts.map +1 -0
  109. package/dist/git-intelligence.js +1099 -0
  110. package/dist/git-intelligence.js.map +1 -0
  111. package/dist/governor.d.ts +77 -0
  112. package/dist/governor.d.ts.map +1 -0
  113. package/dist/governor.js +161 -0
  114. package/dist/governor.js.map +1 -0
  115. package/dist/hierarchical-planner.d.ts +313 -0
  116. package/dist/hierarchical-planner.d.ts.map +1 -0
  117. package/dist/hierarchical-planner.js +981 -0
  118. package/dist/hierarchical-planner.js.map +1 -0
  119. package/dist/index.d.ts +121 -0
  120. package/dist/index.d.ts.map +1 -0
  121. package/dist/index.js +123 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/intent-inference.d.ts +103 -0
  124. package/dist/intent-inference.d.ts.map +1 -0
  125. package/dist/intent-inference.js +605 -0
  126. package/dist/intent-inference.js.map +1 -0
  127. package/dist/interrupt-manager.d.ts +143 -0
  128. package/dist/interrupt-manager.d.ts.map +1 -0
  129. package/dist/interrupt-manager.js +196 -0
  130. package/dist/interrupt-manager.js.map +1 -0
  131. package/dist/kernel.d.ts +564 -0
  132. package/dist/kernel.d.ts.map +1 -0
  133. package/dist/kernel.js +1419 -0
  134. package/dist/kernel.js.map +1 -0
  135. package/dist/language-support.d.ts +232 -0
  136. package/dist/language-support.d.ts.map +1 -0
  137. package/dist/language-support.js +1134 -0
  138. package/dist/language-support.js.map +1 -0
  139. package/dist/llm-client.d.ts +82 -0
  140. package/dist/llm-client.d.ts.map +1 -0
  141. package/dist/llm-client.js +475 -0
  142. package/dist/llm-client.js.map +1 -0
  143. package/dist/mcp-client.d.ts +232 -0
  144. package/dist/mcp-client.d.ts.map +1 -0
  145. package/dist/mcp-client.js +718 -0
  146. package/dist/mcp-client.js.map +1 -0
  147. package/dist/memory-manager.d.ts +200 -0
  148. package/dist/memory-manager.d.ts.map +1 -0
  149. package/dist/memory-manager.js +568 -0
  150. package/dist/memory-manager.js.map +1 -0
  151. package/dist/memory.d.ts +87 -0
  152. package/dist/memory.d.ts.map +1 -0
  153. package/dist/memory.js +341 -0
  154. package/dist/memory.js.map +1 -0
  155. package/dist/model-router.d.ts +245 -0
  156. package/dist/model-router.d.ts.map +1 -0
  157. package/dist/model-router.js +632 -0
  158. package/dist/model-router.js.map +1 -0
  159. package/dist/parallel-executor.d.ts +125 -0
  160. package/dist/parallel-executor.d.ts.map +1 -0
  161. package/dist/parallel-executor.js +201 -0
  162. package/dist/parallel-executor.js.map +1 -0
  163. package/dist/perf-optimizer.d.ts +212 -0
  164. package/dist/perf-optimizer.d.ts.map +1 -0
  165. package/dist/perf-optimizer.js +721 -0
  166. package/dist/perf-optimizer.js.map +1 -0
  167. package/dist/persona.d.ts +305 -0
  168. package/dist/persona.d.ts.map +1 -0
  169. package/dist/persona.js +887 -0
  170. package/dist/persona.js.map +1 -0
  171. package/dist/planner.d.ts +70 -0
  172. package/dist/planner.d.ts.map +1 -0
  173. package/dist/planner.js +264 -0
  174. package/dist/planner.js.map +1 -0
  175. package/dist/qa-pipeline.d.ts +365 -0
  176. package/dist/qa-pipeline.d.ts.map +1 -0
  177. package/dist/qa-pipeline.js +1352 -0
  178. package/dist/qa-pipeline.js.map +1 -0
  179. package/dist/reasoning-adapter.d.ts +116 -0
  180. package/dist/reasoning-adapter.d.ts.map +1 -0
  181. package/dist/reasoning-adapter.js +187 -0
  182. package/dist/reasoning-adapter.js.map +1 -0
  183. package/dist/role-registry.d.ts +55 -0
  184. package/dist/role-registry.d.ts.map +1 -0
  185. package/dist/role-registry.js +192 -0
  186. package/dist/role-registry.js.map +1 -0
  187. package/dist/sandbox-tiers.d.ts +327 -0
  188. package/dist/sandbox-tiers.d.ts.map +1 -0
  189. package/dist/sandbox-tiers.js +928 -0
  190. package/dist/sandbox-tiers.js.map +1 -0
  191. package/dist/security-scanner.d.ts +222 -0
  192. package/dist/security-scanner.d.ts.map +1 -0
  193. package/dist/security-scanner.js +1129 -0
  194. package/dist/security-scanner.js.map +1 -0
  195. package/dist/security.d.ts +93 -0
  196. package/dist/security.d.ts.map +1 -0
  197. package/dist/security.js +393 -0
  198. package/dist/security.js.map +1 -0
  199. package/dist/self-reflection.d.ts +397 -0
  200. package/dist/self-reflection.d.ts.map +1 -0
  201. package/dist/self-reflection.js +908 -0
  202. package/dist/self-reflection.js.map +1 -0
  203. package/dist/session-persistence.d.ts +191 -0
  204. package/dist/session-persistence.d.ts.map +1 -0
  205. package/dist/session-persistence.js +395 -0
  206. package/dist/session-persistence.js.map +1 -0
  207. package/dist/speculative-executor.d.ts +210 -0
  208. package/dist/speculative-executor.d.ts.map +1 -0
  209. package/dist/speculative-executor.js +618 -0
  210. package/dist/speculative-executor.js.map +1 -0
  211. package/dist/state-machine.d.ts +289 -0
  212. package/dist/state-machine.d.ts.map +1 -0
  213. package/dist/state-machine.js +695 -0
  214. package/dist/state-machine.js.map +1 -0
  215. package/dist/sub-agent.d.ts +177 -0
  216. package/dist/sub-agent.d.ts.map +1 -0
  217. package/dist/sub-agent.js +303 -0
  218. package/dist/sub-agent.js.map +1 -0
  219. package/dist/system-prompt.d.ts +26 -0
  220. package/dist/system-prompt.d.ts.map +1 -0
  221. package/dist/system-prompt.js +84 -0
  222. package/dist/system-prompt.js.map +1 -0
  223. package/dist/test-intelligence.d.ts +439 -0
  224. package/dist/test-intelligence.d.ts.map +1 -0
  225. package/dist/test-intelligence.js +1165 -0
  226. package/dist/test-intelligence.js.map +1 -0
  227. package/dist/types.d.ts +632 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +6 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/vector-index.d.ts +314 -0
  232. package/dist/vector-index.d.ts.map +1 -0
  233. package/dist/vector-index.js +618 -0
  234. package/dist/vector-index.js.map +1 -0
  235. 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(`![Version](https://img.shields.io/badge/version-${version}-blue)`);
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