ai-git-tools 2.1.14 → 2.1.15
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 +28 -10
- package/bin/cli.js +8 -2
- package/package.json +1 -1
- package/src/commands/init.js +1 -1
- package/src/commands/redmine-subtasks.js +27 -3
- package/src/core/ai-client.js +1 -1
- package/src/core/config-loader.js +2 -2
- package/src/pr-modules/ai/code-analyzer.js +1 -1
- package/src/redmine/issue-analyzer.js +1 -1
- package/src/redmine/subtask-analyzer.js +767 -133
- package/src/redmine/subtask-formatters.js +346 -3
- package/src/redmine/subtask-sync.js +167 -26
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
analyzeSubtasks as defaultAnalyzeSubtasks,
|
|
3
|
+
MIN_MAX_SUBTASKS,
|
|
4
|
+
MAX_MAX_SUBTASKS,
|
|
5
|
+
normalizeFrontendAnalysis,
|
|
6
|
+
validateFrontendAnalysis,
|
|
7
|
+
} from './subtask-analyzer.js';
|
|
2
8
|
import {
|
|
3
9
|
formatSubtaskContent,
|
|
4
10
|
getManagedSubtaskKey,
|
|
@@ -17,10 +23,33 @@ function sameValue(left, right) {
|
|
|
17
23
|
return (left ?? null) === (right ?? null);
|
|
18
24
|
}
|
|
19
25
|
|
|
26
|
+
function normalizeRelations(relations = []) {
|
|
27
|
+
return (Array.isArray(relations) ? relations : [])
|
|
28
|
+
.map(relation => ({
|
|
29
|
+
id: relation?.id ?? null,
|
|
30
|
+
issueId: relation?.issue_id ?? relation?.issueId ?? null,
|
|
31
|
+
issueToId: relation?.issue_to_id ?? relation?.issueToId ?? null,
|
|
32
|
+
relationType: relation?.relation_type ?? relation?.relationType ?? null,
|
|
33
|
+
}))
|
|
34
|
+
.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
35
|
+
}
|
|
36
|
+
|
|
20
37
|
function getParentSnapshot(parent) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
38
|
+
return {
|
|
39
|
+
id: parent.id,
|
|
40
|
+
subject: parent.subject || '',
|
|
41
|
+
description: parent.description || '',
|
|
42
|
+
projectId: getProjectId(parent),
|
|
43
|
+
trackerId: getTrackerId(parent),
|
|
44
|
+
children: Array.isArray(parent.children)
|
|
45
|
+
? parent.children.map(child => ({
|
|
46
|
+
id: child.id,
|
|
47
|
+
subject: child.subject || '',
|
|
48
|
+
description: child.description || '',
|
|
49
|
+
}))
|
|
50
|
+
: [],
|
|
51
|
+
relations: normalizeRelations(parent.relations),
|
|
52
|
+
};
|
|
24
53
|
}
|
|
25
54
|
|
|
26
55
|
function assertParentMetadata(parent) {
|
|
@@ -34,11 +63,26 @@ function sanitizeError(error, client) {
|
|
|
34
63
|
return secret ? String(error?.message || error).replaceAll(secret, '[REDACTED]') : String(error?.message || error);
|
|
35
64
|
}
|
|
36
65
|
|
|
66
|
+
function assertDraftDoesNotContainPrivateFields(value, path = 'draft') {
|
|
67
|
+
if (!value || typeof value !== 'object') return;
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
value.forEach((item, index) => assertDraftDoesNotContainPrivateFields(item, `${path}[${index}]`));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
for (const [key, item] of Object.entries(value)) {
|
|
73
|
+
if (/raw|secret|token|password|private|credential|api[-_]?key/i.test(key)) {
|
|
74
|
+
throw new Error(`Frontend 子任務 draft 含有禁止保存的欄位:${path}.${key}`);
|
|
75
|
+
}
|
|
76
|
+
assertDraftDoesNotContainPrivateFields(item, `${path}.${key}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
37
80
|
function hasParentConflict(expected, current) {
|
|
38
81
|
return expected.subject !== current.subject
|
|
39
82
|
|| expected.description !== current.description
|
|
40
83
|
|| !sameValue(getProjectId(expected), getProjectId(current))
|
|
41
|
-
|| !sameValue(getTrackerId(expected), getTrackerId(current))
|
|
84
|
+
|| !sameValue(getTrackerId(expected), getTrackerId(current))
|
|
85
|
+
|| JSON.stringify(normalizeRelations(expected.relations)) !== JSON.stringify(normalizeRelations(current.relations));
|
|
42
86
|
}
|
|
43
87
|
|
|
44
88
|
function getExistingChildTitles(children = []) {
|
|
@@ -50,16 +94,30 @@ function getExistingManagedKeys(children = []) {
|
|
|
50
94
|
}
|
|
51
95
|
|
|
52
96
|
async function loadChildDetails(client, children = []) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
97
|
+
const unavailableChildIds = [];
|
|
98
|
+
if (typeof client?.getIssue !== 'function') {
|
|
99
|
+
return {
|
|
100
|
+
children,
|
|
101
|
+
unavailableChildIds: children
|
|
102
|
+
.filter(child => !child.description)
|
|
103
|
+
.map(child => child.id ?? 'unknown'),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const detailedChildren = await Promise.all(children.map(async child => {
|
|
107
|
+
if (child.description) return child;
|
|
108
|
+
if (child.id === undefined || child.id === null) {
|
|
109
|
+
unavailableChildIds.push('unknown');
|
|
110
|
+
return child;
|
|
111
|
+
}
|
|
56
112
|
try {
|
|
57
|
-
const detail = await client.getIssue(child.id
|
|
113
|
+
const detail = await client.getIssue(child.id);
|
|
58
114
|
return { ...child, description: detail.description || '' };
|
|
59
115
|
} catch {
|
|
116
|
+
unavailableChildIds.push(child.id);
|
|
60
117
|
return child;
|
|
61
118
|
}
|
|
62
119
|
}));
|
|
120
|
+
return { children: detailedChildren, unavailableChildIds };
|
|
63
121
|
}
|
|
64
122
|
|
|
65
123
|
/**
|
|
@@ -72,6 +130,7 @@ export async function generateSubtaskDraft({
|
|
|
72
130
|
client,
|
|
73
131
|
analyzeSubtasksFn = defaultAnalyzeSubtasks,
|
|
74
132
|
maxSubtasks = 8,
|
|
133
|
+
repositoryContext = {},
|
|
75
134
|
model,
|
|
76
135
|
maxRetries,
|
|
77
136
|
onProgress = () => {},
|
|
@@ -80,28 +139,66 @@ export async function generateSubtaskDraft({
|
|
|
80
139
|
const parent = await client.getIssue(parentIssueId, { include: 'children,relations' });
|
|
81
140
|
assertParentMetadata(parent);
|
|
82
141
|
onProgress({ phase: 'parent-read', parentIssueId, subject: parent.subject });
|
|
142
|
+
onProgress({ phase: 'scope-analysis', parentIssueId });
|
|
83
143
|
onProgress({ phase: 'analyze-subtasks', parentIssueId });
|
|
84
144
|
const analysis = await analyzeSubtasksFn({
|
|
85
145
|
issue: parent,
|
|
86
146
|
maxSubtasks,
|
|
147
|
+
repositoryContext,
|
|
87
148
|
model,
|
|
88
149
|
maxRetries,
|
|
89
150
|
});
|
|
151
|
+
if (analysis.schemaVersion !== 2) {
|
|
152
|
+
throw new Error('Frontend 子任務分析必須回傳 schemaVersion 2,請重新產生 preview');
|
|
153
|
+
}
|
|
154
|
+
validateFrontendAnalysis(analysis, parentIssueId, maxSubtasks);
|
|
90
155
|
onProgress({ phase: 'subtasks-analyzed', parentIssueId, count: analysis.subtasks.length });
|
|
91
156
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
157
|
+
if (analysis.scopeDecision?.status === 'none') {
|
|
158
|
+
throw new Error(`Issue #${parentIssueId} 沒有可建立的 Frontend 子任務`);
|
|
159
|
+
}
|
|
160
|
+
if (analysis.scopeDecision?.status === 'unclear') {
|
|
161
|
+
throw new Error(`Issue #${parentIssueId} 的 Frontend scope 不明,請先確認 unresolved items`);
|
|
162
|
+
}
|
|
163
|
+
if (!analysis.subtasks.length) {
|
|
164
|
+
throw new Error(`Issue #${parentIssueId} 沒有產生有效的 Frontend 子任務`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const subtasks = analysis.subtasks.map((subtask, index) => {
|
|
168
|
+
onProgress({
|
|
169
|
+
phase: 'screen-enrichment',
|
|
170
|
+
index: index + 1,
|
|
171
|
+
total: analysis.subtasks.length,
|
|
172
|
+
parentIssueId,
|
|
173
|
+
title: subtask.title,
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
...subtask,
|
|
177
|
+
title: subtask.title,
|
|
178
|
+
content: formatSubtaskContent({
|
|
179
|
+
subtask,
|
|
180
|
+
parentIssueId: parent.id,
|
|
181
|
+
scopeDecision: analysis.scopeDecision,
|
|
182
|
+
evidence: analysis.evidence,
|
|
183
|
+
}),
|
|
184
|
+
};
|
|
185
|
+
});
|
|
97
186
|
|
|
98
187
|
return {
|
|
99
|
-
version:
|
|
188
|
+
version: 2,
|
|
189
|
+
schemaVersion: 2,
|
|
100
190
|
generatedAt: new Date().toISOString(),
|
|
191
|
+
analysisScope: analysis.analysisScope,
|
|
192
|
+
maxSubtasks,
|
|
193
|
+
isIndivisible: Boolean(analysis.isIndivisible),
|
|
194
|
+
indivisibleReason: analysis.indivisibleReason || '',
|
|
195
|
+
scopeDecision: analysis.scopeDecision,
|
|
196
|
+
evidence: analysis.evidence || [],
|
|
197
|
+
unresolvedItems: analysis.unresolvedItems || [],
|
|
101
198
|
parent: getParentSnapshot(parent),
|
|
102
199
|
subtasks,
|
|
103
|
-
unresolvedItems: analysis.unresolvedItems || [],
|
|
104
200
|
};
|
|
201
|
+
|
|
105
202
|
}
|
|
106
203
|
|
|
107
204
|
/**
|
|
@@ -118,25 +215,40 @@ export async function applySubtaskDraft({ draft, client, force = false, onProgre
|
|
|
118
215
|
}
|
|
119
216
|
assertParentMetadata(draft.parent);
|
|
120
217
|
|
|
121
|
-
const
|
|
218
|
+
const childDetails = await loadChildDetails(
|
|
122
219
|
client,
|
|
123
220
|
Array.isArray(current.children) ? current.children : []
|
|
124
221
|
);
|
|
222
|
+
if (childDetails.unavailableChildIds.length > 0) {
|
|
223
|
+
return [{
|
|
224
|
+
parentIssueId,
|
|
225
|
+
blocked: true,
|
|
226
|
+
reason: 'child-detail-unavailable',
|
|
227
|
+
childIds: childDetails.unavailableChildIds,
|
|
228
|
+
}];
|
|
229
|
+
}
|
|
230
|
+
const children = childDetails.children;
|
|
125
231
|
const managedKeys = getExistingManagedKeys(children);
|
|
126
232
|
const titles = getExistingChildTitles(children);
|
|
127
233
|
const results = [];
|
|
128
234
|
for (const [index, subtask] of draft.subtasks.entries()) {
|
|
235
|
+
const subtaskId = subtask.id || subtask.key;
|
|
129
236
|
onProgress({
|
|
130
237
|
phase: 'create-child',
|
|
131
238
|
index: index + 1,
|
|
132
239
|
total: draft.subtasks.length,
|
|
133
240
|
parentIssueId,
|
|
134
|
-
key:
|
|
241
|
+
key: subtaskId,
|
|
135
242
|
title: subtask.title,
|
|
136
243
|
});
|
|
137
|
-
const managedKey = buildManagedSubtaskKey(parentIssueId,
|
|
244
|
+
const managedKey = buildManagedSubtaskKey(parentIssueId, subtaskId);
|
|
138
245
|
if (managedKeys.has(managedKey) || titles.has(String(subtask.title).trim().toLocaleLowerCase())) {
|
|
139
|
-
results.push({
|
|
246
|
+
results.push({
|
|
247
|
+
...(draft.version === 2 ? { id: subtaskId } : { key: subtaskId }),
|
|
248
|
+
title: subtask.title,
|
|
249
|
+
skipped: true,
|
|
250
|
+
reason: 'duplicate',
|
|
251
|
+
});
|
|
140
252
|
continue;
|
|
141
253
|
}
|
|
142
254
|
|
|
@@ -149,7 +261,7 @@ export async function applySubtaskDraft({ draft, client, force = false, onProgre
|
|
|
149
261
|
parentIssueId,
|
|
150
262
|
});
|
|
151
263
|
results.push({
|
|
152
|
-
key:
|
|
264
|
+
...(draft.version === 2 ? { id: subtaskId } : { key: subtaskId }),
|
|
153
265
|
title: subtask.title,
|
|
154
266
|
created: true,
|
|
155
267
|
childId: child?.id ?? child?.issue?.id ?? null,
|
|
@@ -158,7 +270,7 @@ export async function applySubtaskDraft({ draft, client, force = false, onProgre
|
|
|
158
270
|
titles.add(String(subtask.title).trim().toLocaleLowerCase());
|
|
159
271
|
} catch (error) {
|
|
160
272
|
results.push({
|
|
161
|
-
key:
|
|
273
|
+
...(draft.version === 2 ? { id: subtaskId } : { key: subtaskId }),
|
|
162
274
|
title: subtask.title,
|
|
163
275
|
created: false,
|
|
164
276
|
error: sanitizeError(error, client),
|
|
@@ -173,17 +285,46 @@ export async function applySubtaskDraft({ draft, client, force = false, onProgre
|
|
|
173
285
|
* @param {object} draft
|
|
174
286
|
*/
|
|
175
287
|
export function validateSubtaskDraft(draft = {}) {
|
|
176
|
-
|
|
177
|
-
|
|
288
|
+
assertDraftDoesNotContainPrivateFields(draft);
|
|
289
|
+
if (draft.version !== 2 || draft.schemaVersion !== 2) {
|
|
290
|
+
throw new Error('Frontend 子任務 draft v1 不受支援,請重新產生 schemaVersion 2 preview');
|
|
291
|
+
}
|
|
292
|
+
if (draft.analysisScope !== 'frontend') {
|
|
293
|
+
throw new Error('Frontend 子任務 draft 的 analysisScope 必須是 frontend');
|
|
294
|
+
}
|
|
295
|
+
const maxSubtasks = draft.maxSubtasks === undefined ? MAX_MAX_SUBTASKS : Number(draft.maxSubtasks);
|
|
296
|
+
if (!Number.isInteger(maxSubtasks) || maxSubtasks < MIN_MAX_SUBTASKS || maxSubtasks > MAX_MAX_SUBTASKS) {
|
|
297
|
+
throw new Error(`Frontend 子任務 draft 的 maxSubtasks 必須介於 ${MIN_MAX_SUBTASKS} 到 ${MAX_MAX_SUBTASKS} 之間`);
|
|
298
|
+
}
|
|
299
|
+
if (!draft.scopeDecision || !['full', 'partial'].includes(draft.scopeDecision.status)) {
|
|
300
|
+
throw new Error('Frontend 子任務 draft 缺少可套用的 scopeDecision');
|
|
301
|
+
}
|
|
302
|
+
if (!draft.parent || !Array.isArray(draft.subtasks) || draft.subtasks.length === 0) {
|
|
303
|
+
throw new Error('Frontend 子任務 draft 格式不受支援');
|
|
178
304
|
}
|
|
179
305
|
if (draft.parent.id === undefined || draft.parent.id === null) {
|
|
180
306
|
throw new Error('子任務 draft 缺少 parent Issue ID');
|
|
181
307
|
}
|
|
308
|
+
const ids = new Set();
|
|
182
309
|
for (const subtask of draft.subtasks) {
|
|
183
|
-
if (!subtask?.
|
|
184
|
-
throw new Error('子任務 draft 缺少
|
|
310
|
+
if (!subtask?.id || !subtask.title?.trim() || !subtask.content?.trim()) {
|
|
311
|
+
throw new Error('Frontend 子任務 draft 缺少 id、title 或 content');
|
|
185
312
|
}
|
|
313
|
+
if (ids.has(subtask.id)) throw new Error(`Frontend 子任務 draft ID 重複:${subtask.id}`);
|
|
314
|
+
ids.add(subtask.id);
|
|
186
315
|
}
|
|
316
|
+
const normalizedAnalysis = normalizeFrontendAnalysis({
|
|
317
|
+
schemaVersion: draft.schemaVersion,
|
|
318
|
+
issueId: draft.parent.id,
|
|
319
|
+
analysisScope: draft.analysisScope,
|
|
320
|
+
isIndivisible: Boolean(draft.isIndivisible),
|
|
321
|
+
indivisibleReason: draft.indivisibleReason || '',
|
|
322
|
+
scopeDecision: draft.scopeDecision,
|
|
323
|
+
evidence: Array.isArray(draft.evidence) ? draft.evidence : [],
|
|
324
|
+
unresolvedItems: Array.isArray(draft.unresolvedItems) ? draft.unresolvedItems : [],
|
|
325
|
+
subtasks: draft.subtasks,
|
|
326
|
+
});
|
|
327
|
+
validateFrontendAnalysis(normalizedAnalysis, draft.parent.id, maxSubtasks);
|
|
187
328
|
}
|
|
188
329
|
|
|
189
330
|
/**
|