@muzzamil-khan/redmine-agent-mcp 1.0.0 → 1.1.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/dist/config/env.d.ts +2 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +8 -0
- package/dist/config/env.js.map +1 -1
- package/dist/mcp/server.js +630 -147
- package/dist/mcp/server.js.map +1 -1
- package/dist/services/assigneeDirectory.d.ts +34 -0
- package/dist/services/assigneeDirectory.d.ts.map +1 -0
- package/dist/services/assigneeDirectory.js +138 -0
- package/dist/services/assigneeDirectory.js.map +1 -0
- package/dist/services/googleSheets.d.ts +32 -0
- package/dist/services/googleSheets.d.ts.map +1 -0
- package/dist/services/googleSheets.js +109 -0
- package/dist/services/googleSheets.js.map +1 -0
- package/dist/services/redmine.d.ts +52 -0
- package/dist/services/redmine.d.ts.map +1 -1
- package/dist/services/redmine.js +103 -0
- package/dist/services/redmine.js.map +1 -1
- package/dist/services/redmineConfig.d.ts +85 -0
- package/dist/services/redmineConfig.d.ts.map +1 -0
- package/dist/services/redmineConfig.js +271 -0
- package/dist/services/redmineConfig.js.map +1 -0
- package/dist/spreadsheet/batchColumnMapping.d.ts +93 -0
- package/dist/spreadsheet/batchColumnMapping.d.ts.map +1 -0
- package/dist/spreadsheet/batchColumnMapping.js +660 -0
- package/dist/spreadsheet/batchColumnMapping.js.map +1 -0
- package/dist/spreadsheet/batchCreationTypes.d.ts +58 -0
- package/dist/spreadsheet/batchCreationTypes.d.ts.map +1 -0
- package/dist/spreadsheet/batchCreationTypes.js +18 -0
- package/dist/spreadsheet/batchCreationTypes.js.map +1 -0
- package/dist/spreadsheet/batchIssueConfig.d.ts +21 -0
- package/dist/spreadsheet/batchIssueConfig.d.ts.map +1 -0
- package/dist/spreadsheet/batchIssueConfig.js +73 -0
- package/dist/spreadsheet/batchIssueConfig.js.map +1 -0
- package/dist/spreadsheet/batchPreviewFormat.d.ts +39 -0
- package/dist/spreadsheet/batchPreviewFormat.d.ts.map +1 -0
- package/dist/spreadsheet/batchPreviewFormat.js +190 -0
- package/dist/spreadsheet/batchPreviewFormat.js.map +1 -0
- package/dist/spreadsheet/batchSheetHeuristics.d.ts +13 -0
- package/dist/spreadsheet/batchSheetHeuristics.d.ts.map +1 -0
- package/dist/spreadsheet/batchSheetHeuristics.js +30 -0
- package/dist/spreadsheet/batchSheetHeuristics.js.map +1 -0
- package/dist/spreadsheet/csvUtils.d.ts +14 -0
- package/dist/spreadsheet/csvUtils.d.ts.map +1 -0
- package/dist/spreadsheet/csvUtils.js +113 -0
- package/dist/spreadsheet/csvUtils.js.map +1 -0
- package/dist/spreadsheet/extractRawBatchSheet.d.ts +14 -0
- package/dist/spreadsheet/extractRawBatchSheet.d.ts.map +1 -0
- package/dist/spreadsheet/extractRawBatchSheet.js +33 -0
- package/dist/spreadsheet/extractRawBatchSheet.js.map +1 -0
- package/dist/spreadsheet/featureRow.d.ts +15 -0
- package/dist/spreadsheet/featureRow.d.ts.map +1 -0
- package/dist/spreadsheet/featureRow.js +119 -0
- package/dist/spreadsheet/featureRow.js.map +1 -0
- package/dist/spreadsheet/parseBatchCreationCsv.d.ts +8 -0
- package/dist/spreadsheet/parseBatchCreationCsv.d.ts.map +1 -0
- package/dist/spreadsheet/parseBatchCreationCsv.js +22 -0
- package/dist/spreadsheet/parseBatchCreationCsv.js.map +1 -0
- package/dist/spreadsheet/parseBatchCreationCsvV1.d.ts +3 -0
- package/dist/spreadsheet/parseBatchCreationCsvV1.d.ts.map +1 -0
- package/dist/spreadsheet/parseBatchCreationCsvV1.js +226 -0
- package/dist/spreadsheet/parseBatchCreationCsvV1.js.map +1 -0
- package/dist/spreadsheet/parseBatchCreationCsvV2.d.ts +4 -0
- package/dist/spreadsheet/parseBatchCreationCsvV2.d.ts.map +1 -0
- package/dist/spreadsheet/parseBatchCreationCsvV2.js +180 -0
- package/dist/spreadsheet/parseBatchCreationCsvV2.js.map +1 -0
- package/dist/spreadsheet/redmineIssueCell.d.ts +38 -0
- package/dist/spreadsheet/redmineIssueCell.d.ts.map +1 -0
- package/dist/spreadsheet/redmineIssueCell.js +176 -0
- package/dist/spreadsheet/redmineIssueCell.js.map +1 -0
- package/dist/spreadsheet/sheetColumnMap.d.ts +38 -0
- package/dist/spreadsheet/sheetColumnMap.d.ts.map +1 -0
- package/dist/spreadsheet/sheetColumnMap.js +75 -0
- package/dist/spreadsheet/sheetColumnMap.js.map +1 -0
- package/dist/spreadsheet/sheetGridToCsv.d.ts +28 -0
- package/dist/spreadsheet/sheetGridToCsv.d.ts.map +1 -0
- package/dist/spreadsheet/sheetGridToCsv.js +88 -0
- package/dist/spreadsheet/sheetGridToCsv.js.map +1 -0
- package/dist/spreadsheet/sheetRedmineFields.d.ts +7 -0
- package/dist/spreadsheet/sheetRedmineFields.d.ts.map +1 -0
- package/dist/spreadsheet/sheetRedmineFields.js +22 -0
- package/dist/spreadsheet/sheetRedmineFields.js.map +1 -0
- package/dist/spreadsheet/sheetWriteBack.d.ts +27 -0
- package/dist/spreadsheet/sheetWriteBack.d.ts.map +1 -0
- package/dist/spreadsheet/sheetWriteBack.js +90 -0
- package/dist/spreadsheet/sheetWriteBack.js.map +1 -0
- package/dist/workflows/batchCreateIssuesWorkflow.d.ts +66 -0
- package/dist/workflows/batchCreateIssuesWorkflow.d.ts.map +1 -0
- package/dist/workflows/batchCreateIssuesWorkflow.js +1014 -0
- package/dist/workflows/batchCreateIssuesWorkflow.js.map +1 -0
- package/dist/workflows/createIssueWorkflow.d.ts +41 -0
- package/dist/workflows/createIssueWorkflow.d.ts.map +1 -0
- package/dist/workflows/createIssueWorkflow.js +697 -0
- package/dist/workflows/createIssueWorkflow.js.map +1 -0
- package/dist/workflows/getAllUsersWorkflow.d.ts +27 -0
- package/dist/workflows/getAllUsersWorkflow.d.ts.map +1 -0
- package/dist/workflows/getAllUsersWorkflow.js +48 -0
- package/dist/workflows/getAllUsersWorkflow.js.map +1 -0
- package/dist/workflows/issueCreationHelpers.d.ts +54 -0
- package/dist/workflows/issueCreationHelpers.d.ts.map +1 -0
- package/dist/workflows/issueCreationHelpers.js +272 -0
- package/dist/workflows/issueCreationHelpers.js.map +1 -0
- package/dist/workflows/logTimeWorkflow.d.ts +6 -1
- package/dist/workflows/logTimeWorkflow.d.ts.map +1 -1
- package/dist/workflows/logTimeWorkflow.js +139 -20
- package/dist/workflows/logTimeWorkflow.js.map +1 -1
- package/dist/workflows/sheetToCsvWorkflow.d.ts +15 -0
- package/dist/workflows/sheetToCsvWorkflow.d.ts.map +1 -0
- package/dist/workflows/sheetToCsvWorkflow.js +65 -0
- package/dist/workflows/sheetToCsvWorkflow.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
import { BATCH_TRACKER_BY_LEVEL, FEATURE_ROOT_NODE_ID, } from "./batchCreationTypes.js";
|
|
2
|
+
import { appendCommentsToDescription, parseHours, splitSubjectAndDescription, } from "./csvUtils.js";
|
|
3
|
+
import { isAggregatingSheetRow, hoursForSubtaskOnRow, hoursForUserStoryOnRow, } from "./batchSheetHeuristics.js";
|
|
4
|
+
import { findFeatureSheetRow, readFeatureRowExistingIdFromSheet, } from "./featureRow.js";
|
|
5
|
+
import { buildSheetWriteBackTarget, readExistingRedmineIdFromRow, } from "./sheetColumnMap.js";
|
|
6
|
+
import { normalizeSheetPriority, trackerForSheetRow, } from "./sheetRedmineFields.js";
|
|
7
|
+
import { STEPS_TO_REPRODUCE_TRACKER_NAMES } from "../services/redmineConfig.js";
|
|
8
|
+
/**
|
|
9
|
+
* Steps to Reproduce is never its own sheet column. Workflow derives it from mapped roles.
|
|
10
|
+
* Returned on map_columns / confirm_mapping so the agent does not invent a column.
|
|
11
|
+
*/
|
|
12
|
+
export const STEPS_TO_REPRODUCE_RULE = {
|
|
13
|
+
notASheetColumn: true,
|
|
14
|
+
doNotMapSeparateColumn: true,
|
|
15
|
+
appliesToTrackers: [...STEPS_TO_REPRODUCE_TRACKER_NAMES],
|
|
16
|
+
valuePriority: [
|
|
17
|
+
{
|
|
18
|
+
priority: 1,
|
|
19
|
+
source: "comments",
|
|
20
|
+
role: "comments",
|
|
21
|
+
label: "comments role (e.g. Comments / Question, RT Comment)",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
priority: 2,
|
|
25
|
+
source: "description",
|
|
26
|
+
role: "description",
|
|
27
|
+
label: "description role (Description column when present)",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
priority: 3,
|
|
31
|
+
source: "issueSubject",
|
|
32
|
+
roles: ["subtask", "userStory"],
|
|
33
|
+
label: "issue subject — first line of subtask cell, else userStory cell",
|
|
34
|
+
},
|
|
35
|
+
{ priority: 4, source: "fallback", label: '"..."' },
|
|
36
|
+
],
|
|
37
|
+
summary: "Map comments and description roles when headers exist. Bug/Task Steps to Reproduce is filled automatically from those mapped values, then issue subject — never map a dedicated Steps to Reproduce column.",
|
|
38
|
+
};
|
|
39
|
+
export const BATCH_COLUMN_FIELD_SCHEMA = [
|
|
40
|
+
{
|
|
41
|
+
role: "userStory",
|
|
42
|
+
required: true,
|
|
43
|
+
redmineUse: "User Story subject (User Stories, Task, or User Story column); first line = subject, rest = description",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
role: "subtask",
|
|
47
|
+
required: true,
|
|
48
|
+
redmineUse: "Task subject (Tasks, Sub-Tasks, or Sub Task column) under current User Story",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
role: "description",
|
|
52
|
+
required: false,
|
|
53
|
+
redmineUse: "Task description body (when Sub-Tasks present); Steps to Reproduce priority 2 (Bug/Task) when comments empty",
|
|
54
|
+
stepsToReproducePriority: 2,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
role: "hours",
|
|
58
|
+
required: false,
|
|
59
|
+
redmineUse: "estimated_hours on Task; on story-only rows, on User Story",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
role: "targetVersion",
|
|
63
|
+
required: false,
|
|
64
|
+
redmineUse: "fixed_version on User Story (applies until next story row)",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
role: "assignedTo",
|
|
68
|
+
required: false,
|
|
69
|
+
redmineUse: "assigned_to on Task rows only",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
role: "comments",
|
|
73
|
+
required: false,
|
|
74
|
+
redmineUse: "Appended to issue description; Steps to Reproduce priority 1 (Bug/Task) when non-empty",
|
|
75
|
+
stepsToReproducePriority: 1,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
role: "redmineId",
|
|
79
|
+
required: false,
|
|
80
|
+
redmineUse: "Existing issue id — skip create; write-back after upload",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
role: "ticket",
|
|
84
|
+
required: false,
|
|
85
|
+
redmineUse: "Ticket:#id (Task) and MainTicket:#id (User Story) in one column; Task-only rows use Ticket:#id",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
role: "system",
|
|
89
|
+
required: false,
|
|
90
|
+
redmineUse: "Optional project hint (e.g. FE/BE)",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
role: "status",
|
|
94
|
+
required: false,
|
|
95
|
+
redmineUse: "Optional override (usually workflow default New)",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
role: "priority",
|
|
99
|
+
required: false,
|
|
100
|
+
redmineUse: "Optional override (usually Normal)",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
role: "sheetType",
|
|
104
|
+
required: false,
|
|
105
|
+
redmineUse: "Redmine tracker (Type column), e.g. Bug, CR → Change Request",
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
const REQUIRED_COLUMN_ROLES = ["userStory", "subtask"];
|
|
109
|
+
const ROLE_QUESTION_LABEL = {
|
|
110
|
+
userStory: "User Story (User Stories / Task column)",
|
|
111
|
+
subtask: "Task (Tasks / Sub-Tasks column under each story)",
|
|
112
|
+
description: "Description (task body when Sub-Tasks present)",
|
|
113
|
+
hours: "Hours (estimated hours)",
|
|
114
|
+
targetVersion: "Target Version (fixed version on User Story)",
|
|
115
|
+
assignedTo: "Assignee (on Task rows)",
|
|
116
|
+
comments: "Comments (appended to description)",
|
|
117
|
+
redmineId: "Task ID / existing Redmine link column",
|
|
118
|
+
ticket: "Ticket (Ticket:#id = Task, MainTicket:#id = User Story)",
|
|
119
|
+
system: "System (optional project hint)",
|
|
120
|
+
status: "Status override",
|
|
121
|
+
priority: "Priority override",
|
|
122
|
+
sheetType: "Type (tracker override, e.g. Bug)",
|
|
123
|
+
};
|
|
124
|
+
const OPTIONAL_UNMAPPED = "(leave unmapped)";
|
|
125
|
+
function resolveColumnIndexDetailed(headers, spec) {
|
|
126
|
+
if (spec === undefined) {
|
|
127
|
+
return { ok: false, reason: "missing_spec" };
|
|
128
|
+
}
|
|
129
|
+
if (typeof spec === "number") {
|
|
130
|
+
if (spec < 0 || spec >= headers.length) {
|
|
131
|
+
return { ok: false, reason: "out_of_range" };
|
|
132
|
+
}
|
|
133
|
+
return { ok: true, index: spec };
|
|
134
|
+
}
|
|
135
|
+
const key = spec.trim().toLowerCase();
|
|
136
|
+
if (!key)
|
|
137
|
+
return { ok: false, reason: "missing_spec" };
|
|
138
|
+
const exactMatches = [];
|
|
139
|
+
for (let i = 0; i < headers.length; i++) {
|
|
140
|
+
const h = headers[i]?.trim() ?? "";
|
|
141
|
+
if (!h)
|
|
142
|
+
continue;
|
|
143
|
+
if (h.toLowerCase() === key)
|
|
144
|
+
exactMatches.push(i);
|
|
145
|
+
}
|
|
146
|
+
if (exactMatches.length === 1) {
|
|
147
|
+
return { ok: true, index: exactMatches[0] };
|
|
148
|
+
}
|
|
149
|
+
if (exactMatches.length > 1) {
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
reason: "ambiguous",
|
|
153
|
+
matches: exactMatches.map((i) => headers[i] ?? "").filter(Boolean),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return { ok: false, reason: "not_found" };
|
|
157
|
+
}
|
|
158
|
+
function resolveColumnIndex(headers, spec) {
|
|
159
|
+
const result = resolveColumnIndexDetailed(headers, spec);
|
|
160
|
+
return result.ok ? result.index : -1;
|
|
161
|
+
}
|
|
162
|
+
function mappedColumnLabel(mapping, headers, role) {
|
|
163
|
+
const spec = mapping.columns[role];
|
|
164
|
+
if (spec === undefined)
|
|
165
|
+
return "(unmapped)";
|
|
166
|
+
const idx = resolveColumnIndex(headers, spec);
|
|
167
|
+
if (idx < 0)
|
|
168
|
+
return String(spec);
|
|
169
|
+
return headers[idx]?.trim() || `col ${idx}`;
|
|
170
|
+
}
|
|
171
|
+
const REDMINE_ID_HEADER_PATTERNS = new Set([
|
|
172
|
+
"task id",
|
|
173
|
+
"taskid",
|
|
174
|
+
"task_id",
|
|
175
|
+
"redmine id",
|
|
176
|
+
"redmine #",
|
|
177
|
+
"issue id",
|
|
178
|
+
"issue #",
|
|
179
|
+
"#",
|
|
180
|
+
]);
|
|
181
|
+
function normalizeHeaderLabel(header) {
|
|
182
|
+
return header.trim().toLowerCase();
|
|
183
|
+
}
|
|
184
|
+
function isRedmineIdHeader(normalized) {
|
|
185
|
+
return REDMINE_ID_HEADER_PATTERNS.has(normalized);
|
|
186
|
+
}
|
|
187
|
+
const OPTIONAL_COLUMN_ROLES = [
|
|
188
|
+
"description",
|
|
189
|
+
"hours",
|
|
190
|
+
"targetVersion",
|
|
191
|
+
"assignedTo",
|
|
192
|
+
"comments",
|
|
193
|
+
"redmineId",
|
|
194
|
+
"ticket",
|
|
195
|
+
"system",
|
|
196
|
+
"status",
|
|
197
|
+
"sheetType",
|
|
198
|
+
"priority",
|
|
199
|
+
];
|
|
200
|
+
/**
|
|
201
|
+
* Derive column roles from detected headers (MCP — agent uses this, no repo file search).
|
|
202
|
+
* Recognizes User Stories + Tasks layout as well as Task + Sub-Tasks.
|
|
203
|
+
*/
|
|
204
|
+
export function suggestColumnMappingFromHeaders(headers, headerRowIndex) {
|
|
205
|
+
const normalized = headers.map((h) => normalizeHeaderLabel(h ?? ""));
|
|
206
|
+
const columns = {};
|
|
207
|
+
const suggestedColumnIndices = {};
|
|
208
|
+
const usedIndices = new Set();
|
|
209
|
+
const ambiguousRoles = [];
|
|
210
|
+
function findIndices(predicate) {
|
|
211
|
+
const matches = [];
|
|
212
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
213
|
+
const n = normalized[i] ?? "";
|
|
214
|
+
if (!n || usedIndices.has(i))
|
|
215
|
+
continue;
|
|
216
|
+
if (predicate(n, i))
|
|
217
|
+
matches.push(i);
|
|
218
|
+
}
|
|
219
|
+
return matches;
|
|
220
|
+
}
|
|
221
|
+
function assignRole(role, indices) {
|
|
222
|
+
if (indices.length === 0)
|
|
223
|
+
return false;
|
|
224
|
+
if (indices.length > 1) {
|
|
225
|
+
ambiguousRoles.push(role);
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
const index = indices[0];
|
|
229
|
+
const label = headers[index]?.trim();
|
|
230
|
+
if (!label)
|
|
231
|
+
return false;
|
|
232
|
+
columns[role] = label;
|
|
233
|
+
suggestedColumnIndices[role] = index;
|
|
234
|
+
usedIndices.add(index);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
assignRole("userStory", findIndices((n) => n === "user stories" || n === "user story" || n === "story").length > 0
|
|
238
|
+
? findIndices((n) => n === "user stories" || n === "user story" || n === "story")
|
|
239
|
+
: findIndices((n) => n === "task" &&
|
|
240
|
+
!n.includes("sub") &&
|
|
241
|
+
!isRedmineIdHeader(n)));
|
|
242
|
+
assignRole("subtask", findIndices((n) => n.includes("sub") &&
|
|
243
|
+
(n.includes("task") || n.includes("tasks"))).length > 0
|
|
244
|
+
? findIndices((n) => n.includes("sub") &&
|
|
245
|
+
(n.includes("task") || n.includes("tasks")))
|
|
246
|
+
: findIndices((n) => n === "tasks"));
|
|
247
|
+
assignRole("description", findIndices((n) => n === "description" || n.includes("description")));
|
|
248
|
+
assignRole("hours", findIndices((n) => n === "hours" || n === "eta" || n === "estimate"));
|
|
249
|
+
assignRole("targetVersion", findIndices((n) => n === "target version" || n === "version" || n === "sprint"));
|
|
250
|
+
assignRole("assignedTo", findIndices((n) => n === "assigned to" || n === "assignee" || n.includes("assignee")));
|
|
251
|
+
assignRole("comments", findIndices((n) => n.includes("comment") || n.includes("question")));
|
|
252
|
+
assignRole("redmineId", findIndices((n) => isRedmineIdHeader(n)));
|
|
253
|
+
if (!columns.redmineId) {
|
|
254
|
+
assignRole("ticket", findIndices((n) => n === "ticket"));
|
|
255
|
+
}
|
|
256
|
+
assignRole("system", findIndices((n) => n === "system" || n.includes("fe/be")));
|
|
257
|
+
assignRole("status", findIndices((n) => n === "status"));
|
|
258
|
+
assignRole("sheetType", findIndices((n) => n === "type" || n === "tracker"));
|
|
259
|
+
assignRole("priority", findIndices((n) => n === "priority"));
|
|
260
|
+
const mappedRoles = new Set(Object.keys(columns));
|
|
261
|
+
const unmappedOptionalRoles = OPTIONAL_COLUMN_ROLES.filter((role) => !mappedRoles.has(role));
|
|
262
|
+
if (!columns.userStory)
|
|
263
|
+
ambiguousRoles.push("userStory");
|
|
264
|
+
if (!columns.subtask)
|
|
265
|
+
ambiguousRoles.push("subtask");
|
|
266
|
+
return {
|
|
267
|
+
headerRowIndex,
|
|
268
|
+
columns,
|
|
269
|
+
suggestedColumnIndices,
|
|
270
|
+
unmappedOptionalRoles,
|
|
271
|
+
ambiguousRoles: [...new Set(ambiguousRoles)],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** Resolved Steps to Reproduce sources for a concrete columnMapping (MCP + mapping preview). */
|
|
275
|
+
export function buildStepsToReproduceMappingSection(mapping, headers) {
|
|
276
|
+
const lines = [
|
|
277
|
+
"",
|
|
278
|
+
"STEPS TO REPRODUCE (Bug + Task — derived from mapped roles, NOT a sheet column)",
|
|
279
|
+
" Do not map a separate column. Workflow fills the Redmine custom field automatically.",
|
|
280
|
+
"",
|
|
281
|
+
` 1. comments role → ${mappedColumnLabel(mapping, headers, "comments")}`,
|
|
282
|
+
` 2. description role → ${mappedColumnLabel(mapping, headers, "description")}`,
|
|
283
|
+
` 3. issue subject → subtask "${mappedColumnLabel(mapping, headers, "subtask")}" or userStory "${mappedColumnLabel(mapping, headers, "userStory")}"`,
|
|
284
|
+
' 4. fallback → "..."',
|
|
285
|
+
];
|
|
286
|
+
return lines.join("\n");
|
|
287
|
+
}
|
|
288
|
+
/** Validate agent columnMapping before building the issue tree. */
|
|
289
|
+
export function validateColumnMapping(raw, mapping) {
|
|
290
|
+
const problems = [];
|
|
291
|
+
const headerRowIndex = mapping.headerRowIndex;
|
|
292
|
+
if (headerRowIndex < 0 || headerRowIndex >= raw.rows.length) {
|
|
293
|
+
problems.push({
|
|
294
|
+
role: "userStory",
|
|
295
|
+
issue: `headerRowIndex ${headerRowIndex} is outside the sheet (${raw.rows.length} row(s)).`,
|
|
296
|
+
});
|
|
297
|
+
return {
|
|
298
|
+
valid: false,
|
|
299
|
+
problems,
|
|
300
|
+
detectedHeaders: [],
|
|
301
|
+
headerRowIndex,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const headerRow = raw.rows[headerRowIndex]?.cells ?? [];
|
|
305
|
+
const detectedHeaders = headerRow.map((h) => h.trim());
|
|
306
|
+
if (detectedHeaders.filter(Boolean).length === 0) {
|
|
307
|
+
problems.push({
|
|
308
|
+
role: "userStory",
|
|
309
|
+
issue: `Header row ${headerRowIndex + 1} has no column labels.`,
|
|
310
|
+
});
|
|
311
|
+
return { valid: false, problems, detectedHeaders, headerRowIndex };
|
|
312
|
+
}
|
|
313
|
+
const resolved = new Map();
|
|
314
|
+
for (const [role, spec] of Object.entries(mapping.columns)) {
|
|
315
|
+
if (spec === undefined)
|
|
316
|
+
continue;
|
|
317
|
+
const result = resolveColumnIndexDetailed(detectedHeaders, spec);
|
|
318
|
+
if (!result.ok) {
|
|
319
|
+
const label = ROLE_QUESTION_LABEL[role] ?? role;
|
|
320
|
+
if (result.reason === "missing_spec") {
|
|
321
|
+
problems.push({ role, issue: `${label}: no column specified.` });
|
|
322
|
+
}
|
|
323
|
+
else if (result.reason === "not_found") {
|
|
324
|
+
problems.push({
|
|
325
|
+
role,
|
|
326
|
+
issue: `${label}: column "${String(spec)}" not found in headers.`,
|
|
327
|
+
spec,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
else if (result.reason === "out_of_range") {
|
|
331
|
+
problems.push({
|
|
332
|
+
role,
|
|
333
|
+
issue: `${label}: column index ${String(spec)} is out of range (0–${detectedHeaders.length - 1}).`,
|
|
334
|
+
spec,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
else if (result.reason === "ambiguous") {
|
|
338
|
+
problems.push({
|
|
339
|
+
role,
|
|
340
|
+
issue: `${label}: "${String(spec)}" matches multiple headers: ${(result.matches ?? []).join(", ")}.`,
|
|
341
|
+
spec,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
resolved.set(role, result.index);
|
|
347
|
+
}
|
|
348
|
+
for (const role of REQUIRED_COLUMN_ROLES) {
|
|
349
|
+
if (!mapping.columns[role]) {
|
|
350
|
+
problems.push({
|
|
351
|
+
role,
|
|
352
|
+
issue: `${ROLE_QUESTION_LABEL[role]} is required but not mapped.`,
|
|
353
|
+
});
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (!resolved.has(role)) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const userStoryIdx = resolved.get("userStory");
|
|
361
|
+
const subtaskIdx = resolved.get("subtask");
|
|
362
|
+
if (userStoryIdx !== undefined &&
|
|
363
|
+
subtaskIdx !== undefined &&
|
|
364
|
+
userStoryIdx === subtaskIdx) {
|
|
365
|
+
problems.push({
|
|
366
|
+
role: "subtask",
|
|
367
|
+
issue: "User Story and Sub-Task cannot use the same column.",
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const seen = new Map();
|
|
371
|
+
for (const [role, index] of resolved) {
|
|
372
|
+
const other = seen.get(index);
|
|
373
|
+
if (other && other !== role) {
|
|
374
|
+
problems.push({
|
|
375
|
+
role,
|
|
376
|
+
issue: `Column "${detectedHeaders[index]}" is mapped to both ${other} and ${role}.`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
seen.set(index, role);
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
valid: problems.length === 0,
|
|
383
|
+
problems,
|
|
384
|
+
detectedHeaders: detectedHeaders.filter(Boolean),
|
|
385
|
+
headerRowIndex,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/** AskQuestion prompts for map_columns / fix_column_mapping steps. */
|
|
389
|
+
export function buildColumnMappingQuestions(validation, options) {
|
|
390
|
+
const headerOptions = validation.detectedHeaders.filter(Boolean);
|
|
391
|
+
const rolesNeeded = new Set();
|
|
392
|
+
for (const problem of validation.problems) {
|
|
393
|
+
rolesNeeded.add(problem.role);
|
|
394
|
+
}
|
|
395
|
+
if (rolesNeeded.size === 0 && options?.includeOptional) {
|
|
396
|
+
for (const field of BATCH_COLUMN_FIELD_SCHEMA) {
|
|
397
|
+
if (field.required)
|
|
398
|
+
rolesNeeded.add(field.role);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const questions = [];
|
|
402
|
+
for (const role of rolesNeeded) {
|
|
403
|
+
const schema = BATCH_COLUMN_FIELD_SCHEMA.find((f) => f.role === role);
|
|
404
|
+
const required = schema?.required ?? REQUIRED_COLUMN_ROLES.includes(role);
|
|
405
|
+
const problem = validation.problems.find((p) => p.role === role);
|
|
406
|
+
const label = ROLE_QUESTION_LABEL[role] ?? role;
|
|
407
|
+
const roleOptions = required
|
|
408
|
+
? [...headerOptions]
|
|
409
|
+
: [...headerOptions, OPTIONAL_UNMAPPED];
|
|
410
|
+
questions.push({
|
|
411
|
+
id: `column-${role}`,
|
|
412
|
+
field: `columnMapping.columns.${role}`,
|
|
413
|
+
question: problem
|
|
414
|
+
? `${label} — ${problem.issue} Pick the correct sheet column.`
|
|
415
|
+
: `${label} — which sheet column? ${schema?.redmineUse ?? ""}`.trim(),
|
|
416
|
+
options: roleOptions.length > 0 ? roleOptions : headerOptions,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
return questions;
|
|
420
|
+
}
|
|
421
|
+
/** Human-readable table of agent columnMapping for confirm_mapping step. */
|
|
422
|
+
export function buildMappingPreview(mapping, headers) {
|
|
423
|
+
const lines = [
|
|
424
|
+
"══════════════════════════════════════════════════════════════",
|
|
425
|
+
" COLUMN MAPPING — review before parsing (confirm or change)",
|
|
426
|
+
"══════════════════════════════════════════════════════════════",
|
|
427
|
+
"",
|
|
428
|
+
`Header row: sheet line ${mapping.headerRowIndex + 1} (0-based index ${mapping.headerRowIndex})`,
|
|
429
|
+
"",
|
|
430
|
+
"Detected headers:",
|
|
431
|
+
headers.map((h, i) => ` [${i}] ${h.trim() || "(empty)"}`).join("\n"),
|
|
432
|
+
"",
|
|
433
|
+
"Role │ Sheet column",
|
|
434
|
+
"──────────────┼────────────────────────────",
|
|
435
|
+
];
|
|
436
|
+
for (const field of BATCH_COLUMN_FIELD_SCHEMA) {
|
|
437
|
+
const spec = mapping.columns[field.role];
|
|
438
|
+
if (spec === undefined) {
|
|
439
|
+
if (field.required) {
|
|
440
|
+
lines.push(`${field.role.padEnd(13)} │ **MISSING (required)**`);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
lines.push(`${field.role.padEnd(13)} │ (unmapped)`);
|
|
444
|
+
}
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const idx = resolveColumnIndex(headers, spec);
|
|
448
|
+
const label = idx >= 0 ? headers[idx]?.trim() || `col ${idx}` : String(spec);
|
|
449
|
+
const mark = field.required ? " *" : "";
|
|
450
|
+
lines.push(`${field.role.padEnd(13)} │ ${label}${mark}`);
|
|
451
|
+
}
|
|
452
|
+
lines.push("");
|
|
453
|
+
lines.push("* = required. Optional roles may stay unmapped.");
|
|
454
|
+
lines.push(buildStepsToReproduceMappingSection(mapping, headers));
|
|
455
|
+
return lines.join("\n");
|
|
456
|
+
}
|
|
457
|
+
export function columnMappingToSheetMap(headers, mapping) {
|
|
458
|
+
const cols = mapping.columns;
|
|
459
|
+
const idx = (role) => resolveColumnIndex(headers, cols[role]);
|
|
460
|
+
return {
|
|
461
|
+
ticket: idx("ticket"),
|
|
462
|
+
userStory: idx("userStory"),
|
|
463
|
+
subtask: idx("subtask"),
|
|
464
|
+
description: idx("description"),
|
|
465
|
+
hours: idx("hours"),
|
|
466
|
+
targetVersion: idx("targetVersion"),
|
|
467
|
+
assignedTo: idx("assignedTo"),
|
|
468
|
+
comments: idx("comments"),
|
|
469
|
+
redmineId: idx("redmineId"),
|
|
470
|
+
system: idx("system"),
|
|
471
|
+
status: idx("status"),
|
|
472
|
+
sheetType: idx("sheetType"),
|
|
473
|
+
priority: idx("priority"),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function cell(row, index) {
|
|
477
|
+
if (index < 0)
|
|
478
|
+
return "";
|
|
479
|
+
return (row[index] ?? "").trim();
|
|
480
|
+
}
|
|
481
|
+
function buildIssue(params) {
|
|
482
|
+
if (!params.subject.trim())
|
|
483
|
+
return null;
|
|
484
|
+
const issue = {
|
|
485
|
+
nodeId: `row-${params.rowIndex}-${params.level}`,
|
|
486
|
+
rowIndex: params.rowIndex,
|
|
487
|
+
level: params.level,
|
|
488
|
+
tracker: trackerForSheetRow(params.sheetType ?? "", params.level),
|
|
489
|
+
subject: params.subject.trim(),
|
|
490
|
+
description: appendCommentsToDescription(params.description, params.comments),
|
|
491
|
+
...(params.comments.trim()
|
|
492
|
+
? { comments: params.comments.trim() }
|
|
493
|
+
: {}),
|
|
494
|
+
targetVersion: params.targetVersion.trim(),
|
|
495
|
+
assignedTo: params.assignedTo.trim(),
|
|
496
|
+
system: params.system?.trim() ?? "",
|
|
497
|
+
parentNodeId: params.parentNodeId,
|
|
498
|
+
...(params.sheetType?.trim()
|
|
499
|
+
? { sheetType: params.sheetType.trim() }
|
|
500
|
+
: {}),
|
|
501
|
+
};
|
|
502
|
+
if (params.hours !== undefined)
|
|
503
|
+
issue.estimatedHours = params.hours;
|
|
504
|
+
if (params.existingRedmineId !== undefined) {
|
|
505
|
+
issue.existingRedmineId = params.existingRedmineId;
|
|
506
|
+
}
|
|
507
|
+
const priority = normalizeSheetPriority(params.sheetPriority ?? "");
|
|
508
|
+
if (priority)
|
|
509
|
+
issue.priority = priority;
|
|
510
|
+
return issue;
|
|
511
|
+
}
|
|
512
|
+
export function parseBatchWithColumnMapping(raw, mapping) {
|
|
513
|
+
const parseWarnings = [];
|
|
514
|
+
const headerRowIndex = mapping.headerRowIndex;
|
|
515
|
+
const headerRow = raw.rows[headerRowIndex]?.cells ?? [];
|
|
516
|
+
const cols = columnMappingToSheetMap(headerRow, mapping);
|
|
517
|
+
const sheetWriteBack = buildSheetWriteBackTarget(cols);
|
|
518
|
+
const featureTitle = raw.featureTitle;
|
|
519
|
+
const issues = [];
|
|
520
|
+
if (cols.userStory < 0 || cols.subtask < 0) {
|
|
521
|
+
return {
|
|
522
|
+
format: "mapped",
|
|
523
|
+
scopeNotes: "",
|
|
524
|
+
headerRowIndex: headerRowIndex + 1,
|
|
525
|
+
issues: [],
|
|
526
|
+
parseWarnings: [
|
|
527
|
+
"columnMapping must include userStory and subtask columns.",
|
|
528
|
+
],
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (featureTitle) {
|
|
532
|
+
const matrix = raw.rows.map((r) => r.cells);
|
|
533
|
+
const featureSheetRow = findFeatureSheetRow(matrix, headerRowIndex);
|
|
534
|
+
const existingFeatureId = readFeatureRowExistingIdFromSheet(matrix, featureSheetRow, cols.redmineId);
|
|
535
|
+
const featureNode = buildIssue({
|
|
536
|
+
rowIndex: featureSheetRow,
|
|
537
|
+
level: "feature",
|
|
538
|
+
subject: featureTitle,
|
|
539
|
+
description: "",
|
|
540
|
+
targetVersion: "",
|
|
541
|
+
assignedTo: "",
|
|
542
|
+
comments: "",
|
|
543
|
+
parentNodeId: null,
|
|
544
|
+
...(existingFeatureId !== undefined
|
|
545
|
+
? { existingRedmineId: existingFeatureId }
|
|
546
|
+
: {}),
|
|
547
|
+
});
|
|
548
|
+
if (featureNode) {
|
|
549
|
+
featureNode.nodeId = FEATURE_ROOT_NODE_ID;
|
|
550
|
+
issues.push(featureNode);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
let currentUserStoryNodeId = null;
|
|
554
|
+
const featureParentId = featureTitle ? FEATURE_ROOT_NODE_ID : null;
|
|
555
|
+
let currentTargetVersion = "";
|
|
556
|
+
for (let i = headerRowIndex + 1; i < raw.rows.length; i++) {
|
|
557
|
+
const row = raw.rows[i]?.cells ?? [];
|
|
558
|
+
const sheetRow = raw.rows[i]?.sheetRow ?? i + 1;
|
|
559
|
+
const taskCell = cell(row, cols.userStory);
|
|
560
|
+
const subCell = cell(row, cols.subtask);
|
|
561
|
+
const descriptionCol = cell(row, cols.description);
|
|
562
|
+
const hoursCol = cols.hours >= 0 ? cell(row, cols.hours) : "";
|
|
563
|
+
const targetVersionCol = cols.targetVersion >= 0 ? cell(row, cols.targetVersion) : "";
|
|
564
|
+
const assignedToCol = cols.assignedTo >= 0 ? cell(row, cols.assignedTo) : "";
|
|
565
|
+
const commentsCol = cols.comments >= 0 ? cell(row, cols.comments) : "";
|
|
566
|
+
const sheetTypeCol = cols.sheetType >= 0 ? cell(row, cols.sheetType) : "";
|
|
567
|
+
const sheetPriorityCol = cols.priority >= 0 ? cell(row, cols.priority) : "";
|
|
568
|
+
const systemCol = cols.system >= 0 ? cell(row, cols.system) : "";
|
|
569
|
+
const storyRedmineId = readExistingRedmineIdFromRow(row, cols, "userStory");
|
|
570
|
+
if (!taskCell && !subCell)
|
|
571
|
+
continue;
|
|
572
|
+
if (isAggregatingSheetRow({ userStoryText: taskCell, subtaskText: subCell })) {
|
|
573
|
+
parseWarnings.push(`Row ${sheetRow}: Rollup/summary row skipped (not created on Redmine).`);
|
|
574
|
+
currentUserStoryNodeId = null;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (taskCell) {
|
|
578
|
+
if (targetVersionCol)
|
|
579
|
+
currentTargetVersion = targetVersionCol;
|
|
580
|
+
const { subject, description: taskBody } = splitSubjectAndDescription(taskCell);
|
|
581
|
+
const storyHoursCell = hoursForUserStoryOnRow(subCell, hoursCol);
|
|
582
|
+
const hours = storyHoursCell ? parseHours(storyHoursCell) : undefined;
|
|
583
|
+
const subRowIdEarly = subCell !== ""
|
|
584
|
+
? readExistingRedmineIdFromRow(row, cols, "subtask")
|
|
585
|
+
: undefined;
|
|
586
|
+
// Same-row subtask gets description/comments when it will be created.
|
|
587
|
+
// When subtask already exists on Redmine, attach row fields to User Story.
|
|
588
|
+
const subtaskWillBeCreated = subCell !== "" && subRowIdEarly === undefined;
|
|
589
|
+
const storyDescription = subCell && !subtaskWillBeCreated
|
|
590
|
+
? descriptionCol.trim() || taskBody
|
|
591
|
+
: taskBody;
|
|
592
|
+
const storyComments = subCell && subtaskWillBeCreated ? "" : commentsCol;
|
|
593
|
+
const node = buildIssue({
|
|
594
|
+
rowIndex: sheetRow,
|
|
595
|
+
level: "userStory",
|
|
596
|
+
subject,
|
|
597
|
+
description: storyDescription,
|
|
598
|
+
...(hours !== undefined ? { hours } : {}),
|
|
599
|
+
targetVersion: currentTargetVersion,
|
|
600
|
+
assignedTo: "",
|
|
601
|
+
comments: storyComments,
|
|
602
|
+
parentNodeId: featureParentId,
|
|
603
|
+
sheetType: sheetTypeCol,
|
|
604
|
+
sheetPriority: sheetPriorityCol,
|
|
605
|
+
system: systemCol,
|
|
606
|
+
...(storyRedmineId !== undefined
|
|
607
|
+
? { existingRedmineId: storyRedmineId }
|
|
608
|
+
: {}),
|
|
609
|
+
});
|
|
610
|
+
if (!node) {
|
|
611
|
+
parseWarnings.push(`Row ${sheetRow}: User Story has no subject.`);
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
issues.push(node);
|
|
615
|
+
currentUserStoryNodeId = node.nodeId;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (subCell) {
|
|
619
|
+
if (!currentUserStoryNodeId) {
|
|
620
|
+
parseWarnings.push(`Row ${sheetRow}: Sub-Task skipped — no User Story above.`);
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const { subject } = splitSubjectAndDescription(subCell);
|
|
624
|
+
const subHoursCell = hoursForSubtaskOnRow(taskCell, hoursCol);
|
|
625
|
+
const hours = subHoursCell ? parseHours(subHoursCell) : undefined;
|
|
626
|
+
const subRowId = readExistingRedmineIdFromRow(row, cols, "subtask");
|
|
627
|
+
const node = buildIssue({
|
|
628
|
+
rowIndex: sheetRow,
|
|
629
|
+
level: "subtask",
|
|
630
|
+
subject,
|
|
631
|
+
description: descriptionCol,
|
|
632
|
+
...(hours !== undefined ? { hours } : {}),
|
|
633
|
+
targetVersion: "",
|
|
634
|
+
assignedTo: assignedToCol,
|
|
635
|
+
comments: commentsCol,
|
|
636
|
+
parentNodeId: currentUserStoryNodeId,
|
|
637
|
+
sheetType: sheetTypeCol,
|
|
638
|
+
sheetPriority: sheetPriorityCol,
|
|
639
|
+
system: systemCol,
|
|
640
|
+
...(subRowId !== undefined ? { existingRedmineId: subRowId } : {}),
|
|
641
|
+
});
|
|
642
|
+
if (!node) {
|
|
643
|
+
parseWarnings.push(`Row ${sheetRow}: Sub-Task has no subject.`);
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
issues.push(node);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
format: "mapped",
|
|
651
|
+
scopeNotes: featureTitle ? `Feature: ${featureTitle}` : "",
|
|
652
|
+
headerRowIndex: headerRowIndex + 1,
|
|
653
|
+
sheetColumns: cols,
|
|
654
|
+
...(sheetWriteBack ? { sheetWriteBack } : {}),
|
|
655
|
+
...(featureTitle ? { featureTitle } : {}),
|
|
656
|
+
issues,
|
|
657
|
+
parseWarnings,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
//# sourceMappingURL=batchColumnMapping.js.map
|