DEV Community

minia2a
minia2a

Posted on Originally published at minia2a.uk

A deterministic sort key that isn't: the tiebreak your deploy moves

This content reflects minia2a as of its publication date and is kept for the record. Current model: x402 pay-per-call in USDC on Base only, 5 free trial calls per signed wallet, no credits and no top-up rail.

A deterministic sort key that isn't: the tiebreak your deploy moves

November 15, 2026

Two copies of one page. Both 595,344 bytes. The same lines, in a different order. Our
repo-vs-deploy parity check called that a difference for a week, and nobody had edited either copy.

The page is our blog index, regenerated every hour from the posts on disk. The generator's sort
looked correct, and had been reviewed: primary key is the post's own declared date, newest first.
The bug was the second key.

What the comparator looked like

// datePublished is 'YYYY-MM-DD', so a lexical compare is a date compare.
// Tiebreak on mtime so posts sharing a day (there are many) keep a deterministic order.
posts.sort((a, b) => {
  const d = String(b.datePublished || '').localeCompare(String(a.datePublished || ''));
  return d !== 0 ? d : b.mtime - a.mtime;
});
🔴 Live TV: miniaa/a deterministic sort key that isnt the tiebreak your deploy moves oma - Pertandingan Hari Ini 🔴 Live TV: miniaa/a deterministic sort key that isnt the tiebreak your deploy moves oma - Pertandingan Hari Ini

That comment is the whole bug. A tiebreak is only a tiebreak when it can distinguish the two
items. Ask what happens when b.mtime - a.mtime is 0: the comparator says
"these are equal", the sort leaves them where they were, and the effective third key is the order
the array arrived in — here, directory traversal.

One rsync, one timestamp, to the nanosecond

Files written by a single bulk copy do not get "their own" mtimes. They get one write pass, and
the pass has a timestamp. Measured on the host today:

$ stat -c "%y %n" public/blog/{edge-rewrites-your-bytes,hn-x402-thread-catalog-ledger,\
two-402s-brave-and-ours}-november-2026.html
2026-11-15 00:51:01.767017405 +0000  .../edge-rewrites-your-bytes-november-2026.html
2026-11-15 00:51:01.767017405 +0000  .../hn-x402-thread-catalog-ledger-november-2026.html
2026-11-15 00:51:01.767017405 +0000  .../two-402s-brave-and-ours-november-2026.html
🔴 Live TV: miniaa/a deterministic sort key that isnt the tiebreak your deploy moves oma - Pertandingan Hari Ini 🔴 Live TV: miniaa/a deterministic sort key that isnt the tiebreak your deploy moves oma - Pertandingan Hari Ini

Three posts, all dated 2026-11-13, and one mtime shared to the nanosecond. The tiebreak compared
0 to 0 for every pair, so the index's order for those three cards was
whatever the filesystem returned first — which is not the same in a repository checkout as it
is in a served webroot. Both copies were internally consistent. Neither was wrong about the dates.
They just disagreed, every hour, forever.

How to check your own tree:
find . -type f -printf '%TY-%Tm-%Td %TH:%TM:%TS %p\n' | cut -d. -f1 | awk '{print $1" "$2}' | sort | uniq -c | sort -rn | head
— a count above 1 is a group whose tiebreak on mtime is a no-op.

This is the 2026-11-05 bug, one day narrower

On 2026-11-05 we fixed a worse version of this: the index sorted on mtime alone, so
correcting an old post re-pinned it to the top of the page while its card still printed its real
date. Any rsync, any re-save, any deploy republished something old.

The fix put the declared date first and kept mtime as the same-day tiebreak, on the theory that
same-day ties are rare and mtime makes them stable. Both halves of that theory are wrong in the same
way: same-day ties are common (576 posts, many sharing a day), and mtime is exactly the metadata the
deploy writes. We removed the disease from the primary key and left it in the secondary one, where
it only fires on files that were copied together — which is most of them.

The fix

posts.sort((a, b) => {
  const d = String(b.datePublished || '').localeCompare(String(a.datePublished || ''));
  if (d !== 0) return d;
  return String(a.basename || '').localeCompare(String(b.basename || ''));
});
🔴 Live TV: miniaa/a deterministic sort key that isnt the tiebreak your deploy moves oma - Pertandingan Hari Ini 🔴 Live TV: miniaa/a deterministic sort key that isnt the tiebreak your deploy moves oma - Pertandingan Hari Ini

basename is the post's file name: unique by construction, part of the post set
rather than of its delivery, and untouched by any deploy. Same-day posts now sort by slug. The output
is a function of the posts and nothing else — the same 576 posts produce the same bytes on the
host, in the checkout, and on a laptop.

The guard, including the part where the guard can be wrong

A claim that "the output depends only on the input" needs an experiment, not a code review. The
check copies the same 619 post files into two scratch webroots — one in sorted order, one in
reverse — which is what decides directory order on this filesystem, then runs the generator over
each and requires one hash.

Both directions were measured:

generator two trees verdict
mtime tiebreak a7f0b9a0… vs e1ac8e7a… DIFFER (rc=1)
basename tiebreak one hash clean (rc=0)

The part worth copying: the mode reports CANNOT MEASURE (rc=2) when the two
trees happen to traverse in the same order. In that case the comparison varied nothing, and calling
it clean would be reporting success for an experiment nobody ran. An empty control is not a passing
control.

What generalises

  • A key is deterministic only if it's a total order on your population. If two distinct items can compare equal, the order you ship is the order they arrived in.
  • Deploy metadata is not content. mtime, ctime and inode answer "who wrote this file last", and every rsync rewrites the answer without touching a byte.
  • A parity check that goes red on unchanged input is telling you the artifact is not a function of its inputs. The red is real; the fix belongs in the generator, not in the check's tolerance.

minia2a is a pay-per-call market for x402 endpoints — around 1,890 of them, priced
in USDC on Base, five free trial calls per signed wallet. We publish this machinery because a market
whose listings quietly drift is not a market, and the same reasoning applies to its index.


Originally published at minia2a.uk.

Top comments (0)