Skip to content

Commit ea76d7e

Browse files
authored
Spec compliance + SDK 1.77 bump (#2)
* restore workflow file-import handle as output Without exposing fImport.handle from the workflow's outputs map, the pl-dev resource graph doesn't track the upload and the workflow waits forever on a phantom import — no Docker container is ever spawned, no error is raised. Was removed during an earlier cleanup pass when no UI consumer was reading getImportProgress(); turns out the output itself is structural, not optional. Also drop the binary (Python venv via runenv) entrypoint from the software package — Docker route only, which removes the dev-mode ambiguity where pl-dev would pick the local venv path and hang on runenv-python-3 download. * add per-structure scores PFrame + R23 counts + R40/R43 annotations Spec R38: scalar scores + surface metrics + threshold flags now ship as a single-row PFrame keyed on a placeholder `structureId` axis (will become the upstream clonotype key once R1-R6 PrimaryRef lands). The JSON report stays untouched as the UI's source of truth — UI refactor to read from the PColumn data is a separate slice. Spec R23: four new summary counts (extraCysCount, exposedExtraCysCount, brokenCanonicalDisulfideCount, missingCanonicalCysCount) emitted as Long columns on the scores PFrame. Spec R40: only the six `*Flag` String columns carry `pl7.app/isScore: "true"`. Raw metrics ship as plain features. Spec R43 (blockId portion): every emitted PColumn — motifs, cysteines, and the new scores frame — carries `domain.pl7.app/blockId` scoped to this block instance via `wf.blockId().getDataAsJson()`. `pl7.app/trace` still waits for a real PrimaryRef. Verified end-to-end on 1N8Z: scores parquet has 1 row × 31 columns, extraCysCount=6 / exposedExtraCysCount=1 (A/214 orphan) / missingCanonicalCysCount=1 (B/92 Chothia offset) match the existing JSON-side numbers, flag sentinels work for mode-inapplicable rows. * add SSBOND cross-check + R33 hallmark tetrad readback New software/src/diagnostics.py runs two defensive checks reported under report.diagnostics: ssbondCrossCheck (spec R21): pairs PDB SSBOND header records with the geometry-detected disulfide bonds from cysteines.py. Returns {headerBondCount, geometryBondCount, matched, headerOnly, geometryOnly} so an auditor can see which bonds the V-domain-only geometry test skips by design (CL / CH1 constants, antigen-chain disulfides). iCode normalization collapses both "" and the "-" sentinel so the two sides of the comparison meet. hallmarkTetrad (spec R33): reads residues at the hallmark positions on the heavy chain (IMGT 42/49/50/52 or Chothia/Kabat 37/44/45/47) and reports them. Informational for now — deterministic IgG/VHH mismatch warning waits for a VHH fixture to anchor expected residue sets. Verified on 1N8Z (Chothia, H=B, L=A): SSBOND matches 2/2 V-domain canonical (A/23-A/88, B/22-B/96), 25 headerOnly (constants + antigen disulfides). Hallmark tetrad: V/G/L/W at 37/44/45/47 — canonical IgG. * parse REMARK 99 PLATFORMA CDR records (spec R10 preferred path) When a PDB carries `REMARK 99 PLATFORMA CDR<role><idx> <chain><s>-<chain><e>` records (written by the upstream Structure Prediction block per its R26), those CDR ranges are now the authoritative source for region tagging. Falls back to the scheme-aware fixed ranges in SCHEME_CDR_RANGES when the records are absent (matches the existing behavior on 1N8Z). Parser: Parsed.platforma_cdrs holds {"H": {"CDR1": (start, end), ...}, "L": {...}}. Only accepts a record when all three of CDR1/CDR2/CDR3 are present for a role — partial records fall through to scheme constants. numbering.region_for: optional `platforma_cdrs` argument; both motifs.py and metrics.py thread `parsed.platforma_cdrs` through. region_for is also called from cysteines.py via numbering.py's classify path — that inherits the override automatically since cysteines reads CANONICAL_CYS_POSITIONS, not CDR ranges. Verified on 1N8Z (no REMARK records): identical region distribution (1 CDR1, 2 CDR2, 1 CDR3, 6 FR3, 1 FR4, 43 null) — fallback path unchanged. Synthetic REMARK 99 fixture verified inside the Docker image: overridden CDR1=(50,60)/CDR2=(80,85)/CDR3=(100,110) routes test positions correctly. * derive heavy-atom Ala-X-Ala SASA references in-block (spec R11) rSASA no longer relies on FreeSASA's Naccess-derived defaults. The 20 reference values are now derived per Yang & Blundell 1996 / Shrake-Rupley 1973 in software/scripts/derive_ala_x_ala_refs.py: each amino acid X gets an extended Ala-X-Ala tripeptide built via PeptideBuilder, FreeSASA runs heavy-atoms-only on the result, and the central residue's total + side-chain SASA become the reference. Output is software/src/ala_x_ala_refs.json, versioned and auditable; main.py loads it once at import. PeptideBuilder is NOT a runtime dep — only the JSON is shipped in the runtime container. To re-derive: docker run --rm \ -v $(pwd)/software/scripts:/work/scripts \ -v $(pwd)/software/src:/work/src \ <liabilities-image> sh -c ' pip install --quiet PeptideBuilder python /work/scripts/derive_ala_x_ala_refs.py /work/src/ala_x_ala_refs.json ' Verified on 1N8Z: 54 motifs / motifScore 0 / devScore 36 / PSH 64.44 / PPC 0 / PNC 0.06 / SFvCSP 4 — identical to the previous FreeSASA-default run. Reference-value shifts are small enough that no residue crosses the 0.075 buried/exposed threshold on this PDB. * expose advanced thresholds in UI + R55 subtitle (spec R49) BlockData V4 surfaces the three advanced knobs that had only been CLI defaults: rsasaBuriedCutoff (R12, default 0.075), frConfThresh (R34 default 4.0 Å), cdrConfThresh (R34 default 6.0 Å). Plumbed through args → workflow → software via three new CLI flags on the existing exec.builder() chain. UI adds a PlAccordionSection "Advanced thresholds" with three PlNumberField controls. Helper text spells out the calibration caveat for crystal vs ImmuneBuilder PDBs (raise confidence thresholds on crystals because their B-factor column is Ų temperature factor, not predicted error). R55 subtitle (active-parameter summary) renders as e.g. "chothia, H=B/L=A, rSASA<0.075, conf-gated FR>4 Å / CDR>6 Å" via model .subtitle() — visible in the block header. Verified on 1N8Z via MCP: V4 migration preserves v3 selections (chothia/B/A) and lands default thresholds. Raising FR/CDR to 50 Å un-gates all 54 motifs (max B-factor on 1N8Z is ~35), motifStructuralRiskScore jumps from 0 to 78.92, structuralDevelopabilityScore from 36 to 114.92 — confirms end-to-end plumbing. * infer heavy/light chain mapping from REMARK 99 records (spec R9) REMARK 99 PLATFORMA CDR records' chain-letter prefix now drives the H/L → physical-chain mapping authoritatively, per spec R9 wording ("Non-canonical PDB chain IDs (A/B) are accepted; REMARKs are authoritative"). Falls back to the user's --chain-h / --chain-l selections (UI dropdowns) when REMARKs are absent. parser.py: regex captures the chain letter at both ends of each range; both ends must agree per record, and conflicting records across the same role (e.g. CDRH1 on X, CDRH2 on Y) drop the mapping rather than silently picking one. New field `Parsed.chain_role_to_pdb_chain`. main.py: heavy_chain_id / light_chain_id derived once, with REMARK 99 overriding the CLI flags when present. All downstream callers (detect_motifs / detect_cysteines / compute_metrics / hallmark tetrad diagnostic) read the derived values. This is prep for the imminent integration with the upstream Structure Prediction block (/home/teo/mictx/blocks/3d-structure-prediction). Once that block's `pl7.app/structure/pdb` PColumn flows in as a PrimaryRef, every PDB carries REMARK 99 PLATFORMA records and the chain dropdowns become a fallback rather than the primary input. Verified on 1N8Z: identical region distribution (1 CDR1, 2 CDR2, 1 CDR3, 6 FR3, 1 FR4, 43 null), motifs=54, gated=54, motifScore=0, devScore=36 — no regression on the no-REMARK-99 fallback path. Synthetic REMARK 99 fixture (chain letters X/Y) verifies the override + the conflict detection inside the Docker image. * per-clonotype PrimaryRef path + results table + histograms (R1-R6, R37, R44/R45, R51, R54) R1-R6: workflow now resolves args.pdbRef (the upstream `pl7.app/structure/pdb` ResourceMap) and dispatches to process-pdb.tpl.tengo via pframes.processColumn, iterating one body invocation per scClonotypeKey. specs.lib.tengo is the shared PColumn-shape source between the legacy single-PDB upload path and the per-clonotype path; only the inner Xsv settings differ. Model keeps both the legacy `pdb` upload (for local fixtures like 1N8Z) and the new `pdbRef: PlRef` field so existing block instances migrate. R37: rename the JSON column to spec's `pl7.app/liabilities/perClonotypeReport`. R44 / R45: run-summary alerts. MainPage fetches *Flag + gated counts from the scoresTable PTable via pfDriver.getData and renders two PlAlert banners when >10% red flags or >25% confidence-gated. R51: new per-clonotype scalar results table (`scoresTable` model output, V6 BlockData migration). Spec's default-visible / hidden column mapping applied via `pl7.app/table/visibility` annotations on each scoresColumnSpecs entry. Flag columns carry the spec's tooltip text via `pl7.app/description`. Sits on `/` as the primary view; the motifs and cysteines drill-down tables moved to `/motifs` and `/cysteines` because mounting all three on one page reliably hangs AG-Grid in placeholder state. R54: five histogram pages (PSH, PPC, PNC, SFvCSP, CDRH3 compactness, developability score), V7 BlockData migration adds one GraphMakerState per histogram. Each page surfaces the metric description and the Raybould 2019 amber/red bands inline. Threshold-line overlays themselves are NOT drawn — graph-maker's histogram compose path doesn't honor `pl7.app/graph/thresholds` yet (only scatter does). * surface upstream cdrh3Length + emit Raybould threshold annotations + subtitle input-mode (spec R5, R54) R5: scoresTable model output now also pulls `pl7.app/structure/cdrh3Length` from the result pool via `findDataWithCompatibleSpec` and feeds it to `createPlDataTableV2` alongside our `scoresData` columns. PFrame driver joins both on `pl7.app/vdj/scClonotypeKey`, so the Main table now carries upstream's CDRH3 length as a side-by-side sanity-check column. R54: emit `pl7.app/graph/thresholds` annotation on the 6 metric columns (totalCdrLength, PSH, PPC, PNC, SFvCSP, CDRH3 compactness) with Raybould 2019 + Gordon 2025 amber/red boundary values. The histogram code path in `@milaboratories/graph-maker` doesn't honor this annotation yet (only the scatter path does); the annotation is already correct for when histogram support lands and is also picked up by the scatter chart type today. R55: subtitle now starts with `predicted-structures · ...` or `single-PDB · ...` so the active input mode is visible in the block header at a glance. * add hydrophobicity scale selector + stringify threshold annotations (spec R48) R48: PSH scale selector with 5 options (Kyte-Doolittle default, Wimley-White, Hessa, Eisenberg-McLachlan, Black-Mould). Raw values stored "higher = more hydrophobic", min-max normalized to [1.0, 2.0] so PSH magnitudes stay comparable across scales. Threaded via biochem.get_hydrophobicity_scale -> metrics.compute_metrics -> --hydrophobicity-scale CLI flag -> workflow (both PrimaryRef and legacy paths) -> BlockData v8 migration -> Advanced UI dropdown. Subtitle shows hScale=<name> when off the default. Also fixes pl7.app/graph/thresholds annotation: PColumn annotations must be strings; tengo json.encode returns bytes which the spec validator rejected. Wrap in string(...) so the JSON round-trips as plain text. * adaptive strip plot at N<20 + render Raybould threshold lines (spec R54) HistogramPage now reads its scoresData PFrame's row count via pFrameDriver.getShape; below 20 clonotypes it swaps the histogram for a new SVG StripPlot component that draws one labeled dot per clonotype. The strip plot also renders dashed vertical lines for the column's pl7.app/graph/thresholds annotation (amber/red colored by position in the Raybould/Gordon threshold set) — the histogram path in graph-maker doesn't honour that annotation yet, so this is where R54 threshold visibility lives today. At >=20 clonotypes the existing GraphMaker histogram template returns, since distribution shape becomes the right signal. * join upstream clonotype labels into scores table + strip plot (R51 polish) scoresTable now joins the upstream pl7.app/label PColumn via resultPool, so PlAgDataTable's isLabelColumn substitutes readable clone names into the row-axis display instead of opaque scClonotypeKey strings. HistogramPage exposes a new clonotypeLabelsPf prop and fetches the label map via pFrameDriver. StripPlot dots fall through to the raw axis key when the label is missing, so legacy / unlabeled inputs still render. * 3D viewer + detail panel + tab layout + custom histogram (spec R52, R53, R54) R52 — PlStructureViewer (@milaboratories/structure-viewer 0.2.0) mounted inline on the Main tab. clonotypePdbsMap resolves the upstream pdb column through ctx.resultPool.getPColumnByRef(args.pdbRef) — name + axes + valueType + domain matching via findDataWithCompatibleSpec silently rejected the column (matchDomain treats undefined vs non-empty target as mismatch). Spec color schemes (by-confidence / by-rsasa / by-hydrophobicity) not supported by the component; default Mol* preset ships as a documented gap. R53 — ClonotypeDetailPanel renders the per-clonotype liabilities.json (motifs grouped by type, cysteines, surface metrics with flag badges) next to the viewer. JSON ResourceMap exposed via clonotypeJsonsMap output. RiskSummaryBar (mode + dev score + Dev/Integrity risk badges) sits above the viewer at the tab page level. R54 — custom SVG MetricHistogram + StripPlot replace the GraphMaker histogram template since that template ignores pl7.app/graph/thresholds. Both renderers fetch values through the scoresTable PTable (PFrameHandle has no pf-driver path for getShape/getSpec/getData). Threshold lines render as amber/red dashed verticals from the column's pl7.app/graph/thresholds annotation. Per-clonotype labels auto-resolve from the joined pl7.app/label column. Layout: tabs switch viewer ↔ per-clonotype table, viewer default. v-show keeps both mounted so AG-Grid and Mol* don't reinit on tab change. Viewer side sticky-top, detail panel grows naturally with content. Chain dropdowns + Advanced threshold grid now use auto-fit so they wrap on narrow screens instead of overlapping. * move inputs into PlSlideModal settings panel Predicted-structures dropdown, single-PDB file upload, numbering scheme + heavy/light chain dropdowns, Advanced thresholds (rSASA cutoff, FR/CDR confidence) and the R48 hydrophobicity scale selector now live inside a PlSlideModal opened via a Settings button in the PlBlockPage #append slot. Auto-opens on first load when neither pdbRef nor pdb is configured so new users see the input form without hunting for it. * join + surface cluster axis (spec R42) When 3d-structure-clustering is upstream, scoresTable auto-joins its pl7.app/clusterId + isCentroid + tmDistanceToCentroid + tmScoreToCentroid columns (all on scClonotypeKey). Viewer tab gets a cluster-info badge above the canvas showing cluster id + CENTROID flag (or TM-score to centroid for non-centroids). A "Centroids only" checkbox filters the clonotype dropdown; flipping it on auto-jumps the selection to the centroid of the current cluster so the viewer never goes blank. clusterMap fetched once per scoresTable change via pf-driver and reused by header + dropdown + filter. Empty map → cluster UI hides. Heavier PColumn-level cluster aggregations (median/max per cluster) intentionally deferred — Lead Selection + AG-Grid groupBy cover the practical use cases at the join level. Documented in progression.md. * python: clarity pass on parser, main, motifs, metrics No semantic changes — only restructuring + commentary so non-Python readers can follow the spec → code mapping: parser.py — added a module docstring explaining which PDB v3.30 records we consume (ATOM/HETATM/SSBOND/REMARK 99) and why; annotated the fixed-offset slices on ATOM/HETATM/SSBOND records with the PDB v3.30 byte-column key. main.py — extracted the 30-line scores-PFrame row construction into _build_scores_row(). Keyword-only signature + spec annotations on each block mean the spec→column mapping is reviewable in one place. motifs.py — extracted per-hit scoring into _score_motif_hit() so the detect_motifs loop reads as "find regex matches → score each match". Inlined a CHEMICALLY_RELEVANT_INDEX explanation at the lookup site and clarified the buried-match suppression rationale. metrics.py — added the unified-index commentary on the TAP-mode merge so the offset arithmetic stops being magic. * workflow: clarity pass on tengo files specs.lib.tengo — extracted three helpers for the scoresColumnSpecs dict so the column → label → threshold mapping is reviewable as a table instead of ten lines of dict-per-column boilerplate: - metricColumnSpec() raw Double surface metric + Raybould thresholds - lowConfFractionSpec() R36 per-metric low-conf fractions (Double, no thresholds) - flagColumnSpec() R39 three-band flag columns (default-visible, isScore) The 14 columns covered by these helpers drop from ~180 lines to ~50. totalCdrLength + pshPatchCount stay inline (Long-valued outliers). main.tpl.tengo — top-of-file docstring explaining the PrimaryRef vs legacy execution paths and their identical output contract. Tightened wf.prepare commentary on why errIfMissing:false keeps both paths alive. Extracted the three identical pframes.pFrameBuilder loops in the legacy path into one `buildLegacyPFrame` helper (47 → 9 lines). Added a longer comment on smart.createJsonResource() wrapping — non-obvious SDK contract for non-tengo readers. process-pdb.tpl.tengo — collapsed seven optional-arg if-guards into two table-driven loops (string args / numeric args) with inline R- spec citations per row. wf.test.ts — replaced the stale hello-world template stub with a comment explaining why it's gone and what a real workflow integration test would look like. No behavior change — verified by full block rebuild (7/7 tasks green) and pl-dev re-run. * ui: clarity pass — composables + histogram configs + chart utils Histograms — extracted the six per-route .vue files' duplicated prop bundles into one histogramConfigs.ts data module. Each per-route wrapper drops to a 10-line `<HistogramPage v-bind="histogramConfigs.X" />` call. Single place to tune the spec descriptions and threshold prose. MainPage — pulled three big watchEffects into composables: • useClusterAssignments — R42 scoresTable → clusterMap • useRunSummaryAlerts — R44/R45 red-fraction + gated-fraction • useClonotypeDetailFetch — R53 blob-driver JSON fetch with race guard MainPage's <script> shrunk from ~470 lines to ~270; the async fetch plumbing now sits in /composables/ where it's individually reviewable. Charts — shared utils/chart.ts hosts niceTicks / niceIntegerTicks / fmtAxisValue. StripPlot + MetricHistogram both consumed inlined duplicates of the same nice-step algorithm; one import each now. Cell access — composables/ptableCell.ts hosts readCell / readNumber / readString so the (TypedArray vs numeric-indexed wrapper, with BigInt) duality stays in one place instead of repeated inside each watchEffect. ClonotypeDetailPanel — dropped six unused CSS classes (scoreGrid / scoreCell / scoreLabel / scoreValue / riskBadge) — leftover from the pre-RiskSummaryBar layout, never referenced after the risk readout moved to its own tab-level component. No behaviour change — verified by full block rebuild (7/7 tasks green) and pl-dev re-run. * drop legacy single-PDB path (PrimaryRef-only) The legacy single-PDB upload path was the original dev-fixture flow for 1N8Z testing before the 3D Structure Prediction block existed. Production runs go through the PrimaryRef path; nothing in the shipping pipeline still depends on the file uploader, the PdbLiabilityMap residue grid, or the single-`liabilities.json` stats panel. Cleaning it all out. Workflow: main.tpl.tengo — drop the entire legacy branch (exec.builder + processWorkdir loop + buildLegacyPFrame helper + file / exec / ptUtil / pt.import-dir imports). wf.prepare now uses `errIfMissing: true` because there is no fallback. specs.lib.tengo — drop scoresAxesSpecLegacy / scoresAxesSpecPerClonotype duality (one `scoresAxesSpec` now), drop motifsFrameName / cysFrameName / scoresFrameName (only used by the legacy processWorkdir branch), refresh the file docstring. Model: index.ts — args() throws on missing pdbRef (no more `or upload a PDB file` branch). Drop `liabilitiesJson` + `pdbImportProgress` outputs. Subtitle drops the input-mode prefix (only one mode now). New BlockData v9 migration strips ten persisted-but-unused fields via destructure-discard: pdb (legacy upload), tableState / cysTableState / scoresTableState (UI now uses local refs), and the six graphState* fields left over from the GraphMaker→SVG histogram migration. Old instances' persisted blobs shrink the next time they're saved. UI: MainPage.vue — drop computedResult, report computed, all 12 report.value?.* derived computeds, fmtLowConf, RISK_COLOR, file uploader, the entire <div v-if="report"> stats panel + residue map import, onPdbRefUpdate (no legacy uploader to clear), and chainOptions (was derived from report.chains). Chain mapping is now two PlTextField overrides labeled "auto from REMARK 99" — spec R9 auto-detection covers the production path; manual fields exist for REMARK-99-free PDBs. PdbLiabilityMap.vue — deleted entire file (189 lines). components/pdb/ directory — removed (empty). Total delta: ~500 lines net removed across model + workflow + UI. No behaviour change on the PrimaryRef path. Verified by full block rebuild (7/7 tasks, full turbo cache hit on second pass) + pl-dev re-run. * README + CHANGELOG + changeset + python 2nd pass + test cleanup Block-level docs (README.md + CHANGELOG.md) and the pending changeset were all leftover boilerplate describing the sequence-liabilities block (different block). Replaced with content that accurately describes THIS block: PrimaryRef-only structural developability analysis consuming the 3D Structure Prediction block's PDBs, producing per-clonotype motif / cysteine / surface-metric PColumns + the composite developability score, with R52 viewer + R53 detail panel + R54 distribution pages in the UI. Python second-pass: cysteines.py — extracted three helpers from the 130-line detect_cysteines body: _collect_cys_records (gather CA + SG-bearing Cys), _scan_disulfides (pairwise geometry test → idx → partner map), _classify_cys (R23 four-state lookup), _phantom_missing_cys (placeholder row builder). The main entry point now reads as a flat pipeline. scoring.py — extracted _developability_risk (R41a fixable-tier promotion ladder) and _has_integrity_issue (R41a structural trigger short-circuit) from the 60-line compute_developability body. Module-level constants _ENGINEERING_FIXABILITIES / _RISK_LEVELS / _RISK_ORDER promoted from in-function locals. Tests: pdb-liabilities.test.ts — deleted. Imported from ../../ui/src/pdb/ paths that don't exist; pure sequence-liabilities-block template cruft. wf.test.ts — replaced empty stub with a block-load sanity test via blockTest, plus a docstring noting what a full PrimaryRef-path integration test would require (multi-block harness with Samples/MiXCR/Prediction upstream). Verified via pl-dev after rebuild: all R51/R52/R53/R54 outputs healthy — pdbsMap 3 entries, jsonsMap 3 entries, clonotypeAxisId resolves to scClonotypeKey, scoresTable / motifsTable / cysTable all have valid fullTableHandles. labelsPfPresent=false is the expected degraded-but-still-works behavior when the upstream dataset doesn't emit pl7.app/label (strip plot falls back to opaque keys). * polish: empty-state CTA + default scheme=imgt + description.md fix Three small wins flagged in the post-cleanup audit: ui/src/pages/MainPage.vue — empty-state CTA when `pdbRef` is unset (no upstream dataset selected). Friendly card with an "Open Settings" button so users who close the auto-open modal without picking a dataset don't land on a blank page. Tab strip + viewer + table now hide behind `v-if="app.model.data.pdbRef"` instead of just rendering empty containers. model/src/index.ts — defaulted `numberingScheme` to `"imgt"` in init(). The upstream 3D Structure Prediction block emits IMGT-numbered PDBs exclusively (the `pl7.app/structure/numbering: "imgt"` domain match in our pdbOptions enforces it), so the previous default of `""` forced first-run users into a redundant dropdown click. Override stays available for non-IMGT PDBs. docs/description.md — replaced sequence-liabilities boilerplate (carried over from the new-block template) with a proper description of the structural developability analysis this block actually does. TODO/FIXME sweep across software/src + workflow/src + ui/src + model/src came back empty — no leftover markers. * update Platforma SDK dependencies Bumps catalog SDK versions to satisfy infra/require-latest CI gate: - @platforma-sdk/model, @platforma-sdk/ui-vue 1.64.0 -> 1.77.4 - @platforma-sdk/workflow-tengo 5.13.1 -> 5.25.0 - @platforma-sdk/tengo-builder 2.5.8 -> 3.0.1 - @platforma-sdk/block-tools 2.7.7 -> 2.8.1 - @platforma-sdk/test 1.64.0 -> 1.77.5 - @milaboratories/ts-builder 1.3.1 -> 1.5.0 * fix model build and unify Vue/oxlint after SDK bump - Add `@milaboratories/helpers` to model deps and to the catalog. The SDK 1.77.4 type signatures inferred for `export const platforma` reference helpers transitively; without a direct dep TypeScript hits TS2742 ("inferred type cannot be named without a reference to @milaboratories/helpers"). This matches the pattern used by other blocks on the same SDK (e.g. `rarefaction`). - Pin `vue` to 3.5.24 via the catalog and move it from peerDependencies to dependencies in `ui/package.json`. `@platforma-sdk/ui-vue@1.77.4` ships against 3.5.24; the previous `"vue": "*"" resolved to 3.5.25, producing two Vue runtimes (broken `provide/inject`, etc). - Pin root `oxlint` to 1.63.0 (was `*", resolving to 1.50.0) so the workspace matches the version ts-builder 1.5.0 pulls in. * ui: table as main view + row-click slideover + pretty labels Two interlocked UX changes from operator feedback: * Table-as-main-view (R52 / R53 modal layout) - Drop the in-page PlTabs (viewer | per-clonotype table) and the Clonotype dropdown / "Centroids only" checkbox that drove the inline viewer's selection. The scoresTable is now the primary surface; rows expose an open button on the clonotype-axis cell via PlAgDataTableV2's `show-cell-button-for-axis-id` + `@cell-button-clicked`, fed from `app.model.outputs.clonotypeAxisId` (already exposed by the model). - Row click seeds a `viewer: ref<PlStructureViewerProps>` and `selectedClonotypeKey`; PlSlideModal opens via `:model-value="viewer !== undefined"`. Closing the modal clears both refs so the detail panel and cluster badge reset between opens. - Matches the upstream pattern in `blocks/3d-structure-prediction/ui/src/pages/MainPage.vue:325`. - Cluster / centroid filtering moves to PlAgDataTable's own column filters on the main grid; `useClusterAssignments` loses the `centroidsOnly` argument and its auto-jump watchEffect. * Pretty clonotype labels (`pl7.app/label`) - New `useClonotypeLabels` composable resolves the `pl7.app/label` PColumn via the PFrame driver (`getColumnsFull` + `getSingleColumnData`) and exposes a `resolveLabel(key)` helper that falls back to the raw key. Pattern lifted from `3d-structure-prediction/ui/src/pages/MainPage.vue:137`. - Wired into the slideover title (`C-AYZAQ · liabilities detail` instead of the 21-char clonotype key), the viewer's `fileName` prop, and ClonotypeDetailPanel's `clonotype-label` prop. - PlAgDataTableV2 keeps doing label substitution inside the table itself via `isLabelColumn` detection — no model-side changes needed. scoresTable sourceId bumped to `scores-v2` so AG-Grid column-order cache from the previous tab layout doesn't survive. * fix(model): strip clonotypeAxisId to AxisId shape The output was returning the raw `AxisSpec` (with `annotations` and other fields). `PlAgDataTableV2` does isJsonEqual(colDef.context.id, props.showCellButtonForAxisId) inside `cellRendererSelector`, and the table-side `colDef.context.id` is run through `getAxisId` first — so it only carries `{ type, name, domain?, contextDomain? }`. The deep-equal silently returned false against our richer object, and the open button never rendered on the clonotype-axis cell. No console error, no overlay — just a row with no button. Switch to `getAxisId(found)` from `@platforma-sdk/model` so the returned axis matches the table-side shape byte-for-byte. The 3D-Structure-Prediction block does the same trim manually at `ui/src/pages/MainPage.vue:248` and that's where the working pattern came from. * drop structureId placeholder axis from scores PFrame The scores PFrame used to declare a `pl7.app/structure/id` axis with a constant `"static"` value on every row. It was added as a defensive workaround for what we thought was an xsv-import-pt "at least one axis" requirement back when the scores frame was emitted via the legacy single-PDB path. On the PrimaryRef path that's now the only path, `pframes.processColumn` auto-prepends the upstream PDB column's axes (`[sampleId, scClonotypeKey]`) to every per-row output, which already uniquely keys every scores row. Re-reading `xsv-import-pt.lib.tengo:110-116`, the parquet path's `PFCONV_IMPORT_CFG_SCHEMA` accepts an empty `axes` array and the generated polars schema is built from `columns` only when axes is empty. So the placeholder column was rendering for no reason — it showed up in the scoresTable as a redundant "Structure" row-axis cell with the same string `"static"` on every row. Changes: - `workflow/src/specs.lib.tengo` — `scoresAxesSpec` is now `[]`, `buildScoresXsvSettings` returns `axes: []`. Comments rewritten so the next reader doesn't reintroduce the placeholder. - `software/src/main.py` — drop `structureId` from `_SCORES_AXES` and from the `_build_scores_row` dict, drop the `_PLACEHOLDER_STRUCTURE_ID` constant. - `workflow/src/process-pdb.tpl.tengo` — refresh the header comment so it documents the actual current behavior (axes = `[sampleId, scClonotypeKey]` from prepend, nothing block-side). NB for local dev: the software image needs rebuilding for the python change to land at runtime — `cd software && PL_DOCKER_BUILD=true PL_DOCKER_NO_AUTOPUSH=true pnpm build`. The workflow-side empty-axes change is forward-compatible with the old software (pfconv silently ignored undeclared parquet columns), so a workflow-only deploy still works against a stale image. * fix(workflow): restore structureId axis, mark visibility=hidden The previous attempt to drop the placeholder axis broke at runtime: cannot eval template: tengo template error: assertion error: condition failed: At least one axis is required @platforma-sdk/workflow-tengo:pt.util:40 `pt.saveFrameDirect` (called from `xsv-import-pt.lib.tengo:204`) hard- requires `len(axes) > 0`. The schema validator's `axes: [_SPEC_AXES_SCHEMA]` accepted an empty array, but pt enforces a non-empty count at runtime before saving the parquet — the empty-axes path can't ship. Instead, keep the constant-valued `structureId="static"` placeholder column AND annotate the axis spec with `pl7.app/table/visibility: "hidden"`. That annotation is read by `selectDisplayableIndices` in `core/platforma/sdk/ui-vue/.../PlAgDataTable/sources/table-source-v2.ts:377` which filters axis columns with `isColumnHidden(spec.spec) === true` out of the rendered grid. Net result: the column still exists in the parquet (satisfies pt), still drives the scores PFrame's row uniqueness, but no longer appears in the scoresTable view. (The earlier `"visibility: optional"` only marked the column as opt-in via AG-Grid's column panel — it still rendered by default. `"hidden"` is the stronger annotation that the table source actually treats as "don't display this axis".) Software image needs a rebuild for the python `structureId` field to land at runtime: cd software && PL_DOCKER_BUILD=true PL_DOCKER_NO_AUTOPUSH=true pnpm build * spec: enforce R7 chain-count + R10 numbering + ship R18 absolute SASA Three Python-side / workflow-spec correctness fixes from the 2026-05-21 audit: * R7 chain-count gating (`software/src/main.py`) - 0 chains → error - 1 chain with >180 residues → reject as suspected scFv - 3+ chains → error Failure raises ValueError before any FreeSASA work, so the workflow's per-row processColumn surfaces the bad clonotype without poisoning the rest of the batch. Drops the previous "complex" / "empty" silent fallthrough; `mode` is now always TAP or TNP at the report stage. * R10 fail-fast on missing numbering (`software/src/main.py`) - Hard error if neither REMARK 99 PLATFORMA CDR records nor --numbering-scheme is available — without one of those, region tagging is None for every residue, motif R19 weights collapse to 1.0, and surface metrics return {} silently. Garbage in, garbage out. - Inline warn on stderr when REMARK absent but scheme provided (scheme-aware fallback path is allowed but worth flagging in logs). * R18 absolute SASA on motif PColumn - `MotifHit` (python) gains `sasa: Optional[float]`, passed through `_score_motif_hit` from the SASA lookup. - `_MOTIF_COLUMNS` parquet schema adds a `sasa: Double` column. - `workflow/src/specs.lib.tengo` adds `motifColumnSpecs.sasa` (visibility optional, ".2f" format) and appends to `motifColumnIds`. - `model/src/index.ts:MotifHit` adds `sasa?: number | null` (optional so reports from older runs still parse). - `MotifsPage.vue` sourceId bumped `motifs-v3 → motifs-v4` so AG-Grid column-order cache doesn't survive the shape change. Software image needs a rebuild for the python changes to land: cd software && PL_DOCKER_BUILD=true PL_DOCKER_NO_AUTOPUSH=true pnpm build * spec: R39 thresholds.json + R33 hallmark warn + R35 stream + R12 lock Four spec-compliance fixes batched together — all touch python / diagnostics / model, none touch the input wire (R1 stays for the next slice). * R39 — Raybould thresholds move out of inline constants - New `software/src/data/thresholds.json` with `cohortSize: 242` for Fv (Raybould 2019 Table 2) and `cohortSize: 36` for VHH (Gordon 2025 CDRH3 compactness). Citation strings live next to the values. - `scoring.py` loads the file at import, coerces JSON `[lo, hi]` arrays back to tuples so the runtime flag predicates stay unchanged. - VHH PSH/PPC/PNC/totalCdrLength thresholds remain Fv-fallbacks (per the existing `{**_FV_THRESHOLDS, **_VHH_THRESHOLDS}` merge) — the JSON deliberately omits them since pinning the TNP values is M1 calibration work, not in scope here. * R33 — hallmark tetrad: informational read → mismatch warn - `check_hallmark_tetrad` now compares observed residues against canonical IgG (V/G/L/W) and VHH (F/E/R/G ± tolerated variants) sets at Kabat 37/44/45/47 (≡ IMGT 42/49/50/52). - Uses ≥3/4-match-and-strictly-greater scoring so ties / partial data stay "ambiguous" and don't false-flag. - When the implied identity disagrees with the chain-count mode (R7), emits a stderr warn explaining the likely engineered / chimeric construct. - Return shape gains `impliedMode`, `impliedScore`, `mismatch`. - `main.py` passes the resolved `mode` through to the check so it has both sides for the comparison. * R35 — confidenceGated motifs split out as `uncertainLiabilities` - JSON report's `motifs[]` now contains only the confident calls; gated calls move to `report.uncertainLiabilities[]`. - PColumn-side motifs table still carries every hit (the gated flag column distinguishes them); the JSON split is downstream- consumer-facing only. - TypeScript `LiabilitiesReport.uncertainLiabilities?: MotifHit[]` is optional so old reports still parse. - `ClonotypeDetailPanel.vue` renders a dedicated "Uncertain liabilities" section below the confident motifs, mirroring the spec's intent that the user can see what was suppressed. * R12 — drop user-tunable rSASA cutoff - BlockData v10 migration strips `rsasaBuriedCutoff` from persisted state. The python's `--rsasa-buried-cutoff` CLI default (0.075, Raybould 2019 canonical) is the single source of truth. - `MainPage.vue` Advanced-thresholds drops the slider; copy explains why it's now hardcoded. - Workflow's `main.tpl.tengo bodyExtra` and `process-pdb.tpl.tengo numericOptArgs` no longer forward the value. - `.subtitle()` drops the rSASA segment from the parameter summary. Software image needs a rebuild for the python changes: cd software && PL_DOCKER_BUILD=true PL_DOCKER_NO_AUTOPUSH=true pnpm build * spec R1: switch input wire from PlRef to PrimaryRef envelope Spec R1 mandates the block accept primary input via `PrimaryRef`, the standard `{__isPrimaryRef, column: PlRef, filter?: PlRef}` envelope from `@platforma-sdk/model`. Until now `BlockData` carried a bare `pdbRef: PlRef` and the workflow / model outputs read it directly — type was loose, the optional `filter` clause of R1 was unimplemented. BlockDataV11 migration: - `pdbRef: PlRef | undefined` → `primaryRef: PrimaryRef | undefined` - migration body wraps any existing PlRef via `createPrimaryRef(pdbRef)` so block instances on disk transparently upgrade. `filter` stays `undefined` (R47 subset selection is a follow-up). Plumbing: - `.args()` returns `primaryRef` instead of `pdbRef`; throws when `primaryRef.column` is empty (same UX as before). - Model outputs `clonotypePdbsMap` and `clonotypeAxisId` read `ctx.args.primaryRef.column` (or fall through to data) instead of `pdbRef`. - Workflow's `wf.prepare` resolves `args.primaryRef.column` for the per-clonotype `processColumn` iteration. - UI `PlDropdownRef` binds to a `computed<PlRef>` getter/setter: reads `data.primaryRef?.column`, writes back via `createPrimaryRef(value)` (PrimaryRef is frozen + has a discriminator brand, so direct v-model would silently mis-mutate). R46 (`PlDatasetSelector` UI) and R47 (`PrimaryRef.filter` subset selection) remain blocked on the upstream block adding `pl7.app/isAnchor: "true"` to its `pdbsMap` column — separate slice. * F3a: graph-maker distributions + slideover polish Migrate the six distribution pages to graph-maker (deletes hand-rolled SVG MetricHistogram / StripPlot / chart.ts; PR #87 in core/visualizations supplies the threshold lines, value labels, and bounds extension). Slideover: read labels off scoresTable.fullPframeHandle (the standalone clonotypeLabelsPf output returns empty under domain-bound axes), tint surface-metric values by flag color, keep only the red badge, and use the resolved label as the panel title. Sweep em / en dashes from UI, model, and workflow source. * spec BlockData.detectedMode: resolve dataset-level mode after run Refreshed spec (work/projects/3d-structures-and-clustering/block-structure-liabilities.md lines 222-231) defines BlockData.detectedMode as a dataset-level mode field driving R51 column selection, R54 mode-specific histogram, and R55 subtitle prefix. The spec says the model resolves it from a workflow output, but a model output callback cannot read PColumn data sync; the UI resolves it instead via a new useDetectedMode composable that reads the per-clonotype pl7.app/liabilities/mode column (uniform per R7) and writes back to app.model.data.detectedMode. BlockData v12 adds the field; existing instances migrate with detectedMode undefined and the watcher fills it once scoresTable becomes available. * spec R52: open viewer slideover with colorScheme='uncertainty' Refreshed spec R52 wants the viewer to open colored by predicted-error confidence (= the 'by-confidence' theme). structure-viewer's 'uncertainty' theme is the same thing; pass it as the new colorScheme prop landing in milaboratory/visualizations#89 (issue milaboratory/visualizations#88). The block consumes the prop locally via a pnpm.overrides entry pointing at a worktree-built tgz until the visualizations PR merges and the catalog version bumps. * fixup R52: rename viewer prop to initialColorScheme Follows milaboratory/visualizations#89 review rename: the prop is initial-value-only (read once at setup, no watch), so the name now reflects that. Slideover behavior is unchanged: each open remounts the viewer and re-seeds the theme to uncertainty. * ui: drop inline R52 comment in MainPage Code is self-evident from the attribute name. * spec trim: drop hydrophobicityScale selector (R48 removed) Refreshed spec deletes R48 entirely and locks Hydrophobicity to KD min-max-normalized to [1.0, 2.0] at the Concept level. Drops the BlockData field (V13 migrate strips it), UI dropdown, workflow arg passthrough, --hydrophobicity-scale CLI flag, and the four non-KD scale tables in biochem.py. compute_metrics no longer takes a scale parameter; the JSON report no longer carries hydrophobicityScale. * spec trim: drop per-residue drill-down (R37 / R53 out of scope) Refreshed spec marks 'Per-residue liability drill-down (motif-level detail panel + per-clonotype JSON report)' explicitly out of scope. Removes: - Python: --output flag + liabilities.json write + uncertainLiabilities split + chains/scores/surfaceMetrics report dict. - Workflow: liabilitiesJson per-row Resource output + jsonsPf builder + liabilitiesJsonsData export + liabilitiesJsons export. - Model: clonotypeJsonsMap output + LiabilitiesReport / MotifHit / CysteineHit / ChainSummary / SequenceRiskClass / Fixability types. - UI: ClonotypeDetailPanel.vue + RiskSummaryBar.vue + useClonotypeDetailFetch.ts. Slideover collapses to viewer only per R52. * vendor: pin in-flight visualizations + structure-viewer tgzs Reviewer flow: pnpm install pulls graph-maker / miplots4 / structure-viewer from ./vendor/ instead of the catalog so the block builds against the work in milaboratory/visualizations#87 and #89 (initialColorScheme prop, histogram threshold lines + numeric labels, hard-narrow bounds). Drop the override block and vendor/ in a cleanup commit once those PRs merge and the catalog bumps. * spec R51: switch mode-specific flag column on detectedMode Refreshed spec R51 default-visible set includes only the mode-relevant surface metric flag (sfvcspFlag for TAP, cdrh3CompactnessFlag for TNP), not both. scoresTable build now reads ctx.data.detectedMode and demotes the off-mode flag column's pl7.app/table/visibility annotation to 'optional' so the user can still reveal it via 'show all'. Before the first successful run the field is undefined and both columns fall through to their workflow-side default visibility. * vendor: README explaining the in-flight visualizations pin * spec R55: subtitle format matches spec, reads detectedMode Spec wants 'TAP, rSASA<0.075, confidence-gated FR>4 Å / CDR>6 Å' with the mode prefix omitted before the first successful run. Drops the numbering scheme + chain-mapping suffix (spec doesn't include them in the example; R10 says fallback uses upstream pl7.app/structure/numbering annotation, not a user-supplied scheme). * spec R54: consolidate to 5 histograms with mode-specific slot Spec lists five distribution pages (PSH, PPC, PNC, mode-specific, developability score). Replaces the two separate SFvCSP + CDRH3 compactness routes with one /histogram-mode-specific page that picks the metric at render time from detectedMode via useDetectedMode. Sidebar label is mode-aware (reads SFvCSP (Fv) / CDRH3 compactness (VHH) / Mode-specific distribution before the first run). * spec: run_liabilities.py CLI + drop per-motif/per-cys drill-down Aligns Python with the spec Software Interface (Section: Software Interface in the refreshed spec): python run_liabilities.py \ --pdb-dir ./pdbs/ \ --pdb-index pdb_index.tsv \ --fr-confidence-gating-threshold 4.0 \ --cdr-confidence-gating-threshold 6.0 \ --rsasa-buried-cutoff 0.075 \ --output-tsv ./results/per_clonotype.tsv run_liabilities.py replaces main.py: reads pdb_index.tsv, loops over each PDB, emits a per_clonotype.tsv with R38 scalar columns + R39 flags. Drops parquet emission and the pframe_writer helper. CLI flag names match the spec verbatim (--fr-confidence-gating-threshold, etc). Workflow: the per-row process-pdb.tpl.tengo template writes a one-row pdb_index.tsv via cmd.writeFile and invokes run_liabilities.py with the new CLI; processColumn aggregates per-row TSV slices via Xsv tsv target. Per-row architecture retained (parallelism). Drops the motifs + cysteines parquet targets and their pFrame builders / exports. Model: drops motifsTable + cysTable outputs (per-residue drill-down out of scope per refreshed spec). UI: deletes MotifsPage.vue + CysteinesPage.vue + corresponding /motifs and /cysteines routes and sidebar entries. * spec: module layout + data file renames per Module Layout software/src -> software/liabilities-script. Files match the spec's Module Layout section: liabilities-script/ +- data/ | +- Hydrophobics.txt (extracted from biochem.py KD dict) | +- heavy_atom_max_sasa.tsv (converted from ala_x_ala_refs.json) | +- thresholds.json +- main.py (was run_liabilities.py) +- structure.py (parser + numbering + diagnostics merged) +- motifs.py +- cysteines.py +- metrics.py (biochem folded in as Section 1) +- scoring.py main.py loads heavy_atom_max_sasa.tsv via TSV parsing; metrics.py loads Hydrophobics.txt via simple whitespace parsing then min-max-normalizes in-process. Workflow Dockerfile context updates to ./liabilities-script and cmd to /app/main.py. * spec: single-shot workflow + 3-column pdb_index.tsv Drops pframes.processColumn per-row; workflow root iterates the PDB ResourceMap directly (pdbCol.data.inputs() + maps.getKeys), addFile-s every blob, writeFile-s a single pdb_index.tsv with three columns (sampleId<TAB>scClonotypeKey<TAB>filename), and runs python /app/main.py once. The resulting per_clonotype.tsv is imported via xsv.importFile; axes come straight from the upstream PDB column so output PColumns preserve the [sampleId, scClonotypeKey] anchor per R2. Python main.py reads the 3-column index, prepends sampleId + clonotypeKey to the output TSV. The sampleId column is the necessary extension to the spec's 2-column example so the R2 anchor survives import. Files removed: workflow/src/process-pdb.tpl.tengo (no per-row template needed). specs.lib.tengo's buildScoresXsvSettings now takes the two axis specs as arguments (inherited from the upstream PDB column at workflow time) instead of declaring a structureId placeholder. R43 trace assembly is left for follow-up; processColumn's traceSteps option doesn't have an xsv.importFile equivalent. Block-id domain is still attached per column via specs.lib.tengo so trace can be reattached without an output shape change. * fix workflow: PDB column has 1 axis (scClonotypeKey), not 2 Runtime fail: 'expected 2 (sampleId + scClonotypeKey), got 1'. Spec R2 says the PDB column carries [sampleId, scClonotypeKey] but the deployed upstream block exposes a single-axis column keyed on scClonotypeKey alone (matches the previous per-row processColumn output shape). Reverts the index TSV + output TSV to the spec's 2-column / single-key shape (clonotypeKey only, no sampleId prefix). xsv inherits the lone scClonotypeKey axis from the resolved PDB column's axesSpec[0]. * fix: unbreak block runtime on PrimaryRef path Three independent bugs that together kept the block from rendering or running end-to-end against the upstream 3D Structure Prediction output: 1. UI null guard: `t.value.fullPframeHandle` threw when `scoresTable` returned `{ok: true}` with no `value` (block not yet computed). Now guards both `t.ok` and `t.value` so MainPage and the mode-specific histogram page both render empty-state cleanly before the first run. 2. Workflow exports: a raw PFrame in `exports.scores` failed the runtime "expected spec and data export structure" check; now returns `exports: {}` (empty, but the key must exist). 3. pdb_index.tsv filenames must be relative to `--pdb-dir`. Switched to bare names while `addFile` still drops files at `pdbs/<name>.pdb`, killing the doubled-path lookup miss in Python. * vendor: refresh graph-maker + miplots4 to PR #87 head Re-pack vendored visualizations tgzs from `feat/histogram-thresholds` head (3aa6912d) so the block consumes the Elena review fix that drops the legacy LineShape string form for `significantLinesStyle`. Pre-fix tgzs produced an unbinned histogram with stray threshold-line labels on the 1-clonotype run path. * spec R52: row double-click opens viewer Spec wording is `row double-click → PlSlideModal`; previously the axis cell open-button fired on single-click. Flip `cell-button-invoke-rows-on-double-click` to true so the trigger matches; the existing `@row-double-clicked` handler keeps the rest of the row clickable on double-click too. * spec R5/R29: consume upstream cdrh3Length as compactness numerator Spec R5/R29 wants the R30 compactness numerator to come from upstream's `pl7.app/structure/cdrh3Length`, not from the in-block REMARK 99 / scheme-fallback CDR3 Cα count. Workflow: `wf.prepare` builds a bundle anchored on the PDB column and addMulti-s `pl7.app/structure/cdrh3Length` on the shared scClonotypeKey axis. When found, `wf.body` exports it to a TSV sidecar via `xsv.exportFrame` and stages it into the exec call with `--cdrh3-lengths`. Skips silently when upstream doesn't emit the column so the in-block count remains the fallback. Python: new `--cdrh3-lengths` CLI flag loads the TSV at startup (keyed by scClonotypeKey, integer-coerced), plumbed through `analyze_pdb` and `compute_metrics` into `_cdrh3_compactness_imgt` where the per-clonotype upstream value (when present) replaces `len(cdr3_ca)` as the numerator. * spec R1: apply PrimaryRef.filter clonotype subset Spec R1 wording: "optional `PrimaryRef.filter` reduces the clonotype set." Filter slot has been on the envelope since the chunk-1 work but the workflow ignored it. Wire it through end-to-end: Model: add `filterOptions` output surfacing compatible Boolean/Int PColumns the upstream prediction block emits (`pl7.app/structure/predictionSuccessful`, `pl7.app/structure/confident`). UI: split `primaryRef` into two computeds. The new `primaryRefFilter` drives a second `PlDropdownRef` in Settings; `createPrimaryRef(col, filter)` rebuilds the envelope on either change. Workflow: `wf.prepare` adds the filter as a `bb.addSingle(..., "filter")` when set, sharing the PDB anchor's scClonotypeKey axis. `wf.body` exports the resolved column via `xsv.exportFrame` and stages it as `clonotype_filter.tsv` with a new `--clonotype-filter` CLI flag. Python: loads the TSV into a `keep_clonotypes` set, parsing common falsy spellings (`0`, `false`, `no`, `null`, empty, `0.0`). Skips clonotypes outside the set before iteration. No-op when the user didn't pick a filter (the in-block path stays the default). * spec R4: per-residue confidence JSON fallback Spec R4: when the PDB's B-factor column is missing (e.g. crystal PDBs or non-ImmuneBuilder synthesis), fall back to upstream's `pl7.app/structure/confidence/perResidue` JSON column for the R34 confidence-gating signal. Workflow: `wf.prepare` adds the JSON column as a `bb.addMulti(...)` enrichment on the PDB anchor's scClonotypeKey axis. `wf.body` exports to a TSV sidecar via `xsv.exportFrame` and stages with `--per-residue-confidence`. Python: loads the TSV, parses each clonotype's JSON (`[{pos, chain, errorAngstroms}, ...]`) into a per-clonotype `{(chain, pos): error}` map. `_mean_b_factor` now takes an optional fallback map + chain id; when the PDB's mean heavy-atom B-factor is 0/missing it returns the upstream errorAngstroms for the residue. `detect_motifs` / `_score_motif_hit` thread the per-clonotype map through. Behavior unchanged when upstream doesn't emit the column or when B-factor is present (the in-PDB value still wins). * spec R43: attach pl7.app/trace on single-shot path Workflow comment admitted R43 trace was deferred during the per-row to single-shot migration. Pattern from 3d-structure-clustering: build `pSpec.makeTrace(pdbCol.spec, {type, id, importance, label})` from the upstream PDB column's existing trace, then iterate the imported `scoresXsvPf` map and stamp every column via `pfb.add(k, trace.inject(v.spec), v.data)`. Downstream blocks consuming our PColumns now see the full upstream chain ending in this block id. * spec R39: pin VHH thresholds from TNP source assign_flag() Spec R39 said VHH PSH/PPC/PNC/totalCdrLength thresholds were "pinned at M1 from TNP source `bin_*` functions". The values live in oxpig/TNP `bin/TNP`'s `assign_flag()` function (not `bin_*`, but the function the spec refers to). Transcribed verbatim with cohortSize 36: totalCdrLength: bidirectional, green 25-36, amber 20-24 OR 37-39, red <20 OR >39 psh: bidirectional, green 79.60-126.82, amber 73.40-79.59 OR 126.83-155.47, red <73.40 OR >155.47 ppc: high_bad, amber 0.39-1.18, red >1.18 pnc: high_bad, amber 1.47-1.88, red >1.88 cdrh3Compactness: bidirectional, green 0.82-1.56, amber 0.56-0.81 OR 1.57-1.61, red <0.56 OR >1.61 `scoring.py` was falling back to Fv values for the four shared metrics when mode == TNP. Now uses the VHH dict end-to-end. Citation in the JSON points to the TNP repo + assign_flag() function name so the source is traceable. Minor edge-case correction to the cdrh3Compactness bands: spec text listed green 0.82-1.57 but TNP source has amber starting at 1.57 (green effectively 0.82-1.56). Source-authoritative. * chore: bump block-tools 2.8.1->2.9.1 + tengo-builder 3.0.1->3.0.4 CI preflight `require-latest` gate enforces newest @platforma-sdk/block-tools and @platforma-sdk/tengo-builder. Match the latest published versions. * cleanup: DRY repeated patterns + stdlib idioms Pure refactor; zero behavior change. Build + type-check green. Python (`liabilities-script/`): - main.py: extract `_iter_clonotype_keyed_tsv(path)` helper. Three ~15-line near-identical loaders for cdrh3_lengths / clonotype_filter / per_residue_confidence collapse to one helper + 3 short callers. -60 lines. - main.py: `_safe_float` NaN check moves from `f != f` to `math.isnan(f)`. - cysteines.py + metrics.py: replace `math.sqrt((a.x - b.x)**2 + (a.y - b.y)**2 + (a.z - b.z)**2)` with `math.dist((a.x, a.y, a.z), (b.x, b.y, b.z))` at three sites (one-liner since Python 3.8). - metrics.py: extract `_centroid(atoms)` helper. The 7-line manual centroid computation inside `_cdrh3_compactness_imgt` collapses to a one-line `math.dist(_centroid(cdr3_ca), _centroid(anchor_ca))`. - metrics.py: `_heavy_atom_min_distance` from accumulator loop to `min(math.dist(...) for ...)` generator. Model (`model/src/index.ts`): - Extract `findScoresCol` / `pfFromScores` / `specFromScores` helpers. Six histogram metrics' 12 nearly-identical output callbacks (5 lines each) collapse to one-liners. -65 lines. Workflow + model comments: - Drop stale "R47 not yet applied" / "filter not yet wired" references; R47 is shipped. - Drop stale "(sampleId, scClonotypeKey, filename) triples" in workflow header; the index is 2-column (clonotypeKey, filename). - Drop stale "Hidden on the legacy single-PDB path" reference in the scoresTable comment; the legacy path was removed at v9. * test: add pytest suite covering spec edge cases + flag logic 65 tests across 4 files. Local run: 0.04s, all pass. No FreeSASA fixtures needed; every test exercises rejection paths (which fire before FreeSASA) or pure functions. Layout: software/liabilities-script/ tests/ __init__.py conftest.py # sys.path bootstrap for module imports pdb_fixtures.py # ATOM-record synthesis helper test_structure.py # parse_pdb, region_for, role_of_chain test_main.py # R7/R10 rejection paths via analyze_pdb test_motifs.py # R4 B-factor JSON fallback test_scoring.py # R39 flags (Fv + VHH) + R41/R41a composite requirements-dev.txt # pytest pin Spec edge cases covered (block-structure-liabilities.md:330): - PDB has 3+ chains → R7 reject (test_main) - Single chain > 180 aa → R7 reject as scFv (test_main) - Non-canonical chain IDs A/B → R9 mapping (test_structure) - REMARK 99 absent → R10 fallback (test_structure) - REMARK 99 + scheme absent → R10 fail-fast (test_main) - B-factor missing → R4 JSON fallback (test_motifs) Plus full Fv + VHH threshold coverage for `compute_flags` (`bidirectional`, `high_bad`, `low_bad` paths) and `compute_developability` (motif/flag/cys contribution accounting + R41a risk classification). Ordering nit fixed: R10 fail-fast moved before compute_sasa in analyze_pdb so the rejection skips FreeSASA cost. Same wall-clock on the happy path; faster on misconfigured runs. Adds `test` script to software/package.json (cd liabilities-script && python -m pytest tests -q). Locally: requires pytest installed (see requirements-dev.txt). CI integration is a follow-up. * test: align Python layout + tooling with workspace standard Match `clonotype-distribution`'s setup so the test layout reads identically across blocks: Layout - Move tests from `software/liabilities-script/tests/` to `software/tests/`. Tests no longer ride along inside the Docker build context (`software/liabilities-script/`), so the image shrinks slightly and tests don't get baked into the runtime. - Drop `tests/__init__.py` (reference doesn't use one). Tooling - Replace `requirements-dev.txt` with `software/pyproject.toml` carrying `[project]` metadata + `[dependency-groups] dev` for pytest 9.0.2+ and pytest-cov 6.0.0+, matching the reference. - `[tool.pytest.ini_options]` declares `testpaths = ["tests"]` and `pythonpath = ["liabilities-script", "tests"]` so module imports work without a manual `sys.path.insert` hack in conftest. - Generate `uv.lock` via `uv lock`. Workspace convention is to use uv for Python dep management; lockfile commits resolved versions. - Update `software/package.json` `test` script to `python -m pytest -q` (now driven by pyproject.toml). Tests - Update `from tests.pdb_fixtures import ...` to plain `from pdb_fixtures import ...` (pyproject's pythonpath puts `tests/` on sys.path, dropping the package qualifier). - Slim conftest.py to a placeholder docstring. All 65 tests still pass locally (0.05s). Block build green (7/7). * cleanup: trim UI comments + shared types + dead destructure Pure cleanup; zero behavior change. ptableCell.ts - Add `readNullableNumber(col, i)` (returns null for missing cells) for the case where 0.0 and "no value" must stay distinguishable. - Export `ScoresTableOutput` as the shared shape; useClusterAssignments and useRunSummaryAlerts both consume the same handle. useClusterAssignments - Drop the duplicated `ScoresTableOutput` local type. - Replace the inline readString-then-readNumber double-check on tmDist/tmScore with `readNullableNumber`. - Trim the unused `clusterMap` and `hasClusterData` exports; no caller destructured either. Composable interface shrinks to just `{ selectedClusterAssignment }`. useRunSummaryAlerts - Drop the duplicated `ScoresTableOutput` local type. - Clarify the `data[flagIndices.length]` indexing with one extra comment line. MainPage - Drop unused `clusterMap` from the destructure. - Strip stale comments: "previous tab UI" (replaced long ago), the `clonotypeJsonsMap` reference (column gone), the by-rsasa / by-hydrophobicity color-scheme list (not in the refreshed spec), the "hydrophobicity scale" reference in the Settings comment (control was removed at V13). - numberingScheme dropdown's "unknown" option no longer reads ", unknown (no region weighting) ," (em-dash residue from the no-em-dashes feedback). Now: "unknown (no region weighting)". Build green (UI + block pack). * docs: rewrite README + CHANGELOG + changeset for current shipping state The README and CHANGELOG described the F1/F2/F3 PoC era of the block: tabs (gone, replaced by row-click slideover), hand-rolled SVG histograms (migrated to graph-maker), `motifs` / `cysteines` / `liabilitiesJsons` PFrames (out of scope per refreshed spec), `hydrophobicityScale` selector (removed at V13), `software/src/` directory (wrong, it's `liabilities-script/`), `structure-liabilities-block/` docs path (wrong, spec moved to `3d-structures-and-clustering/`). Two existing changesets carried stale wording too: hand-rolled SVG, R48 hydrophobicity, R47 deferred. SDK version numbers in the bump changeset were also pre-CI-bump (2.8.1 / 3.0.1 instead of 2.9.1 / 3.0.4). Replaced both stale changesets with `spec-compliance-pass.md` covering the actual PR content: PrimaryRef + filter (R1/R47), R4/R5/R29 sidecars, R7/R10 fail-fast, R39 Fv + VHH thresholds, R43 trace, R51 row table + mode-specific flag, R52 row double-click slideover, R54 five graph-maker histograms, R55 subtitle, plus the 65-test pytest suite. Documents the 3 external blockers (R46, R52 prop, R54 thresholds) with their pending upstream PRs linked. Also: - Drop `workflow/src/wf.test.ts`: comment-only file; the real test lives in `test/src/wf.test.ts`. Removing avoids vitest discovery confusion. - Fix `test/src/wf.test.ts`: replace em-dashes (no em-dashes feedback) and stale `pdbRef` reference (it's `primaryRef` on the envelope now). - Sweep em-dashes from the new pytest suite (test_structure / test_motifs / test_scoring). 11 occurrences from initial draft. 65 tests still pass; block build 7/7. * cleanup: workflow + Python pass Pure refactor; zero behavior change. 65 pytest cases still pass; full block build green (7/7). Workflow (main.tpl.tengo): - Extract `withSidecar(cmd, bundleKey, filename, cliFlag)` helper for the optional enrichment sidecars. Two near-identical 7-line blocks (cdrh3 lengths, per-residue confidence) collapse to two one-liner calls returning the extended `cmd`. Filter stays inline since it uses the single-column accessor. - Extract `withOptArg(cmd, flag, value)` for the three optional CLI string knobs (numbering scheme, chain-h, chain-l). Three 3-line guarded `cmd.arg(...)` chains collapse to three one-liners. - Replace the `pdbIndex := ""` accumulator loop with one-shot `text.join(indexLines, "\n") + "\n"`. - Drop dead `rowCount` increment (never read after the loop). - Hoist `ll` import to module level (was lazy-imported inside wf.body's panic branch only). Python (metrics.py): - Extract `_weight_fns(in_bridge)` factory. The same three weight closures (hydrophobicity / pos_charge_abs / neg_charge_abs) were defined inline in `_chain_metrics` (per-chain, VHH branch) AND again in the TAP branch over the unified H+L `in_bridge`. Both callsites now unpack a single helper call. - Drop the local `h_scale` / `h_glycine` aliases, the helper reads the module constants directly. Python (main.py): - Replace `isinstance(surface_metrics, dict)` with the simpler `sm = surface_metrics` (compute_metrics always returns a dict; the paranoia check is unwarranted). - `hallmark` can be None (per its signature), so the mismatch check is now `if hallmark and hallmark.get("mismatch")`. Em-dash sweep: - Drop em-dashes / en-dashes across all Python source (cysteines.py, motifs.py, scoring.py, structure.py, metrics.py) per the no-em-dashes feedback. ~30 occurrences total. - `check_hallmark_tetrad` warning message switched the em-dash sentinel `'—'` to `'?'` so missing residues show up readably. * cleanup: strip dead motif/cys PFrame setup from specs.lib.tengo The motifs and cysteines per-residue PFrames are out of scope in the refreshed spec ("Out of scope: per-residue liability drill-down"), so the column-spec dictionaries and Xsv builders that produced them have been dead since the spec refresh. main.tpl.tengo only consumes `specs.buildScoresXsvSettings`; everything else was orphaned. Dropped: - `motifAxesSpec`, `motifColumnSpecs`, `motifColumnIds` (~100 lines) - `cysAxesSpec`, `cysColumnSpecs`, `cysColumnIds` (~120 lines) - `motifAxisParquetMapping`, `cysAxisParquetMapping` - `buildMotifsXsvSettings`, `buildCysXsvSettings` - `xsvAxesFromMapping` (only the dead builders used it) - `scoresAxesSpec`: unused single-element placeholder from the `pt.saveFrameDirect` era. The single-shot `…
1 parent a6f6d81 commit ea76d7e

64 files changed

Lines changed: 12536 additions & 3719 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/ane-review-pass.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@platforma-open/milaboratories.3d-structure-based-liabilities": minor
3+
"@platforma-open/milaboratories.3d-structure-based-liabilities.model": minor
4+
"@platforma-open/milaboratories.3d-structure-based-liabilities.ui": minor
5+
"@platforma-open/milaboratories.3d-structure-based-liabilities.workflow": minor
6+
"@platforma-open/milaboratories.3d-structure-based-liabilities.software": minor
7+
---
8+
9+
Review pass: declared `supportedPlatforms` in block meta, removed the >10% red-flag clonotype alert, renamed columns to plain English (Surface hydrophobicity / Positive-charge patches / Negative-charge patches / Fv charge symmetry / Developability cost), switched flag vocabulary to None/Medium/High consistent with the developability risk discrete tiers, added column descriptions for hover tooltips, surfaced an Export button on the Main view, moved histogram titles into the graph-maker plot slot, switched freeSASA to Shrake-Rupley, removed the VHH-PSH same-type restriction that flagged every nanobody red, restructured disulfide classification to one row per canonical pair, and added discrete-filter + score annotations on developabilityRisk so the lead-selection block can filter on it.

.changeset/sequence-liabilities.md

Lines changed: 0 additions & 12 deletions
This file was deleted.

.changeset/spec-compliance-pass.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@platforma-open/milaboratories.3d-structure-based-liabilities": minor
3+
"@platforma-open/milaboratories.3d-structure-based-liabilities.model": minor
4+
"@platforma-open/milaboratories.3d-structure-based-liabilities.ui": minor
5+
"@platforma-open/milaboratories.3d-structure-based-liabilities.workflow": minor
6+
"@platforma-open/milaboratories.3d-structure-based-liabilities.software": minor
7+
---
8+
9+
Per-clonotype structure-based liability scoring with a metrics table, structure viewer, and per-metric distribution histograms. The Python tool ships via the python-3 run-environment (no Docker needed in dev), score columns are exported to the result pool for downstream blocks, and the dataset mode is emitted as a workflow output. Adds an editable block label and settings tooltips.

CHANGELOG.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
11
## 1.0.0
22

3-
Initial release. Sequence-based liability detection for therapeutic antibody
4-
candidates: unpaired cysteines, deamidation hotspots (N-G, N-S), N-linked
5-
glycosylation sequons (`N-X-[S/T]`), and oxidation-prone residues (M, W).
6-
7-
Detection is sequence-only — surface-accessibility filtering and
8-
antibody-specific (CDR-aware) analysis are out of scope for this release.
3+
Initial release. Per-clonotype structural developability analysis (Raybould 2019 TAP for paired Fv, Gordon 2025 TNP for VHH) consuming PDB structures from the 3D Structure Prediction block. See README.md for inputs / outputs / UI.

README.md

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
1-
# 3D-Structure-Based-Liabilities
1+
# 3D Structure-Based Liabilities
22

3-
Identifying potential liabilities in therapeutic antibody
4-
candidates using 3D structure analysis. Takes a protein structure
5-
in PDB format through preprocessing and liability detection to
6-
flag developability risks.
3+
Per-clonotype structural developability analysis for therapeutic antibody candidates. Consumes PDB structures from the [3D Structure Prediction block](../3d-structure-prediction/) and reproduces [Raybould 2019 TAP](https://doi.org/10.1073/pnas.1810576116) (paired Fv: surface hydrophobicity / positive-charge patches / negative-charge patches / Fv charge symmetry) and [Gordon 2025 TNP](https://github.com/oxpig/TNP) (VHH: same metrics with type-restricted charge patches plus CDRH3 compactness), with **region-aware per-residue confidence gating** using ImmuneBuilder B-factor as predicted error.
4+
5+
Sequence-only liability scanners (see our sister [antibody-sequence-liabilities](../antibody-sequence-liabilities/) block) flag every regex match without knowing whether the chemically reactive atom is solvent-exposed. This block adds 3D context: filters motif hits by rSASA, weights each hit by region (CDR3 > CDR1/2 > FR), gates low-confidence regions, and adds structural-only signals (surface charge / hydrophobicity patches, free-Cys state, CDR3 compactness).
6+
7+
## Inputs
8+
9+
`PlDatasetSelector` over `pl7.app/structure/pdb` anchor PColumns. Optional clonotype filter (Boolean/Int subset PColumn, e.g. `predictionSuccessful`, `confident`). Settings: numbering scheme (IMGT default), heavy / light chain overrides (auto-detected from REMARK 99), FR / CDR confidence thresholds (defaults 4.0 / 6.0 Å for ImmuneBuilder).
10+
11+
## Outputs
12+
13+
Per-clonotype scalar PColumns keyed on `pl7.app/vdj/scClonotypeKey`, bundled in the `scoresData` PFrame with `pl7.app/blockId` domain. Composite cost (`structuralDevelopabilityScore`) plus categorical risks (`structuralDevelopabilityRisk`, `structuralIntegrityRisk`); per-metric raw values + threshold flags (None / Medium / High); cysteine and motif counts; per-metric low-confidence-residue fractions. Mode-specific columns (`sfvcsp` for Fv, `cdrh3Compactness` for VHH). Full list with annotations in `workflow/src/specs.lib.tengo`.
14+
15+
## UI
16+
17+
- **Main**: `PlAgDataTableV2` with default-visible columns; row double-click opens a `PlSlideModal` with `PlStructureViewer` (Mol\*, `initialColorScheme="uncertainty"`). Cluster badge surfaces when the 3D Structure Clustering block is upstream.
18+
- **Five distribution pages**: hydrophobicity, positive charge patches, negative charge patches, mode-specific (Fv charge symmetry or CDRH3 compactness), developability cost, via graph-maker with Raybould / Gordon threshold lines.
19+
- **Run-summary alert** fires when more than 25% of clonotypes have at least one confidence-gated motif.
20+
21+
## See Also
22+
23+
- [`software/liabilities-script/`](software/liabilities-script/) , Python source
24+
- [`software/tests/`](software/tests/) , pytest suite covering rejection and scoring paths

block/package.json

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
2-
"name": "@platforma-open/milabs.3d-structure-based-liabilities",
2+
"name": "@platforma-open/milaboratories.3d-structure-based-liabilities",
33
"version": "1.0.0",
44
"scripts": {
55
"build": "shx rm -rf ./block-pack && block-tools pack",
66
"mark-stable": "block-tools mark-stable -r 's3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1'",
7-
"prepublishOnly": "block-tools pack && block-tools publish -r 's3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1'",
7+
"prepublishOnly": "block-tools pack && block-tools publish --unstable -r 's3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1'",
88
"do-pack": "shx rm -f *.tgz && block-tools pack && pnpm pack && shx mv *.tgz package.tgz"
99
},
1010
"files": [
@@ -13,18 +13,18 @@
1313
],
1414
"dependencies": {
1515
"@platforma-sdk/model": "catalog:",
16-
"@platforma-open/milabs.3d-structure-based-liabilities.workflow": "workspace:*",
17-
"@platforma-open/milabs.3d-structure-based-liabilities.model": "workspace:*",
18-
"@platforma-open/milabs.3d-structure-based-liabilities.ui": "workspace:*"
16+
"@platforma-open/milaboratories.3d-structure-based-liabilities.workflow": "workspace:*",
17+
"@platforma-open/milaboratories.3d-structure-based-liabilities.model": "workspace:*",
18+
"@platforma-open/milaboratories.3d-structure-based-liabilities.ui": "workspace:*"
1919
},
2020
"block": {
2121
"components": {
22-
"workflow": "@platforma-open/milabs.3d-structure-based-liabilities.workflow/dist/tengo/tpl/main.plj.gz",
23-
"model": "@platforma-open/milabs.3d-structure-based-liabilities.model/dist/model.json",
24-
"ui": "@platforma-open/milabs.3d-structure-based-liabilities.ui/dist"
22+
"workflow": "@platforma-open/milaboratories.3d-structure-based-liabilities.workflow/dist/tengo/tpl/main.plj.gz",
23+
"model": "@platforma-open/milaboratories.3d-structure-based-liabilities.model/dist/model.json",
24+
"ui": "@platforma-open/milaboratories.3d-structure-based-liabilities.ui/dist"
2525
},
2626
"meta": {
27-
"title": "3D-Structure-Based-Liabilities",
27+
"title": "3D Structure-Based Liabilities",
2828
"logo": "file:../logos/block-logo.png",
2929
"url": "https://github.com/platforma-open/3D-Structure-Based-Liabilities",
3030
"support": "mailto:support@milaboratories.com",
@@ -36,7 +36,14 @@
3636
"name": "MiLaboratories Inc",
3737
"url": "https://milaboratories.com/",
3838
"logo": "file:../logos/organization-logo.png"
39-
}
39+
},
40+
"supportedPlatforms": [
41+
"linux-x64",
42+
"linux-aarch64",
43+
"macosx-x64",
44+
"macosx-aarch64",
45+
"windows-x64"
46+
]
4047
}
4148
},
4249
"devDependencies": {

docs/description.md

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,61 @@
11
# Overview
22

3-
Identifies sequence-level developability liabilities in therapeutic antibody
4-
structures supplied as PDB files. The block parses the uploaded structure and
5-
reports four classes of hits per chain:
6-
7-
- **Unpaired cysteines** — Cys residues not participating in any SSBOND. Free
8-
Cys can drive aggregation, mispair during expression, or be oxidized.
9-
- **Deamidation hotspots** — N-G and N-S dipeptide motifs. The N-G motif in
10-
particular shows the highest rate of non-enzymatic deamidation to aspartate
11-
/ iso-aspartate.
12-
- **N-glycosylation sequons** — N-X-[S/T] consensus motifs (X ≠ P). Surface-
13-
exposed sequons in CDRs are a major manufacturability risk.
14-
- **Oxidation-prone residues** — methionine and tryptophan positions.
15-
M oxidizes to methionine sulfoxide under formulation stress, W to
16-
kynurenine derivatives under photo-oxidation.
17-
18-
## Caveats
19-
20-
All detections are **sequence-only**. A buried Met is reported the same as a
21-
surface-exposed one; a sequon in framework is reported the same as one in a
22-
CDR. Real developability assessment requires solvent-accessible surface area
23-
filtering and antibody numbering (Kabat / Chothia / IMGT) to identify CDRs —
24-
both planned for a future release.
3+
Per-clonotype structural developability analysis for antibody candidates.
4+
Consumes PDB structures from the **3D Structure Prediction** block upstream
5+
and emits per-clonotype liability calls, surface developability metrics, and
6+
composite developability costs that downstream blocks (Lead Selection)
7+
can rank candidates on.
8+
9+
## What it does
10+
11+
For each predicted Fab / VHH, the block runs:
12+
13+
- **Surface-exposed motif detection** — the 11-pattern Raybould motif set
14+
(deamidation N-G, isomerization, N-glycosylation sequons, oxidation M / W,
15+
integrin RGD, etc.), filtered to surface-exposed residues only so buried
16+
false positives drop out.
17+
- **Cysteine state classification** — one row per canonical disulfide pair
18+
(`disulfide` when both Cys are present and bonded, `disulfide_broken` when
19+
both present but not bonded, `disulfide_missing` when at least one
20+
canonical position has no Cys) plus per-residue `cys_extra` rows for
21+
non-canonical Cys.
22+
- **Surface developability metrics** — Raybould 2019 verbatim for paired Fv
23+
(surface hydrophobicity, positive-charge patches, negative-charge patches,
24+
Fv charge symmetry) and Gordon 2025 for VHH (same metrics with
25+
type-restricted charge patches plus CDRH3 compactness).
26+
- **Region-aware confidence gating** — uses the ImmuneBuilder-emitted B-factor
27+
as per-atom predicted error; low-confidence motifs stay in the table for
28+
traceability but are excluded from the composite cost.
29+
- **Composite cost + categorical risks**`structuralDevelopabilityScore`
30+
(motif + metric-flag bumps + cysteine contributions),
31+
`structuralDevelopabilityRisk` (None / Low / Medium / High), and
32+
`structuralIntegrityRisk` (Present / None).
33+
34+
## UI
35+
36+
- **3D viewer** (Mol*) for the selected clonotype, side-by-side with a
37+
per-clonotype detail panel (motifs grouped by type, cysteine state, surface
38+
metrics with None/Medium/High flag badges, composite risk readout).
39+
- **Results table** with default-visible columns + a "Columns" toggle that
40+
reveals raw metric values + low-confidence fractions.
41+
- **Five distribution pages** — one per metric — that render Raybould / Gordon
42+
threshold lines so the user can read each candidate's standing against the
43+
literature thresholds without leaving the chart.
44+
- **Run-summary alert** fires when more than 25% of clonotypes have at least
45+
one confidence-gated motif.
2546

2647
## Input
2748

28-
A single `.pdb` file. The block reads ATOM/HETATM records from the first
29-
MODEL block and SSBOND records to identify residues and disulfide pairs.
49+
A `pl7.app/structure/pdb` PColumn from the upstream 3D Structure Prediction
50+
block. The settings slide-modal exposes the numbering scheme, optional
51+
chain mapping overrides (auto-detected from REMARK 99 in the common case),
52+
and advanced thresholds calibrated for ImmuneBuilder.
53+
54+
## Honest scope
55+
56+
Flags reflect the **predicted apo conformation**. CDR3 in particular can
57+
rearrange on antigen binding, so a residue buried in the apo prediction may
58+
be exposed in the bound state. The value-prop is "removes apo-state-buried
59+
false positives", not "removes all false positives". The per-metric
60+
`<metric>LowConfidenceResidueFraction` column lets users discount metrics
61+
dominated by low-confidence regions of the prediction.

model/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@platforma-open/milabs.3d-structure-based-liabilities.model",
2+
"name": "@platforma-open/milaboratories.3d-structure-based-liabilities.model",
33
"version": "1.0.0",
44
"description": "Block model",
55
"type": "module",
@@ -20,6 +20,8 @@
2020
"check": "ts-builder check --target block-model"
2121
},
2222
"dependencies": {
23+
"@milaboratories/graph-maker": "catalog:",
24+
"@milaboratories/helpers": "catalog:",
2325
"@platforma-sdk/model": "catalog:"
2426
},
2527
"devDependencies": {

0 commit comments

Comments
 (0)