leap
Lifetime Exposures와 Asthma 결과 예측(LEAP) 모델용 파이썬 패키지
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Coordinates are interpreted as GRCh37/hg19 by default — that is the assembly
every resolution endpoint in this app targets (grch37.rest.ensembl.org), so an
hg38 position passed through unconverted would either miss or silently resolve
to the wrong locus. GRCh38 input is supported through an explicit toggle — see
GRCh38 input toggle.
HGVS is accepted in genomic, coding and protein form:
Pasted coordinate rows are also accepted, in the shapes people actually paste
out of VCFs, MAFs, spreadsheets and pipeline reports:README 전문 보기 ↗Coordinates are interpreted as GRCh37/hg19 by default — that is the assembly
every resolution endpoint in this app targets (grch37.rest.ensembl.org), so an
hg38 position passed through unconverted would either miss or silently resolve
to the wrong locus. GRCh38 input is supported through an explicit toggle — see
GRCh38 input toggle.
HGVS is accepted in genomic, coding and protein form:
chrX:g.20148675dup NM_001412.4:c.388dup EIF1AX:p.Gln130ProfsTer3
Pasted coordinate rows are also accepted, in the shapes people actually paste out of VCFs, MAFs, spreadsheets and pipeline reports:
| Input | Notes |
|---|---|
X 20148674 EIF1AX T TG | chrom, pos, gene, ref, alt — tabs or spaces |
X 20148674 T TG | gene column optional |
EIF1AX X 20148674 T TG | MAF column order (Hugo_Symbol first) |
X,20148674,EIF1AX,T,TG | CSV/semicolon/pipe separated, quoted cells fine |
X 20,148,674 EIF1AX T TG | thousands separators in the coordinate |
X 20148674 EIF1AX T TG hg19 | trailing build/zygosity columns ignored |
X 20148674 EIF1AX - G | MAF-style - for an absent allele |
Parsing anchors on the chromosome/position pair wherever it sits in the row and
reads the alleles from the columns that follow. When more than two DNA-like
columns follow the position, the last two are the alleles — a gene symbol
that happens to spell DNA (TTN, CAT, TAT) is always written before them.
A row that looks like coordinates but cannot be parsed is rejected with a
message naming the expected layout. It is deliberately not passed through to
the GENE p.Change interpretation: doing so used to turn
EIF1AX X 20148674 T TG into the query EIF1AX:p.X and then report it back as
a variant that could not be found.
VCF-anchored indels are reduced to the minimal HGVS event before lookup, so
X 20148674 EIF1AX T TG is queried as chrX:g.20148674_20148675insG and
resolves to chrX:g.20148675dup / NM_001412.4:c.388dup.
See tests/coordinate-row.test.js.
A checkbox under the search box — "My coordinates are GRCh38/hg38" — lets
genomic-coordinate input (chr:g. HGVS, pasted VCF/MAF rows) be entered in
GRCh38. Unchecked is the long-standing GRCh37/hg19 default. The choice is kept
in the shareable URL as ?assembly=grch38 (also accepted: hg38, 38).
How it works:
/map/human/GRCh38/…/GRCh37 endpoint (with the GRCh37
mirror as a failover host), and the entire pipeline then runs on the GRCh37
coordinate exactly as if it had been typed. The mapping appears as its own
step in the lookup progress panel.NC_ accession declares its own assembly through its version
(NC_000007.13 = GRCh37, .14 = GRCh38) and overrides the checkbox in
both directions — the accession is unambiguous, the checkbox can be stale.
NC_ genomic HGVS of either build is converted to the chr form and, when
GRCh38, lifted the same way.NM_…:c.…, GENE:p.…, gene-only) has
no assembly and is untouched by the checkbox.pos38 hint, skipping the
server-side liftover round-trip for that variant.Only positions are remapped; the event description (A>T, del, insG, …)
is carried over byte-for-byte. Helpers: splitGenomicHgvsPositions,
ncGenomicToChr, mapGrch38GenomicToGrch37 in script.js; unit tests in
tests/genomic-coordinates.test.js, browser coverage in
tests/browser-smoke.js.
Every nomenclature lookup depends on grch37.rest.ensembl.org. That host
intermittently returns 500/503 and has been measured taking 20-40 seconds to
answer a cold variant_recoder or vep query.
The variant recoder now fails over to rest.ensembl.org when the GRCh37
mirror is unwell (transport errors and retryable 5xx only — a 4xx is a real
answer and is not shopped around). The main host's coordinates are GRCh38, so
candidate conversion reads the assembly off the RefSeq chromosome accession
version (NC_000007.13 = GRCh37, .14 = GRCh38) and lifts only GRCh38
candidates back to hg19. This assembly check also fixed a latent bug: the
GRCh37 mirror's substitution candidates were previously always passed through
the hg38→hg19 liftover, and Ensembl's /map does not error when handed a
coordinate from the wrong assembly — it silently returns a position hundreds of
kb away. A GRCh38 candidate the client-side helper cannot lift (indels; the
helper handles substitutions only) is dropped rather than carried forward
mislabelled as hg19.
When both Ensembl hosts fail, the recoder falls back to NCBI Variation
Services (api.ncbi.nlm.nih.gov/variation/v0, CORS-open, independent
infrastructure): /hgvs/{query}/contextuals places the variant as a SPDI, and
/spdi/{spdi}/all_equivalent_contextual remaps it to the GRCh37 NC_
chromosome placement. The scope is deliberately narrow — accessioned nucleotide
HGVS only (NM_/NC_/NG_/NR_ with c./g./n.); gene-symbol and protein queries
still need Ensembl. NCBI's contextual SPDIs are VCF-anchored ("TAAT→T" for a
deletion), so they are trimmed to the minimal event in both trim orders
(minimalSpdiForms) — in a repeat region the order decides which flank
survives, and MyVariant indexes only its normalised form — and fed into the
existing candidate-conversion path as a recoder-shaped response with no
transcript nomenclature. The progress panel says "resolved via NCBI Variation
Services" rather than pretending Ensembl answered.
The app previously made a single attempt behind a 6-7 second deadline, so a slow or unwell upstream produced:
Variant not found. Please verify the genomic coordinate and reference allele.
— which blames the user's input for an outage, and sends them to re-check a coordinate that was never wrong.
Two changes address this:
fetchWithRetry wraps fetchWithTimeout with bounded retries and
exponential backoff plus jitter. Transport errors and retryable statuses
(408, 425, 429, 500, 502, 503, 504) are retried; 4xx is not, because a 404
from MyVariant.info is a real answer — it holds no record for many indels.
Per-attempt deadlines were raised to match measured latency (recoder 12 s,
VEP 20 s, MyVariant 10 s). See tests/fetch-retry.test.js.
The lookup progress panel (LookupProgress in script.js, .lookup-progress
in style.css) replaces the single mutable status line. It shows what the
input resolved to — gene, genomic/coding/protein HGVS, effect, assembly — and
one row per upstream source with its state, timing and retry count.
The panel distinguishes three outcomes that used to look identical:
| Glyph | State | Meaning |
|---|---|---|
● | ok | the source answered and had data |
◍ | empty | the source answered and holds no record — routine for indels |
✕ | fail | the source did not answer (status, timeout or network error) |
The final error message is derived from that record, so an Ensembl outage now reads "…did not respond. This is an upstream outage, not necessarily a problem with your variant — please retry in a moment" rather than blaming the coordinate.
This repo now includes a serverless endpoint at api/tp53.js for TP53 mutation database lookups.
POST /api/tp53{
"gene": "TP53",
"protein": "p.R175H",
"cdna": "c.524G>A",
"genomic": "chr17:g.7673803C>T"
}
The API returns:
match_countclassificationprevalencematches (top matched rows)dataset_url used by the proxydebug object (when debug: true is passed in the request payload)Protein matching normalizes HGVS three-letter amino-acid notation to single-letter
notation (for example p.Arg273His → R273H) before scoring. A row scores on an
exact protein token match, an exact cDNA substring, or an exact genomic position;
same-codon variants (e.g. R175C when you queried R175H) are surfaced in matches
for context (flagged same_codon_match: true) but do not count toward match_count
(they are reported separately as related_codon_count) and never drive the
pathogenicity summary.
The backend loads the dataset directly from the NCI TP53 database's CSV files under
https://tp53.cancer.gov/static/data/ (e.g. MutationView_r21.csv). It does not
use the view_data?bq_view_name=* URLs, which render HTML pages rather than CSV.
To survive release bumps (r21 → r22 → …), the proxy first scrapes the current-release
CSV links from the download page https://tp53.cancer.gov/get_tp53data; if that can't
be reached it falls back to the hard-coded r21 URLs. If a refresh fails but a dataset
was previously loaded, the last-good copy keeps being served instead of erroring.
Set this in Vercel Project Settings → Environment Variables:
TP53_MUTATION_DATASET_URL = a direct CSV URL that takes priority over discovery and the built-in list.If not set, the function discovers the current release and falls back to the built-in URLs.
Pass debug: true in the POST body to receive:
dataset_attempts)This repo includes an optional AI interpretation endpoint at api/ai-review.js. The webpage adds a bottom "Optional AI Review" card after a variant lookup. The card does not send data automatically; users choose a model and click Run AI review to send the retrieved annotation, transcript, FDA companion-diagnostic, and clinical-trial context to the backend proxy.
POST /api/ai-reviewOPENROUTER_API_KEY (unless the caller supplies their own key — see BYO-key below).openai/gpt-6-luna (default), openai/gpt-5-mini, openai/gpt-5-nano, openai/gpt-oss-120b, anthropic/claude-haiku-5.5, google/gemini-2.5-flash-lite, google/gemini-3.1-flash-lite, deepseek/deepseek-v4-flash, deepseek/deepseek-v4-pro, qwen/qwen3.7-flash, minimax/minimax-m2.7, and nvidia/nemotron-3-super-120b-a12b:free. The list favors cheap models with a 1M-token window because review contexts are input-heavy (hundreds of thousands of tokens for rich genes); gpt-oss-120b has a 131k window and cannot take the largest payloads. The :free Nemotron route costs nothing but is served by third-party free-tier providers whose data-use terms differ from paid routes — check OpenRouter's provider policy before relying on it for sensitive context.{
"model": "openai/gpt-6-luna",
"context": {
"submitted_variant": "BRAF V600E",
"tumor_type": "melanoma",
"gene": "BRAF"
}
}
The endpoint prompts OpenRouter for strict JSON containing pathogenicity, specific AMP tier/subtier, FDA-approved therapies, clinical trials, a short interpretation summary, and limitations. AI output is intended for research and education only and must be verified against current FDA labeling, guidelines, curated databases, and trial eligibility criteria before clinical use.
The proxy protects the owner's OPENROUTER_API_KEY from being drained by anonymous callers. Every layer is graceful — it stays off until its environment variables are set, so the site keeps working before you provision anything:
vercel.json sets maxDuration 300s for api/ai-review.js (the Hobby-plan ceiling with Fluid Compute enabled) and the handler aborts the OpenRouter call at 290s, so slow models on large contexts return this handler's own error rather than a platform timeout.max_tokens 8000, the context is clamped to 1,500,000 chars, user notes to 4,000 chars, and request bodies over ~4 MB (just under Vercel's serverless request-body ceiling) are rejected with 413. These bound the cost of any single call. The context clamp is generous because rich genes assemble very large payloads (e.g. HER2/ERBB2 amplification ≈ 957 KB, almost all openFDA label text). To keep such payloads under the ceiling, the frontend caps the openFDA record count (first 40) for the AI query while keeping each record's full indications_and_usage text; when records are dropped, an openfda_records_truncated note is added to the context so the model knows the list was capped. Large contexts may still exceed some models' windows — prefer a large-window model.https://drdoubleb.com, https://www.drdoubleb.com, https://variant-search-for-chat-gpt.vercel.app, and this project's own previews (variant-search-for-chat-gpt-*.vercel.app) — a bare *.vercel.app was previously allowed, which admitted anyone's Vercel deployment. Override with AI_ALLOWED_ORIGINS (comma-separated; entries may embed * wildcards that match within one DNS label; a bare * disables the check). Requests with no Origin header (curl, server-to-server) are allowed through to the other layers.TURNSTILE_SECRET_KEY to require a bot-challenge token (turnstile_token in the body) on owner-key requests. Unset → skipped.UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN to enable per-IP and a global daily circuit breaker. Defaults: AI_RL_IP_PER_MIN 12, AI_RL_IP_PER_DAY 120, AI_RL_GLOBAL_PER_DAY 1500. On limit, returns 429 (per-IP) or 503 (global daily) with a Retry-After header. Unset → skipped.max_tokens is shared with the model's reasoning tokens on reasoning models (gpt-*, deepseek-v4-*, qwen3.7-flash). A model that thinks for 1,500 tokens has only the remainder left for the answer, so a low cap makes the JSON stop mid-array — or come back empty when reasoning consumes the whole budget. That is why the cap is 8000 rather than 3000, and why the same variant can succeed on one model and fail on another.
The proxy now separates the causes instead of surfacing a raw parser message like Expected ',' or ']' after array element in JSON at position 4348:
| Condition | Message |
|---|---|
finish_reason: 'length' | ran out of output tokens — try another model or a smaller context |
| empty content | spent its whole budget on internal reasoning |
no { in content | replied with text instead of JSON |
unbalanced {...} | JSON response was incomplete or malformed |
Both prompts also restate their output format after the context payload. The payload can be hundreds of thousands of tokens, so a format instruction placed before it is far from the end; restating it last keeps weak models from drifting into prose. tests/ai-review-response.test.js covers the failure branches and tests/ai-review-prompt.test.js pins the ordering.
Callers may supply their own OpenRouter key as openrouter_api_key in the POST body (format sk-...). BYO-key requests pay their own cost, so they bypass Turnstile and the rate limiter (a useful valve when the shared key or a user's own balance runs out). A malformed key returns 400. The success response includes used_user_key: true|false.
Send { "mode": "prompt", "context": { ... } } to receive the fully assembled prompt ({ prompt, model }) without calling any model — this powers the "copy prompt" button so users can paste it into their own LLM. It requires no key and incurs no cost (origin-checked only).
Because a person pasting the prompt into a general chat LLM wants the readable answer (not the strict JSON the site parses into cards), this mode returns a separate, self-contained prompt that asks for a Markdown-formatted, human-readable interpretation covering the same fields and tiering rules. It does not mention JSON at all.
The two prompts share their clinical guidance so they cannot drift apart:
| File | Role |
|---|---|
api/_ai-review-prompt-core.js | Shared clinical guidance — AMP/ASCO/CAP tier definitions, hard tiering rules, controlled vocabularies, interpretation rules, and the self-check. Format-neutral: it never mentions JSON, schemas, or arrays. Tiering and evidence edits go here and take effect in both prompts. |
api/_ai-review-prompt.js | Core + a JSON output-format section (the schema, field semantics). Used by "Run AI review". |
api/_ai-review-prompt-human.js | Core + a Markdown output-format section (numbered sections for a reader). Used by "Copy prompt". |
All three are underscore-prefixed because Vercel turns every non-underscore .js file in api/ into a Serverless Function, and the Hobby plan caps a deployment at 12. These are imported modules, not endpoints — same convention as _ncbi.js, _ratelimit.js, and _turnstile.js. tests/vercel-function-count.test.js enforces the budget and checks that every counted file really does default-export a handler, so a missing underscore fails locally instead of at deploy time.
Earlier versions used one JSON template plus a trailing "disregard the JSON instructions" override for copy-prompt mode. The resulting self-contradiction confused weaker models, so each prompt is now internally consistent end to end. tests/ai-review-prompt.test.js enforces both properties: that the clinical core is byte-identical in the two prompts, and that the human prompt contains no JSON/schema instruction or override phrasing.
Rate limiting uses @upstash/ratelimit and @upstash/redis (declared in package.json). They are imported lazily and only when the UPSTASH_* env vars are present, so deployments that don't enable rate limiting don't need them at runtime.
The AI review payload also includes a supplemental_card_data object populated from live card lookups when available, including direct ClinVar VCV/nearby-variant results, CIViC API assertions, gnomAD v4, SpliceAI Lookup scores, PubMed article previews, COSMIC extended data, and the TP53 mutation database for TP53 variants.
A free-text PubMed search for "BRAF V600E" misses papers that write the same
variant as p.Val600Glu, c.1799T>A, or rs113488022. When the lookup has a
specific variant, the PubMed card's variant tabs go through NCBI's LitVar2
(PubTator3 API) instead: the query is resolved to a variant entity, and the
entity search returns relevance-ranked articles across every nomenclature
spelling — optionally refined with the tumor type. The card shows a provenance
line naming the matched entity/rsID and deep-links to LitVar2; when no entity
matches (or the service is down) the plain term search runs unchanged. This
lives inside api/pubmed.js (gene/variant/extra params), so no extra
serverless function is spent.
Answers "how often is this variant seen, and in which tumor types?" from
cBioPortal's public API, which is CORS-open and therefore called directly from
the browser (no serverless function). v1 queries the MSK-IMPACT 2017 cohort
(10,945 prospectively sequenced advanced-cancer tumors): gene mutation
frequency, the exact protein change's frequency (single-letter-normalized
match), the gene's most frequent changes, and a tumor-type breakdown for the
matched variant. The card labels the cohort explicitly — these are
single-institution advanced-cancer cohort frequencies, not population
prevalence. Results are included in the AI-review payload under
supplemental_card_data.cbioportal_prevalence.
When a variant has a ClinGen VCEP (expert panel) classification, the ClinVar
card shows it — classification, panel, publication date, and a link to the
Evidence Repository record. VCEP assertions apply the ACMG criteria via a
gene-specific expert process, so they are higher-confidence than aggregate
ClinVar where they exist (most variants have none; the line only renders on a
hit). The lookup is a single CORS-open GET against
erepo.clinicalgenome.org using the GRCh37 NC_ genomic HGVS built from the
resolved coordinate, so it matches regardless of which transcript version the
panel used. Included in the AI payload as
supplemental_card_data.clingen_erepo.
The gnomAD card now ends with a gene-constraint line — pLI, LOEUF
(oe_lof_upper), missense Z, and o/e missense from gnomAD v2 — fetched with
one CORS-open GraphQL query. Constraint says how tolerant the gene is to
loss-of-function and missense variation, which is crucial context for
truncating variants. Included in the AI payload as
supplemental_card_data.gnomad_constraint.
The FDA drugs card gains a fourth tab, DGIdb: aggregated drug–gene interaction claims (approved and investigational compounds) from DGIdb's CORS-open GraphQL API, sorted by interaction score with approval status, interaction type, and source counts. Clearly labelled as interaction claims — not efficacy or approval evidence. Deliberately excluded from the AI-review payload: DGIdb's aggregated investigational/preclinical claims would dilute the clinical sources the review is meant to weigh — the tab is for human browsing.
Every serverless proxy (not just ai-review) now checks the browser Origin
header against a shared allowlist (api/_origin.js): the live site, the
production Vercel host, and this project's own variant-search-for-chat-gpt-*
previews. Before this, any third-party website's JavaScript could use the
deployment — and its NCBI_API_KEY quota and Vercel invocation budget — as its
backend. Requests with no Origin header (curl, server-to-server
integrations, GPT actions) are deliberately allowed through; only third-party
browser embedding is blocked. Override with PROXY_ALLOWED_ORIGINS
(comma-separated; falls back to AI_ALLOWED_ORIGINS; * disables the check).
The SpliceAI card uses api/spliceai.js as a lightweight proxy to the Broad Institute SpliceAI Lookup API. The proxy accepts variants in chr-pos-ref-alt format and forwards interactive-use requests with hg, distance, mask, and Gencode (bc) parameters. Returned scores are displayed in the SpliceAI card and included in the AI review payload under supplemental_card_data.spliceai_lookup.
Several cards need more than a gene and a protein change: the UCSC link and the
ClinVar region pull need a genomic position, while gnomAD, SpliceAI and the
gnomAD variant pages need VCF-style chr-pos-ref-alt alleles.
Only substitutions carry their alleles in the notation. A multi-base indel comes
back from the Ensembl variant recoder as a bare span — TSC2 c.2319_2321delAAT
normalises to chr16:g.2122948_2122950del, with no REF or ALT anywhere in the
string — and MyVariant.info does not index every such variant, so its vcf block
is often absent too. Coordinates are therefore resolved in two stages:
parseGenomicHgvs / buildVariantCoordinateTuple parse every genomic HGVS
form (sub, del, dup, ins, inv, delins, and the REF>- / ->ALT
spellings the SPDI converter emits) into a chromosome plus a start/end span.
Alleles are reported as null rather than guessed. Position-only consumers —
the UCSC hg19 link, the ClinVar region search and nearby-variant plot, the
gnomAD region view — work off this alone.resolveVcfAlleles fills in the alleles for allele-dependent consumers,
preferring what the notation already carries, then MyVariant's vcf block, and
finally reading the bases off the GRCh37 reference (Ensembl
/sequence/region, ±60 bp) and anchoring them VCF-style on the preceding base.
The result is then left-aligned, because HGVS shifts indels 3′ while VCF —
and therefore gnomAD — indexes the 5′-most representation. CFTR F508del is
g.117199646_117199648del in HGVS but 7-117199644-ATCT-A in gnomAD; without
left-alignment the lookup misses. Events wider than 1 kb are skipped, as they
are not represented as ref/alt strings by those resources anyway.The resolution runs once per lookup and is shared by the Variant card's VCF (hg19) line, the gnomAD v2 link, the gnomAD v4.1 query, the SpliceAI card and the AI review context. Cards render immediately and upgrade in place when the alleles land.
The Variant card shows the resolved CHROM-POS-REF-ALT next to the g. notation,
since that is the form gnomAD, SpliceAI and most VCF-derived tools index by. When
left-alignment moved the position it is marked (left-aligned) with a tooltip, so
a coordinate that disagrees with the 3'-shifted g. notation above it does not read
as a bug.
api/gnomad-v4.js lifts only the anchor position GRCh37→GRCh38 and reuses the
alleles, which is exact for SNVs but can land an indel a few bases off how gnomAD
indexes it. When the constructed ID misses for a non-SNV, the proxy scans a small
region around the lifted position for a record carrying the same alleles before
reporting not_found; a match is reported with a matchedVia note.
Note that gnomAD reports an absent variant as a GraphQL error (Variant not found) rather than a null result, so that particular message is treated as
absence rather than as an API failure — otherwise the region scan above is never
reached and the card shows an error where "not found" belongs.
api/_liftover.js)The GRCh38 coordinate is resolved in this order:
pos38. myvariant.info returns a GRCh38 start in
dbnsfp.hg38 / dbsnp.hg38 for most indexed variants, and the frontend
already holds that annotation, so it passes it to the proxy. This skips the
network round-trip entirely and is the common path./map, across two hosts — rest.ensembl.org and the
grch37.rest.ensembl.org mirror, which serves the same paths from a separate
deployment. Requests are hedged: the mirror starts once the primary has
failed or gone quiet for ~1.2s, and the first usable answer wins. A healthy
primary answers well inside that window, so the mirror is normally never hit.
The whole ladder works to a 6s deadline, below the 15s function budget it
shares with the gnomAD query.This replaced a single un-retried call to rest.ensembl.org. That host is a real
single point of failure: during one outage it answered HTTP 500 or hung on every
/map and /vep request for hours while /info/ping stayed green, and the card
reported "liftover failed for 4:143094904" — a coordinate that maps fine, to
4:142173751.
Outcomes are also reported distinctly. liftover_unavailable means the provider
could not be reached and retrying later will probably work; liftover_failed
means Ensembl answered and the coordinate has no unambiguous GRCh38 equivalent.
Collapsing the two blames the variant for someone else's downtime. Mappings are
validated against the requested assembly and chromosome, and several disagreeing
candidates count as no answer — the previous code took mappings[0] blind, which
turns a patch/scaffold hit into a confident wrong coordinate.
The client-side hg38→hg19 helper in script.js fails over across the same two
hosts. It returns its input unchanged when conversion fails, so an outage there
silently mislabels an hg38 coordinate as hg19 rather than erroring.
ClinVar recovery from the region pull matches SNVs on position plus alleles (with
strand complementation). Indels have no comparable alleles and their coordinates
differ between HGVS and ClinVar's own representation, so they are matched on the
c. notation in the record title instead, normalised so that the user's
c.2319_2321delAAT and ClinVar's c.2319_2321del compare equal.
The Ensembl variant recoder is the primary route from a non-genomic query to a genomic coordinate, but it does not cover everything, and the fallbacks matter.
cDNA (c.) queries. When the recoder is unavailable or returns no usable
candidate, the app falls back to Ensembl VEP, which resolves gene-level cDNA HGVS
(TSC2:c.4952delA) that the recoder can miss. VEP's response carries the genomic
location, so buildGenomicHgvsFromVep turns it into a g. string and the lookup
keeps a real coordinate. Previously only the consequence was read off that
response, so g. kept echoing the user's own query and every coordinate-derived
card stayed dark even though the variant had been resolved.
Protein (p.) queries. These are fundamentally harder, and for two shapes they are impossible:
p.N1651Mfs*21 can arise from many different indels.p.L773_I774delinsF → "Could not determine nucleotide change from peptide
change"), in single- or three-letter form, against a gene or a protein
accession.Simple substitutions (p.Val600Glu) do resolve, so those are unaffected.
What is determined by protein notation is the codon. Rather than failing the
whole lookup, resolveProteinCodonRegion maps the residue range to GRCh37
coordinates via the gene's canonical translation (Ensembl /lookup/symbol then
/map/translation), and the lookup continues with gVariant set to a bare span
such as chr16:g.2136834_2136836. parseGenomicHgvs reads that as type region:
position-based cards (UCSC, ClinVar region search, nearby variants, gene-level
CIViC/OncoKB/FDA/trials/PubMed) work, while allele-based ones (VCF line, gnomAD
v4.1, SpliceAI) correctly report nothing. The Variant card labels the line
Codon region (hg19) and states which residues resolved and why the exact
nucleotide change is unavailable. A residue beyond the end of the canonical
protein is rejected rather than silently mapped to the wrong locus.
Queries that cannot even be resolved to a codon fail with a message naming the reason and pointing at c./g. notation, instead of the previous bare "Variant not found via Ensembl Variant Recoder".
npm test runs every tests/*.test.js with plain Node — no framework. The
tests used to re-declare the helpers they exercised ("copied verbatim — KEEP
IN SYNC" blocks), which drifted from production by design pressure alone.
They now import the real implementations:
script.js stays a single classic <script> for the browser (no build
step, one file to copy to Hostinger — export statements would be a
SyntaxError there), but Node imports it fine as a module. A guarded block at
the bottom stashes the pure helpers on globalThis.__variantSearchHelpers
when running under Node; tests await import('../script.js') and destructure
from there. The DOMContentLoaded registration is skipped when document
does not exist, and fetchWithRetry is built through a makeFetchWithRetry
factory so the retry loop can be driven with an injected fetch.api/*.js are ES modules already; the handlers now also carry named
exports for their pure helpers (export { buildAssertions, … }), which the
Vercel runtime ignores. api/tp53.js gained extractDatasetUrls — the pure
parsing core of discoverDatasetUrls — as a real seam instead of a test-only
re-implementation.The one remaining intentional re-composition is buildChangeForms in
tests/clinvar-match.test.js, which mirrors logic composed inline in the
ClinVar card rather than a named production function.