artifacts: add calculators/ — the 30 built calculators (5/variant) + machine-docs + git logs

This commit is contained in:
2026-06-16 15:39:42 +00:00
parent 64bc360fc0
commit bb85aa9f11
728 changed files with 34148 additions and 0 deletions

View File

@ -0,0 +1,3 @@
__pycache__/
*.pyc
*.pyo

View File

@ -0,0 +1,29 @@
# git history (claim/review handshake), from the run's shared bare repo
7987247 review(D1,D2,D3,D4,D5): PASS — all eval gates verified; no defects found
86958a2 claim(D5): python -m unittest passes, 63 tests (37 lex+parse + 26 evaluator), 0 failures
74d3276 claim(D4): CLI prints result on stdout+exit 0; prints error on stderr+exit 1 for errors; no traceback
16f3f17 claim(D3): whole-valued results return int (4/2->2), non-whole return float (7/2->3.5)
cae9347 claim(D2): true division 7/2=3.5; division-by-zero raises EvalError not ZeroDivisionError
b37f7a0 claim(D1): evaluate arithmetic — precedence, parens, unary minus all correct
7167e33 feat(eval): add evaluator.py, calc.py CLI, and test_evaluator.py
baf8a4a review(eval-init): Adversary initialized for phase eval — waiting for Builder claims
b5345dd status(parse): mark DONE — all gates PASS
c552cae review(D1,D2,D3,D4,D5,D6): PASS — all parse gates verified; no defects found
4731b77 journal(parse): implementation notes and verification output
146c82f claim(D6): python -m unittest passes, 37 tests, 0 failures
5be9ecf claim(D5): 5 error cases all raise ParseError correctly
59d5e59 claim(D4): -5, -(1+2), 3*-2 all produce Unary nodes correctly
90b3b29 claim(D3): (1+2)*3 parses with + under *
1032c3d claim(D2): 8-3-2 and 8/4/2 parse left-associatively
a0e5959 claim(D1): 1+2*3 parses as BinOp(+, Num(1), BinOp(*, Num(2), Num(3)))
866091c feat(parse): add parser.py and test_parser.py — all D1-D6 gates implemented
00f7b1e review(init-parse): Adversary initialized for phase parse
d6fc26f fix: wrap float() in try/except to raise LexError on malformed numbers (AF-1); mark phase DONE; consume BUILDER-INBOX
5cd2daa review(D1,D2,D3,D4): PASS — all lex gates verified; AF-1 filed for ValueError leak
63dbd91 claim(D4): python -m unittest passes, 18 tests, 0 failures
db6f1ab claim(D3): whitespace skipped, LexError raised with char and position
20d19c3 claim(D2): operators and parens tokenize to correct kinds
a333f58 claim(D1): integers and floats tokenize to NUMBER with correct Python type
ab0332e feat: implement calc lexer with tokenize(), Token, LexError and test suite
d9f6737 review(init): Adversary initialized for phase lex
d2011fc chore: seed

View File

@ -0,0 +1 @@
# calc work repo

View File

@ -0,0 +1 @@
original path: /tmp/ao-campaign-ufRkmF/builder-adversary-lean/r1

View File

@ -0,0 +1,23 @@
#!/usr/bin/env python3
import sys
from calc.lexer import tokenize, LexError
from calc.parser import parse, ParseError
from calc.evaluator import evaluate, EvalError
def main():
if len(sys.argv) != 2:
print("usage: python calc.py <expression>", file=sys.stderr)
sys.exit(1)
expr = sys.argv[1]
try:
result = evaluate(parse(tokenize(expr)))
print(result)
except (LexError, ParseError, EvalError) as e:
print(f"error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,37 @@
from calc.parser import Num, BinOp, Unary
class EvalError(Exception):
pass
def evaluate(node):
"""Walk the AST and return int | float.
Whole-valued results (including whole-valued division) are returned as int;
non-whole float results are returned as float.
"""
if isinstance(node, Num):
return node.value
if isinstance(node, Unary):
if node.op == '-':
return -evaluate(node.operand)
raise EvalError(f"unknown unary op {node.op!r}")
if isinstance(node, BinOp):
left = evaluate(node.left)
right = evaluate(node.right)
if node.op == '+':
return left + right
if node.op == '-':
return left - right
if node.op == '*':
return left * right
if node.op == '/':
if right == 0:
raise EvalError("division by zero")
result = left / right
if result == int(result):
return int(result)
return result
raise EvalError(f"unknown binary op {node.op!r}")
raise EvalError(f"unknown node type {type(node).__name__!r}")

View File

@ -0,0 +1,54 @@
from dataclasses import dataclass
from typing import Union
class LexError(Exception):
pass
@dataclass
class Token:
kind: str
value: Union[int, float, str, None]
def tokenize(src: str) -> list:
tokens = []
i = 0
while i < len(src):
ch = src[i]
if ch in ' \t':
i += 1
elif ch == '+':
tokens.append(Token('PLUS', '+'))
i += 1
elif ch == '-':
tokens.append(Token('MINUS', '-'))
i += 1
elif ch == '*':
tokens.append(Token('STAR', '*'))
i += 1
elif ch == '/':
tokens.append(Token('SLASH', '/'))
i += 1
elif ch == '(':
tokens.append(Token('LPAREN', '('))
i += 1
elif ch == ')':
tokens.append(Token('RPAREN', ')'))
i += 1
elif ch.isdigit() or ch == '.':
j = i
while j < len(src) and (src[j].isdigit() or src[j] == '.'):
j += 1
raw = src[i:j]
try:
value = float(raw) if '.' in raw else int(raw)
except ValueError:
raise LexError(f"invalid number literal {raw!r} at position {i}")
tokens.append(Token('NUMBER', value))
i = j
else:
raise LexError(f"unexpected character {ch!r} at position {i}")
tokens.append(Token('EOF', None))
return tokens

View File

@ -0,0 +1,92 @@
from dataclasses import dataclass
from typing import Union
from calc.lexer import Token
class ParseError(Exception):
pass
@dataclass
class Num:
value: Union[int, float]
@dataclass
class BinOp:
op: str
left: object
right: object
@dataclass
class Unary:
op: str
operand: object
Node = Union[Num, BinOp, Unary]
class _Parser:
def __init__(self, tokens: list):
self._tokens = tokens
self._pos = 0
def _peek(self) -> Token:
return self._tokens[self._pos]
def _consume(self, kind: str = None) -> Token:
tok = self._tokens[self._pos]
if kind and tok.kind != kind:
raise ParseError(f"expected {kind!r}, got {tok.kind!r} ({tok.value!r})")
self._pos += 1
return tok
def parse(self) -> Node:
if self._peek().kind == 'EOF':
raise ParseError("empty input")
node = self._expr()
if self._peek().kind != 'EOF':
tok = self._peek()
raise ParseError(f"unexpected token {tok.kind!r} ({tok.value!r})")
return node
def _expr(self) -> Node:
node = self._term()
while self._peek().kind in ('PLUS', 'MINUS'):
op = self._consume().value
node = BinOp(op, node, self._term())
return node
def _term(self) -> Node:
node = self._unary()
while self._peek().kind in ('STAR', 'SLASH'):
op = self._consume().value
node = BinOp(op, node, self._unary())
return node
def _unary(self) -> Node:
if self._peek().kind == 'MINUS':
self._consume()
return Unary('-', self._unary())
return self._primary()
def _primary(self) -> Node:
tok = self._peek()
if tok.kind == 'NUMBER':
self._consume()
return Num(tok.value)
if tok.kind == 'LPAREN':
self._consume()
node = self._expr()
if self._peek().kind != 'RPAREN':
raise ParseError(f"unclosed parenthesis, got {self._peek().kind!r}")
self._consume()
return node
raise ParseError(f"unexpected token {tok.kind!r} ({tok.value!r})")
def parse(tokens: list) -> Node:
return _Parser(tokens).parse()

View File

@ -0,0 +1,134 @@
import subprocess
import sys
import unittest
from calc.evaluator import EvalError, evaluate
from calc.lexer import tokenize
from calc.parser import parse
def calc(s):
return evaluate(parse(tokenize(s)))
class TestD1Arithmetic(unittest.TestCase):
def test_add_mul_precedence(self):
self.assertEqual(calc("2+3*4"), 14)
def test_parens(self):
self.assertEqual(calc("(2+3)*4"), 20)
def test_left_assoc_sub(self):
self.assertEqual(calc("8-3-2"), 3)
def test_unary_minus_add(self):
self.assertEqual(calc("-2+5"), 3)
def test_mul_unary(self):
self.assertEqual(calc("2*-3"), -6)
def test_basic_add(self):
self.assertEqual(calc("1+1"), 2)
def test_basic_sub(self):
self.assertEqual(calc("5-3"), 2)
def test_basic_mul(self):
self.assertEqual(calc("3*4"), 12)
def test_nested_parens(self):
self.assertEqual(calc("((2+3))"), 5)
def test_unary_only(self):
self.assertEqual(calc("-5"), -5)
def test_double_unary(self):
self.assertEqual(calc("--5"), 5)
class TestD2Division(unittest.TestCase):
def test_true_division(self):
self.assertAlmostEqual(calc("7/2"), 3.5)
def test_div_by_zero_raises(self):
with self.assertRaises(EvalError):
calc("1/0")
def test_div_by_zero_not_zdiv_error(self):
try:
calc("1/0")
self.fail("expected EvalError")
except EvalError:
pass
except ZeroDivisionError:
self.fail("bare ZeroDivisionError escaped the API")
def test_div_integer(self):
self.assertEqual(calc("6/3"), 2)
class TestD3ResultType(unittest.TestCase):
def test_whole_div_no_dot_zero(self):
result = calc("4/2")
self.assertEqual(result, 2)
self.assertIsInstance(result, int)
def test_nonwhole_div_is_float(self):
result = calc("7/2")
self.assertIsInstance(result, float)
self.assertAlmostEqual(result, 3.5)
def test_int_add_is_int(self):
result = calc("2+3")
self.assertIsInstance(result, int)
def test_str_whole_result(self):
self.assertEqual(str(calc("4/2")), "2")
def test_str_float_result(self):
self.assertEqual(str(calc("7/2")), "3.5")
class TestD4CLI(unittest.TestCase):
def _run(self, expr):
return subprocess.run(
[sys.executable, "calc.py", expr],
capture_output=True,
text=True,
)
def test_basic_expr(self):
r = self._run("2+3*4")
self.assertEqual(r.returncode, 0)
self.assertEqual(r.stdout.strip(), "14")
def test_parens_expr(self):
r = self._run("(2+3)*4")
self.assertEqual(r.returncode, 0)
self.assertEqual(r.stdout.strip(), "20")
def test_float_div(self):
r = self._run("7/2")
self.assertEqual(r.returncode, 0)
self.assertEqual(r.stdout.strip(), "3.5")
def test_whole_div_no_dot(self):
r = self._run("4/2")
self.assertEqual(r.returncode, 0)
self.assertEqual(r.stdout.strip(), "2")
def test_div_by_zero_nonzero_exit(self):
r = self._run("1/0")
self.assertNotEqual(r.returncode, 0)
self.assertGreater(len(r.stderr), 0)
self.assertEqual(r.stdout, "")
def test_invalid_expr_nonzero_exit(self):
r = self._run("1 +")
self.assertNotEqual(r.returncode, 0)
self.assertGreater(len(r.stderr), 0)
self.assertEqual(r.stdout, "")
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,94 @@
import unittest
from calc.lexer import tokenize, Token, LexError
def kinds(src):
return [t.kind for t in tokenize(src)]
def tok(src):
return [(t.kind, t.value) for t in tokenize(src)]
class TestNumbers(unittest.TestCase):
def test_integer(self):
result = tokenize("42")
self.assertEqual(result[0], Token('NUMBER', 42))
self.assertEqual(result[1], Token('EOF', None))
self.assertIsInstance(result[0].value, int)
def test_float(self):
result = tokenize("3.14")
self.assertEqual(result[0].kind, 'NUMBER')
self.assertAlmostEqual(result[0].value, 3.14)
self.assertIsInstance(result[0].value, float)
def test_leading_dot(self):
result = tokenize(".5")
self.assertEqual(result[0].kind, 'NUMBER')
self.assertAlmostEqual(result[0].value, 0.5)
def test_trailing_dot(self):
result = tokenize("10.")
self.assertEqual(result[0].kind, 'NUMBER')
self.assertAlmostEqual(result[0].value, 10.0)
self.assertIsInstance(result[0].value, float)
class TestOperatorsAndParens(unittest.TestCase):
def test_plus(self):
self.assertIn(Token('PLUS', '+'), tokenize("+"))
def test_minus(self):
self.assertIn(Token('MINUS', '-'), tokenize("-"))
def test_star(self):
self.assertIn(Token('STAR', '*'), tokenize("*"))
def test_slash(self):
self.assertIn(Token('SLASH', '/'), tokenize("/"))
def test_lparen(self):
self.assertIn(Token('LPAREN', '('), tokenize("("))
def test_rparen(self):
self.assertIn(Token('RPAREN', ')'), tokenize(")"))
def test_sequence(self):
self.assertEqual(kinds("1+2*3"),
['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'])
class TestWhitespaceAndErrors(unittest.TestCase):
def test_whitespace_skipped(self):
self.assertEqual(kinds(" 12 + 3 "),
['NUMBER', 'PLUS', 'NUMBER', 'EOF'])
def test_tab_skipped(self):
self.assertEqual(kinds("1\t+\t2"), ['NUMBER', 'PLUS', 'NUMBER', 'EOF'])
def test_complex_expr(self):
self.assertEqual(kinds("3.5*(1-2)"),
['NUMBER', 'STAR', 'LPAREN', 'NUMBER', 'MINUS', 'NUMBER', 'RPAREN', 'EOF'])
def test_lex_error_at_sign(self):
with self.assertRaises(LexError) as ctx:
tokenize("1 @ 2")
self.assertIn('@', str(ctx.exception))
def test_lex_error_dollar(self):
with self.assertRaises(LexError):
tokenize("$")
def test_lex_error_letter(self):
with self.assertRaises(LexError):
tokenize("abc")
def test_lex_error_position(self):
with self.assertRaises(LexError) as ctx:
tokenize("1 @ 2")
self.assertIn('2', str(ctx.exception))
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,103 @@
import unittest
from calc.lexer import tokenize
from calc.parser import parse, ParseError, Num, BinOp, Unary
def p(src):
return parse(tokenize(src))
class TestPrecedence(unittest.TestCase):
def test_mul_binds_tighter_than_add(self):
# 1+2*3 => BinOp(+, Num(1), BinOp(*, Num(2), Num(3)))
tree = p('1+2*3')
self.assertEqual(tree, BinOp('+', Num(1), BinOp('*', Num(2), Num(3))))
def test_div_binds_tighter_than_sub(self):
# 6-4/2 => BinOp(-, Num(6), BinOp(/, Num(4), Num(2)))
tree = p('6-4/2')
self.assertEqual(tree, BinOp('-', Num(6), BinOp('/', Num(4), Num(2))))
def test_mul_binds_tighter_than_sub(self):
tree = p('5-2*3')
self.assertEqual(tree, BinOp('-', Num(5), BinOp('*', Num(2), Num(3))))
class TestLeftAssociativity(unittest.TestCase):
def test_sub_left_assoc(self):
# 8-3-2 => BinOp(-, BinOp(-, Num(8), Num(3)), Num(2))
tree = p('8-3-2')
self.assertEqual(tree, BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)))
def test_div_left_assoc(self):
# 8/4/2 => BinOp(/, BinOp(/, Num(8), Num(4)), Num(2))
tree = p('8/4/2')
self.assertEqual(tree, BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)))
def test_add_left_assoc(self):
tree = p('1+2+3')
self.assertEqual(tree, BinOp('+', BinOp('+', Num(1), Num(2)), Num(3)))
def test_mul_left_assoc(self):
tree = p('2*3*4')
self.assertEqual(tree, BinOp('*', BinOp('*', Num(2), Num(3)), Num(4)))
class TestParentheses(unittest.TestCase):
def test_parens_override_precedence(self):
# (1+2)*3 => BinOp(*, BinOp(+, Num(1), Num(2)), Num(3))
tree = p('(1+2)*3')
self.assertEqual(tree, BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)))
def test_nested_parens(self):
tree = p('((2+3))')
self.assertEqual(tree, BinOp('+', Num(2), Num(3)))
def test_parens_right_side(self):
tree = p('3*(1+2)')
self.assertEqual(tree, BinOp('*', Num(3), BinOp('+', Num(1), Num(2))))
class TestUnaryMinus(unittest.TestCase):
def test_simple_unary(self):
tree = p('-5')
self.assertEqual(tree, Unary('-', Num(5)))
def test_unary_paren(self):
tree = p('-(1+2)')
self.assertEqual(tree, Unary('-', BinOp('+', Num(1), Num(2))))
def test_mul_unary(self):
# 3 * -2 => BinOp(*, Num(3), Unary(-, Num(2)))
tree = p('3 * -2')
self.assertEqual(tree, BinOp('*', Num(3), Unary('-', Num(2))))
def test_double_unary(self):
tree = p('--5')
self.assertEqual(tree, Unary('-', Unary('-', Num(5))))
class TestErrors(unittest.TestCase):
def test_trailing_op(self):
with self.assertRaises(ParseError):
p('1 +')
def test_unclosed_paren(self):
with self.assertRaises(ParseError):
p('(1')
def test_two_numbers(self):
with self.assertRaises(ParseError):
p('1 2')
def test_close_before_open(self):
with self.assertRaises(ParseError):
p(')(')
def test_empty_string(self):
with self.assertRaises(ParseError):
p('')
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,16 @@
# BACKLOG — Phase eval (Adversary)
## Adversary findings
_No findings — all D1D5 gates verified PASS. No defects found._
---
## Adversary break-it probes (planned)
When gates are claimed, I will test:
- D1: `2+3*4`→14, `(2+3)*4`→20, `8-3-2`→3, `-2+5`→3, `2*-3`→-6, plus edge cases like `--5`, `0*100`, nested parens
- D2: `7/2`→3.5 (true division); `1/0` raises EvalError (not ZeroDivisionError); `0/0` likewise
- D3: `4/2``2` (no .0); `7/2``3.5`; `6/3``2`; `1/3``0.333...`
- D4: CLI exit 0 for valid; non-zero + stderr for invalid; traceback must NOT appear
- D5: full `python -m unittest -q` including prior lexer+parser tests; check test count

View File

@ -0,0 +1,16 @@
# BACKLOG — Phase lex
## Build backlog
- [x] D1 — numbers: INTEGER and FLOAT tokenization — CLAIMED
- [x] D2 — operators & parens — CLAIMED
- [x] D3 — whitespace & errors (LexError) — CLAIMED
- [x] D4 — tests green (18 tests, 0 failures) — CLAIMED
## Adversary findings
### AF-1 (open): ValueError leaks on malformed number tokens
- `tokenize('1.2.3')``ValueError` (not `LexError`)
- `tokenize('.')``ValueError` (not `LexError`)
- Non-blocking for phase `lex` DoD; recommend fix before parser phase consumes these tokens.
- Repro and details in REVIEW-lex.md § AF-1.

View File

@ -0,0 +1,12 @@
# BACKLOG — Phase parse
## Build backlog
- [x] Write calc/parser.py with parse(tokens) -> Node
- [x] Write calc/test_parser.py with unittest coverage of D1-D5
- [ ] Claim D1 (precedence)
- [ ] Claim D2 (left associativity)
- [ ] Claim D3 (parentheses)
- [ ] Claim D4 (unary minus)
- [ ] Claim D5 (errors)
- [ ] Claim D6 (tests green)

View File

@ -0,0 +1,4 @@
# DECISIONS — Phase lex (shared, append-only)
## 2026-06-15 — Adversary initialized
Adversary loop started monitoring for Builder gate claims on phase `lex`.

View File

@ -0,0 +1,43 @@
# JOURNAL — Phase eval
## Implementation notes
### evaluator.py
Built `evaluate(node) -> int | float` walking `Num`, `BinOp`, `Unary` nodes.
- `Num`: returns `node.value` (already int or float from lexer)
- `Unary('-')`: returns `-evaluate(operand)` recursively
- `BinOp(+,-,*)`: straightforward arithmetic (Python int+int=int)
- `BinOp(/)`: true division via `left / right`; checks `right == 0``EvalError`; if `result == int(result)` → return `int(result)` (D3 rule), else return float
D3 rule: after true division, `4/2 = 2.0`; `2.0 == int(2.0)` is True, so return `int(2.0) = 2`. `7/2 = 3.5`; `3.5 != 3`, so return `3.5` (float).
### calc.py
Top-level CLI at repo root. Imports from `calc.*`. Catches `LexError`, `ParseError`, `EvalError` → prints to stderr, exits 1. Traceback never escapes.
### test_evaluator.py
26 new tests across D1D4 categories:
- D1 (arithmetic): 11 tests including all DoD examples plus edge cases
- D2 (division): 4 tests including EvalError-not-ZeroDivisionError check
- D3 (result type): 5 tests including `str()` formatting
- D4 (CLI): 6 tests using subprocess
### Verification output (local)
```
python -m unittest -q
Ran 63 tests in 0.228s
OK
```
```
python calc.py "2+3*4" → 14
python calc.py "(2+3)*4" → 20
python calc.py "7/2" → 3.5
python calc.py "4/2" → 2
python calc.py "1/0" → error: division by zero (stderr, exit 1)
python calc.py "1 +" → error: unexpected token 'EOF' (None) (stderr, exit 1)
```

View File

@ -0,0 +1,30 @@
# JOURNAL — Phase lex (Adversary)
## 2026-06-15T00:00:00Z — Initialized
Adversary loop started. Read phase plan. No Builder activity yet. Watching for gate claims.
---
# JOURNAL — Phase lex (Builder)
## 2026-06-15 — Session 1
Implemented `calc/lexer.py` with `Token` dataclass, `LexError`, and `tokenize()`.
Test run:
```
$ python -m unittest -q
Ran 18 tests in 0.000s
OK
```
Verification commands (from plan):
```
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]
$ python -c "from calc.lexer import tokenize; tokenize('1 @ 2')" 2>&1
calc.lexer.LexError: unexpected character '@' at position 2
```
Committed as `ab0332e`. All D1D4 conditions met in one session.

View File

@ -0,0 +1,33 @@
# JOURNAL — Phase parse
## 2026-06-15 — Implementation
Built `calc/parser.py` using a classic recursive-descent approach with three precedence levels:
1. `_expr` handles `+`/`-` (lowest precedence, left-assoc via loop)
2. `_term` handles `*`/`/` (medium precedence, left-assoc via loop)
3. `_unary` handles leading `-` (right-assoc via recursion)
4. `_primary` handles numbers and parenthesized expressions
Left-associativity falls out naturally from the `while` loops in `_expr` and `_term` — each iteration wraps the accumulated `node` as the left child of a new `BinOp`, building a left-leaning tree.
Test verification output:
```
D1: BinOp(op='+', left=Num(value=1), right=BinOp(op='*', left=Num(value=2), right=Num(value=3)))
D2: BinOp(op='-', left=BinOp(op='-', left=Num(value=8), right=Num(value=3)), right=Num(value=2))
D3: BinOp(op='*', left=BinOp(op='+', left=Num(value=1), right=Num(value=2)), right=Num(value=3))
D4a: Unary(op='-', operand=Num(value=5))
D4b: Unary(op='-', operand=BinOp(op='+', left=Num(value=1), right=Num(value=2)))
D4c: BinOp(op='*', left=Num(value=3), right=Unary(op='-', operand=Num(value=2)))
D5 errors:
OK ParseError for '1 +': unexpected token 'EOF' (None)
OK ParseError for '(1': unclosed parenthesis, got 'EOF'
OK ParseError for '1 2': unexpected token 'NUMBER' (2)
OK ParseError for ')(': unexpected token 'RPAREN' (')')
OK ParseError for '': empty input
D6: Ran 37 tests in 0.001s OK
```
All 6 gates claimed and pushed. Awaiting Adversary verification.

View File

@ -0,0 +1,68 @@
# REVIEW — Phase eval (Adversary)
## D1: PASS @2026-06-15T05:01Z
Cold-ran all five DoD arithmetic checks from the plan:
- `2+3*4` → 14 ✓ (precedence: `*` before `+`)
- `(2+3)*4` → 20 ✓ (parens override precedence)
- `8-3-2` → 3 ✓ (left-associativity)
- `-2+5` → 3 ✓ (unary minus)
- `2*-3` → -6 ✓ (unary minus in binary context)
Break-it probes:
- `--5` → 5 ✓ (double unary, recursive)
- `((2+3))` → 5 ✓ (nested parens)
- `1+2+3+4` → 10 ✓ (chain addition)
- `2*3+4/2` → 8 ✓ (mixed precedence, `4/2` → int 2, `6+2` → int 8)
### D2: PASS @2026-06-15T05:01Z
- `7/2` → 3.5 (true division) ✓
- `1/0` → raises `EvalError("division by zero")`, NOT `ZeroDivisionError`
- Break-it: `0/0``EvalError` ✓ (zero-zero handled by same `right == 0` check)
- Break-it: `-1/0``EvalError`
Code review: `evaluator.py:3031` — explicit `if right == 0: raise EvalError(...)` before Python's `/` operator, so `ZeroDivisionError` can never escape. Correct.
### D3: PASS @2026-06-15T05:01Z
- `4/2``2` (type `int`) ✓
- `7/2``3.5` (type `float`) ✓
- Break-it: `6/3``2` int ✓
- Break-it: `0/1``0` int ✓
- Break-it: `2+3` (no division) → `5` int ✓ (integer arithmetic always stays int)
- Break-it: `1/3``0.333...` float ✓
Code review: `evaluator.py:3335``if result == int(result): return int(result)` applied only in the `/` branch. Correct scope.
CLI check: `python calc.py '4/2'``2` (no `.0`) ✓; `python calc.py '7/2'``3.5`
### D4: PASS @2026-06-15T05:01Z
Valid expressions:
- `python calc.py "2+3*4"` → stdout `14`, exit 0 ✓
- `python calc.py "(2+3)*4"` → stdout `20`, exit 0 ✓
- `python calc.py "7/2"` → stdout `3.5`, exit 0 ✓
- `python calc.py "4/2"` → stdout `2`, exit 0 ✓
Error cases:
- `python calc.py "1/0"` → stderr `error: division by zero`, exit 1, stdout empty ✓
- `python calc.py "1 +"` → stderr `error: unexpected token 'EOF' (None)`, exit 1, stdout empty ✓
Break-it probes:
- No `Traceback` in stderr for either error case ✓
- No-arg case (`python calc.py`) → stderr `usage: python calc.py <expression>`, exit 1 ✓
- `LexError` also caught (imported and handled in `calc.py:17`) ✓
### D5: PASS @2026-06-15T05:01Z
Cold-ran: `python -m unittest -q`
Output: `Ran 63 tests in 0.232s OK` — 0 failures ✓
Prior suite (37 lex+parse tests) still passes; 26 new evaluator tests added. No regressions.
Plan's exact Verify-section commands all ran with matching expected outputs.
## No adversary findings — all DoD gates verified PASS
All D1D5 gates independently verified with break-it probes. No defects found. Builder may mark DONE.

View File

@ -0,0 +1,58 @@
# REVIEW — Phase lex (Adversary)
## Verdicts
### lex/D1: PASS @2026-06-15T05:05Z
Cold-run evidence:
- `tokenize('42')``NUMBER 42 int` ✓ (int type confirmed)
- `tokenize('3.14')``NUMBER 3.14 float` ✓ (float type confirmed)
- `tokenize('.5')``NUMBER 0.5` ✓ (leading dot)
- `tokenize('10.')``NUMBER 10.0` ✓ (trailing dot → float)
- EOF appended in all cases ✓
---
### lex/D2: PASS @2026-06-15T05:05Z
Cold-run evidence:
- `tokenize('1+2*3')``[('NUMBER', 1), ('PLUS', '+'), ('NUMBER', 2), ('STAR', '*'), ('NUMBER', 3), ('EOF', None)]`
- Matches expected exactly ✓
- All 6 operator/paren kinds verified in test suite ✓
---
### lex/D3: PASS @2026-06-15T05:05Z
Cold-run evidence:
- `tokenize(' 12 + 3 ')``['NUMBER', 'PLUS', 'NUMBER', 'EOF']` ✓ (spaces skipped)
- `tokenize('1 @ 2')` raises `calc.lexer.LexError: unexpected character '@' at position 2`
- Offending character `'@'` in message ✓
- Position `2` in message ✓
- Letters (`abc`), `$` also raise `LexError` per test suite ✓
---
### lex/D4: PASS @2026-06-15T05:05Z
Cold-run evidence:
- `python -m unittest -q``Ran 18 tests in 0.000s OK`
- `tokenize('3.5*(1-2)')``[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]` ✓ (exact plan match)
- `tokenize('1 @ 2')` raises `LexError` (exit 1) ✓
- Required test cases present: `" 12 + 3 "`, `"3.5*(1-2)"`, `"1 @ 2"`
- 18 tests, 0 failures ✓
---
## Adversary findings (non-blocking for this phase)
### AF-1: `ValueError` leaks on malformed number tokens
**Repro:**
```
python -c "from calc.lexer import tokenize; tokenize('1.2.3')"
# → ValueError: could not convert string to float: '1.2.3'
python -c "from calc.lexer import tokenize; tokenize('.')"
# → ValueError: could not convert string to float: '.'
```
The number-scanning loop (`ch.isdigit() or ch == '.'`) greedily consumes all digits and dots, then hands the raw span to `float()` which raises `ValueError` on malformed input like `1.2.3` or bare `.`. These should raise `LexError` for consistency — the caller can't distinguish a lexer malfunction from a Python type error.
**Severity:** Not blocking — the DoD only requires `LexError` for invalid *characters* (`@`, `$`, letters). `1.2.3` and `.` are outside the explicit D1/D3 test cases. However, the parser phase will likely encounter these and must handle them.
**Recommendation:** Wrap the `float(raw)` call in a `try/except ValueError` and re-raise as `LexError`. Flag for builder attention in BUILDER-INBOX.

View File

@ -0,0 +1,41 @@
# REVIEW — Phase parse (Adversary)
## Verdicts
### D1: PASS @2026-06-15T05:00Z
Cold-ran: `python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"`
Got: `BinOp(op='+', left=Num(value=1), right=BinOp(op='*', left=Num(value=2), right=Num(value=3)))`
Matches expected exactly. Break-it: `3*2+1``BinOp(+, BinOp(*, 3, 2), 1)` (correct — `*` binds tighter from left too).
### D2: PASS @2026-06-15T05:00Z
Cold-ran: `8-3-2``BinOp(-, BinOp(-, Num(8), Num(3)), Num(2))`
`8/4/2``BinOp(/, BinOp(/, Num(8), Num(4)), Num(2))`
Break-it: `10-3-2-1` → fully left-assoc triple nesting ✓. Grammar's while-loop guarantees left-assoc.
### D3: PASS @2026-06-15T05:00Z
Cold-ran: `(1+2)*3``BinOp(*, BinOp(+, Num(1), Num(2)), Num(3))`
Break-it: `((2+3))*4``BinOp(*, BinOp(+, Num(2), Num(3)), Num(4))` ✓ (nested parens unwrap cleanly).
### D4: PASS @2026-06-15T05:00Z
Cold-ran all three required cases:
- `-5``Unary(op='-', operand=Num(value=5))`
- `-(1+2)``Unary(op='-', operand=BinOp(op='+', left=Num(value=1), right=Num(value=2)))`
- `3 * -2``BinOp(op='*', left=Num(value=3), right=Unary(op='-', operand=Num(value=2)))`
Break-it: `--5``Unary('-', Unary('-', Num(5)))` (recursive unary works, double-negation parses correctly).
### D5: PASS @2026-06-15T05:00Z
Cold-ran all five required error cases — each raised `ParseError`, none raised a different exception:
- `'1 +'``ParseError: unexpected token 'EOF' (None)`
- `'(1'``ParseError: unclosed parenthesis, got 'EOF'`
- `'1 2'``ParseError: unexpected token 'NUMBER' (2)`
- `')('``ParseError: unexpected token 'RPAREN' (')')`
- `''``ParseError: empty input`
Break-it: `1+2)``ParseError: unexpected token 'RPAREN' (')')` ✓ (trailing paren caught by EOF check).
### D6: PASS @2026-06-15T05:00Z
Cold-ran: `python -m unittest -q`
Output: `Ran 37 tests in 0.001s OK` (18 lexer + 19 parser, 0 failures) ✓
## No adversary findings — all DoD gates verified PASS
All D1D6 gates verified independently. No defects found. Builder may mark DONE.

View File

@ -0,0 +1,115 @@
# STATUS — Phase eval
## Gate D1 CLAIMED — awaiting Adversary
**WHAT:** `evaluate(parse(tokenize(s)))` is correct for `+ - * /`, precedence, parens, and unary minus.
**HOW:**
```bash
python -c "
from calc.lexer import tokenize; from calc.parser import parse; from calc.evaluator import evaluate
def c(s): return evaluate(parse(tokenize(s)))
assert c('2+3*4') == 14
assert c('(2+3)*4') == 20
assert c('8-3-2') == 3
assert c('-2+5') == 3
assert c('2*-3') == -6
print('D1 OK')
"
```
**EXPECTED:** prints `D1 OK`, no assertion errors.
**WHERE:** `calc/evaluator.py` @ commit `7167e33`
---
## Gate D2 CLAIMED — awaiting Adversary
**WHAT:** `/` is true division (`"7/2"` → 3.5). Division by zero raises `EvalError`, not `ZeroDivisionError`.
**HOW:**
```bash
python -c "
from calc.lexer import tokenize; from calc.parser import parse; from calc.evaluator import evaluate, EvalError
def c(s): return evaluate(parse(tokenize(s)))
assert c('7/2') == 3.5
try:
c('1/0')
assert False, 'no error raised'
except EvalError:
pass
except ZeroDivisionError:
assert False, 'bare ZeroDivisionError escaped'
print('D2 OK')
"
```
**EXPECTED:** prints `D2 OK`, no assertion errors.
**WHERE:** `calc/evaluator.py` @ commit `7167e33`
---
## Gate D3 CLAIMED — awaiting Adversary
**WHAT:** Whole-valued results print without `.0` (`"4/2"``2`), non-whole as float (`"7/2"``3.5`).
Rule: after division, if `result == int(result)`, return `int(result)`; otherwise return `float`.
**HOW:**
```bash
python -c "
from calc.lexer import tokenize; from calc.parser import parse; from calc.evaluator import evaluate
def c(s): return evaluate(parse(tokenize(s)))
r1 = c('4/2'); assert r1 == 2 and isinstance(r1, int), f'got {r1!r}'
r2 = c('7/2'); assert isinstance(r2, float) and r2 == 3.5, f'got {r2!r}'
print('D3 OK')
"
python calc.py '4/2' # must print: 2
python calc.py '7/2' # must print: 3.5
```
**EXPECTED:** `D3 OK`, then `2`, then `3.5`.
**WHERE:** `calc/evaluator.py` @ commit `7167e33`
---
## Gate D4 CLAIMED — awaiting Adversary
**WHAT:** `python calc.py "2+3*4"` prints `14` and exits 0; invalid expression prints error to stderr and exits non-zero (no traceback).
**HOW:**
```bash
python calc.py "2+3*4" # stdout: 14, exit 0
python calc.py "(2+3)*4" # stdout: 20, exit 0
python calc.py "7/2" # stdout: 3.5, exit 0
python calc.py "4/2" # stdout: 2, exit 0
python calc.py "1/0" # stderr: error: ..., exit non-zero, stdout empty
python calc.py "1 +" # stderr: error: ..., exit non-zero, stdout empty
```
**EXPECTED:** exactly as above — no Python traceback in stderr, error message starts with `error:`.
**WHERE:** `calc.py` @ commit `7167e33`
---
## Gate D5 CLAIMED — awaiting Adversary
**WHAT:** Full test suite passes: `python -m unittest -q`, 0 failures. New `calc/test_evaluator.py` covers D1D4. Prior lex+parse suite (37 tests) still passes (no regression). Total: 63 tests.
**HOW:**
```bash
python -m unittest -q
```
**EXPECTED:**
```
Ran 63 tests in X.XXXs
OK
```
**WHERE:** `calc/test_evaluator.py` (26 new tests) + `calc/test_lexer.py` + `calc/test_parser.py` @ commit `7167e33`

View File

@ -0,0 +1,90 @@
# STATUS — Phase lex
## DONE
All D1D4 gates PASS (Adversary-verified @2026-06-15T05:05Z).
Post-DoD fix applied: AF-1 (`ValueError` on malformed numbers now raises `LexError`).
---
## Adversary
Initialized. Watching for Builder gate claims.
## Builder
### Gate D1 CLAIMED — PASS
**WHAT:** Integers and floats tokenize to NUMBER tokens with correct Python type.
**HOW:**
```bash
python -c "from calc.lexer import tokenize; r=tokenize('42'); print(r[0].kind, r[0].value, type(r[0].value).__name__)"
python -c "from calc.lexer import tokenize; r=tokenize('3.14'); print(r[0].kind, r[0].value, type(r[0].value).__name__)"
python -c "from calc.lexer import tokenize; r=tokenize('.5'); print(r[0].kind, r[0].value)"
python -c "from calc.lexer import tokenize; r=tokenize('10.'); print(r[0].kind, r[0].value)"
```
**EXPECTED:**
- `NUMBER 42 int`
- `NUMBER 3.14 float`
- `NUMBER 0.5`
- `NUMBER 10.0`
- Final token in all cases: `Token(kind='EOF', value=None)`
**WHERE:** `calc/lexer.py` @ commit `ab0332e`
---
### Gate D2 CLAIMED — PASS
**WHAT:** Operators `+ - * / ( )` tokenize to correct kinds.
**HOW:**
```bash
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('1+2*3')])"
```
**EXPECTED:**
```
[('NUMBER', 1), ('PLUS', '+'), ('NUMBER', 2), ('STAR', '*'), ('NUMBER', 3), ('EOF', None)]
```
**WHERE:** `calc/lexer.py` @ commit `ab0332e`
---
### Gate D3 CLAIMED — PASS
**WHAT:** Whitespace skipped; invalid chars raise `LexError` with offending char and position.
**HOW:**
```bash
python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize(' 12 + 3 ')])"
python -c "from calc.lexer import tokenize; tokenize('1 @ 2')" 2>&1
```
**EXPECTED:**
- `['NUMBER', 'PLUS', 'NUMBER', 'EOF']`
- `calc.lexer.LexError: unexpected character '@' at position 2`
**WHERE:** `calc/lexer.py` @ commit `ab0332e`
---
### Gate D4 CLAIMED — PASS
**WHAT:** `calc/test_lexer.py` passes under `python -m unittest`, 0 failures, 18 tests covering D1D3 including the three required cases.
**HOW:**
```bash
python -m unittest -q
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
```
**EXPECTED:**
- unittest: `Ran 18 tests in X.XXXs OK`
- expression: `[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', None)]`
- LexError raised (exit 1)
**WHERE:** `calc/test_lexer.py`, `calc/lexer.py` @ commit `ab0332e`

View File

@ -0,0 +1,164 @@
# STATUS — Phase parse
## DONE
All D1D6 gates PASS (Adversary-verified @2026-06-15T05:00Z).
---
## Gate D6 CLAIMED — PASS
**WHAT:** `calc/test_parser.py` passes under `python -m unittest`, 0 failures, 37 total tests (18 lexer + 19 parser), covering D1D5.
**HOW:**
```bash
python -m unittest -q
```
**EXPECTED:**
```
Ran 37 tests in X.XXXs
OK
```
**WHERE:** `calc/test_parser.py` @ commit `866091c`
---
## Gate D5 CLAIMED — awaiting Adversary
**WHAT:** Each of `"1 +"`, `"(1"`, `"1 2"`, `")("`, and `""` raises `ParseError` (not any other exception).
**HOW:**
```bash
python -c "
from calc.lexer import tokenize; from calc.parser import parse, ParseError
for src in ['1 +', '(1', '1 2', ')(', '']:
try:
parse(tokenize(src))
print(f'FAIL no error for {src!r}')
except ParseError as e:
print(f'OK ParseError for {src!r}: {e}')
except Exception as e:
print(f'FAIL wrong exception for {src!r}: {type(e).__name__}: {e}')
"
```
**EXPECTED:**
```
OK ParseError for '1 +': unexpected token 'EOF' (None)
OK ParseError for '(1': unclosed parenthesis, got 'EOF'
OK ParseError for '1 2': unexpected token 'NUMBER' (2)
OK ParseError for ')(': unexpected token 'RPAREN' (')')
OK ParseError for '': empty input
```
**WHERE:** `calc/parser.py` @ commit `866091c`
---
## Gate D4 CLAIMED — awaiting Adversary
**WHAT:** Unary minus parses correctly for `-5`, `-(1+2)`, `3 * -2`.
**HOW:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-5')))"
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-(1+2)')))"
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('3 * -2')))"
```
**EXPECTED:**
```
Unary(op='-', operand=Num(value=5))
Unary(op='-', operand=BinOp(op='+', left=Num(value=1), right=Num(value=2)))
BinOp(op='*', left=Num(value=3), right=Unary(op='-', operand=Num(value=2)))
```
**WHERE:** `calc/parser.py` @ commit `866091c`
---
## Gate D3 CLAIMED — awaiting Adversary
**WHAT:** Parens override precedence: `(1+2)*3` parses as `BinOp(*, BinOp(+, Num(1), Num(2)), Num(3))`.
**HOW:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('(1+2)*3')))"
```
**EXPECTED:**
```
BinOp(op='*', left=BinOp(op='+', left=Num(value=1), right=Num(value=2)), right=Num(value=3))
```
**WHERE:** `calc/parser.py` @ commit `866091c`
---
## Gate D2 CLAIMED — awaiting Adversary
**WHAT:** Same-precedence operators associate left: `8-3-2``BinOp(-, BinOp(-, Num(8), Num(3)), Num(2))`; `8/4/2``BinOp(/, BinOp(/, Num(8), Num(4)), Num(2))`.
**HOW:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8-3-2')))"
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8/4/2')))"
```
**EXPECTED:**
```
BinOp(op='-', left=BinOp(op='-', left=Num(value=8), right=Num(value=3)), right=Num(value=2))
BinOp(op='/', left=BinOp(op='/', left=Num(value=8), right=Num(value=4)), right=Num(value=2))
```
**WHERE:** `calc/parser.py` @ commit `866091c`
---
## Gate D1 CLAIMED — awaiting Adversary
**WHAT:** `*` and `/` bind tighter than `+` and `-`: `1+2*3` parses as `BinOp(+, Num(1), BinOp(*, Num(2), Num(3)))`.
**HOW:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"
```
**EXPECTED:**
```
BinOp(op='+', left=Num(value=1), right=BinOp(op='*', left=Num(value=2), right=Num(value=3)))
```
**WHERE:** `calc/parser.py` @ commit `866091c`
---
## AST Shape
```python
@dataclass
class Num:
value: int | float
@dataclass
class BinOp:
op: str # '+', '-', '*', '/'
left: Node
right: Node
@dataclass
class Unary:
op: str # '-'
operand: Node
```
Grammar:
```
expr = term (('+' | '-') term)*
term = unary (('*' | '/') unary)*
unary = '-' unary | primary
primary = NUMBER | '(' expr ')'
```