@mcp-consultant-tools/azure-devops 27.0.0-beta.1 → 27.0.0-beta.10

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.
Files changed (40) hide show
  1. package/build/AzureDevOpsService.d.ts +62 -0
  2. package/build/AzureDevOpsService.d.ts.map +1 -1
  3. package/build/AzureDevOpsService.js +395 -2
  4. package/build/AzureDevOpsService.js.map +1 -1
  5. package/build/index.d.ts.map +1 -1
  6. package/build/index.js +930 -13
  7. package/build/index.js.map +1 -1
  8. package/build/sync/file-utils.d.ts +86 -0
  9. package/build/sync/file-utils.d.ts.map +1 -0
  10. package/build/sync/file-utils.js +224 -0
  11. package/build/sync/file-utils.js.map +1 -0
  12. package/build/sync/git-utils.d.ts +31 -0
  13. package/build/sync/git-utils.d.ts.map +1 -0
  14. package/build/sync/git-utils.js +116 -0
  15. package/build/sync/git-utils.js.map +1 -0
  16. package/build/sync/html-converter.d.ts +32 -0
  17. package/build/sync/html-converter.d.ts.map +1 -0
  18. package/build/sync/html-converter.js +91 -0
  19. package/build/sync/html-converter.js.map +1 -0
  20. package/build/sync/html-detection.d.ts +93 -0
  21. package/build/sync/html-detection.d.ts.map +1 -0
  22. package/build/sync/html-detection.js +169 -0
  23. package/build/sync/html-detection.js.map +1 -0
  24. package/build/sync/index.d.ts +12 -0
  25. package/build/sync/index.d.ts.map +1 -0
  26. package/build/sync/index.js +12 -0
  27. package/build/sync/index.js.map +1 -0
  28. package/build/sync/markdown-serializer.d.ts +136 -0
  29. package/build/sync/markdown-serializer.d.ts.map +1 -0
  30. package/build/sync/markdown-serializer.js +646 -0
  31. package/build/sync/markdown-serializer.js.map +1 -0
  32. package/build/sync/task-serializer.d.ts +93 -0
  33. package/build/sync/task-serializer.d.ts.map +1 -0
  34. package/build/sync/task-serializer.js +395 -0
  35. package/build/sync/task-serializer.js.map +1 -0
  36. package/build/tool-examples.d.ts +56 -0
  37. package/build/tool-examples.d.ts.map +1 -0
  38. package/build/tool-examples.js +142 -0
  39. package/build/tool-examples.js.map +1 -0
  40. package/package.json +3 -1
@@ -0,0 +1,646 @@
1
+ /**
2
+ * Markdown Serialization Utilities
3
+ *
4
+ * Convert between ADO work items and local markdown files.
5
+ * Uses YAML frontmatter for metadata and markdown sections for content.
6
+ */
7
+ import { isHtmlContent, getAdditionalFieldConfig, ADDITIONAL_FIELD_DISPLAY_NAMES } from './html-detection.js';
8
+ /**
9
+ * Simple YAML serializer for frontmatter
10
+ * Handles basic types: string, number, boolean, arrays
11
+ */
12
+ function serializeFrontmatter(data) {
13
+ const lines = ['---'];
14
+ for (const [key, value] of Object.entries(data)) {
15
+ if (value === undefined || value === null)
16
+ continue;
17
+ if (Array.isArray(value)) {
18
+ if (value.length === 0)
19
+ continue;
20
+ lines.push(`${key}:`);
21
+ for (const item of value) {
22
+ lines.push(`- ${item}`);
23
+ }
24
+ }
25
+ else if (typeof value === 'string') {
26
+ // Quote strings that might be parsed as other types or contain special chars
27
+ if (value.includes(':') || value.includes('#') || value.includes('\n') ||
28
+ value.match(/^[\d.]+$/) || value === 'true' || value === 'false' ||
29
+ value === 'null' || value === '') {
30
+ lines.push(`${key}: "${value.replace(/"/g, '\\"')}"`);
31
+ }
32
+ else {
33
+ lines.push(`${key}: ${value}`);
34
+ }
35
+ }
36
+ else if (typeof value === 'number' || typeof value === 'boolean') {
37
+ lines.push(`${key}: ${value}`);
38
+ }
39
+ }
40
+ lines.push('---');
41
+ return lines.join('\n');
42
+ }
43
+ /**
44
+ * Simple YAML parser for frontmatter
45
+ */
46
+ function parseFrontmatter(yamlContent) {
47
+ const result = {};
48
+ const lines = yamlContent.split('\n');
49
+ let currentKey = null;
50
+ let currentArray = null;
51
+ for (const line of lines) {
52
+ // Skip empty lines
53
+ if (!line.trim())
54
+ continue;
55
+ // Array item
56
+ if (line.match(/^\s*-\s+/)) {
57
+ if (currentKey && currentArray !== null) {
58
+ const value = line.replace(/^\s*-\s+/, '').trim();
59
+ currentArray.push(parseYamlValue(value));
60
+ }
61
+ continue;
62
+ }
63
+ // Key-value pair
64
+ const match = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)?$/);
65
+ if (match) {
66
+ // Save previous array if any
67
+ if (currentKey && currentArray !== null) {
68
+ result[currentKey] = currentArray;
69
+ }
70
+ currentKey = match[1];
71
+ const rawValue = match[2]?.trim();
72
+ // Check if this is start of array (no value after colon)
73
+ if (!rawValue) {
74
+ currentArray = [];
75
+ }
76
+ else {
77
+ currentArray = null;
78
+ result[currentKey] = parseYamlValue(rawValue);
79
+ }
80
+ }
81
+ }
82
+ // Save final array if any
83
+ if (currentKey && currentArray !== null) {
84
+ result[currentKey] = currentArray;
85
+ }
86
+ return result;
87
+ }
88
+ /**
89
+ * Parse a YAML value string to appropriate type
90
+ */
91
+ function parseYamlValue(value) {
92
+ // Remove quotes
93
+ if ((value.startsWith('"') && value.endsWith('"')) ||
94
+ (value.startsWith("'") && value.endsWith("'"))) {
95
+ return value.slice(1, -1).replace(/\\"/g, '"');
96
+ }
97
+ // Boolean
98
+ if (value === 'true')
99
+ return true;
100
+ if (value === 'false')
101
+ return false;
102
+ // Null
103
+ if (value === 'null' || value === '~')
104
+ return null;
105
+ // Number
106
+ if (value.match(/^-?\d+$/))
107
+ return parseInt(value, 10);
108
+ if (value.match(/^-?\d+\.\d+$/))
109
+ return parseFloat(value);
110
+ return value;
111
+ }
112
+ /**
113
+ * Convert an ADO work item to markdown file content
114
+ *
115
+ * @param workItem - The work item from ADO API
116
+ * @param revision - The revision number
117
+ * @returns Object with markdown content and list of skipped fields (if any were HTML)
118
+ */
119
+ export function workItemToMarkdown(workItem, revision) {
120
+ const fields = workItem.fields || {};
121
+ const fieldConfig = getAdditionalFieldConfig();
122
+ const skippedFields = [];
123
+ // Build frontmatter data
124
+ const frontmatter = {
125
+ id: workItem.id,
126
+ title: fields['System.Title'] || '',
127
+ type: fields['System.WorkItemType'] || 'Unknown',
128
+ state: fields['System.State'] || '',
129
+ url: workItem._links?.html?.href || `https://dev.azure.com/_workitems/edit/${workItem.id}`,
130
+ };
131
+ // Optional fields
132
+ if (fields['System.AssignedTo']?.displayName) {
133
+ frontmatter.assignedTo = fields['System.AssignedTo'].displayName;
134
+ }
135
+ if (fields['Microsoft.VSTS.Scheduling.StoryPoints'] !== undefined) {
136
+ frontmatter.storyPoints = fields['Microsoft.VSTS.Scheduling.StoryPoints'];
137
+ }
138
+ if (fields['System.Parent']) {
139
+ frontmatter.parent = fields['System.Parent'];
140
+ }
141
+ // MoSCoW priority (custom field - varies by org)
142
+ const moscowField = fields['Custom.MoSCoW'] || fields['Microsoft.VSTS.Common.Priority'];
143
+ if (moscowField) {
144
+ frontmatter.moscow = moscowField;
145
+ }
146
+ // Tags
147
+ if (fields['System.Tags']) {
148
+ frontmatter.tags = fields['System.Tags'].split(';').map((t) => t.trim()).filter((t) => t);
149
+ }
150
+ if (fields['System.AreaPath']) {
151
+ frontmatter.areaPath = fields['System.AreaPath'];
152
+ }
153
+ if (fields['System.IterationPath']) {
154
+ frontmatter.iterationPath = fields['System.IterationPath'];
155
+ }
156
+ // Sync metadata
157
+ frontmatter.lastSyncedRevision = revision;
158
+ frontmatter.lastSyncedAt = new Date().toISOString();
159
+ // Build content sections
160
+ const description = fields['System.Description'] || '';
161
+ const acceptanceCriteria = fields['Microsoft.VSTS.Common.AcceptanceCriteria'] || '';
162
+ let content = serializeFrontmatter(frontmatter);
163
+ content += '\n\n# Description\n\n';
164
+ content += description.trim() || '_No description provided._';
165
+ content += '\n\n---\n\n# Acceptance Criteria\n\n';
166
+ content += acceptanceCriteria.trim() || '_No acceptance criteria provided._';
167
+ // Add additional fields if present and in markdown format
168
+ const addOptionalSection = (fieldName, displayName, sectionTitle) => {
169
+ const fieldContent = fields[fieldName];
170
+ if (!fieldContent || !fieldContent.trim()) {
171
+ return; // Field not present, skip silently
172
+ }
173
+ if (isHtmlContent(fieldContent)) {
174
+ skippedFields.push(`${displayName} (HTML)`);
175
+ return; // HTML format, skip with warning
176
+ }
177
+ content += `\n\n---\n\n# ${sectionTitle}\n\n`;
178
+ content += fieldContent.trim();
179
+ };
180
+ addOptionalSection(fieldConfig.howToTest, ADDITIONAL_FIELD_DISPLAY_NAMES.howToTest, 'How to Test');
181
+ addOptionalSection(fieldConfig.predeploymentSteps, ADDITIONAL_FIELD_DISPLAY_NAMES.predeploymentSteps, 'Predeployment Steps');
182
+ addOptionalSection(fieldConfig.postdeploymentSteps, ADDITIONAL_FIELD_DISPLAY_NAMES.postdeploymentSteps, 'Postdeployment Steps');
183
+ addOptionalSection(fieldConfig.deploymentInformation, ADDITIONAL_FIELD_DISPLAY_NAMES.deploymentInformation, 'Deployment Information');
184
+ content += '\n';
185
+ return { content, skippedFields };
186
+ }
187
+ /**
188
+ * Parse a markdown file to extract frontmatter and content sections
189
+ */
190
+ export function parseWorkItemMarkdown(content) {
191
+ // Extract frontmatter between ---
192
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
193
+ if (!frontmatterMatch) {
194
+ throw new Error('Invalid work item markdown file: missing YAML frontmatter');
195
+ }
196
+ const frontmatterRaw = frontmatterMatch[1];
197
+ const frontmatterData = parseFrontmatter(frontmatterRaw);
198
+ // Validate required fields
199
+ if (!frontmatterData.id || typeof frontmatterData.id !== 'number') {
200
+ throw new Error('Invalid work item markdown file: missing or invalid "id" in frontmatter');
201
+ }
202
+ const frontmatter = {
203
+ id: frontmatterData.id,
204
+ title: frontmatterData.title || '',
205
+ type: frontmatterData.type || 'Unknown',
206
+ state: frontmatterData.state || '',
207
+ url: frontmatterData.url || '',
208
+ assignedTo: frontmatterData.assignedTo,
209
+ storyPoints: frontmatterData.storyPoints,
210
+ parent: frontmatterData.parent,
211
+ moscow: frontmatterData.moscow,
212
+ tags: frontmatterData.tags,
213
+ areaPath: frontmatterData.areaPath,
214
+ iterationPath: frontmatterData.iterationPath,
215
+ lastSyncedRevision: frontmatterData.lastSyncedRevision || 0,
216
+ lastSyncedAt: frontmatterData.lastSyncedAt || '',
217
+ };
218
+ // Extract content after frontmatter
219
+ const contentAfterFrontmatter = content.slice(frontmatterMatch[0].length);
220
+ // Helper to extract a section by heading
221
+ // Looks for # SectionName and captures content until the next # heading or end of file
222
+ const extractSection = (sectionName) => {
223
+ // Match section heading followed by content, stopping at next section divider or heading
224
+ const regex = new RegExp(`#\\s+${escapeRegex(sectionName)}\\s*\\n([\\s\\S]*?)(?=\\n---\\n|\\n#\\s+|$)`, 'i');
225
+ const match = contentAfterFrontmatter.match(regex);
226
+ if (!match)
227
+ return undefined;
228
+ const sectionContent = match[1].trim();
229
+ // Return undefined if it's just the placeholder text
230
+ if (sectionContent === `_No ${sectionName.toLowerCase()} provided._`) {
231
+ return undefined;
232
+ }
233
+ return sectionContent || undefined;
234
+ };
235
+ // Extract primary sections
236
+ let description = extractSection('Description') || '';
237
+ if (description === '_No description provided._') {
238
+ description = '';
239
+ }
240
+ let acceptanceCriteria = extractSection('Acceptance Criteria') || '';
241
+ if (acceptanceCriteria === '_No acceptance criteria provided._') {
242
+ acceptanceCriteria = '';
243
+ }
244
+ // Extract additional fields (optional)
245
+ const additionalFields = {};
246
+ const howToTest = extractSection('How to Test');
247
+ if (howToTest)
248
+ additionalFields.howToTest = howToTest;
249
+ const predeploymentSteps = extractSection('Predeployment Steps');
250
+ if (predeploymentSteps)
251
+ additionalFields.predeploymentSteps = predeploymentSteps;
252
+ const postdeploymentSteps = extractSection('Postdeployment Steps');
253
+ if (postdeploymentSteps)
254
+ additionalFields.postdeploymentSteps = postdeploymentSteps;
255
+ const deploymentInformation = extractSection('Deployment Information');
256
+ if (deploymentInformation)
257
+ additionalFields.deploymentInformation = deploymentInformation;
258
+ return {
259
+ frontmatter,
260
+ description,
261
+ acceptanceCriteria,
262
+ additionalFields,
263
+ rawContent: content,
264
+ };
265
+ }
266
+ /**
267
+ * Escape special regex characters in a string
268
+ */
269
+ function escapeRegex(str) {
270
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
271
+ }
272
+ /**
273
+ * Convert ADO comments to a read-only markdown file
274
+ */
275
+ export function commentsToMarkdown(workItem, comments) {
276
+ const fields = workItem.fields || {};
277
+ const frontmatter = {
278
+ id: workItem.id,
279
+ title: fields['System.Title'] || '',
280
+ commentCount: comments.length,
281
+ lastSyncedAt: new Date().toISOString(),
282
+ };
283
+ let content = serializeFrontmatter(frontmatter);
284
+ content += '\n\n# Comments\n\n';
285
+ content += '> **NOTE**: This file is read-only. Comments cannot be pushed back to ADO.\n\n';
286
+ if (comments.length === 0) {
287
+ content += '_No comments on this work item._\n';
288
+ }
289
+ else {
290
+ // Sort comments by date (oldest first)
291
+ const sortedComments = [...comments].sort((a, b) => {
292
+ const dateA = new Date(a.createdDate || a.publishedDate || 0);
293
+ const dateB = new Date(b.createdDate || b.publishedDate || 0);
294
+ return dateA.getTime() - dateB.getTime();
295
+ });
296
+ sortedComments.forEach((comment, index) => {
297
+ content += '---\n\n';
298
+ content += `## Comment #${index + 1}\n`;
299
+ content += `**Author**: ${comment.createdBy?.displayName || 'Unknown'}\n`;
300
+ content += `**Date**: ${comment.createdDate || comment.publishedDate || 'Unknown'}\n\n`;
301
+ content += `${comment.text || comment.content || ''}\n\n`;
302
+ });
303
+ }
304
+ return content;
305
+ }
306
+ /**
307
+ * Build ADO patch operations from parsed markdown changes
308
+ * Only updates fields that have actually changed.
309
+ * Auto-converts HTML fields to markdown format unless skipAutoConvert is true.
310
+ *
311
+ * @param parsed - Parsed work item file
312
+ * @param currentWorkItem - Current work item from ADO
313
+ * @param skipAutoConvert - Skip automatic HTML-to-markdown conversion (default: false)
314
+ */
315
+ export function buildPatchOperations(parsed, currentWorkItem, skipAutoConvert = false) {
316
+ const operations = [];
317
+ const skippedFields = [];
318
+ const convertedFields = [];
319
+ const currentFields = currentWorkItem.fields || {};
320
+ const fieldConfig = getAdditionalFieldConfig();
321
+ // Check Title
322
+ const currentTitle = currentFields['System.Title'] || '';
323
+ if (parsed.frontmatter.title !== currentTitle) {
324
+ operations.push({
325
+ op: 'replace',
326
+ path: '/fields/System.Title',
327
+ value: parsed.frontmatter.title,
328
+ });
329
+ }
330
+ // Check Description - always try to set markdown format when writing
331
+ const currentDescription = currentFields['System.Description'] || '';
332
+ const descriptionIsHtml = isHtmlContent(currentDescription);
333
+ if (descriptionIsHtml && skipAutoConvert) {
334
+ // Skip HTML field when skipAutoConvert is true
335
+ skippedFields.push('Description (HTML in ADO - skipAutoConvert=true)');
336
+ }
337
+ else if (parsed.description !== currentDescription || descriptionIsHtml) {
338
+ // Write field value AND always try to set markdown format
339
+ operations.push({
340
+ op: currentDescription ? 'replace' : 'add',
341
+ path: '/fields/System.Description',
342
+ value: parsed.description,
343
+ });
344
+ // Always add format op - API silently ignores if not applicable
345
+ operations.push({
346
+ op: 'add',
347
+ path: '/multilineFieldsFormat/System.Description',
348
+ value: 'Markdown',
349
+ });
350
+ if (descriptionIsHtml) {
351
+ convertedFields.push('Description');
352
+ }
353
+ }
354
+ // Check Acceptance Criteria - always try to set markdown format when writing
355
+ const currentAC = currentFields['Microsoft.VSTS.Common.AcceptanceCriteria'] || '';
356
+ const acIsHtml = isHtmlContent(currentAC);
357
+ if (acIsHtml && skipAutoConvert) {
358
+ // Skip HTML field when skipAutoConvert is true
359
+ skippedFields.push('Acceptance Criteria (HTML in ADO - skipAutoConvert=true)');
360
+ }
361
+ else if (parsed.acceptanceCriteria !== currentAC || acIsHtml) {
362
+ // Write field value AND always try to set markdown format
363
+ operations.push({
364
+ op: currentAC ? 'replace' : 'add',
365
+ path: '/fields/Microsoft.VSTS.Common.AcceptanceCriteria',
366
+ value: parsed.acceptanceCriteria,
367
+ });
368
+ // Always add format op - API silently ignores if not applicable
369
+ operations.push({
370
+ op: 'add',
371
+ path: '/multilineFieldsFormat/Microsoft.VSTS.Common.AcceptanceCriteria',
372
+ value: 'Markdown',
373
+ });
374
+ if (acIsHtml) {
375
+ convertedFields.push('Acceptance Criteria');
376
+ }
377
+ }
378
+ // Helper to check and add patch operation for additional field
379
+ // Always tries to set markdown format when writing to a field
380
+ const checkAdditionalField = (localValue, fieldName, displayName) => {
381
+ const currentValue = currentFields[fieldName] || '';
382
+ const localContent = localValue || '';
383
+ const fieldIsHtml = currentValue && isHtmlContent(currentValue);
384
+ if (fieldIsHtml && skipAutoConvert) {
385
+ // Skip HTML field when skipAutoConvert is true
386
+ skippedFields.push(`${displayName} (HTML in ADO - skipAutoConvert=true)`);
387
+ return;
388
+ }
389
+ if (localContent !== currentValue || fieldIsHtml) {
390
+ // Write field value AND always try to set markdown format
391
+ operations.push({
392
+ op: currentValue ? 'replace' : 'add',
393
+ path: `/fields/${fieldName}`,
394
+ value: localContent,
395
+ });
396
+ // Always add format op - API silently ignores if not applicable
397
+ operations.push({
398
+ op: 'add',
399
+ path: `/multilineFieldsFormat/${fieldName}`,
400
+ value: 'Markdown',
401
+ });
402
+ if (fieldIsHtml) {
403
+ convertedFields.push(displayName);
404
+ }
405
+ }
406
+ };
407
+ // Check additional fields
408
+ checkAdditionalField(parsed.additionalFields.howToTest, fieldConfig.howToTest, ADDITIONAL_FIELD_DISPLAY_NAMES.howToTest);
409
+ checkAdditionalField(parsed.additionalFields.predeploymentSteps, fieldConfig.predeploymentSteps, ADDITIONAL_FIELD_DISPLAY_NAMES.predeploymentSteps);
410
+ checkAdditionalField(parsed.additionalFields.postdeploymentSteps, fieldConfig.postdeploymentSteps, ADDITIONAL_FIELD_DISPLAY_NAMES.postdeploymentSteps);
411
+ checkAdditionalField(parsed.additionalFields.deploymentInformation, fieldConfig.deploymentInformation, ADDITIONAL_FIELD_DISPLAY_NAMES.deploymentInformation);
412
+ return { operations, skippedFields, convertedFields };
413
+ }
414
+ /**
415
+ * Update the lastSyncedRevision in a markdown file content
416
+ */
417
+ export function updateSyncRevision(content, newRevision) {
418
+ // Update lastSyncedRevision in frontmatter
419
+ const updatedContent = content.replace(/lastSyncedRevision:\s*\d+/, `lastSyncedRevision: ${newRevision}`);
420
+ // Update lastSyncedAt timestamp
421
+ return updatedContent.replace(/lastSyncedAt:\s*[^\n]+/, `lastSyncedAt: ${new Date().toISOString()}`);
422
+ }
423
+ /**
424
+ * Check if a work item frontmatter indicates a new (not yet created) work item
425
+ * New work items don't have an 'id' field
426
+ */
427
+ export function isNewWorkItem(frontmatter) {
428
+ return !frontmatter.id || typeof frontmatter.id !== 'number';
429
+ }
430
+ /**
431
+ * Parse a markdown file for a NEW work item (no id required)
432
+ */
433
+ export function parseNewWorkItemMarkdown(content) {
434
+ // Extract frontmatter between ---
435
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
436
+ if (!frontmatterMatch) {
437
+ throw new Error('Invalid work item markdown file: missing YAML frontmatter');
438
+ }
439
+ const frontmatterRaw = frontmatterMatch[1];
440
+ const frontmatterData = parseFrontmatter(frontmatterRaw);
441
+ // Validate required fields for new work items
442
+ if (!frontmatterData.parent || typeof frontmatterData.parent !== 'number') {
443
+ throw new Error('New work item files require a "parent" field with the parent work item ID');
444
+ }
445
+ if (!frontmatterData.title || typeof frontmatterData.title !== 'string') {
446
+ throw new Error('New work item files require a "title" field');
447
+ }
448
+ const frontmatter = {
449
+ title: frontmatterData.title,
450
+ type: frontmatterData.type || 'User Story',
451
+ state: frontmatterData.state || 'New',
452
+ parent: frontmatterData.parent,
453
+ assignedTo: frontmatterData.assignedTo,
454
+ storyPoints: frontmatterData.storyPoints,
455
+ moscow: frontmatterData.moscow,
456
+ tags: frontmatterData.tags,
457
+ areaPath: frontmatterData.areaPath,
458
+ iterationPath: frontmatterData.iterationPath,
459
+ };
460
+ // Extract content after frontmatter
461
+ const contentAfterFrontmatter = content.slice(frontmatterMatch[0].length);
462
+ // Helper to extract a section by heading (reuse from parseWorkItemMarkdown)
463
+ const extractSection = (sectionName) => {
464
+ const regex = new RegExp(`#\\s+${escapeRegex(sectionName)}\\s*\\n([\\s\\S]*?)(?=\\n---\\n|\\n#\\s+|$)`, 'i');
465
+ const match = contentAfterFrontmatter.match(regex);
466
+ if (!match)
467
+ return undefined;
468
+ const sectionContent = match[1].trim();
469
+ if (sectionContent === `_No ${sectionName.toLowerCase()} provided._`) {
470
+ return undefined;
471
+ }
472
+ return sectionContent || undefined;
473
+ };
474
+ // Extract primary sections
475
+ let description = extractSection('Description') || '';
476
+ if (description === '_No description provided._' || description === '[Your description here]') {
477
+ description = '';
478
+ }
479
+ let acceptanceCriteria = extractSection('Acceptance Criteria') || '';
480
+ if (acceptanceCriteria === '_No acceptance criteria provided._' || acceptanceCriteria === '[Your acceptance criteria here]') {
481
+ acceptanceCriteria = '';
482
+ }
483
+ // Extract additional fields (optional)
484
+ const additionalFields = {};
485
+ const howToTest = extractSection('How to Test');
486
+ if (howToTest)
487
+ additionalFields.howToTest = howToTest;
488
+ const predeploymentSteps = extractSection('Predeployment Steps');
489
+ if (predeploymentSteps)
490
+ additionalFields.predeploymentSteps = predeploymentSteps;
491
+ const postdeploymentSteps = extractSection('Postdeployment Steps');
492
+ if (postdeploymentSteps)
493
+ additionalFields.postdeploymentSteps = postdeploymentSteps;
494
+ const deploymentInformation = extractSection('Deployment Information');
495
+ if (deploymentInformation)
496
+ additionalFields.deploymentInformation = deploymentInformation;
497
+ return {
498
+ frontmatter,
499
+ description,
500
+ acceptanceCriteria,
501
+ additionalFields,
502
+ rawContent: content,
503
+ };
504
+ }
505
+ /**
506
+ * Build ADO fields object for creating a new work item
507
+ * Inherits areaPath and iterationPath from parent work item
508
+ */
509
+ export function buildNewWorkItemFields(parsed, parentWorkItem) {
510
+ const parentFields = parentWorkItem.fields || {};
511
+ const fieldConfig = getAdditionalFieldConfig();
512
+ const fields = {
513
+ 'System.Title': parsed.frontmatter.title,
514
+ 'System.State': parsed.frontmatter.state || 'New',
515
+ };
516
+ // Set description if provided
517
+ if (parsed.description) {
518
+ fields['System.Description'] = parsed.description;
519
+ }
520
+ // Set acceptance criteria if provided
521
+ if (parsed.acceptanceCriteria) {
522
+ fields['Microsoft.VSTS.Common.AcceptanceCriteria'] = parsed.acceptanceCriteria;
523
+ }
524
+ // Story points - only set if it's a valid number (including 0)
525
+ // YAML may parse empty value as null/undefined, or string if quoted
526
+ // Using 'any' cast because YAML parser may return types not matching interface
527
+ const storyPointsValue = parsed.frontmatter.storyPoints;
528
+ if (storyPointsValue !== undefined && storyPointsValue !== null && storyPointsValue !== '') {
529
+ const storyPoints = Number(storyPointsValue);
530
+ if (!isNaN(storyPoints)) {
531
+ fields['Microsoft.VSTS.Scheduling.StoryPoints'] = storyPoints;
532
+ }
533
+ }
534
+ // MoSCoW (custom field) - only set if non-empty
535
+ const moscowValue = parsed.frontmatter.moscow;
536
+ if (moscowValue && String(moscowValue).trim() !== '') {
537
+ fields['Custom.MoSCoW'] = moscowValue;
538
+ }
539
+ // Tags - ensure it's an array with items before joining
540
+ if (Array.isArray(parsed.frontmatter.tags) && parsed.frontmatter.tags.length > 0) {
541
+ fields['System.Tags'] = parsed.frontmatter.tags.join('; ');
542
+ }
543
+ // Inherit areaPath from parent if not specified
544
+ if (parsed.frontmatter.areaPath) {
545
+ fields['System.AreaPath'] = parsed.frontmatter.areaPath;
546
+ }
547
+ else if (parentFields['System.AreaPath']) {
548
+ fields['System.AreaPath'] = parentFields['System.AreaPath'];
549
+ }
550
+ // Inherit iterationPath from parent if not specified
551
+ if (parsed.frontmatter.iterationPath) {
552
+ fields['System.IterationPath'] = parsed.frontmatter.iterationPath;
553
+ }
554
+ else if (parentFields['System.IterationPath']) {
555
+ fields['System.IterationPath'] = parentFields['System.IterationPath'];
556
+ }
557
+ // Additional fields
558
+ if (parsed.additionalFields.howToTest) {
559
+ fields[fieldConfig.howToTest] = parsed.additionalFields.howToTest;
560
+ }
561
+ if (parsed.additionalFields.predeploymentSteps) {
562
+ fields[fieldConfig.predeploymentSteps] = parsed.additionalFields.predeploymentSteps;
563
+ }
564
+ if (parsed.additionalFields.postdeploymentSteps) {
565
+ fields[fieldConfig.postdeploymentSteps] = parsed.additionalFields.postdeploymentSteps;
566
+ }
567
+ if (parsed.additionalFields.deploymentInformation) {
568
+ fields[fieldConfig.deploymentInformation] = parsed.additionalFields.deploymentInformation;
569
+ }
570
+ return fields;
571
+ }
572
+ /**
573
+ * Generate a new work item template markdown file
574
+ */
575
+ export function generateNewWorkItemTemplate(parentId, parentTitle, project, workItemType = 'User Story') {
576
+ // Only include required fields - optional fields omitted to avoid empty value issues
577
+ const frontmatter = {
578
+ title: 'New User Story Title',
579
+ type: workItemType,
580
+ state: 'New',
581
+ parent: parentId,
582
+ };
583
+ let content = serializeFrontmatter(frontmatter);
584
+ content += '\n';
585
+ content += `<!-- Optional frontmatter fields (add to YAML above if needed):\n`;
586
+ content += `storyPoints: 3\n`;
587
+ content += `moscow: Should Have\n`;
588
+ content += `tags:\n`;
589
+ content += `- tag1\n`;
590
+ content += `- tag2\n`;
591
+ content += `-->\n\n`;
592
+ content += `> Parent: **#${parentId}** - ${parentTitle}\n`;
593
+ content += `> Project: ${project}\n`;
594
+ content += '\n# Description\n\n';
595
+ content += '[Your description here]\n\n';
596
+ content += '---\n\n';
597
+ content += '# Acceptance Criteria\n\n';
598
+ content += '[Your acceptance criteria here]\n';
599
+ return content;
600
+ }
601
+ /**
602
+ * Update a new work item file after creation with the assigned ID
603
+ * Converts it from a "new" file to a synced file with proper frontmatter
604
+ */
605
+ export function convertNewFileToSynced(content, workItemId, revision, url) {
606
+ // Parse the existing content
607
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
608
+ if (!frontmatterMatch) {
609
+ throw new Error('Invalid work item markdown file: missing YAML frontmatter');
610
+ }
611
+ const frontmatterRaw = frontmatterMatch[1];
612
+ const frontmatterData = parseFrontmatter(frontmatterRaw);
613
+ const contentAfterFrontmatter = content.slice(frontmatterMatch[0].length);
614
+ // Build new frontmatter with id and sync metadata
615
+ const newFrontmatter = {
616
+ id: workItemId,
617
+ title: frontmatterData.title || '',
618
+ type: frontmatterData.type || 'User Story',
619
+ state: frontmatterData.state || 'New',
620
+ url: url,
621
+ };
622
+ // Copy optional fields
623
+ if (frontmatterData.assignedTo)
624
+ newFrontmatter.assignedTo = frontmatterData.assignedTo;
625
+ if (frontmatterData.storyPoints)
626
+ newFrontmatter.storyPoints = frontmatterData.storyPoints;
627
+ if (frontmatterData.parent)
628
+ newFrontmatter.parent = frontmatterData.parent;
629
+ if (frontmatterData.moscow)
630
+ newFrontmatter.moscow = frontmatterData.moscow;
631
+ if (frontmatterData.tags && frontmatterData.tags.length > 0)
632
+ newFrontmatter.tags = frontmatterData.tags;
633
+ if (frontmatterData.areaPath)
634
+ newFrontmatter.areaPath = frontmatterData.areaPath;
635
+ if (frontmatterData.iterationPath)
636
+ newFrontmatter.iterationPath = frontmatterData.iterationPath;
637
+ // Add sync metadata
638
+ newFrontmatter.lastSyncedRevision = revision;
639
+ newFrontmatter.lastSyncedAt = new Date().toISOString();
640
+ // Remove the "Parent:" and "Project:" note lines from the content
641
+ let cleanedContent = contentAfterFrontmatter
642
+ .replace(/>\s*Parent:.*\n/i, '')
643
+ .replace(/>\s*Project:.*\n/i, '');
644
+ return serializeFrontmatter(newFrontmatter) + cleanedContent;
645
+ }
646
+ //# sourceMappingURL=markdown-serializer.js.map