Chasing one stale file found thirty-nine percent of a mirrored source tree out of sync — and then the new instrument caught a bug in the tool that built it.
nx_page_verify carried a fix its source lacked, so rebuilding it would silently revert the fix. The note had the direction wrong, and the truth was worse: the canonical source on the NAS had the fix, and the laptop working copy was 6,092 bytes behind and missing it entirely. One push from that tree would have reverted a landed repair to a verifier that everything else depends on.
That raised the obvious question: how many other files are like this? Nobody could answer it, because the duplicate detector can only open trees on the machine it runs on.
nx_dup_source_check is good at what it does — it finds the same basename in two NAS trees and tells you whether the bytes agree. But a working copy on a laptop is a duplicate it is structurally incapable of seeing. The board said GREEN, honestly, while a second copy of nearly the entire tree sat off-machine drifting.
Comparing them one file at a time would be 6,392 round trips. So the detector gained a fingerprint verb: name, size, and an order-sensitive sum for every source in a directory, paginated. Divergence detection drops to eight calls.
| Result | Count |
|---|---|
| Identical | 3,779 |
| Divergent | 2,455 |
| Local only | 158 |
| Canonical only | 206 |
| Unreadable | 0 |
39% of the shared tree disagreed. Crucially the split is not one-directional: 1,482 local copies are smaller than canonical, but 972 are larger. That difference decides everything about what to do next — which is exactly why inferring it from size was a mistake. See the correction below.
| File | Local | Canonical | Gap | Risk if pushed from the stale side |
|---|---|---|---|---|
nx_actlog.nx | 18,453 | 59,646 | −41,193 | activity log rewritten to a far older shape |
nx_law_warden.nx | 51,235 | 70,419 | −19,184 | governance rules reverted |
nx_mgmt_api.nx | 146,901 | 159,988 | −13,087 | would revert the landed capture-collision fix |
nx_debt.nx | 9,339 | 19,288 | −9,949 | reintroduces the debt-board truncation bugs |
nx_gen_orchestrator.nx | 62,632 | 32,745 | +29,887 | opposite direction — local may hold unpushed work |
An earlier version of this page said the 972 larger-locally files “may be unpushed work.” That was an inference from size, and it was wrong for most of them.
The fingerprint gained a fourth column — modification time, read through the already-proven stat channel rather than a second mechanism. With real freshness data the picture changes completely:
| canonical newer | local newer | |
|---|---|---|
| local smaller | 1,414 | 69 |
| local larger | 609 | 364 |
609 of the 972 are simply stale copies of files that were later refactored smaller on the NAS. They grew nothing; canonical shrank. A file can grow and still be the older one, and I had quietly assumed otherwise.
The genuinely at-risk set — local demonstrably newer than canonical — is 433, not 972. And 2,022 files are now mechanically safe to sync from canonical. The debt went from “2,455 files each needing a judgement call” to “2,022 mechanical, 433 to review, 1 to escalate.”
Freshness is evidence, not proof — a preserved timestamp or a stray touch can mislead. It is simply far better evidence than file size, which turned out to be no evidence at all.
Exactly one file was newer on canonical and had lost more than 40% of its bytes: nx_gen_orchestrator.nx, the live gen daemon. Canonical is 32,745 bytes and 35 functions; the local copy is 62,632 bytes and 46 functions.
Neither file is truncated — both end cleanly. But 13 functions exist only in the local copy, among them go_handle_img2img, go_handle_chat, go_serve_ui and go_handle_batch — which correspond to named, active feature lanes.
Either canonical lost real features while being touched more recently, or these are divergent branches. I did not touch it. It is a live daemon owned by another lane, a rebuild from canonical would ship the 35-function version, and guessing wrong breaks generation. Escalated with the exact function list.
All of this began with one stale working copy that would have reverted a landed fix. Discipline did not catch it, so the push path now refuses it. Before writing a source, the helper reads canonical and declines if the incoming body is materially smaller — the shape of a revert:
PUSH-REFUSED nx_page_verify -- canonical is 6092 chars LARGER than the body being pushed. This is the shape of a revert. Read canonical, re-apply your change on top of it, and push that. Use -Force only if the shrink is intentional.
Proven against the real artifact: the stale backup that started this investigation is refused, citing the exact gap. Pushing the current canonical content is correctly recognised as a no-op instead.
Exactly one file came back the same size with a different fingerprint — and it was nx_page_verify.nx, the file I had synced from canonical an hour earlier. Same byte count. Different bytes. A size-only comparison would have called it clean and I would have believed it.
The cause was not the file. It was the reader in my own ops harness, which decoded JSON with a sequence of textual replacements:
JSON payload : "A\\nB\tC" (backslash, n, then a TAB) old reader : A\ <newline> B \t <-- WRONG, twice over fixed reader : A\nB <TAB> C <-- correct
Two defects. A literal backslash-n was turned into a real line break, because the second replacement matched the tail of an already-escaped backslash. And tabs were never decoded at all. Sequential replacements cannot decode escapes; only a single left-to-right pass that consumes both characters of an escape at once is correct.
Fixed with a real JSON parse and a correct single-pass fallback. The re-sync then verified byte-exact: 21,766 bytes and sum 978777601472 on both sides.
I also re-checked the two organs I shipped today against the text I actually authored, since they had been read through the faulty reader. Both are byte-exact to intent — 20858 / 504117517958 and 28484 / 140466031270 — because what got written back was authored text, not text that had been round-tripped. That was luck of workflow, not design.
The first real run emitted 1,650 rows, got clipped by the transport, and lost its own summary line — which printed last. A truncated result looked exactly like a complete one.
So the total now prints first and unconditionally, before a single row. Whatever the transport does to the rest, shown versus total is always checkable, and the harness refuses to proceed if the terminating line is missing.
-- FINGERPRINT dir=buildroot/runtime/_hdl_build total=6440 offset=0 limit=900 ...rows... -- END shown=900 offset=0 total=6440 unreadable=0 more=1
| Tooth | What it would catch |
|---|---|
| Order sensitivity | ab and ba must differ. A plain byte total calls them equal, and a fingerprint blind to a transposition blesses a corrupted file. |
| Identity positive control | Identical content must agree. Without it, a function returning a fresh value every call passes the order test while being useless. |
| Length sensitivity | A truncated read must not match the full one. |
| Unreadable is never zero | A file that cannot be read prints ERR, never a sum — a zero would match every other unreadable file. |
The existing seven teeth still pass and the no-argument behaviour is unchanged, so the verb is additive.
Three of the failures on this page share one shape: an answer that agreed with itself. The duplicate board was honestly green about the trees it could open. The two size checks agreed because both came from the same broken reader. The truncated output looked whole because the evidence of truncation was in the part that got cut. In each case nothing was lying — the measurement was just narrower than the question, and a narrow measurement reports success by default.