@slorenzot/mcp-azure 2.4.5 → 2.6.1

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.
@@ -0,0 +1,115 @@
1
+ # Guía de Codificación URL para Azure DevOps MCP
2
+
3
+ ## ¿Cuándo se necesita codificar el nombre del proyecto?
4
+
5
+ El MCP maneja automáticamente la codificación URL. Aquí están las reglas:
6
+
7
+ ### Operaciones que REQUIEREN codificación (automática):
8
+
9
+ 1. **📎 Adjuntos de archivos**
10
+ - `ado_upload_attachment()`
11
+ - `ado_add_attachment()`
12
+ - Razón: Estas funciones construyen URLs REST manualmente
13
+
14
+ ### Operaciones que NO requieren codificación (automática):
15
+
16
+ 1. **📋 Work Items**
17
+ - Crear/actualizar Work Items
18
+ - Consultas WIQL
19
+ - Tipos de Work Item
20
+
21
+ 2. **🌐 Git Operations**
22
+ - Pull Requests
23
+ - Repositorios
24
+ - Branches
25
+
26
+ 3. **📅 Proyecto Configuration**
27
+ - Iteraciones/Sprints
28
+ - Áreas
29
+
30
+ ## Ejemplos de Nombres de Proyecto
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "azure-devops": {
36
+ "env": {
37
+ "AZURE_DEVOPS_PROJECT": "Mi Proyecto" // Se codificará automáticamente a "Mi%20Proyecto"
38
+ }
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ## Mensajes del Sistema
45
+
46
+ Cuando el MCP se inicia, verás mensajes como:
47
+
48
+ ```
49
+ ✅ Conexión establecida exitosamente:
50
+ - Organización: https://dev.azure.com/mi-org
51
+ - Proyecto: "Mi Proyecto"
52
+ - Fuente: .mcp.json
53
+ - Requiere codificación URL: SÍ
54
+ - Versión codificada: "Mi%20Proyecto"
55
+ - NOTA: Solo se usa en operaciones REST manuales (adjuntos)
56
+ ```
57
+
58
+ ## Configuración Recomendada
59
+
60
+ ### Opción 1: Archivo .mcp.json (Recomendado)
61
+
62
+ ```json
63
+ {
64
+ "mcpServers": {
65
+ "azure-devops": {
66
+ "command": "npx",
67
+ "args": ["-y", "@slorenzot/mcp-azure"],
68
+ "env": {
69
+ "AZURE_DEVOPS_ORG": "https://dev.azure.com/tu-organizacion",
70
+ "AZURE_DEVOPS_PAT": "tu-pat-aqui",
71
+ "AZURE_DEVOPS_PROJECT": "Nombre con espacios"
72
+ }
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ ### Opción 2: Variables de Entorno (Fallback)
79
+
80
+ ```bash
81
+ export AZURE_DEVOPS_ORG="https://dev.azure.com/tu-organizacion"
82
+ export AZURE_DEVOPS_PAT="tu-pat-aqui"
83
+ export AZURE_DEVOPS_PROJECT="Nombre con espacios"
84
+ ```
85
+
86
+ ## Solución de Problemas
87
+
88
+ ### Errores Comunes
89
+
90
+ 1. **"No se encontró configuración"**
91
+ - Crea un archivo .mcp.json o configura variables de entorno
92
+
93
+ 2. **"Proyecto requiere codificación URL"**
94
+ - Es normal, el MCP lo maneja automáticamente
95
+
96
+ 3. **"Timeout de conexión"**
97
+ - Verifica tu conexión a internet y el PAT
98
+
99
+ ### Jerarquía de Configuración
100
+
101
+ El MCP busca configuración en este orden:
102
+
103
+ 1. **.mcp.json** en directorio actual
104
+ 2. **.mcp.json** en directorio padre
105
+ 3. **.mcp.json** en directorio del script
106
+ 4. **.mcp.json** en home del usuario
107
+ 5. **Variables de entorno** (fallback)
108
+ 6. **Error** si nada está disponible
109
+
110
+ ## Notas Técnicas
111
+
112
+ - La codificación URL solo se aplica a operaciones REST manuales
113
+ - El SDK de Azure DevOps maneja la codificación automáticamente
114
+ - Los nombres de proyecto con espacios, #, %, u otros caracteres especiales se codifican automáticamente
115
+ - El logging muestra exactamente cuándo y cómo se aplica la codificación
@@ -0,0 +1,512 @@
1
+ # PRD: Corrección de Concurrencia en add_attachment
2
+
3
+ ## 📋 Resumen Ejecutivo
4
+
5
+ Corregir el problema de concurrencia en `ado_add_attachment` que causa el error **TF26071** cuando múltiples intentos de agregar adjuntos ocurren simultáneamente al mismo Work Item en Azure DevOps.
6
+
7
+ ## 🐛 Problema
8
+
9
+ ### Error Reportado
10
+ ```
11
+ TF26071: This work item has been changed by someone else since you opened it.
12
+ ```
13
+
14
+ ### Causa Raíz
15
+ Cuando múltiples llamadas a `ado_add_attachment` golpean el mismo `workItemId` en paralelo, Azure DevOps rechaza los cambios debido a conflicto de revisión. Cada `add_attachment` incrementa la revisión del Work Item.
16
+
17
+ ### Flujo del Problema
18
+ 1. **Usuario A** ejecuta `add_attachment` para WI #123 → Recupera WI versión 10
19
+ 2. **Usuario B** ejecuta `add_attachment` para WI #123 → Recupera WI versión 10
20
+ 3. **Usuario A** intenta actualizar WI #123 con adjunto → Azure DevOps acepta (versión 10→11)
21
+ 4. **Usuario B** intenta actualizar WI #123 con adjunto → **TF26071**: Versión 10 ya no es la actual
22
+ 5. **Resultado**: El segundo adjunto falla con error confuso para el usuario
23
+
24
+ ### Impacto
25
+ - ❌ **Pérdida de funcionalidad**: Los segundos intentos fallan
26
+ - ❌ **Experiencia de usuario**: Errores técnicos no comprensibles
27
+ - ❌ **Inconsistencia**: A veces funciona, a veces falla (race condition)
28
+ - ❌ **Sin workaround documentado**: Los usuarios no saben cómo manejarlo
29
+
30
+ ## ✅ Solución Propuesta
31
+
32
+ ### Estrategia Principal: Sistema de Cola por Work Item
33
+
34
+ Implementar una cola interna por Work Item que asegure que las operaciones de adjuntos sean **secuenciales** por WI, permitiendo paralelismo entre diferentes Work Items.
35
+
36
+ ### Arquitectura de la Solución
37
+
38
+ #### 1. Sistema de Cola por Work Item
39
+
40
+ ```typescript
41
+ interface AttachmentQueue {
42
+ // Mapa de colas por Work Item ID
43
+ queues: Map<number, Promise<AttachmentResult>>;
44
+
45
+ // Agregar operación a la cola del WI
46
+ addToQueue(workItemId: number, operation: () => Promise<AttachmentResult>): Promise<AttachmentResult>;
47
+
48
+ // Verificar si hay cola activa para el WI
49
+ hasActiveQueue(workItemId: number): boolean;
50
+ }
51
+ ```
52
+
53
+ #### 2. Retry Automático con Re-fetch
54
+
55
+ ```typescript
56
+ async function addAttachmentWithRetry(
57
+ workItemId: number,
58
+ attachmentData: AttachmentData,
59
+ maxRetries: number = 3
60
+ ): Promise<AttachmentResult> {
61
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
62
+ try {
63
+ // 1. Recuperar versión actual del WI (importante para cada intento)
64
+ const currentWI = await getWorkItem(workItemId);
65
+
66
+ // 2. Agregar adjunto usando la versión actual
67
+ return await addAttachment(workItemId, attachmentData);
68
+
69
+ } catch (error: any) {
70
+ // 3. Verificar si es error de concurrencia TF26071
71
+ if (isConcurrencyError(error) && attempt < maxRetries) {
72
+ // 4. Esperar backoff exponencial
73
+ await sleep(Math.pow(2, attempt) * 1000); // 2s, 4s, 8s
74
+
75
+ // 5. Re-fetch del WI para obtener la nueva versión
76
+ continue;
77
+ }
78
+
79
+ // 6. Si no es TF26071 o se agotaron reintentos, lanzar error
80
+ throw error;
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ #### 3. Integración en `ado_add_attachment`
87
+
88
+ ```typescript
89
+ server.tool(
90
+ "ado_add_attachment",
91
+ "Agrega un adjunto a un Work Item existente con manejo de concurrencia",
92
+ // ... parámetros existentes ...
93
+ async ({ workItemId, filePath, attachmentUrl, comment, name }) => {
94
+ validateConnection();
95
+ validateProject();
96
+
97
+ // Verificar si hay cola activa para este WI
98
+ if (attachmentQueue.hasActiveQueue(workItemId)) {
99
+ console.error(`⏳ Encolando adjunto para WI #${workItemId} (ya hay operación en curso)`);
100
+ return await attachmentQueue.addToQueue(workItemId, () =>
101
+ addAttachmentWithRetry(workItemId, { filePath, attachmentUrl, comment, name })
102
+ );
103
+ }
104
+
105
+ // No hay cola activa, ejecutar directamente con retry
106
+ console.error(`🚀 Ejecutando adjunto para WI #${workItemId}`);
107
+ return await addAttachmentWithRetry(workItemId, { filePath, attachmentUrl, comment, name });
108
+ }
109
+ );
110
+ ```
111
+
112
+ ## 🔧 Implementación Detallada
113
+
114
+ ### Paso 1: Funciones de Utilidad
115
+
116
+ ```typescript
117
+ // Detectar si el error es TF26071 (concurrencia)
118
+ function isConcurrencyError(error: any): boolean {
119
+ const errorString = error?.message || error?.toString() || '';
120
+ return errorString.includes('TF26071') ||
121
+ errorString.includes('changed by someone else');
122
+ }
123
+
124
+ // Función de sleep para backoff
125
+ function sleep(ms: number): Promise<void> {
126
+ return new Promise(resolve => setTimeout(resolve, ms));
127
+ }
128
+
129
+ // Obtener Work Item actualizado (para re-fetch en retry)
130
+ async function getWorkItem(workItemId: number): Promise<witInterfaces.WorkItem> {
131
+ const api = await getWitApi();
132
+ return await safeApiCall(
133
+ () => api.getWorkItem(workItemId, undefined, undefined, witInterfaces.WorkItemExpand.All),
134
+ `Error al obtener Work Item #${workItemId}`
135
+ );
136
+ }
137
+ ```
138
+
139
+ ### Paso 2: Implementación de Cola
140
+
141
+ ```typescript
142
+ // Sistema de cola por Work Item
143
+ class AttachmentQueueManager {
144
+ private queues: Map<number, Promise<any>> = new Map();
145
+
146
+ async addToQueue<T>(
147
+ workItemId: number,
148
+ operation: () => Promise<T>
149
+ ): Promise<T> {
150
+ // Si ya hay cola para este WI, esperar
151
+ const existingQueue = this.queues.get(workItemId);
152
+ if (existingQueue) {
153
+ console.error(`⏳ Esperando cola para WI #${workItemId}...`);
154
+ try {
155
+ await existingQueue;
156
+ } catch {
157
+ // Si la operación anterior falló, proceder con la nuestra
158
+ }
159
+ }
160
+
161
+ // Crear nueva cola para este WI
162
+ const queue = operation().finally(() => {
163
+ // Limpiar cola cuando termine
164
+ this.queues.delete(workItemId);
165
+ });
166
+
167
+ this.queues.set(workItemId, queue);
168
+ return queue;
169
+ }
170
+
171
+ hasActiveQueue(workItemId: number): boolean {
172
+ return this.queues.has(workItemId);
173
+ }
174
+ }
175
+
176
+ // Instancia global del gestor de colas
177
+ const attachmentQueue = new AttachmentQueueManager();
178
+ ```
179
+
180
+ ### Paso 3: Función Principal con Retry
181
+
182
+ ```typescript
183
+ // Función principal con retry y re-fetch
184
+ async function addAttachmentWithRetry(
185
+ api: witApi.IWorkItemTrackingApi,
186
+ workItemId: number,
187
+ attachmentData: {
188
+ filePath?: string;
189
+ attachmentUrl?: string;
190
+ comment?: string;
191
+ name?: string;
192
+ },
193
+ maxRetries: number = 3
194
+ ): Promise<{ content: any }> {
195
+ let lastError: any = null;
196
+
197
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
198
+ try {
199
+ console.error(`🔄 Intento ${attempt}/${maxRetries} para adjuntar a WI #${workItemId}`);
200
+
201
+ // 1. Recuperar versión actual del WI (CRÍTICO para cada intento)
202
+ const currentWI = await getWorkItem(workItemId);
203
+ console.error(`📄 WI #${workItemId} versión actual: ${currentWI.rev}`);
204
+
205
+ // 2. Procesar adjunto
206
+ let attachmentId: string | undefined;
207
+ let fileName: string | undefined;
208
+ let attachmentLinkUrl: string | undefined;
209
+
210
+ if (attachmentData.filePath) {
211
+ // Subir archivo nuevo
212
+ fileName = attachmentData.name || path.basename(attachmentData.filePath);
213
+ const attachment = await uploadAttachmentRest(attachmentData.filePath, fileName);
214
+ attachmentId = attachment.id;
215
+ attachmentLinkUrl = attachment.url;
216
+ } else if (attachmentData.attachmentUrl) {
217
+ // Usar adjunto existente
218
+ attachmentId = attachmentData.attachmentUrl.split('/attachments/')[1]?.split('?')[0];
219
+ fileName = attachmentData.name || "Archivo adjunto";
220
+ const baseUrl = currentOrg.endsWith("/") ? currentOrg.slice(0, -1) : currentOrg;
221
+ const encodedProject = getEncodedProject(currentProject);
222
+ attachmentLinkUrl = `${baseUrl}/${encodedProject}/_apis/wit/attachments/${attachmentId}`;
223
+ } else {
224
+ throw new Error("Debe proporcionar filePath o attachmentUrl");
225
+ }
226
+
227
+ // 3. Vincular adjunto al WI usando la versión ACTUAL
228
+ const patchDocument: VSSInterfaces.JsonPatchOperation[] = [
229
+ {
230
+ op: VSSInterfaces.Operation.Add,
231
+ path: "/relations/-",
232
+ value: {
233
+ rel: "AttachedFile",
234
+ url: attachmentLinkUrl,
235
+ attributes: {
236
+ name: fileName,
237
+ comment: attachmentData.comment || "",
238
+ },
239
+ },
240
+ },
241
+ ];
242
+
243
+ // 4. Actualizar WI con la versión actual
244
+ const updatedWI = await api.updateWorkItem(
245
+ null, // project
246
+ patchDocument,
247
+ workItemId
248
+ );
249
+
250
+ console.error(`✅ Adjunto agregado exitosamente a WI #${workItemId}`);
251
+
252
+ return {
253
+ content: [{
254
+ type: "text",
255
+ text: `Adjunto agregado exitosamente al Work Item #${workItemId}\n- Nombre: ${fileName}\n- URL: ${attachmentLinkUrl}`,
256
+ }],
257
+ };
258
+
259
+ } catch (error: any) {
260
+ lastError = error;
261
+ console.error(`❌ Error en intento ${attempt}:`, error.message);
262
+
263
+ // Verificar si es error de concurrencia y quedan reintentos
264
+ if (isConcurrencyError(error) && attempt < maxRetries) {
265
+ const backoffTime = Math.pow(2, attempt) * 1000;
266
+ console.error(`⏳ Error TF26071 detectado. Esperando ${backoffTime}ms antes de reintentar...`);
267
+ await sleep(backoffTime);
268
+ console.error(`🔄 Reintentando con nueva versión del WI...`);
269
+ continue;
270
+ }
271
+
272
+ // Si no es TF26071 o se agotaron reintentos, lanzar error
273
+ throw error;
274
+ }
275
+ }
276
+
277
+ // Si se agotaron todos los reintentos, lanzar el último error
278
+ throw lastError;
279
+ }
280
+ ```
281
+
282
+ ### Paso 4: Actualización de `ado_add_attachment`
283
+
284
+ ```typescript
285
+ server.tool(
286
+ "ado_add_attachment",
287
+ "Agrega un adjunto a un Work Item existente. Maneja automáticamente concurrencia cuando múltiples adjuntos se agregan al mismo WI.",
288
+ {
289
+ workItemId: z.number().describe("ID del Work Item"),
290
+ filePath: z.string().optional().describe("Ruta del archivo a subir (opcional si se usa attachmentUrl)"),
291
+ attachmentUrl: z.string().optional().describe("URL de un adjunto ya subido (opcional si se usa filePath)"),
292
+ comment: z.string().optional().describe("Comentario para el adjunto"),
293
+ name: z.string().optional().describe("Nombre del archivo (si no se especifica, usa el nombre del archivo original)"),
294
+ },
295
+ async ({ workItemId, filePath, attachmentUrl, comment, name }) => {
296
+ validateConnection();
297
+ validateProject();
298
+
299
+ const api = await getWitApi();
300
+
301
+ // VERIFICAR CONCURRENCIA: Usar sistema de cola
302
+ if (attachmentQueue.hasActiveQueue(workItemId)) {
303
+ console.error(`⏳ Encolando adjunto para WI #${workItemId} (ya hay operación en curso)`);
304
+ return await attachmentQueue.addToQueue(workItemId, () =>
305
+ addAttachmentWithRetry(api, workItemId, { filePath, attachmentUrl, comment, name })
306
+ );
307
+ }
308
+
309
+ // Sin concurrencia: ejecutar directamente
310
+ console.error(`🚀 Ejecutando adjunto para WI #${workItemId}`);
311
+ return await attachmentQueue.addToQueue(workItemId, () =>
312
+ addAttachmentWithRetry(api, workItemId, { filePath, attachmentUrl, comment, name })
313
+ );
314
+ }
315
+ );
316
+ ```
317
+
318
+ ## 📊 Casos de Uso
319
+
320
+ ### Caso 1: Adjunto Único (sin concurrencia)
321
+ ```
322
+ Usuario: add_attachment(123, "archivo.pdf")
323
+ Resultado: ✅ Adjunto agregado en primer intento
324
+ Logs:
325
+ 🚀 Ejecutando adjunto para WI #123
326
+ 📄 WI #123 versión actual: 10
327
+ ✅ Adjunto agregado exitosamente a WI #123
328
+ ```
329
+
330
+ ### Caso 2: Concurrencia (2 adjuntos simultáneos)
331
+ ```
332
+ Usuario A (hilo 1): add_attachment(123, "archivo1.pdf")
333
+ Usuario B (hilo 2): add_attachment(123, "archivo2.pdf")
334
+
335
+ Resultado:
336
+ Hilo 1: ✅ Adjunto agregado en primer intento
337
+ Hilo 2: ⏳ Encolado, espera a que termine hilo 1
338
+ Hilo 2: ✅ Adjunto agregado después de que hilo 1 terminó
339
+
340
+ Logs:
341
+ Hilo 1: 🚀 Ejecutando adjunto para WI #123
342
+ Hilo 2: ⏳ Encolando adjunto para WI #123 (ya hay operación en curso)
343
+ Hilo 1: 📄 WI #123 versión actual: 10
344
+ Hilo 1: ✅ Adjunto agregado exitosamente a WI #123
345
+ Hilo 2: 📄 WI #123 versión actual: 11
346
+ Hilo 2: ✅ Adjunto agregado exitosamente a WI #123
347
+ ```
348
+
349
+ ### Caso 3: Concurrencia con TF26071 (3 adjuntos simultáneos)
350
+ ```
351
+ Usuario A, B, C: add_attachment(123, "archivoX.pdf")
352
+
353
+ Resultado:
354
+ Hilo 1: ✅ Adjunto agregado (versión 10→11)
355
+ Hilo 2: ⏳ Encolado, espera hilo 1
356
+ Hilo 3: ⏳ Encolado, espera hilo 1
357
+ Hilo 2: 🔄 Intento 1 → TF26071 → Reintenta
358
+ Hilo 2: 📄 WI #123 versión actual: 11
359
+ Hilo 2: ✅ Adjunto agregado (versión 11→12)
360
+ Hilo 3: 🔄 Intento 1 → TF26071 → Reintenta
361
+ Hilo 3: 📄 WI #123 versión actual: 12
362
+ Hilo 3: ✅ Adjunto agregado (versión 12→13)
363
+ ```
364
+
365
+ ## ✅ Beneficios
366
+
367
+ ### Para Usuarios
368
+ - ✅ **Sin errores TF26071**: Manejo automático de concurrencia
369
+ - ✅ **Transparencia**: El sistema maneja los reintentos automáticamente
370
+ - ✅ **Predictibilidad**: Los adjuntos siempre se agregan, eventualmente
371
+ - ✅ **Mejor experiencia**: Sin necesidad de reintentar manualmente
372
+
373
+ ### Para Desarrolladores
374
+ - ✅ **Código limpio**: Lógica de concurrencia encapsulada
375
+ - ✅ **Reutilizable**: Sistema de cola puede usarse para otras operaciones
376
+ - ✅ **Debuggable**: Logs claros de lo que está pasando
377
+ - ✅ **Testable**: Lógica separada en funciones pequeñas
378
+
379
+ ## 🧪 Testing
380
+
381
+ ### Test Unitarios
382
+
383
+ ```typescript
384
+ // Test: Detección de error TF26071
385
+ describe('isConcurrencyError', () => {
386
+ it('debería detectar error TF26071', () => {
387
+ const error = new Error('TF26071: This work item has been changed by someone else since you opened it.');
388
+ expect(isConcurrencyError(error)).toBe(true);
389
+ });
390
+
391
+ it('no debería detectar otros errores', () => {
392
+ const error = new Error('Network error');
393
+ expect(isConcurrencyError(error)).toBe(false);
394
+ });
395
+ });
396
+
397
+ // Test: Sistema de cola
398
+ describe('AttachmentQueueManager', () => {
399
+ it('debería encolar operaciones concurrentes al mismo WI', async () => {
400
+ const queue = new AttachmentQueueManager();
401
+ const results: string[] = [];
402
+
403
+ // Simular 3 operaciones concurrentes al mismo WI
404
+ const op1 = queue.addToQueue(123, async () => { await sleep(100); return 'A'; });
405
+ const op2 = queue.addToQueue(123, async () => { await sleep(50); return 'B'; });
406
+ const op3 = queue.addToQueue(123, async () => { await sleep(50); return 'C'; });
407
+
408
+ results.push(await op1, await op2, await op3);
409
+ expect(results).toEqual(['A', 'B', 'C']);
410
+ });
411
+ });
412
+ ```
413
+
414
+ ### Test de Integración
415
+
416
+ ```typescript
417
+ // Test: Retry automático con concurrencia simulada
418
+ describe('addAttachmentWithRetry', () => {
419
+ it('debería reintentar automáticamente al recibir TF26071', async () => {
420
+ let callCount = 0;
421
+
422
+ // Mock de API que falla 2 veces con TF26071, luego tiene éxito
423
+ const mockApi = {
424
+ updateWorkItem: jest.fn().mockImplementation(async () => {
425
+ callCount++;
426
+ if (callCount <= 2) {
427
+ throw new Error('TF26071: This work item has been changed by someone else since you opened it.');
428
+ }
429
+ return { id: 123, rev: 10 + callCount };
430
+ })
431
+ } as any;
432
+
433
+ const result = await addAttachmentWithRetry(mockApi, 123, { filePath: 'test.pdf' });
434
+
435
+ expect(mockApi.updateWorkItem).toHaveBeenCalledTimes(3);
436
+ expect(callCount).toBe(3);
437
+ expect(result).toBeDefined();
438
+ });
439
+ });
440
+ ```
441
+
442
+ ## 🚀 Plan de Implementación
443
+
444
+ ### Fase 1: Implementación Core (Sprint 1)
445
+ - [ ] Funciones de utilidad: `isConcurrencyError`, `sleep`, `getWorkItem`
446
+ - [ ] Clase `AttachmentQueueManager`
447
+ - [ ] Función `addAttachmentWithRetry` con lógica de retry
448
+ - [ ] Tests unitarios para nuevas funciones
449
+
450
+ ### Fase 2: Integración (Sprint 1)
451
+ - [ ] Actualizar `ado_add_attachment` para usar sistema de cola
452
+ - [ ] Agregar logs informativos de concurrencia
453
+ - [ ] Tests de integración end-to-end
454
+
455
+ ### Fase 3: Documentación (Sprint 1)
456
+ - [ ] Actualizar README.md con manejo de concurrencia
457
+ - [ ] Agregar ejemplos de uso con concurrencia
458
+ - [ ] Documentar comportamientos esperados
459
+
460
+ ## 📝 Notas Técnicas
461
+
462
+ ### Consideraciones de Performance
463
+ - **Cola por WI**: Solo serializa operaciones del mismo WI, permite paralelismo entre diferentes WIs
464
+ - **Backoff exponencial**: Evita sobrecargar Azure DevOps con reintentos agresivos
465
+ - **Re-fetch eficiente**: Solo recupera datos necesarios del WI, no campos completos
466
+
467
+ ### Compatibilidad hacia atrás
468
+ - ✅ **Sin breaking changes**: Comportamiento automático y transparente
469
+ - ✅ **Opcional**: Si no hay concurrencia, funciona igual que antes
470
+ - ✅ **Configurable**: `maxRetries` puede ajustarse por usuario o config
471
+
472
+ ### Manejo de Edge Cases
473
+ - ✅ **Timeout de colas**: Las colas no deben quedarse esperando para siempre
474
+ - ✅ **Error en operación anterior**: Si la op anterior falló, la siguiente debe proceder
475
+ - ✅ **Limpieza de colas**: Las colas se limpian automáticamente cuando terminan
476
+
477
+ ## 🎯 Criterios de Éxito
478
+
479
+ ### Funcionales
480
+ - ✅ Los adjuntos se agregan exitosamente incluso con concurrencia
481
+ - ✅ No se observan más errores TF26071 en producción
482
+ - ✅ El sistema maneja automáticamente los reintentos
483
+ - ✅ Las operaciones a diferentes WIs se ejecutan en paralelo
484
+
485
+ ### No Funcionales
486
+ - ✅ Latencia aceptable (< 5s adicional con retry)
487
+ - ✅ Sin memory leaks en sistema de cola
488
+ - ✅ Logs claros y accionables
489
+ - ✅ Tests con > 80% de cobertura
490
+
491
+ ## 📞 Risk Assessment
492
+
493
+ ### Riesgos
494
+ | Riesgo | Probabilidad | Impacto | Mitigación |
495
+ |---------|-------------|-----------|-------------|
496
+ | Retry infinito con TF26071 persistente | Baja | Alta | Límite de 3 reintentos por defecto |
497
+ | Memory leak en colas no limpiadas | Baja | Media | Limpieza automática en `finally()` |
498
+ | Performance degradation con muchos retries | Media | Baja | Backoff exponencial y límite de reintentos |
499
+
500
+ ## 📊 Estimación de Esfuerzo
501
+
502
+ ### Complejidad Técnica: Media
503
+ - Implementación de sistema de cola: 2-3 días
504
+ - Lógica de retry con re-fetch: 1-2 días
505
+ - Integración y testing: 2-3 días
506
+ - Documentación: 1 día
507
+
508
+ **Total estimado**: 6-9 días de desarrollo
509
+
510
+ ---
511
+
512
+ *Este PRD establece la solución completa para el problema de concurrencia en `ado_add_attachment`, eliminando los errores TF26071 y proporcionando una experiencia de usuario robusta y transparente.*