<?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: Maverick Y</title>
    <description>The latest articles on DEV Community by Maverick Y (@maverickyadav).</description>
    <link>https://dev.to/maverickyadav</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%2F4005246%2Facb385a5-0ea9-4f5b-833e-b905c77cd4a6.png</url>
      <title>DEV Community: Maverick Y</title>
      <link>https://dev.to/maverickyadav</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maverickyadav"/>
    <language>en</language>
    <item>
      <title>How LLMLingua compresses prompts: perplexity, deletion, and trust</title>
      <dc:creator>Maverick Y</dc:creator>
      <pubDate>Sun, 19 Jul 2026 13:57:49 +0000</pubDate>
      <link>https://dev.to/maverickyadav/-how-llmlingua-compresses-prompts-perplexity-deletion-and-trust-35c9</link>
      <guid>https://dev.to/maverickyadav/-how-llmlingua-compresses-prompts-perplexity-deletion-and-trust-35c9</guid>
      <description>&lt;p&gt;I build &lt;a href="https://github.com/antrixy/ctxfold" rel="noopener noreferrer"&gt;ctxfold&lt;/a&gt;, a lossless prompt compressor, so I spend a lot of time thinking about the other school of prompt compression — the one that &lt;em&gt;deletes things on purpose&lt;/em&gt;. The most important tool in that school is LLMLingua, from Microsoft Research. Before I publish a measured comparison (that post is coming), I want to explain fairly how LLMLingua actually works, because it's clever, it's genuinely effective at what it targets, and the design choice at its core is the exact opposite of mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-sentence version
&lt;/h2&gt;

&lt;p&gt;LLMLingua uses a small language model to figure out which tokens in your prompt the big model could have predicted anyway — and deletes them.&lt;/p&gt;

&lt;p&gt;That's the whole philosophical move. If a token is highly predictable from its context, removing it costs the downstream LLM very little, because the information it carried was mostly redundant. The measure of "predictable" is perplexity: tokens the small model assigns low perplexity are candidates for deletion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The original LLMLingua (2023)
&lt;/h2&gt;

&lt;p&gt;The first paper is a coarse-to-fine pipeline with three pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Budget controller.&lt;/strong&gt; Not all parts of a prompt tolerate compression equally. Instructions and the question tend to be information-dense; few-shot demonstrations are usually the flabby part. The budget controller assigns different compression ratios to different sections — it might keep your instruction nearly intact while cutting demonstrations hard, even dropping whole examples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterative token-level compression.&lt;/strong&gt; The surviving text is split into segments and compressed token by token, where each token's perplexity is computed against the &lt;em&gt;already-compressed&lt;/em&gt; preceding context. The iteration matters: deleting tokens changes the perplexity of what follows, so a single pass would misjudge dependencies between tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distribution alignment.&lt;/strong&gt; The small model (GPT2-small or LLaMA-7B scale) is instruction-tuned on data generated by the target LLM, so its sense of "predictable" tracks the big model's, not just its own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The headline result was up to 20x compression on benchmarks like GSM8K and BBH with little loss in task performance — particularly for in-context learning and reasoning, which makes sense: few-shot demos are exactly where prompts carry the most redundancy per token.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the output looks like
&lt;/h2&gt;

&lt;p&gt;Compressed prompts from this family are not pretty. Deletion doesn't respect grammar. You get telegraphic text — function words gone, sentences collapsed into keyword runs. Something shaped like this (illustrative, not tool output):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Original:   Sam bought a dozen boxes, each with 30 highlighter pens
            inside, for $10 each box. He rearranged five of these...
Compressed: Sam bought dozen boxes each 30 highlighter pens $10 each.
            rearranged five...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The striking empirical finding — and the reason this line of work exists — is that LLMs read this stuff far better than humans do. The big model reconstructs the missing connective tissue without being asked. Microsoft's own framing is that compressed prompts may be difficult for humans while staying highly effective for LLMs.&lt;/p&gt;

&lt;h2&gt;
  
  
  LLMLingua-2 (2024): from perplexity to classification
&lt;/h2&gt;

&lt;p&gt;The second generation changes the mechanism entirely. Instead of measuring perplexity with a small causal LM, the team had GPT-4 compress a corpus of prompts, then trained a small bidirectional encoder (XLM-RoBERTa-large) to &lt;em&gt;classify each token: keep or discard&lt;/em&gt;, imitating GPT-4's choices. This data-distillation approach is faster (the encoder sees the whole sequence at once, no iterative left-to-right passes) and it fixed a real weakness: causal perplexity only looks backward, but whether a token matters often depends on what comes after it.&lt;/p&gt;

&lt;p&gt;There's also LongLLMLingua in between, specialized for long-context and RAG prompts — question-aware compression, document reordering to fight "lost in the middle," and subsequence recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it
&lt;/h2&gt;

&lt;p&gt;The library is a pip install:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llmlingua&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PromptCompressor&lt;/span&gt;

&lt;span class="n"&gt;compressor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PromptCompressor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;microsoft/llmlingua-2-xlm-roberta-large-meetingbank&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;use_llmlingua2&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;compressor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compress_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.33&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;force_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compressed_prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;origin_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compressed_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;rate&lt;/code&gt; is your target: keep a third of the tokens, delete the rest. The &lt;code&gt;force_tokens&lt;/code&gt; escape hatch — a list of tokens that must never be deleted — is worth noticing. It exists because sometimes the classifier deletes things you needed. Which brings me to the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trust question
&lt;/h2&gt;

&lt;p&gt;LLMLingua is &lt;em&gt;lossy by design&lt;/em&gt;. Once compressed, the original text is unrecoverable — there is no decompressor. You are trusting that what got deleted didn't matter, and that trust is statistical: the classifier learned what GPT-4 usually considered droppable, on the corpora it was trained on.&lt;/p&gt;

&lt;p&gt;For prose, summaries, and few-shot demos, that bet pays off, and the benchmark numbers back it up. The open question — the one I care about, because ctxfold lives on the other side of it — is what happens to &lt;em&gt;exact values&lt;/em&gt;. SKU codes, prices, quantities, the one log line out of 300 that mattered. A rare identifier is, almost by definition, high-perplexity, which should protect it. But "should" is doing work in that sentence, and the failure mode is silent: nothing tells you a number your task depended on was deleted.&lt;/p&gt;

&lt;p&gt;My own tool makes the opposite trade: byte-perfect losslessness, enforced by round-trip tests, at the cost of far smaller ratios (~39% on the formats it targets, vs. up-to-20x). And I recently learned lossless has its own readability limits — my CSV folding is byte-perfect and models still can't read it directly. So neither school gets to claim purity; the real axis is &lt;em&gt;where the information-recovery work happens and who pays for it&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That's the comparison I want to measure rather than argue: same datasets, same exact-match questions, both tools, results whatever they are. Next post.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write up negative results too — the CSV one is &lt;a href="https://dev.to/maverickyadav/i-built-a-readability-test-for-my-own-compression-format-it-scored-024-4epc"&gt;here&lt;/a&gt;. ctxfold is on &lt;a href="https://www.npmjs.com/package/ctxfold" rel="noopener noreferrer"&gt;npm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a readability test for my own compression format. It scored 0/24.</title>
      <dc:creator>Maverick Y</dc:creator>
      <pubDate>Sat, 18 Jul 2026 14:34:09 +0000</pubDate>
      <link>https://dev.to/maverickyadav/i-built-a-readability-test-for-my-own-compression-format-it-scored-024-4epc</link>
      <guid>https://dev.to/maverickyadav/i-built-a-readability-test-for-my-own-compression-format-it-scored-024-4epc</guid>
      <description>&lt;p&gt;&lt;em&gt;A negative result, its root cause, and the feature it produced.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My last post introduced &lt;a href="https://github.com/antrixy/ctxfold" rel="noopener noreferrer"&gt;ctxfold&lt;/a&gt; — lossless, structure-aware compression for the bulky stuff we put in LLM prompts. Its benchmark table had one cell I wasn't proud of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| CSV / TSV | char reduction | ~30–45%* |

*CSV readability not yet validated against a model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSON and logs had real numbers: a model answering lookup questions off the folded form, scored against exact ground truth, matching raw field-for-field. CSV had a character count and an asterisk.&lt;/p&gt;

&lt;p&gt;So I wrote the same harness for CSV. Generate 400 records with realistic redundancy, fold them, ask GPT-4o-mini to look up specific records in both forms, score against ground truth.&lt;/p&gt;

&lt;p&gt;Raw CSV: &lt;strong&gt;24/24&lt;/strong&gt;. Folded CSV: &lt;strong&gt;0/24&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not "slightly worse." Zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maybe the model is too small?
&lt;/h2&gt;

&lt;p&gt;First hypothesis: capability threshold. Re-ran on GPT-4o.&lt;/p&gt;

&lt;p&gt;Raw: &lt;strong&gt;24/24&lt;/strong&gt;. Folded: &lt;strong&gt;6/24&lt;/strong&gt;. A later run: 9/24. So no — a stronger model helps a little and inconsistently. The format is the problem.&lt;/p&gt;

&lt;p&gt;The failure had structure, which is what made it diagnosable. Asked about record &lt;code&gt;NW-1258&lt;/code&gt;, the model reported a warehouse of &lt;code&gt;WH-2&lt;/code&gt; (a half-applied prefix), a supplier of &lt;code&gt;DAL-2&lt;/code&gt; (a value from the warehouse column), and a price from some other row entirely. In another run it answered qty &lt;code&gt;1238&lt;/code&gt; for sku &lt;code&gt;NW-1238&lt;/code&gt; — it read the record's own identifier back as data. Two distinct failures, every time: it couldn't find the right row, and it couldn't reconstruct the values in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The root cause is embarrassing in hindsight
&lt;/h2&gt;

&lt;p&gt;ctxfold's JSON and logs encoders fold &lt;strong&gt;syntax&lt;/strong&gt;. A JSON array of objects repeats every key on every record; the encoder lifts keys, braces, and quotes into a one-time header, and every &lt;em&gt;value&lt;/em&gt; stays verbatim in its row. Logs are the same story with templates: timestamps, levels, &lt;code&gt;reqId=&lt;/code&gt; prefixes get lifted; the payload stays put. The model reads a plain labeled table containing exactly the values it needs.&lt;/p&gt;

&lt;p&gt;CSV has no syntax to remove. No keys, no braces — it's already a compact table. The only redundancy left is &lt;strong&gt;inside the values&lt;/strong&gt;: shared prefixes like &lt;code&gt;NW-&lt;/code&gt; on every sku, &lt;code&gt;WH-&lt;/code&gt; on every warehouse, a constant &lt;code&gt;USD&lt;/code&gt; column. So the CSV encoder factors those out, and each row keeps only the varying middle. Lossless, byte-exact, verified on every encode.&lt;/p&gt;

&lt;p&gt;And unreadable. To answer a question, the model has to find a row by a &lt;em&gt;partial&lt;/em&gt; key (the sku column only contains &lt;code&gt;258&lt;/code&gt;, because &lt;code&gt;NW-1&lt;/code&gt; was factored out — all 400 skus shared it) and then reconstruct every value through the header's &lt;code&gt;prefix + middle&lt;/code&gt; rules. It can't do either reliably. On GPT-4o-mini it essentially can't do it at all.&lt;/p&gt;

&lt;p&gt;I'd actually seen a milder version of this before. ctxfold's opt-in dictionary coding (low-cardinality values become small integers, mapped once in the header) pushes JSON savings from ~39% to ~46% — and in testing, models resolved the coded values slightly less reliably than plain ones. That's why it ships off by default. The CSV result is the same phenomenon at full strength:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Models read values, not reconstruction rules. Indirection through a header costs readability — and in CSV's case, the savings &lt;em&gt;were&lt;/em&gt; the indirection.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I didn't do
&lt;/h2&gt;

&lt;p&gt;I considered "fixing" the format — don't factor unique columns, keep identifiers whole. But that only patches row-finding; the prefix-reconstruction failure remains, and the savings shrink toward nothing. CSV is already near its readable minimum. That's &lt;em&gt;why&lt;/em&gt; there was nothing safe to fold.&lt;/p&gt;

&lt;p&gt;So the fix was documentation, not code. As of v0.1.4, CSV folding is &lt;strong&gt;pipeline-mode&lt;/strong&gt;: fold it for lossless transit or storage, call &lt;code&gt;decompress()&lt;/code&gt; before the prompt is built, and the model never sees the folded form. For direct model reading, send CSV raw. The benchmark table now says so, with the measured numbers where the asterisk used to be. JSON and logs remain validated direct-readable — their folded output keeps every value intact, and the scores show it.&lt;/p&gt;

&lt;p&gt;The rule I keep coming back to: ctxfold's core contract is &lt;em&gt;lossless or no-op — never lossy&lt;/em&gt;. It turns out the same discipline applies to claims. Either a readability claim has a measurement behind it, or it gets the asterisk. And when the measurement comes in against you, the asterisk becomes documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature the failure produced
&lt;/h2&gt;

&lt;p&gt;Sitting with those numbers, the useful question turned out to be: &lt;em&gt;before&lt;/em&gt; anyone folds anything — where do a prompt's tokens actually go, and what's safely foldable?&lt;/p&gt;

&lt;p&gt;v0.2.0 ships that as &lt;code&gt;ctxfold --profile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ctxfold --profile users.json

[ctxfold profile]
format      JSON array — 300 records × 7 fields
size        65,614 chars ≈ 16,404 tokens (estimated; pass a tokenizer for exact)

where the characters go
  keys         27%   repeated field names (with quotes)
  syntax        7%   braces, brackets, commas, colons
  values       36%   the data itself (with string quotes)
  whitespace   30%   indentation and spacing

foldable (lossless, verified by round-trip)
  fold             -66%   direct-readable — validated 24/24 vs raw
  + --dictionary   -73%   readability tradeoff — off by default, see README

verdict: fold it — 16,404 → ~5,595 tokens (~66% fewer)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a real API response shape — wrapped array, pretty-printed. 64% of the file is structure and formatting, which is why the fold is fat. On a CSV file, the same command reports the fold as &lt;strong&gt;pipeline-only&lt;/strong&gt; and tells you to send it raw or decompress first — the negative result is baked into the tool's own advice.&lt;/p&gt;

&lt;p&gt;The profiler follows the rules the failure taught:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Composition is measured in characters and attributed exactly&lt;/strong&gt; — the categories sum to the input size. Attributing individual &lt;em&gt;tokens&lt;/em&gt; to categories would be false precision, so token figures are totals, marked estimated unless you pass a real tokenizer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "foldable" numbers come from actually running the encoder&lt;/strong&gt; on your input. The profiler cannot promise more than &lt;code&gt;compress()&lt;/code&gt; delivers — that's an invariant with a test on it, not a policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If nothing folds, it says why&lt;/strong&gt; — quoted CSV, nested JSON, too few records, prose — instead of silently no-opping.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it with zero setup (no API key, deterministic output):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/antrixy/ctxfold &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;ctxfold
node examples/profile-demo.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or on your own data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; ctxfold
ctxfold &lt;span class="nt"&gt;--profile&lt;/span&gt; your-file.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The part I'd generalize
&lt;/h2&gt;

&lt;p&gt;If you maintain a tool that makes claims — savings, accuracy, compatibility — the cheapest test you'll ever write is the one that checks your own table. Mine took an afternoon, cost a few cents of API calls, and found that a third of my format list didn't do what a reader would assume. The fix cost nothing but honesty, and the tool that fell out of it is the most useful thing in the package.&lt;/p&gt;

&lt;p&gt;The full harness is in the repo (&lt;code&gt;examples/gpt-csv-equivalence.js&lt;/code&gt;), along with the &lt;a href="https://github.com/antrixy/ctxfold/releases/tag/v0.1.4" rel="noopener noreferrer"&gt;v0.1.4&lt;/a&gt; and &lt;a href="https://github.com/antrixy/ctxfold/releases/tag/v0.2.0" rel="noopener noreferrer"&gt;v0.2.0&lt;/a&gt; release notes. If you push structured data into prompts and your payloads break my assumptions, I want to hear about it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo &amp;amp; docs:&lt;/strong&gt; &lt;a href="https://github.com/antrixy/ctxfold" rel="noopener noreferrer"&gt;https://github.com/antrixy/ctxfold&lt;/a&gt; · &lt;strong&gt;npm:&lt;/strong&gt; &lt;code&gt;npm install ctxfold&lt;/code&gt; · MIT licensed.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>testing</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Differential testing found bugs in all three official TOON implementations — and outlived a moving spec</title>
      <dc:creator>Maverick Y</dc:creator>
      <pubDate>Fri, 17 Jul 2026 00:58:22 +0000</pubDate>
      <link>https://dev.to/maverickyadav/differential-testing-found-bugs-in-all-three-official-toon-implementations-and-outlived-a-moving-1mp8</link>
      <guid>https://dev.to/maverickyadav/differential-testing-found-bugs-in-all-three-official-toon-implementations-and-outlived-a-moving-1mp8</guid>
      <description>&lt;p&gt;In late June I filed &lt;a href="https://github.com/toon-format/toon/issues/322" rel="noopener noreferrer"&gt;toon#322&lt;/a&gt;&lt;br&gt;
against the reference TypeScript implementation of&lt;br&gt;
&lt;a href="https://github.com/toon-format/spec" rel="noopener noreferrer"&gt;TOON&lt;/a&gt;, a serialization format that&lt;br&gt;
promises lossless round-trips. The bug: the encoder emitted a bare &lt;code&gt;[]&lt;/code&gt; for&lt;br&gt;
empty arrays, where spec v3.0 §9.1 required an explicit zero-length header,&lt;br&gt;
&lt;code&gt;[0]:&lt;/code&gt;. Clean report — spec quote, failing test case, versions.&lt;/p&gt;

&lt;p&gt;Two weeks later the issue closed. Not because the encoder changed. Because the&lt;br&gt;
&lt;em&gt;spec&lt;/em&gt; did: v3.1 introduced &lt;code&gt;[]&lt;/code&gt; as a canonical form, v3.3 blessed it as the&lt;br&gt;
SHOULD form. My clause citation was mooted by two spec revisions filed after&lt;br&gt;
the report.&lt;/p&gt;

&lt;p&gt;Here's the part that makes this a story worth telling instead of a complaint:&lt;br&gt;
the finding survived anyway — because the report didn't rest on the clause.&lt;/p&gt;
&lt;h2&gt;
  
  
  Differential evidence outlives clause readings
&lt;/h2&gt;

&lt;p&gt;The bug came out of &lt;a href="https://github.com/antrixy/toon-diff" rel="noopener noreferrer"&gt;toon-diff&lt;/a&gt;, a&lt;br&gt;
differential conformance fuzzer I've been building. It has no opinion about&lt;br&gt;
what's correct. It runs data through one implementation's encoder and a&lt;br&gt;
&lt;em&gt;different&lt;/em&gt; implementation's decoder, for every ordered pair, and checks the&lt;br&gt;
round-trip survived:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;decode_Y( encode_X( value ) )  ==  value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the #322 report carried, alongside the clause citation, a&lt;br&gt;
cross-implementation table: the same input &lt;code&gt;[]&lt;/code&gt; through all three official&lt;br&gt;
implementations. TS→TS round-trips. TS→Python silently returns the corrupted&lt;br&gt;
&lt;em&gt;string&lt;/em&gt; &lt;code&gt;"[]"&lt;/code&gt;. TS→Rust throws a parse error. One input, three official&lt;br&gt;
implementations, three different outcomes.&lt;/p&gt;

&lt;p&gt;When the spec moved, the clause argument evaporated — but that table didn't.&lt;br&gt;
The closure itself says so: the encoder is now compliant, &lt;em&gt;and&lt;/em&gt; the cross-impl&lt;br&gt;
concern "stands until the ports catch up." The spec legitimized one side of&lt;br&gt;
the wire and hardened the obligation on the other (decoders MUST now accept&lt;br&gt;
both forms). The evidence determined which side got fixed; a clause reading&lt;br&gt;
alone would have just been wrong twice.&lt;/p&gt;

&lt;p&gt;For a fast-moving format — TOON went v1.0 → v3.0 in about a month — that's&lt;br&gt;
the durable way to file bugs: anchor to observed round-trip corruption, cite&lt;br&gt;
clauses as supporting context.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the matrix actually found
&lt;/h2&gt;

&lt;p&gt;Thirteen hand-designed seed cases, nine ordered pairs, 117 checks, seven&lt;br&gt;
divergences. The whole disagreement surface fits on one screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GRID (encoder row → decoder col): divergent cases per pair, of 13
  enc\dec  ts  python  rust
  ts        1       2     2
  python    1       ·     ·
  rust      1       ·     ·
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That cross through the TS row and column is &lt;code&gt;2^53+1&lt;/code&gt;: JavaScript's f64 rounds&lt;br&gt;
&lt;code&gt;9007199254740993&lt;/code&gt; to &lt;code&gt;…992&lt;/code&gt; at &lt;code&gt;JSON.parse&lt;/code&gt;, so every pair touching TS&lt;br&gt;
silently loses the integer — while Python↔Rust round-trips it perfectly. Each&lt;br&gt;
implementation round-trips its &lt;em&gt;own&lt;/em&gt; output fine. That's why&lt;br&gt;
single-implementation test suites stay green while this class of bug ships:&lt;br&gt;
the failures live at the boundary between implementations, and they're&lt;br&gt;
silent. A wrong value with no error is worse than a crash, and it's exactly&lt;br&gt;
what crossing implementations surfaces. (That finding is now filed as&lt;br&gt;
&lt;a href="https://github.com/toon-format/toon/issues/329" rel="noopener noreferrer"&gt;toon#329&lt;/a&gt; — a spec&lt;br&gt;
deep-dive showed the sharpest form is decoder-side: &lt;code&gt;decode()&lt;/code&gt; silently&lt;br&gt;
approximates valid wire tokens with no documented out-of-range policy,&lt;br&gt;
which the spec makes a MUST.)&lt;/p&gt;

&lt;p&gt;Other findings from the same loop, all filed upstream with minimized repros:&lt;br&gt;
a Rust encoder emitting grammar its own decoder rejects&lt;br&gt;
(&lt;a href="https://github.com/toon-format/toon-rust/issues/74" rel="noopener noreferrer"&gt;toon-rust#74&lt;/a&gt;), a&lt;br&gt;
Python encoder silently dropping a key&lt;br&gt;
(&lt;a href="https://github.com/toon-format/toon-python/issues/64" rel="noopener noreferrer"&gt;toon-python#64&lt;/a&gt;), and&lt;br&gt;
a TS decoder that parses &lt;em&gt;quoted string content&lt;/em&gt; as structure when a fake&lt;br&gt;
array header's item count happens to match — silent corruption with no error&lt;br&gt;
raised (&lt;a href="https://github.com/toon-format/toon/issues/324" rel="noopener noreferrer"&gt;toon#324&lt;/a&gt;). That&lt;br&gt;
last escalation argued from the spec's own security model, and the&lt;br&gt;
maintainers' triage adopted the framing: quote-aware opacity as the fix&lt;br&gt;
direction, with the count-match variant getting its own regression-test&lt;br&gt;
acceptance criterion.&lt;/p&gt;

&lt;h2&gt;
  
  
  A divergence is evidence, not a verdict
&lt;/h2&gt;

&lt;p&gt;The subtle discipline: two implementations disagreeing doesn't tell you who's&lt;br&gt;
wrong, or wrong by whose standard. Rust's decoder rejects &lt;code&gt;[]&lt;/code&gt; — but Rust&lt;br&gt;
pins spec v3.0, which &lt;em&gt;predates&lt;/em&gt; the &lt;code&gt;[]&lt;/code&gt; rule. It isn't violating its claim;&lt;br&gt;
it's behind it. Python claims no version at all, so it's measured against the&lt;br&gt;
current spec — and violates it. Same divergence, different verdicts.&lt;/p&gt;

&lt;p&gt;toon-diff encodes that distinction as machinery: implementations carry their&lt;br&gt;
claimed spec versions with evidence and verification dates; spec rules carry&lt;br&gt;
the sections and changelog entries that introduced them; verdicts —&lt;br&gt;
&lt;em&gt;behind&lt;/em&gt;, &lt;em&gt;violates-claimed&lt;/em&gt;, &lt;em&gt;violates-current&lt;/em&gt; — are computed per&lt;br&gt;
constrained side, and a decoder rule never indicts the encoder. Rules whose&lt;br&gt;
sections I haven't verified against the live spec render as&lt;br&gt;
citation-&lt;code&gt;PENDING&lt;/code&gt; rather than pretending. The tool doesn't just find&lt;br&gt;
disagreements; it says what they mean, and shows its work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two bugs in my own harness
&lt;/h2&gt;

&lt;p&gt;Honesty section. The v0.1 comparator corrupted &lt;code&gt;2^53+1&lt;/code&gt; the same way the TS&lt;br&gt;
implementation does — native &lt;code&gt;JSON.parse&lt;/code&gt; — so the most important case in the&lt;br&gt;
corpus would have false-PASSed. The fix was a lossless oracle that ingests&lt;br&gt;
numbers at their exact source lexeme, proven by a selftest before any&lt;br&gt;
cross-implementation claim runs. Your test harness is an implementation too,&lt;br&gt;
and it lies the same way until you prove otherwise.&lt;/p&gt;

&lt;p&gt;And twice I recorded upstream facts wrong from early recon — a claimed spec&lt;br&gt;
version, an issue reference. Both were caught by browser verification, and&lt;br&gt;
both fixes added tripwires: every upstream claim in the codebase now carries&lt;br&gt;
its evidence and verification date, and selftests pin every reference so a&lt;br&gt;
correction can't land halfway. Verification is a workflow, not a virtue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The through-line
&lt;/h2&gt;

&lt;p&gt;Every serious finding here is silent: an integer that rounds without an&lt;br&gt;
error, a string that comes back subtly different, a key that vanishes, quoted&lt;br&gt;
data that parses as structure when the counts happen to agree. Loud failures&lt;br&gt;
get fixed because they announce themselves. Silent ones get fixed when&lt;br&gt;
something crosses two implementations and checks.&lt;/p&gt;

&lt;p&gt;The full retrospective — with every finding's upstream trail, the verdict&lt;br&gt;
machinery, and the fuzzing operators that rediscover known bug classes from&lt;br&gt;
scratch — is in the repo:&lt;br&gt;
&lt;a href="https://github.com/antrixy/toon-diff/blob/main/RETROSPECTIVE.md" rel="noopener noreferrer"&gt;toon-diff/RETROSPECTIVE.md&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>opensource</category>
      <category>typescript</category>
      <category>fuzzing</category>
    </item>
    <item>
      <title>Block Google's AI Overviews at the Network Layer, Not the DOM</title>
      <dc:creator>Maverick Y</dc:creator>
      <pubDate>Thu, 02 Jul 2026 18:40:26 +0000</pubDate>
      <link>https://dev.to/maverickyadav/block-googles-ai-overviews-at-the-network-layer-not-the-dom-4aii</link>
      <guid>https://dev.to/maverickyadav/block-googles-ai-overviews-at-the-network-layer-not-the-dom-4aii</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Most extensions block Google's AI Overviews by hiding the panel with a content script after it renders — fragile, flickery, and always a step behind Google's markup changes. A better approach: force &lt;code&gt;udm=14&lt;/code&gt; at the network layer with &lt;code&gt;declarativeNetRequest&lt;/code&gt;, so the AI Overview never loads. The content script becomes a backstop, not the main mechanism. One Chrome API mystery — AI Mode being invisible to four different extension APIs — shows why the DOM was never the right layer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Google puts an AI Overview at the top of most search results now, and a lot of people would rather it didn't. So there's a whole shelf of Chrome extensions that remove it. Almost all of them work the same way, and I think that way is a mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  The obvious approach, and why it's a trap
&lt;/h2&gt;

&lt;p&gt;The default move is DOM-hiding: inject a content script, wait for the AI Overview panel to render, find it by class name or attribute, and set &lt;code&gt;display: none&lt;/code&gt;. It's the first thing anyone reaches for, and it works — until it doesn't.&lt;/p&gt;

&lt;p&gt;The problems are all baked into the approach. You're reacting &lt;em&gt;after&lt;/em&gt; the render, so there's a flash of AI content before your script catches it. You're matching against Google's markup, which is obfuscated and reshuffled constantly, so every layout change is a silent breakage. And you're paying for DOM churn on a page you don't control. You end up in a permanent game of catch-up against a page that changes whenever Google feels like it.&lt;/p&gt;

&lt;p&gt;The deeper issue is that you're operating one layer too high. The panel is a &lt;em&gt;symptom&lt;/em&gt;. By the time it's in the DOM, the work is already done — the server decided to send it, the page rendered it, and now you're scrambling to un-render it. If you can move the decision earlier, none of that scramble has to happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thesis: prevent it at the network layer
&lt;/h2&gt;

&lt;p&gt;Google Search takes a parameter, &lt;code&gt;udm&lt;/code&gt;, that selects which result vertical you get. &lt;code&gt;udm=14&lt;/code&gt; is the plain "Web" results view — the classic list of links, no AI Overview, no AI Mode. It's Google's own filter; we're just always asking for it.&lt;/p&gt;

&lt;p&gt;So instead of hiding the panel after it loads, force &lt;code&gt;udm=14&lt;/code&gt; onto the search request &lt;em&gt;before&lt;/em&gt; it loads. The AI Overview is never generated, never sent, never rendered. Nothing to hide because nothing arrived.&lt;/p&gt;

&lt;p&gt;Manifest V3's &lt;code&gt;declarativeNetRequest&lt;/code&gt; does exactly this — it rewrites requests by rule, without the extension ever reading the traffic. Here's the core rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;redirect&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;queryTransform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;addOrReplaceParams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;udm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;14&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;requestDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;www.google.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;urlFilter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/search?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;requestMethods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;resourceTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;main_frame&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;That's the whole mechanism. Every top-level search navigation gets &lt;code&gt;udm=14&lt;/code&gt; stamped onto it and comes back as clean Web results. No content script races the render, because there's no AI render to race. It's language-independent too — &lt;code&gt;udm&lt;/code&gt; is a server-side parameter, so it doesn't care whether Google is serving you English or German, which is a nice bonus over text-matching a panel heading.&lt;/p&gt;

&lt;p&gt;Note &lt;code&gt;resourceTypes: ["main_frame"]&lt;/code&gt; — this only touches the top-level page load, not the page's own internal requests, so it changes what you searched for without breaking how the page works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The proof that the DOM was never enough: AI Mode
&lt;/h2&gt;

&lt;p&gt;Here's the part that turned me from "the network layer is nicer" to "the DOM layer is genuinely the wrong place."&lt;/p&gt;

&lt;p&gt;Alongside AI Overviews, Google has AI Mode — a separate conversational surface you reach from a tab in the results. I wanted to suppress its page too, and I assumed the content-script backstop would catch it like anything else. It didn't. So I tried to observe that page from every angle a Chrome extension has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;declarativeNetRequest&lt;/code&gt; — no rule matched the navigation.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;MutationObserver&lt;/code&gt; on the document — fired for nothing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chrome.tabs.onUpdated&lt;/code&gt; — never reported the change.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chrome.storage.onChanged&lt;/code&gt; as a last resort signal — silent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four unrelated APIs, and every one came back blank — even though the address bar plainly showed &lt;code&gt;udm=50&lt;/code&gt; for AI Mode. Not "my selector missed it." Zero signal of any kind reached the extension.&lt;/p&gt;

&lt;p&gt;The consistent silence across four different mechanisms is the tell: that surface isn't part of the same page a content script attaches to. Whatever it is, it's stitched into the tab in a way that never injects a content script or surfaces a navigation the extension can see. Which means the DOM layer can't touch it &lt;em&gt;even in principle&lt;/em&gt;. If your entire strategy is "hide it in the DOM," you have no move here at all.&lt;/p&gt;

&lt;p&gt;The network-layer approach at least has an answer: since I can't catch that page, I prevent the click that leads to it — the extension hides the AI Mode tab in the results before it's ever pressed. Prevention again, one step earlier than the thing I couldn't reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the network layer can't do either
&lt;/h2&gt;

&lt;p&gt;I'd be selling you something if I stopped there, so here are the honest limits.&lt;/p&gt;

&lt;p&gt;Forcing &lt;code&gt;udm=14&lt;/code&gt; catches the searches that go through the search box. It does not catch someone who navigates &lt;em&gt;directly&lt;/em&gt; to a &lt;code&gt;udm=50&lt;/code&gt; URL — a typed address, an old bookmark, an external link — because there's no clean search request to rewrite. And it does nothing about Chrome's own omnibox AI button, which is native browser UI, not part of any page; no extension API can remove it.&lt;/p&gt;

&lt;p&gt;Those aren't bugs to fix later; they're the edge of what this layer owns. Naming them is the difference between an approach you trust and one you find out about the hard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the content script goes: the backstop, not the star
&lt;/h2&gt;

&lt;p&gt;I didn't delete the content script — I demoted it. It still runs, but as a labeled safety net for one specific case: Google quietly changing how &lt;code&gt;udm=14&lt;/code&gt; behaves. If that ever happens, the backstop hides whatever slipped through and the extension tells me about it, instead of silently degrading.&lt;/p&gt;

&lt;p&gt;That's the content script in its right role — a fallback for the thing the primary mechanism can't guarantee, not the primary mechanism itself. The same code that's fragile as a front line is perfectly reasonable as a backstop, because it only has to fire when something has already gone wrong.&lt;/p&gt;

&lt;p&gt;There's a small elaboration worth mentioning: I wanted users to be able to summon the AI Overview for a single search on demand. That's a second &lt;code&gt;declarativeNetRequest&lt;/code&gt; rule at higher priority that &lt;em&gt;allows&lt;/em&gt; a specifically-marked request through unmodified — the redirect and the exception live at the same layer, which keeps the whole thing coherent.&lt;/p&gt;

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

&lt;p&gt;Forget Google for a second. The lesson generalizes: when you're fighting a web page's behavior, find the layer that actually owns the decision before you reach for the DOM. The DOM is where behavior becomes &lt;em&gt;visible&lt;/em&gt;, which makes it the tempting place to intervene — and often the wrong one, because by then the decision is already made. Ask what produced the thing you're trying to stop, and see if you can get upstream of it. Sometimes you can't, and the DOM really is your only handle. But it's worth checking, because when you &lt;em&gt;can&lt;/em&gt; move upstream, the fragile parts of the problem tend to disappear rather than get patched.&lt;/p&gt;

&lt;p&gt;For this problem, that meant a request-rewrite rule instead of a render-and-hide loop — and an AI Mode mystery that made the point better than I could have on purpose.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I built this out as a small extension, &lt;a href="https://chromewebstore.google.com/detail/no-ai-search/bpgffbdgipfobnjdoikbbficmenihffg" rel="noopener noreferrer"&gt;No AI Search&lt;/a&gt;, if you want to see the whole thing shipped. Source is on &lt;a href="https://github.com/antrixy/no-ai-search" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>chrome</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>extensions</category>
    </item>
    <item>
      <title>I built a differential tester for TOON, and it found two silent-corruption bugs on the first run</title>
      <dc:creator>Maverick Y</dc:creator>
      <pubDate>Thu, 02 Jul 2026 16:18:56 +0000</pubDate>
      <link>https://dev.to/maverickyadav/i-built-a-differential-tester-for-toon-and-it-found-two-silent-corruption-bugs-on-the-first-run-4ia7</link>
      <guid>https://dev.to/maverickyadav/i-built-a-differential-tester-for-toon-and-it-found-two-silent-corruption-bugs-on-the-first-run-4ia7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — I built a differential tester for TOON: it runs data through one implementation's encoder and a &lt;em&gt;different&lt;/em&gt; implementation's decoder, and checks the round-trip survived. On its first run it found two silent-corruption bugs (a rounded 64-bit integer and an empty array that decoded to a corrupted string), both filed upstream. The hard part wasn't finding bugs — it was building a comparison oracle honest enough that its FAIL means something. Repo: &lt;a href="https://github.com/antrixy/toon-diff" rel="noopener noreferrer"&gt;github.com/antrixy/toon-diff&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I built a small tool that checks whether independent &lt;a href="https://github.com/toon-format/spec" rel="noopener noreferrer"&gt;TOON&lt;/a&gt; implementations actually agree with each other. On its first real run — across the TypeScript reference and the Python port — it found two silent-corruption bugs. Both are now filed upstream.&lt;/p&gt;

&lt;p&gt;This post is about &lt;em&gt;why&lt;/em&gt; the approach finds bugs that ordinary conformance suites miss, and about the one genuinely tricky part: building a comparison oracle that doesn't corrupt the data while it's checking it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup: TOON, and the promise of lossless round-trips
&lt;/h2&gt;

&lt;p&gt;TOON (Token-Oriented Object Notation) is a compact, line-oriented encoding of the JSON data model, designed for things like trimming token counts in LLM prompts. The whole value proposition rests on one property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JSON  →  TOON  →  JSON     should give you back what you started with.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are independent TOON implementations in 25+ languages. Each ships its own conformance tests. Each is green. And yet the moment you have more than one implementation, a new failure mode appears that none of those green test suites can see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why conformance suites miss the interesting bugs
&lt;/h2&gt;

&lt;p&gt;A conformance suite checks one implementation against a set of blessed expected outputs: &lt;em&gt;given input X, the encoder must produce exactly Y.&lt;/em&gt; That's useful, but it has a structural blind spot.&lt;/p&gt;

&lt;p&gt;Every implementation round-trips &lt;strong&gt;its own&lt;/strong&gt; output just fine. The TS encoder produces something the TS decoder reads back perfectly. The Python encoder produces something the Python decoder reads back perfectly. Both suites pass. The bug lives in the gap between them — when TS encodes something and &lt;em&gt;Python&lt;/em&gt; has to decode it, or vice versa.&lt;/p&gt;

&lt;p&gt;That's what differential testing targets directly. Instead of checking against expected outputs, you check implementations against &lt;strong&gt;each other&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;decode_Y( encode_X( value ) )  ==  value     for every ordered pair (X, Y)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With N implementations you run N×N ordered pairs (including each against itself, which is your control). Two implementations is a 4-cell matrix; three is 9. Any cell that fails is a place where two implementations disagree about what a given value means — and because both sides individually pass their own tests, nobody had noticed.&lt;/p&gt;

&lt;p&gt;The harness for this is almost trivial. Each implementation gets wrapped in a tiny adapter with a uniform, text-in/text-out contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Adapter&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="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jsonText&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&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="c1"&gt;// JSON text -&amp;gt; TOON text&lt;/span&gt;
  &lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toonText&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&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="c1"&gt;// TOON text -&amp;gt; JSON text&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Working on text means the harness never has to hold any language's native value model. Adding a new language is one adapter. The driver loop is just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;X&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;adapters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Y&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;adapters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jsonText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;back&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Y&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="nx"&gt;toon&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;oracle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;ingest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;back&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&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;All the difficulty is hiding in one word: &lt;code&gt;equal&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard part: an oracle that doesn't corrupt the data it's judging
&lt;/h2&gt;

&lt;p&gt;Here's the trap. You want to compare the original value against the round-tripped value. The obvious way is to parse both back into native objects and compare. In JavaScript that means &lt;code&gt;JSON.parse&lt;/code&gt;. And &lt;code&gt;JSON.parse&lt;/code&gt; will quietly destroy the exact cases you most want to test.&lt;/p&gt;

&lt;p&gt;Consider the integer &lt;code&gt;9007199254740993&lt;/code&gt;, which is 2⁵³ + 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;9007199254740993&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// -&amp;gt; 9007199254740992   (!!)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It comes back as 2⁵³, off by one, because it rounded through an IEEE-754 double. If your oracle parses values this way, then when one implementation preserves the integer and another rounds it, your oracle rounds &lt;em&gt;both&lt;/em&gt; and reports a false &lt;strong&gt;PASS&lt;/strong&gt; — on the single most important case in the suite. The comparator silently corrupts the evidence.&lt;/p&gt;

&lt;p&gt;My first version "solved" this by quarantining such cases — detecting numbers that couldn't survive a float and benching them. But that benches exactly the inputs where implementations with different number models (JS &lt;code&gt;f64&lt;/code&gt;, Python arbitrary-precision int, Rust &lt;code&gt;i64/u64/f64&lt;/code&gt;) are &lt;em&gt;guaranteed&lt;/em&gt; to diverge. You're throwing away your strongest evidence to protect a broken comparator.&lt;/p&gt;

&lt;p&gt;The fix is to never let a number touch a float. ES2023 added a &lt;code&gt;context&lt;/code&gt; argument to the &lt;code&gt;JSON.parse&lt;/code&gt; reviver that hands you the &lt;strong&gt;exact source lexeme&lt;/strong&gt; of each value, before any numeric conversion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;NUM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;num&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// collision-proof: JSON.parse can't produce a Symbol key&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ingest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawText&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="nx"&gt;Node&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;source&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// ctx.source is the raw digits as written: "9007199254740993",&lt;/span&gt;
      &lt;span class="c1"&gt;// captured BEFORE the lossy f64 conversion that produced `value`.&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;NUM&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nf"&gt;canonicalNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&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;code&gt;ctx.source&lt;/code&gt; is the string &lt;code&gt;"9007199254740993"&lt;/code&gt; — the actual characters from the input — even though &lt;code&gt;value&lt;/code&gt; is already the rounded double. We ignore &lt;code&gt;value&lt;/code&gt; entirely and keep the digits. Numbers are stored as a &lt;code&gt;Symbol&lt;/code&gt;-tagged node so they can never collide with a real object that happens to have a &lt;code&gt;"__num"&lt;/code&gt; key.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;canonicalNumber&lt;/code&gt; then reduces the lexeme to a canonical &lt;strong&gt;value&lt;/strong&gt; form using arbitrary-precision string arithmetic — never an &lt;code&gt;f64&lt;/code&gt; — so &lt;code&gt;2⁵³+1&lt;/code&gt; stays itself all the way through the comparison.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inside canonicalNumber: value identity without a float
&lt;/h3&gt;

&lt;p&gt;The reviver gets us the raw digits; the remaining job is to map two different lexemes that denote the &lt;em&gt;same number&lt;/em&gt; to the &lt;em&gt;same string&lt;/em&gt;, without ever evaluating them numerically. &lt;code&gt;"1.0"&lt;/code&gt;, &lt;code&gt;"1.00"&lt;/code&gt;, and &lt;code&gt;"1e0"&lt;/code&gt; must all become &lt;code&gt;"1"&lt;/code&gt;; &lt;code&gt;"1e-2"&lt;/code&gt; must become &lt;code&gt;"0.01"&lt;/code&gt;; and &lt;code&gt;"9007199254740993"&lt;/code&gt; must stay exactly itself. The whole thing is regex + string shifts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canonicalNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lex&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="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;([&lt;/span&gt;&lt;span class="sr"&gt;+-&lt;/span&gt;&lt;span class="se"&gt;]?)(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)(?:\.(\d&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;))?(?:[&lt;/span&gt;&lt;span class="sr"&gt;eE&lt;/span&gt;&lt;span class="se"&gt;]([&lt;/span&gt;&lt;span class="sr"&gt;+-&lt;/span&gt;&lt;span class="se"&gt;]?\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;))?&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;lex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// not a well-formed JSON number; fail safe, never throw&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// all significant digits, point removed&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pointPos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// where the point lands&lt;/span&gt;

  &lt;span class="c1"&gt;// Shift the decimal point by `pointPos`, padding with zeros when it falls&lt;/span&gt;
  &lt;span class="c1"&gt;// outside the digit run — pure string surgery, no parseFloat anywhere.&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;intStr&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="nx"&gt;fracStr&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pointPos&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;intStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;fracStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;pointPos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pointPos&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;intStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pointPos&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;fracStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;                                &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;intStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pointPos&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;fracStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pointPos&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;intStr&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;intStr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^0+&lt;/span&gt;&lt;span class="se"&gt;(?=\d)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// strip leading zeros, keep one&lt;/span&gt;
  &lt;span class="nx"&gt;fracStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fracStr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/0+$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// strip trailing zeros&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intStr&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;fracStr&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// canonical zero, sign dropped&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fracStr&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;intStr&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fracStr&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;intStr&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;Two things worth calling out. First, the &lt;code&gt;if (!m) return lex&lt;/code&gt; line: a lexeme that doesn't match the JSON number grammar is returned untouched rather than throwing — the oracle should never crash on input, it should compare faithfully and let the &lt;em&gt;result&lt;/em&gt; be the signal. Second, this is the exact spot where the value-vs-representation policy lives. Returning a value-normalized form here is what makes &lt;code&gt;1.0 == 1&lt;/code&gt; and &lt;code&gt;-0 == 0&lt;/code&gt;. If you instead wanted to &lt;em&gt;flag&lt;/em&gt; representational drift — say, to surface that one implementation preserves &lt;code&gt;-0&lt;/code&gt; while another normalizes it — you'd return a representation-preserving form here (keep the trailing &lt;code&gt;.0&lt;/code&gt;, keep the leading &lt;code&gt;-&lt;/code&gt; on zero) and integers would still compare exactly. One function, one deliberate stance, documented in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one real judgment call: compare by value, with exact integers
&lt;/h2&gt;

&lt;p&gt;Once numbers survive ingestion intact, you have to decide what "equal" &lt;em&gt;means&lt;/em&gt;. This is the only genuinely opinionated part of the oracle, and it's worth stating explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.0     == 1        value-equal (RFC 8259: these denote the same number)
-0      == 0        value-equal (JSON's value model has no signed zero)
2^53+1  != 2^53     DIFFERENT integers — precision loss is a real divergence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In other words: compare by mathematical value (so representational noise like &lt;code&gt;1.0&lt;/code&gt; vs &lt;code&gt;1&lt;/code&gt; doesn't generate false positives), but preserve exact integer identity (so genuine precision loss is caught). That's the correct default for a "did the round-trip stay lossless?" question. Everything else in the oracle is strict: types don't coerce (&lt;code&gt;"123"&lt;/code&gt; ≠ &lt;code&gt;123&lt;/code&gt;), array order matters, missing keys differ from explicit nulls, and there's no Unicode normalization (&lt;code&gt;e&lt;/code&gt; + combining acute ≠ precomposed &lt;code&gt;é&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  How equality is actually computed
&lt;/h3&gt;

&lt;p&gt;Rather than a recursive deep-equal with a pile of type checks, the oracle serializes each value tree to a single canonical string and compares the strings. The serialization is where the strictness is enforced structurally, so it can't be forgotten case by case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Node&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&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="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;null&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;boolean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// quoted form&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isNum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;               &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;NUM&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;               &lt;span class="c1"&gt;// unquoted #-token&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                               &lt;span class="c1"&gt;// key order normalized&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;equal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detail that does the heavy lifting: a string serializes to its &lt;strong&gt;quoted&lt;/strong&gt; form (&lt;code&gt;"123"&lt;/code&gt; → &lt;code&gt;"123"&lt;/code&gt;) while a number serializes to a &lt;code&gt;#&lt;/code&gt;-prefixed token (&lt;code&gt;123&lt;/code&gt; → &lt;code&gt;#123&lt;/code&gt;). Those can never collide, so the string &lt;code&gt;"123"&lt;/code&gt; and the number &lt;code&gt;123&lt;/code&gt; are structurally incapable of comparing equal — type-strictness falls out of the representation instead of being a check you have to remember to write. Object keys are sorted so key &lt;em&gt;order&lt;/em&gt; doesn't matter, but arrays aren't, so element order does. And because the leaves are the &lt;code&gt;Symbol&lt;/code&gt;-tagged value-form numbers from &lt;code&gt;canonicalNumber&lt;/code&gt;, exact-integer identity is already baked in by the time we get here.&lt;/p&gt;

&lt;p&gt;This is also what makes the oracle cheaply self-provable: &lt;code&gt;canonical&lt;/code&gt; has no dependency on TOON at all, so its self-test just asserts pairs of values that must (or must not) share a canonical string — &lt;code&gt;1.0&lt;/code&gt;/&lt;code&gt;1&lt;/code&gt; equal, &lt;code&gt;2⁵³+1&lt;/code&gt;/&lt;code&gt;2⁵³&lt;/code&gt; not, &lt;code&gt;"123"&lt;/code&gt;/&lt;code&gt;123&lt;/code&gt; not — and runs before any adapter is touched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the matrix: the &lt;em&gt;shape&lt;/em&gt; of the failures diagnoses the bug
&lt;/h2&gt;

&lt;p&gt;With two adapters (TS, Python) and 13 cases, a run is 2×2×13 = 52 pair-checks. Most cells pass. But the interesting thing isn't &lt;em&gt;that&lt;/em&gt; cells fail — it's &lt;em&gt;which&lt;/em&gt; cells fail, because the pattern tells you what kind of bug you're looking at before you read a single value.&lt;/p&gt;

&lt;p&gt;Here are the two cases that diverged, as 2×2 grids. Rows are the encoder, columns are the decoder; ✓ means the round-trip survived, ✗ means the oracle caught a divergence:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case &lt;code&gt;013&lt;/code&gt; — integer &lt;code&gt;9007199254740993&lt;/code&gt; (2⁵³+1):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                decode_TS   decode_Py
  encode_TS        ✗            ✗
  encode_Py        ✗            ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three of four pairs fail — including &lt;code&gt;TS→TS&lt;/code&gt;, the self-pair that's supposed to be your control. That shape is the diagnosis: when even an implementation's round-trip &lt;em&gt;with itself&lt;/em&gt; fails, the problem isn't a handoff between languages, it's that implementation's &lt;strong&gt;number model&lt;/strong&gt;. TS holds the integer as an &lt;code&gt;f64&lt;/code&gt; the instant it touches it, so &lt;code&gt;encode_TS&lt;/code&gt; has already lost the digit before any decoder runs; and &lt;code&gt;decode_TS&lt;/code&gt; re-loses it even when Python encoded it faithfully. The only surviving cell is &lt;code&gt;Py→Py&lt;/code&gt;, because Python's arbitrary-precision integers never round. A failure that includes the diagonal = an encode/decode-side capability limit, not a protocol disagreement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case &lt;code&gt;002&lt;/code&gt; — empty array &lt;code&gt;[]&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                decode_TS   decode_Py
  encode_TS        ✓            ✗
  encode_Py        ✓            ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exactly &lt;strong&gt;one&lt;/strong&gt; cell fails: &lt;code&gt;encode_TS → decode_Py&lt;/code&gt;. That's the signature of a true cross-implementation handoff bug. TS encodes the empty array as the bare &lt;code&gt;[]&lt;/code&gt; — a form TS's &lt;em&gt;own&lt;/em&gt; decoder happily reads back (so &lt;code&gt;TS→TS&lt;/code&gt; passes and its conformance suite stays green), but which Python's decoder chokes on, returning the corrupted &lt;code&gt;'['&lt;/code&gt;. Python's own output (&lt;code&gt;[0]:&lt;/code&gt;, the spec-canonical form) is read correctly by everyone, so its whole row passes. A single off-diagonal failure points straight at "implementation A emits something only A can read."&lt;/p&gt;

&lt;p&gt;This is the entire argument for differential testing in one picture. The &lt;code&gt;013&lt;/code&gt; pattern (fails on the diagonal) and the &lt;code&gt;002&lt;/code&gt; pattern (fails on exactly one off-diagonal cell) are &lt;em&gt;different bug classes&lt;/em&gt;, and you can tell them apart by the geometry of the matrix without even inspecting the payloads. A single-implementation conformance suite only ever runs the diagonal — so it can see &lt;code&gt;013&lt;/code&gt; (sort of, if it tests the boundary) but is structurally blind to &lt;code&gt;002&lt;/code&gt;, whose only failing cell is off-diagonal by definition.&lt;/p&gt;

&lt;p&gt;Scale this to three adapters and it's a 3×3 grid per case; the Rust adapter (different number model again) turns each case into 9 pair-checks and adds a whole new row and column of handoffs where divergences can hide.&lt;/p&gt;

&lt;p&gt;Thirteen hand-designed probe cases, aimed at known fault lines: empty containers, almost-uniform tables, string-lookalikes, Unicode edge cases, whitespace, and the number boundaries around 2⁵³. First run, two real divergences.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Integer 2⁵³+1 — silent precision loss across the boundary
&lt;/h3&gt;

&lt;p&gt;Input &lt;code&gt;{"unsafe": 9007199254740993}&lt;/code&gt;. The TS path rounds it to &lt;code&gt;...992&lt;/code&gt;; the Python path preserves it exactly. Round-trip each through &lt;em&gt;itself&lt;/em&gt; and both pass — the loss only appears when you cross them, which is precisely what the matrix does.&lt;/p&gt;

&lt;p&gt;The honest framing here matters: TOON's spec permits precision loss for numbers outside a host's safe range &lt;strong&gt;if the implementation documents it&lt;/strong&gt;. So this isn't a flat bug — it's a documented-divergence boundary, and the right place to surface it is the tool's README, not an accusatory issue. Differential testing is what makes the boundary &lt;em&gt;visible&lt;/em&gt; instead of theoretical.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Empty array — a genuine cross-implementation bug, both halves
&lt;/h3&gt;

&lt;p&gt;This one's a real bug, and it has two sides.&lt;/p&gt;

&lt;p&gt;The spec is explicit (&lt;a href="https://github.com/toon-format/spec" rel="noopener noreferrer"&gt;§9.1&lt;/a&gt;): an empty array encodes as &lt;code&gt;[0]:&lt;/code&gt;. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encoder side (TS):&lt;/strong&gt; &lt;code&gt;encode([])&lt;/code&gt; emits the bare &lt;code&gt;[]&lt;/code&gt;, with no length header — non-conformant to §9.1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decoder side (Python):&lt;/strong&gt; &lt;code&gt;decode("[]")&lt;/code&gt; returns the string &lt;code&gt;'['&lt;/code&gt; — a single character, with the &lt;code&gt;]&lt;/code&gt; silently dropped. Not the empty array, not an error, not even the literal string &lt;code&gt;"[]"&lt;/code&gt;. Just corrupted output flowing downstream with no signal.
&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="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# -&amp;gt; '['            corrupted
&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[0]:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# -&amp;gt; []             correct (canonical form)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both halves filed: &lt;a href="https://github.com/toon-format/toon/issues/322" rel="noopener noreferrer"&gt;toon-format/toon#322&lt;/a&gt; (encoder) and &lt;a href="https://github.com/toon-format/toon-python/issues/61" rel="noopener noreferrer"&gt;toon-format/toon-python#61&lt;/a&gt; (decoder). Each side individually "worked" — the bug only existed in the handoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest status: a differential &lt;em&gt;probe&lt;/em&gt;, not yet a &lt;em&gt;fuzzer&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;I want to be precise about what this is. Right now it's 13 curated inputs, not a generative fuzzer. The next step is a mutation-based generator that takes those seeds and pushes along the same fault lines — boundary integers, delimiter-adjacent strings, near-uniform tables, empty containers — then shrinks any failure to a minimal reproducer. That's what turns "I picked inputs that break things" into "the tool finds inputs nobody wrote." A third adapter (Rust, with its &lt;code&gt;i64/u64/f64&lt;/code&gt; split) widens the number-model coverage where the next divergences most likely hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transferable idea
&lt;/h2&gt;

&lt;p&gt;None of this is TOON-specific. If you maintain any format with multiple independent implementations — a serializer, a parser, a protocol codec — the same shape applies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cross, don't self-check.&lt;/strong&gt; Run &lt;code&gt;decode_B(encode_A(x))&lt;/code&gt;, not &lt;code&gt;decode_A(encode_A(x))&lt;/code&gt;. Same-implementation round-trips hide boundary bugs by construction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't let your oracle corrupt the evidence.&lt;/strong&gt; If your comparison path rounds, normalizes, or coerces, it will mask the exact divergences you're hunting. Capture values losslessly (the &lt;code&gt;ctx.source&lt;/code&gt; trick is a clean way to do it in JS) and decide your equality semantics deliberately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove the judge independently.&lt;/strong&gt; The oracle must pass its own self-test with zero dependency on the things it judges.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/antrixy/toon-diff" rel="noopener noreferrer"&gt;https://github.com/antrixy/toon-diff&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two real bugs, no fuzzer yet, on inputs a person hand-wrote. The interesting part wasn't the bugs — it was building a comparator honest enough to believe when it said FAIL.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>fuzzing</category>
    </item>
    <item>
      <title>Decades In, and a Date Field Still Got Me</title>
      <dc:creator>Maverick Y</dc:creator>
      <pubDate>Wed, 01 Jul 2026 02:14:52 +0000</pubDate>
      <link>https://dev.to/maverickyadav/decades-in-and-a-date-field-still-got-me-3e43</link>
      <guid>https://dev.to/maverickyadav/decades-in-and-a-date-field-still-got-me-3e43</guid>
      <description>&lt;p&gt;I've been writing software for a long time. Long enough to have shipped in languages people don't run anymore, long enough to have made just about every mistake in the book at least once. You'd think that by now the simple bugs would be behind me.&lt;/p&gt;

&lt;p&gt;They're not. And this is a story about one of them — written, mostly, for the junior developers who think the dangerous bugs are the complicated ones.&lt;/p&gt;

&lt;p&gt;They aren't. Let me explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;A flow that very few people use, but where each transaction carries a serious dollar amount, quietly stopped letting anyone complete it. No crash. No alarm. No stack trace begging for attention. Just a validation check that calmly, confidently refused to let people finish.&lt;/p&gt;

&lt;p&gt;The cause: a date comparison was treating the &lt;em&gt;current&lt;/em&gt; month as if it were in the future. A perfectly valid entry got rejected as impossible. Nobody on the other end was doing anything wrong — the code had simply decided that "now" hadn't arrived yet.&lt;/p&gt;

&lt;p&gt;Low traffic, high value. That combination matters. When millions of people hit a broken page, you find out in minutes. When a handful of people hit it — but each one represents real money on the line — the failure is quieter and the cost per failure is enormous. It's the kind of thing that doesn't trip your dashboards but absolutely lands on someone's desk.&lt;/p&gt;

&lt;p&gt;And here's the part I have to own: I didn't find it. A user did. They were the ones who hit the wall, figured out something was wrong, and reported it. My tests were green. My dashboards were calm. My decades of instinct hadn't flagged a thing. The failure surfaced because someone on the outside ran into it and bothered to tell us.&lt;/p&gt;

&lt;p&gt;Once it was in front of me, the diagnosis was fast and the fix took minutes. But I want to be honest about how it felt: not triumphant. The bug was an off-by-one in date logic, the kind of mistake I'd have sworn I was long past — and it took a user to point at it before I ever saw it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I want the juniors to hear
&lt;/h2&gt;

&lt;p&gt;Early in your career, you'll assume that big problems have big causes. That if something important breaks, the explanation must be appropriately sophisticated — a race condition, a subtle architectural flaw, something worthy of the damage.&lt;/p&gt;

&lt;p&gt;I'm here, decades in, to tell you that's a comforting lie.&lt;/p&gt;

&lt;p&gt;Software doesn't grade on a curve. A one-line date mistake and a thousand-line design failure can produce the identical outcome: a person blocked, money stalled, and no idea why. The user never sees whether the cause was elegant or embarrassing. They just see a door that won't open.&lt;/p&gt;

&lt;p&gt;The bugs that have cost me — and the businesses I've worked for — the most over my career were almost never the clever ones. The clever ones get attention. They get design reviews, careful testing, three sets of eyes. The simple ones slip through &lt;em&gt;precisely because&lt;/em&gt; they look too trivial to be wrong. Nobody scrutinizes a date check the way they scrutinize the scary concurrency code. That's exactly why the date check is the one that bites.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell my younger self
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dates are a trap that never stops being a trap.&lt;/strong&gt; Timezones, month boundaries, "today," inclusive vs. exclusive ranges, off-by-one at the edges. I have decades of experience and date logic &lt;em&gt;still&lt;/em&gt; demands my full attention. Treat every comparison like it's hiding an edge case, because it is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low volume is not low risk.&lt;/strong&gt; Don't measure a bug's severity by how many people hit it. Measure it by what each hit costs. A rarely used path guarding something valuable deserves more care than a busy path guarding something trivial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The boring bugs are the dangerous ones.&lt;/strong&gt; They survive code review because they look harmless. Your instinct will be to spend your scrutiny on the complex code. Fight that instinct and give the "obvious" code a second look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calm is a senior skill, and you can start building it now.&lt;/strong&gt; I diagnosed this quickly not because I'm brilliant but because once it was in front of me I didn't burn twenty minutes panicking. I read the logic, traced the input, and trusted that the cause was probably mundane. It almost always is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your users are your last line of defense — which means your earlier lines failed.&lt;/strong&gt; A user caught this, not me, not my tests, not my monitoring. That's a gift and a warning at the same time. Be grateful when someone reports a bug clearly; they just did your QA for free. But also ask the harder question: why did it have to reach them at all? Every user-reported bug is a quiet audit of the safety nets that didn't catch it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the boundaries, not the middle.&lt;/strong&gt; Bugs don't live at "obviously valid" or "obviously invalid." They live at the edges — the first of the month, the last second of the year, the value that's exactly equal. Write the test for the edge before you write the code for the middle.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The thing that took me years to accept
&lt;/h2&gt;

&lt;p&gt;When you find a bug this simple, a voice shows up: &lt;em&gt;how did I miss something so obvious?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I still hear that voice. The difference now is that I don't believe it anymore. Obvious-in-hindsight is the natural condition of nearly every bug ever fixed. Hindsight makes everything look inevitable.&lt;/p&gt;

&lt;p&gt;The measure of a good engineer was never &lt;em&gt;never writing the simple mistake.&lt;/em&gt; You will write it. I still write it, after all this time. The measure is whether you can stay calm, find it fast, and fix it before it costs someone something that matters.&lt;/p&gt;

&lt;p&gt;To the juniors reading this: the unglamorous saves count. Quietly. Permanently. Most of the real work of this job looks exactly like this — not heroic, just careful. Get comfortable with that early, and the decades go a lot smoother.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>career</category>
    </item>
    <item>
      <title>Cut LLM prompt tokens on structured data — losslessly</title>
      <dc:creator>Maverick Y</dc:creator>
      <pubDate>Sat, 27 Jun 2026 12:01:37 +0000</pubDate>
      <link>https://dev.to/maverickyadav/cut-llm-prompt-tokens-on-structured-data-losslessly-op5</link>
      <guid>https://dev.to/maverickyadav/cut-llm-prompt-tokens-on-structured-data-losslessly-op5</guid>
      <description>&lt;h1&gt;
  
  
  Cut LLM prompt tokens on structured data — losslessly
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;A small, dependency-free tool for shrinking logs, JSON, and CSV in prompts — without dropping a single byte.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Logs, JSON, and CSV are some of the bulkiest, most repetitive things we feed into LLMs. They're also where prompt-token costs quietly pile up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trouble with lossy compression
&lt;/h2&gt;

&lt;p&gt;The usual fix is &lt;strong&gt;semantic compression&lt;/strong&gt;: have a model summarize the input and drop "low-information" tokens. It works — until the question needs the data that got dropped.&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"How many errors are in this log?"&lt;/em&gt;&lt;br&gt;
&lt;em&gt;"What's the total across these 400 rows?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;…and a lossy compressor can hand back a &lt;strong&gt;confident, wrong answer&lt;/strong&gt; — because the rows it discarded were exactly the ones you needed. The compression looks great. The answer is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  A different bet: lossless or no-op
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ctxfold&lt;/strong&gt; takes the opposite approach. Its single rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lossless or no-op. Never lossy.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of summarizing, it re-encodes &lt;em&gt;structure&lt;/em&gt;. Logs, JSON arrays, and CSV are tables in disguise — the same keys, prefixes, and templates repeat on every line. ctxfold lifts those repeated parts into a one-time header and keeps only what varies per row, producing a compact, self-labeling table the model reads directly. &lt;strong&gt;Nothing is dropped.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The guarantee is enforced in code: every encoder ships with a decoder, and &lt;code&gt;compress()&lt;/code&gt; verifies that decoding its output reproduces the input &lt;em&gt;before&lt;/em&gt; returning it. If it can't, you get your original text back, untouched. It &lt;strong&gt;can't corrupt your data&lt;/strong&gt; — worst case, it does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does the model still read it?
&lt;/h2&gt;

&lt;p&gt;For logs and JSON — yes. On real data, ctxfold cuts &lt;strong&gt;~35–40% of tokens&lt;/strong&gt; on templated logs and JSON arrays, fully losslessly. And because the output is plain, labeled text, the model reads it as well as the raw input — in lookup tests against GPT-4o-mini, answers off the compressed form matched answers off the raw data, field for field.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Readability is validated per format, on GPT-4o-mini; the lossless guarantee is model-independent. CSV turned out differently — see the update below.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Update (July 2026)
&lt;/h2&gt;

&lt;p&gt;When I wrote this, CSV readability was the one unvalidated cell in the benchmark table. I've since measured it with the same harness used for JSON and logs — &lt;strong&gt;and it failed&lt;/strong&gt;: folded CSV scored 0/24 on GPT-4o-mini and 6–9/24 on GPT-4o, against 24/24 raw.&lt;/p&gt;

&lt;p&gt;The root cause is structural: JSON and logs fold &lt;em&gt;syntax&lt;/em&gt; (keys, braces, templates) and every value stays verbatim in the row. CSV has no syntax to remove, so its folding factors the &lt;em&gt;data itself&lt;/em&gt; — and models don't reliably reconstruct values through a header at read time.&lt;/p&gt;

&lt;p&gt;So as of v0.1.4, CSV folding is documented as &lt;strong&gt;pipeline-mode&lt;/strong&gt;: fold for lossless transit, &lt;code&gt;decompress()&lt;/code&gt; before the model reads it. For direct model reading, send CSV raw. JSON and logs remain validated direct-readable.&lt;/p&gt;

&lt;p&gt;v0.2.0 also shipped &lt;code&gt;ctxfold --profile&lt;/code&gt; — it shows where your prompt's characters go and what folding would save, with the same measured-claims rules. Zero-setup demo: &lt;code&gt;node examples/profile-demo.js&lt;/code&gt; after cloning &lt;a href="https://github.com/antrixy/ctxfold" rel="noopener noreferrer"&gt;the repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Full story of the CSV result and the profiler: &lt;a href="https://dev.to/maverickyadav/i-built-a-readability-test-for-my-own-compression-format-it-scored-024-4epc"&gt;I built a readability test for my own compression format. It scored 0/24.&lt;/a&gt;&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;npm &lt;span class="nb"&gt;install &lt;/span&gt;ctxfold
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compress&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ctxfold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bigLogOrJsonOrCsv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// send `text` instead of the original&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokenRatio&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;% fewer tokens, lossless: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lossless&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a pure text transform — no API calls, no model, zero dependencies — so it works with any LLM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not a replacement — the other half
&lt;/h2&gt;

&lt;p&gt;ctxfold isn't a competitor to semantic compression; it's the complement. &lt;strong&gt;Summarize to extract a subset; ctxfold to shrink repetition without losing anything.&lt;/strong&gt; It shines on structured data, not prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;This started from a simple frustration: lossy prompt compressors gave impressive token savings, but on aggregate questions — counts, totals, "find this record" — the answers came back wrong, because the data needed to answer had been summarized away. Great compression, broken results. The fix wasn't a smarter summarizer; it was to stop dropping data at all. Repetitive structured text is compressible &lt;em&gt;losslessly&lt;/em&gt; — you just have to treat it as structure instead of prose.&lt;/p&gt;

&lt;p&gt;If you push a lot of logs, JSON, or CSV into prompts, I'd genuinely like to know what your payloads look like and whether the lossless tradeoff fits your use case. &lt;strong&gt;What's eating the most tokens in your prompts right now?&lt;/strong&gt; Questions, critique, and edge cases that break it are all welcome in the comments.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo &amp;amp; docs:&lt;/strong&gt; &lt;a href="https://github.com/antrixy/ctxfold" rel="noopener noreferrer"&gt;https://github.com/antrixy/ctxfold&lt;/a&gt; · &lt;strong&gt;npm:&lt;/strong&gt; &lt;code&gt;npm install ctxfold&lt;/code&gt; · MIT licensed.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
