FlawCheck: an NDT inspection copilot

FlawCheck turns decades of ultrasonic thickness readings into a three-page inspection report you can defend. You upload a CSV of UT measurements, the app walks you through mapping columns, a pure TypeScript engine computes every engineering value, and an optional language model writes the narrative around those numbers, citing only from a vetted allowlist.

Inspiration

Inspection crews measure steel for years, and the data accumulates in spreadsheets. When a report finally gets written, the interesting part, one corroding location out of forty, often appears on page 19 of a 20-page boilerplate dump. We wanted a tool that starts with the exception, states the math, and signs off like a field engineer would. And we wanted every number to be checkable by hand, because that is what makes a report defensible.

How we built it

The core decision was separation: TypeScript computes, the LLM narrates. All formulas live in lib/calc and load their coefficients from a criteria JSON file that is the single source of truth. The NVIDIA Nemotron model on Nebius Token Factory receives evaluated results only, writes the interpretation, and may cite clauses from a renderer-locked allowlist. It cannot compute, override, or invent a value.

The workflow is ingest, review, evaluate, report. The bundled WBT-S6 fixture is a real dataset from Zenodo record 16780668 (Pudar, 2025): 4,912 ultrasonic readings from a crude-oil tank inspection programme; the demo uses the 420 readings for one tank.

Required thickness takes the larger of the ASME B31.3 pressure branch and the API 574 structural floor, with $A$ the future corrosion allowance:

$$ t_{req} = \max\left(\frac{P D}{2(SEW + PY)} + A,\; t_{struct}\right) $$

Corrosion rates follow the API 570 short-term and long-term definitions. A rate is quarantined when the interval is too short, the change is below gauge resolution, or the wall appears to grow:

$$ CR_{LT} = \frac{t_{init} - t_{actual}}{\Delta t_{LT}}, \qquad CR_{ST} = \frac{t_{prev} - t_{actual}}{\Delta t_{ST}} $$

Remaining life uses the validated governing rate and is bounded below at zero:

$$ RL = \max\left(0,\; \frac{t_{actual} - t_{req}}{CR_{gov}}\right) $$

Verdicts respect the gauge uncertainty $\varepsilon = 0.10\ \text{mm}$. FAIL when $t < t_{req}$. RE-CHECK when $t_{req} \le t \le t_{req} + \varepsilon$, or when any active data-quality anomaly exists. ACCEPT only when $t > t_{req} + \varepsilon$ and no anomaly is active.

Challenges

The dataset has no ground truth. Zenodo gives measurements, not expected verdicts, so we built golden fixtures with hand-written derivations: each expected value traces to arithmetic a judge can repeat.

The original PDF was a 20-page repetition factory. The critique called out 398 boilerplate conclusion lines burying the one real flaw. We rebuilt the report as three pages: an exception-first triage page, a Master CML Register with exactly one row per location, and a trend, audit, and sign-off page. The register rows fit one line each, which matters because the old layout split words across lines (RE-TIRED, INSPEC-TION).

A silent regression cost us a day. Adding new quality flags to the engine without updating the report schema made the PDF export fail with HTTP 400 on the app's own sample data. Tests stayed green because they built snapshots by hand. We now run the real fixture through the real route and check the schema matches the engine's flag union. A green suite and a working product are different things; we verify both.

What we learned

Deterministic arithmetic and governed AI is a stronger answer than either alone. If a number is safety-relevant, the model should explain it, never create it. And the report is the product: shrinking 20 pages to 3, deduplicating the CML register, and keeping every cell on one line changed how the work reads more than any feature did.

Try it

Live: ndt-flawcheck-ai.netlify.app
Source: github.com/ahmed13576/flawcheck

Built With

Share this project:

Updates