@uluops/core 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +543 -0
- package/definitions/starter/code-validator.agent.yaml +134 -0
- package/definitions/starter/docs-validator.agent.yaml +142 -0
- package/definitions/starter/public-interface-validator.agent.yaml +138 -0
- package/definitions/starter/security-analyst.agent.yaml +144 -0
- package/definitions/starter/test-architect.agent.yaml +137 -0
- package/dist/ai/AIProvider.d.ts +198 -0
- package/dist/ai/AIProvider.d.ts.map +1 -0
- package/dist/ai/AIProvider.js +557 -0
- package/dist/ai/AIProvider.js.map +1 -0
- package/dist/ai/ModelCatalog.d.ts +78 -0
- package/dist/ai/ModelCatalog.d.ts.map +1 -0
- package/dist/ai/ModelCatalog.js +193 -0
- package/dist/ai/ModelCatalog.js.map +1 -0
- package/dist/ai/ShellExecutor.d.ts +42 -0
- package/dist/ai/ShellExecutor.d.ts.map +1 -0
- package/dist/ai/ShellExecutor.js +62 -0
- package/dist/ai/ShellExecutor.js.map +1 -0
- package/dist/ai/TokenBudgetTracker.d.ts +49 -0
- package/dist/ai/TokenBudgetTracker.d.ts.map +1 -0
- package/dist/ai/TokenBudgetTracker.js +61 -0
- package/dist/ai/TokenBudgetTracker.js.map +1 -0
- package/dist/ai/ToolAdapter.d.ts +25 -0
- package/dist/ai/ToolAdapter.d.ts.map +1 -0
- package/dist/ai/ToolAdapter.js +135 -0
- package/dist/ai/ToolAdapter.js.map +1 -0
- package/dist/ai/index.d.ts +6 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +4 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/client/UluOpsClient.d.ts +111 -0
- package/dist/client/UluOpsClient.d.ts.map +1 -0
- package/dist/client/UluOpsClient.js +329 -0
- package/dist/client/UluOpsClient.js.map +1 -0
- package/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +9 -0
- package/dist/constants.js.map +1 -0
- package/dist/errors/UluOpsError.d.ts +10 -0
- package/dist/errors/UluOpsError.d.ts.map +1 -0
- package/dist/errors/UluOpsError.js +13 -0
- package/dist/errors/UluOpsError.js.map +1 -0
- package/dist/errors/index.d.ts +64 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +93 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/executor/AgentExecutor.d.ts +57 -0
- package/dist/executor/AgentExecutor.d.ts.map +1 -0
- package/dist/executor/AgentExecutor.js +331 -0
- package/dist/executor/AgentExecutor.js.map +1 -0
- package/dist/executor/CommandExecutor.d.ts +33 -0
- package/dist/executor/CommandExecutor.d.ts.map +1 -0
- package/dist/executor/CommandExecutor.js +183 -0
- package/dist/executor/CommandExecutor.js.map +1 -0
- package/dist/executor/PipelineExecutor.d.ts +55 -0
- package/dist/executor/PipelineExecutor.d.ts.map +1 -0
- package/dist/executor/PipelineExecutor.js +273 -0
- package/dist/executor/PipelineExecutor.js.map +1 -0
- package/dist/executor/ToolHandler.d.ts +47 -0
- package/dist/executor/ToolHandler.d.ts.map +1 -0
- package/dist/executor/ToolHandler.js +615 -0
- package/dist/executor/ToolHandler.js.map +1 -0
- package/dist/executor/WorkflowExecutor.d.ts +55 -0
- package/dist/executor/WorkflowExecutor.d.ts.map +1 -0
- package/dist/executor/WorkflowExecutor.js +368 -0
- package/dist/executor/WorkflowExecutor.js.map +1 -0
- package/dist/executor/preflight.d.ts +8 -0
- package/dist/executor/preflight.d.ts.map +1 -0
- package/dist/executor/preflight.js +102 -0
- package/dist/executor/preflight.js.map +1 -0
- package/dist/executor/symbols.d.ts +13 -0
- package/dist/executor/symbols.d.ts.map +1 -0
- package/dist/executor/symbols.js +102 -0
- package/dist/executor/symbols.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/OutputExtractor.d.ts +52 -0
- package/dist/parser/OutputExtractor.d.ts.map +1 -0
- package/dist/parser/OutputExtractor.js +818 -0
- package/dist/parser/OutputExtractor.js.map +1 -0
- package/dist/parser/outputSchemas.d.ts +223 -0
- package/dist/parser/outputSchemas.d.ts.map +1 -0
- package/dist/parser/outputSchemas.js +73 -0
- package/dist/parser/outputSchemas.js.map +1 -0
- package/dist/registry/RegistryClient.d.ts +75 -0
- package/dist/registry/RegistryClient.d.ts.map +1 -0
- package/dist/registry/RegistryClient.js +419 -0
- package/dist/registry/RegistryClient.js.map +1 -0
- package/dist/registry/index.d.ts +2 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +2 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/types/agent.d.ts +406 -0
- package/dist/types/agent.d.ts.map +1 -0
- package/dist/types/agent.js +2 -0
- package/dist/types/agent.js.map +1 -0
- package/dist/types/ai.d.ts +14 -0
- package/dist/types/ai.d.ts.map +1 -0
- package/dist/types/ai.js +2 -0
- package/dist/types/ai.js.map +1 -0
- package/dist/types/command.d.ts +153 -0
- package/dist/types/command.d.ts.map +1 -0
- package/dist/types/command.js +2 -0
- package/dist/types/command.js.map +1 -0
- package/dist/types/config.d.ts +136 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +2 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/execution.d.ts +172 -0
- package/dist/types/execution.d.ts.map +1 -0
- package/dist/types/execution.js +2 -0
- package/dist/types/execution.js.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/parser.d.ts +75 -0
- package/dist/types/parser.d.ts.map +1 -0
- package/dist/types/parser.js +2 -0
- package/dist/types/parser.js.map +1 -0
- package/dist/types/pipeline.d.ts +155 -0
- package/dist/types/pipeline.d.ts.map +1 -0
- package/dist/types/pipeline.js +2 -0
- package/dist/types/pipeline.js.map +1 -0
- package/dist/types/registry.d.ts +232 -0
- package/dist/types/registry.d.ts.map +1 -0
- package/dist/types/registry.js +2 -0
- package/dist/types/registry.js.map +1 -0
- package/dist/types/tools.d.ts +29 -0
- package/dist/types/tools.d.ts.map +1 -0
- package/dist/types/tools.js +2 -0
- package/dist/types/tools.js.map +1 -0
- package/dist/types/validation.d.ts +237 -0
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/validation.js +2 -0
- package/dist/types/validation.js.map +1 -0
- package/dist/types/workflow.d.ts +131 -0
- package/dist/types/workflow.d.ts.map +1 -0
- package/dist/types/workflow.js +2 -0
- package/dist/types/workflow.js.map +1 -0
- package/dist/utils/formatError.d.ts +6 -0
- package/dist/utils/formatError.d.ts.map +1 -0
- package/dist/utils/formatError.js +10 -0
- package/dist/utils/formatError.js.map +1 -0
- package/dist/utils/parseRef.d.ts +11 -0
- package/dist/utils/parseRef.d.ts.map +1 -0
- package/dist/utils/parseRef.js +16 -0
- package/dist/utils/parseRef.js.map +1 -0
- package/dist/utils/sumTokenMetrics.d.ts +9 -0
- package/dist/utils/sumTokenMetrics.d.ts.map +1 -0
- package/dist/utils/sumTokenMetrics.js +20 -0
- package/dist/utils/sumTokenMetrics.js.map +1 -0
- package/dist/utils/topoSort.d.ts +24 -0
- package/dist/utils/topoSort.d.ts.map +1 -0
- package/dist/utils/topoSort.js +60 -0
- package/dist/utils/topoSort.js.map +1 -0
- package/dist/validation/ValidationClient.d.ts +51 -0
- package/dist/validation/ValidationClient.d.ts.map +1 -0
- package/dist/validation/ValidationClient.js +179 -0
- package/dist/validation/ValidationClient.js.map +1 -0
- package/dist/validation/index.d.ts +2 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +2 -0
- package/dist/validation/index.js.map +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
import { ParseError } from '../errors/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Extracts structured output from LLM responses using a 3-strategy fallback:
|
|
4
|
+
* 1. JSON code fence (highest confidence)
|
|
5
|
+
* 2. Inline JSON detection
|
|
6
|
+
* 3. Structured text pattern matching (lowest confidence)
|
|
7
|
+
*/
|
|
8
|
+
export class OutputExtractor {
|
|
9
|
+
static INLINE_JSON_PATTERN = /\{[\s\S]*?(?:"decision"|"status"|"score")[\s\S]*?\}/;
|
|
10
|
+
static STRUCTURED_PATTERNS = {
|
|
11
|
+
decision: /(?:decision|status|result)\s*[:=]\s*["']?(\w+)["']?/i,
|
|
12
|
+
// Section-header style: "DECISION" on its own line followed by separator, then decision value
|
|
13
|
+
sectionDecision: /^DECISION\s*\n[━═─\-]+\n+[✅❌⚠️🔴🟡]*\s*(PASS|FAIL|WARN|WARNING|ERROR|SHIP|REJECT|SKIP)\b/im,
|
|
14
|
+
// Emoji-prefixed: "✅ PASS" anywhere in text
|
|
15
|
+
emojiDecision: /[✅❌⚠️🔴🟡🟢]\s*(PASS|FAIL|WARN|WARNING|ERROR|SHIP|REJECT)\b/i,
|
|
16
|
+
score: /(?:score|points)\s*[:=]\s*(\d+(?:\.\d+)?)/i,
|
|
17
|
+
// Score with denominator: "95/100"
|
|
18
|
+
scoreFraction: /(?:score|points)\s*[:=]?\s*(\d+)\s*\/\s*(\d+)/i,
|
|
19
|
+
maxScore: /(?:max(?:imum)?[\s_]?score|out[\s_]?of|total)\s*[:=]\s*(\d+)/i,
|
|
20
|
+
// Issue line: "- description: file/path.ts:123 [CODE]"
|
|
21
|
+
issueLine: /^[\s]*[-•🟡🔴🟠🔵]\s+(.+?):\s+([\w/.-]+\.(?:ts|js|tsx|jsx|py|go|rs|java|rb|css|html|json|yaml|yml|toml|md)):(\d+)\s*(?:\[([^\]]+)\])?/gm,
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Extract structured output from LLM response text
|
|
25
|
+
*/
|
|
26
|
+
extract(content, agentType, options = {}) {
|
|
27
|
+
const result = this.extractWithMetadata(content, agentType, options);
|
|
28
|
+
return result.output;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Extract with full metadata about extraction method and confidence
|
|
32
|
+
*/
|
|
33
|
+
extractWithMetadata(content, agentType, options = {}) {
|
|
34
|
+
const warnings = [];
|
|
35
|
+
// Strategy 1: Try JSON code fence (highest confidence)
|
|
36
|
+
const fenceResult = this.extractFromCodeFence(content, options);
|
|
37
|
+
if (fenceResult) {
|
|
38
|
+
return {
|
|
39
|
+
output: this.normalizeOutput(fenceResult, agentType),
|
|
40
|
+
method: 'json_code_fence',
|
|
41
|
+
confidence: 0.95,
|
|
42
|
+
warnings,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Strategy 1b: Try parsing trimmed content as whole JSON object
|
|
46
|
+
const wholeJsonResult = this.extractWholeJson(content);
|
|
47
|
+
if (wholeJsonResult) {
|
|
48
|
+
return {
|
|
49
|
+
output: this.normalizeOutput(wholeJsonResult, agentType),
|
|
50
|
+
method: 'inline_json',
|
|
51
|
+
confidence: 0.9,
|
|
52
|
+
warnings,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Strategy 2: Try inline JSON detection
|
|
56
|
+
const inlineResult = this.extractInlineJson(content);
|
|
57
|
+
if (inlineResult) {
|
|
58
|
+
warnings.push('Extracted from inline JSON - consider using code fence for reliability');
|
|
59
|
+
return {
|
|
60
|
+
output: this.normalizeOutput(inlineResult, agentType),
|
|
61
|
+
method: 'inline_json',
|
|
62
|
+
confidence: 0.75,
|
|
63
|
+
warnings,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Strategy 3: Fall back to structured text parsing
|
|
67
|
+
const textResult = this.extractFromStructuredText(content, agentType);
|
|
68
|
+
if (textResult) {
|
|
69
|
+
warnings.push('Extracted from structured text patterns - JSON output recommended');
|
|
70
|
+
return {
|
|
71
|
+
output: textResult,
|
|
72
|
+
method: 'structured_text',
|
|
73
|
+
confidence: 0.5,
|
|
74
|
+
warnings,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// Extraction failed
|
|
78
|
+
if (options.strict) {
|
|
79
|
+
throw new ParseError('Failed to extract structured output from response', content.substring(0, 500));
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
output: {
|
|
83
|
+
decision: 'ERROR',
|
|
84
|
+
score: 0,
|
|
85
|
+
},
|
|
86
|
+
method: 'structured_text',
|
|
87
|
+
confidence: 0,
|
|
88
|
+
warnings: ['Could not extract structured output from response'],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
extractFromCodeFence(content, options) {
|
|
92
|
+
const lang = options.codeFenceLanguage ?? 'json';
|
|
93
|
+
const pattern = new RegExp(`\`\`\`(?:${lang})?\\s*\\n([\\s\\S]*?)\\n\`\`\``, 'g');
|
|
94
|
+
const matches = [...content.matchAll(pattern)];
|
|
95
|
+
if (matches.length === 0)
|
|
96
|
+
return null;
|
|
97
|
+
const lastMatch = matches[matches.length - 1];
|
|
98
|
+
if (!lastMatch?.[1])
|
|
99
|
+
return null;
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(lastMatch[1].trim());
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
extractWholeJson(content) {
|
|
108
|
+
const trimmed = content.trim();
|
|
109
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}'))
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(trimmed);
|
|
113
|
+
if (typeof parsed === 'object' && parsed !== null)
|
|
114
|
+
return parsed;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Not valid JSON
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
extractInlineJson(content) {
|
|
122
|
+
const match = content.match(OutputExtractor.INLINE_JSON_PATTERN);
|
|
123
|
+
if (!match)
|
|
124
|
+
return null;
|
|
125
|
+
// For multi-step LLM output, the final JSON report is often at the end.
|
|
126
|
+
// Find all valid JSON objects and pick the best one (largest with agent output fields).
|
|
127
|
+
const candidates = [];
|
|
128
|
+
let searchFrom = content.length - 1;
|
|
129
|
+
for (let found = 0; found < 50 && searchFrom >= 0; searchFrom--) {
|
|
130
|
+
if (content[searchFrom] === '{') {
|
|
131
|
+
const jsonStr = this.extractBalancedJson(content, searchFrom);
|
|
132
|
+
if (jsonStr && jsonStr.length >= 20) {
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(jsonStr);
|
|
135
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
136
|
+
candidates.push({ index: searchFrom, parsed, length: jsonStr.length });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch { /* skip */ }
|
|
140
|
+
}
|
|
141
|
+
found++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Also try from the first regex match
|
|
145
|
+
const firstMatchIndex = content.indexOf(match[0]);
|
|
146
|
+
if (!candidates.some(c => c.index === firstMatchIndex)) {
|
|
147
|
+
const jsonStr = this.extractBalancedJson(content, firstMatchIndex);
|
|
148
|
+
if (jsonStr && jsonStr.length >= 20) {
|
|
149
|
+
try {
|
|
150
|
+
const parsed = JSON.parse(jsonStr);
|
|
151
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
152
|
+
candidates.push({ index: firstMatchIndex, parsed, length: jsonStr.length });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch { /* skip */ }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (candidates.length === 0)
|
|
159
|
+
return null;
|
|
160
|
+
// Prefer the largest JSON object — the final report is typically the biggest.
|
|
161
|
+
// Among ties, prefer objects with more agent-relevant fields.
|
|
162
|
+
const agentFields = ['decision', 'final_decision', 'score', 'status', 'categories',
|
|
163
|
+
'validation_results', 'validation_summary', 'validations', 'validationResults',
|
|
164
|
+
'breakdown', 'issues', 'issues_found', 'summary', 'recommendations'];
|
|
165
|
+
const scored = candidates.map(c => {
|
|
166
|
+
const fieldCount = agentFields.filter(f => f in c.parsed).length;
|
|
167
|
+
return { ...c, fieldCount };
|
|
168
|
+
});
|
|
169
|
+
// Sort by length desc (largest JSON object), then by field count desc
|
|
170
|
+
scored.sort((a, b) => b.length - a.length || b.fieldCount - a.fieldCount);
|
|
171
|
+
return scored[0].parsed;
|
|
172
|
+
}
|
|
173
|
+
extractBalancedJson(content, startIndex) {
|
|
174
|
+
let depth = 0;
|
|
175
|
+
let inString = false;
|
|
176
|
+
let escape = false;
|
|
177
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
178
|
+
const char = content[i];
|
|
179
|
+
if (escape) {
|
|
180
|
+
escape = false;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (char === '\\') {
|
|
184
|
+
escape = true;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (char === '"') {
|
|
188
|
+
inString = !inString;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (inString)
|
|
192
|
+
continue;
|
|
193
|
+
if (char === '{')
|
|
194
|
+
depth++;
|
|
195
|
+
if (char === '}') {
|
|
196
|
+
depth--;
|
|
197
|
+
if (depth === 0) {
|
|
198
|
+
return content.substring(startIndex, i + 1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
extractFromStructuredText(content, agentType) {
|
|
205
|
+
const patterns = OutputExtractor.STRUCTURED_PATTERNS;
|
|
206
|
+
// Try multiple decision patterns in priority order
|
|
207
|
+
const decisionMatch = content.match(patterns.decision)
|
|
208
|
+
?? content.match(patterns.sectionDecision)
|
|
209
|
+
?? content.match(patterns.emojiDecision);
|
|
210
|
+
const scoreMatch = content.match(patterns.scoreFraction)
|
|
211
|
+
?? content.match(patterns.score);
|
|
212
|
+
if (!decisionMatch && !scoreMatch) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
const output = {
|
|
216
|
+
decision: decisionMatch
|
|
217
|
+
? this.normalizeDecision(decisionMatch[1] ?? '', agentType)
|
|
218
|
+
: 'UNKNOWN',
|
|
219
|
+
};
|
|
220
|
+
if (scoreMatch?.[1]) {
|
|
221
|
+
output.score = parseFloat(scoreMatch[1]);
|
|
222
|
+
}
|
|
223
|
+
if (agentType === 'validator') {
|
|
224
|
+
// Extract maxScore from fraction pattern (95/100) or explicit pattern
|
|
225
|
+
if (scoreMatch?.[2]) {
|
|
226
|
+
output.maxScore = parseInt(scoreMatch[2], 10);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
const maxScoreMatch = content.match(patterns.maxScore);
|
|
230
|
+
if (maxScoreMatch?.[1]) {
|
|
231
|
+
output.maxScore = parseInt(maxScoreMatch[1], 10);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Extract issues from structured text (warning/suggestion lines with file:line references)
|
|
235
|
+
const issues = this.extractIssuesFromText(content);
|
|
236
|
+
if (issues.length > 0) {
|
|
237
|
+
output.categories = [{
|
|
238
|
+
name: 'Extracted Issues',
|
|
239
|
+
score: output.score ?? 0,
|
|
240
|
+
maxPoints: output.maxScore ?? 100,
|
|
241
|
+
findings: [{
|
|
242
|
+
criterion: 'Text-extracted findings',
|
|
243
|
+
pointsEarned: 0,
|
|
244
|
+
pointsPossible: 0,
|
|
245
|
+
issues,
|
|
246
|
+
}],
|
|
247
|
+
}];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return output;
|
|
251
|
+
}
|
|
252
|
+
extractIssuesFromText(content) {
|
|
253
|
+
const issues = [];
|
|
254
|
+
const pattern = OutputExtractor.STRUCTURED_PATTERNS.issueLine;
|
|
255
|
+
// Reset lastIndex for global regex
|
|
256
|
+
pattern.lastIndex = 0;
|
|
257
|
+
let match;
|
|
258
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
259
|
+
const [, title, filePath, lineStr, failureCode] = match;
|
|
260
|
+
if (title && filePath) {
|
|
261
|
+
issues.push({
|
|
262
|
+
title: title.trim(),
|
|
263
|
+
priority: this.inferPriorityFromContext(content, match.index),
|
|
264
|
+
severity: this.inferSeverityFromContext(content, match.index),
|
|
265
|
+
failureCode: failureCode?.trim(),
|
|
266
|
+
filePath,
|
|
267
|
+
lineNumber: lineStr ? parseInt(lineStr, 10) : undefined,
|
|
268
|
+
description: title.trim(),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return issues;
|
|
273
|
+
}
|
|
274
|
+
inferPriorityFromContext(content, matchIndex) {
|
|
275
|
+
// Look backwards from match for section headers
|
|
276
|
+
const preceding = content.slice(Math.max(0, matchIndex - 200), matchIndex).toLowerCase();
|
|
277
|
+
if (preceding.includes('critical') || preceding.includes('blocker') || preceding.includes('🔴'))
|
|
278
|
+
return 'critical';
|
|
279
|
+
if (preceding.includes('suggestion') || preceding.includes('consider') || preceding.includes('🔵'))
|
|
280
|
+
return 'backlog';
|
|
281
|
+
return 'suggested';
|
|
282
|
+
}
|
|
283
|
+
inferSeverityFromContext(content, matchIndex) {
|
|
284
|
+
const preceding = content.slice(Math.max(0, matchIndex - 200), matchIndex).toLowerCase();
|
|
285
|
+
if (preceding.includes('critical') || preceding.includes('🔴'))
|
|
286
|
+
return 'critical';
|
|
287
|
+
if (preceding.includes('warning') || preceding.includes('🟡'))
|
|
288
|
+
return 'medium';
|
|
289
|
+
if (preceding.includes('suggestion') || preceding.includes('🔵'))
|
|
290
|
+
return 'low';
|
|
291
|
+
return 'medium';
|
|
292
|
+
}
|
|
293
|
+
/** Resolved source objects from common nesting patterns. Reduces parameter passing across resolve methods. */
|
|
294
|
+
buildParseSources(obj) {
|
|
295
|
+
const result = this.asRecord(obj['result']);
|
|
296
|
+
const summary = this.asRecord(obj['summary']) ?? this.asRecord(result?.['summary']);
|
|
297
|
+
const report = this.asRecord(obj['report']);
|
|
298
|
+
const reportResults = this.asRecord(report?.['results']) ?? this.asRecord(obj['results']);
|
|
299
|
+
const reportSummary = this.asRecord(report?.['summary']) ?? this.asRecord(reportResults?.['summary']);
|
|
300
|
+
const validationSummary = this.findWrapperWithScoreOrDecision(obj);
|
|
301
|
+
return { obj, result, summary, report, reportResults, reportSummary, validationSummary };
|
|
302
|
+
}
|
|
303
|
+
normalizeOutput(raw, agentType) {
|
|
304
|
+
if (!raw || typeof raw !== 'object') {
|
|
305
|
+
return { decision: 'ERROR' };
|
|
306
|
+
}
|
|
307
|
+
const obj = raw;
|
|
308
|
+
const sources = this.buildParseSources(obj);
|
|
309
|
+
const output = {
|
|
310
|
+
decision: this.normalizeDecision(this.resolveDecisionField(sources), agentType),
|
|
311
|
+
rawJson: raw,
|
|
312
|
+
};
|
|
313
|
+
// Resolve score
|
|
314
|
+
const rawScore = this.resolveScoreField(sources);
|
|
315
|
+
if (typeof rawScore === 'number') {
|
|
316
|
+
output.score = rawScore;
|
|
317
|
+
}
|
|
318
|
+
else if (typeof rawScore === 'string') {
|
|
319
|
+
output.score = parseFloat(rawScore);
|
|
320
|
+
}
|
|
321
|
+
if (agentType === 'validator') {
|
|
322
|
+
this.resolveValidatorFields(output, sources);
|
|
323
|
+
}
|
|
324
|
+
if (agentType === 'executor' && Array.isArray(obj['artifacts'])) {
|
|
325
|
+
output.artifacts = this.parseArtifacts(obj['artifacts']);
|
|
326
|
+
}
|
|
327
|
+
return output;
|
|
328
|
+
}
|
|
329
|
+
resolveValidatorFields(output, sources) {
|
|
330
|
+
const { obj, result, summary, report } = sources;
|
|
331
|
+
// Resolve maxScore
|
|
332
|
+
const rawMaxScore = obj['maxScore'] ?? obj['max_score']
|
|
333
|
+
?? result?.['max_score'] ?? result?.['maxScore']
|
|
334
|
+
?? summary?.['max_score'] ?? summary?.['maxScore']
|
|
335
|
+
?? obj['pass_threshold'];
|
|
336
|
+
if (typeof rawMaxScore === 'number') {
|
|
337
|
+
output.maxScore = rawMaxScore;
|
|
338
|
+
}
|
|
339
|
+
else if (typeof rawMaxScore === 'string') {
|
|
340
|
+
output.maxScore = parseInt(rawMaxScore, 10);
|
|
341
|
+
}
|
|
342
|
+
// Resolve categories
|
|
343
|
+
output.categories = this.resolveCategories(obj, result, report);
|
|
344
|
+
// If no score found but categories exist, sum category scores
|
|
345
|
+
if (output.score === undefined && output.categories && output.categories.length > 0) {
|
|
346
|
+
output.score = output.categories.reduce((sum, c) => sum + c.score, 0);
|
|
347
|
+
}
|
|
348
|
+
// Resolve flat issues and attach to categories
|
|
349
|
+
this.attachFlatIssues(output, sources);
|
|
350
|
+
}
|
|
351
|
+
attachFlatIssues(output, sources) {
|
|
352
|
+
const flatIssues = this.resolveIssuesFlat(sources.obj, sources.result, sources.report, sources.validationSummary);
|
|
353
|
+
if (flatIssues.length === 0)
|
|
354
|
+
return;
|
|
355
|
+
const issuesFinding = {
|
|
356
|
+
criterion: 'Extracted findings',
|
|
357
|
+
pointsEarned: 0,
|
|
358
|
+
pointsPossible: 0,
|
|
359
|
+
issues: flatIssues,
|
|
360
|
+
};
|
|
361
|
+
if (!output.categories || output.categories.length === 0) {
|
|
362
|
+
output.categories = [{
|
|
363
|
+
name: 'Extracted Issues',
|
|
364
|
+
score: output.score ?? 0,
|
|
365
|
+
maxPoints: output.maxScore ?? 100,
|
|
366
|
+
findings: [issuesFinding],
|
|
367
|
+
}];
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
const emptyCategory = output.categories.find(c => c.findings.length === 0);
|
|
371
|
+
if (emptyCategory) {
|
|
372
|
+
emptyCategory.findings.push(issuesFinding);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
output.categories.push({
|
|
376
|
+
name: 'Extracted Issues',
|
|
377
|
+
score: output.score ?? 0,
|
|
378
|
+
maxPoints: output.maxScore ?? 100,
|
|
379
|
+
findings: [issuesFinding],
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
asRecord(value) {
|
|
385
|
+
return (value && typeof value === 'object' && !Array.isArray(value))
|
|
386
|
+
? value
|
|
387
|
+
: undefined;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Scan all top-level object values for one that contains 'score' or 'decision'.
|
|
391
|
+
* Handles arbitrary wrapper names (validation, validations, validationResults, etc.)
|
|
392
|
+
* without whitelisting specific field names.
|
|
393
|
+
*/
|
|
394
|
+
findWrapperWithScoreOrDecision(obj) {
|
|
395
|
+
// Skip known non-wrapper fields
|
|
396
|
+
const skip = new Set(['issues', 'categories', 'recommendations', 'evidence',
|
|
397
|
+
'reasoning', 'reasoning_trace', 'notes', 'auto_fail_conditions', 'filesReviewed',
|
|
398
|
+
'files_reviewed', 'artifacts', 'result', 'summary', 'report']);
|
|
399
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
400
|
+
if (skip.has(key))
|
|
401
|
+
continue;
|
|
402
|
+
const rec = this.asRecord(value);
|
|
403
|
+
if (!rec)
|
|
404
|
+
continue;
|
|
405
|
+
if ('score' in rec || 'score_total' in rec || 'total_score' in rec || 'decision' in rec || 'status' in rec || 'breakdown' in rec || 'score_breakdown' in rec) {
|
|
406
|
+
return rec;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
resolveDecisionField(ctx) {
|
|
412
|
+
const { obj } = ctx;
|
|
413
|
+
const sources = [ctx.obj, ctx.summary, ctx.result, ctx.report, ctx.reportResults, ctx.reportSummary, ctx.validationSummary];
|
|
414
|
+
// Check each source for decision/final_decision fields
|
|
415
|
+
for (const source of sources) {
|
|
416
|
+
if (!source)
|
|
417
|
+
continue;
|
|
418
|
+
for (const key of ['decision', 'final_decision']) {
|
|
419
|
+
const d = source[key];
|
|
420
|
+
if (typeof d === 'string')
|
|
421
|
+
return d;
|
|
422
|
+
// Handle decision as object: { pass: true, label: "PASS" } or { result: "PASS" }
|
|
423
|
+
if (d && typeof d === 'object') {
|
|
424
|
+
const dObj = d;
|
|
425
|
+
if (typeof dObj['result'] === 'string')
|
|
426
|
+
return dObj['result'];
|
|
427
|
+
if (typeof dObj['label'] === 'string')
|
|
428
|
+
return dObj['label'];
|
|
429
|
+
if (typeof dObj['value'] === 'string')
|
|
430
|
+
return dObj['value'];
|
|
431
|
+
if (typeof dObj['status'] === 'string')
|
|
432
|
+
return dObj['status'];
|
|
433
|
+
if (typeof dObj['pass'] === 'boolean')
|
|
434
|
+
return dObj['pass'] ? 'PASS' : 'FAIL';
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// Fallback to status field
|
|
439
|
+
for (const source of sources) {
|
|
440
|
+
if (!source)
|
|
441
|
+
continue;
|
|
442
|
+
if (typeof source['status'] === 'string')
|
|
443
|
+
return source['status'];
|
|
444
|
+
}
|
|
445
|
+
// Fallback: check if summary is a string starting with a decision word
|
|
446
|
+
if (typeof obj['summary'] === 'string') {
|
|
447
|
+
const summaryFirst = obj['summary'].split(/[\s\-–—]+/)[0]?.toUpperCase();
|
|
448
|
+
if (summaryFirst && ['PASS', 'FAIL', 'WARN', 'ERROR', 'COMPLETE'].includes(summaryFirst)) {
|
|
449
|
+
return summaryFirst;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return 'UNKNOWN';
|
|
453
|
+
}
|
|
454
|
+
resolveScoreField(ctx) {
|
|
455
|
+
const { obj } = ctx;
|
|
456
|
+
const sources = [ctx.obj, ctx.summary, ctx.result, ctx.report, ctx.reportResults, ctx.reportSummary, ctx.validationSummary];
|
|
457
|
+
// Check each source for a score value
|
|
458
|
+
for (const source of sources) {
|
|
459
|
+
if (!source)
|
|
460
|
+
continue;
|
|
461
|
+
for (const scoreKey of ['score', 'total_score', 'score_total']) {
|
|
462
|
+
const s = source[scoreKey];
|
|
463
|
+
if (typeof s === 'number')
|
|
464
|
+
return s;
|
|
465
|
+
if (typeof s === 'string' && !isNaN(parseFloat(s)))
|
|
466
|
+
return s;
|
|
467
|
+
// Handle score as object: { total: 85, ... }
|
|
468
|
+
if (s && typeof s === 'object') {
|
|
469
|
+
const sObj = s;
|
|
470
|
+
for (const key of ['total', 'value', 'overall', 'final']) {
|
|
471
|
+
if (typeof sObj[key] === 'number')
|
|
472
|
+
return sObj[key];
|
|
473
|
+
if (typeof sObj[key] === 'string')
|
|
474
|
+
return sObj[key];
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// Note: validationSummary (from findWrapperWithScoreOrDecision) is already in the sources loop above.
|
|
480
|
+
// Check scores object with named sub-scores (gpt-4.1-nano shape: { scores: { "Code Quality": 23, ... } })
|
|
481
|
+
const scores = this.asRecord(obj['scores']);
|
|
482
|
+
if (scores) {
|
|
483
|
+
if (typeof scores['Total'] === 'number')
|
|
484
|
+
return scores['Total'];
|
|
485
|
+
if (typeof scores['total'] === 'number')
|
|
486
|
+
return scores['total'];
|
|
487
|
+
}
|
|
488
|
+
// Check breakdown with sub-scores sum (search wrapper objects too)
|
|
489
|
+
const wrapper = this.findWrapperWithScoreOrDecision(obj);
|
|
490
|
+
const breakdown = this.asRecord(obj['breakdown'])
|
|
491
|
+
?? this.asRecord(obj['score_breakdown'])
|
|
492
|
+
?? this.asRecord(wrapper?.['breakdown'])
|
|
493
|
+
?? this.asRecord(wrapper?.['score_breakdown']);
|
|
494
|
+
if (breakdown) {
|
|
495
|
+
const values = [];
|
|
496
|
+
for (const v of Object.values(breakdown)) {
|
|
497
|
+
if (typeof v === 'number') {
|
|
498
|
+
values.push(v);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
// Handle { points: N, deductions: N } shape
|
|
502
|
+
const rec = this.asRecord(v);
|
|
503
|
+
if (rec && typeof rec['points'] === 'number') {
|
|
504
|
+
const deductions = typeof rec['deductions'] === 'number' ? rec['deductions'] : 0;
|
|
505
|
+
values.push(rec['points'] - deductions);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (values.length > 0)
|
|
509
|
+
return values.reduce((a, b) => a + b, 0);
|
|
510
|
+
}
|
|
511
|
+
// Check criteria with sub-scores sum (gpt-4.1-nano shape)
|
|
512
|
+
const criteria = this.asRecord(obj['criteria']);
|
|
513
|
+
if (criteria) {
|
|
514
|
+
const values = [];
|
|
515
|
+
for (const v of Object.values(criteria)) {
|
|
516
|
+
if (typeof v === 'number') {
|
|
517
|
+
values.push(v);
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
const rec = this.asRecord(v);
|
|
521
|
+
if (rec && typeof rec['score'] === 'number')
|
|
522
|
+
values.push(rec['score']);
|
|
523
|
+
}
|
|
524
|
+
if (values.length > 0)
|
|
525
|
+
return values.reduce((a, b) => a + b, 0);
|
|
526
|
+
}
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
resolveCategories(obj, result, report) {
|
|
530
|
+
// Direct categories array
|
|
531
|
+
for (const source of [obj, result, report]) {
|
|
532
|
+
if (!source)
|
|
533
|
+
continue;
|
|
534
|
+
if (Array.isArray(source['categories'])) {
|
|
535
|
+
return this.parseCategories(source['categories']);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// Named scores object → synthetic categories (e.g., { scores: { "Code Quality": 23, ... } })
|
|
539
|
+
const scores = this.asRecord(obj['scores']) ?? this.asRecord(report?.['scores']);
|
|
540
|
+
if (scores) {
|
|
541
|
+
const cats = [];
|
|
542
|
+
for (const [name, value] of Object.entries(scores)) {
|
|
543
|
+
if (typeof value === 'number' && name !== 'Total' && name !== 'total' && name !== 'pass_threshold') {
|
|
544
|
+
cats.push({ name, score: value, maxPoints: 100, findings: [] });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (cats.length > 0)
|
|
548
|
+
return cats;
|
|
549
|
+
}
|
|
550
|
+
// Breakdown object → synthetic categories
|
|
551
|
+
// Search top-level breakdown, score_breakdown, and inside any wrapper object
|
|
552
|
+
const wrapper = this.findWrapperWithScoreOrDecision(obj);
|
|
553
|
+
const breakdown = this.asRecord(obj['breakdown'])
|
|
554
|
+
?? this.asRecord(obj['score_breakdown'])
|
|
555
|
+
?? this.asRecord(wrapper?.['breakdown'])
|
|
556
|
+
?? this.asRecord(wrapper?.['score_breakdown']);
|
|
557
|
+
if (breakdown) {
|
|
558
|
+
const cats = [];
|
|
559
|
+
for (const [name, value] of Object.entries(breakdown)) {
|
|
560
|
+
if (typeof value === 'number') {
|
|
561
|
+
cats.push({ name, score: value, maxPoints: 100, findings: [] });
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
// Handle { points: N, deductions: N } shape
|
|
565
|
+
const rec = this.asRecord(value);
|
|
566
|
+
if (rec && typeof rec['points'] === 'number') {
|
|
567
|
+
const points = rec['points'];
|
|
568
|
+
const deductions = typeof rec['deductions'] === 'number' ? rec['deductions'] : 0;
|
|
569
|
+
cats.push({ name, score: points - deductions, maxPoints: 100, findings: [] });
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (cats.length > 0)
|
|
574
|
+
return cats;
|
|
575
|
+
}
|
|
576
|
+
// Criteria object with nested scores → synthetic categories
|
|
577
|
+
const criteria = this.asRecord(obj['criteria']);
|
|
578
|
+
if (criteria) {
|
|
579
|
+
const cats = [];
|
|
580
|
+
for (const [name, value] of Object.entries(criteria)) {
|
|
581
|
+
const rec = this.asRecord(value);
|
|
582
|
+
if (rec && typeof rec['score'] === 'number') {
|
|
583
|
+
cats.push({
|
|
584
|
+
name,
|
|
585
|
+
score: rec['score'],
|
|
586
|
+
maxPoints: 100,
|
|
587
|
+
findings: this.parseIssues(Array.isArray(rec['issues']) ? rec['issues'] : []).map(issue => ({
|
|
588
|
+
criterion: issue.title,
|
|
589
|
+
pointsEarned: 0,
|
|
590
|
+
pointsPossible: 0,
|
|
591
|
+
issues: [issue],
|
|
592
|
+
})),
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (cats.length > 0)
|
|
597
|
+
return cats;
|
|
598
|
+
}
|
|
599
|
+
return undefined;
|
|
600
|
+
}
|
|
601
|
+
resolveIssuesFlat(obj, result, report, wrapper) {
|
|
602
|
+
const issues = [];
|
|
603
|
+
// Check multiple issue-like keys across all source objects
|
|
604
|
+
const issueKeys = ['issues', 'recommendations', 'warnings', 'findings'];
|
|
605
|
+
for (const source of [obj, result, report, wrapper]) {
|
|
606
|
+
if (!source)
|
|
607
|
+
continue;
|
|
608
|
+
for (const key of issueKeys) {
|
|
609
|
+
if (Array.isArray(source[key])) {
|
|
610
|
+
issues.push(...this.parseIssues(source[key]));
|
|
611
|
+
if (issues.length > 0)
|
|
612
|
+
return issues;
|
|
613
|
+
}
|
|
614
|
+
// Nested: { issues: { items: [...] } } or { issues: { details: [...] } }
|
|
615
|
+
const nested = this.asRecord(source[key]);
|
|
616
|
+
if (nested) {
|
|
617
|
+
const nestedArray = nested['items'] ?? nested['details'] ?? nested['list'];
|
|
618
|
+
if (Array.isArray(nestedArray)) {
|
|
619
|
+
issues.push(...this.parseIssues(nestedArray));
|
|
620
|
+
if (issues.length > 0)
|
|
621
|
+
return issues;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
// issues_found with warnings/suggestions (gpt-5-mini shape)
|
|
627
|
+
for (const source of [obj, wrapper]) {
|
|
628
|
+
if (!source)
|
|
629
|
+
continue;
|
|
630
|
+
const issuesFound = this.asRecord(source['issues_found']);
|
|
631
|
+
if (issuesFound) {
|
|
632
|
+
if (Array.isArray(issuesFound['critical'])) {
|
|
633
|
+
issues.push(...this.parseIssues(issuesFound['critical']));
|
|
634
|
+
}
|
|
635
|
+
if (Array.isArray(issuesFound['warnings'])) {
|
|
636
|
+
issues.push(...this.parseIssues(issuesFound['warnings']));
|
|
637
|
+
}
|
|
638
|
+
if (Array.isArray(issuesFound['suggestions'])) {
|
|
639
|
+
issues.push(...this.parseIssues(issuesFound['suggestions']));
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return issues;
|
|
644
|
+
}
|
|
645
|
+
normalizeDecision(decision, agentType) {
|
|
646
|
+
// Strip emojis and non-ASCII symbols before processing
|
|
647
|
+
const cleaned = decision.replace(/[\u{1F000}-\u{1FFFF}]|[\u{2600}-\u{27BF}]|[\u{FE00}-\u{FE0F}]|[\u{200D}]/gu, '').trim();
|
|
648
|
+
const upper = cleaned.toUpperCase().trim();
|
|
649
|
+
// Extract first word for labels like "PASS - Ready for next phase"
|
|
650
|
+
const firstWord = upper.split(/[\s\-–—]+/)[0] ?? upper;
|
|
651
|
+
if (agentType === 'validator') {
|
|
652
|
+
if (['PASS', 'PASSED', 'OK', 'SUCCESS'].includes(firstWord))
|
|
653
|
+
return 'PASS';
|
|
654
|
+
if (['WARN', 'WARNING', 'CAUTION'].includes(firstWord))
|
|
655
|
+
return 'WARN';
|
|
656
|
+
if (['FAIL', 'FAILED', 'ERROR', 'REJECT'].includes(firstWord))
|
|
657
|
+
return 'FAIL';
|
|
658
|
+
}
|
|
659
|
+
if (agentType === 'executor') {
|
|
660
|
+
if (['SUCCESS', 'COMPLETE', 'DONE', 'PASS'].includes(firstWord))
|
|
661
|
+
return 'COMPLETE';
|
|
662
|
+
if (['PARTIAL', 'INCOMPLETE'].includes(firstWord))
|
|
663
|
+
return 'PARTIAL';
|
|
664
|
+
if (['FAIL', 'FAILED', 'ERROR'].includes(firstWord))
|
|
665
|
+
return 'FAILED';
|
|
666
|
+
}
|
|
667
|
+
return upper;
|
|
668
|
+
}
|
|
669
|
+
parseCategories(raw) {
|
|
670
|
+
return raw
|
|
671
|
+
.filter((item) => typeof item === 'object' && item !== null)
|
|
672
|
+
.map(item => ({
|
|
673
|
+
name: String(item['name'] ?? item['category'] ?? 'Unknown'),
|
|
674
|
+
score: Number(item['score'] ?? item['points'] ?? 0),
|
|
675
|
+
maxPoints: Number(item['maxPoints'] ?? item['max_points'] ?? item['total'] ?? 100),
|
|
676
|
+
findings: this.parseFindings(Array.isArray(item['findings']) ? item['findings'] : []),
|
|
677
|
+
}));
|
|
678
|
+
}
|
|
679
|
+
parseFindings(raw) {
|
|
680
|
+
return raw
|
|
681
|
+
.filter((item) => typeof item === 'object' && item !== null)
|
|
682
|
+
.map(item => ({
|
|
683
|
+
criterion: String(item['criterion'] ?? item['name'] ?? 'Unknown'),
|
|
684
|
+
pointsEarned: Number(item['pointsEarned'] ?? item['points_earned'] ?? item['score'] ?? 0),
|
|
685
|
+
pointsPossible: Number(item['pointsPossible'] ?? item['points_possible'] ?? item['maxPoints'] ?? 0),
|
|
686
|
+
issues: this.parseIssues(Array.isArray(item['issues']) ? item['issues'] : []),
|
|
687
|
+
}));
|
|
688
|
+
}
|
|
689
|
+
parseIssues(raw) {
|
|
690
|
+
// Flatten grouped issues: [{severity: "CRITICAL", issues: [...]}, ...] → flat array
|
|
691
|
+
const flatItems = [];
|
|
692
|
+
for (const item of raw) {
|
|
693
|
+
if (typeof item !== 'object' || item === null)
|
|
694
|
+
continue;
|
|
695
|
+
const rec = item;
|
|
696
|
+
if (Array.isArray(rec['issues'])) {
|
|
697
|
+
// This is a group — recurse into the nested issues array, inheriting severity
|
|
698
|
+
const groupSeverity = rec['severity'];
|
|
699
|
+
for (const sub of rec['issues']) {
|
|
700
|
+
if (typeof sub === 'object' && sub !== null) {
|
|
701
|
+
const subRec = sub;
|
|
702
|
+
if (groupSeverity && !subRec['severity'])
|
|
703
|
+
subRec['severity'] = groupSeverity;
|
|
704
|
+
flatItems.push(subRec);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
flatItems.push(rec);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return flatItems
|
|
713
|
+
.map(item => {
|
|
714
|
+
// Resolve file path and line number from various shapes
|
|
715
|
+
let filePath = item['filePath']
|
|
716
|
+
?? item['file_path']
|
|
717
|
+
?? item['file'];
|
|
718
|
+
let lineNumber = typeof item['lineNumber'] === 'number'
|
|
719
|
+
? item['lineNumber']
|
|
720
|
+
: typeof item['line_number'] === 'number'
|
|
721
|
+
? item['line_number']
|
|
722
|
+
: typeof item['line'] === 'number'
|
|
723
|
+
? item['line']
|
|
724
|
+
: typeof item['line_start'] === 'number'
|
|
725
|
+
? item['line_start']
|
|
726
|
+
: undefined;
|
|
727
|
+
// Handle line as string: "24-50" or "24"
|
|
728
|
+
if (lineNumber === undefined && typeof item['line'] === 'string') {
|
|
729
|
+
const lineMatch = item['line'].match(/^(\d+)/);
|
|
730
|
+
if (lineMatch)
|
|
731
|
+
lineNumber = parseInt(lineMatch[1], 10);
|
|
732
|
+
}
|
|
733
|
+
if (lineNumber === undefined && typeof item['line_number'] === 'string') {
|
|
734
|
+
const lineMatch = item['line_number'].match(/^(\d+)/);
|
|
735
|
+
if (lineMatch)
|
|
736
|
+
lineNumber = parseInt(lineMatch[1], 10);
|
|
737
|
+
}
|
|
738
|
+
if (lineNumber === undefined && typeof item['lineNumber'] === 'string') {
|
|
739
|
+
const lineMatch = item['lineNumber'].match(/^(\d+)/);
|
|
740
|
+
if (lineMatch)
|
|
741
|
+
lineNumber = parseInt(lineMatch[1], 10);
|
|
742
|
+
}
|
|
743
|
+
// Handle combined fields: "file_line" or "location" like "src/foo.ts:42-50"
|
|
744
|
+
for (const combinedKey of ['file_line', 'location']) {
|
|
745
|
+
if (!filePath && typeof item[combinedKey] === 'string') {
|
|
746
|
+
const flMatch = item[combinedKey].match(/^([\w/.@-]+\.\w+):(\d+)/);
|
|
747
|
+
if (flMatch) {
|
|
748
|
+
filePath = flMatch[1];
|
|
749
|
+
lineNumber = lineNumber ?? parseInt(flMatch[2], 10);
|
|
750
|
+
}
|
|
751
|
+
else if (combinedKey === 'file_line') {
|
|
752
|
+
filePath = item[combinedKey];
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// Handle locations array: [{ file: "...", line_start: N }]
|
|
757
|
+
if (!filePath && Array.isArray(item['locations']) && item['locations'].length > 0) {
|
|
758
|
+
const loc = this.asRecord(item['locations'][0]);
|
|
759
|
+
if (loc) {
|
|
760
|
+
filePath = loc['file'] ?? loc['filePath'];
|
|
761
|
+
lineNumber = lineNumber ?? (typeof loc['line_start'] === 'number' ? loc['line_start'] : undefined);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
// Resolve title: prefer explicit title/message, fall back to issue/summary/description/name
|
|
765
|
+
const hasExplicitTitle = item['title'] !== undefined || item['message'] !== undefined
|
|
766
|
+
|| item['issue'] !== undefined || item['summary'] !== undefined;
|
|
767
|
+
const title = String(item['title'] ?? item['message'] ?? item['issue'] ?? item['summary']
|
|
768
|
+
?? item['name'] ?? item['description'] ?? 'Untitled Issue');
|
|
769
|
+
// For description: if title consumed 'description', use explanation/suggestion/recommendation instead
|
|
770
|
+
const detailsStr = typeof item['details'] === 'string' ? item['details'] : undefined;
|
|
771
|
+
const description = hasExplicitTitle
|
|
772
|
+
? String(item['description'] ?? detailsStr ?? item['explanation'] ?? item['suggestion'] ?? item['recommendation'] ?? '')
|
|
773
|
+
: String(item['explanation'] ?? detailsStr ?? item['suggestion'] ?? item['recommendation'] ?? item['description'] ?? '');
|
|
774
|
+
return {
|
|
775
|
+
title,
|
|
776
|
+
priority: this.normalizePriority(item['priority'] ?? item['type']),
|
|
777
|
+
severity: this.normalizeSeverity(item['severity']),
|
|
778
|
+
failureCode: item['failureCode']
|
|
779
|
+
?? item['failure_code']
|
|
780
|
+
?? item['code'],
|
|
781
|
+
filePath,
|
|
782
|
+
lineNumber,
|
|
783
|
+
description,
|
|
784
|
+
};
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
parseArtifacts(raw) {
|
|
788
|
+
return raw
|
|
789
|
+
.filter((item) => typeof item === 'object' && item !== null)
|
|
790
|
+
.map(item => ({
|
|
791
|
+
name: String(item['name'] ?? 'Untitled'),
|
|
792
|
+
path: String(item['path'] ?? ''),
|
|
793
|
+
size: typeof item['size'] === 'number' ? item['size'] : undefined,
|
|
794
|
+
contentType: item['contentType'] ?? item['content_type'],
|
|
795
|
+
}));
|
|
796
|
+
}
|
|
797
|
+
normalizePriority(value) {
|
|
798
|
+
const str = String(value ?? 'suggested').toLowerCase();
|
|
799
|
+
if (['critical', 'high', 'p0'].includes(str))
|
|
800
|
+
return 'critical';
|
|
801
|
+
if (['backlog', 'low', 'p2'].includes(str))
|
|
802
|
+
return 'backlog';
|
|
803
|
+
return 'suggested';
|
|
804
|
+
}
|
|
805
|
+
normalizeSeverity(value) {
|
|
806
|
+
const str = String(value ?? 'medium').toLowerCase();
|
|
807
|
+
if (str === 'critical')
|
|
808
|
+
return 'critical';
|
|
809
|
+
if (str === 'high')
|
|
810
|
+
return 'high';
|
|
811
|
+
if (str === 'low')
|
|
812
|
+
return 'low';
|
|
813
|
+
if (['info', 'informational', 'note'].includes(str))
|
|
814
|
+
return 'info';
|
|
815
|
+
return 'medium';
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
//# sourceMappingURL=OutputExtractor.js.map
|