@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,1099 @@
1
+ /**
2
+ * @module git-intelligence
3
+ * @description Git Intelligence module for the YUAN coding agent.
4
+ * Provides smart commit message generation, PR description synthesis,
5
+ * conflict prediction, history analysis, and branch management.
6
+ *
7
+ * Uses only `node:child_process` and `node:path` — no external dependencies.
8
+ */
9
+ import { execFile as execFileCb } from "node:child_process";
10
+ import { promisify } from "node:util";
11
+ const execFile = promisify(execFileCb);
12
+ // ─── Constants ───
13
+ const TEST_FILE_RE = /\.(test|spec)\.(ts|tsx|js|jsx)$/;
14
+ const DOCS_FILE_RE = /\.(md|mdx|txt|rst)$|README|CHANGELOG|LICENSE/i;
15
+ const CONFIG_FILE_RE = /(package\.json|tsconfig.*\.json|\.eslintrc|\.prettierrc|jest\.config|vitest\.config|webpack\.config|vite\.config|rollup\.config|\.gitignore|\.npmrc|pnpm-workspace)/i;
16
+ const STYLE_FILE_RE = /\.(css|scss|sass|less|styl)$/;
17
+ const CI_FILE_RE = /(\.(github|gitlab)|Dockerfile|docker-compose|\.circleci|Jenkinsfile|\.travis)/i;
18
+ const BUILD_FILE_RE = /(Makefile|CMakeLists|\.cmake|build\.gradle|pom\.xml)/i;
19
+ const FIX_KEYWORDS_RE = /\b(fix|bug|error|crash|issue|patch|resolve|hotfix|regression|broken|typo|wrong|incorrect|NaN|undefined|null\s+check)\b/i;
20
+ const FEAT_KEYWORDS_RE = /\b(add|create|implement|introduce|new|support|enable|feature)\b/i;
21
+ const REFACTOR_KEYWORDS_RE = /\b(refactor|restructure|reorganize|simplify|extract|inline|rename|move|clean\s*up)\b/i;
22
+ const PERF_KEYWORDS_RE = /\b(perf|performance|optimize|speed|fast|cache|memoize|lazy|debounce|throttle)\b/i;
23
+ /** Pattern for detecting removed exports. */
24
+ const REMOVED_EXPORT_RE = /^-\s*export\s+(function|class|interface|type|enum|const|let|var)\s+(\w+)/gm;
25
+ /** Pattern for detecting renamed/changed exports. */
26
+ const CHANGED_SIGNATURE_RE = /^-\s*export\s+(?:function|const)\s+(\w+)\s*\(([^)]*)\)/gm;
27
+ const MAX_BRANCH_NAME_LEN = 50;
28
+ // ─── GitIntelligence Class ───
29
+ /**
30
+ * Git Intelligence engine for the YUAN coding agent.
31
+ *
32
+ * Analyzes git history, diffs, and blame data to generate smart commit messages,
33
+ * PR descriptions, conflict predictions, and codebase hotspot analysis.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const gi = new GitIntelligence({ projectPath: "/home/user/project" });
38
+ * const msg = await gi.generateCommitMessage(true);
39
+ * console.log(msg.fullMessage);
40
+ * ```
41
+ */
42
+ export class GitIntelligence {
43
+ config;
44
+ defaultBranch = null;
45
+ constructor(config) {
46
+ this.config = {
47
+ projectPath: config.projectPath,
48
+ defaultBranch: config.defaultBranch ?? "",
49
+ maxHistoryDepth: config.maxHistoryDepth ?? 100,
50
+ conventionalCommits: config.conventionalCommits ?? true,
51
+ };
52
+ }
53
+ // ─── Commit Intelligence ───
54
+ /**
55
+ * Analyze staged or unstaged changes and generate a smart commit message.
56
+ * @param staged - If true, analyze staged changes (`--cached`). Default: true.
57
+ */
58
+ async generateCommitMessage(staged = true) {
59
+ const diffArgs = staged ? ["diff", "--cached"] : ["diff"];
60
+ const diff = await this.git(diffArgs);
61
+ if (!diff.trim()) {
62
+ return {
63
+ subject: "chore: empty commit",
64
+ body: null,
65
+ footer: null,
66
+ fullMessage: "chore: empty commit",
67
+ };
68
+ }
69
+ const analysis = this.analyzeDiff(diff);
70
+ // Build subject
71
+ const scopePart = analysis.scope ? `(${analysis.scope})` : "";
72
+ const subject = `${analysis.type}${scopePart}: ${analysis.description}`;
73
+ // Build body (list changed files if > 3)
74
+ let body = null;
75
+ if (analysis.filesChanged > 3) {
76
+ const statDiff = staged
77
+ ? await this.git(["diff", "--cached", "--stat"])
78
+ : await this.git(["diff", "--stat"]);
79
+ const lines = statDiff
80
+ .split("\n")
81
+ .filter((l) => l.includes("|"))
82
+ .map((l) => `- ${l.trim()}`)
83
+ .slice(0, 10);
84
+ body = lines.join("\n");
85
+ }
86
+ if (analysis.body) {
87
+ body = analysis.body + (body ? "\n\n" + body : "");
88
+ }
89
+ // Build footer (breaking changes)
90
+ let footer = null;
91
+ if (analysis.breakingChange) {
92
+ footer = `BREAKING CHANGE: ${analysis.breakingChange.description}`;
93
+ if (analysis.breakingChange.affectedExports.length > 0) {
94
+ footer += `\nAffected exports: ${analysis.breakingChange.affectedExports.join(", ")}`;
95
+ }
96
+ }
97
+ // Combine
98
+ const parts = [subject];
99
+ if (body)
100
+ parts.push("", body);
101
+ if (footer)
102
+ parts.push("", footer);
103
+ return {
104
+ subject,
105
+ body,
106
+ footer,
107
+ fullMessage: parts.join("\n"),
108
+ };
109
+ }
110
+ /**
111
+ * Analyze a raw diff string and produce a CommitAnalysis.
112
+ * @param diff - Raw `git diff` output.
113
+ */
114
+ analyzeDiff(diff) {
115
+ const parsed = this.parseDiff(diff);
116
+ const files = parsed.files.map((f) => f.path);
117
+ const allHunks = parsed.files.flatMap((f) => f.hunks).join("\n");
118
+ // Count insertions/deletions from diff lines
119
+ let insertions = 0;
120
+ let deletions = 0;
121
+ for (const line of diff.split("\n")) {
122
+ if (line.startsWith("+") && !line.startsWith("+++"))
123
+ insertions++;
124
+ else if (line.startsWith("-") && !line.startsWith("---"))
125
+ deletions++;
126
+ }
127
+ const type = this.inferCommitType(diff, files);
128
+ const scope = this.inferScope(files);
129
+ const description = this.generateDescription(allHunks, type);
130
+ const breakingChanges = this.detectBreakingChanges(diff);
131
+ const breakingChange = breakingChanges.length > 0 ? breakingChanges[0] : null;
132
+ return {
133
+ type,
134
+ scope,
135
+ description,
136
+ body: null,
137
+ breakingChange,
138
+ filesChanged: files.length,
139
+ insertions,
140
+ deletions,
141
+ };
142
+ }
143
+ /**
144
+ * Detect breaking changes in a diff by looking for removed/changed exports.
145
+ * @param diff - Raw `git diff` output.
146
+ */
147
+ detectBreakingChanges(diff) {
148
+ const results = [];
149
+ const affectedExports = [];
150
+ // Detect removed exports
151
+ let match;
152
+ const removedRe = new RegExp(REMOVED_EXPORT_RE.source, "gm");
153
+ while ((match = removedRe.exec(diff)) !== null) {
154
+ affectedExports.push(match[2]);
155
+ }
156
+ // Detect changed function signatures
157
+ const changedRe = new RegExp(CHANGED_SIGNATURE_RE.source, "gm");
158
+ const changedNames = [];
159
+ while ((match = changedRe.exec(diff)) !== null) {
160
+ changedNames.push(match[1]);
161
+ }
162
+ // Check if the same function was re-added with different signature
163
+ for (const name of changedNames) {
164
+ const addedRe = new RegExp(`^\\+\\s*export\\s+(?:function|const)\\s+${name}\\s*\\(([^)]*)\\)`, "m");
165
+ const addedMatch = addedRe.exec(diff);
166
+ if (addedMatch) {
167
+ // Signature changed
168
+ if (!affectedExports.includes(name)) {
169
+ affectedExports.push(name);
170
+ }
171
+ }
172
+ }
173
+ if (affectedExports.length > 0) {
174
+ results.push({
175
+ description: `Exported API changed: ${affectedExports.join(", ")}`,
176
+ affectedExports,
177
+ });
178
+ }
179
+ return results;
180
+ }
181
+ // ─── PR Intelligence ───
182
+ /**
183
+ * Generate a PR description by analyzing the diff between current branch and base.
184
+ * @param baseBranch - Base branch to diff against; auto-detected if omitted.
185
+ */
186
+ async generatePRDescription(baseBranch) {
187
+ const base = baseBranch ?? (await this.detectDefaultBranch());
188
+ const currentBranch = (await this.git(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
189
+ // Get commits on this branch
190
+ const logOutput = await this.git([
191
+ "log",
192
+ `${base}..HEAD`,
193
+ "--oneline",
194
+ "--format=%H|%s|%an|%aI",
195
+ ]);
196
+ const commits = this.parseLog(logOutput);
197
+ // Get diff summary
198
+ const diffSummary = await this.getDiffSummary(base);
199
+ // Analyze each commit
200
+ const analyses = [];
201
+ for (const commit of commits.slice(0, 20)) {
202
+ const commitDiff = await this.git(["show", commit.hash, "--format="]);
203
+ if (commitDiff.trim()) {
204
+ analyses.push(this.analyzeDiff(commitDiff));
205
+ }
206
+ }
207
+ // Determine primary type
208
+ const typeCounts = new Map();
209
+ for (const a of analyses) {
210
+ typeCounts.set(a.type, (typeCounts.get(a.type) ?? 0) + 1);
211
+ }
212
+ const primaryType = [...typeCounts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? "chore";
213
+ // Primary scope
214
+ const scopeCounts = new Map();
215
+ for (const a of analyses) {
216
+ if (a.scope)
217
+ scopeCounts.set(a.scope, (scopeCounts.get(a.scope) ?? 0) + 1);
218
+ }
219
+ const primaryScope = [...scopeCounts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? null;
220
+ // Title
221
+ const scopePart = primaryScope ? `(${primaryScope})` : "";
222
+ const title = `${primaryType}${scopePart}: ${commits[0]?.subject ?? currentBranch}`;
223
+ // Summary bullets
224
+ const summary = commits.map((c) => c.subject);
225
+ // Changes breakdown
226
+ const changesBreakdown = diffSummary.map((entry) => {
227
+ const desc = entry.status === "A"
228
+ ? "New file"
229
+ : entry.status === "D"
230
+ ? "Deleted"
231
+ : `Modified (+${entry.insertions}/-${entry.deletions})`;
232
+ return { file: entry.file, description: desc };
233
+ });
234
+ // Breaking changes
235
+ const breakingChanges = [];
236
+ for (const a of analyses) {
237
+ if (a.breakingChange) {
238
+ breakingChanges.push(a.breakingChange.description);
239
+ }
240
+ }
241
+ // Reviewers
242
+ const changedFiles = diffSummary.map((d) => d.file);
243
+ const reviewers = await this.suggestReviewers(changedFiles);
244
+ // Labels
245
+ const labels = this.suggestLabelsFromAnalyses(analyses);
246
+ // Test plan
247
+ const testPlan = this.generateTestPlan(diffSummary, analyses);
248
+ return {
249
+ title: title.length > 70 ? title.slice(0, 67) + "..." : title,
250
+ summary,
251
+ changesBreakdown,
252
+ testPlan,
253
+ breakingChanges,
254
+ reviewers,
255
+ labels,
256
+ };
257
+ }
258
+ /**
259
+ * Suggest reviewers based on git blame of the given files.
260
+ * @param files - File paths to check; if omitted, uses files changed vs default branch.
261
+ */
262
+ async suggestReviewers(files) {
263
+ const fileList = files ??
264
+ (await this.git(["diff", "--name-only", `${await this.detectDefaultBranch()}..HEAD`]))
265
+ .trim()
266
+ .split("\n")
267
+ .filter(Boolean);
268
+ const authorCounts = new Map();
269
+ const currentUser = (await this.git(["config", "user.name"]).catch(() => "")).trim();
270
+ for (const file of fileList.slice(0, 10)) {
271
+ try {
272
+ const blameOutput = await this.git(["blame", "--porcelain", "-L", "1,50", "--", file]);
273
+ const parsed = this.parseBlame(blameOutput);
274
+ for (const entry of parsed) {
275
+ if (entry.author && entry.author !== currentUser && entry.author !== "Not Committed Yet") {
276
+ authorCounts.set(entry.author, (authorCounts.get(entry.author) ?? 0) + 1);
277
+ }
278
+ }
279
+ }
280
+ catch {
281
+ // File may not exist on current branch, skip
282
+ }
283
+ }
284
+ return [...authorCounts.entries()]
285
+ .sort((a, b) => b[1] - a[1])
286
+ .slice(0, 5)
287
+ .map(([name]) => name);
288
+ }
289
+ /**
290
+ * Suggest labels based on a CommitAnalysis.
291
+ * @param analysis - The commit analysis to derive labels from.
292
+ */
293
+ suggestLabels(analysis) {
294
+ return this.suggestLabelsFromAnalyses([analysis]);
295
+ }
296
+ // ─── Branch Intelligence ───
297
+ /**
298
+ * Suggest a branch name based on a task description.
299
+ * @param taskDescription - Natural-language description of the task.
300
+ */
301
+ suggestBranchName(taskDescription) {
302
+ const lower = taskDescription.toLowerCase();
303
+ let type;
304
+ if (FIX_KEYWORDS_RE.test(lower)) {
305
+ type = "fix";
306
+ }
307
+ else if (FEAT_KEYWORDS_RE.test(lower)) {
308
+ type = "feat";
309
+ }
310
+ else if (REFACTOR_KEYWORDS_RE.test(lower)) {
311
+ type = "refactor";
312
+ }
313
+ else if (PERF_KEYWORDS_RE.test(lower)) {
314
+ type = "perf";
315
+ }
316
+ else if (/\b(doc|readme|changelog)\b/i.test(lower)) {
317
+ type = "docs";
318
+ }
319
+ else if (/\b(test|spec|coverage)\b/i.test(lower)) {
320
+ type = "test";
321
+ }
322
+ else if (/\b(ci|deploy|pipeline|workflow)\b/i.test(lower)) {
323
+ type = "ci";
324
+ }
325
+ else {
326
+ type = "feat";
327
+ }
328
+ const slug = this.sanitizeBranchName(taskDescription);
329
+ const name = `${type}/${slug}`;
330
+ const basedOn = this.config.defaultBranch || "main";
331
+ return {
332
+ name,
333
+ basedOn,
334
+ reason: `Detected "${type}" intent from task description`,
335
+ };
336
+ }
337
+ /**
338
+ * Detect the default branch of the repository (main, master, develop, etc.).
339
+ * Caches the result after first detection.
340
+ */
341
+ async detectDefaultBranch() {
342
+ if (this.config.defaultBranch)
343
+ return this.config.defaultBranch;
344
+ if (this.defaultBranch)
345
+ return this.defaultBranch;
346
+ // Try origin/HEAD
347
+ try {
348
+ const ref = (await this.git(["symbolic-ref", "refs/remotes/origin/HEAD"])).trim();
349
+ const branch = ref.replace("refs/remotes/origin/", "");
350
+ if (branch) {
351
+ this.defaultBranch = branch;
352
+ return branch;
353
+ }
354
+ }
355
+ catch {
356
+ // Not set
357
+ }
358
+ // Check for common branch names
359
+ for (const candidate of ["main", "master", "develop"]) {
360
+ try {
361
+ await this.git(["rev-parse", "--verify", candidate]);
362
+ this.defaultBranch = candidate;
363
+ return candidate;
364
+ }
365
+ catch {
366
+ // Branch doesn't exist
367
+ }
368
+ }
369
+ this.defaultBranch = "main";
370
+ return "main";
371
+ }
372
+ /**
373
+ * Predict potential merge conflicts between current branch and a target branch.
374
+ * @param targetBranch - Branch to check against; defaults to default branch.
375
+ */
376
+ async predictConflicts(targetBranch) {
377
+ const target = targetBranch ?? (await this.detectDefaultBranch());
378
+ const predictions = [];
379
+ // Files changed on current branch
380
+ let ourFiles;
381
+ try {
382
+ ourFiles = (await this.git(["diff", "--name-only", `${target}...HEAD`]))
383
+ .trim()
384
+ .split("\n")
385
+ .filter(Boolean);
386
+ }
387
+ catch {
388
+ return [];
389
+ }
390
+ // Files changed on target branch since merge base
391
+ let theirFiles;
392
+ try {
393
+ theirFiles = (await this.git(["diff", "--name-only", `HEAD...${target}`]))
394
+ .trim()
395
+ .split("\n")
396
+ .filter(Boolean);
397
+ }
398
+ catch {
399
+ return [];
400
+ }
401
+ // Find intersection
402
+ const theirSet = new Set(theirFiles);
403
+ const overlapping = ourFiles.filter((f) => theirSet.has(f));
404
+ for (const file of overlapping) {
405
+ let lastModifiedBy;
406
+ let lastModifiedAt;
407
+ try {
408
+ const logLine = (await this.git(["log", "-1", `--format=%an|%aI`, target, "--", file])).trim();
409
+ const parts = logLine.split("|");
410
+ if (parts.length >= 2) {
411
+ lastModifiedBy = parts[0];
412
+ lastModifiedAt = parts[1];
413
+ }
414
+ }
415
+ catch {
416
+ // skip
417
+ }
418
+ // Estimate risk by checking if same hunks are modified
419
+ let risk = "medium";
420
+ let reason = `File modified on both branches`;
421
+ try {
422
+ // Get our changes to the file
423
+ const ourDiff = await this.git(["diff", `${target}...HEAD`, "--", file]);
424
+ const theirDiff = await this.git(["diff", `HEAD...${target}`, "--", file]);
425
+ // Extract line ranges from hunks
426
+ const ourRanges = this.extractHunkRanges(ourDiff);
427
+ const theirRanges = this.extractHunkRanges(theirDiff);
428
+ // Check for overlapping ranges
429
+ const hasOverlap = ourRanges.some((ourRange) => theirRanges.some((theirRange) => ourRange.start <= theirRange.end && theirRange.start <= ourRange.end));
430
+ if (hasOverlap) {
431
+ risk = "high";
432
+ reason = "Same code regions modified on both branches";
433
+ }
434
+ else if (ourRanges.length > 0 && theirRanges.length > 0) {
435
+ risk = "medium";
436
+ reason = "Different regions modified in the same file";
437
+ }
438
+ else {
439
+ risk = "low";
440
+ reason = "Changes are in non-overlapping areas";
441
+ }
442
+ }
443
+ catch {
444
+ // Could not do detailed analysis, keep medium
445
+ }
446
+ predictions.push({
447
+ file,
448
+ risk,
449
+ reason,
450
+ otherBranch: target,
451
+ lastModifiedBy,
452
+ lastModifiedAt,
453
+ });
454
+ }
455
+ // Sort by risk: high → medium → low
456
+ const riskOrder = { high: 0, medium: 1, low: 2 };
457
+ predictions.sort((a, b) => (riskOrder[a.risk] ?? 1) - (riskOrder[b.risk] ?? 1));
458
+ return predictions;
459
+ }
460
+ // ─── History Analysis ───
461
+ /**
462
+ * Find frequently-changed files (hotspots) in the repository.
463
+ * @param days - Number of days to look back; default 30.
464
+ */
465
+ async findHotspots(days = 30) {
466
+ const since = `${days} days ago`;
467
+ // Get file change counts
468
+ const logOutput = await this.git([
469
+ "log",
470
+ `--since=${since}`,
471
+ "--name-only",
472
+ "--format=COMMIT:%H|%aI|%s",
473
+ ]);
474
+ const lines = logOutput.split("\n");
475
+ const fileCounts = new Map();
476
+ const fileAuthors = new Map();
477
+ let currentDate = "";
478
+ let currentSubject = "";
479
+ for (const line of lines) {
480
+ if (line.startsWith("COMMIT:")) {
481
+ const parts = line.slice(7).split("|");
482
+ currentDate = parts[1] ?? "";
483
+ currentSubject = parts.slice(2).join("|");
484
+ continue;
485
+ }
486
+ const file = line.trim();
487
+ if (!file)
488
+ continue;
489
+ const entry = fileCounts.get(file) ?? { count: 0, dates: [], subjects: [] };
490
+ entry.count++;
491
+ if (currentDate)
492
+ entry.dates.push(currentDate);
493
+ entry.subjects.push(currentSubject);
494
+ fileCounts.set(file, entry);
495
+ // Track authors per file
496
+ if (!fileAuthors.has(file))
497
+ fileAuthors.set(file, new Set());
498
+ }
499
+ // Get authors for top files
500
+ const sorted = [...fileCounts.entries()].sort((a, b) => b[1].count - a[1].count);
501
+ const topFiles = sorted.slice(0, 30);
502
+ for (const [file] of topFiles) {
503
+ try {
504
+ const authorLog = await this.git([
505
+ "log",
506
+ `--since=${since}`,
507
+ "--format=%an",
508
+ "--",
509
+ file,
510
+ ]);
511
+ const authors = new Set(authorLog.trim().split("\n").filter(Boolean));
512
+ fileAuthors.set(file, authors);
513
+ }
514
+ catch {
515
+ // skip
516
+ }
517
+ }
518
+ const weeks = Math.max(days / 7, 1);
519
+ return topFiles.map(([file, data]) => {
520
+ const bugFixCount = data.subjects.filter((s) => FIX_KEYWORDS_RE.test(s)).length;
521
+ const lastDate = data.dates.sort().pop() ?? new Date().toISOString();
522
+ return {
523
+ file,
524
+ changeCount: data.count,
525
+ authors: [...(fileAuthors.get(file) ?? [])],
526
+ lastChanged: lastDate,
527
+ churnRate: Math.round((data.count / weeks) * 100) / 100,
528
+ bugFixRate: data.count > 0 ? Math.round((bugFixCount / data.count) * 100) : 0,
529
+ };
530
+ });
531
+ }
532
+ /**
533
+ * Get the change frequency (commit count) for specific files.
534
+ * @param files - File paths to check.
535
+ */
536
+ async getChangeFrequency(files) {
537
+ const result = new Map();
538
+ const depth = this.config.maxHistoryDepth;
539
+ for (const file of files) {
540
+ try {
541
+ const output = await this.git([
542
+ "log",
543
+ `--max-count=${depth}`,
544
+ "--oneline",
545
+ "--",
546
+ file,
547
+ ]);
548
+ const count = output.trim().split("\n").filter(Boolean).length;
549
+ result.set(file, count);
550
+ }
551
+ catch {
552
+ result.set(file, 0);
553
+ }
554
+ }
555
+ return result;
556
+ }
557
+ /**
558
+ * Get contributors for a specific file.
559
+ * @param file - File path relative to repo root.
560
+ */
561
+ async getFileContributors(file) {
562
+ try {
563
+ const output = await this.git([
564
+ "log",
565
+ `--max-count=${this.config.maxHistoryDepth}`,
566
+ "--format=%an|%aI",
567
+ "--",
568
+ file,
569
+ ]);
570
+ const authorMap = new Map();
571
+ for (const line of output.trim().split("\n").filter(Boolean)) {
572
+ const [name, date] = line.split("|");
573
+ if (!name || !date)
574
+ continue;
575
+ const entry = authorMap.get(name);
576
+ if (entry) {
577
+ entry.commits++;
578
+ if (date > entry.lastCommit)
579
+ entry.lastCommit = date;
580
+ }
581
+ else {
582
+ authorMap.set(name, { commits: 1, lastCommit: date });
583
+ }
584
+ }
585
+ return [...authorMap.entries()]
586
+ .map(([name, data]) => ({ name, ...data }))
587
+ .sort((a, b) => b.commits - a.commits);
588
+ }
589
+ catch {
590
+ return [];
591
+ }
592
+ }
593
+ /**
594
+ * Analyze commit patterns for the repo or a specific file.
595
+ * @param file - Optional file path to narrow analysis.
596
+ */
597
+ async analyzeCommitPatterns(file) {
598
+ const args = [
599
+ "log",
600
+ `--max-count=${this.config.maxHistoryDepth}`,
601
+ "--format=%aI|%s",
602
+ ];
603
+ if (file)
604
+ args.push("--", file);
605
+ const output = await this.git(args);
606
+ const lines = output.trim().split("\n").filter(Boolean);
607
+ const typeBreakdown = {};
608
+ const dayCounts = new Map();
609
+ const dates = [];
610
+ for (const line of lines) {
611
+ const pipeIdx = line.indexOf("|");
612
+ if (pipeIdx < 0)
613
+ continue;
614
+ const dateStr = line.slice(0, pipeIdx);
615
+ const subject = line.slice(pipeIdx + 1);
616
+ const day = dateStr.slice(0, 10); // YYYY-MM-DD
617
+ dates.push(dateStr);
618
+ dayCounts.set(day, (dayCounts.get(day) ?? 0) + 1);
619
+ // Detect type from conventional commit prefix
620
+ const conventionalMatch = /^(\w+)(?:\([^)]*\))?!?:/.exec(subject);
621
+ const type = conventionalMatch ? conventionalMatch[1] : "other";
622
+ typeBreakdown[type] = (typeBreakdown[type] ?? 0) + 1;
623
+ }
624
+ // Busy days (top 5)
625
+ const busyDays = [...dayCounts.entries()]
626
+ .sort((a, b) => b[1] - a[1])
627
+ .slice(0, 5)
628
+ .map(([day]) => day);
629
+ // Average commits per week
630
+ let avgCommitsPerWeek = 0;
631
+ if (dates.length >= 2) {
632
+ const oldest = new Date(dates[dates.length - 1]);
633
+ const newest = new Date(dates[0]);
634
+ const diffMs = newest.getTime() - oldest.getTime();
635
+ const weeks = Math.max(diffMs / (7 * 24 * 60 * 60 * 1000), 1);
636
+ avgCommitsPerWeek = Math.round((lines.length / weeks) * 100) / 100;
637
+ }
638
+ return {
639
+ totalCommits: lines.length,
640
+ typeBreakdown,
641
+ busyDays,
642
+ avgCommitsPerWeek,
643
+ };
644
+ }
645
+ // ─── Status ───
646
+ /**
647
+ * Get a summary of the current git status.
648
+ */
649
+ async getStatus() {
650
+ const branch = (await this.git(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
651
+ // Count staged/unstaged
652
+ const statusOutput = await this.git(["status", "--porcelain"]);
653
+ const statusLines = statusOutput.trim().split("\n").filter(Boolean);
654
+ let stagedChanges = 0;
655
+ let uncommittedChanges = 0;
656
+ for (const line of statusLines) {
657
+ const index = line[0];
658
+ const worktree = line[1];
659
+ if (index && index !== " " && index !== "?")
660
+ stagedChanges++;
661
+ if (worktree && worktree !== " " && worktree !== "?")
662
+ uncommittedChanges++;
663
+ if (index === "?")
664
+ uncommittedChanges++;
665
+ }
666
+ // Ahead/behind
667
+ let aheadOfRemote = 0;
668
+ let behindRemote = 0;
669
+ try {
670
+ const abOutput = (await this.git(["rev-list", "--left-right", "--count", `HEAD...@{upstream}`])).trim();
671
+ const parts = abOutput.split(/\s+/);
672
+ aheadOfRemote = parseInt(parts[0] ?? "0", 10) || 0;
673
+ behindRemote = parseInt(parts[1] ?? "0", 10) || 0;
674
+ }
675
+ catch {
676
+ // No upstream configured
677
+ }
678
+ // Recent commits
679
+ const logOutput = await this.git([
680
+ "log",
681
+ "--max-count=10",
682
+ "--format=%H|%s|%an|%aI",
683
+ ]);
684
+ const recentCommits = this.parseLog(logOutput);
685
+ return {
686
+ currentBranch: branch,
687
+ uncommittedChanges,
688
+ stagedChanges,
689
+ aheadOfRemote,
690
+ behindRemote,
691
+ recentCommits,
692
+ };
693
+ }
694
+ /**
695
+ * Check whether the working directory is clean (no staged or unstaged changes).
696
+ */
697
+ async isClean() {
698
+ const status = await this.git(["status", "--porcelain"]);
699
+ return status.trim().length === 0;
700
+ }
701
+ /**
702
+ * Get a per-file diff summary against a base branch.
703
+ * @param baseBranch - Base branch for comparison; defaults to default branch.
704
+ */
705
+ async getDiffSummary(baseBranch) {
706
+ const base = baseBranch ?? (await this.detectDefaultBranch());
707
+ const output = await this.git(["diff", "--numstat", `${base}..HEAD`]);
708
+ const results = [];
709
+ for (const line of output.trim().split("\n").filter(Boolean)) {
710
+ const parts = line.split("\t");
711
+ if (parts.length < 3)
712
+ continue;
713
+ const insertions = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
714
+ const deletions = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
715
+ const file = parts[2];
716
+ // Determine status
717
+ let status = "M";
718
+ try {
719
+ const nameStatus = await this.git([
720
+ "diff",
721
+ "--name-status",
722
+ `${base}..HEAD`,
723
+ "--",
724
+ file,
725
+ ]);
726
+ const s = nameStatus.trim().split("\t")[0];
727
+ if (s)
728
+ status = s[0];
729
+ }
730
+ catch {
731
+ // keep M
732
+ }
733
+ results.push({ file, insertions, deletions, status });
734
+ }
735
+ return results;
736
+ }
737
+ // ─── Private Methods ───
738
+ /**
739
+ * Execute a git command in the project directory.
740
+ * @param args - Arguments to pass to `git`.
741
+ * @returns stdout output.
742
+ */
743
+ async git(args) {
744
+ try {
745
+ const { stdout } = await execFile("git", args, {
746
+ cwd: this.config.projectPath,
747
+ maxBuffer: 10 * 1024 * 1024, // 10 MB
748
+ timeout: 30_000,
749
+ });
750
+ return stdout;
751
+ }
752
+ catch (err) {
753
+ const error = err;
754
+ throw new Error(`git ${args.join(" ")} failed: ${error.stderr ?? error.message ?? "unknown"}`);
755
+ }
756
+ }
757
+ /**
758
+ * Parse raw `git diff` output into structured file/hunk data.
759
+ * @param diff - Raw diff output.
760
+ */
761
+ parseDiff(diff) {
762
+ const files = [];
763
+ const fileSections = diff.split(/^diff --git /m).filter(Boolean);
764
+ for (const section of fileSections) {
765
+ // Extract file path from "a/path b/path"
766
+ const headerMatch = /^a\/(.+?) b\/(.+)/m.exec(section);
767
+ const path = headerMatch?.[2] ?? headerMatch?.[1] ?? "unknown";
768
+ // Extract hunks (@@...@@)
769
+ const hunks = [];
770
+ const hunkParts = section.split(/^@@/m);
771
+ for (let i = 1; i < hunkParts.length; i++) {
772
+ hunks.push("@@" + hunkParts[i]);
773
+ }
774
+ files.push({ path, hunks });
775
+ }
776
+ return { files };
777
+ }
778
+ /**
779
+ * Infer the conventional commit type from diff content and file paths.
780
+ */
781
+ inferCommitType(diff, files) {
782
+ // Check file patterns first
783
+ const allTest = files.length > 0 && files.every((f) => TEST_FILE_RE.test(f));
784
+ if (allTest)
785
+ return "test";
786
+ const allDocs = files.length > 0 && files.every((f) => DOCS_FILE_RE.test(f));
787
+ if (allDocs)
788
+ return "docs";
789
+ const allStyle = files.length > 0 && files.every((f) => STYLE_FILE_RE.test(f));
790
+ if (allStyle)
791
+ return "style";
792
+ const allCI = files.length > 0 && files.every((f) => CI_FILE_RE.test(f));
793
+ if (allCI)
794
+ return "ci";
795
+ const allBuild = files.length > 0 && files.every((f) => BUILD_FILE_RE.test(f));
796
+ if (allBuild)
797
+ return "build";
798
+ const allConfig = files.length > 0 && files.every((f) => CONFIG_FILE_RE.test(f));
799
+ if (allConfig)
800
+ return "chore";
801
+ // Check diff content for keywords
802
+ const hunkContent = diff
803
+ .split("\n")
804
+ .filter((l) => l.startsWith("+") || l.startsWith("-"))
805
+ .join("\n");
806
+ if (FIX_KEYWORDS_RE.test(hunkContent))
807
+ return "fix";
808
+ if (PERF_KEYWORDS_RE.test(hunkContent))
809
+ return "perf";
810
+ // Count additions vs deletions
811
+ let additions = 0;
812
+ let deletions = 0;
813
+ for (const line of diff.split("\n")) {
814
+ if (line.startsWith("+") && !line.startsWith("+++"))
815
+ additions++;
816
+ else if (line.startsWith("-") && !line.startsWith("---"))
817
+ deletions++;
818
+ }
819
+ // Mostly new files or more additions = feat
820
+ const hasNewFiles = diff.includes("new file mode");
821
+ if (hasNewFiles || (additions > deletions * 2))
822
+ return "feat";
823
+ // More deletions than additions = refactor
824
+ if (deletions > additions)
825
+ return "refactor";
826
+ return "feat";
827
+ }
828
+ /**
829
+ * Infer a scope from file paths by finding a common directory.
830
+ */
831
+ inferScope(files) {
832
+ if (files.length === 0)
833
+ return null;
834
+ // Extract meaningful directory segments
835
+ const segments = files.map((f) => {
836
+ const parts = f.split("/").filter(Boolean);
837
+ // Skip top-level generic dirs
838
+ const skip = new Set(["src", "lib", "dist", "build", "packages"]);
839
+ const meaningful = parts.filter((p) => !skip.has(p));
840
+ return meaningful.length > 0 ? meaningful[0] : parts[parts.length - 1] ?? null;
841
+ });
842
+ // If all files share the same segment, use it
843
+ const unique = [...new Set(segments.filter(Boolean))];
844
+ if (unique.length === 1) {
845
+ return this.cleanScopeName(unique[0]);
846
+ }
847
+ // If there are 2 segments, use the more specific one
848
+ if (unique.length === 2 && files.length <= 5) {
849
+ // Check for a common parent
850
+ const dirs = files.map((f) => f.split("/").slice(0, -1).join("/"));
851
+ const commonDir = this.longestCommonPrefix(dirs);
852
+ if (commonDir) {
853
+ const lastPart = commonDir.split("/").filter(Boolean).pop();
854
+ if (lastPart)
855
+ return this.cleanScopeName(lastPart);
856
+ }
857
+ }
858
+ return null;
859
+ }
860
+ /**
861
+ * Clean a scope name by removing file extensions and trimming.
862
+ */
863
+ cleanScopeName(name) {
864
+ return name.replace(/\.\w+$/, "").replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
865
+ }
866
+ /**
867
+ * Find the longest common prefix among strings.
868
+ */
869
+ longestCommonPrefix(strs) {
870
+ if (strs.length === 0)
871
+ return "";
872
+ let prefix = strs[0];
873
+ for (let i = 1; i < strs.length; i++) {
874
+ while (!strs[i].startsWith(prefix)) {
875
+ prefix = prefix.slice(0, -1);
876
+ if (!prefix)
877
+ return "";
878
+ }
879
+ }
880
+ return prefix;
881
+ }
882
+ /**
883
+ * Generate a short description from diff hunks and commit type.
884
+ */
885
+ generateDescription(hunks, type) {
886
+ const lines = hunks.split("\n");
887
+ // Collect added lines (skip diff metadata)
888
+ const added = [];
889
+ const removed = [];
890
+ for (const line of lines) {
891
+ if (line.startsWith("+") && !line.startsWith("+++") && !line.startsWith("@@")) {
892
+ const content = line.slice(1).trim();
893
+ if (content && !content.startsWith("//") && !content.startsWith("*")) {
894
+ added.push(content);
895
+ }
896
+ }
897
+ else if (line.startsWith("-") && !line.startsWith("---") && !line.startsWith("@@")) {
898
+ const content = line.slice(1).trim();
899
+ if (content && !content.startsWith("//") && !content.startsWith("*")) {
900
+ removed.push(content);
901
+ }
902
+ }
903
+ }
904
+ // Try to find a meaningful first added line
905
+ const significantAdded = added.find((l) => {
906
+ return (/^(export\s+)?(function|class|interface|type|enum|const|let|var)\s+\w+/.test(l) ||
907
+ /^(async\s+)?(\w+)\s*\(/.test(l) ||
908
+ l.length > 10);
909
+ });
910
+ if (significantAdded) {
911
+ // Extract a symbol name
912
+ const symbolMatch = /^(?:export\s+)?(?:async\s+)?(?:function|class|interface|type|enum|const|let|var)\s+(\w+)/.exec(significantAdded);
913
+ if (symbolMatch) {
914
+ const verb = type === "feat" ? "add" : type === "fix" ? "fix" : type === "refactor" ? "refactor" : "update";
915
+ return `${verb} ${symbolMatch[1]}`;
916
+ }
917
+ }
918
+ // Fall back to describing the action
919
+ if (added.length > 0 && removed.length === 0) {
920
+ return `add new code (${added.length} lines)`;
921
+ }
922
+ if (removed.length > 0 && added.length === 0) {
923
+ return `remove unused code (${removed.length} lines)`;
924
+ }
925
+ if (added.length > 0 && removed.length > 0) {
926
+ return `update code (+${added.length}/-${removed.length} lines)`;
927
+ }
928
+ return "update files";
929
+ }
930
+ /**
931
+ * Parse git log output in the format `%H|%s|%an|%aI`.
932
+ */
933
+ parseLog(output) {
934
+ return output
935
+ .trim()
936
+ .split("\n")
937
+ .filter(Boolean)
938
+ .map((line) => {
939
+ const parts = line.split("|");
940
+ return {
941
+ hash: parts[0] ?? "",
942
+ subject: parts[1] ?? "",
943
+ author: parts[2] ?? "",
944
+ date: parts[3] ?? "",
945
+ };
946
+ })
947
+ .filter((e) => e.hash.length > 0);
948
+ }
949
+ /**
950
+ * Parse git blame --porcelain output.
951
+ */
952
+ parseBlame(output) {
953
+ const results = [];
954
+ const lines = output.split("\n");
955
+ let currentAuthor = "";
956
+ let currentDate = "";
957
+ let currentLine = 0;
958
+ for (const line of lines) {
959
+ // Header line: "hash origLine finalLine numLines"
960
+ const headerMatch = /^[0-9a-f]{40}\s+\d+\s+(\d+)/.exec(line);
961
+ if (headerMatch) {
962
+ currentLine = parseInt(headerMatch[1], 10);
963
+ continue;
964
+ }
965
+ if (line.startsWith("author ")) {
966
+ currentAuthor = line.slice(7);
967
+ }
968
+ else if (line.startsWith("author-time ")) {
969
+ const ts = parseInt(line.slice(12), 10);
970
+ currentDate = new Date(ts * 1000).toISOString();
971
+ }
972
+ else if (line.startsWith("\t")) {
973
+ // Content line — marks end of this entry
974
+ results.push({ author: currentAuthor, line: currentLine, date: currentDate });
975
+ }
976
+ }
977
+ return results;
978
+ }
979
+ /**
980
+ * Sanitize a string into a valid git branch name.
981
+ */
982
+ sanitizeBranchName(name) {
983
+ return name
984
+ .toLowerCase()
985
+ .replace(/[^a-z0-9\s-]/g, "")
986
+ .replace(/\s+/g, "-")
987
+ .replace(/-+/g, "-")
988
+ .replace(/^-|-$/g, "")
989
+ .slice(0, MAX_BRANCH_NAME_LEN);
990
+ }
991
+ /**
992
+ * Extract hunk line ranges from a diff for conflict overlap analysis.
993
+ */
994
+ extractHunkRanges(diff) {
995
+ const ranges = [];
996
+ const hunkRe = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/gm;
997
+ let match;
998
+ while ((match = hunkRe.exec(diff)) !== null) {
999
+ const start = parseInt(match[1], 10);
1000
+ const count = parseInt(match[2] ?? "1", 10);
1001
+ ranges.push({ start, end: start + count - 1 });
1002
+ }
1003
+ return ranges;
1004
+ }
1005
+ /**
1006
+ * Suggest labels from multiple commit analyses.
1007
+ */
1008
+ suggestLabelsFromAnalyses(analyses) {
1009
+ const labels = new Set();
1010
+ for (const a of analyses) {
1011
+ // Type-based labels
1012
+ switch (a.type) {
1013
+ case "feat":
1014
+ labels.add("enhancement");
1015
+ break;
1016
+ case "fix":
1017
+ labels.add("bug");
1018
+ break;
1019
+ case "docs":
1020
+ labels.add("documentation");
1021
+ break;
1022
+ case "test":
1023
+ labels.add("testing");
1024
+ break;
1025
+ case "perf":
1026
+ labels.add("performance");
1027
+ break;
1028
+ case "refactor":
1029
+ labels.add("refactor");
1030
+ break;
1031
+ case "ci":
1032
+ labels.add("ci/cd");
1033
+ break;
1034
+ case "build":
1035
+ labels.add("build");
1036
+ break;
1037
+ case "chore":
1038
+ labels.add("chore");
1039
+ break;
1040
+ case "style":
1041
+ labels.add("style");
1042
+ break;
1043
+ }
1044
+ // Size-based labels
1045
+ const totalChanges = a.insertions + a.deletions;
1046
+ if (totalChanges > 500)
1047
+ labels.add("size/large");
1048
+ else if (totalChanges > 100)
1049
+ labels.add("size/medium");
1050
+ else
1051
+ labels.add("size/small");
1052
+ // Breaking change
1053
+ if (a.breakingChange)
1054
+ labels.add("breaking-change");
1055
+ }
1056
+ return [...labels];
1057
+ }
1058
+ /**
1059
+ * Generate a test plan checklist based on changed files and analyses.
1060
+ */
1061
+ generateTestPlan(diffSummary, analyses) {
1062
+ const plan = [];
1063
+ // Check if any source files changed
1064
+ const hasSourceChanges = diffSummary.some((d) => !TEST_FILE_RE.test(d.file) && !DOCS_FILE_RE.test(d.file) && !CONFIG_FILE_RE.test(d.file));
1065
+ if (hasSourceChanges) {
1066
+ plan.push("[ ] Verify TypeScript compilation (`tsc --noEmit`)");
1067
+ plan.push("[ ] Run existing test suite");
1068
+ }
1069
+ // Check for test file changes
1070
+ const hasTestChanges = diffSummary.some((d) => TEST_FILE_RE.test(d.file));
1071
+ if (hasTestChanges) {
1072
+ plan.push("[ ] Verify new/updated tests pass");
1073
+ }
1074
+ else if (hasSourceChanges) {
1075
+ plan.push("[ ] Consider adding tests for new functionality");
1076
+ }
1077
+ // Breaking changes
1078
+ const hasBreaking = analyses.some((a) => a.breakingChange);
1079
+ if (hasBreaking) {
1080
+ plan.push("[ ] Verify backward compatibility or document migration path");
1081
+ plan.push("[ ] Check downstream consumers for breakage");
1082
+ }
1083
+ // Config changes
1084
+ const hasConfigChanges = diffSummary.some((d) => CONFIG_FILE_RE.test(d.file));
1085
+ if (hasConfigChanges) {
1086
+ plan.push("[ ] Verify configuration changes work in dev and prod");
1087
+ }
1088
+ // New files
1089
+ const newFiles = diffSummary.filter((d) => d.status === "A");
1090
+ if (newFiles.length > 0) {
1091
+ plan.push(`[ ] Review ${newFiles.length} new file(s) for correctness`);
1092
+ }
1093
+ if (plan.length === 0) {
1094
+ plan.push("[ ] Smoke test the affected area");
1095
+ }
1096
+ return plan;
1097
+ }
1098
+ }
1099
+ //# sourceMappingURL=git-intelligence.js.map