core/docs/decisions/ADR-0139-arithmetic-as-versor-spike.md
Shay 589297b79a
feat(ADR-0139): arithmetic-as-versor spike — add closes exactly in Cl(4,1) (#212)
First step of the Engine A lift program (CLAUDE.md commits the project to a
single deterministic cognitive engine; Engine B / math pipeline was always
intentional scaffolding per math_solver.py:24). Proves the load-bearing
unknown: one arithmetic operation can be represented as a closed versor at
the required tolerance, with no new normalization and no weakened invariant.

Scope (frozen by ADR-0139):
- One operation: add
- Single-axis embedding: quantities on e1 axis
- No graph wiring, no pipeline integration, no GSM8K case routed
- Unit carried as caller metadata

Construction:
- embed_quantity(v, u) = embed_point([v, 0, 0])  (existing CGA primitive)
- translator(b)         = 1 - 0.5 * (b*e1 * n_inf)   (textbook CGA translator)
- decode_quantity(F, u) = (F[1], u)                  (e1 coordinate)

Measured values (all 11 fixed cases + composability):

      a         b      vcond(T)         |<R,R>|     decode_err
    0.0       0.0     0.000e+00       0.000e+00      0.000e+00
    0.0       1.0     0.000e+00       0.000e+00      0.000e+00
    1.0       0.0     0.000e+00       0.000e+00      0.000e+00
    3.0       4.0     0.000e+00       0.000e+00      0.000e+00
    7.0      -3.0     0.000e+00       0.000e+00      0.000e+00
   0.25      0.75     0.000e+00       0.000e+00      0.000e+00
    1.5       2.5     0.000e+00       0.000e+00      0.000e+00
   -5.0       5.0     0.000e+00       0.000e+00      0.000e+00
   -2.0      -3.0     0.000e+00       0.000e+00      0.000e+00
  100.0       1.0     0.000e+00       0.000e+00      0.000e+00
    1.0     100.0     0.000e+00       0.000e+00      0.000e+00
  compose (2, 3, 5) → 10:   |<R2,R2>| = 0.000e+00, decode_err = 0.000e+00

Every residual is exactly 0.0 in float64. The construction is algebraically
closed: T_t * reverse(T_t) = 1 - 0.25*B^2 where B = t*n_inf, and B^2 = 0
because (e14)^2 + (e15)^2 = -1 + 1 and cross-terms cancel. No machine-epsilon
drift accumulates because the relevant cancellation happens at the algebraic
level before float arithmetic.

ADR-0139 acceptance items 1-6 (one parametrized test family each):
  1. Embedding well-formedness   — test_family1_embedding_is_null         (11 cases)
  2. Translator well-formedness  — test_family2_translator_unit_versor    (11 cases)
  3. Closure                     — test_family3_sandwich_preserves_null   (11 cases)
  4. Arithmetic correctness      — test_family4_decode_matches_sum        (11 cases)
  5. Replay determinism          — test_family5_replay_byte_identical     (11 cases)
  6. Composability               — test_family6_two_translators_compose   (1 case)
  Total: 56 tests, all passing.

Lift program decision: proceeds. Follow-on ADRs (subtract, multiply, Rate,
compare, MathProblemGraph → PropositionGraph, pipeline integration, first
GSM8K case end-to-end through Engine A) are now justified by a concrete
algebraic foundation rather than design speculation.

Out of scope per ADR-0139:
- No modifications to algebra/, core/cognition/, chat/, math_solver.py,
  math_verifier.py, math_realizer.py, math_candidate_parser.py
- No GSM8K runner changes
- No pack changes
- Engine B continues serving GSM8K unchanged; the 3/50 admission set is
  preserved

CLI lanes intentionally not run — main has known test-rot orthogonal to
this PR. The 56 new tests are self-contained and the diff touches only
three new files.
2026-05-24 06:57:39 -07:00

14 KiB
Raw Permalink Blame History

ADR-0139 — Arithmetic-as-Versor Spike: add Only

Status: Draft Date: 2026-05-24 Author: CORE agents Parent / supersedes context: ADR-0114a, ADR-0115, ADR-0116 Engine target: CGA cognitive engine (algebra/versor.py, algebra/cga.py, core/cognition/pipeline.py)


Context

CLAUDE.md commits the project to a single deterministic cognitive engine:

listen → comprehend → recall → think → articulate → learn → replay

built on CGA Cl(4,1) versor algebra, exact recall, PropositionGraph, ArticulationTarget, deterministic realizer, and trace-hash invariance.

Between ADR-0114a and the present, a second engine grew alongside the first:

Engine A (CGA cognitive engine) Engine B (math pipeline)
Substrate versor multivectors in Cl(4,1) frozen Python dataclasses
Graph PropositionGraph (from graph_planner.py) MathProblemGraph (from math_problem_graph.py)
State propagation versor_apply(V, F) — sandwich product pure-Python arithmetic in math_solver.py
Closure invariant versor_condition(F) < 1e-6 asserts on dataclass fields
Trace contract core/cognition/trace.py SolutionTrace.canonical_bytes()
Used by chat/runtime.py, cognition eval evals/gsm8k_math/runner.py

Engine B was always intentional scaffolding — math_solver.py:24 states "the 'expert' tier (ADR-0120) is not in scope here; ADR-0116 is the Phase 2 substrate the eventual capability claim will rest on."

The GSM8K corridor (ADR-0123 / 0131 / 0136 / 0138) has been extending the parser side of Engine B without ever testing the lift to Engine A. Every PR through that corridor reports cognition eval byte-identical — the symptom that Engine A is not being invoked by math work, even though math is the nominal capability claim the engine should eventually demonstrate.

This ADR begins the lift. It does not finish it. It does not even cover one GSM8K case end-to-end through Engine A. It does one thing: prove that one arithmetic operation can be represented as a closed versor in Cl(4,1) without weakening any existing invariant.


Decision

Run a one-operation algebraic spike: add only, algebra only, no graph or pipeline wiring.

Embedding choice

A Quantity(value: int|float, unit: str) is embedded as a single conformal point on the e1 axis:

embed_quantity(value, unit) = embed_point([value, 0, 0])

Existing primitive: algebra/cga.py:embed_point.

This choice:

  • Places quantities on the CGA null cone (cga_inner(X, X) ≈ 0).
  • Uses only the existing CGA point-embedding primitive — no new algebra invented in this ADR.
  • Treats the unit field as carried metadata, not as a multivector coordinate. Unit handling is propositional, not algebraic.
  • Lets the standard CGA translator versor represent additive operations.

Open question (deferred): whether multi-unit problems require multiple axes (e1 for unit A, e2 for unit B) or whether each unit gets its own embedding context. ADR-0139 covers single-unit add only and does not commit either way.

Operation choice

add(addend: int|float) → versor is constructed as the standard CGA translator along e1 by addend:

T_a = 1 - 0.5 * a * e1 * n_inf

(exact sign/normalization to be derived against the existing cga.py / cl41.py conventions during implementation; the construction must produce a unit versor satisfying versor_condition(T_a) < 1e-6 at runtime.)

This is well-known CGA — translators are the canonical versor representation of Euclidean translations. Adding b to a quantity is geometrically translating its point on e1 by b.

Application

result = versor_apply(T_addend, embed_quantity(value, unit))

versor_apply already has the correct dual-path behavior for this embedding: null inputs (CGA points) get the raw sandwich path (algebra/versor.py:160-162) so the null property is preserved through the operation. No change to versor_apply is required.

Decoding

A decode_quantity(F, unit) → (value, unit) extracts the e1 coordinate of the result point. This is the inverse of embed_point restricted to the e1 axis.


Acceptance

A single test module — tests/test_arithmetic_as_versor_add.py — passes with these assertions on a small fixed set of (a, b) pairs covering integer, fractional, negative, and zero cases:

  1. Embedding well-formedness. For each input value:

    • cga_inner(embed_quantity(value, "u"), embed_quantity(value, "u")) ≈ 0 (null cone preserved).
  2. Translator well-formedness. For each addend b:

    • versor_condition(translator(b)) < 1e-6.
  3. Closure. For each (a, b):

    • Let R = versor_apply(translator(b), embed_quantity(a, "u")).
    • cga_inner(R, R) ≈ 0 (result remains on null cone).
  4. Arithmetic correctness. For each (a, b):

    • decode_quantity(R, "u") == (a + b, "u") byte-equal at the tolerance chosen by the embedding (decimal value match within 1e-9 for the fixed-point test cases listed below).
  5. Replay determinism. Running the test twice produces byte-identical multivector arrays (no nondeterministic float ordering, no platform drift).

  6. Composability (in-ADR scope). versor_apply(translator(c), versor_apply(translator(b), embed_quantity(a, "u"))) decodes to (a + b + c, "u") — proves two consecutive translations compose correctly. This is the smallest two-step program the engine path could run.

Fixed test cases

(0, 0), (0, 1), (1, 0),
(3, 4), (7, -3),
(0.25, 0.75), (1.5, 2.5),
(-5, 5), (-2, -3),
(100, 1), (1, 100),

Plus the composability case (2, 3, 5) → 10.


Non-goals

Explicit out-of-scope for this ADR:

  • No subtract, multiply, divide, transfer, apply_rate, compare_additive, compare_multiplicative operations. Each gets its own follow-on ADR once add is proven.
  • No MathProblemGraph consumer. The new functions take typed inputs directly. They do not import from math_problem_graph.py.
  • No PropositionGraph construction. Engine A's graph layer is not touched.
  • No CognitiveTurnPipeline integration. The pipeline file is not imported.
  • No chat/runtime.py invocation path. The chat surface is not touched.
  • No GSM8K case routed through this code. The runner is not modified.
  • No deprecation of Engine B. math_solver.py, math_verifier.py, math_realizer.py, and the S.x corridor parsers remain in place, unmodified, scoring GSM8K as they do today. The 3/50 admission set is preserved.
  • No pack changes. en_arithmetic_v1 is not touched. Pack-binding for the versor path is a separate concern.

The ADR succeeds if add works algebraically. It does not claim that the math pipeline has been lifted. It only proves the lift is feasible for one operation.


Rationale

Two design choices are load-bearing and should be defended explicitly:

Why a spike instead of a phased plan?

The arithmetic-as-versor algebra is the single load-bearing unknown for the entire lift program. Every follow-on ADR — subtract, multiply, compare, graph integration, pipeline integration, GSM8K routing — assumes that arithmetic can be represented as closed versors at the required tolerance. If add doesn't work cleanly, every downstream ADR is built on sand. The spike forces that assumption to be tested in code, not in design documents.

Why add instead of multiply or compare?

Translators are the most canonical CGA versor. The construction T_t = 1 - 0.5 * t * n_inf is textbook. If anything in the CGA substrate is going to behave well, translators will.

Multiplication is dilation in CGA — also a known versor, but it requires the n_o ∧ n_inf blade and exponentiation. Riskier first step.

Comparisons (compare_additive, compare_multiplicative) are relational predicates, not transformations. They may not be versor-shaped at all — they might land at the proposition layer instead. Trying to make them versor-shaped first would entangle two unknowns.

So add is the smallest, cleanest, most-textbook starting point.

Why no graph or pipeline wiring?

Engine A's graph and pipeline layers already exist and work. The risk isn't whether versor_apply integrates with graph_from_intent — that's plumbing. The risk is whether arithmetic can be represented as versors at all. Wiring before the algebra is proven would create the appearance of progress without removing the load-bearing unknown.


Open questions for follow-on ADRs

The following must be answered, but not by this ADR:

  1. Multi-axis embedding. Does a two-unit problem (5 apples + 3 oranges style — even though that's not valid arithmetic, mixed-unit intermediate states do appear in word problems) need orthogonal axes (e1 for apples, e2 for oranges)? Or does each unit context get its own embedding session?

  2. Multiplication as dilation. The dilator D_s = cosh(α/2) + sinh(α/2)·(n_o ∧ n_inf) where s = exp(α) represents scaling. Does it close at versor_condition < 1e-6 for the value ranges GSM8K actually requires? At what precision?

  3. Comparison as proposition vs versor. Is compare_additive("more by 5", x) a versor operation, a proposition node, or both? Strongest guess: proposition. But this needs an ADR.

  4. Rate as bivector. A Rate(2.0, "dollars", "apple") is inherently two-axis. It is probably a grade-2 object connecting two Euclidean axes. Does the existing CGA substrate support this cleanly?

  5. PropositionGraph construction from MathProblemGraph. Once add and subtract are proven as versors, an ADR is needed that constructs a PropositionGraph from a MathProblemGraph so the engine pipeline can articulate the answer through the existing realizer.

  6. Trace-hash story. Engine A's compute_trace_hash and Engine B's SolutionTrace.canonical_bytes() need to converge. Probably the versor sequence becomes the trace, with the existing hash function applied. Defer to the integration ADR.

  7. Refusal floor. The versor path must preserve wrong == 0. When the algebra cannot represent a needed operation, the engine must refuse, not approximate. Mechanism TBD by the integration ADR.


Risks

  • The translator construction may not close at 1e-6. The construction-residue tolerance in algebra/versor.py:13 is 1e-2 and the runtime closure tolerance is 1e-6. If translator(b) lands between those, _close_applied_versor will project it through _seed_to_rotor, which may not preserve the exact translation. The spike must verify this empirically; if it fails, the embedding or the construction has to be reconsidered before the ADR can ship.

  • Float32 truncation. algebra/cl41.py uses float32 for multivectors. Large additions (e.g. 100 + 1) may not decode back to exactly 101.0 after the sandwich. The test cases above include values that probe this. If float32 doesn't carry the required precision, the embedding may need to use the float64 path that algebra/versor.py:18 already defines for runtime fields.

  • Decoding may not be exact for arbitrary float values. The e1 component of an embedded point is the raw value, but the e4/e5 coefficients carry 0.5 * (value^2 ± 1). Round-tripping requires the e1 coordinate alone — the e4/e5 components are dependent. If the sandwich introduces error in e1 vs e4/e5 differently, decoding from e1 alone may not equal the input. This is the most likely failure mode and the spike's primary falsification target.

  • The user-facing capability gauge does not move in this ADR. GSM8K admissions stay at 3/50. The cognition eval stays byte-identical. The only signal this ADR produces is a test file that does or does not pass. That is intentional but easy to misread as "no progress."


Replay & invariants

The spike is governed by the same invariants as the rest of CORE:

  • versor_condition(F) < 1e-6 for all unit versors constructed (translators in this ADR).
  • Null inputs to versor_apply stay null. Verified by cga_inner(R, R) ≈ 0 on every result.
  • No normalization is introduced outside the allowed sites (ingest/gate.py, language_packs/compiler.py, algebra/versor.py). The new functions live in a new module — proposed path generate/math_versor_arithmetic.py — and call only existing primitives. They do not add any new normalization.
  • Determinism: float64 path used end-to-end where precision matters; no platform-conditional code; no randomness.

Work sequencing for follow-on

Only if this ADR's tests pass:

  1. ADR-0140: subtract as inverse translator. (Trivial follow-on; should pass nearly for free.)
  2. ADR-0141: multiply as dilator. (Risk concentrates here.)
  3. ADR-0142: Rate as bivector and apply_rate as combined translator-dilator. (Open question 4.)
  4. ADR-0143: compare_* at the proposition layer, not versor layer. (Open question 3.)
  5. ADR-0144: PropositionGraph from MathProblemGraph. (Open question 5.)
  6. ADR-0145: One GSM8K case (gsm8k-0014) routed end-to-end through Engine A. First moment the capability gauge is honestly attached to the engine.

If this ADR's tests fail, the lift program is paused and the failure mode is documented. Engine B continues serving GSM8K. A revised embedding strategy is required before any follow-on ADR.


Decision summary

Add one new module (generate/math_versor_arithmetic.py — name provisional) with three functions: embed_quantity, translator, decode_quantity. Add one test module verifying add works as a closed versor at the required tolerance. Change nothing else. Ship as a single PR small enough to audit in one sitting.

Acceptance is binary: every test in the new module passes, or the ADR is withdrawn and the lift program is paused pending a new embedding choice.