Context: apps/vxdata-jobs/src/vxdata/jobs/f_20260702_vxannotate_sync (mono
PR #161) syncs vxAnnotate submissions (PIRADS/PIQUAL scoring, annotations,
comments, artifacts) into vxData.
ID mapping (the part that’s expensive to rediscover)
- An annotation’s
series_id= the vxAnnotateSeries.idUUID. - Retrieve that series:
Series.series_identifier="{volume_uid}_{description}". - vxData
Volume.identifier="volume/{volume_uid}". VxAnnotateCaseRecord.included_resources= the case’s Volume identifiers.- Artifact comments target the mask’s series Volume; annotator comments
target the case’s
ImagingStudy(parent_identifier). reference_mricomes frommask.series_id— never derive it from a T2/transversal heuristic.PIRADSLesionAssessment.derived_fromlinks to the lesion’sVoxelMap.
Score payload shapes
- PIRADS is per-lesion, camelCase keys (
T2Score,DWIScore,laterality,zones, …);label_name= the lesion mask id.zonesis a JSON-encoded string array, not a native array. - PIQUAL is case-level, snake_case.
- vxAnnotate’s
piradsextension is lesion-only — vxData’s globalPIRADSAssessment(index_lesion_id,score,free_text) has no annotation source from this sync and would need a v2.1 derivation; it is intentionally left unproduced. - Turn silent fallbacks into loud raises for: missing annotator email,
missing
label_name, unmapped submission status, non-strzones.
Mask truth model — use the SDK, don’t hand-roll RLE
MaskAnnotation.rle(NiiVue PackBits, native-resolution, binary per mask; class lives inlabel_value) is the authoritative source.- The export’s
session.mask_niftis(CBOR-baked) is best-effort only — it can come back empty or at the wrong resolution. Root cause found 2026-07-09: vxAnnotate frontendgenerateMaskNiftis(radiology/page.tsx) pickedseries.find(s => s.image_type != "SEG")— the first non-SEG series — as the reference volume for all masks, ignoringmask.seriesId;decodeRLE(rle, wrongVoxelCount)then truncated the native-res drawing into a background sub-region. Fixed in draft PRvirdx/vxannotate.com#51(per-series reference). - Use
vxa-sdk >= 0.1.5’sclient.exports.mask_niftis(reuses the stored NIfTI if it matches the RLE, else rebuilds from RLE +series.file) and the publicclient.series.file. Do not reconstruct RLE decoding in a consumer job — a job-localrle_helper.pyworkaround was built and then thrown away the moment SDK 0.1.5 shipped it properly. When the correct fix belongs in the SDK/frontend layer and is plausibly imminent, coordinate before building a workaround in your own job.
Known non-applicable path
essen01 (MRI project id c7cc1c0b-289e-4525-a2a1-6e9638f5b55a) has
NIfTI-uploaded series (no DICOMSeries), so the DICOM-bridge integration
path (f_20260305_vxannotate_integration) does not apply to it.
Hygiene note
Never git add <dir> on a job directory that contains untracked
creds/PHI (run.sh, run_debug.sh, vxa.json) — it silently stages them,
and a post-commit git reset does not undo an already-pushed commit
(history rewrite via --amend --force-with-lease doesn’t un-expose a
pushed secret either — rotate it). Keep such files gitignored per job.
Job layout and SDK-surface split (verified 2026-08-06)
f_20260702_vxannotate_synchas ahandlers/directory (not ahandlers.py); thePROJECT_HANDLERSregistry lives inmain.py.- The job isolates its heavy imaging stack in a dedicated pixi feature
(
[tool.pixi.feature.vxannotate.dependencies], own solve-group) and depends on the published condavxa-sdk >=0.1.5,<0.2rather than a source build. Reuse that shape for any job pulling a large third-party stack. - Two SDK surfaces coexist in
apps/vxdata-jobs: ~51 older jobs still buildResourceCreateRequestobjects, which no longer exist in currentvxdata-schemas, so those jobs cannot run as written. Copyf_20260612_essen01_histoinstead — it is the current-surface template (typedClientnamespaces, flat schemas). See vxdata-sdk-and-schema-model.md.