Skip to content
crawlforgeEspañol
It broke12 October 20267 min readLeer en español

The diff file nobody could open

0.12.0 reopens the file that diff --out saves, lists every row without the cut-off, and fixes a summary that hid 1,171 sitemap changes.

Since the first version of crawlforge diff there has been a --out flag. It writes the comparison to a SQLite file of its own, with one row per change and a header table that says which two crawls it came from and whether the result can be trusted. The idea was sound: a diff you can keep is a diff you can look at again next month, or hand to someone else.

The tool had no way to read that file back. If you passed it to report, you got a polite error explaining that it was a diff file and not a crawl. That message was itself a fix: before it, the answer was no such table: urls. But polite or not, the only way to see what you had saved was to open it with a SQLite client and write queries. A tool that produces a file it can’t open has only done half the job, and 0.12.0 does the other half.

Same command, one file

crawlforge diff before.sqlite after.sqlite --out changes.sqlite   # as always
crawlforge diff changes.sqlite                                    # new

I considered a separate command and dropped it. With changes.sqlite in front of you, the first thing you type is crawlforge diff changes.sqlite, so that’s what opens it. If you pass a single file that turns out to be a crawl, the error says comparing needs two files, and if the crawl has its .prev.sqlite next to it, it prints the exact command to run.

The reopened summary comes from the same printing code as the live one. That was the whole point: a reader that drew its own version of the summary would end up saying something slightly different from the original, and then you’d have two truths about one diff. To check it, I compared two real crawls of especialmundial.com, from 31 July and 2 August, saved the result, and ran a plain diff on the two outputs:

5a6
>   Saved     diff generated 2026-10-12T08:14:45Z by crawlforge 0.11.0 · …/em.diff.sqlite

That’s the only difference: one line saying when the file was generated and by which version. It’s there because a diff from a month ago read as if it were today’s gets people fixing things that were already fixed.

The warnings were baked in English

A diff can be inconclusive. If one of the two crawls was cut short by a URL limit, a URL that’s missing from it may have disappeared, or the crawl may simply never have reached it. The tool then hides the absences it can’t assert and says so first, in a warning. That warning matters more than any other line in the file.

When --out was written, we decided to store warnings in English only, so that the same file wouldn’t say different things depending on whose terminal created it. That decision was correct and it left a problem: reopening the file with --lang es would show a Spanish summary with English warnings in the middle.

Each warning is now stored with a kind and its data, like {"kind": "truncated", "side": "after", "reason": "max_urls"}, next to the English text that was already there. The reader rebuilds the warning and translates it. The keys are additive, so the file format version doesn’t change, and a file written by 0.11.0 still opens. I tested that with the installed 0.11.0 binary: its warnings come back as the English text they saved, and a diff that was inconclusive is still marked as inconclusive. A warning lost on reopening could have been exactly the one that said the diff couldn’t be trusted.

Every row, not three

The terminal summary shows three examples per group on purpose. A report that prints four hundred URLs doesn’t get read. The rest was in the file, and until now, getting to it meant SQL.

Two flags list everything, on a saved diff or on a live comparison:

crawlforge diff changes.sqlite --rule CONTENT-THIN
crawlforge diff changes.sqlite --change url_removed

On the especialmundial pair, --rule CONTENT-THIN gives the five pages with a new thin-content finding and the one that stopped having it. Title, meta description and canonical changes show the full text before and after, since that’s what you came for. A changed robots.txt shows only the lines that went and the lines that came, not both files in full. If you mistype the change type, the error lists the twelve valid ones. It’s the same idea as report --rule, which did this for a single crawl.

There’s one flag a saved diff refuses: --fail-on, the CI gate. Deciding whether new findings can be asserted depends on whether the “before” crawl finished, and the file doesn’t store that. A gate that passes for lack of data is worse than no gate, so the error says to run it on the two crawls.

What turned up on a real crawl

Building the reader meant looking at the summary with fresh eyes, and on the biggest crawl we have, two 20,000-URL runs of diariodelamancha.com, it showed this:

── Other changes ─────────────────────────────
  sitemap https://diariodelamancha.com/category-sitemap.xml present: yes → no
  …
  sitemap https://diariodelamancha.com/post-sitemap10.xml url_count: 196 → 150
  Findings still present            278,244

Six sitemap lines and nothing more. The file had 1,177. Every other list in the summary ends with “… and N more”; this one stopped at six without saying anything. Those two crawls were truncated, so that diff was already marked inconclusive, but a list that hides 99% of its rows without saying so is a bug in any diff. It now says “… and 1,171 more”, and when anything has been cut short, the summary ends by naming the two flags that show the rest.

The test that caught me first

The tests for the reader include one for robots.txt: a file that goes from Disallow: /wp-admin/ to Disallow: / should list exactly those two lines. It failed the first time it ran, and the output was this:

- User-agent: *�Disallow: /wp-admin/

The text from the site goes through a filter that replaces control characters, so that a title with escape sequences can’t paint your terminal. I applied it before splitting the file into lines, and a newline is a control character. Each line now gets cleaned on its own, after the split.

That one went red without help. For the rest, I followed the rule that came out of the test that passed with the bug: break the code on purpose and watch each new test fail. Fourteen of the fifteen did. The fifteenth checks that reopening a diff doesn’t modify the file, and it stays green even when I open the file for writing, because opening without writing changes nothing. It guards against a regression but proves nothing on its own. 1,069 tests pass across the workspace.

The pre-publish benchmark reported 61,831 elements per second, well under the usual figure. As in the last entry, the Mac was busy, with a load average between 11 and 14. And this release doesn’t touch the engine or the rules at all: every change is in the command line.

Built in the open

Every two weeks: measurements, defects, worked examples. Nothing else.

← Back to the devlog