openspec-stat 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,12 +3,15 @@
3
3
  [![NPM version](https://img.shields.io/npm/v/openspec-stat.svg?style=flat)](https://npmjs.com/package/openspec-stat)
4
4
  [![NPM downloads](http://img.shields.io/npm/dm/openspec-stat.svg?style=flat)](https://npmjs.com/package/openspec-stat)
5
5
 
6
+ English | [简体中文](./README.zh-CN.md)
7
+
6
8
  A CLI tool for tracking team members' OpenSpec proposals and code changes in Git repositories.
7
9
 
8
10
  ## Features
9
11
 
10
12
  - ✅ Track Git commits within specified time ranges
11
13
  - ✅ Identify commits containing both OpenSpec proposals and code changes
14
+ - ✅ **Proposal-based statistics summary** - Aggregate statistics by proposal to avoid merge commit bias
12
15
  - ✅ Group statistics by author (commits, proposals, code changes)
13
16
  - ✅ Support multiple branches and wildcard filtering
14
17
  - ✅ Author name mapping (handle multiple Git accounts for the same person)
@@ -161,6 +164,11 @@ Statistics include:
161
164
  - **Deletions**: Lines of code deleted
162
165
  - **Net Changes**: Additions - Deletions
163
166
 
167
+ The tool provides two perspectives:
168
+
169
+ 1. **Proposal Summary**: Aggregates statistics by proposal, showing total code changes per proposal and all contributors. This avoids statistical bias from merge commits.
170
+ 2. **Author Summary**: Groups statistics by contributor, showing individual author contributions.
171
+
164
172
  ## Output Formats
165
173
 
166
174
  ### Table Format (Default)
@@ -171,6 +179,16 @@ Time Range: 2024-01-01 00:00:00 ~ 2024-01-31 23:59:59
171
179
  Branches: origin/master
172
180
  Total Commits: 15
173
181
 
182
+ 📋 Proposal Summary (by proposal)
183
+ ┌──────────────┬─────────┬──────────────────┬────────────┬───────────┬───────────┬─────────────┐
184
+ │ Proposal │ Commits │ Contributors │ Code Files │ Additions │ Deletions │ Net Changes │
185
+ ├──────────────┼─────────┼──────────────────┼────────────┼───────────┼───────────┼─────────────┤
186
+ │ feature-123 │ 5 │ John Doe, Jane S.│ 30 │ +890 │ -234 │ +656 │
187
+ │ feature-456 │ 3 │ John Doe │ 15 │ +344 │ -100 │ +244 │
188
+ └──────────────┴─────────┴──────────────────┴────────────┴───────────┴───────────┴─────────────┘
189
+ 📊 Total: 2 proposals | 8 commits | 45 files | +1234/-334 lines (net: +900)
190
+
191
+ 👥 Author Summary (by contributor)
174
192
  ┌──────────┬─────────┬──────────────────┬────────────┬───────────┬───────────┬─────────────┐
175
193
  │ Author │ Commits │ OpenSpec Proposals│ Code Files │ Additions │ Deletions │ Net Changes │
176
194
  ├──────────┼─────────┼──────────────────┼────────────┼───────────┼───────────┼─────────────┤
package/README.zh-CN.md CHANGED
@@ -11,6 +11,7 @@
11
11
 
12
12
  - ✅ 追踪指定时间范围内的 Git 提交
13
13
  - ✅ 识别同时包含 OpenSpec 提案和代码变更的提交
14
+ - ✅ **提案维度统计汇总** - 按提案聚合统计,避免 merge commit 导致的统计偏差
14
15
  - ✅ 按作者分组统计(提交数、提案数、代码变更)
15
16
  - ✅ 支持多分支和通配符过滤
16
17
  - ✅ 作者名称映射(处理同一人的多个 Git 账号)
@@ -163,6 +164,11 @@ openspec-stat --lang zh-CN --verbose
163
164
  - **删除行数**:删除的代码行数
164
165
  - **净变更**:新增行数 - 删除行数
165
166
 
167
+ 工具提供两个统计视角:
168
+
169
+ 1. **提案汇总**:按提案聚合统计,显示每个提案的总代码变更量和所有贡献者,避免 merge commit 导致的统计偏差
170
+ 2. **作者汇总**:按贡献者分组统计,显示各个作者的个人贡献情况
171
+
166
172
  ## 输出格式
167
173
 
168
174
  ### 表格格式(默认)
@@ -173,6 +179,16 @@ openspec-stat --lang zh-CN --verbose
173
179
  分支:origin/master
174
180
  总提交数:15
175
181
 
182
+ 📋 提案汇总(按提案统计)
183
+ ┌──────────────┬─────────┬──────────────────┬────────────┬───────────┬───────────┬─────────────┐
184
+ │ 提案 │ 提交数 │ 贡献者 │ 代码文件 │ 新增行数 │ 删除行数 │ 净变更 │
185
+ ├──────────────┼─────────┼──────────────────┼────────────┼───────────┼───────────┼─────────────┤
186
+ │ feature-123 │ 5 │ 张三, 李四 │ 30 │ +890 │ -234 │ +656 │
187
+ │ feature-456 │ 3 │ 张三 │ 15 │ +344 │ -100 │ +244 │
188
+ └──────────────┴─────────┴──────────────────┴────────────┴───────────┴───────────┴─────────────┘
189
+ 📊 总计:2 个提案 | 8 次提交 | 45 个文件 | +1234/-334 行(净变更:+900)
190
+
191
+ 👥 作者汇总(按贡献者统计)
176
192
  ┌──────────┬─────────┬──────────────────┬────────────┬───────────┬───────────┬─────────────┐
177
193
  │ 作者 │ 提交数 │ 提案数 │ 代码文件 │ 新增行数 │ 删除行数 │ 净变更 │
178
194
  ├──────────┼─────────┼──────────────────┼────────────┼───────────┼───────────┼─────────────┤
package/dist/cjs/cli.js CHANGED
@@ -41,19 +41,10 @@ program.name("openspec-stat").description("Track team members' OpenSpec proposal
41
41
  let since;
42
42
  let until;
43
43
  if (options.since || options.until) {
44
- since = options.since ? (0, import_time_utils.parseDateTime)(options.since) : (0, import_time_utils.getDefaultTimeRange)(
45
- config.defaultSinceHours,
46
- config.defaultUntilHours
47
- ).since;
48
- until = options.until ? (0, import_time_utils.parseDateTime)(options.until) : (0, import_time_utils.getDefaultTimeRange)(
49
- config.defaultSinceHours,
50
- config.defaultUntilHours
51
- ).until;
44
+ since = options.since ? (0, import_time_utils.parseDateTime)(options.since) : (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours).since;
45
+ until = options.until ? (0, import_time_utils.parseDateTime)(options.until) : (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours).until;
52
46
  } else {
53
- const defaultRange = (0, import_time_utils.getDefaultTimeRange)(
54
- config.defaultSinceHours,
55
- config.defaultUntilHours
56
- );
47
+ const defaultRange = (0, import_time_utils.getDefaultTimeRange)(config.defaultSinceHours, config.defaultUntilHours);
57
48
  since = defaultRange.since;
58
49
  until = defaultRange.until;
59
50
  }
@@ -73,14 +64,16 @@ program.name("openspec-stat").description("Track team members' OpenSpec proposal
73
64
  })
74
65
  )
75
66
  );
76
- console.log(import_chalk.default.blue((0, import_i18n.t)("info.branches", {
77
- branches: branches.join(", ") || (0, import_i18n.t)("info.allBranches")
78
- })));
67
+ console.log(
68
+ import_chalk.default.blue(
69
+ (0, import_i18n.t)("info.branches", {
70
+ branches: branches.join(", ") || (0, import_i18n.t)("info.allBranches")
71
+ })
72
+ )
73
+ );
79
74
  const analyzer = new import_git_analyzer.GitAnalyzer(options.repo, config);
80
75
  console.log(import_chalk.default.blue((0, import_i18n.t)("loading.activeUsers")));
81
- const activeAuthors = await analyzer.getActiveAuthors(
82
- config.activeUserWeeks || 2
83
- );
76
+ const activeAuthors = await analyzer.getActiveAuthors(config.activeUserWeeks || 2);
84
77
  if (options.verbose) {
85
78
  console.log(
86
79
  import_chalk.default.gray(
@@ -103,10 +96,12 @@ program.name("openspec-stat").description("Track team members' OpenSpec proposal
103
96
  const commit = commits[i];
104
97
  if (options.verbose && i % 10 === 0) {
105
98
  console.log(
106
- import_chalk.default.gray((0, import_i18n.t)("info.analysisProgress", {
107
- current: String(i + 1),
108
- total: String(commits.length)
109
- }))
99
+ import_chalk.default.gray(
100
+ (0, import_i18n.t)("info.analysisProgress", {
101
+ current: String(i + 1),
102
+ total: String(commits.length)
103
+ })
104
+ )
110
105
  );
111
106
  }
112
107
  const analysis = await analyzer.analyzeCommit(commit);
@@ -115,24 +110,12 @@ program.name("openspec-stat").description("Track team members' OpenSpec proposal
115
110
  }
116
111
  }
117
112
  if (analyses.length === 0) {
118
- console.log(
119
- import_chalk.default.yellow((0, import_i18n.t)("warning.noQualifyingCommits"))
120
- );
113
+ console.log(import_chalk.default.yellow((0, import_i18n.t)("warning.noQualifyingCommits")));
121
114
  return;
122
115
  }
123
- console.log(
124
- import_chalk.default.blue(
125
- (0, import_i18n.t)("info.qualifyingCommits", { count: String(analyses.length) })
126
- )
127
- );
116
+ console.log(import_chalk.default.blue((0, import_i18n.t)("info.qualifyingCommits", { count: String(analyses.length) })));
128
117
  const aggregator = new import_stats_aggregator.StatsAggregator(config, activeAuthors);
129
- const result = aggregator.aggregate(
130
- analyses,
131
- since,
132
- until,
133
- branches,
134
- options.author
135
- );
118
+ const result = aggregator.aggregate(analyses, since, until, branches, options.author);
136
119
  const formatter = new import_formatters.OutputFormatter();
137
120
  if (options.json) {
138
121
  console.log(formatter.formatJSON(result));
@@ -47,9 +47,60 @@ var OutputFormatter = class {
47
47
  );
48
48
  output += import_chalk.default.gray((0, import_i18n.t)("output.branches", { branches: result.branches.join(", ") }));
49
49
  output += import_chalk.default.gray((0, import_i18n.t)("output.totalCommits", { count: String(result.totalCommits) }));
50
- const sortedAuthors = Array.from(result.authors.values()).sort(
51
- (a, b) => b.commits - a.commits
52
- );
50
+ if (result.proposals.size > 0) {
51
+ output += import_chalk.default.bold.magenta(`
52
+ ${(0, import_i18n.t)("output.proposalSummary")}
53
+ `);
54
+ const proposalTable = new import_cli_table3.default({
55
+ head: [
56
+ import_chalk.default.magenta((0, import_i18n.t)("table.proposal")),
57
+ import_chalk.default.magenta((0, import_i18n.t)("table.commits")),
58
+ import_chalk.default.magenta((0, import_i18n.t)("table.contributors")),
59
+ import_chalk.default.magenta((0, import_i18n.t)("table.codeFiles")),
60
+ import_chalk.default.magenta((0, import_i18n.t)("table.additions")),
61
+ import_chalk.default.magenta((0, import_i18n.t)("table.deletions")),
62
+ import_chalk.default.magenta((0, import_i18n.t)("table.netChanges"))
63
+ ],
64
+ style: {
65
+ head: [],
66
+ border: []
67
+ }
68
+ });
69
+ const sortedProposals = Array.from(result.proposals.values()).sort((a, b) => b.netChanges - a.netChanges);
70
+ for (const proposalStats of sortedProposals) {
71
+ const contributors = Array.from(proposalStats.contributors).join(", ");
72
+ proposalTable.push([
73
+ proposalStats.proposal,
74
+ proposalStats.commits.toString(),
75
+ contributors,
76
+ proposalStats.codeFilesChanged.toString(),
77
+ import_chalk.default.green(`+${proposalStats.additions}`),
78
+ import_chalk.default.red(`-${proposalStats.deletions}`),
79
+ proposalStats.netChanges >= 0 ? import_chalk.default.green(`+${proposalStats.netChanges}`) : import_chalk.default.red(`${proposalStats.netChanges}`)
80
+ ]);
81
+ }
82
+ output += proposalTable.toString() + "\n";
83
+ const totalProposals = result.proposals.size;
84
+ const totalProposalCommits = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.commits, 0);
85
+ const totalProposalFiles = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.codeFilesChanged, 0);
86
+ const totalProposalAdditions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.additions, 0);
87
+ const totalProposalDeletions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.deletions, 0);
88
+ const totalProposalNetChanges = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.netChanges, 0);
89
+ output += import_chalk.default.gray(
90
+ (0, import_i18n.t)("output.proposalTotal", {
91
+ count: totalProposals.toString(),
92
+ commits: totalProposalCommits.toString(),
93
+ files: totalProposalFiles.toString(),
94
+ additions: totalProposalAdditions.toString(),
95
+ deletions: totalProposalDeletions.toString(),
96
+ netChanges: totalProposalNetChanges.toString()
97
+ })
98
+ );
99
+ }
100
+ output += import_chalk.default.bold.cyan(`
101
+ ${(0, import_i18n.t)("output.authorSummary")}
102
+ `);
103
+ const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
53
104
  for (const stats of sortedAuthors) {
54
105
  output += import_chalk.default.bold.cyan(`
55
106
  ${stats.author}
@@ -70,9 +121,7 @@ ${stats.author}
70
121
  border: []
71
122
  }
72
123
  });
73
- const sortedBranches = Array.from(stats.branchStats.values()).sort(
74
- (a, b) => b.commits - a.commits
75
- );
124
+ const sortedBranches = Array.from(stats.branchStats.values()).sort((a, b) => b.commits - a.commits);
76
125
  for (const branchStat of sortedBranches) {
77
126
  branchTable.push([
78
127
  branchStat.branch,
@@ -135,6 +184,26 @@ ${stats.author}
135
184
  },
136
185
  branches: result.branches,
137
186
  totalCommits: result.totalCommits,
187
+ proposals: {
188
+ items: Array.from(result.proposals.values()).map((stats) => ({
189
+ proposal: stats.proposal,
190
+ commits: stats.commits,
191
+ contributors: Array.from(stats.contributors),
192
+ contributorCount: stats.contributors.size,
193
+ codeFilesChanged: stats.codeFilesChanged,
194
+ additions: stats.additions,
195
+ deletions: stats.deletions,
196
+ netChanges: stats.netChanges
197
+ })),
198
+ summary: {
199
+ totalProposals: result.proposals.size,
200
+ totalCommits: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.commits, 0),
201
+ totalCodeFiles: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.codeFilesChanged, 0),
202
+ totalAdditions: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.additions, 0),
203
+ totalDeletions: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.deletions, 0),
204
+ totalNetChanges: Array.from(result.proposals.values()).reduce((sum, p) => sum + p.netChanges, 0)
205
+ }
206
+ },
138
207
  authors: Array.from(result.authors.values()).map((stats) => {
139
208
  var _a;
140
209
  return {
@@ -155,12 +224,46 @@ ${stats.author}
155
224
  formatCSV(result) {
156
225
  var _a;
157
226
  const rows = [];
227
+ rows.push(`
228
+ # ${(0, import_i18n.t)("output.proposalSummary")}`);
158
229
  rows.push(
159
- `${(0, import_i18n.t)("table.author")},${(0, import_i18n.t)("table.period")},${(0, import_i18n.t)("table.commits")},${(0, import_i18n.t)("table.proposalsCount")},${(0, import_i18n.t)("table.proposalsList")},${(0, import_i18n.t)("table.codeFiles")},${(0, import_i18n.t)("table.additions")},${(0, import_i18n.t)("table.deletions")},${(0, import_i18n.t)("table.netChanges")},${(0, import_i18n.t)("table.lastCommitDate")}`
230
+ `${(0, import_i18n.t)("table.proposal")},${(0, import_i18n.t)("table.commits")},${(0, import_i18n.t)("table.contributors")},${(0, import_i18n.t)("table.codeFiles")},${(0, import_i18n.t)("table.additions")},${(0, import_i18n.t)("table.deletions")},${(0, import_i18n.t)("table.netChanges")}`
160
231
  );
161
- const sortedAuthors = Array.from(result.authors.values()).sort(
162
- (a, b) => b.commits - a.commits
232
+ const sortedProposals = Array.from(result.proposals.values()).sort((a, b) => b.netChanges - a.netChanges);
233
+ for (const stats of sortedProposals) {
234
+ const contributors = Array.from(stats.contributors).join(";");
235
+ rows.push(
236
+ [
237
+ stats.proposal,
238
+ stats.commits,
239
+ `"${contributors}"`,
240
+ stats.codeFilesChanged,
241
+ stats.additions,
242
+ stats.deletions,
243
+ stats.netChanges
244
+ ].join(",")
245
+ );
246
+ }
247
+ const totalProposals = result.proposals.size;
248
+ const totalProposalCommits = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.commits, 0);
249
+ const totalProposalFiles = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.codeFilesChanged, 0);
250
+ const totalProposalAdditions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.additions, 0);
251
+ const totalProposalDeletions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.deletions, 0);
252
+ const totalProposalNetChanges = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.netChanges, 0);
253
+ rows.push("");
254
+ rows.push(`# ${(0, import_i18n.t)("output.proposalTotalLabel")}`);
255
+ rows.push(`${(0, import_i18n.t)("table.proposals")},${totalProposals}`);
256
+ rows.push(`${(0, import_i18n.t)("table.commits")},${totalProposalCommits}`);
257
+ rows.push(`${(0, import_i18n.t)("table.codeFiles")},${totalProposalFiles}`);
258
+ rows.push(`${(0, import_i18n.t)("table.additions")},${totalProposalAdditions}`);
259
+ rows.push(`${(0, import_i18n.t)("table.deletions")},${totalProposalDeletions}`);
260
+ rows.push(`${(0, import_i18n.t)("table.netChanges")},${totalProposalNetChanges}`);
261
+ rows.push(`
262
+ # ${(0, import_i18n.t)("output.authorSummary")}`);
263
+ rows.push(
264
+ `${(0, import_i18n.t)("table.author")},${(0, import_i18n.t)("table.period")},${(0, import_i18n.t)("table.commits")},${(0, import_i18n.t)("table.proposalsCount")},${(0, import_i18n.t)("table.proposalsList")},${(0, import_i18n.t)("table.codeFiles")},${(0, import_i18n.t)("table.additions")},${(0, import_i18n.t)("table.deletions")},${(0, import_i18n.t)("table.netChanges")},${(0, import_i18n.t)("table.lastCommitDate")}`
163
265
  );
266
+ const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
164
267
  for (const stats of sortedAuthors) {
165
268
  const proposals = Array.from(stats.openspecProposals).join(";");
166
269
  rows.push(
@@ -189,13 +292,48 @@ ${stats.author}
189
292
  });
190
293
  md += (0, import_i18n.t)("markdown.branches", { branches: result.branches.join(", ") });
191
294
  md += (0, import_i18n.t)("markdown.totalCommits", { count: String(result.totalCommits) });
192
- md += (0, import_i18n.t)("markdown.statistics");
295
+ md += `
296
+ ## ${(0, import_i18n.t)("output.proposalSummary")}
297
+
298
+ `;
299
+ md += `| ${(0, import_i18n.t)("table.proposal")} | ${(0, import_i18n.t)("table.commits")} | ${(0, import_i18n.t)("table.contributors")} | ${(0, import_i18n.t)("table.codeFiles")} | ${(0, import_i18n.t)("table.additions")} | ${(0, import_i18n.t)("table.deletions")} | ${(0, import_i18n.t)("table.netChanges")} |
300
+ `;
301
+ md += "|--------|---------|-------------|------------|-----------|-----------|-------------|\n";
302
+ const sortedProposals = Array.from(result.proposals.values()).sort((a, b) => b.netChanges - a.netChanges);
303
+ for (const stats of sortedProposals) {
304
+ const contributors = Array.from(stats.contributors).join(", ");
305
+ md += `| ${stats.proposal} | ${stats.commits} | ${contributors} | ${stats.codeFilesChanged} | +${stats.additions} | -${stats.deletions} | ${stats.netChanges >= 0 ? "+" : ""}${stats.netChanges} |
306
+ `;
307
+ }
308
+ const totalProposals = result.proposals.size;
309
+ const totalProposalCommits = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.commits, 0);
310
+ const totalProposalFiles = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.codeFilesChanged, 0);
311
+ const totalProposalAdditions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.additions, 0);
312
+ const totalProposalDeletions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.deletions, 0);
313
+ const totalProposalNetChanges = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.netChanges, 0);
314
+ md += `
315
+ **${(0, import_i18n.t)("output.proposalTotalLabel")}**
316
+ `;
317
+ md += `- ${(0, import_i18n.t)("table.proposals")}: ${totalProposals}
318
+ `;
319
+ md += `- ${(0, import_i18n.t)("table.commits")}: ${totalProposalCommits}
320
+ `;
321
+ md += `- ${(0, import_i18n.t)("table.codeFiles")}: ${totalProposalFiles}
322
+ `;
323
+ md += `- ${(0, import_i18n.t)("table.additions")}: +${totalProposalAdditions}
324
+ `;
325
+ md += `- ${(0, import_i18n.t)("table.deletions")}: -${totalProposalDeletions}
326
+ `;
327
+ md += `- ${(0, import_i18n.t)("table.netChanges")}: ${totalProposalNetChanges >= 0 ? "+" : ""}${totalProposalNetChanges}
328
+ `;
329
+ md += `
330
+ ## ${(0, import_i18n.t)("output.authorSummary")}
331
+
332
+ `;
193
333
  md += `| ${(0, import_i18n.t)("table.author")} | ${(0, import_i18n.t)("table.period")} | ${(0, import_i18n.t)("table.commits")} | ${(0, import_i18n.t)("table.proposals")} | ${(0, import_i18n.t)("table.codeFiles")} | ${(0, import_i18n.t)("table.additions")} | ${(0, import_i18n.t)("table.deletions")} | ${(0, import_i18n.t)("table.netChanges")} |
194
334
  `;
195
335
  md += "|--------|--------|---------|-----------|------------|-----------|-----------|-------------|\n";
196
- const sortedAuthors = Array.from(result.authors.values()).sort(
197
- (a, b) => b.commits - a.commits
198
- );
336
+ const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
199
337
  for (const stats of sortedAuthors) {
200
338
  md += `| ${stats.author} | ${stats.statisticsPeriod || "-"} | ${stats.commits} | ${stats.openspecProposals.size} | ${stats.codeFilesChanged} | +${stats.additions} | -${stats.deletions} | ${stats.netChanges >= 0 ? "+" : ""}${stats.netChanges} |
201
339
  `;
@@ -44,13 +44,15 @@ var GitAnalyzer = class {
44
44
  const untilStr = until.toISOString();
45
45
  const logOptions = {
46
46
  "--since": sinceStr,
47
- "--until": untilStr,
48
- "--all": null
47
+ "--until": untilStr
49
48
  };
50
49
  if (branches.length > 0) {
50
+ delete logOptions["--all"];
51
51
  for (const branch of branches) {
52
- logOptions[`--remotes=${branch}`] = null;
52
+ logOptions[branch] = null;
53
53
  }
54
+ } else {
55
+ logOptions["--all"] = null;
54
56
  }
55
57
  const log = await this.git.log(logOptions);
56
58
  const commits = [];
@@ -83,11 +85,7 @@ var GitAnalyzer = class {
83
85
  }
84
86
  async analyzeCommit(commit) {
85
87
  try {
86
- const show = await this.git.show([
87
- "--numstat",
88
- "--format=",
89
- commit.hash
90
- ]);
88
+ const show = await this.git.show(["--numstat", "--format=", commit.hash]);
91
89
  const lines = show.split("\n").filter((line) => line.trim());
92
90
  const fileChanges = [];
93
91
  const openspecProposals = /* @__PURE__ */ new Set();
@@ -103,9 +101,7 @@ var GitAnalyzer = class {
103
101
  const additions = addStr === "-" ? 0 : parseInt(addStr, 10);
104
102
  const deletions = delStr === "-" ? 0 : parseInt(delStr, 10);
105
103
  if (path.startsWith(openspecDir)) {
106
- const proposalMatch = path.match(
107
- new RegExp(`^${openspecDir}changes/([^/]+)`)
108
- );
104
+ const proposalMatch = path.match(new RegExp(`^${openspecDir}changes/([^/]+)`));
109
105
  if (proposalMatch) {
110
106
  openspecProposals.add(proposalMatch[1]);
111
107
  }
@@ -123,14 +119,8 @@ var GitAnalyzer = class {
123
119
  }
124
120
  }
125
121
  if (openspecProposals.size > 0 && hasCodeChanges) {
126
- const totalAdditions = fileChanges.reduce(
127
- (sum, f) => sum + f.additions,
128
- 0
129
- );
130
- const totalDeletions = fileChanges.reduce(
131
- (sum, f) => sum + f.deletions,
132
- 0
133
- );
122
+ const totalAdditions = fileChanges.reduce((sum, f) => sum + f.additions, 0);
123
+ const totalDeletions = fileChanges.reduce((sum, f) => sum + f.deletions, 0);
134
124
  return {
135
125
  commit,
136
126
  openspecProposals,
@@ -155,10 +145,7 @@ var GitAnalyzer = class {
155
145
  });
156
146
  const authors = /* @__PURE__ */ new Set();
157
147
  for (const commit of log.all) {
158
- const normalizedAuthor = (0, import_config.normalizeAuthor)(
159
- commit.author_name,
160
- this.config.authorMapping
161
- );
148
+ const normalizedAuthor = (0, import_config.normalizeAuthor)(commit.author_name, this.config.authorMapping);
162
149
  authors.add(normalizedAuthor);
163
150
  }
164
151
  return authors;
@@ -33,7 +33,7 @@ var __filename = (0, import_url.fileURLToPath)(import_meta.url);
33
33
  var __dirname = (0, import_path.dirname)(__filename);
34
34
  var currentLanguage = "en";
35
35
  var translations = {
36
- "en": {},
36
+ en: {},
37
37
  "zh-CN": {}
38
38
  };
39
39
  function loadTranslations() {
@@ -12,11 +12,11 @@
12
12
  "cli.option.config": "Configuration file path",
13
13
  "cli.option.verbose": "Verbose output mode",
14
14
  "cli.option.lang": "Language for output (en, zh-CN)",
15
-
15
+
16
16
  "loading.config": "🔍 Loading configuration...",
17
17
  "loading.activeUsers": "🔍 Fetching active users...",
18
18
  "loading.analyzing": "🔍 Analyzing commit history...",
19
-
19
+
20
20
  "info.timeRange": "📅 Time Range: {{since}} ~ {{until}}",
21
21
  "info.branches": "🌿 Branches: {{branches}}",
22
22
  "info.allBranches": "All branches",
@@ -24,13 +24,13 @@
24
24
  "info.foundCommits": "📝 Found {{count}} commits, analyzing...",
25
25
  "info.analysisProgress": " Analysis progress: {{current}}/{{total}}",
26
26
  "info.qualifyingCommits": "✅ Found {{count}} qualifying commits (containing OpenSpec proposals and code changes)",
27
-
27
+
28
28
  "warning.noCommits": "⚠️ No commits found matching the criteria",
29
29
  "warning.noQualifyingCommits": "⚠️ No commits found containing both OpenSpec proposals and code changes",
30
30
  "warning.noBranches": "⚠️ No remote branches found",
31
-
31
+
32
32
  "error.prefix": "❌ Error:",
33
-
33
+
34
34
  "branch.fetching": "\n🔍 Fetching active branches...",
35
35
  "branch.selectMode": "How would you like to select branches?",
36
36
  "branch.mode.select": "Select from active branches",
@@ -42,17 +42,23 @@
42
42
  "branch.customSeparator": "--- Custom input ---",
43
43
  "branch.selected": "\n✓ Selected branches:",
44
44
  "branch.lastCommit": "last commit: {{date}}",
45
-
45
+
46
46
  "output.title": "\n📊 OpenSpec Statistics Report\n",
47
47
  "output.timeRange": "Time Range: {{since}} ~ {{until}}\n",
48
48
  "output.branches": "Branches: {{branches}}\n",
49
49
  "output.totalCommits": "Total Commits: {{count}}\n\n",
50
50
  "output.proposals": " Proposals: {{proposals}}\n",
51
-
51
+ "output.proposalSummary": "📋 Proposal Summary (by proposal)",
52
+ "output.proposalTotal": " 📊 Total: {{count}} proposals | {{commits}} commits | {{files}} files | +{{additions}}/-{{deletions}} lines (net: {{netChanges}})\n",
53
+ "output.proposalTotalLabel": "Proposal Summary Total",
54
+ "output.authorSummary": "👥 Author Summary (by contributor)",
55
+
52
56
  "table.branch": "Branch",
53
57
  "table.period": "Period",
54
58
  "table.commits": "Commits",
55
59
  "table.proposals": "Proposals",
60
+ "table.proposal": "Proposal",
61
+ "table.contributors": "Contributors",
56
62
  "table.codeFiles": "Code Files",
57
63
  "table.additions": "Additions",
58
64
  "table.deletions": "Deletions",
@@ -62,7 +68,7 @@
62
68
  "table.proposalsList": "Proposals List",
63
69
  "table.proposalsCount": "Proposals Count",
64
70
  "table.totalDeduplicated": "TOTAL (Deduplicated)",
65
-
71
+
66
72
  "markdown.title": "# OpenSpec Statistics Report\n\n",
67
73
  "markdown.timeRange": "**Time Range**: {{since}} ~ {{until}}\n\n",
68
74
  "markdown.branches": "**Branches**: {{branches}}\n\n",
@@ -12,11 +12,11 @@
12
12
  "cli.option.config": "配置文件路径",
13
13
  "cli.option.verbose": "详细输出模式",
14
14
  "cli.option.lang": "输出语言(en, zh-CN)",
15
-
15
+
16
16
  "loading.config": "🔍 正在加载配置...",
17
17
  "loading.activeUsers": "🔍 正在获取活跃用户...",
18
18
  "loading.analyzing": "🔍 正在分析提交历史...",
19
-
19
+
20
20
  "info.timeRange": "📅 时间范围:{{since}} ~ {{until}}",
21
21
  "info.branches": "🌿 分支:{{branches}}",
22
22
  "info.allBranches": "所有分支",
@@ -24,13 +24,13 @@
24
24
  "info.foundCommits": "📝 找到 {{count}} 个提交,正在分析...",
25
25
  "info.analysisProgress": " 分析进度:{{current}}/{{total}}",
26
26
  "info.qualifyingCommits": "✅ 找到 {{count}} 个符合条件的提交(包含 OpenSpec 提案和代码变更)",
27
-
27
+
28
28
  "warning.noCommits": "⚠️ 未找到符合条件的提交",
29
29
  "warning.noQualifyingCommits": "⚠️ 未找到同时包含 OpenSpec 提案和代码变更的提交",
30
30
  "warning.noBranches": "⚠️ 未找到远程分支",
31
-
31
+
32
32
  "error.prefix": "❌ 错误:",
33
-
33
+
34
34
  "branch.fetching": "\n🔍 正在获取活跃分支...",
35
35
  "branch.selectMode": "您想如何选择分支?",
36
36
  "branch.mode.select": "从活跃分支中选择",
@@ -42,17 +42,23 @@
42
42
  "branch.customSeparator": "--- 自定义输入 ---",
43
43
  "branch.selected": "\n✓ 已选择的分支:",
44
44
  "branch.lastCommit": "最后提交:{{date}}",
45
-
45
+
46
46
  "output.title": "\n📊 OpenSpec 统计报告\n",
47
47
  "output.timeRange": "时间范围:{{since}} ~ {{until}}\n",
48
48
  "output.branches": "分支:{{branches}}\n",
49
49
  "output.totalCommits": "总提交数:{{count}}\n\n",
50
50
  "output.proposals": " 提案:{{proposals}}\n",
51
-
51
+ "output.proposalSummary": "📋 提案汇总(按提案统计)",
52
+ "output.proposalTotal": " 📊 总计:{{count}} 个提案 | {{commits}} 次提交 | {{files}} 个文件 | +{{additions}}/-{{deletions}} 行(净变更:{{netChanges}})\n",
53
+ "output.proposalTotalLabel": "提案汇总总计",
54
+ "output.authorSummary": "👥 作者汇总(按贡献者统计)",
55
+
52
56
  "table.branch": "分支",
53
57
  "table.period": "周期",
54
58
  "table.commits": "提交数",
55
59
  "table.proposals": "提案数",
60
+ "table.proposal": "提案",
61
+ "table.contributors": "贡献者",
56
62
  "table.codeFiles": "代码文件",
57
63
  "table.additions": "新增行数",
58
64
  "table.deletions": "删除行数",
@@ -62,7 +68,7 @@
62
68
  "table.proposalsList": "提案列表",
63
69
  "table.proposalsCount": "提案数量",
64
70
  "table.totalDeduplicated": "总计(去重)",
65
-
71
+
66
72
  "markdown.title": "# OpenSpec 统计报告\n\n",
67
73
  "markdown.timeRange": "**时间范围**:{{since}} ~ {{until}}\n\n",
68
74
  "markdown.branches": "**分支**:{{branches}}\n\n",
@@ -30,11 +30,9 @@ var StatsAggregator = class {
30
30
  }
31
31
  aggregate(analyses, since, until, branches, filterAuthor) {
32
32
  const authorStatsMap = /* @__PURE__ */ new Map();
33
+ const proposalStatsMap = /* @__PURE__ */ new Map();
33
34
  for (const analysis of analyses) {
34
- const normalizedAuthor = (0, import_config.normalizeAuthor)(
35
- analysis.commit.author,
36
- this.config.authorMapping
37
- );
35
+ const normalizedAuthor = (0, import_config.normalizeAuthor)(analysis.commit.author, this.config.authorMapping);
38
36
  if (this.activeAuthors && !this.activeAuthors.has(normalizedAuthor)) {
39
37
  continue;
40
38
  }
@@ -62,6 +60,29 @@ var StatsAggregator = class {
62
60
  stats.codeFilesChanged += analysis.codeFiles.length;
63
61
  for (const proposal of analysis.openspecProposals) {
64
62
  stats.openspecProposals.add(proposal);
63
+ let proposalStats = proposalStatsMap.get(proposal);
64
+ if (!proposalStats) {
65
+ proposalStats = {
66
+ proposal,
67
+ commits: 0,
68
+ contributors: /* @__PURE__ */ new Set(),
69
+ codeFilesChanged: 0,
70
+ additions: 0,
71
+ deletions: 0,
72
+ netChanges: 0,
73
+ commitHashes: /* @__PURE__ */ new Set()
74
+ };
75
+ proposalStatsMap.set(proposal, proposalStats);
76
+ }
77
+ if (!proposalStats.commitHashes.has(analysis.commit.hash)) {
78
+ proposalStats.commitHashes.add(analysis.commit.hash);
79
+ proposalStats.commits++;
80
+ proposalStats.contributors.add(normalizedAuthor);
81
+ proposalStats.codeFilesChanged += analysis.codeFiles.length;
82
+ proposalStats.additions += analysis.totalAdditions;
83
+ proposalStats.deletions += analysis.totalDeletions;
84
+ proposalStats.netChanges += analysis.netChanges;
85
+ }
65
86
  }
66
87
  if (!stats.lastCommitDate || analysis.commit.date > stats.lastCommitDate) {
67
88
  stats.lastCommitDate = analysis.commit.date;
@@ -103,10 +124,19 @@ var StatsAggregator = class {
103
124
  stats.statisticsPeriod = days === 0 ? "1 day" : `${days + 1} days`;
104
125
  }
105
126
  }
127
+ const actualBranches = /* @__PURE__ */ new Set();
128
+ for (const stats of authorStatsMap.values()) {
129
+ if (stats.branchStats) {
130
+ for (const branch of stats.branchStats.keys()) {
131
+ actualBranches.add(branch);
132
+ }
133
+ }
134
+ }
106
135
  return {
107
136
  timeRange: { since, until },
108
- branches,
137
+ branches: actualBranches.size > 0 ? Array.from(actualBranches) : branches,
109
138
  authors: authorStatsMap,
139
+ proposals: proposalStatsMap,
110
140
  totalCommits: analyses.length
111
141
  };
112
142
  }