<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Na'aman Hirschfeld (Goldziher)</title>
    <description>The latest articles on DEV Community by Na'aman Hirschfeld (Goldziher) (@nhirschfeld).</description>
    <link>https://dev.to/nhirschfeld</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F988181%2F746d781b-8ccb-4694-beaa-2f7e33782cee.jpeg</url>
      <title>DEV Community: Na'aman Hirschfeld (Goldziher)</title>
      <link>https://dev.to/nhirschfeld</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nhirschfeld"/>
    <language>en</language>
    <item>
      <title>300+ languages doesn't mean what you think: three tiers of code understanding</title>
      <dc:creator>Na'aman Hirschfeld (Goldziher)</dc:creator>
      <pubDate>Sat, 08 Aug 2026 09:28:50 +0000</pubDate>
      <link>https://dev.to/nhirschfeld/300-languages-doesnt-mean-what-you-think-three-tiers-of-code-understanding-3k3f</link>
      <guid>https://dev.to/nhirschfeld/300-languages-doesnt-mean-what-you-think-three-tiers-of-code-understanding-3k3f</guid>
      <description>&lt;p&gt;Every tool in this space leads with a language count. basemind's README says 300+. Serena says 40+.&lt;br&gt;
aider says 130+. codebase-memory-mcp says 158.&lt;/p&gt;

&lt;p&gt;Those numbers are not comparable, because they are not measuring the same thing. Most of them are&lt;br&gt;
counting &lt;strong&gt;grammars&lt;/strong&gt; - how many languages the tool can parse into a syntax tree. Parsing is the&lt;br&gt;
cheap part. It tells you where the functions are. It does not tell you which &lt;code&gt;get&lt;/code&gt; this &lt;code&gt;get&lt;/code&gt; is.&lt;/p&gt;

&lt;p&gt;So here is the honest version of basemind's number, split by what you actually get.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Languages&lt;/th&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full resolution&lt;/td&gt;
&lt;td&gt;JavaScript, TypeScript&lt;/td&gt;
&lt;td&gt;oxc&lt;/td&gt;
&lt;td&gt;Scope + import/export resolution, intra- and cross-file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full resolution&lt;/td&gt;
&lt;td&gt;Python, Java&lt;/td&gt;
&lt;td&gt;stack-graphs &lt;code&gt;.tsg&lt;/code&gt; rules&lt;/td&gt;
&lt;td&gt;Name binding over the parse tree, cross-file via specifier resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope only&lt;/td&gt;
&lt;td&gt;everything else&lt;/td&gt;
&lt;td&gt;tree-sitter &lt;code&gt;locals&lt;/code&gt; query&lt;/td&gt;
&lt;td&gt;Intra-file lexical binding, no imports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Symbols + calls&lt;/td&gt;
&lt;td&gt;~100 grammars&lt;/td&gt;
&lt;td&gt;tree-sitter &lt;code&gt;tags.scm&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Symbol and call extraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parse only&lt;/td&gt;
&lt;td&gt;371&lt;/td&gt;
&lt;td&gt;tree-sitter-language-pack&lt;/td&gt;
&lt;td&gt;A file map. JSON, YAML and TOML produce no symbols at all&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three languages get real resolution. About a hundred get symbols. The rest parse. That is a much&lt;br&gt;
less impressive sentence than "300+ languages," and it is the one that survives contact with a user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just run a language server
&lt;/h2&gt;

&lt;p&gt;The obvious objection: language servers already solve this, correctly, with a real type checker.&lt;br&gt;
rust-analyzer knows things about your code that no amount of tree-sitter will ever recover.&lt;/p&gt;

&lt;p&gt;That is true, and I want to concede it up front rather than have it raised in a comment thread.&lt;br&gt;
For generics, macros, dynamic dispatch, dependency injection and monkey-patching, an LSP-backed tool&lt;br&gt;
like Serena - or an SCIP-backed one like Sourcegraph - will be right where basemind is wrong.&lt;/p&gt;

&lt;p&gt;What you pay for that is a build. A language server needs your project to compile, or close to it: a&lt;br&gt;
resolved dependency tree, a populated &lt;code&gt;node_modules&lt;/code&gt;, a &lt;code&gt;cargo build&lt;/code&gt; that has actually run. It needs&lt;br&gt;
a server process per language, each with its own startup cost and its own memory. On a polyglot&lt;br&gt;
monorepo that means several daemons, several indexes, several cold starts, and a hard failure the&lt;br&gt;
moment the build is broken - which, if an agent is halfway through editing, it usually is.&lt;/p&gt;

&lt;p&gt;basemind is optimizing for a different constraint: an agent asking a hundred questions per session&lt;br&gt;
about a repo that may not compile, in a language mix nobody planned for. So the design bet is &lt;strong&gt;no&lt;br&gt;
build step, no language server, no per-language daemon&lt;/strong&gt; - and the honest cost of that bet is that&lt;br&gt;
resolution is an approximation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody else shipped
&lt;/h2&gt;

&lt;p&gt;For JavaScript and TypeScript, the answer is easy and good: &lt;a href="https://oxc.rs" rel="noopener noreferrer"&gt;oxc&lt;/a&gt;. &lt;code&gt;oxc_semantic&lt;/code&gt;&lt;br&gt;
builds a real scope tree, symbol table and reference bindings; &lt;code&gt;oxc_resolver&lt;/code&gt; does Node and tsconfig&lt;br&gt;
module resolution, including solution-style monorepo path aliases. It is fast, actively developed,&lt;br&gt;
and funded. A monorepo rarely imports by relative path - it writes &lt;code&gt;@app/hooks/useSettings&lt;/code&gt; - and&lt;br&gt;
&lt;code&gt;oxc_resolver&lt;/code&gt; handles that natively.&lt;/p&gt;

&lt;p&gt;For Python and Java there was no equivalent, so basemind uses &lt;strong&gt;stack-graphs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Stack graphs come out of GitHub research: Douglas Creager's&lt;br&gt;
&lt;a href="https://arxiv.org/pdf/2211.01224" rel="noopener noreferrer"&gt;&lt;em&gt;Name resolution at scale&lt;/em&gt;&lt;/a&gt;. The idea is that name binding can be&lt;br&gt;
expressed as a graph-reachability problem, with per-language rules written declaratively in a DSL&lt;br&gt;
(&lt;code&gt;.tsg&lt;/code&gt;) that runs against a tree-sitter parse tree. No type checker, no build, and - critically -&lt;br&gt;
the per-file work is independent, so it caches.&lt;/p&gt;

&lt;p&gt;It is a genuinely good idea, and it is also abandoned. GitHub &lt;strong&gt;archived the repository on&lt;br&gt;
2025-09-09&lt;/strong&gt; with the note that it is no longer supported and that you should fork it if you want to&lt;br&gt;
keep going. The &lt;code&gt;tree-sitter-stack-graphs&lt;/code&gt; crate has not had a release since v0.10.0 in December 2024.&lt;/p&gt;

&lt;p&gt;I am not the first person to look at this. aider evaluated stack-graphs and went with tree-sitter&lt;br&gt;
tags plus PageRank instead (&lt;a href="https://github.com/Aider-AI/aider/issues/534" rel="noopener noreferrer"&gt;#534&lt;/a&gt;). So did&lt;br&gt;
OpenHands (&lt;a href="https://github.com/OpenHands/OpenHands/issues/742" rel="noopener noreferrer"&gt;#742&lt;/a&gt;) and&lt;br&gt;
SWE-agent (&lt;a href="https://github.com/SWE-agent/SWE-agent/issues/38" rel="noopener noreferrer"&gt;#38&lt;/a&gt;). Three serious teams reached the&lt;br&gt;
same conclusion: too much work, unmaintained upstream, not worth it.&lt;/p&gt;

&lt;p&gt;They were probably right about the cost. Here is what taking it on actually involved.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four rule bugs that silently deleted a file's entire resolution
&lt;/h3&gt;

&lt;p&gt;The upstream Python rules abort the &lt;em&gt;whole file's&lt;/em&gt; stack-graph build on several perfectly ordinary&lt;br&gt;
constructs. Not "resolve that one line wrong" - the file produces no resolution at all, quietly. A&lt;br&gt;
sweep over a real Python codebase surfaced four:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;typed_parameter&lt;/code&gt; matched too broadly.&lt;/strong&gt; A typed splat parameter - &lt;code&gt;**kwargs: T&lt;/code&gt;, &lt;code&gt;*args: T&lt;/code&gt; -
bound the splat pattern as if it were a plain name. Fixed by restricting the rule to
identifier-named params.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyword arguments in a class header were treated as superclasses.&lt;/strong&gt; &lt;code&gt;class X(TypedDict, total=False)&lt;/code&gt;
and anything with &lt;code&gt;metaclass=…&lt;/code&gt; broke. Fixed by restricting the superclass list to identifiers,
attributes and subscripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parameter-less lambdas got no call node.&lt;/strong&gt; The combined function/lambda stanza required a
&lt;code&gt;parameters&lt;/code&gt; field, so &lt;code&gt;lambda: x&lt;/code&gt; was skipped entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chained assignment referenced an undefined output.&lt;/strong&gt; &lt;code&gt;a = b = c&lt;/code&gt; nests, and the inner assignment
had no &lt;code&gt;.output&lt;/code&gt; to flow from. Fixed by having every assignment carry one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With those four fixed, that codebase goes to &lt;strong&gt;100% of files building&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two panics on the stitching hot path
&lt;/h3&gt;

&lt;p&gt;A panic while resolving one file must never take down a scan of an 82k-file repository, and two&lt;br&gt;
upstream ones could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Database::get_incoming_path_degree&lt;/code&gt; indexed a lazily-grown supplemental arena directly, which
panics for any node that is not already the end node of some partial path in the database. It now
reports &lt;code&gt;Degree::Zero&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ForwardPartialPathStitcher::extend&lt;/code&gt; called &lt;code&gt;.expect()&lt;/code&gt; on the cycle detector. The cycle test
replays a &lt;em&gt;suffix&lt;/em&gt; of the path against freshly minted stack variables, which can legitimately fail
to unify. An undecidable cycle test now discontinues the path - the same conservative outcome as a
detected cycle - instead of panicking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both have regression tests. Alongside that, the scanner persists the code map &lt;em&gt;before&lt;/em&gt; running the&lt;br&gt;
optional lanes and wraps each lane in &lt;code&gt;catch_unwind&lt;/code&gt;, so a per-file panic in the resolve pass costs&lt;br&gt;
that file's edges and nothing else. It used to cost the entire map, which meant gigabytes of blobs&lt;br&gt;
sitting behind a &lt;code&gt;file_count&lt;/code&gt; of 0 and a full re-scan on every launch.&lt;/p&gt;

&lt;p&gt;The fork is four workspace crates - &lt;code&gt;stack-graphs&lt;/code&gt;, &lt;code&gt;tree-sitter-graph&lt;/code&gt;, &lt;code&gt;tree-sitter-stack-graphs&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;lsp-positions&lt;/code&gt; - carrying upstream copyright, ported to tree-sitter 0.26 and edition 2024, with the&lt;br&gt;
C FFI, serde, visualization and storage modules stripped because none of that surface is reachable&lt;br&gt;
when you build the graph in memory and stitch it in-process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-file is where the interesting caching lives
&lt;/h2&gt;

&lt;p&gt;Intra-file resolution links a use to its definition inside one file. That result is deterministic in&lt;br&gt;
the file's bytes, so it is cached in a content-addressed blob next to the code map: unchanged file,&lt;br&gt;
no re-analysis.&lt;/p&gt;

&lt;p&gt;Cross-file resolution is the opposite. It resolves an import's module specifier - Node/tsconfig for&lt;br&gt;
JS/TS, dotted and relative path arithmetic for Python, fully-qualified names over Maven and Gradle&lt;br&gt;
source roots for Java - then joins the imported name against the target file's exports, following&lt;br&gt;
re-export chains through package &lt;code&gt;__init__.py&lt;/code&gt; files and TypeScript barrels up to eight hops.&lt;/p&gt;

&lt;p&gt;That result is &lt;em&gt;not&lt;/em&gt; a function of one file's bytes. So it is deliberately &lt;strong&gt;not&lt;/strong&gt; cached in the&lt;br&gt;
per-file blob. If a file is unchanged but its dependency moved, the edges still get re-stitched. The&lt;br&gt;
incremental path restages only the changed files' intra-file facts and re-stitches only the affected&lt;br&gt;
importers - the changed files plus everything that imports one.&lt;/p&gt;

&lt;p&gt;Caching the thing that is safe to cache, and refusing to cache the thing that looks similar but&lt;br&gt;
isn't, is most of what makes this fast enough to sit under an agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contract that matters more than the resolution
&lt;/h2&gt;

&lt;p&gt;Here is the design decision I would defend hardest, and it is a decision to do &lt;em&gt;less&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;callers&lt;/code&gt; never narrows to the resolved subset.&lt;/p&gt;

&lt;p&gt;It returns every call site a plain name scan would return - the complete, no-scope floor - and&lt;br&gt;
&lt;em&gt;annotates&lt;/em&gt; each hit with whether resolution proved it binds to this definition, plus a&lt;br&gt;
&lt;code&gt;resolved_total&lt;/code&gt; count. It does not filter down to the proven ones.&lt;/p&gt;

&lt;p&gt;That looks wrong. Filtering to proven hits is more precise, and precision is the whole point of&lt;br&gt;
building a resolver.&lt;/p&gt;

&lt;p&gt;It is wrong because resolution cannot see through a module-object import - &lt;code&gt;from pkg import mod&lt;/code&gt;&lt;br&gt;
then &lt;code&gt;mod.f()&lt;/code&gt; - or an unresolvable path alias. Those are real callers that resolution will not&lt;br&gt;
prove. Filter to the proven subset and you return a smaller set with no indication anything is&lt;br&gt;
missing.&lt;/p&gt;

&lt;p&gt;We know exactly how that fails, because it did. Resolution saw &lt;strong&gt;2 of 172 real call sites&lt;/strong&gt; and the&lt;br&gt;
tool reported &lt;code&gt;total: 2&lt;/code&gt;, with no truncation flag. A confident, complete-looking, wrong answer. A&lt;br&gt;
Python case returned &lt;strong&gt;7 of roughly 4,000&lt;/strong&gt;. An agent acting on either would conclude a function had&lt;br&gt;
almost no callers and refactor accordingly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;resolved: false&lt;/code&gt; is not evidence a hit is not a real caller. It is evidence resolution could not&lt;br&gt;
prove it either way. So the sound floor is the answer, and precision is an annotation on top. Filter&lt;br&gt;
on &lt;code&gt;resolved&lt;/code&gt; when you want confidence; trust &lt;code&gt;total&lt;/code&gt; when you need completeness.&lt;/p&gt;

&lt;p&gt;The same principle runs through the provenance model on the code graph: every edge carries a&lt;br&gt;
confidence - extracted, inferred, or ambiguous - derived on read, and it degrades &lt;em&gt;downward&lt;/em&gt;. A&lt;br&gt;
binding whose proof is unreachable is reported as inferred, never as falsely extracted. Import and&lt;br&gt;
inheritance edges are name-resolved by construction, so they can never claim to be extracted at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this still doesn't do
&lt;/h2&gt;

&lt;p&gt;Java qualified member calls on an imported type - &lt;code&gt;Foo.greet()&lt;/code&gt; - are unresolved, and&lt;br&gt;
&lt;code&gt;import static&lt;/code&gt; is not distinguished from a type import. Both are misses, never wrong answers, which&lt;br&gt;
is the failure mode the contract above is built to guarantee.&lt;/p&gt;

&lt;p&gt;basemind is read-only. It will tell you every caller of a symbol; it will not rename it for you.&lt;br&gt;
Serena does symbol-level editing and refactoring, and its users lean on that heavily.&lt;/p&gt;

&lt;p&gt;And the PageRank-over-a-tree-sitter-symbol-graph idea for ranking repo architecture is not mine -&lt;br&gt;
aider &lt;a href="https://aider.chat/2023/10/22/repomap.html" rel="noopener noreferrer"&gt;published that in October 2023&lt;/a&gt;. basemind blends&lt;br&gt;
git churn into the weighting, which is an increment on their work, not a replacement for it.&lt;/p&gt;




&lt;p&gt;The number on the box is 371 languages. The number that describes what the tool understands is&lt;br&gt;
three, plus about a hundred that give up symbols, plus a long tail that parses. Both are true. Only&lt;br&gt;
one of them is useful when you are deciding whether to install something.&lt;/p&gt;

&lt;p&gt;basemind is MIT, pure Rust, and lives at github.com/Goldziher/basemind.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>programming</category>
      <category>software</category>
    </item>
    <item>
      <title>Scythe: SQL as a first-class language (and where that leaves your ORM)</title>
      <dc:creator>Na'aman Hirschfeld (Goldziher)</dc:creator>
      <pubDate>Wed, 05 Aug 2026 10:42:22 +0000</pubDate>
      <link>https://dev.to/nhirschfeld/scythe-sql-as-a-first-class-typed-language-and-where-that-leaves-your-orm-1fhh</link>
      <guid>https://dev.to/nhirschfeld/scythe-sql-as-a-first-class-typed-language-and-where-that-leaves-your-orm-1fhh</guid>
      <description>&lt;p&gt;&lt;em&gt;I build and maintain &lt;a href="https://github.com/Goldziher/scythe" rel="noopener noreferrer"&gt;scythe&lt;/a&gt; (MIT). This is the argument behind it: why SQL deserves the same tooling as the rest of your code, what generating your data access layer from it replaces, and the one case where it doesn't.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Every app has a data access layer. The question is who writes it
&lt;/h2&gt;

&lt;p&gt;Whether or not anyone on your team calls it that, your application has a data access layer: the code that maps parameters in, maps result rows out, keeps the types on both sides aligned, and gets rewritten every time a query or the schema changes. It is tedious, it is where a lot of quiet bugs live, and nobody enjoys maintaining it.&lt;/p&gt;

&lt;p&gt;There are roughly five ways teams get one. A full ORM writes it for you at runtime and hides it. A data mapper writes most of it and asks you to be explicit. A query builder gives you the pieces to assemble — this is what people usually mean when they say "we skipped the ORM and wrote our own DAL". Raw SQL plus manual mapping means you write every line by hand. And then there is the option this article is about: &lt;strong&gt;generate the layer from the SQL itself, at build time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;sqlc did this cleanly for Go. You write plain &lt;code&gt;.sql&lt;/code&gt; files, it reads your schema and your queries, and it generates typed Go functions and structs before anything compiles. No runtime ORM, no DSL, just SQL in and a typed access layer out. It is one of the nicer developer experiences in the Go ecosystem, and it is the tool scythe is most directly inspired by.&lt;/p&gt;

&lt;p&gt;The obvious question is why that idea should be locked to one language. Scythe is my answer: the same SQL-first, compile-time approach, generating an idiomatic layer for 10 languages, with a real type-inference engine and a SQL quality toolchain underneath it.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQL as a first-class language
&lt;/h2&gt;

&lt;p&gt;Here is the thing that bothers me about how most codebases treat SQL. Your Python gets a formatter, a linter, a type checker, and a pre-commit hook that blocks the bad version from ever reaching a branch. Your &lt;code&gt;.sql&lt;/code&gt; files, holding the queries that decide whether the product is fast or correct, get none of that. They sit in a directory as untyped strings, reviewed by eye.&lt;/p&gt;

&lt;p&gt;SQL is a 50-year-old language every database speaks, expressive and heavily optimized. Treating it as a first-class language in your repo means giving it exactly what everything else already has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A formatter.&lt;/strong&gt; &lt;code&gt;scythe fmt&lt;/code&gt; normalizes style so SQL diffs show intent instead of whitespace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A linter.&lt;/strong&gt; 23 schema-aware rules — &lt;code&gt;UPDATE&lt;/code&gt; without &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;= NULL&lt;/code&gt; instead of &lt;code&gt;IS NULL&lt;/code&gt;, ambiguous columns in joins, leading-wildcard &lt;code&gt;LIKE&lt;/code&gt;, unbounded &lt;code&gt;ORDER BY&lt;/code&gt; — plus sqruff's 69 style rules underneath.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A security auditor.&lt;/strong&gt; &lt;code&gt;scythe audit&lt;/code&gt; is static analysis for SQL: &lt;code&gt;GRANT ALL&lt;/code&gt;, grants to &lt;code&gt;PUBLIC&lt;/code&gt;, &lt;code&gt;SECURITY DEFINER&lt;/code&gt; without a pinned &lt;code&gt;search_path&lt;/code&gt;, literal passwords, &lt;code&gt;SELECT *&lt;/code&gt; over PII. 35 rules, emitting SARIF with CWE tags so findings land in GitHub code scanning like any other scanner's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-commit hooks.&lt;/strong&gt; &lt;code&gt;scythe-fmt&lt;/code&gt;, &lt;code&gt;scythe-lint&lt;/code&gt;, and &lt;code&gt;scythe-audit&lt;/code&gt; ship as hooks, so bad SQL fails at commit time rather than in review or production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Types.&lt;/strong&gt; Which is the part that needs a real example.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The boundary between a query and your program's types is where this has always been weakest, so that is where the interesting work is. Here is the case that shows why the inference has to be genuine — a query with a &lt;code&gt;LEFT JOIN&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- @name GetUserOrders&lt;/span&gt;
&lt;span class="c1"&gt;-- @returns :many&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The right side of a &lt;code&gt;LEFT JOIN&lt;/code&gt; is nullable. A user with no orders still produces a row, with &lt;code&gt;total&lt;/code&gt; and &lt;code&gt;notes&lt;/code&gt; as NULL. If your generated types say those columns are non-null, you have a crash waiting for the first user without an order. Scythe knows this, and encodes it in every language it targets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;sqlx::FromRow)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;GetUserOrdersRow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;rust_decimal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GetUserOrdersRow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decimal&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;GetUserOrdersRow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same query, three languages, and in each one &lt;code&gt;total&lt;/code&gt; and &lt;code&gt;notes&lt;/code&gt; come out correctly optional. You did not annotate anything. The nullability fell out of reading the SQL.&lt;/p&gt;

&lt;p&gt;That inference is the core of the tool, and it goes well past joins: &lt;code&gt;COALESCE&lt;/code&gt;, &lt;code&gt;CASE&lt;/code&gt; branch widening, window functions (&lt;code&gt;ROW_NUMBER&lt;/code&gt;, &lt;code&gt;LAG&lt;/code&gt;, &lt;code&gt;LEAD&lt;/code&gt;, and friends each with their own nullability), aggregates, CTEs including recursive ones, and &lt;code&gt;RETURNING&lt;/code&gt; clauses. Postgres enums, arrays, and composite types map to native language types too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where scythe goes past sqlc
&lt;/h2&gt;

&lt;p&gt;sqlc is excellent and I want to be precise about the difference. Scythe is wider on two axes.&lt;/p&gt;

&lt;p&gt;Breadth: it generates native, idiomatic code for &lt;strong&gt;10 languages&lt;/strong&gt; (Rust, Python, TypeScript, Go, Java, Kotlin, C#, Elixir, Ruby, PHP) across &lt;strong&gt;10 databases&lt;/strong&gt; (PostgreSQL, MySQL, MariaDB, SQLite, DuckDB, CockroachDB, Redshift, SQL Server, Oracle, Snowflake) with &lt;strong&gt;70+ backend drivers&lt;/strong&gt;. Adding a language or a driver is mostly a declarative type-mapping manifest, not a plugin in a separate repo.&lt;/p&gt;

&lt;p&gt;Depth on the SQL itself: sqlc infers nullability primarily from column constraints. Scythe infers it from the query structure (the join and expression analysis above). And it treats SQL as code you lint, format and audit, which sqlc leaves to other tools: &lt;strong&gt;58 built-in rules&lt;/strong&gt; (23 lint, 35 audit) with sqruff's 69 style rules layered underneath, plus &lt;code&gt;scythe inspect&lt;/code&gt;, which runs operational health checks against a live database — foreign keys with no covering index, tables with policies but RLS disabled, duplicate indexes. All of it runs in the compile pipeline, so bad SQL is caught before code generation rather than at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one job ORMs still do better
&lt;/h2&gt;

&lt;p&gt;There are ORMs I genuinely like. SQLAlchemy is probably the most sophisticated data-access library in any language, largely because Core and ORM are separable layers rather than one monolith. Drizzle fits its ecosystem well. Ecto is a better-designed thing than the category it gets filed under. This is not an argument that the category was a mistake.&lt;/p&gt;

&lt;p&gt;It is an argument about which layer you want, for this app. And there is exactly one job an ORM does that scythe does not answer: &lt;strong&gt;bring-your-own-database portability.&lt;/strong&gt; If your product lets the user pick the database, so the same application code has to run on PostgreSQL or MySQL or SQLite depending on deployment, an ORM abstracts those dialect differences at runtime and that is genuinely valuable.&lt;/p&gt;

&lt;p&gt;Scythe takes the opposite bet on purpose. You write SQL for a specific engine, which means you get that engine's real features (Postgres arrays, MySQL JSON functions, whatever you actually deploy on) and let its optimizer do its job. The cost is that targeting multiple engines means multiple SQL files.&lt;/p&gt;

&lt;p&gt;So the honest split is simple. If you must run on whatever database the customer brings, use an ORM. If you control the database, which most teams building most products do, then a full ORM is mostly overhead: N+1 queries from lazy loading, SQL you cannot see or predict, model-versus-schema drift, and weak types exactly where SQL gets interesting — aggregates, conditional expressions, nullable joins.&lt;/p&gt;

&lt;p&gt;There is also a related decision people conflate with this one, and shouldn't: how thick a layer you put over whatever you chose. Repository, DAO, Unit of Work, or nothing. That is orthogonal — you can put a repository over raw SQL or over Hibernate, and teams do both. It is also where a lot of wasted effort lives, since wrapping a full ORM in a repository is largely rebuilding what a Data Mapper with a Unit of Work already is. The pattern earns its keep further down, where you are the one deciding what a "user" is when it comes back from three joined tables.&lt;/p&gt;

&lt;p&gt;For a single-engine architecture on Postgres, generating the layer removes the boilerplate and the runtime dependency, kills a class of hidden query-generation bugs, and leaves you with code you can actually read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more with coding agents
&lt;/h2&gt;

&lt;p&gt;There is a newer reason this approach wins, and it is about who reads the code now. A coding agent working in your repo reasons about plain, explicit, statically typed code far better than it reasons about ORM runtime behavior. With scythe the SQL is right there in a file, the generated function has concrete argument and return types, and the compiler and linter catch a mistake immediately. There is no lazy-loading, no identity map, no query builder assembling strings at runtime for the agent (or you) to simulate in its head. Legible, static, deterministic output is easier for a human to review and easier for a model to get right. SQL-first codegen produces exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What scythe deliberately does not do
&lt;/h2&gt;

&lt;p&gt;Static queries are the trade. Scythe does not build queries dynamically at runtime the way jOOQ's DSL does. If you need conditional composition you write separate queries, or push the condition into SQL (&lt;code&gt;WHERE ($1 IS NULL OR status = $1)&lt;/code&gt;). That is a deliberate choice: static queries are the ones you can lint, analyze, and optimize. If runtime query composition is central to your app, jOOQ or an ORM query builder is the better fit, and if you are a Go-only shop already invested in sqlc, staying there is completely reasonable.&lt;/p&gt;

&lt;p&gt;Coming from sqlc, &lt;code&gt;scythe migrate sqlc.yaml&lt;/code&gt; reads your existing config and emits a &lt;code&gt;scythe.toml&lt;/code&gt; to get you started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;scythe-cli
&lt;span class="c"&gt;# or prebuilt binaries:&lt;/span&gt;
cargo binstall scythe-cli
&lt;span class="c"&gt;# or&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;Goldziher/tap/scythe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To hold the line at commit time, in &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/Goldziher/scythe&lt;/span&gt;
  &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v0.11.0&lt;/span&gt;
  &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scythe-fmt&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scythe-lint&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scythe-audit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/Goldziher/scythe" rel="noopener noreferrer"&gt;https://github.com/Goldziher/scythe&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://goldziher.github.io/scythe" rel="noopener noreferrer"&gt;https://goldziher.github.io/scythe&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Discord: &lt;a href="https://discord.gg/xt9WY3GnKR" rel="noopener noreferrer"&gt;https://discord.gg/xt9WY3GnKR&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it against a gnarly query, a chain of CTEs, a pile of window functions, a join graph that should confuse the nullability, I want to know where it gets the types wrong. That is the feedback the inference engine gets built from.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>rust</category>
      <category>database</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Knip Keeps My JS/TS Dependencies Honest (I Wish Python Had It)</title>
      <dc:creator>Na'aman Hirschfeld (Goldziher)</dc:creator>
      <pubDate>Fri, 24 Jul 2026 12:32:53 +0000</pubDate>
      <link>https://dev.to/nhirschfeld/knip-keeps-my-jsts-dependencies-honest-i-wish-python-had-it-11fp</link>
      <guid>https://dev.to/nhirschfeld/knip-keeps-my-jsts-dependencies-honest-i-wish-python-had-it-11fp</guid>
      <description>&lt;p&gt;Every long-lived JS/TS project I've worked on accumulates the same three kinds of rot:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies in &lt;code&gt;package.json&lt;/code&gt; that nothing imports anymore.&lt;/strong&gt; You added &lt;code&gt;moment&lt;/code&gt;, migrated off it, and the line stayed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exports nothing consumes.&lt;/strong&gt; A function was public once, the last caller was deleted, the &lt;code&gt;export&lt;/code&gt; is still there advertising an API with no users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whole files that fell out of the import graph&lt;/strong&gt; but never got deleted, so every new engineer reads them trying to understand code that runs nowhere.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this breaks the build. That's exactly why it survives. The compiler is happy, the tests pass, and the dead weight compounds quietly until onboarding takes a week and your bundle ships code no user will ever execute.&lt;/p&gt;

&lt;p&gt;The tool I've settled on for this is &lt;a href="https://knip.dev/" rel="noopener noreferrer"&gt;knip&lt;/a&gt; (by Lars Kappert). I run it on the enterprise codebase I maintain and on basically every other TS project I touch. One command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npx knip
&lt;span class="go"&gt;
Unused files (2)
  src/legacy/formatValue.ts
  src/hooks/useLegacyModal.ts
Unused dependencies (3)
  lodash        package.json
  moment        package.json
  @types/uuid   package.json
Unused exports (5)
  parseLegacy   src/parse.ts:42:14
  toLegacyDate  src/date.ts:9:14
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Files, dependencies, and exports in one pass, cross-referenced against the actual import graph. It's the first tool I've used that treats all three as the same problem, which they are: something is declared, nothing uses it, delete it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest caveat
&lt;/h2&gt;

&lt;p&gt;Knip is not zero-config on a real codebase. Anything resolved dynamically, runtime &lt;code&gt;import()&lt;/code&gt;, plugin systems, framework entry points it doesn't recognize (Next.js pages, a CLI bin, config files loaded by string), gets flagged as unused when it isn't. You will get false positives on day one.&lt;/p&gt;

&lt;p&gt;The fix is a &lt;code&gt;knip.json&lt;/code&gt; that names your real entry points, and after that it's accurate. But budget an afternoon to tune it before you trust the output enough to delete on it. Anyone who tells you it's instant hasn't run it on a large app.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually want
&lt;/h2&gt;

&lt;p&gt;Here's the part that bugs me. This problem is not specific to JavaScript. Python &lt;code&gt;pyproject.toml&lt;/code&gt; files rot the same way. Go modules accumulate unused &lt;code&gt;require&lt;/code&gt;s. Rust &lt;code&gt;Cargo.toml&lt;/code&gt; grows dependencies that no &lt;code&gt;use&lt;/code&gt; references anymore.&lt;/p&gt;

&lt;p&gt;Knip is genuinely good, and it's stuck being JS/TS-only because it leans on the TypeScript compiler's understanding of the module graph. I'd trade a lot for the same three-in-one report, unused files, unused deps, unused exports, in Python and everywhere else. There are partial tools (&lt;code&gt;deptry&lt;/code&gt;, &lt;code&gt;vulture&lt;/code&gt;, &lt;code&gt;cargo-udeps&lt;/code&gt;), but nothing that does all three with knip's precision and single-command ergonomics.&lt;/p&gt;

&lt;p&gt;If you write JS/TS and you haven't run knip on your repo, do it this week. If you're building tooling and looking for a real gap: this, for every other language.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>tooling</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Stop Writing Specs for Your Agents</title>
      <dc:creator>Na'aman Hirschfeld (Goldziher)</dc:creator>
      <pubDate>Thu, 23 Jul 2026 07:24:23 +0000</pubDate>
      <link>https://dev.to/nhirschfeld/stop-writing-specs-for-your-agents-3fan</link>
      <guid>https://dev.to/nhirschfeld/stop-writing-specs-for-your-agents-3fan</guid>
      <description>&lt;p&gt;Spec-driven development is having a moment. Write a detailed spec, hand it to the agent, get working code. It's a clean story and it maps neatly onto how we already think about requirements.&lt;/p&gt;

&lt;p&gt;I think it's the wrong shape, and the failure mode isn't obvious until you're six months in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Specs rot at four different speeds
&lt;/h2&gt;

&lt;p&gt;Here's the actual problem with a spec: it isn't one document. It's four documents wearing a trench coat.&lt;/p&gt;

&lt;p&gt;A typical spec contains, all interleaved:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Why we're building this&lt;/strong&gt; — the product requirement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How the system is structured&lt;/strong&gt; — the architectural decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The order of operations&lt;/strong&gt; — the implementation plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What "done" looks like&lt;/strong&gt; — the contract.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of these has a completely different half-life.&lt;/p&gt;

&lt;p&gt;The product requirement changes when a customer complains. The architectural decision holds for years — and when it doesn't, the &lt;em&gt;record&lt;/em&gt; of it still matters, because someone will ask why the old thing looked like that. The implementation plan is dead the moment the PR merges. The contract only matters if it's executable, because anything else is a lie waiting to happen.&lt;/p&gt;

&lt;p&gt;Put them in one file and the file decays at the speed of its fastest-rotting component. Within a couple of sprints, that document is 30% accurate. It's the worst possible state — confidently wrong, and now indistinguishable from the parts that were right.&lt;/p&gt;

&lt;p&gt;That was bad enough when only humans read it. Now you have agents ingesting it as ground truth. A stale spec doesn't just confuse a new hire who'll ask a follow-up question in Slack. It actively steers an agent into building against a system that no longer exists, and the agent won't push back, because it has no way to know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation should be layered by lifetime, not by topic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what I actually run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: Product requirements → the ticket system
&lt;/h2&gt;

&lt;p&gt;GitHub Issues, Linear, Jira. Inside larger orgs, PRDs and RFCs.&lt;/p&gt;

&lt;p&gt;These are &lt;em&gt;supposed&lt;/em&gt; to churn. That's not a defect, it's the job. Tickets have closed states, owners, and timestamps, which makes their staleness legible — a ticket from eight months ago is visibly a ticket from eight months ago.&lt;/p&gt;

&lt;p&gt;Critically: this layer does not belong in the repo. The moment you mirror product requirements into markdown next to your code, you've created two sources of truth that will diverge, and the one in git will lose — because nobody closes a markdown file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: Architectural decisions → ADRs in git
&lt;/h2&gt;

&lt;p&gt;This is the layer that actually earns its place in version control.&lt;/p&gt;

&lt;p&gt;I use &lt;a href="https://adr.github.io/madr/" rel="noopener noreferrer"&gt;MADR&lt;/a&gt;. Each decision gets a numbered file: context, options considered, decision, consequences. Decisions are never edited to reflect new reality — they're superseded by a new record that references the old one.&lt;/p&gt;

&lt;p&gt;Two things fall out of this that matter enormously for agents:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's append-only, so it can't rot.&lt;/strong&gt; An ADR describes a decision made at a point in time. It's accurate forever, by construction. "We chose Postgres over DynamoDB in March 2024 for these reasons" doesn't stop being true when you migrate — you just add ADR-0042 marking 0017 as superseded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It captures the &lt;em&gt;why&lt;/em&gt;, which is the thing agents cannot infer.&lt;/strong&gt; An agent can read your code and reconstruct &lt;em&gt;what&lt;/em&gt; it does. It cannot reconstruct why you rejected the obvious alternative. That's the highest-value context you can possibly hand it, and it's exactly what specs bury under implementation detail.&lt;/p&gt;

&lt;p&gt;When an agent asks "should I add a queue here?", the ADR directory is the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: Implementation plans → ephemeral, never committed
&lt;/h2&gt;

&lt;p&gt;This is where I differ most sharply from the spec-driven crowd.&lt;/p&gt;

&lt;p&gt;The implementation plan is scaffolding. It exists to get from "we agreed on the approach" to "the code works." Then it should die.&lt;/p&gt;

&lt;p&gt;Modern agents handle this natively now — Claude and Codex both maintain working plans internally and iterate against them. Before that, I'd write a &lt;code&gt;todo.md&lt;/code&gt;, work through it, and delete it. Same thing, worse ergonomics.&lt;/p&gt;

&lt;p&gt;The important part is that it stays out of git. A committed implementation plan is a fossil that looks like a map. Six months later someone — or some agent — reads it as a description of the system, when it was only ever a description of an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: Contracts → black-box integration tests
&lt;/h2&gt;

&lt;p&gt;Prose cannot define a contract. Prose describes a contract, and the description drifts.&lt;/p&gt;

&lt;p&gt;What actually defines my code at the contract level is a suite of integration tests exercising it from the outside in — no internal mocks, no reaching into private state. Call the public surface, assert on observable behavior.&lt;/p&gt;

&lt;p&gt;This is interface-first design. It gives you three things at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A specification that &lt;strong&gt;cannot&lt;/strong&gt; go stale, because CI fails when it does.&lt;/li&gt;
&lt;li&gt;A refactoring boundary — internals are free to change as long as the black box holds.&lt;/li&gt;
&lt;li&gt;A verification loop an agent can run itself, without a human in the middle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is underrated. An agent with executable contracts can self-correct. An agent with a prose spec can only guess whether it complied, and will report success either way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is not TDD.&lt;/strong&gt; TDD means writing tests and defining correctness up front, before the implementation. Sometimes that's right. Often you don't know the correct interface until you've built enough to feel where it's wrong, and committing to a test suite prematurely just means you refactor tests instead of code. What I'm claiming is narrower: &lt;em&gt;by the time it's merged&lt;/em&gt;, the contract is expressed as executable black-box tests. How you got there is your business.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Lives in&lt;/th&gt;
&lt;th&gt;Lifetime&lt;/th&gt;
&lt;th&gt;Who reads it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Product requirements&lt;/td&gt;
&lt;td&gt;Ticket system&lt;/td&gt;
&lt;td&gt;Churns constantly&lt;/td&gt;
&lt;td&gt;Humans, mostly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architectural decisions&lt;/td&gt;
&lt;td&gt;Git (MADR)&lt;/td&gt;
&lt;td&gt;Permanent, append-only&lt;/td&gt;
&lt;td&gt;Humans + agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implementation plan&lt;/td&gt;
&lt;td&gt;Agent context / scratch file&lt;/td&gt;
&lt;td&gt;Dies with the PR&lt;/td&gt;
&lt;td&gt;Agent, briefly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contract&lt;/td&gt;
&lt;td&gt;Integration test suite&lt;/td&gt;
&lt;td&gt;Enforced by CI&lt;/td&gt;
&lt;td&gt;CI, agents, humans&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four artifacts. Four locations. Four decay rates, each matched to a medium that handles it.&lt;/p&gt;

&lt;p&gt;Nothing here is novel on its own — ADRs are a decade old, black-box testing is older than that. The claim is about the separation itself: that the layering &lt;em&gt;is&lt;/em&gt; the methodology, and that collapsing it into a single spec document is the thing quietly poisoning your agent's context window.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable part
&lt;/h2&gt;

&lt;p&gt;The pitch for spec-driven development is that you write one document and the agent does the rest. What I'm describing is more work up front. You maintain a ticket system, you write ADRs, you build real integration tests.&lt;/p&gt;

&lt;p&gt;But you were going to do all three anyway. Spec-driven development doesn't remove that work — it duplicates it into a fifth artifact that has no owner and no expiry date, and then feeds that artifact to a machine that can't tell it's out of date.&lt;/p&gt;

&lt;p&gt;Write less documentation. Put each piece where it can't lie to you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>documentation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Debugging basemind with Claude Code</title>
      <dc:creator>Na'aman Hirschfeld (Goldziher)</dc:creator>
      <pubDate>Wed, 08 Jul 2026 16:53:10 +0000</pubDate>
      <link>https://dev.to/nhirschfeld/debugging-basemind-with-claude-code-4c92</link>
      <guid>https://dev.to/nhirschfeld/debugging-basemind-with-claude-code-4c92</guid>
      <description>&lt;p&gt;basemind is my code-intelligence tool: a pure-Rust code map and scanner, tree-sitter across 300+ languages, a content-addressed blob store, a Fjall-backed inverted index. I harden it by pointing it at a repo big enough to hurt, a 20M-line, 200k-commit monorepo, and watching what it does.&lt;/p&gt;

&lt;p&gt;I did this debugging session with Claude Code. It started as "which version is installed?" and ended with a 7x speedup, two upstream PRs, and a feature turned off. Here is how I worked, and the methods that made it effective.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom is not the bug
&lt;/h2&gt;

&lt;p&gt;The report: basemind's cache grew from 1.8 GB to 4 GB while CPU pinned for a long time. I did not open the scanner code. I looked at the live system. &lt;code&gt;ps&lt;/code&gt; and &lt;code&gt;lsof&lt;/code&gt; found a process at ~70% CPU holding the index open, and it was a &lt;em&gt;serve&lt;/em&gt; process, not a &lt;em&gt;scan&lt;/em&gt;. Different bug entirely.&lt;/p&gt;

&lt;p&gt;That process was an old release. So I had Claude dig through git instead of the debugger: &lt;code&gt;git log&lt;/code&gt; on the watcher, then &lt;code&gt;git merge-base --is-ancestor&lt;/code&gt; and commit dates. A commit titled "fix: stop watcher CPU runaway" was dated four days after the running binary shipped. The bug (a watcher re-scanning its own writes forever) was already fixed. It was a stale process left over from another editor session.&lt;/p&gt;

&lt;p&gt;Two rules, both applied before reading a line of source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verify the running artifact, not a proxy.&lt;/strong&gt; Homebrew, the PATH, and the editor plugin each reported a different version. Find the one that actually runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Live bug or old build?" is the first question.&lt;/strong&gt; Dating the fix against the running version can end an investigation before it starts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Measurement tells a scan from a leak
&lt;/h2&gt;

&lt;p&gt;I killed the ghost, and a fresh scan appeared at 488% CPU, nearly five cores. That is rayon pegging every core, and it is indistinguishable from a runaway loop on a single reading.&lt;/p&gt;

&lt;p&gt;So I sampled three signals over time: CPU%, cache size, blob count. Disk climbed then plateaued. Blob writes burst then stopped. CPU fell to one core. That is a one-time rebuild converging, not a leak. A leak keeps allocating, a loop keeps writing, a scan converges. The derivative is the diagnosis, not the number.&lt;/p&gt;

&lt;p&gt;To watch without babysitting, I had Claude write background monitors: shell loops sampling every 30s, printing only on a state transition. One fired on "SCAN COMPLETE + IDLE," another on "0% CPU and no disk growth for 4 minutes." The alert encodes the hypothesis: a hang is low CPU &lt;em&gt;and&lt;/em&gt; no progress, a healthy scan is high CPU &lt;em&gt;or&lt;/em&gt; growing disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolate the confound
&lt;/h2&gt;

&lt;p&gt;A multi-worktree test then hung: scans wedged at 0% CPU, and &lt;code&gt;lsof&lt;/code&gt; on a stuck process itself hung for two minutes. A hanging &lt;code&gt;lsof&lt;/code&gt; means a stuck socket. The last log line was &lt;code&gt;Registering VLM OCR backend&lt;/code&gt;. The OCR tier made a network call with no timeout, a firewall dropped it, and the connection blocked forever.&lt;/p&gt;

&lt;p&gt;I did not debug the hang in place. That tier is opt-in behind a cargo feature, so I built basemind with default features, a binary that cannot make the call, and studied the scanner in isolation. When two failures tangle, change one variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read for the mechanism, then measure the fix
&lt;/h2&gt;

&lt;p&gt;On a clean binary I took a baseline. A fresh worktree scan took 181s, re-parsed all 67,700 files though their blobs already existed, and rebuilt the git-history index per worktree at ~160s. Now I read code, to confirm the mechanism, not guess a patch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The "unchanged file" fast paths keyed off the per-worktree index, empty on a fresh worktree. So every file fell through to a full re-parse, despite its blob already sitting in the shared store.&lt;/li&gt;
&lt;li&gt;The git-history index opened at the worktree's own cache dir, though it derives entirely from the shared &lt;code&gt;.git&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both fixes followed patterns already in the code, and each got a regression test written to fail first. Re-measured: 181s to 25s, ~7x, all 67,700 files reused, peak RSS about halved. The re-measurement is the point. It turns "should be faster" into "is 7x faster."&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow the failure across the boundary
&lt;/h2&gt;

&lt;p&gt;The network hang still deserved a fix. I traced it: the OCR and embedding models download through &lt;code&gt;hf_hub&lt;/code&gt;, whose builder in 0.4 and 0.5 constructs a &lt;code&gt;ureq&lt;/code&gt; agent with no timeout. A firewalled download blocks forever. The root cause was in the dependency.&lt;/p&gt;

&lt;p&gt;I forked it and had Claude map every download call site across two crates, then add a watchdog: run the blocking fetch on a detached thread, error on a tunable deadline, degrade by skipping the model instead of hanging. The mechanical multi-file edit went to a subagent with a tight spec. Then I read the diff and reran the build and tests myself. The subagent even corrected my assumption about a type not being &lt;code&gt;Clone&lt;/code&gt;. That shipped as an upstream PR plus an honest bug report. Delegate the mechanical work, verify with diffs and reruns, never a summary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sometimes the fix is deletion
&lt;/h2&gt;

&lt;p&gt;Monitoring caught the embedding pass stuck on a general-purpose model, an empty model dir with a stale lock, spinning a core. The sharper question was not why it was slow, but whether code embeddings should exist at all.&lt;/p&gt;

&lt;p&gt;I traced what gets embedded (symbol, signature, doc, body) against what embeds it (a general-English model), and basemind already builds a BM25 index over the same text. A prose model embeds code weakly, the one real win (natural language to symbol) is already covered by the keyword lane, and the cost is a 100 MB pull plus gigabytes of vectors. Verdict: code embeddings off by default, kept for docs and images. A feature that is off cannot hang, leak, or pin a core.&lt;/p&gt;

&lt;h2&gt;
  
  
  The methodology
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Observe the live system (ps, lsof, du) before reading source.&lt;/li&gt;
&lt;li&gt;Separate stale-artifact bugs from live bugs by dating the fix against the running version.&lt;/li&gt;
&lt;li&gt;Measure dynamics, not snapshots. The derivative separates converging from leaking.&lt;/li&gt;
&lt;li&gt;Encode the hypothesis into a monitor so the machine watches for you.&lt;/li&gt;
&lt;li&gt;Isolate confounds by changing one variable.&lt;/li&gt;
&lt;li&gt;Read code for the mechanism, then write the smallest fix.&lt;/li&gt;
&lt;li&gt;Prove it with a test that fails without the fix, and re-measure the metric you started from.&lt;/li&gt;
&lt;li&gt;Follow failures into dependencies, and report them honestly.&lt;/li&gt;
&lt;li&gt;Ask whether the costly feature should exist, not just why it broke.&lt;/li&gt;
&lt;li&gt;Delegate the mechanical work to an agent, but verify every diff yourself.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Worktree cache reuse: cold scan 181s to 25s (~7x), 67,700 files reused, peak RSS about halved.&lt;/li&gt;
&lt;li&gt;Code embeddings off by default, per-file &lt;code&gt;embed_exclude&lt;/code&gt;, safe preset swapping.&lt;/li&gt;
&lt;li&gt;An upstream timeout guardrail in the dependency, plus a bug report.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The method I kept coming back to: verify the artifact, watch the live system, measure the change over time, isolate the confound, read for the mechanism, and prove the fix by the number you started from. basemind lives at &lt;a href="https://github.com/Goldziher/basemind" rel="noopener noreferrer"&gt;github.com/Goldziher/basemind&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>performance</category>
      <category>debugging</category>
      <category>ai</category>
    </item>
    <item>
      <title>One Rust core, native SDKs in 16 languages: how we stopped hand-writing bindings</title>
      <dc:creator>Na'aman Hirschfeld (Goldziher)</dc:creator>
      <pubDate>Mon, 06 Jul 2026 08:47:33 +0000</pubDate>
      <link>https://dev.to/nhirschfeld/one-rust-core-native-sdks-in-16-languages-how-we-stopped-hand-writing-bindings-4mik</link>
      <guid>https://dev.to/nhirschfeld/one-rust-core-native-sdks-in-16-languages-how-we-stopped-hand-writing-bindings-4mik</guid>
      <description>&lt;p&gt;&lt;em&gt;Building &lt;a href="https://github.com/xberg-io" rel="noopener noreferrer"&gt;xberg&lt;/a&gt;, a document-intelligence engine with a Rust core, we needed the same engine callable from Python, TypeScript, Ruby, Go, PHP, and a dozen more. This is how we ship native SDKs in every one of them without hand-writing a single binding layer.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The polyglot binding tax
&lt;/h2&gt;

&lt;p&gt;A Rust core is a good decision right up until you have to ship it to other languages. Then you inherit a second, quieter job: keep a PyO3 wrapper, a NAPI-RS addon, a Magnus extension, a cgo layer, and a handful more in lockstep with a core that moves every week.&lt;/p&gt;

&lt;p&gt;That is where polyglot projects rot. The core adds a field, one language's wrapper lags, and nobody notices until a Ruby user files a bug that a Python user never sees. Multiply by sixteen languages and the binding layer becomes the most expensive code in the project, none of which is the actual product.&lt;/p&gt;

&lt;p&gt;We did not want to write that code, let alone maintain it. So we generate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One config, every target
&lt;/h2&gt;

&lt;p&gt;The tool is &lt;a href="https://github.com/xberg-io/alef" rel="noopener noreferrer"&gt;&lt;code&gt;alef&lt;/code&gt;&lt;/a&gt;. It extracts the Rust API surface and emits language-native bindings, package scaffolding, type stubs, docs, and e2e tests from one &lt;code&gt;alef.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[workspace]&lt;/span&gt;
&lt;span class="py"&gt;languages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ffi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"go"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;alef_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.32.7"&lt;/span&gt;

&lt;span class="nn"&gt;[[crates]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"sample_core"&lt;/span&gt;
&lt;span class="py"&gt;sources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"src/lib.rs"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;version_from&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Cargo.toml"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;alef generate &lt;span class="nt"&gt;--format&lt;/span&gt;   &lt;span class="c"&gt;# bindings + type stubs&lt;/span&gt;
alef scaffold            &lt;span class="c"&gt;# package manifests + native build files&lt;/span&gt;
alef verify &lt;span class="nt"&gt;--exit-code&lt;/span&gt;  &lt;span class="c"&gt;# fail CI if generated files are stale&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each target gets an idiomatic package, not a lowest-common-denominator shim:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;PyO3 + type stubs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript / Node&lt;/td&gt;
&lt;td&gt;NAPI-RS addon with &lt;code&gt;.d.ts&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ruby&lt;/td&gt;
&lt;td&gt;Magnus extension&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;cgo over a generated C FFI layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP&lt;/td&gt;
&lt;td&gt;native extension&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elixir / Gleam&lt;/td&gt;
&lt;td&gt;Rustler NIF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swift, Kotlin, C#, Dart, R, Zig, WASM&lt;/td&gt;
&lt;td&gt;native bridge per platform&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sixteen languages, eighteen targets (C FFI and a JNI shim round out the list). The Rust author writes Rust; the Python user gets a dataclass with type stubs, the Go user gets a cgo package, and neither knows the other exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that keeps them honest
&lt;/h2&gt;

&lt;p&gt;Generating bindings is the easy half. The hard half is trust: how do you know all sixteen behave the same after you change the core?&lt;/p&gt;

&lt;p&gt;Alef's answer is shared fixtures. You write one set of JSON fixtures describing inputs and expected outputs, and it generates a cross-language e2e suite plus standalone registry-mode test apps from them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;alef e2e        &lt;span class="c"&gt;# generate the cross-language suite from shared fixtures&lt;/span&gt;
alef test-apps  &lt;span class="c"&gt;# standalone apps that install the published package and run it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a change in the Rust core does not surface as a runtime surprise in one binding. It surfaces as the same failing assertion in every language at once, in CI, before release. The fixtures are the contract, and every SDK is held to it.&lt;/p&gt;

&lt;p&gt;Generated files also carry an Alef hash, so &lt;code&gt;alef verify --exit-code&lt;/code&gt; fails the build if someone hand-edits a generated binding or forgets to regenerate after a core change. The generated layer stays generated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest tradeoffs
&lt;/h2&gt;

&lt;p&gt;This is not free, and it is not magic.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The core has to be designed for it.&lt;/strong&gt; A stable, extractable API surface is a constraint you accept up front. Exotic Rust signatures do not always have a clean home in every target language, and the ones that matter live behind adapters rather than leaking into the core.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idioms live in the adapter, not the core.&lt;/strong&gt; Async wrappers, context managers, callbacks, error types: these are per-language, generated per-language, and configured in &lt;code&gt;[crates.&amp;lt;language&amp;gt;]&lt;/code&gt; sections. Shared semantics stay in the core so they cannot drift; ergonomics stay local.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When codegen is not enough, you extend it.&lt;/strong&gt; Domain-specific generation (an HTTP service API, a plugin registry) is an &lt;code&gt;Extension&lt;/code&gt; trait rather than a fork, shipped as a linked binary, a dynamic library, or template-only blocks in &lt;code&gt;alef.toml&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We build xberg this way because the alternative, sixteen hand-maintained binding layers chasing one moving core, is exactly the kind of work that reads productive and quietly sinks a polyglot project. Generate the boring layer, test it against one fixture set, and spend the human time on the engine that is actually the product.&lt;/p&gt;

&lt;p&gt;Alef is MIT and standalone: &lt;a href="https://github.com/xberg-io/alef" rel="noopener noreferrer"&gt;github.com/xberg-io/alef&lt;/a&gt;. If you maintain a Rust library that people keep asking for bindings to, it is worth a look.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>programming</category>
      <category>opensource</category>
      <category>ffi</category>
    </item>
    <item>
      <title>Code review can't keep up with AI. Build a verification layer instead.</title>
      <dc:creator>Na'aman Hirschfeld (Goldziher)</dc:creator>
      <pubDate>Sun, 05 Jul 2026 10:48:07 +0000</pubDate>
      <link>https://dev.to/nhirschfeld/code-review-cant-keep-up-with-ai-build-a-verification-layer-instead-1oh4</link>
      <guid>https://dev.to/nhirschfeld/code-review-cant-keep-up-with-ai-build-a-verification-layer-instead-1oh4</guid>
      <description>&lt;p&gt;There is a popular argument going around that code review no longer matters. The reasoning is that a model wrote the code, another model can check it, and a human reading the diff is a slow, expensive step you can now skip. I think that gets the problem exactly backwards.&lt;/p&gt;

&lt;p&gt;Code review did not stop mattering. It stopped scaling. You can now generate code faster than any human can read it, let alone reason about it. The old model, where an engineer reading the diff is the last line of defense, quietly broke the moment a single afternoon started producing more change than a person can hold in their head. Skipping review does not fix that. It just removes the one check that was already failing and puts nothing in its place.&lt;/p&gt;

&lt;p&gt;The answer is to move verification onto things that keep up. I have been building xberg, a document-intelligence engine with a Rust core and a dozen language SDKs in one repo, and this is the setup that lets me ship at generation speed without shipping garbage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static analysis and types do the cheap work
&lt;/h2&gt;

&lt;p&gt;The first layer is everything a machine can check without running your program: linting, static analysis, type checks. This is the cheapest verification you will ever buy, and it catches whole classes of errors before a test ever runs. A type error, an unused binding, a dangerous cast, a lint rule you agreed to as a team, none of these need a human to notice.&lt;/p&gt;

&lt;p&gt;For a polyglot repo this is its own project. I ended up building polylint, two self-contained Rust binaries driven by one config, because wiring a separate linter and formatter per language into pre-commit was itself a maintenance tax. The point is not the tool. The point is that mechanical checks should run on every change, locally and in CI, and they should be fast enough that nobody is tempted to skip them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comprehensive tests prove behavior
&lt;/h2&gt;

&lt;p&gt;Static analysis tells you the code is well formed. It does not tell you the code is correct. That is what tests are for, and at generation speed the test suite is doing far more of the reviewing than any human is.&lt;/p&gt;

&lt;p&gt;The tests that earn their keep here are end-to-end and integration tests. Unit tests are useful, but a wall of them can be green while the system as a whole does the wrong thing. What you want is tests that exercise real paths through the software the way a user would: real inputs, real boundaries, real failure modes. When I add a format to xberg's extraction pipeline, the test that matters is the one that runs a real file of that format end to end and checks the output, not the one that mocks the parser and asserts it was called.&lt;/p&gt;

&lt;p&gt;Comprehensive coverage of behavior is what lets you accept a large generated change with confidence. The suite is the reviewer that never gets tired and never skims.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents review against written guidelines
&lt;/h2&gt;

&lt;p&gt;Here is where the "another model can check it" idea is actually right, as long as you give it something to check against. An agent doing a first-pass review is fast, tireless, and consistent, but only if the standard it reviews against is explicit and written down. Vague instructions produce vague reviews.&lt;/p&gt;

&lt;p&gt;So write the guidelines. The conventions, the security rules, the patterns you want and the ones you have banned, the things a reviewer on your team would flag. I keep those rules in one place and generate the per-tool configs from it with ai-rulez, so Cursor, Claude, and Copilot all review against the same standard instead of three slightly different ones. An agent with clear guidelines catches a real fraction of what a human reviewer would, at a speed a human never will, and it frees the human to look at the things that actually need judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then you actually test it, by hand
&lt;/h2&gt;

&lt;p&gt;None of the above removes the need to run the software yourself. This is the step people are most tempted to skip now, and it is the one that catches what everything else misses.&lt;/p&gt;

&lt;p&gt;A green pipeline is a signal. It is not proof that your software does what you think it does. Automated tests only check the things you thought to check. Running the thing yourself, exercising the real paths, poking at the edges, is how you find the assumption the whole test suite quietly shares with the bug. QA is not a box you tick after the checks pass. At generation speed it becomes one of the highest-leverage things an engineer does, because it is the only layer that brings human judgment to bear on software no human wrote line by line.&lt;/p&gt;

&lt;p&gt;Teams that treat QA as a core engineering function, rather than a phase they graduated out of, are the ones this speed will not break.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open source makes the code better, for free
&lt;/h2&gt;

&lt;p&gt;There is one more layer, and it is the strongest one I know of. Open source your code and it gets tested at a scale you could never reproduce in-house. Thousands of people run it in environments you never imagined, on inputs you never thought to generate, and every bug they hit and report comes back and makes the code more robust. No internal test suite competes with real usage at that volume.&lt;/p&gt;

&lt;p&gt;Auditability and good citizenship are real, but the benefit that changes the code itself is simpler. Public code is massively tested code, and massively tested code is better code. Every one of xberg's libraries is open source partly for this reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of it
&lt;/h2&gt;

&lt;p&gt;Put together, the verification layer looks like this: static analysis and types catch the cheap errors, comprehensive end-to-end and integration tests prove the behavior, agents do first-pass review against written guidelines, you test it by hand to catch what the machines missed, and open source turns your users into the largest test suite you will ever have.&lt;/p&gt;

&lt;p&gt;Code review did not become optional. It became a system instead of a person. Build that system, and you can move as fast as the models let you without lying to yourself about whether it works.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>devtools</category>
      <category>softwarequality</category>
    </item>
    <item>
      <title>Deleting the toolchain: linting a polyglot monorepo with one Rust binary</title>
      <dc:creator>Na'aman Hirschfeld (Goldziher)</dc:creator>
      <pubDate>Sat, 04 Jul 2026 08:02:42 +0000</pubDate>
      <link>https://dev.to/nhirschfeld/deleting-the-toolchain-linting-a-polyglot-monorepo-with-one-rust-binary-7om</link>
      <guid>https://dev.to/nhirschfeld/deleting-the-toolchain-linting-a-polyglot-monorepo-with-one-rust-binary-7om</guid>
      <description>&lt;p&gt;A repository that mixes languages pays a tax most single-language repos never see. Python wants ruff, and ruff wants a Python. JavaScript and TypeScript want oxlint or prettier, and those want a Node. Shell wants shfmt and shellcheck. Then taplo for TOML, rumdl for Markdown, sqruff for SQL, hadolint for Dockerfiles, and one more for whatever you added last month. Each tool is installed separately, pinned separately, and wired into &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; separately.&lt;/p&gt;

&lt;p&gt;The result is slow setup, version drift, CI images fat with language runtimes you only need for linting, and a hook stack that breaks the day a system dependency goes missing or bumps a version. I hit this hard while building xberg, a document-intelligence engine with a Rust core and a dozen language SDKs in one repo, which is where the itch to fix it came from.&lt;/p&gt;

&lt;p&gt;polylint is the fix I landed on. It ships two self-contained Rust binaries, &lt;code&gt;poly lint&lt;/code&gt; and &lt;code&gt;poly fmt&lt;/code&gt;, driven by one &lt;code&gt;poly.toml&lt;/code&gt;. No language runtime is required on the default path. The goal was not to add one more tool beside the stack. It was to delete the stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  What using it looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;poly &lt;span class="nb"&gt;fmt&lt;/span&gt; &lt;span class="nt"&gt;--check&lt;/span&gt;
&lt;span class="go"&gt;would format crates/example/src/main.rs

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;poly &lt;span class="nb"&gt;fmt&lt;/span&gt; &lt;span class="nt"&gt;--fix&lt;/span&gt;
&lt;span class="go"&gt;formatted 1 file

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;poly lint &lt;span class="nt"&gt;--format&lt;/span&gt; toon
&lt;span class="go"&gt;path: crates/example/src/main.rs
diagnostics[0]: engine=ruff, code=F401, severity=warning, title="`os` imported but unused"

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;poly hooks &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="go"&gt;installed git hooks: pre-commit, commit-msg
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;poly fmt&lt;/code&gt; is a dry run by default, which is what you want in CI. Add &lt;code&gt;--fix&lt;/code&gt; to write changes. &lt;code&gt;poly hooks install&lt;/code&gt; wires the git hooks once, and lint, format, and commit checks then run on every &lt;code&gt;git commit&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it avoids the runtimes
&lt;/h2&gt;

&lt;p&gt;There are three tiers, and a file falls through them until something can handle it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Curated Rust backends, in process.&lt;/strong&gt; The Rust ecosystem has grown real, high-quality linters and formatters that are libraries, not just CLIs. polylint links them directly: oxc for JS/TS, ruff internals for Python, taplo for TOML, rumdl for Markdown, sqruff for SQL, malva and markup_fmt for CSS and HTML, mago for PHP. Every backend implements one trait and returns one diagnostic shape, so reporting, caching, and the MCP server stay uniform no matter which tool did the work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;trait&lt;/span&gt; &lt;span class="n"&gt;Engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Send&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Sync&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;'static&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Language&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Capabilities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// lint / format / fix&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;// folded into the cache key&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;lint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;SourceFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;EngineConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Diagnostic&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;SourceFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;EngineConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FormatOutput&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A tree-sitter fallback for everything else.&lt;/strong&gt; For languages without a dedicated backend, polylint uses &lt;code&gt;tree-sitter-language-pack&lt;/code&gt;, parses the source, and re-emits it with structural reindentation and whitespace normalization. This covers hundreds of grammars, including Java, using each language's &lt;code&gt;indents.scm&lt;/code&gt; queries. So you get consistent indentation and clean whitespace on a Java file with no JDK on the machine. Grammars where whitespace carries meaning are left untouched on purpose, since a reindent there would corrupt the file rather than tidy it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An opt-in catalog for the rest.&lt;/strong&gt; Some tools have no Rust equivalent yet. For those, polylint vendors a tool catalog derived from the &lt;a href="https://github.com/hougesen/mdsf" rel="noopener noreferrer"&gt;mdsf&lt;/a&gt; project, which maps a tool to its binary, argument vector, and languages. It is data only, so polylint does not depend on or shell out to mdsf itself. Each catalog tool is probed on &lt;code&gt;PATH&lt;/code&gt; and skipped when absent, so enabling one never makes a run fail on a machine that lacks it.&lt;/p&gt;

&lt;p&gt;Configuration for all of this lives in one file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[defaults]&lt;/span&gt;
&lt;span class="py"&gt;line_length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;
&lt;span class="py"&gt;line_ending&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"lf"&lt;/span&gt;

&lt;span class="nn"&gt;[lint.python.ruff]&lt;/span&gt;
&lt;span class="py"&gt;select&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"E"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"F"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"W"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[per-file-ignores]&lt;/span&gt;
&lt;span class="py"&gt;"tests/**"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"F401"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[hooks.builtin]&lt;/span&gt;
&lt;span class="py"&gt;polylint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;polyfmt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;commit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;stages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"commit-msg"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;file_safety&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;[hooks]&lt;/code&gt; table replaces &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; outright. polylint dogfoods it: the commit-msg hook runs Conventional Commit checks and strips AI-attribution trailers, and the file-safety builtin catches merge-conflict markers, large files, leaked private keys, and case collisions, all in pure Rust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it is fast enough to run on every commit
&lt;/h2&gt;

&lt;p&gt;Two things do the work. Nothing spawns a subprocess per file on the default path, so there is no process-startup cost multiplied across a large tree, and the whole run is a rayon parallel loop over files.&lt;/p&gt;

&lt;p&gt;On top of that sits a blake3 result cache. The cache key is not just the file contents. It folds in the engine name, the engine's own version, and the resolved config, alongside the file bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;key = blake3(namespace, engine, engine_version, resolved_config, file_bytes)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That detail matters more than it looks. A plain content hash would happily serve you a stale result after you upgraded a formatter or edited a rule, because the bytes did not change. Folding the tool version and config into the key means an upgrade or a config edit invalidates exactly the affected entries and nothing else.&lt;/p&gt;

&lt;p&gt;In CI, the binary itself is cached by version, OS, and architecture, so a run downloads it once and reuses it until you bump the version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lint and Format&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses: actions/checkout@v4      - uses: Goldziher/polylint@v1      - run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;poly lint .&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;poly fmt --check .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The tradeoffs
&lt;/h2&gt;

&lt;p&gt;I would rather name these than have you find them.&lt;/p&gt;

&lt;p&gt;The binary is large, around 70 MB per platform, because it bundles several big parsers and linters. For a tool you install once and cache in CI, I think that is the right trade against carrying zero system dependencies, but if you expected a 3 MB download, that is not this.&lt;/p&gt;

&lt;p&gt;The coverage is uneven by design. Some backends lint only, some format only, so it is not true that every language gets every capability. The tree-sitter tier is a formatter, not a linter, and it deliberately does nothing on whitespace-sensitive grammars rather than risk a destructive rewrite.&lt;/p&gt;

&lt;p&gt;And it is not fully subprocess-free in every case. &lt;code&gt;rustfmt&lt;/code&gt; and &lt;code&gt;gofmt&lt;/code&gt; are used when present, because reimplementing them in process is not worth it, and a few other native formatters are opt-in. The default path stays runtime-free; those are the documented exceptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;polylint is MIT licensed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;polylint
&lt;span class="c"&gt;# or&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;Goldziher/tap/polylint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source and docs are at &lt;a href="https://github.com/Goldziher/polylint" rel="noopener noreferrer"&gt;github.com/Goldziher/polylint&lt;/a&gt;. If you run a polyglot repo and your pre-commit config has quietly become its own maintenance project, point &lt;code&gt;poly lint&lt;/code&gt; at it and see what falls out.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>devtools</category>
      <category>tooling</category>
      <category>showdev</category>
    </item>
    <item>
      <title>When an LLM response fails validation, feed the error back into the retry</title>
      <dc:creator>Na'aman Hirschfeld (Goldziher)</dc:creator>
      <pubDate>Thu, 02 Jul 2026 05:17:08 +0000</pubDate>
      <link>https://dev.to/nhirschfeld/when-an-llm-response-fails-validation-feed-the-error-back-into-the-retry-2e1e</link>
      <guid>https://dev.to/nhirschfeld/when-an-llm-response-fails-validation-feed-the-error-back-into-the-retry-2e1e</guid>
      <description>&lt;p&gt;If you ask an LLM for structured output and validate it against a schema, you already know the failure&lt;br&gt;
mode: most of the time it is fine, and every so often it hands back something that does not parse or&lt;br&gt;
misses a required field. The usual reflex is to wrap the call in a retry and move on.&lt;/p&gt;

&lt;p&gt;The problem is that a plain retry is the same prompt, the same temperature, roughly the same odds. You&lt;br&gt;
are paying for another round and hoping the dice land differently.&lt;/p&gt;

&lt;p&gt;There is a better move, and it is almost free to add: when validation fails, put the validation error&lt;br&gt;
and the model's own bad output back into the next prompt, and ask it to fix that specific thing.&lt;/p&gt;

&lt;p&gt;Here is the core of the loop I used for this on a RAG platform I built (trimmed to the essential path):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;msgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;msgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;msgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="c1"&gt;# last attempt's error rides along
&lt;/span&gt;        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;make_completion_request&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;msgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;# raises ValidationError on bad output
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;error_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        The last response from the API failed validation due to the following error:
            &amp;lt;error&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;format_error_for_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/error&amp;gt;
        Your task is to fix the error and return the corrected response data:
            &amp;lt;data&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/data&amp;gt;
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details do the work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The error is described for the model, not for a log.&lt;/strong&gt; &lt;code&gt;format_error_for_llm&lt;/code&gt; turns the raw
validation exception into a plain instruction ("field X must be an integer, you sent a string"). The
model is good at patching a concrete, named mistake; it is bad at guessing why an opaque retry keeps
failing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You hand back its own previous output as the thing to correct.&lt;/strong&gt; It is not regenerating from
scratch, it is editing. That keeps the parts that were already right and usually fixes the one field
that was wrong on the first pass.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tradeoffs, because there always are some:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It costs an extra call on a bad response, and the follow-up prompt is longer (it carries the error
plus the prior payload). On a schema that fails often, that adds up. Cap the attempts.&lt;/li&gt;
&lt;li&gt;It only works when the bad response is parseable enough to serialize back into the prompt. Truly
empty or truncated output has nothing to correct, so you still need a normal retry underneath.&lt;/li&gt;
&lt;li&gt;Prompt semantics do not always transfer if you also fail over between providers mid-loop. If you do
that, do not count a provider swap as a real attempt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the whole idea. It is not a framework, it is about ten lines around a call you already have. If&lt;br&gt;
you are generating structured output at any volume, it turns a chunk of your "model was flaky" retries&lt;br&gt;
into first-try-after-feedback successes.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>python</category>
      <category>ai</category>
      <category>rag</category>
    </item>
    <item>
      <title>Benchmark-Driven Development: let agents build the harness you never had time for</title>
      <dc:creator>Na'aman Hirschfeld (Goldziher)</dc:creator>
      <pubDate>Tue, 30 Jun 2026 04:41:22 +0000</pubDate>
      <link>https://dev.to/nhirschfeld/benchmark-driven-development-let-agents-build-the-harness-you-never-had-time-for-5ha4</link>
      <guid>https://dev.to/nhirschfeld/benchmark-driven-development-let-agents-build-the-harness-you-never-had-time-for-5ha4</guid>
      <description>&lt;p&gt;Most teams ship on two signals: does it compile, and do the tests pass. Both are correctness signals. Neither tells you whether the thing is fast, whether it got slower this week, or whether the output is actually good against some ground truth. We close that gap with a third signal, and we let it drive the work. We call it benchmark-driven development.&lt;/p&gt;

&lt;p&gt;The loop is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write the plan and the spec first. Decide what the code should do before writing it.&lt;/li&gt;
&lt;li&gt;Use TDD to drive it to correct. Red, green, refactor, the usual.&lt;/li&gt;
&lt;li&gt;Test it for real with proper benchmarks, against ground truth, with profiling.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Correctness and performance are both requirements. So both get measured, on every change, not once at the end when a customer complains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why benchmarks used to be a luxury
&lt;/h2&gt;

&lt;p&gt;Everyone agrees benchmarks are good. Almost no one builds a serious one. The reason is cost. A real harness is its own project: a fixture corpus with trustworthy ground truth, scoring that reflects quality and not just a checksum, resource sampling, a way to compare across implementations, and CI plumbing to run it all and publish results. That is weeks of work that ships no features. So it gets cut, and teams fall back to a stopwatch around a hot loop and a vibe about whether output looks right.&lt;/p&gt;

&lt;p&gt;This is the part that changed. With an agent in the loop, the harness is no longer a quarter of engineering budget. It is a few focused sessions. You describe the corpus, the metrics, and the comparison you want, and you iterate on the harness the same way you iterate on a feature. The economics flipped. The thing that was too expensive to justify is now the cheap part.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete harness
&lt;/h2&gt;

&lt;p&gt;To make this concrete, here is the benchmark harness we run for xberg, our document-extraction engine. It is a Rust CLI, and it does the following.&lt;/p&gt;

&lt;p&gt;It compares 13 language bindings of our own engine against 7 external reference frameworks, across a corpus of 318 fixtures that carry markdown ground truth, spanning 17 formats: PDF, HTML, DOCX, ODT, RTF, XLSX, CSV, EPUB, PPTX, and more.&lt;/p&gt;

&lt;p&gt;It scores quality two ways, because text and structure fail differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Text F1 (TF1): token-level bag-of-words F1 between extracted text and ground truth, with a separate numeric-token score for number-heavy documents like financial and scientific PDFs.&lt;/li&gt;
&lt;li&gt;Structural F1 (SF1): block-level matching between extracted markdown and ground-truth markdown. Headings, code blocks, formulas, tables, list items, and images are weighted by how much they matter, matched greedily with fuzzy cross-type compatibility, and scored for ordering with a longest-increasing-subsequence pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When markdown ground truth is present it combines them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;quality_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;f1_text&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;f1_numeric&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;f1_layout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It samples CPU and memory during each run, and behind a feature flag it generates flamegraphs so a regression in latency points you straight at the function that caused it. It runs each binding in both single-file mode for fair latency and batch mode for throughput. And it runs as a CI matrix: build, validate ground truth, fan out one job per binding, gate, run the external frameworks, then consolidate everything into percentiles and publish the aggregate as a release artifact.&lt;/p&gt;

&lt;p&gt;None of that is exotic. All of it used to be too much work to justify for anything but a flagship. That is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "real benchmarks" actually means
&lt;/h2&gt;

&lt;p&gt;A benchmark you can drive development with has to clear a higher bar than a stopwatch around a hot loop. Four properties matter.&lt;/p&gt;

&lt;p&gt;Ground truth you trust. A number is only as good as what it is measured against. We keep ground truth in version control, validate its integrity on every CI run, and clean known artifacts so the target does not drift silently. If you cannot defend the reference, you cannot defend the score.&lt;/p&gt;

&lt;p&gt;Metrics that reflect the goal. "Bytes match" is not quality for document extraction. A table flattened into a paragraph can have high text overlap and be useless. That is why structure gets its own score and its own weights. Pick metrics that punish the failures you actually care about.&lt;/p&gt;

&lt;p&gt;Resource truth, not just wall-clock. Throughput hides memory blowups. We sample CPU and memory and keep flamegraphs around so "it got slower" turns into "this function got slower."&lt;/p&gt;

&lt;p&gt;Comparison and regression gates. A score in isolation is trivia. The harness compares pipelines and implementations against each other and against history, and it can fail the build on a quality regression. That is what makes it a development driver rather than a report.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to adopt it without a big-bang rewrite
&lt;/h2&gt;

&lt;p&gt;You do not need 318 fixtures on day one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with ten fixtures and one honest metric. Ten documents you understand beat a thousand you cannot defend.&lt;/li&gt;
&lt;li&gt;Write the spec, then the failing test, then make it pass. Keep TDD for correctness. The benchmark is the layer on top, not a replacement.&lt;/li&gt;
&lt;li&gt;Add a benchmark the first time you guess about performance. The moment you say "this is probably fine," measure it instead.&lt;/li&gt;
&lt;li&gt;Put it in CI and gate on regressions once the metric is stable. A benchmark nobody runs is documentation.&lt;/li&gt;
&lt;li&gt;Lean on an agent for the unglamorous parts: fixture wrangling, scoring code, CI matrix, percentile aggregation. This is exactly the work that used to make a harness too expensive, and it is exactly the work an agent is good at.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The payoff
&lt;/h2&gt;

&lt;p&gt;When correctness and speed are both measured on every change, you stop guessing and you stop arguing from intuition. A refactor either held the line on quality and latency or it did not, and the harness tells you which before the diff merges. Spec-driven, TDD for correctness, real benchmarks for the rest. Build the harness you never had time for, because now you do.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>performance</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Kreuzberg v4.0.0-RC.8 is Available</title>
      <dc:creator>Na'aman Hirschfeld (Goldziher)</dc:creator>
      <pubDate>Mon, 15 Dec 2025 13:06:14 +0000</pubDate>
      <link>https://dev.to/kreuzberg/kreuzberg-v400-rc8-is-available-4fma</link>
      <guid>https://dev.to/kreuzberg/kreuzberg-v400-rc8-is-available-4fma</guid>
      <description>&lt;p&gt;Hi Peeps,&lt;/p&gt;

&lt;p&gt;I'm excited to announce that &lt;a href="https://github.com/kreuzberg-dev/kreuzberg" rel="noopener noreferrer"&gt;Kreuzberg&lt;/a&gt; v4.0.0 is coming very soon. We will release v4.0.0 at the beginning of next year - in just a couple of weeks time. For now, v4.0.0-rc.8 has been released to all channels.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Kreuzberg?
&lt;/h2&gt;

&lt;p&gt;Kreuzberg is a document intelligence toolkit for extracting text, metadata, tables, images, and structured data from 56+ file formats. It was originally written in Python (v1-v3), where it demonstrated strong performance characteristics compared to alternatives in the ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's new in V4?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A Complete Rust Rewrite with Polyglot Bindings
&lt;/h3&gt;

&lt;p&gt;The new version of Kreuzberg represents a massive architectural evolution. &lt;strong&gt;Kreuzberg has been completely rewritten in Rust&lt;/strong&gt; - leveraging Rust's memory safety, zero-cost abstractions, and native performance. The new architecture consists of a high-performance Rust core with native bindings to multiple languages. That's right - it's no longer just a Python library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kreuzberg v4 is now available for 7 languages across 8 runtime bindings:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rust&lt;/strong&gt; (native library)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt; (PyO3 native bindings)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; - Node.js (NAPI-RS native bindings) + Deno/Browser/Edge (WASM)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruby&lt;/strong&gt; (Magnus FFI)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java 25+&lt;/strong&gt; (Panama Foreign Function &amp;amp; Memory API)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C#&lt;/strong&gt; (P/Invoke)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt; (cgo bindings)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Post v4.0.0 roadmap includes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP&lt;/li&gt;
&lt;li&gt;Elixir (via Rustler - with Erlang and Gleam interop)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, it's available as a &lt;strong&gt;CLI&lt;/strong&gt; (installable via &lt;code&gt;cargo&lt;/code&gt; or &lt;code&gt;homebrew&lt;/code&gt;), &lt;strong&gt;HTTP REST API server&lt;/strong&gt;, &lt;strong&gt;Model Context Protocol (MCP) server&lt;/strong&gt; for Claude Desktop/Continue.dev, and as &lt;strong&gt;public Docker images&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the Rust Rewrite? Performance and Architecture
&lt;/h3&gt;

&lt;p&gt;The Rust rewrite wasn't just about performance - though that's a major benefit. It was an opportunity to fundamentally rethink the architecture:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architectural improvements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero-copy operations&lt;/strong&gt; via Rust's ownership model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;True async concurrency&lt;/strong&gt; with Tokio runtime (no GIL limitations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming parsers&lt;/strong&gt; for constant memory usage on multi-GB files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SIMD-accelerated text processing&lt;/strong&gt; for token reduction and string operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory-safe FFI boundaries&lt;/strong&gt; for all language bindings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin system&lt;/strong&gt; with trait-based extensibility&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  v3 vs v4: What Changed?
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;v3 (Python)&lt;/th&gt;
&lt;th&gt;v4 (Rust Core)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Core Language&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pure Python&lt;/td&gt;
&lt;td&gt;Rust 2024 edition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;File Formats&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30-40+ (via Pandoc)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;56+ (native parsers)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Python only&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;7 languages&lt;/strong&gt; (Rust/Python/TS/Ruby/Java/Go/C#)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dependencies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires Pandoc (system binary)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Zero system dependencies&lt;/strong&gt; (all native)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Embeddings&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;✓ FastEmbed with ONNX (3 presets + custom)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Semantic Chunking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Via semantic-text-splitter library&lt;/td&gt;
&lt;td&gt;✓ Built-in (text + markdown-aware)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Token Reduction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built-in (TF-IDF based)&lt;/td&gt;
&lt;td&gt;✓ Enhanced with 3 modes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language Detection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optional (fast-langdetect)&lt;/td&gt;
&lt;td&gt;✓ Built-in (68 languages)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Keyword Extraction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optional (KeyBERT)&lt;/td&gt;
&lt;td&gt;✓ Built-in (YAKE + RAKE algorithms)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OCR Backends&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tesseract/EasyOCR/PaddleOCR&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Same + better integration&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plugin System&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limited extractor registry&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Full trait-based&lt;/strong&gt; (4 plugin types)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Page Tracking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Character-based indices&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Byte-based with O(1) lookup&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Servers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;REST API (Litestar)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;HTTP (Axum) + MCP + MCP-SSE&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Installation Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~100MB base&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16-31 MB complete&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Python heap management&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RAII with streaming&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Concurrency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;asyncio (GIL-limited)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Tokio work-stealing&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Replacement of Pandoc - Native Performance
&lt;/h3&gt;

&lt;p&gt;Kreuzberg v3 relied on &lt;strong&gt;Pandoc&lt;/strong&gt; - an amazing tool, but one that had to be invoked via subprocess because of its GPL license. This had significant impacts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v3 Pandoc limitations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;System dependency (installation required)&lt;/li&gt;
&lt;li&gt;Subprocess overhead on every document&lt;/li&gt;
&lt;li&gt;No streaming support&lt;/li&gt;
&lt;li&gt;Limited metadata extraction&lt;/li&gt;
&lt;li&gt;~500MB+ installation footprint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;v4 native parsers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero external dependencies&lt;/strong&gt; - everything is native Rust&lt;/li&gt;
&lt;li&gt;Direct parsing with full control over extraction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Substantially more metadata&lt;/strong&gt; extracted (e.g., DOCX document properties, section structure, style information)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming support&lt;/strong&gt; for massive files (tested on multi-GB XML documents with stable memory)&lt;/li&gt;
&lt;li&gt;Example: PPTX extractor is now a &lt;strong&gt;fully streaming parser&lt;/strong&gt; capable of handling gigabyte-scale presentations with constant memory usage and high throughput&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  New File Format Support
&lt;/h3&gt;

&lt;p&gt;v4 expanded format support from ~20 to &lt;strong&gt;56+ file formats&lt;/strong&gt;, including:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Added legacy format support:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.doc&lt;/code&gt; (Word 97-2003)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.ppt&lt;/code&gt; (PowerPoint 97-2003)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.xls&lt;/code&gt; (Excel 97-2003)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.eml&lt;/code&gt; (Email messages)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.msg&lt;/code&gt; (Outlook messages)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Added academic/technical formats:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LaTeX (&lt;code&gt;.tex&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;BibTeX (&lt;code&gt;.bib&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Typst (&lt;code&gt;.typ&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;JATS XML (scientific articles)&lt;/li&gt;
&lt;li&gt;DocBook XML&lt;/li&gt;
&lt;li&gt;FictionBook (&lt;code&gt;.fb2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;OPML (&lt;code&gt;.opml&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Better Office support:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;XLSB, XLSM (Excel binary/macro formats)&lt;/li&gt;
&lt;li&gt;Better structured metadata extraction from DOCX/PPTX/XLSX&lt;/li&gt;
&lt;li&gt;Full table extraction from presentations&lt;/li&gt;
&lt;li&gt;Image extraction with deduplication&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  New Features: Full Document Intelligence Solution
&lt;/h3&gt;

&lt;p&gt;The v4 rewrite was also an opportunity to close gaps with commercial alternatives and add features specifically designed for &lt;strong&gt;RAG applications and LLM workflows&lt;/strong&gt;:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. &lt;strong&gt;Embeddings (NEW)&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FastEmbed integration&lt;/strong&gt; with full ONNX Runtime acceleration&lt;/li&gt;
&lt;li&gt;Three presets: &lt;code&gt;"fast"&lt;/code&gt; (384d), &lt;code&gt;"balanced"&lt;/code&gt; (512d), &lt;code&gt;"quality"&lt;/code&gt; (768d/1024d)&lt;/li&gt;
&lt;li&gt;Custom model support (bring your own ONNX model)&lt;/li&gt;
&lt;li&gt;Local generation (no API calls, no rate limits)&lt;/li&gt;
&lt;li&gt;Automatic model downloading and caching&lt;/li&gt;
&lt;li&gt;Per-chunk embedding generation
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;kreuzberg&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ExtractionConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmbeddingConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmbeddingModelType&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ExtractionConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;EmbeddingConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;EmbeddingModelType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;balanced&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;normalize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kreuzberg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# result.embeddings contains vectors for each chunk
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. &lt;strong&gt;Semantic Text Chunking (NOW BUILT-IN)&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Now integrated directly into the core (v3 used external semantic-text-splitter library):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structure-aware chunking&lt;/strong&gt; that respects document semantics&lt;/li&gt;
&lt;li&gt;Two strategies:

&lt;ul&gt;
&lt;li&gt;Generic text chunker (whitespace/punctuation-aware)&lt;/li&gt;
&lt;li&gt;Markdown chunker (preserves headings, lists, code blocks, tables)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Configurable chunk size and overlap&lt;/li&gt;
&lt;li&gt;Unicode-safe (handles CJK, emojis correctly)&lt;/li&gt;
&lt;li&gt;Automatic chunk-to-page mapping&lt;/li&gt;
&lt;li&gt;Per-chunk metadata with byte offsets&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. &lt;strong&gt;Byte-Accurate Page Tracking (BREAKING CHANGE)&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;This is a critical improvement for LLM applications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;v3&lt;/strong&gt;: Character-based indices (&lt;code&gt;char_start&lt;/code&gt;/&lt;code&gt;char_end&lt;/code&gt;) - incorrect for UTF-8 multi-byte characters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v4&lt;/strong&gt;: Byte-based indices (&lt;code&gt;byte_start&lt;/code&gt;/&lt;code&gt;byte_end&lt;/code&gt;) - correct for all string operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additional page features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;O(1) lookup: "which page is byte offset X on?" → instant answer&lt;/li&gt;
&lt;li&gt;Per-page content extraction&lt;/li&gt;
&lt;li&gt;Page markers in combined text (e.g., &lt;code&gt;--- Page 5 ---&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Automatic chunk-to-page mapping for citations&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  4. &lt;strong&gt;Enhanced Token Reduction for LLM Context&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Enhanced from v3 with three configurable modes to save on LLM costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Light mode&lt;/strong&gt;: ~15% reduction (preserve most detail)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Moderate mode&lt;/strong&gt;: ~30% reduction (balanced)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggressive mode&lt;/strong&gt;: ~50% reduction (key information only)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Uses TF-IDF sentence scoring with position-aware weighting and language-specific stopword filtering. SIMD-accelerated for improved performance over v3.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. &lt;strong&gt;Language Detection (NOW BUILT-IN)&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;68 language support with confidence scoring&lt;/li&gt;
&lt;li&gt;Multi-language detection (documents with mixed languages)&lt;/li&gt;
&lt;li&gt;ISO 639-1 and ISO 639-3 code support&lt;/li&gt;
&lt;li&gt;Configurable confidence thresholds&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  6. &lt;strong&gt;Keyword Extraction (NOW BUILT-IN)&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Now built into core (previously optional KeyBERT in v3):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;YAKE&lt;/strong&gt; (Yet Another Keyword Extractor): Unsupervised, language-independent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAKE&lt;/strong&gt; (Rapid Automatic Keyword Extraction): Fast statistical method&lt;/li&gt;
&lt;li&gt;Configurable n-grams (1-3 word phrases)&lt;/li&gt;
&lt;li&gt;Relevance scoring with language-specific stopwords&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  7. &lt;strong&gt;Plugin System (NEW)&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Four extensible plugin types for customization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DocumentExtractor&lt;/strong&gt; - Custom file format handlers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OcrBackend&lt;/strong&gt; - Custom OCR engines (integrate your own Python models)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostProcessor&lt;/strong&gt; - Data transformation and enrichment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validator&lt;/strong&gt; - Pre-extraction validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plugins defined in Rust work across all language bindings. Python/TypeScript can define custom plugins with thread-safe callbacks into the Rust core.&lt;/p&gt;

&lt;h4&gt;
  
  
  8. &lt;strong&gt;Production-Ready Servers (NEW)&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP REST API&lt;/strong&gt;: Production-grade Axum server with OpenAPI docs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Server&lt;/strong&gt;: Direct integration with Claude Desktop, Continue.dev, and other MCP clients&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP-SSE Transport&lt;/strong&gt; (RC.8): Server-Sent Events for cloud deployments without WebSocket support&lt;/li&gt;
&lt;li&gt;All three modes support the same feature set: extraction, batch processing, caching&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance: Benchmarked Against the Competition
&lt;/h2&gt;

&lt;p&gt;We maintain &lt;strong&gt;continuous benchmarks&lt;/strong&gt; comparing Kreuzberg against the leading OSS alternatives:&lt;/p&gt;

&lt;h3&gt;
  
  
  Benchmark Setup
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Platform&lt;/strong&gt;: Ubuntu 22.04 (GitHub Actions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test Suite&lt;/strong&gt;: 30+ documents covering all formats&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metrics&lt;/strong&gt;: Latency (p50, p95), throughput (MB/s), memory usage, success rate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competitors&lt;/strong&gt;: Apache Tika, Docling, Unstructured, MarkItDown&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How Kreuzberg Compares
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Installation Size&lt;/strong&gt; (critical for containers/serverless):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kreuzberg&lt;/strong&gt;: &lt;strong&gt;16-31 MB complete&lt;/strong&gt; (CLI: 16 MB, Python wheel: 22 MB, Java JAR: 31 MB - all features included)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MarkItDown&lt;/strong&gt;: ~251 MB installed (58.3 KB wheel, 25 dependencies)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unstructured&lt;/strong&gt;: ~146 MB minimal (open source base) - &lt;strong&gt;several GB with ML models&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docling&lt;/strong&gt;: ~1 GB base, &lt;strong&gt;9.74GB Docker image&lt;/strong&gt; (includes PyTorch CUDA)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apache Tika&lt;/strong&gt;: ~55 MB (tika-app JAR) + dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GROBID&lt;/strong&gt;: 500MB (CRF-only) to &lt;strong&gt;8GB&lt;/strong&gt; (full deep learning)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Performance Characteristics:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;th&gt;Accuracy&lt;/th&gt;
&lt;th&gt;Formats&lt;/th&gt;
&lt;th&gt;Installation&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kreuzberg&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚡ Fast (Rust-native)&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;56+&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16-31 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;General-purpose, production-ready&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚡ Fast (3.1s/pg x86, 1.27s/pg ARM)&lt;/td&gt;
&lt;td&gt;Best&lt;/td&gt;
&lt;td&gt;7+&lt;/td&gt;
&lt;td&gt;1-9.74 GB&lt;/td&gt;
&lt;td&gt;Complex documents, when accuracy &amp;gt; size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GROBID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚡⚡ Very Fast (10.6 PDF/s)&lt;/td&gt;
&lt;td&gt;Best&lt;/td&gt;
&lt;td&gt;PDF only&lt;/td&gt;
&lt;td&gt;0.5-8 GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Academic/scientific papers only&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unstructured&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚡ Moderate&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;25-65+&lt;/td&gt;
&lt;td&gt;146 MB-several GB&lt;/td&gt;
&lt;td&gt;Python-native LLM pipelines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MarkItDown&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚡ Fast (small files)&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;11+&lt;/td&gt;
&lt;td&gt;~251 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Lightweight Markdown conversion&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Apache Tika&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;⚡ Moderate&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1000+&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~55 MB&lt;/td&gt;
&lt;td&gt;Enterprise, broadest format support&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Kreuzberg's sweet spot:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smallest full-featured installation&lt;/strong&gt;: 16-31 MB complete (vs 146 MB-9.74 GB for competitors)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5-15x smaller&lt;/strong&gt; than Unstructured/MarkItDown, &lt;strong&gt;30-300x smaller&lt;/strong&gt; than Docling/GROBID&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust-native performance&lt;/strong&gt; without ML model overhead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broad format support&lt;/strong&gt; (56+ formats) with native parsers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-language support&lt;/strong&gt; unique in the space (7 languages vs Python-only for most)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production-ready&lt;/strong&gt; with general-purpose design (vs specialized tools like GROBID)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Is Kreuzberg a SaaS Product?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No.&lt;/strong&gt; Kreuzberg is and will remain &lt;strong&gt;MIT-licensed open source&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;However, we are building &lt;strong&gt;Kreuzberg.cloud&lt;/strong&gt; - a commercial SaaS and self-hosted document intelligence solution built &lt;em&gt;on top of&lt;/em&gt; Kreuzberg. This follows the proven open-core model: the library stays free and open, while we offer a cloud service for teams that want managed infrastructure, APIs, and enterprise features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will Kreuzberg become commercially licensed?&lt;/strong&gt; Absolutely not. There is no BSL (Business Source License) in Kreuzberg's future. The library was MIT-licensed and will remain MIT-licensed. We're building the commercial offering as a separate product around the core library, not by restricting the library itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Target Audience
&lt;/h2&gt;

&lt;p&gt;Any developer or data scientist who needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Document text extraction (PDF, Office, images, email, archives, etc.)&lt;/li&gt;
&lt;li&gt;OCR (Tesseract, EasyOCR, PaddleOCR)&lt;/li&gt;
&lt;li&gt;Metadata extraction (authors, dates, properties, EXIF)&lt;/li&gt;
&lt;li&gt;Table and image extraction&lt;/li&gt;
&lt;li&gt;Document pre-processing for RAG pipelines&lt;/li&gt;
&lt;li&gt;Text chunking with embeddings&lt;/li&gt;
&lt;li&gt;Token reduction for LLM context windows&lt;/li&gt;
&lt;li&gt;Multi-language document intelligence in production systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ideal for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RAG application developers&lt;/li&gt;
&lt;li&gt;Data engineers building document pipelines&lt;/li&gt;
&lt;li&gt;ML engineers preprocessing training data&lt;/li&gt;
&lt;li&gt;Enterprise developers handling document workflows&lt;/li&gt;
&lt;li&gt;DevOps teams needing lightweight, performant extraction in containers/serverless&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comparison with Alternatives
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Open Source Python Libraries
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Unstructured.io&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strengths&lt;/strong&gt;: Established, modular, broad format support (25+ open source, 65+ enterprise), LLM-focused, good Python ecosystem integration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-offs&lt;/strong&gt;: Python GIL performance constraints, 146 MB minimal installation (several GB with ML models)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: Apache-2.0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to choose&lt;/strong&gt;: Python-only projects where ecosystem fit &amp;gt; performance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;MarkItDown (Microsoft)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strengths&lt;/strong&gt;: Fast for small files, Markdown-optimized, simple API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-offs&lt;/strong&gt;: Limited format support (11 formats), less structured metadata, ~251 MB installed (despite small wheel), requires OpenAI API for images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: MIT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to choose&lt;/strong&gt;: Markdown-only conversion, LLM consumption&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Docling (IBM)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strengths&lt;/strong&gt;: Excellent accuracy on complex documents (97.9% cell-level accuracy on tested sustainability report tables), state-of-the-art AI models for technical documents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-offs&lt;/strong&gt;: Massive installation (1-9.74 GB), high memory usage, GPU-optimized (underutilized on CPU)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: MIT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to choose&lt;/strong&gt;: Accuracy on complex documents &amp;gt; deployment size/speed, have GPU infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Open Source Java/Academic Tools
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Apache Tika&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strengths&lt;/strong&gt;: Mature, stable, broadest format support (1000+ types), proven at scale, Apache Foundation backing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-offs&lt;/strong&gt;: Java/JVM required, slower on large files, older architecture, complex dependency management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: Apache-2.0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to choose&lt;/strong&gt;: Enterprise environments with JVM infrastructure, need for maximum format coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;GROBID&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strengths&lt;/strong&gt;: Best-in-class for academic papers (F1 0.87-0.90), extremely fast (10.6 PDF/sec sustained), proven at scale (34M+ documents at CORE)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-offs&lt;/strong&gt;: Academic papers only, large installation (500MB-8GB), complex Java+Python setup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: Apache-2.0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to choose&lt;/strong&gt;: Scientific/academic document processing exclusively&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Commercial APIs
&lt;/h3&gt;

&lt;p&gt;There are numerous commercial options from startups (LlamaIndex, Unstructured.io paid tiers) to big cloud providers (AWS Textract, Azure Form Recognizer, Google Document AI). These are not OSS but offer managed infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kreuzberg's position&lt;/strong&gt;: As an open-source library, Kreuzberg provides a self-hosted alternative with no per-document API costs, making it suitable for high-volume workloads where cost efficiency matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Community &amp;amp; Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: Star us at &lt;a href="https://github.com/kreuzberg-dev/kreuzberg" rel="noopener noreferrer"&gt;https://github.com/kreuzberg-dev/kreuzberg&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discord&lt;/strong&gt;: Join our community server at &lt;a href="https://discord.gg/pXxagNK2zN" rel="noopener noreferrer"&gt;discord.gg/pXxagNK2zN&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subreddit&lt;/strong&gt;: Join the discussion at &lt;a href="https://www.reddit.com/r/kreuzberg_dev/" rel="noopener noreferrer"&gt;r/kreuzberg_dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt;: &lt;a href="https://kreuzberg.dev" rel="noopener noreferrer"&gt;kreuzberg.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'd love to hear your feedback, use cases, and contributions!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Kreuzberg v4 is a complete Rust rewrite of a document intelligence library, offering native bindings for 7 languages (8 runtime targets), 56+ file formats, Rust-native performance, embeddings, semantic chunking, and production-ready servers - all in a 16-31 MB complete package (5-15x smaller than alternatives). Releasing January 2026. MIT licensed forever.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>programming</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I benchmarked 4 Python text extraction libraries (2025)</title>
      <dc:creator>Na'aman Hirschfeld (Goldziher)</dc:creator>
      <pubDate>Sun, 06 Jul 2025 10:25:38 +0000</pubDate>
      <link>https://dev.to/nhirschfeld/i-benchmarked-4-python-text-extraction-libraries-2025-4e7j</link>
      <guid>https://dev.to/nhirschfeld/i-benchmarked-4-python-text-extraction-libraries-2025-4e7j</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Comprehensive benchmarks of Kreuzberg, Docling, MarkItDown, and Unstructured across 94 real-world documents. Results might surprise you.&lt;/p&gt;

&lt;h2&gt;
  
  
  📊 &lt;strong&gt;Live Results&lt;/strong&gt;: &lt;a href="https://goldziher.github.io/python-text-extraction-libs-benchmarks/" rel="noopener noreferrer"&gt;https://goldziher.github.io/python-text-extraction-libs-benchmarks/&lt;/a&gt;
&lt;/h2&gt;




&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;As the author of &lt;a href="https://github.com/Goldziher/kreuzberg" rel="noopener noreferrer"&gt;Kreuzberg&lt;/a&gt;, I wanted to create an &lt;strong&gt;honest, comprehensive benchmark&lt;/strong&gt; of Python text extraction libraries. No cherry-picking, no marketing fluff - just real performance data across 94 documents (~210MB) ranging from tiny text files to 59MB academic papers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full disclosure&lt;/strong&gt;: I built Kreuzberg, but these benchmarks are automated, reproducible, and the methodology is completely open-source.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔬 &lt;strong&gt;What I Tested&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Libraries Benchmarked:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/Goldziher/kreuzberg" rel="noopener noreferrer"&gt;Kreuzberg&lt;/a&gt;&lt;/strong&gt; (71MB, 20 deps) - My library&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/DS4SD/docling" rel="noopener noreferrer"&gt;Docling&lt;/a&gt;&lt;/strong&gt; (1,032MB, 88 deps) - IBM's ML-powered solution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/microsoft/markitdown" rel="noopener noreferrer"&gt;MarkItDown&lt;/a&gt;&lt;/strong&gt; (251MB, 25 deps) - Microsoft's Markdown converter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/Unstructured-IO/unstructured" rel="noopener noreferrer"&gt;Unstructured&lt;/a&gt;&lt;/strong&gt; (146MB, 54 deps) - Enterprise document processing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Test Coverage:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;94 real documents&lt;/strong&gt;: PDFs, Word docs, HTML, images, spreadsheets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5 size categories&lt;/strong&gt;: Tiny (&amp;lt;100KB) to Huge (&amp;gt;50MB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6 languages&lt;/strong&gt;: English, Hebrew, German, Chinese, Japanese, Korean&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU-only processing&lt;/strong&gt;: No GPU acceleration for fair comparison&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple metrics&lt;/strong&gt;: Speed, memory usage, success rates, installation sizes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🏆 &lt;strong&gt;Results Summary&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Speed Champions 🚀
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Kreuzberg&lt;/strong&gt;: 35+ files/second, handles everything&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unstructured&lt;/strong&gt;: Moderate speed, excellent reliability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MarkItDown&lt;/strong&gt;: Good on simple docs, struggles with complex files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docling&lt;/strong&gt;: Often 60+ minutes per file (!!)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Installation Footprint 📦
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kreuzberg&lt;/strong&gt;: 71MB, 20 dependencies ⚡&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unstructured&lt;/strong&gt;: 146MB, 54 dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MarkItDown&lt;/strong&gt;: 251MB, 25 dependencies (includes ONNX)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docling&lt;/strong&gt;: 1,032MB, 88 dependencies 🐘&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reality Check ⚠️
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docling&lt;/strong&gt;: Frequently fails/times out on medium files (&amp;gt;1MB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MarkItDown&lt;/strong&gt;: Struggles with large/complex documents (&amp;gt;10MB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kreuzberg&lt;/strong&gt;: Consistent across all document types and sizes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unstructured&lt;/strong&gt;: Most reliable overall (88%+ success rate)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎯 &lt;strong&gt;When to Use What&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ⚡ &lt;strong&gt;&lt;a href="https://github.com/Goldziher/kreuzberg" rel="noopener noreferrer"&gt;Kreuzberg&lt;/a&gt;&lt;/strong&gt; (Disclaimer: I built this)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best for&lt;/strong&gt;: Production workloads, edge computing, AWS Lambda&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: Smallest footprint (71MB), fastest speed, handles everything&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bonus&lt;/strong&gt;: Both sync/async APIs with OCR support&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🏢 &lt;strong&gt;&lt;a href="https://github.com/Unstructured-IO/unstructured" rel="noopener noreferrer"&gt;Unstructured&lt;/a&gt;&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best for&lt;/strong&gt;: Enterprise applications, mixed document types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: Most reliable overall, good enterprise features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade-off&lt;/strong&gt;: Moderate speed, larger installation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📝 &lt;strong&gt;&lt;a href="https://github.com/microsoft/markitdown" rel="noopener noreferrer"&gt;MarkItDown&lt;/a&gt;&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best for&lt;/strong&gt;: Simple documents, LLM preprocessing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: Good for basic PDFs/Office docs, optimized for Markdown&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limitation&lt;/strong&gt;: Fails on large/complex files&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔬 &lt;strong&gt;&lt;a href="https://github.com/DS4SD/docling" rel="noopener noreferrer"&gt;Docling&lt;/a&gt;&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best for&lt;/strong&gt;: Research environments (if you have patience)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: Advanced ML document understanding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reality&lt;/strong&gt;: Extremely slow, frequent timeouts, 1GB+ install&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📈 &lt;strong&gt;Key Insights&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Installation size matters&lt;/strong&gt;: Kreuzberg's 71MB vs Docling's 1GB+ makes a huge difference for deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance varies dramatically&lt;/strong&gt;: 35 files/second vs 60+ minutes per file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document complexity is crucial&lt;/strong&gt;: Simple PDFs vs complex layouts show very different results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability vs features&lt;/strong&gt;: Sometimes the simplest solution works best&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🔧 &lt;strong&gt;Methodology&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automated CI/CD&lt;/strong&gt;: GitHub Actions run benchmarks on every release&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real documents&lt;/strong&gt;: Academic papers, business docs, multilingual content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple iterations&lt;/strong&gt;: 3 runs per document, statistical analysis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open source&lt;/strong&gt;: Full code, test documents, and results available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory profiling&lt;/strong&gt;: psutil-based resource monitoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout handling&lt;/strong&gt;: 5-minute limit per extraction&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🤔 &lt;strong&gt;Why I Built This&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Working on &lt;a href="https://github.com/Goldziher/kreuzberg" rel="noopener noreferrer"&gt;Kreuzberg&lt;/a&gt;, I worked on performance and stability, and then wanted a tool to see how it measures against other frameworks - which I could also use to further develop and improve Kreuzberg itself. I therefore created this benchmark. Since it was fun, I invested some time to pimp it out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses &lt;strong&gt;real-world documents&lt;/strong&gt;, not synthetic tests&lt;/li&gt;
&lt;li&gt;Tests &lt;strong&gt;installation overhead&lt;/strong&gt; (often ignored)&lt;/li&gt;
&lt;li&gt;Includes &lt;strong&gt;failure analysis&lt;/strong&gt; (libraries fail more than you think)&lt;/li&gt;
&lt;li&gt;Is &lt;strong&gt;completely reproducible&lt;/strong&gt; and open&lt;/li&gt;
&lt;li&gt;Updates &lt;strong&gt;automatically&lt;/strong&gt; with new releases&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📊 &lt;strong&gt;Data Deep Dive&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://goldziher.github.io/python-text-extraction-libs-benchmarks/" rel="noopener noreferrer"&gt;interactive dashboard&lt;/a&gt; shows some fascinating patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kreuzberg dominates&lt;/strong&gt; on speed and resource usage across all categories&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unstructured excels&lt;/strong&gt; at complex layouts and has the best reliability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MarkItDown is useful&lt;/strong&gt; for simple docs shows in the data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docling's ML models&lt;/strong&gt; create massive overhead for most use cases making it a hard sell&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;Try It Yourself&lt;/strong&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Goldziher/python-text-extraction-libs-benchmarks.git
&lt;span class="nb"&gt;cd &lt;/span&gt;python-text-extraction-libs-benchmarks
uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--all-extras&lt;/span&gt;
uv run python &lt;span class="nt"&gt;-m&lt;/span&gt; src.cli benchmark &lt;span class="nt"&gt;--framework&lt;/span&gt; kreuzberg_sync &lt;span class="nt"&gt;--category&lt;/span&gt; small
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or just check the live results: &lt;a href="https://goldziher.github.io/python-text-extraction-libs-benchmarks/" rel="noopener noreferrer"&gt;https://goldziher.github.io/python-text-extraction-libs-benchmarks/&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🔗 &lt;strong&gt;Links&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;📊 Live Benchmark Results&lt;/strong&gt;: &lt;a href="https://goldziher.github.io/python-text-extraction-libs-benchmarks/" rel="noopener noreferrer"&gt;https://goldziher.github.io/python-text-extraction-libs-benchmarks/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📁 Benchmark Repository&lt;/strong&gt;: &lt;a href="https://github.com/Goldziher/python-text-extraction-libs-benchmarks" rel="noopener noreferrer"&gt;https://github.com/Goldziher/python-text-extraction-libs-benchmarks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⚡ Kreuzberg (my library)&lt;/strong&gt;: &lt;a href="https://github.com/Goldziher/kreuzberg" rel="noopener noreferrer"&gt;https://github.com/Goldziher/kreuzberg&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔬 Docling&lt;/strong&gt;: &lt;a href="https://github.com/DS4SD/docling" rel="noopener noreferrer"&gt;https://github.com/DS4SD/docling&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📝 MarkItDown&lt;/strong&gt;: &lt;a href="https://github.com/microsoft/markitdown" rel="noopener noreferrer"&gt;https://github.com/microsoft/markitdown&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🏢 Unstructured&lt;/strong&gt;: &lt;a href="https://github.com/Unstructured-IO/unstructured" rel="noopener noreferrer"&gt;https://github.com/Unstructured-IO/unstructured&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🤝 &lt;strong&gt;Discussion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;What's your experience with these libraries? Any others I should benchmark? I tried benchmarking &lt;code&gt;marker&lt;/code&gt;, but the setup required a GPU.&lt;/p&gt;

&lt;p&gt;Some important points regarding how I used these benchmarks for Kreuzberg:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I fine tuned the default settings for Kreuzberg.&lt;/li&gt;
&lt;li&gt;I updated our docs to give recommendations on different settings for different use cases. E.g. Kreuzberg can actually get to 75% reliability, with about 15% slow-down.&lt;/li&gt;
&lt;li&gt;I made a best effort to configure the frameworks following the best practices of their docs and using their out of the box defaults. If you think something is off or needs adjustment, feel free to let me know here or open an issue in the repository.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>webdev</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
