Skip to content

coherence: stop four more surfaces asserting numbers they don't have - #342

Merged
rdhyee merged 1 commit into
isamplesorg:mainfrom
rdhyee:fix/coherence-honesty-closeout
Aug 5, 2026
Merged

coherence: stop four more surfaces asserting numbers they don't have#342
rdhyee merged 1 commit into
isamplesorg:mainfrom
rdhyee:fix/coherence-honesty-closeout

Conversation

@rdhyee

@rdhyee rdhyee commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Why

Follow-up to #341. That fix stopped facet counts showing the unfiltered baseline under an active search — but it removed one trigger, not the hazard class.

So we audited every number the Explorer displays, asking one question of each:

What happens if the thing meant to replace this number never returns?

If the answer is "the old value stays up, unmarked or marked only by a colour change," that's a finding. The full audit is added here as COHERENCE_AUDIT_2026-08-04.md (Claude draft → Codex adversarial review → reconciled, with disagreements recorded rather than smoothed over).

This PR implements four of its findings. Grant is over, so the bar was honest, not complete.

What's fixed

F3 (HIGH) — the legacy per-dimension count catch painted applyFacetCounts(d.key, null) — the unfiltered global baseline — when a constrained query threw. The repaint even cleared .recomputing, so the wrong number looked settled. Now an honest dash. This is PLAN_305 Phase 4's explicit open item for this site.

F5 (MEDIUM) — in point mode, when the 5,000 render cap is hit, a follow-up COUNT establishes the true total. If it threw, the UI asserted "5,000 samples in view" when the truth could be 10-100× higher (same bug shape #206 fixed for the no-failure case). Now stats show an em-dash, the phase message says the total is unavailable, and the diagnostics say "unknown" instead of the cap.

M2 (MEDIUM)reconcileSettledCamera() refreshed the collapsed-by-default stats panel but not the always-visible phase message, so after exiting point mode an ordinary pan stranded a previous viewport's "N clusters in view" on screen as fact. Now repainted — behind a freshness guard that also closes the _clusterFilterSig blindness flagged as F4.

Structural (partial)markFacetCountsRecomputing() added a CSS class and left the old numeric text intact. That was the entire mechanism of #340: "in flight" signalled by opacity alone. It now also arms a 400 ms timer that swaps unsettled counts to (Loading…). Deliberately not a watchdog forcing a terminal state — honesty only requires that invalid data stop looking current.

Verification

Local render with real R2 data, then the deployed staging build (rdhyee.github.io):

Check Result
tests/test_smoke.py (staging) PASS
tests/test_frontend_derived.py 40 passed
#341 behavior intact — search 60/60 counts (—), 0 stuck recomputing, 0 stale zero-rows
#341 behavior intact — facet-only real counts (SESAR 0, OpenContext 261,086)
pageerrors none

Known-open (documented in the audit doc, not attempted here)

  • Comprehensive pre-await invalidation — the single most valuable remaining fix. The source/facet handlers still do awaited globe work before refreshFacetCounts(), so a hang before invalidation can still strand unmarked numbers. This is what would make the honesty rule an actual invariant rather than a convention.
  • Legacy per-dimension atomicity; a regression test for the 400 ms timer; findings M1 / F11 / F4 / F7 / F12; static-prose numeric drift (the "How This Demo Works" sizes are wrong against the release manifest).

Two things worth reading even if you skip the diff

Both are recorded in the commit message and the audit doc, because both were caught late:

  1. The first attempt killed the entire Explorer. I declared the timer state with top-level const/let inside a Quarto {ojs} cell. Those are illegal — each top-level statement is a reactive cell — and boot died. Caught by tests/test_smoke.py, not by review. State now lives on window.__facetCountsStaleTimer, matching the file's existing pattern.
  2. The first M2 attempt reintroduced this very bug class while fixing it. With no freshness guard, a stale cluster cache could overwrite an honest "Loading H3…" or "Failed to load…" with a green settled count. Caught by Codex in adversarial review, not by tests. Treat any "just repaint the number here" change in this file as guilty until proven fresh.

Reviewed by Codex over two rounds; blocked on the M2 guard in round 1, approved after it landed.

Refs #340, #304, #305

🤖 Generated with Claude Code

https://claude.ai/code/session_01QCCDurpcLzMe7L72y2HDAa

Follow-up to isamplesorg#341. That fix stopped facet counts showing the unfiltered
baseline under an active search, but it removed one TRIGGER, not the hazard
class. A read-only audit of every number the Explorer displays — asking "what
happens if the thing meant to replace this never returns?" — found the same
shape elsewhere. Audit doc added: COHERENCE_AUDIT_2026-08-04.md.

F3 (HIGH) — the legacy per-dimension count catch painted
applyFacetCounts(d.key, null), i.e. the UNFILTERED GLOBAL BASELINE, when a
constrained query threw. The repaint even cleared `.recomputing`, so a wrong
number looked settled. Now markFacetCountsUnavailable([d.key]). This is
PLAN_305 Phase 4's explicit open item ("remove the misleading active-filter
baseline fallback everywhere") for this site.

F5 (MEDIUM) — in point mode, when the render cap (POINT_BUDGET 5,000) is hit,
a follow-up COUNT establishes the true total. If that COUNT threw, the code
set totalCount = data.length and capReached = false, so the UI asserted
"5,000 samples in view" when the truth could be 10-100x higher — the same bug
shape isamplesorg#206 fixed for the no-failure case. Now tracked via totalCountKnown:
stats show an em-dash, the phase message says the total is unavailable, and
the a1dbg/console diagnostics report "unknown" rather than the cap.

M2 (MEDIUM) — reconcileSettledCamera() refreshed the collapsed-by-default
stats panel but not the ALWAYS-VISIBLE phase message, so after exiting point
mode an ordinary pan stranded a previous viewport's "N clusters in view" on
screen as fact. It now repaints that message — but only behind a freshness
guard (!loading && computeTargetMode(alt)==='cluster' && currentRes===targetRes
&& _clusterFilterSig===desiredClusterSig()). The guard also gates the stats
repaint, closing the _clusterFilterSig blindness the audit flagged as F4.

Structural (partial) — markFacetCountsRecomputing() added a CSS class and left
the old numeric TEXT intact. That was the entire mechanism of isamplesorg#340: "in
flight" signalled by opacity alone, so an unsettled recompute displayed
confident wrong numbers indefinitely. It now also arms a 400 ms timer; counts
still unsettled when it fires have their text replaced with "(Loading...)".
The delay keeps short interactions from flickering. Per the audit's structural
recommendation this is deliberately NOT a watchdog forcing a terminal state —
honesty only requires that invalid data stop looking current.

Known-open and documented in the audit doc: legacy per-dimension atomicity;
comprehensive pre-await invalidation (the most valuable remaining fix — a hang
BEFORE invalidation still strands unmarked numbers); a regression test for the
timer; and findings M1/F11/F4/F7/F12 plus the static-prose numeric drift.

Two process notes worth preserving:
- The first attempt declared the timer state with top-level const/let inside a
  Quarto {ojs} cell. Those are illegal — each top-level statement is a reactive
  cell — and it killed the entire Explorer. Caught by tests/test_smoke.py, not
  by review. State now lives on window.__facetCountsStaleTimer, matching the
  file's existing window.__searchFilter / window.__facetIndexStatus pattern.
- The first M2 attempt had no freshness guard and would have let a stale
  cluster cache overwrite an honest "Loading H3..." or "Failed to load..." with
  a green settled count — reintroducing this very failure class while fixing
  it. Caught by Codex in adversarial review, not by tests.

Verified against a local render with real R2 data: tests/test_smoke.py passes;
tests/test_frontend_derived.py 40 passed; the isamplesorg#341 behavior is intact (search
gives 60/60 dashes with 0 stuck recomputing and 0 stale zero-rows, facet-only
still gives real counts — SESAR 0, OpenContext 261,086); no pageerrors.

Reviewed by Codex over two rounds; it blocked round 1 on the missing M2
freshness guard and approved after it landed.

Refs isamplesorg#340, isamplesorg#304, isamplesorg#305

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCCDurpcLzMe7L72y2HDAa
@rdhyee
rdhyee merged commit 1d42da5 into isamplesorg:main Aug 5, 2026
2 checks passed
rdhyee added a commit that referenced this pull request Aug 6, 2026
…prose figures

#343 (Eric Kansa) — the Guided Tour rendered section headings as "1 Stop 1 —",
"2 Stop 2 —" and so on: the site sets number-sections globally, and the headings
already carried a hand-written "Stop N". Two fixes, both as requested:

- number-sections: false for this page only. The step numbers are written by
  hand because the numbering is part of the tour's voice, so the automatic
  numbering is what gets removed, not the manual one.
- "Stop" -> "Step" in all seven headings, plus the three prose references, so
  the page doesn't say "steps" in the titles and "stops" in the body.

Coherence audit follow-up — the "How This Demo Works" table asserted file sizes
that no longer matched anything published. Corrected against the release
manifest, with the cluster counts verified independently against the parquet row
counts (38,462 / 112,019 / 176,669 — the manifest doesn't carry row counts).

Worth recording WHY they were wrong, because the obvious fix was the wrong one:
the old figures (580 KB / 1.6 MB / 2.5 MB) are CORRECT for the 202601 snapshot,
which SERIALIZATIONS.md, how-to-use.qmd, data.qmd and tutorials/why_h3.qmd still
legitimately document. This page describes the live app, which serves 202608, so
it had silently drifted one generation behind. Those four other files were
checked against the live 202601 objects (594,541 / 1,627,502 / 2,543,412 B) and
are accurate — they are deliberately NOT touched. A "wrong number" in this repo
may be a right number for a different release.

Also dropped the "4 parquet files" claim, which was false — the published
release is 21 objects (18 parquet + 3 JSON search-index files) and the boot
manifest check enumerates them. Replaced with the real figure plus a pointer to
isamples_202608_release_manifest.json and CANONICAL.md as the authoritative
inventory, and a collapsed callout giving the curl/jq and SQL one-liners to
re-derive every number on the page without trusting the page.

Units are now consistently decimal MB. The old row silently mixed conventions
(580 KB was KiB while 1.6/2.5 MB were decimal), which is part of why it read as
internally consistent while being a generation stale.

Verified: quarto render of both pages clean; tests/test_smoke.py passes;
tests/test_frontend_derived.py 40 passed; the #341/#342 count behavior is
unaffected (search -> 60/60 dashes, 0 stuck; facet-only -> real counts); no
pageerrors. Rendered headings confirmed as "Step 1 ... Step 7" with no duplicate
numbering.

Refs #343

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCCDurpcLzMe7L72y2HDAa
rdhyee added a commit to rdhyee/isamplesorg.github.io that referenced this pull request Aug 6, 2026
…unable to false-pass

Codex blocked round 1 on two findings, both valid:

1. A TERMINAL EXCEPTION COULD STRAND (Loading...). invalidateFacetCountsNow()
   bumps facetCountsReqId and cancels the pending debounce, but
   refreshFacetCounts() sat only on the success path. If anything between the
   invalidate and the end of the globe work threw — updateSourceLegendState,
   writeQueryState, refreshHeatmap, reconcileGlobeForFilters — nothing would ever
   schedule a recompute and the counts would sit at (Loading...) until some
   unrelated later event. A HUNG promise staying Loading is honest (isamplesorg#342); a
   REJECTED one is terminal, so Loading becomes a different kind of lie.

   Fixed with an inner try/finally in both handlers, so refreshFacetCounts() runs
   on success AND on throw. No watchdog, no query issued while work is pending —
   it only guarantees a successor once the work settles or rejects. The outer
   finally still owns busyRelease(), and selection revalidation still runs after.

   Deliberately NOT applied to applySearchFilterChange(): Codex analysed it and
   it is not a stale-number hole (activation paints the (—) dash synchronously,
   and clearing leaves that honest dash visible during reconciliation).

2. THE VERIFIER COULD FALSE-PASS. It printed ever_loading without asserting it,
   checked stale-unmarked only before t=12s while sampling past 18s, would accept
   a permanently dimmed numeric value, and never proved a successor repaint.
   All four clauses are now required, the stale check covers every observation,
   and it un-throttles at the end and requires real numbers to come back — which
   is precisely the anti-stranding assertion for finding 1.

Also took Codex's idempotence hardening: under an active search
invalidateFacetCountsNow() now repaints the (—) dash rather than merely
returning, so it does not depend on an earlier isamplesorg#340 caller having painted it.

Verified on the rebuilt page: verifier passes all four clauses (invalidated
within 0.4s / swapped to Loading / 0 stale-unmarked windows / successor
repainted); test_smoke.py passes; test_frontend_derived 40 passed; isamplesorg#341/isamplesorg#342
behaviour unaffected.

STILL OPEN from the review, deliberately not done here: converting the verifier
to a discovered .spec.js that deterministically holds a globe request, and
hardening facet-tree.spec.js's fixed 3s sleeps (they parse numeric text and
could now read (Loading...) if a globe reload exceeds 3s).

Refs isamplesorg#340, isamplesorg#304, isamplesorg#305

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCCDurpcLzMe7L72y2HDAa
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