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,16 @@
# git history (claim/review handshake), from the run's shared bare repo
e34a7a8 status(eval): ## DONE — all D1-D5 Adversary-verified PASS
691f290 review(D1,D2,D3,D4,D5): PASS — all 56 tests green, all DoD commands verified cold
9662369 claim(D1,D2,D3,D4,D5): eval phase complete — 56 tests green, CLI verified
2e76913 review(eval/init): Adversary initialized for eval phase, awaiting Builder claims
aee82a9 status: parse phase DONE — all D1-D6 Adversary-verified PASS
f87dc76 review(D1,D2,D3,D4,D5,D6): PASS — all 41 tests green, all DoD commands verified cold
c965498 status: add commit sha to parse claim
23d0ae9 claim(D1,D2,D3,D4,D5,D6): parser complete, all 41 tests green
ae7bfb0 review(parse/init): Adversary initialized for parse phase, awaiting Builder claims
496ad69 status: phase lex DONE — all D1-D4 PASS from Adversary
fc428fc review(D1,D2,D3,D4): PASS — all 22 tests green, all DoD commands verified cold
43d4c26 status: add commit sha to claim
7022854 claim(D1,D2,D3,D4): lexer complete, all 22 tests green
0fa8ea4 review(init): Adversary initialized, watching for Builder claims
c6a3e58 chore: seed

View File

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

View File

@ -0,0 +1 @@
original path: /tmp/ao-campaign-Ofyz4E/builder-adversary/r5

View File

@ -0,0 +1,26 @@
import sys
from calc.lexer import tokenize, LexError
from calc.parser import parse, ParseError
from calc.evaluator import evaluate, EvalError
def _fmt(result) -> str:
return str(result)
def main():
if len(sys.argv) != 2:
print("usage: calc.py <expression>", file=sys.stderr)
sys.exit(1)
try:
tokens = tokenize(sys.argv[1])
ast = parse(tokens)
result = evaluate(ast)
print(_fmt(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,33 @@
from calc.parser import Num, BinOp, Unary
class EvalError(Exception):
pass
def evaluate(node) -> "int | float":
"""Walk the AST and return a numeric result.
Type rule: integer arithmetic stays int; division returns float, except when
the quotient is whole-valued — then it is normalised to int so the CLI can
print it without a trailing '.0'.
"""
if isinstance(node, Num):
return node.value
if isinstance(node, Unary):
return -evaluate(node.operand)
if isinstance(node, BinOp):
left = evaluate(node.left)
right = evaluate(node.right)
if node.op == 'PLUS':
return left + right
if node.op == 'MINUS':
return left - right
if node.op == 'STAR':
return left * right
if node.op == 'SLASH':
if right == 0:
raise EvalError("division by zero")
result = left / right
return int(result) if result == int(result) else result
raise EvalError(f"unknown node: {type(node).__name__}")

View File

@ -0,0 +1,56 @@
from dataclasses import dataclass
from typing import Union
class LexError(Exception):
pass
@dataclass
class Token:
kind: str
value: Union[int, float, None] = None
def tokenize(src: str) -> list:
tokens = []
i = 0
n = len(src)
while i < n:
ch = src[i]
if ch in ' \t':
i += 1
elif ch.isdigit() or ch == '.':
j = i
while j < n and src[j].isdigit():
j += 1
if j < n and src[j] == '.':
j += 1
while j < n and src[j].isdigit():
j += 1
tokens.append(Token('NUMBER', float(src[i:j])))
else:
tokens.append(Token('NUMBER', int(src[i:j])))
i = j
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
else:
raise LexError(f"unexpected character {ch!r} at position {i}")
tokens.append(Token('EOF'))
return tokens

View File

@ -0,0 +1,104 @@
from dataclasses import dataclass
from typing import Union
class ParseError(Exception):
pass
@dataclass
class Num:
value: Union[int, float]
def __repr__(self):
return f"Num({self.value})"
@dataclass
class BinOp:
op: str
left: object
right: object
def __repr__(self):
return f"BinOp({self.op}, {self.left!r}, {self.right!r})"
@dataclass
class Unary:
op: str
operand: object
def __repr__(self):
return f"Unary({self.op}, {self.operand!r})"
class _Parser:
def __init__(self, tokens):
self.tokens = tokens
self.pos = 0
def _peek(self):
return self.tokens[self.pos]
def _consume(self, kind=None):
tok = self.tokens[self.pos]
if kind is not None and tok.kind != kind:
raise ParseError(f"expected {kind!r}, got {tok.kind!r}")
self.pos += 1
return tok
def _expr(self):
left = self._term()
while self._peek().kind in ('PLUS', 'MINUS'):
op = self._consume().kind
right = self._term()
left = BinOp(op, left, right)
return left
def _term(self):
left = self._unary()
while self._peek().kind in ('STAR', 'SLASH'):
op = self._consume().kind
right = self._unary()
left = BinOp(op, left, right)
return left
def _unary(self):
if self._peek().kind == 'MINUS':
self._consume()
return Unary('MINUS', self._unary())
return self._primary()
def _primary(self):
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"expected ')', got {self._peek().kind!r}")
self._consume()
return node
raise ParseError(f"unexpected token {tok.kind!r}")
def parse(tokens) -> object:
"""Parse a token list into an AST.
Nodes:
Num(value) — numeric literal (int or float)
BinOp(op, left, right) — binary op; op in {'PLUS','MINUS','STAR','SLASH'}
Unary(op, operand) — unary minus; op == 'MINUS'
Raises ParseError on malformed input.
"""
p = _Parser(tokens)
if p._peek().kind == 'EOF':
raise ParseError("empty input")
node = p._expr()
if p._peek().kind != 'EOF':
raise ParseError(f"unexpected token {p._peek().kind!r} after expression")
return node

View File

@ -0,0 +1,74 @@
import unittest
from calc.lexer import tokenize
from calc.parser import parse
from calc.evaluator import evaluate, EvalError
def calc(s):
return evaluate(parse(tokenize(s)))
class TestArithmetic(unittest.TestCase):
def test_add_mul_precedence(self):
self.assertEqual(calc("2+3*4"), 14)
def test_paren_override(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(self):
self.assertEqual(calc("-2+5"), 3)
def test_unary_in_mul(self):
self.assertEqual(calc("2*-3"), -6)
def test_add(self):
self.assertEqual(calc("1+2"), 3)
def test_sub(self):
self.assertEqual(calc("5-3"), 2)
def test_mul(self):
self.assertEqual(calc("3*4"), 12)
class TestDivision(unittest.TestCase):
def test_true_division(self):
self.assertEqual(calc("7/2"), 3.5)
def test_exact_division(self):
self.assertEqual(calc("4/2"), 2)
def test_division_by_zero(self):
with self.assertRaises(EvalError):
calc("1/0")
def test_division_by_zero_not_bare(self):
# Must raise EvalError, not the raw ZeroDivisionError
try:
calc("1/0")
self.fail("expected EvalError")
except EvalError:
pass
except ZeroDivisionError:
self.fail("bare ZeroDivisionError escaped; must be EvalError")
class TestResultType(unittest.TestCase):
def test_int_arithmetic_stays_int(self):
self.assertIsInstance(calc("2+3"), int)
def test_whole_division_returns_int(self):
# 4/2 = 2.0 → normalised to int so fmt can omit '.0'
self.assertIsInstance(calc("4/2"), int)
self.assertEqual(calc("4/2"), 2)
def test_non_whole_division_returns_float(self):
self.assertIsInstance(calc("7/2"), float)
self.assertEqual(calc("7/2"), 3.5)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,125 @@
import unittest
from calc.lexer import tokenize, Token, LexError
def kinds(src):
return [t.kind for t in tokenize(src)]
def pairs(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, [Token("NUMBER", 42), Token("EOF")])
def test_float(self):
result = tokenize("3.14")
self.assertEqual(result[0], Token("NUMBER", 3.14))
self.assertEqual(result[1], Token("EOF"))
def test_float_leading_dot(self):
result = tokenize(".5")
self.assertEqual(result[0], Token("NUMBER", 0.5))
self.assertEqual(result[1], Token("EOF"))
def test_float_trailing_dot(self):
result = tokenize("10.")
self.assertEqual(result[0], Token("NUMBER", 10.0))
self.assertEqual(result[1], Token("EOF"))
def test_number_value_int(self):
result = tokenize("0")
self.assertIsInstance(result[0].value, int)
def test_number_value_float(self):
result = tokenize("3.14")
self.assertIsInstance(result[0].value, float)
class TestOperatorsAndParens(unittest.TestCase):
def test_plus(self):
self.assertIn("PLUS", kinds("+"))
def test_minus(self):
self.assertIn("MINUS", kinds("-"))
def test_star(self):
self.assertIn("STAR", kinds("*"))
def test_slash(self):
self.assertIn("SLASH", kinds("/"))
def test_lparen(self):
self.assertIn("LPAREN", kinds("("))
def test_rparen(self):
self.assertIn("RPAREN", kinds(")"))
def test_expression_1_plus_2_times_3(self):
self.assertEqual(
kinds("1+2*3"),
["NUMBER", "PLUS", "NUMBER", "STAR", "NUMBER", "EOF"],
)
def test_all_operators(self):
self.assertEqual(
kinds("+-*/()"),
["PLUS", "MINUS", "STAR", "SLASH", "LPAREN", "RPAREN", "EOF"],
)
class TestWhitespaceAndErrors(unittest.TestCase):
def test_whitespace_between_tokens(self):
result = tokenize(" 12 + 3 ")
self.assertEqual(kinds(" 12 + 3 "), ["NUMBER", "PLUS", "NUMBER", "EOF"])
self.assertEqual(result[0].value, 12)
self.assertEqual(result[2].value, 3)
def test_tabs_skipped(self):
self.assertEqual(kinds("1\t+\t2"), ["NUMBER", "PLUS", "NUMBER", "EOF"])
def test_complex_expression(self):
self.assertEqual(
pairs("3.5*(1-2)"),
[
("NUMBER", 3.5),
("STAR", None),
("LPAREN", None),
("NUMBER", 1),
("MINUS", None),
("NUMBER", 2),
("RPAREN", None),
("EOF", None),
],
)
def test_invalid_at_raises_lex_error(self):
with self.assertRaises(LexError):
tokenize("1 @ 2")
def test_invalid_dollar_raises_lex_error(self):
with self.assertRaises(LexError):
tokenize("$5")
def test_invalid_letter_raises_lex_error(self):
with self.assertRaises(LexError):
tokenize("x")
def test_lex_error_message_contains_char(self):
try:
tokenize("1 @ 2")
except LexError as e:
self.assertIn("@", str(e))
def test_lex_error_message_contains_position(self):
try:
tokenize("1 @ 2")
except LexError as e:
self.assertIn("2", str(e))
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,104 @@
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_over_add(self):
# 1+2*3 → 1+(2*3)
self.assertEqual(repr(p('1+2*3')),
'BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3)))')
def test_div_over_sub(self):
# 6-4/2 → 6-(4/2)
self.assertEqual(repr(p('6-4/2')),
'BinOp(MINUS, Num(6), BinOp(SLASH, Num(4), Num(2)))')
def test_mul_over_add_reversed(self):
# 2*3+1 → (2*3)+1
self.assertEqual(repr(p('2*3+1')),
'BinOp(PLUS, BinOp(STAR, Num(2), Num(3)), Num(1))')
class TestLeftAssociativity(unittest.TestCase):
def test_subtraction_left(self):
# 8-3-2 → (8-3)-2
self.assertEqual(repr(p('8-3-2')),
'BinOp(MINUS, BinOp(MINUS, Num(8), Num(3)), Num(2))')
def test_division_left(self):
# 8/4/2 → (8/4)/2
self.assertEqual(repr(p('8/4/2')),
'BinOp(SLASH, BinOp(SLASH, Num(8), Num(4)), Num(2))')
def test_addition_left(self):
# 1+2+3 → (1+2)+3
self.assertEqual(repr(p('1+2+3')),
'BinOp(PLUS, BinOp(PLUS, Num(1), Num(2)), Num(3))')
def test_mul_left(self):
# 2*3*4 → (2*3)*4
self.assertEqual(repr(p('2*3*4')),
'BinOp(STAR, BinOp(STAR, Num(2), Num(3)), Num(4))')
class TestParentheses(unittest.TestCase):
def test_parens_override_precedence(self):
# (1+2)*3 → STAR at top, PLUS inside
self.assertEqual(repr(p('(1+2)*3')),
'BinOp(STAR, BinOp(PLUS, Num(1), Num(2)), Num(3))')
def test_parens_on_right(self):
# 3*(1+2)
self.assertEqual(repr(p('3*(1+2)')),
'BinOp(STAR, Num(3), BinOp(PLUS, Num(1), Num(2)))')
def test_nested_parens(self):
self.assertEqual(repr(p('((2))')), 'Num(2)')
class TestUnaryMinus(unittest.TestCase):
def test_leading_unary(self):
self.assertEqual(repr(p('-5')), 'Unary(MINUS, Num(5))')
def test_unary_paren(self):
self.assertEqual(repr(p('-(1+2)')),
'Unary(MINUS, BinOp(PLUS, Num(1), Num(2)))')
def test_unary_in_mul(self):
self.assertEqual(repr(p('3 * -2')),
'BinOp(STAR, Num(3), Unary(MINUS, Num(2)))')
def test_double_unary(self):
self.assertEqual(repr(p('--5')),
'Unary(MINUS, Unary(MINUS, Num(5)))')
class TestErrors(unittest.TestCase):
def test_trailing_operator(self):
with self.assertRaises(ParseError):
p('1 +')
def test_unclosed_paren(self):
with self.assertRaises(ParseError):
p('(1')
def test_two_consecutive_numbers(self):
with self.assertRaises(ParseError):
p('1 2')
def test_close_open_paren(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 (shared)
## Build backlog
- [x] Write `calc/evaluator.py` with `evaluate(node)` and `EvalError`
- [x] Write `calc/test_evaluator.py` (unittest, 15 cases, D1D3)
- [x] Write `calc.py` CLI (D4)
- [x] Verify 56 tests green (D5, no regression)
- [x] Write STATUS-eval.md with cold-verify commands
- [ ] Await Adversary PASS on all gates; write ## DONE
## Adversary findings
<!-- Adversary-owned — filed when breaking probes find issues -->
No findings yet.

View File

@ -0,0 +1,15 @@
# BACKLOG — phase: lex
## Build backlog (Builder-owned)
- [x] Create machine-docs phase files
- [x] Create calc/__init__.py
- [x] Create calc/lexer.py with Token, LexError, tokenize()
- [x] Create calc/test_lexer.py with unittest suite
- [x] Run tests locally and verify all pass (22/22)
- [x] Claim D1-D4
- [ ] Wait for Adversary PASS on all gates
- [ ] Write ## DONE to STATUS-lex.md
## Adversary findings
(Adversary-owned — do not edit)

View File

@ -0,0 +1,12 @@
# BACKLOG — phase: parse (Builder)
## Build backlog
- [x] Implement `calc/parser.py` with ParseError, Num, BinOp, Unary, parse()
- [x] Implement `calc/test_parser.py` with 19 tests covering D1D5
- [x] Run full test suite (41 tests green)
- [x] Write STATUS-parse.md with WHAT/HOW/EXPECTED/WHERE
- [x] Claim D1D6
## Adversary findings
(Read-only — Adversary writes here)

View File

@ -0,0 +1,11 @@
# DECISIONS — shared (append-only)
## 2026-06-15 — Token.value for non-numeric tokens
Decision: `Token.value` is `None` for operator/paren/EOF tokens, and `int` or `float` for NUMBER tokens.
Rationale: Parser phases only need the numeric value; operator/paren tokens carry no useful payload beyond their kind.
## 2026-06-15 — Float parsing strategy
Decision: Use Python's built-in `float()` for converting float literals. Detect float vs int by presence of `.` in the matched string.
Rationale: Handles edge cases like `.5` and `10.` correctly via stdlib, avoids manual parsing bugs.

View File

@ -0,0 +1,39 @@
# JOURNAL — phase: eval (Builder)
## 2026-06-15 — implementation
### Design
AST nodes from parse phase: `Num(value)`, `BinOp(op, left, right)`, `Unary(op, operand)`.
Evaluator is a recursive tree walk. Division uses Python's `/` (true division), then normalises
whole-valued floats to `int` (avoids trailing `.0` in CLI output without needing special fmt logic).
### Runs
```
$ python -m unittest -q
----------------------------------------------------------------------
Ran 56 tests in 0.002s
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' (stderr, exit 1)
```
### Type normalisation choice
`int(result) if result == int(result) else result` — chose this over a `fmt` conversion in the CLI
so `evaluate` itself is honest about the value type and tests can assert `isinstance(result, int)`.

View File

@ -0,0 +1,57 @@
# JOURNAL — phase: lex
## 2026-06-15 — Start
Read phase plan. Mission: build `calc/lexer.py` with `tokenize()`, Token type, LexError, and unittest suite.
Plan:
- Token: dataclass with `kind` (str) and `value` (int | float | None)
- Kinds: NUMBER, PLUS, MINUS, STAR, SLASH, LPAREN, RPAREN, EOF
- LexError: custom Exception with char + position
- tokenize(): iterate over chars, match numbers (int/float), operators, parens; skip whitespace; raise LexError on unknown char; append EOF at end
## 2026-06-15 — Implementation
Created:
- `calc/__init__.py` (empty package marker)
- `calc/lexer.py` — token scanner using manual char-by-char iteration
- `calc/test_lexer.py` — 22 unittest cases
Float parsing design: detect float vs int by presence of `.` in scanned substring. Handle `.5` (leading dot) and `10.` (trailing dot) correctly by scanning digits before/after the dot separately.
## 2026-06-15 — Test run
```
$ python -m unittest -q
----------------------------------------------------------------------
Ran 22 tests in 0.001s
OK
```
## 2026-06-15 — Verify commands
```
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('42')])"
[('NUMBER', 42), ('EOF', None)]
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.14')])"
[('NUMBER', 3.14), ('EOF', None)]
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('.5')])"
[('NUMBER', 0.5), ('EOF', None)]
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('10.')])"
[('NUMBER', 10.0), ('EOF', None)]
$ python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize('1+2*3')])"
['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF']
$ python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
[('NUMBER', 3.5), ('STAR', None), ('LPAREN', None), ('NUMBER', 1), ('MINUS', None), ('NUMBER', 2), ('RPAREN', None), ('EOF', None)]
$ python -c "from calc.lexer import tokenize; tokenize('1 @ 2')" 2>&1
calc.lexer.LexError: unexpected character '@' at position 2
```
All plan verify commands produce expected output. Claiming D1D4.

View File

@ -0,0 +1,46 @@
# JOURNAL — phase: parse (Builder)
## 2026-06-15
### Design decisions
Chose a classic recursive-descent parser with separate grammar levels for precedence:
- `_expr` handles `+`/`-` (low precedence, left-associative via while-loop)
- `_term` handles `*`/`/` (high precedence, left-associative via while-loop)
- `_unary` handles prefix `-` (right-associative via recursion)
- `_primary` handles `NUMBER` and `(expr)`
Left-associativity comes naturally from the iterative loop pattern (each iteration wraps the accumulating `left` node deeper).
### Verification runs
```
$ python -m unittest -q
Ran 41 tests in 0.001s
OK
```
Key shape outputs verified:
```
1+2*3 → BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3))) ✓ D1
8-3-2 → BinOp(MINUS, BinOp(MINUS, Num(8), Num(3)), Num(2)) ✓ D2
8/4/2 → BinOp(SLASH, BinOp(SLASH, Num(8), Num(4)), Num(2)) ✓ D2
(1+2)*3 → BinOp(STAR, BinOp(PLUS, Num(1), Num(2)), Num(3)) ✓ D3
-5 → Unary(MINUS, Num(5)) ✓ D4
-(1+2) → Unary(MINUS, BinOp(PLUS, Num(1), Num(2))) ✓ D4
3*-2 → BinOp(STAR, Num(3), Unary(MINUS, Num(2))) ✓ D4
```
D5 errors all raise `ParseError` (not `SyntaxError`, `ValueError`, etc.):
- `"1 +"` → ParseError: unexpected token 'EOF'
- `"(1"` → ParseError: expected ')', got 'EOF'
- `"1 2"` → ParseError: unexpected token 'NUMBER' after expression
- `")("` → ParseError: unexpected token 'RPAREN'
- `""` → ParseError: empty input
### Empty-string handling
`tokenize('')` returns `[Token('EOF')]`. The `parse()` function checks the first token;
if it's `EOF`, raises `ParseError("empty input")` immediately, avoiding an ambiguous
"unexpected token 'EOF'" message.

View File

@ -0,0 +1,52 @@
# REVIEW — phase: eval (Adversary)
## Status
Initialized @2026-06-15T01:43:40Z
Verdicts written @2026-06-15T01:58:00Z — ALL PASS
## Gate verdicts
### eval/D1: PASS @2026-06-15T01:58:00Z
All 5 required expressions correct from cold run:
- `2+3*4``14` (precedence: * before +) ✓
- `(2+3)*4``20` (parens override precedence) ✓
- `8-3-2``3` (left-associativity) ✓
- `-2+5``3` (unary minus leading) ✓
- `2*-3``-6` (unary minus in mul) ✓
Additional probes: `2*(3+4)-1`→13, `-(3+4)`→-7, `--5`→5 all correct.
### eval/D2: PASS @2026-06-15T01:58:00Z
- `7/2``3.5` (true division, not floor) ✓
- `1/0` raises `EvalError("division by zero")` — not bare `ZeroDivisionError`
- `0/0` also raises `EvalError` (edge case probed) ✓
- Error goes to stderr, exit code 1 ✓
- API-level check confirmed: `try/except EvalError` catches it, `ZeroDivisionError` does not ✓
### eval/D3: PASS @2026-06-15T01:58:00Z
- `4/2``2` (int, no trailing `.0`) ✓
- `7/2``3.5` (float) ✓
- `6/3``2`, `9/3``3`, `10/5``2` (whole-valued normalisation consistent) ✓
- Integer arithmetic stays `int`: `2+3` → int, `3*4` → int, `-5` → int ✓
- Type assertions: `assertIsInstance(calc("4/2"), int)` and `assertIsInstance(calc("7/2"), float)` both pass ✓
### eval/D4: PASS @2026-06-15T01:58:00Z
- `python calc.py "2+3*4"``14`, exit 0 ✓
- `python calc.py "(2+3)*4"``20`, exit 0 ✓
- `python calc.py "7/2"``3.5`, exit 0 ✓
- `python calc.py "4/2"``2`, exit 0 ✓
- `python calc.py "1/0"` → error to stderr, exit 1 ✓
- `python calc.py "1 +"` → error to stderr, exit 1 ✓
- `python calc.py ""``error: empty input`, exit 1 ✓
- `python calc.py "abc"` → exit 1 (LexError caught) ✓
- No tracebacks leak to stderr (grep for 'traceback' found nothing) ✓
- Exact output format confirmed: `[14]`, `[3.5]` — no extra whitespace ✓
### eval/D5: PASS @2026-06-15T01:58:00Z
- `python -m unittest -q``Ran 56 tests in 0.001s / OK`
- 41 lex+parse tests: still all green (no regression) ✓
- 15 new evaluator tests: all green ✓
- Test classes: TestArithmetic (8), TestDivision (4), TestResultType (3) ✓
## No findings, no VETO
All five gates PASS. No defects found. Builder may write ## DONE.

View File

@ -0,0 +1,56 @@
# REVIEW — phase `lex` (Adversary)
## Status
All 4 gates verified. Phase COMPLETE.
## Verdicts
### lex/D1: PASS @2026-06-15T01:35:56Z
Cold-run evidence:
```
tokenize('42') → [('NUMBER', 42), ('EOF', None)] — int ✓
tokenize('3.14') → [('NUMBER', 3.14), ('EOF', None)] — float ✓
tokenize('.5') → [('NUMBER', 0.5), ('EOF', None)] — float ✓
tokenize('10.') → [('NUMBER', 10.0), ('EOF', None)] — float ✓
type(tokenize('42')[0].value) == int ✓
type(tokenize('3.14')[0].value) == float ✓
```
Value is int for integers, float for floats. DoD fully met.
### lex/D2: PASS @2026-06-15T01:35:56Z
Cold-run evidence:
```
tokenize('1+2*3') → ['NUMBER','PLUS','NUMBER','STAR','NUMBER','EOF'] ✓
tokenize('+-*/()') → ['PLUS','MINUS','STAR','SLASH','LPAREN','RPAREN','EOF'] ✓
tokenize('3.5*(1-2)') → correct kind/value pairs ✓
```
All 6 operator/paren kinds present and correct.
### lex/D3: PASS @2026-06-15T01:35:56Z
Cold-run evidence:
```
tokenize(' 12 + 3 ') → NUMBER(12) PLUS NUMBER(3) EOF — spaces skipped ✓
tokenize('1\t+\t2') → NUMBER PLUS NUMBER EOF — tabs skipped ✓
tokenize('1 @ 2') → LexError: unexpected character '@' at position 2 ✓
tokenize('$5') → LexError: unexpected character '$' at position 0 ✓
tokenize('x') → LexError: unexpected character 'x' at position 0 ✓
```
Offending char and position both present in message. DoD fully met.
### lex/D4: PASS @2026-06-15T01:35:56Z
```
python -m unittest -q
Ran 22 tests in 0.001s
OK
```
Zero failures, zero errors. All 22 tests cover D1D3 including required cases:
`" 12 + 3 "`, `"3.5*(1-2)"`, and `"1 @ 2"` raising LexError.
## Informational finding (non-blocking)
**F1 — lone `.` raises `ValueError` not `LexError`:** `tokenize('.')` crashes with
`ValueError: could not convert string to float: '.'` instead of a `LexError`. The
DoD does not require this case to be handled, so this is NOT a blocker for any gate.
Noted for future phases that may extend the lexer.
No VETO. All DoD items independently verified.

View File

@ -0,0 +1,106 @@
# REVIEW — phase: parse (Adversary)
## Verdict: ALL GATES PASS @2026-06-15T01:42:00Z
Cold-verified from commit `23d0ae9` (work-adv clone, fresh shell, no cached state).
---
### D6 (tests green): PASS @2026-06-15T01:42:00Z
```
python -m unittest -q
Ran 41 tests in 0.001s
OK
```
41 tests (22 lexer + 19 parser), 0 failures. Tests assert on `repr()`/tree structure, not evaluation — satisfies the plan requirement.
---
### D1 (precedence): PASS @2026-06-15T01:42:00Z
```
1+2*3 → BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3))) ✓
6-4/2 → BinOp(MINUS, Num(6), BinOp(SLASH, Num(4), Num(2))) ✓
```
Re-derived independently: grammar `expr: term((+|-)term)*` / `term: unary((*|/)unary)*` guarantees `*`/`/` subtrees sit inside `+`/`-` nodes. Observed matches.
Extra adversarial probe:
```
1+2*3+4 → BinOp(PLUS, BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3))), Num(4)) ✓
```
---
### D2 (left associativity): PASS @2026-06-15T01:42:00Z
```
8-3-2 → BinOp(MINUS, BinOp(MINUS, Num(8), Num(3)), Num(2)) ✓
8/4/2 → BinOp(SLASH, BinOp(SLASH, Num(8), Num(4)), Num(2)) ✓
```
Extra probes:
```
1+2+3 → BinOp(PLUS, BinOp(PLUS, Num(1), Num(2)), Num(3)) ✓
2*3*4 → BinOp(STAR, BinOp(STAR, Num(2), Num(3)), Num(4)) ✓
4*3/2 → BinOp(SLASH, BinOp(STAR, Num(4), Num(3)), Num(2)) ✓
```
Left-associativity correctly implemented via iterative while-loop (not recursion).
---
### D3 (parentheses): PASS @2026-06-15T01:42:00Z
```
(1+2)*3 → BinOp(STAR, BinOp(PLUS, Num(1), Num(2)), Num(3)) ✓
((2)) → Num(2) ✓
(-5) → Unary(MINUS, Num(5)) ✓
```
---
### D4 (unary minus): PASS @2026-06-15T01:42:00Z
```
-5 → Unary(MINUS, Num(5)) ✓
-(1+2) → Unary(MINUS, BinOp(PLUS, Num(1), Num(2))) ✓
3 * -2 → BinOp(STAR, Num(3), Unary(MINUS, Num(2))) ✓
--5 → Unary(MINUS, Unary(MINUS, Num(5))) ✓ (double unary)
2+-3 → BinOp(PLUS, Num(2), Unary(MINUS, Num(3))) ✓ (unary after binary)
```
Right-recursive `_unary` correctly handles chained negation.
---
### D5 (errors raise ParseError): PASS @2026-06-15T01:42:00Z
All five required cases verified — each raises `ParseError`, not `ValueError`, `IndexError`, or any other exception:
```
'1 +' → ParseError: unexpected token 'EOF' ✓
'(1' → ParseError: expected ')', got 'EOF' ✓
'1 2' → ParseError: unexpected token 'NUMBER' after expression ✓
')(' → ParseError: unexpected token 'RPAREN' ✓
'' → ParseError: empty input ✓
```
Extra adversarial error probes (all raise ParseError):
```
'*5' → ParseError: unexpected token 'STAR' ✓
'5*' → ParseError: unexpected token 'EOF' ✓
'()' → ParseError: unexpected token 'RPAREN' ✓
'(+5)' → ParseError: unexpected token 'PLUS' ✓
'1++2' → ParseError: unexpected token 'PLUS' ✓
```
---
## Summary
All six DoD gates D1D6 independently verified cold. No defects found. The grammar in STATUS exactly matches the implementation (`_expr`/`_term`/`_unary`/`_primary`). Node shape is stable and documented in the `parse()` docstring. No veto.
Builder may write `## DONE` to STATUS-parse.md.

View File

@ -0,0 +1,44 @@
# STATUS — phase: eval (Builder)
## DONE
Gate: D1 D2 D3 D4 D5 — Adversary PASS @2026-06-15T01:58:00Z. Phase complete.
## What was built
- `calc/evaluator.py``evaluate(node) -> int | float`; `EvalError` for bad nodes / division by zero
- `calc/test_evaluator.py` — 15 unittest cases covering D1D3
- `calc.py` (repo root) — CLI: `python calc.py "<expr>"`
## Commit SHA
See `git log --oneline -1` on origin/main after this push.
## Verify commands (cold — run from repo root)
```bash
python -m unittest -q
# Expected: Ran 56 tests in <N>s / OK
python calc.py "2+3*4" # Expected: 14
python calc.py "(2+3)*4" # Expected: 20
python calc.py "7/2" # Expected: 3.5
python calc.py "4/2" # Expected: 2
python calc.py "1/0" # Expected: prints error to stderr, exits 1
python calc.py "1 +" # Expected: prints error to stderr, exits 1
```
## Gate mapping
| Gate | DoD | Verify |
|------|-----|--------|
| D1 | arithmetic: `+ - * /`, precedence, parens, unary minus | `python -m unittest -q` (TestArithmetic) |
| D2 | true division; `EvalError` on div-by-zero (not bare `ZeroDivisionError`) | `python -m unittest -q` (TestDivision) |
| D3 | whole-valued → int (no `.0`); non-whole → float | `python -m unittest -q` (TestResultType) |
| D4 | `python calc.py "2+3*4"` → 14 exit 0; error → stderr + exit 1 | manual CLI commands above |
| D5 | 56 tests total (lex+parse+eval), 0 failures | `python -m unittest -q` |
## Result type rule (D3)
Division result is normalised: `result = left / right; return int(result) if result == int(result) else result`.
The CLI's `_fmt` calls `str(result)`, so `int` prints as `"2"` and `float` prints as `"3.5"`.

View File

@ -0,0 +1,70 @@
# STATUS — phase: lex (Builder)
## DONE
All gates verified PASS by Adversary @2026-06-15T01:35:56Z.
## Gate: D1, D2, D3, D4 — PASS
### WHAT is claimed
All four gates D1D4 are implemented and tested.
### HOW to verify (run from a fresh clone)
```bash
cd <repo>
python -m unittest -q
```
Expected: 22 tests, 0 failures, 0 errors.
```bash
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('42')])"
```
Expected: `[('NUMBER', 42), ('EOF', None)]`
```bash
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.14')])"
```
Expected: `[('NUMBER', 3.14), ('EOF', None)]`
```bash
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('.5')])"
```
Expected: `[('NUMBER', 0.5), ('EOF', None)]`
```bash
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('10.')])"
```
Expected: `[('NUMBER', 10.0), ('EOF', None)]`
```bash
python -c "from calc.lexer import tokenize; print([t.kind for t in tokenize('1+2*3')])"
```
Expected: `['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF']`
```bash
python -c "from calc.lexer import tokenize; print([(t.kind,t.value) for t in tokenize('3.5*(1-2)')])"
```
Expected: `[('NUMBER', 3.5), ('STAR', None), ('LPAREN', None), ('NUMBER', 1), ('MINUS', None), ('NUMBER', 2), ('RPAREN', None), ('EOF', None)]`
```bash
python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
```
Expected: raises `calc.lexer.LexError: unexpected character '@' at position 2`
### WHERE (commit sha)
`7022854acf94b35ebc79cb48315e91b82c7cc4ec`
### Files
- `calc/__init__.py` — empty package marker
- `calc/lexer.py` — Token dataclass, LexError, tokenize()
- `calc/test_lexer.py` — 22 unittest cases covering D1D3
### Gate checklist
- D1 (numbers): PASS @2026-06-15T01:35:56Z
- D2 (operators & parens): PASS @2026-06-15T01:35:56Z
- D3 (whitespace & errors): PASS @2026-06-15T01:35:56Z
- D4 (tests green): PASS @2026-06-15T01:35:56Z
## Informational (non-blocking)
- F1: lone `.` raises `ValueError` not `LexError` — not required by DoD, noted for future phases

View File

@ -0,0 +1,115 @@
# STATUS — phase: parse (Builder)
## DONE
## Gate: D1D6 — ALL PASS (Adversary-verified @2026-06-15T01:42:00Z)
### WHAT is claimed
All six gates D1D6 are implemented and tested.
- **D1 (precedence):** `*`/`/` bind tighter than `+`/`-`
- **D2 (left associativity):** Same-precedence ops associate left
- **D3 (parentheses):** Parens override precedence
- **D4 (unary minus):** Leading and nested unary minus
- **D5 (errors):** Five malformed inputs each raise `ParseError`
- **D6 (tests green):** 19 parser tests + 22 lexer tests = 41 total, 0 failures
### HOW to verify (run from a fresh clone)
**D6 — all tests green:**
```bash
python -m unittest -q
```
Expected output: `Ran 41 tests in 0.00xs` / `OK`
**D1 — precedence (structural check):**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"
```
Expected: `BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3)))`
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('6-4/2')))"
```
Expected: `BinOp(MINUS, Num(6), BinOp(SLASH, Num(4), Num(2)))`
**D2 — left associativity:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8-3-2')))"
```
Expected: `BinOp(MINUS, BinOp(MINUS, Num(8), Num(3)), Num(2))`
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('8/4/2')))"
```
Expected: `BinOp(SLASH, BinOp(SLASH, Num(8), Num(4)), Num(2))`
**D3 — parentheses:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('(1+2)*3')))"
```
Expected: `BinOp(STAR, BinOp(PLUS, Num(1), Num(2)), Num(3))`
**D4 — unary minus:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-5')))"
```
Expected: `Unary(MINUS, Num(5))`
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('-(1+2)')))"
```
Expected: `Unary(MINUS, BinOp(PLUS, Num(1), Num(2)))`
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('3 * -2')))"
```
Expected: `BinOp(STAR, Num(3), Unary(MINUS, Num(2)))`
**D5 — errors (each must raise `ParseError`, not a different exception):**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize('1 +'))"
# raises: ParseError: unexpected token 'EOF'
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize('(1'))"
# raises: ParseError: expected ')', got 'EOF'
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize('1 2'))"
# raises: ParseError: unexpected token 'NUMBER' after expression
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize(')('))"
# raises: ParseError: unexpected token 'RPAREN'
python -c "from calc.lexer import tokenize; from calc.parser import parse; parse(tokenize(''))"
# raises: ParseError: empty input
```
### EXPECTED AST shapes (re-derivable from grammar)
AST node types:
- `Num(value)` — numeric literal; value is int or float
- `BinOp(op, left, right)` — op in `{'PLUS','MINUS','STAR','SLASH'}`
- `Unary(op, operand)` — op == `'MINUS'`
Grammar used (drives precedence + associativity):
```
expr : term (('+' | '-') term)* ← left-assoc, low precedence
term : unary (('*' | '/') unary)* ← left-assoc, high precedence
unary : '-' unary | primary ← right-assoc chain, prefix only
primary : NUMBER | '(' expr ')'
```
### WHERE (commit sha)
`23d0ae9` (claim(D1,D2,D3,D4,D5,D6): parser complete, all 41 tests green)
### Files
- `calc/parser.py` — ParseError, Num, BinOp, Unary, _Parser, parse()
- `calc/test_parser.py` — 19 unittest cases covering D1D5
### Gate checklist
- D1 (precedence): CLAIMED
- D2 (left associativity): CLAIMED
- D3 (parentheses): CLAIMED
- D4 (unary minus): CLAIMED
- D5 (errors): CLAIMED
- D6 (tests green): CLAIMED