algomath-extract 1.0.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 +260 -0
- package/bin/algo-extract.js +143 -0
- package/bin/algo-generate.js +102 -0
- package/bin/algo-help.js +136 -0
- package/bin/algo-list.js +56 -0
- package/bin/algo-run.js +141 -0
- package/bin/algo-status.js +88 -0
- package/bin/algo-verify.js +189 -0
- package/bin/install.js +349 -0
- package/package.json +57 -0
- package/requirements.txt +20 -0
- package/src/__pycache__/intent.cpython-313.pyc +0 -0
- package/src/cli/__pycache__/commands.cpython-313.pyc +0 -0
- package/src/cli/cli_entry.py +106 -0
- package/src/cli/commands.py +339 -0
- package/src/execution/__init__.py +74 -0
- package/src/execution/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/execution/__pycache__/display.cpython-313.pyc +0 -0
- package/src/execution/__pycache__/errors.cpython-313.pyc +0 -0
- package/src/execution/__pycache__/executor.cpython-313.pyc +0 -0
- package/src/execution/__pycache__/sandbox.cpython-313.pyc +0 -0
- package/src/execution/display.py +261 -0
- package/src/execution/errors.py +158 -0
- package/src/execution/executor.py +253 -0
- package/src/execution/sandbox.py +333 -0
- package/src/extraction/__init__.py +102 -0
- package/src/extraction/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/boundaries.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/errors.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/llm_extraction.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/notation.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/parser.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/pdf_processor.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/prompts.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/review.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/schema.cpython-313.pyc +0 -0
- package/src/extraction/__pycache__/validation.cpython-313.pyc +0 -0
- package/src/extraction/boundaries.py +281 -0
- package/src/extraction/errors.py +156 -0
- package/src/extraction/llm_extraction.py +225 -0
- package/src/extraction/notation.py +240 -0
- package/src/extraction/parser.py +402 -0
- package/src/extraction/pdf_processor.py +281 -0
- package/src/extraction/prompts.py +90 -0
- package/src/extraction/review.py +298 -0
- package/src/extraction/schema.py +173 -0
- package/src/extraction/validation.py +202 -0
- package/src/generation/__init__.py +79 -0
- package/src/generation/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/code_generator.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/errors.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/hybrid.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/llm_generator.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/persistence.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/prompts.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/review.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/templates.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/types.cpython-313.pyc +0 -0
- package/src/generation/__pycache__/validation.cpython-313.pyc +0 -0
- package/src/generation/code_generator.py +375 -0
- package/src/generation/errors.py +84 -0
- package/src/generation/hybrid.py +210 -0
- package/src/generation/llm_generator.py +223 -0
- package/src/generation/persistence.py +221 -0
- package/src/generation/prompts.py +202 -0
- package/src/generation/review.py +254 -0
- package/src/generation/templates.py +208 -0
- package/src/generation/types.py +196 -0
- package/src/generation/validation.py +278 -0
- package/src/intent.py +323 -0
- package/src/verification/__init__.py +63 -0
- package/src/verification/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/verification/__pycache__/checker.cpython-313.pyc +0 -0
- package/src/verification/__pycache__/comparison.cpython-313.pyc +0 -0
- package/src/verification/__pycache__/explainer.cpython-313.pyc +0 -0
- package/src/verification/__pycache__/static_analysis.cpython-313.pyc +0 -0
- package/src/verification/checker.py +220 -0
- package/src/verification/comparison.py +492 -0
- package/src/verification/explainer.py +414 -0
- package/src/verification/static_analysis.py +540 -0
- package/src/workflows/__init__.py +21 -0
- package/src/workflows/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/workflows/__pycache__/extract.cpython-313.pyc +0 -0
- package/src/workflows/__pycache__/generate.cpython-313.pyc +0 -0
- package/src/workflows/__pycache__/run.cpython-313.pyc +0 -0
- package/src/workflows/__pycache__/verify.cpython-313.pyc +0 -0
- package/src/workflows/extract.py +181 -0
- package/src/workflows/generate.py +155 -0
- package/src/workflows/run.py +187 -0
- package/src/workflows/verify.py +334 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""Expected results comparison module for AlgoMath.
|
|
2
|
+
|
|
3
|
+
Per VER-02: Compare output against expected results.
|
|
4
|
+
Implements decisions D-09, D-10, D-11, D-12 from 05-CONTEXT.md.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import difflib
|
|
8
|
+
import json
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any, Dict, List, Optional, Union
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ComparisonStatus(Enum):
|
|
15
|
+
"""Comparison status enumeration.
|
|
16
|
+
|
|
17
|
+
MATCH: Expected and actual values match completely
|
|
18
|
+
MISMATCH: Expected and actual values differ
|
|
19
|
+
PARTIAL: Some fields match, some differ (for structured data)
|
|
20
|
+
NOT_PROVIDED: No expected value was provided
|
|
21
|
+
"""
|
|
22
|
+
MATCH = "match"
|
|
23
|
+
MISMATCH = "mismatch"
|
|
24
|
+
PARTIAL = "partial"
|
|
25
|
+
NOT_PROVIDED = "not_provided"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ComparisonResult:
|
|
30
|
+
"""Result of comparing expected vs actual output.
|
|
31
|
+
|
|
32
|
+
Per D-11: Shows expected vs actual with highlight of differences.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
status: Overall comparison status
|
|
36
|
+
expected: The expected value (what user provided)
|
|
37
|
+
actual: The actual value (what execution produced)
|
|
38
|
+
diff: Unified diff string showing differences
|
|
39
|
+
match_percentage: Float from 0.0 to 1.0 indicating match quality
|
|
40
|
+
differences: List of per-field differences for structured data
|
|
41
|
+
"""
|
|
42
|
+
status: ComparisonStatus
|
|
43
|
+
expected: Any
|
|
44
|
+
actual: Any
|
|
45
|
+
diff: Optional[str] = None
|
|
46
|
+
match_percentage: float = 0.0
|
|
47
|
+
differences: List[Dict[str, Any]] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
def format_inline(self) -> str:
|
|
50
|
+
"""Format comparison result for inline display per D-11.
|
|
51
|
+
|
|
52
|
+
Shows expected vs actual with markdown formatting.
|
|
53
|
+
For visual distinction: green (match) / red (differ) indicators.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Formatted string with clear visual indicators
|
|
57
|
+
"""
|
|
58
|
+
if self.status == ComparisonStatus.MATCH:
|
|
59
|
+
return f"✅ **Match**: Expected and actual values are identical."
|
|
60
|
+
elif self.status == ComparisonStatus.NOT_PROVIDED:
|
|
61
|
+
return "⚠️ **No expected value provided**: Cannot perform comparison."
|
|
62
|
+
elif self.status == ComparisonStatus.MISMATCH:
|
|
63
|
+
lines = [
|
|
64
|
+
f"❌ **Mismatch**: Expected and actual values differ.",
|
|
65
|
+
f"",
|
|
66
|
+
f"**Expected:**",
|
|
67
|
+
f"```",
|
|
68
|
+
f"{self._format_value(self.expected)}",
|
|
69
|
+
f"```",
|
|
70
|
+
f"",
|
|
71
|
+
f"**Actual:**",
|
|
72
|
+
f"```",
|
|
73
|
+
f"{self._format_value(self.actual)}",
|
|
74
|
+
f"```",
|
|
75
|
+
]
|
|
76
|
+
if self.diff:
|
|
77
|
+
lines.extend([
|
|
78
|
+
f"",
|
|
79
|
+
f"**Differences:**",
|
|
80
|
+
f"```diff",
|
|
81
|
+
f"{self.diff}",
|
|
82
|
+
f"```",
|
|
83
|
+
])
|
|
84
|
+
return "\n".join(lines)
|
|
85
|
+
elif self.status == ComparisonStatus.PARTIAL:
|
|
86
|
+
return f"⚡ **Partial Match**: {self.match_percentage*100:.1f}% of fields match."
|
|
87
|
+
else:
|
|
88
|
+
return f"Comparison result: {self.status.value}"
|
|
89
|
+
|
|
90
|
+
def _format_value(self, value: Any) -> str:
|
|
91
|
+
"""Format a value for display."""
|
|
92
|
+
if isinstance(value, str):
|
|
93
|
+
return value
|
|
94
|
+
try:
|
|
95
|
+
return json.dumps(value, indent=2, default=str)
|
|
96
|
+
except:
|
|
97
|
+
return str(value)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class OutputComparator:
|
|
101
|
+
"""Comparator for expected vs actual output.
|
|
102
|
+
|
|
103
|
+
Performs comparison with support for:
|
|
104
|
+
- Exact string matching
|
|
105
|
+
- Numeric tolerance (for floating point comparisons)
|
|
106
|
+
- Multi-line string diff with line numbers
|
|
107
|
+
- Structured data comparison (dicts, lists)
|
|
108
|
+
|
|
109
|
+
Per D-11: Shows unified diff format for text comparison.
|
|
110
|
+
Per D-12: Optional feature - user can skip comparison.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(self, tolerance: float = 0.001):
|
|
114
|
+
"""Initialize comparator with tolerance for numeric comparisons.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
tolerance: Maximum difference allowed for float equality (default 0.001)
|
|
118
|
+
"""
|
|
119
|
+
self.tolerance = tolerance
|
|
120
|
+
|
|
121
|
+
def compare(self, expected: Any, actual: Any) -> ComparisonResult:
|
|
122
|
+
"""Compare expected vs actual values.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
expected: The expected value (from user)
|
|
126
|
+
actual: The actual value (from execution)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
ComparisonResult with status and diff information
|
|
130
|
+
"""
|
|
131
|
+
# Handle both None - they match
|
|
132
|
+
if expected is None and actual is None:
|
|
133
|
+
return ComparisonResult(
|
|
134
|
+
status=ComparisonStatus.MATCH,
|
|
135
|
+
expected=expected,
|
|
136
|
+
actual=actual,
|
|
137
|
+
match_percentage=1.0
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Handle None expected (no expected provided)
|
|
141
|
+
if expected is None:
|
|
142
|
+
return ComparisonResult(
|
|
143
|
+
status=ComparisonStatus.NOT_PROVIDED,
|
|
144
|
+
expected=expected,
|
|
145
|
+
actual=actual,
|
|
146
|
+
match_percentage=0.0
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Handle type mismatch
|
|
150
|
+
if type(expected) != type(actual) and not (isinstance(expected, (int, float)) and isinstance(actual, (int, float))):
|
|
151
|
+
return ComparisonResult(
|
|
152
|
+
status=ComparisonStatus.MISMATCH,
|
|
153
|
+
expected=expected,
|
|
154
|
+
actual=actual,
|
|
155
|
+
diff=f"Type mismatch: expected {type(expected).__name__}, got {type(actual).__name__}",
|
|
156
|
+
match_percentage=0.0
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Compare based on type
|
|
160
|
+
if isinstance(expected, dict):
|
|
161
|
+
return self._compare_structured(expected, actual)
|
|
162
|
+
elif isinstance(expected, list):
|
|
163
|
+
return self._compare_list(expected, actual)
|
|
164
|
+
elif isinstance(expected, (int, float)):
|
|
165
|
+
return self._compare_numeric(expected, actual)
|
|
166
|
+
elif isinstance(expected, str):
|
|
167
|
+
return self._compare_strings(expected, actual)
|
|
168
|
+
else:
|
|
169
|
+
# Generic comparison for other types
|
|
170
|
+
if expected == actual:
|
|
171
|
+
return ComparisonResult(
|
|
172
|
+
status=ComparisonStatus.MATCH,
|
|
173
|
+
expected=expected,
|
|
174
|
+
actual=actual,
|
|
175
|
+
match_percentage=1.0
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
return ComparisonResult(
|
|
179
|
+
status=ComparisonStatus.MISMATCH,
|
|
180
|
+
expected=expected,
|
|
181
|
+
actual=actual,
|
|
182
|
+
diff=f"Expected: {expected}\nActual: {actual}",
|
|
183
|
+
match_percentage=0.0
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def _compare_numeric(self, expected: Union[int, float], actual: Union[int, float]) -> ComparisonResult:
|
|
187
|
+
"""Compare numeric values with tolerance.
|
|
188
|
+
|
|
189
|
+
Per D-11: Default tolerance of 0.001 for floating point comparisons.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
expected: Expected numeric value
|
|
193
|
+
actual: Actual numeric value
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
ComparisonResult with numeric comparison result
|
|
197
|
+
"""
|
|
198
|
+
if isinstance(expected, int) and isinstance(actual, int):
|
|
199
|
+
if expected == actual:
|
|
200
|
+
return ComparisonResult(
|
|
201
|
+
status=ComparisonStatus.MATCH,
|
|
202
|
+
expected=expected,
|
|
203
|
+
actual=actual,
|
|
204
|
+
match_percentage=1.0
|
|
205
|
+
)
|
|
206
|
+
else:
|
|
207
|
+
return ComparisonResult(
|
|
208
|
+
status=ComparisonStatus.MISMATCH,
|
|
209
|
+
expected=expected,
|
|
210
|
+
actual=actual,
|
|
211
|
+
diff=f"- {expected}\n+ {actual}",
|
|
212
|
+
match_percentage=0.0
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
# Float comparison with tolerance
|
|
216
|
+
diff = abs(float(expected) - float(actual))
|
|
217
|
+
if diff <= self.tolerance:
|
|
218
|
+
return ComparisonResult(
|
|
219
|
+
status=ComparisonStatus.MATCH,
|
|
220
|
+
expected=expected,
|
|
221
|
+
actual=actual,
|
|
222
|
+
match_percentage=1.0 - (diff / max(abs(float(expected)), 1.0))
|
|
223
|
+
)
|
|
224
|
+
else:
|
|
225
|
+
return ComparisonResult(
|
|
226
|
+
status=ComparisonStatus.MISMATCH,
|
|
227
|
+
expected=expected,
|
|
228
|
+
actual=actual,
|
|
229
|
+
diff=f"- {expected}\n+ {actual} (diff: {diff:.6f}, tolerance: {self.tolerance})",
|
|
230
|
+
match_percentage=max(0.0, 1.0 - (diff / max(abs(float(expected)), 1.0)))
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def _compare_strings(self, expected: str, actual: str) -> ComparisonResult:
|
|
234
|
+
"""Compare string values with unified diff.
|
|
235
|
+
|
|
236
|
+
Per D-11: Use unified diff format for text comparison.
|
|
237
|
+
Per D-11: Show "expected" vs "actual" with clear labels.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
expected: Expected string value
|
|
241
|
+
actual: Actual string value
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
ComparisonResult with string comparison result
|
|
245
|
+
"""
|
|
246
|
+
if expected == actual:
|
|
247
|
+
return ComparisonResult(
|
|
248
|
+
status=ComparisonStatus.MATCH,
|
|
249
|
+
expected=expected,
|
|
250
|
+
actual=actual,
|
|
251
|
+
match_percentage=1.0
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Generate unified diff
|
|
255
|
+
expected_lines = expected.splitlines(keepends=True)
|
|
256
|
+
actual_lines = actual.splitlines(keepends=True)
|
|
257
|
+
|
|
258
|
+
# Handle missing newlines at end
|
|
259
|
+
if expected_lines and not expected_lines[-1].endswith('\n'):
|
|
260
|
+
expected_lines[-1] += '\n'
|
|
261
|
+
if actual_lines and not actual_lines[-1].endswith('\n'):
|
|
262
|
+
actual_lines[-1] += '\n'
|
|
263
|
+
|
|
264
|
+
diff = difflib.unified_diff(
|
|
265
|
+
expected_lines,
|
|
266
|
+
actual_lines,
|
|
267
|
+
fromfile='expected',
|
|
268
|
+
tofile='actual',
|
|
269
|
+
lineterm=''
|
|
270
|
+
)
|
|
271
|
+
diff_str = ''.join(diff)
|
|
272
|
+
|
|
273
|
+
# Calculate match percentage (simple character-based)
|
|
274
|
+
if len(expected) == 0 and len(actual) == 0:
|
|
275
|
+
match_pct = 1.0
|
|
276
|
+
elif len(expected) == 0 or len(actual) == 0:
|
|
277
|
+
match_pct = 0.0
|
|
278
|
+
else:
|
|
279
|
+
# Use difflib.SequenceMatcher for similarity ratio
|
|
280
|
+
matcher = difflib.SequenceMatcher(None, expected, actual)
|
|
281
|
+
match_pct = matcher.ratio()
|
|
282
|
+
|
|
283
|
+
return ComparisonResult(
|
|
284
|
+
status=ComparisonStatus.MISMATCH,
|
|
285
|
+
expected=expected,
|
|
286
|
+
actual=actual,
|
|
287
|
+
diff=diff_str if diff_str else None,
|
|
288
|
+
match_percentage=match_pct,
|
|
289
|
+
differences=[]
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def _compare_structured(self, expected: Dict[str, Any], actual: Dict[str, Any]) -> ComparisonResult:
|
|
293
|
+
"""Compare structured data (dictionaries).
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
expected: Expected dictionary
|
|
297
|
+
actual: Actual dictionary
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
ComparisonResult with structured comparison result
|
|
301
|
+
"""
|
|
302
|
+
differences = []
|
|
303
|
+
matched_keys = 0
|
|
304
|
+
total_keys = len(expected)
|
|
305
|
+
|
|
306
|
+
for key in expected:
|
|
307
|
+
if key not in actual:
|
|
308
|
+
differences.append({
|
|
309
|
+
'field': key,
|
|
310
|
+
'expected': expected[key],
|
|
311
|
+
'actual': None,
|
|
312
|
+
'status': 'missing_in_actual'
|
|
313
|
+
})
|
|
314
|
+
else:
|
|
315
|
+
# Recursively compare values
|
|
316
|
+
sub_result = self.compare(expected[key], actual[key])
|
|
317
|
+
if sub_result.status != ComparisonStatus.MATCH:
|
|
318
|
+
differences.append({
|
|
319
|
+
'field': key,
|
|
320
|
+
'expected': expected[key],
|
|
321
|
+
'actual': actual[key],
|
|
322
|
+
'status': sub_result.status.value,
|
|
323
|
+
'diff': sub_result.diff
|
|
324
|
+
})
|
|
325
|
+
else:
|
|
326
|
+
matched_keys += 1
|
|
327
|
+
|
|
328
|
+
# Check for extra keys in actual
|
|
329
|
+
for key in actual:
|
|
330
|
+
if key not in expected:
|
|
331
|
+
differences.append({
|
|
332
|
+
'field': key,
|
|
333
|
+
'expected': None,
|
|
334
|
+
'actual': actual[key],
|
|
335
|
+
'status': 'extra_in_actual'
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
if len(differences) == 0:
|
|
339
|
+
return ComparisonResult(
|
|
340
|
+
status=ComparisonStatus.MATCH,
|
|
341
|
+
expected=expected,
|
|
342
|
+
actual=actual,
|
|
343
|
+
match_percentage=1.0
|
|
344
|
+
)
|
|
345
|
+
elif matched_keys > 0:
|
|
346
|
+
total_fields = len(set(expected.keys()) | set(actual.keys()))
|
|
347
|
+
match_pct = matched_keys / total_fields if total_fields > 0 else 0.0
|
|
348
|
+
return ComparisonResult(
|
|
349
|
+
status=ComparisonStatus.PARTIAL,
|
|
350
|
+
expected=expected,
|
|
351
|
+
actual=actual,
|
|
352
|
+
match_percentage=match_pct,
|
|
353
|
+
differences=differences
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
return ComparisonResult(
|
|
357
|
+
status=ComparisonStatus.MISMATCH,
|
|
358
|
+
expected=expected,
|
|
359
|
+
actual=actual,
|
|
360
|
+
match_percentage=0.0,
|
|
361
|
+
differences=differences
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def _compare_list(self, expected: List[Any], actual: List[Any]) -> ComparisonResult:
|
|
365
|
+
"""Compare list values.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
expected: Expected list
|
|
369
|
+
actual: Actual list
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
ComparisonResult with list comparison result
|
|
373
|
+
"""
|
|
374
|
+
if expected == actual:
|
|
375
|
+
return ComparisonResult(
|
|
376
|
+
status=ComparisonStatus.MATCH,
|
|
377
|
+
expected=expected,
|
|
378
|
+
actual=actual,
|
|
379
|
+
match_percentage=1.0
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
differences = []
|
|
383
|
+
max_len = max(len(expected), len(actual))
|
|
384
|
+
matches = 0
|
|
385
|
+
|
|
386
|
+
for i in range(max_len):
|
|
387
|
+
if i < len(expected) and i < len(actual):
|
|
388
|
+
sub_result = self.compare(expected[i], actual[i])
|
|
389
|
+
if sub_result.status != ComparisonStatus.MATCH:
|
|
390
|
+
differences.append({
|
|
391
|
+
'index': i,
|
|
392
|
+
'expected': expected[i],
|
|
393
|
+
'actual': actual[i],
|
|
394
|
+
'status': sub_result.status.value
|
|
395
|
+
})
|
|
396
|
+
else:
|
|
397
|
+
matches += 1
|
|
398
|
+
elif i >= len(expected):
|
|
399
|
+
differences.append({
|
|
400
|
+
'index': i,
|
|
401
|
+
'expected': None,
|
|
402
|
+
'actual': actual[i],
|
|
403
|
+
'status': 'extra_element'
|
|
404
|
+
})
|
|
405
|
+
else:
|
|
406
|
+
differences.append({
|
|
407
|
+
'index': i,
|
|
408
|
+
'expected': expected[i],
|
|
409
|
+
'actual': None,
|
|
410
|
+
'status': 'missing_element'
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
match_pct = matches / max_len if max_len > 0 else 0.0
|
|
414
|
+
|
|
415
|
+
if len(differences) == 0:
|
|
416
|
+
return ComparisonResult(
|
|
417
|
+
status=ComparisonStatus.MATCH,
|
|
418
|
+
expected=expected,
|
|
419
|
+
actual=actual,
|
|
420
|
+
match_percentage=1.0
|
|
421
|
+
)
|
|
422
|
+
elif matches > 0:
|
|
423
|
+
return ComparisonResult(
|
|
424
|
+
status=ComparisonStatus.PARTIAL,
|
|
425
|
+
expected=expected,
|
|
426
|
+
actual=actual,
|
|
427
|
+
match_percentage=match_pct,
|
|
428
|
+
differences=differences
|
|
429
|
+
)
|
|
430
|
+
else:
|
|
431
|
+
return ComparisonResult(
|
|
432
|
+
status=ComparisonStatus.MISMATCH,
|
|
433
|
+
expected=expected,
|
|
434
|
+
actual=actual,
|
|
435
|
+
match_percentage=match_pct,
|
|
436
|
+
differences=differences
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
def interactive_prompt(self) -> Optional[Any]:
|
|
440
|
+
"""Prompt user for expected value per D-09.
|
|
441
|
+
|
|
442
|
+
Per D-09: Ask "Do you have expected results to compare?"
|
|
443
|
+
Per D-10: Support both inline value and file path.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Parsed expected value or None if skipped
|
|
447
|
+
"""
|
|
448
|
+
# This method is a placeholder for interactive prompting
|
|
449
|
+
# In a real implementation, this would use input() or a GUI
|
|
450
|
+
# For now, returns None indicating no expected value provided
|
|
451
|
+
return None
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def compare_outputs(expected: Any, actual: Any, tolerance: float = 0.001) -> ComparisonResult:
|
|
455
|
+
"""High-level function to compare expected vs actual outputs.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
expected: The expected value
|
|
459
|
+
actual: The actual value
|
|
460
|
+
tolerance: Numeric tolerance for float comparisons (default 0.001)
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
ComparisonResult with comparison status and diff
|
|
464
|
+
"""
|
|
465
|
+
comparator = OutputComparator(tolerance=tolerance)
|
|
466
|
+
return comparator.compare(expected, actual)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def prompt_for_expected() -> Optional[Any]:
|
|
470
|
+
"""Prompt user for expected results per D-09, D-10.
|
|
471
|
+
|
|
472
|
+
Asks user if they have expected results to compare.
|
|
473
|
+
Supports inline value or file path input.
|
|
474
|
+
|
|
475
|
+
Per D-09: "Do you have expected results to compare?"
|
|
476
|
+
Per D-10: Support both inline value and file path.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
Parsed expected value or None if skipped
|
|
480
|
+
"""
|
|
481
|
+
# Placeholder implementation - would integrate with UI in real use
|
|
482
|
+
# Returns None to indicate no expected value was provided
|
|
483
|
+
return None
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
__all__ = [
|
|
487
|
+
'ComparisonStatus',
|
|
488
|
+
'ComparisonResult',
|
|
489
|
+
'OutputComparator',
|
|
490
|
+
'compare_outputs',
|
|
491
|
+
'prompt_for_expected',
|
|
492
|
+
]
|