Skip to content

explorer: extract _selGen freshness primitive (post-mortem on PR #186 6-round Codex loop) #187

@rdhyee

Description

@rdhyee

Background

PR #186 (cluster URL state, &h3=<cell>) went through six rounds of Codex review. Every round surfaced at least one P2 issue, all of them resolved before merge. This issue is a post-mortem: was that iteration count a sign of architectural rot, scope creep, or genuinely complicated interactions — and is there a small refactor that would have prevented most of it?

Filed at the author's request after a meta-conversation with Claude Code; will post Codex's independent take as a follow-up comment, then we discuss.

Author's question (verbatim)

are the 6 rounds of iterations needed to converge a sign of the need for some architectural refactoring, creeping complexity, or there are just a lot of darn complicated interactions that need trial and error to surface?

Claude Code's analysis

Honest read: ~80% of the findings are the same class of bug — async ops mutating shared state where the state can change mid-await — surfaced in different code paths. The remaining ~20% is a data-layer surprise (UBIGINT precision) plus a doc lag.

Looking at it that way:

Round Surface Class
R1 hashchange handler async + shared-state race
R2 hydrateClusterUI internal await same
R3a boot deep-link same
R3b fetchClusterByH3 ignoring source filter data consistency
R4a source-filter doesn't invalidate selection same
R4b suppressHashWrite leak on early-return cleanup edge
R5a rehydrate-on-filter when meta truthy same data consistency
R5b UBIGINT > 2^53 → Number precision-loss data-layer surprise
R6 stale docs nit

So this was mostly one problem reappearing in different surfaces, not creeping complexity from feature pile-up.

The actual architectural smell

There's no single "selection store." Every path that touches selection (click handler, hashchange, boot, source-filter handler) imperatively does its own DOM writes, URL writes, and _globeState mutations. The _selGen token is a poor person's race guard duct-taped onto each path. We're now using it in four places.

A small refactor would prevent future bugs of this class

  1. withFreshness(token, fn) — a wrapper that auto-bails the inner function's DOM mutations if the token's generation has bumped. ~15 lines.
  2. (Optional, larger) consolidate selection mutations into a selectSample(pid) / selectCluster(h3) / clearSelection() trio that handles state + URL + DOM in one place.

Step 1 alone would have prevented R2 + R5a (the "internal await forgot to check freshness" bugs). Step 2 would prevent R3b/R4a/R5a (the "filter-X-doesn't-do-Y" bugs).

Wholesale rewrite (Redux/state machine): not justified. The OJS-imperative idiom is the project's convention; readers expect it.

Bottom-line answer

A bit of all three, but mostly the third. Six rounds reflect Codex doing what humans rarely do — re-reading the diff after each fix and spotting the next missed edge in the same family. We were genuinely converging, just not in straight-line steps. Worth filing this small follow-up so future selection-state work can't trip the same wire.

Refs

Next step

Codex's independent take will be posted as a comment. Then discussion + decision on whether to scope the refactor (and at what size) before any code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestexplorerInteractive Explorer features

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions