Single source of truth for what is happening right now. Every session reads this after CONTRIBUTING.md and plan.md. Rules: edit only your own status rows and your own role section. The shared log at the bottom is append-only: add new entries at the very end, never edit old ones. Format is in plan.md §12.3.
.gitattributessetsmerge=unionfor this file.
| Role | Person | Machine / GPU | Groq key variable |
|---|---|---|---|
| P1 Imaging Core | GROQ_KEY_AUDIO | ||
| P2 Data, Training & Validation | laptop, RTX 4050 6 GB | GROQ_KEY_JUDGE | |
| P3 Clinical Reasoning & Trust | GROQ_KEY_REPORT | ||
| P4 Experience & Platform | GROQ_KEY_EXTRACT |
- Everyone: repo access, git identity set
- Everyone: own Groq key in local
.env - Everyone:
git config core.hooksPath .githooks(hook verified to reject assistant mentions) - P2: Kaggle token; RSNA rules accepted; BDNeuro-MRI downloaded
- P3: HF token; MedGemma 1.5 + MedSigLIP terms accepted; Kaggle phone-verified
- P4: skeleton committed;
make setupgreen on a fresh clone - Groq
/modelschecked at T+0; IDs in config match (record below) - Local NVIDIA GPU available? (who, VRAM):
| Gate | Target time | Criterion | Status | Notes |
|---|---|---|---|---|
| G0 | T+1 | Contract frozen, fixtures committed | todo | |
| G1 | T+4 | CXR end to end with real output | todo | |
| G2 | T+7 | All 4 modalities end to end; status rule + firewall live | todo | |
| G3 | T+9 | Metrics frozen in reports/ |
todo | |
| G4 | T+11 | Feature freeze | todo | |
| Submit | T+13 | Repo link submitted via form; backup video recorded | todo |
States: todo · doing · blocked · done · cut
| WP | Task | State | Branch | Updated | Note |
|---|---|---|---|---|---|
| P1.1 | Decode PNG/JPG/DICOM + hashing | done | p1/intake | 2026-10-06 | intake/decode.py, intake/preprocess.py |
| P1.2 | DICOM PHI scrub | done | p1/intake | 2026-10-06 | intake/phi.py |
| P1.3 | Quality gate | done | p1/intake | 2026-10-06 | thresholds provisional, refit on real data |
| P1.4 | Router (MedSigLIP probe) | done | p1/intake | 2026-10-07 | real MedSigLIP; held-out n=50 acc 1.00 (CI 0.93 to 1.00); zero-shot 95.6% on 320; classes come from separate datasets, so the 97% mixed-set number is P2's; body-part head unvalidated |
| P1.5 | OOD score | done | p1/intake | 2026-10-07 | Mahalanobis per modality, AUROC 1.0 on held-out (small n); energy score tested and rejected (AUROC 0.05 on chest); thresholds provisional |
| P1.6 | CXR reader + anatomy zones | done | p1/intake | 2026-10-07 | 67 reader tests incl. 5 on real weights; localization quality not yet benchmarked (P2.7) |
| P1.7 | Bone reader wrapper | done | p1/intake | 2026-10-07 | trained bone_det weights (hash matches model_meta.json) load through BoneReader.from_dir; real boxes on staged FracAtlas films (scores 0.58 and 0.74 on two fractures, near 0 on non-fractures); check_trained_models.py ok |
| P1.8 | Brain + skin inference wrappers | done | p1/intake | 2026-10-07 | trained brain_cls, brain_seg and skin_cls load and run; Grad-CAM++ boxes, U-Net tumour masks (0.6-4.8% of slice), faithfulness and stability measured per model (results/faithfulness_trained.json); MedSAM masks on the trained skin classifier's CAM boxes checked on a staged ISIC image (real MedSAM weights, masks of about 47,000 px, no flags) |
| P1.9 | Faithfulness test (D1) | done | p1/intake | 2026-10-07 | chest: 0/18 findings pass on 12 films, random regions 0/18. Trained models on staged images: brain 42%, skin 44% pass (heatmap test), bone 6 of 7 fracture findings pass (new box-region test, equal-area random boxes) |
| P1.10 | Saliency sanity check (D2) | done | p1/intake | 2026-10-07 | chest table committed; gradcam++ chosen, gradcam and hirescam collapse to empty maps; single film, repeat on more; other models wait for weights |
| P1.11 | Stability score (D5) | done | p1/intake | 2026-10-07 | validated on real chest model; flip rate 0 at severity 2 on the sample film, severity 5 flips contrast |
| P1.12 | MedSAM service | done | p1/intake | 2026-10-07 | real MedSAM on 30 ISIC 2018 validation lesions: mean Jaccard 0.878, Dice 0.934, 0 failures below 0.65; CPU about 30 s per image |
| WP | Task | State | Branch | Updated | Note |
|---|---|---|---|---|---|
| P2.1 | Dataset downloads + manifests | done | main | 2026-10-07 | all script-fetchable sets present with manifests and a pinned lock file; BDNeuro registered from the manual download |
| P2.2 | Leakage audit + group splits | done | main | 2026-10-07 | all six datasets audited; reports/leakage.json |
| P2.3 | Brain classifier (Kaggle) | done | main | 2026-10-07 | done on Kaggle (second account); registered |
| P2.4 | Brain segmenter (Kaggle) | done | main | 2026-10-07 | done on Kaggle (second account); registered |
| P2.5 | Skin classifier (Kaggle) | done | main | 2026-10-07 | official-test BMA 0.680 [0.632, 0.728]; registered |
| P2.6 | Bone detector (Kaggle) | done | main | 2026-10-07 | mAP50 0.453, image AUROC 0.923; registered |
| P2.7 | CXR calibration + external validation | done | main | 2026-10-07 | RSNA: chex AUROC 0.785, mimic_ch 0.749, contaminated all 0.875; pointing game and IoU in reports/cxr.json |
| P2.8 | Temperature scaling + conformal | done | main | 2026-10-07 | backend/medproof/calibrate; skin official-test coverage 0.898 at the 0.90 target; chest and bone FNR within 3 points; brain over-covers (0.94) |
| P2.9 | Selective prediction | done | main | 2026-10-07 | AURC and risk-coverage for skin, brain, chest, bone; reports/figures/risk_coverage.png |
| P2.10 | Trust-signal validation (D13) | done | main | 2026-10-07 | unstable, abstain, OOD-energy predict errors; quality gate does not; discordant and unfaithful pending P3 and P1.9 (harness ready) |
| P2.11 | Subgroup audit | done | main | 2026-10-07 | age, sex, site, body part, view; worst subgroups named in reports/subgroups.json |
| P2.12 | Corruption benchmark | done | main | 2026-10-07 | 8 perturbations x 5 severities on 5 models; reports/corruption.json and figure |
| P2.13 | Model cards + datasheets | done | main | 2026-10-07 | docs/model_cards (8) and docs/datasheets (7), generated from metrics.json |
| P2.14 | make eval | done | main | 2026-10-07 | python ml/eval/run_all.py: about 100 s, byte-reproducible; reports/metrics.json is the single source |
| WP | Task | State | Branch | Updated | Note |
|---|---|---|---|---|---|
| P3.1 | MedGemma service + batch reads | doing | p3/medgemma-groq | 2026-10-07 | real read verified on the local RTX 4060 (bf16 4-bit, 3.2 GiB, 5.5 s/read after a 23 s load); FracAtlas batch complete (569 reads); HAM10000 batch complete (1,000 reads, run on a new CUDA venv .venv-medgemma); Kaggle/Colab notebooks untested; CXR box format still not verified |
| P3.2 | Generalist reader + concordance | done | p3/medgemma-groq | 2026-10-07 | reports/concordance.json final for both batches; concordance now matches vocabulary names and dataset codes, and a disagreement only demotes a finding in modalities listed in DOWNGRADE_VALIDATED (none yet: bone flags 83% of correct reports, skin effect not significant) |
| P3.3 | Groq pool | done | p3/medgemma-groq | 2026-10-07 | 27 tests with a mocked Groq; live call confirmed on gpt-oss-20b and 120b; Prompt Guard score() added |
| P3.4 | Synthetic note generator | done | p3/medgemma-groq | 2026-10-07 | ml/data/gen_notes.py, 200 notes: 60 contradictions, 48 attacks in 8 families, 12 benign look-alikes, clean twins; notes_v1.jsonl committed |
| P3.5 | Fact extraction with spans | done | p3/medgemma-groq | 2026-10-07 | exact-span F1 0.919 on 62 held-out notes (target 0.85); cue scan added for statements the model skips; end-to-end numbers in reports/p3_end_to_end_test.json |
| P3.6 | Injection guard | done | p3/medgemma-groq | 2026-10-07 | regex + Prompt Guard + LLM layers; 27 of 28 fresh attacks with all three layers (1 false positive in 15 benign), so the 100% target is not met on fresh attacks; two regex phrasings found by red-teaming are now covered |
| P3.7 | Contradiction rules | done | p3/medgemma-groq | 2026-10-07 | P/R 1.00/1.00 on gold facts (templated notes, so it shows the rules fire, not that they generalise); end-to-end with extracted facts not yet measured |
| P3.8 | Retrieval / precedents | done | p3/medgemma-groq | 2026-10-07 | MedSigLIP indices for all four modalities (train-only, or the calibration split for chest; own id and group excluded). Precision@5 against chance: FracAtlas 0.84 (0.70), HAM10000 0.67 (0.47), brain 0.92 (0.27), chest 0.67 (0.36); majority-vote balanced accuracy brain 0.95, chest 0.72, HAM10000 0.49. Precedent stage covers all four; the weakest class everywhere is the rare one (fracture 0.48, melanoma 0.43, chest opacity 0.57) |
| P3.9 | Slot-filled report | done | p3/medgemma-groq | 2026-10-07 | slot renderer, frames, model-chooses-frame drafts with offline template fallback; live gpt-oss-120b call verified |
| P3.10 | Hallucination firewall | done | p3/medgemma-groq | 2026-10-07 | R1-R12; 100% of 100+ planted bad claims blocked, 0 of 10 good claims blocked; R5 treats faithful=None as unverified until P1.9 |
| P3.11 | Entailment judge | done | main | 2026-10-07 | 60 labelled sentences: first pass 0.917; after fixing what the judge is shown, 59/60 on a fresh variant (kept 29/30 true, caught 30/30 defective) and 60/60 on the tuned one; judge outage leaves claims explicitly unchecked |
| P3.12 | Voice (Whisper) | done | main | 2026-10-07 | real round trip: synthesized dictation -> Whisper on Groq -> guard -> facts; spoken injection flagged and kept out of the facts |
| P3.13 | FHIR export | done | main | 2026-10-07 | collection Bundle (DiagnosticReport + Observations) validated with fhir.resources R4B; quotes redacted unless requested |
| WP | Task | State | Branch | Updated | Note |
|---|---|---|---|---|---|
| P4.1 | Repo skeleton + Makefile + tooling | done | main | 2026-10-07 | Makefile, root + backend pyproject tooling, pre-commit, minimal web/ (Vite+React+TS) |
| P4.2 | Contract + fixtures (G0) | done | main | 2026-10-07 | schemas.py unchanged (additive only); core/vocab.py, core/status_rule.py; JSON Schema + TS export; 4 fixtures |
| P4.3 | Pipeline orchestrator | done | main | 2026-10-07 | pipeline.py + core/{context,config,cache}.py; only intake + CXR reader stages exist today, new ones append to PIPELINE |
| P4.4 | API + SSE | done | main | 2026-10-07 | api/{app,studies,system}.py; FastAPI + sse-starlette; in-memory core/store.py (not persisted) |
| P4.5 | Evidence ledger | done | main | 2026-10-07 | core/ledger.py; single global hash-chained JSONL; GET /ledger/verify; doctor feedback logged |
| P4.6 | Web app pages | done | main | 2026-10-07 | Scrollytelling landing (prerendered, two Vite entries) + /read workstation + validation, models, audit, report, 404; Playwright + axe e2e green |
| P4.7 | WebGL viewer | done | main | 2026-10-07 | Own WebGL2 engine web/src/gl/; R16F full-depth path, W/L, overlays, keyboard map. 60 fps on 2048² not measured on a real GPU yet (see role section) |
| P4.8 | Docker + CI + e2e + a11y | todo | |||
| P4.9 | Offline demo pack | todo | |||
| P4.10 | README | done | main | 2026-10-07 | Submission README (what, pipeline, models, evidence, sample I/O, results with CIs, scope, install, configure, run, reproduce, licences); real chest run in demo/sample_output/; screenshots in docs/screenshots/ |
Now: every P1 package is implemented and validated on real models or data, including P1.7 and P1.8 on P2's trained weights (placed locally at ml/artifacts/<model>/weights.pt, git-ignored, hashes match model_meta.json).
Next: refit quality, faithfulness and OOD thresholds on real validation sets; P4 to wire the router and the faithfulness and stability stages for brain, skin and bone.
Blockers: none for P1.7 and P1.8. Still open for others: P2 must revoke the Kaggle token that was committed in .env (still in history); P2's test_eval_reproducible fails on this machine.
Replies to P4's assigned work (P1-A to P1-E), 2026-10-07:
- P1-A masks, done.
readers.classifier.run(ctx, reader, unet=None, medsam=None)andreaders.bone.run(ctx, reader, medsam=None)callsegmenter.attach_masksbeforeto_findings; the mask PNG goes underctx.artifact_dir. Loader calls:unet = UNetSegmenter.from_dir("ml/artifacts/brain_seg")(frommedproof.readers.segmenter),medsam = MedSAM.load()(frommedproof.segment.medsam; it readsMEDPROOF_MEDSAM_MODEL, a local folder or the hub id). Brain:classifier.run(ctx, reader=brain_reader, unet=unet); skin:classifier.run(ctx, reader=skin_reader, medsam=medsam); bone:bone.run(ctx, reader=bone_reader, medsam=medsam). Load once at start-up, not per study. A failing mask model keeps the finding and adds a generic warning. Checked end to end on the real trained models: brain finding gets heatmap + U-Net mask, skin gets heatmap + MedSAM mask, bone gets bbox + MedSAM mask. Needssegmentation-models-pytorch(in themlextra) on the demo laptop. - P1-B bone faithfulness, done.
verify.faithfulness.assess_outputnow runsassess_boxfor findings with a box and no heatmap: the strongest box is blurred and the probability drop is compared with equal-area boxes elsewhere (same permutation rule: p <= 0.10 and drop >= 0.05). Boxes above 35% of the image are "not assessable". Real bone detector on the 7 staged fracture films: 6 faithful, mean drop 0.43. No change needed in P4's stage chain: the existing faithfulness stage fillsfaithfulfor bone findings. - P1-C brain heatmap, investigated and acted on. On the demo brain image the heatmap peaks at the left image border (x=0) while the U-Net puts the tumour at x 327-437; deleting the tumour box does not beat random boxes (p=0.6). So the classifier is not using the tumour there. Measured on 16 staged brain slices with a U-Net mask: Grad-CAM++, Grad-CAM and HiResCAM, final or earlier layers, all put only 6-14% of their salient pixels inside the tumour mask (14 of 16 below 0.2 for the default). No CAM variant fixes it, so it is the model, not the explanation method. New rule: when the U-Net mask is attached and less than 20% of the heatmap's salient pixels fall inside it, the finding gets the flag
heatmap_off_targetand its heatmap evidence is markedfaithful=false(flagunfaithful) even if the deletion test passes, because a region the model uses but that is not the lesion cannot prove the finding. Provisional: the U-Net was trained on LGG FLAIR and is applied to a different dataset, so its masks are not validated on these slices. - P1-D router and OOD, ready to hand over. Verified from the exported folder: with
MEDPROOF_ROUTER=1andMEDPROOF_ROUTER_MODELset, uploads without a hint are routed andoodappears (distance, threshold, score, is_ood). Files (git-ignored, copied by hand):backend/artifacts/router_probe.npzsha2560f3042717e9230dd1be9c9e444a7b7c954115a48ef3f8f575099f07f44c97aba,backend/artifacts/ood.npzsha25638d268cc045bfb56bd713abe751567283ebbbb2a1216dc2f0b5bf4ea0e07d8be.python scripts/fetch_models.pydownloads MedSigLIP and MedSAM intobackend/models/with the machine's own HF token (the account must have accepted the MedSigLIP terms) and prints the two env lines;--from-cachecopies from a machine that already holds them. A ready folder set is atmedproof-kit/handover/on P1's machine (medsiglip/,medsam/, both.npz). - P1-E MedSigLIP for precedents, same folder.
MEDPROOF_ROUTER_MODELpoints at it; P3's embedder can use the same folder. For P2 (available now on branchp1/intake): - Decode any upload:
from medproof.intake.decode import load_imagereturnsDecodedImage(.displayuint8,.analysisfloat32 in [0,1],.sha256). DICOM handles rescale slope/intercept, window tags and MONOCHROME1 (RSNA). RaisesDecodeErroron bad input. Use this for pHash and for every eval loader so you see the pixels the product sees. - Training and serving preprocessing:
from medproof.intake.preprocess import prepare, get_spec; specscxr_xrv,brain_effnet,skin_cls,bone_yolo. Callprepare(decoded_or_float_array, "brain_effnet")to get a float32 CHW tensor. Change a spec only via a Decisions entry so train and serve stay equal. - Corruption benchmark:
from medproof.verify.perturbations import apply, NAMES, SEVERITIES;apply(name, img_float01, severity 1..5, seed). Eight perturbations, deterministic for a given seed. - Quality gate:
from medproof.intake.quality import assess, QualityConfig;assess(decoded, "cxr")gives.passed,.reasons(code, level, fix),.metrics. Thresholds are inQualityConfigand were set on a synthetic phantom; refit them on real validation images (target: at most 5% of clean images flagged). - Reader output shape:
medproof.readers.base.ReaderOutput(final with P1.6). - P2.7 handoff (CXR reader, available on
p1/intakeandmain):- Needs:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpuandpip install torchxrayvision. Weights download on first use to~/.torchxrayvision/(DenseNet 28 MB, anatomy PSPNet 273 MB). - Probabilities only:
CxrReader.load(CxrConfig(weights="densenet121-res224-chex")).score(img)returns an (18,) float32 array aligned toreader.labels(empty-string labels are unsupported by that weight set, ignore them). Formimic_chorchexexternal validation on RSNA use those weights;-allsaw RSNA (contaminated).MEDPROOF_CXR_WEIGHTSoverrides the default. - These outputs are NOT raw logits: torchxrayvision applies sigmoid then per-label operating-point scaling, so 0.5 is each label's operating threshold. Calibrate on these values as given and say so in the model card.
ReaderOutput.output_kindsays "probability". - Full read with localization:
CxrReader.load(anatomy_fn=AnatomySegmenter.try_load()).predict(img)thenevidence.to_findings(out, cfg, artifact_dir=...)gives contractFindingandImageEvidence(prob_calibratedequalsprob_raw, flaguncalibrated,conformal_set=[]: calibration fills these). Stage entry point:readers.cxr.run(ctx). - Localization eval: boxes are
bbox_xyxyin original-image pixels (max edge exclusive); heatmaps are float32 [0,1] on the original grid. The model reads only the central square of non-square images; heatmap outside it is exactly 0 and the finding carries flagpartial_view. RSNA images are square, so no effect there. - Preprocessing:
intake.preprocess.prepare(img, "cxr_xrv")now centre-crops to a square then resizes to 224 (TorchXRayVision's own convention). The spec changed from stretch to centre-crop, so any notebook using it for CXR should re-run; brain, skin and bone specs are unchanged.
- Needs:
- P1.4 to P1.12 handoff (all on
p1/intakeandmain):- Router:
intake.router.Router(embedder, config);run(ctx)stage returnspayload = {modality, body_part, confidence, probs, method, flags, ood}. Needspip install transformers scikit-learnand the gatedgoogle/medsiglip-448(HF token in.env; orMEDPROOF_ROUTER_MODEL=<local folder>).intake.run_bytes(raw, router=...)routes before the quality checks. Artifacts are git-ignored; register these hashes: proberouter_probe.npzsha2560f3042717e9230dd1be9c9e444a7b7c954115a48ef3f8f575099f07f44c97aba(45 KB), OODood.npzsha25638d268cc045bfb56bd713abe751567283ebbbb2a1216dc2f0b5bf4ea0e07d8be(21 MB). Both were trained on 320 public images; retrain withpython -m medproof.intake.router_train --data DIRandpython -m medproof.intake.ood --data DIR(DIR//... layout;scripts/stage_router_data.pybuilds a small one) on the full datasets for the final numbers. - P2, router and OOD evaluation:
intake.router_train.evaluate_folder(embedder, DIR)scores only the held-out groups;intake.ood.score_folder(embedder, DIR, OODModel.load(path))returns one row per image (distance, threshold, score, is_ood) for AUROC against other modalities and natural images. The 97% target is yours to report on the mixed held-out set; my staged set cannot show it (see Decisions). - Faithfulness:
verify.faithfulness.assess_output(reader, decoded, reader_output)thenapply_to_findings(findings, results)fillsImageEvidence.faithfulandfaithfulness_dropand adds flagunfaithful. Stability:verify.stability.assess_outputthenapply_to_findingsfills the contractStabilityand flagunstable(flip rate above 0.25). Both are model-agnostic: any reader with.score(img)works (CXR, classifier, bone). - P3: readers expose boxes as
bbox_xyxyin original-image pixels (max edge exclusive), oneImageEvidenceper finding plus a second mask-kind evidence when a U-Net or MedSAM mask exists; bone findings have no heatmap and use kindbbox. - P4 wiring recipe (the pipeline's reader step still says "no router available yet (P1.4)"): (1) when
ctx.modality_hint is None, callintake.router.run(ctx)(it usesget_default_router(), which is off unlessartifacts/router_probe.npzexists orMEDPROOF_ROUTER=1) and set the hint only whenpayload["confidence"]is at leastRouterConfig().trust_confidence(0.60); copy the router flags (ood,router_uncertain) onto every finding'sflags, sincecore/status_rule.pydowngradesood. (2) Reader:readers.cxr.run(ctx),readers.classifier.run(ctx, reader=ImageClassifierReader.from_dir(...))orreaders.bone.run(ctx, reader=BoneReader.from_dir(...)); each now leavesctx.reader,ctx.reader_outputandctx.findingson the context. (3) Thenverify.faithfulness.run(ctx, reader=ctx.reader)andverify.stability.run(ctx, reader=ctx.reader)return updated findings inpayload["findings"](faithful, faithfulness_drop, stability, flags). Budget: stability costs 8 forward passes (about 3 s on CPU for the chest model); faithfulness costs about 14 s per finding with the default 16 random placements (9 is the minimum), so run it on the top two or three findings or as a later refinement streamed over SSE. - P4: stage entry points are
intake.run.run,intake.router.run,readers.cxr.run,readers.classifier.run,readers.bone.run,verify.faithfulness.run,verify.stability.run,segment.medsam.run_box; each returns aStageResultand never raises.backend/pyproject.tomlgainedmlandservicesextras; please fold them into youruvsetup. Real-model tests skip with a reason unless the weights are on disk.
- Router:
- Setup:
cd backend, Python 3.12 venv,pip install -e .[dev,ml,services](CPU torch from https://download.pytorch.org/whl/cpu), thenpython -m pytest -q. Real-model tests needMEDPROOF_ROUTER_MODEL(local MedSigLIP folder),MEDPROOF_ROUTER_DATA(staged images) and the cached chest and anatomy weights.
Now: every P2 work package is done and pushed to main (P2.1 to P2.14). python ml/eval/run_all.py --check reproduces reports/metrics.json exactly in about 100 s from committed files only; docs/validation_report.md, 8 model cards, 7 datasheets and 6 figures are generated from it. Tests: python -m pytest ml/tests -q and cd backend && python -m pytest -q (555 passed).
Next: nothing blocking. Open items that need other roles are listed below. Optional P2 latitude if time remains: MILK10k external skin test (M4), a higher-resolution skin model or ensemble (official-test balanced accuracy is 0.68), the discordant and unfaithful signals once their inputs exist.
Blockers: none. BDNeuro-MRI licence is unconfirmed (README placeholder); the datasheet says so.
Machine: laptop, NVIDIA RTX 4050 6 GB.
For P1 (everything below is on main):
- Weights are not in git. They are in
ml/artifacts/<model>/weights.pton the P2 laptop and in the private Kaggle outputs (rishijayanath/parallax-p2-6-bone-fracture-detector;johannshonigeorge/parallax-p2-3-brain-mri-classifier,parallax-p2-4-brain-tumour-segmenter,parallax-p2-5-skin-lesion-classifier). Ask P2 to send them, or pull with the Kaggle CLI.ml/artifacts/registry.jsonhas the class order, preprocessing spec name, sha256 and licence for each. - Rebuild a classifier with
timm.create_model(arch, pretrained=False, num_classes=len(classes))andload_state_dict(torch.load(weights.pt)); the segmenter issmp.Unet("resnet34", encoder_weights=None, in_channels=3, classes=1); the bone model loads withultralytics.YOLO(weights.pt)andpredict(imgsz=640, conf=0.001)(the library letterboxes itself). The segmenter uses a new specbrain_seg(256 px, 3 channels, ImageNet mean and std, stretch): please add it tointake/preprocess.py(its definition isSEG_SPECinml/train/seg.py). - Calibration to apply at serving time, no torch needed:
from medproof.calibrate.calibrator import Calibrator; Calibrator.load("ml/artifacts/skin_cls/calibration.json").calibrate(logits)returns calibrated probabilities, a conformal set of class names (target coverage 90%) and a tier (high/moderate/low/abstain) per input; same forbrain_cls. Binary readers:BinaryCalibrator.load("ml/artifacts/bone_det/calibration.json")andml/artifacts/cxr_chex/calibration_lung.json(.prob(score),.decide(score)with the FNR-controlled report flag and abstention band). The product demo reads the chest modelall, which saw RSNA, so the calibrator fitted for it on RSNA is optimistic: either switch the product to thechexweights, or accept uncalibrated-looking confidence and say so. - Quality gate needs refitting. On real clean images it flags 43% (skin), 88% (brain), 45% (chest) and 91% (bone) of images, and flagged images are not wrong more often (reports/signals.json). Dominant reasons:
clipped(black borders in MRI and chest films count as clipped pixels),low_contrastandoverexposedfor dermoscopy,low_resolutionfor small FracAtlas images. Percentile tables per metric are in the same file. - Truncated JPEGs. 59 FracAtlas negatives lose their last few bytes;
load_imageraisesDecodeErroron them although OpenCV and tolerant PIL decode them fine. A tolerant fallback inintake/decode.pywould keep such uploads working. - fp16 inference on the brain classifier produced non-finite logits for one unusual image; serving on CPU float32 is unaffected, but guard against non-finite outputs.
- Skin ensemble (optional, recommended). Weights for the two extra seeds are
ml/artifacts/skin_cls_s2/weights.ptandskin_cls_s3/weights.pt(P2 will sendD:\\parallax_weights_ensemble.zip). Serving: average the three models' logits (optionally over four flips of the preprocessed tensor), thenCalibrator.load("ml/artifacts/skin_ens/calibration.json"). It costs three times the weights and inference time for about +0.023 balanced accuracy; keep the singleskin_clsif latency matters. - Faithfulness (P1.9) and stability (P1.11):
ml/eval/signals.py::compare(df, flag)validates any per-image boolean column against errors. The corruption benchmark already shows flip rate at severity 2 above 0.25 predicts errors strongly (skin 0.61 vs 0.13 error; brain 0.87 vs 0.03). For P3: specialist predictions on the same images as your MedGemma batches are cached:ml/artifacts/skin_cls/predictions/official_test.npz(ids, logits, y, group) andml/artifacts/bone_det/predictions/test.npz(ids, packed boxes). Once the reads are complete, build a per-imagediscordantcolumn and callsignals.compare; a bonediscordantflag must not downgrade findings until it passes. The chest reader is validated inreports/cxr.json. For P4:reports/metrics.jsonis the only file the validation page needs (headline rows with CIs and contamination status, calibration, selective prediction, trust signals, subgroups, corruption, OOD, leakage, registry);docs/model_cards/index.jsonanddocs/datasheets/index.jsonlist the cards and sheets for the/modelspage;reports/figures/*.pngare static fallbacks.ml/Makefile.incdefinesdata,eval,eval-check,figures,ml-test: please include it and ack the.gitignorecarve-out forml/data/(Decisions, 2026-10-06). Kaggle jobs: (notebook slug, started, status, artifact hash) - rishijayanath/parallax-p2-5-skin-lesion-classifier: 2026-10-07 01:00 to 01:17, COMPLETE. Official ISIC test balanced accuracy 0.689 [0.640, 0.738]; output not pulled (Kaggle rate limit), superseded by the rerun below
- rishijayanath/parallax-p2-6-bone-fracture-detector: v1 trained 80 epochs then failed at prediction export on truncated JPEGs (fixed in 8539baf); v2 COMPLETE 2026-10-07 02:56, pulled, registered as bone_det@8539baf9. Test (569 images, 103 fractured): mAP50 0.453 [0.367, 0.547], image AUROC 0.923 [0.886, 0.957], sensitivity 0.835 [0.757, 0.903] at the validation-fixed 90%-specificity threshold (test specificity 0.880)
- johannshonigeorge/parallax-p2-5-skin-lesion-classifier: rerun COMPLETE 2026-10-07 02:26, pulled, registered as skin_cls@a217517b. Official ISIC 2018 test balanced accuracy 0.680 [0.632, 0.728], melanoma recall 0.637, melanoma AUROC 0.924; the two runs differ by about 0.01 (GPU nondeterminism)
- johannshonigeorge/parallax-p2-3-brain-mri-classifier: COMPLETE 2026-10-07, pulled, registered as brain_cls@d351f062. Leakage-free test accuracy 0.940 [0.921, 0.958]
- johannshonigeorge/parallax-p2-4-brain-tumour-segmenter: COMPLETE, pulled, registered as brain_seg@d351f062. Mean per-patient Dice 0.831 [0.705, 0.891] over 9 test groups. A duplicate of this job also ran on rishijayanath (the launcher fired before I stopped it); its output is unused
Now: Everything that needs only P3 is done and committed: pipeline stage adapters (medproof/reasoning_stages.py: second_read, context, report, precedents), the discordance policy, label canonicalisation, lexicon derived from core/vocab.py, a third injection set, demo second reads in demo/medgemma_reads, and a 96-case red-team suite. The adapters wait on a small orchestrator change (Contract change request P3-1).
Next: once P4 acks P3-1, register stage_specs() and check a live study end to end; brain and CXR retrieval, MedGemma box quality on RSNA and any CXR/brain second-read batch when P2's images arrive; Kaggle/Colab notebooks need a manual GPU session; M1/M2 only if time remains.
Blockers: (1) Real faithful flags for brain, skin and bone: P4 and P1 still need to register those readers in pipeline._reader_step and wire the faithfulness and stability stages. (2) Nothing else on the P3 side is waiting on another role; the RSNA work was finished by P3 (see the log) and the earlier handoff to P2 is withdrawn.
MedGemma endpoint: local only. uvicorn services.medgemma.server:app --port 8001 on the RTX 4060 (nf4 weights, bf16 compute, 3.2 GiB VRAM, about 5.5 s per read after a 23 s load). Kaggle/Colab notebooks written but not run.
For P1: report/firewall.py::is_supported treats faithful=None as unverified; set faithful (P1.9) and R5 tightens automatically. readers/generalist.py reads DecodedImage.display and .sha256; verify/concordance.apply(findings, read) fills Finding.second_read and adds flags only, never status.
For P2 / P4 (important): the second reader is weak on bone: on a random 403-image prefix of the FracAtlas batch MedGemma 4B (4-bit, zero-shot) found about 19% of fractures (sensitivity 0.19, 95% CI 0.11-0.29) at 99.4% specificity (kappa 0.27, 0.15-0.38); the missed reads literally say "No obvious fractures are visible". A discordant status driven by second_reader_disagrees would downgrade most true bone positives. Per plan 9.4, validate the flag (D13) per modality before letting it downgrade anything; until then treat it as an audit flag for bone. Numbers will be in reports/concordance.json.
For P2 (D13 input, ready): reports/concordance.json has datasets.<name>.specialist_vs_medgemma.discordant_signal (computed with ml.eval.signals.compare). Bone: the specialist is wrong far more often when MedGemma disagrees (0.63 vs 0.02), but 83% of the detector's correct fracture reports are also flagged, because MedGemma names a fracture on only about 4% of images. So the flag predicts errors and still must not downgrade bone findings. Skin (1,000 reads, 420 where MedGemma named a lesion type): error rate 0.21 flagged vs 0.15 unflagged, difference 0.057, CI -0.017 to 0.125, not significant, so do not downgrade on it. Side note: signals.compare raises IndexError when no unflagged case is wrong (the risk-ratio bootstrap is all-NaN); my wrapper catches it, but you may want to guard it too.
RSNA work (done by P3, no action needed from P2): the images were downloaded with python ml/data/download.py rsna_pneumonia (needs pip install kaggle, KAGGLE_API_TOKEN, the competition rules accepted on Kaggle, and PYTHONUTF8=1 on Windows). The chest precedent index (ml/artifacts/retrieval/rsna_medsiglip-448.{json,npy}, git-ignored) and the box test were built from them; results are in reports/retrieval_rsna_medsiglip-448.json and reports/medgemma_boxes_rsna_{optout,forced}.json. P4: the precedents stage needs the index file on the server machine or MEDPROOF_RETRIEVAL_DIR; ask P3 for the 35 MB .npy.
Answers to P4's four requests (P3-A, P3-C, P3-D, P3-E), 2026-10-07:
- P3-A, precedent indices and thumbnails. All four indices exist (skin, bone, chest and brain) and are packaged as
retrieval_pack.zip(127 MB, kept off git; the folder layout, hashes of the index files, licences and how to install are indocs/retrieval_indices.md). Unzip intoml/artifacts/retrieval/or setMEDPROOF_RETRIEVAL_DIR. Thumbnails: FracAtlas yes (CC BY 4.0, credit it), brain yes too (CC0 as stated; it merges sources with their own terms, so credit it), HAM10000/ISIC no (CC BY-NC), RSNA no. That is enforced in code, not by convention: a precedent from skin or chest always hasthumb_ref == ""and shows label and similarity only. For bone and brain the stage copies the thumbnail into the study's artifact folder and setsthumb_refto/studies/{id}/artifacts/precedent_<dataset>_<case>.png, which your artifact route already serves (PNG, which is why they are PNG). MedSigLIP comes fromgoogle/medsiglip-448with an accepted-termsHF_TOKEN; the precedents stage needs only its image tower, so it does not hit the tokenizer problem the router has. Warm it at start-up (first call 10 to 45 s on a CPU). - P3-C, voice dictation.
transcribe_dictationandprocess_dictationare final, and the audio key works: a synthesized 9 s dictation went through Whisper on Groq withGROQ_KEY_AUDIOtoday ("Sixty two year old woman ..." came back as "62-year-old woman ..."). The transcript appends to the typed note, it does not replace it. Usefrom medproof.context.voice import dictation_to_note;dictation_to_note(audio_bytes, filename, typed_text, pool)returns.ok,.note(typed text, a newline, then the transcript; just the typed text if transcription failed),.transcript,.language,.duration_sand.warnings, and never raises. Suggested flow: a smallPOST /transcribethat takes the audio and the current notes text and returns.note, which you put back into the notes box so the doctor can read and correct it before pressing Analyse; the audio itself never enters the study. It is one note (n1), so the guard, the fact spans and the highlighting cover the dictated part: a spoken "ignore previous instructions" is quarantined like a typed one (tested). Allowed types: wav, mp3, m4a, mp4, mpeg, mpga, ogg, flac, webm (a browserMediaRecordermakes webm); limit 25 MB. - P3-D, should a failed or untestable region cap a finding at uncertain? Decision: a failed region is withheld; an untestable region caps the finding at
uncertain; only a region that passed the test can make a findingverified. (1) Failed (faithful is False): already handled on my side: the context stage no longer lets a fitting symptom count as support for it, so the finding isrejectedwith no claim (this is what withholds most brain findings). (2) Untestable (faithful is None, for example a chest finding outside the top three that get tested): notes may raise it touncertainbut never toverified. That is your one-line change, incore/status_rule.py, inside_is_uncertain(see Contract change request P3-2). (3) Bone is no longer untestable: with P1's box-region test the demo fracture passes (faithful True) and is verified on the image, so the bone case you saw (verified on note support alone) is out of date. - P3-E, report wording. Fixed. The sentence that cites the note now keeps the region ("Doctor, consider Pneumonia in the right lower zone (moderate confidence); the note states "fever".") and leaves the phrase out only when no region was named. A cough no longer supports a mass, nodule or lung lesion (only weight loss and haemoptysis do), and
weightno longer matchesweight lossfor a fracture (only bearing weight does). Recommendation to P4 (status rule), from the live runs: a finding whose image evidence failed the faithfulness test (unfaithful) can still becomeverifiedon one non-specific symptom, and the report then states it at the model's tier. On the brain sample, glioma withunfaithfuland a note that only says headaches is reported as consider glioma (high confidence). Plan D1 says a region is verified only when the deletion drop clears its threshold, so I suggestcompute_statusreturnsuncertainwhen every image evidence item hasfaithful is Falseand the supporting text is the only evidence, and that the tier shown in the report is capped atmoderatein that case. That is P4's function, so I have not changed it; the firewall already blocks claims forrejectedfindings and requires wording to match the tier. For P4 and P1 (wiring):pipeline._reader_stepneeds the brain, skin and bone readers registered, otherwise those studies stop at the reader. They load withreaders.classifier.ImageClassifierReader.from_dir(ml/artifacts/<skin_cls|brain_cls>, modality=...)andreaders.bone.BoneReader.from_dir(ml/artifacts/bone_det), and theirrun(ctx, reader=...)stage functions already leavectx.findingsfor later stages. I tested my stages on all three that way. The precedents stage loads MedSigLIP on its first call (10-35 s on a CPU), so warm it up at server start when you add the model warm-up. For P4 (ready now, see Contract change request P3-1):from medproof.reasoning_stages import stage_specs, merge_findings, collect_claims.stage_specs()returns fourStageSpecs forPIPELINE:second_read(MedGemma throughMEDGEMMA_URL, or the seed indemo/medgemma_reads),context(injection guard, extraction, contradictions; readsctx.notes),report(claims through the firewall and entailment judge; returnspayload["claims"]and never findings) andprecedents(MedSigLIP index for skin and bone). Each iscacheable=Falsebecause the notes change the result and the cache key is the image hash. Each never raises and degrades with a warning when its service is missing. The status rule needs no change:concordance.applyleavessecond_read.agreesandbox_iouempty for unvalidated modalities, so a second-reader disagreement is an audit flag (second_reader_disagrees) and never makes a findingdiscordant; the UI chain step for the second read will therefore show as inconclusive, not failed, on those modalities. For P4 (stages ready to wire, in this order):context.voice.process_dictation(optional audio) ->context.injection_guard.InjectionGuard.check->context.extract.extract->context.contradictions.check->report.drafts.draft_claims(includes the firewall) ->report.entailment.judge->report.fhir.build_bundle. A report consumer must show only claims withblocked_reason is None; a claim removed by the judge keeps itsrenderedtext so the audit view can strike it through.build_bundlewrites only reportable claims and redacts note quotes unlessinclude_quotes=True. For P4:report.drafts.draft_claims(view, pool)returns claims + aStageResult(stage="report"); withpool=Noneor Groq down it returns the template-only report and says so inwarnings.context.contradictions.check(...)returns findings withTextEvidenceattached (onete_nid per finding/fact pair) plusmissing_contextprompts. The status rule can countpolarity == "supports"text evidence.
Now: P4.1 to P4.7 done. P4.3 added the orchestrator (pipeline.run_study, core/{context,config,cache}.py) — see the shared log for that pass's detail. P4.4+P4.5 add:
core/ledger.py(Ledger): one global, append-only, hash-chained JSONL (not per-study — matches the singleGET /ledger/verifyendpoint).append()hashesprev_hash + event + payload_hash + ts + actor + study_id, so tampering any of those fields alone (not just the payload) is caught.verify()replays the whole chain.core/store.py(StudyStore): in-memory, per-study bookkeeping for the API — status, accumulatedStageResults, finalStudyResult, anasyncio.Queuefor live SSE subscribers. Not persisted: a server restart loses every in-flight/completed study. That's a known, stated limitation, not an oversight — plan.md's P4.4 Done-when doesn't ask for persistence, and the ledger is the durable record.api/{app,studies,system}.py(FastAPI +sse-starlette):POST /studies(multipart upload, runs the pipeline on an executor thread, returns 202 immediately),GET /studies/{id}/events(SSE: onestageevent perStageResult, a finaldonewith the fullStudyResult),GET /studies/{id},POST /studies/{id}/feedback(the only place a doctor's accept/reject reaches the ledger — D17),GET /studies/{id}/fhir/GET /metrics/GET /model-cards(honest "not available yet" bodies — P3.13/P2.14/P2.13 aren't built, so these never fabricate data),GET /ledger/verify.pipeline.run_studygained one additive parameter,on_stage_result(callback fired after every stage, cache hit or miss) — the API uses it to push SSE events and append ledger entries; nothing inpipeline.pyitself imports the ledger, keeping it usable standalone. The API overwritesStudyResult.ledger_headwith the real ledger's head after a run; directrun_study()callers (tests, future batch tooling) still get_fold_ledger_head's in-memory provisional value, now clearly the "no ledger wired" fallback rather than the only option.- Verified end to end with a real running server (not just
TestClient): starteduvicorn medproof.api.app:app, uploaded a real phantom PNG, confirmedGET /studies/{id},POST /studies/{id}/feedback, andGET /ledger/verifyall behave correctly over real HTTP (ledgerentrieswent 2 → 3 after feedback,ok: truethroughout).
Honesty note carried over from P4.3, now doubly true: the generalist/MedGemma stage still doesn't exist (P3.1 todo), so "kill MedGemma mid-run" is still unverified as literally stated — proven instead via the same stand-ins (synthetic stages, real brain/skin/bone degradation), now exercised through the HTTP API too, not just run_study() directly.
P4.6 + P4.7 (UI), added the same day:
- Landing
/("Two lines of sight"): ten chapters, each a real verification stage, around one persistent WebGL2 radiograph. Prerendered HTML (readable with JS off), hydrated, then the story engine (GSAP 3.15 ScrollTrigger + SplitText, Lenis, own GL renderer) loads afterload/idle. Phones, reduced motion and devices without WebGL2 get an article layout with the same copy. - Workstation
/read: study rail (four sample cases, upload), WebGL viewport, findings panel, verification chain, notes with two-way span linking, action bar, command palette, shortcut sheet. Live mode =POST /studies+ SSE; sample mode = staticweb/public/cases/*(works without the backend). - Pages:
/validation(realreports/leakage.json; "publishes at gate G3" where P2 data is pending),/models(declared resources with licences;/model-cardswhen P2.13 lands),/audit/:id(server verify + in-browser entry-hash recompute),/report/:id(A4 print), 404. - Backend additions (additive):
GET /studies/{id}/image|pixels|artifacts/{name}|ledger; refs rewritten to URLs before storing; artifacts content-addressed by input sha256; intake no longer cached and failed stages never cached (both were real bugs found while driving the UI live; regression tests added). - Media is all CC0 / CC BY / public domain and listed in
web/public/media/credits.json: Häggström chest PA (CC0), FracAtlas wrist (CC BY 4.0), ISIC_0015552 + Task 1 mask (CC-0), AFIP glioblastoma MRI (public domain).scripts/bake_web_assets.pyran the real CXR reader, anatomy segmenter, deletion test, eight perturbations and quality gate on the chest film; landing numbers come from that bake. - Measured: critical landing JS 83.9 KB gz (React 69.3 + landing 10.3 + shared 4.3); story engine 64.7 KB gz, deferred; CSS 11.1 KB gz. Landing HTML 22.4 KB prerendered.
P3-1 applied (2026-10-07): StudyContext.stage_results (appended right after each stage, cache hits included); findings merge one per id with the latest stage winning, then compute_status; StudyResult.claims collected from stage payloads; P3's four stages registered after the reader; GET /studies/{id}/fhir returns P3's validated R4B bundle for a finished study (409 while running). Workstation: an empty second_read.agrees shows as "Inconclusive" (never "agrees"), and the Firewall step counts passed and blocked claims; the report page lists only unblocked claims and downloads the FHIR bundle. Live run on the CC0 chest film: six stages, five findings (not 20), P3's demo second read merged onto each; context and report degrade here because this machine has no Groq key, so note evidence and claims were not exercised live.
P1 stages wired (2026-10-07, P1's recipe): intake → router → reader → faithfulness → stability → P3 stages. The router runs only when no modality was given and is trusted at confidence ≥ 0.60 (RouterConfig.trust_confidence); its ood/router_uncertain flags are copied onto every finding. The reader dispatches chest (readers.cxr), brain and skin (readers.classifier, bundles brain_cls, skin_cls) and bone (readers.bone, bone_det) from PipelineConfig.models_root (MEDPROOF_MODELS_DIR, default ml/artifacts); missing weights degrade the reader with P1's reason. Faithfulness runs on the MEDPROOF_FAITHFULNESS_TOP_K (default 3) most probable findings; the rest stay untested and are withheld. Live chest run on the CC0 film: 51 s end to end (reader 6.6 s warm-ish, faithfulness 35 s for three findings, stability 2.6 s); Lung Opacity, Pneumonia and Consolidation came back verified (faithful, flip rate 0), Infiltration and Mass withheld as untested. Brain, skin and bone are wired and unit-tested with stubs but not run live here: their weights are not on this machine.
UI audit and calibration (2026-10-07): a calibration stage (after stability, before P3's report) applies P2's calibrators from models_root: temperature + APS conformal set + tier for brain and skin, Platt + abstention band for bone; chest stays uncalibrated with a stated reason (product weights saw RSNA; P2's chest calibrator belongs to the chex weights). Live: glioma 0.86→0.95 {glioma}, nevus 0.87→0.90 {nv}, fracture 0.61→0.89 (high tier, so it is now verified on the note's support). The workstation now shows what the backend already produced but the UI dropped: quarantined instructions in the notes (boxed, struck through, explained), the context stage's missing-history prompts, finding flags in plain words, image-quality reasons with their fixes; the selected finding now owns its note spans. Validation shows P2's six figures; Models shows the datasheets. Not in the UI because nothing produces them on this machine: precedents (needs P3's retrieval index and MedSigLIP), router and OOD score (MedSigLIP), masks (brain segmenter and MedSAM not wired), voice dictation (P3 module has no API route yet).
For the team (status rule): a finding is verified when its image region passes the deletion test or a note supports it. On the live demo cases the brain glioma and the bone fracture are verified by the note alone (glioma's region failed the test; bone boxes cannot take it). The UI shows the failed region in red, but a clinician may still challenge "verified". Decide whether a failed or untestable region should cap a note-supported finding at uncertain (a one-line change in core/status_rule.py plus tests).
Assigned remaining work (2026-10-07, demo laptop = P4's). P4 wires each item into the pipeline, API and UI as soon as it lands; please reply in the shared log.
- P1-A · Masks (brain U-Net, MedSAM).
readers.classifier.runandreaders.bone.rungo straight frompredicttoto_findings, so no mask ever reaches a finding. Add optionalunet=/medsam=arguments that callreaders.segmenter.attach_unet_masks/attach_medsam_masksbeforeto_findings, writing the mask PNG underctx.artifact_dir. Tell P4 the loader calls (e.g.UNetSegmenter.from_dir(ml/artifacts/brain_seg), the MedSAM constructor) and P4 passes them from the pipeline. On the demo laptop: MedSAM weights as a local folder (MEDPROOF_MEDSAM_MODEL) andsegmentation-models-pytorch. Done when a live brain study and a live skin study show the Mask layer. - P1-B · Faithfulness for box-only findings. Bone findings have no heatmap, so
verify.faithfulnessreturns "not assessable" and bone can only ever be supported by notes. A box-region deletion test (blur the box against random boxes of equal area, same permutation rule) would let the image count. Done when bone findings getfaithfultrue or false. - P1-D · Router and OOD on the demo laptop (moved from P3: MedSigLIP and the probe are on P1's machine). The router stage is wired but off here: it needs MedSigLIP (
google/medsiglip-448, as a local folder viaMEDPROOF_ROUTER_MODEL) plusrouter_probe.npzandood.npz(sha256 in P1's section) underbackend/artifacts/. Done when an upload without a chosen modality is routed andoodflags appear. - P1-E · MedSigLIP for precedents. P3's precedents stage embeds with the same MedSigLIP model; the local folder from P1-D covers it. Please hand it over together with P1-D.
- P1-C · FYI. On the demo brain image (AFIP glioblastoma) the classifier's heatmap sits on the lower-left skull edge while the tumour is on the right; the deletion test correctly fails it. Worth a look before the panel.
- P3-A · Precedents on the demo laptop. Needs the indices under
ml/artifacts/retrieval/(skin, bone, chest); the MedSigLIP model comes from P1 (P1-E). Licence constraint (only CC-0/CC-BY images in the web app): FracAtlas thumbnails (CC BY 4.0) may be shown; HAM10000 (CC BY-NC) and RSNA (competition rules) may not, so those precedents show label and similarity only. Please mark which datasets' thumbnails are displayable; P4 adds the thumbnail route and a "similar confirmed cases" panel. Done when a live skin or bone study returns precedents. - P3-C · Voice dictation.
context.voice.process_dictationhas no API route. Confirm it is final and thatGROQ_KEY_AUDIOworks, and say whether the transcript replaces or appends to typed notes; P4 adds anaudiofield toPOST /studiesand a record button. Done when a dictated note shows its spans in the workstation. - P3-D · Decide the note-only "verified" policy (see "For the team" above); P4 makes the one-line status-rule change and tests once decided.
- P3-E · Report wording. Live claims omit the region ("Doctor, consider Pneumonia (moderate confidence)…" with no "in the right upper zone"), and "productive cough" is counted as supporting Mass.
Deliveries checked (2026-10-07): P1-A masks, P1-B box faithfulness, P1-C off-target flag, P1-D/E router hand-over script, P3-A licence-gated thumbnails, P3-C dictation, P3-D decision (applied as P3-2), P3-E wording are all on
mainand verified live on this laptop except where a file is missing here: masks for brain (U-Net), skin and bone (MedSAM) are served and drawn; bone is verified by its own box (drop 0.58); chest sentences name the region. P4 addedPOST /transcribeand a Dictate button, a "similar confirmed cases" panel, the P3-2 status cap, and a cache-key fix (verify caches keyed on their input findings; a stale cache had silently dropped the new masks). Still missing on this laptop, by hand: MedSigLIP (the Hugging Face account behind.env's token has not accepted its terms, sofetch_models.pywas refused: copy P1'sbackend/models/medsiglip), P1'sbackend/artifacts/router_probe.npzandood.npz, and P3'sretrieval_pack.zip(unzip toml/artifacts/retrieval/). MedSAM was fetched (backend/models/medsam);start.batnow points the app at both model folders when present. Note: P3'sdemo/DEMO_GUIDE.mdassumes P3's laptop (with MedGemma on its GPU) as the demo machine; the team should pick one. Known gaps (P4-owned): (0) a cold first chest read is ~17 s (reader timeout is now 45 s); a startup warm-up is still the fix for the first-stage target. (1) live chest findings outside the faithfulness top 3 are withheld as untested; raisingMEDPROOF_FAITHFULNESS_TOP_Ktrades ~12 s per finding on CPU. Serving-time calibration is now wired (see below); the brain segmenter mask and MedSAM masks are still not wired, so the Mask layer stays empty. (2) First SSE stage: ~720 ms cold, ~180 ms warm, against the 500 ms target; needs a startup model warm-up. (3) Viewer 60 fps on 2048² is untested on a real GPU; this machine's headless runs use SwiftShader, which can't measure it meaningfully. (4) Lighthouse not run yet. (5) View Transition handoff landing → workstation not verified in a supporting browser. Run it:start.bat(repo root) installs missing deps, starts the API on :8000 (loading keys from the root.envvia uvicorn--env-file) and the site on :5173 in their own windows, opens the browser, then runsscripts/warm_demo.pyin the background so the four sample images hit the cached checks during a demo (chest: ~50 s cold, ~8 s warm).start.bat previewserves the production build on :4173. Next: P4.8 (Docker + CI wiring for the existing pytest/vitest/Playwright suites), startup warm-up for the 500 ms first-stage target, then P4.9 demo pack and P4.10 README. Blockers: none. Known gaps to flag, not blockers: in-memoryStudyStoreisn't persisted (above); the SSE queue fans out to at most one live drainer per study (fine for today's single-viewer-per-study UI, would need rework for multi-viewer); nomake/pnpmbinary on this machine somake setup/make devare untested end-to-end here (the pieces run fine individually —uvicornwas started and curled directly);ml/still has no dependency declarations of its own.
Deviations from plan.md, newest last. Format: date · role · decision · evidence.
-
2026-10-06 · P1 · Pulled
verify/perturbations.pyforward from P1.11 · shared by the quality-gate fixtures, the stability score and P2.12, so it is defined once. -
2026-10-06 · P1 · Added
intake/preprocess.py(not in plan) · one preprocessing path for training notebooks and inference prevents train/serve skew. -
2026-10-06 · P1 · Blur metric is Laplacian variance divided by intensity variance, computed on a 512 px copy · raw Laplacian variance changes with contrast and resolution; test shows scores within 35% across a 512 to 1024 px resize.
-
2026-10-06 · P1 · CXR tilt is estimated from the rotation that maximises left-right mirror symmetry (with an off-centre shift allowed), not the lung-mask axis · the anatomy segmenter arrives with P1.6; replace then. Only a tilt with symmetry gain of at least 0.03 is reported.
-
2026-10-06 · P1 · PHI scrub keeps PatientAge and PatientSex, blanks names and identifiers, removes free-text descriptions, shifts dates by one random per-study offset that is not recorded, remaps UIDs, drops private tags, and warns on burned-in annotation · the context checks need age and sex; this is a PS3.15 subset, not a certified de-identifier.
-
2026-10-06 · P1 · Quality thresholds are provisional values tuned on a synthetic phantom: the clean phantom passes with zero reasons across 8 seeds, and 100% of noise, blur and contrast degradations at severity 4 and 5 fail the gate · must be refit on real validation images.
-
2026-10-07 · P1 · Wrote the Grad-CAM family (gradcam, gradcam++, hirescam) in
readers/cam.pyinstead of using pytorch-grad-cam · fewer heavy dependencies on a slow network, full control for the saliency sanity check (P1.10), and unit tests against a model with a known answer. Default isgradcam++, configurable (MEDPROOF_CXR_CAM); P1.10 picks the final variant. -
2026-10-07 · P1 · Reader findings leave as
status="uncertain",prob_calibrated=prob_raw,conformal_set=[], flaguncalibrated· a reader cannot verify itself; faithfulness (P1.9), stability, second read and the status rule upgrade it, and calibration fills the rest. No contract change. -
2026-10-07 · P1 · One
ImageEvidence(kindheatmap, withbbox_xyxy,heatmap_ref,region_name) per finding; a finding with neither a box nor a heatmap is withheld with a warning, and a flat CAM is flaggedno_localization· a finding with no image location fails the judges' rule. -
2026-10-07 · P1 · Changed
cxr_xrvpreprocessing from stretch to centre-crop (see Verified facts) and addedcxr_anatomy(512 px, same crop). Only P1 code and tests usedcxr_xrv; P2 is told in the handoff above. -
2026-10-07 · P1 · Positive threshold 0.5 (each label's own operating point) and tiers high >= 0.8, moderate >= 0.65, else low · provisional until P2 calibration; they live in
readers/cxr_config.py. Labels are TorchXRayVision's names until P4 suppliescore/vocab.py. -
2026-10-07 · P1 · Anatomy pixels inside the heart mask count as heart when naming ("cardiac region"); lung thirds still use each full lung extent · found on the real sample film, where Cardiomegaly was first named "left middle zone".
-
2026-10-07 · P1 · Reader degrades instead of failing: no segmenter gives findings without region names and flag
anatomy_unavailable; a heart on the image-left gives flaglaterality_uncertain; model load failure gives a failedStageResult. -
2026-10-06 · P1 · Python 3.12 venv with pip instead of 3.11 with uv (uv not installed here);
backend/pyproject.tomlis minimal for the platform owner to absorb · nomake smokeexists yet, so verification ispython -m pytestinbackend/. -
2026-10-06 · P1 ·
core/schemas.pyis a verbatim copy of the section 4 sketch because the contract was not yet committed · platform owner replaces it; P1 code only usesStageResult. -
2026-10-06 · P2 · FracAtlas source corrected: figshare file 65518038 (v7, CC BY 4.0, md5 fe9da2c7...; dataset.csv lists 4,083 images, 719 fractured, the figshare text says 4,073), resolved at run time through the figshare API · plan.md's file id 43283628 returns HTTP 202 with no body.
-
2026-10-06 · P2 · HAM10000 metadata is fetched from Harvard Dataverse (doi:10.7910/DVN/DBW86T, no login); Kaggle is the fallback only · removes the Kaggle-token dependency for metadata.
-
2026-10-06 · P2 · Official ISIC 2018 Task 3 test metadata (age, sex, site, lesion_id) is fetched from the ISIC API · lets the official test set carry lesion-grouped CIs and a subgroup audit.
-
2026-10-06 · P2 · Splits get a separate conformal calibration set: train 70 / val 10 / cal 10 / internal test 10 (HAM10000, brain), 65/10/10/15 (FracAtlas), LGG fold 0 of a 5-fold patient CV · temperature scaling uses val, conformal uses cal, so neither sees test.
-
2026-10-06 · P2 · Own 64-bit DCT pHash in
ml/data/phash.pyinstead ofimagehash; duplicate groups = native id (lesion, patient) plus Hamming <= 4 edges; official test sets are never modified · avoids a PyWavelets build on new Pythons and guarantees a duplicate cannot straddle splits. -
2026-10-06 · P2 · Brain classifier is trained twice (original Kaggle split vs leakage-free split) · measures the inflation gap instead of estimating it.
-
2026-10-06 · P2 · Bootstrap: percentile cluster bootstrap, B=2000, class-stratified for balanced accuracy, BCa for per-patient Dice; bone sensitivity at 90% specificity uses a threshold fixed on val · plan said B=1000.
-
2026-10-06 · P2 · YOLO26n (released 2026-01-14) for the bone detector, yolo11n as fallback, via config · closes the [VERIFY] on the latest nano model.
-
2026-10-06 · P2 ·
.gitignorecarve-out!ml/data/plusml/data/raw/ignored (thedata/rule hidml/data/);ml/Makefile.incsupplied for P4 to include · P4 owns both files, so P4 please ack. -
2026-10-06 · P2 ·
gen_notes.py(synthetic notes) is left to P3; P2 supplies seed columns (age, sex, site, body part, label) inml/data/splits/*.csv· P3.4 owns note content and injection cases. -
2026-10-06 · P2 · Flag for P1/P4: the LGG segmenter is trained on 3-channel TCGA data; the brain classifier data is single-channel T1-CE. Masks on classifier-style uploads are out of domain, and the model card will say so. Channel-replication augmentation reduces the gap but does not remove it.
-
2026-10-07 · P2 · BDNeuro-MRI is not an independent external test: 4,238 of its 5,941 images (71%) are near-duplicates (pHash Hamming <= 4, pixel correlation median 0.93 to 1.0 against 0.47 for random same-class pairs) of images in the Kaggle brain dataset. The external test uses only the 1,644 images with no match (
ml/data/splits/bdneuro.csv,split == external_test) · its README claims hospital provenance and exact/near-duplicate removal only within itself. -
2026-10-07 · P4 · Acked P2's
.gitignorecarve-out andml/Makefile.inc(2026-10-06 entry above) · both already correct; rootMakefilenowincludesml/Makefile.incunchanged. -
2026-10-07 · P4 · G0 contract freeze (P4.2) is purely additive: every existing field/class in
schemas.pyis untouched, confirmed by grepping all ofbackend/forcore.schemasimports (onlyFinding,ImageEvidence,StageResultare consumed anywhere today) · no Contract change request needed; nothing P1/P2 built against it changes shape. -
2026-10-07 · P4 ·
StudyContext(plan.md section 4's stage interface) is deliberately not defined as a class yet;intake/run.pyalready works via duck-typedctx.raw_bytes/ctx.modality_hint/ctx.path· belongs with the P4.3 orchestrator, where stage ordering and caching actually need a concrete type. -
2026-10-07 · P4 · Pydantic's auto-generated per-field
"title"is stripped before writingcontracts/*.schema.jsonand before the TS generation step · untitled, each field became its own hoisted top-level TS type (e.g.Label1) instead of an inline property; titles aren't part of the contract, field names/types are. -
2026-10-07 · P4 ·
StudyContext(core/context.py) is a plain dataclass, not pydantic · it carries liveDecodedImage/numpy data that has no business on a wire-contract model; kept duck-type-compatible with what P1's existinggetattr(ctx, name, default)stages already read. -
2026-10-07 · P4 · The orchestrator's
intakestep callsintake.run_bytes()directly instead of the genericintake.run(ctx)stage wrapper ·run(ctx)discards theDecodedImageit decodes, but the reader stage needs it;run_bytes's own docstring says it returns both specifically so a caller can do this. No change to P1's intake module. -
2026-10-07 · P4 · Added
diskcachetobackend/pyproject.tomldependencies (not dev) forcore/cache.py· already plan-approved in plan.md section 5.5 (Apache-2.0), logging here for visibility rather than because it needed a CCR. -
2026-10-07 · P4 · Per-stage timeout uses a daemon-less
ThreadPoolExecutorwithfuture.result(timeout=...), shut down withwait=Falseon timeout ·signal.alarmisn't available on Windows (the team develops on Windows laptops and Linux Kaggle/Colab alike); multiprocessing was rejected as picklingDecodedImage/reloading a torch model per call costs more than the problem it solves. Caveat that matters: this stops the orchestrator from waiting, it cannot forcibly kill a CPU-bound native stage (e.g. a blocking torch forward pass) — the thread keeps running in the background and its result is discarded. Correct for stages that raise or block on I/O (HTTP calls to MedGemma/Groq); not a hard kill switch for a hung CPU-bound stage. -
2026-10-07 · P4 ·
StudyResult.ledger_headis filled by foldingsha256(prev + stage + payload_hash)over the stage results in-memory (pipeline._fold_ledger_head) · explicitly provisional, not the real ledger: no persistence, no append-only JSONL, no/ledger/verify. That's P4.5. The fold formula is meant to generalize into the real one, not be thrown away. -
2026-10-07 · P4 · P4.3's stated Done-when ("kill MedGemma mid-run") is not literally tested: the MedGemma/generalist stage doesn't exist yet (P3.1
todo) · verified instead with a synthetic slow/raising stage (mechanism) and the real brain/skin/bone "no reader yet" case (an actual present-day degradation). Flagging this rather than overclaiming the Done-when was met as written. -
2026-10-07 · P4 · The evidence ledger (P4.5) is one global JSONL, not one per study · matches plan.md's single
GET /ledger/verifyroute (no study id in the path); entries carrystudy_idso a future per-study audit view (P4.6) can filter client-side without a new endpoint. -
2026-10-07 · P4 ·
StudyStore(API-side, in-memory, P4.4) is explicitly not persisted — a server restart loses every study · plan.md's P4.4 Done-when doesn't ask for persistence, and the ledger is the durable record; revisit only if a demo scenario needs studies to survive a restart. -
2026-10-07 · P4 · Ledger/store are constructor-injected onto
app.stateinapi/app.py, not module-level singletons likereaders/cxr.py'sget_reader()· the API specifically needs per-test isolation since many pytest tests share one process; a global would leak state between them.cxr.py's singleton pattern is for an expensive model load, a different concern. -
2026-10-07 · P4 ·
GET /studies/{id}/fhir,GET /metrics,GET /model-cardsreturn an honest{"available": false, ...}/empty body rather than fabricated data, since P3.13/P2.14/P2.13 don't exist yet · consistent with the no-fabrication stance already applied topipeline.py's placeholders (ood_score=0.0,claims=[]). -
2026-10-07 · P4 · The per-study SSE queue fans out to at most one live drainer at a time (a second concurrent subscriber only gets the terminal state once the first drains it) · matches today's single-viewer-per-study UI; true multi-viewer broadcast would need a pub/sub fan-out, not asked for yet.
-
2026-10-07 · P4 · Landing expands plan §10.5's "boldness in one place" into a full scrollytelling narrative at the owner's request; every chapter is a real verification stage with a provenance caption (real output, dataset annotation, or illustrative), and the app stays quiet · owner request for an awwwards-level landing.
-
2026-10-07 · P4 · GSAP 3.15 (ScrollTrigger, SplitText) under its Standard no-charge licence, approved by the P4 owner; Motion, DrawSVG and twgl.js dropped (a small own WebGL2 helper shared by story and viewer) · one animation engine per tree; fewer dependencies.
-
2026-10-07 · P4 · Two Vite entries:
index.html(landing, no router, prerendered withrenderToStringthen hydrated) andapp.html(React Router lazy routes); a Vite plugin rewrites/read|validation|models|audit|reporttoapp.htmlin dev/preview, P4.8's server must do the same · keeps the landing light and readable without JS. -
2026-10-07 · P4 · Scroll story uses sticky stages and treats the scene as a pure function of scroll position, not ScrollTrigger pins or a master timeline · React-safe (no pin-spacers), identical layout to the prerendered HTML, End key/anchor jumps can't leave stale state.
-
2026-10-07 · P4 · Phones (< 1024 px), reduced motion and no-WebGL2 get an article layout (same copy, still images), predicted in an inline head script before paint · no layout jump; story engine never downloads there.
-
2026-10-07 · P4 · Brain sample is a public-domain AFIP glioblastoma MRI (Wikimedia), not the Kaggle repackage or figshare · licence verifiable on its own page; brain mask layer stays empty until P2.4.
-
2026-10-07 · P4 · Wax red gets a text shade
--color-pencil-red-ink: #e8695e; strokes keep plan §10.2's#e2574c·#e2574ctext measures 4.48:1 on film-base and 3.97:1 on panel (below WCAG AA 4.5:1); found by the axe suite. -
2026-10-07 · P4 · Browser ledger verification recomputes the entry-hash chain (string-only material) for real entries; payload re-hashing in JS uses Python-compatible canonical JSON and refuses floats (Python prints
1.0, JS1), so float payloads are verified server-side · parity test pins JS output to a Python-generated fixture. -
2026-10-07 · P4 · Pipeline caches are keyed by content: artifacts live under
artifact_root/<input sha256>, intake is uncacheable (it must set the decoded image), failed stages are never cached; readercache_versionbumped to v3 · three real bugs found driving the UI against a live server. -
2026-10-07 · P4 · Stage cache keys include a digest of the findings the stage starts from · cached faithfulness/stability results carry full finding copies; after P1 added masks to the reader, a cache hit from before won the merge and silently dropped every mask. Regression test
test_a_changed_reader_output_invalidates_cached_verification. -
2026-10-07 · P4 · Applied P3-2 (a note alone caps a finding with an image region at
uncertain) and updated P3'stest_support_stands_unless_every_image_evidence_failedcases for untestable regions fromverifiedtouncertainto match · P3's own decision; the cases predate it. -
2026-10-07 · P4 ·
compute_statustreats tierabstainas uncertain, likelow· P2's calibrated tiers addabstainbelowlow; without this an abstaining finding could beverified. Test intests/test_calibration_stage.py. -
2026-10-07 · P4 · Chest findings are deliberately left uncalibrated at serving time · the product reads
densenet121-res224-all, which saw RSNA, and P2's chest calibrators were fitted for the external chex weights (P2's handoff gives the two options; switching weights is a team decision, not made here). -
2026-10-07 · P4 · P1's stages wired with three deviations from the recipe: (a) the reader is no longer cached and faithfulness/stability are (
v1), because P1's readers leave the live model onctxand a cached reader would starve both verify stages; (b) the stage cache key now includes the modality, so the same bytes read as another modality never reuse a result; (c) faithfulness and stability read the latest merged findings, not the reader'sctx.findingssnapshot, otherwise stability (the later stage) would overwrite the faithfulness result when findings merge · found while wiring; regression tests intests/test_pipeline_wiring.py. -
2026-10-07 · P4 · P3-1 applied with two adjustments: the orchestrator keeps its own id-merge and claim collection (same semantics as
reasoning_stages.merge_findings/collect_claims, pinned by a parity test), and P3's stages are registered behind anImportErrorguard · importingreasoning_stagestakes ~1.7 s and needsfhir.resources(backend[services]), so a hard import would stop the API from starting on an image-only install;start.batnow installs.[services]. -
2026-10-07 · P2 · Brain classifier headline: the Kaggle benchmark inflation is measured inside one model. Model A scores 0.942 on the original Testing folder, 0.903 once the 727 images with a duplicate in Training are removed, and 0.865 once the 1,051 images with a same-scan neighbour are removed (n=549). The across-model difference A minus B (0.002, CI -0.020 to 0.026) shows no inflation and is confounded by different models and test sets, so it must not be quoted · B (leakage-free group split, test n=563) scores 0.940 [0.921, 0.958].
-
2026-10-07 · P2 · Two Kaggle accounts: the first account's concurrent GPU limit of 2 stalled the brain jobs, so they ran on a second existing account (johannshonigeorge, owner agreed) · plan.md section 14 lists several accounts as the fallback; its token is kept in the gitignored
.env.kaggle2. -
2026-10-06 · P3 · Slot
frameenum on top of free templates, so the "Doctor, consider..." framing is structural · docs/report_slots.md -
2026-10-06 · P3 · Firewall rule R4 binds cited evidence to the finding named in a slot; adds a mandatory false-block test on good claims · docs/report_slots.md
-
2026-10-06 · P3 · MedGemma service returns boxes on a normalized 0..1000 grid; pixel conversion happens in the reader (P3.2) · services/medgemma/README.md
-
2026-10-06 · P3 · Groq pool adds RPM and daily counters, a total deadline, and
PoolResult.source(live/cache/fallback) so degradation shows up in StageResult warnings · backend/tests/llm/test_groq_pool.py -
2026-10-06 · P3 · Pool defaults to
json_objectplus schema text in the system prompt, not strictjson_schema; unsupported params (reasoning_effort,response_format) are dropped once on a 400 · live call succeeded without needing the drop -
2026-10-06 · P3 · Real-GPU smoke is a separate script (
python -m services.medgemma.smoke); CI tests use a fake backend · services/medgemma/ -
2026-10-06 · P3 · Added
.cache/andml/artifacts/medgemma_reads/to .gitignore (LLM cache can hold note text; cached reads are large) · plan.md 11.3 -
2026-10-07 · P3 · MedGemma runs in bf16 compute, not fp16: fp16 returned an empty reply on the RTX 4060 (Gemma-family activations overflow). The loader picks bf16 when the GPU supports it, else fp32 · services/medgemma/loader.py, smoke log
-
2026-10-07 · P3 · The second reader is asked for a short FINDINGS/IMPRESSION report with a research-evaluation framing, not strict JSON: the JSON-only prompt gave looping labels, refusals and reasoning-only output. Labels are derived deterministically from the text (negation and hedging aware) in
verify/report_labels.py; boxes from MedGemma are not requested yet · services/medgemma/prompts, 44 label-mapping tests -
2026-10-07 · P3 · Injection guard is three layers (regex, Prompt Guard per sentence at 0.9, gpt-oss-20b classifier). On 28 fresh attacks: regex 4, Prompt Guard 4 (7 at 0.5), classifier 25, all three 27, with 1 false positive in 15 benign. The regex 15/15 on the corpus is in-sample and must not be quoted · reports/p3_guard_blind.json
-
2026-10-07 · P3 · Gold quote conventions in gen_notes were changed after reading dev-split extraction errors (medication names without 'on', no compound symptoms, foreign negators kept, foreign age fact added). The test split had not been scored · commit "fix(notes): align gold quote conventions"
-
2026-10-07 · P3 · Differential injection test measures (1) facts overlapping injected text and (2) F1 against gold on injected notes vs their clean twins, not identical fact sets, which only reflect run-to-run variance (5/15 identical) · ml/eval_p3/eval_context.py
-
2026-10-07 · P3 · TextEvidence ids are unique per (finding, fact) pair so each id has one owner, as the firewall's evidence-owner rule (R4) assumes · context/contradictions.py
-
2026-10-07 · P3 · Report claims are drafted as frames chosen by the model (ids only, no labels or quotes in its input), expanded by code into slot templates; offline fallback picks frames deterministically · report/drafts.py
-
2026-10-07 · P3 · FHIR is validated against
fhir.resources.R4B(the library ships R4B, R5 and STU3, no plain R4); the resources used are the same in R4 and R4B. The library checks structure and data types but not value-set codes, so status codes are kept as constants in report/fhir.py · backend/tests/report/test_fhir.py -
2026-10-07 · P3 · Entailment judge input shows the second reader as an explicit word ("agrees", "disagrees", "inconclusive", "unavailable"), a summary of what the notes contain (to verify "the notes do not mention history"), and readable flag phrases. These came from the first-pass errors on variant 0; variant 1 was written afterwards and is the fair measurement · reports/p3_entailment_*.json
-
2026-10-07 · P3 · Voice: Whisper gets a static clinical-vocabulary prompt (no patient data) and the transcript is treated as an untrusted note: same guard and extraction as typed text · context/voice.py
-
2026-10-07 · P3 · MedGemma service reads its prompt files once at start. A git rebase during a long batch removed a prompt file for a moment and killed the run; resumable by image id, so nothing was lost but time · services/medgemma/reader.py
-
2026-10-07 · P3 · The discordant flag is judged by two things: does it predict specialist errors (P2's
signals.compare), and what share of the specialist's correct reports it would also demote. Bone passes the first and fails the second (83%); skin does not pass the first so far · ml/eval_p3/eval_second_reader.py, reports/concordance.json -
2026-10-07 · P3 · Two regex rules added to the injection guard after offline red-teaming (forget everything above; pretend or act as the attending), with tests that clinical uses of the same words are not flagged. The regex layer stays a cheap first filter, not the defence: recall on fresh attacks is carried by the classifier layer · backend/tests/context/test_guard.py
-
2026-10-07 · P3 · The red-team suite is offline and deterministic (language models replaced by fakes) so it can run in CI and under
make redteam; it covers P3 components only, the pixel-level cases are P1 and P4's · backend/tests/redteam, reports/redteam_p3.json -
2026-10-07 · P3 · MedGemma needs CUDA torch, which no existing venv has (
.venv-gpuis the CPU build); a separate.venv-medgemmawas created rather than changing.venv-gpu, and is excluded locally through .git/info/exclude · setup note, not committed -
2026-10-07 · P3 · P4's status rule demotes a finding whenever
second_read.agrees is Falseorbox_iou < 0.1, so the concordance stage now leaves both empty unless the modality is inDOWNGRADE_VALIDATED(empty). Disagreement stays visible as the flagsecond_reader_disagreesand insecond_read.label· backend/medproof/verify/concordance.py, reports/concordance.json -
2026-10-07 · P3 · Finding labels are canonicalised before matching the second read (case, spaces, underscores, and the full skin names to the dataset codes), because P4's
core/vocab.pynames the skin classes in full while the classifier and the report parser use codes. Without it a finding calledmelanomacould never agree with a report saying melanoma · medproof/verify/report_labels.py::canonical_label -
2026-10-07 · P3 · The report lexicon takes its label terms from
core/vocab.pyplus local aliases, so a label added to the shared vocabulary is blocked outside a slot without editing the lexicon · tests/report/test_lexicon_vocab.py -
2026-10-07 · P3 · Pipeline stages are
cacheable=False: the stage cache is keyed by (image hash, stage, version) and these stages depend on the notes and on live services · medproof/reasoning_stages.py -
2026-10-07 · P3 · Second reads for the four sample images are committed under
demo/medgemma_reads(a new folder in P4'sdemo/, additive), keyed by the SHA-256 of the exactimage.webpand the prompt version, so they only apply if that exact file is uploaded · demo/medgemma_reads, tests/test_reasoning_stages.py -
2026-10-07 · P3 · Third injection set (26 attacks, 16 benign, not used to develop the rules): regex 6/26 with 0 false positives, Prompt Guard at 0.9 9/26 with 1, the language-model classifier 25/26 with 1, all layers 26/26 with 2/16 benign flagged (both from model layers). The target of zero benign flagged is not met; the cost is withheld note text, not a missed attack · reports/p3_guard_blind_fresh2.json
-
2026-10-07 · P2 · Conformal sets use the randomised APS with a zero conformity score for a true top class, and the set always contains the top class · the deterministic APS over-covered badly (coverage 0.985 at a 0.90 target on the brain set, 0.953 on skin), because wrong cases sit near score 1 and the quantile jumps the gap; forcing the top class into a randomised rule overshot too (0.958), so the score for a true top class is zero, which is valid split conformal and gives 0.898 on the official skin test. The per-input random draw is a hash of the logits, so a study always gets the same set.
-
2026-10-07 · P2 · Binary readers (chest label, bone image score) use Platt scaling on logit(score) plus a conformal false-negative-rate threshold and an abstention band at calibrated probability 0.4 to 0.6, fitted on separate halves of the calibration data · temperature alone cannot fix the base rate of operating-point-scaled chest scores (ECE 0.220 to 0.018 with Platt); only Lung Opacity and Pneumonia can be calibrated on RSNA, the other 16 labels stay uncalibrated.
-
2026-10-07 · P2 ·
make evaluses B=1000 bootstrap resamples (notebooks used 2000), B=500 for subgroup cells, B=300 for corruption cells and for the validation and calibration splits, and AUPRC and temperature-only ECE as point estimates · keeps the run near 100 s; intervals agree with the notebook ones to the third decimal on the headline metrics. -
2026-10-07 · P2 · Quality-gate flags and RSNA header demographics are committed (
ml/artifacts/<model>/predictions/quality.csv,ml/data/splits/rsna_meta.csv) · both need the raw images, andmake evalmust run from a fresh clone. -
2026-10-07 · P2 · Brain similarity grouping uses Hamming radius 6, not 10, and LGG patients merge only on near-duplicate tumour slices · radius 10 chained 5,619 of 7,200 images into one group and dark top and bottom slices matched across LGG patients; both made the splits useless.
-
2026-10-07 · P2 · The energy-score OOD baseline reaches AUROC 0.83 to 0.98 against other modalities and CIFAR-10, below the 0.95 target · P1.5 (MedSigLIP Mahalanobis) is still needed; the product decoder also rejects 59 truncated FracAtlas JPEGs (see For P1).
-
2026-10-07 · P1 · Autonomous run: P1.4 to P1.12 were built without a fresh plan-mode approval per package, on the instruction to continue unattended; the approved roadmap plan covered the order and scope.
-
2026-10-07 · P1 · Faithfulness pass rule is a permutation test, not a global threshold: a heatmap passes if its deletion drop at k=10% beats same-shaped random regions placed elsewhere (overlap at most 0.25) with p at most 0.10 and the drop is at least 0.05;
n_randommust be at least 9 or the config is rejected, because nothing could ever pass. Random regions pass at most 10% by construction; measured 0 of 18. Median fill is available beside blur because blur keeps silhouette evidence such as heart size (Cardiomegaly drop was negative under blur). -
2026-10-07 · P1 · Stability: severity 2, seed 0, a finding flips when it crosses its positive threshold in either direction;
worst_perturbationis named only when something flipped; flagunstableabove flip rate 0.25 (the plan's status-rule limit). Severity is configurable; P2.12 sweeps all five. -
2026-10-07 · P1 · Saliency sanity chooses among methods whose maps do not collapse to empty after randomisation (more than half the stages empty excludes a method). On the chest model gradcam and hirescam collapse once the classifier is randomised, so gradcam++ is chosen; the result is one image and should be repeated.
-
2026-10-07 · P1 · Thread counts measured on the dev machine: chest DenseNet 0.33 s per image on 1 thread versus 4.3 s on 8 (default 1,
MEDPROOF_THREADS); MedSigLIP 15 s on 1 thread versus 9 s on 6 (default 6,MEDPROOF_ROUTER_THREADS). -
2026-10-07 · P1 · Router uses a canonical
model_id(defaultgoogle/medsiglip-448, stored in the probe and cache keys) separate from an optional localmodel_path; a local folder used as the id overflowed Windows path limits and would have baked a machine path into the probe. Probe and OOD files are plain.npz(no pickle) and record the embedder id; a probe for another embedder is refused with a warning and routing falls back to zero-shot. -
2026-10-07 · P1 · Zero-shot fallback uses a softmax over cosine similarity times the model's logit scale across the five class prototypes (mean of several prompts per class, kept in
router_config.py); SigLIP's own sigmoid and bias are not used because only the ranking across our classes matters, so its confidence is relative, not a calibrated probability. -
2026-10-07 · P1 · Router probe temperature fitted on the validation split reached its lower bound (0.05): the staged classes are almost perfectly separable, so the confidence is saturated and not informative. It needs harder, mixed data (P2's held-out mix, CT, fundus and pathology as
other) before the confidence means anything.otheris represented only by 60 CIFAR-10 natural images here. -
2026-10-07 · P1 · Router grouping: every image is its own group except brain MRI, where a patient folder is one group; FracAtlas patient ids were not used. Held-out evaluation counts only test groups.
-
2026-10-07 · P1 · OOD threshold is the 95th percentile of cross-fitted (group k-fold) Mahalanobis distances over all non-test groups, with Ledoit-Wolf shrinkage; a plain 95th percentile of 6 to 15 validation points flagged up to a third of normal images. Energy score on the chest model's logits was implemented, calibrated and tested: AUROC 0.05 separating chest films from skin, brain, bone and natural images (the other images look more confident), so it is NOT used as a flag; the OOD flag is Mahalanobis only. Result in
intake/results/ood_energy_cxr.json. -
2026-10-07 · P1 · MedSAM runs on CPU through transformers (
flaviagiammarino/medsam-vit-base, Apache-2.0, frompytorch_model.bin); results cached by image hash, box and model id in memory and optionally on disk. -
2026-10-07 · P1 · Brain and skin readers are one generic
ImageClassifierReaderbuilt from P2'smodel_meta.json(arch, classes,preproc_specname or dict, weights hash); layer pathsbn2(EfficientNet) andstages.3(ConvNeXt) were checked on the real timm models. Brainnotumoris not reported as a finding. Bone image score is the highest box confidence, positive cut-off 0.25 until P2.8 calibrates it. -
2026-10-07 · P1 · Tests never download model weights: real-model tests run only when the weights are already on disk and otherwise skip with a reason;
MEDPROOF_ROUTER=0is set intests/conftest.pyso a locally trained router does not change test behaviour. -
2026-10-07 · P2 · SECURITY: a
.envcontaining the P2 Kaggle API token was uploaded to the public repo through the GitHub web UI (commit 73b4105, 08:28) and untracked in d7c8f71, but it stays in git history · rotate that Kaggle token (kaggle.com > Settings > API > expire and create a new one) and the same file in any other role's keys if they were uploaded the same way; never upload.env, use the private channel. Removing the file from history needs a force-push that P4 must coordinate. -
2026-10-07 · P2 · Optional extras done. (1) MILK10k external skin test (4,566 dermoscopic images after removing 94 duplicates of HAM10000 and 576 images of classes the model does not cover): balanced accuracy 0.571 [0.543, 0.602] against 0.680 on the official test; conformal coverage 0.73 against a 0.90 target, so the guarantee fails under dataset shift. (2) Skin-tone audit on MILK10k (grades 0 very dark to 5 very light, 0 and 1 merged): no monotone trend, darkest groups only about 100 images. (3) Skin ensemble of three seeds with flip averaging: official test 0.728 [0.680, 0.775], MILK10k 0.609 [0.580, 0.636], +0.023 and +0.024 over the mean single seed (seeds differ by 0.04); recommendation ship it, registry entry
skin_ens, calibratorml/artifacts/skin_ens/calibration.json. (4) RAPS and Mondrian APS compared with the shipped randomised APS (reports/conformal_variants.json); building that comparison exposed a bug in the Mondrian set rule (top class not always named, coverage read 0.53), fixed in dfe9b05.
Format: id · proposer · change · affected roles · P4 ack (yes/no) · applied in commit.
-
P3-2 · P3 · Status rule: a finding cannot be
verifiedunless at least one of its image evidence items passed the faithfulness test. Incore/status_rule.py,_is_uncertaingets one more line:if finding.image_evidence and not any(ie.faithful is True for ie in finding.image_evidence): return True, placed after therejectedanddiscordantchecks. Today plan section 4 lets supporting note text alone verify a finding; with this, notes can lift a finding touncertainbut not toverified, which is what the judges' rule and plan D1 (a region counts only if deleting it matters) ask for. Effect on the demo files: bone, skin and brain findings that passed their test stayverified; chest findings outside the three that are tested becomeuncertain; no passing finding changes. Needs a test update intests/test_status_rule.pyfor the case of a supporting note with an untested region. · P4 (status_rule.py), P1 (nothing) · P4 ack: yes, 2026-10-07 · applied in commit: see the P4 shared-log entry 'Deliveries checked and wired' -
P3-1 · P3 · Let later stages see earlier results and let the study carry P3's output. (a)
core/context.py: addstage_results: list[StageResult] = field(default_factory=list). (b)pipeline._run:ctx.stage_results.append(result)right after each stage, beforeon_stage_result. (c)pipeline._collect_findings: build the list withreasoning_stages.merge_findings(stage_results)(one finding per id, the latest stage wins) and keepcompute_statuson each; today every stage's findings are concatenated, so a stage that returns updated findings would duplicate them. (d)StudyResult(claims=collect_claims(stage_results), ...)instead of[]. (e)PIPELINE.extend(stage_specs()); the specs carry their own timeouts (second_read 130 s because a live MedGemma read is 6-11 s and the client allows 120 s, context 25 s, report 45 s, precedents 30 s) andcacheable=False. (f)GET /studies/{id}/fhir: callreport.fhir.build_bundle(findings, claims, study_id=..., issued=..., ledger_head=...)on the finished StudyResult; no stage is needed. Not a schema change:StageResult,Finding,Claimare untouched. · P4 (pipeline.py, core/context.py, api/studies.py) · Observed with the realrun_study(reader step stubbed to setctx.findingsas P1's reader now does, thenPIPELINE+stage_specs()): all four P3 stages run and return aStageResult, but the finishedStudyResult.findingsholds four copies of findingf1(one per stage, each carrying only that stage's update: none has the second read and the note evidence together) andclaimsis empty. (c) and (d) fix exactly that; (a) and (b) matter for faithfulness and stability updates, because the reader'sctx.findingsis only a pre-verification snapshot, which my stages fall back to whenstage_resultsis absent. This also affects P1's faithfulness and stability stages, which return full finding lists too. · P4 ack: yes, 2026-10-07 (merge and claim collection kept inpipeline.pywith a parity test againstmerge_findings/collect_claims; registration behind anImportErrorguard; see Decisions) · applied in commit: 1a92ebd (orchestrator), 91b8459 (FHIR route), c38c217 (UI)
Record anything marked [VERIFY] in plan.md once checked. Format: date · role · item · result · source.
-
2026-10-07 · P1 · torchxrayvision 1.5.5 DenseNet
densenet121-res224-alloutput · forward() applies sigmoid then operating-point scaling (op_norm) whenop_threshsis set (it is, for every pretrained set), so outputs are probabilities with 0.5 at each label's operating point; labels with a NaN threshold (some chex and mimic sets) output a constant 0.5 · installedmodels.pysource. -
2026-10-07 · P1 · Label set and order of
-all· 18 labels, identical toxrv.datasets.default_pathologies; other weight sets blank some labels with empty strings · installedmodels.pyand a real-weights test. -
2026-10-07 · P1 · Backbone layer for CAM ·
model.featuresis annn.Sequentialending innorm5, applied before the final ReLU, so the captured map can be negative; Grad-CAM++ inverted the map on such inputs until activations were rectified (fixed, regression test) · installedmodels.py, test_cam. -
2026-10-07 · P1 · Input convention · single channel, range [-1024, 1024], square. The library's transform is
XRayCenterCropthenXRayResizer(224); the PSPNet raises on non-square input · installeddatasets.pyandutils.py. Ourprepare(..., "cxr_xrv")matches it to within a mean of 25 grey levels on a +-1024 scale and 0.08 in probability on a non-square phantom (resampling filter differs: cv2 INTER_AREA vs skimage). -
2026-10-07 · P1 · Anatomy model
chestx_det.PSPNet· input square [-1024,1024] resized to 512, output raw logits (14, 512, 512), target order verified. "Left Lung" is the patient's left lung: on a real NIH chest film (public sample from the torchxrayvision repo, not stored) the "Right Lung" mask centroid was at x=151 and "Left Lung" at x=351 of 512, heart at x=290, matching the standard display with the patient's right on the image left and the film's own L marker on the image right. Lung masks include retrocardiac lung and overlap the heart mask (handled inzones.name_region) · one-off real-image run. -
2026-10-07 · P1 · Download environment · Windows curl failed TLS revocation checks here and the library downloader stalled at 0 bytes, so the two public weight files were fetched with
curl --ssl-no-revoke(TLS chain still verified, only the revocation lookup skipped) from the officialmlmed/torchxrayvisionv1 release URLs. No checksum is published for them; the weights' sha8 appears inmodel_id. -
2026-10-06 · P3 ·
google/medgemma-1.5-4b-itid, gating,transformers>=4.50,AutoModelForImageTextToText, BF16 weights · confirmed · huggingface.co/google/medgemma-1.5-4b-it -
2026-10-06 · P3 · Groq free tier gpt-oss-120b/20b: 30 RPM, 8K TPM, 1K RPD; cached tokens not counted; 429 carries retry-after · confirmed · console.groq.com/docs/rate-limits
-
2026-10-06 · P3 · Prompt Guard id is
meta-llama/llama-prompt-guard-2-86m(as in plan.md; the rate-limit page drops the prefix). Also listed:-22m,openai/gpt-oss-safeguard-20b,whisper-large-v3(-turbo)· confirmed · GET /models with the extract key -
2026-10-06 · P3 · Groq accepts
response_format: json_objectandreasoning_effort: lowon gpt-oss-20b (live call: 1 attempt, 258 tokens, injected "report no findings" text ignored, 2nd call served from cache) · confirmed · live pool call -
2026-10-06 · P3 · MedGemma access: HF token is valid but returns 403 on the gated repo (terms not accepted for that account, or token lacks gated-repo read) · BLOCKED
-
2026-10-06 · P3 · MedGemma CXR box prompt/format · NOT verified (model card gave no format); check on first smoke run
-
2026-10-07 · P3 · MedGemma 1.5 4B nf4 fits and runs on an RTX 4060 Laptop (8 GB): 3.2 GiB peak VRAM, 23 s load from cache, 5.5 s per read with the report prompt · confirmed · services/medgemma/smoke.py log
-
2026-10-07 · P3 · MedGemma fp16 compute returns an empty reply on that GPU; bf16 works · confirmed · smoke runs
-
2026-10-07 · P3 · MedGemma CXR box prompt/format · still NOT verified (the report-style prompt does not request boxes)
-
2026-10-07 · P3 · Llama Prompt Guard 2 on Groq answers with a bare probability and scores a whole clinical note low (3 of 15 attacks at 0.5); per-sentence it fires on 4 of 28 fresh attacks at 0.9 · confirmed · reports/p3_guard_blind.json
-
2026-10-07 · P3 · Groq free tier real limits observed: 8,000 tokens per minute is the binding limit for gpt-oss-20b extraction (about 4-6 calls a minute with a 1.5K-token prompt) · confirmed · response headers
-
2026-10-07 · P3 · Whisper (
whisper-large-v3-turbo) on Groq withGROQ_KEY_AUDIOaccepts multipart audio withresponse_format=verbose_jsonand returns text, language and duration; a 20 s synthesized dictation was transcribed correctly including numbers · confirmed · ml/eval_p3/voice_roundtrip.py -
2026-10-07 · P3 · fhir.resources 8.3.0 does not enforce value-set bindings (an invalid
statusstring validates); required fields and date formats are enforced · confirmed · test_fhir.py -
2026-10-07 · P3 ·
google/medsiglip-448returns 403 with this account's HF token whilegoogle/medgemma-1.5-4b-itreturns 200 · confirmed · HTTP check; accept the terms on the MedSigLIP page with this account to unblock -
2026-10-07 · P3 · The PyTorch CUDA wheel (cu128, about 2.9 GB) times out with pip's default read timeout on this network;
--default-timeout 300 --retries 10worked · confirmed · first attempt failed with ReadTimeoutError, second succeeded (torch 2.11.0+cu128, CUDA available) -
2026-10-07 · P3 ·
google/medsiglip-448access now returns 200 for this account's token (terms accepted) and the embedder works with transformers 5 once it uses the image processor only and readspooler_outputfrom the output object · confirmed · retrieval builds on FracAtlas and HAM10000 -
2026-10-07 · P3 · MedSigLIP vs BiomedCLIP precedents: FracAtlas precision@5 0.844 (0.823-0.865) vs 0.835; HAM10000 0.667 (0.643-0.692) vs 0.619, majority-vote balanced accuracy 0.486 (0.429-0.541) vs 0.362; MedSigLIP is kept as the retrieval embedder · confirmed · reports/retrieval__.json
-
2026-10-07 · P3 · Brain MRI images downloaded with P2's downloader (needs
pip install kaggle, KAGGLE_API_TOKEN, andPYTHONUTF8=1on Windows: the downloader's subprocess reader crashes on cp1252 otherwise). The archive was 228,676,924 bytes against the pinned 164,670,110, so the downloader reported a size mismatch, but all 4,825 non-dropped split rows resolve to files and 60 of 60 sampled images have a perceptual hash identical to P2's stored one, so the images are the same and only the packaging differs · confirmed · ml/data/splits/brain_mri.csv -
2026-10-07 · P3 · Brain precedents with MedSigLIP: precision@5 0.918 (0.895-0.944) against 0.273 for chance, majority-vote accuracy 0.945 and balanced 0.952 (0.935-0.974), by class glioma 0.87, meningioma 0.92, no tumour 0.91, pituitary 0.98 · confirmed · reports/retrieval_brain_mri_medsiglip-448.json
-
2026-10-07 · P3 · MedGemma chest boxes carry no localisation (100 RSNA test images with a true opacity, 50 normal). With a prompt that lets it decline it answers
[]to all 150, so it finds 0 of 100 opacities. Told the opacity is there it boxes every image, but pointing-game hits are 6% (2-11%) against 10% (5-16%) for a fixed central box, IoU of at least 0.1 on 59% against 69%, mean best IoU 0.136 against 0.160, and all 50 normal films also get a box. The convention is [y_min, x_min, y_max, x_max] on a 0-1000 grid; the median forced box is [430, 350, 700, 650], the middle of the chest. Box overlap with the specialist stays out of concordance (box_iouis left empty by policy) · confirmed · reports/medgemma_boxes_rsna_optout.json, reports/medgemma_boxes_rsna_forced.json -
2026-10-07 · P3 · Chest precedents with MedSigLIP, index = RSNA calibration split (7,621 images), queries = the test eval batch (999): precision@5 0.666 (0.648-0.685) against 0.357 for chance, majority-vote accuracy 0.723 and balanced 0.716 (0.688-0.744); by class Normal 0.83, no-opacity-not-normal 0.60, opacity 0.57 · confirmed · reports/retrieval_rsna_medsiglip-448.json
-
2026-10-07 · P3 · All four demo images through the default pipeline after P4 registered the readers and wired faithfulness and stability (real models, real Groq, shipped MedGemma reads, no service; first call per process includes the MedSigLIP load): chest 59.7 s (faithfulness 28.8 s, precedents 16 s), bone 15.3 s, skin 29.4 s, brain 62.6 s (precedents 45.8 s). Chest: 5 findings, 3 faithful and verified, 2 low-tier uncertain, 5 claims, 5 precedents each. Bone: faithfulness not assessable for a box detector, 1 finding, 1 claim. Skin: nevus fails the deletion test, rejected, no claim. Brain: glioma fails the deletion test (flag
unfaithful) but isverifiedon the note's headaches, and the report says consider glioma, high confidence · confirmed · run on this machine -
2026-10-07 · P3 · A symptom no longer rescues a heatmap that failed the faithfulness test:
contradictions.checkmarks a fitting symptomneutralwhen every image evidence item hasfaithful is False, so the finding staysrejectedand no claim is made. This implements my earlier recommendation to P4 for the symptom case without touchingcompute_status. Effect on the live runs: brain findings that fail the test (most of them) are withheld instead of being reported at high confidence on the word headaches; a normal brain no longer produces a low-confidence glioma claim · backend/tests/context/test_contradictions.py -
2026-10-07 · P3 · Precedent thumbnails are licence-gated in code:
THUMBNAIL_DATASETS = {fracatlas, brain_mri}(CC BY 4.0 and CC0); for ham10000 (CC BY-NC) and rsna (not redistributable) the stage never copies a thumbnail andthumb_refis empty. Thumbnails are built ahead of time as 128 px PNGs (build_retrieval --thumbs-only, 2,447 bone and 3,349 brain, 52 MB), and my earlier thumbnail folders for ham10000 and rsna were deleted · docs/retrieval_indices.md, backend/tests/test_reasoning_stages.py -
2026-10-07 · P3 · Dictation appends to the typed note as one note and returns the merged text for the doctor to review; the audio never enters the study · backend/medproof/context/voice.py (
append_dictation,dictation_to_note) -
2026-10-07 · P3 · Voice check, live: a synthesized 12 s dictation through Whisper on Groq (
GROQ_KEY_AUDIO) came back accurately ("Fifty eight year old" as "58-year-old", "two thousand fifteen" as "2015") in 3.4 s; a second dictation containing a spoken attack was flagged by the guard and its sentence kept out of the facts. Neither the API (no audio field, no transcribe route) nor the website (no microphone or recording code) uses voice yet: the library works and is tested (34 voice tests), the wiring is P4's and is not done · confirmed · run on this machine -
2026-10-07 · P3 · Extraction now tolerates dropped filler words: a spoken or spelled-out note ("History of tuberculosis in 2015") lost its history fact because the model returned "tuberculosis 2015", which is not exact text in the note. A multi-word quote now also matches when only up to two small filler words (in, of, the, a, on, at, for, with, to, from, since, and) sit between its words; the fact's quote and span are taken from the note's own text, and a match never lands inside a flagged span. Held-out test split (62 notes, cached Groq answers), exact-span F1 0.919 to 0.922 (precision 0.922 to 0.906, recall 0.917 to 0.939); lenient F1 0.985 · backend/tests/context/test_extract.py, reports/p3_context_test.json
-
2026-10-07 · P3 · P4's voice work verified live:
POST /transcribereturns the merged note for real speech (also through the website proxy at /api/transcribe), rejects a wrong file type with a warning and an empty file with 422, and the Dictate button in Edge, driven with a WAV file as the fake microphone, recorded, transcribed and put the text in the notes box with the review message and 0 browser errors. Not tested: a person speaking in a noisy room · confirmed · backend/tests/api/test_transcribe.py, a Playwright run -
2026-10-07 · P3 · Startup warm-up (the cold-start gap P4 listed as known gap 0):
api/warmup.pyloads the four readers, the precedent embedder and the language-model pool in a background thread after the server starts, and wakes the MedGemma service with one tiny read (a unique image id each time so the reader's cache never answers). The server answers while it warms and a failed load never stops it.MEDPROOF_WARMUPchooses:1default,0off, or a list (cxr,second_read). Measured on a film the system had never seen, after a full restart: second read 67 s to 8 s, whole study 109 s to 50 s (what remains is the faithfulness test, about 23 s on a CPU, and the precedent lookup, about 9 s). The warm API holds about 4.4 GB, the same footprint it reached after a day of use, so on a memory-tight laptop warm only the chest path · backend/tests/api/test_warmup.py -
2026-10-07 · P3 · The sentence that cites the note keeps the region; a cough no longer supports a mass, nodule or lung lesion;
weightno longer matchesweight lossfor a fracture · backend/medproof/report/drafts.py, context/contradictions.py -
2026-10-07 · P3 · Claims name findings in words and skip an unnamed region:
display_labelmaps the dataset codes (bcc, nv, mel, pituitary) to names, and the plain frame is used when no region was named, so the report no longer reads consider bcc in the unspecified region · backend/medproof/report/render.py, drafts.py -
2026-10-07 · P3 · When a finding carries
laterality_conflict, the report sentence is chosen by code, not by the model: the note and the image disagree about the side for the finding, please review the image directly, with no confidence stated. Without it a conflicted finding was still reported as consider X (high confidence) · backend/tests/report/test_drafts.py -
2026-10-07 · P3 · The automatic router (modality Not sure) does not work on this machine: it is off unless
artifacts/router_probe.npzexists orMEDPROOF_ROUTER=1, and loading MedSigLIP's processor needssentencepieceplus tokenizer files that the gated repo does not serve to this account (401 onspiece.modelandspecial_tokens_map.json). Studies uploaded with Not sure stop at the reader; the demo always picks the modality by hand · confirmed · run on this machine; P1 to check the setup on their machine -
2026-10-07 · P3 · Demo pack and guide:
demo/DEMO_GUIDE.md(run order for P1 to P4, start-up, troubleshooting, what not to claim) and a git-ignoreddemo_inputs/of 16 files with paste-ready notes andwarm_pack.py, which sends each file through the live API and prints what it should show; all 16 matched. The inputs are not a random sample: each modality has one proven and one withheld example, and some are chosen to show the quality gate and the laterality check · confirmed · demo_inputs/warm_pack.py -
2026-10-07 · P3 · RSNA download speed on this network swung between 0.2 and 2.5 MB/s (about 3.9 GB, roughly 1.5 hours); the downloader extracted all 26,684 training images; the DICOM decode costs 91 ms per image · measured
-
2026-10-07 · P3 · P2's trained weights (bone_det, brain_cls, brain_seg, skin_cls) arrived as
parallax_weights/; all four SHA-256 values match both the shipped list and the hashes inml/artifacts/registry.json; copied toml/artifacts/<model>/weights.pt(git-ignored by*.pt) and the staging folder is excluded locally · confirmed · sha256sum, registry -
2026-10-07 · P3 · P4's
pipeline._reader_stepregisters only the chest reader, so brain, skin and bone studies end withno reader available for modality ... yetand no findings. With the real trained readers loaded by hand and then P4'srun_studyplus my four stages, on the demo images: bone 17.8 s, one located finding (fracture, low tier, uncertain), real precedents, one claim citing the note; skin 20.2 s, one finding (nv, high tier) which stays rejected because nothing supports it yet, so no claim; brain 35 s, one finding (glioma, high tier) verified on the note's headaches, one claim, precedents timed out at 30 s because the first call loads MedSigLIP on a CPU (10-35 s), so the stage timeout is now 90 s · confirmed · run on this machine -
2026-10-07 · P3 · A note that agrees with a finding on side is consistency, not support:
contradictions.checknow marks matching lateralityneutral(a conflict is stillcontradicts), the firewall and the template report ignore laterality when deciding support, and the report cites a clinical quote (symptom, history, lab) or uses the plain frame. Found by running the real pipeline on the chest sample: the single word Right made all five findings supported and two of them verified before any faithfulness test ran, and every claim read the note states Right · backend/tests/context/test_contradictions.py, tests/report -
2026-10-07 · P3 · End-to-end run through P4's real pipeline (chest sample image, real chest model, real Groq, shipped MedGemma seed, no service): 12.7 s, intake 0.2 s, reader 8.1 s, second read 0.2 s, context 0.2 s, report 2.1 s, precedents unavailable for CXR (no index); 5 findings, 5 claims, one finding per id. Faithfulness and stability are not in the pipeline yet, so faithful stays None and findings are verified on supporting symptoms alone · confirmed · run on this machine
-
2026-10-07 · P3 · MedGemma CXR box format · RESOLVED for the convention, NOT for quality: a grounding prompt returns JSON with
box_2das [y_min, x_min, y_max, x_max] on a roughly 0-1000 grid, and boxes drawn that way land on the thorax; asked for pixel coordinates it still answers on the ~1000 grid, so pixel prompts are wrong. On one chest film (services/medgemma/fixtures/smoke_cxr.png, anatomy only, no lesion) the heart box was too small and too high and the right and left lung boxes were identical, so box IoU against the specialist is not trustworthy yet · confirmed · one-off probe, overlay inspected by eye; the reader keeps not requesting boxes -
2026-10-07 · P1 · MedSigLIP (
google/medsiglip-448) · gated; accessible with a personal read token after accepting the terms. Loads withtransformers.AutoModelin transformers 5.19. Image tower input 448 px, embedding dimension 1152, text length 64,logit_scaleparameter exp = 10.03; 3,513,309,984-bytemodel.safetensors. Embedding takes about 9 s per image on 6 CPU threads · downloaded model and runs. -
2026-10-07 · P1 · transformers 5.x
get_image_featuresandget_text_featuresreturn an output object, not a tensor; itspooler_output, L2-normalised, equals the model's ownimage_embedsandtext_embeds(checked on a public tiny random SigLIP). The embedder accepts both forms ·test_router_real.py. -
2026-10-07 · P1 · MedSigLIP preprocessing assumption (448 px, mean and std 0.5, bilinear stretch) was applied by our own
prepare, not by the model's processor; embeddings behaved sensibly (zero-shot 95.6%), but equality with the processor output was not checked. -
2026-10-07 · P1 · FracAtlas download · the plan's figshare file id 43283628 is outdated; article 22363012 now serves
FracAtlas.zip(338,436,460 bytes, CC BY 4.0) as file 65518038 · figshare API. -
2026-10-07 · P1 · Brain Tumor Dataset (Cheng) · figshare article 1512427, CC BY 4.0, four ~215 MB zips of MATLAB v7.3 files with
cjdata/imageandcjdata/PID; used for brain MRI router images · figshare API. -
2026-10-07 · P1 · ISIC 2018 S3 archives support HTTP range requests (images read without full downloads); Task 1 validation ground-truth masks are
ISIC_xxxx_segmentation.png· direct reads. NLM Montgomery CXR PNGs are readable per file with a browser user agent; the directory index returns 403. -
2026-10-07 · P1 · Windows: curl fails TLS revocation checks and the Hugging Face and torchxrayvision downloaders stalled; large files were fetched with
curl --ssl-no-revoke -C -(TLS chain still verified). Thehf-xetpath stalled after 1 GB. -
2026-10-07 · P1 · P2's committed
model_meta.jsonformats · classifierpreproc_specis a name (brain_effnet,skin_cls); segmenterpreproc_specis a dict (name, size, channels, mean, std, resize, value_range) andthreshold0.5; detectorimgsz640 and weights loadable withultralytics.YOLO. ultralytics 8.4.174 buildsyolo26n.yamland its checkpoints load throughBoneReader.from_dir·tests/readers/test_real_architectures.py. -
2026-10-07 · P1 · Chest model on 12 public Montgomery films (top two findings each, 18 total): median deletion drop at 10% was 0.012 (max 0.116); no finding passed the faithfulness test and no random region did either. Findings were mostly near their operating threshold (0.50 to 0.63), so this says the Grad-CAM++ regions are not demonstrably causal for weak findings, not that the test is broken ·
verify/results/faithfulness_cxr.json. -
2026-10-07 · P1 · Saliency sanity on the chest model, one real film, class Cardiomegaly, seed 0: mean absolute Spearman after cumulative randomisation gradcam 0.0004 (empty in 10 of 11 stages), gradcam++ 0.436 (empty in 5 of 11), hirescam 0.0004; chosen gradcam++ ·
verify/results/saliency_sanity_cxr.json. -
2026-10-07 · P1 · Router, 320 public images (cxr 40 Montgomery, brain 61 Brain Tumor Dataset, skin 100 ISIC, bone 59 FracAtlas, other 60 CIFAR-10), group split 70/15/15: held-out n=50, accuracy 1.00 (Wilson 95% 0.93 to 1.00), every class present; zero-shot over all 320: 0.956 (0.928 to 0.974), bone confused with cxr 5 times and CIFAR with brain 6 times ·
intake/results/*.json. -
2026-10-07 · P1 · OOD on the same set, held-out test: AUROC 1.0 for each of cxr, brain, skin, bone; flagged in-distribution rate cxr 0.17 (1 of 6), brain 0.09, skin 0.20, bone 0.0; every out-of-distribution image flagged ·
intake/results/ood_report.json. -
2026-10-07 · P1 · MedSAM zero-shot with reference-mask boxes, 30 ISIC 2018 Task 1 validation lesions: mean Jaccard 0.878, mean Dice 0.934, failures (Jaccard below 0.65) 0 of 30 ·
verify/results/medsam_isic_val.json.
Did: DICOM/PNG/JPG decode with rescale, windowing and MONOCHROME1 inversion; shared preprocessing specs; eight seeded perturbations; PHI scrub with receipt; quality gate with fix text per failure; intake stage adapter returning StageResult.
State: P1.1 to P1.3 done. Perturbation library done early (P1.11 logic still todo).
Verified: cd backend && python -m pytest -q gives 121 passed. Includes a pydicom-built MONOCHROME1 fixture, a PHI sentinel test on the serialized bytes, and six degraded images each giving the right reason.
Next: CXR reader and anatomy zones (P1.6) so P2.7 can calibrate; then router.
Decisions: see Decisions, entries dated 2026-10-06 for P1.
Did: Groq pool (cache, RPM/TPM budgeter, retry-after backoff, repair retry, fallback); MedGemma read service (loader, API, batch, Kaggle/Colab notebooks); note schema with injection taxonomy; slot grammar and firewall spec.
State: P3.3 done. P3.1 code done, real-model smoke NOT run (no HF_TOKEN here). P3.4 schema done, generator todo.
Verified: .venv/Scripts/python -m pytest ml backend services -q -> 48 passed, 1 skipped (the gpu-marked smoke); mypy --strict backend/medproof/llm clean. make smoke does not exist yet (P4.1).
Next: run smoke on the local RTX 4060 once HF_TOKEN is set; check CXR box format; gen_notes.py; P3.2.
Decisions: see Decisions section (7 entries).
Contract change requests: none.
Did: TorchXRayVision DenseNet121 reader with Grad-CAM family localization (own implementation), PSPNet anatomy masks, patient-side zone naming, contract Finding and ImageEvidence mapping, run(ctx) stage. Switched CXR preprocessing to the library's centre-crop convention after reading its source. Fixed a Grad-CAM++ inversion found by the stub tests.
State: P1.6 done. Not yet measured: localization quality (pointing game, box IoU vs RSNA boxes) and faithfulness; those are P2.7 and P1.9. Tiers and the 0.5 threshold are provisional.
Verified: cd backend && python -m pytest -q gives 191 passed, 0 skipped, including 5 tests on the real downloaded weights (labels, forward pass, preprocessing parity against the library, end-to-end stage, 14-channel PSPNet). A one-off run on a real public chest film confirmed left/right mask placement and a cardiac-region name for Cardiomegaly; image not stored.
Next: P1.4 router, P1.9 faithfulness, P1.11 flip-rate logic.
Decisions: see Decisions and Verified facts log, entries dated 2026-10-07.
Contract change requests: none.
Did: resumable checksummed downloader with a pinned lock file; pHash leakage audit and group-aware splits; split files, reports/leakage.json and eval_index.json for fracatlas and ham10000; cluster-bootstrap CIs and metrics; shared training code; four Kaggle notebooks (brain classifier, brain segmenter, skin classifier, bone detector) plus a builder and a model registry script.
State: P2.1 doing (credential-free sets done; brain, LGG, RSNA wait for the Kaggle token). P2.2 doing (brain, LGG, RSNA splits wait for data). P2.3 to P2.6 blocked: notebooks written and tested, launch needs the Kaggle token and a pushed p2/data branch.
Verified: python -m pytest ml/tests -q gives 86 passed (brain and segmenter notebooks run end to end on synthetic data; bone notebook smoke-run for one epoch on CPU). Short real skin run on the laptop GPU (3 epochs, 32 s/epoch, same notebook source, throwaway sanity check, not a Kaggle job): official ISIC 2018 test balanced accuracy 0.697 [0.650, 0.745], melanoma recall 0.667. The full 15-epoch run will be higher.
Next: Kaggle token, push p2/data, launch the four jobs, build brain/LGG/RSNA splits, then P2.7 (CXR calibration) and the eval harness.
Decisions: see Decisions, entries dated 2026-10-06 for P2.
Contract change requests: none. .gitignore carve-out and ml/Makefile.inc need a P4 ack (see Decisions).
Did: root Makefile (setup, test, smoke, contracts, stub dev/eval/demo; includes ml/Makefile.inc); root pyproject.toml (ruff + mypy spanning backend/ and ml/); added ruff/mypy to backend/pyproject.toml's dev extra; .pre-commit-config.yaml + .secrets.baseline; minimal web/ (Vite + React + TS) so pnpm install/pnpm dev work and contracts.ts has a home. Contract: reviewed schemas.py against every actual import in backend/ and left it untouched (purely additive freeze); added core/vocab.py and core/status_rule.py (compute_status, precedence rejected > discordant > uncertain > verified); scripts/export_contract.py → contracts/{finding,study_result}.schema.json; scripts/gen_contracts_ts.mjs → web/src/contracts.ts; one hand-written StudyResult fixture per core modality in contracts/fixtures/.
State: P4.1 and P4.2 done.
Verified: cd backend && python -m pytest -q → 213 passed, 1 skipped, 1 failed (the failure is pre-existing and unrelated: tests/readers/test_anatomy.py needs torchxrayvision, not installed on this machine). backend/tests/core (new) → 28 passed. cd web && npx tsc -b and npx eslint . → clean. node scripts/gen_contracts_ts.mjs regenerates contracts.ts byte-identical on a second run.
Next: P4.3 pipeline orchestrator (define StudyContext for real here), then P4.4 API + SSE on these fixtures.
Decisions: see Decisions, entries dated 2026-10-07 for P4.
Contract change requests: none (additive only).
Did: backend/medproof/pipeline.py (StageSpec, PIPELINE registry, run_study()); core/context.py (StudyContext dataclass), core/config.py (PipelineConfig, env-backed), core/cache.py (StageCache over diskcache, keyed by (input_sha256, stage, version)). Wired the intake stage via run_bytes() (not the generic run(ctx) wrapper, which drops the decoded image) and a modality-dispatching reader step (only cxr exists; anything else degrades honestly). _collect_findings is the first real caller of status_rule.compute_status outside its own unit tests. Per-stage timeout via a non-context-managed ThreadPoolExecutor (first attempt used with ThreadPoolExecutor(...), whose __exit__ blocks on the abandoned thread and silently defeated the timeout — caught by the timeout test itself, fixed to call shutdown(wait=False) explicitly). Added diskcache to backend/pyproject.toml. Documented .env.example's previously-missing MEDPROOF_* vars.
State: P4.3 done, with one caveat recorded plainly rather than glossed over: the WP's literal Done-when ("kill MedGemma mid-run") isn't testable yet since no MedGemma stage exists (P3.1 todo). Verified instead with synthetic slow/raising stages plus the real "no reader for this modality yet" case.
Verified: cd backend && python -m pytest tests/test_pipeline.py -v → 8 passed. Full suite python -m pytest -q → 221 passed, 1 skipped, 1 failed (same pre-existing unrelated torchxrayvision gap as the previous entry, nothing newly broken). python -m ruff check backend/medproof/pipeline.py backend/medproof/core/ clean.
Next: P4.4, API + SSE, wrapping run_study() and streaming each StageResult as it completes.
Decisions: see Decisions, entries dated 2026-10-07 for P4.
Contract change requests: none (additive only).
Did: core/ledger.py (Ledger: one global hash-chained append-only JSONL, append/head/verify, hash covers ts/actor/event/study_id too, not just the payload); core/store.py (StudyStore, in-memory, not persisted — stated limitation, see Decisions); api/{app,studies,system}.py (FastAPI + sse-starlette): POST /studies, GET /studies/{id}/events (SSE), GET /studies/{id}, POST /studies/{id}/feedback (the only path to D17's ledger-logged doctor feedback), GET /studies/{id}/fhir, GET /metrics, GET /model-cards, GET /ledger/verify. Added one additive parameter to pipeline.run_study (on_stage_result) so the API can push SSE events and ledger entries per stage without pipeline.py itself depending on the ledger. Makefile's dev target now actually starts uvicorn instead of echoing a stub.
State: P4.4 and P4.5 done. Found and fixed one real bug while writing the SSE test: StudyStore.stream()'s original "already-terminal, skip the queue" shortcut silently dropped every buffered stage event for a fast-finishing study (routine with a stub reader finishing before the SSE GET even arrives) — fixed by always draining the queue (it holds every event regardless of when a subscriber connects) and only falling back to the stored terminal state once a queue has been fully drained once already.
Verified: cd backend && python -m pytest tests/core/test_ledger.py tests/api -v → 14 passed. Full suite python -m pytest -q → 235 passed, 1 skipped, 1 failed (same pre-existing unrelated torchxrayvision gap, nothing newly broken). ruff check on all new/changed files clean. Manual end-to-end sanity on a real running server (not just TestClient): started uvicorn medproof.api.app:app, uploaded a real phantom PNG via curl, fetched the study, posted feedback, confirmed GET /ledger/verify (entries 2 → 3, ok: true throughout).
Next: P4.6 (web app pages) and P4.7 (WebGL viewer), the first real UI, built against web/src/contracts.ts and this API.
Decisions: see Decisions, entries dated 2026-10-07 for P4.
Contract change requests: none (additive only).
Did: scrollytelling landing (ten chapters on one WebGL2 radiograph, prerendered + hydrated, story engine deferred); /read workstation with live (SSE) and sample modes, WebGL2 viewer (R8/RGBA8/R16F, highp window/level, cividis heatmap + iso-contour, hatched mask, anatomy contours, SDF boxes, zoom about cursor, keyboard map), two-way finding/note linking; validation, models, audit, report and 404 pages; additive API routes (image, float16 pixels, artifacts with traversal guard, per-study ledger); scripts/bake_web_assets.py (real reader outputs on a CC0 chest film); Playwright + axe e2e suite (cd web && pnpm run e2e).
State: P4.6 and P4.7 done, with the gaps in the P4 role section stated plainly: live findings withheld until P1.9/P1.11 exist, first SSE stage ~720 ms cold (target 500), viewer 60 fps and Lighthouse not measured on real hardware yet.
Verified: cd backend && python -m pytest -q → 266 passed. cd web && npx vitest run → 22 passed; npx tsc -b clean; npx eslint . 0 errors; npx playwright test → 17 passed (no-JS article, zero em/en dashes, axe zero serious/critical on landing, prerendered markup, workstation, validation and models; story boots without console errors; ledger tamper detected in the browser; phone and reduced-motion layouts; keyboard map; span highlight under 100 ms).
Next: P4.8 Docker + CI around these suites; model warm-up at startup for the first-stage target; P4.9, P4.10.
Decisions: see Decisions, entries dated 2026-10-07 for P4 (landing scope, GSAP licence, two entries, sticky stages, article mode, brain image source, red text shade, browser ledger caveat, content-addressed caches).
Contract change requests: none (API routes additive; schemas.py untouched).
Did: slot renderer + firewall + model-chosen frames with offline fallback; seeded 200-note generator; injection guard (regex, per-sentence Prompt Guard, classifier) and span-grounded extraction; contradiction rules; report-label mapping, MedGemma client, concordance; real MedGemma reads on the local GPU; eval scripts that recompute from cached output.
State: P3.3, P3.4, P3.7, P3.9, P3.10 done. P3.1/P3.2 doing (eval batch reads running; concordance.json is partial until they finish). P3.5/P3.6 measured below.
Verified: .venv/Scripts/python -m pytest backend ml/tests/test_notes_schema.py ml/tests/test_gen_notes.py ml/tests/test_eval_second_reader.py ml/tests/test_eval_contradictions.py services -q -> all pass (ml/ tests that need pandas/sklearn/torch are P2's and were not run in my light venv).
Numbers (62 held-out test-split notes, live Groq, cached): extraction exact-span F1 0.919 (P 0.922, R 0.917), lenient 0.973, 0 failures; weakest types laterality 0.79, history 0.80, device 0.82. Guard on the corpus 15/15 attacks, 0/47 false positives (regex is in-sample). Guard on 28 fresh attacks / 15 fresh benign: regex 4/28, Prompt Guard@0.9 4/28, classifier 25/28 (1 FP), all three 27/28 (1 FP). Differential: 0 facts overlap the injected text; F1 0.954 on injected notes vs 0.936 on their clean twins. Contradiction rules P/R 1.00/1.00 on gold facts (templated notes: rules fire as designed, not a generalisation claim). Firewall: 104 planted bad claims all blocked, 10 good claims none blocked. Second reader on bone (275-read prefix, reports/concordance.json): sensitivity 0.19 (0.09-0.32), specificity 0.995, kappa 0.27 (0.12-0.43).
Next: finish the batch reads and write reports/concordance.json; retrieval (needs P1's embeddings); entailment judge; end-to-end context stage on extracted facts; wire into P4's pipeline.
Decisions: see Decisions (7 entries dated 2026-10-07).
Contract change requests: none.
Did: entailment judge (second wall behind the firewall); Whisper transcription in the Groq pool plus voice dictation through the guard and extraction; FHIR bundle export. Pulled P2's skin-classifier push and wrote the specialist-vs-MedGemma comparison from its saved official-test predictions (agreement, kappa, and whether agreement predicts that the specialist is right); it runs when the HAM10000 reads are in. Found and fixed a crash in the long MedGemma batch (see Decisions).
State: P3.11, P3.12, P3.13 done and pushed. HAM10000 reads have only just started, so the skin comparison has no numbers yet; FracAtlas reads about 70% done.
Verified: pytest backend + ml/tests (mine) + services: 560+ passed, 5 skipped (the gpu-marked smoke and P2 tests that need torch/sklearn). Entailment: first pass 55/60; fixed judge 59/60 on the fresh variant (29/30 true sentences kept, 30/30 defective caught), 60/60 on the tuned variant; false rejections are the judge's main error (about 2-3%). Voice: real round trip OK. FHIR: bundle validates; 11 tests.
Next: run python -m ml.eval_p3.eval_second_reader when the reads finish, then report whether specialist agreement predicts specialist correctness (D13 input for P2); retrieval needs P1's embedding function; wire stages into P4's pipeline.
Decisions: see Decisions (4 entries dated 2026-10-07 added).
Contract change requests: none.
Did: modality router (MedSigLIP embeddings, temperature-scaled probe, zero-shot fallback, routing before the quality checks); per-modality Mahalanobis OOD score with a cross-fitted threshold, plus an energy score that was tested and rejected; faithfulness (deletion, insertion, permutation test against random regions); stability (eight perturbations, flip rate); saliency sanity (cascading randomisation); MedSAM box-to-mask service with a cache; brain and skin classifier reader, U-Net and MedSAM mask attachment, bone detector wrapper; a script that checks the trained models once their weights are in place.
State: P1.4, P1.5, P1.9 to P1.12 done and validated on real models or data. P1.7 and P1.8 blocked: P2's trained weight files are not in the repository (only metadata and predictions); the code is checked on the real timm, segmentation-models and ultralytics architectures with random weights and on P2's committed metadata.
Verified: cd backend && python -m pytest -q with the real-model environment set gives 694 passed, 0 skipped. Without the local model folder the real-model tests skip with a reason. Real-data results: router held-out 50 of 50 (CI 0.93 to 1.00), zero-shot 0.956 on 320; OOD AUROC 1.0 held-out; faithfulness 0 of 18 on 12 chest films (random 0 of 18); stability 0 flips at severity 2; MedSAM mean Jaccard 0.878 on 30 lesions.
Next: place P2's weights, run python scripts/check_trained_models.py, then close P1.7 and P1.8; retrain router and OOD on the full datasets; repeat faithfulness, stability and saliency per modality.
Decisions: see Decisions and Verified facts log, entries dated 2026-10-07 (energy score rejected; faithfulness permutation rule; router confidence saturated on easy classes).
Did: downloaded and audited all datasets (Kaggle brain, LGG and RSNA included); launched and registered the four Kaggle jobs (skin, brain classifier, segmenter, bone; two accounts because of the 2-GPU-session limit); calibration library and per-model calibration; chest-reader validation on RSNA with external and contaminated weights and Grad-CAM localisation scoring; external brain test on BDNeuro; corruption benchmark; trust-signal and subgroup audits; model cards, datasheets, figures, validation report; make eval.
State: P2.1 to P2.14 done. P2.10 is partial by design: discordant needs P3's complete second-reader batch and unfaithful needs P1.9; the harness is ready.
Verified: cd backend && python -m pytest -q gives 555 passed; python -m pytest ml/tests -q gives 129 passed; python ml/eval/run_all.py --check reproduces reports/metrics.json exactly in about 100 s.
Headline: skin official-test balanced accuracy 0.680 [0.634, 0.726]; brain leakage-free accuracy 0.940 [0.921, 0.958], external BDNeuro 0.755 [0.735, 0.775]; segmenter mean per-patient Dice 0.831 [0.716, 0.890]; bone image AUROC 0.923 [0.885, 0.957]; chest AUROC on RSNA 0.785 (chex) and 0.749 (mimic_ch) against 0.875 for the contaminated all-data weights.
Next: P1 to refit the quality gate and add the brain_seg spec; P3 to finish the second-reader batch; P4 to render reports/metrics.json and the cards.
Decisions: see Decisions, entries dated 2026-10-07 for P2.
Contract change requests: none.
Did: synced with main (fast-forward to 73b4105), untracked the accidentally committed .env, re-checked model artifacts, ran the import check over every module and the full backend suite.
State: ml/artifacts still has metadata and predictions only, no weight files, so P1.7 and P1.8 stay blocked. All medproof modules import; P2's evaluation code uses my decode, preprocess, perturbation, CAM and quality interfaces unchanged. core/schemas.py is still the verbatim copy of the section 4 sketch (no contracts/, web/ or pipeline from P4 yet).
Verified: cd backend && python -m pytest -q gives 772 passed, 1 skipped after merging P4's API and web work (693 passed before that merge; the skip is the router accuracy test that needs the staged images, all other real-model tests ran). python -m pytest ml/tests -q gives 128 passed, 1 failed: test_eval_reproducible (ml/eval/run_all.py --check reports that reports/metrics.json differs from a fresh recomputation; confirmed with an LF working copy, so it is not a line-ending effect). Running P2's eval here also rewrites eight calibration*.json files with differences around 1e-7 (different numeric library versions on this machine), which I discarded instead of committing; it looks like P2's calibration fit is not bit-for-bit reproducible across machines. This is a P2 reproducibility issue, not a P1 interface problem; P2 may want to round the stored values or pin the library versions.
Next: unchanged (see the P1 section).
Decisions: none new.
Contract change requests: none.
Did: merged the latest main into p3/medgemma-groq; committed the pending work (cue scan, one-call reasoning stage, end-to-end eval, FracAtlas retrieval report); validated the discordant flag against specialist errors for bone and skin; added two guard rules after offline red-teaming; wrote an 83-case offline red-team suite; built and scored the HAM10000 precedent index; refreshed concordance.json from the cached reads; created a CUDA venv and resumed the HAM10000 batch.
State: P3.5 and P3.6 done. P3.1 and P3.2 doing (HAM10000 batch running). P3.8 doing (FracAtlas and HAM10000 indices; brain and CXR skipped, MedSigLIP 403).
Verified: cd backend && python -m pytest -q gave 732 passed, 10 skipped; python -m tests.redteam.run_redteam gives 83/83 (notes 19, contradictions 13, injection 25, firewall 17, degradation 9); python -m pytest ml/tests/test_eval_second_reader.py 16 passed. Numbers: bone MedGemma sensitivity 0.18 (0.12-0.26) over 569 reads; bone specialist-vs-MedGemma kappa 0.07; discordant-flag error rate 0.63 vs 0.02, with 83% of correct fracture reports flagged; skin over 531 reads, MedGemma names a lesion on 43%, flag effect 0.11 (-0.01, 0.20) not significant; retrieval precision@5 FracAtlas 0.83 (chance 0.70), HAM10000 0.62 (chance 0.47). make smoke still does not exist (P4.1).
Not run: Kaggle and Colab notebooks, CXR box format check, brain and CXR retrieval.
Next: refresh concordance when the HAM10000 reads finish; wire into P4's pipeline when StudyContext exists.
Decisions: see Decisions (4 entries dated 2026-10-07 added). Contract change requests: none.
Did: finished the HAM10000 MedGemma batch (1,000 of 1,000 reads, 0 failed) on the local RTX 4060 and refreshed reports/concordance.json.
State: P3.1 batch reads complete for both datasets; P3.2 numbers final.
Verified: python -m ml.eval_p3.eval_second_reader. HAM10000 (official test batch): MedGemma names a lesion type on 42% of images, balanced accuracy 0.107 (0.081-0.136), melanoma sensitivity 0.115 (0.056-0.177); on the 420 images where it did, specialist accuracy 0.82 vs MedGemma 0.51; agreement 0.53, kappa 0.065 (-0.012, 0.145); specialist accuracy when the readers agree 0.85, disagree 0.79; discordant-flag difference in error rate 0.057 (-0.017, 0.125), not significant, so it must not downgrade skin findings. This replaces the 531-read figures in my earlier entry and role notes.
Next: CXR box format check; wire into P4's pipeline when StudyContext exists.
Decisions: none new. Contract change requests: none.
Did: MedSigLIP access came through; fixed the embedder (image processor only, transformers 5 output object) and rebuilt the FracAtlas and HAM10000 precedent indices with it; reports are now named per embedder and compared with BiomedCLIP.
State: P3.8 doing. FracAtlas and HAM10000 done with MedSigLIP; brain and CXR waiting on P2's images.
Verified: cd backend && python -m pytest tests/retrieval -q 13 passed; builds ran on the local RTX 4060. FracAtlas precision@5 0.844 vs chance 0.704 (fracture class 0.476); HAM10000 0.667 vs chance 0.469, balanced vote accuracy 0.486 vs 0.362 for BiomedCLIP, melanoma precision 0.430.
Next: brain and CXR retrieval once P2's images are available; wire into P4's pipeline when StudyContext exists.
Decisions: none new. Contract change requests: none.
Did: checked ml/artifacts after syncing (origin/main had no new commits): weights.pt now present locally for bone_det, brain_cls, brain_seg and skin_cls, sha256 equal to model_meta.json for all four. Ran check_trained_models.py and a new scripts/faithfulness_trained.py on staged public images.
State: P1.7 and P1.8 done. Brain and skin classifiers, the U-Net and the bone detector load through the pipeline's reader code unchanged and give real findings, boxes and masks.
Verified: cd backend && python -m pytest -q 1016 passed, 2 skipped. Faithfulness (deletion permutation test) and stability on staged images: over every staged image, brain_cls 69 findings on 59 images, 42% faithful, 20% unstable, mean top-10% drop 0.40; skin_cls 107 findings on 100 images, 44% faithful, 13% unstable, mean drop 0.18 (backend/medproof/verify/results/faithfulness_trained.json). Many brain and skin findings fail the faithfulness test, so faithful=false and the firewall's tighter handling will apply to them; this is a measured property of the trained models on the staged public images (not P2's held-out test sets). Bone gives box evidence only, no heatmap, so faithfulness does not apply to it.
Next: P4 wiring; refit thresholds when P2 shares validation images. Skin MedSAM masks were later checked with the trained skin weights on one staged image (masks produced, no flags).
Decisions: none new. Contract change requests: none.
Did: pipeline stage adapters (second_read, context, report, precedents) with merge_findings and collect_claims for the orchestrator; stopped second-reader disagreement from demoting findings in unvalidated modalities (P4's status rule reads agrees and box_iou); canonical labels so vocabulary names match dataset codes; lexicon derived from core/vocab.py; a third held-out injection set and its measurement; real MedGemma reads for the four sample images in demo/medgemma_reads; 14 more red-team cases (96 in total).
State: P3.2 done. P3.6 measured again, zero-false-positive target not met. Adapters complete on the P3 side and waiting on Contract change request P3-1.
Verified: cd backend && python -m pytest -q gives 993 passed, 12 skipped; python -m tests.redteam.run_redteam 96/96; mypy --strict clean on reasoning.py and reasoning_stages.py. Guard third set: regex 6/26, Prompt Guard 0.9 9/26, classifier 25/26, all layers 26/26 with 2/16 benign flagged.
Not run: the stages inside a live P4 pipeline (needs P3-1); brain and CXR retrieval; Kaggle and Colab notebooks.
Next: P4's reply on P3-1; then a live end-to-end study.
Decisions: see Decisions (6 entries dated 2026-10-07 added). Contract change requests: P3-1.
Did: P2 supplied the download route for brain and RSNA images; downloaded the brain set, verified it against P2's split by coverage and perceptual hash, built and scored the brain precedent index with MedSigLIP, and added brain to the precedent stage.
State: P3.8 doing (FracAtlas, HAM10000, brain done; CXR waits for RSNA).
Verified: cd backend && python -m pytest tests/test_reasoning_stages.py tests/redteam -q 131 passed; brain precision@5 0.918 vs 0.273 chance, balanced vote accuracy 0.952. The pinned archive size did not match (228,676,924 vs 164,670,110) but 60 of 60 sampled image hashes equal P2's.
Not run: RSNA download (needs the competition rules accepted on Kaggle for the token's account), CXR retrieval, MedGemma box quality.
Answer to P2: my discordant results have been on main since commit 5a986ca (reports/concordance.json, datasets..specialist_vs_medgemma.discordant_signal), so the metrics and validation report can be regenerated now; I did not need the signals.compare wrapper after 6801d15 but kept it.
Next: RSNA download and CXR retrieval once the rules are accepted; P4's reply on P3-1.
Decisions: none new. Contract change requests: none new.
Did: applied P3-1. StudyContext.stage_results; per-id finding merge (latest stage wins) and claim collection in the orchestrator; P3's stage_specs() registered after the reader behind an ImportError guard; GET /studies/{id}/fhir builds P3's bundle for a finished study (409 while running); workstation shows unvalidated second reads as inconclusive and counts firewall claims; report page lists unblocked claims and downloads the FHIR bundle; start.bat installs .[services].
State: P3-1 done and acked. Live run on the CC0 chest film (real reader, P3's demo second read): six stages, five merged findings with the second read on each, FHIR 200. Not exercised here: note evidence and claims (no Groq key on this machine), so P3 should confirm a live study with keys.
Verified: new orchestrator tests (stage_results visible, merge without duplicates, claims collected, parity with merge_findings, registration order and no caching) and FHIR route tests; full backend suite cd backend && python -m pytest -q 998 passed, 8 skipped; cd web && npx vitest run 25 passed, npx tsc -b and npx eslint . clean, npx playwright test 17 passed.
Next: P3's end-to-end check with keys; reader warm-up at startup (16.7 s cold against a 20 s timeout); P4.8.
Decisions: see Decisions, P3-1 entry dated 2026-10-07.
Contract change requests: P3-1 acked and applied.
Did: MILK10k external skin test and skin-tone audit; two extra skin seeds on Kaggle and a 3-model ensemble with flip averaging; RAPS, Mondrian and randomised-APS comparison; fixed the Mondrian set rule; added the ML datasheet for MILK10k; make eval now includes the external skin results and the ensemble (about 2 minutes).
State: all optional extras from plan.md section 7.2 are done except MedSigLIP linear probes (needs a Hugging Face token).
Verified: python ml/eval/run_all.py --check reproduces reports/metrics.json exactly; cd backend && python -m pytest -q and python -m pytest ml/tests -q pass.
Next: P1 decides whether the product serves the ensemble; P1 refits the quality gate.
Decisions: see Decisions, entries dated 2026-10-07 for P2 (including the exposed Kaggle token).
Contract change requests: none.
Did: ran a real end-to-end study on the chest sample through P4's pipeline (with the P3-1 wiring on main) and found that a bare side word counted as supporting evidence for every finding; fixed it in the contradiction rules, the firewall and the template report; added a red-team case. P1's trained-model faithfulness results (brain 3 of 12 findings faithful, skin 7 of 13) mean the firewall will withhold most image-only claims for those modalities once faithfulness is wired in.
State: P3.7 and P3.10 corrected. Chest weights are present on this machine, so P1's real-weights tests now run.
Verified: cd backend && python -m pytest -q gives 1020 passed, 6 skipped; red-team suite 97/97 (policy 3/3); mypy --strict clean on medproof/report; ruff clean on the files I changed.
Not run: end-to-end on bone, skin and brain (their trained weights are not on this machine); CXR retrieval and box quality (RSNA images).
Next: P4 wiring of faithfulness and stability so faithful gets set; RSNA download once the Kaggle rules are accepted.
Decisions: see Decisions (2 entries dated 2026-10-07 added). Contract change requests: none new.
Did: assigned the RSNA-dependent work (CXR precedent index and the MedGemma chest-box test) to P2, who already has the images; recorded the commands and outputs under the P2 handoff in my role section.
State: P3.8 waits on P2 for CXR; the box-quality question stays open until P2 reports.
Verified: not run by me (RSNA download on this machine reached about 700 MB of 3.9 GB at 0.3 MB/s). Both scripts have tests: python -m pytest ml/tests/test_build_retrieval.py ml/tests/test_eval_medgemma_boxes.py 18 passed.
Next: P2's reports; then PRECEDENT_DATASET gets cxr and I decide on box IoU.
Decisions: none. Contract change requests: none.
Did: verified and placed P2's trained weights; ran my four stages with the real bone, skin and brain readers on the demo images through P4's run_study (the readers loaded by hand, because the pipeline does not register them); raised the precedents stage timeout to 90 s after a cold-start timeout on brain.
State: stages work with all four modalities' real findings. The pipeline itself still needs the three readers registered (P4 and P1).
Verified: bone 17.8 s with a located finding, precedents and a claim; skin 20.2 s, finding rejected and no claim; brain 35 s, finding verified on the note's headaches with a claim and a precedents timeout that the new limit covers; python -m pytest tests/test_reasoning_stages.py tests/redteam 133 passed. P1's check_trained_models.py could not run here because it needs sample images staged under backend/data/router, which only exist on P1's machine.
Not run: the faithfulness and stability stages on these findings (not in the pipeline), RSNA retrieval and box quality (assigned to P2).
Next: P4 and P1 register the readers; then a live study per modality through the API.
Decisions: none new. Contract change requests: none.
Did: downloaded RSNA, built the chest precedent index (MedSigLIP, calibration split as the index), measured MedGemma's chest boxes in two prompt variants, added chest to the precedent stage, and withdrew the handoff to P2 because the work is done.
State: P3.8 done for all four modalities. The box question is closed: MedGemma's boxes are not usable.
Verified: python -m pytest ml/tests/test_build_retrieval.py ml/tests/test_eval_medgemma_boxes.py passes; cd backend && python -m pytest tests/test_reasoning_stages.py tests/redteam 133 passed; a real chest study through P4's pipeline now returns 5 precedents per finding (first call 28 s while MedSigLIP loads). Chest retrieval precision@5 0.666 against 0.357 for chance; MedGemma boxes 0 of 100 opacities when it may decline, and no better than a fixed central box when forced.
Not run: faithfulness and stability on these findings (not in the pipeline), live studies for brain, skin and bone through the API (readers not registered).
Next: P4 and P1 register the readers and wire faithfulness and stability; then a live study per modality.
Decisions: none new. Contract change requests: none.
Did: wired P1's router, the brain/skin/bone readers, faithfulness (top 3 findings by default) and stability into PIPELINE per P1's recipe; config gained models_root and faithfulness_top_k; reader no longer cached, verify stages cached with the modality in the key; verify stages read merged findings so neither overwrites the other; workstation copy no longer says faithfulness is pending.
State: done for chest end to end (live run: three findings verified, two withheld as untested, 51 s). Brain, skin and bone are wired and tested with stubs only; this machine has no P2 weights. Per P1's latest verify/results/faithfulness_trained.json (59 brain and 100 skin images), 42% of brain findings (29 of 69) and 44% of skin findings (47 of 107) pass faithfulness (the earlier 12- and 13-finding sample gave about a quarter and a half), so more than half of brain and skin findings will be withheld unless notes support them. Bone findings carry boxes but no heatmap, so P1's deletion test returns "not assessable" for them and they stay withheld unless notes support them; P1 to confirm whether that's intended.
Verified: tests/test_pipeline_wiring.py 11 passed (router gating and flags, missing-weights degradation per modality, bundle paths, faithfulness and stability on the same finding, cache reuse, modality in the key); full backend suite 1016 passed, 8 skipped; cd web && npx vitest run 25 passed, npx playwright test 17 passed.
Next: run a brain/skin/bone study on a machine with P2's weights; startup warm-up; serving-time calibration with P2's Calibrator.
Decisions: see Decisions, P1-wiring entry dated 2026-10-07.
Contract change requests: none (schemas.py untouched).
Did: merged P4's wiring of the router, the brain, skin and bone readers, faithfulness and stability; pushed all P3 work to main (6a90173); ran every demo image through the default pipeline.
State: all four modalities complete end to end with my four stages. One design issue found for P4 (an unfaithful finding verified by a non-specific symptom).
Verified: cd backend && python -m pytest -q 1032 passed, 6 skipped after the merge; live runs as in the Decisions entry. The shown claims all cite a real note quote, none cites a side word, and flagged note text never appears.
Not run: studies through the HTTP API and the web app; calibration is still not in the pipeline (findings carry the flag uncalibrated).
Next: P4's reply on the status-rule recommendation; P4 and P2 wiring calibration.
Decisions: see the live-run entry. Contract change requests: none.
Did: pulled everything, built the website, ran the API, the MedGemma service and the site, and drove the live flows in a browser (sample, DICOM upload, report, audit, validation, models; 0 browser errors). Found and fixed three things the real inputs exposed: a fitting symptom verified findings whose heatmap had failed the faithfulness test; claims used dataset codes and an unspecified-region phrase; a laterality conflict was reported at full confidence. Wrote the demo guide and a 16-file input pack with a warm-up script.
State: all four modalities run end to end through the default pipeline with my four stages; the website works; demo guide in demo/DEMO_GUIDE.md.
Verified: cd backend && python -m pytest -q 1051 passed, 6 skipped before the last small change (report package 354 passed after it); red-team 97/97; python demo_inputs/warm_pack.py all 16 files ok; Playwright flows with Edge: landing, workstation, live sample 18.6 s, DICOM upload 22.4 s, report with FHIR download, audit Verify chain.
Not working or not shown: automatic routing (Not sure); the website does not display precedents; findings still carry the flag uncalibrated; the quality gate warns but does not lower confidence.
Next: P1 to check the router set-up; P4 and P2 to wire calibration; push these commits.
Decisions: see the five entries above. Contract change requests: none new.
Did: audited every backend output against the UI. Added to the workstation: quarantined note instructions, missing-history prompts, finding flags in words, quality reasons with fixes, selected-finding span ownership, a "Run live analysis" button on samples (earlier today), failed regions drawn in red. Added P2's figures to Validation and the datasheets to Models. Wired P2's calibrators as a calibration stage (brain, skin, bone; chest uncalibrated with reason) and made tier abstain uncertain in the status rule.
State: done. Live on this laptop with P2's weights and Groq keys: chest 3 verified + 2 uncertain with 5 claims; brain glioma verified on the note (region failed, drawn red); bone fracture verified on the note after calibration; skin nevus withheld. Not shown because not produced here: precedents, router/OOD, masks, voice dictation.
Verified: tests/test_calibration_stage.py 7 passed; full backend suite 1023 passed, 8 skipped (one order assertion updated for the new stage); cd web && npx vitest run 33 passed, npx playwright test 17 passed; browser checks of the injection upload, the four Run-live cases and the Validation figures.
Next: team decision on note-only verified (see P4 section); brain segmenter / MedSAM masks; P4.8.
Decisions: see Decisions, entries dated 2026-10-07 (abstain tier, chest uncalibrated).
Contract change requests: none.
Did: assigned the items nothing on the demo laptop produces yet: P1-A masks (U-Net and MedSAM hooks in the reader run() functions), P1-B box-region faithfulness for bone, P1-C FYI on the brain heatmap; P3-A precedents (MedSigLIP and indices; HAM10000 and RSNA thumbnails not displayable under the image-licence rule), P3-B router and OOD artifacts, P3-C voice dictation contract, P3-D the note-only "verified" decision, P3-E report wording. Details and "done when" in the P4 section.
State: P4 wiring for each is ready to follow as soon as it lands.
Verified: interfaces read from readers/segmenter.py, segment/medsam.py, context/voice.py, retrieval/index.py, intake/router.py.
Next: wire each delivery; P4.8.
Decisions: none new.
Contract change requests: none.
Did: MedSigLIP lives on P1's machine, so router and OOD set-up moved from P3 (old P3-B) to P1 (P1-D), and P1 also hands over the MedSigLIP folder that P3's precedents stage needs (P1-E). P3-A now covers only the retrieval indices and the thumbnail licences. State: assignments updated in the P4 section; nothing else changed. Verified: n/a (documentation). Next: wire each delivery. Decisions: none new. Contract change requests: none.
- 2026-10-07 · P1 · Heatmap evidence is marked unfaithful when it is off the segmented tumour (
heatmap_off_target, under 20% of salient pixels inside the U-Net mask) · the deletion test passed on a heatmap at the image border while the tumour was elsewhere (demo brain image); across 16 brain slices no CAM variant puts more than 14% of its mass in the mask. Threshold provisional; the U-Net is trained on a different dataset. - 2026-10-07 · P1 · Box-only findings (bone) get a box-region deletion test against equal-area random boxes, boxes over 35% of the image not assessable · same permutation rule and thresholds as the heatmap test, so one definition of "faithful".
Did: mask wiring for the brain, skin and bone reader stages; box-region faithfulness for bone; off-target heatmap rule for brain; router, OOD and model-folder hand-over (scripts/fetch_models.py).
State: P1-A, P1-B, P1-D, P1-E done; P1-C investigated (cause is the classifier, not the CAM method) and handled with a flag.
Verified: cd backend && python -m pytest -q 1057 passed, 2 skipped; real trained models: brain heatmap + U-Net mask, skin heatmap + MedSAM mask, bone bbox + MedSAM mask; bone box faithfulness 6 of 7 on staged fracture films; router and OOD run from the exported MedSigLIP folder.
Next: P4 passes unet= and medsam= from the pipeline and loads both once at start-up; P2 may want to look at why the brain classifier ignores the tumour (possible dataset shortcut).
Decisions: two new, in Decisions. Contract change requests: none.
Did: packaged the four precedent indices and licence-gated thumbnails (retrieval_pack.zip, docs/retrieval_indices.md); added dictation_to_note (a transcript appends to the typed note) and verified the audio key live; decided P3-D (failed region withheld, untestable region capped at uncertain, filed as P3-2); fixed the report wording (region kept, cough no longer supports a mass).
State: all four requests answered; P3-2 waits for P4's one-line change.
Verified: cd backend && python -m pytest -q gives 1119 passed, 4 skipped; the P3 ml tests (retrieval build, box evaluation, second reader) 37 passed; a live dictation round trip through Whisper on Groq returned the merged note; thumbnails built (2,447 bone, 3,349 brain); mypy --strict clean on medproof/report and reasoning_stages.
Not run: the browser after these changes (the API and website were stopped by the system for low memory, so the thumbnails and the new sentences have not been seen on screen), and P4's side of P3-2.
Next: P4 ack on P3-2, the /transcribe route and the notes-box flow, and showing bone and brain thumbnails.
Decisions: see the three entries above. Contract change requests: P3-2.
Did: pipeline.py now passes the mask models to the reader stages (brain gets the U-Net, skin and bone get MedSAM; loaded once per process, a failed load is remembered for five minutes, MEDPROOF_MASKS=0 switches masks off); StudyResult.ood_score now carries the router's score instead of the 0.0 placeholder; fixed _router_step, which flagged every routed study ood because the router's ood is a dict that is always truthy: only is_ood: true flags now.
State: P1-A and P1-D done end to end. Through the live API with the router on and no modality given: brain, skin and bone uploads were routed correctly, each got a Mask layer (served as image/png from /studies/{id}/artifacts/), and the brain finding carries heatmap_off_target. The bone finding gets faithful=true from the new box test.
Verified: cd backend && python -m pytest -q 1069 passed, 2 skipped. Env for a laptop that runs it: MEDPROOF_ROUTER=1, MEDPROOF_ROUTER_MODEL=<medsiglip folder>, MEDPROOF_MEDSAM_MODEL=<medsam folder>, plus router_probe.npz and ood.npz in backend/artifacts/ (see the P1 section for hashes). MedSAM on CPU is slow; the live runs took 4 to 9 s per study here because the first calls were warm.
Next: P4 to warm the mask models at start-up (first MedSAM call loads about 375 MB); P4 to check the UI shows the Mask layer and the heatmap_off_target flag.
Decisions: none new. Contract change requests: none. Files outside P1's area touched at the team's request: backend/medproof/pipeline.py (stage wiring, router flag fix, ood score) and a new backend/tests/test_pipeline_masks.py.
Did: checked the whisper and voice path at every layer; fixed an extraction gap found by it (spelled-out history statements were dropped).
State: the voice library works; the API and the website have no voice input yet (P4's to add).
Verified: voice unit tests 34 passed; live Whisper run on two synthesized dictations (clean and with a spoken attack): transcripts correct, attack flagged, facts correct including history after the fix; cd backend && python -m pytest -q 1139 passed, 4 skipped; held-out extraction F1 0.922 exact, 0.985 lenient.
Not run: a microphone recording in a browser (there is no recording code), and real human speech (the test audio is synthesized).
Next: P4 adds the transcribe route and the record button.
Decisions: see the two entries above. Contract change requests: none.
Did: checked every assigned item on main; applied P3-2 in core/status_rule.py (+4 tests); added POST /transcribe (P3's dictation_to_note, 25 MB limit, audio not stored) and a Dictate button in the notes box; added the "similar confirmed cases" panel (thumbnails only where P3's licence gate supplied one); fixed stage caching so verify results are keyed on their input findings; labelled an off-target heatmap as such instead of blaming the lesion box; start.bat points at local MedSAM/MedSigLIP folders; installed segmentation-models-pytorch and sentencepiece, fetched MedSAM.
State: live on this laptop: brain glioma uncertain (heatmap off the U-Net lesion, note supports it, mask drawn), skin nevus withheld (mask drawn), bone fracture verified by its box (mask drawn), chest 3 verified with region-naming sentences. Not testable here until files are copied: router/OOD and precedents (MedSigLIP refused for this token's account; probe files and retrieval pack are with P1 and P3). P1 edited pipeline.py directly for P1-A/P1-D; reviewed and kept.
Verified: full backend suite 1129 passed, 7 skipped, 1 failed (test_real_architectures.py::test_bone_reader_loads_a_real_yolo_checkpoint...: needs a newer ultralytics than this laptop's 8.3.221 to build yolo26n.yaml; the trained bone reader itself loads and runs); cd web && npx vitest run 34 passed, npx playwright test 17 passed; browser checks of brain and bone with masks.
Next: copy MedSigLIP, the two router files and the retrieval pack to this laptop, then check routing and precedents live; team to pick the demo machine.
Decisions: see Decisions (cache key digest, P3-2 applied).
Contract change requests: P3-2 acked and applied.
Did: found that P4 had already delivered the transcribe route, the Dictate button, the similar-cases panel and the P3-2 status cap, so verified them with real audio instead of rebuilding them; then closed P4's known gap 0 (cold start) with a startup warm-up that also wakes the MedGemma service.
State: voice works end to end in the API and the browser; cold start fixed; nothing of P4's listed items is left that I can do without their files.
Verified: cd backend && python -m pytest -q 1164 passed, 4 skipped; API tests 44 passed; /transcribe with real speech; Dictate button in Edge with a fake microphone; a never-seen chest film 109 s to 50 s after a cold restart.
Not run: real human speech; the warm-up on a machine with less memory.
Next: P4 may want warm-up status in the UI; the team still has to pick one demo laptop.
Decisions: see the two entries above. Contract change requests: none.
Did: wrote README.md against the submission guidelines: what Parallax does, the data pipeline (stage table, status rule, API routes, diagram), core models with ids, evidence and where the doctor sees it, a real sample input and output (demo/sample_output/: input, StudyResult, stage trace with ledger hashes and timestamps, FHIR bundle from a live chest run), validation headline with 95% CIs from reports/metrics.json, scope note (MVP vs stretch vs partial), technologies, install, model files, configuration, run (start.bat and manual), reproduction commands, demo order, layout, licences, limitations, team roles. Six screenshots in docs/screenshots/ (CC0, CC BY and public-domain images only).
State: P4.10 done. Every number is from reports/metrics.json, the committed sample run, or the code.
Verified: all paths the README references exist; sample output has no local filesystem paths.
Next: team to decide how the trained weights are distributed for reproduction (e.g. a GitHub release), then link it from the README's "Model files" table.
Decisions: none new.
Contract change requests: none.