@in-the-loop-labs/pair-review 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,18 @@
2
2
  const logger = require('./logger');
3
3
 
4
4
  /**
5
- * Extract JSON from text responses using multiple strategies
6
- * This is a shared utility to ensure consistent JSON extraction across the application
7
- * @param {string} response - Raw response text
5
+ * Extract JSON from text responses using multiple strategies.
6
+ * This is a shared utility to ensure consistent JSON extraction across the application.
7
+ *
8
+ * Strategies are tried in order:
9
+ * 1. Markdown code blocks (```json ... ```)
10
+ * 2. Direct JSON.parse of the trimmed response
11
+ * 3. First { to last } substring
12
+ * 4. Known JSON key anchors (e.g. {"level", {"suggestions")
13
+ * 5. Forward scan: try JSON.parse from every top-level { in the text
14
+ * 6. Bracket-matched substring from the first {
15
+ *
16
+ * @param {string} response - Raw response text (may include preamble/postamble prose)
8
17
  * @param {string|number} level - Level identifier for logging (e.g., 1, 2, 3, 'orchestration', 'unknown')
9
18
  * @returns {Object} Extraction result with success flag and data/error
10
19
  */
@@ -35,51 +44,132 @@ function extractJSON(response, level = 'unknown') {
35
44
  }
36
45
  throw new Error('No JSON code block found');
37
46
  },
38
-
39
- // Strategy 2: Look for JSON between first { and last }
47
+
48
+ // Strategy 2: Try the entire response as JSON (fast path for clean responses)
49
+ () => {
50
+ return JSON.parse(response.trim());
51
+ },
52
+
53
+ // Strategy 3: Look for JSON between first { and last }
54
+ // Works when the response is just JSON or has minimal wrapping
40
55
  () => {
41
56
  const firstBrace = response.indexOf('{');
42
57
  const lastBrace = response.lastIndexOf('}');
43
- if (firstBrace !== -1 && lastBrace !== -1 && lastBrace >= firstBrace) {
58
+ if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
44
59
  return JSON.parse(response.substring(firstBrace, lastBrace + 1));
45
60
  }
46
61
  throw new Error('No valid JSON braces found');
47
62
  },
48
-
49
- // Strategy 3: Try to find JSON-like structure with bracket matching
63
+
64
+ // Strategy 4: Anchor-based extraction look for known JSON key patterns
65
+ // that mark the start of our expected response structures.
66
+ // This handles the common case where preamble text contains { characters
67
+ // (e.g. LLM discussing code: "the function handleEvent(event) { ... }")
68
+ // which would cause Strategy 3 to grab the wrong first brace.
50
69
  () => {
51
- const jsonMatch = response.match(/\{[\s\S]*\}/);
52
- if (jsonMatch) {
53
- // Try to find the complete JSON by matching brackets
54
- const jsonStr = jsonMatch[0];
55
- let braceCount = 0;
56
- let endIndex = -1;
57
- const maxIterations = Math.min(jsonStr.length, 100000); // Prevent infinite loops
58
-
59
- for (let i = 0; i < maxIterations; i++) {
60
- if (jsonStr[i] === '{') braceCount++;
61
- else if (jsonStr[i] === '}') {
62
- braceCount--;
63
- if (braceCount === 0) {
64
- endIndex = i;
65
- break;
70
+ // Look for patterns that start our expected JSON structures
71
+ const anchors = [
72
+ /\{"level"\s*:/,
73
+ /\{"suggestions"\s*:/,
74
+ /\{"fileLevelSuggestions"\s*:/,
75
+ /\{"summary"\s*:/,
76
+ /\{"overview"\s*:/,
77
+ ];
78
+
79
+ for (const anchor of anchors) {
80
+ const match = response.match(anchor);
81
+ if (match) {
82
+ const startIdx = match.index;
83
+ // Find the matching closing brace from the end
84
+ const lastBrace = response.lastIndexOf('}');
85
+ if (lastBrace > startIdx) {
86
+ const candidate = response.substring(startIdx, lastBrace + 1);
87
+ return JSON.parse(candidate);
88
+ }
89
+ }
90
+ }
91
+ throw new Error('No known JSON anchor found');
92
+ },
93
+
94
+ // Strategy 5: Forward scan — try JSON.parse starting from each { in the text.
95
+ // Handles arbitrary preamble text with braces by trying every { as a potential
96
+ // JSON start. Stops at the first successful parse.
97
+ () => {
98
+ let searchFrom = 0;
99
+ // Limit attempts to avoid excessive parsing on very large non-JSON text
100
+ const maxAttempts = 20;
101
+ let attempts = 0;
102
+ const lastBrace = response.lastIndexOf('}');
103
+
104
+ while (searchFrom < response.length && attempts < maxAttempts) {
105
+ const braceIdx = response.indexOf('{', searchFrom);
106
+ if (braceIdx === -1) break;
107
+
108
+ attempts++;
109
+ try {
110
+ // Try parsing from this brace to the end of the response.
111
+ // JSON.parse is lenient about trailing content only if we trim to the
112
+ // right boundary, so use lastIndexOf('}') from the end.
113
+ if (lastBrace > braceIdx) {
114
+ const candidate = response.substring(braceIdx, lastBrace + 1);
115
+ const parsed = JSON.parse(candidate);
116
+ if (parsed && typeof parsed === 'object') {
117
+ return parsed;
66
118
  }
67
119
  }
120
+ } catch {
121
+ // This { wasn't the start of valid JSON, try the next one
68
122
  }
69
-
70
- if (endIndex > -1) {
71
- return JSON.parse(jsonStr.substring(0, endIndex + 1));
123
+ searchFrom = braceIdx + 1;
124
+ }
125
+ throw new Error('Forward scan found no valid JSON');
126
+ },
127
+
128
+ // Strategy 6: Bracket-matched substring from the first {.
129
+ // Counts balanced braces (ignoring those inside JSON strings) to find
130
+ // the end of the first top-level object. No iteration cap — the loop
131
+ // runs for the full length of the matched region.
132
+ () => {
133
+ const firstBrace = response.indexOf('{');
134
+ if (firstBrace === -1) throw new Error('No opening brace found');
135
+
136
+ let braceCount = 0;
137
+ let inString = false;
138
+ let escaped = false;
139
+
140
+ for (let i = firstBrace; i < response.length; i++) {
141
+ const ch = response[i];
142
+
143
+ if (escaped) {
144
+ escaped = false;
145
+ continue;
146
+ }
147
+
148
+ if (ch === '\\' && inString) {
149
+ escaped = true;
150
+ continue;
151
+ }
152
+
153
+ if (ch === '"') {
154
+ inString = !inString;
155
+ continue;
156
+ }
157
+
158
+ if (inString) continue;
159
+
160
+ if (ch === '{') braceCount++;
161
+ else if (ch === '}') {
162
+ braceCount--;
163
+ if (braceCount === 0) {
164
+ return JSON.parse(response.substring(firstBrace, i + 1));
165
+ }
72
166
  }
73
167
  }
74
168
  throw new Error('No balanced JSON structure found');
75
169
  },
76
-
77
- // Strategy 4: Try the entire response as JSON (for simple cases)
78
- () => {
79
- return JSON.parse(response.trim());
80
- }
81
170
  ];
82
171
 
172
+ const strategyErrors = [];
83
173
  for (let i = 0; i < strategies.length; i++) {
84
174
  try {
85
175
  const data = strategies[i]();
@@ -88,17 +178,17 @@ function extractJSON(response, level = 'unknown') {
88
178
  return { success: true, data };
89
179
  }
90
180
  } catch (error) {
91
- // Continue to next strategy
92
- if (i === strategies.length - 1) {
93
- // Last strategy failed, log the error
94
- logger.warn(`${levelPrefix} All JSON extraction strategies failed`);
95
- logger.warn(`${levelPrefix} Response preview: ${response.substring(0, 200)}...`);
96
- }
181
+ strategyErrors.push(`S${i + 1}: ${error.message}`);
97
182
  }
98
183
  }
99
184
 
100
- return {
101
- success: false,
185
+ // All strategies failed — log details for debugging
186
+ logger.warn(`${levelPrefix} All JSON extraction strategies failed`);
187
+ logger.warn(`${levelPrefix} Strategy errors: ${strategyErrors.join('; ')}`);
188
+ logger.warn(`${levelPrefix} Response length: ${response.length} chars, preview: ${response.substring(0, 200)}...`);
189
+
190
+ return {
191
+ success: false,
102
192
  error: 'Failed to extract JSON from response',
103
193
  response: response.substring(0, 500) // Include preview for debugging
104
194
  };