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.