@probelabs/probe 0.6.0-rc100

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 (115) hide show
  1. package/README.md +583 -0
  2. package/bin/.gitkeep +0 -0
  3. package/bin/probe +158 -0
  4. package/bin/probe-binary +0 -0
  5. package/build/agent/ProbeAgent.d.ts +199 -0
  6. package/build/agent/ProbeAgent.js +1486 -0
  7. package/build/agent/acp/README.md +347 -0
  8. package/build/agent/acp/connection.js +237 -0
  9. package/build/agent/acp/connection.test.js +311 -0
  10. package/build/agent/acp/examples/simple-client.js +212 -0
  11. package/build/agent/acp/examples/tool-lifecycle.js +230 -0
  12. package/build/agent/acp/final-test.js +173 -0
  13. package/build/agent/acp/index.js +5 -0
  14. package/build/agent/acp/integration.test.js +385 -0
  15. package/build/agent/acp/manual-test.js +410 -0
  16. package/build/agent/acp/protocol-test.js +190 -0
  17. package/build/agent/acp/server.js +448 -0
  18. package/build/agent/acp/server.test.js +371 -0
  19. package/build/agent/acp/test-runner.js +216 -0
  20. package/build/agent/acp/test-utils/README.md +315 -0
  21. package/build/agent/acp/test-utils/acp-tester.js +484 -0
  22. package/build/agent/acp/test-utils/mock-acp-client.js +434 -0
  23. package/build/agent/acp/tools.js +368 -0
  24. package/build/agent/acp/tools.test.js +334 -0
  25. package/build/agent/acp/types.js +218 -0
  26. package/build/agent/acp/types.test.js +327 -0
  27. package/build/agent/appTracer.js +360 -0
  28. package/build/agent/fileSpanExporter.js +169 -0
  29. package/build/agent/index.js +7426 -0
  30. package/build/agent/mcp/client.js +338 -0
  31. package/build/agent/mcp/config.js +313 -0
  32. package/build/agent/mcp/index.js +64 -0
  33. package/build/agent/mcp/xmlBridge.js +371 -0
  34. package/build/agent/mockProvider.js +53 -0
  35. package/build/agent/probeTool.js +257 -0
  36. package/build/agent/schemaUtils.js +1726 -0
  37. package/build/agent/simpleTelemetry.js +267 -0
  38. package/build/agent/telemetry.js +225 -0
  39. package/build/agent/tokenCounter.js +395 -0
  40. package/build/agent/tools.js +163 -0
  41. package/build/cli.js +49 -0
  42. package/build/delegate.js +267 -0
  43. package/build/directory-resolver.js +237 -0
  44. package/build/downloader.js +750 -0
  45. package/build/extract.js +149 -0
  46. package/build/index.js +70 -0
  47. package/build/mcp/index.js +514 -0
  48. package/build/mcp/index.ts +608 -0
  49. package/build/query.js +116 -0
  50. package/build/search.js +247 -0
  51. package/build/tools/common.js +410 -0
  52. package/build/tools/index.js +40 -0
  53. package/build/tools/langchain.js +88 -0
  54. package/build/tools/system-message.js +121 -0
  55. package/build/tools/vercel.js +271 -0
  56. package/build/utils/file-lister.js +193 -0
  57. package/build/utils.js +128 -0
  58. package/cjs/agent/ProbeAgent.cjs +5829 -0
  59. package/cjs/index.cjs +6217 -0
  60. package/cjs/package.json +3 -0
  61. package/index.d.ts +401 -0
  62. package/package.json +114 -0
  63. package/scripts/postinstall.js +172 -0
  64. package/src/agent/ProbeAgent.d.ts +199 -0
  65. package/src/agent/ProbeAgent.js +1486 -0
  66. package/src/agent/acp/README.md +347 -0
  67. package/src/agent/acp/connection.js +237 -0
  68. package/src/agent/acp/connection.test.js +311 -0
  69. package/src/agent/acp/examples/simple-client.js +212 -0
  70. package/src/agent/acp/examples/tool-lifecycle.js +230 -0
  71. package/src/agent/acp/final-test.js +173 -0
  72. package/src/agent/acp/index.js +5 -0
  73. package/src/agent/acp/integration.test.js +385 -0
  74. package/src/agent/acp/manual-test.js +410 -0
  75. package/src/agent/acp/protocol-test.js +190 -0
  76. package/src/agent/acp/server.js +448 -0
  77. package/src/agent/acp/server.test.js +371 -0
  78. package/src/agent/acp/test-runner.js +216 -0
  79. package/src/agent/acp/test-utils/README.md +315 -0
  80. package/src/agent/acp/test-utils/acp-tester.js +484 -0
  81. package/src/agent/acp/test-utils/mock-acp-client.js +434 -0
  82. package/src/agent/acp/tools.js +368 -0
  83. package/src/agent/acp/tools.test.js +334 -0
  84. package/src/agent/acp/types.js +218 -0
  85. package/src/agent/acp/types.test.js +327 -0
  86. package/src/agent/appTracer.js +360 -0
  87. package/src/agent/fileSpanExporter.js +169 -0
  88. package/src/agent/index.js +813 -0
  89. package/src/agent/mcp/client.js +338 -0
  90. package/src/agent/mcp/config.js +313 -0
  91. package/src/agent/mcp/index.js +64 -0
  92. package/src/agent/mcp/xmlBridge.js +371 -0
  93. package/src/agent/mockProvider.js +53 -0
  94. package/src/agent/probeTool.js +257 -0
  95. package/src/agent/schemaUtils.js +1726 -0
  96. package/src/agent/simpleTelemetry.js +267 -0
  97. package/src/agent/telemetry.js +225 -0
  98. package/src/agent/tokenCounter.js +395 -0
  99. package/src/agent/tools.js +163 -0
  100. package/src/cli.js +49 -0
  101. package/src/delegate.js +267 -0
  102. package/src/directory-resolver.js +237 -0
  103. package/src/downloader.js +750 -0
  104. package/src/extract.js +149 -0
  105. package/src/index.js +70 -0
  106. package/src/mcp/index.ts +608 -0
  107. package/src/query.js +116 -0
  108. package/src/search.js +247 -0
  109. package/src/tools/common.js +410 -0
  110. package/src/tools/index.js +40 -0
  111. package/src/tools/langchain.js +88 -0
  112. package/src/tools/system-message.js +121 -0
  113. package/src/tools/vercel.js +271 -0
  114. package/src/utils/file-lister.js +193 -0
  115. package/src/utils.js +128 -0
@@ -0,0 +1,1726 @@
1
+ /**
2
+ * Utility functions for cleaning and validating schema responses from AI models
3
+ * Supports JSON and Mermaid diagram validation
4
+ */
5
+
6
+ import { createMessagePreview } from '../tools/common.js';
7
+
8
+ /**
9
+ * HTML entity decoder map for common entities that might appear in mermaid diagrams
10
+ */
11
+ const HTML_ENTITY_MAP = {
12
+ '&lt;': '<',
13
+ '&gt;': '>',
14
+ '&amp;': '&',
15
+ '&quot;': '"',
16
+ '&#39;': "'",
17
+ '&nbsp;': ' '
18
+ };
19
+
20
+ /**
21
+ * Decode HTML entities in text without requiring external dependencies
22
+ * @param {string} text - Text that may contain HTML entities
23
+ * @returns {string} - Text with HTML entities decoded
24
+ */
25
+ export function decodeHtmlEntities(text) {
26
+ if (!text || typeof text !== 'string') {
27
+ return text;
28
+ }
29
+
30
+ let decoded = text;
31
+ for (const [entity, character] of Object.entries(HTML_ENTITY_MAP)) {
32
+ // Use global replacement to catch all instances
33
+ decoded = decoded.replace(new RegExp(entity, 'g'), character);
34
+ }
35
+
36
+ return decoded;
37
+ }
38
+
39
+ /**
40
+ * Clean AI response by extracting JSON content when response contains JSON
41
+ * Only processes responses that contain JSON structures { or [
42
+ * @param {string} response - Raw AI response
43
+ * @returns {string} - Cleaned response with JSON boundaries extracted if applicable
44
+ */
45
+ export function cleanSchemaResponse(response) {
46
+ if (!response || typeof response !== 'string') {
47
+ return response;
48
+ }
49
+
50
+ const trimmed = response.trim();
51
+
52
+ // First, look for JSON after code block markers
53
+ const codeBlockPatterns = [
54
+ /```json\s*\n?([{\[][\s\S]*?[}\]])\s*\n?```/,
55
+ /```\s*\n?([{\[][\s\S]*?[}\]])\s*\n?```/,
56
+ /`([{\[][\s\S]*?[}\]])`/
57
+ ];
58
+
59
+ for (const pattern of codeBlockPatterns) {
60
+ const match = trimmed.match(pattern);
61
+ if (match) {
62
+ return match[1].trim();
63
+ }
64
+ }
65
+
66
+ // Look for code block start followed immediately by JSON
67
+ const codeBlockStartPattern = /```(?:json)?\s*\n?\s*([{\[])/;
68
+ const codeBlockMatch = trimmed.match(codeBlockStartPattern);
69
+
70
+ if (codeBlockMatch) {
71
+ const startIndex = codeBlockMatch.index + codeBlockMatch[0].length - 1; // Position of the bracket
72
+
73
+ // Find the matching closing bracket
74
+ const openChar = codeBlockMatch[1];
75
+ const closeChar = openChar === '{' ? '}' : ']';
76
+ let bracketCount = 1;
77
+ let endIndex = startIndex + 1;
78
+
79
+ while (endIndex < trimmed.length && bracketCount > 0) {
80
+ const char = trimmed[endIndex];
81
+ if (char === openChar) {
82
+ bracketCount++;
83
+ } else if (char === closeChar) {
84
+ bracketCount--;
85
+ }
86
+ endIndex++;
87
+ }
88
+
89
+ if (bracketCount === 0) {
90
+ return trimmed.substring(startIndex, endIndex);
91
+ }
92
+ }
93
+
94
+ // Fallback: Find JSON boundaries anywhere in the text
95
+ const firstBracket = Math.min(
96
+ trimmed.indexOf('{') >= 0 ? trimmed.indexOf('{') : Infinity,
97
+ trimmed.indexOf('[') >= 0 ? trimmed.indexOf('[') : Infinity
98
+ );
99
+
100
+ const lastBracket = Math.max(
101
+ trimmed.lastIndexOf('}'),
102
+ trimmed.lastIndexOf(']')
103
+ );
104
+
105
+ // Only extract if we found valid JSON boundaries
106
+ if (firstBracket < Infinity && lastBracket >= 0 && firstBracket < lastBracket) {
107
+ // Check if the response likely starts with JSON (directly or after minimal content)
108
+ const beforeFirstBracket = trimmed.substring(0, firstBracket).trim();
109
+
110
+ // If there's minimal content before the first bracket, extract the JSON
111
+ if (beforeFirstBracket === '' ||
112
+ beforeFirstBracket.match(/^```\w*$/) ||
113
+ beforeFirstBracket.split('\n').length <= 2) {
114
+ return trimmed.substring(firstBracket, lastBracket + 1);
115
+ }
116
+ }
117
+
118
+ return response; // Return original if no extractable JSON found
119
+ }
120
+
121
+ /**
122
+ * Validate that the cleaned response is valid JSON if expected
123
+ * @param {string} response - Cleaned response
124
+ * @param {Object} options - Options for validation
125
+ * @param {boolean} [options.debug=false] - Enable debug logging
126
+ * @returns {Object} - {isValid: boolean, parsed?: Object, error?: string}
127
+ */
128
+ export function validateJsonResponse(response, options = {}) {
129
+ const { debug = false } = options;
130
+
131
+ if (debug) {
132
+ console.log(`[DEBUG] JSON validation: Starting validation for response (${response.length} chars)`);
133
+ const preview = createMessagePreview(response);
134
+ console.log(`[DEBUG] JSON validation: Preview: ${preview}`);
135
+ }
136
+
137
+ try {
138
+ const parseStart = Date.now();
139
+ const parsed = JSON.parse(response);
140
+ const parseTime = Date.now() - parseStart;
141
+
142
+ if (debug) {
143
+ console.log(`[DEBUG] JSON validation: Successfully parsed in ${parseTime}ms`);
144
+ console.log(`[DEBUG] JSON validation: Object type: ${typeof parsed}, keys: ${Object.keys(parsed || {}).length}`);
145
+ }
146
+
147
+ return { isValid: true, parsed };
148
+ } catch (error) {
149
+ if (debug) {
150
+ console.log(`[DEBUG] JSON validation: Parse failed with error: ${error.message}`);
151
+ console.log(`[DEBUG] JSON validation: Error at position: ${error.message.match(/position (\d+)/) ? error.message.match(/position (\d+)/)[1] : 'unknown'}`);
152
+
153
+ // Try to identify common JSON issues
154
+ if (error.message.includes('Unexpected token')) {
155
+ console.log(`[DEBUG] JSON validation: Likely syntax error - unexpected character`);
156
+ } else if (error.message.includes('Unexpected end')) {
157
+ console.log(`[DEBUG] JSON validation: Likely incomplete JSON - missing closing brackets`);
158
+ } else if (error.message.includes('property name')) {
159
+ console.log(`[DEBUG] JSON validation: Likely unquoted property names`);
160
+ }
161
+ }
162
+
163
+ return { isValid: false, error: error.message };
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Validate that the cleaned response is valid XML if expected
169
+ * @param {string} response - Cleaned response
170
+ * @returns {Object} - {isValid: boolean, error?: string}
171
+ */
172
+ export function validateXmlResponse(response) {
173
+ // Basic XML validation - check for matching opening/closing tags
174
+ const xmlPattern = /<\/?[\w\s="'.-]+>/g;
175
+ const tags = response.match(xmlPattern);
176
+
177
+ if (!tags) {
178
+ return { isValid: false, error: 'No XML tags found' };
179
+ }
180
+
181
+ // Simple check for basic XML structure
182
+ if (response.includes('<') && response.includes('>')) {
183
+ return { isValid: true };
184
+ }
185
+
186
+ return { isValid: false, error: 'Invalid XML structure' };
187
+ }
188
+
189
+ /**
190
+ * Process schema response with cleaning and optional validation
191
+ * @param {string} response - Raw AI response
192
+ * @param {string} schema - Original schema for context
193
+ * @param {Object} options - Processing options
194
+ * @returns {Object} - {cleaned: string, validation?: Object}
195
+ */
196
+ export function processSchemaResponse(response, schema, options = {}) {
197
+ const { validateJson = false, validateXml = false, debug = false } = options;
198
+
199
+ if (debug) {
200
+ console.log(`[DEBUG] Schema processing: Starting with response length ${response.length}`);
201
+ console.log(`[DEBUG] Schema processing: Schema type detection...`);
202
+
203
+ if (isJsonSchema(schema)) {
204
+ console.log(`[DEBUG] Schema processing: Detected JSON schema`);
205
+ } else {
206
+ console.log(`[DEBUG] Schema processing: Non-JSON schema detected`);
207
+ }
208
+ }
209
+
210
+ // Clean the response
211
+ const cleanStart = Date.now();
212
+ const cleaned = cleanSchemaResponse(response);
213
+ const cleanTime = Date.now() - cleanStart;
214
+
215
+ const result = { cleaned };
216
+
217
+ if (debug) {
218
+ console.log(`[DEBUG] Schema processing: Cleaning completed in ${cleanTime}ms`);
219
+ result.debug = {
220
+ originalLength: response.length,
221
+ cleanedLength: cleaned.length,
222
+ wasModified: response !== cleaned,
223
+ cleaningTimeMs: cleanTime,
224
+ removedContent: response !== cleaned ? {
225
+ before: response.substring(0, 100) + (response.length > 100 ? '...' : ''),
226
+ after: cleaned.substring(0, 100) + (cleaned.length > 100 ? '...' : '')
227
+ } : null
228
+ };
229
+
230
+ if (response !== cleaned) {
231
+ console.log(`[DEBUG] Schema processing: Response was modified during cleaning`);
232
+ console.log(`[DEBUG] Schema processing: Original length: ${response.length}, cleaned length: ${cleaned.length}`);
233
+ } else {
234
+ console.log(`[DEBUG] Schema processing: Response unchanged during cleaning`);
235
+ }
236
+ }
237
+
238
+ // Optional validation
239
+ if (validateJson) {
240
+ if (debug) {
241
+ console.log(`[DEBUG] Schema processing: Running JSON validation...`);
242
+ }
243
+ result.jsonValidation = validateJsonResponse(cleaned, { debug });
244
+ }
245
+
246
+ if (validateXml) {
247
+ if (debug) {
248
+ console.log(`[DEBUG] Schema processing: Running XML validation...`);
249
+ }
250
+ result.xmlValidation = validateXmlResponse(cleaned);
251
+ }
252
+
253
+ return result;
254
+ }
255
+
256
+ /**
257
+ * Detect if a schema expects JSON output
258
+ * @param {string} schema - The schema string
259
+ * @returns {boolean} - True if schema appears to be JSON-based
260
+ */
261
+ export function isJsonSchema(schema) {
262
+ if (!schema || typeof schema !== 'string') {
263
+ return false;
264
+ }
265
+
266
+ const trimmedSchema = schema.trim().toLowerCase();
267
+
268
+ // Check for JSON-like patterns
269
+ const jsonIndicators = [
270
+ trimmedSchema.startsWith('{') && trimmedSchema.includes('}'),
271
+ trimmedSchema.startsWith('[') && trimmedSchema.includes(']'),
272
+ trimmedSchema.includes('"type"') && trimmedSchema.includes('object'),
273
+ trimmedSchema.includes('"properties"'),
274
+ trimmedSchema.includes('json'),
275
+ trimmedSchema.includes('application/json')
276
+ ];
277
+
278
+ // Return true if any JSON indicators are found
279
+ return jsonIndicators.some(indicator => indicator);
280
+ }
281
+
282
+ /**
283
+ * Detect if a JSON response is actually a JSON schema definition instead of data
284
+ * @param {string} jsonString - The JSON string to check
285
+ * @param {Object} options - Options
286
+ * @param {boolean} [options.debug=false] - Enable debug logging
287
+ * @returns {boolean} - True if this appears to be a schema definition
288
+ */
289
+ export function isJsonSchemaDefinition(jsonString, options = {}) {
290
+ const { debug = false } = options;
291
+
292
+ if (!jsonString || typeof jsonString !== 'string') {
293
+ if (debug) {
294
+ console.log(`[DEBUG] Schema definition check: Invalid input (${typeof jsonString})`);
295
+ }
296
+ return false;
297
+ }
298
+
299
+ try {
300
+ const parsed = JSON.parse(jsonString);
301
+
302
+ if (debug) {
303
+ console.log(`[DEBUG] Schema definition check: JSON parsed successfully, checking indicators...`);
304
+ }
305
+
306
+ // Check for common JSON schema properties
307
+ const schemaIndicators = [
308
+ parsed.$schema,
309
+ parsed.$id,
310
+ parsed.title && parsed.description,
311
+ parsed.type === 'object' && parsed.properties,
312
+ parsed.type === 'array' && parsed.items,
313
+ parsed.required && Array.isArray(parsed.required),
314
+ parsed.definitions,
315
+ parsed.additionalProperties !== undefined,
316
+ parsed.patternProperties,
317
+ parsed.anyOf || parsed.oneOf || parsed.allOf
318
+ ];
319
+
320
+ const indicatorCount = schemaIndicators.filter(Boolean).length;
321
+ const isSchemaDefinition = indicatorCount >= 2;
322
+
323
+ if (debug) {
324
+ console.log(`[DEBUG] Schema definition check: Found ${indicatorCount} schema indicators`);
325
+ console.log(`[DEBUG] Schema definition check: Indicators found: ${schemaIndicators.map((indicator, i) => {
326
+ const names = ['$schema', '$id', 'title+description', 'object+properties', 'array+items', 'required', 'definitions', 'additionalProperties', 'patternProperties', 'anyOf/oneOf/allOf'];
327
+ return indicator ? names[i] : null;
328
+ }).filter(Boolean).join(', ')}`);
329
+ console.log(`[DEBUG] Schema definition check: Is schema definition: ${isSchemaDefinition}`);
330
+ }
331
+
332
+ return isSchemaDefinition;
333
+ } catch (error) {
334
+ if (debug) {
335
+ console.log(`[DEBUG] Schema definition check: JSON parse failed: ${error.message}`);
336
+ }
337
+ return false;
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Create a correction prompt for invalid JSON
343
+ * @param {string} invalidResponse - The invalid JSON response
344
+ * @param {string} schema - The original schema
345
+ * @param {string} error - The JSON parsing error
346
+ * @param {number} [retryCount=0] - The current retry attempt (0-based)
347
+ * @returns {string} - Correction prompt for the AI
348
+ */
349
+ export function createJsonCorrectionPrompt(invalidResponse, schema, error, retryCount = 0) {
350
+ // Create increasingly stronger prompts based on retry attempt
351
+ const strengthLevels = [
352
+ {
353
+ prefix: "CRITICAL JSON ERROR:",
354
+ instruction: "You MUST fix this and return ONLY valid JSON.",
355
+ emphasis: "Return ONLY the corrected JSON, with no additional text or markdown formatting."
356
+ },
357
+ {
358
+ prefix: "URGENT - JSON PARSING FAILED:",
359
+ instruction: "This is your second chance. Return ONLY valid JSON that can be parsed by JSON.parse().",
360
+ emphasis: "ABSOLUTELY NO explanatory text, greetings, or formatting. ONLY JSON."
361
+ },
362
+ {
363
+ prefix: "FINAL ATTEMPT - CRITICAL JSON ERROR:",
364
+ instruction: "This is the final retry. You MUST return ONLY raw JSON without any other content.",
365
+ emphasis: "EXAMPLE: {\"key\": \"value\"} NOT: ```json{\"key\": \"value\"}``` NOT: Here is the JSON: {\"key\": \"value\"}"
366
+ }
367
+ ];
368
+
369
+ const level = Math.min(retryCount, strengthLevels.length - 1);
370
+ const currentLevel = strengthLevels[level];
371
+
372
+ let prompt = `${currentLevel.prefix} Your previous response is not valid JSON and cannot be parsed. Here's what you returned:
373
+
374
+ ${invalidResponse.substring(0, 500)}${invalidResponse.length > 500 ? '...' : ''}
375
+
376
+ Error: ${error}
377
+
378
+ ${currentLevel.instruction}
379
+
380
+ Schema to match:
381
+ ${schema}
382
+
383
+ ${currentLevel.emphasis}`;
384
+
385
+ return prompt;
386
+ }
387
+
388
+ /**
389
+ * Create a correction prompt specifically for when AI returns schema definition instead of data
390
+ * @param {string} schemaDefinition - The JSON schema definition that was incorrectly returned
391
+ * @param {string} originalSchema - The original schema that should be followed
392
+ * @param {number} [retryCount=0] - The current retry attempt (0-based)
393
+ * @returns {string} - Correction prompt for the AI
394
+ */
395
+ export function createSchemaDefinitionCorrectionPrompt(schemaDefinition, originalSchema, retryCount = 0) {
396
+ const strengthLevels = [
397
+ {
398
+ prefix: "CRITICAL MISUNDERSTANDING:",
399
+ instruction: "You returned a JSON schema definition instead of data. You must return ACTUAL DATA that follows the schema.",
400
+ example: "Instead of: {\"type\": \"object\", \"properties\": {...}}\nReturn: {\"actualData\": \"value\", \"realField\": 123}"
401
+ },
402
+ {
403
+ prefix: "URGENT - WRONG RESPONSE TYPE:",
404
+ instruction: "You are returning the SCHEMA DEFINITION itself. I need DATA that MATCHES the schema, not the schema structure.",
405
+ example: "Schema defines structure - you provide content that fits that structure!"
406
+ },
407
+ {
408
+ prefix: "FINAL ATTEMPT - SCHEMA VS DATA CONFUSION:",
409
+ instruction: "STOP returning schema definitions! Return REAL DATA that conforms to the schema structure.",
410
+ example: "If schema has 'properties.name', return {\"name\": \"actual_value\"} NOT {\"properties\": {\"name\": {...}}}"
411
+ }
412
+ ];
413
+
414
+ const level = Math.min(retryCount, strengthLevels.length - 1);
415
+ const currentLevel = strengthLevels[level];
416
+
417
+ let prompt = `${currentLevel.prefix} You returned a JSON schema definition when I asked for data that matches a schema.
418
+
419
+ What you returned (WRONG - this is a schema definition):
420
+ ${schemaDefinition.substring(0, 300)}${schemaDefinition.length > 300 ? '...' : ''}
421
+
422
+ What I need: ACTUAL DATA that conforms to this schema structure:
423
+ ${originalSchema}
424
+
425
+ ${currentLevel.instruction}
426
+
427
+ ${currentLevel.example}
428
+
429
+ Return ONLY the JSON data object/array that follows the schema structure. NO schema definitions, NO explanations, NO markdown formatting.`;
430
+
431
+ return prompt;
432
+ }
433
+
434
+ /**
435
+ * Detect if a schema expects Mermaid diagram output
436
+ * @param {string} schema - The schema string
437
+ * @returns {boolean} - True if schema appears to expect Mermaid diagrams
438
+ */
439
+ export function isMermaidSchema(schema) {
440
+ if (!schema || typeof schema !== 'string') {
441
+ return false;
442
+ }
443
+
444
+ const trimmedSchema = schema.trim().toLowerCase();
445
+
446
+ // Check for Mermaid-related keywords
447
+ const mermaidIndicators = [
448
+ trimmedSchema.includes('mermaid'),
449
+ trimmedSchema.includes('diagram'),
450
+ trimmedSchema.includes('flowchart'),
451
+ trimmedSchema.includes('sequence'),
452
+ trimmedSchema.includes('gantt'),
453
+ trimmedSchema.includes('pie chart'),
454
+ trimmedSchema.includes('state diagram'),
455
+ trimmedSchema.includes('class diagram'),
456
+ trimmedSchema.includes('entity relationship'),
457
+ trimmedSchema.includes('user journey'),
458
+ trimmedSchema.includes('git graph'),
459
+ trimmedSchema.includes('requirement diagram'),
460
+ trimmedSchema.includes('c4 context')
461
+ ];
462
+
463
+ return mermaidIndicators.some(indicator => indicator);
464
+ }
465
+
466
+ /**
467
+ * Extract Mermaid diagrams from markdown code blocks with position tracking
468
+ * @param {string} response - Response that may contain markdown with mermaid blocks
469
+ * @returns {Object} - {diagrams: Array<{content: string, fullMatch: string, startIndex: number, endIndex: number}>, cleanedResponse: string}
470
+ */
471
+ export function extractMermaidFromMarkdown(response) {
472
+ if (!response || typeof response !== 'string') {
473
+ return { diagrams: [], cleanedResponse: response };
474
+ }
475
+
476
+ // Find all mermaid code blocks with enhanced regex to capture more variations
477
+ // This regex captures optional attributes on same line as ```mermaid, and all diagram content
478
+ const mermaidBlockRegex = /```mermaid([^\n]*)\n([\s\S]*?)```/gi;
479
+ const diagrams = [];
480
+ let match;
481
+
482
+ while ((match = mermaidBlockRegex.exec(response)) !== null) {
483
+ const attributes = match[1] ? match[1].trim() : '';
484
+ const fullContent = match[2].trim();
485
+
486
+ // If attributes exist, they were captured separately, so fullContent is just the diagram
487
+ // If no attributes, the first line of fullContent might be diagram type or actual content
488
+ diagrams.push({
489
+ content: fullContent,
490
+ fullMatch: match[0],
491
+ startIndex: match.index,
492
+ endIndex: match.index + match[0].length,
493
+ attributes: attributes
494
+ });
495
+ }
496
+
497
+ // Return cleaned response (original for now, could be modified if needed)
498
+ return { diagrams, cleanedResponse: response };
499
+ }
500
+
501
+ /**
502
+ * Replace mermaid diagrams in original markdown with corrected versions
503
+ * @param {string} originalResponse - Original response with markdown
504
+ * @param {Array} correctedDiagrams - Array of corrected diagram objects
505
+ * @returns {string} - Response with corrected diagrams in original format
506
+ */
507
+ export function replaceMermaidDiagramsInMarkdown(originalResponse, correctedDiagrams) {
508
+ if (!originalResponse || typeof originalResponse !== 'string') {
509
+ return originalResponse;
510
+ }
511
+
512
+ if (!correctedDiagrams || correctedDiagrams.length === 0) {
513
+ return originalResponse;
514
+ }
515
+
516
+ let modifiedResponse = originalResponse;
517
+
518
+ // Sort diagrams by start index in reverse order to preserve indices during replacement
519
+ const sortedDiagrams = [...correctedDiagrams].sort((a, b) => b.startIndex - a.startIndex);
520
+
521
+ for (const diagram of sortedDiagrams) {
522
+ // Reconstruct the code block with original attributes if they existed
523
+ const attributesStr = diagram.attributes ? ` ${diagram.attributes}` : '';
524
+ const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${diagram.content}\n\`\`\``;
525
+
526
+ // Replace the original code block
527
+ modifiedResponse = modifiedResponse.slice(0, diagram.startIndex) +
528
+ newCodeBlock +
529
+ modifiedResponse.slice(diagram.endIndex);
530
+ }
531
+
532
+ return modifiedResponse;
533
+ }
534
+
535
+ /**
536
+ * Validate a single Mermaid diagram
537
+ * @param {string} diagram - Mermaid diagram code
538
+ * @returns {Promise<Object>} - {isValid: boolean, diagramType?: string, error?: string, detailedError?: string}
539
+ */
540
+ export async function validateMermaidDiagram(diagram) {
541
+ if (!diagram || typeof diagram !== 'string') {
542
+ return { isValid: false, error: 'Empty or invalid diagram input' };
543
+ }
544
+
545
+ try {
546
+ const trimmedDiagram = diagram.trim();
547
+
548
+ // Check for markdown code block markers
549
+ if (trimmedDiagram.includes('```')) {
550
+ return {
551
+ isValid: false,
552
+ error: 'Diagram contains markdown code block markers',
553
+ detailedError: 'Mermaid diagram should not contain ``` markers when extracted from markdown'
554
+ };
555
+ }
556
+
557
+ // Check for common mermaid diagram types (more flexible patterns)
558
+ const diagramPatterns = [
559
+ { pattern: /^(graph|flowchart)/i, type: 'flowchart' },
560
+ { pattern: /^sequenceDiagram/i, type: 'sequence' },
561
+ { pattern: /^gantt/i, type: 'gantt' },
562
+ { pattern: /^pie/i, type: 'pie' },
563
+ { pattern: /^stateDiagram/i, type: 'state' },
564
+ { pattern: /^classDiagram/i, type: 'class' },
565
+ { pattern: /^erDiagram/i, type: 'er' },
566
+ { pattern: /^journey/i, type: 'journey' },
567
+ { pattern: /^gitgraph/i, type: 'gitgraph' },
568
+ { pattern: /^requirementDiagram/i, type: 'requirement' },
569
+ { pattern: /^C4Context/i, type: 'c4' },
570
+ ];
571
+
572
+ // Find matching diagram type
573
+ let diagramType = null;
574
+ for (const { pattern, type } of diagramPatterns) {
575
+ if (pattern.test(trimmedDiagram)) {
576
+ diagramType = type;
577
+ break;
578
+ }
579
+ }
580
+
581
+ if (!diagramType) {
582
+ return {
583
+ isValid: false,
584
+ error: 'Diagram does not match any known Mermaid diagram pattern',
585
+ detailedError: 'The diagram must start with a valid Mermaid diagram type (graph, sequenceDiagram, gantt, pie, etc.)'
586
+ };
587
+ }
588
+
589
+ // GitHub-compatible strict syntax validation
590
+ const lines = trimmedDiagram.split('\n');
591
+
592
+ for (let i = 0; i < lines.length; i++) {
593
+ const line = lines[i].trim();
594
+ if (!line) continue;
595
+
596
+ // Check for GitHub-incompatible patterns that cause "got 'PS'" errors
597
+ if (diagramType === 'flowchart') {
598
+ // Check for unbalanced brackets in node labels
599
+ const brackets = line.match(/\[[^\]]*$/); // Unclosed bracket
600
+ if (brackets) {
601
+ return {
602
+ isValid: false,
603
+ error: `Unclosed bracket on line ${i + 1}`,
604
+ detailedError: `Line "${line}" contains an unclosed bracket`
605
+ };
606
+ }
607
+
608
+ // GitHub-strict: Check for parentheses inside node labels (causes PS token error)
609
+ // But allow parentheses inside double-quoted strings
610
+ const nodeWithParens = line.match(/\[[^"\[\]]*\([^"\[\]]*\]/);
611
+ if (nodeWithParens) {
612
+ return {
613
+ isValid: false,
614
+ error: `Parentheses in node label on line ${i + 1} (GitHub incompatible)`,
615
+ detailedError: `Line "${line}" contains parentheses inside node label brackets. GitHub mermaid renderer fails with 'got PS' error. Use quotes or escape characters instead.`
616
+ };
617
+ }
618
+
619
+ // GitHub-strict: Check for single quotes and backticks inside node labels (causes PS token error)
620
+ const nodeWithQuotes = line.match(/\{[^{}]*['`][^{}]*\}|\[[^[\]]*['`][^[\]]*\]/);
621
+ if (nodeWithQuotes) {
622
+ const hasBacktick = line.includes('`');
623
+ const quoteType = hasBacktick ? 'backticks' : 'single quotes';
624
+ return {
625
+ isValid: false,
626
+ error: `${hasBacktick ? 'Backticks' : 'Single quotes'} in node label on line ${i + 1} (GitHub incompatible)`,
627
+ detailedError: `Line "${line}" contains ${quoteType} inside node label. GitHub mermaid renderer fails with 'got PS' error. Use double quotes or escape characters instead.`
628
+ };
629
+ }
630
+
631
+ // GitHub-strict: Check for complex expressions inside diamond nodes
632
+ // Allow double-quoted strings in diamond nodes, but catch problematic single quotes and complex expressions
633
+ // Allow HTML breaks (<br/>, <br>, etc.) but catch other problematic patterns
634
+ const diamondWithComplexContent = line.match(/\{[^"{}]*[()'"<>&][^"{}]*\}/);
635
+ const hasHtmlBreak = line.match(/\{[^{}]*<br\s*\/?>.*\}/);
636
+ if (diamondWithComplexContent && !line.match(/\{\"[^\"]*\"\}/) && !hasHtmlBreak) {
637
+ return {
638
+ isValid: false,
639
+ error: `Complex expression in diamond node on line ${i + 1} (GitHub incompatible)`,
640
+ detailedError: `Line "${line}" contains special characters in diamond node that may cause GitHub parsing errors. Use simpler text or escape characters.`
641
+ };
642
+ }
643
+
644
+ // GitHub-strict: Check for parentheses in subgraph labels (causes PS token error)
645
+ if (line.startsWith('subgraph ') && line.match(/subgraph\s+[^"]*\([^"]*\)/)) {
646
+ return {
647
+ isValid: false,
648
+ error: `Parentheses in subgraph label on line ${i + 1} (GitHub incompatible)`,
649
+ detailedError: `Line "${line}" contains parentheses in subgraph label. GitHub mermaid renderer fails with 'got PS' error. Use quotes around the label or avoid parentheses.`
650
+ };
651
+ }
652
+ }
653
+
654
+ if (diagramType === 'sequence') {
655
+ // Check for missing colon in sequence messages
656
+ if (line.includes('->>') && !line.includes(':')) {
657
+ return {
658
+ isValid: false,
659
+ error: `Missing colon in sequence message on line ${i + 1}`,
660
+ detailedError: `Line "${line}" appears to be a sequence message but is missing a colon`
661
+ };
662
+ }
663
+ }
664
+ }
665
+
666
+ // If we get here, basic validation passed
667
+ return {
668
+ isValid: true,
669
+ diagramType
670
+ };
671
+
672
+ } catch (error) {
673
+ return {
674
+ isValid: false,
675
+ error: error.message || 'Unknown mermaid parsing error',
676
+ detailedError: error.stack || error.toString()
677
+ };
678
+ }
679
+ }
680
+
681
+ /**
682
+ * Validate all Mermaid diagrams in a response
683
+ * @param {string} response - Response that may contain mermaid diagrams
684
+ * @returns {Promise<Object>} - {isValid: boolean, diagrams: Array, errors?: Array}
685
+ */
686
+ export async function validateMermaidResponse(response) {
687
+ const { diagrams } = extractMermaidFromMarkdown(response);
688
+
689
+ if (diagrams.length === 0) {
690
+ return { isValid: false, diagrams: [], errors: ['No mermaid diagrams found in response'] };
691
+ }
692
+
693
+ const results = [];
694
+ const errors = [];
695
+
696
+ for (let i = 0; i < diagrams.length; i++) {
697
+ const diagramObj = diagrams[i];
698
+ const validation = await validateMermaidDiagram(diagramObj.content);
699
+ results.push({
700
+ ...diagramObj,
701
+ ...validation
702
+ });
703
+
704
+ if (!validation.isValid) {
705
+ errors.push(`Diagram ${i + 1}: ${validation.error}`);
706
+ }
707
+ }
708
+
709
+ const isValid = results.every(result => result.isValid);
710
+
711
+ return {
712
+ isValid,
713
+ diagrams: results,
714
+ errors: errors.length > 0 ? errors : undefined
715
+ };
716
+ }
717
+
718
+ /**
719
+ * Create a correction prompt for invalid Mermaid diagrams
720
+ * @param {string} invalidResponse - The response with invalid Mermaid
721
+ * @param {string} schema - The original schema
722
+ * @param {Array} errors - Array of validation errors
723
+ * @param {Array} diagrams - Array of diagram validation results
724
+ * @returns {string} - Correction prompt for the AI
725
+ */
726
+ export function createMermaidCorrectionPrompt(invalidResponse, schema, errors, diagrams) {
727
+ let prompt = `Your previous response contains invalid Mermaid diagrams that cannot be parsed. Here's what you returned:
728
+
729
+ ${invalidResponse}
730
+
731
+ Validation Errors:`;
732
+
733
+ errors.forEach((error, index) => {
734
+ prompt += `\n${index + 1}. ${error}`;
735
+ });
736
+
737
+ if (diagrams && diagrams.length > 0) {
738
+ prompt += `\n\nDiagram Details:`;
739
+ diagrams.forEach((diagramResult, index) => {
740
+ if (!diagramResult.isValid) {
741
+ prompt += `\n\nDiagram ${index + 1}:`;
742
+ const diagramContent = diagramResult.content || diagramResult.diagram || '';
743
+ prompt += `\n- Content: ${diagramContent.substring(0, 100)}${diagramContent.length > 100 ? '...' : ''}`;
744
+ prompt += `\n- Error: ${diagramResult.error}`;
745
+ if (diagramResult.detailedError && diagramResult.detailedError !== diagramResult.error) {
746
+ prompt += `\n- Details: ${diagramResult.detailedError}`;
747
+ }
748
+ }
749
+ });
750
+ }
751
+
752
+ prompt += `\n\nPlease correct your response to include valid Mermaid diagrams that match this schema:
753
+ ${schema}
754
+
755
+ Ensure all Mermaid diagrams are properly formatted within \`\`\`mermaid code blocks and follow correct Mermaid syntax.`;
756
+
757
+ return prompt;
758
+ }
759
+
760
+ /**
761
+ * Specialized Mermaid diagram fixing agent
762
+ * Uses a separate ProbeAgent instance optimized for Mermaid syntax correction
763
+ */
764
+ export class MermaidFixingAgent {
765
+ constructor(options = {}) {
766
+ // Import ProbeAgent dynamically to avoid circular dependencies
767
+ this.ProbeAgent = null;
768
+ this.options = {
769
+ sessionId: options.sessionId || `mermaid-fixer-${Date.now()}`,
770
+ path: options.path || process.cwd(),
771
+ provider: options.provider,
772
+ model: options.model,
773
+ debug: options.debug,
774
+ tracer: options.tracer,
775
+ // Set to false since we're only fixing syntax, not implementing code
776
+ allowEdit: false
777
+ };
778
+ }
779
+
780
+ /**
781
+ * Get the specialized prompt for mermaid diagram fixing
782
+ */
783
+ getMermaidFixingPrompt() {
784
+ return `You are a world-class Mermaid diagram syntax correction specialist. Your expertise lies in analyzing and fixing Mermaid diagram syntax errors while preserving the original intent, structure, and semantic meaning.
785
+
786
+ CORE RESPONSIBILITIES:
787
+ - Analyze Mermaid diagrams for syntax errors and structural issues
788
+ - Fix syntax errors while maintaining the original diagram's logical flow
789
+ - Ensure diagrams follow proper Mermaid syntax rules and best practices
790
+ - Handle all diagram types: flowchart, sequence, gantt, pie, state, class, er, journey, gitgraph, requirement, c4
791
+
792
+ MERMAID DIAGRAM TYPES & SYNTAX RULES:
793
+ 1. **Flowchart/Graph**: Start with 'graph' or 'flowchart', use proper node definitions and arrows
794
+ 2. **Sequence**: Start with 'sequenceDiagram', use proper participant and message syntax
795
+ 3. **Gantt**: Start with 'gantt', use proper date formats and task definitions
796
+ 4. **State**: Start with 'stateDiagram-v2', use proper state transitions
797
+ 5. **Class**: Start with 'classDiagram', use proper class and relationship syntax
798
+ 6. **Entity-Relationship**: Start with 'erDiagram', use proper entity and relationship syntax
799
+
800
+ FIXING METHODOLOGY:
801
+ 1. **Identify diagram type** from the first line or content analysis
802
+ 2. **Validate syntax** against Mermaid specification for that diagram type
803
+ 3. **Fix errors systematically**:
804
+ - Unclosed brackets, parentheses, or quotes
805
+ - Missing or incorrect arrows and connectors
806
+ - Invalid node IDs or labels
807
+ - Incorrect formatting for diagram-specific elements
808
+ - **Parentheses in node labels or subgraph names**: Wrap text containing parentheses in double quotes to prevent GitHub parsing errors
809
+ - Single quotes in node labels (GitHub's parser expects double quotes)
810
+ 4. **Preserve semantic meaning** - never change the intended flow or relationships
811
+ 5. **Use proper escaping** for special characters and spaces
812
+ 6. **Ensure consistency** in naming conventions and formatting
813
+
814
+ CRITICAL RULES:
815
+ - ALWAYS output only the corrected Mermaid code within a \`\`\`mermaid code block
816
+ - NEVER add explanations, comments, or additional text outside the code block
817
+ - PRESERVE the original diagram's intended meaning and flow
818
+ - FIX syntax errors without changing the logical structure
819
+ - ENSURE the output is valid, parseable Mermaid syntax
820
+ - WRAP text containing parentheses in double quotes for GitHub compatibility
821
+
822
+ When presented with a broken Mermaid diagram, analyze it thoroughly and provide the corrected version that maintains the original intent while fixing all syntax issues.`;
823
+ }
824
+
825
+ /**
826
+ * Initialize the ProbeAgent if not already done
827
+ */
828
+ async initializeAgent() {
829
+ if (!this.ProbeAgent) {
830
+ // Dynamic import to avoid circular dependency
831
+ const { ProbeAgent } = await import('./ProbeAgent.js');
832
+ this.ProbeAgent = ProbeAgent;
833
+ }
834
+
835
+ if (!this.agent) {
836
+ this.agent = new this.ProbeAgent({
837
+ sessionId: this.options.sessionId,
838
+ customPrompt: this.getMermaidFixingPrompt(),
839
+ path: this.options.path,
840
+ provider: this.options.provider,
841
+ model: this.options.model,
842
+ debug: this.options.debug,
843
+ tracer: this.options.tracer,
844
+ allowEdit: this.options.allowEdit
845
+ });
846
+ }
847
+
848
+ return this.agent;
849
+ }
850
+
851
+ /**
852
+ * Fix a single Mermaid diagram using the specialized agent
853
+ * @param {string} diagramContent - The broken Mermaid diagram content
854
+ * @param {Array} originalErrors - Array of errors detected in the original diagram
855
+ * @param {Object} diagramInfo - Additional context about the diagram (type, position, etc.)
856
+ * @returns {Promise<string>} - The corrected Mermaid diagram
857
+ */
858
+ async fixMermaidDiagram(diagramContent, originalErrors = [], diagramInfo = {}) {
859
+ // First, try auto-fixing HTML entities without AI
860
+ const decodedContent = decodeHtmlEntities(diagramContent);
861
+
862
+ // If HTML entity decoding changed the content, validate it first
863
+ if (decodedContent !== diagramContent) {
864
+ try {
865
+ const quickValidation = await validateMermaidDiagram(decodedContent);
866
+ if (quickValidation.isValid) {
867
+ // HTML entity decoding fixed the issue, no need for AI
868
+ if (this.options.debug) {
869
+ console.error('[DEBUG] Fixed Mermaid diagram with HTML entity decoding only');
870
+ }
871
+ return decodedContent;
872
+ }
873
+ } catch (error) {
874
+ // If validation fails, continue with AI fixing using decoded content
875
+ if (this.options.debug) {
876
+ console.error('[DEBUG] HTML entity decoding didn\'t fully fix diagram, continuing with AI fixing');
877
+ }
878
+ }
879
+ }
880
+
881
+ await this.initializeAgent();
882
+
883
+ const errorContext = originalErrors.length > 0
884
+ ? `\n\nDetected errors: ${originalErrors.join(', ')}`
885
+ : '';
886
+
887
+ const diagramTypeHint = diagramInfo.diagramType
888
+ ? `\n\nExpected diagram type: ${diagramInfo.diagramType}`
889
+ : '';
890
+
891
+ // Use decoded content for AI fixing to ensure HTML entities are handled
892
+ const contentToFix = decodedContent !== diagramContent ? decodedContent : diagramContent;
893
+
894
+ const prompt = `Analyze and fix the following Mermaid diagram.${errorContext}${diagramTypeHint}
895
+
896
+ Broken Mermaid diagram:
897
+ \`\`\`mermaid
898
+ ${contentToFix}
899
+ \`\`\`
900
+
901
+ Provide only the corrected Mermaid diagram within a mermaid code block. Do not add any explanations or additional text.`;
902
+
903
+ try {
904
+ const result = await this.agent.answer(prompt, [], {
905
+ schema: 'Return only valid Mermaid diagram code within ```mermaid code block'
906
+ });
907
+
908
+ // Extract the mermaid code from the response
909
+ const extractedDiagram = this.extractCorrectedDiagram(result);
910
+ return extractedDiagram || result;
911
+ } catch (error) {
912
+ if (this.options.debug) {
913
+ console.error(`[DEBUG] Mermaid fixing failed: ${error.message}`);
914
+ }
915
+ throw new Error(`Failed to fix Mermaid diagram: ${error.message}`);
916
+ }
917
+ }
918
+
919
+ /**
920
+ * Extract the corrected diagram from the agent's response
921
+ * @param {string} response - The agent's response
922
+ * @returns {string} - The extracted mermaid diagram
923
+ */
924
+ extractCorrectedDiagram(response) {
925
+ // Try to extract mermaid code block
926
+ const mermaidMatch = response.match(/```mermaid\s*\n([\s\S]*?)\n```/);
927
+ if (mermaidMatch) {
928
+ return mermaidMatch[1].trim();
929
+ }
930
+
931
+ // Fallback: try to extract any code block
932
+ const codeMatch = response.match(/```\s*\n([\s\S]*?)\n```/);
933
+ if (codeMatch) {
934
+ return codeMatch[1].trim();
935
+ }
936
+
937
+ // If no code blocks found, return the response as-is (cleaned)
938
+ return response.replace(/```\w*\n?/g, '').replace(/\n?```/g, '').trim();
939
+ }
940
+
941
+ /**
942
+ * Get token usage information from the specialized agent
943
+ * @returns {Object} - Token usage statistics
944
+ */
945
+ getTokenUsage() {
946
+ return this.agent ? this.agent.getTokenUsage() : null;
947
+ }
948
+
949
+ /**
950
+ * Cancel any ongoing operations
951
+ */
952
+ cancel() {
953
+ if (this.agent) {
954
+ this.agent.cancel();
955
+ }
956
+ }
957
+ }
958
+
959
+ /**
960
+ * Enhanced Mermaid validation with specialized agent fixing
961
+ * @param {string} response - Response that may contain mermaid diagrams
962
+ * @param {Object} options - Options for validation and fixing
963
+ * @returns {Promise<Object>} - Enhanced validation result with fixing capability
964
+ */
965
+ export async function validateAndFixMermaidResponse(response, options = {}) {
966
+ const { schema, debug, path, provider, model, tracer } = options;
967
+ const startTime = Date.now();
968
+
969
+ if (debug) {
970
+ console.log(`[DEBUG] Mermaid validation: Starting enhanced validation for response (${response.length} chars)`);
971
+ console.log(`[DEBUG] Mermaid validation: Options - path: ${path}, provider: ${provider}, model: ${model}`);
972
+ }
973
+
974
+ /**
975
+ * Helper function to determine if node content needs quoting due to problematic characters
976
+ * @param {string} content - The node content to check
977
+ * @returns {boolean} - True if content needs to be quoted
978
+ */
979
+ const needsQuoting = (content) => {
980
+ return /[()'"<>&`]/.test(content) || // Core problematic characters
981
+ content.includes('e.g.') ||
982
+ content.includes('i.e.') ||
983
+ content.includes('src/') ||
984
+ content.includes('defaults/') ||
985
+ content.includes('.ts') ||
986
+ content.includes('.js') ||
987
+ content.includes('.yaml') ||
988
+ content.includes('.json') ||
989
+ content.includes('.md') ||
990
+ content.includes('.html') ||
991
+ content.includes('.css');
992
+ };
993
+
994
+ // Record mermaid validation start in telemetry
995
+ if (tracer) {
996
+ tracer.recordMermaidValidationEvent('started', {
997
+ 'mermaid_validation.response_length': response.length,
998
+ 'mermaid_validation.provider': provider,
999
+ 'mermaid_validation.model': model
1000
+ });
1001
+ }
1002
+
1003
+ // First, run standard validation
1004
+ const validationStart = Date.now();
1005
+ const validation = await validateMermaidResponse(response);
1006
+ const validationTime = Date.now() - validationStart;
1007
+
1008
+ if (debug) {
1009
+ console.log(`[DEBUG] Mermaid validation: Initial validation completed in ${validationTime}ms`);
1010
+ console.log(`[DEBUG] Mermaid validation: Found ${validation.diagrams?.length || 0} diagrams, valid: ${validation.isValid}`);
1011
+ if (validation.diagrams) {
1012
+ validation.diagrams.forEach((diag, i) => {
1013
+ console.log(`[DEBUG] Mermaid validation: Diagram ${i + 1}: ${diag.isValid ? 'valid' : 'invalid'} (${diag.diagramType || 'unknown type'})`);
1014
+ if (!diag.isValid) {
1015
+ console.log(`[DEBUG] Mermaid validation: Error for diagram ${i + 1}: ${diag.error}`);
1016
+ }
1017
+ });
1018
+ }
1019
+ }
1020
+
1021
+ // Always check for HTML entities, even if diagrams are technically valid
1022
+ let needsHtmlEntityCheck = false;
1023
+ if (validation.diagrams && validation.diagrams.length > 0) {
1024
+ for (const diagram of validation.diagrams) {
1025
+ if (diagram.content && (diagram.content.includes('&lt;') || diagram.content.includes('&gt;') || diagram.content.includes('&amp;') || diagram.content.includes('&quot;') || diagram.content.includes('&#39;'))) {
1026
+ needsHtmlEntityCheck = true;
1027
+ break;
1028
+ }
1029
+ }
1030
+ }
1031
+
1032
+ if (validation.isValid && !needsHtmlEntityCheck) {
1033
+ if (debug) {
1034
+ console.log(`[DEBUG] Mermaid validation: All diagrams valid and no HTML entities found, no fixing needed`);
1035
+ }
1036
+
1037
+ // Record successful validation in telemetry
1038
+ if (tracer) {
1039
+ tracer.recordMermaidValidationEvent('completed', {
1040
+ 'mermaid_validation.success': true,
1041
+ 'mermaid_validation.diagrams_found': validation.diagrams?.length || 0,
1042
+ 'mermaid_validation.fixes_needed': false,
1043
+ 'mermaid_validation.duration_ms': Date.now() - startTime
1044
+ });
1045
+ }
1046
+
1047
+ // All diagrams are valid and no HTML entities found, no fixing needed
1048
+ return {
1049
+ ...validation,
1050
+ wasFixed: false,
1051
+ originalResponse: response,
1052
+ fixedResponse: response
1053
+ };
1054
+ }
1055
+
1056
+ // If no diagrams found at all, return without attempting to fix
1057
+ if (!validation.diagrams || validation.diagrams.length === 0) {
1058
+ if (debug) {
1059
+ console.log(`[DEBUG] Mermaid validation: No mermaid diagrams found in response, skipping fixes`);
1060
+ }
1061
+ return {
1062
+ ...validation,
1063
+ wasFixed: false,
1064
+ originalResponse: response,
1065
+ fixedResponse: response
1066
+ };
1067
+ }
1068
+
1069
+ // Try HTML entity decoding auto-fix (for both invalid diagrams and valid diagrams with HTML entities)
1070
+ const invalidCount = validation.diagrams.filter(d => !d.isValid).length;
1071
+ if (debug) {
1072
+ if (invalidCount > 0) {
1073
+ console.log(`[DEBUG] Mermaid validation: ${invalidCount} invalid diagrams detected, trying HTML entity auto-fix first...`);
1074
+ } else {
1075
+ console.log(`[DEBUG] Mermaid validation: Diagrams are valid but HTML entities detected, applying HTML entity auto-fix...`);
1076
+ }
1077
+ }
1078
+
1079
+ try {
1080
+ let fixedResponse = response;
1081
+ const fixingResults = [];
1082
+ let htmlEntityFixesApplied = false;
1083
+
1084
+ // Extract diagrams with position information for replacement
1085
+ const { diagrams } = extractMermaidFromMarkdown(response);
1086
+
1087
+ // First pass: Try HTML entity decoding on ALL diagrams (not just invalid ones)
1088
+ // HTML entities in mermaid diagrams are almost always unintended, even if the diagram is technically valid
1089
+ const allDiagrams = validation.diagrams
1090
+ .map((result, index) => ({ ...result, originalIndex: index }))
1091
+ .reverse();
1092
+
1093
+ for (const diagram of allDiagrams) {
1094
+ const originalContent = diagram.content;
1095
+ const decodedContent = decodeHtmlEntities(originalContent);
1096
+
1097
+ if (decodedContent !== originalContent) {
1098
+ // HTML entities were found and decoded, validate the result
1099
+ try {
1100
+ const quickValidation = await validateMermaidDiagram(decodedContent);
1101
+ if (quickValidation.isValid) {
1102
+ // HTML entity decoding improved this diagram!
1103
+ const originalDiagram = diagrams[diagram.originalIndex];
1104
+ const attributesStr = originalDiagram.attributes ? ` ${originalDiagram.attributes}` : '';
1105
+ const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${decodedContent}\n\`\`\``;
1106
+
1107
+ fixedResponse = fixedResponse.slice(0, originalDiagram.startIndex) +
1108
+ newCodeBlock +
1109
+ fixedResponse.slice(originalDiagram.endIndex);
1110
+
1111
+ fixingResults.push({
1112
+ diagramIndex: diagram.originalIndex,
1113
+ wasFixed: true,
1114
+ originalContent: originalContent,
1115
+ fixedContent: decodedContent,
1116
+ originalError: diagram.error || 'HTML entity cleanup',
1117
+ fixedWithHtmlDecoding: true
1118
+ });
1119
+
1120
+ htmlEntityFixesApplied = true;
1121
+
1122
+ if (debug) {
1123
+ console.log(`[DEBUG] Mermaid validation: Fixed diagram ${diagram.originalIndex + 1} with HTML entity decoding`);
1124
+ console.log(`[DEBUG] Mermaid validation: Original status: ${diagram.isValid ? 'valid' : 'invalid'} - ${diagram.error || 'no error'}`);
1125
+ console.log(`[DEBUG] Mermaid validation: Decoded HTML entities`);
1126
+ }
1127
+ }
1128
+ } catch (error) {
1129
+ if (debug) {
1130
+ console.log(`[DEBUG] Mermaid validation: HTML entity decoding didn't improve diagram ${diagram.originalIndex + 1}: ${error.message}`);
1131
+ }
1132
+ }
1133
+ }
1134
+ }
1135
+
1136
+ // If HTML entity fixes were applied, re-validate the entire response
1137
+ if (htmlEntityFixesApplied) {
1138
+ const revalidation = await validateMermaidResponse(fixedResponse);
1139
+ if (revalidation.isValid) {
1140
+ // All diagrams are now valid, return without AI fixing
1141
+ const totalTime = Date.now() - startTime;
1142
+ if (debug) {
1143
+ console.log(`[DEBUG] Mermaid validation: All diagrams fixed with HTML entity decoding in ${totalTime}ms, no AI needed`);
1144
+ console.log(`[DEBUG] Mermaid validation: Applied ${fixingResults.length} HTML entity fixes`);
1145
+ }
1146
+
1147
+ // Record HTML entity fix success in telemetry
1148
+ if (tracer) {
1149
+ tracer.recordMermaidValidationEvent('html_fix_completed', {
1150
+ 'mermaid_validation.success': true,
1151
+ 'mermaid_validation.fix_method': 'html_entity_decoding',
1152
+ 'mermaid_validation.diagrams_fixed': fixingResults.length,
1153
+ 'mermaid_validation.duration_ms': totalTime
1154
+ });
1155
+ }
1156
+ return {
1157
+ ...revalidation,
1158
+ wasFixed: true,
1159
+ originalResponse: response,
1160
+ fixedResponse: fixedResponse,
1161
+ fixingResults: fixingResults
1162
+ };
1163
+ }
1164
+ }
1165
+
1166
+ // Proactive pass: Fix common node label issues in ALL diagrams (not just invalid ones)
1167
+ let proactiveFixesApplied = false;
1168
+
1169
+ // Re-extract diagrams after HTML entity fixes
1170
+ const { diagrams: currentDiagrams } = extractMermaidFromMarkdown(fixedResponse);
1171
+
1172
+ for (let diagramIndex = currentDiagrams.length - 1; diagramIndex >= 0; diagramIndex--) {
1173
+ const diagram = currentDiagrams[diagramIndex];
1174
+ const originalContent = diagram.content;
1175
+ const lines = originalContent.split('\n');
1176
+ let wasFixed = false;
1177
+
1178
+ // Proactively fix node labels that contain special characters
1179
+ const fixedLines = lines.map(line => {
1180
+ const trimmedLine = line.trim();
1181
+ let modifiedLine = line;
1182
+
1183
+ // Enhanced auto-fixing for square bracket nodes [...]
1184
+ if (trimmedLine.match(/\[[^\]]*\]/)) {
1185
+ modifiedLine = modifiedLine.replace(/\[([^\]]*)\]/g, (match, content) => {
1186
+ // Skip if already properly quoted
1187
+ if (content.trim().startsWith('"') && content.trim().endsWith('"')) {
1188
+ return match;
1189
+ }
1190
+
1191
+ // Check if content needs quoting (contains problematic patterns)
1192
+ if (needsQuoting(content)) {
1193
+ wasFixed = true;
1194
+ // Replace internal double quotes with single quotes to avoid nesting
1195
+ const safeContent = content.replace(/"/g, "'");
1196
+ return `["${safeContent}"]`;
1197
+ }
1198
+
1199
+ return match;
1200
+ });
1201
+ }
1202
+
1203
+ // Enhanced auto-fixing for diamond nodes {...}
1204
+ if (trimmedLine.match(/\{[^{}]*\}/)) {
1205
+ modifiedLine = modifiedLine.replace(/\{([^{}]*)\}/g, (match, content) => {
1206
+ // Skip if already properly quoted
1207
+ if (content.trim().startsWith('"') && content.trim().endsWith('"')) {
1208
+ return match;
1209
+ }
1210
+
1211
+ // Check if content needs quoting (contains problematic patterns)
1212
+ if (needsQuoting(content)) {
1213
+ wasFixed = true;
1214
+ // Replace internal double quotes with single quotes to avoid nesting
1215
+ const safeContent = content.replace(/"/g, "'");
1216
+ return `{"${safeContent}"}`;
1217
+ }
1218
+
1219
+ return match;
1220
+ });
1221
+ }
1222
+
1223
+ return modifiedLine;
1224
+ });
1225
+
1226
+ if (wasFixed) {
1227
+ const fixedContent = fixedLines.join('\n');
1228
+
1229
+ // Replace the diagram in the response
1230
+ const attributesStr = diagram.attributes ? ` ${diagram.attributes}` : '';
1231
+ const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${fixedContent}\n\`\`\``;
1232
+
1233
+ fixedResponse = fixedResponse.slice(0, diagram.startIndex) +
1234
+ newCodeBlock +
1235
+ fixedResponse.slice(diagram.endIndex);
1236
+
1237
+ fixingResults.push({
1238
+ diagramIndex: diagramIndex,
1239
+ wasFixed: true,
1240
+ originalContent: originalContent,
1241
+ fixedContent: fixedContent,
1242
+ originalError: 'Proactive node label quoting',
1243
+ fixMethod: 'node_label_quote_wrapping',
1244
+ fixedWithProactiveQuoting: true
1245
+ });
1246
+
1247
+ proactiveFixesApplied = true;
1248
+
1249
+ if (debug) {
1250
+ console.log(`[DEBUG] Mermaid validation: Proactively fixed diagram ${diagramIndex + 1} with node label quoting`);
1251
+ console.log(`[DEBUG] Mermaid validation: Applied automatic quoting to special characters`);
1252
+ }
1253
+ }
1254
+ }
1255
+
1256
+ // If proactive fixes were applied, re-validate the entire response
1257
+ if (proactiveFixesApplied) {
1258
+ const revalidation = await validateMermaidResponse(fixedResponse);
1259
+ if (revalidation.isValid) {
1260
+ // All diagrams are now valid, return without AI fixing
1261
+ const totalTime = Date.now() - startTime;
1262
+ if (debug) {
1263
+ console.log(`[DEBUG] Mermaid validation: All diagrams fixed with proactive quoting in ${totalTime}ms, no AI needed`);
1264
+ console.log(`[DEBUG] Mermaid validation: Applied ${fixingResults.length} proactive fixes`);
1265
+ }
1266
+
1267
+ // Record proactive fix success in telemetry
1268
+ if (tracer) {
1269
+ tracer.recordMermaidValidationEvent('proactive_fix_completed', {
1270
+ 'mermaid_validation.success': true,
1271
+ 'mermaid_validation.fix_method': 'node_label_quote_wrapping',
1272
+ 'mermaid_validation.diagrams_fixed': fixingResults.length,
1273
+ 'mermaid_validation.duration_ms': totalTime
1274
+ });
1275
+ }
1276
+ return {
1277
+ ...revalidation,
1278
+ wasFixed: true,
1279
+ originalResponse: response,
1280
+ fixedResponse: fixedResponse,
1281
+ fixingResults: fixingResults,
1282
+ performanceMetrics: {
1283
+ totalTimeMs: totalTime,
1284
+ aiFixingTimeMs: 0,
1285
+ finalValidationTimeMs: 0,
1286
+ diagramsProcessed: fixingResults.length,
1287
+ diagramsFixed: fixingResults.length
1288
+ }
1289
+ };
1290
+ }
1291
+ }
1292
+
1293
+ // Second pass: Try auto-fixing unquoted subgraph names with parentheses
1294
+ let subgraphFixesApplied = false;
1295
+
1296
+ // Re-extract diagrams and re-validate after HTML entity fixes
1297
+ const { diagrams: postHtmlDiagrams } = extractMermaidFromMarkdown(fixedResponse);
1298
+ const postHtmlValidation = await validateMermaidResponse(fixedResponse);
1299
+
1300
+ const stillInvalidAfterHtml = postHtmlValidation.diagrams
1301
+ .map((result, index) => ({ ...result, originalIndex: index }))
1302
+ .filter(result => !result.isValid)
1303
+ .reverse();
1304
+
1305
+ for (const invalidDiagram of stillInvalidAfterHtml) {
1306
+ // Check if this is a subgraph parentheses error that we can auto-fix
1307
+ if (invalidDiagram.error && invalidDiagram.error.includes('Parentheses in subgraph label')) {
1308
+ const originalContent = invalidDiagram.content;
1309
+ const lines = originalContent.split('\n');
1310
+ let wasFixed = false;
1311
+
1312
+ // Find and fix unquoted subgraph lines with parentheses
1313
+ const fixedLines = lines.map(line => {
1314
+ const trimmedLine = line.trim();
1315
+ if (trimmedLine.startsWith('subgraph ') &&
1316
+ !trimmedLine.match(/subgraph\s+"[^"]*"/) && // not already quoted
1317
+ trimmedLine.match(/subgraph\s+[^"]*\([^"]*\)/)) { // has unquoted parentheses
1318
+
1319
+ // Extract the subgraph name part
1320
+ const match = trimmedLine.match(/^(\s*subgraph\s+)(.+)$/);
1321
+ if (match) {
1322
+ const prefix = match[1];
1323
+ const name = match[2];
1324
+ const fixedLine = line.replace(trimmedLine, `${prefix.trim()} "${name}"`);
1325
+ wasFixed = true;
1326
+ return fixedLine;
1327
+ }
1328
+ }
1329
+ return line;
1330
+ });
1331
+
1332
+ if (wasFixed) {
1333
+ const fixedContent = fixedLines.join('\n');
1334
+
1335
+ // Validate the fixed content
1336
+ try {
1337
+ const quickValidation = await validateMermaidDiagram(fixedContent);
1338
+ if (quickValidation.isValid) {
1339
+ // Subgraph auto-fix worked!
1340
+ const originalDiagram = postHtmlDiagrams[invalidDiagram.originalIndex];
1341
+ const attributesStr = originalDiagram.attributes ? ` ${originalDiagram.attributes}` : '';
1342
+ const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${fixedContent}\n\`\`\``;
1343
+
1344
+ fixedResponse = fixedResponse.slice(0, originalDiagram.startIndex) +
1345
+ newCodeBlock +
1346
+ fixedResponse.slice(originalDiagram.endIndex);
1347
+
1348
+ fixingResults.push({
1349
+ originalIndex: invalidDiagram.originalIndex,
1350
+ wasFixed: true,
1351
+ originalError: invalidDiagram.error,
1352
+ fixMethod: 'subgraph_quote_wrapping',
1353
+ fixedWithSubgraphQuoting: true
1354
+ });
1355
+
1356
+ subgraphFixesApplied = true;
1357
+
1358
+ if (debug) {
1359
+ console.log(`[DEBUG] Mermaid validation: Fixed diagram ${invalidDiagram.originalIndex + 1} with subgraph quote wrapping`);
1360
+ console.log(`[DEBUG] Mermaid validation: Original error: ${invalidDiagram.error}`);
1361
+ }
1362
+ }
1363
+ } catch (error) {
1364
+ if (debug) {
1365
+ console.log(`[DEBUG] Mermaid validation: Subgraph auto-fix didn't work for diagram ${invalidDiagram.originalIndex + 1}: ${error.message}`);
1366
+ }
1367
+ }
1368
+ }
1369
+ }
1370
+ }
1371
+
1372
+ // If subgraph fixes were applied, re-validate the entire response
1373
+ if (subgraphFixesApplied) {
1374
+ const revalidation = await validateMermaidResponse(fixedResponse);
1375
+ if (revalidation.isValid) {
1376
+ // All diagrams are now valid, return without AI fixing
1377
+ const totalTime = Date.now() - startTime;
1378
+ if (debug) {
1379
+ console.log(`[DEBUG] Mermaid validation: All diagrams fixed with auto-fixes in ${totalTime}ms, no AI needed`);
1380
+ console.log(`[DEBUG] Mermaid validation: Applied ${fixingResults.length} auto-fixes (HTML entities + subgraph quotes)`);
1381
+ }
1382
+
1383
+ // Record auto-fix success in telemetry
1384
+ if (tracer) {
1385
+ tracer.recordMermaidValidationEvent('auto_fix_completed', {
1386
+ 'mermaid_validation.success': true,
1387
+ 'mermaid_validation.fix_method': 'auto_fixes',
1388
+ 'mermaid_validation.diagrams_fixed': fixingResults.length,
1389
+ 'mermaid_validation.duration_ms': totalTime
1390
+ });
1391
+ }
1392
+ return {
1393
+ ...revalidation,
1394
+ wasFixed: true,
1395
+ originalResponse: response,
1396
+ fixedResponse: fixedResponse,
1397
+ fixingResults: fixingResults,
1398
+ performanceMetrics: {
1399
+ totalTimeMs: totalTime,
1400
+ aiFixingTimeMs: 0,
1401
+ finalValidationTimeMs: 0,
1402
+ diagramsProcessed: fixingResults.length,
1403
+ diagramsFixed: fixingResults.length
1404
+ }
1405
+ };
1406
+ }
1407
+ }
1408
+
1409
+ // Third pass: Try auto-fixing node labels with parentheses or single quotes
1410
+ let nodeLabelFixesApplied = false;
1411
+
1412
+ // Re-extract diagrams and re-validate after previous fixes
1413
+ const { diagrams: postSubgraphDiagrams } = extractMermaidFromMarkdown(fixedResponse);
1414
+ const postSubgraphValidation = await validateMermaidResponse(fixedResponse);
1415
+
1416
+ const stillInvalidAfterSubgraph = postSubgraphValidation.diagrams
1417
+ .map((result, index) => ({ ...result, originalIndex: index }))
1418
+ .filter(result => !result.isValid)
1419
+ .reverse();
1420
+
1421
+ for (const invalidDiagram of stillInvalidAfterSubgraph) {
1422
+ // Check if this is a node label error that we can auto-fix
1423
+ if (invalidDiagram.error &&
1424
+ (invalidDiagram.error.includes('Parentheses in node label') ||
1425
+ invalidDiagram.error.includes('Complex expression in diamond node') ||
1426
+ invalidDiagram.error.includes('Single quotes in node label') ||
1427
+ invalidDiagram.error.includes('Backticks in node label'))) {
1428
+ const originalContent = invalidDiagram.content;
1429
+ const lines = originalContent.split('\n');
1430
+ let wasFixed = false;
1431
+
1432
+ // Find and fix node labels with special characters that need quoting
1433
+ const fixedLines = lines.map(line => {
1434
+ const trimmedLine = line.trim();
1435
+ let modifiedLine = line;
1436
+
1437
+ // Enhanced auto-fixing for square bracket nodes [...]
1438
+ // Look for any node labels that contain special characters and aren't already quoted
1439
+ if (trimmedLine.match(/\[[^\]]*\]/)) {
1440
+ modifiedLine = modifiedLine.replace(/\[([^\]]*)\]/g, (match, content) => {
1441
+ // Skip if already properly quoted
1442
+ if (content.trim().startsWith('"') && content.trim().endsWith('"')) {
1443
+ return match;
1444
+ }
1445
+
1446
+ // Check if content needs quoting (contains problematic patterns)
1447
+ if (needsQuoting(content)) {
1448
+ wasFixed = true;
1449
+ // Replace internal double quotes with single quotes to avoid nesting
1450
+ const safeContent = content.replace(/"/g, "'");
1451
+ return `["${safeContent}"]`;
1452
+ }
1453
+
1454
+ return match;
1455
+ });
1456
+ }
1457
+
1458
+ // Enhanced auto-fixing for diamond nodes {...}
1459
+ if (trimmedLine.match(/\{[^{}]*\}/)) {
1460
+ modifiedLine = modifiedLine.replace(/\{([^{}]*)\}/g, (match, content) => {
1461
+ // Skip if already properly quoted
1462
+ if (content.trim().startsWith('"') && content.trim().endsWith('"')) {
1463
+ return match;
1464
+ }
1465
+
1466
+ // Check if content needs quoting (contains problematic patterns)
1467
+ if (needsQuoting(content)) {
1468
+ wasFixed = true;
1469
+ // Replace internal double quotes with single quotes to avoid nesting
1470
+ const safeContent = content.replace(/"/g, "'");
1471
+ return `{"${safeContent}"}`;
1472
+ }
1473
+
1474
+ return match;
1475
+ });
1476
+ }
1477
+
1478
+ return modifiedLine;
1479
+ });
1480
+
1481
+ if (wasFixed) {
1482
+ const fixedContent = fixedLines.join('\n');
1483
+
1484
+ // Validate the fixed content
1485
+ try {
1486
+ const quickValidation = await validateMermaidDiagram(fixedContent);
1487
+ if (quickValidation.isValid) {
1488
+ // Node label auto-fix worked!
1489
+ const originalDiagram = postSubgraphDiagrams[invalidDiagram.originalIndex];
1490
+ const attributesStr = originalDiagram.attributes ? ` ${originalDiagram.attributes}` : '';
1491
+ const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${fixedContent}\n\`\`\``;
1492
+
1493
+ fixedResponse = fixedResponse.slice(0, originalDiagram.startIndex) +
1494
+ newCodeBlock +
1495
+ fixedResponse.slice(originalDiagram.endIndex);
1496
+
1497
+ fixingResults.push({
1498
+ originalIndex: invalidDiagram.originalIndex,
1499
+ wasFixed: true,
1500
+ originalError: invalidDiagram.error,
1501
+ fixMethod: 'node_label_quote_wrapping',
1502
+ fixedWithNodeLabelQuoting: true
1503
+ });
1504
+
1505
+ nodeLabelFixesApplied = true;
1506
+
1507
+ if (debug) {
1508
+ console.log(`[DEBUG] Mermaid validation: Fixed diagram ${invalidDiagram.originalIndex + 1} with node label quote wrapping`);
1509
+ console.log(`[DEBUG] Mermaid validation: Original error: ${invalidDiagram.error}`);
1510
+ }
1511
+ }
1512
+ } catch (error) {
1513
+ if (debug) {
1514
+ console.log(`[DEBUG] Mermaid validation: Node label auto-fix didn't work for diagram ${invalidDiagram.originalIndex + 1}: ${error.message}`);
1515
+ }
1516
+ }
1517
+ }
1518
+ }
1519
+ }
1520
+
1521
+ // If node label fixes were applied, re-validate the entire response
1522
+ if (nodeLabelFixesApplied) {
1523
+ const revalidation = await validateMermaidResponse(fixedResponse);
1524
+ if (revalidation.isValid) {
1525
+ // All diagrams are now valid, return without AI fixing
1526
+ const totalTime = Date.now() - startTime;
1527
+ if (debug) {
1528
+ console.log(`[DEBUG] Mermaid validation: All diagrams fixed with auto-fixes in ${totalTime}ms, no AI needed`);
1529
+ console.log(`[DEBUG] Mermaid validation: Applied ${fixingResults.length} auto-fixes (HTML entities + subgraph quotes + node label quotes)`);
1530
+ }
1531
+
1532
+ // Record auto-fix success in telemetry
1533
+ if (tracer) {
1534
+ tracer.recordMermaidValidationEvent('auto_fix_completed', {
1535
+ 'mermaid_validation.success': true,
1536
+ 'mermaid_validation.fix_method': 'auto_fixes',
1537
+ 'mermaid_validation.diagrams_fixed': fixingResults.length,
1538
+ 'mermaid_validation.duration_ms': totalTime
1539
+ });
1540
+ }
1541
+ return {
1542
+ ...revalidation,
1543
+ wasFixed: true,
1544
+ originalResponse: response,
1545
+ fixedResponse: fixedResponse,
1546
+ fixingResults: fixingResults,
1547
+ performanceMetrics: {
1548
+ totalTimeMs: totalTime,
1549
+ aiFixingTimeMs: 0,
1550
+ finalValidationTimeMs: 0,
1551
+ diagramsProcessed: fixingResults.length,
1552
+ diagramsFixed: fixingResults.length
1553
+ }
1554
+ };
1555
+ }
1556
+ }
1557
+
1558
+ // Re-extract diagrams and re-validate after HTML entity fixes
1559
+ const { diagrams: updatedDiagrams } = extractMermaidFromMarkdown(fixedResponse);
1560
+ const updatedValidation = await validateMermaidResponse(fixedResponse);
1561
+
1562
+ // Still have invalid diagrams after all auto-fixes, proceed with AI fixing
1563
+ if (debug) {
1564
+ const stillInvalidAfterHtml = updatedValidation?.diagrams?.filter(d => !d.isValid)?.length || invalidCount;
1565
+ console.log(`[DEBUG] Mermaid validation: ${stillInvalidAfterHtml} diagrams still invalid after HTML entity decoding, starting AI fixing...`);
1566
+ console.log(`[DEBUG] Mermaid validation: HTML entity fixes applied: ${fixingResults.length}`);
1567
+ }
1568
+
1569
+ // Create specialized fixing agent for remaining invalid diagrams
1570
+ if (debug) {
1571
+ console.log(`[DEBUG] Mermaid validation: Creating specialized AI fixing agent...`);
1572
+ }
1573
+ const aiFixingStart = Date.now();
1574
+ const mermaidFixer = new MermaidFixingAgent({
1575
+ path, provider, model, debug, tracer
1576
+ });
1577
+
1578
+ const stillInvalidDiagrams = updatedValidation.diagrams
1579
+ .map((result, index) => ({ ...result, originalIndex: index }))
1580
+ .filter(result => !result.isValid)
1581
+ .reverse();
1582
+
1583
+ if (debug) {
1584
+ console.log(`[DEBUG] Mermaid validation: Found ${stillInvalidDiagrams.length} diagrams requiring AI fixing`);
1585
+ }
1586
+
1587
+ for (const invalidDiagram of stillInvalidDiagrams) {
1588
+ if (debug) {
1589
+ console.log(`[DEBUG] Mermaid validation: Attempting AI fix for diagram ${invalidDiagram.originalIndex + 1}`);
1590
+ console.log(`[DEBUG] Mermaid validation: Diagram type: ${invalidDiagram.diagramType || 'unknown'}`);
1591
+ console.log(`[DEBUG] Mermaid validation: Error to fix: ${invalidDiagram.error}`);
1592
+ }
1593
+
1594
+ const diagramFixStart = Date.now();
1595
+ try {
1596
+ const fixedContent = await mermaidFixer.fixMermaidDiagram(
1597
+ invalidDiagram.content,
1598
+ [invalidDiagram.error],
1599
+ { diagramType: invalidDiagram.diagramType }
1600
+ );
1601
+ const diagramFixTime = Date.now() - diagramFixStart;
1602
+
1603
+ if (fixedContent && fixedContent !== invalidDiagram.content) {
1604
+ // Replace the diagram in the response
1605
+ const originalDiagram = updatedDiagrams[invalidDiagram.originalIndex];
1606
+ const attributesStr = originalDiagram.attributes ? ` ${originalDiagram.attributes}` : '';
1607
+ const newCodeBlock = `\`\`\`mermaid${attributesStr}\n${fixedContent}\n\`\`\``;
1608
+
1609
+ fixedResponse = fixedResponse.slice(0, originalDiagram.startIndex) +
1610
+ newCodeBlock +
1611
+ fixedResponse.slice(originalDiagram.endIndex);
1612
+
1613
+ fixingResults.push({
1614
+ diagramIndex: invalidDiagram.originalIndex,
1615
+ wasFixed: true,
1616
+ originalContent: invalidDiagram.content,
1617
+ fixedContent: fixedContent,
1618
+ originalError: invalidDiagram.error,
1619
+ aiFixingTimeMs: diagramFixTime
1620
+ });
1621
+
1622
+ if (debug) {
1623
+ console.log(`[DEBUG] Mermaid validation: Successfully fixed diagram ${invalidDiagram.originalIndex + 1} in ${diagramFixTime}ms`);
1624
+ console.log(`[DEBUG] Mermaid validation: Original error: ${invalidDiagram.error}`);
1625
+ console.log(`[DEBUG] Mermaid validation: Content changes: ${invalidDiagram.content.length} -> ${fixedContent.length} chars`);
1626
+ }
1627
+ } else {
1628
+ fixingResults.push({
1629
+ diagramIndex: invalidDiagram.originalIndex,
1630
+ wasFixed: false,
1631
+ originalContent: invalidDiagram.content,
1632
+ originalError: invalidDiagram.error,
1633
+ fixingError: 'No valid fix generated',
1634
+ aiFixingTimeMs: diagramFixTime
1635
+ });
1636
+
1637
+ if (debug) {
1638
+ console.log(`[DEBUG] Mermaid validation: AI fix failed for diagram ${invalidDiagram.originalIndex + 1} - no valid fix generated`);
1639
+ }
1640
+ }
1641
+ } catch (error) {
1642
+ const diagramFixTime = Date.now() - diagramFixStart;
1643
+ fixingResults.push({
1644
+ diagramIndex: invalidDiagram.originalIndex,
1645
+ wasFixed: false,
1646
+ originalContent: invalidDiagram.content,
1647
+ originalError: invalidDiagram.error,
1648
+ fixingError: error.message,
1649
+ aiFixingTimeMs: diagramFixTime
1650
+ });
1651
+
1652
+ if (debug) {
1653
+ console.log(`[DEBUG] Mermaid validation: AI fix failed for diagram ${invalidDiagram.originalIndex + 1} after ${diagramFixTime}ms: ${error.message}`);
1654
+ }
1655
+ }
1656
+ }
1657
+
1658
+ // Re-validate the fixed response
1659
+ const finalValidationStart = Date.now();
1660
+ const finalValidation = await validateMermaidResponse(fixedResponse);
1661
+ const finalValidationTime = Date.now() - finalValidationStart;
1662
+ const totalTime = Date.now() - startTime;
1663
+ const aiFixingTime = Date.now() - aiFixingStart;
1664
+
1665
+ // Check if any diagrams were actually fixed
1666
+ const wasActuallyFixed = fixingResults.some(result => result.wasFixed);
1667
+ const fixedCount = fixingResults.filter(result => result.wasFixed).length;
1668
+ const totalAttempts = fixingResults.length;
1669
+
1670
+ if (debug) {
1671
+ console.log(`[DEBUG] Mermaid validation: Final validation completed in ${finalValidationTime}ms`);
1672
+ console.log(`[DEBUG] Mermaid validation: Total process time: ${totalTime}ms (AI fixing: ${aiFixingTime}ms)`);
1673
+ console.log(`[DEBUG] Mermaid validation: Fixed ${fixedCount}/${totalAttempts} diagrams with AI`);
1674
+ console.log(`[DEBUG] Mermaid validation: Final result - all valid: ${finalValidation.isValid}`);
1675
+
1676
+ if (mermaidFixer.getTokenUsage) {
1677
+ const tokenUsage = mermaidFixer.getTokenUsage();
1678
+ console.log(`[DEBUG] Mermaid validation: AI token usage - prompt: ${tokenUsage?.promptTokens || 0}, completion: ${tokenUsage?.completionTokens || 0}`);
1679
+ }
1680
+ }
1681
+
1682
+ // Record final completion in telemetry
1683
+ if (tracer) {
1684
+ tracer.recordMermaidValidationEvent('completed', {
1685
+ 'mermaid_validation.success': finalValidation.isValid,
1686
+ 'mermaid_validation.was_fixed': wasActuallyFixed,
1687
+ 'mermaid_validation.diagrams_processed': totalAttempts,
1688
+ 'mermaid_validation.diagrams_fixed': fixedCount,
1689
+ 'mermaid_validation.total_duration_ms': totalTime,
1690
+ 'mermaid_validation.ai_fixing_duration_ms': aiFixingTime,
1691
+ 'mermaid_validation.final_validation_duration_ms': finalValidationTime,
1692
+ 'mermaid_validation.token_usage': mermaidFixer.getTokenUsage ? mermaidFixer.getTokenUsage() : null
1693
+ });
1694
+ }
1695
+
1696
+ return {
1697
+ ...finalValidation,
1698
+ wasFixed: wasActuallyFixed,
1699
+ originalResponse: response,
1700
+ fixedResponse: fixedResponse,
1701
+ fixingResults: fixingResults,
1702
+ performanceMetrics: {
1703
+ totalTimeMs: totalTime,
1704
+ aiFixingTimeMs: aiFixingTime,
1705
+ finalValidationTimeMs: finalValidationTime,
1706
+ diagramsProcessed: totalAttempts,
1707
+ diagramsFixed: fixedCount
1708
+ },
1709
+ tokenUsage: mermaidFixer.getTokenUsage()
1710
+ };
1711
+
1712
+ } catch (error) {
1713
+ if (debug) {
1714
+ console.error(`[DEBUG] Mermaid fixing agent failed: ${error.message}`);
1715
+ }
1716
+
1717
+ // Return original validation with fixing error
1718
+ return {
1719
+ ...validation,
1720
+ wasFixed: false,
1721
+ originalResponse: response,
1722
+ fixedResponse: response,
1723
+ fixingError: error.message
1724
+ };
1725
+ }
1726
+ }