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.
Files changed (90) hide show
  1. package/README.md +260 -0
  2. package/bin/algo-extract.js +143 -0
  3. package/bin/algo-generate.js +102 -0
  4. package/bin/algo-help.js +136 -0
  5. package/bin/algo-list.js +56 -0
  6. package/bin/algo-run.js +141 -0
  7. package/bin/algo-status.js +88 -0
  8. package/bin/algo-verify.js +189 -0
  9. package/bin/install.js +349 -0
  10. package/package.json +57 -0
  11. package/requirements.txt +20 -0
  12. package/src/__pycache__/intent.cpython-313.pyc +0 -0
  13. package/src/cli/__pycache__/commands.cpython-313.pyc +0 -0
  14. package/src/cli/cli_entry.py +106 -0
  15. package/src/cli/commands.py +339 -0
  16. package/src/execution/__init__.py +74 -0
  17. package/src/execution/__pycache__/__init__.cpython-313.pyc +0 -0
  18. package/src/execution/__pycache__/display.cpython-313.pyc +0 -0
  19. package/src/execution/__pycache__/errors.cpython-313.pyc +0 -0
  20. package/src/execution/__pycache__/executor.cpython-313.pyc +0 -0
  21. package/src/execution/__pycache__/sandbox.cpython-313.pyc +0 -0
  22. package/src/execution/display.py +261 -0
  23. package/src/execution/errors.py +158 -0
  24. package/src/execution/executor.py +253 -0
  25. package/src/execution/sandbox.py +333 -0
  26. package/src/extraction/__init__.py +102 -0
  27. package/src/extraction/__pycache__/__init__.cpython-313.pyc +0 -0
  28. package/src/extraction/__pycache__/boundaries.cpython-313.pyc +0 -0
  29. package/src/extraction/__pycache__/errors.cpython-313.pyc +0 -0
  30. package/src/extraction/__pycache__/llm_extraction.cpython-313.pyc +0 -0
  31. package/src/extraction/__pycache__/notation.cpython-313.pyc +0 -0
  32. package/src/extraction/__pycache__/parser.cpython-313.pyc +0 -0
  33. package/src/extraction/__pycache__/pdf_processor.cpython-313.pyc +0 -0
  34. package/src/extraction/__pycache__/prompts.cpython-313.pyc +0 -0
  35. package/src/extraction/__pycache__/review.cpython-313.pyc +0 -0
  36. package/src/extraction/__pycache__/schema.cpython-313.pyc +0 -0
  37. package/src/extraction/__pycache__/validation.cpython-313.pyc +0 -0
  38. package/src/extraction/boundaries.py +281 -0
  39. package/src/extraction/errors.py +156 -0
  40. package/src/extraction/llm_extraction.py +225 -0
  41. package/src/extraction/notation.py +240 -0
  42. package/src/extraction/parser.py +402 -0
  43. package/src/extraction/pdf_processor.py +281 -0
  44. package/src/extraction/prompts.py +90 -0
  45. package/src/extraction/review.py +298 -0
  46. package/src/extraction/schema.py +173 -0
  47. package/src/extraction/validation.py +202 -0
  48. package/src/generation/__init__.py +79 -0
  49. package/src/generation/__pycache__/__init__.cpython-313.pyc +0 -0
  50. package/src/generation/__pycache__/code_generator.cpython-313.pyc +0 -0
  51. package/src/generation/__pycache__/errors.cpython-313.pyc +0 -0
  52. package/src/generation/__pycache__/hybrid.cpython-313.pyc +0 -0
  53. package/src/generation/__pycache__/llm_generator.cpython-313.pyc +0 -0
  54. package/src/generation/__pycache__/persistence.cpython-313.pyc +0 -0
  55. package/src/generation/__pycache__/prompts.cpython-313.pyc +0 -0
  56. package/src/generation/__pycache__/review.cpython-313.pyc +0 -0
  57. package/src/generation/__pycache__/templates.cpython-313.pyc +0 -0
  58. package/src/generation/__pycache__/types.cpython-313.pyc +0 -0
  59. package/src/generation/__pycache__/validation.cpython-313.pyc +0 -0
  60. package/src/generation/code_generator.py +375 -0
  61. package/src/generation/errors.py +84 -0
  62. package/src/generation/hybrid.py +210 -0
  63. package/src/generation/llm_generator.py +223 -0
  64. package/src/generation/persistence.py +221 -0
  65. package/src/generation/prompts.py +202 -0
  66. package/src/generation/review.py +254 -0
  67. package/src/generation/templates.py +208 -0
  68. package/src/generation/types.py +196 -0
  69. package/src/generation/validation.py +278 -0
  70. package/src/intent.py +323 -0
  71. package/src/verification/__init__.py +63 -0
  72. package/src/verification/__pycache__/__init__.cpython-313.pyc +0 -0
  73. package/src/verification/__pycache__/checker.cpython-313.pyc +0 -0
  74. package/src/verification/__pycache__/comparison.cpython-313.pyc +0 -0
  75. package/src/verification/__pycache__/explainer.cpython-313.pyc +0 -0
  76. package/src/verification/__pycache__/static_analysis.cpython-313.pyc +0 -0
  77. package/src/verification/checker.py +220 -0
  78. package/src/verification/comparison.py +492 -0
  79. package/src/verification/explainer.py +414 -0
  80. package/src/verification/static_analysis.py +540 -0
  81. package/src/workflows/__init__.py +21 -0
  82. package/src/workflows/__pycache__/__init__.cpython-313.pyc +0 -0
  83. package/src/workflows/__pycache__/extract.cpython-313.pyc +0 -0
  84. package/src/workflows/__pycache__/generate.cpython-313.pyc +0 -0
  85. package/src/workflows/__pycache__/run.cpython-313.pyc +0 -0
  86. package/src/workflows/__pycache__/verify.cpython-313.pyc +0 -0
  87. package/src/workflows/extract.py +181 -0
  88. package/src/workflows/generate.py +155 -0
  89. package/src/workflows/run.py +187 -0
  90. 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
+ ]