================================================================================
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).
        [x] 19.1.9.  complex — a + b*i over two fixed-point parts; imaginary
                     unit written `1i`. Exact + - *, rounds / and the
                     transcendentals onto the grid; no ordering/bitwise/
                     integer/solver support (each refuses).
    [ ] 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. The variadic
        aggregates (28.x) were 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.


[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] 28.  MORE FUNCTIONS  (beyond abs/sqrt; call machinery is section 22. Each is the
         two-step add — a _FUNCS entry + its per-mode Value method. The source now
         documents each function; this is just the checklist.)
    [x] 28.1.   time() — Unix epoch, nullary (rides 29); whole seconds default, exact
                except float. [28.1.1 per-mode render, 28.1.2 one instant per run]
    [x] 28.2.   max(...) — variadic, selection-only, exact.
    [x] 28.3.   min(...) — variadic, mirror of max.
    [x] 28.4.   avg(...) — variadic mean; follows the mode's / rule.
    [x] 28.7.   median(...) — variadic; odd selects (exact), even averages.
    [x] 28.8.   variance(...) — population variance (/ n); follows the / rule.
    [x] 28.9.   stddev(...) — sqrt(variance); inherits sqrt's per-mode story.
    [x] 28.10.  sin(x) — fixed-point Taylor series, else inexact/refuse.
                [28.10.1 internal fixed-point pi helper]
    [x] 28.11.  cos(x) — sin's machinery.
    [x] 28.12.  tan(x) — sin/cos; guards cos->0.
    [x] 28.13.  cot(x) — cos/sin; mirror of tan.
    [x] 28.14.  asin(x) — atan(x/sqrt(1-x^2)), domain |x|<=1.
    [x] 28.15.  acos(x) — pi/2 - asin(x), domain |x|<=1.
    [x] 28.16.  atan(x) — the arctan series itself, all reals.
    [x] 28.17.  log(x) — natural log (ln alias); base-10 reduction + atanh series.
                [28.17.1 atanh series helper, 28.17.2 ln(10)/ln(2) constants]
    [x] 28.18.  log10(x) — log(x)/ln(10), exact on powers of ten.
    [x] 28.19.  log2(x) — log(x)/ln(2), exact only at log2(1)=0.
    [x] 28.20.  pow(x, y) — call form of **; fixed-arity 2.
                [28.20.1 fixed-point fractional exponent: exact q-th root or exp(y*ln x)]
    [x] 28.21.  cbrt(x) — cube root; odd root so negatives OK.
    [x] 28.22.  OPTIONAL-ARG ARITY — the (1,2) call shape for the rounding ndigits forms.
    [x] 28.23.  floor(x[, ndigits]) — toward -inf.
    [x] 28.24.  ceil(x[, ndigits]) — toward +inf.
    [x] 28.25.  round(x[, ndigits]) — nearest, ties to even.
    [x] 28.26.  trunc(x[, ndigits]) — toward zero.
    [x] 28.27.  exp(x) — e**x; reduce by ln2 + exp series.
                [28.27.1 internal exp series helper]


[ ] 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).
    [x] 33.1.  Bisection — bracketed root, sign change, robust. Scans the bracket
               for the first sign-changing cell (endpoints need not straddle) and
               halves it; find-root only; distinct "no sign change" error. See
               solver.bisection.
    [x] 33.2.  Brent's method — root, bisection + secant/inverse-quad.
    [x] 33.3.  Secant — root, no derivative, superlinear.
    [x] 33.4.  Newton-Raphson — root via symbolic derivative.
    [x] 33.5.  Ridders' — bracketed root, exponential fit. Bisection's sign-change
               bracket + scan, but an exponential-fit step (order ~1.84) instead of
               halving; find-root only. See solver.ridders.
    [x] 33.6.  Ternary search — unimodal optimise alternative.
    [x] 33.7.  Chandrupatla's — bracketed root, often beats Brent.
    [x] 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.
    [x] 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.
    [x] 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.
    [x] 33.25. Share ONE sign-change bracketing harness across the find-root engines,
               so a new bracketer is only its refinement step. Four engines now
               hand-roll the same ~120 lines around a ~25-line core: bisection
               (solver.py:717), ridders (:898), brent_dekker (:1096) and chandrupatla
               (:1333). Identical in all four: the find-root-only objective guard (only
               the engine name differs in the message), the scale / tolerance / deadline
               setup, the `evaluate_objective` closure returning the SIGNED value with
               None on a domain error and |expr| best-tracking as a side effect, the
               coarse 64-cell scan for the first sign-changing cell (bisection keeps only
               the lower endpoint value, the other three keep both), the fixed-point /
               rational grid polish, the float snap polish, and all three error paths
               (evaluates nowhere / no sign change / off-grid) plus the SolverResult
               construction. Four copies of `_*_SCAN_CELLS = 64` go with them. Shape:
               the harness takes the engine's Algorithm and a refine callback
               `(a, b, fa, fb, evaluate, x_tol, deadline) -> iterations` and owns
               everything else. Verify by iteration-count parity on the existing e2e
               suites — the refactor must not move a single reported count.
               NOTE the minimisers are a NEAR miss, not a match: search (:400) and
               brent_parabolic (:541) share the polish / error tail but their
               `evaluate_objective` returns the FOLDED objective with +inf on a domain
               error (not the signed value with None) and they do no scan, and
               nelder_mead (:1554) is multivariate. So either factor the tail out
               separately for those, or accept that this harness covers the four
               bracketers only — DECIDE which when doing it, and do NOT contort the
               minimisers to fit.


[ ] 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)."


[x] 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).)
    [x] 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.
    [x] 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.
    [x] 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.
    [x] 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).


[ ] 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.


[ ] 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).
    [x] 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,
               deferred to 40.5.1.
        [ ] 40.5.1.  comb(n, k) for NON-INTEGER arguments — the gamma-generalized
                     binomial coefficient gamma(n+1)/(gamma(k+1)*gamma(n-k+1)):
                     transcendental (float/fixed-point inexact, rational refuses),
                     built on gamma (40.4.1) and refused where its poles bite.
    [x] 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.
               A non-integer argument is the gamma-generalized permutation,
               deferred to 40.6.1.
        [ ] 40.6.1.  perm(n, k) for NON-INTEGER arguments — the gamma-generalized
                     permutation gamma(n+1)/gamma(n-k+1): transcendental
                     (float/fixed-point inexact, rational refuses), built on gamma
                     (40.4.1) and refused where its poles bite.
    [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.
    [x] 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.
    [x] 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.
    [x] 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.
    [x] 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.
    [x] 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.
    [x] 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).
    [x] 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).
    [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.
    [x] 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).
    [x] 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).
    [x] 40.23. factor(n) — prime factorization, UNARY, RETURNS a VECTOR of primes in
               ascending order WITH multiplicity (factor(12) = [2, 2, 3], factor(7) =
               [7]) so the product of the elements reconstructs n. EXACT in every mode
               (pure integer math, no rounding — trial division / Pollard rho). This is
               the FIRST function whose RESULT is a VECTOR: until now a VECTOR only comes
               from a `[...]` literal (19.1.10, the one place Value.vector is built). So
               the work is two-sided — not just a Value method but a producer feeding the
               VECTOR container, and the server already renders a non-scalar vector result
               (server.py:268). Element mode follows the run mode (integer-valued primes
               in the fixed-point/rational/float spelling). DOMAIN integer-valued n only,
               the gcd/lcm stance (40.7/40.8) — a non-integer refuses (exact-or-refuse).
               DECIDE: (a) lower bound — n >= 2 only, or accept n = 1 as the empty vector
               [] (the empty product), and refuse 0 / negatives (or factor |n| and drop
               sign like gcd?); (b) shape — repeated primes WITH multiplicity (above) vs
               unique primes vs prime^exponent pairs (the latter two need a second
               function or a pair encoding the flat VECTOR can't hold); (c) name —
               factor / factorize / primefactors / factorint. Settle the vector-PRODUCER
               plumbing here; it is the prerequisite any future list-returning function
               (divisors, range, …) reuses.
    [x] 40.24. map(vector, name, body) — evaluate an EXPRESSION per element and RETURN
               a new VECTOR. Higher-order like diff/integral/sum-fold (40.17/40.18/40.19):
               arg 1 is a vector, arg 2 a BARE NAME, arg 3 the UNEVALUATED body; for each
               element the body runs with `name` bound to that element, and the results
               collect into a same-length vector (map([1,2,3], x, x**2) = [1, 4, 9]).
               This is the missing element-wise primitive — the vector type refuses
               operators/functions today (19.1.10), so map is how you transform a series.
               First function that is BOTH a vector CONSUMER and a vector PRODUCER (reuses
               the 40.23 producer plumbing). Element mode follows the run mode; exactness
               is the BODY's (map itself only dispatches). DECIDE: (a) name — map / apply /
               transform; (b) does the result nest — is map over a scalar an error or a
               1-elt vector; (c) whether a two-vector zip form (map two series in lockstep)
               is in scope here or a separate item — 40.25 needs the parallel walk.
    [x] 40.25. residual_sum_squares(expr, var, xs, ys) — least-squares cost of a MODEL
               against PAIRED DATA: sum over i of (ys_i - expr[var := xs_i])**2, the sum
               of squared residuals (RSS/SSE). Spelled out, NOT an acronym — the
               covariance/correlation/variance stance (a cryptic `rss`/`sse` would collide
               and read as nothing). Higher-order — arg 1 the UNEVALUATED model expr, arg 2
               a BARE NAME (the free variable), args 3/4 two EQUAL-LENGTH vectors of the
               observed x and y. Walks xs/ys in PARALLEL (the two-vector lockstep 40.24(c)
               flags), binds `var` to each xs_i, and folds the squared gap. RETURNS a
               scalar; the arithmetic stance of sum/avg (EXACT in rational, may round in
               fixed-point/float). This is the direct answer to "square distance of a set
               of (x, y) pairs and an expression" — the caller-supplied-model analogue of
               curve_fit's residual error (which only ranks its built-in forms). Once map
               (40.24) + a zip exist it is just sumsq of the per-point gaps, so it is
               convenience sugar — DECIDE whether it earns its own function or callers
               compose sumsq(map2(...)). Reserve the plural `residuals(expr, var, xs, ys)`
               for a future sibling that RETURNS the signed per-point gap VECTOR (that is
               the standard meaning of "residuals" — it falls out of map/zip). DECIDE:
               (a) refuse unequal-length xs/ys and empty data; (b) name — lead with
               residual_sum_squares, sum_squared_residuals the alt order; (c) whether a
               weighted form (a third vector w) is a later variant.
    [x] 40.26. sumsq(...) — sum of squares x1**2 + x2**2 + ..., VARIADIC (the sum/hypot
               shape, 40.20) with a VECTOR overload (sumsq(vector)); a reducer, so it
               joins _VECTOR_FUNCS. Plain squaring + addition: EXACT in rational, may
               round in fixed-point/float (the avg stance, not sqrt's). Note hypot (40.20)
               is sqrt(sumsq(...)) — this is that inner sum exposed on its own, and the
               reduce half of residual_sum_squares (40.25). Every real allowed. Keep the
               short name `sumsq`: it is a recognized idiom, not an acronym, and a real
               function in Excel (SUMSQ) and Octave — the terse primitive callers compose.
               DECIDE: name — sumsq (lead) / sum_of_squares if full-spelling wins.


[ ] 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.
    [x] 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.
    [ ] 43.4.  Remove `^` entirely. It is ambiguous -- programmers read XOR,
               math/LLMs read power -- so `2^3` silently returns 1, not 8. Stop
               accepting `^` at all: reject it with an error naming the two real
               operators, `**` for power and `xor` for bitwise XOR. Requires
               providing `xor` (word operator or xor(a, b) function) to keep XOR
               reachable once the symbol is gone. No computing-then-explaining.
    [x] 43.5.  "Did you mean" on unknown names. Add nearest-match suggestions to
               the unknown-function / unknown-mode / unknown-algorithm errors
               (sqr->sqrt, arcsin->asin) -- the expression-layer analog of 43.2.
               Use stdlib difflib.get_close_matches over canonical names +
               aliases (cutoff ~0.6 so a true typo suggests, an unrelated word
               suggests nothing). Runs after alias resolution fails; catches
               typos only, not semantic misses (ln->log belongs in aliases).
    [ ] 43.6.  Accept the equation form in solver. Let `solver` take `f = g` (or
               `f == g`) and rewrite to `f - g` for find-root, so the caller need
               not rearrange the equation themselves.
    [x] 43.7.  Zero-config solver default. The simplest fixed-point solve errors
               demanding min_fixed_point_precision (39). Rather than switch the
               default mode, give solver's min_fixed_point_precision a non-None
               default (e.g. 9, matching the hint) so the bare call just works
               while the default mode stays fixed-point, consistent with
               calculate. An explicit null still hits the 39 refusal.


[x] 44.  CURVE FITTING TOOL  (a new MCP tool, solver-adjacent like diff/integral
         40.17-40.19: the caller passes paired x/y data, the tool tries every
         curve form it knows, and returns the best-fitting few with their fitted
         equation and error. Reuses the parser/evaluator and the solver's
         optimise engine to estimate each form's parameters.)
    [x] 44.1.  New MCP tool `fit(x, y, mode="fixed-point", ...)`. `x` and `y` are
               equal-length numeric arrays (the observations); reuse calculate's
               mode / min_fixed_point_precision front-end (the 31.3 shared helper).
    [x] 44.2.  Curve library — a registry of candidate model forms, each a named
               template with free parameters. Extensible the way _FUNCS is; each
               entry knows its parameter names, its objective, and any domain limits
               (power/log need x>0, etc.). The variants:
        [x] 44.2.1.  linear — a*x + b.
        [x] 44.2.2.  quadratic — a*x**2 + b*x + c.
        [x] 44.2.3.  cubic — a*x**3 + b*x**2 + c*x + d.
        [x] 44.2.4.  power — a*x**b; domain x>0.
        [x] 44.2.5.  exponential — a*exp(b*x).
        [x] 44.2.6.  logarithmic — a + b*ln(x); domain x>0.
        [x] 44.2.7.  square-root — a*sqrt(x) + b; domain x>=0.
        [x] 44.2.8.  reciprocal — a/x + b; domain x!=0.
        [x] 44.2.9.  sinusoidal — a*sin(b*x + c) + d.
        [x] 44.2.10. gaussian — a*exp(-(x-b)**2/(2*c**2)) (peak a at centre b, width c);
                     domain y>0. Closed form via Caruana's method: ln y is quadratic in x,
                     so fit the quadratic and recover a, b, c from its coefficients (needs
                     a downward parabola, c**2 = -1/(2*c2)).
        [x] 44.2.11. saturation (Michaelis-Menten) — x/(a*x + b); rise to a plateau y->1/a;
                     domain x!=0, y!=0. Closed form via the double-reciprocal line
                     1/y = a + b*(1/x) (Lineweaver-Burk).
        [x] 44.2.12. hyperbolic — 1/(a*x + b); smooth 1/x-style falloff; domain y!=0.
                     Closed form via the line 1/y = a*x + b (distinct from 44.2.8's
                     additive a/x + b).
        [x] 44.2.13. Laurent — a + b*x + c/x; linear trend with a near-origin pole;
                     domain x!=0. Closed form: direct linear basis {1, x, 1/x} (44.9),
                     no transform.
        [x] 44.2.14. exp-reciprocal (Arrhenius) — a*exp(b/x); rate/vapor-pressure shape;
                     domain x!=0, y>0. Closed form via ln y = ln a + b*(1/x), linear
                     basis {1, 1/x} in log-space (cousin of exponential 44.2.5 with 1/x).
        [x] 44.2.15. Hoerl — a * b**x * x**c; rise-then-decay; domain x>0, y>0. Closed
                     form via ln y = ln a + (ln b)*x + c*ln x, linear basis {1, x, ln x}
                     in log-space.
        [x] 44.2.16. Weibull CDF — 1 - exp(-(x/L)**k); reliability/survival S-curve;
                     domain x>0, 0<y<1. Closed form (Weibull plot) via
                     ln(-ln(1-y)) = k*ln x - k*ln L, linear in ln x.
        [x] 44.2.17. logistic (fixed L) — L/(1 + exp(-(a*x + b))); S-curve with fixed
                     carrying capacity L (L=1 for proportions, else L=max y); domain
                     0<y<L. Closed form via the logit ln(y/(L-y)) = a*x + b, linear.
        [x] 44.2.18. generalized hyperbolic — 1/(a*x**2 + b*x + c); domain y!=0. Closed
                     form via 1/y = quadratic in x (generalizes 44.2.12 to a quadratic
                     denominator); shares the reciprocal-quadratic wrapper with 44.2.19.
        [x] 44.2.19. Lorentzian/Cauchy peak — a/(1 + ((x-b)/c)**2) (peak a at centre b,
                     half-width c); the symmetric-peak counterpart to gaussian 44.2.10;
                     domain y!=0. Closed form via 1/y = quadratic in x, recovering a, b,
                     c from the coefficients (needs an upward parabola in 1/y).
    [x] 44.3.  Fit each form independently — estimate its parameters by minimising
               the residual error over the data, driving the existing solver
               optimise engine (least-squares objective Σ(model(xᵢ)-yᵢ)²). Forms are
               fitted in parallel; a form that can't fit the data (domain miss,
               non-convergence) is dropped, not fatal.
    [x] 44.4.  Error metric — one comparable goodness number per fitted form (sum of
               squared residuals, or RMSE), computed in the active mode so the
               verdict carries the usual exact/inexact honesty.
    [x] 44.5.  Rank by error and return the best 3: each result gives the curve's
               fitted equation (parameters substituted, rendered in the mode's
               normal string form) and its error. Fewer than 3 if fewer forms fit.
    [x] 44.6.  Reply shape — the ranked list plus `mode`, `exact`, `precision`, and
               an `error` path, annotated like solver (31.8 / 32).
    [x] 44.7.  TESTS — a clean linear set, a set best fit by a non-linear form, and
               the error cases (mismatched x/y lengths, too few points, unknown
               mode). In-process + over-the-wire.
    [x] 44.8.  DOCS — tool docstring, a `fit` help section (reference.py), and a
               README bullet.
    [x] 44.9.  Generalise `_polynomial_fitter` into a linear-least-squares fitter over an
               arbitrary basis {f1(x)..fk(x)}, so the linear-in-parameters forms share one
               fitter instead of each hand-rolling its normal equations: logarithmic
               {1, ln x} (44.2.6), square-root {1, sqrt x} (44.2.7), reciprocal {1, 1/x}
               (44.2.8), and any future combo. The transform-linearised forms (exponential
               44.2.5, power 44.2.4, gaussian 44.2.10, saturation 44.2.11, hyperbolic
               44.2.12) stay thin wrappers that fit a linear-basis model in transformed
               coordinates and map the parameters back. The sinusoid (44.2.9) is the lone
               form with no closed form — it keeps the solver's optimise engine.


[x] 45.  FIXED-POINT `**`: the fractional-exponent path can hang the server on an
         ordinary expression. `1.05 ** 14.2067` in fixed-point takes ~80 seconds;
         at the solver's default floor of 9 (`1.05 ** 14.206699916`) it never
         returns and eats memory until it is killed. Reachable from `calculate`,
         not just the solver — found 2026-07-23 while smoke-testing a compound-
         interest solve (`p*(1+r)**n - 2000` for n, fixed-point), where every
         engine hangs alike, so it is not a solver bug. The 2s solver cap cannot
         help: it is checked BETWEEN candidate evaluations and this is ONE of them.
    [x] 45.1.  PATH A must not materialise `x**p` to decide whether the root is
               exact (value.py, pow's fixed-point branch). A scale-9 exponent
               reduces to p ~ 3.5e9 / q = 2.5e8, and `Fraction(105,100)**p` is a
               4.7-BILLION-digit integer built before anything is tested — then
               thrown away, since 1.05^14.2067 is irrational and the answer comes
               from PATH B's exp(y*ln x) series anyway. Test the perfect q-th
               power on the REDUCED BASE instead: p and q are coprime by
               construction, so for u = prod(pi^ei), u**p is a perfect q-th power
               iff q | p*ei for all i iff q | ei for all i iff u is one. Root the
               small base, and raise to p only when both numerator and denominator
               come out exact.
    [x] 45.2.  `_iroot(n, q)` must shortcut `n.bit_length() <= q` (i.e. n < 2**q,
               so the floor of the q-th root is 1). Needed BECAUSE of 45.1: the
               test then runs with a huge q against a tiny radicand, and
               `_iroot(21, 250000000)` costs 1.4s today — twice per pow, 65 times
               over the bracketers' scan alone. Independently worth having: the
               Newton loop computes `x**(q-1)` per step, which is absurd when the
               answer is 1 by inspection.
