openspec-stat 1.3.2 → 1.3.4

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
@@ -275,6 +275,11 @@ pnpm build
275
275
  node dist/esm/cli.js
276
276
  ```
277
277
 
278
+ ## Contributing & Release Process
279
+
280
+ - See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup, branching, and pull-request expectations.
281
+ - See [RELEASE.md](./RELEASE.md) for the Changesets-driven versioning and publishing workflow.
282
+
278
283
  ## LICENSE
279
284
 
280
285
  MIT
package/README.zh-CN.md CHANGED
@@ -273,6 +273,11 @@ pnpm build
273
273
  node dist/esm/cli.js
274
274
  ```
275
275
 
276
+ ## 贡献与发布流程
277
+
278
+ - 请阅读 [CONTRIBUTING.md](./CONTRIBUTING.md) 了解开发环境、分支策略和 PR 规范。
279
+ - 请阅读 [RELEASE.md](./RELEASE.md) 了解基于 Changesets 的版本与发布流程。
280
+
276
281
  ## 许可证
277
282
 
278
283
  MIT
@@ -102,6 +102,10 @@ async function selectBranches(repoPath, defaultBranches) {
102
102
  choices: choices.slice(0, -1),
103
103
  pageSize: 15
104
104
  });
105
+ if (selected.length === 0) {
106
+ console.log(import_chalk.default.yellow((0, import_i18n.t)("warning.noBranchesSelected")));
107
+ process.exit(1);
108
+ }
105
109
  if (selected.length > 0) {
106
110
  console.log(import_chalk.default.green((0, import_i18n.t)("branch.selected")));
107
111
  selected.forEach((branch) => {
@@ -115,7 +119,7 @@ async function selectBranches(repoPath, defaultBranches) {
115
119
  const customBranches = customInput.split(",").map((b) => b.trim()).filter((b) => b);
116
120
  return [...selected.filter((b) => b !== "__custom__"), ...customBranches];
117
121
  }
118
- return selected.length > 0 ? selected : defaultBranches || [];
122
+ return selected;
119
123
  }
120
124
  // Annotate the CommonJS export names for ESM import in node:
121
125
  0 && (module.exports = {
package/dist/cjs/cli.js CHANGED
@@ -6,7 +6,7 @@ var import_single = require("./commands/single.js");
6
6
  var import_multi = require("./commands/multi.js");
7
7
  var import_init = require("./commands/init.js");
8
8
  var program = new import_commander.Command();
9
- program.name("openspec-stat").description("Track team members' OpenSpec proposals and code changes in Git repositories").version("1.3.2").enablePositionalOptions().passThroughOptions();
9
+ program.name("openspec-stat").description("Track team members' OpenSpec proposals and code changes in Git repositories").version("1.3.4").enablePositionalOptions().passThroughOptions();
10
10
  program.argument("[repo]", "Repository path", ".").option("-r, --repo <path>", "Repository path (alternative)", ".").option("-b, --branches <branches>", "Branch list, comma-separated").option("--no-interactive", "Disable interactive branch selection").option("-s, --since <datetime>", "Start time (default: yesterday 20:00)").option("-u, --until <datetime>", "End time (default: today 20:00)").option("-a, --author <name>", "Filter by specific author").option("--json", "Output in JSON format").option("--csv", "Output in CSV format").option("--markdown", "Output in Markdown format").option("-c, --config <path>", "Configuration file path").option("-v, --verbose", "Verbose output mode").option("-l, --lang <language>", "Language for output (en, zh-CN)", "en").option("--no-fetch", "Skip fetching remote branches").action(async (repo, options) => {
11
11
  await (0, import_single.runSingleRepoCommand)({ ...options, repo: repo || options.repo || "." });
12
12
  });
@@ -69,8 +69,9 @@ ${(0, import_i18n.t)("output.proposalSummary")}
69
69
  const sortedProposals = Array.from(result.proposals.values()).sort((a, b) => b.netChanges - a.netChanges);
70
70
  for (const proposalStats of sortedProposals) {
71
71
  const contributors = Array.from(proposalStats.contributors).join(", ");
72
+ const proposalName = proposalStats.multiProposalCommits > 0 ? `${proposalStats.proposal} ${import_chalk.default.yellow("⚠")}` : proposalStats.proposal;
72
73
  proposalTable.push([
73
- proposalStats.proposal,
74
+ proposalName,
74
75
  proposalStats.commits.toString(),
75
76
  contributors,
76
77
  proposalStats.codeFilesChanged.toString(),
@@ -80,13 +81,24 @@ ${(0, import_i18n.t)("output.proposalSummary")}
80
81
  ]);
81
82
  }
82
83
  output += proposalTable.toString() + "\n";
84
+ const hasMultiProposalCommits = Array.from(result.proposals.values()).some((p) => p.multiProposalCommits > 0);
85
+ if (hasMultiProposalCommits) {
86
+ output += import_chalk.default.yellow(`
87
+ ⚠ ${(0, import_i18n.t)("output.multiProposalWarning")}
88
+ `);
89
+ const affectedProposals = Array.from(result.proposals.values()).filter((p) => p.multiProposalCommits > 0).map(
90
+ (p) => ` • ${p.proposal}: ${p.multiProposalCommits}/${p.commits} commits ${(0, import_i18n.t)("output.sharedWithOthers")}`
91
+ ).join("\n");
92
+ output += import_chalk.default.gray(affectedProposals + "\n");
93
+ }
83
94
  const totalProposals = result.proposals.size;
84
95
  const totalProposalCommits = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.commits, 0);
85
96
  const totalProposalFiles = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.codeFilesChanged, 0);
86
97
  const totalProposalAdditions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.additions, 0);
87
98
  const totalProposalDeletions = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.deletions, 0);
88
99
  const totalProposalNetChanges = Array.from(result.proposals.values()).reduce((sum, p) => sum + p.netChanges, 0);
89
- output += import_chalk.default.gray(
100
+ output += import_chalk.default.cyan("─".repeat(80)) + "\n";
101
+ output += import_chalk.default.bold.cyan(
90
102
  (0, import_i18n.t)("output.proposalTotal", {
91
103
  count: totalProposals.toString(),
92
104
  commits: totalProposalCommits.toString(),
@@ -96,6 +108,7 @@ ${(0, import_i18n.t)("output.proposalSummary")}
96
108
  netChanges: totalProposalNetChanges.toString()
97
109
  })
98
110
  );
111
+ output += import_chalk.default.cyan("─".repeat(80)) + "\n";
99
112
  }
100
113
  output += import_chalk.default.bold.cyan(`
101
114
  ${(0, import_i18n.t)("output.authorSummary")}
@@ -231,7 +244,10 @@ ${stats.author}
231
244
  codeFilesChanged: stats.codeFilesChanged,
232
245
  additions: stats.additions,
233
246
  deletions: stats.deletions,
234
- netChanges: stats.netChanges
247
+ netChanges: stats.netChanges,
248
+ multiProposalCommits: stats.multiProposalCommits,
249
+ hasSharedCommits: stats.multiProposalCommits > 0,
250
+ sharedCommitHashes: Array.from(stats.sharedCommitHashes)
235
251
  })),
236
252
  summary: {
237
253
  totalProposals: result.proposals.size,
@@ -375,7 +391,8 @@ ${stats.author}
375
391
  const sortedProposals = Array.from(result.proposals.values()).sort((a, b) => b.netChanges - a.netChanges);
376
392
  for (const stats of sortedProposals) {
377
393
  const contributors = Array.from(stats.contributors).join(", ");
378
- md += `| ${stats.proposal} | ${stats.commits} | ${contributors} | ${stats.codeFilesChanged} | +${stats.additions} | -${stats.deletions} | ${stats.netChanges >= 0 ? "+" : ""}${stats.netChanges} |
394
+ const proposalName = stats.multiProposalCommits > 0 ? `${stats.proposal} ⚠️` : stats.proposal;
395
+ md += `| ${proposalName} | ${stats.commits} | ${contributors} | ${stats.codeFilesChanged} | +${stats.additions} | -${stats.deletions} | ${stats.netChanges >= 0 ? "+" : ""}${stats.netChanges} |
379
396
  `;
380
397
  }
381
398
  const totalProposals = result.proposals.size;
@@ -399,6 +416,19 @@ ${stats.author}
399
416
  `;
400
417
  md += `- ${(0, import_i18n.t)("table.netChanges")}: ${totalProposalNetChanges >= 0 ? "+" : ""}${totalProposalNetChanges}
401
418
  `;
419
+ const hasMultiProposalCommits = Array.from(result.proposals.values()).some((p) => p.multiProposalCommits > 0);
420
+ if (hasMultiProposalCommits) {
421
+ md += `
422
+ > ⚠️ **${(0, import_i18n.t)("output.multiProposalWarning")}**
423
+ >
424
+ `;
425
+ const affectedProposals = Array.from(result.proposals.values()).filter((p) => p.multiProposalCommits > 0);
426
+ for (const p of affectedProposals) {
427
+ md += `> - ${p.proposal}: ${p.multiProposalCommits}/${p.commits} commits ${(0, import_i18n.t)("output.sharedWithOthers")}
428
+ `;
429
+ }
430
+ md += "\n";
431
+ }
402
432
  md += `
403
433
  ## ${(0, import_i18n.t)("output.authorSummary")}
404
434
 
@@ -29,6 +29,7 @@
29
29
  "warning.noCommits": "⚠️ No commits found matching the criteria",
30
30
  "warning.noQualifyingCommits": "⚠️ No commits found containing both OpenSpec proposals and code changes",
31
31
  "warning.noBranches": "⚠️ No remote branches found",
32
+ "warning.noBranchesSelected": "⚠️ No branches selected. Please select at least one branch to analyze.",
32
33
 
33
34
  "error.prefix": "❌ Error:",
34
35
 
@@ -52,6 +53,8 @@
52
53
  "output.proposalSummary": "📋 Proposal Summary (by proposal)",
53
54
  "output.proposalTotal": " 📊 Total: {{count}} proposals | {{commits}} commits | {{files}} files | +{{additions}}/-{{deletions}} lines (net: {{netChanges}})\n",
54
55
  "output.proposalTotalLabel": "Proposal Summary Total",
56
+ "output.multiProposalWarning": "Note: Some proposals contain commits shared with other proposals. Code changes have been evenly distributed among proposals.",
57
+ "output.sharedWithOthers": "shared with other proposals",
55
58
  "output.authorSummary": "👥 Author Summary (by contributor)",
56
59
  "output.contributorHint": "💡 Use --show-contributors to see detailed statistics for each contributor",
57
60
 
@@ -29,6 +29,7 @@
29
29
  "warning.noCommits": "⚠️ 未找到符合条件的提交",
30
30
  "warning.noQualifyingCommits": "⚠️ 未找到同时包含 OpenSpec 提案和代码变更的提交",
31
31
  "warning.noBranches": "⚠️ 未找到远程分支",
32
+ "warning.noBranchesSelected": "⚠️ 未选择任何分支。请至少选择一个分支进行分析。",
32
33
 
33
34
  "error.prefix": "❌ 错误:",
34
35
 
@@ -52,6 +53,8 @@
52
53
  "output.proposalSummary": "📋 提案汇总(按提案统计)",
53
54
  "output.proposalTotal": " 📊 总计:{{count}} 个提案 | {{commits}} 次提交 | {{files}} 个文件 | +{{additions}}/-{{deletions}} 行(净变更:{{netChanges}})\n",
54
55
  "output.proposalTotalLabel": "提案汇总总计",
56
+ "output.multiProposalWarning": "注意:部分提案包含多提案共享的 commit,代码变更已按提案数量平均分配",
57
+ "output.sharedWithOthers": "与其他提案共享",
55
58
  "output.authorSummary": "👥 作者汇总(按贡献者统计)",
56
59
  "output.contributorHint": "💡 使用 --show-contributors 选项可查看每个贡献者的详细统计信息",
57
60
 
@@ -70,7 +70,9 @@ var StatsAggregator = class {
70
70
  additions: 0,
71
71
  deletions: 0,
72
72
  netChanges: 0,
73
- commitHashes: /* @__PURE__ */ new Set()
73
+ commitHashes: /* @__PURE__ */ new Set(),
74
+ multiProposalCommits: 0,
75
+ sharedCommitHashes: /* @__PURE__ */ new Set()
74
76
  };
75
77
  proposalStatsMap.set(proposal, proposalStats);
76
78
  }
@@ -78,10 +80,16 @@ var StatsAggregator = class {
78
80
  proposalStats.commitHashes.add(analysis.commit.hash);
79
81
  proposalStats.commits++;
80
82
  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;
83
+ const proposalCount = analysis.openspecProposals.size;
84
+ const isMultiProposal = proposalCount > 1;
85
+ if (isMultiProposal) {
86
+ proposalStats.multiProposalCommits++;
87
+ proposalStats.sharedCommitHashes.add(analysis.commit.hash);
88
+ }
89
+ proposalStats.codeFilesChanged += Math.floor(analysis.codeFiles.length / proposalCount);
90
+ proposalStats.additions += Math.floor(analysis.totalAdditions / proposalCount);
91
+ proposalStats.deletions += Math.floor(analysis.totalDeletions / proposalCount);
92
+ proposalStats.netChanges += Math.floor(analysis.netChanges / proposalCount);
85
93
  }
86
94
  }
87
95
  if (!stats.lastCommitDate || analysis.commit.date > stats.lastCommitDate) {
@@ -77,6 +77,8 @@ export interface ProposalStats {
77
77
  deletions: number;
78
78
  netChanges: number;
79
79
  commitHashes: Set<string>;
80
+ multiProposalCommits: number;
81
+ sharedCommitHashes: Set<string>;
80
82
  }
81
83
  export interface StatsResult {
82
84
  timeRange: {
@@ -182,6 +182,10 @@ function _selectBranches() {
182
182
  });
183
183
  case 21:
184
184
  selected = _context2.sent;
185
+ if (selected.length === 0) {
186
+ console.log(chalk.yellow(t('warning.noBranchesSelected')));
187
+ process.exit(1);
188
+ }
185
189
  if (selected.length > 0) {
186
190
  console.log(chalk.green(t('branch.selected')));
187
191
  selected.forEach(function (branch) {
@@ -189,14 +193,14 @@ function _selectBranches() {
189
193
  });
190
194
  }
191
195
  if (!selected.includes('__custom__')) {
192
- _context2.next = 29;
196
+ _context2.next = 30;
193
197
  break;
194
198
  }
195
- _context2.next = 26;
199
+ _context2.next = 27;
196
200
  return input({
197
201
  message: t('branch.additionalInput')
198
202
  });
199
- case 26:
203
+ case 27:
200
204
  _customInput = _context2.sent;
201
205
  customBranches = _customInput.split(',').map(function (b) {
202
206
  return b.trim();
@@ -206,9 +210,9 @@ function _selectBranches() {
206
210
  return _context2.abrupt("return", [].concat(_toConsumableArray(selected.filter(function (b) {
207
211
  return b !== '__custom__';
208
212
  })), _toConsumableArray(customBranches)));
209
- case 29:
210
- return _context2.abrupt("return", selected.length > 0 ? selected : defaultBranches || []);
211
213
  case 30:
214
+ return _context2.abrupt("return", selected);
215
+ case 31:
212
216
  case "end":
213
217
  return _context2.stop();
214
218
  }
package/dist/esm/cli.js CHANGED
@@ -13,7 +13,7 @@ import { runSingleRepoCommand } from "./commands/single.js";
13
13
  import { runMultiRepoCommand } from "./commands/multi.js";
14
14
  import { runInitCommand } from "./commands/init.js";
15
15
  var program = new Command();
16
- program.name('openspec-stat').description("Track team members' OpenSpec proposals and code changes in Git repositories").version("1.3.2").enablePositionalOptions().passThroughOptions();
16
+ program.name('openspec-stat').description("Track team members' OpenSpec proposals and code changes in Git repositories").version("1.3.4").enablePositionalOptions().passThroughOptions();
17
17
 
18
18
  // Default command for single-repository mode (for backward compatibility)
19
19
  program.argument('[repo]', 'Repository path', '.').option('-r, --repo <path>', 'Repository path (alternative)', '.').option('-b, --branches <branches>', 'Branch list, comma-separated').option('--no-interactive', 'Disable interactive branch selection').option('-s, --since <datetime>', 'Start time (default: yesterday 20:00)').option('-u, --until <datetime>', 'End time (default: today 20:00)').option('-a, --author <name>', 'Filter by specific author').option('--json', 'Output in JSON format').option('--csv', 'Output in CSV format').option('--markdown', 'Output in Markdown format').option('-c, --config <path>', 'Configuration file path').option('-v, --verbose', 'Verbose output mode').option('-l, --lang <language>', 'Language for output (en, zh-CN)', 'en').option('--no-fetch', 'Skip fetching remote branches').action( /*#__PURE__*/function () {
@@ -55,7 +55,8 @@ export var OutputFormatter = /*#__PURE__*/function () {
55
55
  for (_iterator.s(); !(_step = _iterator.n()).done;) {
56
56
  var proposalStats = _step.value;
57
57
  var contributors = Array.from(proposalStats.contributors).join(', ');
58
- 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
+ var proposalName = proposalStats.multiProposalCommits > 0 ? "".concat(proposalStats.proposal, " ").concat(chalk.yellow('⚠')) : proposalStats.proposal;
59
+ proposalTable.push([proposalName, 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))]);
59
60
  }
60
61
  } catch (err) {
61
62
  _iterator.e(err);
@@ -63,6 +64,18 @@ export var OutputFormatter = /*#__PURE__*/function () {
63
64
  _iterator.f();
64
65
  }
65
66
  output += proposalTable.toString() + '\n';
67
+ var hasMultiProposalCommits = Array.from(result.proposals.values()).some(function (p) {
68
+ return p.multiProposalCommits > 0;
69
+ });
70
+ if (hasMultiProposalCommits) {
71
+ output += chalk.yellow("\n\u26A0 ".concat(t('output.multiProposalWarning'), "\n"));
72
+ var affectedProposals = Array.from(result.proposals.values()).filter(function (p) {
73
+ return p.multiProposalCommits > 0;
74
+ }).map(function (p) {
75
+ return " \u2022 ".concat(p.proposal, ": ").concat(p.multiProposalCommits, "/").concat(p.commits, " commits ").concat(t('output.sharedWithOthers'));
76
+ }).join('\n');
77
+ output += chalk.gray(affectedProposals + '\n');
78
+ }
66
79
 
67
80
  // Proposal summary totals
68
81
  var totalProposals = result.proposals.size;
@@ -81,7 +94,8 @@ export var OutputFormatter = /*#__PURE__*/function () {
81
94
  var totalProposalNetChanges = Array.from(result.proposals.values()).reduce(function (sum, p) {
82
95
  return sum + p.netChanges;
83
96
  }, 0);
84
- output += chalk.gray(t('output.proposalTotal', {
97
+ output += chalk.cyan('─'.repeat(80)) + '\n';
98
+ output += chalk.bold.cyan(t('output.proposalTotal', {
85
99
  count: totalProposals.toString(),
86
100
  commits: totalProposalCommits.toString(),
87
101
  files: totalProposalFiles.toString(),
@@ -89,6 +103,7 @@ export var OutputFormatter = /*#__PURE__*/function () {
89
103
  deletions: totalProposalDeletions.toString(),
90
104
  netChanges: totalProposalNetChanges.toString()
91
105
  }));
106
+ output += chalk.cyan('─'.repeat(80)) + '\n';
92
107
  }
93
108
  output += chalk.bold.cyan("\n".concat(t('output.authorSummary'), "\n"));
94
109
  var sortedAuthors = Array.from(result.authors.values()).sort(function (a, b) {
@@ -207,7 +222,10 @@ export var OutputFormatter = /*#__PURE__*/function () {
207
222
  codeFilesChanged: stats.codeFilesChanged,
208
223
  additions: stats.additions,
209
224
  deletions: stats.deletions,
210
- netChanges: stats.netChanges
225
+ netChanges: stats.netChanges,
226
+ multiProposalCommits: stats.multiProposalCommits,
227
+ hasSharedCommits: stats.multiProposalCommits > 0,
228
+ sharedCommitHashes: Array.from(stats.sharedCommitHashes)
211
229
  };
212
230
  }),
213
231
  summary: {
@@ -405,7 +423,8 @@ export var OutputFormatter = /*#__PURE__*/function () {
405
423
  for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
406
424
  var _stats3 = _step6.value;
407
425
  var contributors = Array.from(_stats3.contributors).join(', ');
408
- md += "| ".concat(_stats3.proposal, " | ").concat(_stats3.commits, " | ").concat(contributors, " | ").concat(_stats3.codeFilesChanged, " | +").concat(_stats3.additions, " | -").concat(_stats3.deletions, " | ").concat(_stats3.netChanges >= 0 ? '+' : '').concat(_stats3.netChanges, " |\n");
426
+ var proposalName = _stats3.multiProposalCommits > 0 ? "".concat(_stats3.proposal, " \u26A0\uFE0F") : _stats3.proposal;
427
+ md += "| ".concat(proposalName, " | ").concat(_stats3.commits, " | ").concat(contributors, " | ").concat(_stats3.codeFilesChanged, " | +").concat(_stats3.additions, " | -").concat(_stats3.deletions, " | ").concat(_stats3.netChanges >= 0 ? '+' : '').concat(_stats3.netChanges, " |\n");
409
428
  }
410
429
 
411
430
  // Proposal totals
@@ -437,6 +456,28 @@ export var OutputFormatter = /*#__PURE__*/function () {
437
456
  md += "- ".concat(t('table.additions'), ": +").concat(totalProposalAdditions, "\n");
438
457
  md += "- ".concat(t('table.deletions'), ": -").concat(totalProposalDeletions, "\n");
439
458
  md += "- ".concat(t('table.netChanges'), ": ").concat(totalProposalNetChanges >= 0 ? '+' : '').concat(totalProposalNetChanges, "\n");
459
+ var hasMultiProposalCommits = Array.from(result.proposals.values()).some(function (p) {
460
+ return p.multiProposalCommits > 0;
461
+ });
462
+ if (hasMultiProposalCommits) {
463
+ md += "\n> \u26A0\uFE0F **".concat(t('output.multiProposalWarning'), "**\n>\n");
464
+ var affectedProposals = Array.from(result.proposals.values()).filter(function (p) {
465
+ return p.multiProposalCommits > 0;
466
+ });
467
+ var _iterator7 = _createForOfIteratorHelper(affectedProposals),
468
+ _step7;
469
+ try {
470
+ for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
471
+ var p = _step7.value;
472
+ md += "> - ".concat(p.proposal, ": ").concat(p.multiProposalCommits, "/").concat(p.commits, " commits ").concat(t('output.sharedWithOthers'), "\n");
473
+ }
474
+ } catch (err) {
475
+ _iterator7.e(err);
476
+ } finally {
477
+ _iterator7.f();
478
+ }
479
+ md += '\n';
480
+ }
440
481
 
441
482
  // Author summary
442
483
  md += "\n## ".concat(t('output.authorSummary'), "\n\n");
@@ -446,24 +487,24 @@ export var OutputFormatter = /*#__PURE__*/function () {
446
487
  if (showContributors) {
447
488
  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");
448
489
  md += '|--------|--------|---------|-----------|------------|-----------|-----------|-------------|\n';
449
- var _iterator7 = _createForOfIteratorHelper(sortedAuthors),
450
- _step7;
490
+ var _iterator8 = _createForOfIteratorHelper(sortedAuthors),
491
+ _step8;
451
492
  try {
452
- for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
453
- var stats = _step7.value;
493
+ for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
494
+ var stats = _step8.value;
454
495
  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");
455
496
  }
456
497
  } catch (err) {
457
- _iterator7.e(err);
498
+ _iterator8.e(err);
458
499
  } finally {
459
- _iterator7.f();
500
+ _iterator8.f();
460
501
  }
461
502
  md += t('markdown.proposalDetails');
462
- var _iterator8 = _createForOfIteratorHelper(sortedAuthors),
463
- _step8;
503
+ var _iterator9 = _createForOfIteratorHelper(sortedAuthors),
504
+ _step9;
464
505
  try {
465
- for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
466
- var _stats2 = _step8.value;
506
+ for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {
507
+ var _stats2 = _step9.value;
467
508
  if (_stats2.openspecProposals.size > 0) {
468
509
  md += "### ".concat(_stats2.author, "\n\n");
469
510
  for (var _i = 0, _Array$from = Array.from(_stats2.openspecProposals); _i < _Array$from.length; _i++) {
@@ -474,9 +515,9 @@ export var OutputFormatter = /*#__PURE__*/function () {
474
515
  }
475
516
  }
476
517
  } catch (err) {
477
- _iterator8.e(err);
518
+ _iterator9.e(err);
478
519
  } finally {
479
- _iterator8.f();
520
+ _iterator9.f();
480
521
  }
481
522
  } else {
482
523
  md += "| ".concat(t('table.contributors'), " | ").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");
@@ -29,6 +29,7 @@
29
29
  "warning.noCommits": "⚠️ No commits found matching the criteria",
30
30
  "warning.noQualifyingCommits": "⚠️ No commits found containing both OpenSpec proposals and code changes",
31
31
  "warning.noBranches": "⚠️ No remote branches found",
32
+ "warning.noBranchesSelected": "⚠️ No branches selected. Please select at least one branch to analyze.",
32
33
 
33
34
  "error.prefix": "❌ Error:",
34
35
 
@@ -52,6 +53,8 @@
52
53
  "output.proposalSummary": "📋 Proposal Summary (by proposal)",
53
54
  "output.proposalTotal": " 📊 Total: {{count}} proposals | {{commits}} commits | {{files}} files | +{{additions}}/-{{deletions}} lines (net: {{netChanges}})\n",
54
55
  "output.proposalTotalLabel": "Proposal Summary Total",
56
+ "output.multiProposalWarning": "Note: Some proposals contain commits shared with other proposals. Code changes have been evenly distributed among proposals.",
57
+ "output.sharedWithOthers": "shared with other proposals",
55
58
  "output.authorSummary": "👥 Author Summary (by contributor)",
56
59
  "output.contributorHint": "💡 Use --show-contributors to see detailed statistics for each contributor",
57
60
 
@@ -29,6 +29,7 @@
29
29
  "warning.noCommits": "⚠️ 未找到符合条件的提交",
30
30
  "warning.noQualifyingCommits": "⚠️ 未找到同时包含 OpenSpec 提案和代码变更的提交",
31
31
  "warning.noBranches": "⚠️ 未找到远程分支",
32
+ "warning.noBranchesSelected": "⚠️ 未选择任何分支。请至少选择一个分支进行分析。",
32
33
 
33
34
  "error.prefix": "❌ 错误:",
34
35
 
@@ -52,6 +53,8 @@
52
53
  "output.proposalSummary": "📋 提案汇总(按提案统计)",
53
54
  "output.proposalTotal": " 📊 总计:{{count}} 个提案 | {{commits}} 次提交 | {{files}} 个文件 | +{{additions}}/-{{deletions}} 行(净变更:{{netChanges}})\n",
54
55
  "output.proposalTotalLabel": "提案汇总总计",
56
+ "output.multiProposalWarning": "注意:部分提案包含多提案共享的 commit,代码变更已按提案数量平均分配",
57
+ "output.sharedWithOthers": "与其他提案共享",
55
58
  "output.authorSummary": "👥 作者汇总(按贡献者统计)",
56
59
  "output.contributorHint": "💡 使用 --show-contributors 选项可查看每个贡献者的详细统计信息",
57
60
 
@@ -71,7 +71,9 @@ export var StatsAggregator = /*#__PURE__*/function () {
71
71
  additions: 0,
72
72
  deletions: 0,
73
73
  netChanges: 0,
74
- commitHashes: new Set()
74
+ commitHashes: new Set(),
75
+ multiProposalCommits: 0,
76
+ sharedCommitHashes: new Set()
75
77
  };
76
78
  proposalStatsMap.set(_proposal, proposalStats);
77
79
  }
@@ -81,10 +83,16 @@ export var StatsAggregator = /*#__PURE__*/function () {
81
83
  proposalStats.commitHashes.add(analysis.commit.hash);
82
84
  proposalStats.commits++;
83
85
  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;
86
+ var proposalCount = analysis.openspecProposals.size;
87
+ var isMultiProposal = proposalCount > 1;
88
+ if (isMultiProposal) {
89
+ proposalStats.multiProposalCommits++;
90
+ proposalStats.sharedCommitHashes.add(analysis.commit.hash);
91
+ }
92
+ proposalStats.codeFilesChanged += Math.floor(analysis.codeFiles.length / proposalCount);
93
+ proposalStats.additions += Math.floor(analysis.totalAdditions / proposalCount);
94
+ proposalStats.deletions += Math.floor(analysis.totalDeletions / proposalCount);
95
+ proposalStats.netChanges += Math.floor(analysis.netChanges / proposalCount);
88
96
  }
89
97
  }
90
98
  } catch (err) {
@@ -77,6 +77,8 @@ export interface ProposalStats {
77
77
  deletions: number;
78
78
  netChanges: number;
79
79
  commitHashes: Set<string>;
80
+ multiProposalCommits: number;
81
+ sharedCommitHashes: Set<string>;
80
82
  }
81
83
  export interface StatsResult {
82
84
  timeRange: {
package/package.json CHANGED
@@ -1,35 +1,13 @@
1
1
  {
2
2
  "name": "openspec-stat",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
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",
7
7
  "type": "module",
8
- "packageManager": "pnpm@10.12.1",
9
8
  "bin": {
10
9
  "openspec-stat": "dist/esm/cli.js"
11
10
  },
12
- "scripts": {
13
- "dev": "father dev",
14
- "build": "father build",
15
- "build:deps": "father prebundle",
16
- "prepublishOnly": "father doctor && npm run build",
17
- "test:cli": "node dist/esm/cli.js --help",
18
- "link:global": "npm link",
19
- "link:local": "pnpm run build && chmod +x dist/esm/cli.js && npm link .",
20
- "unlink": "npm unlink -g openspec-stat",
21
- "lint": "eslint src --ext .ts,.tsx",
22
- "lint:fix": "eslint src --ext .ts,.tsx --fix",
23
- "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,md}\"",
24
- "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,md}\"",
25
- "prepare": "husky",
26
- "changeset": "changeset",
27
- "changeset:add": "changeset add",
28
- "version": "changeset version && git add . && git commit -m 'chore: version packages'",
29
- "release": "pnpm run release:version && pnpm run release:tag",
30
- "release:version": "pnpm changeset version && git add . && git commit -m 'chore: version packages' && git push origin main",
31
- "release:tag": "git tag v$(node -p \"require('./package.json').version\") && git push origin v$(node -p \"require('./package.json').version\")"
32
- },
33
11
  "keywords": [
34
12
  "openspec",
35
13
  "git",
@@ -85,5 +63,20 @@
85
63
  "commander": "^14.0.2",
86
64
  "date-fns": "^4.1.0",
87
65
  "simple-git": "^3.30.0"
66
+ },
67
+ "scripts": {
68
+ "dev": "father dev",
69
+ "build": "father build",
70
+ "build:deps": "father prebundle",
71
+ "test:cli": "node dist/esm/cli.js --help",
72
+ "link:global": "npm link",
73
+ "link:local": "pnpm run build && chmod +x dist/esm/cli.js && npm link .",
74
+ "unlink": "npm unlink -g openspec-stat",
75
+ "lint": "eslint src --ext .ts,.tsx",
76
+ "lint:fix": "eslint src --ext .ts,.tsx --fix",
77
+ "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,md}\"",
78
+ "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,md}\"",
79
+ "changeset": "changeset",
80
+ "changeset:add": "changeset add"
88
81
  }
89
- }
82
+ }