feat(biochem): Solr 9 compatibility, per-corpus endpoint configuration, and color-blind-safe atom mapping - #231
Merged
Conversation
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.
…ged mapping groups
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.
# Conflicts: # CHANGELOG.md # VERSION.md # package.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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).staginginto this branch and push. The metadata resolution isalready worked out and CI-verified locally (see Known CI state below).
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=highcurrently fails on this branch with the singlenanoid <3.3.18high advisory. That advisory is inherited from the base commit this branch was cutfrom — this PR does not touch
package-lock.jsonand does not introduce it. It is exactly what#230 fixes. Once #230 is merged and this branch is updated from
staging, the audit step goesgreen. Every other CI step passes today (evidence below).
Overlap with #230
Only three metadata files overlap:
CHANGELOG.md,VERSION.md,package.json. Zero sourceconflicts. Resolution after #230 lands: version
3.4.0(it supersedes3.1.1), and thechangelog keeps both sections, ordered
3.4.0 > 3.3.0 > 3.2.0 > 3.1.1 > 3.1.0.What changed
Solr 9 compatibility
nested-document schema (parent docs plus
[child]rows) as well as the legacy flat schema.NEXT_PUBLIC_SOLR_NESTED_SCHEMAoverride. A failed, refused, or non-OK probe falls back tolegacy flat queries — it never throws and never breaks a page.
Per-corpus endpoint configuration
single corpus can be migrated to a new instance while the others stay on the legacy backend.
NEXT_PUBLIC_SOLR_BASE_URL. Nothing configured means today's behavior, unchanged.SOLR_PROXY_UPSTREAMregisters a/solr/:path*rewrite so a deployment canreach an internal Solr through its own origin. Unset ⇒ no proxy route is registered at all.
Accessibility
Docs
docs/DEPLOYMENT.mdnow documents the complete Solr environment surface and adds aPointing 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.mdno longer names configuration keys that were never environment variables.Verification
Run on a clean worktree at
b58514d, in themodelseed-uiconda env:npm run lintnpx tsc --noEmitnpm run test:runnpm run buildnpm audit --omit=dev --audit-level=highnanoid <3.3.18, inherited from base, fixed by #230Focused 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
3.1.1work → fix: clear nightly CI audit failure (nanoid 3.3.18) and group Dependabot updates — 3.1.1 #230