Skip to content

feat(biochem): Solr 9 compatibility, per-corpus endpoint configuration, and color-blind-safe atom mapping - #231

Merged
samseaver merged 35 commits into
ModelSEED:stagingfrom
VibhavSetlur:solr-upgrade
Aug 26, 2026
Merged

feat(biochem): Solr 9 compatibility, per-corpus endpoint configuration, and color-blind-safe atom mapping#231
samseaver merged 35 commits into
ModelSEED:stagingfrom
VibhavSetlur:solr-upgrade

Conversation

@VibhavSetlur

Copy link
Copy Markdown
Collaborator

Summary

Upgrades the biochemistry UI to work against the new Solr 9 backend, while keeping the current
(legacy) Solr working unchanged when nothing is configured. 33 commits, 41 files.

This PR contains no security/dependency work and no repo cleanup — those are deliberately
separate PRs so each can be reviewed on its own merits.

Merge order (please read)

This PR is stacked on #230 (nanoid GHSA-2v37-7h3g-55p8 + Dependabot grouping).

  1. Merge fix: clear nightly CI audit failure (nanoid 3.3.18) and group Dependabot updates — 3.1.1 #230 first.
  2. I will then merge upstream staging into this branch and push. The metadata resolution is
    already worked out and CI-verified locally (see Known CI state below).
  3. Merge this PR.

Merging in the other order also works but forces the security patch to be re-versioned on top of
3.4.0, so security-first is cheaper.

Known CI state

npm audit --omit=dev --audit-level=high currently fails on this branch with the single
nanoid <3.3.18 high advisory. That advisory is inherited from the base commit this branch was cut
from — this PR does not touch package-lock.json and does not introduce it. It is exactly what
#230 fixes. Once #230 is merged and this branch is updated from staging, the audit step goes
green. Every other CI step passes today (evidence below).

Overlap with #230

Only three metadata files overlap: CHANGELOG.md, VERSION.md, package.json. Zero source
conflicts.
Resolution after #230 lands: version 3.4.0 (it supersedes 3.1.1), and the
changelog keeps both sections, ordered 3.4.0 > 3.3.0 > 3.2.0 > 3.1.1 > 3.1.0.

What changed

Solr 9 compatibility

  • Reactions, compounds, structures, thermodynamics and pKa lookups now work against the Solr 9
    nested-document schema (parent docs plus [child] rows) as well as the legacy flat schema.
  • Schema detection is automatic: one cached probe query per collection, with an explicit
    NEXT_PUBLIC_SOLR_NESTED_SCHEMA override. A failed, refused, or non-OK probe falls back to
    legacy flat queries
    — it never throws and never breaks a page.

Per-corpus endpoint configuration

  • Reactions, compounds and structures can each point at their own Solr base URL and core, so a
    single corpus can be migrated to a new instance while the others stay on the legacy backend.
  • Precedence per corpus: explicit override → deployment-mode variant → the shared
    NEXT_PUBLIC_SOLR_BASE_URL. Nothing configured means today's behavior, unchanged.
  • Optional server-only SOLR_PROXY_UPSTREAM registers a /solr/:path* rewrite so a deployment can
    reach an internal Solr through its own origin. Unset ⇒ no proxy route is registered at all.

Accessibility

  • Reaction structure atom mapping now uses a color-blind-safe palette.

Docs

  • docs/DEPLOYMENT.md now documents the complete Solr environment surface and adds a
    Pointing Biochemistry at a Different Solr section covering four modes: legacy/do-nothing,
    a whole-site Solr 9 switch, a single-corpus or temporary instance, and an internal upstream via
    the built-in proxy — plus the restart/rebuild rule for NEXT_PUBLIC_* values.
  • README.md no longer names configuration keys that were never environment variables.

Verification

Run on a clean worktree at b58514d, in the modelseed-ui conda env:

Check Result
npm run lint ✅ 0 errors, 16 pre-existing warnings
npx tsc --noEmit ✅ 0 errors
npm run test:run ✅ 345 passed, 4 skipped
npm run build ✅ success
npm audit --omit=dev --audit-level=high ⚠️ 1 high — nanoid <3.3.18, inherited from base, fixed by #230

Focused Solr/biochemistry suites: 215 passed, 4 skipped. The 4 skipped tests are legacy
legend assertions retired by this change.

Not validated here

There is no live Solr 9 instance reachable from this environment — the current endpoint still
serves the legacy backend. The nested-schema paths are covered by unit tests against recorded
response shapes, not against a live Solr 9. Behavior against the real new backend should be
confirmed on staging after the proxy is switched over.

Deliberately not in this PR

getReactions/getCompounds now detect the Solr-9 nested-document schema
(parent reaction/compound docs plus [child] rows) via a cached, one-time
probe per collection and add a doc_type parent filter when detected, so
list queries return only parent docs instead of duplicated child rows.

- lib/api/solrSchema.ts (new): parentDocTypeFilter, hasNestedSchema
  (probe + cache, never rejects), resetSolrSchemaCache (test-only).
- lib/api/config.ts: SOLR_NESTED_SCHEMA_OVERRIDE tri-state manual override
  read from NEXT_PUBLIC_SOLR_NESTED_SCHEMA.
- lib/api/biochem.ts: SolrQueryOpts gains optional filterQueries; buildSolrUrl
  appends fq= per entry; getReactions/getCompounds await hasNestedSchema and
  add the parent-doc filter only when nested. Behavior against the legacy
  schema is unchanged.
- tests: new lib/api/solrSchema.test.ts; tests/unit/api/biochem.test.ts
  updated for the extra probe fetch call and covers both schema paths.
…diagram

Add summarizeAtomFlows(), which folds parsed atom-mapping pairs into one
directed flow per (reactant compound -> product compound), counting distinct
source atoms per element, and AtomFlowDiagram, an inline-SVG bipartite view of
those flows with linked compound ids and per-element edge titles.

Per-atom colouring is deliberately not attempted: the indices are InChI
canonical per-element positions, and live data (rxn00001, stoichiometry
(2) cpd00009) maps several distinct source atoms onto the same target index,
so an atom-level correspondence is not derivable from this data alone.
…ource operators

Derive the direction-agreement label from the per-source thermodynamics
direction operators instead of the single server-supplied
sources_agree_direction flag, which cannot express three states.

- all operators identical -> Sources agree on direction
- only one angle-bracket direction, optionally mixed with '=' -> Sources could agree on direction
- both '>' and '<' present -> Sources disagree on direction

The server boolean remains a fallback when no source reports an operator.
The reaction detail page also renders the new AtomFlowDiagram above the
existing atom-mapping summary.
Colour only semantically safe, fully mapped element blocks.
Preserve atom identity across both sides without trusting InChI indices as render indices.
Present reaction structures as one continuous equation rather than compound cards.
Keep compound context readable and linkable while retaining chemical notation.
…op the flat atom-flow diagram

The reaction detail page now parses its atom-mapping entries once and hands
the pairs to the structure equation, which renders them as coloured atom and
bond groups. The old flat atom-flow diagram duplicated that information in a
weaker form and is removed; the raw mapping list stays as secondary detail.
Records the reaction atom-mapping canvas: open continuous structure layout,
element-block mapping colours carried across reactant and product, an explicit
unmappable disclosure, and removal of the flat atom-flow diagram.
- do not paint multi-element simple-ion text tokens one arbitrary mapping
  colour; leave them uncoloured and disclose the ambiguity in the legend
- drop the border and background chrome from the MoleculeRenderer png and
  hidden placeholders so a missing structure no longer reintroduces a box
  inside the open canvas
- surface a plain caption when compound details fail to load, so a failed
  fetch is distinguishable from genuinely absent data
- pin the degradation cases: missing name, missing SMILES, zero/negative
  charge, stoichiometry of one, fetch error, multi-element simple ion
…g them

The atom-mapping colour plan required strict pairwise reciprocity: a
(compound, element) block whose atoms mapped to more than one counterpart
compound was marked `multiple-destinations` and dropped, which also
disqualified its otherwise-valid partners. On rxn00002 that silently
removed every oxygen block, because CO2 oxygen is fed by both water and
allophanate.

Group blocks of the same element into connected components and colour the
whole component, distinguishing `one-to-one` from `merged` correspondence
and naming the members that could not be coloured. Set-level correspondence
is proven by the data; atom-level correspondence still is not, so colour is
still asserted only for a block whose mapped index count equals its
structural atom count.

Add `lib/utils/chemicalFormula.ts` so element inventories can be derived
from a compound formula instead of only from a rendered structure.
Reaction participants were routed to a text token by a heuristic that
counted non-hydrogen atoms in the RDKit-reported inventory and sent
anything with three or fewer to text. On rxn00002 that left water, CO2 and
ammonium as bare labels even though all three carry valid SMILES and all
three are atom-mapped, so the mapping was invisible. The heuristic also ran
off a render callback, so the decision arrived after first paint.

Decide from the compound record instead: draw whenever SMILES is present
and the formula is either absent, unparseable, or contains at least one
heavy atom. Only genuinely heavy-atom-free species such as H+ stay
textual, and they now keep their name, formula, charge and compound link.

Seed element inventories from the formula so a species that never renders
is no longer classified `structure-unknown`; stop tinting text tokens with
an element colour and show a labelled colour-dot row instead; keep tokens
in equation order; and state merged groups and uncoloured members in the
legend. Every token holds its footprint with a skeleton while the compound
query is in flight.
Add hover, focus, and sticky selection emphasis for equation mapping groups.\n\nKeep formula inventory seeding limited to textual participants so drawn structures await RDKit inventory before colour safety checks.
Records the atom_mapping_data grammar as published by the Poplar Solr index, the verified reason per-atom and per-bond colouring cannot be derived in the browser (no InChI string, no molfile and no atom-order field in any core; @rdkit/rdkit 2025.3.4 exposes no AuxInfo or canonical-ranking API), the (compound, element) block model the UI uses instead, and the three alternative server-side field contracts that would make exact per-atom mapping possible.
The reaction viewer needs each compound's raw InChI, not its InChIKey, to
interpret atom-mapping references. Add a dedicated, resilient client over the
structures collection with explicit staging/production configuration so the
existing reaction and compound collection resolution is untouched.
Atom-mapping references are 1-based positions in the InChI canonical order,
which differs from the SMILES order the renderer draws in. Parse the InChI
formula and connection layers, enumerate every element-preserving isomorphism
onto the local heavy-atom graph exhaustively, and colour an atom only when
every canonical index it could denote belongs to one mapping group.
Fetch raw InChI and the precomputed depiction alongside each participant,
report the local heavy-atom graph up from the renderer, and drive atom and
bond colours from the orbit plan. The stored SVG is used only as an
unhighlighted fallback depiction.
…equation

Each participant now discloses whether its colours are exact-atom, symmetry-orbit, element-block or unresolved, with a plain-English reason, a keyboard-reachable disclosure, an upgraded legend with a precision summary, and an accurate notice when structure data cannot be loaded. Adds live full-layer InChI fixtures.
Element-block degradation compared only the size of the mapped index set
against the element count, so an out-of-range reference such as O#3 on a
two-oxygen compound matched by cardinality and coloured both atoms as
fully covered. Require the contiguous set 1..count instead.

The structures Solr client also interpolated compound ids into the query
unescaped; filter Solr-unsafe ids before building the request.
Rewrite the atom-mapping contract document around the structures Solr
core, the Hill/connection-layer orbit method and the four-level precision
ladder, and record the remaining server-side gap. Add the structures
collection variables to .env.example and bump to 3.3.0.
Reaction, compound and structure lookups each resolve their own Solr
base URL from NEXT_PUBLIC_SOLR_<CORPUS>_BASE_URL, its mode-specific
_STAGING/_PRODUCTION default, and finally the shared legacy base, so a
deployment can point structures at a different Solr than the public
proxy without moving the other corpora.

Adds the missing NEXT_PUBLIC_SOLR_STRUCTURES_COLLECTION* keys to the
statically referenced PUBLIC_ENV map; without them Next never inlined
the values and structure-core overrides were silently inert in the
browser. buildSolrUrl is narrowed to the reactions/compounds union so
no corpus can fall through to the shared base unnoticed.

Adds an optional server-side rewrite from /solr/:path* to
$SOLR_PROXY_UPSTREAM/:path*, enabled only when that server-only
variable is set, so a checkout can serve Solr same-origin from a host
that sends no CORS headers.

Release 3.4.0.
Seaver: atoms in the reaction-page SVGs were not individually
identifiable because RDKit renders carbons as bare skeletal vertices,
so a per-atom mapping colour on a carbon had no glyph to land on.

- add buildExplicitAtomLabels() to force an explicit "C" label on plain
  carbons (skips charged/isotopic atoms so native labels are kept)
- MoleculeRenderer gains opt-in showAllAtomLabels and renders the base
  SVG through get_svg_with_highlights({atomLabels}); the existing
  applyAtomLabelColors/applyBondColors recolouring is unchanged
- ReactionStructureEquation enables it for every equation compound
- drop the redundant AtomMappingSummary subsection and its test; the
  in-place legend and the collapsed "Mapping details" caveat remain

Mapping semantics are untouched: colours still come from the existing
orbit/mapping plan, and unmapped atoms stay uncoloured.
Sam Seaver reported the SVG atom-mapping colors are unusable for
color-blind users. The previous 12-color palette mixed Okabe-Ito with
tab10/NEJM colors, which collapse under red-green color vision
deficiency: brown #8C564B and green #20854E differ by only dE 4.2 under
deuteranopia, and #E69F00 / #56B4E9 sat at 2.25:1 and 2.31:1 contrast
against the white SVG canvas.

Replace it with 8 colors built on the four Okabe-Ito members that clear
WCAG 2.1 SC 1.4.11 (3:1) on white, plus four optimized extensions.
Worst-case pairwise separation under protanopia/deuteranopia rises from
dE 4.2 to 19.0, every color clears 3:1 on white, and all stay dE >= 59
from the black RDKit uses for unmapped atoms.

Palette values are the only executable change; group assignment stays
MAPPING_PALETTE[index % length], so ordering, atom correspondence, RDKit
indices, and API contracts are untouched, and no mapping legend, rows,
or details UI returns.

Add tests/unit/utils/mappingPaletteSafety.test.ts, which derives WCAG
contrast, CIE-Lab dE, and Vienot 1999 dichromacy simulation from the hex
values rather than snapshotting them, and proves the checks have teeth
by asserting the retired palette fails them.

Known limitation: vermillion and reddish purple remain close under
tritanopia (~0.01% prevalence); this is inherent to Okabe-Ito and is
pinned by an explicit test.
… scenarios

Cover legacy, Solr 9, per-corpus temporary, and internal proxy modes.
@samseaver
samseaver merged commit 6ee661d into ModelSEED:staging Aug 26, 2026
2 checks passed
@samseaver
samseaver deleted the solr-upgrade branch August 26, 2026 20:56
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.

2 participants