Home: make instant-paint boundary-aware (Codex)

Codex caught a trust bug: instant-painting a brief saved under OLD boundaries
could briefly flash content the reader's current settings should hide — and
boundaries are trust-critical for this product. Add a filter signature
(prefs param + sorted dismissals) saved alongside the brief; instant-paint and
the merge-fallback only reuse a saved brief when the signature still matches the
current settings. A mismatch falls through to "Gathering…" + a fresh fetch.
Also closes the same latent leak in the merge's `?? it` fallback. Briefs saved
before this change lack a sig → won't instant-paint until re-saved (fails safe).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-12 09:52:01 -04:00
parent 854f06401f
commit 456b1a0547
+19 -4
View File
@@ -217,6 +217,13 @@
return P.param(P.merge(prefs.data, viewFilter()));
}
// Signature of the filters that shaped a brief — boundaries/prefs + dismissals.
// Instant-paint and the merge only reuse a saved brief when this still matches,
// so a boundary change can never briefly resurface content it should now hide.
function briefSig() {
return P.param(prefs.data) + '|' + Array.from(dismissed).sort().join(',');
}
async function loadToday(fresh) {
const q = P.param(prefs.data);
const ex = Array.from(dismissed).join(',');
@@ -227,17 +234,22 @@
if (brief) return; // already showing a saved brief — a failed background refresh stays invisible
throw e; // true first load with nothing painted → let the caller surface the error
}
const sig = briefSig();
const view = P.loadJSON(BRIEF_VIEW_KEY, null);
// Reuse the saved arrangement only when BOTH the server brief and the reader's
// filter signature are unchanged — otherwise the merge's `?? it` fallback could
// re-add a story the current boundaries now hide.
const sameServerBrief =
view && view.generated_at && fetched.generated_at && view.generated_at === fetched.generated_at;
view && view.generated_at && fetched.generated_at &&
view.generated_at === fetched.generated_at && view.sig === sig;
if (!fresh && sameServerBrief && Array.isArray(view.items) && view.items.length) {
const freshById = new Map(fetched.items.map((a) => [a.id, a]));
const items = view.items.map((it) => freshById.get(it.id) ?? it);
brief = { ...fetched, items };
P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items });
P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items, sig });
} else {
brief = fetched;
P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items: fetched.items });
P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items: fetched.items, sig });
}
heroIdx = 0;
markDisplayed(brief.items);
@@ -441,7 +453,10 @@
// shows on a genuine first visit with nothing cached yet.
if (selected === 'today') {
const cached = P.loadJSON(BRIEF_VIEW_KEY, null);
if (cached && Array.isArray(cached.items) && cached.items.length) {
// Only paint instantly if the saved brief was shaped by the SAME
// boundaries/prefs/dismissals — never flash content the current settings
// should hide. A mismatch falls through to "Gathering…" + a fresh fetch.
if (cached && Array.isArray(cached.items) && cached.items.length && cached.sig === briefSig()) {
brief = cached;
heroIdx = 0;
loading = false;