Skip to content

Catch more changes between builds, and date files by their last change - #3

Merged
czj merged 6 commits into
mainfrom
fresher-incremental-builds
Nov 12, 2026
Merged

czj merged 6 commits into
mainfrom
fresher-incremental-builds

Conversation

@czj

@czj czj commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

A shard's fingerprint was its count and latest change, so a record deleted while another was inserted with an older timestamp left the file listing the deleted page. A new shard_size kept files cut for the old ranges. update_columns, a change in another table or a new url block went unseen until someone ran a rebuild. In the index, a file that lost a record kept its date or went back in time, and a build that died after uploading a new file left that file there for good.

The fingerprint now also holds the sum of the ids (as offsets within the range) and the shard size, files are compared by content before upload, the index dates a changed file by its change, and a full build comes once a week by itself.

  • Stronger fingerprint: size/count/sum/latest per range catches a record replaced by an older one and a new shard size; SQLite cannot overflow on large ids.
  • Upload by content: a re-rendered file that comes out the same is not uploaded and keeps its index date; a changed file without a later lastmod takes the build's time, so crawlers read it again.
  • Weekly full build: config.rebuild_every (a week by default, nil to turn off) catches update_columns, other tables, new url blocks and files lost from the storage; Result#full and the build.mapitaine payload say when it happened.
  • No orphan files: every shard is rendered first, the names about to be added go into the manifest as pending in one write, and the next build removes them if this one dies.
  • ruby-head: Active Support 8.0's JSON encoder passes json an option ruby-head's json no longer takes; the digest and the IndexNow body now use JSON.generate, which writes the same bytes.
Proof

64 runs, 183 assertions, green on Rails 8.1 and 8.0; RuboCop clean. The new tests fail on main: a record replaced by an older one, a new shard size, a deletion moving the index date forward, a re-rendered file not uploaded, a full build restoring a deleted file without changing dates, the weekly full build, a build dying before its index, ids near 9×10^18, and a lastmod from an older manifest compared by time.

The ruby-head failure on #2 was ArgumentError: unknown keyword: quirks_mode from ActiveSupport::JSON::Encoding::JSONGemEncoder#stringify, raised by Configuration#digest. JSON.generate gives the same string as to_json for these values, so the digest does not change.

bench/build.rb (30,000 records × 24 languages, Ruby 3.3.6 without YJIT): full build 29.9 s and 30.7 s, nothing new 0.014 s, three records changed 1.06 s, the same as on main within its spread.

  • Tests cover the change (a bug fix comes with a test that fails without it)
  • Output unchanged, or every difference is intended and listed above

Generated by Claude Code

A URL built by hand could put a space, an accent or a control character
in a file, which made it unreadable. Rails locales such as pt_BR went out
as hreflang="pt_BR", which search engines ignore. A lastmod string was
written whatever it held, and the index took the greatest string rather
than the latest time. x_default was missed when the locales block returned
strings, and two languages sharing a URL listed it twice.

URLs are now percent-encoded outside RFC 3986's characters, locales are
written with a hyphen, lastmods are read as W3C dates and written in UTC,
and a page lists each of its URLs once. The check reports URLs over 2,048
characters, hreflang values that are not language codes, lastmods that are
not dates, and an index with no file or more than 50,000.

Assisted-by: Claude Code
An impossible date such as 2026-13-01 crashed the build, and 2026-02-30
was written out. A date alone counted from UTC midnight, so today's date
looked like the future east of UTC. A url block that forgot the locale no
longer warned once its URLs were listed once per page. A protocol-relative
URL got the host in front, and a % not followed by two hex digits stayed.

Dates are validated, a date alone starts at UTC+14, a page whose languages
share one URL is reported once per source, as is a lastmod that is not a
date, and the lastmod is read once per page. An empty site no longer fails
the check.

Assisted-by: Claude Code
A record deleted and another inserted with an older timestamp left a
shard's count and latest change alone, so its file kept the deleted page.
A new shard size kept files cut for the old ranges. update_columns, a
change in another table or a new url block went unseen until someone ran
a rebuild. In the index, a file that lost a record kept its date or went
back in time, and a build that died after uploading a new file left it
there for good.

The fingerprint adds the sum of the ids and the shard size. A file that
comes out the same is not uploaded and keeps its date; one that changed
without a later lastmod takes the build's time. A full build comes every
week by itself (config.rebuild_every). The names a build is about to add
are recorded in the manifest first, so the next build can remove them.

Assisted-by: Claude Code
…builds

SQLite's SUM overflows on ids near 2^63, so the fingerprint sums offsets
within each range instead. A lastmod from an older manifest is compared as
a time, not as text. A build that turned full because rebuild_every had
passed says so in the result and in the build.mapitaine payload.

Assisted-by: Claude Code
# Conflicts:
#	lib/mapitaine/builder.rb
On ruby-head, Active Support 8.0's encoder passes json an option it no
longer takes (quirks_mode), so the configuration digest and the IndexNow
body raised. Both hold plain values: JSON.generate writes the same bytes.

Assisted-by: Claude Code
@czj
czj enabled auto-merge (squash) November 12, 2026 07:57
@czj
czj merged commit bc99558 into main Nov 12, 2026
10 of 11 checks passed
@czj
czj deleted the fresher-incremental-builds branch November 12, 2026 07:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants