Skip to content

explorer: support described-by=<concept-uri> search (#248 Flavor A)#252

Merged
rdhyee merged 4 commits into
isamplesorg:mainfrom
rdhyee:feat/described-by-concept
Jun 1, 2026
Merged

explorer: support described-by=<concept-uri> search (#248 Flavor A)#252
rdhyee merged 4 commits into
isamplesorg:mainfrom
rdhyee:feat/described-by-concept

Conversation

@rdhyee
Copy link
Copy Markdown
Contributor

@rdhyee rdhyee commented Jun 1, 2026

Closes #248 (Flavor A).

What

Adds a described-by=<concept-uri> query param that filters the entire explorer — table, globe points, facet counts, side panel — down to material samples whose URI-valued iSamples-vocabulary concept (object_type / material / context in sample_facets_v2) exactly matches the URI.

Example deep link (cross-domain — biology, not archaeology):
/explorer.html?described-by=https://w3id.org/isample/vocabulary/materialsampleobjecttype/1.0/wholeorganism
"Samples described by: Whole organism material sample (50 of 291,210)", every surface filtered.

This is Eric Kansa's requested concept-URI search. Flavor B (arbitrary external/Getty URIs needing URI→label resolution + free-text fallback) is a follow-up.

How

Builds directly on the A1 search_pids machinery (#251). Because A1 made every surface read from the single shared search_pids table, #248 only needed a second producer — no surface changes:

  • buildConceptFilter(uri) — mirrors buildSearchFilter's token-scoped staging + finally-drop guards; WHERE is exact-URI match across the three URI-valued facet columns; ranks object_type > material > context. (escSql-escaped.)
  • doDescribedBy(uri) — the second entry point. Reuses buildConceptFilter + applySearchFilterChange (kind-agnostic) and renders its own side panel. Deliberately does not touch doSearch — keeps A1's just-shipped text-search stale-guard hot path untouched.
  • writeQueryStatesearch= and described-by= are mutually exclusive; intent-aware so a committed text search and a committed concept filter each own the URL, and draft (un-submitted) text can't clobber an active concept filter.
  • Boot — a described-by= deep link auto-commits and wins over search=.

Review / verification

  • Codex adversarial review, 2 rounds → converged. Round 1 found 3 real issues (URL-intent on draft text + facet toggle; a superseded-producer URL-write race; a reflected-XSS path on the URL-derived concept label) — all fixed. Round 2: no remaining bugs.
  • tests/playwright/described-by-verify.mjs (new): concept deep-link coherence (globe + panel + URL) + the draft-text-safety regression + mutual-exclusivity with text search. Green headless.
  • a1-verify.mjs still green — no A1 regression.
  • Pre-deploy smoke gate (tests/test_smoke.py) green locally — text search path unaffected.

🤖 Generated with Claude Code

rdhyee and others added 4 commits May 31, 2026 22:28
…Uri + buildConceptFilter

First, additive pieces for `described-by=<concept-uri>` (isamplesorg#248), riding the A1
search_pids machinery (Codex plan-reviewed: "mostly sound + guardrails"):

- window.conceptLabelForUri(uri): expose the facetFilters cell's URI→prefLabel
  resolver so the concept producer can label a URI without re-querying
  vocab_labels (guardrail #1).
- buildConceptFilter(uri): a SECOND search_pids producer — exact-URI match
  across the object_type/material/context columns of sample_facets_v2, with
  object_type>material>context relevance ranking. Same token-scoped staging,
  finally-drop, shared _searchFilterToken, and empty-clear invariant as
  buildSearchFilter (guardrails #2/#6). Tags __searchFilter.kind ('concept' vs
  'text') for mutual exclusivity (#5).

Not yet wired: doDescribedBy flow (shared runPidSetResults render), the
described-by= URL param + writeQueryState kind-preservation, and mutual
exclusivity at producer entry. buildConceptFilter isn't called yet, so this is
behavior-neutral. Verified: conceptLabelForUri('…organismpart')→"Organism part";
free-text a1-verify still ✅ COHERENT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… filter

Wire the concept-URI filter end to end on top of the A1 search_pids
machinery. A `described-by=<uri>` deep link selects material samples whose
URI-valued facet concept (object_type / material / context) exactly matches
the URI, and filters EVERY surface (table, globe points, facet counts, side
panel) via the same `pid IN (SELECT pid FROM search_pids)` semi-join.

- doDescribedBy(uri): second entry point into search_pids. Reuses the already
  -reviewed buildConceptFilter (producer) + applySearchFilterChange (refresher,
  kind-agnostic) and renders its OWN side panel. Deliberately does NOT touch
  doSearch — keeps A1's just-shipped (isamplesorg#251) text-search stale-guard hot path
  untouched (RY decision: protect the path Kerstin demos Wed).
- writeQueryState: search= and described-by= are mutually exclusive. Keyed off
  the text input (always current) rather than window.__searchFilter (stale at
  doSearch's early writeQueryState call), so a committed text search drops
  described-by= and vice-versa.
- Boot: described-by= deep link auto-commits and wins over search= if both
  present.
- tests/playwright/described-by-verify.mjs: HEADLESS deep-link coherence test
  (globe + panel + URL all reflect the concept; mutual-exclusivity with text).

Verified: described-by-verify green; a1-verify still green (no A1 regression).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e guard, XSS)

Codex round-1 found 3 real issues; all fixed + regression-tested:

1. URL could flip concept→phantom-text on draft text + facet/source toggle.
   writeQueryState is now intent-aware: doSearch passes commitText to
   authoritatively persist/clear a text search (it runs before __searchFilter
   updates); every other caller mirrors the COMMITTED filter in __searchFilter,
   so draft (un-submitted) text no longer clobbers described-by=. New Playwright
   check: draft text + source toggle preserves described-by=.
2. A superseded doDescribedBy could still rewrite the URL. Moved the
   _searchSeq stale-check to BEFORE writeQueryState (and re-check after the
   async applySearchFilterChange) so only the current producer persists state.
3. Reflected-XSS path: the concept side panel interpolated a URL-derived label
   (and result label/pid/url/name) into innerHTML. Now escapeHtml'd (the same
   helper the table renderer uses), covering text + attribute contexts.

Verified: described-by-verify green (concept + draft-text + mutual-exclusivity);
a1-verify still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

explorer: support searches of material samples described by a URI or PID identified concept

1 participant