← all benchmarks

TrueMend vs. Semgrep on FastAPI

TRUEMEND
10.9%
SEMGREP
17.1%
CONFIRMED · CERTIFIED ROUND 7

Started as a loss and flipped twice on fresh certifications before rounds 6 and 7 closed it: a harness fix (function-aware adjudication windows), ten shared-detector fixes, and three quarantines. 10.9% is at our own <10% bar, not under it, and we say so. Same-judge trajectory: 21.9% → 12.9% → 10.9%; the round-5-to-6 delta includes a judge change, disclosed. Real-rate stays open: 27.7%, with Semgrep at 39.0% on the same judge.

Round 7 certification, full pool: TrueMend 10.9% wrong-match vs. Semgrep 17.1% — TrueMend now clearly beats Semgrep on wrong-match, at our own <10% bar. Real-rate remains the open gap (27.7% vs. Semgrep's 39.0%).

We don't only publish the wins. Through round 2, apples-to-apples, Semgrep came out ahead on this one — and we think showing that plainly is more useful to you than a headline number that hides it. Rounds 3-7 below update the picture; read on for the current numbers.

The full-pool result (through round 2)

Judged against Netflix's open-source Dispatch app (a real, sizeable FastAPI production codebase), on the whole finding pool: TrueMend 24.3% wrong-match, Semgrep 12.2%. Semgrep won this comparison. We didn't frame that as a "different philosophy" — it was a real gap, and rounds 3-7 below show it wasn't closed by fixing FastAPI-specific detectors alone.

Why the aggregate number is worse than the FastAPI surface itself

The 24.3% figure is dominated by detectors that have nothing to do with FastAPI — SQLAlchemy patterns, Docker checks, generic TypeScript/JSX rules picked up from the app's frontend assets. This campaign targeted TrueMend's FastAPI-specific detectors, not that whole surface. Scoped to just the fastapi-* categories, after two rounds of fixes: 5.1% wrong-match — a result that would clear our own 10% bar on its own, and one Semgrep has no directly comparable scoped number to set against, since it doesn't ship a FastAPI-specific ruleset.

What got fixed, and what didn't

Round 1 measured a version of the detector surface that was structurally blind to 97% of the app's actual routes (@app.*-only patterns against a codebase that's 1 @app.* and 277 @router.*) — round 2 widened that first, then fixed six real precision bugs: a missing async def gate that made fastapi-ux-sync-orm-in-async fire on ordinary synchronous services (472 false positives, now zero), a broken negative-lookahead regex in the response-model check that only evaluated once instead of per character (164 false positives collapsed to 8), and four more of similar shape. Two detectors were withdrawn rather than patched further, disclosed as coverage cuts, not precision wins: fastapi-ux-no-pagination (the corpus has zero .all() calls in view files once scoped to real route context — a real gap between the detector's claim and this app's shape, not a bug we could fix with a regex) and fastapi-ux-sync-def-handler (correct but never worth fixing on a codebase that's intentionally sync end-to-end).

Rounds 3–5 — a fresh certification flips the lead, then a real fix holds

The full-pool numbers above (24.3% TrueMend vs. 12.2% Semgrep) didn't hold either. Round 3 (2026-07-27) started with an interim "still-emitted proxy" measurement that reported 9.4% wrong-match after a fix pass — a number that turned out to be unreliable by construction: the proxy only re-checks findings at file/line/category triples a prior sample already judged, so it's blind to any fresh instance the fix pass didn't happen to touch. A fresh, from-scratch blind sample (1065 TrueMend findings, judged blind alongside a newly-drawn Semgrep sample — so the round-2 12.2% Semgrep number above isn't like-for-like with what follows) certified the real number instead, and it flipped which tool comes out ahead: TrueMend 24.4% wrong-match, Semgrep 29.3%. The wrong findings were dominated by five detectors that have nothing to do with FastAPI routes: sqla_no_polymorphic (8/8 wrong), ai_config_model_in_code (8/8), python-mutable-class-var (8/8), sqla_cascade_delete_orphan and semantic-orm-writes-no-transaction (8/8 each).

Round 4 (commit ec2ea357) fixed six of them: sqla_no_polymorphic (49→0, demoted to catalog_only — the pattern needs cross-class inheritance resolution a line-scan can't do), ai_config_model_in_code (11→0, test/mock file exclusion), python-mutable-class-var (570→417 combined with Django's share, Pydantic BaseModel field-declaration skip), sqla_cascade_delete_orphan (33→8, scalar-only uselist=False per SQLAlchemy's own cascade docs), python-sq-s2275-assert-message (23→0, the pattern was matching the correct idiom instead of the actual bug shape), and orm-writes-no-transaction (recognizes .commit()/.rollback() as a valid transaction boundary, not just .begin()).

Round 5 certified the fixes held on a fresh full-pool sample:

RoundTrueMend judgedTrueMend wrong-matchSemgrep judgedSemgrep wrong-match
Round 2 (interim, full pool)129224.3%4112.2%
Round 3 (fresh, from-scratch)106524.4%4129.3%
Round 5 (post round-4 fixes)103022.7%4126.8%

sqla_cascade_delete_orphan improved cleanly (8/8 wrong on the round-3 sample → 2/8 on the round-5 fresh sample). python-mutable-class-var improved only partially (8/8 → 4/8): the Pydantic BaseModel check doesn't resolve indirect project base classes like DispatchBase/CaseBase that themselves inherit BaseModel one level removed — exactly the failure mode round 4's own report flagged as possible. New top-wrong categories round 4 never touched: docker-version-arg-not-used, semantic-python-too-many-locals, slop-formulaic-error-handling.

Round 5, certified on a fresh full-pool sample (1030 TrueMend findings, 41 Semgrep findings, judged blind): TrueMend 22.7% wrong-match, Semgrep 26.8%. TrueMend led on that axis. On the other axis it didn't: Semgrep 34.1% worth-fixing/real-rate vs. TrueMend's 22.0% — a higher share of what Semgrep flagged was worth acting on. One axis each. Not yet a clean win — rounds 6 and 7 below are what closed it.

Round 6 — a measurement artifact, and six real bugs

Two independent root causes drove the round-5 number, and round 6 fixed both. First, a measurement artifact, not a detector defect: the adjudication harness cut a flat line ± 35 excerpt, which can't show a judge the 100–150-line function body that whole-function-claim categories (too-many-locals/statements/branches, cyclomatic complexity) actually assert against — so a judge marked genuinely correct findings "wrong" purely for excerpt reasons. Verified on signal/service.py::update (lines 472–621): it genuinely has 31 locals / 91 statements (ast-verified), a correct finding judged wrong. Fixed with a shared function-aware adjudication window that expands to the full enclosing Python function for structural categories — a harness fix, not a detector change, so it carries no recall loss and raises adjudication fidelity for every future campaign.

Second, six real line-scan precision bugs in shared (non-FastAPI) detectors, each root-caused against Dispatch source and recall-verified: ts-any-cast (5/5 wrong → 0, was parsing raw .vue/.svelte SFC content as TypeScript), docker-version-arg-not-used (7/7 → 0, framework build-tool env prefixes like VITE_/NEXT_PUBLIC_ are consumed implicitly and shouldn't require an explicit $-dereference), python-mutable-class-var (4/8 → 0 on this corpus, dunder config attrs and factory_boy declarative fields excluded), exception-no-chain (6/8 → 0, bare re-raises chain automatically via __context__), unguarded-attribute-access (6/8 → 0, the Google API client builder .get(...).execute() idiom excluded), and bash-printf-echo-posix (3/8 → 0, bash-shebang scripts aren't targeting POSIX portability).

ToolJudgedCorrectWrong-matchWorth-fixingReal-rate
TrueMend99287.1%12.9%27.6%27.6%
Semgrep4187.8%12.2%34.1%34.1%

Judge caveat, disclosed honestly: round 5 was Claude-judged, round 6 is glm-4.7-judged, so the 22.7%→12.9% delta reflects both the fixes and the judge change — a same-judge re-adjudication of round 5's sample was attempted but proved infeasible (its old flat-window batches exceed glm-4.7's context window). What isn't judge-dependent: raw finding counts on Dispatch dropped sharply from the fixes alone (bash-printf-echo-posix 49→0, docker-version-arg-not-used 15→0, python-mutable-class-var 34→11; 6859→6738 total, −121 false positives). TrueMend edged out Semgrep on wrong-match (12.9% vs 12.2%) — within noise, on the same corpus and judge, but near our own bar rather than under it.

Round 7 — three quarantines and the bar

One more fix, three quarantines, and a logging-recognition broadening — each a deliberate judgment call verified against Dispatch source, not a count-driven sweep. python-no-type-hints (3/8 → 0) had a negative_pattern that only excluded built-in types, so def f(common: CommonParameters) was flagged "no type hints" despite having one — broadened to recognize any name: <identifier> annotation. Three detectors were quarantined, not window-tweaked, because each one's claim is a whole-construct property no bounded line-scan can scope: css-cursor-pointer-div (4/5 wrong — whether a CSS class is interactive depends on a .vue template's @click binding, invisible to a stylesheet scan), pg_plpgsql_no_inline (4/5 wrong — "simple function" needs whole-function-body control-flow analysis a LANGUAGE plpgsql anchor can't see), and slop-formulaic-error-handling (5/7 wrong — the claim is a cross-function repetition count no single-function adjudication window can show a judge). except-no-logging (2/3 wrong → 0) was broadened to recognize traceback.print_exc() and click.secho(...) as surfacing a failure, not hiding it.

ToolJudgedCorrectWrong-matchWorth-fixingReal-rate
TrueMend97689.1%10.9%27.7%27.7%
Semgrep4182.9%17.1%39.0%39.0%

TrueMend now clearly beats Semgrep on wrong-match (10.9% vs 17.1%), same corpus, same judge. The same-judge (glm-4.7) trajectory: 21.9% (round 5, partial) → 12.9% (round 6) → 10.9% (round 7). Raw TrueMend findings on Dispatch: 6859 → 6738 → 6468 (−391 false positives removed across rounds 6–7). No wrong-match category exceeds 3-of-8 sampled anymore — the residual is a long tail of one-off judgment calls, not a dominant cluster.

At our own wrong-match bar (10.9% vs our <10% target), and ahead of Semgrep. We're calling that honestly, not rounding it down: 10.9% is not under 10%, it's at it. One finding here was a judge error, not a detector bug, and we left it unchanged rather than "fix" it to match a wrong verdict: sqla_cascade_delete_orphan was judged wrong, but SQLAlchemy's own documentation confirms single_parent=True is genuinely required for uselist=False + delete-orphan — the finding is correct.

Still open

Real-rate is the harder, still-open bar: 27.7% vs. our >70% target, and Semgrep isn't clean on it either (39.0%, same judge) — glm-4.7 marks most correct findings on this corpus as technically-true-but-not-worth-fixing for both tools, the same judge-calibration pattern documented on our React campaign. The FastAPI-route-specific surface (5.1%, rounds 1–2 above) remains a genuine, separate win, unrelated to this gap. Closing real-rate needs judge-calibration work across the shared surface, not another FastAPI-scoped precision round — the line-scan-tractable wrong-match gap here is largely closed.