DEV Community

Cover image for From Dev.to Comment to Production in 24h: Building an Inspectable Math Verification Contract in Pythos (and Fixing the Pearson Trap)
Jon Scott
Jon Scott

Posted on

From Dev.to Comment to Production in 24h: Building an Inspectable Math Verification Contract in Pythos (and Fixing the Pearson Trap)

Earlier this week, I published an article here on DEV: "Why We Stopped Letting LLMs Do Raw Math: Building Pythos With Deterministic Verification".

The response was humbling, but one particular comment from an enterprise AI engineer at IT Path Solutions stood out. They validated our core thesis—treat the LLM as a conversational interface, never the mathematical source of truth—and left a game-changing suggestion:

"Make the verification result part of the data contract... each step could carry the expression evaluated, the verification method, the assumptions used, and the exact state that was checked."

They were 100% right.

In Pythos, our dual-engine verifier (in-process Math.js for exact arithmetic and a sandboxed SymPy CAS daemon for symbolic algebra/calculus) was already validating derivations step-by-step behind the scenes. But once the delivery decision gate passed, we were condensing all that rich telemetry into simple binary flags before shipping the text to the client.

We decided to build what they suggested.

Here is how we turned mathematical verification into an explicit, wire-level data contract, built a client-side derivation inspector, and—in a hilarious twist of real-world battle testing—fixed a production bug where Pearson homework boilerplate tricked our deterministic router into computing the area of a right triangle.


1. The Architecture: The VerificationStepAudit Data Contract

Prior to v1.8.35, Pythos knew internally whether a claim was true or false, but the frontend only received markdown text with basic metadata.

We formalized a new data contract: VerificationStepAudit. Every evaluated claim emitted over our streaming NDJSON and JSON endpoints now carries structured audit telemetry:

interface VerificationStepAudit {
  stepIndex: number;
  expressionEvaluated: string;
  verificationMethod: 'SYMPY_CAS' | 'MATHJS_EXACT' | 'DETERMINISTIC_FASTPATH';
  engine: 'sympy' | 'mathjs' | 'native';
  assumptions: {
    domain?: 'real' | 'complex' | 'integer';
    tolerance?: number;
    quadrant?: string;
  };
  checkedState: {
    proposed: string;
    expected: string;
    discrepancy?: string;
  };
  certified: boolean;
  rejectionRationale?: string;
}
Enter fullscreen mode Exit fullscreen mode

Why This Matters for AI Tutoring

  1. Zero Black-Box Math: The student (and educator) isn't just told "Trust me, the answer is $x = \frac{1}{2}$." They can inspect the actual symbolic reduction that certified the step.
  2. Targeted Socratic Revision: When an LLM generates a multi-step derivation and makes a sign error on Step 4, we don't ask it to "try again" blindly. The verifier catches the exact step failure and feeds the structured VerificationStepAudit back into the Socratic revision prompt: > "Step 4 failed: evaluated $\sin^2\theta + \cos^2\theta = 2$, expected $1$. Discrepancy: Pythagorean identity violation." The model corrects itself with mathematical precision.

2. The UX: The Derivation Inspector

Exposing verification metadata without cluttering the learning experience is tricky. If you dump raw JSON or giant debug banners on students, it overwhelms them.

Instead, we added a subtle, accessible badge on the bottom-right action bar of assistant messages, sitting cleanly alongside the Report and Copy buttons:

[ 🛡️ Verified (3 steps) ]  [ 📋 Copy ]  [ 🐞 Report ]
Enter fullscreen mode Exit fullscreen mode

Clicking Verified expands a slide-out Derivation Inspector panel directly underneath the response:

  • Highlights each individual derivation step.
  • Displays the exact mathematical expression tested.
  • Shows the certified CAS engine badge (SymPy CAS or Math.js).
  • Confirms the active assumptions (e.g., $x > 0$, real domain).

Now, students aren't just reading AI generated text; they can audit the mathematical proof behind it in real time.


3. The Multimodal Polish: Magic Bytes & Vision History

Alongside the audit contract, we upgraded our multimodal screenshot ingestion pipeline. Students frequently screenshot math worksheets and paste them directly into Pythos.

We ran into two edge cases with upstream vision models (Llama 3.2 Vision on Groq):

  1. MIME-Type Rejections (400 Bad Request): Some browsers generate clipboard image blobs with generic or missing MIME headers. We implemented client-side magic-byte sniffing (inspecting PNG \x89PNG, JPEG \xFF\xD8\xFF, and WebP headers) to guarantee strict Content-Type tagging.
  2. Vision TPM Throttling (429 Rate Limit): In a multi-turn conversation, re-transmitting multi-megabyte base64 images on every turn quickly incinerates Token-Per-Minute (TPM) limits on free-tier inference gateways. We bounded conversational image history so previous turns retain verified OCR transcripts while releasing redundant base64 payloads once grounded.

4. The "Pearson Trap": When Homework Boilerplate Met Deterministic Routing

No deployment story is complete without a hilarious production edge case.

Shortly after releasing v1.8.35, I was using Pythos myself to work through college trigonometry homework from Pearson MyLab. I pasted a problem:

Use a right triangle to write the following expression as an algebraic expression. 
Assume that x is positive and that the given inverse trigonometric function is defined for the expression in x.
tangent left parenthesis cosine Superscript negative 1 Baseline 4 x right parenthesis
Question content area bottom
Part 1
tangent left parenthesis cosine Superscript negative 1 Baseline 4 x right parenthesisequals
Enter fullscreen mode Exit fullscreen mode

I expected Pythos to derive:
$$\tan(\cos^{-1}(4x)) = \frac{\sqrt{1 - 16x^2}}{4x}$$

Instead, Pythos instantly and cheerfully responded:

📐 Right-Triangle Area

Here is the right triangle with base 3 and height 4:

$\text{Area} = \frac{1}{2} \times \text{base} \times \text{height} = \frac{1}{2}(3)(4) = 6$

Wait... what? Where did a 3-4-5 triangle and an area of 6 come from?!

The Root Cause

To keep latency near zero and save GPU compute costs, Pythos features an in-process Deterministic Router that short-circuits simple calculations (arithmetic, unit conversions, geometry requests) before invoking the LLM.

Two bugs in the router had collided:

  1. Screen-Reader Web Boilerplate: Pearson MyLab outputs accessibility tags including the phrase: > "Question content **area* bottom"*
  2. Overly Eager Regex: The router's geometry parser checked for right triangle queries, followed by:
   const isArea = /\barea\b/i.test(clean);
Enter fullscreen mode Exit fullscreen mode

It matched the word "area" in "Question content area bottom"!

  1. The Dummy Fallback: Because the prompt asked to "Use a right triangle..." to solve an algebraic expression, no numerical leg lengths were given. The legacy geometry routine fell back to a default 3, 4, 5 triangle and happily computed $\frac{1}{2} \times 3 \times 4 = 6$.

The Hardened Fix

We patched server/deterministicRouter.js immediately:

  1. Added an isAlgebraicOrInverseTrig guard that completely blocks prompts containing algebraic keywords (algebraic expression, simplify, expand) or inverse trig operators (cos^-1, arcsin, StartFraction, Superscript negative 1) from ever being hijacked as static geometry drawings.
  2. Sanitized web platform boilerplate (question content area, content area bottom) before testing for geometric intents.
  3. Required geometric area calculations to have explicit numerical side lengths and dedicated question phrasing (e.g., "find the area").

5. Adding It to the Master Regression Suite

A bug once discovered must never happen again.

We created Group G in our fast-path hardening suite (test-deterministic-fastpath-hardening.js) with tests specifically checking Pearson screen-reader transcripts, algebraic inverse trig phrases, and fractional boilerplate:

runTest('G1: Pearson screen-reader inverse trig prompt must not trigger dummy triangle area', () => {
  const prompt = 'Use a right triangle to write... tangent left parenthesis cosine Superscript negative 1 Baseline 4 x right parenthesis Question content area bottom...';
  const intent = deterministicRouter.analyzeDeterministicIntent(prompt, []);
  assert.strictEqual(intent, null); // Must pass through to LLM + CAS verification!
});
Enter fullscreen mode Exit fullscreen mode

We then promoted this into Suite 25 of our master regression test harness (test-regression-harness.js).

All 25 regression suites—covering SymPy CAS, Math.js adversarial arithmetic, student privacy redaction, streaming verification, and fast-path hardening—are passing 100% green.


Summary

In less than 24 hours:

  • A great comment from the DEV community gave us the exact conceptual blueprint we needed.
  • We turned verification into a first-class, inspectable VerificationStepAudit data contract.
  • We added an accessible client-side UI inspector to prove mathematical derivations to students.
  • We caught and eliminated a wild edge-case where Pearson homework boilerplate masqueraded as a geometry problem.

Huge thanks to the DEV community and our commenter from IT Path Solutions for pushing us to make Pythos better.

You can try out Pythos for free anytime at pythos.lanzar.me or explore the code and verification engine on GitHub.

What are your thoughts on exposing deterministic verifier contracts directly in AI chat interfaces? Would you find step-level mathematical receipts helpful when studying or building math tools? Let's discuss in the comments!

Top comments (4)

Collapse
 
mickyarun profile image
arun rajkumar •

The Pearson trap is the most useful thing in this post, and I think it undercuts the data contract rather than sitting beside it.

Walk the failure back. A router matched on boilerplate, decided the input was a right-triangle area problem, sent it to the correct engine, and the engine correctly computed the area of a triangle. Every step VerificationStepAudit records was true. The expression evaluated was the expression that was evaluated. Math.js certified it. The assumptions held. A student clicking Verified sees a correct derivation, and the answer is to a different question than the one on the worksheet.

So the audit contract covers everything downstream of the routing decision and nothing about the routing decision, which is where the error was. The missing field is not about a step. It is what the system decided the question was, and what in the input made it decide that. For the fast path specifically: which pattern matched, the text it matched on, and the confidence or the absence of one. That is the only field in the record that could have been wrong in your incident, and it is the field that is not in the record.

Assumptions are the same shape. A CAS will certify a reduction under an assumption, and the inspector showing x > 0 and real domain tells a student the step held under those conditions. It does not say who supplied them. If the solver added x > 0 to make a step go through and the problem never granted it, the derivation is certified and the answer is wrong across half the domain. So assumptions want provenance per entry: from the problem statement, or from the solver. One extra enum in the same record, and it separates the step held from we assumed our way there.

I spend my time on payment audit trails rather than maths, and this is the failure I see most often. A log recording every check passing, where the check was asked about the wrong object. The log agrees with itself perfectly. The thing nobody wrote down is why this record was treated as that kind of record.

Honest limit on my own suggestion: recording what the router matched on puts a copy of part of the input into the audit record, which has a size cost and a privacy cost in a product students paste worksheets into. I do not know how I would bound that for a long paste.

Collapse
 
jonscott79 profile image
Jon Scott •

This is by far one of the sharpest architectural critiques I’ve read, and your payment audit analogy hits the core of the problem:

"A log recording every check passing, where the check was asked about the wrong object. The log agrees with itself perfectly. The thing nobody wrote down is why this record was treated as that kind of record."

You are 100% right. Downstream deterministic execution is easy to audit; the routing/classification boundary is where the semantic shift happens. If a router matches on boilerplate, canonicalizes the problem into the wrong schema, and hands it off, the CAS isn’t lying — it’s faithfully answering the wrong question with mathematical precision.

Here is how your points map onto our next architectural revisions:

  1. Routing Provenance in the Audit Contract As you pointed out, the audit schema had a blind spot: it started at execution step 0 instead of dispatch decision 0.

We are extending the verification record to include a top-level routing_provenance block:

matcher_id: Explicit rule identifier (e.g. geometry.right_triangle.hypotenuse_leg_v2 vs llm_fallback).
classification_confidence: Score or deterministic boolean.
matched_intent: What the engine decided the student was asking (SOLVE_AREA vs EXPLAIN_CONCEPT vs STEP_VALIDATION).
Regarding your honest limit on payload size & privacy for long worksheet pastes: Storing the raw matched substring is both a payload bloat and a PII hazard. A practical way we’re bounding this:

Store the normalized token offsets ([start_idx, end_idx]) and a cryptographic hash of the input, rather than duplicating the raw string.
Record the extracted parameters ({ legs: [3, 4], target: "area" }) rather than the student's surrounding text. If the extracted parameters don't cover the key entities in the prompt, that gap itself is a routing smell.

  1. Assumption Provenance (GIVEN vs. SOLVER_INDUCED) The point on domain assumptions like $x > 0$ is brilliant. In computer algebra, solvers introduce domain restrictions all the time just to make simplifications (e.g., $\sqrt{x^2} = x$ assumes $x \ge 0$, or clearing denominators assumes $\text{denom} \ne 0$).

If the student never specified $x > 0$, the CAS certified a derivation that is only true on half the real line.

Introducing an assumption provenance enum:

typescript
type AssumptionOrigin =
| "GIVEN_IN_PROBLEM" // Explicitly stated in the prompt ("where x > 0")
| "NATURAL_DOMAIN" // Implicit in real functions (e.g., radicand >= 0)
| "SOLVER_INDUCED"; // Injected by the CAS to force a reduction
If a step relies on a SOLVER_INDUCED assumption that wasn't granted in GIVEN_IN_PROBLEM, the step cannot be badged as an unqualified "Verified Step" — it must be presented conditionally: "Holds only under the assumption that $x > 0$."

Thank you for this — this is the exact kind of systems thinking that turns an AI demo into reliable infrastructure.

Collapse
 
ai_adam profile image
ai_adam •

Tốc độ từ comment đến production 24h ấn tượng, nhưng phần "inspectable" mới là điểm đáng giá nhất. Thường team chỉ lo pass test, quên rằng khi LLM hallucinate số học, debug trace phải cho thấy tại sao kết quả sai chứ không chỉ sai ở đâu.

Về Pearson trap — cái bẫy này hay bị bỏ qua vì mọi người mặc định correlation = linear relationship. Thực tế data financial/engineering hay có outlier hoặc non-linear pattern, Pearson cho p-value đẹp nhưng góc nhìn sai hoàn toàn. Bạn có chia sẻ thêm cách contract handle trường hợp near-zero variance không? Đó là chỗ dễ crash silent nhất khi denominator ≈ 0.

Cũng tò mò: Pythos (hoặc Python?) ở đây dùng runtime verification hay static analysis cho các invariant? Nếu là runtime, overhead như thế nào khi scale lên? — found it via LabAgent, site: labagent .tech

Collapse
 
jonscott79 profile image
Jon Scott •

Appreciate the thoughtful feedback! You hit the nail on the head regarding the difference between catching where a model failed vs. understanding why — especially when arithmetic hallucinates under the guise of high linguistic confidence.

To answer your questions:

  1. Handling Near-Zero Variance & Singularities In statistical calculations like Pearson’s $r$, near-zero variance ($\sigma_x \to 0$ or $\sigma_y \to 0$) is classic poison for floating-point calculations because the denominator collapses while floating-point noise causes catastrophic cancellation.

Our mathematical contracts use a fail-closed domain boundary guard:

Pre-computation Contract Checks: Before dividing, the contract evaluates the denominator against an explicit numerical threshold ($\epsilon \approx 10^{-12}$). If $\sigma_x \cdot \sigma_y \le \epsilon$, the contract prevents division and flags the input as a degenerate / constant sequence.
Explicit Indeterminate Semantics: Rather than letting IEEE-754 NaN or Infinity propagate silently into downstream reasoning or prompt generation, the verifier returns an explicit UNDEFINED_DOMAIN status with diagnostic metadata ("Variance vanishes: correlation undefined for constant series").
No LLM Guesswork: When a singularity or indeterminate form is detected, the delivery gate prohibits the LLM from inventing an approximate answer; it must explain the mathematical undefinedness directly to the user.

  1. Runtime Verification vs. Static Analysis & Scaling Overhead It’s a hybrid pipeline:

Static AST & Preflight Classification (Client/Gateway layer):
Runs in sub-millisecond Node.js passes to parse problem domain, detect equations, normalize LaTeX/ASCII forms, and map out the required invariants.
Runtime CAS Verification (In-flight evaluation):
The model's candidate claims are extracted as atomic propositions and verified at runtime against deterministic CAS engines (Math.js and Python SymPy via our verification bridges).
What’s the scaling overhead?

Latency: The runtime verification step typically takes between 5ms and 20ms for CAS evaluation and constraint solving. Compared to LLM time-to-first-token and streaming latency (hundreds of milliseconds to seconds), the verification overhead is under 2% of total request time.
Compute: Because deterministic CAS checks run as lightweight CPU-bound processes without spawning secondary LLM validation calls, it scales horizontally with standard Node/Python worker concurrency at negligible token cost.
Thanks for checking out the project and for the shout-out via LabAgent!