Skip to content

Technical documentationInternal

For the team and anyone taking the product over. Not linked from the site and not indexed.

Technical documentation pages

Protein

Protein (registry slug protein-explorer) looks up one UniProtKB entry and joins, in the browser, its UniProt annotation, the PDB entries mapped to it, its AlphaFold DB model, variants, AlphaMissense scores and Open Targets drug and target evidence, with two Mol* viewers linked to SVG sequence tracks. Since Wave 4 (the discovery line, 11 October 2026) it also checks one amino-acid substitution against AlphaMissense and aligns the entry with another UniProt entry using its own pairwise aligner (D-0055). This page describes how the code does that, what it computes, how it is tested and where it diverges from its design documents.

Purpose and users

The app answers "what is known about this protein, structurally and pharmacologically" for a scientist who has a gene symbol, a protein name or a UniProt accession. The PRD (docs/design/prd-protein-explorer.md §1.2) names structural biologists, target-assessment and translational scientists, and medicinal chemists as users; that is design intent, not something the code enforces.

What the app does, as implemented:

  • Searches UniProt (and a local catalogue of reviewed human entries) and opens one entry at a time.
  • Shows UniProt's names, function and subcellular location text, the canonical sequence and its features.
  • Ranks every PDB entry mapped to the protein by PDBe's SIFTS data and loads the top one (or a chosen one) into an "experimental" Mol* viewer; loads the AlphaFold DB model into a "predicted" viewer coloured by pLDDT.
  • Maps a residue or range picked on the tracks onto both structures (loaded-file residue-level SIFTS correspondence for the PDB chain; only unique exact standard-residue partners across all co-optimal alignments for the AlphaFold model).
  • Lists curated UniProt variants, and on request the EBI Proteins API variant list (COSMIC-only records dropped) and per-residue mean AlphaMissense scores with the number of scores each mean is over. AlphaMissense is withheld for the whole entry when any row of its file names a reference residue different from the model's (DEF-W4-2).
  • Checks one typed substitution (V600E, p.Val600Glu and similar) against the current UniProt sequence and shows AlphaMissense's own score for it only when the model, the file and the sequence agree exactly ("Check a substitution", Wave 4 step 7).
  • Aligns the entry, or a picked range of it, with another UniProt entry or isoform: global Gotoh alignment with BLOSUM62, in its own worker, with the residue pairs shared by every co-optimal alignment ("Compare with another entry", Wave 4 step 8, D1).
  • Carries the 3D camera and zoom of one viewer in the share link (cam, afv; Wave 4 step 3).
  • Lists Open Targets drugs and clinical candidates whose mechanism names the target, with an approval and withdrawal policy, plus Open Targets disease associations and tractability.
  • Computes molecular weight and theoretical pI from the sequence with ProtParam conventions.

It ships no data files; everything except the local search catalogue is fetched live from public APIs by the browser (the BLOSUM62 table is generated code, not data; see Pairwise alignment). Registry status is preview, version 0.4.0, updated: "2026-10-25" (Stage F's changelog entry) (content/apps/protein-explorer.ts); Feature 9 adds the retained-image receipt described below. Wave 4 did not change the registry entry, while its docs update was dated 2026-10-11. The registry flags notMedicalAdvice: true. The build record is docs/progress/wave4-discovery.md; the merge reviews are the CSO's, CPO's and CTO's memos of 11 October 2026 in docs/board/memos/.

Addresses and entry points

Address What it is Source
/protein Product page productHref("protein-explorer"), copy in content/apps/protein-explorer.ts (page) and content/tools/protein-explorer.ts (detail)
/protein/app The workspace (welcome with no subject) appHref("protein-explorer"); segment protein from APP_SEGMENTS in content/apps/routes.ts
/protein/app#s=id%3DP00533%26view%3Dworkbench… The workspace with a protein; all state after #s= src/apps/protein-explorer/url-schema.ts
/protein/app?id=P00533 Legacy and cross-app inbound query; accepted on arrival and rewritten into the fragment proteinSchema, shared useUrlState
/docs/protein Public docs page (methods, exports, verification, security, limits) content/apps/protein-explorer.docs.ts
/share/protein/<accession> Crawler-facing share preview for a public accession src/lib/sharing/links.ts (richShareLink), src/lib/sharing/cards.ts (proteinCard)
Object page tab for a target The dossier embedded on the target's object page src/apps/protein-explorer/object-tab.tsx, registered in object-tabs.ts
/apps/protein-explorer, /apps/protein-explorer/app, /tools/protein-explorer Old addresses, permanent redirects legacyRedirects() in content/redirects.ts; legacyPath in content/apps/identity.ts

src/apps/protein-explorer/page-ids.ts declares no extra static pages under /protein/app/<id>; only the shell's about page exists there. The app has no task pages and no explainers.

Inbound links from other apps, declared as crossLinks with to: "protein-explorer" in their registry entries: Assay (target card, id), Cell (tour step 8, HBB P68871 and HBA P69905, id), Calculators (A280 target card, id), Molecule (name-not-found message, no parameters) and Plasmid (a coding part with a UniProt entry, id). The shell's object hand-off opens a target at ?id=<accession> (objectInbound in src/apps/protein-explorer/objects.ts).

File map

App definition and workspace (src/apps/protein-explorer/)

File Role
entry.tsx Client entry loaded by src/apps/definitions.ts only on this workspace; hands the definition and the full registry entry to the shell. displayName drybench-entry:protein-explorer is the marker the size check finds.
definition.ts AppDefinition: Main (lazy), object tabs, empty rail, inspector: "selection" hidden until explicitly opened (D-0060), the global shortcuts, five palette commands, widget keys, usesUrlState, sources() for the freshness chip, search focus, share link and schema, the About section.
lazy.tsx next/dynamic import of views/workspace.tsx with ssr: false, wrapped in ToolBoundary.
views/workspace.tsx The workspace component: address state, welcome / dossier / workbench switch, the pick, the inspector wiring, keyboard actions, focus management, live-region announcements, saved reference views, exports registration and the view header; the camera link binding (cam, afv) and the var and cmp keys. ProteinMain.displayName is drybench-island:protein-workspace, the size check's marker for the workspace chunk.
camera-link.ts cameraLinkStore: the link's pending camera for the viewer structure names, and the report/reject callbacks between the workspace and the 3D viewer. Imported only by the workspace chunk.
substitution-store.ts, align-store.ts The substitution check and the alignment on screen, written by their panels and read by the export menu; cleared when a panel closes.
use-protein.ts The one data hook both views share: every loader, its in-memory cache, derived figures (coverage depth, best resolution, method counts, ProtParam properties, approved drugs), provenance stamps, shell subject and source chips, and the object "refresh" write.
use-attempt-key.ts, confidence-feedback.ts, confidence-notice.tsx Per-transition ownership, status explanations and source/mapping Retry on existing local feedback surfaces.
evidence-exact-map.ts, evidence-metadata-v1.ts, evidence-metadata.ts Strict new exact scientific facts and hashes; independent historical reader; closed version-matrix dispatch to common envelope validation. evidence-capture-identity.ts invalidates preparation when source/current/mapping context changes.
store.ts Minimal external stores shared with the shell outside React: sources on screen, workspace actions, current view, subject, share-link builder.
url-schema.ts proteinSchema (the fragment grammar) and helpers to read view, section, structure mode and selection from state.
selection.ts The shared pick: a UniProt range plus its label and the architecture region it equals, if any.
regions.ts Topology segments and domains derived from UniProt features, for the dossier map and region card.
structure-stage.tsx Builds the MolViewSpec scene for a viewer (useScene) and maps a UniProt focus range into viewer numbering (useViewerFocus); wraps MolstarView in the suite Stage.
inspector.tsx Inspector body (ProteinInspector) and headings; reads inspectStore and publishedStore.
exports.ts Export menu items and the provenance block builder (proteinBlock).
objects.ts Object inputs (targetInput, sequenceInput, drugInput, indicationInput), the inbound rule and the hand-off payload (proteinPayload).
object-tab.tsx, object-tabs.ts The target object page tab: the dossier in compact form.
workspace-inputs.ts Shape and validator of a saved reference view (PROTEIN_WORK_SCHEMA, readProteinWorkspace).
provenance.ts Retrieval-time stamps per source and key, provenance chip builders, and an unused protParamMethod record.
palettes.ts Light and dark drawing colours for tracks and variant classes; the AlphaFold DB pLDDT key.
recent.ts, recent-hooks.ts The last eight proteins opened on this device, in localStorage.
subject-focus.ts Pure rules for where focus goes after a protein loads.
about-sections.tsx "Reading the views" section of the shell's About page.
views/section-panel.tsx Mounts a content section on first visit and keeps it mounted but hidden afterwards.
views/dossier/* dossier.tsx (the three tabs as the atlas's two-column body since D-0084: Biology's Structure and Biology panels beside the Protein facts tiles and the Record and source panel; Map's map and Regions table beside the Selection rows and the numbering note; the summary section labelled by the subject heading wraps the Biology tab), protein-map.tsx (whole-protein SVG map and the Regions table), evidence-cards.tsx (the Evidence tab: the counts as tiles, the structures panel, the Open Targets drugs table, the diseases panel, Source details, and the three drawers), region-card.tsx (the region card, in the inspector and in the Map tab's Region details row), welcome.tsx (ProteinStart, the start for both layouts), ids.ts.
views/workbench/* workbench.tsx (the settings strip, the two viewer panels, and the Sequence tab's two-column body: the Sequence controls, Sequence tracks and Further analysis panels beside the Features table and the Selected range), entry-picker.tsx (PDB entry popover, a chip in the strip), about-protein.tsx (inspector "About this protein": on the dossier the identifiers, names, isoforms and the PDB entries that map to the record, which the Biology panel does not show; on Compare the facts and function text as well, UI-18); Wave 4: section-disclosure.tsx (a row of the shared DisclosureList whose body loads on open), substitution-disclosure.tsx (SubstitutionDisclosure and AlignDisclosure), substitution.tsx and substitution-loader.ts (the substitution panel and its CSV rows, chunk drybench-island:protein-substitution), align.tsx and align-loader.ts (the alignment panel, its UniProt fetch and its aligned-FASTA and correspondence-CSV writers, chunk drybench-island:protein-align). Each panel is imported only through its loader so the build makes one chunk.
poster.tsx MarkPoster; no longer imported anywhere (see "Known gaps, defects and divergences" below).

Domain modules (src/components/tools/protein-explorer/)

Pure modules (no React, relative imports) are marked "pure"; they are what the Node unit tests and pipeline/protein-explorer/verify.ts import.

File Role
api.ts Loaders: fetch from each service and hand JSON to the parsers. searchProteins, loadEntry, loadStructures, loadChainMapping, loadAlphaFold, loadVariantCount, loadVariants, loadMissense, loadIsoformLength, loadDrugs.
fetch.ts fetchWithTimeout, fetchJson, FetchError, progress readers. Every request goes through here and is counted by host for the status bar (trackRequest).
use-async.ts useAsync (keyed, abortable loader hook with a "slow" flag and retry) and createCache (in-memory LRU).
uniprot.ts (pure) URLs, accession validation, query sanitising, search parsing and ranking, entry parsing (features, curated variants, subcellular location, isoforms, Ensembl gene).
structures.ts (pure) PDBe/RCSB URLs, best_structures and UniPDB parsing, resolution factor and ranking, SIFTS identity re-ranking, SIFTS segments, compareChain, range algebra, coverage depth, selection notes.
alphafold.ts (pure) Selection-only live model choice and source descriptor, historical placement wrapper/parser for compatibility tests, pLDDT bands/runs, AlphaMissense CSV parsing with the reference-letter check (parseMissenseCsv returning MissenseData), maskModelDifferences (DEF-W4-3), missenseTransfer, provider credits.
confidence-fetch.ts Stream-counted 512 KiB confidence body before JSON parsing, explicit non-retryable size refusal and existing fetch cancellation.
residue-map-loader.ts, residue-map-binding.ts, residue-map-protocol.ts, residue-map-client.ts, residue-map.worker.ts Readiness-gated client import, shared input/hash/refusal primitives, closed wire request/response validation, exactly-once cancellable worker lifecycle, F1a-only execution and separately tagged legacy placement; client/worker markers identify their actual optional closures. Evidence imports the binding leaf directly, without the response-validator dependency. The limits (10,000 residues, 10,000,000 cells, 10,020,001 trace bytes, 12 MiB of typed arrays) are named once in src/lib/protein/residue-map-limits.ts for these files; RESIDUE_MAP_POLICY keeps its own values because the engine imports only ./align/*, and residue-map-limits.test.ts pins the two equal (MNT-206).
legacy-model-placement.ts (pure) The unchanged offset/differences algorithm used live only inside that worker for existing AlphaMissense eligibility. Over-product jobs permit cheap branches only.
substitution.ts (pure) readSubstitution (the typed notation read or refused) and checkSubstitution (reference check, then the AlphaMissense row or the reason it is withheld).
align-protocol.ts, align-client.ts, align.worker.ts The alignment worker's messages (AlignRequest, AlignResponse, keyOf, sameKey), runAlignment (a new worker per job), and the worker (drybench-worker:protein-align), which calls alignPair and never fetches.
sequence-placement.ts (pure) gappedAlignmentCompetes: affine-gap check that refuses an offset-only residue map.
variants.ts (pure) EBI Proteins API variation parsing, COSMIC exclusion, clinical classes, per-position summary, variant names, download size estimates and thresholds.
drugs.ts (pure) Open Targets GraphQL documents, drug parsing, stage order, salt grouping, approval basis, market withdrawals, condition query for the pipeline link.
evidence.ts Open Targets target-evidence document and parser; loadEvidence.
mvs.ts (pure) MolViewSpec scene builders and withLocalFile.
molstar-view.tsx Mol* loader, kept viewer slots, scene loading with local file copies, focus, zoom, reset, screenshot.
parse-off-thread.ts, parse.worker.ts Web Worker parsing of the variant JSON and the AlphaMissense CSV (with the model sequence, so the reference check runs there), with a same-thread fallback.
tracks.tsx The workbench's SVG sequence tracks: layout, drawing, zoom, keyboard slider, go-to-residue field.
sequence-extras.tsx Variant and AlphaMissense request controls, class chips, feature list.
residue-card.tsx, entry-tab.tsx, drugs-panel.tsx Inspector bodies for a residue or range, the chosen PDB entry, and one drug.
search-box.tsx Debounced search field with local and live suggestions.
scale.ts (pure) Tick steps, zoom levels, residue under x, greedy row packing.
fasta.ts (pure) UniProt-style FASTA text.
residues.ts (pure) Three-letter codes, range formatting, plurals.
sweep.ts pLDDT "sweep" animation hook; currently always passed a null token (see gaps).
examples.ts The seven example accessions.
copy-button.tsx, ui.tsx Small UI helpers (copy-button.tsx re-exports the suite's shared CopyButton).
types.ts Shared shapes: ProteinEntry, ProteinFeature, Variant, Range and others.

The pairwise aligner (src/lib/protein/align/)

A new engine (D-0055), pure TypeScript importing only its own folder. src/lib/protein/boundary.test.ts forbids imports between src/lib/protein/** and src/lib/seq/** and restricts the aligner to ./ imports, so Plasmid's aligner (src/lib/seq/features/align.ts) and its fixtures are untouched. Details are in Pairwise alignment and Engines.

File Role
blosum62.ts Generated by pipeline/protein-explorer/blosum62.py; do not edit. BLOSUM62_ALPHABET ARNDCQEGHILKMFPSTWYVBZX*, the 24 × 24 table in half bits, BLOSUM62_SHA256 of the stored NCBI file.
policy.ts Modes, doubled costs, CELL_LIMIT, ENGINE_VERSION (protein-align 1.0.0), prepare (refusals before any allocation).
gotoh.ts forward (the dynamic programme with one tie byte per cell) and traceback (one optimal alignment in a fixed order).
correspondence.ts uniquePartners (the residue pairs in every co-optimal alignment) and coOptimalCount.
report.ts report (counts, gapped strings, differences), scoreText, CO_OPTIMAL_CAP, and alignPair, the one entry point the worker calls.

Content and copy

File Holds
content/apps/protein-explorer.ts The registry entry content: capabilities, features, shortcuts, sources and sourceMeta, excluded sources, dataPolicy.endpoints, CSP allowances, cross-links, objects, product page copy.
content/tools/protein-explorer.ts detail: the methods write-up, the source list with credits, the limits (the single copy; the registry imports them) and the "demonstrates" list.
content/apps/protein-explorer.workspace.ts All workspace copy, plus PROTEIN_VIEWS, DOSSIER_SECTIONS and WORKBENCH_SECTIONS.
content/apps/protein-explorer.docs.ts Public docs page.
content/apps/protein-explorer.entry.ts Joins the content with the identity fields (accent viridian, mark fold, group discovery).
content/apps/protein-explorer.loading.ts The loading label, alone so the first chunk stays small.
content/apps/protein-explorer.discovery.ts Wave 4 copy read only by the workspace chunk: missenseCopy (withheld sentences, the positional-mean label, meanN, meanHelp, the DEF-W4-3 sentence), cameraCopy (the two camera notices), and the entry points substitutionEntry and alignEntry (disclosure and export labels). The suite name is passed in (SUITE_NAME), never typed.
content/apps/protein-explorer.substitution.ts substitutionCopy, read only by the substitution chunk: refusals, the result's terms in the CPO's order, the class lines with AlphaMissense's thresholds, the CSO's qualifier and withheld sentences, the export README and the limits.
content/apps/protein-explorer.align.ts alignCopy, read only by the alignment chunk: the CSO's qualification and short-sequence line (verbatim), the summary lines with their denominators, refusals, the co-optimal line, the Differences table, the export README and the limits. src/components/tools/protein-explorer/align-copy.test.ts pins the sentences.
content/apps/changelog/protein-explorer.ts Dated release notes.

See Registry and content for how these are assembled.

Shared modules it depends on

  • src/lib/seq/protein-mass.ts and src/lib/seq/pi.ts: the ProtParam engine (also used by Calculators and Sequence).
  • src/lib/search/proteins.ts, src/lib/search/use-catalogs.ts: the local search catalogue of reviewed human UniProt entries, built by scripts/build-search-catalogs.mjs into public/data/search/ (see Data pipeline).
  • src/lib/search/conditions.ts (canonicalCondition): used when an indication becomes a pipeline condition.
  • src/lib/handoff/params.ts: HANDOFF_CHECKS for name and mine, SEQ_MAX_LENGTH (1,800).
  • src/components/suite/store/camera-param.ts (the cam grammar: checkCamera, encodeCamera, decodeCamera, cameraDistanceOk; pure, written for Protein now and Cell later, never imported by url-state.ts) and camera-orbit.ts (orbit, the keyboard turn).
  • Suite modules: useUrlState, Stage, ProvenanceChip, ObjectActions, WorkCaptureControls, export writers, trackRequest, loadScript, vendor. See Shared shell and Storage and state.

UI structure

The start, Dossier and Compare

The composition follows the redesign atlas of 13 October 2026 (frames 001 to 015; D-0084), applied on 14 October 2026 on top of D-0082.

With no id in the address, views/workspace.tsx renders the start (ProteinStart, views/dossier/welcome.tsx; frame 001), the same for both layouts. It uses the shared DiscoveryStart composition also used by Molecule and Pipeline: a compact card with the one H1 "Find a protein", one-line purpose, a visibly labelled search field, the Search key, the first five example proteins and up to five proteins opened on this device as quiet links, the optional guide and the privacy line. A preview stage fills the right-hand side on wide screens. The loaded search field has the same explicit X button as Molecule; clearing it returns to this start. The [data-discovery-start], [data-discovery-search], [data-discovery-examples] and [data-discovery-privacy] hooks are shared with the discovery smoke spec.

With a protein the workspace renders, top to bottom:

  1. ViewHeader: the shared loaded header (DiscoveryHeader) whose first strip is the compact search (holding the gene or accession on screen; its label is spoken, its key keeps the accent as every app's search key does since the CEO's feedback of 14 October 2026; D-0058 item 4), then the protein name as the page heading (h1, or h2 in the product-page preview), gene, organism, length, accession and entry name, the UniProt provenance chip (its version and the date it was read; the time is in the chip's details, UI-17) and reviewed status. There is no Layout control (removed 14 October 2026, the CEO): the layout follows the tab.
  2. Notes for an isoform request, a secondary accession, or an inbound source label (name).
  3. Section tabs (ViewTabs, id pe-section-tabs), ruled across the content width, with the page's keys at the row's right end (actions, [data-protein-actions]; the CEO, 14 October 2026): in R1 order the split Save to Bench (WorkCaptureControls), then the target's Pin and ⋯ (ObjectActions variant="row", whose menu holds Send to and Add to Study). Where the row is narrower than 48 rem they sit beneath the tabs. They come before the tabs in the page's order and are drawn after them, so Tab from the selected tab still reaches the panel; they are not in the product-page preview.
  4. The view body (#pe-view-body, role="tabpanel"). Each tab is the atlas's two-column body (columnsMainAside: 60/40 from a 58rem main, one column below it, DOM order left then right). The Compare layout's Structures tab opens with the settings strip (role="group" "Comparison settings": the accession and gene as a pill, the PDB entry picker chip with Entry details, and About this protein).

The limits notice is rendered once by the shell below the body (D-0060; above it until the CEO's feedback of 14 October 2026), as in every app: one line led by "Not medical advice.", limit 1 on first open, collapsing to the statement and "Read all N limits" once read or dismissed (UI design review of 11 October 2026, UI-07). A one-line limits footer opens the shell's Limits drawer.

There are two layouts (D-0015), stored in the fragment as view=dossier|workbench. Since 14 October 2026 (the CEO) their sections share one tab row, Biology, Protein map, Evidence, Structures, Sequence tracks (PROTEIN_SECTIONS); a tab in the other layout switches view with it, and focus returns to the selected tab when the body is replaced. Old links with view= keep opening their layout and section. The user-facing name of the workbench is "Compare" (workspaceCopy.views.names.workbench); code, URL and storage still say workbench.

Layout Sections (section=) Content
Dossier (views/dossier/dossier.tsx) biology, map, evidence Biology (frame 002): left, "Structure" (one Mol* stage, the Predicted/Experimental switch in its title row, the pLDDT key over the model, and an entry row beneath naming what is shown, "PDB 4UV7 · X-ray · 2.10 Å" or "AF-P00533-F1 · AlphaFold DB", with Entry details and an Open link to PDBe or AlphaFold DB) and "Biology" (function text, subcellular location, other names, the UniProt source line and Copy FASTA); right, "Protein facts" as tinted ReadoutStrip tiles (molecular weight and theoretical pI from the sequence with the shared ProtParam engine, the model's mean pLDDT, the PDB entries mapped with the best resolution and entry, the PDB coverage, the approved drugs with the total at any stage; a source still answering or failed shows a word with the reason, never a dash, and a source that gives no value for this protein, no AlphaFold model, a non-human or unmatched target, leaves its tile out) and "Record and source" (gene, organism and length; accession, entry name and reviewed status; the UniProt release and retrieval time; Open in UniProt; then PDBe-KB, AlphaFold DB and, for a human entry with an Ensembl gene, Open Targets, each with its identifier, one line and an Open link). Map (frame 003): left, "Sequence architecture" (ProteinMap: architecture, pLDDT, coverage depth, curated variants, sites) and "Regions" (one row per segment and domain: Region as a key with its swatch, pressed while picked, Start, End; every annotated feature in a closed row beneath); right, "Selection" (the pick with Inspect and Clear, then Region details and Residue details as native disclosure rows holding the region card and the residue card, or a line saying what to pick) and "Numbering". Evidence (frame 004): left, "Evidence sources" (Structures, Drugs and Diseases counts as tiles), "Experimental structures" (method shares, coverage, the best ranked entries, View structures) and "Targeted drugs and clinical candidates" (the first eight of the ranked list: compound and type, stage pill, mechanism, Inspect and the row's object actions; the stage group counts as pills above it; View drugs); right, "Disease associations" (the strongest associations, tractability, View diseases) and "Source details" (each source's state with Retry where it failed and its chip once it answered). The View keys open drawers listing up to 120 rows each (frames 009 to 011: PDB entry, Method, Resolution, Coverage; Drug, Highest stage; Disease, Score, Strongest evidence).
Workbench (views/workbench/workbench.tsx) structures, sequence Structures (frame 005): the experimental and predicted viewers, each in its own panel, "Experimental structure" (PDB id, method, resolution and chain beneath; a "Residue-level SIFTS mapping" pill when loaded-file correspondence admits residues, "No usable residue mapping" for a ready map with no admitted pairs, "Mapping the chain" while pending and "Mapping unavailable" for an unavailable source; F-2 derives focus solely from the loaded file and shows qualification separately) and "Predicted structure" (the source model id, version and provider-reported mean pLDDT beneath; a "Source model · pLDDT" pill), with the colour legends over the stage; the entry picker, Entry details and About this protein stay in the settings strip above (the frame draws the picker under the stage; the strip keeps every control in one row and the Reset key is the stage's own). Sequence (frame 006): left, "Sequence controls" (the variant and AlphaMissense requests with their sizes and confirmation, and the class chips once the full list is in), "Sequence tracks" (the numbering note, Tracks with Go to residue in its own toolbar, the AlphaMissense caveat when loaded, not when withheld) and "Further analysis" (the "Check a substitution" and "Compare with another entry" rows, frames 007 and 008, each body an on-demand chunk mounted on first open; neither in the product-page preview); right, "Features" (every annotated feature as a table, Feature, Start, End, in groups by kind, scrolling inside a labelled region) and "Selected range" (the pick, how the chosen entry resolves it, Inspect and Clear, or a line saying what to pick). The atlas's Variants and AlphaMissense toggles are not controls the app has: the loads are explicit keys with their size shown first, kept as they are.

ProteinSectionPanel mounts a section on first visit and keeps it mounted (hidden) afterwards, so viewer state and track controls survive tab changes. The rail is collapsed transiently (shell.rail.setTransient("collapsed")) while the workbench shows a protein.

The inspector

The shell's inspector (inspector: "selection", hidden until something is shown) renders ProteinInspector. What it shows is held in inspectStore as one of:

Kind Body Opened by
pick RegionCard (dossier, when the range equals an architecture region) or ResidueCard A pick on the map, tracks, buttons or feature list (without focus), or Inspect (with focus)
entry EntryTab for the chosen PDB entry Entry details in the workbench caption
drug DrugDetails A drug row or the drugs drawer
about AboutProtein The palette command, or opening the inspector with nothing picked

The workspace republishes its data, pick, view, theme and actions into publishedStore on every render, so the open inspector follows changes without being reopened. Closing the inspector clears inspectStore and returns focus to the element that opened it when focus was inside.

Keyboard

Global shortcuts registered in definition.ts (active only while a protein is shown): v toggle layout, 1 and 2 reveal and focus the experimental or predicted viewer, r reset the focused viewer, , and . previous and next PDB entry in ranked order, g switch to Sequence tracks and focus the go-to-residue field (#residue-go). g, 1 and 2 use flushSync so the target exists in the same keypress.

The tracks are one role="slider" element: Left and Right move one residue (ten with Shift), PageUp and PageDown fifty, Home and End the ends, Enter or Space pick, +/= and - zoom (tracks.tsx onKeyDown). The registry's shortcut list also names + and - (they work only with the tracks focused). Palette commands: show the dossier, compare structures, "Compare with another entry" (protein.compare-entries), "Check a substitution" (protein.check-substitution), About this protein. The two Wave 4 commands switch to the workbench's Sequence section with flushSync and focus the panel's field through a nonce.

In a 3D viewer, Shift plus an arrow key turns the focused viewer (keys 1 and 2 focus it) by 15° about its target (orbit in camera-orbit.ts, Rodrigues' rotation: yaw about the up vector, pitch about the camera's right vector); the stage carries aria-keyshortcuts and the shortcuts overlay lists it. Mol*'s own fly, roll and Shift+arrow bindings (MOLSTAR_KEYS in molstar-view.tsx) are emptied: they act only with the pointer over the canvas, and R clashed with the app's reset key. A turn sets the whole camera at once because Mol* 5.11 left a partial camera with a transition duration unapplied. Mol* applies that camera on its next frame, so on a busy page the next turn or the key-up's report could read the camera before the turn had landed (one turn lost, an unturned camera written to cam; camera.spec.ts:24 under load, 13 October 2026). The key handler keeps the last turn as { from, to }: while the live camera still equals from, the next turn starts from to and key-up reports to; once Mol* has applied it, or anything else moves the camera, the live camera is used.

State in the address

All link state lives in the #s= fragment through the shared useUrlState(proteinSchema); see Storage and state and Security and network for the fragment rule. proteinSchema (url-schema.ts), subject key id:

Key Check Meaning
id checks.uniprot(): UniProt accession regex with optional -<n> isoform suffix The subject. A change pushes a history entry; every other change replaces.
view dossier or workbench Written once a protein is shown, so a copied link names it. Without it the device preference (suite:protein-explorer:view) applies, defaulting to dossier.
section one of the five sections Falls back to the layout's first section when foreign to the layout.
pdb ^[1-9][a-z0-9]{3}$ after lower-casing The chosen PDB entry; absent means the top-ranked entry.
structure predicted or experimental Which structure the dossier's single stage shows; older links with pdb and no structure read as experimental (structureFromState).
sel ^\d{1,6}(-\d{1,6})?$, start ≥ 1, end ≥ start The pick, in UniProt numbering. Native authority whenever supplied. On load the whole interval must lie inside the entry; a request partly or wholly outside it is refused, with the reason where the selection would be, and never clipped (F5, selection-admission.ts).
range The shared hand-off grammar ^[1-9]\d{0,5}-[1-9]\d{0,5}$, start ≤ end The shared residue range (Sequence, Calc, a target object's fields.range; in deepLinkParams and objectInbound.target), read as an alias of sel. Used only when sel is absent and was not dropped as malformed (a dropped sel blocks it). An accepted alias is written back once as sel with a tidy replace; a used-up or ignored alias is removed and, where it conflicted or was blocked, a note says so. Same whole-interval bounds rule. The admission, tidy and notes, with their copy (content/apps/protein-explorer.link-selection.ts), are the selection-links.ts leaf, loaded by dynamic import only when the address carries range, a dropped sel or a sel outside the entry (CTO ruling of 24 October 2026; budget row protein-selection-links); the workspace chunk keeps only the native sel parse and its whole-bounds check. A dropped sel is read from the shared URL store's admission (the current navigation's dropped values, Storage and state), so Back, Forward or a new address with a malformed sel blocks its range too. A blocked or ignored-range note outlives the tidy that removes its cause, and ends with a new subject, a pick, Clear, or a later navigation: Protein's restore callback clears it, and it shows only under the admission token it was set with (25 October 2026).
name HANDOFF_CHECKS.name An inbound source label, shown as a note.
mine HANDOFF_CHECKS.mine A saved reference view to open (see below).
cam checkCamera (camera-param.ts): ten comma-separated numbers px,py,pz,tx,ty,tz,ux,uy,uz,r, each -?\d+(\.\d+)?; up vector of length 0.9 to 1.1 (then normalised); radius above 0. Normalised on read: position, target and radius to 0.1 Å, up vector to 0.001, trailing zeros and -0 removed. fragmentOnly: a cam in a query string is dropped The camera and zoom of the viewer structure names (Wave 4 step 3).
afv checks.integer(1, 99) The AlphaFold DB model version a predicted view's camera was set on; written only with cam.
cmp checks.uniprot() (isoforms kept) The entry aligned against; the alignment is recomputed when the link opens, never stored.
var Shape only, at most 24 characters: ^(?:p\.)?\(?[A-Za-z]{1,3}\d{1,5}[A-Za-z]{1,3}\)?$; kept as typed (the full reading is in the substitution chunk) A substitution to check.

Wave 4 keys are written with replace history. A new protein (search or pick) clears var and cmp; reopening a saved view clears them unless the view holds them.

The camera's binding. cam belongs to the viewer structure names (default predicted) and is bound to pdb (experimental) or afv (predicted). It is applied once, when that structure file is first shown, after Mol* has fitted the scene, and only when the distance from position to target is between max(5 Å, 0.02 × the home distance) and 5 × the home distance (cameraDistanceOk). Another entry, model version or structure, or a camera that does not fit, drops cam and afv with the toast "Ignored the camera angle: this link was made for a different structure or model version." (cameraCopy.cameraIgnored). A viewer reports its camera only when a gesture ends (pointer up after a press on the canvas, 400 ms after the last wheel event, a released Shift+arrow, the zoom and reset buttons) and never re-applies a camera it wrote. A link naming a structure opens on that viewer on a phone too.

Over-long links. fitLinkState (url-schema.ts) leaves keys out of a written link in LINK_DROP_ORDER, cam and afv first, then var, then cmp, never id, until it fits LINK_MAX_LENGTH (2,000 characters). When the camera is what would be dropped, the workspace leaves it out and says once per visit "The camera angle is not in this link: with it the link would be longer than 2,000 characters." (cameraCopy.cameraLeftOut).

Not in the address: requests for the full variant list, AlphaMissense scores and the whole large entry. These are React state keyed by accession (and PDB id for the whole entry) in views/workspace.tsx, so they are lost on reload and are not carried by a link. A substitution that reads (from the field or a var link) requests the AlphaMissense file itself when AlphaFold DB offers one and it is not yet loading (SubstitutionPanel, onRequestMissense).

The share link (definition.share) uses the workspace's own url.href(); the shell may turn it into a /share/protein/<accession> preview address for crawlers (richShareLink). The product-page preview (ProteinMain with preview) keeps its state in memory and never writes the page address.

Device storage and saved reference views

Key Content
suite:protein-explorer:view Last layout chosen on this device.
suite:protein-explorer:recent, or suite:protein-explorer:recent:workspace:<scopeId> Up to eight recent proteins (accession, gene, name, organism, openedAt), validated on read by parseRecent.

A saved reference view is the app's saved work. WorkCaptureControls stores an object of type note whose data is ProteinWorkspaceData (workspace-inputs.ts): { v: 1, app, kind: "reference-view", state, context, reference: { accession, retrievedAt, release } } with schema id protein-reference-view version 1. state is the URL state (without mine); context is a free-text research note of at most 20,000 characters. Opening #s=mine=<ref> loads the record through the object store, validates it with readProteinWorkspace (every key re-checked against proteinSchema, accessions compared without isoform suffix) and applies its state. A saved view reopens with freshly fetched reference data; it does not freeze the sequence or structure records (workspaceCopy.savedViewNote). The same JSON is offered as an export ("research inputs").

Kept edits. Edits to a named task are kept as its draft beside the record, never written into it by the timer or by leaving (D-0068 rule 8; Storage and state). The mine open path reads the record (readSavedView), then the live task that owns it (readOpenWork) and that task's draft (readOpenWorkDraft) through keptOrSaved; a draft that readProteinWorkspace accepts is put on screen instead of the record, and showOpened sets appliedDraft in the same update as the view's state and note (a refused draft is offered as unshown and stays in its row). An address alone (a bookmark, the object page) opens the owning task with the view, so the offer shows there too. reloadOpenView serves onDraftResolved: Discard puts the saved view back, its Undo re-reads the draft; both check that the open view and the account scope are unchanged before they set anything. Two things keep an unchanged view from counting as an edit: the opened view's reference (accession, retrievedAt, release) stays in the inputs while the entry on screen has the same accession and UniProt release, so a refetch with a new arrival time is not a change; and once a saved view's entry is in and its structure list has settled (it names the chosen PDB entry), the view bumps savedOnScreen so the control takes those inputs as the task's baseline. Save is disabled until the structure list has settled, so a save never captures a view whose late data then reads as an edit (REL-41 c). Protein has no autosave of its own.

Data flow

Order of requests

useProtein (use-protein.ts) drives every request from the address. For an accession:

  1. loadEntry (UniProt). Everything else waits for a parsed active entry.
  2. In parallel, keyed on the entry's accession: loadStructures (PDBe), loadAlphaFold (AlphaFold DB), loadDrugs (Open Targets), loadEvidence (Open Targets; human entries with an Ensembl gene id only), loadIsoformLength (only when the address names an isoform other than -1) and loadVariantCount (human entries only).
  3. When the structure list is ranked: loadChainMapping for the chosen entry and chain.
  4. On request only: loadVariants (full variant list) and loadMissense (needs a placed AlphaFold model; the model's own sequence travels with the request for the reference check).
  5. When a viewer comes within 400 px of the screen: Mol* itself, then the structure file (see "3D viewers (Mol*)" below).
  6. When a substitution is checked: UniProt's sequence version for the entry. When an alignment runs: both sequences with their version and checksums. Both are fetched by their panels' chunks, not by useProtein.

The search box makes its own requests (below). Non-human entries skip the variant count, the evidence document and the drugs query (loadDrugs returns { kind: "not-human" } without a request).

Endpoints and request shapes

All hosts are declared in dataPolicy.endpoints in content/apps/protein-explorer.ts, which generates the CSP (see Security and network).

Loader Request Timeout, retries Parser
searchProteins Up to four GET rest.uniprot.org/uniprotkb/search?fields=accession,id,protein_name,gene_primary,organism_name,organism_id,reviewed,length&format=json&size=…&query=…: accession:<acc> (size 1, accession-shaped text only), (gene_exact:<q>) AND (reviewed:true) AND (organism_id:9606) (size 2, single words only; failure ignored), (<q>) AND (reviewed:true) AND (organism_id:9606) (size 6), (<q>) (size 10) 12 s each parseSearch, rankSuggestions
loadEntry GET rest.uniprot.org/uniprotkb/<acc>.json?fields=<ENTRY_FIELDS>; reads the x-uniprot-release response header 15 s parseEntry; 404 means "missing", any other error is retryable
loadIsoformLength GET rest.uniprot.org/uniprotkb/<acc-n>.json?fields=accession,length 12 s; 404 or 400 means unknown inline
loadStructures GET www.ebi.ac.uk/pdbe/api/mappings/best_structures/<acc> and GET www.ebi.ac.uk/pdbe/graph-api/uniprot/unipdb/<acc> in parallel; then identity rounds GET …/pdbe/api/mappings/uniprot/<pdb> 20 s; 20 s (failure tolerated); identity 8 s, no retry, failure counts as unchecked parseBestStructures, parseUniPdb, buildStructureOptions, parseChainIdentity, applyIdentities
loadChainMapping GET …/pdbe/api/mappings/uniprot_segments/<pdb>, GET data.rcsb.org/rest/v1/core/entry/<pdb> (for deposited_atom_count), GET …/pdbe/api/pdb/entry/molecules/<pdb>; when the chain differs from UniProt, GET …/pdbe/api/mappings/isoforms/<pdb> 15 s; 8 s no retry (failure tolerated); 10 s no retry; 8 s no retry parseSegments, parseEntitySequences, compareChain, parseIsoformMapping
loadAlphaFold GET alphafold.ebi.ac.uk/api/prediction/<acc>, then the confidence JSON (plddtDocUrl from the response) and GET alphafold.ebi.ac.uk/api/providers/<providerId> 15 s; 15 s; 10 s; 404 means "no model"; confidence body capped at 512 KiB before JSON parsing selection-only selectAlphaFoldModel, listIsoformModels, sourceDomain, parseSourceConfidence, bandRuns, parseProvider
loadVariantCount HEAD www.ebi.ac.uk/proteins/api/variation/<acc> with Accept: application/json; reads x-feature-records 12 s; 404 means zero inline
loadVariants GET of the same URL, read in chunks with progress min(300 s, 60 s + records × 15 ms) (5,000 records assumed when unknown) parseVariation in the worker
loadMissense GET of amAnnotationsUrl from the prediction response (CSV) 30 s parseMissenseCsv(csv, length, offset, modelSequence) in the worker
Substitution panel (substitution.tsx) GET rest.uniprot.org/uniprotkb/<acc>.json?fields=sequence_version, once per accession; the entry request is unchanged, so its recorded fixtures stand 12 s; a failure gives "(version not available)", never a number inline (entryAudit.sequenceVersion)
Alignment panel (align.tsx) GET rest.uniprot.org/uniprotkb/<acc>.json?fields=accession,sequence,sequence_version for entry B only, afresh for every job (no cache; aborted when the job ends); A is the entry on screen, whose MD5, CRC64 and sequence version are written "unknown" in the exports (the entry Protein holds does not carry them; a second fetch could describe other bytes) (F4) 20 s; a failure shows "could not be fetched" with retry, which refetches inline (sequence.value, md5, crc64, entryAudit.sequenceVersion)
loadDrugs POST api.platform.opentargets.org/api/v4/graphql with DRUGS_QUERY and { id: <ENSG> }; if the entry has no Ensembl id, first SEARCH_TARGET_QUERY with { q: <accession> } 20 s each; a GraphQL errors[] becomes an error parseDrugs
loadEvidence POST the same endpoint with EVIDENCE_QUERY 20 s; errors[] is not checked parseEvidence
Structure files (molstar-view.tsx) GET www.ebi.ac.uk/pdbe/entry-files/download/<pdb>.bcif, fallback GET models.rcsb.org/<pdb>.bcif; or GET www.ebi.ac.uk/pdbe/model-server/v1/<pdb>/atoms?auth_asym_id=<chain>&encoding=bcif for a large entry (no fallback); AlphaFold model from bcifUrl, fallback cifUrl 60 s, one retry on a dropped connection Mol*

ENTRY_FIELDS requests: accession, id, protein_name, gene_names, organism_name, organism_id, length, sequence, cc_function, cc_subcellular_location, cc_alternative_products, ft_signal, ft_transit, ft_transmem, ft_intramem, ft_topo_dom, ft_chain, ft_peptide, ft_propep, ft_init_met, ft_domain, ft_dna_bind, ft_zn_fing, ft_binding, ft_act_site, ft_mod_res, ft_variant, xref_ensembl, xref_opentargets, reviewed.

DRUGS_QUERY reads meta.dataVersion { year month } and target(ensemblId) { id approvedSymbol drugAndClinicalCandidates { count rows { maxClinicalStage drug { id name drugType maximumClinicalStage parentMolecule drugWarnings mechanismsOfAction } diseases clinicalReports } } }. EVIDENCE_QUERY reads tractability, geneticConstraint, associatedDiseases(page: { index: 0, size: 50 }) and safetyLiabilities. The schema was checked against Open Targets 26.09 (comment in drugs.ts; pipeline/protein-explorer/README.md).

Timeouts, retries and cancellation

fetchWithTimeout (fetch.ts) applies one timer to headers and body together, links the caller's AbortSignal, and classifies failures as http, timeout, network or parse (FetchError). Only network failures (a dropped connection) are retried, by default twice, after 700 ms and 1,400 ms; HTTP errors, including 429 and 503, and timeouts are not retried. There is no rate limiter, backoff or circuit breaker in this app. Every request increments the host's in-flight count for the status bar's network chip (trackRequest).

useAsync (use-async.ts) runs a loader when its key changes, aborts the previous run, marks the state slow after 6 s, and exposes retry(), which always goes to the network.

Caching

Only in memory, for the life of the page. createCache(max) is an LRU keyed by string, consulted once when a key first appears (a later render never swaps data on screen for a new download). Capacities in use-protein.ts: entry 12, structures 8, drugs 8, evidence 8, chain mapping 12, variant count 12, variant lists 3, AlphaMissense 3 (MissenseData, keyed by URL and offset), isoform length 12; search suggestions 64 (search-box.tsx). AlphaFold source results and derived correspondence/projection have no application cache. Their semantic keys include the actual current sequence; useAttemptKey adds a fresh owner on each transition, including A→B→A, so a previous ready value cannot flash during an effect gap. Retries also start a new mapping generation. The substitution panel's sequence versions and the alignment panel's sequences are module-level Maps of promises with no cap, a failed fetch removed so a retry goes to the network. There is no IndexedDB or localStorage cache of API data and no expiry. The browser's HTTP cache applies to GET requests as each provider's headers allow; the Open Targets POSTs are never cached by the browser.

Error and empty states

  • The entry: missing (404) and inactive (UniProt entryType: "Inactive", with mergeDemergeTo replacements offered as buttons) render EmptyState; other failures render ErrorBox with retry. An invalid accession typed in the search shows "Not a UniProt accession" with example chips.
  • Each panel handles its own loader's failure; the rest of the page keeps working. One live-region sentence summarises the protein shown and every failed source (workspaceCopy.states.failures).
  • Partial AlphaFold results distinguish missing confidence URL, transport failure with source Retry, malformed data, a valid empty file, an unestablished source domain, mapping/refusal and operational worker failure with mapping Retry. Source-model colours remain available when source confidence is valid but correspondence fails; correspondence and exact focus remain available when confidence alone fails. A zero-partner result is stated only after a settled mapped result.
  • A failed chain mapping leaves the experimental scene with only the auth chain colour and no residue focus (mappingFailed copy).
  • Search: if UniProt fails but local catalogue matches exist, they are offered; an accession-shaped query can still be opened directly.

Off-main-thread parsing

parse-off-thread.ts keeps one module Worker (parse.worker.ts) for the page. The variant JSON and the AlphaMissense CSV are transferred as ArrayBuffers, decoded with TextDecoder and parsed there by the same pure parsers the tests run; the MissenseData result's typed arrays come back transferred (missenseTransfer).

The aligner runs in its own worker, one job per new Worker (align-client.ts runAlignment): every ending (a result, a refusal, a worker error or unreadable message, the 60 s limit ALIGN_TIMEOUT_MS, cancel, or a worker that cannot be made or posted to) goes through one settlement, once, which terminates the worker and frees its tables (F4). A request carries both accessions, sequence versions, MD5s, lengths and the region; the protocol (admitRegion) and so the worker refuse a region that is not wholly inside its own sequence, never clipping it; the response echoes the key (keyOf), and the client discards a result whose key differs. The panel keys each job on the displayed entry object, accession, partner, region and attempt and cancels on a key change, clearing the previous report's export before paint, which is the guard that matters (the CTO's review notes that sameKey compares the request with itself). There is no main-thread fallback. Without Worker (Node, or a refused worker) the parsers run on the calling thread, so pipeline/protein-explorer/verify.ts and the tests share the code path. A worker error terminates it and fails pending requests; the next request creates a new one.

Provenance stamps

provenance.ts keeps a module-level map of the first arrival time per <source>:<key> (stamp, retrievedAt). Panels build ProvenanceChips from these stamps plus the release each provider reports: UniProt's x-uniprot-release header, AlphaFold DB's latestVersion (shown as v<n>), Open Targets' meta.dataVersion (<year>.<month>). sourcesStore feeds the status bar's freshness chip. PDBe, RCSB and the Proteins API carry no version.

Science and methods

This section states each rule and constant exactly as implemented. Where a rule is a browsing heuristic rather than a scientific method, it says so. See Science overview for the suite-wide position.

Search ranking

sanitiseQuery keeps letters, digits, spaces, ., ' and -, strips leading hyphens, caps the text at 80 characters and lower-cases AND, OR, NOT. Fewer than two characters makes no request. rankSuggestions puts the accession hit, the exact human gene-symbol hits and reviewed human hits first in UniProt's order, then the general results ordered reviewed human, reviewed other, unreviewed, deduplicated by accession, at most ten. The search box merges these after the local catalogue's matches (mergeProteinSuggestions). Enter on accession-shaped text opens that accession unless the top suggestion is a human gene of exactly that name (P2RY12, B3GAT1).

The server route /api/search/proteins (see Server and accounts) is not used by this app's search box; the literature search and the objects palette use it.

UniProt entry

parseEntry (uniprot.ts): the name is the recommended full name, else the first submission name. Function texts have (PubMed:…) groups removed and a full stop added; the molecule field (an isoform qualifier) is parsed but not displayed. Features are mapped by type into kinds topology (Signal, Transit peptide, Transmembrane, Intramembrane, Topological domain), chain (Chain, Peptide, Propeptide, Initiator methionine), domain (Domain, DNA binding, Zinc finger), binding, active and modified; other feature types are ignored. Curated natural variants are parsed from Natural variant features with an empty alternative sequence meaning deletion; they carry clinical: "none" because UniProt's feature has no clinical class. The Ensembl gene id comes from the OpenTargets cross-reference, else the first Ensembl GeneId property without its version, kept only if it matches ^ENSG\d+$.

Isoforms are not supported. normaliseAccession drops a -n suffix, the canonical entry is shown, and a note names the requested isoform and its UniProt length (or says UniProt has no such isoform). A secondary accession resolves through UniProt's redirect and a note gives the current primary accession.

Sequence tracks and requests use current UniProt numbering from the first residue of the precursor (initiator Met and signal peptide included). The predicted model, its colours and actual applied viewer targets retain source-model coordinates; their correspondence is explicit.

Default PDB entry

buildStructureOptions (structures.ts) makes one option per PDB entry from PDBe best_structures rows. The chain is UniPDB's observed chain with the most observed residues among the chains PDBe lists for that entry, else PDBe's first row. UniPDB observed ranges are residues with observed === "Y" and indexType === "UNIPROT", merged.

extent = observedCount                      if UniPDB observed ranges are known
       = unp_end − unp_start + 1            otherwise (the mapped construct)

f(r, method) = 1                            r ≤ 2 Å
             = 1 − 0.25 (r − 2)             2 < r ≤ 4 Å   (0.5 at 4 Å)
             = 0.4                          r > 4 Å
             = 0.5                          no resolution and method matches /NMR/i
             = 0.4                          no resolution otherwise

score = extent × f(r, method)

A resolution of 0 or less is treated as absent. Options sort by score descending, then resolution ascending (absent counts as 99), then PDBe's order. The factor drops from 0.5 to 0.4 just above 4 Å, a step in an otherwise continuous curve.

SIFTS identity re-ranking (loadStructures, applyIdentities): the identity of each leading entry's chain is the minimum identity over its segments in /mappings/uniprot/<pdb>. An entry with identity ≤ MIN_IDENTITY (0.95) has its score halved. Checking runs in at most five rounds of up to four unchecked entries, stopping once the top three are checked; a failed check leaves the entry's score unchanged. After re-ranking, ties keep the previous order.

This ranking is a browsing heuristic (the October scientific audit, docs/science/2026-10-scientific-audit.md, Protein section, says so). It can pick a truncated construct, an engineered mutant or an entry without the ligand of interest; limit 3 in content/tools/protein-explorer.ts says this to users. Ranking uses the UniPDB extent before the chosen chain's sequence check; the check (below) reduces the chosen entry's resolved residues but does not re-rank.

API segment placement for coverage and historical receipts

parseSegments reads SIFTS uniprot_segments for the chosen auth chain: each segment pairs a UniProt range with a label_seq_id range on a struct_asym_id. mapToLabel is retained for historical reference tests; it maps a UniProt range through segments with a constant offset per segment. It has no live focus caller after F-2.

compareChain checks the chain's own sequence (from /pdb/entry/molecules, with (XXX) modified residues read as X) against the UniProt sequence through those segments:

  • A segment whose UniProt and label lengths differ by d (0 < |d| ≤ 20, UniProt length ≥ 2) holds a gap. It is split at the point k that maximises matches on the prefix at the start offset plus matches on the suffix at the end offset; the |d| UniProt residues left without a partner (when the chain is shorter) are unaligned.
  • A residue matches when the residues are equal, either is missing, or the chain has X.
  • Differences are grouped: a gap of more than four positions between consecutive differences starts a new run. Runs of four or more differences become differing ranges; shorter runs are single points (engineered mutations).
  • If more than half the compared residues differ, nothing is claimed (check: null) and the segments are used unchanged.
  • When differences exist, /mappings/isoforms/<pdb> names the UniProt isoform with the highest identity for that chain, if it is not the canonical one.

withSequenceCheck subtracts differing and unaligned ranges from the chosen entry's observed residues. The historical selectionNote helper describes segment-era picks as resolved, partly resolved, differing or unaligned; it no longer supplies live experimental selection text. The whole check runs only when all segments are on one label_asym_id.

Coverage depth

coverageDepth counts, for each residue 1..length, how many options' observed ranges (or mapped ranges when observed is unknown) include it, saturating at 65,535 (Uint16Array). The chosen entry contributes its sequence-checked ranges. "PDB coverage" is the share of residues with depth above zero, rounded to whole percent. The dossier map draws depth with a square-root height scale per pixel column (maximum depth within the column). "Best" in the headline facts is the entry with the lowest resolution value, not the default entry.

AlphaFold model choice and cross-release placement

The live selectAlphaFoldModel keeps prediction records whose uniprotAccession equals the selected canonical accession exactly and that have a bcifUrl or cifUrl, prefers one whose sequence equals the current UniProt sequence, then the highest latestVersion. It performs no sequence placement. File URLs are taken from the response and bound to the selected model ID/version; a conflicting CIF fallback is omitted. Isoform models (<acc>-n) are listed separately. pickAlphaFoldModel remains only as a historical pure-test wrapper; the import/call boundary test prohibits live callers.

AlphaFold DB models are built for the UniProt sequence of their release. For the existing AlphaMissense path only, legacy-model-placement.ts::placeModelSequence decides whether model residue i can be read as UniProt residue i + offset. The dedicated mapping worker runs this compatibility calculation separately from the new correspondence result:

  1. Identical sequences: offset 0.
  2. Model is a substring of UniProt (UniProt added residues): offset = its index, only if the substring occurs once.
  3. UniProt is a substring of the model (UniProt removed residues): offset = −index, only if unique.
  4. Different lengths otherwise: no placement (null).
  5. Equal lengths: refuse if gappedAlignmentCompetes (below); else accept the best ungapped diagonal with at most 5 % mismatches over an overlap of at least max(16, ⌊0.9 × shorter length⌋), trying offset 0 first.

gappedAlignmentCompetes (sequence-placement.ts) is a global affine-gap alignment with match +2, mismatch −3, gap open −5, gap extension −1. If the best path containing a gap scores at least as high as the ungapped diagonal, the offset map is refused. Sequences of unequal length or longer than 10,000 residues are always refused (quadratic cost). These scores are app policies (docs/science/2026-10-scientific-audit-implementation.md), introduced after audit finding B5 (an internal insertion could be accepted as substitutions). The 10 October audit notes that in practice the non-zero offset search in step 5 is unreachable; placement is more conservative than its docstring.

With no model sequence, no legacy placement is attempted. Above a 10-million length product the worker permits only the unchanged cheap equality/unique-containment branches; a failed cheap branch returns legacy-unavailable/resources without running the quadratic check. Above 10,000 code units on either side no worker is dispatched. No legacy placement or refusal is a fallback for exact confidence transfer or predicted focus, and the new map does not broaden AlphaMissense eligibility.

Gap-aware correspondence reference (Stage F)

The live F-1b worker consumes the fixed F-1a reference. Method protein.residue-map@1.0.0, policy standard-invariant-free-ends-v1, is specified in pipeline/protein-explorer/residue-map/method-v1.md. Its independent Biopython 1.88 oracle intersects every co-optimal global BLOSUM62 alignment with free terminal gaps and gap cost 10 + 0.5(k − 1). Only invariant, identical standard amino acids qualify for exact transfer; uniquely paired standard substitutions are differs, while B/Z/X and positions without an invariant partner are unmapped. A zero partner does not identify a gap versus competing placements. Separate transfer arrays use null for every non-exact position. Every stop, including a terminal stop, and non-ASCII input is refused without changing coordinates.

The write-once fixture pipeline/protein-explorer/fixtures/residue-map-v2.json and frozen oracle are published under /verification/protein-explorer/0.5.1/ before their implementation. They cover indels in both directions, compensating indels, terminal changes, the historical 100/101 insertion after 95, ambiguous letters, adverse adjacent gaps, refusals and missing sequences, plus a completely enumerated 4,096-optimum example. This reference publication preceded its runtime consumer. The exact map now supplies pLDDT and predicted focus; AlphaMissense and experimental SIFTS policy remain separate. See Oracles and verification. The initial v1/0.5.0 candidate stays frozen. Its publication entrypoint is withdrawn in favour of v2's version-path validation and complete recomputation.

src/lib/protein/residue-map.ts::buildResidueMap implements the pure foundation. Its two inputs contain actual raw sequence text (or null for unavailable) and optional provenance fields; a successful result retains that text, its ASCII-uppercase form and a snapshot of supplied metadata. Metadata is not validated as identity or used as a stale-result token. Outcomes distinguish unavailable, typed refused, and a completed mapped result. Forward/reverse raw Int32Array partners have index zero set to 0; state and exact-transfer arrays use null at index zero. The method/policy, normalised lengths and conservative allocation estimate accompany the map.

The module calls D1 prepare, forward in explicit global mode and uniquePartners, without a traceback, report or capped alignment count. Before those allocations it bounds each input at 10,000 raw code units, the length product at 10,000,000, padded trace at 10,020,001 bytes and a conservative typed-array sum at 12 MiB. That sum includes encodings, trace, all forward/correspondence rolling rows and forward/reverse partner arrays together. It excludes JS strings/arrays/objects and runtime overhead and is not a measured peak-memory claim. Every stop is refused; invalid-input offsets are raw UTF-16 string offsets, not biological residue numbers. The import test in residue-map.test.ts permits a single runtime importer, residue-map.worker.ts, and ignores type-only imports. No page can execute the dynamic programme. residue-map-client.ts owns a new worker per job and propagates AbortSignal through hashing and execution. Every success, cancellation, timeout, bad message, worker/message error, posting error or constructor error settles once and terminates the owned worker. The worker's own refusal ({ error }: an invalid request, a binding mismatch or an execution failure) settles as a worker failure, not a bad message (REL-541). The watchdog is 10 seconds including hashing; the performance target remains a quiet-host median below 2 seconds. There is no main-thread fallback.

The hook allocates an attempt generation before residue-map-loader.ts imports the marked client, only after source/current inputs are available. One 10-second deadline includes import, hashing and worker work. Abort or timeout settles even while import is held; late completion cannot start hashing or construct a worker. A linked controller cancels imported work and the client receives only the remaining deadline. Import failure follows the existing mapping error/Retry path. The source parser, model colours and linear exact consumers do not wait for this optional client module. Its actual closure is budgeted separately without removing any response or receipt validator.

Closed bounded requests carry raw source/current sequences, selected canonical/requested identity, source asset descriptor, parser and mapping policy, SHA-256 of both actual sequence texts and canonical consumed confidence arrays, plus a SHA-256 binding key. Worker-side recomputation and response binding/generation checks prevent stale or different-subject use. Full policy, allocation, array dimensions, reciprocal monotone partners, exact letters and legacy coordinate bounds are checked before accepting a reply. Integration byte accounting is separate from the foundation's typed-array sum and is not a whole-process memory measurement.

The integration counters describe UTF-8 sequence digest inputs, the retained source Float64 scores and potential current Float64 score capacity (8 × (length + 1) each, at most 80,008 bytes per array). The canonical confidence digest input has its own measured encoded 512 KiB cap before hashing. Descriptor/binding serialisation is bounded by the closed primitive field lengths; request structured cloning duplicates those bounded sequences and confidence arrays. Partner buffers return by transfer, conservatively at most 2 × 10,001 × 4 bytes. JS strings/objects/array slots, crypto internals, incoming network chunks and clone/runtime overhead remain outside these counters. A refused map need not allocate the potential projection array. There is no projection cache retaining additional copies.

F2 independent experimental source reference

The fixture-first F2 reference is now pinned in pipeline/protein-explorer/experimental-map/method-v1.md and published at /verification/protein-explorer/0.7.0/. Its Python3.12/Biopython1.88 oracle uses retained text-CIF/XML and checks consumed fields against the actual BCIF input hashes. It covers exact observed correspondence, explicitly qualified deposited differences, unobserved/conflicting rows, author insertions/nulls and strict experimental-v3 inner receipts. The real3IKA T790→MET/labelA99 and1PPB Y410→labelB47/authH60A anchors are distinct from synthetic derivatives and application claims. All reproducibility inputs, unresolved source terms and publisher overwrite/path/hash guards are retained.

The readable republication (MNT-206, 25 October 2026) is pipeline/protein-explorer/experimental-map/readable/, published at /verification/protein-explorer/0.10.0/: the same tools one statement to a line, with the mapping-admission limits named once in limits-v1.json. Its publish_v1.py --check reproduces 0.7.0's three outputs byte for byte on CPython 3.12.3 and 3.13.17 with Biopython 1.88. 0.10.0 names 0.7.0's outputs and inputs by path and SHA-256 rather than copying them. The runtime's limits live in src/lib/protein/experimental-map-limits.ts (read by experimental-frame.ts), and experimental-map-limits.test.ts pins them to limits-v1.json, to the 32 MiB coordinate limit and to the 10,000-residue sequence limit. The BCIF inventory decodes with Mol* 5.11.0, the runtime's own decoder, so it cross-checks columns only; the expectations come from the Python text-CIF and XML extraction. The F2 live consumer now uses these frozen facts for experimental focus and new receipts; historical and predicted readers retain their original meanings.

pLDDT

F-1b's fixture-first reference is specified in pipeline/protein-explorer/confidence/method-v1.md and independently computed by confidence/reference_v1.py with Biopython 1.88. The new confidence-v1.json, published under /verification/protein-explorer/0.6.0/, extends F-1a correspondence to strict source-coordinate confidence, exact-only projection, contributing-count means, split source targets and the retained evidence version matrix. It also checks all 1,210 positions of the recorded EGFR full-model coordinate/confidence assets. Unverified fragment domains are withheld. Runtime agreement is checked by confidence.test.ts, source admission and worker lifecycle tests; browser rendering and performance evidence remain separate.

parseSourceConfidence reads source-coordinate residueNumber and confidenceScore arrays. Both must exist, be arrays and have equal lengths; every index must be an integer in 1..actual source length, every score a finite number in [0,100], and duplicate indices invalidate the whole file even if scores agree. Sparse unordered rows and empty arrays are valid; omissions stay NaN, never zero. Float64 preserves precision at band edges. Supplied JSON null or other malformed shapes are invalid, distinct from transport failure and missing URL. confidence-fetch.ts stream-counts the decompressed body and cancels before retaining an overflow chunk beyond 512 KiB; it never retries that refusal. Retained/assembled buffers are bounded separately from incoming network chunks and parsed-object overhead. Bands come from the score, never from category letters:

Band Rule Colour (AlphaFold DB key)
Very high score ≥ 90 #0053D6
Confident 70 ≤ score < 90 #65CBF3
Low 50 ≤ score < 70 #FFDB13
Very low score < 50 #FF7D45

The 3D model is coloured by source-coordinate band runs independently of correspondence. The source descriptor requires its own sequence, full-model start/end semantics, and model/confidence asset identity agreement with the selected canonical accession and version. Supplied malformed bounds, conflicting domains, and unverified fragment domains withhold colours and projection. The representative recorded EGFR check establishes the provider full-model contract; it is not per-loaded-file structural sequence verification. Actual loaded coordinate bytes have a separate capture digest.

projectConfidence transfers a value only along a unique exact standard-residue pair. Tracks, dossier maps, cards, substitution pLDDT and range means use this current-UniProt array; substitutions and ambiguous/non-unique positions remain holes. A range mean includes only finite transferred values and states contributing n over selected N. A range with no transferable value reads one sentence chosen by rangeConfidenceState (confidence-feedback.ts, in confidenceFeedback's order) through rangeReadout in content/apps/protein-explorer.confidence.ts: "0 of N" only once the mapping has settled, "pending" while confidence loads or residues map, "unavailable" with the reason otherwise; the region tile's value is the no-transfer line, "Pending" or "Not available". The whole-protein coverage appears only in the confidence notice (CSO ruling of 24 October 2026, P-06). The provider's finite [0,100] globalMetricValue, including zero, is labelled provider-reported model mean; it has no invented residue denominator. Provider band fractions are accepted only in [0,1]. Map/track SVG descriptions and figure provenance carry the exact-transfer qualification; 3D exports describe source-model coordinates separately. The cropper receives a confidence profile only when every current residue has a finite exact-transferred score; otherwise confidence-based suggestions are withheld with a reason. PAE remains a separate uploaded/source-image view described below.

AlphaMissense

parseMissenseCsv(csv, length, offset, modelSequence) (alphafold.ts) reads protein_variant,am_pathogenicity,am_class lines and accepts variants matching ^([A-Z])(\d+)([A-Z])$ with a finite score. Positions in the file are the model's numbering.

Reference-letter check (DEF-W4-2, CSO pre-rulings ruling 3, zero tolerance). Every row's reference letter is compared with the model residue at its position (modelSequence[position − 1], the AlphaFold DB sequence kept on AlphaFoldModel.sequence). One mismatch anywhere, a row beyond the model, or no model sequence withholds AlphaMissense for the whole entry: withheld is { reason: "mismatch", count, first } (the first in file order) or { reason: "no-model-sequence" }, with no means and no rows. The track then draws nothing, and the controls say why (missenseCopy.withheldMismatch, withheldNoSequence). The Python oracle found one such entry among its five: POLR2A (P24928, AF-P24928-F1 v6), whose file names T at 1835 and H at 1837 where the model has A and P (38 rows).

Otherwise the result (MissenseData) keeps every substitution row in compact typed arrays (MissenseRows: model position, reference and alternative as char codes, the score as a number with its decimal count so score.toFixed(decimals) is the file's own text, and the class index into MISSENSE_CLASSES LBen, Amb, LPath, 255 for anything else), and per residue in UniProt numbering (model position + offset; rows that land outside 1..length are left out of the means):

mean_AM(i) = (1 / n_i) × Σ am_pathogenicity over the n_i rows at residue i      (accumulated in Float64, stored Float32)

counts[i] is n_i; positions with no rows are NaN. Duplicate rows are not removed. The file does not always hold 19 rows per position.

Model differences (DEF-W4-3, CSO merge review C4). Where the placed model's residue differs from the UniProt residue (model.differences, possible on the ≥95 % diagonal placement), the mean is the model residue's, not this sequence's. maskModelDifferences sets those residues to NaN for the track, and the residue card shows "No mean here: the AlphaFold model's residue differs from this sequence at this position." instead of a value.

Display. The residue card labels the value "{SUITE_NAME} mean of {n} AlphaMissense scores at this residue", shows it to 2 dp with "n = {n}", and adds the CPO's help text (missenseCopy.meanHelp: calculated here, the arithmetic mean of the n substitution scores the file holds, "{n} of 19 possible substitutions", not a score for any one substitution and not an AlphaMissense output, read against the source's thresholds 0.34 and 0.564). The track colours each residue by round(mean × 20) into 21 steps on a diverging ramp (rgb 59,111,182 at 0; 226,226,226 at 0.5; 194,65,12 at 1). workspaceCopy.missense.ready now reads "On the track: at each residue, the mean of the AlphaMissense substitution scores the file holds there; the residue card gives how many."; the caveat (workspaceCopy.missense.note, audit item C5) is shown only when the entry is not withheld. The download size shown before loading is length × 19 × 20 bytes. The scores need a placed model.

Checking a substitution

substitution.ts (pure; Wave 4 step 7; CPO Wave 4 §2.4; CSO Wave 4 memo D2 and pre-rulings ruling 3).

readSubstitution(text) accepts one amino-acid substitution in five notations, V600E, p.V600E, Val600Glu, p.Val600Glu and p.(Val600Glu) (one-letter or capitalised three-letter codes for the 20 standard residues), and refuses with a reason: nucleotide (c., g., n., m., r.), frameshift (fs), indel (del, ins, dup or a range with _), nonsense (*, Ter, X), synonymous (= or the same residue) and unreadable. The normalised form is {ref}{position}{alt} in UniProt numbering.

checkSubstitution(input, context) then applies, in order:

  1. Position beyond the sequence: out-of-range. Reference letter differs from the current UniProt residue: reference-mismatch, and nothing further is shown ("Check the isoform or numbering").
  2. The score, withheld with a reason unless every condition holds: an AlphaFold DB model exists (no-model); the accession is not an isoform (isoform: AlphaMissense covers the canonical sequence only); the model is placed (model.offset !== null, else no-placement) with zero differing residues (differences, with the count); AlphaFold DB lists an AlphaMissense file (no-file); the file passed DEF-W4-2 (file); and a row exists at model position n − offset for this alternative (no-row). The row's reference letter must equal both the model residue and the UniProt residue (asserted, ruling 3 amendment 2, not inferred from the placement).
  3. The score is the file's text to the file's digits (rows.score.toFixed(rows.decimals); four fixture rows read "1.0"), with AlphaMissense's own class.

The positional mean is returned apart (mean: { value, n }) and is kept only with a score or a no-row result. The panel ("Check a substitution", views/workbench/substitution.tsx) shows a definition list in the CPO's order: as typed; read as; the reference residue with UniProt's sequence version (fetched when a check runs; "(version not available)" if it cannot be read); known variants at the residue (from the variants already loaded; the panel has no load button of its own); "AlphaMissense prediction for {ref}{n}{alt}" with "AlphaMissense score (0 to 1): {value}" and the class line ("likely benign (score below 0.34)", "ambiguous (0.34 to 0.564)", "likely pathogenic (score above 0.564)") and the CSO's qualifier; the positional mean apart, with its label, n and help text; structure context in the chosen PDB entry and the model's pLDDT; "Show in the structures", which writes sel. The six withheld sentences are the CSO's ruling 3 verbatim; two more ("no AlphaMissense file", "no model sequence") were written for the CSO's signature. One announcement per check; the limits are always shown.

Pairwise alignment (Compare with another entry)

The engine is src/lib/protein/align/ (D-0055; CSO Wave 4 memo D1; CTO Wave 4 §2.2), signed off by the CSO and the CTO on 11 October 2026, the CSO's sign-off effective once conditions C1 (the EMBOSS Needle cross-checks) and C2 (the adjacent-gap fixture) were in the tree (docs/progress/wave4-discovery.md, "Merge review conditions").

Scoring. BLOSUM62 (NCBI's matrix file; half-bit units) with affine gaps, all doubled to integers so ties are exact (policy.ts):

s2(x, y)  = 2 × BLOSUM62(x, y)
gap2(k)   = 20 + (k − 1) × 1          (GAP_OPEN2 = 20, GAP_EXTEND2 = 1)
          = 2 × (10 + 0.5 (k − 1))    a gap of k residues costs 10 + 0.5 (k − 1) in BLOSUM62 units
score     = score2 / 2, shown to one decimal ("BLOSUM62 units")

Recurrences (gotoh.ts, Gotoh's affine-gap programme; H ends anywhere, E ends with b_j against a gap, F with a_i against a gap):

E(i,j) = max( H(i, j−1) − 20,  E(i, j−1) − 1 )
F(i,j) = max( H(i−1, j) − 20,  F(i−1, j) − 1 )
H(i,j) = max( H(i−1, j−1) + s2(a_i, b_j),  E(i,j),  F(i,j) )          local: max(…, 0)

E starts each row and F row 0 and column 0 at −2³⁰, so no gap state exists before the first residue. Modes (AlignMode): global (the default and the only mode the panel uses: end gaps free; row 0 and column 0 are 0; the score is the best over the last row and the last column), global-end-gaps (the edges carry −gap2(k); the score is the corner) and local (scores floor at 0 with a START bit; no alignment when the best is 0). Where gaps in the two sequences sit next to each other at an end, only the one that reaches its own sequence's end is free; the other costs as internal (the CSO's C2 fixture, matching Biopython). Building that fixture found one bug, fixed in gotoh.ts: row 0's last cell (all of B as a leading gap, then all of A as a trailing one) is now an end in global mode.

Memory. Rolling Int32Array rows for H and F and one byte per cell of tie bits (bit 0 diagonal, 1 H from E, 2 H from F, 3 E opened, 4 E extended, 5 F opened, 6 F extended, 7 local start), about 10 MB at the limit. prepare refuses before any allocation: more than CELL_LIMIT = 10,000,000 cells (len(A) × len(B)), U or O (no BLOSUM62 row; never scored as X, C or K), an internal * or -, a letter outside ARNDCQEGHILKMFPSTWYVBZX*, or an empty sequence. One trailing * is removed and said so. forward re-checks the limit and throws rather than allocate. A partial alignment is never shown.

The shown alignment. traceback takes one optimal alignment in a fixed tie order (diagonal, then E, then F; extend before open) from a preferred end (the corner if optimal, else the furthest end along the last row, then the last column), emitting end gaps as columns so every residue of both sequences appears in the global modes.

Co-optimal alignments (correspondence.ts, global modes only). uniquePartners walks back from every optimal end over the tie bits with two Uint8 rows of state marks; a residue of A has a unique partner when every marked consumption of its row is the same diagonal, and 0 when it has none (a gap in some optimal alignment, or different partners). coOptimalCount counts the optimal alignments over the same bits with rolling Float64 rows, capped at CO_OPTIMAL_CAP = 1,000 ("1,000 or more"). Local mode returns neither (partners and coOptimal are null).

What is reported (report.ts): the score; identities (equal letters), similar pairs (BLOSUM62 above 0) and gap columns with gap opens, each over the alignment's columns including end gaps, and identities also over each sequence's length; the gapped strings; and the differences in each entry's own numbering (substitution, gap in A, gap in B). An X against X column counts as identical but not similar (BLOSUM62 X/X is −1; DEF-W4-4, low). No E-value, significance figure or verdict is computed.

The panel ("Compare with another entry", views/workbench/align.tsx). Entry B is typed as an accession or isoform (cmp); entry A is the loaded entry exactly as displayed (never refetched, F4), or with a picked range of more than one residue and "Align the selected residues only" checked, that range (sliced in the worker; positions shown in A's own numbering). The summary gives every count with its denominator, the score in BLOSUM62 units, the CSO's qualification verbatim ("Global alignment, BLOSUM62, gap 10 + 0.5 per extra residue, end gaps free. Identity is a count of matching columns, not evidence of shared function; a raw pairwise score has no significance value."), "Short sequences align well by chance." when the shorter sequence has fewer than 30 residues, and, when more than one alignment shares the best score, "{k} alignments share the best score; the one shown is one of them, in a fixed order." The alignment wraps in blocks of a multiple of 10 columns (at least 20) sized to the container, each a figure with a text caption and residue lines hidden from assistive technology (key: | identical, : similar, - gap). The "Differences" table is the accessible twin: a focusable region, the first 200 rows then "Show all", each with "Select residue {n}" and an "In every best alignment" column (from partners) for substitutions. "Open {B}" opens entry B. Over the cell limit the panel refuses with the cell count and offers a selection. One announcement per result. The convention is stated in words, not by naming EMBOSS: the licence scan forbids a GPL tool's name in src/ and content/ (held for the General Counsel).

Variants

Default: UniProt's curated natural variants from the entry. On request (human entries only), the EBI Proteins API variation list replaces them on the track. parseVariation (variants.ts):

  • Drops records whose cross-references are only COSMIC (cosmic curated, NCI-TCGA Cosmic), and also drops the Ensembl cross-reference when COSMIC is present (Ensembl imports COSMIC). COSMIC sources are removed from significance assertions.
  • Keeps disease names only where ClinVar or PubMed evidence backs them; descriptions only from UniProt or ClinVar; links only for UniProt, ClinVar, dbSNP, Ensembl and gnomAD over https://, at most six.
  • Takes protein HGVS against this accession where given, else the first protein HGVS on another sequence.
  • Classifies each significance label: contains "conflicting" or "uncertain" → uncertain; "pathogenic" → pathogenic; "benign" → benign; "not provided", "other" or empty → none; anything else → other. A variant's class is the worst of its labels in the order pathogenic, uncertain, other, benign, none. This worst-class summary is not a consensus classification (audit, Protein optional items).

Download thresholds (measured sizes in variants.ts comments, September 2026): 1,800 to 6,000 bytes per record; above 20,000 records the list is not offered; above 8,000 it asks for confirmation; when the count is unknown and the protein is longer than 3,000 residues it is not offered. The size range shown is records × 1,800 to records × 6,000 bytes, rounded.

Drugs and clinical candidates

parseDrugs (drugs.ts) reads Open Targets drugAndClinicalCandidates:

  • Stage order: APPROVAL, PHASE_4, PHASE_3, PHASE_2_3, PHASE_2, PHASE_1_2, PHASE_1, EARLY_PHASE_1, IND, PRECLINICAL, UNKNOWN. A drug's stage is the drug-level maximumClinicalStage (row stage as fallback); WITHDRAWAL as a drug stage is read as APPROVAL, with the withdrawal reported separately.
  • Approval basis: the sources of APPROVAL clinical reports. CLASSIFICATION_ONLY = ATC, TTD, USAN, INN. If every approval source is in that set the basis is classification; if any is not, regulator; none, null.
  • trialStage: the highest stage among reports other than withdrawals and classification-only approvals.
  • Indications: per disease, the highest stage across INDICATION reports (withdrawal reports excluded), with their sources; falls back to the row's diseases at UNKNOWN.
  • Mechanisms: those whose targets include this target, else all.
  • Market withdrawal (parseMarketWithdrawal): drugWarnings with warningType "withdrawn", countries split on ;, latest year, distinct reasons. global when the countries include "Worldwide" or both "United States" and "European Union".
  • Salt grouping: members are grouped by baseName(parentMolecule.name ?? name), which strips trailing salt words (sodium, hydrochloride, mesylate, free base and others in SALT_WORDS). The group takes the highest stage, merged withdrawals and indications, and the id of the member named exactly like the base, else the parent molecule id.
isApproved(d) = stage ∈ {APPROVAL, PHASE_4}
                and not marketWithdrawal.global
                and approvalBasis ≠ "classification"
                and not withdrawnWithoutApproval(d)

withdrawnWithoutApproval(d) = marketWithdrawal present and not global
                              and approvalBasis ≠ "regulator"
                              and (approvalBasis is null
                                   or the withdrawal names the US, the EU or the UK)

"Approved drugs" in the headline facts counts isApproved. The dossier's drug card groups the rest into "Approved, with caveats" (APPROVAL or PHASE_4 but not isApproved) and stage groups. The Evidence tab's associated-disease rows (and its Diseases drawer) link each disease to the pipeline app the same way, the link's hover text drugs.trialsFor (it was a hard-coded string naming the app until 17 October 2026); Pipeline receives the words by its search rule (Pipeline, searchOutcome). conditionQuery turns an indication label into the pipeline app's condition query: one alias (acquired polycythemia vera → polycythemia vera), " by … classification" qualifiers dropped, then canonicalCondition. Unit cases cover efaproxiral, voxelotor, ketorolac, metamizole, troglitazone, clometacin and dibenzepin (drugs.test.ts).

The stage is the highest Open Targets reports for any indication and region; it is not an approval for each indication shown.

Target evidence

parseEvidence (evidence.ts): disease associations (first 50 rows; count is the total), each with its overall score and the datatype with the highest score; tractability labels marked true, by modality (SM, AB, PR, OC); the lof genetic constraint's oeUpper as LOEUF with upperBin as its decile; the number of safety liabilities. The dossier shows the count, the top five diseases with score bars, all 50 in a drawer, and the first true tractability label per modality. LOEUF and the safety count are parsed but not displayed.

Sequence properties (ProtParam conventions)

sequenceProperties (src/lib/seq/protein-mass.ts, shared with Calculators and Sequence), computed on the full UniProt canonical sequence:

MW (Da)  = 18.01524 + Σ n_aa × m_aa          (ProtParam average residue masses, PROTPARAM_RESIDUE_MASS)

charge(pH) = 1 / (1 + 10^(pH − pK_N))  −  1 / (1 + 10^(3.55 − pH))
           + Σ_{K,R,H} n × 1 / (1 + 10^(pH − pK))
           − Σ_{D,E,C,Y} n × 1 / (1 + 10^(pK − pH))
pI = pH where charge = 0, by bisection on [0, 14] to 0.0001, rounded to 2 dp

Bjellqvist pK values as ProtParam applies them: N-terminus 7.59 (A), 7.0 (M), 6.93 (S), 8.36 (P), 6.82 (T), 7.44 (V), 7.7 (E), otherwise 7.5; C-terminus 3.55 regardless of residue (ProtParam's output, not the published table); K 10.0, R 12.0, H 5.98, D 4.05, E 4.45, C 9.0, Y 10.0. Letters outside the priced alphabet (B, Z, X, J) are skipped; the Protein UI does not show that any were skipped. The app shows MW in kDa to 1 dp and pI to 2 dp. ε280 (Pace et al. 1995: 5,500 per Trp, 1,490 per Tyr, 125 per cystine) is computed but not displayed here. The values describe the unprocessed precursor chain: no signal-peptide removal, no PTMs. Cited in copy: Gasteiger et al. 2005; Bjellqvist et al. 1993 and 1994; Pace et al. 1995. Verification is in src/lib/seq/protein-mass.test.ts (see "Verification" below).

Construct boundary cropper and affinity-tag biophysics

A disclosure in the Dossier's Record-and-source panel (src/components/suite/protein/construct-cropper-panel.tsx, engine src/lib/seq/construct-cropper.ts) lets the researcher crop the canonical chain to a residue range and add a terminal affinity tag (His6, His6-TEV/3C/thrombin, Strep-II, FLAG, AviTag-His6), then reads the wild-type, tagged and (where a protease is in the tag) post-cleavage length, MW, pI, ε280 and A280 at 1 mg/mL. The panel mounts, and its chunk loads, the first time the disclosure opens (MountOnOpen in dossier.tsx): a closed disclosure adds nothing to the page, so its tables never repeat the summary's MW and pI in the DOM (the dossier browser test failed on that duplicate until 17 October 2026). MW/ε280/pI come from the same sequenceProperties engine above — nothing is re-implemented — so a construct's numbers match Calculators and Sequence for the identical sequence. Two new computations: the Ikai (1980) aliphatic index (X(Ala) + 2.9·X(Val) + 3.9·(X(Ile)+X(Leu)), mole fractions), and a terminal-trim that removes residues whose per-residue AlphaFold pLDDT is below 50 — the band AlphaFold DB marks very low confidence and often disordered (Tunyasuvunakool et al. 2021 and the AlphaFold DB confidence guidance) — until a window of 5 stays at or above it. pLDDT is a confidence measure, not a solubility prediction, so the suggestion is a "confident core", never a "soluble" one; when no window reaches the floor the panel reports no ordered core rather than calling the termini ordered. The tag model follows the CSO's ruling of 20 October 2026 (docs/board/memos/2026-10-20-cso-audit-fixes.md, section 2 item 2). Each tag carries an explicit protease — TEV, thrombin, HRV 3C or none — and is cleavable only when it names one, so only a protease tag has a post-cleavage sequence, mass and column; His6, Strep-II, FLAG and AviTag-His6 have none. AviTag-His6 is a biotin-acceptor and purification tag, not a cleavable one: it may sit at either terminus (the C-terminal construct is the core followed by GLNDIFEAQKIEWHEGHHHHHH), its cleavedSequence and cleaved properties are null, and the panel shows no cleaved column for it. Its note names BirA's residue: K10 of the 15-residue AviTag GLNDIFEAQKIEWHE, residue 11 of the tag as stored, because the leading Met shifts it by one (the engine said K12 until 20 October 2026). Biotinylation adds C10H14N2O2S; its extent is not predicted and no biotinylated mass is reported. A protease tag asked for at the C-terminus is refused with the code c-terminal-cleavage-not-modelled (ConstructRefusal, not a profile), never moved to the N-terminus: the panel shows the refusal and withholds the table, the FASTA and the hand-off links, because no construct exists to send. When the construct would not start with Met — no N-terminal tag and a core that does not begin with M, as in an internal crop — the engine prepends one, sets initiatorMetAdded, and the panel says "Initiator Met added" with "The host may remove the initiator Met; this is not predicted." One mass is reported, for the sequence shown. The "Send construct to Calculators / Sequence" links hand off the assembled tagged sequence with seq and alphabet=protein; above the hand-off limit (SEQ_MAX_LENGTH) the links are withheld with a reason rather than truncating the sequence. Verification: src/lib/seq/construct-cropper.test.ts against the independent Biopython + Ikai oracle pipeline/protein-explorer/construct-cropper/reference.py, both frozen fixtures — construct-cropper-v1.json (2 cases) and construct-cropper-v2.json (6 cases, published as /verification/protein-explorer/0.4.0/, see Oracles and verification): ε280 and the aliphatic index exact, MW within 1 Da, assembled sequences, the refusal code and the added initiator Met exact, plus the low-confidence-throughout case. These are designed-construct compositions; nothing here says a construct expresses, folds, is cleaved in practice or is biotinylated.

Construct drafts and source coordinates (Stage F F3, 24 October 2026). The cropper sits in the Biology panel, whose call site now passes the protein's current selection (dossier.tsx, BiologyPanel). The start and end are text drafts, kept as typed and admitted only as a whole interval by src/lib/protein/construct-draft.ts: ASCII digits, safe integers, 1 ≤ start ≤ end ≤ length; the first failing check names its reason ("The end is beyond the last residue, 1,210."), and the table, the FASTA and the hand-offs are withheld until the draft is valid. Nothing is rounded, clamped or swapped, so the engine's own rounding and clamping never act on an admitted draft. The parent sequence must be ASCII letters (admitConstructSource), because the engine strips anything else before slicing and a blank, dash or stop would shift every later residue; such a sequence is refused with its reason. The default and reset draft is full length with His6-TEV at the N-terminus; "Crop to confident core" (only through F1b's complete-profile gate) and "Full length" stay explicit actions, and "Use selected region (UniProt X–Y)" applies the current selection, checked by the same rule, only when pressed: a later selection or late confidence changes what is offered, never the draft. The draft is owned by the accession and the exact sequence; when either changes it resets in the same render and says "Construct draft reset for the current protein sequence." Beside the full-length default the trim suggestion reads "Suggestion, not applied: …", a heuristic on predicted confidence, not a measured domain boundary (the panel words it from the engine's fields; the engine's other rationale sentences are unchanged). The FASTA header names the source interval in UniProt numbering apart from the tagged sequence (>{label} | source UniProt {accession} release {release} residues {s}-{e} (UniProt numbering) | {tag}, N-terminal | tagged sequence {n} aa, numbered from 1, then "| source Met1 omitted; the tag supplies the initiator Met" when an N-terminal tag replaces Met1 and "| initiator Met added" when one is added; sourceMet1Omitted in construct-draft.ts), the tagged column reads "Tagged, from UniProt residues s–e" with "Source Met1 is omitted: the tag supplies the initiator Met." under the table when it applies, and the body is the exact tagged sequence (CSO ruling of 24 October 2026, docs/board/memos/2026-10-24-cso-protein-f3-pae-cropper-copy.md); the copy key is the suite's CopyButton, keyed by the FASTA, so a failure says so and an earlier copy's state never shows after an edit. Sequence and Calc receive that body with alphabet=protein and no range. Construct science is unchanged. Verification: src/lib/protein/construct-draft.test.ts and src/components/suite/protein/construct-cropper-coordinates.test.ts against the hand-derived construct-coordinates-v1.json (verification 0.9.0, see Oracles and verification), which also checks the unchanged engine's tagged species; e2e/protein-explorer/construct-pae.spec.ts in the browser. Known issues REL-690 to REL-692, REL-696 to REL-698.

Predicted aligned error (PAE) viewer

The predicted-structure section has a disclosure that, once opened (it mounts on first open, like the cropper's), shows AlphaFold's own PAE plot PNG, and beneath it an interactive heatmap (src/components/suite/protein/pae-heatmap.tsx, parser src/lib/protein/pae.ts) that reads a PAE matrix the user uploads — the JSON AlphaFold DB or ColabFold produce, or a raw square array. The suite fetches no PAE itself; the file is read in the browser. The parser accepts, each with a fixture case, the current predicted_aligned_error 2D-array form with max_predicted_aligned_error, the legacy AlphaFold DB parallel-array form (residue1/residue2/distance, refusing a duplicate or missing pair), a flat row-major array, a pae/max_pae object, a ColabFold single-element array wrapper and a pre-shaped {matrix,size,maxPae} (re-validated, not trusted); max_pae floors at 31.75 Å; it validates the matrix is square and non-negative and otherwise refuses with a reason (never a blank or fabricated matrix). The parser records the AlphaFold DB convention that entry [i][j] is the expected position error in residue j when the model is aligned on residue i (PAE is asymmetric); an upload's direction is the file's own, so the viewer labels cells by matrix row and column (below). There is no inter-chain interface scoring. The canvas and its legend both come from one fixed ångström colour scale (src/lib/protein/pae-scale.ts, paeToRgb / paeLegendGradient) over AlphaFold DB's 0–31.75 Å range, interpolated between stops at 0, 5, 15 and 31.75 Å, so the legend shows exactly what is drawn and is ticked in Å with no confidence words (the 5 and 15 Å edges are the conventional confident/uncertain relative-placement marks, for the CSO to confirm). The canvas is fluid (full width, square, capped) so it fits a 375 px screen. Verification: src/lib/protein/pae.test.ts against the numpy oracle pipeline/protein-explorer/pae/reference.py (pipeline/protein-explorer/fixtures/pae-v1.json), and src/lib/protein/pae-scale.test.ts pins the scale and legend.

Uploaded PAE: worker, bounds and identity (Stage F F3, 24 October 2026). The heatmap above reads only a file the user uploads; its data, chains and onResidueSelect props are gone, so nothing can hand it a matrix unvalidated and no upload can drive the protein's selection. The file goes to a dedicated, cancellable worker (src/components/tools/protein-explorer/pae-upload.worker.ts, client pae-upload-client.ts, protocol pae-upload-protocol.ts, marker drybench-worker:protein-pae-upload): a file over 16 MiB is refused before it is read, on the page and again in the worker; the worker reads the bytes, takes their SHA-256 (it identifies the bytes, never the model), decodes them as UTF-8 and JSON, and src/lib/protein/pae-decode.ts decodes every encoding parsePae admits, with the same routing and index conventions, straight into one row-major Float64Array, checking each encoded dimension and pair count against 2,000 a side and 4,000,000 cells before allocating. The bounds are the CTO's initial operational ones, not measured performance; at about 5.5 bytes a value in AlphaFold DB JSON the 16 MiB cap binds near 1,750 cells a side, before the 2,000 cap (CSO estimate, 24 October 2026), so a larger file is refused as too large to read. Cells are kept exactly: no transpose, no average with the transpose, no Float32, no cap at the scale's 31.75 Å. One 10-second watchdog covers the job; the client settles once and terminates the worker on success, refusal, error, a malformed or mismatched reply (job, generation, file size, buffer lengths), timeout, abort, a newer upload, a changed context or unmount; there is no main-thread fallback. The worker also builds the preview, at most 1,024 pixels a side whatever the matrix or the screen: pixel p shows the original cell ⌊(p + ½)·N/P⌋ with P = min(N, 1,024), nearest cell, never a block mean (src/lib/protein/pae-preview.ts), and above 1,024 a side the panel says the preview is drawn at 1,024 pixels a side, how many rows and columns are not drawn and that a narrow feature can be missed; every upload says the readout always comes from the full matrix. Hover reads the original matrix at row ⌊y/H·N⌋ + 1, column ⌊x/W·N⌋ + 1 and nothing outside it, as "Matrix row i, column j: PAE v Å" (rounded to at most two decimals; the matrix keeps the file's value, and values above 31.75 Å are not capped). The upload is labelled "Uploaded PAE · {file}" with the CSO's note: file matrix coordinates, counted from 1 in the file's order, are not residue numbers; the app has not checked that the file belongs to this protein or model; in AlphaFold files the row is the position aligned on and the column the position whose error is given; values are read as ångström; values at or above 31.75 Å share the end colour. The dimension reads "N × N cells". ("Unverified" is not used: D-0043 and the methods sheet reserve it.) The dossier passes context (accession, sequence, model id and version); a change clears the matrix in the same render, cancels any read, and says "Uploaded PAE cleared because the protein or model changed." The provider's image is captioned "AlphaFold DB's PAE image for AF-… v…, in AlphaFold DB's colours, not the scale used below." Verification: src/lib/protein/pae-decode.test.ts (every encoding of the 0.9.0 matrix and of D-0090's pae-v1.json, every refusal also refused by parsePae, the bounds), pae-preview.test.ts, pae-upload-worker.test.ts (the real handler across structured clone and transfer), pae-upload-client.test.ts (lifecycle) and the browser spec above. Known issues REL-693 to REL-695.

3D viewers (Mol*)

The app uses Mol* only; it does not use 3Dmol.js or RDKit. See Engines for the vendoring.

Loading

loadMolstar (molstar-view.tsx) loads /vendor/molstar@5.11.0/molstar.js and molstar.css (paths from src/lib/vendor.ts) with loadScript/loadStylesheet, and reads window.molstar. public/vendor/ is generated by scripts/vendor.mjs from the molstar npm package (5.11.0) and is not committed; LICENSE.txt is copied beside the bundle. The script is about 5.0 MB uncompressed on disk; the registry's "about 1.5 MB" (flags.sizeNote) is not verified here. Mol* starts only when its frame comes within 400 px of the viewport (IntersectionObserver), with a 45 s start timeout. WebGL absence (hasWebGL) shows a message instead. The CSP allows 'unsafe-eval' and a data: connect-src for this app because the Mol* bundle compiles an embedded WebAssembly module (dataPolicy.cspAllowances).

createViewer disables 16 Mol* extensions (DISABLED_EXTENSIONS), hides every Mol* panel and viewport control, sets pdbProvider: "pdbe" and volumeStreamingDisabled: true. The suite Stage draws zoom, reset and focus controls; zoomBy moves the camera along its view vector (distance clamped to 3 to 5,000), reset calls requestCameraReset, screenshot uses Mol*'s viewportScreenshot helper. Once a viewer is ready its trackball key bindings are emptied (emptyKeyBindings), and the camera link (cam) is applied and reported as described under State in the address; zoom and reset report their camera after 200 and 400 ms (0 under reduced motion).

Viewer slots and reuse

Two viewers are kept per page in a module-level slots map keyed experimental and predicted (VIEWER_NAME in structure-stage.tsx). A view that unmounts leaves its viewer's element in the slot; the next view with the same name adopts it, with its loaded file and camera. releaseViewers() disposes them when the workspace (or the object tab) unmounts. The reason recorded in the code is that each new Mol* viewer costs a WebGL context and memory that dispose did not release.

MolViewSpec scenes

Scenes are plain MVS JSON (mvs.ts), built per stage colour set (STAGE_PAPER or STAGE_OBSIDIAN from src/components/suite/tokens.ts; the Stage follows the suite theme):

  • predictedScene: canvas background, download, parse, a polymer component as cartoon with a base grey colour then one color node per pLDDT band holding all its label_seq_id ranges. An optional mode fades residues the experimental entry does not resolve (opacity 0.18); the app never passes those options, so it is exercised only by mvs.test.ts.
  • experimentalScene: polymer cartoon in the "other polymers" colour, every chain mapped to this accession (targetChains, label asym ids) in the "target copies" colour, the chosen chain in the target colour (by label_asym_id, or auth_asym_id when the mapping failed); ligand and branched as ball-and-stick, ion as spacefill (size factor 0.6). Water is left out.

Structure file download and fallback

Mol*'s own downloader has no timeout, so the component downloads each file itself through fetchWithTimeout (60 s, one retry), creates an object URL and hands Mol* a scene rewritten by withLocalFile (the URL gets a #structure.bcif or #structure.cif fragment so Mol* sees the right extension). A recolour (theme change) reuses the object URL without a new download. After loadMvsData, a load that produced no structure is treated as a failure. On failure the fallback scene is tried once (RCSB for PDB files, the mmCIF for AlphaFold models); a second failure shows an error with retry.

Residue focus

useViewerFocus maps a predicted pick through exact pairs only, splitting source targets whenever either source or current coordinates are discontinuous. Adjacent source positions separated by a current-sequence insertion remain separate targets, including in the confirmed application receipt. Zero exact targets clear previous native selection/focus. The experimental pick is intersected with the chosen entry's observed (sequence-checked) residues and mapped through SIFTS segments. The viewer selects and focuses those label_seq_id ranges (400 ms, or 0 under reduced motion); for one to three residues it also shows side chains through Mol*'s focus manager. Source scene identity depends on the loaded model and source band runs, so completion/retry of correspondence does not reload its coordinate file.

Large entries

When RCSB reports deposited_atom_count above LARGE_ENTRY_ATOMS (150,000) the experimental viewer loads only the chosen chain from the PDBe ModelServer until the person asks for the whole entry. The viewer shows the deposited model, not necessarily the biological assembly.

Objects, Studies and hand-off

The suite's object model is described in Storage and state. Registry declaration: objects: { owns: ["target"], reads: ["target"], writes: ["target", "sequence", "compound", "condition", "note"], tabs: ["target"] }.

Object Ref Built by When written
Target uniprot:<accession>; label "GENE · name"; aliases gene, entry name, synonyms, ensembl:<ENSG>; facts { source: "uniprot", retrievedAt, summary: { gene, protein, organism, length, entryName, release } } targetInput Refreshed with { as: "refresh" } on every load, which under D-0045 updates only a record the person already holds; listed when the person pins, adds to a Study or saves a view (ObjectActions, onWorkSaved with { as: "carry" }).
Sequence sequenceRef(sequence, "protein"); alias uniprot:<accession>; data { residues } sequenceInput Refreshed alongside the target.
Compound chembl:<id> from the Open Targets drug id; facts include drugType and stage label drugInput Through a drug row's object actions.
Condition cond:<conditionQuery(name)>; facts carry the Open Targets disease id indicationInput Through an indication's object actions, once the search catalogue has loaded.
Note Saved reference view (protein-reference-view v1) WorkCaptureControls Explicit save.

Outbound hand-off: ObjectActions on the header sends the target with proteinPayload = { id, range?, seq? }; seq is included only when the sequence is at most 1,800 residues (SEQ_MAX_LENGTH). Which apps accept it is decided by each receiving app's inbound rule (src/apps/objects.ts, tested in src/apps/objects.test.ts). Direct links: a small-molecule drug name opens Molecule (host.links["molecule-workbench"]({ name }); biologics get no link), and an indication opens Pipeline (host.links["drug-pipeline-explorer"]({ condition: conditionQuery(name) })). On the target object page, a drug opens its compound object page instead of the inspector.

Inbound: a target opens at ?id=<accession>. Of the payload keys other apps may send, Protein reads only id, name and mine; an inbound range or seq is not part of its schema and is dropped.

Exports

Registered with useExports in views/workspace.tsx from buildExportItems (exports.ts). File names come from exportBase: protein-explorer-<gene or accession>-<kind>-<YYYYMMDD>-<HHMM>. Every file carries a provenance block (proteinBlock): app slug, name and version, the subject, the canonical URL and URL state, each source with licence, version and retrieval time, the limits, the regulatory statement and the not-medical-advice statement. The writers themselves are shared (see Storage and state).

Item id Format Content Sources in the block
protein.entries CSV, or ZIP with sidecars One row per ranked PDB entry: rank, PDB id, chain, method, resolution (Å), UniProt start and end, residues resolved, sequence identity, title; plus per-row source, source_version, retrieved_at_utc, app_version PDBe
protein.drugs CSV, or ZIP Drug, ChEMBL id, type, highest stage, mechanisms, indications, withdrawal countries Open Targets
protein.annotations CSV, or ZIP Features then the variants shown (curated or Proteins API): kind, type, label, start, end, wild type, mutant, clinical class, significance sources, description UniProt, plus Proteins API when loaded
protein.fasta .fasta and .provenance.json fastaText: UniProt-style header (sp or tr, accession, entry name, protein name, then OS=, OX=, GN=), 60 residues per line UniProt
protein.figure.dossier / protein.figure.workbench SVG or PNG, current or transparent background The protein map (dossier) or the sequence tracks (workbench) as drawn, found by data-export-figure; the map's hover cursor is marked data-export="exclude" UniProt, AlphaFold DB, PDBe (plus variant sources for the tracks)
protein.viewer.experimental / protein.viewer.predicted PNG at 1× Mol*'s screenshot with provenance in PNG text chunks (insertPngText) Actual successful coordinate provider (PDBe, RCSB or AlphaFold DB), plus consumed identification/mapping metadata
protein.research-inputs JSON The saved-reference-view payload none
protein.substitution.csv (workbench only; disabled until a check is shown) CSV bundle One row: accession, sequence_version, as_typed, normalised, reference_check, alphamissense_score (the file's text), alphamissense_class, alphamissense_withheld (the sentence), alphafold_model_version, drybench_mean_alphamissense and _n, variants_at_residue (as their sources report them), structure_context; the README carries the AlphaMissense attribution and the research-use sentence UniProt, AlphaFold DB, AlphaMissense, plus variant sources
protein.alignment.fasta (workbench only; disabled until an alignment is shown) .fasta and .provenance.json Both gapped sequences, 60 per line, each header >{accession}[:{start}-{end}] sequence_version= md5= crc64= UniProt
protein.alignment.csv (likewise) CSV bundle One row per column: column, position_a, residue_a, residue_b, position_b (each in its own entry's numbering), kind, unique_partner, matrix (BLOSUM62), gap_open 10, gap_extend 0.5, end_gaps "free", engine (protein-align 1.0.0), both accessions UniProt

The three Wave 4 items sit in the menu's labelled object group with the app's other CSVs (CPO merge review, condition 3). Their rows are built by the panels' chunks, loaded when an export runs.

Figure and viewer exports first reveal their section (prepareFigure) and refuse if the protein changed meanwhile. Viewer items are disabled until that viewer has drawn. Recorded-image history separately exports its exact retained PNG and input/result JSON receipt. No general JSON data export, .mvsj scene, coordinate file, PyMOL/ChimeraX command or printable report exists, although the build specification lists them (C8).

Methods and checks dialog

The methods and checks dialog (D-0085; Shared shell, Registry and content) reads content/apps/protein-explorer.methods-sheet.ts: Dossier, Compare, the substitution check and the two-entry alignment. Those four browsing groups keep no method record or level (the CSO's ruling R4). F-1b adds the fixed exact-correspondence policy and its independent checks, without a biological-equivalence claim. Retained evidence separately names capture 3.0.0 for new exact predicted views and 4.0.0 for new experimental views; neither grants biological validity to the source structure. The registry links the frozen 47-case correspondence and 102-case confidence/projection/binding publications separately from the unchanged app version. Levels (the CSO's ruling of 25 October 2026, docs/board/memos/2026-10-25-cso-protein-stage-f-close.md, SCI-245): protein.residue-map@1.0.0 is verified against the 47-case Biopython enumeration (0.5.1) and the 8-case path-counting script (0.8.0); protein.source-confidence@1.0.0 has its own method record, verified against the 102 confidence cases (0.6.0: 39 parser, 49 projection, 14 binding); capture 3.0.0 is unit-tested, since the Mol* image itself has no reference. definition.methods.group() returns viewStore.

Verification

See Oracles and verification for the suite's approach and Build, test and CI for runners.

Unit tests

Run with Node's test runner (npm test, or node --test <file>). The twelve app test files hold 82 tests and passed on 10 October 2026 together with src/lib/seq/protein-mass.test.ts (104 tests in that run).

File Tests What it pins
src/components/tools/protein-explorer/uniprot.test.ts 10 Accession formats and isoform suffixes, query sanitising, search URLs (accession-shaped symbols searched both ways), suggestion ranking, curated variants, entry parsing, inactive entries
…/structures.test.ts 13 best_structures and UniPDB parsing, resolution factor, ranking with and without UniPDB, coverage depth, SIFTS mapping, compareChain on 5TAR (K-Ras4B stretch), an engineered point mutation, 6GOD Gln165 unaligned, identity demotion at exactly 0.95
…/alphafold.test.ts 13 Model choice and URLs, sequence mismatch, RAB12-, SLC3A2- and POLR2A-like placements, unrelated sequences, balanced indels and repeated fragments refused, pLDDT cut-offs, provider letter schemes, AlphaMissense averaging
…/variants.test.ts 6 COSMIC exclusion, classes, per-position summary, variant naming (HBB residues 7 and 8), size estimates
…/drugs.test.ts 13 Salt grouping, on-target mechanisms, stage ranking, classification-only approvals, market and partial withdrawals, condition queries
…/evidence.test.ts 2 Evidence parsing, missing target
…/mvs.test.ts 5 Scene structure, fade option, chain highlight, withLocalFile, track scale helpers
…/fetch.test.ts 3 Host attribution and in-flight counting of requests
…/fasta.test.ts 2 FASTA header and wrapping
src/apps/protein-explorer/url-schema.test.ts 15 Fragment round trips, legacy ?id=, dropped values, sel, view and section rules, saved reference views; Wave 4: the checked cam, afv, cmp and var keys, no cam from a query, every key at its ASCII maximum within 2,000 characters with a 36-character origin, a 120-character CJK name dropping cam and afv first
src/apps/protein-explorer/recent.test.ts 3 Recent list ordering, cap, malformed values
src/apps/protein-explorer/subject-focus.test.ts 4 Focus rules after a load
src/components/suite/store/camera-param.test.ts 5 cam grammar, precision, refusals, the distance rule, the keyboard orbit
src/components/tools/protein-explorer/alphamissense.test.ts 7 parseMissenseCsv against the five Python oracle fixtures (pipeline/protein-explorer/fixtures/alphamissense/<acc>@2026-10-11/): n exact, mean within 1e-6, POLR2A's withholding (count and first) exact; maskModelDifferences
src/components/tools/protein-explorer/substitution.test.ts 31 Every case of fixtures/substitution/checks@2026-10-11.json; each shown score found in the AlphaMissense source rows with its text and class; pending and withheld paths; the row-level assertion; the six CSO sentences verbatim; no verdict words
src/components/tools/protein-explorer/align-copy.test.ts 3 The alignment copy's pinned sentences
src/components/tools/protein-explorer/align-protocol.test.ts 3 F4, hand-derived: a region is admitted only as safe integers with 1 ≤ start ≤ end ≤ its sequence's length; the worker's reader refuses any other, never slicing; the key binds each side's length
src/components/tools/protein-explorer/align-client.test.ts 4 F4: one settlement per job with a fake worker: a result resolves once (a late message changes nothing); refusal, stale key, worker error and unreadable message reject; another job's message is ignored; cancel and the time limit settle; a worker that cannot be made or posted to rejects; the worker is terminated each time
src/lib/protein/align/align.test.ts 45 Against Biopython 1.88 (below), BLOSUM62 entry by entry, refusals
src/lib/protein/residue-map.test.ts 53 All 47 independent v2 outcomes and metadata envelopes exactly; raw ambiguity withholding, historical insertion suffix, reciprocal pairs, allocation bounds including 1,000 × 10,000 cells, and sole-worker runtime import boundary
src/lib/protein/residue-map-v3.test.ts 8 The runtime against the second, counting reference (verification 0.8.0): partners, states, exact maps and null masks exactly for 20,000, 4^7, 4^8, 4^40 and 5^499 optima (3,000 by 3,000, about 0.4 s) and three real AlphaFold DB/UniProt revised-sequence pairs. The runtime keeps no count and no cap.
src/lib/protein/experimental-map.test.ts 48 F-2 classification and targets against all 46 cases of the independent reference (verification 0.7.0): exact, annotated substitution and every withheld reason; requested/admitted/withheld counts and split targets; the snapshot is never mutated. The live selection consumer uses this classification.
src/lib/protein/experimental-frame.test.ts 10 The F-2 frame reader on the real 3IKA and 1PPB BCIF bytes, decoded by Mol*, equals the independent Python text-CIF extraction exactly; the RCSB fallback has no SIFTS category (unavailable, not empty).
src/components/tools/protein-explorer/experimental-source.test.ts 9 F-2: the source read from actual loaded bytes is bound to their SHA-256 (equal to the reference's 3IKA hash), the viewer's file generation, URL and model; the RCSB fallback's own bytes give no SIFTS and never the primary's; no model or a non-CIF source is unavailable; an aborted read is refused, not partial; the viewer's read (readLoadedSource) publishes pending first, reads once per loaded file (a recolour reads nothing, a newer file gets a newer generation), does nothing for a viewer no longer current and discards a read finishing after replacement. The read runs on experimental loads and feeds focus, visible qualification and new evidence capture.
pipeline/protein-explorer/residue-map/reference_v1.test.ts, reference_v2.test.ts 4 Both frozen publications' byte lengths/hashes, oracle/input hashes and complete-enumeration counts
src/lib/protein/boundary.test.ts 2 No imports between src/lib/protein/** and src/lib/seq/**; the aligner imports only ./

The Wave 4 files above were run singly with node --test on 11 October 2026; all passed.

Aligner accuracy (align.test.ts). The fixtures are pipeline/protein-explorer/fixtures/alignment/2026-10-11/ (UniProt inputs, release 2026_03, with sequence version, MD5 and CRC64), 2026-10-11-ties/ (runs and repeats with up to 8 co-optimal alignments) and 2026-10-11-adjacent/ (the CSO's C2 pairs: W against D, and WWW or DDD before or after MKTAYIAK). For every pair and mode the test asserts the score exactly (doubled integers); re-scores the shown alignment independently of the engine with the mode's end-gap rule and requires the optimum (so it is co-optimal); requires its identity, similarity, gap-column, gap-open and column counts among Biopython's enumerated tuples; requires the co-optimal count equal to Biopython's (global modes); and requires the unique residue pairs equal to the intersection of Biopython's enumerated alignments as a set (global modes). Pairs and global scores: TP53 human against mouse (P04637, P02340; 1558.0, 2 co-optimal), TP53 against isoform 2 (1793.0), the tagged EGFR kinase construct against P00533 (free ends 1711.0; penalised 1292.5), the B5 insertion (493.0), BRAF against itself (4000.0), B, Z and X codes (96.0), glucagon against GLP-1 slices of P01275 (88.0); refusals: EGFR against titin (41,563,500 cells) and SELENOP (P49908, selenocysteine). The adjacent set gives global 28.0, end gaps penalised 27.0 and local 39.0 for both 11-residue pairs, and W against D 0.0, −4.0 and no local alignment. EMBOSS Needle cross-check (CSO condition C1): EMBOSS needle 6.6.0.0, run locally on 11 October 2026, matched every figure for four runs (TP53 1558.0 with 304/326/9 of 396; the construct 1711.0 and, end gaps charged, 1292.5; the peptides 88.0); docs/science/evidence/protein-align-2026-10-11/. Needle reports one alignment, so the co-optimal count and the unique pairs rest on Biopython alone.

Speed gate. Target 2,000 ms; measured 622.9 ms median, 4-core host, 11 October 2026; not measured on CI. A release needs a quiet-host pass within seven days (REL-12, as REL-06). e2e/perf/protein-align.spec.ts (run by npm run test:perf:worker in the weekly/on-demand scheduled.yml worker-gate matrix on its own runner, CTO 11 October 2026 under D-0047; never by npm run test:e2e or the required CI perf job): EGFR (1,210 residues) against a synthetic 8,264-residue sequence (9,999,440 cells), three runs each in a new worker, the worker's own reported ms (forward pass, traceback, co-optimal pairs and count, report), median at most 2,000 ms; the one-minute load average is recorded before and after, and a run where either exceeds the core count fails as "inconclusive" (an assertion, not a skip). On the merge candidate (CTO condition 2): 622.9, 572.5 and 651.8 ms, median 622.9 ms, load 0.85 then 1.26 on 4 cores (docs/progress/wave4-discovery.md).

Shared tests that cover this app: src/apps/objects.test.ts (target and sequence inputs, payload, drug and indication objects, Send to plans to Molecule and Pipeline) and src/lib/seq/protein-mass.test.ts. The latter compares sequenceProperties with ExPASy ProtParam output stored in src/lib/seq/fixtures/protparam.json (retrieved 5 October 2026) for nine proteins (P01308, P68871, P02766, P01116, P04637, P00533, P0DTC2, P69905, P07437): MW within 0.02 Da, pI equal to 2 dp, both ε280 values exact; and pI for p53 fragments that show ProtParam's fixed C-terminal pK. ProtParam is an external reference implementation, not an independent oracle written for this project.

F-1b adds source-loader admission/byte-cap tests, all frozen confidence/projection/binding cases, cancellable worker wire/lifecycle tests, per-transition ownership/capture identity checks and strict v2 receipt/mixed-version/projector tests. The focused confidence.spec.ts adds native split-target capture, sparse/zero/missing scores, n/N means, malformed/empty source data, source Retry, delayed real-worker A→B→A replies, timeout, constructor failure and mapping Retry. use-async.ts, parse-off-thread.ts, molstar-view.tsx, tracks.tsx and workspace-inputs.ts still have no dedicated pure unit suite beyond their consumer/browser checks.

Browser tests

e2e/protein-explorer/ (Playwright; see Build, test and CI) replays responses recorded on 5 October 2026 for EGFR (P00533) from e2e/fixtures/protein-explorer/ (21 responses: the UniProt entry and three searches, AlphaFold DB record, confidence, provider and model, PDBe best_structures, UniPDB, four identity checks, segments, molecules and the 4UV7 file, RCSB entry, the Proteins API HEAD, and the two Open Targets queries, told apart by request body in fixtures.ts). record.mjs re-records them against a running app. e2e/fixtures/protein-explorer-3ika/ adds five PDB 3IKA responses (PDBe segments, mapping and molecules, the RCSB entry and the 3IKA BinaryCIF), fetched with curl on 17 October 2026; serveEgfr3ika in fixtures.ts serves them with the EGFR set, the AlphaMissense file and the sequence version for the two 3IKA reference pages in e2e/reference-routes.spec.ts.

Spec Tests Covers
home.spec.ts 3 The start: its H1, the Search proteins panel, the labelled field, no Layout menu, examples, System theme
dossier.spec.ts 4 The facts tiles, the map and Regions table, the evidence counts and panels with sources and no blocked requests; region pick to inspector and link; F5 range admission (an alias written back as sel, a partly out-of-bounds request refused whole with its note, sel beating a conflicting range); the View keys' drawers
workbench.spec.ts 7 Two stages; go to residue; About and Entry details; entry picker; Wave 4: the positional-mean label and n at EGFR 858 from the real CSV; a one-row mismatch withholds AlphaMissense
camera.spec.ts 3 A keyboard turn writes cam and afv=6; the link reopens with the camera kept; another model version and an unfitting camera are dropped with the notice
substitution.spec.ts 4 A var link opens the check with the oracle's numbers (axe, no sideways scroll at 375 px), Show writes sel; L834R and a nucleotide notation refused by keyboard; a withheld entry; the CSV (desktop)
align.spec.ts 4 EGFR against itself by link; titin refused with its cells; a synthetic one-substitution entry (Q9ZZZ9, a test double) by keyboard with Select; axe and no sideways scroll at 375 px; the FASTA headers (desktop)
content-tabs.spec.ts 6 Section tabs, deferred viewers, shortcuts revealing content, figure and viewer exports revealing sections
toggle.spec.ts 3 Default layout, remembered layout, v and g focus behaviour
keyboard.spec.ts 1 Keyboard-only path
exports.spec.ts 1 Entries CSV (393 rows plus header, per-row provenance columns), FASTA and provenance sidecar, map SVG with <metadata id="mbio-provenance">, viewer PNG items
motion.spec.ts 2 Nothing animating under reduced motion (OS setting and Settings switch)
phone.spec.ts 2 Dossier and workbench at 375 px

Six specs also run axe (expectNoSeriousViolations), and the Wave 4 specs do too. Shared specs in e2e/objects/, e2e/smoke/, e2e/search/, e2e/sharing/ and other apps' specs exercise Protein hand-offs and layout. The Wave 4 specs serve AlphaMissense from the oracle's stored EGFR CSV (pipeline/protein-explorer/fixtures/alphamissense/P00533@2026-10-11/aa-substitutions.csv.gz), UniProt's sequence version from e2e/fixtures/protein-explorer-substitution/P00533-sequence-version.json, and alignment partners from the oracle's stored inputs or synthetic test doubles. The recordings do not include the full variant list, an isoform, a large entry, a non-human protein or any failure response, so those paths have no browser test. The build record lists Protein browser failures that also fail at the wave's base 83d1e09 (home on desktop, dossier and workbench on phone) and toggle.spec.ts as flaky there; nothing was run for this page.

Live checks

pipeline/protein-explorer/verify.ts (run node pipeline/protein-explorer/verify.ts; it makes network calls) checks that each example accession is active with the expected gene and organism, that AlphaFold DB still returns a placeable model for it, that PDBe best_structures still parses, and that DRUGS_QUERY still validates against Open Targets, printing EGFR's approved count. It exits 1 on any failure. It is not part of CI (see Data pipeline). Feature 9 publishes retained-image references under public/verification/protein-explorer/0.3.0/, declared in the registry; the Wave 4 oracle fixtures (AlphaMissense, substitution, alignment, BLOSUM62) live under pipeline/protein-explorer/fixtures/ and are read in place by the unit tests. See Oracles and verification.

Known gaps, defects and divergences

These are collected for Known issues.

Defects

Where What
src/apps/protein-explorer/views/dossier/evidence-cards.tsx:132 For a human entry without an Ensembl gene id, the evidence loader is idle (use-protein.ts:149) but evidenceLoading is true, so the Diseases card shows a loading skeleton indefinitely.
src/components/tools/protein-explorer/evidence.ts:90-98 loadEvidence ignores GraphQL errors[]; a rejected query parses as "no target" and the card says "Open Targets has no target record" instead of showing an error with retry.
src/apps/protein-explorer/provenance.ts:24-31 stamp keeps the first arrival time for a source and key for the whole page. After an LRU eviction or a retry re-fetches, chips and exports still report the earlier retrieval time. The evidence loader records no stamp of its own, so its chip uses the drugs query's time.
content/apps/protein-explorer.workspace.ts:43-44, definition.ts:76 The shortcuts overlay lists [ and ] as previous/next PDB entry with the entry table focused; no handler implements them.
content/apps/protein-explorer.workspace.ts:582 The entries CSV README says "One row per PDB chain mapped to the protein"; rows are one per PDB entry (buildStructureOptions).
src/components/tools/protein-explorer/parse-off-thread.ts:60-72 The abort listener added per worker request is never removed after the request resolves (a small listener leak on long-lived signals).
src/lib/protein/align/report.ts:47-49 DEF-W4-4 (CSO, low): identity counts equal letters, so an X against X column is identical but not similar (BLOSUM62 X/X is −1). Not stated beside the result.
src/components/tools/protein-explorer/align-protocol.ts (sameKey) The client compares the worker's echoed key with a key built from the same request, so the check cannot fail; the panel's own job key is the effective stale guard (CTO review, a nit).
src/lib/protein/align/policy.ts (prepare) The engine checks the cell limit before U/O and other letters; the oracle (align_reference.py) checks U/O first. No fixture exercises both at once, so a pair over the limit that also holds U would be refused for a different reason by each (CTO review).

Wave 4 items carried

From docs/progress/wave4-discovery.md and docs/NEXT.md (Wave 4 section):

  • D2, the gap-aware residue map, supplies exact-only pLDDT and predicted focus through the F-1b worker. Its bounded full-model source contract refuses unverified fragments. AlphaMissense keeps its separate historical placement and eligibility; experimental SIFTS semantics are unchanged by F-1b.
  • Not built: a load button for the variant list inside the substitution panel; Pin for entry B of an alignment (only "Open {B}"); local mode and pasted sequences in the panel (the engine has local mode); the share dialog's "recomputed on open" line for cmp.
  • The original alignment fixtures enumerated up to 8 co-optima; F-1a adds a fully enumerated 4,096-optimum correspondence case. The EMBOSS cross-check covers scores and counts, not co-optimal sets.

Code and copy divergences

  • Hard-coded English strings in components, against the rule that copy lives in content files: search-box.tsx:144,155,186,190, drugs-panel.tsx:53,82 and the classification names at :34-40, views/workspace.tsx:476, residue-card.tsx KIND_LABELS, and the sentences returned by selectionNote in structures.ts:498-516. In the alignment panel, the Differences table's screen-reader header for the Select column is made in the component by trimming " 0" from select(0) (align.tsx:322; CPO merge review).
  • The substitution CSV's column names drybench_mean_alphamissense and _n type the product name in lower case in a component (views/workbench/substitution.tsx), while the on-screen label takes SUITE_NAME.
  • The pLDDT "sweep" animation (sweep.ts, build spec §3.2) is wired into tracks.tsx but workbench.tsx:457 passes sweep={null}, so it never plays.
  • Dead code: protParamMethod (provenance.ts:83), MarkPoster (poster.tsx), paeImageUrl (alphafold.ts), the predicted-scene fade options (mvs.ts, unused by structure-stage.tsx:34). Because protParamMethod is unused, the mass and pI tiles carry no method chip; their detail line names ProtParam.
  • dataPolicy.endpoints for alphafold.ebi.ac.uk keeps cspAlso: ["img-src"] (content/apps/protein-explorer.ts:192) for the PAE image that is not rendered (audit M9: copy fixed, rendering parked).
  • The public docs page lists "unresolved residues faded" among tested scene behaviours (content/apps/protein-explorer.docs.ts); the app never applies it.
  • examples.ts:3 refers to pipeline/protein-explorer/verify-examples.ts, which does not exist; the checks are in verify.ts. pipeline/protein-explorer/README.md:3 gives the old address /apps/protein-explorer/app.
  • The dossier drops UniProt's isoform qualifier (functionText[].molecule) on function text (10 October audit).
  • The dossier map colours curated variants by clinical class, but UniProt curated variants always carry class none, so the colour carries no information until the Proteins API list is loaded (which the map does not use).
  • Open Targets' meta.dataVersion is read only when year and month are strings (drugs.ts:179, evidence.ts:48); the fixtures use strings. Whether the live API always returns strings was not verified here.
  • The variant, AlphaMissense and whole-entry requests are not in the URL, so a link cannot reproduce them (a var link requests AlphaMissense through the check).
  • The product-page copy in content/apps/protein-explorer.ts still describes AlphaMissense as "an optional track" and says its scores "are CC BY 4.0, which allows commercial use with attribution"; Wave 4 also shows AlphaMissense's score for one substitution, and the licence of the source file is held (below). The registry entry was not changed by Wave 4.

The redesign atlas pass (14 October 2026, D-0084)

The control-parity check of frames 001 to 015 against the components on d1e71d62, with what differs and why (the discrepancies are logged in Known issues):

  • Frame 001 draws a Layout select under the subtitle; the start has none since 14 October 2026 (the layouts are tabs). The frame's right column is empty; the start has no column there.
  • Frame 002 draws the page's keys at the right of the Layout row as Save to Bench, Pin, Send to and Add to Study; they sit in the view toolbar under the tabs (at the top of the tab panel), the Layout stays on the subject row (the CPO's rule), and Send to and Add to Study stay in the ⋯ menu of ObjectActions' row variant, with their names unchanged: the bar variant would show them as keys but drops the ⋯ menu other specs drive. The four stat cards of the 13 October pass (Length, Experimental structures, Selected, Source) are replaced: the length and the source went into "Record and source", the PDB entry count into the facts tiles, and the pick into the Map tab's Selection panel and the inspector. The drugs tile no longer shows "0" for a protein Open Targets could not match to a target (the facts of the 13 October pass did); the tile is left out and the Evidence tab says why.
  • Frame 003's "Selecting a region opens RegionCard" holds (the inspector); the Selection rows also hold the region and residue cards, so the object page's tab, which has no inspector, reads a pick there. The features disclosure is kept under the Regions table, which the frame does not draw.
  • Frame 004's "Source details" is new: each source's state with Retry, which the panels' chips did not offer before.
  • Frame 005 draws "Choose PDB entry · Entry details · Reset" under the experimental stage; the picker and Entry details stay in the settings strip with About this protein, and Reset is the stage's own "Reset view".
  • Frame 006 draws Variants and AlphaMissense as toggles with Load keys; the app has only the loads (with their sizes and the confirmation for a large list), kept. "Go to residue" stays in the tracks' own toolbar. The Further analysis rows mount their bodies on first open (each is an on-demand chunk), so Find on this page does not reach a closed check or alignment: a deviation from the DisclosureList rule, recorded.
  • Frames 009 to 011: the Structures drawer gained the frame's Method column; the Drugs and Diseases drawers keep their columns.
  • Frames 012 to 015: the inspector is unchanged; its title already takes the serif panel title (the shell's inspector.tsx).
  • Dashes for missing values went: the facts and counts tiles say "Loading" or "Not loaded" with the source named; the About panel says "Not available"; an entry without a resolution says "no resolution"; a drug without a stated mechanism says so; the entry picker says the list is loading or empty.

Divergence from the PRD and build specification

docs/design/build-spec-protein-explorer.md says at its head that its R1 scope "remain[s] a historical specification" and is not marked shipped. Against that specification and the PRD, the code does not implement: networking through suite/net with backoff, circuit breaker and per-service lanes (C3, PRD §4.4); the IndexedDB data cache with TTLs, "cached" chips and offline opening (C4, PRD §4.3); the extended UniProt comments (disease, subunit, catalytic activity, PTM and others) and ε280 display (C5); the structure DataTable with filters, validation percentiles and author numbering (C6); sessions with notes, annotations and versions (C7; the app instead has the shared saved-reference-view capture described above); JSON, .mvsj, coordinate, command and print-report exports and multi-scale viewer PNGs (C8); the About sub-pages and request log (C9); the Evidence panel's constraint, safety, homologue and probe tabs (C10); and every stretch item S1 to S9. The build spec's fragment keys (chain, fade, variants, classes, tracks, sf, sup, print and others) are not in proteinSchema, and its v key ("fit the focused viewer") is now the layout toggle. The shell layout follows D-0015, D-0028, D-0040 and the 9 October content-tab correction recorded at the head of the build spec.

Licence facts held for the General Counsel

Recorded as facts; conclusions are held for the General Counsel (D-0010).

  • Open Targets data is CC0 1.0, but drug identifiers and mechanisms originate from ChEMBL (CC BY-SA 3.0). The app relies on Open Targets' statement that its sources agreed to unrestricted use; flags.shareAlikeData is false. Whether that position holds for commercial redistribution in exports is an open legal question.
  • EBI Proteins API variants carry "EMBL-EBI terms; source terms apply" (licenceId: "EMBL-EBI-terms"); per-source terms (ClinVar submitters, gnomAD, dbSNP) are named in copy but not individually dated in sourceMeta.
  • UniProt's licenceReadOn (2026-09-27) is the API check's date; the registry comment says the licence page could not be read on 5 October 2026.
  • Excluded on licence grounds (registry excludedSources): COSMIC, DrugBank, KEGG, OMIM, AlphaFold 3 outputs.
  • Mol* 5.11.0 is MIT (molstar displayOnly: true); see Dependencies and licences.
  • AlphaMissense (Wave 4 finding). The registry records alphamissense as CC-BY-4.0 (read 2026-10-05) for the AlphaFold DB per-protein CSV the app reads, which carries no licence header. The Zenodo bulk file AlphaMissense_aa_substitutions.tsv.gz (record 8208688, retrieved 11 October 2026), read only at pipeline time by the substitution oracle, states "Licensed under CC BY-NC-SA 4.0 license" in its header. The oracle keeps only the ten rows its cases need (pipeline/protein-explorer/fixtures/alphamissense-source/2026-10-11/about.json); nothing under src/ or content/ names the Zenodo file. Which licence governs the per-protein CSV, and what a non-commercial share-alike source would mean for the per-residue means and the substitution score in a suite with a paid tier, is held for the General Counsel and the CEO. App strings and export READMEs still say CC BY 4.0.
  • BLOSUM62. blosum62.ts is generated from NCBI's file (https://ftp.ncbi.nlm.nih.gov/blast/matrices/BLOSUM62, stored with its sha256); its redistribution terms are held for the General Counsel (D-0055). So is whether to exempt the alignment copy from the licence scan so it can name EMBOSS Needle's convention.

Extending it safely

  • Read the cited design sections first. Code comments cite the build spec, the design system and the contracts by section; the head of docs/design/build-spec-protein-explorer.md records which older prescriptions are superseded.
  • Keep parsers pure. New API parsing belongs in a pure module under src/components/tools/protein-explorer/ with relative .ts imports and a *.test.ts beside it built from a recorded response. Loaders in api.ts only fetch and call parsers.
  • Declare every host. A new service needs an entry in dataPolicy.endpoints (operator, purpose, what it receives, consent, terms), a sources row, a sourceIds entry and sourceMeta with licenceId and licenceReadOn (or "To be checked" during development). The CSP is generated from the endpoints; an undeclared host is blocked. Say so if the source cannot ship commercially.
  • Go through fetch.ts. It gives timeouts, abort, error classes and the network chip; pick a timeout and decide whether failure should be soft (return null) or a panel error.
  • Use useAsync with a cache in use-protein.ts, with a stable string key that includes everything the result depends on, and stamp the source in the loader for provenance.
  • Keep coordinate domains explicit. User ranges use current UniProt numbering. Map into PDB label_seq_id through the experimental SIFTS policy and into the AlphaFold model through exact residue-map pairs only. Source colours stay in model coordinates. Never borrow the new map to widen AlphaMissense eligibility or reinterpret historical evidence.
  • URL state goes through proteinSchema. Add a key with a check, decide push or replace, extend url-schema.test.ts, and keep readProteinWorkspace accepting older saved views.
  • Copy goes in content/apps/protein-explorer.workspace.ts (or the registry entry for product-page text); product names only from content/app-names.ts. Limits live once, in content/tools/protein-explorer.ts.
  • Colours go in palettes.ts with light and dark values; tokens.test.ts checks dark contrast. Mol* scene colours come from the stage tokens.
  • Objects. New object types written by the app must be added to the registry's objects.writes, built in objects.ts, and tested in src/apps/objects.test.ts. Respect D-0045: looking something up must not create a record; use { as: "refresh" } for passive updates.
  • Exports must carry proteinBlock provenance with the right SourceIds; add a case to e2e/protein-explorer/exports.spec.ts.
  • Browser tests run against recorded responses; re-record with e2e/fixtures/protein-explorer/record.mjs when an API shape changes, and run pipeline/protein-explorer/verify.ts after an Open Targets or AlphaFold DB release.
  • Budgets. scripts/size-budgets/protein-explorer.json caps the workspace route (309 KB interim under D-0048, 265 KB at release), the definition chunk (19.9 KB), and since Wave 4 the workspace chunk (protein-workspace-chunk, marker drybench-island:protein-workspace, 165.5 KB since 24 October 2026, when 1.2 KB of ceiling moved to protein-evidence-capture (23.4 KB) with the capture copy: the viewers' two Record labels live in content/apps/protein-explorer.evidence-prepare.ts, so the workspace no longer loads protein-explorer.evidence.ts; CTO addendum in docs/board/memos/2026-10-24-cto-bioreactor-b2-engine-landing.md), the substitution chunk (6.6 KB), the alignment chunk (9.2 KB) and the alignment worker (9.2 KB). Keep heavy code behind lazy.tsx and Mol* behind its viewer gate; put new panels behind a loader with their own marker, and keep Wave 4 copy out of protein-explorer.workspace.ts, which the definition chunk carries.
  • The aligner. Never import src/lib/seq/** from src/lib/protein/** or the reverse (boundary.test.ts). Do not edit blosum62.ts; regenerate it with blosum62.py. A change to scoring or tie order needs new write-once fixtures from align_reference.py (a new dated set) and the CSO's and CTO's review under D-0055.
  • AlphaMissense. Show a substitution score only through checkSubstitution; never label the positional mean as a substitution score, and keep the DEF-W4-2 and DEF-W4-3 withholding.
  • Before finishing, run npm run lint, npm test, npm run build, npx tsc --noEmit and npm run test:e2e.

Public reference workflows (16 October 2026)

The EGFR reference entry selects P00533 and experimental structure 4uv7 through the normal client workflow. Source requests remain governed by Protein’s existing policy. See Public reference workflows for the shared route, state, source and export contracts.

Retained structure evidence: independent reference inputs

Feature 9's first committed reference is pipeline/protein-explorer/fixtures/evidence-v1.json, generated independently by the frozen Python standard-library oracle pipeline/protein-explorer/evidence/reference_v1.py. It hashes the existing EGFR/3IKA recorded source bytes, enumerates SIFTS mappings, contrasts native camera values with lossy share-link rounding, generates PNG framing/dimension cases with zlib CRCs and declares primary/fallback scene identities. These are adapter reference inputs; they do not certify a capture implementation or prove a selection rendered. The 3IKA sequence differs at canonical residue 790, so that mapping alone cannot establish a canonical-residue highlight.

src/apps/protein-explorer/evidence-oracle.test.ts (added 20 October 2026, after the audit found nothing read these sections) reads the mappings, confidence, camera.roundedLink and mismatch3ika sections of both v1 and v2 against the product functions that apply those rules: parseSegments and mapToLabel on the recorded 3IKA SIFTS response (five cases), bandForScore (eight band edges), encodeCamera (the share-link rounding) and, for residue 790, mapToLabel, parseEntitySequences on the recorded molecules response, the recorded canonical UniProt sequence and compareChain, whose sequence check must also equal v3's. The atomCount of 5,104 has no Node-side product function; Mol* counts atoms at run time.

Exact predicted-image receipt v2 (Stage F F-1b)

New predicted captures use input schema 2, payload v:2, adapter protein-structure-evidence/2 and capture procedure 3.0.0. Result schema/output stay v1. F-2 new experimental captures separately use input/payload/adapter3 and capture4.0.0; historical experimental v1/capture1.0.0 or2.0.0 remains readable. Historical predicted schema/payload/adapter v1 with capture 1.0.0 or 2.0.0 retain their original offset-era meaning. Every mixed tuple is refused, including a mismatch in settings.adapter; v1's independent reader is preserved in evidence-metadata-v1.ts.

evidence-exact-map.ts validates the new source descriptor, whole raw sequences, SHA-256 bindings, strict source-confidence consumed arrays/digest, parser/mapping policies, settled status-specific reason/binding/generation shapes, and compact ordered exact paired runs. Exact runs must be unique, bounded and canonically equal; selected targets derive solely from their intersection with the requested current range. Mapped receipts pass the complete linear alphabet/resource preflight without rerunning dynamic programming. Status/URL/domain contradictions refuse. The loaded-file URL and format must be the bound primary asset or its same-model/version CIF fallback; the coordinate-byte digest remains separate and does not claim structural sequence verification.

Pending correspondence cannot be prepared or read as a completed v2 capture. Settled refusal, unavailability or operational failure may retain a qualified source-only image. Readable omissions preserve that reason, source-confidence failure, exact partner count over selected N, and actual applied atom-bearing source-residue count separately. Preparation identity includes current sequence, source descriptor/status, mapping attempt/binding/generation, selection and research note, so changed content invalidates a prepared image even when the source URL stays the same. Retained PNG bytes and all existing image/metadata/record limits stay unchanged.

The asynchronous recorded projector selects the existing historical-only 8 KB entry for v1 and a separately marked full strict entry for any v2/adapter2/capture3 hint, including malformed combinations. The protein-exact capability is part of its key, so inputs arriving later reload the correct validator. Mixed captured old/new inputs use the full reader; synchronous joined projection and loaded projection have parity tests. No providers or Mol* are loaded to read retained images, and no existing size ceiling is raised.

Retained-image receipt v1 (Feature 9)

evidence-record.ts, evidence-metadata.ts, evidence-format.ts and evidence-validation.ts define protein-explorer.structure-evidence-input / structure-evidence-result v1, with capture procedure protein.structure-evidence-capture 2.0.0 (1.0.0 is still read; see below) and adapter protein-structure-evidence/1. The value is a retained PNG plus a receipt, not coordinates or an executable scene. Existing reference-view records and fragment camera rounding retain their earlier meaning.

The receipt records actual loaded coordinate-byte SHA-256 separately from canonical consumed-metadata SHA-256, requested and successful source URLs, per-response retrieval times and exposed headers, native finite camera and screenshot-pass settings, requested/mapped/observed/applied selection, the researcher's note and captured qualifications. Missing provider values remain null. 3IKA's isolated T790M substitution remains selectable; its qualification names structure M versus canonical T, independently of renderer confirmation.

The reader accepts only the supported schema/procedure/adapter and PNG framing, CRC and dimensions. Since 20 October 2026 its shape is closed: every object the capture writes with literal keys (capturedFacts, captureLoadedView, responseReceipt, and the result's output and image) admits only those keys, and an unknown field refuses the record as input.schema (an unsupported format; raw recovery is kept). Values copied from Mol* or a provider, such as the native camera snapshot, screenshot values and pass settings, the palette, the scene and provider facts inside arrays, are checked as before and not closed. Envelope, PNG, sequence and metadata digests are checked; contradictory source/viewer/selection identities are refused. Limits are 1 MiB PNG bytes, 2,048 pixels per side, 256 KiB non-image metadata, 2 MiB combined record and 32 MiB coordinate hashing. Refusals are closed field codes with content parity tests. Since 20 October 2026 the four size refusals (structure file, image, receipt and record) carry what they measured: refuse(code, { bytes, limit }) sets EvidenceRefusal.measured, and the message, from proteinEvidenceSizeRefusals in content/apps/protein-explorer.evidence-refusals.ts, names the measured bytes beside the limit and its remedy (the CPO drift memo of 18 October). A refusal with no measurement, such as base64 whose length is not a multiple of four, keeps the plain text. The independent v2/v3 references precede this code; see Oracles and verification.

Historical consumers use captured values only. A text projection explicitly states that the retained image is omitted and its bytes are unverified in that projection; it does not turn Protein evidence into an Assay coordinate figure. Exact PNG export preserves the stored bytes, with the result/input envelopes available as a separate JSON receipt. Source credits are retained as capture-time facts, not fetched on historical opening.

Capture 2.0.0 (20 October 2026) retains and hashes the PNG exactly as the viewer's screenshot helper returned it: image.sha256 identifies the captured screenshot bytes, as contract 09 and the CTO's memo of 16 October require (CTO audit-fix ruling 8, docs/board/memos/2026-10-20-cto-audit-fixes.md). Capture 1.0.0, from 18 to 20 October, ran insertPngText first, embedding the work's title, capture time, successful source URL and captured provenance as iTXt chunks, so its digest identifies the text-enriched bytes (REL-407). The reader accepts both versions (CAPTURE_VERSIONS in evidence-format.ts); 1.0.0 records keep their bytes and hash, still verify and still download byte-exact, with no rehash or migration, and their history view adds one qualification from content (proteinEvidenceText.embeddedText), never stored with the record. Any other procedure version is refused as input.schema. Downloading history returns the stored bytes unchanged; it does not insert today's metadata or regenerate the image. An annotated PNG, if one is wanted, is a separately named export built from the stored bytes and the captured facts, disclosing its own digest; none exists yet.

The capture UI requires an already saved, unchanged task. It prepares a frozen preview for one visible viewer and records only after explicit confirmation of the displayed source, selection, stage and settings. Missing mapping can be a recorded qualification; unconfirmed selection application is a refusal. The viewer retains the successful coordinate Blob and source receipt, serialises scene loads and rechecks native selection/focus and the render generation at capture. Both screenshot allocation and crop dimensions are bounded. No new scientific choice is inferred from a default view.

viewer-runtime.ts keeps the lightweight slot/controller interface apart from the lazy molstar-view.tsx adapter. Capture and historical image modules load on demand. History verifies image framing, hashes and browser decoding before showing or exporting the PNG, preserves its aspect ratio across theme changes, and releases object URLs on replacement or exit. The metadata-only projection shares strict schema and identity validation but does not decode or verify PNG bytes; its hashes are explicitly recorded values. Mol* and providers are not required to read retained evidence once the application reader is available.

Version 0.3.0 publishes the independent references and scripts with a hash manifest. Their byte-identical publication is separate from browser acceptance; see Oracles and verification.

The evidence generation observer compares complete unrounded native camera snapshots for Mol* changed events: anti-aliasing view-offset jitter does not move the scientific camera. Explicit stateChanged, pointer/wheel, resize, source, selection, focus and settings events still invalidate it, including genuine away-and-back changes. The upstream camera-event fixture precedes this observer; no camera reset, tolerance or silent recapture is used. Preparation briefly runs the owned viewer until transition, commit queue and draw settle, before freezing its generation.

The metadata format/reader and image-byte verifier are separate modules. evidence-bytes.ts owns SHA-256 byte hashing, base64 decoding and PNG framing/CRC checks; only full capture/history paths import it. The text projector retains every metadata/settings check and explicitly disclaims image-byte verification, without loading the PNG implementation.

Selected-residue focus is memoised by start, end and explicit request nonce, alongside its mapping/model dependencies. Saving a note or opening evidence review must not reapply unchanged native focus; the nonce still permits an intentional repeat request. The offline fallback/selection browser journeys exercise this during saved-owner and capture updates.

Feature 9 adds five route-reachable lazy-entry budgets, each measured plus 15%, rounded up to 0.1 decimal KB: capture 19.3 / 22.2 KB, history 5.4 / 6.2 KB, metadata projection 6.9 / 8.0 KB, saved-owner bridge 0.8 / 1.0 KB and viewer adapter 7.8 / 9.0 KB. Their declared routes are /[app]/app for capture/owner/viewer and /o for history/projection. Markers live only at real entry boundaries; shared helpers remain marker-free. No existing ceiling is widened. Preparation keeps the native loop running until the currently owned viewer settles, even if opening the review caused its intersection observer to pause it, then restores the prior loop policy.

Evidence preparation first paints the preparing review across two animation frames, then rechecks the attempt, scope, note/source request and saved binding before settling or invoking native WebGL. This applies equally to the initial preparation and Prepare again. The renderer can occupy the main thread during a software screenshot; queued microtasks alone do not guarantee that the review was presented first. The visual-only theme-test Mol* stub now models the pinned event subscriptions observed by the adapter; its theme assertions remain unchanged.

Opening evidence review is idle: it does not settle, screenshot, hash or allocate capture IDs. Prepare image is explicit; the first preparation and Prepare again both yield for the preparing status to paint before native work. Immediate saved-task/input/scope refusals disable Prepare and Record, and the original owner/access session applies even before a preview exists. The browser probe wraps the real native screenshot helper only in the test and verifies zero calls on opening/cancelling, one on explicit preparation and none on recording. The CPO matrix-repair memo amends the original automatic-first-preparation sequence; no record schema, scientific fact, image limit or retry identity changes.

The visual-only Mol* theme stub supplies the native helpers container as an empty map; it does not supply a screenshot helper. This preserves ordinary theme testing while capture verification continues to use the real renderer.

Save work and Study retrieval (18 October 2026)

Save work replaces Save to Bench in the action cluster, with an explicit Study destination on first save and a visible repeat Save beside secondary Work details. New Study saves organise both editable Protein inputs and the displayed UniProt target, with distinct identities and one atomic work/membership commit. Comparison accessions stay within the saved input payload. Pin is in More; the actual target's Study membership is visible separately from work ownership.

Repeated saves preserve material removals; changed primary targets add without removing previous materials. Historical Protein manifests already carried subjectRefs even when no material membership existed. Their absent membership is retained conservatively on re-save, since absence cannot identify deliberate removal; use Add to Study to include that existing target explicitly. New work and Save as new include it automatically. Calculations and recorded result provenance do not change.

Experimental correspondence and receipt v3 (Stage F F-2)

Experimental focus resolves the actual loaded file's residue-level SIFTS rows against current UniProt, selected native model and author/label chain identity. Exact observed standard residues and explicitly annotated, observed standard substitutions may be selected. The independent 3IKA anchor is current T790, deposited MET/M, label A99 and author A790. The viewer identifies the difference; it never calls the displayed methionine Thr790. Unobserved, conflicting, ambiguous, modified or unmatched positions are withheld; absent correspondence is counted separately. API segments still describe source coverage/ranking but cannot authorise focus or fill gaps. The existing alternative-entry offer now says PDBe coverage overlaps the selection and that loaded-file correspondence has not been checked; Try navigates without promising focus. Viewer keyboard requests are retained across the lazy stage mount so the existing shortcut still focuses its figure.

The source snapshot binds actual coordinate SHA-256, successful URL/format, file generation, model number/index/runtime ID and polymer-chain identities. It is cleared on replacement/release; asynchronous reads and focus compare owner/file/model identity before publication or application. Scene construction does not depend on map publication. Fallback bytes are assessed on their own: a whole-entry RCSB file without SIFTS may remain viewable and produce a qualified unavailable receipt, with its actual model/chain binding. Chain-only views remain inspectable but their preparation is explicitly refused: the frozen v3 inner accepts only the supported whole-entry PDBe and RCSB URLs. No cross-file offset or alignment is substituted.

experimental-frame.ts preflights category counts and required accessors, then bounds labels/pairs, source fields, selected-model atom aggregates, 8 MiB logical retained encoding and 2 MiB output encoding. Reservation occurs before insertion/allocation with maximum counter widths; every 256 work items, including output assembly, participates in cancellation/yielding. These are encoded-data bounds, not JS heap or peak memory claims. Incoming Mol* frame storage, objects and runtime overhead are excluded. Invalid UTF-16 strings refuse rather than encoding replacement characters. Large/refused structures remain viewable without inferred correspondence.

New experimental evidence uses input schema/payload3, adapter protein-structure-evidence/3, capture4.0.0 and unchanged result schema/output1. Preparation requires an explicit residue/range; pending correspondence cannot become a settled receipt. The scientific inner retains application: {kind: "not-asserted"}. A separate application claim binds actual file/model/chain/current generation, ordered non-overlapping eligible targets and confirmed count/representation. Actual renderer application is observed separately from scientific eligibility.

The authoritative inner is retained as JSON text. experimentalParts bounds it to256 KiB before strict integer-token parsing: 3.0 and 3e0 cannot become accepted integer tokens through generic Study recovery. Outer stored envelope numbers have the documented safe-integer value contract; their lost original lexical spelling is not certified. The reader recomputes decisions, eligibility and physical-row lower bounds, validates tuple/outer consistency, then asynchronously verifies sequence/source/binding hashes before capture or download. Required experimental qualification is derived from retained facts and must remain in output omissions even if an altered result is rehashed. Historical schema1 and predicted schema2 readers keep their original semantics.

Captions/omissions distinguish requested positions, exact/differing/withheld/unmapped assessment and actual highlight/surroundings/empty application. Non-ready receipts state the settled reason without invented assessed counts. Live PNG export uses existing PNG provenance text for qualification and guards source/application/controller identity across screenshot creation; pending mapping refuses export. Retained PNG download remains byte-exact, paired with the separate JSON source receipt. Metadata can be discarded by downstream image software; no visible annotation is claimed.

Acceptance includes frozen0.7.0/0.10.0 replay, integer/Unicode/byte/tamper tests, application relations, full envelope and two Study recovery transfers, and native browser focus/capture/offline export. Synthetic application fixtures are consistency cases, not renderer observations. Candidate build, size, browser and seat-review evidence is recorded in the F2 continuation progress record; this section does not independently certify Stage F completion.

Native figure export and retained evidence use the same serialized bounded image-pass adapter (native-capture.ts). It obtains Mol*'s screenshot imagePass once, disables export-only multisampling, applies current canvas postprocessing with ambient occlusion capped at 32 samples and resolution scale 1, and refuses enabled illumination. It renders the original native dimensions/crop, transparency, axes, markings and camera through Mol*'s supported task runtime; it neither copies the live framebuffer nor downsamples. The existing 2,048-pixel allocation/crop ceiling remains. The pass and prior animation state are restored in finally; an unowned or hidden viewer is not restarted. Generation, ownership, visibility and selection/source guards still surround capture.

New receipts retain their existing closed screenshot wrapper and native effective pass properties, with drybench:capturePolicy: "viewport-bounded-v1" inside the open pass payload. Settings comparisons retain the pass object rather than rereading the quality-upgrading helper getter. Historic receipt versions and bytes are unchanged. Retained-image methods text states: “PNG capture uses a bounded native rendering pass. The receipt records the effective rendering settings; edge smoothing and shading may differ from the on-screen view.” Optional local diagnostic callbacks time the native getImageData operation (render and GPU readback together) and encoding; no public telemetry is collected. Native browser probes count ImagePass.getImageData calls without constructing a pass before explicit capture.

Enabled illumination produces the runtime refusal viewer.illumination, with instructions to turn illumination off before preparing again; it is not reported as malformed image bytes. Camera and screenshot settings are snapshotted inside the serialized capture, before restoring the animation loop, alongside the exact pass used. This adds no retained schema field or reader version.

Live PNG provenance uses the corresponding bounded-rendering qualification without claiming a settings receipt; the full effective-settings statement belongs to retained evidence. Task progress wording is content-owned.

Runtime illumination wording is isolated from the historical refusal table: its typed code and message travel with the native capture failure, while historical readers keep their existing refusal content. New retained-image methods load only when preparing an image carrying the bounded-pass policy. The lazy Mol* task adapter has its own real entry marker (protein-native-capture-task); it remains outside historical projection and capture-review entry closures.

The native Task adapter and retained rendering-methods content are independently metered optional entries (protein-native-capture-task and protein-native-capture-methods). Their runtime markers identify their actual route-reachable closures; existing capture and historical-projection ceilings remain unchanged.

Evidence record construction is an explicit lazy writer entry (protein-evidence-writer). The public prepareProteinEvidence function loads it on preparation; review/history entry retains full reader and byte verification without eagerly loading new-record serialization, qualification construction, or hashing of newly built snapshots. The writer still revalidates the complete prepared result before returning; no receipt guard, wording, version, or historic-byte behavior changes. The public preparation boundary snapshots caller-owned work, facts, PNG bytes and identity synchronously before the module-loading await, so the split introduces no new mutation window. Caller lease/source guards still run around preparation as before. PNG bounds/framing and the already-available v1/v2 readers admit inputs before copying or loading the writer. Schema 3 retains its closed lazy validation on the held snapshot. The old v3 path already awaited its reader before cloning; this split now snapshots that format before the new writer await as well.