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,4 @@
__pycache__/
*.pyc
*.pyo
.pytest_cache/

View File

@ -0,0 +1,15 @@
# git history (claim/review handshake), from the run's shared bare repo
a3263ed status(eval): ## DONE — all D1-D5 Adversary-verified PASS
df84a12 review(D1-D5): PASS — all gates cold-verified, 62 tests OK, break-it probes held
165c7cc claim(D1-D5): implement evaluator + CLI — all eval gates ready for Adversary verification
e1dece1 review(init): Adversary initialized for phase eval — watching for Builder claims
4146139 status(parse): ## DONE — all D1-D6 Adversary-verified PASS
bed53d5 review(D1-D6): PASS — all gates cold-verified, 45 tests OK, break-it probes held
7768832 claim(D1-D6): implement parser — all gates ready for Adversary verification
1bcba29 review(init): Adversary initialized for phase parse — watching for Builder claims
026f551 status(lex): ## DONE — all D1-D4 Adversary-verified PASS
2f4fd6b review(D1-D4): PASS — all gates cold-verified, 21 tests OK, break-it probes held
071871f chore: add .gitignore, remove __pycache__ from tracking
531a8cf claim(D1-D4): implement lexer — all gates ready for Adversary verification
5826b69 review(init): Adversary initialized for phase lex — watching for Builder claims
13c0db5 chore: seed

View File

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

View File

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

View File

@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""calc.py — command-line arithmetic calculator.
Usage: python calc.py "<expression>"
"""
import sys
from calc.lexer import LexError, tokenize
from calc.parser import ParseError, parse
from calc.evaluator import EvalError, evaluate
def main() -> None:
if len(sys.argv) != 2:
print("Usage: calc.py \"<expression>\"", file=sys.stderr)
sys.exit(1)
expr = sys.argv[1]
try:
tokens = tokenize(expr)
ast = parse(tokens)
result = evaluate(ast)
except (LexError, ParseError, EvalError) as exc:
print(f"Error: {exc}", file=sys.stderr)
sys.exit(1)
print(result)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,47 @@
"""Evaluator for the arithmetic AST produced by calc.parser."""
from __future__ import annotations
from calc.parser import BinOp, Num, Node, Unary
class EvalError(Exception):
"""Raised on a runtime evaluation error (e.g. division by zero)."""
def evaluate(node: Node) -> int | float:
"""Walk *node* and return its numeric value.
Result type rule: if the mathematical result is a whole number, return int;
otherwise return float. This guarantees '4/2' → 2 and '7/2' → 3.5.
Raises EvalError on division by zero.
"""
if isinstance(node, Num):
return node.value
if isinstance(node, Unary):
v = evaluate(node.operand)
return -v
if isinstance(node, BinOp):
left = evaluate(node.left)
right = evaluate(node.right)
if node.op == '+':
result = left + right
elif node.op == '-':
result = left - right
elif node.op == '*':
result = left * right
elif node.op == '/':
if right == 0:
raise EvalError("Division by zero")
result = left / right
else:
raise EvalError(f"Unknown operator {node.op!r}")
if isinstance(result, float) and result == int(result):
return int(result)
return result
raise EvalError(f"Unknown node type {type(node)!r}")

View File

@ -0,0 +1,67 @@
"""Lexer for arithmetic expressions."""
from dataclasses import dataclass
from typing import Union
class LexError(Exception):
"""Raised on an unrecognised character."""
@dataclass
class Token:
kind: str # NUMBER PLUS MINUS STAR SLASH LPAREN RPAREN EOF
value: Union[int, float, str, None]
_SINGLE = {
'+': 'PLUS',
'-': 'MINUS',
'*': 'STAR',
'/': 'SLASH',
'(': 'LPAREN',
')': 'RPAREN',
}
def tokenize(src: str) -> list:
"""Return a list of Token for *src*, ending with EOF."""
tokens = []
i = 0
n = len(src)
while i < n:
ch = src[i]
# Skip whitespace
if ch in ' \t\r\n':
i += 1
continue
# Number: integer or float (leading dot allowed, trailing dot allowed)
if ch.isdigit() or ch == '.':
j = i
has_dot = False
while j < n and (src[j].isdigit() or (src[j] == '.' and not has_dot)):
if src[j] == '.':
has_dot = True
j += 1
raw = src[i:j]
if raw == '.':
raise LexError(f"Unexpected character '.' at position {i}")
value = float(raw) if has_dot else int(raw)
tokens.append(Token('NUMBER', value))
i = j
continue
# Single-character operators and parentheses
if ch in _SINGLE:
tokens.append(Token(_SINGLE[ch], ch))
i += 1
continue
# Anything else is an error
raise LexError(f"Unexpected character {ch!r} at position {i}")
tokens.append(Token('EOF', None))
return tokens

View File

@ -0,0 +1,143 @@
"""Recursive-descent parser for arithmetic expressions.
AST node types:
Num(value) -- a numeric literal (int or float)
BinOp(op, left, right) -- binary operation; op in ('+','-','*','/')
Unary(op, operand) -- unary operation; op == '-'
Grammar (precedence low → high):
expr := term (('+' | '-') term)*
term := unary (('*' | '/') unary)*
unary := '-' unary | primary
primary := NUMBER | '(' expr ')'
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Union
from calc.lexer import Token, tokenize
class ParseError(Exception):
"""Raised on syntactically invalid input."""
# ---------------------------------------------------------------------------
# AST nodes
# ---------------------------------------------------------------------------
@dataclass
class Num:
value: Union[int, float]
def __repr__(self) -> str:
return f"Num({self.value!r})"
@dataclass
class BinOp:
op: str
left: "Node"
right: "Node"
def __repr__(self) -> str:
return f"BinOp({self.op!r}, {self.left!r}, {self.right!r})"
@dataclass
class Unary:
op: str
operand: "Node"
def __repr__(self) -> str:
return f"Unary({self.op!r}, {self.operand!r})"
Node = Union[Num, BinOp, Unary]
# ---------------------------------------------------------------------------
# Parser
# ---------------------------------------------------------------------------
class _Parser:
def __init__(self, tokens: list[Token]) -> None:
self._tokens = tokens
self._pos = 0
def _peek(self) -> Token:
return self._tokens[self._pos]
def _consume(self, kind: str | None = None) -> Token:
tok = self._tokens[self._pos]
if kind is not None and tok.kind != kind:
raise ParseError(
f"Expected {kind}, 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}) after expression"
)
return node
def _expr(self) -> Node:
node = self._term()
while self._peek().kind in ('PLUS', 'MINUS'):
op_tok = self._consume()
op = op_tok.value # '+' or '-'
right = self._term()
node = BinOp(op, node, right)
return node
def _term(self) -> Node:
node = self._unary()
while self._peek().kind in ('STAR', 'SLASH'):
op_tok = self._consume()
op = op_tok.value # '*' or '/'
right = self._unary()
node = BinOp(op, node, right)
return node
def _unary(self) -> Node:
if self._peek().kind == 'MINUS':
self._consume('MINUS')
operand = self._unary()
return Unary('-', operand)
return self._primary()
def _primary(self) -> Node:
tok = self._peek()
if tok.kind == 'NUMBER':
self._consume('NUMBER')
return Num(tok.value)
if tok.kind == 'LPAREN':
self._consume('LPAREN')
node = self._expr()
if self._peek().kind != 'RPAREN':
raise ParseError(
f"Expected ')' but got {self._peek().kind!r}"
)
self._consume('RPAREN')
return node
if tok.kind == 'EOF':
raise ParseError("Unexpected end of input")
raise ParseError(
f"Unexpected token {tok.kind!r} ({tok.value!r})"
)
def parse(tokens: list[Token]) -> Node:
"""Parse *tokens* (from lexer.tokenize) into an AST Node.
Raises ParseError on malformed input.
"""
return _Parser(tokens).parse()

View File

@ -0,0 +1,87 @@
"""Tests for calc.evaluator — covers D1, D2, D3."""
import unittest
from calc.lexer import tokenize
from calc.parser import parse
from calc.evaluator import EvalError, evaluate
def calc(s):
return evaluate(parse(tokenize(s)))
class TestArithmetic(unittest.TestCase):
"""D1 — arithmetic, precedence, parens, unary minus."""
def test_add(self):
self.assertEqual(calc("2+3"), 5)
def test_mul(self):
self.assertEqual(calc("3*4"), 12)
def test_precedence(self):
self.assertEqual(calc("2+3*4"), 14)
def test_parens(self):
self.assertEqual(calc("(2+3)*4"), 20)
def test_sub_left_assoc(self):
self.assertEqual(calc("8-3-2"), 3)
def test_unary_minus(self):
self.assertEqual(calc("-2+5"), 3)
def test_unary_minus_mul(self):
self.assertEqual(calc("2*-3"), -6)
def test_sub(self):
self.assertEqual(calc("10-3"), 7)
def test_nested_parens(self):
self.assertEqual(calc("((4))"), 4)
def test_unary_double(self):
self.assertEqual(calc("--5"), 5)
class TestDivision(unittest.TestCase):
"""D2 — true division, division by zero → EvalError."""
def test_true_division(self):
self.assertAlmostEqual(calc("7/2"), 3.5)
def test_division_by_zero(self):
with self.assertRaises(EvalError):
calc("1/0")
def test_division_by_zero_expr(self):
with self.assertRaises(EvalError):
calc("5/(3-3)")
class TestResultType(unittest.TestCase):
"""D3 — whole-valued results are int, non-whole are float."""
def test_whole_division_is_int(self):
result = calc("4/2")
self.assertEqual(result, 2)
self.assertIsInstance(result, int)
def test_non_whole_is_float(self):
result = calc("7/2")
self.assertEqual(result, 3.5)
self.assertIsInstance(result, float)
def test_integer_arithmetic_stays_int(self):
result = calc("2+3")
self.assertIsInstance(result, int)
def test_whole_negative_is_int(self):
result = calc("6/2")
self.assertEqual(result, 3)
self.assertIsInstance(result, int)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,134 @@
"""Unit tests for calc.lexer — covers D1, D2, D3."""
import unittest
from calc.lexer import tokenize, Token, LexError
def kinds(src):
return [t.kind for t in tokenize(src)]
def values(src):
return [(t.kind, t.value) for t in tokenize(src)]
class TestNumbers(unittest.TestCase):
"""D1 — integers and floats."""
def test_integer(self):
toks = tokenize("42")
self.assertEqual(toks[0].kind, 'NUMBER')
self.assertEqual(toks[0].value, 42)
self.assertIsInstance(toks[0].value, int)
self.assertEqual(toks[1].kind, 'EOF')
def test_float(self):
toks = tokenize("3.14")
self.assertEqual(toks[0].kind, 'NUMBER')
self.assertAlmostEqual(toks[0].value, 3.14)
self.assertIsInstance(toks[0].value, float)
def test_float_leading_dot(self):
toks = tokenize(".5")
self.assertEqual(toks[0].kind, 'NUMBER')
self.assertAlmostEqual(toks[0].value, 0.5)
self.assertIsInstance(toks[0].value, float)
def test_float_trailing_dot(self):
toks = tokenize("10.")
self.assertEqual(toks[0].kind, 'NUMBER')
self.assertAlmostEqual(toks[0].value, 10.0)
self.assertIsInstance(toks[0].value, float)
def test_integer_eof(self):
toks = tokenize("42")
self.assertEqual(kinds("42"), ['NUMBER', 'EOF'])
class TestOperatorsAndParens(unittest.TestCase):
"""D2 — operators and parentheses."""
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_expr_kinds(self):
self.assertEqual(
kinds("1+2*3"),
['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'],
)
def test_complex_expr(self):
self.assertEqual(
kinds("3.5*(1-2)"),
['NUMBER', 'STAR', 'LPAREN', 'NUMBER', 'MINUS', 'NUMBER', 'RPAREN', 'EOF'],
)
class TestWhitespaceAndErrors(unittest.TestCase):
"""D3 — whitespace is skipped; invalid chars raise LexError."""
def test_whitespace_skipped(self):
self.assertEqual(
kinds(" 12 + 3 "),
['NUMBER', 'PLUS', 'NUMBER', 'EOF'],
)
def test_tab_whitespace(self):
self.assertEqual(kinds("1\t+\t2"), ['NUMBER', 'PLUS', 'NUMBER', 'EOF'])
def test_at_raises_lexerror(self):
with self.assertRaises(LexError) as ctx:
tokenize("1 @ 2")
self.assertIn('@', str(ctx.exception))
def test_dollar_raises_lexerror(self):
with self.assertRaises(LexError) as ctx:
tokenize("$")
self.assertIn('$', str(ctx.exception))
def test_letter_raises_lexerror(self):
with self.assertRaises(LexError) as ctx:
tokenize("x")
self.assertIn('x', str(ctx.exception))
def test_lexerror_includes_position(self):
with self.assertRaises(LexError) as ctx:
tokenize("1 @ 2")
# position 2 (0-indexed) where '@' appears
self.assertIn('2', str(ctx.exception))
def test_required_whitespace_expr(self):
# " 12 + 3 " from the DoD
toks = tokenize(" 12 + 3 ")
self.assertEqual(toks[0].value, 12)
self.assertEqual(toks[2].value, 3)
def test_required_complex_expr(self):
# "3.5*(1-2)" from the DoD
toks = tokenize("3.5*(1-2)")
self.assertAlmostEqual(toks[0].value, 3.5)
self.assertEqual(toks[1].kind, 'STAR')
self.assertEqual(toks[2].kind, 'LPAREN')
self.assertEqual(toks[3].value, 1)
self.assertEqual(toks[4].kind, 'MINUS')
self.assertEqual(toks[5].value, 2)
self.assertEqual(toks[6].kind, 'RPAREN')
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,149 @@
"""Tests for calc/parser.py — D1 through D5."""
import unittest
from calc.lexer import tokenize
from calc.parser import parse, ParseError, Num, BinOp, Unary
def p(src: str):
"""Shorthand: tokenize + parse."""
return parse(tokenize(src))
class TestPrecedence(unittest.TestCase):
"""D1 — * and / bind tighter than + and -."""
def test_add_then_mul(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_mul_then_add(self):
# 2*3+4 → BinOp('+', BinOp('*', Num(2), Num(3)), Num(4))
tree = p("2*3+4")
self.assertEqual(tree, BinOp('+', BinOp('*', Num(2), Num(3)), Num(4)))
def test_sub_then_div(self):
# 10-6/2 → BinOp('-', Num(10), BinOp('/', Num(6), Num(2)))
tree = p("10-6/2")
self.assertEqual(tree, BinOp('-', Num(10), BinOp('/', Num(6), Num(2))))
def test_div_then_sub(self):
# 6/2-1 → BinOp('-', BinOp('/', Num(6), Num(2)), Num(1))
tree = p("6/2-1")
self.assertEqual(tree, BinOp('-', BinOp('/', Num(6), Num(2)), Num(1)))
class TestLeftAssociativity(unittest.TestCase):
"""D2 — same-precedence operators associate left."""
def test_sub_left(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(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(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_mul_left(self):
# 2*3*4 → BinOp('*', BinOp('*', Num(2), Num(3)), Num(4))
tree = p("2*3*4")
self.assertEqual(tree, BinOp('*', BinOp('*', Num(2), Num(3)), Num(4)))
class TestParentheses(unittest.TestCase):
"""D3 — parentheses override precedence."""
def test_paren_overrides_mul(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_paren_on_right(self):
# 3*(1+2) → BinOp('*', Num(3), BinOp('+', Num(1), Num(2)))
tree = p("3*(1+2)")
self.assertEqual(tree, BinOp('*', Num(3), BinOp('+', Num(1), Num(2))))
def test_nested_parens(self):
# ((4)) → Num(4)
tree = p("((4))")
self.assertEqual(tree, Num(4))
def test_paren_changes_assoc(self):
# 8-(3-2) → BinOp('-', Num(8), BinOp('-', Num(3), Num(2)))
tree = p("8-(3-2)")
self.assertEqual(tree, BinOp('-', Num(8), BinOp('-', Num(3), Num(2))))
class TestUnaryMinus(unittest.TestCase):
"""D4 — leading and nested unary minus."""
def test_simple_negative(self):
# -5 → Unary('-', Num(5))
tree = p("-5")
self.assertEqual(tree, Unary('-', Num(5)))
def test_unary_paren(self):
# -(1+2) → Unary('-', BinOp('+', Num(1), Num(2)))
tree = p("-(1+2)")
self.assertEqual(tree, Unary('-', BinOp('+', Num(1), Num(2))))
def test_mul_unary_rhs(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):
# --5 → Unary('-', Unary('-', Num(5)))
tree = p("--5")
self.assertEqual(tree, Unary('-', Unary('-', Num(5))))
def test_unary_in_expr(self):
# 1 + -2 → BinOp('+', Num(1), Unary('-', Num(2)))
tree = p("1 + -2")
self.assertEqual(tree, BinOp('+', Num(1), Unary('-', Num(2))))
class TestErrors(unittest.TestCase):
"""D5 — malformed input raises ParseError."""
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_numbers(self):
with self.assertRaises(ParseError):
p("1 2")
def test_close_open_paren(self):
# ")(" has no valid parse
with self.assertRaises(ParseError):
p(")(")
def test_empty_string(self):
with self.assertRaises(ParseError):
p("")
def test_close_paren_only(self):
with self.assertRaises(ParseError):
p(")")
def test_only_operator(self):
with self.assertRaises(ParseError):
p("*")
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,7 @@
# BACKLOG — eval phase
## Build backlog
_(Builder's section — read-only for Adversary)_
## Adversary findings
_(none yet)_

View File

@ -0,0 +1,13 @@
# BACKLOG-lex
## Build backlog
- [x] Create calc/__init__.py
- [x] Create calc/lexer.py with Token, LexError, tokenize()
- [x] Create calc/test_lexer.py covering D1D4 (21 tests)
- [x] Run tests: 21 passed, 0 failed
- [ ] Claim D1 + D2 + D3 + D4 (all gates, single claim)
- [ ] Await Adversary verification
## Adversary findings
(none yet)

View File

@ -0,0 +1,20 @@
# BACKLOG — Phase parse
## Build backlog
- [x] Create calc/parser.py (ParseError, Num, BinOp, Unary, recursive-descent parse())
- [x] Create calc/test_parser.py (24 unittest tests, D1D5 coverage)
- [x] Verified 45 tests pass (21 lexer + 24 parser), 0 failures
- [x] CLAIM D1D6 (all gates claimed together)
## Adversary findings
(none yet)
## Break-it probes planned
- Precedence weak test: ensure `1+2*3` really builds `BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))` not `BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))`
- Associativity weak test: ensure `8-3-2` builds `BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))` not `BinOp('-', Num(8), BinOp('-', Num(3), Num(2)))`
- Nested unary: `--5`, `-(-(1+2))` should work
- ParseError specificity: check it's ParseError (not generic Exception) for all 5 error cases in D5
- Empty input edge case

View File

@ -0,0 +1,12 @@
# DECISIONS — shared append-only log
## 2026-06-15T00:12Z — Adversary initialized
Adversary started for phase lex. Watching for Builder claims via git.
## 2026-06-15 — Token representation (Builder)
Using a dataclass for Token with `kind: str` and `value`. The `kind` is a plain string constant
(e.g. "NUMBER"), not an enum, keeping pure stdlib with minimal boilerplate.
## 2026-06-15 — NUMBER value type (Builder)
`value` for NUMBER tokens is `int` if no decimal point, else `float`. Matches Python's natural
numeric types; convenient for the evaluator phase.

View File

@ -0,0 +1,43 @@
# JOURNAL — eval phase
## 2026-06-15 — Implementation
Built evaluator, CLI, and tests in one go.
### evaluator.py
`evaluate(node)` walks the AST recursively:
- `Num` → return `node.value` directly (already int or float from lexer)
- `Unary('-', operand)` → negate result
- `BinOp(op, left, right)` → evaluate both sides, apply op
Division: uses Python `left / right` (always returns float). If `right == 0`, raises `EvalError("Division by zero")`.
D3 result type rule: after computing a float result in BinOp, if `result == int(result)` we cast to int. This ensures `4/2 → 2` (int) and `7/2 → 3.5` (float). Integer arithmetic returns int naturally.
### calc.py CLI
`main()` accepts exactly one argv argument (the expression string).
Catches `LexError`, `ParseError`, `EvalError` → prints `Error: <msg>` to stderr, exits 1.
On success prints `result` (which is already int or float with correct type per D3 rule).
### Test run output
```
$ python -m unittest -q
Ran 62 tests in 0.001s
OK
```
(45 prior tests from lex+parse phases, 17 new evaluator tests)
### CLI checks
```
$ 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 (exit 1)
$ python calc.py "1 +" → Error: Unexpected end of input (exit 1)
```

View File

@ -0,0 +1,48 @@
# JOURNAL-lex — Adversary
## 2026-06-15T00:12Z — Cold start
Read phase plan lex.md. Builder has not pushed any code yet (only seed commit 13c0db5).
Initialized REVIEW, BACKLOG, JOURNAL files. Waiting for Builder to push work.
DoD gates to verify:
- D1: numbers (integers and floats)
- D2: operators & parens
- D3: whitespace & errors (LexError)
- D4: tests green (python -m unittest -q)
## 2026-06-15T00:20Z — Verification complete
Builder pushed claim(D1-D4). Pulled, ran cold verification, ran break-it probes.
All four gates PASS. No vetoes. Wrote verdicts to REVIEW-lex.md.
Adversary probes: empty string, lone dot, whitespace-only, double-dot, 1.2.3, position accuracy — all held.
---
# JOURNAL-lex — Builder
## 2026-06-15 — Implementation
Read phase plan. Built calc/lexer.py and calc/test_lexer.py from scratch.
### Implementation choices
- Token is a dataclass with `kind: str` and `value: int | float | str | None`
- NUMBER value is int (no dot) or float (dot present)
- LexError message includes repr of the offending char and its 0-based position
- Leading dot (`.5`) and trailing dot (`10.`) are valid floats (scanned by loop: stops when second dot seen)
- Single lone dot is an error (raw == '.')
### Test run output
```
python -m unittest -q
----------------------------------------------------------------------
Ran 21 tests in 0.000s
OK
```
### Verification commands output
```
$ 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')"
calc.lexer.LexError: Unexpected character '@' at position 2
```

View File

@ -0,0 +1,44 @@
# JOURNAL — Phase parse (Adversary)
## 2026-06-15T00:18Z — Adversary initialized
- Phase parse kicked off. Lex phase confirmed ## DONE.
- Initialized REVIEW-parse.md, STATUS-parse.md, BACKLOG-parse.md, JOURNAL-parse.md.
- Watching for Builder to claim gates D1D6.
- Planned break-it probes logged in BACKLOG.
## 2026-06-15T00:22Z — Cold verification complete, all gates PASS
- Watchdog pinged: Builder claimed D1D6 in commit 7768832.
- Read parser.py and test_parser.py cold (no prior state).
- Ran `python -m unittest -q` → 45 tests, 0 failures.
- Ran all AST shape checks from STATUS-parse.md — every output matched expected.
- Ran full break-it probe suite: right-assoc trap, triple unary, deep nesting, float, extra error cases.
- All held. No defects found. PASS recorded in REVIEW-parse.md for D1D6.
---
# JOURNAL — Phase parse (Builder)
## 2026-06-15T00:19Z — Implementation complete
Design: recursive-descent with grammar:
```
expr := term (('+' | '-') term)*
term := unary (('*' | '/') unary)*
unary := '-' unary | primary
primary := NUMBER | '(' expr ')'
```
Left-associativity emerges from iterative loops (not recursion) in expr/term.
Ran `python -m unittest -q``Ran 45 tests in 0.001s OK`
Key outputs verified:
- `1+2*3``BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))`
- `8-3-2``BinOp('-', BinOp('-', Num(8), Num(3)), Num(2))`
- `8/4/2``BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))`
- `(1+2)*3``BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))`
- `-5``Unary('-', Num(5))`
- `-(1+2)``Unary('-', BinOp('+', Num(1), Num(2)))`
- `3 * -2``BinOp('*', Num(3), Unary('-', Num(2)))`
- All D5 error cases → `ParseError`
Claiming all gates D1D6.

View File

@ -0,0 +1,96 @@
# REVIEW — eval phase (Adversary)
_Adversary cold-verification log. Each verdict is recorded here._
## Status summary
- D1 arithmetic: PASS
- D2 division + EvalError: PASS
- D3 result type: PASS
- D4 CLI: PASS
- D5 tests green + end-to-end: PASS
## Verdicts
### D1 — arithmetic: PASS @2026-06-15T00:27:02Z
Cold run, commit 165c7cc.
Commands run and results:
```
python calc.py "2+3*4" → 14 ✓
python calc.py "(2+3)*4" → 20 ✓
python calc.py "8-3-2" → 3 ✓
python calc.py "-2+5" → 3 ✓
python calc.py "2*-3" → -6 ✓
python calc.py "--5" → 5 ✓ (double unary)
python calc.py "10-3-2" → 5 ✓ (left-associativity)
python calc.py "-(2+3)" → -5 ✓ (unary on paren)
```
### D2 — division + EvalError: PASS @2026-06-15T00:27:02Z
```
python calc.py "7/2" → 3.5 ✓ (true division)
python calc.py "1/0" → "Error: Division by zero" on stderr, exit 1 ✓
python calc.py "5/(3-3)" → "Error: Division by zero" on stderr, exit 1 ✓
```
Verified `EvalError` is raised (not bare `ZeroDivisionError`) via:
- `calc/evaluator.py:37-38` explicitly checks `right == 0` and raises `EvalError`
- `calc.py:24` catches `EvalError` — ZeroDivisionError would escape if not caught; confirmed not raised
`EvalError` is a proper subclass of `Exception`: confirmed True.
### D3 — result type: PASS @2026-06-15T00:27:02Z
```
evaluate(parse(tokenize("4/2"))) → int 2 ✓
evaluate(parse(tokenize("7/2"))) → float 3.5 ✓
evaluate(parse(tokenize("2+3"))) → int 5 ✓ (integer arithmetic stays int)
evaluate(parse(tokenize("-6/2"))) → int -3 ✓ (negative whole result is int)
evaluate(parse(tokenize("1000*1000"))) → int 1000000 ✓
```
Rule documented in `calc/evaluator.py` docstring. `print(4/2)` outputs `2` (not `2.0`). ✓
### D4 — CLI: PASS @2026-06-15T00:27:02Z
```
python calc.py "2+3*4" → prints 14, exit 0 ✓
python calc.py "1 +" → "Error: Unexpected end of input" on stderr, exit 1, no traceback ✓
python calc.py "1/0" → "Error: Division by zero" on stderr, exit 1, no traceback ✓
python calc.py "" → "Error: Empty input" on stderr, exit 1 ✓
python calc.py → usage message on stderr, exit 1 ✓
python calc.py "((1+2)" → error on stderr, exit 1, no traceback ✓
```
No tracebacks in any error path. ✓
### D5 — tests green + end-to-end: PASS @2026-06-15T00:27:02Z
```
python -m unittest -q → Ran 62 tests in 0.001s OK
```
62 tests = lex + parse + evaluator suites. 0 failures, 0 errors. No regression. ✓
## Adversary findings
_(none — all gates pass, no defects found)_
## Break-it probes run
- Traceback check on all error paths: no traceback in any case ✓
- No-args invocation: graceful usage error ✓
- Empty string input: graceful error ✓
- Double unary minus `--5` → 5 ✓
- Left-associativity `10-3-2` → 5 ✓
- Unary in division `-8/2` → -4 ✓
- Negative whole result type `-6/2` → int -3 ✓
- Large numbers `1000*1000` → int 1000000 ✓
- Division by zero via expression `5/(3-3)` → EvalError ✓
- Unclosed paren `((1+2)` → parse error, no crash ✓
---
Initialized: 2026-06-15T00:24:45Z
Verdicts filed: 2026-06-15T00:27:02Z

View File

@ -0,0 +1,67 @@
# REVIEW-lex — Adversary verdicts
Phase: lex
Adversary cold-started: 2026-06-15
## Status
All gates verified PASS. No vetoes.
## Verdicts
### D1: PASS @2026-06-15T00:20Z
**Evidence (cold run from work-adv):**
```
tokenize("42") → NUMBER(42 int), EOF ✓
tokenize("3.14") → NUMBER(3.14 float) ✓
tokenize(".5") → NUMBER(0.5 float) ✓
tokenize("10.") → NUMBER(10.0 float) ✓
```
Plan spec: "tokenize('42') → [NUMBER(42), EOF]" — confirmed exactly.
### D2: PASS @2026-06-15T00:20Z
**Evidence:**
```
kinds("1+2*3") → ['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'] ✓
all operators +-*/() → PLUS MINUS STAR SLASH LPAREN RPAREN ✓
tokens("3.5*(1-2)") → [('NUMBER', 3.5), ('STAR','*'), ('LPAREN','('), ('NUMBER', 1),
('MINUS','-'), ('NUMBER', 2), ('RPAREN',')'), ('EOF', None)] ✓
```
### D3: PASS @2026-06-15T00:20Z
**Evidence:**
```
tokenize(" 12 + 3 ") → ['NUMBER', 'PLUS', 'NUMBER', 'EOF'] ✓ (whitespace skipped)
tokenize("1 @ 2") → LexError: Unexpected character '@' at position 2 ✓
tokenize("$") → LexError: ... '$' ... ✓
tokenize("x") → LexError: ... 'x' at position 0 ✓
tokenize(".") → LexError: ... '.' at position 0 ✓
LexError message includes offending char AND position ✓
```
### D4: PASS @2026-06-15T00:20Z
**Evidence:**
```
$ python -m unittest -q
----------------------------------------------------------------------
Ran 21 tests in 0.000s
OK
```
Exit code: 0. 21 tests, 0 failures.
Required cases covered: " 12 + 3 " ✓, "3.5*(1-2)" ✓, "1 @ 2" → LexError ✓.
## Adversarial probes (break-it attempts — all held)
- Empty string → `[EOF]` (correct)
- Whitespace-only → `[EOF]` (correct)
- Lone dot → `LexError: Unexpected character '.' at position 0` (correct)
- `..` → LexError on lone dot (correct)
- `1.2.3``NUMBER(1.2) NUMBER(0.3) EOF` (valid lexer behavior; parser rejects)
- `12 @ 5` → LexError at position 3 (position accuracy confirmed)
- All operators in sequence `+-*/()` → correct kinds
- Newline treated as whitespace (conservative, robust)
No defects found. Implementation is correct per the phase plan.

View File

@ -0,0 +1,91 @@
# REVIEW — Phase parse (Adversary)
## Status
All gates PASS. Ready to approve ## DONE.
## Gate verdicts
| Gate | Verdict | Timestamp | Evidence |
|------|---------|-----------|----------|
| D1 — precedence | PASS | 2026-06-15T00:22:39Z | See below |
| D2 — left associativity | PASS | 2026-06-15T00:22:39Z | See below |
| D3 — parentheses | PASS | 2026-06-15T00:22:39Z | See below |
| D4 — unary minus | PASS | 2026-06-15T00:22:39Z | See below |
| D5 — errors | PASS | 2026-06-15T00:22:39Z | See below |
| D6 — tests green | PASS | 2026-06-15T00:22:39Z | Ran 45 tests in 0.001s OK |
## Cold-verification evidence
### D6 — tests green
```
python -m unittest -q
Ran 45 tests in 0.001s
OK
```
21 lexer + 24 parser tests, 0 failures.
### D1 — precedence (cold AST shape check)
```
1+2*3 → BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) ✓ (* binds tighter)
2*3+4 → BinOp('+', BinOp('*', Num(2), Num(3)), Num(4)) ✓
10-6/2 → BinOp('-', Num(10), BinOp('/', Num(6), Num(2))) ✓
```
Independently derived: `1+2*3` must have `+` at root with `*` in right child — confirmed.
### D2 — left associativity (cold AST shape check)
```
8-3-2 → BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)) ✓ (left-assoc)
8/4/2 → BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)) ✓
1+2+3 → BinOp('+', BinOp('+', Num(1), Num(2)), Num(3)) ✓
2*3*4 → BinOp('*', BinOp('*', Num(2), Num(3)), Num(4)) ✓
1-2-3 → BinOp('-', BinOp('-', Num(1), Num(2)), Num(3)) ✓ (break-it: not right-assoc)
```
Iterative while-loop in `_expr`/`_term` enforces left-assoc by construction.
### D3 — parentheses (cold AST shape check)
```
(1+2)*3 → BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) ✓
3*(1+2) → BinOp('*', Num(3), BinOp('+', Num(1), Num(2))) ✓
((4)) → Num(4) ✓
8-(3-2) → BinOp('-', Num(8), BinOp('-', Num(3), Num(2))) ✓
((((1+2)))) → BinOp('+', Num(1), Num(2)) ✓ (deep nesting)
```
### D4 — unary minus (cold AST shape check)
```
-5 → Unary('-', Num(5)) ✓
-(1+2) → Unary('-', BinOp('+', Num(1), Num(2))) ✓
3 * -2 → BinOp('*', Num(3), Unary('-', Num(2))) ✓
--5 → Unary('-', Unary('-', Num(5))) ✓
1 + -2 → BinOp('+', Num(1), Unary('-', Num(2))) ✓
---5 → Unary('-', Unary('-', Unary('-', Num(5)))) ✓ (break-it: triple unary)
6 / -2 → BinOp('/', Num(6), Unary('-', Num(2))) ✓ (break-it: unary in denom)
```
`_unary` is right-recursive: `'-' _unary | _primary` — correct for unary.
### D5 — errors (cold: all five plan cases + extras)
```
'1 +' → OK ParseError
'(1' → OK ParseError
'1 2' → OK ParseError
')(' → OK ParseError
'' → OK ParseError
'*' → OK ParseError (break-it)
')' → OK ParseError (break-it)
'1+2)' → OK ParseError (break-it)
'((1+2)' → OK ParseError (break-it)
```
All raise `ParseError` specifically, not a generic exception.
## Break-it probes run
- Right-assoc trap (`1-2-3`, `8-3-2`, `8/4/2`): held — correctly left-assoc
- Deep nesting `((((1+2))))`: held
- Triple unary `---5`: held
- Unary in denominator `6/-2`: held
- Float literal `3.14 → Num(3.14)`: parsed fine
- Trailing `)` after valid expr `1+2)`: ParseError ✓
- Solo operator `*`: ParseError ✓
- Double-unclosed paren `((1+2)`: ParseError ✓
## Adversary findings
None. No defects found. All probes held.

View File

@ -0,0 +1,23 @@
# STATUS — Phase eval
## DONE
All DoD gates Adversary-verified PASS. Phase eval is complete. This is the final phase — the calculator is end-to-end: string → tokens → AST → number.
| Gate | Status | Verified |
|------|--------|----------|
| D1 — arithmetic | PASS | 2026-06-15T00:27:02Z |
| D2 — division + EvalError | PASS | 2026-06-15T00:27:02Z |
| D3 — result type | PASS | 2026-06-15T00:27:02Z |
| D4 — CLI | PASS | 2026-06-15T00:27:02Z |
| D5 — tests green + end-to-end | PASS | 2026-06-15T00:27:02Z |
Adversary ran 62 tests (exit 0), cold-verified all D1D5 gates, and ran full break-it probe suite
(double unary, left-assoc, negative whole result type, division by zero via expression, unclosed
paren, empty string, no-args) — all held. No defects found.
## Artifacts
- `calc/evaluator.py``EvalError`, `evaluate()`
- `calc/test_evaluator.py` — 17 unittest tests (D1D3 coverage, type assertions)
- `calc.py` — top-level CLI (D4 + end-to-end check)

View File

@ -0,0 +1,20 @@
# STATUS — Phase lex
## DONE
All DoD gates Adversary-verified PASS. Phase lex is complete.
| Gate | Status | Verified |
|------|--------|----------|
| D1 — numbers | PASS | 2026-06-15T00:20Z |
| D2 — operators & parens | PASS | 2026-06-15T00:20Z |
| D3 — whitespace & errors | PASS | 2026-06-15T00:20Z |
| D4 — tests green | PASS | 2026-06-15T00:20Z |
Adversary ran 21 tests (exit 0), cold-verified all token outputs, and ran break-it probes — all held.
## Artifacts
- `calc/lexer.py` — Token dataclass, LexError, tokenize()
- `calc/test_lexer.py` — 21 unittest tests
- `calc/__init__.py` — package marker

View File

@ -0,0 +1,22 @@
# STATUS — Phase parse
## DONE
All DoD gates Adversary-verified PASS. Phase parse is complete.
| Gate | Status | Verified |
|------|--------|----------|
| D1 — precedence | PASS | 2026-06-15T00:22:39Z |
| D2 — left associativity | PASS | 2026-06-15T00:22:39Z |
| D3 — parentheses | PASS | 2026-06-15T00:22:39Z |
| D4 — unary minus | PASS | 2026-06-15T00:22:39Z |
| D5 — errors | PASS | 2026-06-15T00:22:39Z |
| D6 — tests green | PASS | 2026-06-15T00:22:39Z |
Adversary ran 45 tests (exit 0), cold-verified all AST shapes, and ran full break-it probe suite
(right-assoc trap, triple unary, deep nesting, float, extra error cases) — all held. No defects found.
## Artifacts
- `calc/parser.py` — ParseError, Num, BinOp, Unary, parse()
- `calc/test_parser.py` — 24 unittest tests (D1D5 coverage, tree-structure assertions)