ai-git-tools 2.1.4 → 2.1.5

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
@@ -366,6 +366,8 @@ Issue note 的基本責任分工如下:`description` 保存原始需求,`sta
366
366
 
367
367
  當 preview 選擇「已解決」後套用,工具會同步將完成百分比設為 `100%`,並依欄位名稱找到 `程式碼更版進度` custom field,勾選測試機選項 `測`。custom field ID 會從 Issue API 動態取得,不會寫死公司 Redmine 的 ID。
368
368
 
369
+ 同一個 Issue 若已經有先前的 notes,後續再次執行更新仍會新增一筆新的 Redmine journal,不會因為已有 notes 而略過;只有 Issue 的需求內容或狀態在產生草稿後被其他人修改時,才會停下來要求重新確認。
370
+
369
371
  ## ⚙️ 配置
370
372
 
371
373
  配置檔案範例(\`.ai-git-config.js\`):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-git-tools",
3
- "version": "2.1.4",
3
+ "version": "2.1.5",
4
4
  "description": "AI-powered Git automation tools for commit messages and PR generation",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -2,6 +2,7 @@ import { AIClient } from '../core/ai-client.js';
2
2
 
3
3
  const ANALYSIS_FIELDS = [
4
4
  'developmentSummary',
5
+ 'requirementDescription',
5
6
  'codeChanges',
6
7
  'apiChanges',
7
8
  'technicalDetails',
@@ -15,6 +16,12 @@ function asStringArray(value) {
15
16
  return Array.isArray(value) ? value.filter(item => typeof item === 'string') : [];
16
17
  }
17
18
 
19
+ function asBehaviorRules(value) {
20
+ return Array.isArray(value)
21
+ ? value.filter(rule => rule && typeof rule.scenario === 'string' && typeof rule.behavior === 'string')
22
+ : [];
23
+ }
24
+
18
25
  function filterEvidenceBackedClaims(result, evidence = {}) {
19
26
  const source = JSON.stringify(evidence);
20
27
  const evidenceRefs = result.evidence.filter(
@@ -59,6 +66,8 @@ ${JSON.stringify(evidence, null, 2)}
59
66
  {
60
67
  "issueId": ${issue.id},
61
68
  "developmentSummary": [],
69
+ "requirementDescription": "",
70
+ "behaviorRules": [],
62
71
  "codeChanges": [],
63
72
  "apiChanges": [],
64
73
  "technicalDetails": [],
@@ -69,7 +78,7 @@ ${JSON.stringify(evidence, null, 2)}
69
78
  "evidence": []
70
79
  }
71
80
 
72
- 各欄位都必須是字串陣列,flowchart 必須是合法 Mermaid source 字串或 null。`;
81
+ developmentSummary、codeChanges、apiChanges、technicalDetails、modifiedFiles、verification、unresolvedItems 與 evidence 必須是字串陣列;requirementDescription 必須是字串;behaviorRules 必須是包含 scenario 與 behavior 字串的物件陣列;flowchart 必須是合法 Mermaid source 字串或 null。`;
73
82
  }
74
83
 
75
84
  /**
@@ -83,8 +92,11 @@ export function parseIssueAnalysis(content, issueId) {
83
92
  const result = { issueId };
84
93
 
85
94
  for (const field of ANALYSIS_FIELDS) {
86
- result[field] = asStringArray(parsed[field]);
95
+ result[field] = field === 'requirementDescription'
96
+ ? (typeof parsed[field] === 'string' ? parsed[field].trim() : '')
97
+ : asStringArray(parsed[field]);
87
98
  }
99
+ result.behaviorRules = asBehaviorRules(parsed.behaviorRules);
88
100
 
89
101
  result.flowchart = typeof parsed.flowchart === 'string' && parsed.flowchart.trim()
90
102
  ? parsed.flowchart.trim()
@@ -88,6 +88,7 @@ export async function generateRedmineDraft({
88
88
  const syncId = createSyncId({ issueId, evidence, pullRequest });
89
89
  const note = formatRedmineNote({
90
90
  issueId,
91
+ issue,
91
92
  analysis,
92
93
  evidence,
93
94
  pullRequest,
@@ -125,11 +126,6 @@ export async function generateRedmineDraft({
125
126
  };
126
127
  }
127
128
 
128
- function hasSyncId(issue, syncId) {
129
- const journals = issue.raw?.journals || [];
130
- return journals.some(journal => journal.notes?.includes(syncId));
131
- }
132
-
133
129
  /**
134
130
  * 套用已審核的 Redmine 更新草稿
135
131
  * @param {{draft: object, client: object, force?: boolean}} options
@@ -144,10 +140,6 @@ export async function applyRedmineDraft({ draft, client, force = false }) {
144
140
  for (const item of draft.drafts) {
145
141
  try {
146
142
  const current = await client.getIssue(item.issueId, { include: 'allowed_statuses,journals' });
147
- if (!force && hasSyncId(current, item.syncId)) {
148
- results.push({ issueId: item.issueId, skipped: true, reason: 'duplicate' });
149
- continue;
150
- }
151
143
 
152
144
  const statusChanged = item.originalStatusId !== null && current.status?.id !== item.originalStatusId;
153
145
  const descriptionChanged = current.description !== item.originalDescription;
@@ -6,6 +6,20 @@ function bulletSection(title, items) {
6
6
  return `### ${title}\n${items.map(item => `- ${item}`).join('\n')}\n`;
7
7
  }
8
8
 
9
+ function paragraphSection(title, content) {
10
+ if (!content) return '';
11
+ return `### ${title}\n\n${content}\n`;
12
+ }
13
+
14
+ function formatBehaviorRules(rules = []) {
15
+ if (!Array.isArray(rules) || rules.length === 0) return '';
16
+ const rows = rules
17
+ .filter(rule => rule?.scenario && rule?.behavior)
18
+ .map(rule => `| ${String(rule.scenario).replaceAll('|', '\\|')} | ${String(rule.behavior).replaceAll('|', '\\|')} |`);
19
+ if (rows.length === 0) return '';
20
+ return `### 行為規則\n\n| 情境 | 行為 |\n|---|---|\n${rows.join('\n')}\n`;
21
+ }
22
+
9
23
  const MERMAID_START = '<!-- ai-git-tools:mermaid:start -->';
10
24
  const MERMAID_END = '<!-- ai-git-tools:mermaid:end -->';
11
25
 
@@ -70,9 +84,13 @@ export function formatRedmineDescription(description = '', source = null) {
70
84
  * @param {{issueId: number|string, analysis: object, evidence: object, pullRequest?: object|null, syncId?: string}} input
71
85
  * @returns {string}
72
86
  */
73
- export function formatRedmineNote({ issueId, analysis = {}, evidence = {}, pullRequest = null, syncId }) {
74
- const sections = [`## Git 開發更新(Issue #${issueId})`, ''];
75
- const development = bulletSection('開發內容', analysis.developmentSummary);
87
+ export function formatRedmineNote({ issueId, issue = {}, analysis = {}, evidence = {}, pullRequest = null, syncId }) {
88
+ const sections = ['## 開發內容', ''];
89
+ const development = Array.isArray(analysis.developmentSummary)
90
+ ? analysis.developmentSummary.join('\n\n')
91
+ : analysis.developmentSummary || '';
92
+ const requirementDescription = analysis.requirementDescription || issue.description || '';
93
+ const behaviorRules = formatBehaviorRules(analysis.behaviorRules);
76
94
  const codeChanges = bulletSection('程式修改重點', analysis.codeChanges);
77
95
  const apiChanges = bulletSection('API 變更', analysis.apiChanges);
78
96
  const technicalDetails = bulletSection('重要技術細節', analysis.technicalDetails);
@@ -83,15 +101,26 @@ export function formatRedmineNote({ issueId, analysis = {}, evidence = {}, pullR
83
101
  : evidence.changedFiles?.map(file => `\`\`${file}\`\``)
84
102
  );
85
103
  const verification = bulletSection('驗證結果', analysis.verification);
86
- const unresolved = bulletSection('尚未完成或待確認', analysis.unresolvedItems);
87
-
88
- sections.push(development, codeChanges, apiChanges, technicalDetails, modifiedFiles, verification);
104
+ const unresolved = analysis.unresolvedItems?.length
105
+ ? `### 待確認\n${analysis.unresolvedItems.map(item => `- [ ] ${item}`).join('\n')}\n`
106
+ : '';
107
+
108
+ sections.push(
109
+ development,
110
+ paragraphSection('需求說明', requirementDescription),
111
+ behaviorRules,
112
+ modifiedFiles,
113
+ codeChanges,
114
+ apiChanges,
115
+ technicalDetails,
116
+ verification
117
+ );
89
118
  sections.push(unresolved);
90
119
 
91
120
  if (evidence.currentBranch || evidence.baseBranch) {
92
121
  sections.push(
93
- `### Git 關聯\n\n- Branch:\`\`${evidence.currentBranch || '—'}\`\``,
94
- `- 比較基準:\`\`${evidence.baseBranch || '—'}\`\``,
122
+ `### Git\n\n- Branch:\`\`${evidence.currentBranch || '—'}\`\``,
123
+ `- Base:\`\`${evidence.baseBranch || '—'}\`\``,
95
124
  ''
96
125
  );
97
126
  }