openspec-stat 1.1.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
@@ -11,6 +11,7 @@ A CLI tool for tracking team members' OpenSpec proposals and code changes in Git
11
11
 
12
12
  - ✅ Track Git commits within specified time ranges
13
13
  - ✅ Identify commits containing both OpenSpec proposals and code changes
14
+ - ✅ **Proposal-based statistics summary** - Aggregate statistics by proposal to avoid merge commit bias
14
15
  - ✅ Group statistics by author (commits, proposals, code changes)
15
16
  - ✅ Support multiple branches and wildcard filtering
16
17
  - ✅ Author name mapping (handle multiple Git accounts for the same person)
@@ -163,6 +164,11 @@ Statistics include:
163
164
  - **Deletions**: Lines of code deleted
164
165
  - **Net Changes**: Additions - Deletions
165
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
+
166
172
  ## Output Formats
167
173
 
168
174
  ### Table Format (Default)
@@ -173,6 +179,16 @@ Time Range: 2024-01-01 00:00:00 ~ 2024-01-31 23:59:59
173
179
  Branches: origin/master
174
180
  Total Commits: 15
175
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)
176
192
  ┌──────────┬─────────┬──────────────────┬────────────┬───────────┬───────────┬─────────────┐
177
193
  │ Author │ Commits │ OpenSpec Proposals│ Code Files │ Additions │ Deletions │ Net Changes │
178
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
  ├──────────┼─────────┼──────────────────┼────────────┼───────────┼───────────┼─────────────┤
@@ -47,6 +47,59 @@ 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
+ 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
+ `);
50
103
  const sortedAuthors = Array.from(result.authors.values()).sort((a, b) => b.commits - a.commits);
51
104
  for (const stats of sortedAuthors) {
52
105
  output += import_chalk.default.bold.cyan(`
@@ -131,6 +184,26 @@ ${stats.author}
131
184
  },
132
185
  branches: result.branches,
133
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
+ },
134
207
  authors: Array.from(result.authors.values()).map((stats) => {
135
208
  var _a;
136
209
  return {
@@ -151,6 +224,42 @@ ${stats.author}
151
224
  formatCSV(result) {
152
225
  var _a;
153
226
  const rows = [];
227
+ rows.push(`
228
+ # ${(0, import_i18n.t)("output.proposalSummary")}`);
229
+ rows.push(
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")}`
231
+ );
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")}`);
154
263
  rows.push(
155
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")}`
156
265
  );
@@ -183,7 +292,44 @@ ${stats.author}
183
292
  });
184
293
  md += (0, import_i18n.t)("markdown.branches", { branches: result.branches.join(", ") });
185
294
  md += (0, import_i18n.t)("markdown.totalCommits", { count: String(result.totalCommits) });
186
- 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
+ `;
187
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")} |
188
334
  `;
189
335
  md += "|--------|--------|---------|-----------|------------|-----------|-----------|-------------|\n";
@@ -48,11 +48,17 @@
48
48
  "output.branches": "Branches: {{branches}}\n",
49
49
  "output.totalCommits": "Total Commits: {{count}}\n\n",
50
50
  "output.proposals": " Proposals: {{proposals}}\n",
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)",
51
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",
@@ -48,11 +48,17 @@
48
48
  "output.branches": "分支:{{branches}}\n",
49
49
  "output.totalCommits": "总提交数:{{count}}\n\n",
50
50
  "output.proposals": " 提案:{{proposals}}\n",
51
+ "output.proposalSummary": "📋 提案汇总(按提案统计)",
52
+ "output.proposalTotal": " 📊 总计:{{count}} 个提案 | {{commits}} 次提交 | {{files}} 个文件 | +{{additions}}/-{{deletions}} 行(净变更:{{netChanges}})\n",
53
+ "output.proposalTotalLabel": "提案汇总总计",
54
+ "output.authorSummary": "👥 作者汇总(按贡献者统计)",
51
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": "删除行数",
@@ -30,6 +30,7 @@ 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
35
  const normalizedAuthor = (0, import_config.normalizeAuthor)(analysis.commit.author, this.config.authorMapping);
35
36
  if (this.activeAuthors && !this.activeAuthors.has(normalizedAuthor)) {
@@ -59,6 +60,29 @@ var StatsAggregator = class {
59
60
  stats.codeFilesChanged += analysis.codeFiles.length;
60
61
  for (const proposal of analysis.openspecProposals) {
61
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
+ }
62
86
  }
63
87
  if (!stats.lastCommitDate || analysis.commit.date > stats.lastCommitDate) {
64
88
  stats.lastCommitDate = analysis.commit.date;
@@ -112,6 +136,7 @@ var StatsAggregator = class {
112
136
  timeRange: { since, until },
113
137
  branches: actualBranches.size > 0 ? Array.from(actualBranches) : branches,
114
138
  authors: authorStatsMap,
139
+ proposals: proposalStatsMap,
115
140
  totalCommits: analyses.length
116
141
  };
117
142
  }
@@ -66,6 +66,16 @@ export interface BranchStats {
66
66
  deletions: number;
67
67
  netChanges: number;
68
68
  }
69
+ export interface ProposalStats {
70
+ proposal: string;
71
+ commits: number;
72
+ contributors: Set<string>;
73
+ codeFilesChanged: number;
74
+ additions: number;
75
+ deletions: number;
76
+ netChanges: number;
77
+ commitHashes: Set<string>;
78
+ }
69
79
  export interface StatsResult {
70
80
  timeRange: {
71
81
  since: Date;
@@ -73,5 +83,6 @@ export interface StatsResult {
73
83
  };
74
84
  branches: string[];
75
85
  authors: Map<string, AuthorStats>;
86
+ proposals: Map<string, ProposalStats>;
76
87
  totalCommits: number;
77
88
  }
@@ -34,14 +34,70 @@ export var OutputFormatter = /*#__PURE__*/function () {
34
34
  output += chalk.gray(t('output.totalCommits', {
35
35
  count: String(result.totalCommits)
36
36
  }));
37
+
38
+ // Proposal Summary Table
39
+ if (result.proposals.size > 0) {
40
+ output += chalk.bold.magenta("\n".concat(t('output.proposalSummary'), "\n"));
41
+ var proposalTable = new Table({
42
+ head: [chalk.magenta(t('table.proposal')), chalk.magenta(t('table.commits')), chalk.magenta(t('table.contributors')), chalk.magenta(t('table.codeFiles')), chalk.magenta(t('table.additions')), chalk.magenta(t('table.deletions')), chalk.magenta(t('table.netChanges'))],
43
+ style: {
44
+ head: [],
45
+ border: []
46
+ }
47
+ });
48
+ var sortedProposals = Array.from(result.proposals.values()).sort(function (a, b) {
49
+ return b.netChanges - a.netChanges;
50
+ });
51
+ var _iterator = _createForOfIteratorHelper(sortedProposals),
52
+ _step;
53
+ try {
54
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
55
+ var proposalStats = _step.value;
56
+ var contributors = Array.from(proposalStats.contributors).join(', ');
57
+ proposalTable.push([proposalStats.proposal, proposalStats.commits.toString(), contributors, proposalStats.codeFilesChanged.toString(), chalk.green("+".concat(proposalStats.additions)), chalk.red("-".concat(proposalStats.deletions)), proposalStats.netChanges >= 0 ? chalk.green("+".concat(proposalStats.netChanges)) : chalk.red("".concat(proposalStats.netChanges))]);
58
+ }
59
+ } catch (err) {
60
+ _iterator.e(err);
61
+ } finally {
62
+ _iterator.f();
63
+ }
64
+ output += proposalTable.toString() + '\n';
65
+
66
+ // Proposal summary totals
67
+ var totalProposals = result.proposals.size;
68
+ var totalProposalCommits = Array.from(result.proposals.values()).reduce(function (sum, p) {
69
+ return sum + p.commits;
70
+ }, 0);
71
+ var totalProposalFiles = Array.from(result.proposals.values()).reduce(function (sum, p) {
72
+ return sum + p.codeFilesChanged;
73
+ }, 0);
74
+ var totalProposalAdditions = Array.from(result.proposals.values()).reduce(function (sum, p) {
75
+ return sum + p.additions;
76
+ }, 0);
77
+ var totalProposalDeletions = Array.from(result.proposals.values()).reduce(function (sum, p) {
78
+ return sum + p.deletions;
79
+ }, 0);
80
+ var totalProposalNetChanges = Array.from(result.proposals.values()).reduce(function (sum, p) {
81
+ return sum + p.netChanges;
82
+ }, 0);
83
+ output += chalk.gray(t('output.proposalTotal', {
84
+ count: totalProposals.toString(),
85
+ commits: totalProposalCommits.toString(),
86
+ files: totalProposalFiles.toString(),
87
+ additions: totalProposalAdditions.toString(),
88
+ deletions: totalProposalDeletions.toString(),
89
+ netChanges: totalProposalNetChanges.toString()
90
+ }));
91
+ }
92
+ output += chalk.bold.cyan("\n".concat(t('output.authorSummary'), "\n"));
37
93
  var sortedAuthors = Array.from(result.authors.values()).sort(function (a, b) {
38
94
  return b.commits - a.commits;
39
95
  });
40
- var _iterator = _createForOfIteratorHelper(sortedAuthors),
41
- _step;
96
+ var _iterator2 = _createForOfIteratorHelper(sortedAuthors),
97
+ _step2;
42
98
  try {
43
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
44
- var stats = _step.value;
99
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
100
+ var stats = _step2.value;
45
101
  output += chalk.bold.cyan("\n".concat(stats.author, "\n"));
46
102
  if (stats.branchStats && stats.branchStats.size > 0) {
47
103
  var branchTable = new Table({
@@ -54,17 +110,17 @@ export var OutputFormatter = /*#__PURE__*/function () {
54
110
  var sortedBranches = Array.from(stats.branchStats.values()).sort(function (a, b) {
55
111
  return b.commits - a.commits;
56
112
  });
57
- var _iterator2 = _createForOfIteratorHelper(sortedBranches),
58
- _step2;
113
+ var _iterator3 = _createForOfIteratorHelper(sortedBranches),
114
+ _step3;
59
115
  try {
60
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
61
- var branchStat = _step2.value;
116
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
117
+ var branchStat = _step3.value;
62
118
  branchTable.push([branchStat.branch, branchStat.commits.toString(), branchStat.openspecProposals.size.toString(), branchStat.codeFilesChanged.toString(), chalk.green("+".concat(branchStat.additions)), chalk.red("-".concat(branchStat.deletions)), branchStat.netChanges >= 0 ? chalk.green("+".concat(branchStat.netChanges)) : chalk.red("".concat(branchStat.netChanges))]);
63
119
  }
64
120
  } catch (err) {
65
- _iterator2.e(err);
121
+ _iterator3.e(err);
66
122
  } finally {
67
- _iterator2.f();
123
+ _iterator3.f();
68
124
  }
69
125
  branchTable.push([chalk.bold.yellow(t('table.totalDeduplicated')), chalk.bold(stats.commits.toString()), chalk.bold(stats.openspecProposals.size.toString()), chalk.bold(stats.codeFilesChanged.toString()), chalk.bold.green("+".concat(stats.additions)), chalk.bold.red("-".concat(stats.deletions)), stats.netChanges >= 0 ? chalk.bold.green("+".concat(stats.netChanges)) : chalk.bold.red("".concat(stats.netChanges))]);
70
126
  output += branchTable.toString() + '\n';
@@ -86,9 +142,9 @@ export var OutputFormatter = /*#__PURE__*/function () {
86
142
  }
87
143
  }
88
144
  } catch (err) {
89
- _iterator.e(err);
145
+ _iterator2.e(err);
90
146
  } finally {
91
- _iterator.f();
147
+ _iterator2.f();
92
148
  }
93
149
  return output;
94
150
  }
@@ -102,6 +158,38 @@ export var OutputFormatter = /*#__PURE__*/function () {
102
158
  },
103
159
  branches: result.branches,
104
160
  totalCommits: result.totalCommits,
161
+ proposals: {
162
+ items: Array.from(result.proposals.values()).map(function (stats) {
163
+ return {
164
+ proposal: stats.proposal,
165
+ commits: stats.commits,
166
+ contributors: Array.from(stats.contributors),
167
+ contributorCount: stats.contributors.size,
168
+ codeFilesChanged: stats.codeFilesChanged,
169
+ additions: stats.additions,
170
+ deletions: stats.deletions,
171
+ netChanges: stats.netChanges
172
+ };
173
+ }),
174
+ summary: {
175
+ totalProposals: result.proposals.size,
176
+ totalCommits: Array.from(result.proposals.values()).reduce(function (sum, p) {
177
+ return sum + p.commits;
178
+ }, 0),
179
+ totalCodeFiles: Array.from(result.proposals.values()).reduce(function (sum, p) {
180
+ return sum + p.codeFilesChanged;
181
+ }, 0),
182
+ totalAdditions: Array.from(result.proposals.values()).reduce(function (sum, p) {
183
+ return sum + p.additions;
184
+ }, 0),
185
+ totalDeletions: Array.from(result.proposals.values()).reduce(function (sum, p) {
186
+ return sum + p.deletions;
187
+ }, 0),
188
+ totalNetChanges: Array.from(result.proposals.values()).reduce(function (sum, p) {
189
+ return sum + p.netChanges;
190
+ }, 0)
191
+ }
192
+ },
105
193
  authors: Array.from(result.authors.values()).map(function (stats) {
106
194
  var _stats$lastCommitDate;
107
195
  return {
@@ -123,23 +211,72 @@ export var OutputFormatter = /*#__PURE__*/function () {
123
211
  key: "formatCSV",
124
212
  value: function formatCSV(result) {
125
213
  var rows = [];
214
+
215
+ // Proposal summary section
216
+ rows.push("\n# ".concat(t('output.proposalSummary')));
217
+ rows.push("".concat(t('table.proposal'), ",").concat(t('table.commits'), ",").concat(t('table.contributors'), ",").concat(t('table.codeFiles'), ",").concat(t('table.additions'), ",").concat(t('table.deletions'), ",").concat(t('table.netChanges')));
218
+ var sortedProposals = Array.from(result.proposals.values()).sort(function (a, b) {
219
+ return b.netChanges - a.netChanges;
220
+ });
221
+ var _iterator4 = _createForOfIteratorHelper(sortedProposals),
222
+ _step4;
223
+ try {
224
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
225
+ var stats = _step4.value;
226
+ var contributors = Array.from(stats.contributors).join(';');
227
+ rows.push([stats.proposal, stats.commits, "\"".concat(contributors, "\""), stats.codeFilesChanged, stats.additions, stats.deletions, stats.netChanges].join(','));
228
+ }
229
+
230
+ // Proposal totals
231
+ } catch (err) {
232
+ _iterator4.e(err);
233
+ } finally {
234
+ _iterator4.f();
235
+ }
236
+ var totalProposals = result.proposals.size;
237
+ var totalProposalCommits = Array.from(result.proposals.values()).reduce(function (sum, p) {
238
+ return sum + p.commits;
239
+ }, 0);
240
+ var totalProposalFiles = Array.from(result.proposals.values()).reduce(function (sum, p) {
241
+ return sum + p.codeFilesChanged;
242
+ }, 0);
243
+ var totalProposalAdditions = Array.from(result.proposals.values()).reduce(function (sum, p) {
244
+ return sum + p.additions;
245
+ }, 0);
246
+ var totalProposalDeletions = Array.from(result.proposals.values()).reduce(function (sum, p) {
247
+ return sum + p.deletions;
248
+ }, 0);
249
+ var totalProposalNetChanges = Array.from(result.proposals.values()).reduce(function (sum, p) {
250
+ return sum + p.netChanges;
251
+ }, 0);
252
+ rows.push('');
253
+ rows.push("# ".concat(t('output.proposalTotalLabel')));
254
+ rows.push("".concat(t('table.proposals'), ",").concat(totalProposals));
255
+ rows.push("".concat(t('table.commits'), ",").concat(totalProposalCommits));
256
+ rows.push("".concat(t('table.codeFiles'), ",").concat(totalProposalFiles));
257
+ rows.push("".concat(t('table.additions'), ",").concat(totalProposalAdditions));
258
+ rows.push("".concat(t('table.deletions'), ",").concat(totalProposalDeletions));
259
+ rows.push("".concat(t('table.netChanges'), ",").concat(totalProposalNetChanges));
260
+
261
+ // Author summary section
262
+ rows.push("\n# ".concat(t('output.authorSummary')));
126
263
  rows.push("".concat(t('table.author'), ",").concat(t('table.period'), ",").concat(t('table.commits'), ",").concat(t('table.proposalsCount'), ",").concat(t('table.proposalsList'), ",").concat(t('table.codeFiles'), ",").concat(t('table.additions'), ",").concat(t('table.deletions'), ",").concat(t('table.netChanges'), ",").concat(t('table.lastCommitDate')));
127
264
  var sortedAuthors = Array.from(result.authors.values()).sort(function (a, b) {
128
265
  return b.commits - a.commits;
129
266
  });
130
- var _iterator3 = _createForOfIteratorHelper(sortedAuthors),
131
- _step3;
267
+ var _iterator5 = _createForOfIteratorHelper(sortedAuthors),
268
+ _step5;
132
269
  try {
133
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
270
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
134
271
  var _stats$lastCommitDate2;
135
- var stats = _step3.value;
136
- var proposals = Array.from(stats.openspecProposals).join(';');
137
- rows.push([stats.author, stats.statisticsPeriod || '-', stats.commits, stats.openspecProposals.size, "\"".concat(proposals, "\""), stats.codeFilesChanged, stats.additions, stats.deletions, stats.netChanges, ((_stats$lastCommitDate2 = stats.lastCommitDate) === null || _stats$lastCommitDate2 === void 0 ? void 0 : _stats$lastCommitDate2.toISOString()) || ''].join(','));
272
+ var _stats = _step5.value;
273
+ var proposals = Array.from(_stats.openspecProposals).join(';');
274
+ rows.push([_stats.author, _stats.statisticsPeriod || '-', _stats.commits, _stats.openspecProposals.size, "\"".concat(proposals, "\""), _stats.codeFilesChanged, _stats.additions, _stats.deletions, _stats.netChanges, ((_stats$lastCommitDate2 = _stats.lastCommitDate) === null || _stats$lastCommitDate2 === void 0 ? void 0 : _stats$lastCommitDate2.toISOString()) || ''].join(','));
138
275
  }
139
276
  } catch (err) {
140
- _iterator3.e(err);
277
+ _iterator5.e(err);
141
278
  } finally {
142
- _iterator3.f();
279
+ _iterator5.f();
143
280
  }
144
281
  return rows.join('\n');
145
282
  }
@@ -162,33 +299,81 @@ export var OutputFormatter = /*#__PURE__*/function () {
162
299
  md += t('markdown.totalCommits', {
163
300
  count: String(result.totalCommits)
164
301
  });
165
- md += t('markdown.statistics');
302
+
303
+ // Proposal summary
304
+ md += "\n## ".concat(t('output.proposalSummary'), "\n\n");
305
+ md += "| ".concat(t('table.proposal'), " | ").concat(t('table.commits'), " | ").concat(t('table.contributors'), " | ").concat(t('table.codeFiles'), " | ").concat(t('table.additions'), " | ").concat(t('table.deletions'), " | ").concat(t('table.netChanges'), " |\n");
306
+ md += '|--------|---------|-------------|------------|-----------|-----------|-------------|\n';
307
+ var sortedProposals = Array.from(result.proposals.values()).sort(function (a, b) {
308
+ return b.netChanges - a.netChanges;
309
+ });
310
+ var _iterator6 = _createForOfIteratorHelper(sortedProposals),
311
+ _step6;
312
+ try {
313
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
314
+ var stats = _step6.value;
315
+ var contributors = Array.from(stats.contributors).join(', ');
316
+ md += "| ".concat(stats.proposal, " | ").concat(stats.commits, " | ").concat(contributors, " | ").concat(stats.codeFilesChanged, " | +").concat(stats.additions, " | -").concat(stats.deletions, " | ").concat(stats.netChanges >= 0 ? '+' : '').concat(stats.netChanges, " |\n");
317
+ }
318
+
319
+ // Proposal totals
320
+ } catch (err) {
321
+ _iterator6.e(err);
322
+ } finally {
323
+ _iterator6.f();
324
+ }
325
+ var totalProposals = result.proposals.size;
326
+ var totalProposalCommits = Array.from(result.proposals.values()).reduce(function (sum, p) {
327
+ return sum + p.commits;
328
+ }, 0);
329
+ var totalProposalFiles = Array.from(result.proposals.values()).reduce(function (sum, p) {
330
+ return sum + p.codeFilesChanged;
331
+ }, 0);
332
+ var totalProposalAdditions = Array.from(result.proposals.values()).reduce(function (sum, p) {
333
+ return sum + p.additions;
334
+ }, 0);
335
+ var totalProposalDeletions = Array.from(result.proposals.values()).reduce(function (sum, p) {
336
+ return sum + p.deletions;
337
+ }, 0);
338
+ var totalProposalNetChanges = Array.from(result.proposals.values()).reduce(function (sum, p) {
339
+ return sum + p.netChanges;
340
+ }, 0);
341
+ md += "\n**".concat(t('output.proposalTotalLabel'), "**\n");
342
+ md += "- ".concat(t('table.proposals'), ": ").concat(totalProposals, "\n");
343
+ md += "- ".concat(t('table.commits'), ": ").concat(totalProposalCommits, "\n");
344
+ md += "- ".concat(t('table.codeFiles'), ": ").concat(totalProposalFiles, "\n");
345
+ md += "- ".concat(t('table.additions'), ": +").concat(totalProposalAdditions, "\n");
346
+ md += "- ".concat(t('table.deletions'), ": -").concat(totalProposalDeletions, "\n");
347
+ md += "- ".concat(t('table.netChanges'), ": ").concat(totalProposalNetChanges >= 0 ? '+' : '').concat(totalProposalNetChanges, "\n");
348
+
349
+ // Author summary
350
+ md += "\n## ".concat(t('output.authorSummary'), "\n\n");
166
351
  md += "| ".concat(t('table.author'), " | ").concat(t('table.period'), " | ").concat(t('table.commits'), " | ").concat(t('table.proposals'), " | ").concat(t('table.codeFiles'), " | ").concat(t('table.additions'), " | ").concat(t('table.deletions'), " | ").concat(t('table.netChanges'), " |\n");
167
352
  md += '|--------|--------|---------|-----------|------------|-----------|-----------|-------------|\n';
168
353
  var sortedAuthors = Array.from(result.authors.values()).sort(function (a, b) {
169
354
  return b.commits - a.commits;
170
355
  });
171
- var _iterator4 = _createForOfIteratorHelper(sortedAuthors),
172
- _step4;
356
+ var _iterator7 = _createForOfIteratorHelper(sortedAuthors),
357
+ _step7;
173
358
  try {
174
- for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
175
- var stats = _step4.value;
176
- md += "| ".concat(stats.author, " | ").concat(stats.statisticsPeriod || '-', " | ").concat(stats.commits, " | ").concat(stats.openspecProposals.size, " | ").concat(stats.codeFilesChanged, " | +").concat(stats.additions, " | -").concat(stats.deletions, " | ").concat(stats.netChanges >= 0 ? '+' : '').concat(stats.netChanges, " |\n");
359
+ for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
360
+ var _stats2 = _step7.value;
361
+ md += "| ".concat(_stats2.author, " | ").concat(_stats2.statisticsPeriod || '-', " | ").concat(_stats2.commits, " | ").concat(_stats2.openspecProposals.size, " | ").concat(_stats2.codeFilesChanged, " | +").concat(_stats2.additions, " | -").concat(_stats2.deletions, " | ").concat(_stats2.netChanges >= 0 ? '+' : '').concat(_stats2.netChanges, " |\n");
177
362
  }
178
363
  } catch (err) {
179
- _iterator4.e(err);
364
+ _iterator7.e(err);
180
365
  } finally {
181
- _iterator4.f();
366
+ _iterator7.f();
182
367
  }
183
368
  md += t('markdown.proposalDetails');
184
- var _iterator5 = _createForOfIteratorHelper(sortedAuthors),
185
- _step5;
369
+ var _iterator8 = _createForOfIteratorHelper(sortedAuthors),
370
+ _step8;
186
371
  try {
187
- for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
188
- var _stats = _step5.value;
189
- if (_stats.openspecProposals.size > 0) {
190
- md += "### ".concat(_stats.author, "\n\n");
191
- for (var _i = 0, _Array$from = Array.from(_stats.openspecProposals); _i < _Array$from.length; _i++) {
372
+ for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
373
+ var _stats3 = _step8.value;
374
+ if (_stats3.openspecProposals.size > 0) {
375
+ md += "### ".concat(_stats3.author, "\n\n");
376
+ for (var _i = 0, _Array$from = Array.from(_stats3.openspecProposals); _i < _Array$from.length; _i++) {
192
377
  var proposal = _Array$from[_i];
193
378
  md += "- ".concat(proposal, "\n");
194
379
  }
@@ -196,9 +381,9 @@ export var OutputFormatter = /*#__PURE__*/function () {
196
381
  }
197
382
  }
198
383
  } catch (err) {
199
- _iterator5.e(err);
384
+ _iterator8.e(err);
200
385
  } finally {
201
- _iterator5.f();
386
+ _iterator8.f();
202
387
  }
203
388
  return md;
204
389
  }
@@ -48,11 +48,17 @@
48
48
  "output.branches": "Branches: {{branches}}\n",
49
49
  "output.totalCommits": "Total Commits: {{count}}\n\n",
50
50
  "output.proposals": " Proposals: {{proposals}}\n",
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)",
51
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",
@@ -48,11 +48,17 @@
48
48
  "output.branches": "分支:{{branches}}\n",
49
49
  "output.totalCommits": "总提交数:{{count}}\n\n",
50
50
  "output.proposals": " 提案:{{proposals}}\n",
51
+ "output.proposalSummary": "📋 提案汇总(按提案统计)",
52
+ "output.proposalTotal": " 📊 总计:{{count}} 个提案 | {{commits}} 次提交 | {{files}} 个文件 | +{{additions}}/-{{deletions}} 行(净变更:{{netChanges}})\n",
53
+ "output.proposalTotalLabel": "提案汇总总计",
54
+ "output.authorSummary": "👥 作者汇总(按贡献者统计)",
51
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": "删除行数",
@@ -21,6 +21,7 @@ export var StatsAggregator = /*#__PURE__*/function () {
21
21
  key: "aggregate",
22
22
  value: function aggregate(analyses, since, until, branches, filterAuthor) {
23
23
  var authorStatsMap = new Map();
24
+ var proposalStatsMap = new Map();
24
25
  var _iterator = _createForOfIteratorHelper(analyses),
25
26
  _step;
26
27
  try {
@@ -58,6 +59,33 @@ export var StatsAggregator = /*#__PURE__*/function () {
58
59
  for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
59
60
  var _proposal = _step4.value;
60
61
  stats.openspecProposals.add(_proposal);
62
+
63
+ // Aggregate by proposal
64
+ var proposalStats = proposalStatsMap.get(_proposal);
65
+ if (!proposalStats) {
66
+ proposalStats = {
67
+ proposal: _proposal,
68
+ commits: 0,
69
+ contributors: new Set(),
70
+ codeFilesChanged: 0,
71
+ additions: 0,
72
+ deletions: 0,
73
+ netChanges: 0,
74
+ commitHashes: new Set()
75
+ };
76
+ proposalStatsMap.set(_proposal, proposalStats);
77
+ }
78
+
79
+ // Only count each commit once per proposal
80
+ if (!proposalStats.commitHashes.has(analysis.commit.hash)) {
81
+ proposalStats.commitHashes.add(analysis.commit.hash);
82
+ proposalStats.commits++;
83
+ proposalStats.contributors.add(normalizedAuthor);
84
+ proposalStats.codeFilesChanged += analysis.codeFiles.length;
85
+ proposalStats.additions += analysis.totalAdditions;
86
+ proposalStats.deletions += analysis.totalDeletions;
87
+ proposalStats.netChanges += analysis.netChanges;
88
+ }
61
89
  }
62
90
  } catch (err) {
63
91
  _iterator4.e(err);
@@ -167,6 +195,7 @@ export var StatsAggregator = /*#__PURE__*/function () {
167
195
  },
168
196
  branches: actualBranches.size > 0 ? Array.from(actualBranches) : branches,
169
197
  authors: authorStatsMap,
198
+ proposals: proposalStatsMap,
170
199
  totalCommits: analyses.length
171
200
  };
172
201
  }
@@ -66,6 +66,16 @@ export interface BranchStats {
66
66
  deletions: number;
67
67
  netChanges: number;
68
68
  }
69
+ export interface ProposalStats {
70
+ proposal: string;
71
+ commits: number;
72
+ contributors: Set<string>;
73
+ codeFilesChanged: number;
74
+ additions: number;
75
+ deletions: number;
76
+ netChanges: number;
77
+ commitHashes: Set<string>;
78
+ }
69
79
  export interface StatsResult {
70
80
  timeRange: {
71
81
  since: Date;
@@ -73,5 +83,6 @@ export interface StatsResult {
73
83
  };
74
84
  branches: string[];
75
85
  authors: Map<string, AuthorStats>;
86
+ proposals: Map<string, ProposalStats>;
76
87
  totalCommits: number;
77
88
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openspec-stat",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Track team members' OpenSpec proposals and code changes in Git repositories",
5
5
  "main": "dist/cjs/index.js",
6
6
  "types": "dist/cjs/index.d.ts",