Skip to content

fix(reader): the archive banner was ~100% false — gate on the hash#186

Merged
bryanmatthewsimonson merged 2 commits into
mainfrom
claude/archive-banner-hash
Jul 18, 2026
Merged

fix(reader): the archive banner was ~100% false — gate on the hash#186
bryanmatthewsimonson merged 2 commits into
mainfrom
claude/archive-banner-hash

Conversation

@bryanmatthewsimonson

Copy link
Copy Markdown
Owner

Stacked on #185 (base: claude/archive-identity-check). Fixes: "Major bugs are experienced daily by me in the content comparisons... very misleading banners so frequent that I ignore them."

You were right to ignore them. The banner wasn't mistuned — it could not have worked.

Root cause

shouldOfferArchive compared raw HTML strings, and the two sides are different substrates:

side shape
capture article.content — Readability innerHTML, wrapped in <div id="readability-page-1">
relay markdownToHtml(markdown), rebuilt from the event

For any multi-paragraph article — every real one — these cannot match: markdownToHtml joins paragraphs with \n\n while Readability emits </p><p> with no separator. So neither the equality guard nor the containment guard could fire, and 'always' (the default) probes unconditionally. Every published article, every visit, forever. Once a banner is always on, it's off.

One correction to how this was first described to you. I said both guards were "structurally unreachable." That's too strong, and the test caught me: a single-paragraph body has no \n\n to introduce, so it is a clean substring of its wrapper and the containment guard suppressed correctly. Short pieces behaved; real articles didn't. That asymmetry is why the flood looked erratic rather than total. Both halves are now pinned, with the relay fixture taken verbatim from real markdownToHtml output rather than hand-written.

The fix

Gate on the canonical 13.4 hash — which was already correct on both sides and simply never consulted. The published x tag (read back as _articleHash), the archive row's articleHash, and state.articleHash agree by construction.

The gate only ever suppresses: equal hashes ⇒ same canonical content ⇒ nothing to offer, in any mode. A missing hash (older rows, pre-13.4 events) or a genuine difference falls through to the prior heuristics untouched, so 'richer' stays exactly as conservative as it was.

Two things fell out of doing it properly:

  • The decision moved to reader/archive-banner.js — pure, so it can be tested (index.js isn't importable from tests; this matches how findings-section.js / lens-section.js are already factored).
  • checkArchiveAvailability now computes the current hash itself when state.articleHash hasn't landed. It's filled by an async IIFE at load while the probe runs on a 100ms timer — the probe can win that race, and silently degrading to the unsound body compare is precisely the bug. An edited article leaves the hash null and lets the body heuristics answer, as before.

Not fixed here

The markdown→HTML→markdown escape doubling that mints a new x tag on a Load-archive round trip — already fixed for the PDF path only (JOURNAL 2026-07-08). Until that lands, a hash difference after a round trip may be an artifact rather than a real edit. That's the third of the four archive bugs and wants its own PR.

Verification

  • No wire-format change.
  • npm test1739 pass, 0 fail
  • npm run build — clean · npm run lint0 errors

Once this lands you can put Archive banner back to always — it will finally mean something.

🤖 Generated with Claude Code

shouldOfferArchive compared raw HTML strings, and the two sides are
different substrates: the capture is Readability innerHTML (wrapped in
<div id="readability-page-1">), the relay side is markdownToHtml(md).
For any multi-paragraph article — every real one — they cannot match:
markdownToHtml joins paragraphs with "\n\n", Readability emits "</p><p>".
Neither the equality guard nor the containment guard could fire, and
'always' (the default) probes unconditionally, so the banner fired on
every published article, every visit, carrying no information.

Stated precisely: containment is not unreachable in principle. A
single-paragraph body IS a clean substring of its wrapper, so the guard
suppressed correctly there. Short pieces behaved; real articles did not.
The tests pin both halves, with the relay fixture taken verbatim from
real markdownToHtml output.

Gate on the canonical 13.4 hash, which was already correct on both sides
and never consulted — the published `x` tag (_articleHash), the archive
row's articleHash, and state.articleHash agree by construction. The gate
only ever SUPPRESSES: equal hashes ⇒ same content ⇒ nothing to offer, in
any mode. A missing hash or a real difference falls through to the prior
heuristics untouched, so 'richer' stays conservative.

The decision moves to reader/archive-banner.js (pure — index.js is not
importable from tests). checkArchiveAvailability now computes the current
hash itself when state.articleHash has not landed: it is filled by an
async IIFE at load while the probe runs on a 100ms timer, and silently
degrading to the unsound body compare is exactly the bug. An edited
article leaves the hash null and the body heuristics answer, as before.

Does not address the markdown→HTML→markdown escape doubling that mints a
new x tag on a Load-archive round trip (fixed for PDFs only, 2026-07-08).

No wire-format change. 1739 tests green; lint 0 errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bryanmatthewsimonson
bryanmatthewsimonson merged commit 7794ea1 into main Jul 18, 2026
@bryanmatthewsimonson
bryanmatthewsimonson deleted the claude/archive-banner-hash branch July 18, 2026 05:19
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.

1 participant