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,9 @@
# BACKLOG — Phase eval (Builder)
## Build backlog
- [x] D1 — arithmetic: CLAIMED
- [ ] D2 — division: pending Adversary PASS on D1
- [ ] D3 — result type: pending Adversary PASS on D1
- [ ] D4 — CLI: pending Adversary PASS on D1
- [ ] D5 — tests green: pending Adversary PASSes on D1-D4

View File

@ -0,0 +1,19 @@
# BACKLOG — Phase lex
## Build backlog
- [x] Create calc/ package with __init__.py
- [x] Implement calc/lexer.py: Token dataclass, LexError, tokenize()
- [x] Implement calc/test_lexer.py: unittest 15 tests covering D1-D3
- [x] Run tests — 15/15 PASS
- [ ] Claim D1 (numbers) → await PASS
- [ ] Claim D2 (operators & parens) → await PASS
- [ ] Claim D3 (whitespace & errors) → await PASS
- [ ] Claim D4 (tests green) → await PASS
## Adversary findings
### AF-1 (non-blocking): bare `.` leaks `ValueError` instead of `LexError`
- Repro: `tokenize('.')``ValueError: could not convert string to float: '.'`
- Expected: `LexError` (or any error that doesn't expose Python internals)
- Status: OPEN, non-blocking — not required by DoD, but may bite future phases

View File

@ -0,0 +1,18 @@
# BACKLOG — Phase parse (Builder)
## Build backlog
- [x] Read phase plan
- [ ] Implement calc/parser.py (Num, BinOp, Unary, parse(), ParseError)
- [ ] Implement calc/test_parser.py
- [ ] Run tests locally — confirm green
- [ ] Claim D1 (precedence)
- [ ] Claim D2 (left associativity)
- [ ] Claim D3 (parentheses)
- [ ] Claim D4 (unary minus)
- [ ] Claim D5 (errors)
- [ ] Claim D6 (tests green)
## Adversary findings
_None yet._

View File

@ -0,0 +1,3 @@
# DECISIONS — Phase lex (shared, append-only)
_No decisions recorded yet._

View File

@ -0,0 +1,27 @@
# JOURNAL — Phase eval (Builder)
## 2026-06-15 — Implementation
Built evaluator, CLI, and test suite in one pass.
AST walk in `calc/evaluator.py`:
- `Num`: return `node.value` (int or float as stored by lexer)
- `Unary('-')`: negate result of recursive evaluate
- `BinOp('+'/'-'/'*')`: straightforward arithmetic on evaluated children
- `BinOp('/')`: true division; guard `right == 0``EvalError`; if result is float and whole, return `int(result)` (D3 rule)
D3 result type rule: `evaluate()` returns `int` for whole-valued results, `float` for non-whole. This means `4/2``int(2)` (prints `2`), `7/2``float(3.5)` (prints `3.5`). Pure integer arithmetic stays int throughout (Python int + int = int).
CLI `calc.py`: catches `LexError`, `ParseError`, `EvalError` → stderr + exit 1. No traceback exposed.
Test run: 46 tests pass (15 lexer + 20 parser + 11 evaluator). No regressions.
Verified all plan cases:
```
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 to stderr, exit 1
python calc.py "1 +" → error to stderr, exit 1
```

View File

@ -0,0 +1,21 @@
# JOURNAL — Phase lex (Adversary)
## 2026-06-15T06:23:05Z — Adversary initialized
- Read phase plan from /home/loops/project-orchestrator/projects/agent-orchestrator-benchmark/plans/calc/lex.md
- DoD gates: D1 (numbers), D2 (operators & parens), D3 (whitespace & errors), D4 (tests green)
- Builder has not yet created STATUS-lex.md or any code
- Initialized REVIEW-lex.md and BACKLOG-lex.md
- Going idle; will check again in 10 min
## 2026-06-15T06:30:00Z — Builder: initial implementation complete
- Created calc/__init__.py, calc/lexer.py, calc/test_lexer.py
- Token is a dataclass with kind:str and value:int|float|str
- LexError is a plain Exception subclass
- tokenize() loops over src with index, handles: whitespace skip, NUMBER (int+float incl. leading/trailing dot), operators/parens via dict, raises LexError for unknowns
- 15 unittest tests, all PASS: `Ran 15 tests in 0.000s OK`
- Plan verify commands output:
- `tokenize('3.5*(1-2)')``[('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', '')]`
- `tokenize('1 @ 2')``LexError: unexpected character '@' at position 2`
- Claiming D1, D2, D3, D4 in one commit

View File

@ -0,0 +1,56 @@
# JOURNAL — Phase parse (Builder)
## 2026-06-15 — Start
Reading phase plan. Lex phase is DONE. Building parser on top of `calc/lexer.py`.
### Design
Grammar (recursive descent):
```
expr → term (('+' | '-') term)*
term → unary (('*' | '/') unary)*
unary → '-' unary | primary
primary → NUMBER | '(' expr ')'
```
- `expr`/`term` loop = left-associative by construction
- `term` deeper than `expr` = `*`/`/` higher precedence than `+`/`-`
- `unary` recursive = right-associative unary (`--5` parses as `-(-5)`)
AST nodes:
- `Num(value)` — leaf
- `BinOp(op, left, right)` — binary operator
- `Unary(op, operand)` — unary operator
ParseError raised for: empty string, trailing operator, unclosed paren, consecutive numbers, mismatched parens.
## 2026-06-15 — Implementation + tests run
```
$ python -m unittest -q
Ran 35 tests in 0.001s
OK
```
(15 from test_lexer + 20 from test_parser)
Plan verify commands:
```
$ python -c "...parse(tokenize('1+2*3'))"
BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) # D1 ✓
$ python -c "...parse(tokenize('(1+2)*3'))"
BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) # D3 ✓
$ python -c "...parse(tokenize('8-3-2'))"
BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)) # D2 ✓
$ python -c "...parse(tokenize('-5'))"
Unary('-', Num(5)) # D4 ✓
$ python -c "...parse(tokenize('3 * -2'))"
BinOp('*', Num(3), Unary('-', Num(2))) # D4 ✓
$ python -c "...parse(tokenize('1 +'))"
calc.parser.ParseError: unexpected end of expression # D5 ✓
```

View File

@ -0,0 +1,37 @@
# REVIEW — Phase eval (Adversary)
## Status
All D1D5 verified PASS. Awaiting Builder DONE declaration.
## Gate verdicts
### eval/D1: PASS @2026-06-15T06:37Z
Cold run of all 5 plan cases: 2+3*4→14, (2+3)*4→20, 8-3-2→3, -2+5→3, 2*-3→-6 all correct.
Adversarial extras (1+2+3→6, 10-2*3→4, 6/2→3, 2*3+4*5→26, -(3)→-3, -(-5)→5) all correct.
Implementation in calc/evaluator.py (commit 0fc263d) handles Num, BinOp, Unary nodes correctly.
### eval/D2: PASS @2026-06-15T06:43Z
7/2→3.5 confirmed. 1/0 raises EvalError (not ZeroDivisionError). ZeroDivisionError does not escape.
Adversarial: 5/(2-2) and 3/(1-1) both raise EvalError; 9/3→3 (int), 10/4→2.5 (float).
### eval/D3: PASS @2026-06-15T06:43Z
4/2→2 (int, no .0), 7/2→3.5 (float), 2+3*4→14 (int). print() output matches DoD spec exactly.
Extra: 9/3→3, 10/4→2.5. Whole-valued division returns int; non-whole returns float.
### eval/D4: PASS @2026-06-15T06:43Z
`python calc.py "2+3*4"` → 14 exit:0; (2+3)*4 → 20; 7/2 → 3.5; 4/2 → 2; all exit:0.
1/0 → stderr "error: division by zero", exit:1. "1 +" → stderr error, exit:1. No traceback.
Adversarial: wrong arg count exits 1 with usage message; lex error (abc) exits 1 with error.
### eval/D5: PASS @2026-06-15T06:43Z
`python -m unittest -q` → Ran 46 tests in 0.001s — OK. 0 failures, no regressions.
15 lexer + 20 parser + 11 evaluator tests all pass (commit 0fc263d).
## Adversary findings
<!-- Defects will be filed here -->

View File

@ -0,0 +1,73 @@
# REVIEW — Phase lex (Adversary)
## Verdicts
### D1: PASS @2026-06-15T06:26:41Z
Cold run evidence:
```
NUMBER 42 int ✓ (integer, value==42, isinstance int)
NUMBER 3.14 float ✓
NUMBER 0.5 float ✓ (leading dot)
NUMBER 10.0 float ✓ (trailing dot)
```
Structure check: `tokenize("42")` → 2 tokens, `[NUMBER(42), EOF]`. Exact match.
Break-it probes: bare `.` raises `ValueError` (not `LexError`) — see Adversary finding AF-1 below. Not a DoD blocker (bare dot not in spec), logged as defect.
### D2: PASS @2026-06-15T06:26:41Z
Cold run evidence:
```
tokenize("1+2*3") → ['NUMBER', 'PLUS', 'NUMBER', 'STAR', 'NUMBER', 'EOF'] ✓
tokenize("+-*/()") → ['PLUS','MINUS','STAR','SLASH','LPAREN','RPAREN','EOF'] ✓
```
All 6 operator/paren kinds verified.
### D3: PASS @2026-06-15T06:26:41Z
Cold run evidence:
```
tokenize(" 12 + 3 ") → ['NUMBER', 'PLUS', 'NUMBER', 'EOF'] ✓
tokenize("1\t+\t2") → ['NUMBER', 'PLUS', 'NUMBER', 'EOF'] ✓
tokenize("1 @ 2") → calc.lexer.LexError: unexpected character '@' at position 2 ✓
tokenize("abc") → calc.lexer.LexError: unexpected character 'a' at position 0 ✓
tokenize("$") → calc.lexer.LexError: unexpected character '$' at position 0 ✓
```
LexError message contains offending char and its position.
### D4: PASS @2026-06-15T06:26:41Z
Cold run evidence:
```
Ran 15 tests in 0.000s
OK
```
Plan's exact commands:
```
tokenize('3.5*(1-2)') → [('NUMBER', 3.5), ('STAR', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', '')] ✓
tokenize('1 @ 2') → calc.lexer.LexError: unexpected character '@' at position 2 ✓
```
Tests cover D1D3 including all plan-required cases: `" 12 + 3 "`, `"3.5*(1-2)"`, `"1 @ 2"`.
---
## Adversary Findings
### AF-1 (non-blocking): bare `.` leaks `ValueError` instead of `LexError`
**Repro:** `python -c "from calc.lexer import tokenize; tokenize('.')"`
**Actual:** `ValueError: could not convert string to float: '.'`
**Expected:** `LexError` (or at minimum, not a raw `ValueError` from Python internals)
**Impact:** The DoD does not list bare dot as a required error case. Not blocking DONE, but future parser phases may hit this if they ever pass a stray `.` to the lexer. Recommend wrapping in a try/except and re-raising as LexError.
**Status:** OPEN (non-blocking)
---
## No VETO
All DoD gates (D1, D2, D3, D4) verified PASS. Builder may write `## DONE` to STATUS-lex.md.

View File

@ -0,0 +1,113 @@
# REVIEW — Phase parse (Adversary)
## Status
All gates D1D6 verified PASS @2026-06-15T06:32:30Z.
---
## Verdicts
### parse/D1: PASS @2026-06-15T06:32:30Z
Cold re-run of Builder's claimed commands:
```
BinOp('+', Num(1), BinOp('*', Num(2), Num(3))) # 1+2*3 — * binds tighter ✓
BinOp('+', BinOp('*', Num(2), Num(3)), Num(4)) # 2*3+4 — * evaluated first ✓
```
Independent break-it probes:
- `1+2*3+4``BinOp('+', BinOp('+', Num(1), BinOp('*', Num(2), Num(3))), Num(4))`
- `4*5-6/2``BinOp('-', BinOp('*', Num(4), Num(5)), BinOp('/', Num(6), Num(2)))`
Derivation confirmed: `_expr` loops over `+/-` consuming `_term` results; `_term` loops over `*//` consuming `_unary` results — two-level precedence grammar is structurally correct.
---
### parse/D2: PASS @2026-06-15T06:32:30Z
Cold re-run:
```
BinOp('-', BinOp('-', Num(8), Num(3)), Num(2)) # 8-3-2 — left-assoc ✓
BinOp('/', BinOp('/', Num(8), Num(4)), Num(2)) # 8/4/2 — left-assoc ✓
```
Independent break-it probe:
- `1-2+3``BinOp('+', BinOp('-', Num(1), Num(2)), Num(3))` ✓ (left-assoc across mixed +/-)
While-loop accumulation pattern in `_expr` and `_term` correctly produces left-leaning trees.
---
### parse/D3: PASS @2026-06-15T06:32:30Z
Cold re-run:
```
BinOp('*', BinOp('+', Num(1), Num(2)), Num(3)) # (1+2)*3 — + under * ✓
BinOp('+', Num(2), Num(3)) # ((2+3)) — parens stripped ✓
```
Independent break-it probe:
- `3*(1+2)*4``BinOp('*', BinOp('*', Num(3), BinOp('+', Num(1), Num(2))), Num(4))`
`_primary()` correctly enters `_expr()` recursively inside parens and consumes the closing RPAREN.
---
### parse/D4: PASS @2026-06-15T06:32:30Z
Cold re-run:
```
Unary('-', Num(5)) # -5 ✓
Unary('-', BinOp('+', Num(1), Num(2))) # -(1+2) ✓
BinOp('*', Num(3), Unary('-', Num(2))) # 3 * -2 ✓
Unary('-', Unary('-', Num(5))) # --5 ✓
```
Independent break-it probes:
- `1 - -2``BinOp('-', Num(1), Unary('-', Num(2)))`
- `-1 + -2``BinOp('+', Unary('-', Num(1)), Unary('-', Num(2)))`
- `-(-(3))``Unary('-', Unary('-', Num(3)))`
`_unary()` is right-recursive (calls itself), so multiple leading negations stack correctly.
---
### parse/D5: PASS @2026-06-15T06:32:30Z
Cold re-run — all 5 plan-required error cases:
```
OK ParseError: '1 +' -> unexpected end of expression
OK ParseError: '(1' -> expected ')' but got 'EOF' ('')
OK ParseError: '1 2' -> unexpected token 'NUMBER' (2)
OK ParseError: ')(' -> unexpected token 'RPAREN' (')')
OK ParseError: '' -> empty expression
```
All raise `ParseError` (not bare `ValueError`, `IndexError`, etc.) ✓
Independent extra probes:
- `'*'``ParseError: unexpected token 'STAR' ('*')`
- `'((1)'``ParseError: expected ')' but got 'EOF' ('')`
- `'1++2'``ParseError: unexpected token 'PLUS' ('+')`
- `'+5'``ParseError: unexpected token 'PLUS' ('+')`
---
### parse/D6: PASS @2026-06-15T06:32:30Z
Cold `python -m unittest -q` run:
```
Ran 35 tests in 0.001s
OK
```
Test file inspection confirms:
- All 20 parser tests use `assertEqual` on dataclass instances (e.g. `BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))`), not on evaluated numeric results ✓
- Coverage: D1 (4 tests), D2 (4 tests), D3 (3 tests), D4 (4 tests), D5 (5 tests) ✓
---
## Adversary findings
No findings. All DoD gates pass with no defects detected.

View File

@ -0,0 +1,167 @@
# STATUS — Phase eval (Builder)
## DONE
All gates D1D5 Adversary-verified PASS @2026-06-15T06:43Z. Phase eval complete.
## Current State
Gates D1D5 implemented, claimed, and Adversary-verified. Phase complete.
Implementation commit: 0fc263d
---
## Gate D1 — arithmetic — 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
cases = [('2+3*4', 14), ('(2+3)*4', 20), ('8-3-2', 3), ('-2+5', 3), ('2*-3', -6)]
for expr, expected in cases:
result = evaluate(parse(tokenize(expr)))
status = 'OK' if result == expected else f'FAIL (got {result!r})'
print(f'{status}: {expr!r} -> {result}')
"
```
**EXPECTED:**
```
OK: '2+3*4' -> 14
OK: '(2+3)*4' -> 20
OK: '8-3-2' -> 3
OK: '-2+5' -> 3
OK: '2*-3' -> -6
```
**WHERE:** `calc/evaluator.py` — commit 0fc263d
---
## Gate D2 — division — CLAIMED, awaiting Adversary
**WHAT:** `/` is true division (`"7/2"` → 3.5). Division by zero raises `EvalError`, not bare `ZeroDivisionError`.
**HOW:**
```bash
python -c "
from calc.lexer import tokenize
from calc.parser import parse
from calc.evaluator import evaluate, EvalError
print(evaluate(parse(tokenize('7/2'))))
try:
evaluate(parse(tokenize('1/0')))
print('FAIL: no exception')
except EvalError as e:
print(f'OK EvalError: {e}')
except ZeroDivisionError:
print('FAIL: bare ZeroDivisionError escaped')
"
```
**EXPECTED:**
```
3.5
OK EvalError: division by zero
```
**WHERE:** `calc/evaluator.py` `evaluate()` BinOp '/' branch — commit 0fc263d
---
## Gate D3 — result type — CLAIMED, awaiting Adversary
**WHAT:** Whole-valued results return `int` (no trailing `.0`); non-whole return `float`.
Rule: after division, if `result.is_integer()`, return `int(result)`. Integer arithmetic stays `int` natively.
**HOW:**
```bash
python -c "
from calc.lexer import tokenize
from calc.parser import parse
from calc.evaluator import evaluate
r1 = evaluate(parse(tokenize('4/2')))
r2 = evaluate(parse(tokenize('7/2')))
r3 = evaluate(parse(tokenize('2+3*4')))
print(repr(r1), type(r1).__name__)
print(repr(r2), type(r2).__name__)
print(repr(r3), type(r3).__name__)
"
```
**EXPECTED:**
```
2 int
3.5 float
14 int
```
**WHERE:** `calc/evaluator.py` `evaluate()` BinOp '/' branch — commit 0fc263d
---
## Gate D4 — CLI — CLAIMED, awaiting Adversary
**WHAT:** `python calc.py "2+3*4"` prints `14` and exits 0; invalid input prints error to stderr and exits non-zero (no traceback).
**HOW:**
```bash
python calc.py "2+3*4"; echo "exit:$?"
python calc.py "(2+3)*4"; echo "exit:$?"
python calc.py "7/2"; echo "exit:$?"
python calc.py "4/2"; echo "exit:$?"
python calc.py "1/0" 2>&1; echo "exit:$?"
python calc.py "1 +" 2>&1; echo "exit:$?"
```
**EXPECTED:**
```
14
exit:0
20
exit:0
3.5
exit:0
2
exit:0
error: division by zero
exit:1
error: unexpected end of expression
exit:1
```
**WHERE:** `calc.py` — commit 0fc263d
---
## Gate D5 — tests green + end-to-end — CLAIMED, awaiting Adversary
**WHAT:** Full unittest suite passes with 0 failures: 15 lexer + 20 parser + 11 evaluator = 46 tests.
**HOW:**
```bash
python -m unittest -q
```
**EXPECTED:**
```
Ran 46 tests in 0.00Xs
OK
```
Test coverage in `calc/test_evaluator.py`:
- `TestArithmetic` (5 tests): D1 — 2+3*4, (2+3)*4, 8-3-2, -2+5, 2*-3
- `TestDivision` (3 tests): D2 — 7/2, 1/0, 5/(2-2)
- `TestResultType` (3 tests): D3 — 4/2→int, 7/2→float, 2+3*4→int
**WHERE:** `calc/test_evaluator.py`, `calc/evaluator.py` — commit 0fc263d

View File

@ -0,0 +1,103 @@
# STATUS — Phase lex (Builder)
## DONE
All gates D1, D2, D3, D4 verified PASS by Adversary @2026-06-15T06:26:41Z.
AF-1 fixed: bare `.` now raises `LexError` instead of leaking `ValueError`.
## Current State
All implementation complete. All gates PASSED.
---
## Gate D1 — numbers — CLAIMED, awaiting Adversary
**WHAT:** `calc/lexer.py` tokenizes integers and floats to NUMBER tokens with numeric values.
**HOW:** Run from repo root (where calc/ lives):
```bash
python -c "from calc.lexer import tokenize; t=tokenize('42'); print(t[0].kind, t[0].value, type(t[0].value).__name__)"
python -c "from calc.lexer import tokenize; t=tokenize('3.14'); print(t[0].kind, t[0].value, type(t[0].value).__name__)"
python -c "from calc.lexer import tokenize; t=tokenize('.5'); print(t[0].kind, t[0].value, type(t[0].value).__name__)"
python -c "from calc.lexer import tokenize; t=tokenize('10.'); print(t[0].kind, t[0].value, type(t[0].value).__name__)"
```
**EXPECTED:**
```
NUMBER 42 int
NUMBER 3.14 float
NUMBER 0.5 float
NUMBER 10.0 float
```
(Each followed by EOF as second token.)
**WHERE:** `calc/lexer.py` — commit to be pushed with this status.
---
## Gate D2 — operators & parens — CLAIMED, awaiting Adversary
**WHAT:** `+ - * / ( )` each tokenize to PLUS, MINUS, STAR, SLASH, LPAREN, RPAREN.
**HOW:**
```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']
```
---
## Gate D3 — whitespace & errors — CLAIMED, awaiting Adversary
**WHAT:** Spaces/tabs are skipped; invalid chars raise LexError with 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')"
```
**EXPECTED:**
```
['NUMBER', 'PLUS', 'NUMBER', 'EOF']
calc.lexer.LexError: unexpected character '@' at position 2
```
---
## Gate D4 — tests green — CLAIMED, awaiting Adversary
**WHAT:** `calc/test_lexer.py` passes with 0 failures.
**HOW:**
```bash
python -m unittest -q
```
**EXPECTED:**
```
Ran 15 tests in 0.00Xs
OK
```
Also the plan's exact verification commands:
```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', '*'), ('LPAREN', '('), ('NUMBER', 1), ('MINUS', '-'), ('NUMBER', 2), ('RPAREN', ')'), ('EOF', '')]
```
```bash
python -c "from calc.lexer import tokenize; tokenize('1 @ 2')"
```
**Expected:** raises `calc.lexer.LexError: unexpected character '@' at position 2`
**WHERE:** `calc/test_lexer.py`, `calc/lexer.py` — see commit sha after push.

View File

@ -0,0 +1,157 @@
# STATUS — Phase parse (Builder)
## DONE
All gates D1D6 Adversary-verified PASS @2026-06-15T06:32:30Z. Phase parse complete.
## Current State
Gates D1 through D6 implemented, claimed, and Adversary-verified. Phase complete.
---
## Gate D1 — precedence — CLAIMED, awaiting Adversary
**WHAT:** `*` and `/` bind tighter than `+` and `-`. `1+2*3` parses as `BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))`, not `BinOp('+', BinOp('+',…))`.
**HOW:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('1+2*3')))"
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('2*3+4')))"
```
**EXPECTED:**
```
BinOp('+', Num(1), BinOp('*', Num(2), Num(3)))
BinOp('+', BinOp('*', Num(2), Num(3)), Num(4))
```
**WHERE:** `calc/parser.py` — commit 64d0252
---
## Gate D2 — left associativity — CLAIMED, awaiting Adversary
**WHAT:** Same-precedence operators associate left. `8-3-2``(8-3)-2`; `8/4/2``(8/4)/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('-', BinOp('-', Num(8), Num(3)), Num(2))
BinOp('/', BinOp('/', Num(8), Num(4)), Num(2))
```
**WHERE:** `calc/parser.py` — commit 64d0252
---
## Gate D3 — parentheses — CLAIMED, awaiting Adversary
**WHAT:** Parens override precedence. `(1+2)*3` parses with `+` under `*`.
**HOW:**
```bash
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('(1+2)*3')))"
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('((2+3))')))"
```
**EXPECTED:**
```
BinOp('*', BinOp('+', Num(1), Num(2)), Num(3))
BinOp('+', Num(2), Num(3))
```
**WHERE:** `calc/parser.py` `_primary()` method — commit 64d0252
---
## Gate D4 — unary minus — CLAIMED, awaiting Adversary
**WHAT:** Leading and nested unary minus parses correctly: `-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')))"
python -c "from calc.lexer import tokenize; from calc.parser import parse; print(parse(tokenize('--5')))"
```
**EXPECTED:**
```
Unary('-', Num(5))
Unary('-', BinOp('+', Num(1), Num(2)))
BinOp('*', Num(3), Unary('-', Num(2)))
Unary('-', Unary('-', Num(5)))
```
**WHERE:** `calc/parser.py` `_unary()` method — commit 64d0252
---
## Gate D5 — errors — CLAIMED, awaiting Adversary
**WHAT:** Malformed input raises `ParseError` (not any other exception) for all five plan cases.
**HOW:**
```bash
python -c "
from calc.lexer import tokenize
from calc.parser import parse, ParseError
cases = ['1 +', '(1', '1 2', ')(' , '']
for src in cases:
try:
parse(tokenize(src))
print(f'FAIL: {src!r} did not raise')
except ParseError as e:
print(f'OK ParseError: {src!r} -> {e}')
except Exception as e:
print(f'FAIL wrong exc: {src!r} -> {type(e).__name__}: {e}')
"
```
**EXPECTED:**
```
OK ParseError: '1 +' -> unexpected end of expression
OK ParseError: '(1' -> expected ')' but got 'EOF' ('')
OK ParseError: '1 2' -> unexpected token 'NUMBER' (2)
OK ParseError: ')(' -> unexpected token 'RPAREN' (')')
OK ParseError: '' -> empty expression
```
**WHERE:** `calc/parser.py` `parse()`, `_primary()`, `_expr()` — commit 64d0252
---
## Gate D6 — tests green — CLAIMED, awaiting Adversary
**WHAT:** `calc/test_parser.py` (unittest) passes with 0 failures, covering D1D5. Total 35 tests pass (15 lexer + 20 parser).
**HOW:**
```bash
python -m unittest -q
```
**EXPECTED:**
```
Ran 35 tests in 0.00Xs
OK
```
Test coverage in `calc/test_parser.py`:
- `TestPrecedence` (4 tests): D1 — `1+2*3`, `2*3+4`, `6-2*3`, `1+6/2`
- `TestAssociativity` (4 tests): D2 — `8-3-2`, `8/4/2`, `1+2+3`, `2*3*4`
- `TestParentheses` (3 tests): D3 — `(1+2)*3`, `((2+3))`, `3*(2+1)`
- `TestUnaryMinus` (4 tests): D4 — `-5`, `-(1+2)`, `3 * -2`, `--5`
- `TestErrors` (5 tests): D5 — `1 +`, `(1`, `1 2`, `)(`, `""`
All tests assert on tree structure via `assertEqual` on dataclass instances (not on evaluation).
**WHERE:** `calc/test_parser.py`, `calc/parser.py` — commit 64d0252