Background
Step 2 of #187 — deferred at PR-merge time per the Codex synthesis on that issue. Filed as its own tracking issue so #187 can close.
What this is
Today, every code path that touches selection (cluster click, sample click, hashchange, source-filter handler, boot deep-link) does its own:
- Mutates
viewer._globeState.selectedPid / selectedH3
- Updates the URL via
history.pushState / replaceState / nothing (depending on path)
- Mutates side-panel DOM (
#clusterSection, #samplesSection)
Five paths × three mutation surfaces = a matrix of inconsistency that took six Codex rounds on PR #186 + two more on PR #188 to converge. The freshness primitive (freshSelectionToken) closed the async race class of bug. What's still distributed is the mutation triple itself.
Proposed Step 2
Three controller functions in explorer.qmd:
async function selectSample(viewer, pid, opts = {}) { ... }
async function selectCluster(viewer, h3Cell, opts = {}) { ... }
function clearSelection(viewer, opts = {}) { ... }
Each handles all three surfaces (state + URL + DOM) in one place. opts carries the freshness token (isStale), the URL-write mode ('push' | 'replace' | 'none'), and any caller-specific overrides.
YAGNI gate (stays as-filed until met)
Per the Codex synthesis on #187:
Consider a larger selectSample / selectCluster / clearSelection controller only if the next selection feature again has to touch click, boot, hashchange, filter, and URL paths.
So don't open a PR for this yet. Open it when:
- A new selection feature lands that would otherwise touch all five existing paths, OR
- A bug like "X path forgot to update the URL" / "Y path forgot to clear the side panel" recurs after Step 1's freshness primitive (which would suggest the mutation class isn't covered by freshness alone).
Until either trigger fires, the cost of consolidation isn't justified — three call sites isn't enough to force it, and the OJS-imperative idiom is the project's existing convention.
What this issue is NOT
Refs
Background
Step 2 of #187 — deferred at PR-merge time per the Codex synthesis on that issue. Filed as its own tracking issue so #187 can close.
What this is
Today, every code path that touches selection (cluster click, sample click, hashchange, source-filter handler, boot deep-link) does its own:
viewer._globeState.selectedPid/selectedH3history.pushState/replaceState/ nothing (depending on path)#clusterSection,#samplesSection)Five paths × three mutation surfaces = a matrix of inconsistency that took six Codex rounds on PR #186 + two more on PR #188 to converge. The freshness primitive (
freshSelectionToken) closed the async race class of bug. What's still distributed is the mutation triple itself.Proposed Step 2
Three controller functions in
explorer.qmd:Each handles all three surfaces (state + URL + DOM) in one place.
optscarries the freshness token (isStale), the URL-write mode ('push' | 'replace' | 'none'), and any caller-specific overrides.YAGNI gate (stays as-filed until met)
Per the Codex synthesis on #187:
So don't open a PR for this yet. Open it when:
Until either trigger fires, the cost of consolidation isn't justified — three call sites isn't enough to force it, and the OJS-imperative idiom is the project's existing convention.
What this issue is NOT
Refs
EXPLORER_STATE.md §4— current async-selection invariant doc.