================================================================================
mcp-abacus — CORE CONCEPT  (the single most important feature)
--------------------------------------------------------------------------------
* The defining feature: TYPE-FAITHFUL CALCULATION over many numeric types.
  The user sets a type/mode (e.g. 32-bit integer) and evaluates an expression
  (like a + b * 10^3); the WHOLE expression behaves EXACTLY as a 32-bit
  calculation would — it overflows, it underflows, it then CARRIES the bad
  value onward through the rest of the expression, and so on.
* Purpose: the programmer (the AI) can simulate a calculation to see what
  happened, or what would happen, in real code — bit-for-bit.
* It must support a huge lot of number formats (fixed-width ints, IEEE-754
  floats, fixed-point, decimal, rational, bigfloat — see 10.1 / 12 below).
* It does NOT approximate or "simulate" a type — it CALCULATES USING that
  type: every intermediate result is exactly what the real type produces.
================================================================================


================================================================================
mcp-abacus — PROJECT NOTES  (apply to the feature sections 10-16 below)
--------------------------------------------------------------------------------
* LANGUAGE: section 16 decides on PURE PYTHON for the whole project (decision
  revised — the earlier C++-core plan is dropped). The skeleton's
  hatchling/pure-Python choices (2.3, 3.1) stand permanently; no
  build-backend migration happens. Exact per-type semantics are achievable
  in pure Python for every planned mode (see 10.L); performance is
  irrelevant at one-expression-per-call.
* Backlog provenance: copied from ~/Desktop/mcp-primitives-ideas.txt
  (source §3 = Server A "mcp-abacus", plus its sections 3.A and SA/SHARED).
  RENUMBERED for this file: source 3.0-3.5 -> 10-15, 3.A -> 16; SA and
  SHARED keep their source names.
* Feature work starts AFTER the bootstrap skeleton is green (9.5).

Source legend:  [BUILD] core surface   [MAYBE] bundle only if cheap
                [SKIP]  not building (model does it fine without a tool)
                rel = how reliably the model does it WITHOUT a tool
                      (low = tool adds most value)
================================================================================


================================================================================
mcp-abacus — BOOTSTRAP TODO
--------------------------------------------------------------------------------
GOAL: from this empty directory, stand up the skeleton of a pure-Python MCP
server. When every box below is checked the server starts over stdio, exposes
exactly ONE tool, and that tool reports the server's availability + version
information. No domain features yet (the programmer's-calculator tools — base
conversion, bitwise ops, etc. — come later).

DONE = `mcp-abacus` runs, a client lists one tool, and calling it returns
       {status: "available", name, version, ...}.

Checkbox legend:  [ ] not started   [~] in progress   [x] done
================================================================================


[x] 1.  PROJECT SCAFFOLDING (src layout)
    [x] 1.1.  Create the package tree:
              src/mcp_abacus/__init__.py
              src/mcp_abacus/server.py
              src/mcp_abacus/__main__.py
              tests/test_info.py
    [x] 1.2.  Add top-level files: README.md, LICENSE (MIT), .gitignore
              (Python template); this TODO stays at repo root.
    [x] 1.3.  Version control:
        [x] 1.3.1.  `git init`; first commit once 1.x + 2.x exist.
                    (2.x is enumeration only — recorded in this file, no
                    artifacts — so the first commit is the 1.x scaffolding.)
        [x] 1.3.2.  Create a NEW repository for this project on the local
                    homelab git server (host named "git") — this is the
                    PRIVATE server, not public.
                    (bare repo: git@git.homelab.local:/home/git/mcp-abacus.git)
        [x] 1.3.3.  Add that repo as the `origin` remote and push the first
                    commit, e.g.
                    `git remote add origin git@git:mcp-abacus.git`
                    then `git push -u origin master`.
        [x] 1.3.4.  Pushing to "git" is a PRIVATE upload to the local server
                    only — NOT public. (A public GitHub remote for CI is
                    separate; see section 8.)
    [x] 1.4.  Distribution name "mcp-abacus", import package name
              "mcp_abacus" (underscore). Keep them consistent.


[x] 2.  DEPENDENCIES (enumerate now; install via uv)
        (verified 2026-06-11: uv 0.11.19 installed; system Python 3.12.3;
        mcp latest on PyPI = 1.27.2, requires-python >= 3.10 — the >=1.2
        floor is valid. Install happens at 6.1 via `uv sync` once
        pyproject.toml exists.)
    [x] 2.1.  Runtime (required):
        [x] 2.1.1.  mcp — the official MCP Python SDK; bundles FastMCP.
                    Import: `from mcp.server.fastmcp import FastMCP`.
                    Pin a recent floor, e.g. mcp >= 1.2.
        [x] 2.1.2.  Python >= 3.10 (FastMCP requirement).
        [x] 2.1.3.  Version read at runtime from package metadata via
                    importlib.metadata (stdlib) — NO extra dep.
    [x] 2.2.  Dev / tooling:
        [x] 2.2.1.  uv — env, dependency resolution, lockfile (matches the
                    existing mcp-tmux workflow).
        [x] 2.2.2.  pytest — test runner.
        [x] 2.2.3.  pytest-asyncio — only if tools are async (add lazily;
                    FastMCP tools may be plain sync).  [EXCLUDED: info() is sync]
        [x] 2.2.4.  ruff — lint + format.
        [x] 2.2.5.  mypy — optional static typing.
    [x] 2.3.  Build backend: hatchling.
    [x] 2.4.  FUTURE optional extras — likely NONE: the planned calculator
              features (base conversion, bitwise/shift ops, two's-complement
              arithmetic at fixed word sizes, IEEE-754 float inspection) are
              pure stdlib (int, struct, math). Declare extras later only if a
              feature genuinely needs one, e.g.:
              expr  = lark (expression parsing, if an eval-style tool is added)
              units = pint (unit conversions, if ever in scope)


[x] 3.  pyproject.toml
        (mirrors ../mcp-tmux/pyproject.toml; also added [tool.mypy] to match.
        No [project.urls] yet — added with the public GitHub remote in §8.
        pytest config deferred to 7.1 as planned.)
    [x] 3.1.  [build-system] requires = ["hatchling"];
              build-backend = "hatchling.build".
    [x] 3.2.  [project]: name="mcp-abacus", version="0.0.1",
              requires-python=">=3.10", dependencies=["mcp>=1.2"],
              description, readme, license, authors.
    [x] 3.3.  [project.optional-dependencies]: dev=[pytest, ruff, mypy].
              (No FUTURE extras placeholders — see 2.4; add only when needed.)
    [x] 3.4.  [project.scripts]:
              mcp-abacus = "mcp_abacus.__main__:main".
    [x] 3.5.  [tool.hatch.build.targets.wheel]
              packages = ["src/mcp_abacus"].
    [x] 3.6.  Add ruff config (line length, target-version) under [tool.ruff].


[x] 4.  ARCHITECTURE / WIRING
        (verified: `uv run python` imports the app, mcp.name == "mcp-abacus",
        __version__ resolves to 0.0.1 from metadata. uv created .venv with
        CPython 3.13.13 as a side effect; 6.1 formalizes the sync.)
    [x] 4.1.  server.py: construct the singleton FastMCP app —
              `mcp = FastMCP("mcp-abacus")`. All tools register here.
    [x] 4.2.  __main__.py: `def main(): mcp.run()` (stdio is default) and the
              `if __name__ == "__main__": main()` guard. Import the app from
              server.py so tools register on import.
    [x] 4.3.  __init__.py: expose `__version__` via
              importlib.metadata.version("mcp-abacus"), with a fallback for
              the editable/unbuilt case.
    [x] 4.4.  Transport = stdio (what Claude Code/Desktop launch). No HTTP/SSE
              in the skeleton.
    [x] 4.5.  Leave a marked spot in server.py for FUTURE opt-in toolset
              gating (numeric/bits/eval — see SA.1 in the feature backlog
              below) — comment only, not built.


[x] 5.  THE ONE TOOL  (availability + version report)
        (verified through the MCP layer: mcp.list_tools() shows exactly one
        tool "info" with description + input schema; mcp.call_tool returns
        {status: "available", name, version: 0.0.1, python: 3.13.13,
        mcp_sdk: 1.27.2, toolsets: []}.)
    [x] 5.1.  In server.py add a single tool: `@mcp.tool()` on
              `def info() -> dict: ...`.
    [x] 5.2.  Return a dict with at least:
              status   = "available"
              name     = "mcp-abacus"
              version  = __version__ (from 4.3)
              python   = platform.python_version()
              mcp_sdk  = installed mcp SDK version (importlib.metadata)
              toolsets = []   (placeholder; filled in a later task)
    [x] 5.3.  One-line docstring + typed return so FastMCP auto-generates the
              JSON schema (docstring = the tool description).
    [x] 5.4.  Keep it pure stdlib (platform, importlib.metadata) — no new deps.


[x] 6.  RUN & VERIFY
    [x] 6.1.  `uv sync` (or `uv pip install -e .[dev]`) into a venv.
              (`uv sync --extra dev` — dev tools pytest/ruff/mypy installed.)
    [x] 6.2.  Smoke-run: `uv run mcp-abacus` starts and blocks on stdio
              without error.  (Held stdin open; still blocking after 3 s,
              no output — killed by timeout, exit 124.)
    [x] 6.3.  Inspect with the MCP dev inspector
              (`uv run mcp dev src/mcp_abacus/server.py`); confirm the
              single `info` tool is listed with its schema. If the inspector
              is not available on this computer, install it first (it ships
              with the SDK's CLI extra, e.g. `uv add "mcp[cli]"` / ensure the
              `mcp` CLI + Node-based Inspector are present), then run it.
              (Used the Inspector's scriptable CLI mode via npx — node v22
              present: tools/list shows exactly `info` + schema; tools/call
              returns the payload. No mcp[cli] dep added.)
    [x] 6.4.  ON THIS LOCAL COMPUTER: install + register the server at USER
              scope so it is available in EVERY project (not just this repo) —
              same model as the existing mcp-kodi / mcp-tmux user-scoped
              servers.
        [x] 6.4.1.  Install so the `mcp-abacus` entry point is runnable
                    outside this repo's venv (e.g. `uv tool install .`, or
                    register the command as `uv run --directory
                    /home/pipas/projects/mcp-abacus mcp-abacus`).
                    (`uv tool install .` -> ~/.local/bin/mcp-abacus, same as
                    mcp-tmux/mcp-bytesmith. NOTE: snapshot install — rerun
                    `uv tool install --reinstall .` after future changes.)
        [x] 6.4.2.  Register user-scoped with Claude Code: `claude mcp add
                    --scope user abacus -- mcp-abacus` -> ✔ Connected.
        [x] 6.4.3.  Verify in a DIFFERENT project that the `info` tool is
                    listed and returns the availability/version payload.
                    (From ~/projects/mcp-tmux: Inspector CLI against the
                    registered `mcp-abacus` command returns the payload;
                    `claude mcp list` shows abacus ✔ Connected.)


[x] 7.  UNIT TESTS (pytest; tests/ directory)
    [x] 7.1.  Configure pytest under [tool.pytest.ini_options] in
              pyproject.toml: testpaths=["tests"]; set asyncio mode if
              pytest-asyncio is used (2.2.3).
              (No asyncio mode — async FastMCP calls go through asyncio.run()
              in plain sync tests, keeping pytest-asyncio excluded.)
    [x] 7.2.  tests/test_info.py — assert the `info` tool's return payload:
        [x] 7.2.1.  returns a dict; status == "available".
        [x] 7.2.2.  name == "mcp-abacus".
        [x] 7.2.3.  version is a non-empty string AND equals
                    importlib.metadata.version("mcp-abacus").
        [x] 7.2.4.  python and mcp_sdk fields present + non-empty.
        [x] 7.2.5.  toolsets is a list (empty for now).
    [x] 7.3.  tests/test_server.py — prove the tool is wired into FastMCP, not
              merely importable:
        [x] 7.3.1.  list the app's registered tools; assert EXACTLY ONE is
                    exposed, named "info".
        [x] 7.3.2.  that tool has a non-empty description (the docstring) and
                    a generated input schema.
        [x] 7.3.3.  invoke it through the app; payload matches 7.2 (end-to-end
                    via the MCP layer).
    [x] 7.4.  tests/test_package.py — import / packaging smoke:
        [x] 7.4.1.  `import mcp_abacus` succeeds; `__version__` is defined
                    and non-empty.
        [x] 7.4.2.  the editable/unbuilt version fallback (4.3) does not raise.
                    (monkeypatch importlib.metadata.version to raise
                    PackageNotFoundError + reload; asserts "0.0.0+unknown".)
    [x] 7.5.  All tests run OFFLINE — no network, no runtime deps beyond `mcp`
              + stdlib.
    [x] 7.6.  Once the tests above exist, RUN the suite: `uv run pytest`
              green (moved here from RUN & VERIFY — run the tests as soon as
              they are created).  (10 passed in 0.24s; ruff check also clean.)
        7.7.  (OPTIONAL) pytest-cov for coverage; not required for the
              skeleton — add only if useful.  [SKIPPED for the skeleton]


[x] 8.  CI (minimal; mirror the mcp-tmux setup)
    [x] 8.0.  REFERENCE: the mcp-tmux project lives at ../mcp-tmux/ (sibling
              of this repo). It is a SEPARATE project — treat it as READ-ONLY:
              read its CI / publish config to copy the pattern, never edit
              anything under it.  (Read ci.yml + release.yml; nothing edited.)
    [x] 8.1.  .github/workflows/ci.yml: on push/PR — set up uv, `uv sync`,
              `uv run ruff check`, `uv run pytest`.
              (Mirrors mcp-tmux exactly: also `ruff format --check` + `mypy`.
              All four steps rehearsed locally on Python 3.10 AND 3.13 —
              green. NOTE: workflow is inert until a GitHub remote exists —
              creating the public repo is a separate, user-approved step.)
    [x] 8.2.  Pin action versions; run on a current Python (3.12+).
              (actions/checkout@v6, astral-sh/setup-uv@v7; matrix
              3.10/3.11/3.12/3.13 — same as mcp-tmux.)
        8.3.  (LATER, not part of skeleton) PyPI publish workflow via Trusted
              Publishing on git tag — reuse the mcp-tmux pattern
              (release.yml, OIDC, environment "pypi").


[x] 9.  DEFINITION OF DONE  (acceptance check — all re-run fresh 2026-06-11)
    [x] 9.1.  `uv run mcp-abacus` launches a stdio MCP server with no
              errors.  (Blocked on stdio until killed at 3 s; zero output.)
    [x] 9.2.  A client lists EXACTLY ONE tool (`info`).  (Inspector CLI
              tools/list -> count 1, name "info".)
    [x] 9.3.  Calling it returns status "available" + correct name + a real
              version string.  (Inspector CLI tools/call -> status
              "available", name "mcp-abacus", version "0.0.1".)
    [x] 9.4.  `uv run pytest` (section 7) and `uv run ruff check` both pass.
              (10 passed; "All checks passed!")
    [x] 9.5.  Commit the green skeleton; tag-free. Feature work starts after.

================================================================================
BOOTSTRAP COMPLETE (sections 1-9 done 2026-06-11). Feature work (10-16, SA)
can start. Remember: after changing server code, `uv tool install --reinstall .`
so the user-scoped `abacus` MCP registration picks up the new build.
================================================================================


--- expression evaluator (10 — the flagship calculator) ---
[ ] 10.  Multi-model expression evaluator               [BUILD] rel=low
         Eval ONE expression under several numeric regimes at once;
         return each regime's result as its normal string (see 10.3).
         Why a tool: the model CANNOT predict exact float/double
         rounding, and this makes
         "why float != money" runnable. Crypto/fixed-point native.
    [ ] 10.1.  Modes (each = representation + precision/scale + rounding
               + division/irrational rule)
        [ ] 10.1.1.  fixed-point / scaled-integer   *** FLAGSHIP ***
                     your system: value = (mantissa:int, decimals:int);
                     +/- align to max scale, * SUMS decimals, / needs a
                     target scale + rounding. Exact, deterministic,
                     ERC-20 / money-safe. Scale is data on the value.
        [ ] 10.1.2.  binary32  (IEEE-754 float, ~7 sig digits)
        [ ] 10.1.3.  binary64  (IEEE-754 double, ~15-17 sig digits;
                     JS/JSON default)
        [ ] 10.1.4.  binary16 / bfloat16  (ML; optional)
        [ ] 10.1.5.  decimal(prec, rounding)  (base-10 arbitrary
                     precision)
        [ ] 10.1.6.  rational  (exact numerator/denominator; no
                     irrationals)
        [ ] 10.1.7.  bigfloat(prec)  (arbitrary-precision binary;
                     sqrt/pi/trig)
    [ ] 10.2.  Per-mode parameters
        [ ] 10.2.1.  precision / scale (sig digits, or fixed decimals)
        [ ] 10.2.2.  rounding mode (half-even / half-up / toward-zero /
                     floor / ceil)
        [ ] 10.2.3.  division & irrational handling (target scale;
                     exact vs approx)
        [ ] 10.2.4.  overflow / fixed-width behavior (wrap / saturate /
                     error)
    [ ] 10.3.  Output: the calculated value per mode, converted to its
               normal string format — nothing more.
    10.L.  Libraries — reference  (pure-Python implementations; all
           exact, bit-for-bit — see notes):
        10.L.1.  fixed-point / scaled-int -> stdlib int math (Python int
                 is arbitrary-precision natively; ZERO deps)
        10.L.2.  binary16/32/64 -> CPython float IS IEEE-754 binary64;
                 f32/f16: compute each op in binary64, then round via
                 struct ('f'/'e') — EXACT by the double-rounding theorem
                 (53 >= 2*24+2 and 53 >= 2*11+2). bfloat16: round-to-
                 nearest-even bit manipulation on the f32 pattern.
                 CAVEAT: struct 'e' raises OverflowError past f16 range
                 instead of rounding to inf — handle that branch
                 manually (values >= 65520 round to inf).
        10.L.3.  decimal(prec, rounding)  -> decimal (stdlib; contexts
                 give per-mode precision + rounding directly)
        10.L.4.  rational                 -> fractions (stdlib)
        10.L.5.  bigfloat(prec)           -> mpmath (pip; pure Python).
                 If MPFR-grade correctly-rounded transcendentals are
                 ever required, gmpy2 (pip, prebuilt wheels) — a dep we
                 install, not code we build.
        10.L.6.  big-integer (for L.1/L.4 at scale) -> Python int,
                 arbitrary-precision natively
        10.L.7.  expression parsing       -> ast (stdlib, safe-eval) or
                 a small Pratt parser; NEVER raw eval()
        10.L.8.  fixed-width ints (12.2 + 10.2.4) -> Python int +
                 mask/re-sign after EVERY op (wrap); compare-and-clamp
                 (saturate); range-check (error)

--- numbers & bits (11-14) ---
[ ] 11.  Float internals
    [ ] 11.1.  IEEE-754 float <-> raw bits, f16/f32/f64
               (sign / exponent / mantissa)             [BUILD] rel=low
[ ] 12.  Integer interpretation
    [ ] 12.1.  two's-complement signed<->unsigned
               at fixed width                           [BUILD] rel=low
    [ ] 12.2.  fixed-width overflow / wraparound
               (int8..int64, signed & unsigned)         [BUILD] rel=low
[ ] 13.  Bit operations
    [ ] 13.1.  bitwise AND/OR/XOR/NOT (32/64-bit)       [BUILD] rel=medium
    [ ] 13.2.  shifts (logical/arithmetic) + rotates
               (ROL/ROR) at fixed width                 [BUILD] rel=medium
    [ ] 13.3.  popcount / Hamming weight / bit-length   [BUILD] rel=medium
    [ ] 13.4.  byte-order swap / endianness
               (bswap16/32/64)                          [MAYBE] rel=medium
    [ ] 13.5.  Gray-code encode/decode                  [MAYBE] rel=medium
        13.6.  single bit set/clear/toggle/test         [SKIP]  rel=high
[ ] 14.  Big-integer
    [ ] 14.1.  modpow / modinv / gcd                    [BUILD] rel=low
    [ ] 14.2.  base conversion hex/oct/bin/dec
               (large values)                           [BUILD] rel=medium

--- text, unicode & bytes (15 — moved from old 1.5) ---
[ ] 15.  Text, Unicode & byte manipulation
    [ ] 15.1.  string escape/unescape
               (JSON/JS/Python/C/shell)                 [BUILD] rel=medium
    [ ] 15.2.  \uXXXX / \xNN unicode-escape <-> text    [BUILD] rel=medium
    [ ] 15.3.  code point <-> char; UTF-8 / UTF-16
               byte view                                [BUILD] rel=low
    [ ] 15.4.  Unicode normalization (NFC/NFD/NFKC/
               NFKD)                                    [BUILD] rel=low
    [ ] 15.5.  hex dump / hexdump <-> bytes             [BUILD] rel=low
    [ ] 15.6.  charset transcode (latin-1 / cp1252
               <-> utf-8)                               [BUILD] rel=low
        15.7.  ROT13 / Caesar shift                     [SKIP]  rel=high
    (Libraries: all stdlib — unicodedata / codecs / binascii)


[ ] 16.  ARCHITECTURE / BUILD  (DECISION REVISED: PURE PYTHON, whole
         project. The earlier C++-core plan — nanobind, scikit-build-core,
         CMake, cibuildwheel, abi3 wheel matrix — is DROPPED.)
    16.1.  LANGUAGE (decision, for reference):
        16.1.1.  §1 encoding/hashing/crypto (Server B "mcp-bytesmith")
                 -> PURE PYTHON STDLIB, as before. hashlib/base64/hmac/
                 secrets are already thin bindings over OpenSSL's C.
        16.1.2.  §3 calculator / bit-math / fixed-point eval -> PURE PYTHON
                 too. Rationale: every planned mode is implementable with
                 EXACT, bit-for-bit type semantics in pure Python (see
                 10.L — verified for int32 wrap, binary32, binary16);
                 per-call performance is irrelevant for an MCP calculator;
                 and dropping the compiled core deletes the project's
                 single biggest cost (the ~10-30 wheel CI matrix).
    [ ] 16.2.  TOOLCHAIN (unchanged from the skeleton — nothing migrates):
        [ ] 16.2.1.  hatchling build backend (2.3, 3.1) stays.
        [ ] 16.2.2.  FastMCP (Python SDK) -> auto-generates each tool's
                     JSON schema from type hints + docstrings.
        [ ] 16.2.3.  Deps: stdlib for everything except bigfloat (mpmath,
                     pip) and expression parsing if lark is chosen over a
                     hand-written Pratt parser (2.4).
    [ ] 16.3.  PYPI PUBLISHING (now trivial):
        16.3.1.  Pure Python = ONE universal wheel + sdist. No compilers,
                 no cibuildwheel, no platform babysitting.
        [ ] 16.3.2.  Reuse the EXISTING mcp-tmux Trusted-Publishing
                     workflow unchanged (see 8.3).
    16.4.  COST: effectively none — CI is just lint + tests + a universal
           wheel, identical in shape to mcp-tmux.


[x] 17.  EXPRESSION AST  (foundation for the evaluator 10; pure stdlib)
         Design decided 2026-06-11: arithmetic-core node set, frozen
         dataclasses, literals stored as SOURCE LEXEME strings,
         IN-NODE evaluation (see 18 — methods added there, not here;
         17 ships data-only nodes + pretty()).
         (done 2026-06-11: expr/nodes.py + expr/__init__.py +
         tests/test_nodes.py; pretty() format "Kind op/lexeme (line N)",
         2-space indent; line is kw-only with NO default.)
    [x] 17.1.  New subpackage src/mcp_abacus/expr/ — home of the future
               expression engine (nodes now; lexer/parser and the
               multi-mode evaluator are LATER items). __init__.py
               re-exports the public names from nodes.py.
    [x] 17.2.  expr/nodes.py — frozen-dataclass AST node classes
               (dataclass(frozen=True, slots=True); stdlib only):
        [x] 17.2.1.  Node — abstract base, no fields (__slots__ = ()).
        [x] 17.2.2.  Number(lexeme: str) — the literal kept EXACTLY as
                     written ("0.1", "1e-3"); each mode interprets the
                     string in its OWN representation (type-
                     faithfulness: parsing to float here would bake in
                     binary64 rounding before fixed-point/decimal modes
                     ever see the value). Lexemes are UNSIGNED — sign
                     is UnaryOp.
        [x] 17.2.3.  UnaryOp(op: str, operand: Node) — op in {+, -}.
        [x] 17.2.4.  BinOp(op: str, left: Node, right: Node) — op in
                     {+, -, *, /, //, %, ^}; ^ is POWER in this
                     expression language (per the CORE CONCEPT example
                     "a + b * 10^3"), not XOR.
        [x] 17.2.5.  Validate in __post_init__ -> ValueError (unknown
                     op; empty lexeme; line < 1). Operator sets as
                     module-level frozensets UNARY_OPS / BINARY_OPS.
        [x] 17.2.6.  line: int on EVERY node (keyword-only, 1-based) —
                     the input line the node came from, so error
                     messages from the later parser/evaluator can say
                     WHERE. Excluded from __eq__/__hash__ (compare=
                     False): trees are structurally equal regardless
                     of where they appeared in the input.
        [x] 17.2.7.  Recursive pretty printer — Node.pretty() -> str:
                     multi-line, indented tree view (one node per
                     line: kind + op/lexeme + line number), children
                     indented one level deeper. For seeing the tree
                     on demand; repr stays the dataclass default.
                     (18.6 later appends each node's calculated
                     value to its line.)
    [x] 17.3.  tests/test_nodes.py (plain sync pytest, like 7.x):
               construction + field round-trip; the "1 + 2 * 10^3"
               example tree nests correctly; structural equality
               (same tree, different line numbers -> still equal);
               hashable; frozen (FrozenInstanceError); ValueError on
               bad op / empty lexeme / line < 1; match/case
               destructuring works (for external consumers; the
               evaluator itself is in-node — 18);
               pretty() golden test on the example tree (exact
               expected multi-line string, line numbers shown);
               re-export imports from mcp_abacus.expr work.
    [x] 17.4.  Verify: uv run pytest green (new + existing 10), ruff
               check + format --check clean, mypy clean. NO MCP tool
               surface change — server.py untouched, so NO
               `uv tool install --reinstall` needed for this item.
               Commit when green.  (24 passed — 14 new; all 4 gates
               clean.)
    17.5.  Deliberately EXCLUDED for now (added later, when a consumer
           exists): Name/variables, Call/functions (sqrt etc. —
           anticipates 10.1.7), column/character-span position info
           (line number IS in scope — 17.2.6; finer spans only if
           error messages ever need them), evaluate() methods (18).


[x] 18.  EVALUATION — IN-NODE  (decision recorded 2026-06-11;
         full planning TBD in a later session)
         (done 2026-06-11: evaluate() on the nodes against a MINIMAL
         Value slice built ahead of 19 — expr/values.py, modes
         "rational" + "binary64" as plain strings, see the 19 note.
         Base Node.evaluate() wraps abstract _evaluate() per class:
         one shared place for error-wrapping (18.4) + storing (18.5).
         EvalError(message, line) lives in nodes.py. value field is
         init=False, compare=False, repr=False — repr stays
         structural; pretty() is the values view.)
    [x] 18.1.  Each node class implements evaluate(mode) -> Value
               (19) — the post-order recursion lives ON the nodes.
               Nodes from 17 gain their evaluate() methods HERE,
               once Value exists.
    [x] 18.2.  ALL arithmetic delegates to Value: BinOp.evaluate
               computes left and right, then combines them with
               plain Python operators on Values (a + b, a * b);
               our ^ translates to a ** b (Python's ^ is XOR —
               never overloaded as power, see 19.5).
    [x] 18.3.  Number.evaluate calls Value.from_lexeme(lexeme, mode);
               the mode parameter only threads down to the literals.
    [x] 18.4.  Error reporting split: Value ops raise mode-agnostic
               arithmetic errors WITHOUT position info (19.8);
               node.evaluate() catches and re-raises EvalError
               carrying node.line (the reason for 17.2.6). One mode
               erroring must not stop the other modes' results —
               that isolation lives in the item-10 runner (18.7.2),
               not here.
    [x] 18.5.  Nodes HOLD their calculated value: every node gets a
               value: Value | None annotation field — None until
               evaluated; evaluate() stores each node's result as the
               recursion unwinds. compare=False keeps structural
               equality/hash untouched. NOTE: on a frozen dataclass
               evaluate() must set it via object.__setattr__ (the
               same bypass frozen dataclasses' own generated __init__
               uses) — the structural fields stay frozen; only this
               designated annotation slot is written.
               ONE value, NOT per-mode: a calculation always runs in
               ONE type at a time, like a real calculator set to a
               type ("how much is 12 / 766 on a 64-bit integer
               calculator?") — every part of the expression is done
               in that type. Re-evaluating under another mode
               OVERWRITES; the item-10 multi-mode runner just does
               N single-mode runs and collects the ROOT result after
               each.
    [x] 18.6.  pretty() extended (from 17.2.7): when a node has a
               stored value, append " = <formatted value>" to its
               line — so printing the tree shows what each PART of
               the expression got in the current mode's run.
    [ ] 18.7.  STILL TO PLAN (separate items, later):
            18.7.1.  lexer/parser text -> AST — DONE as item 20
                     (2026-06-11).
        [ ] 18.7.2.  item-10 tool wiring: run evaluate() once per
                     requested mode, return each mode's root result as
                     its normal string (10.3).


[ ] 19.  UNIVERSAL VALUE CLASS — the shell, design decisions, and open
         faithfulness questions all live in expr/value.py's docstring;
         the working minimal slice stays at expr/values.py until rewired.
    [ ] 19.1.  Enum recording the type of the held value AND the storage
               that holds it — each type below adds an enum member plus
               the capability to store that type's value:
        [x] 19.1.1.  floating-point — IEEE-754 double, ~15-17 sig digits;
                     the default float type (C's "double"). The narrower
                     floats (19.1.3-19.1.5) keep width-tagged names
                     (float32/float16/bfloat16) so "floating-point" stays
                     unambiguous as the default (decided 2026-06-12).
        [x] 19.1.2.  fixed-point — scaled integer (mantissa, decimals);
                     exact, money/ERC-20-safe (flagship).
        [ ] 19.1.3.  binary32 — IEEE-754 single, ~7 significant digits.
        [ ] 19.1.4.  binary16 — IEEE-754 half, ~3-4 sig digits.
        [ ] 19.1.5.  bfloat16 — brain float: binary32 range, 8-bit mantissa.
        [ ] 19.1.6.  decimal — base-10 arbitrary precision (prec, rounding).
        [x] 19.1.7.  rational — exact numerator/denominator; no irrationals.
        [ ] 19.1.8.  bigfloat — arbitrary-precision binary (sqrt/pi/trig).
    [ ] 19.2.  Implement from_lexeme() — raw literal -> Value for every
               20.1.3 form, incl. base-prefixed integers (20.4). The
               behaviour contract is in value.py's class docstring.
        [x] 19.2.1.  Restrict `M@D` to hex/octal/binary literals — raw
                     integers that have no other way to write a fraction.
                     Decimal literals set precision by writing the decimals:
                     273645.00 is scale 2. Decimal `@` is a nightmare: it
                     point-shifts to a tiny number instead of setting scale.
                     Drop decimal `@`. Abacus today returns for 67@8:
                       0.00000067 (exact), precision 8
                     — should be 67.00000000 at scale 8, or just not allowed.
    [x] 19.3.  Operator methods (named methods — the class design and
               the no-overloading decision live in value.py's docstring):
        [x] 19.3.1.  Implement add()       (binary +)
        [x] 19.3.2.  Implement sub()       (binary -)
        [x] 19.3.3.  Implement mul()       (binary *)
        [x] 19.3.4.  Implement div()       (binary /)
        [x] 19.3.5.  Implement floordiv()  (binary //)
        [x] 19.3.6.  Implement mod()       (binary %)
        [x] 19.3.7.  Implement pow()       (binary **, power)
        [x] 19.3.8.  Implement neg()       (unary -)
        [x] 19.3.9.  Implement pos()       (unary +)
        [x] 19.3.10. Implement bitand()    (binary &, bitwise AND) — 24.3.2
        [x] 19.3.11. Implement bitor()     (binary |, bitwise OR)  — 24.3.2
        [x] 19.3.12. Implement bitxor()    (binary ^, bitwise XOR) — 24.3.2
        [x] 19.3.13. Implement bitnot()    (unary ~, bitwise NOT)  — 24.3.2
    [x] 19.4.  Implement to_string() — render the Value in its mode's
               normal string format (10.3). Named method (not __str__)
               so it can grow formatting options later, per 19.3's
               no-dunder rationale.
    [ ] 19.5.  Function methods — the per-mode implementation home for
               the call functions (section 22). Named methods on Value,
               each its own per-mode match like 19.3; dispatched from
               nodes._FUNCS. Per-mode semantics live in each method's
               docstring. Fixed-point math stays on int — NEVER route it
               through decimal: decimal's sig-digit context silently rounds
               arithmetic on big values; decimal is for literal PARSING
               only (exact construction).
        [x] 19.5.1.  Implement abs_()  — exact in every mode (cf. neg()).
        [x] 19.5.2.  Implement sqrt()  — irrational: fixed-point + binary64
                     compute and round (exact=False); rational exact only
                     for perfect squares, else NotRepresentableError.
                     Fixed-point: math.isqrt on the scaled mantissa (exact
                     floor, bignum-native).
        [ ] 19.5.3.  Implement pi()  — nullary constant. binary64: math.pi.
                     rational: NotRepresentableError (irrational). fixed-point:
                     irrational, compute to a target scale (bc-style series on
                     int, see section 22).
                     ISSUE TO PLAN: a nullary function has NO operand, so there
                     is no scale to round to (every other function inherits its
                     scale from its argument). Where does pi()'s fixed-point
                     scale come from — a required precision arg, a per-mode
                     default scale, or some ambient setting? Affects every
                     future constant/nullary function.


[x] 20.  LEXER / PARSER  (text -> AST; planned 2026-06-11; fleshes
         out stub 18.7.1)
         Hand-written, pure stdlib — no lark dep (keeps 2.4/16.2.3
         clean). stdlib ast is UNUSABLE here: our ^ is POWER with
         tighter-than-* precedence; Python's ^ is XOR with looser.
         (done 2026-06-11: expr/lexer.py + expr/parser.py +
         tests/test_lexer.py + tests/test_parser.py — 52 new tests,
         122 total green; ruff check/format + mypy clean. The two
         left-assoc binary levels run on a Pratt binding-power
         table; unary/power/atom are plain descent. LexError
         propagates out of parse() unchanged. ASCII-only digits, no
         underscore separators. NO MCP surface change — no
         `uv tool install --reinstall` needed.)
    [x] 20.1.  expr/lexer.py — the tokenizer:
        [x] 20.1.1.  Token = frozen dataclass (kind, lexeme, line).
                     Every token carries its 1-based input line —
                     this is what feeds node.line (17.2.6).
        [x] 20.1.2.  Token kinds: NUMBER, OP (+ - * / // % ^),
                     LPAREN, RPAREN, EOF. Longest match: '//' is ONE
                     token, never two '/'.
        [x] 20.1.3.  NUMBER lexemes (kept as RAW SOURCE — 17.2.2):
                     integers (42), decimals (0.1, 3., .5),
                     scientific (1e-3, 2.5E+6), and base-prefixed
                     INTEGERS 0xFF / 0b1010 / 0o17 (decided
                     2026-06-11: included from the start, ahead of
                     the fixed-width/bit modes 12.x/13.x). UNSIGNED —
                     sign is parsed as a unary op (17.2.2).
        [x] 20.1.4.  Whitespace skipped; '\n' increments the line
                     counter (an expression may span lines).
        [x] 20.1.5.  LexError(message, line) on unknown characters
                     and malformed numbers ('0x' without digits,
                     '1e' without exponent, '1.2.3').
    [x] 20.2.  expr/parser.py — recursive descent (Pratt-style
               binding powers for the binary levels), producing 17's
               nodes:
        [x] 20.2.1.  Grammar / precedence (loose -> tight), DECIDED
                     2026-06-11 — math convention, NOT Excel:
                     1. additive        + -        left-assoc
                     2. multiplicative  * / // %   left-assoc
                     3. unary           + -
                     4. power           ^          RIGHT-assoc and
                        TIGHTER than unary minus:
                        -2^2 = -(2^2) = -4;  2^3^2 = 2^(3^2) = 512;
                        exponent may itself be unary: 2^-3 = 2^(-3).
                     5. atom: NUMBER | '(' expr ')'.
        [x] 20.2.2.  parse(text: str) -> Node is the public API;
                     every node built with line= from its defining
                     token. Parens only shape the tree — no Group
                     node.
        [x] 20.2.3.  ParseError(message, line) on unexpected/missing
                     tokens: unbalanced parens, trailing garbage,
                     empty input, operator without operand.
        [x] 20.2.4.  ONE expression per parse() call (the calculator
                     model, 18.5); input may span lines but must
                     contain exactly one expression.
    [x] 20.3.  tests/test_lexer.py + tests/test_parser.py:
               token kinds/lexemes/line numbers incl. multi-line
               input; '//' longest match; every literal form of
               20.1.3 round-trips as raw lexeme; precedence proven
               via tree shapes (pretty() golden tests): 1+2*3,
               -2^2 -> -(2^2), 2^3^2 right-assoc, 2^-3, (1+2)*3,
               7//2%3 left-assoc; LexError/ParseError cases carry
               the correct line number.
    [x] 20.4.  Consequence for 19.3 (from_lexeme): base-prefixed
               lexemes are INTEGER literals, valid in EVERY mode as
               that integer value (0xFF == 255 in fixed-point,
               binary64, decimal, ...) — each mode converts the raw
               string itself.  (Already implemented in the item-18
               values.py slice; covered end-to-end by
               test_parse_then_evaluate_end_to_end.)
    [x] 20.5.  Fixed-point DECIMALS literal notation (decided
               2026-06-12; consumed by from_lexeme 19.2 / fixed-point
               19.1.2). Scale from a DECIMAL's own digits, or a raw
               integer + an explicit '@'<decimals> tag. Notation
               rationale + examples: lexer.py module docstring.
        [x] 20.5.1.  Lexer: integer '@'<decimals> suffix, kept as raw
                     source. See lexer.py _lex_number / _lex_at_decimals.
        [x] 20.5.2.  Tests (20.3): '@' forms round-trip as raw lexeme;
                     the (0xFF@9)^2 binding; LexError line numbers for
                     1.5@9 and a trailing '@'.


[ ] 21.  LANGUAGE HELP TOOL  (planned 2026-06-12; an in-server reference
         the AI reads to drive the evaluator correctly — terse, facts
         only, no prose; the model fills in the rest)
    [x] 21.1.  New MCP tool taking ONE argument `section`; returns that
               section's reference text. Extensible — more sections added
               later without changing the call shape.
    [ ] 21.2.  Initial sections:
        [x] 21.2.1.  `types` — short list of the numeric types the
                     CURRENT build supports: each = name + a one-liner
                     (e.g. "binary64 — IEEE-754 double, ~15-17 sig
                     digits"). Sourced from the live Value type enum
                     (19.1) so it tracks what is actually implemented,
                     not what is planned.
        [x] 21.2.2.  `language` — the expression grammar: operator set +
                     precedence/associativity (20.2.1) and the literal
                     forms incl. base prefixes and the M@D fixed-point
                     notation (20.1.3 / 20.5). Just the grammar, nothing
                     verbose.
        [x] 21.2.3.  `functions` — short list of the call functions the
                     CURRENT build supports: each = name + signature/arity
                     + a one-liner (e.g. "sqrt(x) — square root; inexact in
                     fixed-point/binary64"). Sourced from the nodes._FUNCS
                     registry (22.3) so it tracks what is actually wired,
                     not what is planned.


[ ] 22.  FUNCTIONS  (call syntax + abs/sqrt; planned 2026-06-12)
    DECISION (2026-06-13): functions stay per-mode METHODS on Value (19.5/22.4),
    dispatched from the _FUNCS name->method table in nodes.py (the "function set"
    is that registry, parallel to _UNARY_FUNCS/_BINARY_FUNCS). REJECTED: an
    external function-set class computing from a list of Values + payload getters
    on Value. Reason: per-type math (sqrt: isqrt vs math.sqrt vs perfect-square)
    needs each mode's representation, so it would re-introduce per-Mode branching
    OUTSIDE Value and break the single-chokepoint / two-step-add-a-type invariant
    (value.py:4-13). Class-bloat worry is handled by module-level per-mode helpers
    (_fp_value/_bitwise), NOT by crossing the mode boundary.
    [x] 22.1.  Lexer: add NAME token [A-Za-z_][A-Za-z0-9_]*; add COMMA
               (for future n-ary funcs). Generic NAME — function set is
               not the lexer's concern.
    [x] 22.2.  Parser: function call is an atom (tightest binding):
               atom := NUMBER | '(' expr ')' | NAME '(' expr (',' expr)* ')'.
               Validate name + arity against the registry HERE -> ParseError
               (carries the token line); unknown-name/wrong-arity are parse
               errors.
    [x] 22.3.  Nodes: FuncCall(name, args: tuple[Node,...]); reuses
               evaluate()/pretty(). Dispatch via a _FUNCS name->Value-method
               table, mirroring _UNARY_FUNCS/_BINARY_FUNCS.
    [ ] 22.4.  Value: one named method per function, each its own per-mode
               match (NOT a giant switch). Camp split:
        [x] 22.4.1.  abs FIRST — exact in every mode (shape of neg()).
                     Pure plumbing check: exercises 22.1-22.3 end to end.
        [x] 22.4.2.  sqrt SECOND — first irrational:
                     - fixed-point: SUPPORTED (crypto). isqrt on the scaled
                       int, half-even quantize, exact=False. Inexactness is
                       documented in sqrt's docstring (the spec). Default
                       round-to operand scale; leave room for an optional
                       target-scale arg.
                     - binary64: math.sqrt; negative -> NotRepresentableError.
                     - rational: perfect square -> exact; else
                       NotRepresentableError (no scale to round to).
    [x] 22.5.  Tests: abs/sqrt per mode; sqrt exact vs inexact flag;
               rational/negative refusals carry the right line; parse
               errors for unknown name and wrong arity.
    STILL TO PLAN:
      - [DONE] n-ary (variadic) call support is built: FUNCTION_ARITIES is now a
        (min, max|None) range, so a *args method declares a minimum. sum (28.5)
        was the plumbing check. COMMA was reserved in 22.1.
      - fixed-point sqrt target-scale argument (default = operand scale).
      - constants pi/e as zero-arg names (out of scope for now).
      - nullary-call support — a zero-argument call shape (grammar atom
        NAME '(' ')'; arity 0 in the registry; mode-without-operand dispatch,
        since no operand carries the mode). Prerequisite for the pi/e constants
        above and for time() (28.1); to plan before any nullary function lands.
      - functions feed the language-help `functions` section (21).


[x] 23.  EVALUATE TOOL  (planned 2026-06-12; the flagship — expose the
         evaluator to callers; 10 / SA.2.1)
    [x] 23.1.  New MCP tool `calculate`: expression string -> result.
    [x] 23.2.  Optional `mode` arg; default fixed-point.
    [x] 23.3.  Errors -> message + line (Lex/Parse/EvalError).
    [x] 23.4.  Output: always the result's string representation.
    [x] 23.5.  Unknown mode -> error "Unknown mode." + list valid modes.
    [x] 23.6.  mode-arg aliases: accept float64 and double as aliases for
               floating-point (19.1.1). Canonical name resolved before
               dispatch; help/output report the canonical name, not the
               alias. No aliases for other modes.


[ ] 24.  LATENCY / INTERFACE  (planned 2026-06-12; cut model round trips, not
         wire speed. Each answer floors at 2 inference turns; expected turns
         rise by P(model calls `help` first) + P(first calculate errors ->
         retry). These drive both probabilities toward 0 via schema/text only
         — no engine work.)
    [x] 24.1.  Self-sufficient `calculate` docstring: replace the "call the
               `help` tool for types/grammar" tail with an inline cheatsheet
               (3 mode names, ^ is POWER not XOR, no implicit type promotion,
               literal forms incl. M@D). Kills the `help` discovery turn for
               the common case; `help` demoted to fallback for full detail.
    [x] 24.2.  Broaden mode aliases (extends 23.6) to the model's likely
               first guesses so a plausible name resolves in turn 1 instead
               of erroring -> retry: float, ieee754 -> floating-point;
               fraction, frac -> rational; decimal -> fixed-point. Flag
               decimal if a true decimal Mode (SA.2.1) lands later. Same
               rule as 23.6: aliases resolve to canonical; never surfaced.
    [ ] 24.3.  Make first-attempt mistakes self-correcting in one turn:
               the ^-as-XOR and type-mixing gotchas live only inside `help`
               today, so 24.1 surfaces them up front; additionally embed the
               correction in the Parse/EvalError text (23.3) for type-mixing
               so a single retry fixes it rather than help+retry.

               DECISION (2026-06-13): primarily a PROGRAMMER's tool, so adopt
               C/Python operator conventions — flip the symbols (24.3.1 + 24.3.2)
               rather than document around the ^-as-XOR surprise.
        [x] 24.3.1.  Power moves from `^` to `**` (right-assoc, tightest), freeing
                     `^` for XOR; `^`-as-power then errors, steering to `**`.
        [x] 24.3.2.  Add bitwise XOR `^`, AND `&`, OR `|`, NOT `~` (unary),
                     `^` reprecedenced loose.

                     DECISION (2026-06-13): implement the bitwise ops for EVERY
                     type, not integer-only. Masking a floating-point value may
                     look pointless, but it is not ours to decide which bit
                     manipulation is beneficial and which is not — offer it on
                     all types and let the caller judge.

                     DECISION (2026-06-13): when the two operands differ in bit
                     width, the WIDER one wins — the narrower operand is
                     zero-extended, i.e. the bits it does not cover are treated
                     as 0.


[ ] 25.  ENHANCING THE TOOLS
    [x] 25.1.  The precision of the return value MUST absolutely be indicated
               in the return. Otherwise a simple `/` operator may mislead the
               caller seriously. The exact/inexact information is ALREADY there
               (computed today as the Value.exact flag) — this is purely about
               returning it, NOT about the calculation. When an inexact value
               is reported back for a fixed-point calculation, the precision
               must also be reported. The return string itself should carry the
               exact/inexact information; AND separate fields should be returned
               alongside it — the precision (when known) and the exact/inexact
               property (as it is calculated now).
    [x] 25.2.  Make `help` and the `calculate` description make it obvious that
               M@D is base-prefixed-ONLY (19.2.1): only 0x.. 0o.. 0b.. take @D;
               a DECIMAL mantissa (123.45@2) is INVALID.
    [x] 25.3.  The "inexact" reply pushes the caller the WRONG way: toward
               floating-point, instead of toward a higher-precision fixed-
               point. Steer the inexact case toward more fixed-point
               precision, NOT toward float.

                 | # | Expression          | Mode            | Result                                    |
                 |---|---------------------|-----------------|-------------------------------------------|
                 | 1 | 928347569 / 2345    | fixed-point     | 395884 (inexact, rounded to 0 decimals)   |
                 | 2 | 928347569 % 2345    | fixed-point     | 1934 (exact)                              |
                 | 3 | 928347569 / 2345    | floating-point  | 395883.82473347546 (inexact)              |

               TESTED 2026-06-13: docstring path confirmed — a fresh caller
               (Claude) asked 89237465 / 2345 and made its FIRST call with
               min_fixed_point_precision=10 already set, never touching
               floating-point. The precision came from the DOCSTRING
               description of the argument (25.3.2's "documented in the
               calculate docstring, known before the first call"), NOT from
               the inline =K annotation hint — there was no prior inexact
               reply in-session to read that hint from. So this validates the
               docstring-steering path only; the annotation-string hint path
               (25.3.2 HOW, first clause) remains untested here.
        [x] 25.3.1.  The call should take a min_fixed_point_precision argument,
                     accepted only in fixed-point mode.
        [x] 25.3.2.  Direct the caller to min_fixed_point_precision.
                     WHEN — only on an inexact fixed-point result (the one
                     case the arg helps and float tempts).
                     HOW — name it in that result's inexact annotation
                     ("...rounded to N decimals — pass
                     min_fixed_point_precision=K for more"), and document it
                     in the calculate docstring (known before the first call).
        [x] 25.3.3.  Cheap to compute twice: alongside the result, preview
                     what a higher precision would have shown — "the result
                     is 27, but with min_fixed_point_precision=4 it would be
                     27.6523". Add this ONLY if: fixed-point mode (the
                     default); min_fixed_point_precision was NOT given in the
                     call; AND the preview precision actually changes the
                     value (not 27.0000).
                     REPLY SHAPE (decided 2026-06-13). ONE representative
                     preview, NOT a ladder — a single nullable `preview`
                     sibling field that mirrors the reply's own string +
                     structured-fields duality, recursed one level:
                       "preview": { "min_fixed_point_precision": K,
                                    "value": "395883.82", "exact": false }
                     null whenever the three gate conditions above are not all
                     met. The nested object is self-documenting — its
                     min_fixed_point_precision key IS the argument to pass and
                     its value is what you'd get back — and the ACTUAL answer
                     stays at the TOP level, so nesting (not just naming) keeps
                     the what-if from being mistaken for the result. The
                     `value` annotation string ALSO carries the hint inline
                     ("...for more; e.g. =K -> 395883.82") so the string never
                     lies on its own.
                     PREVIEW PRECISION K = result scale + N, a small fixed bump
                     (N = 4): deterministic and cheap, no adaptive search. The
                     "changes the value" gate still applies — if scale+N renders
                     value-equal to the result (the rare "27.0000" case), omit
                     the preview (null).

[x] 26.  Make the `analyze` AST printout more useful to the caller.
    [x] 26.1.  Print the value as "value = 1/10 (type [prec])".
    [x] 26.2.  Rename "Number" to "Literal"; show the lexeme (already shown)
               quoted, e.g. "12", so it reads as the source string.
    [x] 26.3.  Also show the value in hex — even float/double (raw-bits
               hexdump is useful). Fixed-point in hex uses M@D notation.
    [x] 26.4.  Drop the line numbers — meaningless at this stage. Maybe later.
    [x] 26.5.  Mark each node exact/inexact (straight from the node's own
               value.exact; a parent is inexact simply by using an inexact child,
               so no separate flip / first-loss flag is needed).
    [x] 26.6.  For floating-point, also show the exact decimal the double
               holds (e.g. 0.1 -> 0.1000000000000000055...).
    [x] 26.7.  For rational, show a decimal approximation beside the exact
               fraction (e.g. 1/3 = 0.3333...).


[x] 27.  ENRICH THE `calculate` REPLY (and its docstring) for the AI caller.
         Six reply-shape changes on top of 25.*, all so the caller reads the
         numbers back without guessing the regime or re-deriving the bits.
         Decided 2026-06-13. Scope is the `calculate` STRUCTURED reply; `analyze`
         already renders hex and per-mode detail inline in its tree (26.3).
    [x] 27.1.  Always return `mode` in the reply — the RESOLVED Mode.value
               (e.g. "fixed-point"), even on success where it equals the
               request. The reply must stand on its own: a request alias like
               "double" must read back as its canonical "floating-point". On
               the error path `mode` is null alongside value/exact/precision
               (an unknown mode never resolved), matching _error's all-null shape.
    [x] 27.2.  Rename the `preview` field to `offered_precision`. Same gate and
               meaning as 25.3.3 — the what-if at result-scale + 4 decimals, null
               unless fixed-point AND inexact AND no floor was passed — but the
               name states what it IS: a higher precision being offered to the
               caller, not a preview of the answer they asked for.
    [x] 27.3.  `offered_precision` carries `mode` too (always "fixed-point" — it
               is only ever produced in fixed-point), mirroring 27.1 so the
               nested object is as self-describing as the top-level reply.
    [x] 27.4.  `offered_precision.value` is ANNOTATED exactly as the top-level
               `value` is — with its OWN exact/inexact verdict and rounding,
               e.g. "3.3333 (inexact, rounded to 4 decimals — ...)", not a bare
               "3.3333". The offered value is itself a fixed-point result and may
               still be inexact (10/3 never terminates at any scale), so it must
               declare its own precision the same way the main value does. Show
               the offered value the same way the main value shows its value.
    [x] 27.5.  Wherever a value STRING is returned, return a sibling
               `value_hex_dump` with that value in hex: at the top level beside
               `value`, and inside `offered_precision` beside its `value`. Reuse
               Value.details()'s hex rendering (26.3) — fixed-point as M@D hex
               (mantissa in whole-byte hex, `@scale` dropped at scale 0),
               floating-point as the raw 64-bit IEEE-754 pattern.
               DECISION (2026-06-13): rational has no single integer to dump (it
               is a numerator/denominator pair) — `value_hex_dump` is NULL in
               rational mode. It is meaningful only for the bit-backed types
               (fixed-point's mantissa, float's IEEE bits). Null on the error
               path too (no value to dump).
    [x] 27.6.  The top-level `value` string KEEPS its inline worked-example hint
               ("...for more; e.g. =4 → 3.3333") when an offered_precision exists
               — even though offered_precision now carries that same value
               structurally AND annotated (27.4). DECISION (2026-06-13): the
               string must stay honest read on its own (25.3.3's rationale), so
               the inline hint is duplicated, not relocated.
    [x] 27.7.  Update the `calculate` docstring (the AI-facing result
               description) to cover the new/renamed fields: `mode`,
               `value_hex_dump` (with the null-in-rational rule), and
               `offered_precision` (the rename of `preview`, now carrying `mode`,
               an annotated `value`, and its own `value_hex_dump`).


[ ] 28.  MORE FUNCTIONS  (the growing function set beyond abs/sqrt; the call
         machinery itself is section 22. Each entry is normally the two-step
         add — one _FUNCS entry + its per-mode Value method — UNLESS it needs a
         call shape we have not built yet, flagged per entry.)
    [x] 28.1.  time() — current Unix epoch in UTC, read once per run from the
               REALTIME clock (time.time_ns -> integer nanoseconds since epoch).
               NULLARY: rides the zero-arg call path built in 29 — one
               _NULLARY_FUNCS entry + a Value classmethod taking the EvalContext
               (no operand carries the mode). DEFAULT resolution is WHOLE SECONDS
               (the C library time()); finer resolution down to tv_nsec is folded
               in only where the mode's precision can carry it, reusing the 29.3
               derived scale verbatim. UNLIKE the irrational constants
               (pi/e/tau/phi, 29.5) a clock reading is EXACTLY rational, so time()
               does NOT refuse in rational and is inexact only where the type
               rounds — the first nullary that is representable in every mode.
        [x] 28.1.1.  PER-MODE RENDER — feed the single ns reading through
                     _from_scaled_int (the per-mode chokepoint stays in Value):
                     - fixed-point: render at the derived scale s (29.3). s = 0
                       (default, no literal/floor) -> whole seconds; 1..9 ->
                       seconds + tv_nsec TRUNCATED to s decimals; >= 9 -> full ns
                       (further decimals are true zeros, the clock has no finer
                       grain). Exact — the rendered decimal IS the sampled value,
                       not an approximation (truncation is a resolution choice).
                     - float: float(ns / 1e9) — native double, ~sub-microsecond at
                       epoch magnitude, so inexact (the only mode that rounds).
                     - rational: Fraction(ns, 10**9) — full ns, exact, no refuse.
        [x] 28.1.2.  IMPURITY + ONE INSTANT PER RUN — first clock-dependent
                     function. Sample the clock ONCE and hold it on the EvalContext
                     (29.1) as now_ns, shared by every time() in the expression, so
                     a run sees a single instant (time() - time() == 0 exactly).
                     evaluate() defaults now_ns from the real clock; tests inject a
                     fixed epoch to assert exact per-mode/scale renders, a live call
                     asserting only type/range. Keeps Value.time a pure function of
                     the context.
    [x] 28.2.  max(...) — the largest of its operands. VARIADIC (>= 1 arg); all
               operands the same mode (no mixing, like every binary op). EXACT:
               it only selects an operand, never computes, so the result carries
               the chosen operand's own exactness — no rounding anywhere.
    [x] 28.3.  min(...) — the smallest of its operands; the mirror of max (28.2):
               variadic, same-mode, selection-only and therefore exact.
    [x] 28.4.  avg(...) — the arithmetic mean (sum / count). VARIADIC (>= 1 arg),
               same-mode. Unlike max/min it COMPUTES: the division can round, so
               it follows the mode's own `/` rule — fixed-point quantizes to the
               covering scale and may be inexact, rational is exact, float rounds.
    [x] 28.5.  sum(...) — the total of its operands. VARIADIC (>= 1 arg),
               same-mode. No division, so EXACT in every mode (like repeated `+`);
               the building block avg (28.4) divides. Fixed-point result scale is
               the covering max() of the operands', as every fixed-point op.
    [x] 28.6.  product(...) — the product of its operands. VARIADIC (>= 1 arg),
               same-mode. Like repeated `*`: fixed-point may round to the covering
               scale (and flag inexact), rational is exact, float rounds.
    [x] 28.7.  median(...) — the middle operand by value. VARIADIC (>= 1 arg),
               same-mode. Sorting is order-only (no arithmetic). ODD count is pure
               selection -> EXACT, carrying the chosen operand's exactness (like
               max/min). EVEN count averages the two middles -> follows the mode's
               `/` rule, exactly as avg (28.4).
    [x] 28.8.  variance(...) — POPULATION variance, sum of squared deviations / n
               (DECISION: divide by n, not n-1; a sample variant stddev_s/var_s is
               STILL TO PLAN). VARIADIC (>= 1 arg), same-mode. COMPUTES (squares +
               division), so it follows the mode's `/` rule: fixed-point may round,
               rational exact, float rounds.
    [x] 28.9.  stddev(...) — POPULATION standard deviation == sqrt(variance)
               (28.8), so it INHERITS sqrt's per-mode story (19.5.2): inexact in
               fixed-point and float, and in RATIONAL mode it honestly raises
               NotRepresentableError when the root is irrational rather than
               fabricate digits — the engine's exact-or-refuse pitch. VARIADIC
               (>= 1 arg), same-mode.
    [x] 28.10.  sin(x) — sine, argument in radians. UNARY operand-method (the
                shape of sqrt, 19.5.2), so it needs NO new call machinery — the
                two-step add — but with real transcendental math inside.
                floating-point: math.sin; transcendental -> unconditionally
                  inexact.
                fixed-point: SUPPORTED via a TAYLOR SERIES summed at the operand's
                  scale (plus guard digits), after RANGE-REDUCING the argument into
                  a small interval around 0 (mod 2*pi) so the series converges fast
                  and stays accurate. The reduction needs pi to at least the working
                  precision -> an INTERNAL high-precision fixed-point pi helper
                  (28.10.1). Transcendental -> exact=False.
                rational: sin of a rational is irrational except the trivial
                  sin(0) = 0; otherwise NotRepresentableError (no scale to round
                  to), mirroring sqrt's rational refusal.
        [x] 28.10.1.  INTERNAL fixed-point pi helper — computes pi to a requested
                  precision (enough guard digits for 28.10's range reduction and
                  series summation). This is an ENGINE helper we call when high-
                  precision pi is needed; it is NOT the pi() abacus language
                  function (that would be a nullary constant, still unplanned in
                  22's STILL TO PLAN). Pure-stdlib integer algorithm (e.g. a
                  Machin-type arctan series on scaled ints) — no new dependency.
    [x] 28.11.  cos(x) — cosine, radians. UNARY operand-method, the SAME machinery
                as sin (28.10): fixed-point Taylor series at the operand's scale +
                guard digits, range-reduced mod 2*pi via the internal pi helper
                (28.10.1); float math.cos inexact; rational irrational except the
                trivial cos(0) = 1, else NotRepresentableError.
    [x] 28.12.  tan(x) — tangent, == sin/cos (28.10/28.11). UNARY. Undefined at odd
                multiples of pi/2 where cos = 0: in fixed-point that point is never
                hit EXACTLY (pi is irrational, so pi/2 is not representable) — the
                result is just a large inexact value — UNLESS cos rounds to 0 at
                the working scale, which must raise ZeroDivisionError. float
                math.tan; rational irrational except tan(0) = 0.
    [x] 28.13.  cot(x) — cotangent, == cos/sin: the mirror of tan (28.12), undefined
                where sin = 0 (multiples of pi; same round-to-0 division guard).
                Same per-mode story; rational irrational except where trivially
                defined.
    NAMING (28.12/28.13): canonical spellings are tan/cot (the usual programming
    ones — math.tan, C), DECIDED over the European tg/ctg. Offering tg/ctg as
    aliases (cf. the mode aliases) is optional, still to plan.
    [x] 28.14.  asin(x) — arcsine, result in radians within [-pi/2, pi/2]. UNARY
                operand-method. DOMAIN-RESTRICTED: |x| <= 1, else
                NotRepresentableError (float math.asin raises outside [-1,1] ->
                map to it). Transcendental -> inexact in float/fixed-point. The
                plain arcsin Taylor series converges badly near |x| = 1, so
                fixed-point should instead compute asin(x) = atan(x / sqrt(1-x**2)),
                reusing sqrt (19.5.2) and an INTERNAL arctan series — the same
                Machin-type arctan already behind the pi helper (28.10.1), worth
                factoring out as a shared engine primitive. rational: irrational
                except the trivial asin(0) = 0, else refuse.
    [x] 28.15.  acos(x) — arccosine, result in [0, pi]. UNARY, same domain |x| <= 1.
                == pi/2 - asin(x) (28.14), so it also needs the internal pi helper
                (28.10.1) for pi/2. Same inexact / refuse story; rational exact only
                at the trivial acos(1) = 0.
    [x] 28.16.  atan(x) — arctangent, result in radians within (-pi/2, pi/2). UNARY,
                domain ALL reals (NO restriction, unlike asin/acos). It is the
                PRIMITIVE the inverse trig reduces to: the internal Machin-type
                arctan series behind the pi helper (28.10.1) IS atan, so once that
                series is factored out as a shared engine primitive, exposing atan
                as a language function is nearly free. float math.atan;
                transcendental -> inexact in float/fixed-point; rational irrational
                except the trivial atan(0) = 0.
    [x] 28.17.  log(x) — NATURAL logarithm, base e (DECISION 2026-06-13: bare `log`
                is ln, matching math.log in C/Python/JS and this engine's own
                canonical-spelling precedent, cf. the tan/cot-over-tg/ctg call; `ln`
                is an alias, base-10 is the separate log10, 28.18). UNARY operand-
                method (the shape of sqrt/sin), so NO new call machinery — the two-
                step add — with real transcendental math inside. The PRIMITIVE the
                whole log family reduces to: log10/log2 are just this divided by a
                constant. DOMAIN x > 0; x <= 0 has no real log (x = 0 is -inf) and
                raises NotRepresentableError in every mode (math.log's ValueError
                maps to it), mirroring sqrt's negative refusal.
                floating-point: math.log; transcendental -> unconditionally inexact.
                fixed-point: SUPPORTED via base-10 argument reduction + an atanh
                  series, the log-family mirror of the trig family's reduce-then-
                  Taylor (28.10). Reducing in base 10 is EXACT here (a bare mantissa
                  shift): write x = r * 10**n with r in [sqrt(0.1), sqrt(10)) so
                  log(x) = n*ln(10) + log(r); then log(r) for r near 1 via
                  log(r) = 2*atanh(t), t = (r-1)/(r+1), summed as the all-plus odd
                  series t + t**3/3 + t**5/5 + ... on scaled ints at the operand's
                  scale plus guard digits (|t| <= 0.52, so it converges fast — a
                  tighter sqrt-based reduction is a possible later optimization),
                  rounded half-to-even back. Transcendental -> exact=False, EXCEPT
                  the trivial log(1) = 0, which is exact.
                rational: log of a rational is transcendental except the trivial
                  log(1) = 0 (Lindemann-Weierstrass: ln of a rational != 1 is
                  irrational); otherwise NotRepresentableError, the same exact-or-
                  refuse stance as sqrt.
        [x] 28.17.1.  INTERNAL fixed-point atanh series helper — sums the all-plus
                  odd series t + t**3/3 + t**5/5 + ... for t/unity in (-1, 1) on
                  scaled ints, the log-family analogue of _fp_sin_series (28.10) and
                  the all-plus sibling of the alternating _arctan_inv (28.10.1).
                  Shared by the ln core (the reduced-argument log(r), with a general
                  t) AND the ln constants (28.17.2, with t = unity/k a unit
                  fraction), so ONE series primitive covers both — no separate
                  _artanh_inv needed. Pure-stdlib integer arithmetic, no dependency.
        [x] 28.17.2.  INTERNAL ln-constant helpers — ln(10) and ln(2) to a requested
                  precision, the log-family analogue of the _pi_scaled Machin
                  constant (28.10.1). Both fall out of the 28.17.1 series at unit-
                  fraction arguments: ln(2) = 2*atanh(1/3); ln(10) = 6*atanh(1/3) +
                  2*atanh(1/9) (since ln(5) = 2*ln(2) + 2*atanh(1/9) and ln(10) =
                  ln(2)+ln(5)) — exactly paralleling pi = 16*arctan(1/5) -
                  4*arctan(1/239). Summed with guard digits then truncated back,
                  correct to the last place like _pi_scaled. ln(10) feeds both the
                  base-10 reduction (28.17) and log10 (28.18); ln(2) feeds log2 (28.19).
    [x] 28.18.  log10(x) — base-10 logarithm == log(x) / ln(10) (28.17 + the ln(10)
                constant 28.17.2). UNARY operand-method, the two-step add. Same
                domain (x > 0, else NotRepresentableError) and the same float/refuse
                story. RICHER exact landmarks than log: because the base-10 reduction
                already extracts n with x = r * 10**n, log10(x) = n + log(r)/ln(10),
                so the integer part n is exact and only the log(r)/ln(10) fraction is
                transcendental. A power of ten therefore logs EXACTLY:
                floating-point: math.log10; transcendental -> inexact.
                fixed-point: SUPPORTED. Reuse the ln core, divide by ln(10) at the
                  working scale, quantize half-to-even to the operand's scale.
                  exact=False EXCEPT when x is an exact power of ten at the operand's
                  scale (r = 1 after reduction), where the result is the integer n
                  exactly — e.g. log10(100) = 2, log10(0.001) = -3.
                rational: log10 of a rational is irrational UNLESS x is an integer
                  power of ten (the only rational-result case — x = 10**k for integer
                  k, k possibly negative like 1/10, 1/100), where it is k exactly;
                  otherwise NotRepresentableError. A cleaner exact set than sqrt's
                  perfect-square, and the analogue of it for logs.
    [x] 28.19.  log2(x) — base-2 logarithm == log(x) / ln(2) (28.17 + the ln(2)
                constant 28.17.2). UNARY operand-method, the two-step add; planned
                with ln/log10 because this engine is bit-oriented (entropy / bit-
                width work). Same domain (x > 0, else NotRepresentableError) and
                float/refuse story.
                floating-point: math.log2; transcendental -> inexact.
                fixed-point: SUPPORTED. Reuse the ln core, divide by ln(2) at the
                  working scale, quantize half-to-even. Unlike log10 the reduction is
                  base 10, NOT base 2, so a power of two does NOT fall out exactly —
                  exact=False EXCEPT the trivial log2(1) = 0. (A base-2 reduction
                  would make powers of two exact but breaks the shared base-10 ln
                  core; not worth a second reduction path — note as a possible later
                  refinement.)
                rational: log2 of a rational is irrational except log2(1) = 0
                  (a non-unit power of two like 2, 1/2 logs to a rational integer,
                  but proving/landing that exactly needs the base-2 reduction above,
                  so for now ONLY log2(1) = 0 is the rational-exact case);
                  otherwise NotRepresentableError.
    [x] 28.20.  pow(x, y) — exponentiation, x to the power y: the CALL form of the
                `**` operator (24.3.1 / 19.3.7), routing to the SAME Value.pow method a
                `**` BinOp does — NO new arithmetic, the function is a pure facade.
                It is the FIRST FIXED-ARITY-2 (binary-call) function, but needs NO new
                call machinery either: _arity_of already reads (2, 2) off a (self,
                other) method, so the binary-call shape the 28.17-28.19 NAMING note
                flagged as still-to-plan (for a future log(x, base)) is ALREADY built —
                pow just proves it. Adding it is literally one _FUNCS entry
                ("pow": Value.pow), the usual two-step add minus step two (the method
                exists). Per-mode story IS Value.pow's verbatim (same method):
                floating-point: x ** y; a negative base to a fractional exponent goes
                  complex -> NotRepresentableError (float can't hold it); inexact when
                  the operands are.
                fixed-point: INTEGER exponent only — exact via mantissa arithmetic
                  (negative exponent = reciprocal; zero base to a negative power is
                  ZeroDivisionError); a non-integer exponent is irrational ->
                  NotRepresentableError.
                rational: INTEGER exponent only — exact (Fraction ** int); a non-integer
                  exponent -> NotRepresentableError, the same exact-or-refuse stance.
                NOTE: pow(x, y) DELIBERATELY DUPLICATES the `**` operator (a named-call
                alias, like ln for log) — convenience for users who reach for a
                function; it adds no capability `**` lacks. A future fractional-exponent
                pow via exp(y*ln(x)) (reusing the 28.17 ln core + an exp series) would be
                a SEPARATE, inexact extension, NOT this item.
        [x] 28.20.1.  FIXED-POINT FRACTIONAL EXPONENT — lift fixed-point pow past the
                  integer-only core (28.20). A fixed-point exponent y is a scaled
                  integer, so in lowest terms it is p/q with q | 10**decimals — i.e. q
                  is a product of 2s and 5s (2, 4, 5, 8, 10, 20, ...). So x**y is NOT a
                  repeated multiply but the q-th ROOT of x**p — which the engine already
                  has two playbooks for. They LAYER (try exact, fall back), the same
                  exact-then-approximate ladder sqrt wants:
                  PATH A — exact-or-refuse (the sqrt model, 19.5.2). With x = M/10**E,
                    x**(p/q) = (M**p)**(1/q) / (10**(E*p))**(1/q); EXACT iff both M**p
                    and 10**(E*p) are perfect q-th powers (the generalization of sqrt's
                    perfect-square exemption), checked with an integer q-th root (Newton
                    on ints + verify, the bignum sibling of math.isqrt). Pure integer, no
                    series: 100**0.5 -> 10, 0.25**0.5 -> 0.5 exact; 2**0.5 refuses. BONUS:
                    this path reaches ODD roots of NEGATIVE bases ((-8)**(1/3) -> -2) that
                    Path B cannot (ln of a negative).
                  PATH B — inexact series (the sin/log model). x**y = exp(y*ln(x)), summed
                    at the working scale + guard digits, exact=False. Domain x > 0
                    (negative base -> complex). Reuses the 28.17 ln core and needs ONE new
                    exp series (a 28.x sibling of the atanh/Taylor helpers). Answers for
                    EVERY fixed-point exponent, at the cost of inexactness.
                  LADDER: integer -> exact (28.20); fractional perfect root -> exact
                    (A); fractional irrational, x>0 -> inexact (B); fractional, x<0 ->
                    NotRepresentableError (complex). This is a FIXED-POINT-only story:
                    rational still refuses (a Fraction can't hold 2**0.5, and rational
                    never approximates) and float already does it via math.pow — the
                    per-mode asymmetry stays intact. Promotes Value.pow's fixed-point
                    arm from the 28.20 facade to a real sqrt-shaped implementation.
    [x] 28.21.  cbrt(x) — cube root, x**(1/3). UNARY operand-method, the SHAPE of
                sqrt (19.5.2): the two-step add, no new call machinery. The q=3 case
                of the integer q-th root 28.20.1 Path A flags, so cbrt could later be
                a thin facade over a fractional pow; as its own named function it is
                the convenient spelling (the math.cbrt sibling of math.sqrt). The ONE
                real difference from sqrt: a cube root is an ODD root, so NEGATIVE
                inputs are in DOMAIN — cbrt(-8) == -2, real, NOT a NotRepresentable
                refusal the way sqrt(-1) is. Per-mode story mirrors sqrt otherwise:
                floating-point: math.cbrt is 3.11+ but the project floor is 3.10
                  (pyproject requires-python), so compute sign-preserving via
                  math.copysign(abs(x) ** (1/3), x) — handles negatives, inexact.
                fixed-point: SUPPORTED. Integer cube root of the scaled mantissa
                  (Newton-on-ints + verify, the q=3 bignum sibling of math.isqrt that
                  28.20.1 Path A needs anyway), quantized to the operand scale,
                  exact=False — EXCEPT a perfect cube at the working scale is exact
                  (sqrt's perfect-square exemption). Negative mantissa is fine (odd
                  root): cbrt the magnitude, carry the sign.
                rational: exact ONLY when numerator and denominator are BOTH perfect
                  cubes (incl. negative numerator — odd root); else
                  NotRepresentableError, the same exact-or-refuse stance as sqrt.
    [x] 28.22.  OPTIONAL-ARG ARITY — the (1, 2) call shape: a REQUIRED first operand
                plus ONE OPTIONAL trailing argument. The prerequisite for the ndigits
                forms of the rounding family (28.23-28.26), the way nullary support
                (29) preceded pi()/time(). SMALL: _arity_of (22.2) already reads a
                (min, max) range off the signature but counts every positional param
                as required; teach it to treat a param WITH A DEFAULT as optional, so
                round(self, ndigits=None) reads (1, 2). The parser's range check (22.2)
                and FuncCall dispatch already honour a range — the method's default
                fills the absent arg — so this is the one-line arity fix plus tests, no
                new node or grammar. NDIGITS is a COUNT, not a value-in-the-mode (cf.
                the sqrt target-scale arg still-to-plan in 22's list): the second
                operand is evaluated like any other, then required to be an exact
                INTEGER (any mode) and read as a Python int — non-integer refuses (the
                pow integer-exponent stance, 28.20). Negative ndigits rounds to
                tens/hundreds (round(1234, -2) -> 1200), the standard Python semantics.
    [x] 28.23.  floor(x[, ndigits]) — round toward NEGATIVE infinity. UNARY operand-
                method (the shape of abs, 19.5.1) with the 28.22 optional ndigits.
                EXACT in every mode (it snaps to a representable value, no compute) —
                EXCEPT floating-point with ndigits > 0, where the n-decimal target is
                not binary-representable; float to an INTEGER stays exact. Per mode:
                floating-point: math.floor (to int) -> float; ndigits via the decimal
                  shift floor(x*10**n)/10**n.
                fixed-point: M // 10**d on the scaled mantissa (Python // already
                  floors toward -inf), result scale 0; ndigits n floors at scale n
                  (result scale max(0, n)). Exact.
                rational: math.floor(Fraction) -> int; ndigits floors the shifted
                  fraction. Exact.
                floor(2.7) -> 2, floor(-2.1) -> -3.
    [x] 28.24.  ceil(x[, ndigits]) — round toward POSITIVE infinity; the mirror of
                floor (28.23). Same shape and per-mode exactness story. fixed-point via
                -((-M) // 10**d); floating-point math.ceil; rational math.ceil.
                ceil(2.1) -> 3, ceil(-2.7) -> -2. Canonical spelling ceil (the math
                module / programming name), NOT ceiling.
    [x] 28.25.  round(x[, ndigits]) — round to NEAREST, ties to EVEN. DECISION
                (2026-06-14): half-to-even (banker's) — round(2.5) -> 2, round(3.5) ->
                4 — matching Python's builtin round, the IEEE 754 default, AND the
                engine's existing fixed-point half-even quantize (sqrt, 22.4.2), so the
                whole engine rounds ONE way. Same unary + optional-ndigits shape and
                exact-except-float-with-ndigits story as floor (28.23).
                floating-point: builtin round(x) / round(x, n) (already half-even).
                rational: round(Fraction) / round(Fraction, n) — Fraction.__round__ is
                  half-even, returns int / Fraction -> exact.
                fixed-point: half-even on the scaled mantissa at the target scale,
                  exact.
    [x] 28.26.  trunc(x[, ndigits]) — round toward ZERO (drop the fraction): floor for
                x >= 0, ceil for x < 0. Same shape and exactness story as floor (28.23).
                floating-point math.trunc; fixed-point sign * (abs(M) // 10**d);
                rational math.trunc. trunc(2.7) -> 2, trunc(-2.7) -> -2 (vs
                floor(-2.7) -> -3). DISTINCT from fixed-point's literal truncation in
                time() (28.1.1): same toward-zero idea, here an explicit function over
                any value.
    NAMING (28.23-28.26): canonical spellings floor, ceil, round, trunc — the
    math.floor/ceil/trunc + builtin round family, the names every language uses. All
    four are EXACT in every mode (rounding selects a representable value), the lone
    exception floating-point with ndigits > 0 (a non-representable decimal). They
    differ ONLY in tie/sign handling: floor -> -inf, ceil -> +inf, trunc -> 0,
    round -> nearest (half-even, 28.25).
    [x] 28.27.  exp(x) — the exponential e**x, the INVERSE of log/ln (28.17). UNARY
                operand-method (the shape of sqrt/sin/log), so NO new call machinery —
                the two-step add — transcendental math inside. Building it ALSO yields
                the exp series the still-to-plan fractional pow needs (the 28.20 note +
                28.20.1 Path B's exp(y*ln(x))), so landing exp() supplies that shared
                piece. (exp(1) is e, the 29.2 nullary constant, here recomputed via the
                series rather than read from _e_scaled.)
                floating-point: math.exp; transcendental -> unconditionally inexact.
                fixed-point: SUPPORTED via the all-plus Taylor series 1 + x + x**2/2! +
                  x**3/3! + ... summed at the operand's scale + guard digits, after
                  RANGE-REDUCING so it converges fast. Reduce by ln(2) (reuse
                  _ln2_scaled, 28.17.2): write x = k*ln2 + r with k = round(x/ln2) and
                  |r| <= ln2/2 ~ 0.35, then exp(x) = 2**k * exp(r) — the 2**k an EXACT
                  mantissa scaling, only exp(r) summed. Negative x is fine (k < 0).
                  Transcendental -> exact=False, EXCEPT the trivial exp(0) = 1, exact.
                rational: exp of a rational is transcendental except the trivial
                  exp(0) = 1 (Lindemann-Weierstrass: e**r is irrational for rational
                  r != 0); otherwise NotRepresentableError, the same exact-or-refuse
                  stance as log/sqrt.
        [x] 28.27.1.  INTERNAL fixed-point exp series helper — sums 1 + x + x**2/2! +
                  ... for a small reduced |x| on scaled ints, the all-plus Taylor
                  sibling of _fp_sin_series (28.10) and the inverse of the atanh/ln
                  series (28.17.1). REUSED by exp() AND by the fractional-pow Path B
                  exp(y*ln(x)) (28.20.1) — ONE series primitive, the way the atanh
                  helper (28.17.1) covers both ln and the ln constants. Pure-stdlib
                  integer arithmetic, no dependency.
    NAMING (28.17-28.19): canonical spellings are log (== natural, ln alias), log10,
    log2 — the math.log/log10/log2 family. DECIDED that bare `log` is the NATURAL log
    (not base-10) over the calculator convention, for consistency with the programming
    spellings already chosen (tan/cot, 28.12/28.13). A two-argument log(x, base) =
    log(x)/log(base) is a possible later add but needs a fixed-arity binary-call shape
    (the current funcs are unary or variadic); still to plan if wanted.
    NOTE: statistical "mode" (most-frequent value) is intentionally NOT added —
    the name collides with the type-regime `mode`; revisit under another name if
    ever wanted.
    VARIADIC call support is BUILT (see 22's STILL TO PLAN): FUNCTION_ARITIES is a
    (min, max|None) range read off the signature, so a variadic func is the usual
    two-step add with a ``*others`` method. sum (28.5) landed as the plumbing
    check; max/min (28.2/28.3) followed, adding the value-only Value._compare
    ordering primitive (selection-only, so they return the chosen operand verbatim
    — its own scale and exactness). The rest (28.4 avg, 28.6 product, 28.7 median,
    28.8-28.9 variance/stddev) are ordinary adds — median reuses _compare, the
    others just compute.


[ ] 29.  NULLARY FUNCTIONS  (zero-arg calls — pi(), e(), time(); 22's STILL
         TO PLAN, blocking 19.5.3 and 28.1). Grammar atom NAME '(' ')', arity 0
         in the registry, mode-without-operand dispatch.
    [x] 29.1.  EVAL CONTEXT — an object carrying per-run evaluation state (the
               nullary mode + precision below, room for more later), threaded
               down the evaluate walk rather than a module global.
    [x] 29.2.  DISPATCH SHAPE — nullaries are NOT operand-methods (every other
               func is a Value method with self as the first operand). With no
               operand, a nullary is a new registered-callable kind that takes
               the eval context (29.1) instead of operands.
    [x] 29.3.  MODE & PRECISION — nullaries have no operand to carry them.
               Mode = the request mode (already known). Fixed-point precision:
               start from the default floor (0 or min_fixed_point_precision,
               already implemented), then PRE-WALK the AST once before
               evaluation and raise it to the largest decimal scale of any
               literal — so fixed-point/4 with a 0xffff@8 literal gives the
               nullaries scale 8, and a 2.000000000 literal pushes them to 9.
               Held on the eval context (29.1) for the run; the written scale is
               recoverable since Number keeps the lexeme verbatim. The pre-walk
               is FIXED-POINT-ONLY: float uses the native type (math.pi) and
               rational has no scale (irrational constants refuse, like sqrt(2)).
    [x] 29.4.  TESTS — parse pi() (empty arg list); arity error on pi(1);
               precision derivation: 0xffff@8 -> scale 8, 2.000000000 -> 9,
               no-literal -> the default floor; per-mode dispatch.
    [ ] 29.5.  MORE CONSTANTS — two more nullaries, each one registry entry
               (29.2) plus a Value classmethod (29.3), reusing the fixed-point
               series helpers already in place. Both irrational, so the per-mode
               story is pi/e's verbatim: float a native double, fixed-point
               truncated to the derived scale, rational REFUSES.
        [ ] 29.5.1.  tau() — the circle constant 2*pi. Reuses _pi_scaled
                     (doubled); float math.tau. Transcendental, like pi.
        [ ] 29.5.2.  phi() — the golden ratio (1 + sqrt(5)) / 2. Reuses the
                     fixed-point math.isqrt path sqrt() already uses; float
                     (1 + 5**0.5) / 2. ALGEBRAIC irrational — a deliberate
                     category contrast to pi/e/tau (transcendental), same
                     multi-mode behaviour. (ln2/ln10 deferred: derivable as
                     log(2)/log(10), so convenience not new capability.)
    [x] 29.6.  BARE CONSTANTS — pi and e usable WITHOUT parens, so `2*pi` reads
               the constant like a literal. The parser turns a bare NAME in
               nodes.CONSTANT_NAMES into the SAME nullary FuncCall as pi()/e()
               (29.2), so evaluation is identical in every mode. These names are
               RESERVED: assigning to one (`pi = ...`) is a parse error, not a
               shadowing rebind (30.3). time() is excluded — a clock read is an
               action, not a constant, so it keeps its parens. (tau/phi from 29.5
               would join CONSTANT_NAMES when added.)


[ ] 30.  VARIABLES  (named values: `x = expr` assignment + bare-name
         reference, and multi-line input — several instructions per call).
         SCRATCH — flesh out when picked up.
    [x] 30.1.  Variable store — a small class holding name (str) -> Value
               pairs, with set/get by name (get on an unknown name raises).
    [x] 30.2.  Carry a store instance on the eval context (29.1), so it
               lives for the whole run and threads down the evaluate walk.
    [x] 30.3.  Parser — assignment `NAME '=' expr`, lowest precedence; the
               AST Assign node holds the name lexeme + the value subtree.
    [x] 30.4.  Parser — a bare NAME atom is a variable reference (an AST
               Var node holding the lexeme); keep it distinct from the
               NAME '(' ... ')' call / nullary forms.
    [x] 30.5.  Parser — instruction list: statements separated by newlines,
               an AST Sequence node wrapping them in order.
    [x] 30.6.  AST eval — Assign sets the store and yields its value; Var
               gets from the store (error if unset); Sequence evaluates each
               in turn and returns the last result.
    [x] 30.7.  TESTS — parse + evaluate assignment, reference, unknown-name
               error, and a multi-line program; per-mode it's value passthrough
               (the store holds Values verbatim, no mode/scale of its own).


[x] 31.  SOLVER TOOL  (a new MCP tool that finds the value of one variable that
         drives a calculate-language expression to zero (solve) or to an extremum
         (optimise), over a required bracket, reusing the parser/evaluator).
         Single-variable first; multivariate is a later item.
    [x] 31.1.  New MCP tool `solver(expression, variable, lower, upper, goal=None,
               type=None, mode="fixed-point", min_fixed_point_precision=None)`.
               Same expression language as calculate, including multi-line programs
               whose assignments set the constants.
    [x] 31.2.  `type` (optional) in {solve, optimise}; inferred when omitted — goal
               present -> optimise, goal absent -> solve. If supplied, validate
               against goal (optimise requires a goal; solve forbids one).
    [x] 31.3.  Reuse calculate's front-end for `mode` / `min_fixed_point_precision`:
               factor the mode-resolve + precision-validation out of
               _evaluate_request into a shared helper used by both tools (same
               resolution, validation, error messages).
    [x] 31.4.  Parse the expression once (parser.parse). The named `variable` is the
               unknown; every other name is a constant set by an assignment in the
               program; an unset non-unknown name -> EvalError. Validate via an AST
               walk that `variable` occurs as a reference and is not an assignment
               target.
    [x] 31.5.  Objective from `goal`: minimise -> expr; maximise -> -expr; no goal
               (solve) -> |expr|, with a final acceptance check |expr| <= tol (else
               no-solution). Accept min/max/American spellings as aliases.
    [x] 31.6.  `variable` (single name) with required `lower`, `upper` defining the
               bracket; require lower < upper and that the variable occurs in the
               expression.
    [x] 31.7.  Engine — golden-section search on [lower, upper], one minimizer for
               all cases. Each candidate x is materialised as a Value in the active
               mode (quantised to precision) and bound into a seeded VariableStore;
               the program is evaluated and its Value reduced to a float to drive the
               search. Candidate eval failures are penalised (+inf) so the search
               steers away. Stop when interval width < tolerance or at a
               max-iteration cap; tolerance defaults from the mode (fixed-point:
               10^-precision; float: small epsilon).
    [x] 31.8.  Reply shape — {variable, solution (+ solution_hex_dump), value (the
               expression's value at the solution, + value_hex_dump), mode, exact,
               precision, goal, type, iterations, error}, annotated like calculate
               (solutions marked approximate / within-tolerance, honest about
               inexactness). Error path sets the value fields to null with an `error`
               string; "no solution" when solve cannot reach zero in the bracket
               (report the closest |expr|).
    [x] 31.9.  TESTS — per type/mode: a solvable root case (e.g. x**2 - 2 over [0,2]
               -> sqrt 2), an optimise case (min/max of a unimodal expr), a
               constants-via-assignment case, and the error cases (variable not in
               expression, lower >= upper, unknown mode, min_fixed_point_precision
               misuse, no-solution). In-process (test_solver.py) + over-the-wire.
    [x] 31.10. DOCS — solver tool docstring; a `solver` help section (reference.py
               _SECTIONS); README "What it gives you" bullet.


[x] 32.  SOLVER VOCABULARY — make the reported strategy self-describing and report
         the algorithm used (supersedes 31's `type`/`goal` naming). The solver
         always "solves", so `type: solve` says nothing; split WHAT the search
         looked for from HOW it searched.
    [x] 32.1.  Field `objective` in {find-root, find-minimum, find-maximum}
               replaces the `type`/`goal` pair, as both the (optional) argument and
               the reported value: find-minimum / find-maximum carry the direction
               the old `goal` held, find-root is the former solve. One field names
               the whole intent. When omitted it still defaults to find-root.
    [x] 32.2.  Drop the `goal` argument. Accept the old spellings as never-surfaced
               aliases resolving to the canonical objective (the 23.6 alias rule):
               solve -> find-root; optimise+minimise/minimize/min -> find-minimum;
               optimise+maximise/maximize/max -> find-maximum. Fold
               resolve_type/resolve_goal/objective into one objective resolver;
               reword the type/goal disagreement errors to the new vocabulary.
    [x] 32.3.  New reply field `algorithm` reporting the search method —
               "golden-section search" (the one engine today, including the fixed-
               point / rational grid-polish pass). Future-proofs the reply for a
               second algorithm.
    [x] 32.4.  Propagate everywhere: SolverResult + _solver_reply / _solver_error
               (server.py), the solver docstring + `solver` help section
               (reference.py), the README bullet, and the test fixtures / expected
               replies (test_solver.py + over-the-wire). The `type` and `goal` keys
               leave the reply; `objective` and `algorithm` take their place.


[ ] 33.  MORE SOLVER ALGORITHMS — candidates beyond golden-section search,
         each reported via the `algorithm` field (32.3).
    [ ] 33.1.  Bisection — bracketed root, sign change, robust.
    [ ] 33.2.  Brent's method — root, bisection + secant/inverse-quad.
    [ ] 33.3.  Secant — root, no derivative, superlinear.
    [ ] 33.4.  Newton-Raphson — root via symbolic derivative.
    [ ] 33.5.  Ridders' — bracketed root, exponential fit.
    [ ] 33.6.  Ternary search — unimodal optimise alternative.
    [ ] 33.7.  Chandrupatla's — bracketed root, often beats Brent.
    [ ] 33.8.  Halley's / Householder — root, cubic+ convergence.
    [ ] 33.9.  Durand-Kerner / Aberth — all polynomial roots at once.
    [ ] 33.10. Sturm sequences — real-root isolation/counting.
    [ ] 33.11. Companion-matrix eigenvalues — polynomial roots.
    [x] 33.12. Brent (parabolic) minimise — derivative-free optimise.
    [ ] 33.13. Newton/BFGS — gradient optimise via symbolic deriv.
    [x] 33.14. Nelder-Mead — derivative-free, multivariate-ready.
               Maintain a simplex of n+1 vertices over the variables and walk it
               downhill with reflect / expand / contract / shrink moves on the bare
               expression evaluator — no gradient needed. Seed the simplex from the
               initial guess (per-axis steps), and reuse the 2-second wall-clock and
               tolerance machinery already wrapping golden-section.
    [ ] 33.15. Differential evolution / annealing — global, non-unimodal.
    [ ] 33.16. Newton (systems) — multivariate root via Jacobian. DEFERRED:
               a true system is n equations in n unknowns (a square Jacobian to
               invert, J·Δ = −F), but the solver drives ONE scalar expression —
               1 equation in n unknowns, no square system to solve. Implement
               only if/when the expression language can express MULTIPLE
               equations (a vector of residuals); the same prerequisite gates
               33.17 (Broyden's) and the other square-system root methods below.
    [ ] 33.17. Broyden's — multivariate root, quasi-Newton, no Jacobian.
    [ ] 33.18. Powell's — derivative-free conjugate directions.
    [ ] 33.19. Conjugate gradient — large-scale gradient optimise.
    [ ] 33.20. L-BFGS(-B) — limited-memory quasi-Newton, bounds.
    [ ] 33.21. Gauss-Newton / Levenberg-Marquardt — nonlinear least squares.
               DEFERRED: least squares fits m residuals over a dataset (minimise
               Σ rᵢ², stepping with the m×n Jacobian, (JᵀJ + λI)·Δ = −Jᵀr), but
               the solver drives ONE scalar expression — a single residual, no
               sum to minimise. Implement only if/when the expression language
               can express a VECTOR of residuals over supplied observations (a
               data-fitting harness) — a larger prerequisite than the
               multi-equation support 33.16/33.17 need.
    [ ] 33.22. Trust-region (dogleg / Newton-CG) — robust step control.
    [ ] 33.23. SLSQP / interior-point — constrained optimise.
    [ ] 33.24. CMA-ES / particle swarm — global, derivative-free.


[ ] 34.  ROUNDING MODES  (web research 2026-06-14: explicit rounding is a named
         top-3 financial-error class for LLM callers — "never round intermediates,
         round only at display, by a system-defined rule". Today fixed-point/float
         round implicitly by ONE policy; expose the policy so the caller can match
         the rule the production code actually uses, and so the precision verdict
         can name WHICH rounding produced an inexact result. Fits the type-faithful
         thesis: don't just round, round the way the target system rounds.)
    [ ] 34.1.  Enumerate the modes: half-up, half-even (banker's), half-down,
               toward-zero (truncate), away-from-zero, floor, ceil — the IEEE-754 /
               decimal.ROUND_* set. Name them in one place; reuse decimal's where
               the engine already leans on it.
    [ ] 34.2.  Surface as an optional `rounding` argument on `calculate` (and
               wherever fixed-point quantisation happens), defaulting to today's
               behaviour so nothing breaks. Banker's-rounding (half-even) is the
               financial default worth recommending in the docstring.
    [ ] 34.3.  Thread the chosen mode through the per-mode quantisation chokepoint
               (the _from_scaled_int / `/` path, cf. 28.4) so EVERY rounding op in
               the expression obeys it, and name the mode in the `precision` verdict
               ("inexact, rounded half-even to N decimals") so the answer stays
               self-describing.
    [ ] 34.4.  HELP + README: add a `rounding` note to the relevant help section
               and the calculate reply enrichment (27); a fixtures pass like 32.4.
    [ ] 34.5.  ROUND ONLY AT DISPLAY (fixed-point) — decided this session: fixed-
               point must STOP rounding intermediates and quantise ONCE, at the
               final render. Today it rounds per-op (every `/`, product,
               transcendental snaps to the covering scale via _fp_quantize), so
               1/3+1/3+1/3 @ scale 2 = 0.99; deferred rounding makes it 1.00
               (exact). Bonus: one final bounded rounding — not a compounding
               per-op chain whose bound is murky — is what makes the 35.1.2 error
               magnitude cleanly computable.


[ ] 35.  EXACT / INEXACT CALCULATIONS  (split from 34 this session, and independent
         of it: whether a result is exact is a property of the MATH — the operands
         and operations — not of any rounding mode. Every value already carries a
         binary exact/inexact flag; this section is about the RICHER information we
         can collect about exactness DURING the calculation, and what to do with it.)
    [ ] 35.1.  INFORMATION WE CAN COLLECT — the binary exact/inexact flag hides
               things a caller needs, all KNOWABLE at compute time:
        [ ] 35.1.1.  WHICH KIND of inexact — three fates the binary flag can't tell
                     apart: resolution-limited (terminating decimal, a larger scale
                     makes it exact: 1.5*1.5 @1 inexact -> 2.25 @2 exact); non-
                     terminating rational (no finite scale ever works: 100/3, but
                     EXACT in rational mode); irrational (no finite form at all:
                     2**0.5). Decidable: evaluate in rational mode — exact =>
                     rational, then it terminates iff the reduced denominator's only
                     prime factors are 2 and 5; refuses => irrational. So the verdict
                     could name the class.
        [ ] 35.1.2.  HOW inexact — the error magnitude. _fp_quantize already computes
                     the exact remainder r/den and discards it, keeping only
                     `lossless`; report the residual (or its bound — each fixed-point
                     rounding is within 1/2 ULP = 0.5*10**-scale at the result scale)
                     instead of just False. This IS "how many decimals are
                     trustworthy".
                     CAVEAT (cross-type): the residual is a RATIONAL, not a fixed-
                     point number — Fraction(stored) - Fraction(true), computed in
                     exact rational arithmetic. It must be: the true value (e.g.
                     100/3) is often unrepresentable in fixed-point at any scale, so
                     its distance isn't either; rendering the error in fixed-point
                     would round it ("the error of the rounding, rounded"). So the
                     CALCULATION stays fixed-point while the error DIAGNOSTIC borrows
                     rational — same as the 35.1.1 oracle. First step already landed:
                     Value.error + the analyze-tree "error <frac> ≈ <dec>" fragment,
                     set at _fp_value.
        [ ] 35.1.3.  WHERE the inexactness entered — name the op/operand that first
                     made the result inexact, so an inexact verdict points at its
                     cause instead of just flagging the whole expression.
    [x] 35.2.  INEXACT-HANDLING ENUM — introduce a new enum, supplied by the CALLER,
               carried through the evaluation context and threaded all the way down
               into the calculation code, that selects what happens when an operation
               is inexact. Defaults to 35.1 so nothing breaks. Sub-points are its
               possible values:
        [x] 35.2.1.  CONTINUE-AND-REPORT (default) — the 35.1 behaviour: compute,
                     never reject, surface the verdict.
        [x] 35.2.2.  ABORT ON INEXACT — the moment any op is detected inexact, we are
                     in the code that knows EVERYTHING about that inexactness (its
                     kind 35.1.1, magnitude 35.1.2, and site 35.1.3). Compose a smart
                     error message from all of it and raise it like an exception,
                     unwinding the whole calculation. The caller — who explicitly
                     asked for this mode — then learns precisely what went inexact,
                     by how much, and where, rather than a bare failure.

    [ ] 35.3.  INEXACT ABORT MESSAGES — tested in tests/test_inexact_fixed_point.py.
        [x] 35.3.1.  The first part should always look the same: "Inexact
                     calculation in line X: 1.00 / 3.00 = 1.33 is not exact."
                     The numbers are NOT lexemes, they are the VALUES: the user
                     may have written 1 / 3, but we show 1.00 and 3.00 with the
                     active precision.
        [x] 35.3.2.  The second part of the message: a new line, the first hint
                     " - Pass inexact_handling='xxx' to enable inexact
                     calculations."
        [ ] 35.3.3.  The error hint, only if we know the error: " - The error
                     was xxxxx, maybe raising min_precision..."
        [ ] 35.3.4.  If we know the given function can't be precisely calculated:
                     " - The sin(x) function can not be exactly calculated."
        [ ] 35.3.5.  If we know the rational mode would be exact we can add a new
                     hint: " - In rational mode this calculation would be exact
                     (the exact value)."


[ ] 36.  FINANCIAL HELPERS  (web research 2026-06-14: money is high-demand and
         partially served by fixed-point already, but the NAMED error classes are
         basis-point confusion (25 bps read as 25%) and rate-period confusion (an
         annual rate applied monthly). A handful of named functions kill those
         classes directly and lean on the money-safe fixed-point story. Each is the
         two-step function add of 28 unless flagged; all COMPUTE, so they follow
         the active mode's `/` rule and rounding mode (34).)
    [ ] 36.1.  pct(x, p) / pct_change(old, new) — p percent of x, and the signed
               relative change (new-old)/old. The everyday percentage op LLMs get
               wrong; explicit so the caller never hand-rolls /100.
    [ ] 36.2.  bps(x, b) — b basis points of x (b/10000 * x). Exists SOLELY to make
               the bps-vs-percent distinction un-confusable at the call site.
    [ ] 36.3.  compound(principal, rate, periods) and the rate-period helpers —
               make the period explicit so an annual rate can't silently act
               monthly; document that `rate` is per-period and `periods` counts the
               same unit.
    [ ] 36.4.  pmt / fv / pv (amortisation: payment, future value, present value)
               — the standard time-value-of-money trio, each a closed-form formula
               over the operand evaluator; the amortisation case named in the
               research. May need an internal pow over the active type (have it).


[ ] 37.  UNIT CONVERSION  (web research 2026-06-14: comprehensive competitors lead
         with "158 conversions / 15 categories" as a headline, and LLMs show error
         rates up to ~20% on large units. We have NONE. Add it the type-faithful
         way — a conversion is exact-rational where the factor is rational
         (in/cm, lb/kg defined factors) and inexact only where it isn't, so the
         verdict still tells the truth, unlike a bare float table.)
    [ ] 37.1.  Decide the surface: a `convert` tool (value, from-unit, to-unit) vs
               a convert(x, "from", "to") function in the language. Lean tool — the
               unit names are strings, not numeric literals, and a tool keeps the
               grammar clean.
    [ ] 37.2.  Seed the high-value categories first: length, mass, time, volume,
               temperature (affine — offset + scale, not a bare factor), data
               (KiB/KB binary-vs-decimal — a known LLM trap), angle. Store factors
               as exact rationals so the conversion is exact where the definition is.
    [ ] 37.3.  Convert via the base unit: store each unit as an exact rational
               ratio to a per-category base (inch = 25.4 mm, metre = 1000 mm, ...)
               and compute as ONE composed rational op (value * from_ratio /
               to_ratio), not round-to-base-then-round-to-target — two rounded
               steps can lose exactness even when the true result is exact.
               Temperature is affine (scale*x + offset, e.g. F = C*9/5 + 32), still
               exact-rational. Reuse the active numeric type so results compose with
               the rest of the engine.
    [ ] 37.3a.  Exact/inexact verdict + printing. A rational p/q (lowest terms) has
               a finite decimal iff q factors into only 2s and 5s. Three cases:
               (a) terminates -> print all digits, verdict "exact" (5 in -> 12.7 cm;
               1 lb -> 0.45359237 kg). (b) non-terminating rational -> exact as a
               fraction but infinite as decimal; print rounded decimal with "≈",
               verdict "inexact", and expose the exact fraction on request
               (10 cm -> 500/127 in). (c) irrational -> always inexact decimal; only
               angle (deg<->rad via π) hits this in the seeded set. For (b)/(c)
               default precision reuses the engine's display-precision setting,
               don't invent a conversion-specific knob.
    [ ] 37.4.  HELP section listing categories + units; README bullet; fixtures.
    [ ] 37.5.  Distance units: millimeter (mm), centimeter (cm), metre (m),
               kilometre (km), inch (in), foot (ft), yard (yd), mile (mi)
    [ ] 37.6.  Temperature units: Celsius (°C, C), Fahrenheit (°F, F),
               Kelvin (K, kelvin)
    [ ] 37.7.  Speed units: metres per second (m/s, mps), kilometres per hour
               (km/h, kph), miles per hour (mph), knots (kn, knot)
    [ ] 37.8.  Area units: square metre (sq m, m²), square kilometre (sq km, km²),
               square foot (sq ft, ft²), square mile (sq mi, mi²), acre (ac, acre),
               hectare (ha, hectare)
    [ ] 37.9.  Mass units: gram (g, gram), kilogram (kg, kilogram), ounce (oz, ounce),
               pound (lb, lbs, pound), stone (st, stone)
    [ ] 37.10.  Volume units: millilitre (ml, millilitre), litre (l, litre),
               teaspoon (tsp, teaspoon), tablespoon (tbsp, tablespoon), cup (cup),
               fluid ounce (fl oz, floz), quart (qt, quart), gallon (gal, gallon)
    [ ] 37.11.  Time units: second (s, sec, second), minute (min, minute),
               hour (h, hr, hour), day (d, day)
    [ ] 37.12.  Energy units: joule (J, joule), kilojoule (kJ, kilojoule),
               calorie (cal, calorie), kilocalorie (kcal, kilocalorie),
               kilowatt-hour (kWh, kilowatt-hour), BTU (btu, british thermal unit)
    [ ] 37.13.  Power units: watt (W, watt), kilowatt (kW, kilowatt),
               horsepower (hp, horsepower)
    [ ] 37.14.  Pressure units: pascal (Pa, pascal), kilopascal (kPa, kilopascal),
               bar (bar), PSI (psi), atmosphere (atm, atmosphere)
    [ ] 37.15.  Force units: newton (N, newton), pound-force (lbf),
               kilogram-force (kgf, kilogram-force)
    [ ] 37.16.  Data units: bit (bit), byte (B, byte), kilobyte (KB, kilobyte),
               megabyte (MB, megabyte), gigabyte (GB, gigabyte), terabyte (TB, terabyte)
    [ ] 37.17.  Fuel economy units: MPG US (mpg, miles per gallon),
               MPG Imperial (mpg imp, miles per gallon (imperial)),
               L/100km (l/100km, liters per 100 km), km/L (km/l, kilometers per liter)
    [ ] 37.18.  Angle units: degree (deg, °, degree), radian (rad, radian)
    [ ] 37.19.  Frequency units: hertz (Hz, hertz), kilohertz (kHz, kilohertz),
               megahertz (MHz, megahertz), gigahertz (GHz, gigahertz)


[ ] 38.  HUMAN-SCALE NUMBER INGESTION  (web research 2026-06-14: the input side of
         the same failures — "3.2 billion", "25 bps", US-vs-EU decimal (12.345 as
         decimal vs thousands), and date tokens (Q3 2024 vs 2024-09-15) are named
         error classes because the model mis-tokenises them BEFORE any math. Let
         the caller hand us the messy human string and get back a trustworthy
         typed value, rather than pre-parsing it wrong itself. Scope carefully —
         our strict grammar (LANGUAGE help) is a feature; this is a deliberate,
         opt-in lenient front door, NOT a loosening of the core literal rules.)
    [ ] 38.1.  Magnitude words: parse a trailing k/M/B/bn/billion/trillion suffix
               into the exact scaled value (3.2 billion -> 3200000000), in the
               active type. The single highest-value item — the ~20% large-unit
               error class.
    [ ] 38.2.  Locale-aware decimal/grouping: an explicit US vs EU mode (or a
               grouping/decimal-separator pair) so "12.345" and "12,345" are never
               guessed. Default to today's strict US form; this is an opt-in arg,
               never silent auto-detection (auto-detect is the bug, not the fix).
    [ ] 38.3.  Percent / bps literal sugar on input ("25%", "25 bps") resolving to
               the same values as 36.1/36.2 — input-side mirror of the financial
               helpers.
    [ ] 38.4.  DATE/TIME ARITHMETIC (we have only time(), 28.1) — parse common date
               forms to an epoch/ordinal so date differences and offsets are
               computable; normalise Q3 2024 / Sept 2024 / 2024-09-15 to one
               representation. Larger than 38.1-38.3; may warrant its own section
               once scoped. Keep exact (a date is exactly rational, cf. 28.1).
    [ ] 38.5.  KEEP IT OPT-IN: every lenient parse is a named mode/argument, the
               result still carries its exact/inexact verdict, and the strict
               grammar stays the default so existing callers are unaffected.


[x] 39.  SOLVER REQUIRES min_fixed_point_precision IN FIXED-POINT MODE  (decided
         2026-06-16. Root cause of a real failure: a caller invokes `solver` with the
         default fixed-point mode and no precision, so the search variable is floored
         to scale 0 — integer-only. The bracket cannot resolve a non-integer root, so
         golden-section does 0 useful iterations and returns a silently WRONG integer
         guess, e.g. x**2 - 2 over [1,2] -> "x = 1", value "-1 (exact)", error null —
         no failure surfaced. The fix is to make the precision mandatory exactly where
         it is needed.)
    [x] 39.1.  Keep fixed-point as the solver's default mode, but REQUIRE
               min_fixed_point_precision when the mode is fixed-point: error if it is
               missing, rather than silently searching at scale 0. The error names the
               argument (and can point at floating-point as the alternative).
    [x] 39.2.  Other modes are unaffected: floating-point and rational have sub-unit
               resolution natively and need no precision, so the argument stays
               OPTIONAL (and, as today, invalid) outside fixed-point mode. `calculate`
               and `analyze` are unchanged — this requirement is solver-only.
    [x] 39.3.  STILL TO PLAN: exact error wording; whether to suggest a concrete
               precision or steer to floating-point; tests for the fixed-point-without-
               precision refusal and that float/rational still solve precision-free.


[ ] 40.  TIER 2 FUNCTIONS
    [x] 40.1.  atan2(y, x) — correct-quadrant angle; atan alone can't
               disambiguate.
    [x] 40.2.  sinh, cosh, tanh — the hyperbolic functions, each a UNARY
               operand-method (the sqrt/sin/exp shape). All reduce to exp
               (28.27): sinh(x) = (e**x - e**-x)/2, cosh(x) = (e**x + e**-x)/2,
               tanh(x) = (e**(2x) - 1)/(e**(2x) + 1). Transcendental, so the
               exp/log per-mode stance: float inexact; fixed-point inexact via
               the exp core except the exact landmarks (sinh(0)=0, cosh(0)=1,
               tanh(0)=0); rational refuses non-zero (exp of a non-zero rational
               is transcendental).
    [x] 40.3.  asinh, acosh, atanh — the inverse hyperbolics, UNARY, each
               reducing to log/sqrt (28.17/19.5.2): asinh(x) = ln(x +
               sqrt(x**2 + 1)), all reals; acosh(x) = ln(x + sqrt(x**2 - 1)),
               DOMAIN x >= 1 (else refuse, like sqrt's negative); atanh(x) =
               ln((1 + x)/(1 - x))/2, DOMAIN |x| < 1 (x = +/-1 is +/-inf). NB the
               internal _fp_atanh_series already exists (ln's core, 28.17.1) —
               the public atanh goes through ln, it does NOT re-expose the series.
               Transcendental: inexact except the zero landmarks, rational
               refuses non-zero.
    [x] 40.4.  factorial(n) — UNARY. n! for a NON-NEGATIVE INTEGER n, EXACT in
               every mode (a product of integers, no rounding). Refuses a negative
               or non-integer operand (that is the gamma extension, 40.4.1) and
               caps n so a huge operand cannot blow up. The common case is the
               exact integer path (combinatorics, comb/perm below).
        [ ] 40.4.1.  gamma(x) — the continuous factorial, n! = gamma(n+1):
                     transcendental (float/fixed-point inexact, rational refuses
                     like exp), with POLES at zero and the negative integers
                     (refuse there in every mode). Also extends factorial() to
                     negative / non-integer operands via gamma(n+1).
    [ ] 40.5.  comb(n, k) — binomial coefficient n!/(k!(n-k)!), the BINARY
               fixed-arity-2 shape (pow/atan2, 28.20/40.1). EXACT for integer
               n >= k >= 0 in every mode (cancel to an integer; compute the
               product form, not three factorials); k < 0 or k > n is 0. A
               non-integer argument is the gamma-generalized coefficient
               (inexact/refuse, per 40.4).
    [ ] 40.6.  perm(n, k) — falling factorial n!/(n-k)! (k-permutations of n),
               BINARY fixed-arity 2 like comb. EXACT for integer n >= k >= 0
               (a product of k consecutive integers); k < 0 or k > n is 0.
               Non-integer arguments go through gamma (inexact/refuse, per 40.4).
    [x] 40.7.  gcd(...) — greatest common divisor, VARIADIC (the sum/max shape,
               math.gcd), EXACT in every mode (pure integer math, no rounding).
               DOMAIN integer-valued operands only — a non-integer (fixed-point
               with a fractional part, rational with denominator != 1, a float
               that is not whole) refuses, the exact-or-refuse stance. Sign is
               dropped (gcd works on magnitudes); gcd(0, 0) = 0.
    [x] 40.8.  lcm(...) — least common multiple, VARIADIC like gcd, EXACT in
               every mode via lcm(a, b) = |a*b| / gcd(a, b) folded across the
               operands. Same integer-only DOMAIN and refusal as gcd; any zero
               operand makes lcm 0.
    [ ] 40.9.  sign(x) — signum, UNARY: -1, 0, or +1 by the operand's sign.
               EXACT in every mode (a comparison to zero, not arithmetic, so it
               works on ANY value, integer or not — unlike gcd/lcm). Result type
               follows the mode (the fixed-point/rational/float spelling of the
               three values). DECIDE whether an inexact operand yields an exact
               sign (the classification is certain) or carries the flag like abs.
    [x] 40.10. log(x, base) — general logarithm log(x)/log(base), the TWO-ARG
               form flagged in the 28.17-28.19 naming note (today only the unary
               natural log and log10/log2 exist). Reuses the _fp_ln core (28.17.1)
               for both x and the base; exact only where the quotient lands on the
               grid (e.g. log(8, 2) = 3, log(1, base) = 0), else inexact, rational
               refuses, and base <= 0 or base = 1 refuses alongside x <= 0.
               DESIGN: this OVERLOADS the existing unary `log` to arity (1, 2) —
               log(x) stays natural log, log(x, base) takes the base. The current
               _arity_of (22.2) counts every positional param as required, so it
               cannot express (1, 2) from a `def log(self, base=None)` default;
               either teach _arity_of to honour defaults (POSITIONAL_OR_KEYWORD
               with a default -> optional) or special-case the registry. Settle
               that before implementing — it is the one piece here that touches the
               call machinery, not just a new Value method.
    [x] 40.11. degrees(x) / radians(x) — angle unit conversion, each UNARY:
               degrees(x) = x * 180/pi, radians(x) = x * pi/180 (the trig family
               is radians-only, 28.10, so these bridge to/from degrees). Both
               multiply by an irrational (pi), so the per-pi stance: float inexact;
               fixed-point inexact via the _pi_scaled core (29.3) except the
               trivial degrees(0) = radians(0) = 0; rational refuses non-zero (pi
               has no rational value). NB this is unit scaling, NOT a trig function
               — no domain limit, every real converts.
    [ ] 40.12. percentile(p, ...) / quantile(q, ...) — the value at rank p (0-100)
               or q (0-1) of the operands, the order-statistic generalization of
               median (28.7, which is the 50th percentile). A leading point arg +
               the data set: arity (2, None) (the point plus >= 1 datum). EXACT
               when the rank lands on a datum (like median's odd case); otherwise
               INTERPOLATED between the two nearest (linear, the common default),
               so it may round in fixed-point / be inexact in float, and rational
               stays exact (rational interpolation is a Fraction). DECIDE the
               interpolation convention and whether percentile and quantile are one
               core under two scalings.
    [ ] 40.13. covariance — population covariance mean((x-mx)*(y-my)) over PAIRED
               series x and y. Follows variance's stance (28.8): no sqrt, so EXACT
               in rational, may round in fixed-point/float. BLOCKER: needs TWO
               vectors, which the current flat-variadic call shape cannot express
               (a single operand list, cf. the multi-residual gap noted at 33.7/
               33.8). Settle a paired/multi-vector call shape FIRST (interleaved
               pairs, or a real list type); until then this and 40.14 cannot land.
    [ ] 40.14. correlation — Pearson r = cov(x, y)/(stddev(x)*stddev(y)), range
               [-1, 1], over PAIRED series. Inherits stddev's sqrt (28.9): always
               inexact in float/fixed-point, rational refuses (irrational root).
               Same two-vector BLOCKER as covariance (40.13) — depends on the same
               call-shape decision.
    [ ] 40.15. geometric mean — geomean(...) = (x1*x2*...*xn)**(1/n), the n-th root
               of the product, VARIADIC (the avg shape). DOMAIN non-negative
               operands (a negative makes an even root complex — refuse, like
               sqrt). Irrational in general: inexact in float/fixed-point (via the
               exp/log core or _iroot, 28.20.1/28.27), rational exact ONLY for a
               perfect n-th power. geomean(0, ...) = 0.
    [ ] 40.16. harmonic mean — harmean(...) = n / sum(1/xi), VARIADIC. DOMAIN
               positive operands (a zero makes a term undefined — ZeroDivisionError;
               mixed signs are ill-defined, refuse). No sqrt, so it follows avg's
               division stance: EXACT in rational, may round in fixed-point/float.
    [x] 40.17. diff(expr, var, at) — numerical derivative of an expression w.r.t. a
               named variable, evaluated at a point. Like the solver, `expr`/`var`
               are an unevaluated program + a free variable NAME, not values — so it
               is solver-adjacent, not an ordinary 40.x operand-method. Inherently
               inexact in every mode (a finite-difference quotient).
    [x] 40.18. integral(expr, var, a, b) — definite integral of an expression w.r.t.
               a named variable over [a, b]. Same solver-adjacent shape as diff
               (40.17): `expr`/`var` are an unevaluated program + a free variable
               NAME, not values. Inherently inexact in every mode (a numerical
               quadrature).
    [ ] 40.19. sum(i, 1, n, expr) / product(i, 1, n, expr) — range summation (Σ) and
               product (Π): fold `expr` over the index variable `i` from 1 to n.
               Solver-adjacent shape like diff/integral (40.17/40.18) — `expr`/`i`
               are an unevaluated program + a free variable NAME, not values — but a
               FINITE EXACT fold over integer steps, so unlike diff/integral it keeps
               the per-mode exactness of the body (no truncation of its own). NB the
               name collides with the existing variadic sum(...) (28.x).
    [ ] 40.20. hypot(x, ...) — Euclidean norm sqrt(x1**2 + x2**2 + ...), VARIADIC
               (the sum shape), an ordinary value-taking operand-method (not
               solver-adjacent). Every real allowed. Inherits sqrt's stance (19.5.2):
               inexact in float/fixed-point, rational exact only for a perfect square.
    [ ] 40.21. clamp(x, lo, hi) — constrain x to [lo, hi] = min(hi, max(lo, x)),
               TERNARY fixed-arity 3. EXACT in every mode (comparison + selection, no
               arithmetic — the sign(x) stance, 40.9), works on any value. DOMAIN
               lo <= hi (else refuse).
    [ ] 40.22. lerp(a, b, t) — linear interpolation a + (b - a)*t, TERNARY fixed-arity
               3. Plain arithmetic (no sqrt), so the avg/division stance: EXACT in
               rational, may round in fixed-point/float. t unrestricted (t outside
               [0, 1] extrapolates).


[ ] 41.  GLAMA TOOL-DEFINITION-QUALITY FIXES  (audit 2026-06-17. Root cause:
         schema coverage 0% on every tool — params are bare signature args, no
         Field/Annotated/Literal in src/. This is Glama's top deduction.)
    [x] 41.1.  Add Annotated[T, Field(description=...)] to every tool param —
               calculate (server.py:308-313), analyze (462-464), solver (508-518),
               help (54). Takes coverage 0%->100%, no behaviour change. Biggest lever.
    [x] 41.2.  Make help's `section` a Literal["types","language","functions",
               "solver"] enum (server.py:54), mirroring reference._SECTIONS; add a
               test asserting the two stay in sync.
    [x] 41.3.  Add an MCP server instructions= string at server.py:37 (currently
               bare FastMCP("mcp-abacus")); bytesmith/tmux/molecules all set one.
               Lift from README lede (three modes, exact/inexact verdict).
    [x] 41.4.  Fix pyproject.toml:8 description — claims five regimes ("fixed-width
               int … decimal … rational"); only three modes exist (fixed-point,
               floating-point, rational). Contradicts engine + README.
    [x] 41.5.  Add a "when to use vs siblings" sentence to calculate's docstring;
               cross-refs are one-directional (analyze/solver point to calculate,
               not back).
    [x] 41.6.  Encode solver's single (variable+lower+upper) vs multiple (variables)
               form coupling in the param descriptions; schema shows them as
               independent optionals (no oneOf).
    [x] 41.7.  Drop or document info()'s always-empty "toolsets": [] (server.py:49)
               until the SA.1 gating lands.
    [x] 41.8.  Consider exposing the help reference as an MCP resource (e.g.
               abacus://reference/{section}); Glama introspection probes
               resources/prompts and none are exposed today.
    [x] 41.9.  Add classifiers to pyproject.toml (License, Python versions, Topic)
               for metadata completeness.
    [x] 41.10. Document analyze's per-node `· error <residual> ≈ <approx>` segment.
               describe() (value.py:2555-2556) appends it for every inexact
               fixed-point node, but analyze's return-shape docstring (server.py:617-
               620) lists only the hex / ≈ fragments — the residual is undocumented.
               Worse, "error" collides with the reply's `error` failure channel, so a
               reader may misread `· error -1/2` as "this node failed". Document it and
               consider renaming the fragment (residual / rounding).
    [x] 41.11. Document (or trim) the unadvertised mode aliases. resolve_mode accepts
               decimal->fixed-point, float/ieee754->floating-point, fraction/frac->
               rational (value.py:134-142), but every tool description advertises only
               float64/double. decimal->fixed-point is the trap: a caller expecting
               decimal-FLOAT silently gets fixed-point, no error to learn from (the
               resolved-name echo is the only tell). List the alias set or drop the
               misleading ones.
    [x] 41.12. Add "rational" to pyproject keywords (pyproject.toml:16). It is one of
               the three headline modes yet absent from the keyword list — a
               discoverability gap against the description's own three-mode claim.
    [x] 41.13. Make the tool cross-references bidirectional. calculate points to BOTH
               analyze and solver, but analyze mentions only calculate (never solver)
               and solver mentions only calculate/help (never analyze). An LLM that
               lands on analyze or solver is never steered to the sibling diagnostic.
    [x] 41.14. Fix the stale value.py:110 comment — FLOATING_POINT is labelled "the
               default (19.1.1)", left from when it was the first mode built; the
               user-facing default is fixed-point everywhere. Engine-internal (cannot
               mislead a client), cosmetic only.

[ ] 42.  SOLVER: compare candidate objectives in-mode, not via to_float(). The
         folded objective is a mode-faithful Value but every engine reduces it with
         .to_float() to drive the search (solver.py:384/520/704), capping the
         DECISION at ~16 significant digits. Fails on large-offset/flat-near-solution
         objectives (e.g. 1e6+(x-3)^2): variation below the offset's last mantissa
         digit vanishes and the search stalls. Use Value._compare (already same-mode,
         cheap); needs a "worse-than-any-Value" sentinel for the +inf domain penalty
         and Value forms of best_obj (residual test + message). NOTE: only lifts the
         decision ceiling — positions/bracket/tolerances stay float, so sub-float
         resolution needs the search geometry in exact arithmetic too (bigger change).


[ ] 43.  Help the AI to send acceptable JSON requests.  (LLMs sometimes send
         invalid JSON strings to the MCP server; help them succeed instead of
         failing the call.)
    [x] 43.1.  Tolerant-parse / JSON-repair fallback. When strict json.loads()
               throws, retry once through a lenient parser before giving up. The
               classic offenders an LLM produces:
                 - stringified / double-encoded arguments: a nested object (or the
                   whole args blob) sent as a JSON STRING instead of an object,
                   e.g. "arguments": "{\"variable\": \"n\"}". RESEARCH (2026-06-18)
                   says this is the MOST common offender in the wild, not the three
                   below (FastMCP #932, continue #5590, opencode #8102). Fix it
                   the way the community does: at the PARAM-MODEL boundary, accept
                   `dict | str` and json.loads() if it arrives as a string. This
                   also answers the 43-wide interception-point question — the
                   repair lives at the arg boundary, NOT inside the tool body,
                   because FastMCP parses args before our code runs (server.py
                   has no json.loads today).
                 - unquoted barewords ("variable": n -> "n")
                 - single quotes instead of double
                 - trailing commas
               Library option: json-repair (pure Python, zero deps, built for
               LLM output) covers all of the above incl. barewords; json5 does
               NOT (rejects bareword VALUES). Tradeoff vs. a ~30-line hand-rolled
               preprocessor is the project's mcp+stdlib-only stance (2.4/16.2.3).
    [x] 43.2.  Make the JSON parse error message actionable (so a model
               self-corrects instead of looping): be as specific as possible in
               the JSON parse error message. Best-practice refinements (research
               2026-06-18, MCPcat error-handling guide + MCP fault taxonomy
               arxiv 2603.05637):
                 - split the JSON-RPC codes: -32700 (parse error, "fix request
                   format") vs -32602 (invalid params) — distinct signals.
                 - name the exact field + expected-vs-received, e.g. "Argument
                   'quantity' expected an integer, but received 'five'"; never a
                   raw stack trace.
                 - return isError=True with descriptive TextContent so the model
                   gets the message instead of the loop crashing; validate early.
    [ ] 43.3.  Simplify the solver calls. For single-variable solves, if
               `variable` is omitted, auto-detect the sole free name in the
               expression (12*n - (450 + 3*n) has exactly one). This won't fix
               malformed JSON on its own, but it removes the most error-prone
               field from the common case so there's less to get wrong.

