Spec compliance + SDK 1.77 bump - #2
Merged
Merged
Conversation
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.
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.
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.
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.
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.
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.
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.
…7, 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).
…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.
…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.
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.
…lish) 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.
…53, 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.
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.
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.
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.
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.
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.
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.
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).
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.
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
- 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.
xnacly
force-pushed
the
chore/update-sdk-deps
branch
from
May 21, 2026 11:16
f6aae6a to
b2ff61f
Compare
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.
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.
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.
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
milaboratory/visualizations#87 merged on 2026-05-28 and shipped as `@milaboratories/graph-maker` 1.4.3 (with miplots4 transitively). The vendor pin was dropped in 3bde140; two callouts still framed the histogram threshold rendering as "vendored, waiting on #87": - ui/src/components/HistogramPage.vue: comment said "honored by the histogram template after milaboratory/visualizations PR #87"; swapped to "(graph-maker 1.4.3+, miplots4 transitively)". - .changeset: dropped the "R54 still vendored" caveat from the one-paragraph summary, added the graph-maker 1.4.3 line to the externally-shipped list. `progression.md` updated separately (kept uncommitted per the "never commit progression docs" rule).
main.py gains --mode-output, which the workflow reads to emit a scalar detectedMode output. Docker packaging is unchanged. Rename the package org to milaboratories.
Emit the dataset mode as a scalar detectedMode output (from the Python mode.json) and re-emit the upstream PDB ResourceMap as pdbsMap, so the model reads both directly. Export the trace-stamped score pframe to the result pool. Drop spec-id comments.
Add a typed BlockArgs (types.ts), resolve detectedMode and the PDB map from workflow outputs instead of a result-pool double-cast, and replace createPlDataTableV2 with createPlDataTableV3. Add an editable block label via customBlockLabel.
Delete useDetectedMode and its watch (the output to data hairpin); the mode page and label now read the model output. Move settings remarks into tooltips, rename the input to 3D Structures, add the editable subtitle label, and trim custom CSS toward design tokens.
Finish renaming the package org from milabs to milaboratories, fix the block meta title, and refresh the changeset and lockfile. GitHub repo rename to lowercase still pending.
1.4.3 ignores the pl7.app/graph/thresholds annotation; threshold-line rendering shipped in 1.4.4. Restores the dashed amber/red lines on the distribution histograms.
_iter_clonotype_keyed_tsv matched a hardcoded scClonotypeKey column name, which is absent for non-single-cell datasets and silently returned no rows (broke filter datasets). Use the first column as the key instead. Remove the one-off derive_ala_x_ala_refs.py dev script shipped in the block.
Drop the custom empty-state page (the table not-ready text covers it) and the fixed-height wrapper so the table fills the page. Strip description/threshold headers from histogram pages and shorten plot titles. Keep the viewer's uncertainty coloring per spec R52.
The default subtitle contained < and > which break rendering in the block-list view. Reword without angle brackets.
The histogram pages drew dashed threshold lines via graph-maker but
nothing on the page explained what they meant. Adds a small
color-keyed legend rendered above the chart, one pill per band:
- Green (light-green): the safe range
- Amber: the borderline range
- Red: the bad-flag range
Wired through `HistogramConfig.thresholds: { green?, amber?, red? }`
with five metrics populated (PSH/PPC/PNC/SFvCSP/CDRH3 compactness);
developability score has no fixed band cuts so it stays legend-less.
Also restores the subtitle's comparison semantics. Commit c907d32
stripped `<` / `>` because the block-list view parsed them as HTML
tags, but the replacement `rSASA 0.075, FR 4 Å` lost the
"buried below" / "gated above" meaning. Swapped in `≤` / `≥`
(U+2264 / U+2265), which are separate codepoints from the bare
ASCII brackets and render literally:
TAP, rSASA ≤ 0.075, gating FR ≥ 4 Å / CDR ≥ 6 Å
The threshold legend was rendering at the top of the page, above the chart's internal title row. Lift the page heading to PlBlockPage via its `title` prop and clear the duplicate chart-internal title in makeGraphState. Order is now: page heading -> legend pills -> chart.
Color names told the user what color a pill was, which is already visible. Swap to state words that name what the value means: - Green pill -> "Pass" - Amber pill -> "Borderline" - Red pill -> "Fail" Same pill colors (the visual encoding) so the chart's dashed threshold lines still pair with their corresponding band; the text now adds severity instead of restating the color.
The block-overview sidebar caught the model's `.subtitle()` callback throwing and substituted the literal placeholder string "Invalid subtitle" (pl-middle-layer/.../project_overview.ts:218). Cause: the args-only context the middle layer constructs for sidebar rendering can surface `ctx.data` as undefined before block storage parses — and our callback dereferenced `ctx.data.customBlockLabel` unconditionally. Same hazard inside `defaultBlockLabelFor` reading `data.frConfThresh` / `data.cdrConfThresh`. Optional-chain both: `ctx.data?.customBlockLabel || ...` in the subtitle callback, and widen `defaultBlockLabelFor`'s data arg to `Partial<BlockData> | undefined` with `data?.fr...` / `data?.cdr...` reads. The block page (which always has data) is unaffected; the sidebar now resolves to the rendered default instead of the error placeholder.
The previous fix (optional-chain `ctx.data?.customBlockLabel`) did not stop the sidebar's "Invalid subtitle" error. Root cause was deeper: `resolveMode(ctx)` reads `ctx.outputs?.resolve(...)`, and the `ctx.outputs` getter itself throws "Staging context not available" when called from the middle layer's args-only context (js_render/computable_context.ts:116). Optional chaining on `ctx.outputs?` does not help because the throw happens inside the getter, before the chain has a value to short-circuit. Wrap the `ctx.outputs?.resolve(...).getDataAsJson<string>()` call in try/catch and return undefined when outputs are unavailable. The sidebar then renders the default label without the mode prefix; the block page (which has full outputs access) keeps the mode prefix. Verified via pl logs: zero "Error in block model subtitle" entries since the dev-block reload that picked up this change.
…ilities entrypoint
Deriving from the PDB ref's spec failed when its scClonotypeKey axis carried a domain the table column didn't (or vice versa). Reading the axis from the first scoresData column ensures the AxisId matches the table column's id byte-for-byte, so PlAgDataTableV2's isJsonEqual(axisId, showCellButtonForAxisId) wins.
Removed in the previous commit's clonotypeAxisId refactor but left dangling. oxlint --deny-warnings flags it.
Catalog updates: - graph-maker 1.4.4 -> 1.4.5 - @platforma-sdk/block-tools 2.9.1 -> 2.10.6 - @platforma-sdk/model 1.77.4 -> 1.77.20 - @platforma-sdk/ui-vue 1.77.4 -> 1.77.20 - @platforma-sdk/test 1.77.5 -> 1.77.20 - runenv-python-3 1.7.8 -> 1.10.1 - turbo 2.8.11 -> 2.9.16 - changesets/cli 2.29.8 -> 2.31.0 Held vue at 3.5.24 because 3.5.35 pulls TypeScript 5.9.3 via peer resolution and the stricter Plugin type check there rejects @platforma-sdk/ui-vue's SdkPluginV3 install signature. Held SDK at 1.77.20 instead of 1.78.6 for the same reason: 1.78.x requires the newer Vue/TS combo that fails the same check. Major bumps for workflow-tengo (5->6) and tengo-builder (3->4) deferred to a separate change.
Add Block modal's version dropdown filters by platform match. With no supportedPlatforms declared, the preset version ends up outside the (empty) options list and the modal reports "The selected value is not one of the options." Declared all 5: linux-x64 / linux-aarch64 / macosx-x64 / macosx-aarch64 / windows-x64. The Python tool runs in either the Docker image (linux-x64/aarch64) or the runenv-python-3.12.10 binary mode (all 5) so every supported runtime covers the matrix.
Block: - declare supportedPlatforms so the version dropdown matches the preset - drop "more than 10% red flag" alert (kept the gated-motif alert) UI labels and flow: - column headers: PSH / PPC / PNC / SFvCSP / Developability score become Surface hydrophobicity / Positive-charge patches / Negative-charge patches / Fv charge symmetry / Developability cost - flag cell values: green / amber / red become None / Medium / High, matching the developability-risk discrete tiers - histogram pages: chart title comes from graph-maker so the page no longer stacks a heading above the chart, and the legend pills follow the None / Medium / High vocabulary - section labels: Hydrophobicity, Positive charge patches, Negative charge patches, CDRH3 compactness / Fv charge symmetry, Developability cost - table now exposes the SDK Export button (show-export-button) - every metric / flag / count column carries a pl7.app/description so AG-Grid renders a hover tooltip Spec annotations for downstream filter consumers: - structuralDevelopabilityRisk: isDiscreteFilter / discreteValues (None, Low, Medium, High) / isScore / rankingOrder=decreasing / defaultCutoff [None, Low] - structuralIntegrityRisk: isDiscreteFilter / discreteValues (None, Present) - flagColumnSpec helper now stamps isDiscreteFilter with the shared None / Medium / High vocabulary Python: - freeSASA pinned to Shrake-Rupley (Raybould 2019 / Gordon 2025 reference algorithm; default Lee-Richards drifts the rsasa scale) - VHH PSH no longer restricted to hydrophobic-only pairs; the same-type predicate was collapsing the sum 5-10x and flagging every nanobody red. PPC and PNC keep their same-type restriction - disulfide classification reworked to emit one row per canonical pair (disulfide / disulfide_broken / disulfide_missing) plus per-Cys rows for non-canonical extras. Fixes both the double-count by residue and the partial-deletion double-classification Threshold cohort calibration with the new SASA algorithm and the fixed VHH PSH sum is a follow-up for Ane to verify.
Per Ane's review: the runtime SASA algorithm flipped from FreeSASA's default Lee-Richards to Shrake-Rupley (Raybould 2019 / Gordon 2025 reference algorithm). The Ala-X-Ala reference table the runtime divides by to compute rSASA was generated under the old default, so rSASA was silently inconsistent between numerator (per-residue Shrake-Rupley) and denominator (per-residue Lee-Richards). Restored software/scripts/derive_ala_x_ala_refs.py (dropped in 8877521), pinned its freesasa.calc to Shrake-Rupley too, and regenerated heavy_atom_max_sasa.tsv. Values shift by a few percent, mostly upward. rsasa_buried_cutoff stays at 0.075 per Ane: it's paper-derived under Shrake-Rupley already.
Two layout fixes on the histogram pages: - Drop the in-chart title (state.title = '') so the section nav is the only place that labels the page. Graph-maker was rendering its own editable title alongside the section nav, which read as a duplicate heading. - Move the threshold pills from a sibling div above the chart into graph-maker's titleLineSlot so they sit next to the title bar instead of pushing the chart down (the layout misalignment Ane called out).
Switched from coloured pills (heavy, busy in the title bar) to a single-line inline legend: 8px dot, bold tier label, muted range string. Better fit for the chart's title-bar real estate and visually consistent with the chart's own toolbar text.
Graph-maker's .chart_titleLineSlot defaults to margin-left: auto, pinning the slot to the right edge of .chart_header. Override globally (scoped styles can't reach the slot wrapper from inside the slot) so the legend sits next to the chart title on the left.
The block carried scattered references to an internal spec document (R-numbered citations, prose mentions of "the spec", a top-of-README link to docs/text/work/projects/...). The spec doc lives in the workspace, not in this repo; pinning the block's source on a moving target inside another repo is brittle and leaks an internal construct into a public block. Stripped from: - README.md, CHANGELOG.md, docs/description.md - data/thresholds.json and heavy_atom_max_sasa.tsv headers - Python sources (main, scoring, metrics, motifs, cysteines, structure) - workflow/src/specs.lib.tengo - ui/src/pages/histogramConfigs.ts - software/scripts/derive_ala_x_ala_refs.py - software/tests/* review.md is gone too (work-in-progress doc, no longer needed). Also reverted the chart-header left-align override, because the legend slot is graph-maker's concern. Default right-align stays.
Mirror 3d-structure-prediction's prepublishOnly: add --unstable so each main-branch push lands the block in the registry's unstable channel instead of going straight to stable. Promotion to stable stays a deliberate workflow_dispatch via mark-stable.yaml.
CI's require-latest preflight flagged tengo-builder as stale. The major bump is API-compatible with our workflow (pl-tengo check and build run clean against src/main.tpl.tengo + src/specs.lib.tengo).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
End-to-end implementation of the 3D Structure-Based Liabilities spec on a fresh SDK, consuming the upstream
3D Structure Predictionblock'spdbsMapPColumn.Spec compliance
48 of 48 in-scope requirements implemented. No remaining external blockers.
All three previously-external dependencies shipped during this branch:
platforma-open/3d-structure-prediction#13, merged + released as v1.0.11 (pl7.app/isAnchorannotation; R46PlDatasetSelectorunblock).milaboratory/visualizations#89, merged + published as@milaboratories/structure-viewer0.3.0 (R52initialColorSchemeprop).milaboratory/visualizations#87, merged + published as@milaboratories/graph-maker1.4.3 (R54pl7.app/graph/thresholdshistogram rendering). Vendor pin dropped; catalog now points at the published release.What ships
Single-shot architecture:
wf.prepareresolves the upstreampl7.app/structure/pdbPColumn from the PrimaryRef envelope and builds a bundle for enrichments (cdrh3Length,confidence/perResidue, optional filter).wf.bodystages every PDB into one exec call, runspython main.pyonce against the whole dataset, importsper_clonotype.tsvviaxsv.importFile, stampspl7.app/traceon every emitted PColumn.Python (
software/liabilities-script/): R7 chain-count fail-fast, R10 numbering source check, R11 in-block heavy-atom Ala-X-Ala SASA references, R15a salt-bridge detection, R16-R20 motif scoring with rSASA gating + region weights + R20 exposure logistic, R21-R23 four-state cysteine classification, R24-R33 Raybould 2019 (TAP) / Gordon 2025 (TNP) surface metrics with R31 type-restricted pair filter and R28 whole-V-domain SFvCSP, R34/R36 region-aware confidence gating, R39 verbatim thresholds (Fv: Raybould Table 2 cohortSize 242 / VHH: TNPassign_flag()cohortSize 36), R41 composite + R41a categorical risks.UI:
PlDatasetSelectorover anchor-markedpdbsMap.PlAgDataTableV2with R51 default-visible columns; mode-specific flag selected fromBlockData.detectedMode. Row double-click opensPlSlideModalcontainingPlStructureViewer(Mol*,initialColorScheme=\"uncertainty\") plus a cluster badge when the 3D Structure Clustering block is upstream. Five distribution histograms via graph-maker withpl7.app/graph/thresholdslines. R44 / R45 run-summary alerts.SDK catalog bump
@platforma-sdk/model@platforma-sdk/ui-vue@platforma-sdk/workflow-tengo@platforma-sdk/tengo-builder@platforma-sdk/block-tools@platforma-sdk/test@milaboratories/ts-builder@milaboratories/structure-viewer@milaboratories/graph-makerPins:
@milaboratories/helpers,vue3.5.24,oxlint1.63.0.Tests
65 pytest tests at
software/tests/covering R4 / R7 / R9 / R10 / R39 (Fv + VHH) / R41 / R41a paths. Block-load smoke test intest/src/wf.test.ts.Cleanup arc
Post-audit cleanup pass collapsed ~500 net lines across the codebase: dropped the dead SSBOND cross-check chain + 9 unused
CysteineHitfields, factoredcompute_metricsinto separate TAP / TNP helpers, deduped UI / model boilerplate, migrated MainPage inline styles to scoped CSS, trimmed README + CHANGELOG to workspace convention, dropped deadclonotypeLabelsPfmodel output, and centralised duplicated amino-acid maps / regex compiles. Full arc: see git log on this branch.