Commit ea76d7e
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
File tree
- .changeset
- block
- docs
- model
- src
- software
- liabilities-script
- data
- scripts
- src
- tests
- test
- src
- ui
- src
- components
- pdb
- composables
- pages
- workflow
- src
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
This file was deleted.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
4 | | - | |
5 | | - | |
6 | | - | |
7 | | - | |
8 | | - | |
| 3 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | | - | |
4 | | - | |
5 | | - | |
6 | | - | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
| 7 | + | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
17 | | - | |
18 | | - | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | | - | |
23 | | - | |
24 | | - | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
| 27 | + | |
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
40 | 47 | | |
41 | 48 | | |
42 | 49 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
4 | | - | |
5 | | - | |
6 | | - | |
7 | | - | |
8 | | - | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
21 | | - | |
22 | | - | |
23 | | - | |
24 | | - | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
25 | 46 | | |
26 | 47 | | |
27 | 48 | | |
28 | | - | |
29 | | - | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
| 24 | + | |
23 | 25 | | |
24 | 26 | | |
25 | 27 | | |
| |||
0 commit comments