Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
08083ac
pin-overlay: implementation plan (option C), amended per Codex round 1
rdhyee Jul 17, 2026
321925d
feat(explorer): additive search-result pin overlay (#172 Inc 1, optio…
rdhyee Jul 17, 2026
0cda4e9
test(playwright): e2e for search-result pin overlay (#172 Inc 1)
rdhyee Jul 17, 2026
69f8506
fix(explorer): address Codex round-1 P1.1/P1.2/P1.3/P1.5 + P2 for pin…
rdhyee Jul 17, 2026
24a96cc
test(playwright): strengthen pin-overlay e2e (Codex round-1 P1.4)
rdhyee Jul 17, 2026
9b931e9
docs(plan): amend Inc 1 camera/lifecycle scope to match implementation
rdhyee Jul 17, 2026
88b3fa7
fix(explorer): clear pins at all list-teardown paths (Codex round-2 P1)
rdhyee Jul 17, 2026
dd2fa33
test(playwright): exercise real selection + assert exact cap (Codex r…
rdhyee Jul 17, 2026
b65a5e5
fix(explorer): make list+pin lifecycle atomic via panel ownership (Co…
rdhyee Jul 17, 2026
d4277ba
test(playwright): lifecycle cases for the pin overlay (Codex round-3 P2)
rdhyee Jul 17, 2026
d556c4f
fix(explorer): dedicated panel-generation ownership closes both race …
rdhyee Jul 17, 2026
b79e842
test(playwright): honor config baseURL + deterministic Back test (Cod…
rdhyee Jul 17, 2026
d547b80
fix(explorer): unified panel-generation rule + entry teardown + statu…
rdhyee Jul 17, 2026
2f1ea84
test(playwright): assert new-search entry teardown of old rows (Codex…
rdhyee Jul 17, 2026
84f5df8
test(playwright): deterministic entry-teardown observation (Codex rou…
rdhyee Jul 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions PLAN_PIN_OVERLAY_2026-07-17.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Pin-overlay implementation plan (option C, #164/#165 β†’ #172 Β§7 track)

*2026-07-17, RY green-light "start now". Strategy: same playbook that shipped FTS
this morning β€” additive increment first, behavior change behind a flag, staging
verify, flip on confirmation.*

## Context deltas since the contract was written (May)

1. **Search is now a global filter** (#234 A1): `search_pids` semi-joins constrain
table, points, facet counts. The contract's "facet counts exclude search" clause
describes the *target* state, not today's. Users have 6+ weeks of habit with
filter-everything behavior.
2. **Area/world scope already exists** (#178 landed): `?search_scope`, two-button UI,
viewport-scoped SQL. Contract's "Light-path addendum" is DONE β€” do not rebuild.
3. **FTS is the default backend** (#335, today). Contract explicitly says (C) is
backend-orthogonal β€” confirmed still true (both producers emit `search_pids`).

## Increments

### Inc 1 β€” additive pin overlay (no behavior removed) ← TODAY
After any search completes, render matching samples as a pin overlay per the
contract's non-negotiable rules:
- `viewer.searchResultPoints` = new `PointPrimitiveCollection`, added to
`scene.primitives` AFTER `h3Points`/`samplePoints` (z-order rule)
- ≀ 50 pins (displayed result set), hollow-ring styling, `SOURCE_COLORS` palette,
pixel size 8
- hover label + click β†’ `updateSampleCard()` + pid hash write (dispatch on
`meta.type === 'searchResult'`)
- lifecycle: populate on results; clear per SNAPSHOT SEMANTICS below
- camera (amended per Codex round-1 P1.5): Inc 1 ships only the cheap half β€”
fly to the first *located* result @ 200000 (explicit null checks so 0Β° coords
are valid), auto-fly suppressed in area scope (existing behavior). **Fit-to-
bounds (the 30°×30Β° extent rule) is DEFERRED to Inc 2** β€” it is the only
camera work not in Inc 1.
- global refiltering UNCHANGED in Inc 1 β€” pins are purely additive
- acceptance: the contract's four shape cases (zero / one / local-many /
global-many). Inc 1 asserts pin *identity/count/replacement/clear* for all
four; the **local-many fit-to-bounds ZOOM assertion is deferred to Inc 2**
with the fit-to-bounds implementation (Inc 1 flies to the first located
result in every non-area case instead)
- **lifecycle (amended, Codex round-1 P1.2 β€” SNAPSHOT SEMANTICS):** the pins are
a snapshot of the *rendered results list* and reconcile with the LIST
lifecycle, not with filter state. Cleared wherever the list is torn down
(new/empty submit, described-by takeover, globe sample/cluster click). They
deliberately PERSIST when the list persists stale (a source/facet toggle while
a search is active updates #300 filtered surfaces but not the frozen list) β€”
that staleness is pre-existing list behavior, documented, not a new pin bug.
Known pre-existing incoherence left for a follow-up issue: a draft-input clear
followed by a source/facet change makes `writeQueryState()` drop `search=`
from the URL while the runtime filter (and thus list + pins) survive.
- **pin-count semantics (amended per review):** one pin per *located* displayed
result (world scope legitimately returns coordinate-less rows β€” never coerce to
(0,0)); panel reports displayed vs pinned counts when they differ; camera rules
use the first *located* result
- **click parity (amended, Codex round-1 P1.3):** the sample-click ceremony
(freshness token, card, selection/hash, detail hydration) is the shared
`selectSearchResult()` helper with a preserve-results-list option; pin clicks
use it β€” no shallow duplicate branch. Pin clicks get the FULL in-map card
hydration (material / feature / specimen / thumbnail) by reusing
`openInMapCardForSample()`, not a description-only fetch; the helper takes the
caller's freshness token to avoid a double-bump
- **styling (amended):** carry `scaleByDistance` explicitly; use the bounded
`POINT_DEPTH_TEST_DISTANCE` for `disableDepthTestDistance` (Codex round-1
P1.1 β€” `Infinity` bleeds pins through the far side of the globe)

### Inc 2 β€” `?searchui=pin` mode (the latency win) ← behind flag
**Seam corrected per Codex plan review (P0):** skipping `applySearchFilterChange()`
is NOT sufficient β€” `window.__searchFilter.active` is consulted independently by
searchFilterSQL() (table/export/points/heatmap/facets), computeTargetMode(),
table messaging, heatmap cache keys, and camera moveEnd refreshes. The seam is a
single semantic predicate **`searchFiltersSurfaces()`** (false only for
`kind === 'text' && searchui === 'pin'`; concept/described-by filtering stays
global), used consistently by every consumer. Skipping the direct call then
becomes a latency optimization, not the correctness mechanism.
- expected effect: search response β‰ˆ filter-build time (~1-5s cold) instead of
+10-20s of surface refiltering β€” this is what makes #172 Β§7 winnable
- table/facets/clusters keep their pre-search state (facet counts stay
search-independent, per original #158 contract)
- clear messaging in panel: "N matches pinned on globe" replaces "N of N in view"
- default stays current behavior until team confirms (Andrea/Eric habit risk);
the eventual flip is a real change (tests/docs/rollback/telemetry), not 1 line β€”
`searchui=pin|filter` recorded as a temporary rollout flag (deliberate,
documented deviation from the "URL params unchanged" clause)
- URL: `searchui=pin` round-trips like `fts` param (state contract: no new hash state)

### Inc 3 β€” NOT in scope
Scope buttons (done), mockup-v1 sidebar mirror (#200 β€” parked per closure triage).

## Verification
- 4 acceptance cases on staging (fork Pages dispatch from branch, as this morning)
- e2e spec additions: pins count == min(50, results); pins cleared on clear;
`?searchui=pin` skips refilter (assert facet counts unchanged after search)
- Codex loop: plan review (round 1 DONE 7/17 β€” P0 seam corrected, this doc amended)
β†’ implement β†’ per-increment review rounds β†’ LGTM gate
- honest sizing (Codex): Inc 1 β‰ˆ 1–2 days Β· Inc 2 β‰ˆ 2–4 days Β· staging β‰ˆ 1 day

## Files
- `explorer.qmd`: viewer cell (~:773 area) for collection; `doSearch` result path
for populate/clear; mouse/click handlers for pick dispatch; search-complete path
for Inc 2 skip
- `tests/playwright/pin-overlay.spec.js` (new)
<!-- cc:2026.07.17 -->
10 changes: 10 additions & 0 deletions assets/js/explorer-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ export function escapeHtml(value) {
.replace(/'/g, '&#39;');
}

// Panel/list ownership check (#172 Inc 1, Codex round-4 P1). Every producer that
// writes the #samplesSection list (search / concept renders, cluster
// nearby-samples, selection-invalidation clears) captures a generation via
// `++viewer._panelGen` when it takes the list; an async producer may only write
// list + pins while it still holds the latest generation. Pure so the ownership
// invariant is unit-testable independently of the OJS runtime.
export function panelWriteAllowed(captured, current) {
return captured === current;
}

// Split a free-text query into whitespace-delimited terms (no empties).
export function searchTerms(value) {
return String(value || '').trim().split(/\s+/).filter(Boolean);
Expand Down
Loading
Loading