ai-git-tools 2.1.5 → 2.1.6
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 +1 -1
- package/package.json +1 -1
- package/src/redmine/issue-analyzer.js +38 -6
- package/src/redmine/issue-sync.js +1 -1
- package/src/redmine/redmine-formatters.js +66 -14
package/README.md
CHANGED
|
@@ -352,7 +352,7 @@ ai-git-tools redmine-update --apply --from redmine-update.json
|
|
|
352
352
|
|
|
353
353
|
Preview 會以色彩分開顯示每個 Issue 的開發內容、程式修改重點、API 變更、重要技術細節、修改檔案與實際 note。狀態由 Redmine API 提供選單,只有使用者選擇的 status 才會寫入。
|
|
354
354
|
|
|
355
|
-
原始 Issue description
|
|
355
|
+
原始 Issue description 會保留原始內容;AI 可產生多張流程圖,每張流程圖都有穩定的用途 ID。相同 ID 會更新原流程圖,不同 ID 會追加新流程圖;既有沒有 ID 的 Mermaid 會保留,內容相同時不重複追加。更新摘要會寫入 notes,並使用 Markdown 標題、項目符號與雙反引號標示程式路徑、API path、function 與 class。只有找到明確測試或驗證證據時才會加入驗證段落;流程圖會使用 Redmine Mermaid macro 並放在 description(公司 Redmine 的 notes 不支援 Mermaid):
|
|
356
356
|
|
|
357
357
|
```text
|
|
358
358
|
{{mermaid
|
package/package.json
CHANGED
|
@@ -12,6 +12,38 @@ const ANALYSIS_FIELDS = [
|
|
|
12
12
|
'evidence',
|
|
13
13
|
];
|
|
14
14
|
|
|
15
|
+
function normalizeFlowcharts(value) {
|
|
16
|
+
const flowcharts = Array.isArray(value)
|
|
17
|
+
? value
|
|
18
|
+
: value
|
|
19
|
+
? [{ source: value }]
|
|
20
|
+
: [];
|
|
21
|
+
|
|
22
|
+
const usedIds = new Set();
|
|
23
|
+
return flowcharts
|
|
24
|
+
.filter(flowchart => flowchart && typeof flowchart === 'object')
|
|
25
|
+
.map((flowchart, index) => {
|
|
26
|
+
const title = typeof flowchart.title === 'string' ? flowchart.title.trim() : '';
|
|
27
|
+
const requestedId = typeof flowchart.id === 'string' ? flowchart.id.trim() : '';
|
|
28
|
+
const stableId = (requestedId || title || `flowchart-${index + 1}`)
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
31
|
+
.replace(/^-+|-+$/g, '') || `flowchart-${index + 1}`;
|
|
32
|
+
|
|
33
|
+
let uniqueId = stableId;
|
|
34
|
+
if (usedIds.has(uniqueId)) uniqueId = `${stableId}-${index + 1}`;
|
|
35
|
+
usedIds.add(uniqueId);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
id: uniqueId,
|
|
39
|
+
title,
|
|
40
|
+
source: typeof flowchart.source === 'string' ? flowchart.source.trim() : '',
|
|
41
|
+
evidence: asStringArray(flowchart.evidence),
|
|
42
|
+
};
|
|
43
|
+
})
|
|
44
|
+
.filter(flowchart => flowchart.source);
|
|
45
|
+
}
|
|
46
|
+
|
|
15
47
|
function asStringArray(value) {
|
|
16
48
|
return Array.isArray(value) ? value.filter(item => typeof item === 'string') : [];
|
|
17
49
|
}
|
|
@@ -52,7 +84,8 @@ export function buildIssueAnalysisPrompt({ issue, evidence }) {
|
|
|
52
84
|
- 不得捏造 API endpoint、參數、檔案、function、class、測試或部署結果。
|
|
53
85
|
- 沒有明確測試或驗證證據時,verification 必須是空陣列。
|
|
54
86
|
- 沒有可靠 API 變更證據時,apiChanges 必須是空陣列。
|
|
55
|
-
-
|
|
87
|
+
- requirementDescription 應整理指定 Issue 的需求重點;behaviorRules 使用 scenario/behavior 物件陣列。
|
|
88
|
+
- 若能從實際流程推導出可靠的複製流程,才產生 flowcharts;否則為空陣列。每張流程圖必須有穩定、描述用途的 id,source 必須以 graph TD 開始,不要加入 Mermaid wrapper 或 code fence。
|
|
56
89
|
- developmentSummary、codeChanges、apiChanges、technicalDetails 中的程式路徑、API path、function 與 class 使用雙反引號標示;需要多行程式片段時使用 code block。
|
|
57
90
|
- 不要產生 suggestedStatus、coverage 或 confidence,也不要決定 Redmine status。
|
|
58
91
|
|
|
@@ -73,12 +106,12 @@ ${JSON.stringify(evidence, null, 2)}
|
|
|
73
106
|
"technicalDetails": [],
|
|
74
107
|
"modifiedFiles": [],
|
|
75
108
|
"verification": [],
|
|
76
|
-
"
|
|
109
|
+
"flowcharts": [],
|
|
77
110
|
"unresolvedItems": [],
|
|
78
111
|
"evidence": []
|
|
79
112
|
}
|
|
80
113
|
|
|
81
|
-
developmentSummary、codeChanges、apiChanges、technicalDetails、modifiedFiles、verification、unresolvedItems 與 evidence 必須是字串陣列;requirementDescription 必須是字串;behaviorRules 必須是包含 scenario 與 behavior 字串的物件陣列;
|
|
114
|
+
developmentSummary、codeChanges、apiChanges、technicalDetails、modifiedFiles、verification、unresolvedItems 與 evidence 必須是字串陣列;requirementDescription 必須是字串;behaviorRules 必須是包含 scenario 與 behavior 字串的物件陣列;flowcharts 必須是包含 id、title、source 與 evidence 的物件陣列。`;
|
|
82
115
|
}
|
|
83
116
|
|
|
84
117
|
/**
|
|
@@ -97,10 +130,9 @@ export function parseIssueAnalysis(content, issueId) {
|
|
|
97
130
|
: asStringArray(parsed[field]);
|
|
98
131
|
}
|
|
99
132
|
result.behaviorRules = asBehaviorRules(parsed.behaviorRules);
|
|
133
|
+
result.flowcharts = normalizeFlowcharts(parsed.flowcharts || parsed.flowchart);
|
|
100
134
|
|
|
101
|
-
result.flowchart =
|
|
102
|
-
? parsed.flowchart.trim()
|
|
103
|
-
: null;
|
|
135
|
+
result.flowchart = result.flowcharts[0]?.source || null;
|
|
104
136
|
|
|
105
137
|
return result;
|
|
106
138
|
}
|
|
@@ -84,7 +84,7 @@ export async function generateRedmineDraft({
|
|
|
84
84
|
const analysis = await analyzeIssueFn({ issue, evidence: { ...evidence, pullRequest }, model, maxRetries });
|
|
85
85
|
const selectedStatus = await selectStatusFn(issue, { client });
|
|
86
86
|
const completion = buildCompletionUpdate(issue, selectedStatus);
|
|
87
|
-
const description = formatRedmineDescription(issue.description, analysis.
|
|
87
|
+
const description = formatRedmineDescription(issue.description, analysis.flowcharts);
|
|
88
88
|
const syncId = createSyncId({ issueId, evidence, pullRequest });
|
|
89
89
|
const note = formatRedmineNote({
|
|
90
90
|
issueId,
|
|
@@ -20,8 +20,52 @@ function formatBehaviorRules(rules = []) {
|
|
|
20
20
|
return `### 行為規則\n\n| 情境 | 行為 |\n|---|---|\n${rows.join('\n')}\n`;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const MERMAID_START = '<!-- ai-git-tools:mermaid:start
|
|
24
|
-
const MERMAID_END = '<!-- ai-git-tools:mermaid:end
|
|
23
|
+
const MERMAID_START = '<!-- ai-git-tools:mermaid:start';
|
|
24
|
+
const MERMAID_END = '<!-- ai-git-tools:mermaid:end';
|
|
25
|
+
|
|
26
|
+
function escapeRegExp(value) {
|
|
27
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeFlowchartId(id, index) {
|
|
31
|
+
const source = typeof id === 'string' && id.trim() ? id : `flowchart-${index + 1}`;
|
|
32
|
+
return source
|
|
33
|
+
.trim()
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
36
|
+
.replace(/^-+|-+$/g, '') || `flowchart-${index + 1}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeFlowchartInput(flowcharts) {
|
|
40
|
+
if (Array.isArray(flowcharts)) return flowcharts;
|
|
41
|
+
if (typeof flowcharts === 'string' && flowcharts.trim()) {
|
|
42
|
+
return [{ id: 'flowchart-1', title: '開發流程', source: flowcharts }];
|
|
43
|
+
}
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasSameMermaid(description, mermaid) {
|
|
48
|
+
const existing = description.match(/\{\{\s*mermaid\b[\s\S]*?\}\}/gi) || [];
|
|
49
|
+
return existing.some(block => formatMermaid(block) === mermaid);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function managedFlowchartPattern(id) {
|
|
53
|
+
const escapedId = escapeRegExp(id);
|
|
54
|
+
return new RegExp(
|
|
55
|
+
`${escapeRegExp(MERMAID_START)} id="${escapedId}" -->[\\s\\S]*?${escapeRegExp(MERMAID_END)} id="${escapedId}" -->`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatManagedFlowchart(flowchart, index) {
|
|
60
|
+
const mermaid = formatMermaid(flowchart.source);
|
|
61
|
+
if (!mermaid) return '';
|
|
62
|
+
|
|
63
|
+
const id = normalizeFlowchartId(flowchart.id, index);
|
|
64
|
+
const title = typeof flowchart.title === 'string' && flowchart.title.trim()
|
|
65
|
+
? flowchart.title.trim()
|
|
66
|
+
: '開發流程';
|
|
67
|
+
return `${MERMAID_START} id="${id}" -->\n\n### 流程圖:${title}\n\n${mermaid}\n\n${MERMAID_END} id="${id}" -->`;
|
|
68
|
+
}
|
|
25
69
|
|
|
26
70
|
/**
|
|
27
71
|
* 將 Mermaid source 包成 Redmine macro
|
|
@@ -37,9 +81,9 @@ export function formatMermaid(source) {
|
|
|
37
81
|
.replace(/\s*```$/, '')
|
|
38
82
|
.trim();
|
|
39
83
|
|
|
40
|
-
if (
|
|
84
|
+
if (/^\{\{\s*mermaid\b/i.test(cleanSource)) {
|
|
41
85
|
cleanSource = cleanSource
|
|
42
|
-
.replace(/^\{\{mermaid\s
|
|
86
|
+
.replace(/^\{\{\s*mermaid\s*/i, '')
|
|
43
87
|
.replace(/\s*\}\}$/, '')
|
|
44
88
|
.trim();
|
|
45
89
|
}
|
|
@@ -64,19 +108,27 @@ export function formatMermaid(source) {
|
|
|
64
108
|
* @returns {string}
|
|
65
109
|
*/
|
|
66
110
|
export function formatRedmineDescription(description = '', source = null) {
|
|
67
|
-
|
|
68
|
-
|
|
111
|
+
let result = description;
|
|
112
|
+
const flowcharts = normalizeFlowchartInput(source);
|
|
69
113
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
);
|
|
114
|
+
flowcharts.forEach((flowchart, index) => {
|
|
115
|
+
const mermaid = formatMermaid(flowchart?.source);
|
|
116
|
+
if (!mermaid) return;
|
|
74
117
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
118
|
+
const block = formatManagedFlowchart(flowchart, index);
|
|
119
|
+
const id = normalizeFlowchartId(flowchart?.id, index);
|
|
120
|
+
const markerPattern = managedFlowchartPattern(id);
|
|
121
|
+
if (markerPattern.test(result)) {
|
|
122
|
+
result = result.replace(markerPattern, block);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!hasSameMermaid(result, mermaid)) {
|
|
127
|
+
result = `${result.trim()}\n\n${block}`.trim();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
78
130
|
|
|
79
|
-
return
|
|
131
|
+
return result;
|
|
80
132
|
}
|
|
81
133
|
|
|
82
134
|
/**
|