From dd4544ba520102d7ac8683bf739c924406bf27cf Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 6 Nov 2026 22:27:36 +0100 Subject: [PATCH 01/14] fix(contract): make the 1.0 output contract match where each file is written, and declare --pedigree The read-count table was contracted under aligned/, where ALIGN_MINIMAP2's saveAs drops it; both legs write it to qc/. The static check matched every saveAs process by basename only, so the wrong folder passed. It now reads a saveAs closure of the form `f -> f.endsWith|startsWith|== '...' ? A : B`: a publishDir that returns null for a file does not count, one that keeps the name is matched on the full path, and only a real rename (or a closure it cannot read) falls back to the basename. 100 published rows said legs nextflow for a file the step script writes under the same path too. The static check now expands each step script's shell variables and refuses a nextflow row whose path it finds there; the rows are both. The viral depth table, which neither leg keeps, is internal. --pedigree, the switch of the family workflow, is now a declared param (nextflow.config, nextflow_schema.json, an experimental contract row), and main.nf reads it directly. The schema's default --tools now includes traits, as nextflow.config's does, and the static check compares every schema default with nextflow.config. Bead: pgp-5bja.20 --- assets/output_contract.tsv | 205 ++++++++++++++-------------- docs/output-contract.md | 4 +- main.nf | 5 +- nextflow.config | 1 + nextflow_schema.json | 8 +- scripts/ci/check-output-contract.py | 137 +++++++++++++++++-- tests/test_output_contract.py | 63 ++++++++- 7 files changed, 302 insertions(+), 121 deletions(-) diff --git a/assets/output_contract.tsv b/assets/output_contract.tsv index cca7450b..a97fa568 100644 --- a/assets/output_contract.tsv +++ b/assets/output_contract.tsv @@ -52,50 +52,49 @@ file {family}/{sample}_dnapainter.csv 38 FAMILY_REPORT nextflow csv experimental file {family}/{sample}_inheritance.tsv.gz 38 FAMILY_REPORT nextflow tsv experimental - - file {family}/{sample}_segments.svg 38 FAMILY_REPORT nextflow svg experimental - - file {family}/{sample}_segments.tsv 38 FAMILY_REPORT nextflow tsv experimental - - -file {sample}/aligned/{sample}_read_count.tsv 02 ALIGN_MINIMAP2 nextflow tsv stable - - file {sample}/aligned/{sample}_sorted.bam 02 ALIGN_MINIMAP2 both bam stable bash - file {sample}/aligned/{sample}_sorted.bam.bai 02 ALIGN_MINIMAP2 both index stable bash - -file {sample}/aligned/{sample}_sorted.cram 34 CRAM_ARCHIVE nextflow cram experimental - - -file {sample}/aligned/{sample}_sorted.cram.crai 34 CRAM_ARCHIVE nextflow index experimental - - +file {sample}/aligned/{sample}_sorted.cram 34 CRAM_ARCHIVE both cram experimental - - +file {sample}/aligned/{sample}_sorted.cram.crai 34 CRAM_ARCHIVE both index experimental - - file {sample}/aligned/{sample}_sorted.cram.flagstat 34 CRAM_ARCHIVE nextflow bin experimental - - -file {sample}/ancestry/{sample}_ancestry.tsv 25 PRS_SUMMARY nextflow tsv stable - - -file {sample}/ancestry_fine/{sample}_ancestry_fine.tsv 26b ANCESTRY_FINE_REPORT nextflow tsv experimental - - +file {sample}/ancestry/{sample}_ancestry.tsv 25 PRS_SUMMARY both tsv stable - - +file {sample}/ancestry_fine/{sample}_ancestry_fine.tsv 26b ANCESTRY_FINE_REPORT both tsv experimental - - file {sample}/ancestry_fine/{sample}_ancestry_fine.txt 26b ANCESTRY_FINE_REPORT nextflow text experimental - - -file {sample}/ancestry_fine/{sample}_painting.bed 26b ANCESTRY_FINE_REPORT nextflow bed experimental - - +file {sample}/ancestry_fine/{sample}_painting.bed 26b ANCESTRY_FINE_REPORT both bed experimental - - file {sample}/ancestry_fine/{sample}_painting.png 26b ANCESTRY_FINE_REPORT nextflow png experimental - - file {sample}/ancient/{sample}_ancient.tsv 44 ANCIENT_REPORT nextflow tsv experimental - - file {sample}/ancient/{sample}_ancient.txt 44 ANCIENT_REPORT nextflow text experimental - - file {sample}/ancient/{sample}_ancient_info.tsv 44 ANCIENT_REPORT nextflow tsv experimental - - file {sample}/ancient/{sample}_ancient_pca.svg 44 ANCIENT_REPORT nextflow svg experimental - - -file {sample}/annotsv/{sample}_annotsv_input.vcf 05 ANNOTSV nextflow vcf stable - - -file {sample}/annotsv/{sample}_sv_annotated.tsv 05 ANNOTSV nextflow tsv stable - - -file {sample}/annotsv/{sample}_sv_class4_5.tsv 05 ANNOTSV nextflow tsv stable - - -file {sample}/annotsv/{sample}_sv_class4_5_checked.tsv 05 ANNOTSV nextflow tsv stable - - -file {sample}/archaic/{sample}_archaic.tsv 43 ARCHAIC_REPORT nextflow tsv experimental - - +file {sample}/annotsv/{sample}_annotsv_input.vcf 05 ANNOTSV both vcf stable - - +file {sample}/annotsv/{sample}_sv_annotated.tsv 05 ANNOTSV both tsv stable - - +file {sample}/annotsv/{sample}_sv_class4_5.tsv 05 ANNOTSV both tsv stable - - +file {sample}/annotsv/{sample}_sv_class4_5_checked.tsv 05 ANNOTSV both tsv stable - - +file {sample}/archaic/{sample}_archaic.tsv 43 ARCHAIC_REPORT both tsv experimental - - file {sample}/archaic/{sample}_archaic.txt 43 ARCHAIC_REPORT nextflow text experimental - - -file {sample}/archaic/{sample}_archaic_segments.tsv 43 ARCHAIC_REPORT nextflow tsv experimental - - -file {sample}/bloodtype/{sample}_bloodtype.tsv 41 BLOODTYPE nextflow tsv experimental - - -file {sample}/bloodtype/{sample}_bloodtype_depth.tsv 41 BLOODTYPE_DEPTH nextflow tsv experimental - - -file {sample}/bloodtype/{sample}_bloodtype_evidence.tsv 41 BLOODTYPE nextflow tsv experimental - - -file {sample}/chip/{sample}_chip.tsv 49 CHIP_REPORT nextflow tsv experimental - - +file {sample}/archaic/{sample}_archaic_segments.tsv 43 ARCHAIC_REPORT both tsv experimental - - +file {sample}/bloodtype/{sample}_bloodtype.tsv 41 BLOODTYPE both tsv experimental - - +file {sample}/bloodtype/{sample}_bloodtype_depth.tsv 41 BLOODTYPE_DEPTH both tsv experimental - - +file {sample}/bloodtype/{sample}_bloodtype_evidence.tsv 41 BLOODTYPE both tsv experimental - - +file {sample}/chip/{sample}_chip.tsv 49 CHIP_REPORT both tsv experimental - - file {sample}/chip/{sample}_chip_depth.tsv 49 CHIP_REPORT nextflow tsv experimental - - -file {sample}/chip/{sample}_chip_filtered.vcf.gz 49 CHIP_MUTECT2 nextflow vcf experimental - - +file {sample}/chip/{sample}_chip_filtered.vcf.gz 49 CHIP_MUTECT2 both vcf experimental - - file {sample}/clinical/{sample}_clinical.vcf.gz 23 CLINICAL_FILTER both vcf stable bash - file {sample}/clinical/{sample}_clinical.vcf.gz.tbi 23 CLINICAL_FILTER nextflow index stable - - -file {sample}/clinical/{sample}_clinical_summary.tsv 23 CLINICAL_FILTER nextflow tsv stable - - -file {sample}/clinvar/{sample}_clinvar_cdna_check.tsv 06 CLINVAR_CDNA_CHECK nextflow tsv stable - - -file {sample}/clinvar/{sample}_clinvar_cdna_scan.tsv 06 CLINVAR_CDNA_CHECK nextflow tsv stable - - -file {sample}/clinvar/{sample}_clinvar_deep_intronic.tsv 06 CLINVAR_SCREEN nextflow tsv stable - - +file {sample}/clinical/{sample}_clinical_summary.tsv 23 CLINICAL_FILTER both tsv stable - - +file {sample}/clinvar/{sample}_clinvar_cdna_check.tsv 06 CLINVAR_CDNA_CHECK both tsv stable - - +file {sample}/clinvar/{sample}_clinvar_cdna_scan.tsv 06 CLINVAR_CDNA_CHECK both tsv stable - - +file {sample}/clinvar/{sample}_clinvar_deep_intronic.tsv 06 CLINVAR_SCREEN both tsv stable - - file {sample}/clinvar/{sample}_clinvar_hits.tsv 06 CLINVAR_SCREEN both tsv stable both chrom,pos,ref,alt,genotype,clinvar_id,geneinfo,clnsig,clnrevstat file {sample}/clinvar/{sample}_clinvar_hits.vcf 06 CLINVAR_SCREEN both vcf stable both INFO/GENEINFO,INFO/CLNSIG,INFO/CLNREVSTAT file {sample}/clinvar/{sample}_pass.vcf.gz 06 CLINVAR_SCREEN both vcf stable both FORMAT/GT file {sample}/clinvar/{sample}_pass.vcf.gz.tbi 06 CLINVAR_SCREEN both index stable both - file {sample}/cnvpytor/{sample}_cnvs.raw.vcf 18 CNVPYTOR nextflow vcf internal - - -file {sample}/cnvpytor/{sample}_cnvs.txt 18 CNVPYTOR nextflow text stable - - -file {sample}/cnvpytor/{sample}_cnvs.vcf.gz 18 CNVPYTOR_VCF nextflow vcf stable - - +file {sample}/cnvpytor/{sample}_cnvs.txt 18 CNVPYTOR both text stable - - +file {sample}/cnvpytor/{sample}_cnvs.vcf.gz 18 CNVPYTOR_VCF both vcf stable - - file {sample}/cnvpytor/{sample}_cnvs.vcf.gz.tbi 18 CNVPYTOR_VCF nextflow index stable - - -file {sample}/consumer_export/{sample}_23andme_v5.zip 40 CONSUMER_EXPORT nextflow zip experimental - - -file {sample}/consumer_export/{sample}_ancestrydna_layout.zip 40 CONSUMER_EXPORT nextflow zip experimental - - +file {sample}/consumer_export/{sample}_23andme_v5.zip 40 CONSUMER_EXPORT both zip experimental - - +file {sample}/consumer_export/{sample}_ancestrydna_layout.zip 40 CONSUMER_EXPORT both zip experimental - - file {sample}/consumer_export/{sample}_consumer_export.tsv 40 CONSUMER_EXPORT nextflow tsv experimental - - file {sample}/couple/{sample}_couple_carrier.tsv 39 COUPLE_CARRIER nextflow tsv experimental - - file {sample}/coverage/{sample}.mosdepth.global.dist.txt 16b MOSDEPTH nextflow text stable - - @@ -105,121 +104,122 @@ file {sample}/coverage/{sample}.thresholds.bed.gz 16b MOSDEPTH nextflow bed stab file {sample}/coverage/{sample}_callability.bed.gz 16b MOSDEPTH nextflow bed stable - - file {sample}/cpic/{sample}_cpic_recommendations.txt 27 CPIC_LOOKUP both text stable both - file {sample}/cpic/{sample}_phenotypes.tsv 27 CPIC_LOOKUP both tsv stable both - -file {sample}/cpsr/{sample}.cpsr.grch38.classification.tsv.gz 17 CPSR nextflow tsv stable - - -file {sample}/cpsr/{sample}.cpsr.grch38.html 17 CPSR nextflow html stable - - -file {sample}/cyrius/{sample}_cyp2d6.tsv 21 CYRIUS nextflow tsv stable - - -file {sample}/cyrius/{sample}_cyp2d6_depth_check.tsv 21 CYRIUS nextflow tsv stable - - -file {sample}/delly/{sample}_sv.vcf.gz 19 DELLY_BCF2VCF nextflow vcf stable nextflow INFO/SVTYPE +file {sample}/cpsr/{sample}.cpsr.grch38.classification.tsv.gz 17 CPSR both tsv stable - - +file {sample}/cpsr/{sample}.cpsr.grch38.html 17 CPSR both html stable - - +file {sample}/cyrius/{sample}_cyp2d6.tsv 21 CYRIUS both tsv stable - - +file {sample}/cyrius/{sample}_cyp2d6_depth_check.tsv 21 CYRIUS both tsv stable - - +file {sample}/delly/{sample}_sv.vcf.gz 19 DELLY_BCF2VCF both vcf stable nextflow INFO/SVTYPE file {sample}/delly/{sample}_sv.vcf.gz.tbi 19 DELLY_BCF2VCF nextflow index stable nextflow - -file {sample}/exomiser/{sample}_exomiser.txt 48 EXOMISER_REPORT nextflow text experimental - - -file {sample}/exomiser/{sample}_exomiser.variants.tsv 48 EXOMISER nextflow tsv experimental - - -file {sample}/exomiser/{sample}_exomiser_terms.tsv 48 EXOMISER_REPORT nextflow tsv experimental - - -file {sample}/exomiser/{sample}_exomiser_top.tsv 48 EXOMISER_REPORT nextflow tsv experimental - - +file {sample}/exomiser/{sample}_exomiser.txt 48 EXOMISER_REPORT both text experimental - - +file {sample}/exomiser/{sample}_exomiser.variants.tsv 48 EXOMISER both tsv experimental - - +file {sample}/exomiser/{sample}_exomiser_terms.tsv 48 EXOMISER_REPORT both tsv experimental - - +file {sample}/exomiser/{sample}_exomiser_top.tsv 48 EXOMISER_REPORT both tsv experimental - - file {sample}/expansion_hunter/{sample}.json 09 EXPANSION_HUNTER nextflow json stable - - file {sample}/expansion_hunter/{sample}.vcf 09 EXPANSION_HUNTER nextflow vcf stable - - -file {sample}/expansion_hunter/{sample}_eh_stranger.vcf 09b STRANGER nextflow vcf stable - - +file {sample}/expansion_hunter/{sample}_eh_stranger.vcf 09b STRANGER both vcf stable - - file {sample}/expansion_hunter/{sample}_realigned.bam 09 EXPANSION_HUNTER nextflow bam stable - - -file {sample}/expansion_hunter/{sample}_rfc1_motif.tsv 09b STRANGER_RFC1 nextflow tsv stable - - -file {sample}/expansion_hunter/{sample}_str_motifs.tsv 09b STRANGER_RFC1 nextflow tsv stable - - +file {sample}/expansion_hunter/{sample}_rfc1_motif.tsv 09b STRANGER_RFC1 both tsv stable - - +file {sample}/expansion_hunter/{sample}_str_motifs.tsv 09b STRANGER_RFC1 both tsv stable - - file {sample}/fastq_trimmed/{sample}_R1.fastq.gz 01b FASTP nextflow bin internal - - file {sample}/fastq_trimmed/{sample}_R2.fastq.gz 01b FASTP nextflow bin internal - - -file {sample}/fastq_trimmed/{sample}_fastp.html 01b FASTP nextflow html stable - - -file {sample}/fastq_trimmed/{sample}_fastp.json 01b FASTP nextflow json stable - - +file {sample}/fastq_trimmed/{sample}_fastp.html 01b FASTP both html stable - - +file {sample}/fastq_trimmed/{sample}_fastp.json 01b FASTP both json stable - - file {sample}/hla/{sample}_hla_genotype.tsv 08 HLA_TYPING nextflow tsv stable - - -file {sample}/hla_optitype/{sample}_hla_concordance.tsv 08 HLA_QC nextflow tsv experimental - - -file {sample}/hla_optitype/{sample}_optitype_result.tsv 08 HLA_QC nextflow tsv experimental - - +file {sample}/hla_optitype/{sample}_hla_concordance.tsv 08 HLA_QC both tsv experimental - - +file {sample}/hla_optitype/{sample}_optitype_result.tsv 08 HLA_QC both tsv experimental - - file {sample}/hla_t1k/{sample}_hla_genotype.tsv 08 - bash tsv stable bash - -file {sample}/indexcov 16 INDEXCOV nextflow dir stable - - +file {sample}/indexcov 16 INDEXCOV both dir stable - - file {sample}/indexcov/indexcov-indexcov.ped 16 - bash tsv stable bash - file {sample}/kir/database_release.txt 08 KIR_TYPING nextflow text experimental - - -file {sample}/kir/{sample}_kir_genotype.tsv 08 KIR_TYPING nextflow tsv experimental - - +file {sample}/kir/{sample}_kir_genotype.tsv 08 KIR_TYPING both tsv experimental - - file {sample}/logs/run_status.tsv - - nextflow tsv stable - - -file {sample}/lpa/{sample}_lpa_kiv2.tsv 46 LPA nextflow tsv experimental - - -file {sample}/lpa/{sample}_lpa_kiv2_depth.tsv 46 LPA_DEPTH nextflow tsv experimental - - -file {sample}/lpa/{sample}_lpa_kiv2_depth_q1.tsv 46 LPA_DEPTH nextflow tsv experimental - - -file {sample}/manta/results/variants/candidateSV.vcf.gz 04 MANTA nextflow vcf stable - - -file {sample}/manta/results/variants/diploidSV.raw.vcf.gz 04 MANTA nextflow vcf stable - - -file {sample}/manta/results/variants/diploidSV.raw.vcf.gz.tbi 04 MANTA nextflow index stable - - -file {sample}/manta/results/variants/diploidSV.vcf.gz 04 MANTA nextflow vcf stable - - -file {sample}/manta/results/variants/diploidSV.vcf.gz.tbi 04 MANTA nextflow index stable - - +file {sample}/lpa/{sample}_lpa_kiv2.tsv 46 LPA both tsv experimental - - +file {sample}/lpa/{sample}_lpa_kiv2_depth.tsv 46 LPA_DEPTH both tsv experimental - - +file {sample}/lpa/{sample}_lpa_kiv2_depth_q1.tsv 46 LPA_DEPTH both tsv experimental - - +file {sample}/manta/results/variants/candidateSV.vcf.gz 04 MANTA both vcf stable - - +file {sample}/manta/results/variants/diploidSV.raw.vcf.gz 04 MANTA both vcf stable - - +file {sample}/manta/results/variants/diploidSV.raw.vcf.gz.tbi 04 MANTA both index stable - - +file {sample}/manta/results/variants/diploidSV.vcf.gz 04 MANTA both vcf stable - - +file {sample}/manta/results/variants/diploidSV.vcf.gz.tbi 04 MANTA both index stable - - file {sample}/mei/{sample}_megane.log 50 MEI nextflow text experimental - - -file {sample}/mei/{sample}_megane.vcf 50 MEI nextflow vcf experimental - - -file {sample}/mei/{sample}_mei.tsv 50 MEI_REPORT nextflow tsv experimental - - -file {sample}/mei/{sample}_mei_annotsv.tsv 50 MEI_REPORT nextflow tsv experimental - - +file {sample}/mei/{sample}_megane.vcf 50 MEI both vcf experimental - - +file {sample}/mei/{sample}_mei.tsv 50 MEI_REPORT both tsv experimental - - +file {sample}/mei/{sample}_mei_annotsv.tsv 50 MEI_REPORT both tsv experimental - - file {sample}/mei/{sample}_mei_depth.tsv 50 MEI_DEPTH nextflow tsv experimental - - file {sample}/mei/{sample}_mei_mainchr.txt 50 MEI_DEPTH nextflow text internal - - -file {sample}/mei/{sample}_mei_sites.tsv 50 MEI_REPORT nextflow tsv experimental - - -file {sample}/mito/{sample}_chrM.fasta 12 MITO_EXTRACT_CHRM nextflow fasta stable - - +file {sample}/mei/{sample}_mei_sites.tsv 50 MEI_REPORT both tsv experimental - - +file {sample}/mito/{sample}_chrM.fasta 12 MITO_EXTRACT_CHRM both fasta stable - - file {sample}/mito/{sample}_chrM.vcf.gz 12 MITO_EXTRACT_CHRM nextflow vcf internal - - file {sample}/mito/{sample}_chrM.vcf.gz.tbi 12 MITO_EXTRACT_CHRM nextflow index internal - - -file {sample}/mito/{sample}_chrM_annotated.tsv 20 MITO_VARIANTS_ANNOTATE nextflow tsv stable - - -file {sample}/mito/{sample}_chrM_deletions.tsv 20 MITO_DELETIONS nextflow tsv stable - - +file {sample}/mito/{sample}_chrM_annotated.tsv 20 MITO_VARIANTS_ANNOTATE both tsv stable - - +file {sample}/mito/{sample}_chrM_deletions.tsv 20 MITO_DELETIONS both tsv stable - - file {sample}/mito/{sample}_chrM_filtered.vcf.gz 20 MITO_VARIANTS both vcf stable bash - file {sample}/mito/{sample}_chrM_filtered.vcf.gz.tbi 20 MITO_VARIANTS nextflow index stable - - file {sample}/mito/{sample}_chrM_mutect2.vcf.gz.stats 20 MITO_VARIANTS nextflow bin stable - - -file {sample}/mito/{sample}_haplocheck.txt 12 HAPLOCHECK nextflow text stable - - +file {sample}/mito/{sample}_haplocheck.txt 12 HAPLOCHECK both text stable - - file {sample}/mito/{sample}_haplogroup.txt 12 MITO_HAPLOGROUP both text stable bash - -file {sample}/mito/{sample}_mtdna_copies.tsv 20 MITO_DELETIONS nextflow tsv stable - - +file {sample}/mito/{sample}_mtdna_copies.tsv 20 MITO_DELETIONS both tsv stable - - file {sample}/mosaic/{sample}_mocha.log 51 MOCHA_CALL nextflow text experimental - - -file {sample}/mosaic/{sample}_mocha.tsv 51 MOCHA_REPORT nextflow tsv experimental - - +file {sample}/mosaic/{sample}_mocha.tsv 51 MOCHA_REPORT both tsv experimental - - file {sample}/mosaic/{sample}_mocha_calls_raw.tsv 51 MOCHA_CALL nextflow tsv experimental - - file {sample}/mosaic/{sample}_mocha_sites.tsv 51 MOCHA_CALL nextflow tsv experimental - - file {sample}/mosaic/{sample}_mocha_stats.tsv 51 MOCHA_CALL nextflow tsv experimental - - -file {sample}/mosaic/{sample}_mocha_summary.tsv 51 MOCHA_REPORT nextflow tsv experimental - - +file {sample}/mosaic/{sample}_mocha_summary.tsv 51 MOCHA_REPORT both tsv experimental - - file {sample}/locityper/{sample}_locityper.log 53 LOCITYPER both text experimental - - file {sample}/locityper/{sample}_locityper.tsv 53 LOCITYPER both tsv experimental - - file {sample}/locityper/work 53 LOCITYPER both dir experimental - - file {sample}/mosdepth/{sample}.mosdepth.summary.txt 16b - bash tsv stable bash chrom,length,bases,mean,min,max file {sample}/paralogs/{sample}_*_copy_number.tsv 35 PARASCOPY nextflow tsv experimental - - -file {sample}/paralogs/{sample}_cn_dosage.tsv 35 PARASCOPY nextflow tsv experimental - - -file {sample}/paralogs/{sample}_cn_intervals.tsv 35 PARASCOPY nextflow tsv experimental - - +file {sample}/paralogs/{sample}_cn_dosage.tsv 35 PARASCOPY both tsv experimental - - +file {sample}/paralogs/{sample}_cn_intervals.tsv 35 PARASCOPY both tsv experimental - - file {sample}/paralogs/{sample}_cyp21a2_psv.tsv 35 PARASCOPY both tsv experimental - - -file {sample}/paralogs/{sample}_hba_copy_number.tsv 35 PARASCOPY nextflow tsv experimental - - -file {sample}/paralogs/{sample}_paraphase 35b PARAPHASE nextflow dir experimental - - -file {sample}/paralogs/{sample}_paraphase_copy_number.tsv 35b PARAPHASE_REPORT nextflow tsv experimental - - -file {sample}/paralogs/{sample}_paraphase_variants.tsv 35b PARAPHASE_REPORT nextflow tsv experimental - - -file {sample}/paralogs/{sample}_parascopy 35 PARASCOPY nextflow dir experimental - - +file {sample}/paralogs/{sample}_hba_copy_number.tsv 35 PARASCOPY both tsv experimental - - +file {sample}/paralogs/{sample}_paraphase 35b PARAPHASE both dir experimental - - +file {sample}/paralogs/{sample}_paraphase_copy_number.tsv 35b PARAPHASE_REPORT both tsv experimental - - +file {sample}/paralogs/{sample}_paraphase_variants.tsv 35b PARAPHASE_REPORT both tsv experimental - - +file {sample}/paralogs/{sample}_parascopy 35 PARASCOPY both dir experimental - - file {sample}/paralogs/{sample}_smn_copy_number.tsv 35 PARASCOPY nextflow tsv experimental - - -file {sample}/pgx_consensus/{sample}_outside_calls.tsv 36 PGX_CONSENSUS nextflow tsv stable - - -file {sample}/pgx_consensus/{sample}_pgx_consensus.tsv 36 PGX_CONSENSUS nextflow tsv stable - - +file {sample}/pgx_consensus/{sample}_outside_calls.tsv 36 PGX_CONSENSUS both tsv stable - - +file {sample}/pgx_consensus/{sample}_pgx_consensus.tsv 36 PGX_CONSENSUS both tsv stable - - file {sample}/pharmcat/{sample}.match.json 07 PHARMCAT nextflow json stable - - file {sample}/pharmcat/{sample}.phenotype.json 07 PHARMCAT nextflow json stable - - file {sample}/pharmcat/{sample}.report.html 07 PHARMCAT nextflow html stable - - file {sample}/pharmcat/{sample}.report.json 07 PHARMCAT nextflow json stable nextflow - -file {sample}/prs/pgsc_calc 25 PRS nextflow dir stable - - +file {sample}/prs/pgsc_calc 25 PRS both dir stable - - file {sample}/prs/{sample}_prs_summary.*.tsv 25 PRS_SUMMARY nextflow tsv stable - - -file {sample}/prs/{sample}_prs_summary.tsv 25 PRS_SUMMARY nextflow tsv stable - - -file {sample}/pypgx/{sample}_cyp2d6_depth_check.tsv 32 PYPGX nextflow tsv stable - - -file {sample}/pypgx/{sample}_pharmcat_comparison.tsv 27 CPIC_LOOKUP nextflow tsv stable - - +file {sample}/prs/{sample}_prs_summary.tsv 25 PRS_SUMMARY both tsv stable - - +file {sample}/pypgx/{sample}_cyp2d6_depth_check.tsv 32 PYPGX both tsv stable - - +file {sample}/pypgx/{sample}_pharmcat_comparison.tsv 27 CPIC_LOOKUP both tsv stable - - file {sample}/pypgx/{sample}_pypgx_results 32 PYPGX nextflow dir stable - - file {sample}/pypgx/{sample}_pypgx_summary.tsv 32 PYPGX both tsv stable bash - file {sample}/qc/somalier/{sample}.somalier 33 SOMALIER nextflow bin experimental - - file {sample}/qc/somalier/{sample}.somalier_id 33 SOMALIER nextflow bin internal - - file {sample}/qc/verifybamid2/{sample}.Ancestry 33 VERIFYBAMID2 nextflow bin experimental - - -file {sample}/qc/verifybamid2/{sample}.log 33 VERIFYBAMID2 nextflow text experimental - - +file {sample}/qc/verifybamid2/{sample}.log 33 VERIFYBAMID2 both text experimental - - file {sample}/qc/verifybamid2/{sample}.marker_check 33 VERIFYBAMID2 nextflow bin internal - - -file {sample}/qc/verifybamid2/{sample}.selfSM 33 VERIFYBAMID2 nextflow bin experimental - - -file {sample}/qc/{sample}_sample_qc.tsv 33 SAMPLE_QC nextflow tsv experimental - - +file {sample}/qc/verifybamid2/{sample}.selfSM 33 VERIFYBAMID2 both bin experimental - - +file {sample}/qc/{sample}_read_count.tsv 02 ALIGN_MINIMAP2 both tsv stable - - +file {sample}/qc/{sample}_sample_qc.tsv 33 SAMPLE_QC both tsv experimental - - file {sample}/roh/{sample}_roh.txt 11 ROH nextflow text stable nextflow - file {sample}/roh/{sample}_roh_summary.txt 11 ROH nextflow tsv stable nextflow chrom,start,end,length_bp,length_mb file {sample}/run_manifest.tsv - - nextflow tsv stable - - file {sample}/slivar/{sample}.ped 31 SLIVAR_PRIORITIZE nextflow tsv internal - - -file {sample}/slivar/{sample}_compound_het_pairs.tsv 31 COMPHET_PHASE nextflow tsv stable - - +file {sample}/slivar/{sample}_compound_het_pairs.tsv 31 COMPHET_PHASE both tsv stable - - file {sample}/slivar/{sample}_compound_hets.vcf.gz 31 SLIVAR both vcf stable bash - file {sample}/slivar/{sample}_prioritized.vcf.gz 31 SLIVAR_PRIORITIZE both vcf stable bash - file {sample}/slivar/{sample}_prioritized.vcf.gz.tbi 31 SLIVAR_PRIORITIZE nextflow index stable - - -file {sample}/slivar/{sample}_slivar_summary.tsv 31 SLIVAR_PRIORITIZE nextflow tsv stable - - +file {sample}/slivar/{sample}_slivar_summary.tsv 31 SLIVAR_PRIORITIZE both tsv stable - - file {sample}/summary.json 24 HTML_REPORT both json stable bash schema_version,sample,generated_utc,run,databases,manifest,sections,cyp2d6,not_run,not_assessed file {sample}/sv_duphold/{sample}_sv_duphold.vcf 15 DUPHOLD nextflow vcf stable - - file {sample}/sv_filtered/{sample}_sv_filtered.log 15 DUPHOLD_FILTER nextflow text stable - - file {sample}/sv_filtered/{sample}_sv_filtered.vcf.gz 15 DUPHOLD_FILTER nextflow vcf stable - - file {sample}/sv_filtered/{sample}_sv_filtered.vcf.gz.tbi 15 DUPHOLD_FILTER nextflow index stable - - -file {sample}/sv_merged/{sample}_sv_consensus.vcf.gz 22 SURVIVOR_SORT nextflow vcf stable - - -file {sample}/sv_merged/{sample}_sv_consensus.vcf.gz.tbi 22 SURVIVOR_SORT nextflow index stable - - -file {sample}/telomere/{sample} 10 TELOMERE_HUNTER nextflow dir stable - - -file {sample}/traits/{sample}_genotype_rules.tsv 42 TRAITS nextflow tsv stable - - -file {sample}/traits/{sample}_traits.tsv 42 TRAITS nextflow tsv stable - - -file {sample}/traits/{sample}_traits_genotypes.tsv 42 TRAITS_SITES nextflow tsv stable - - +file {sample}/sv_merged/{sample}_sv_consensus.vcf.gz 22 SURVIVOR_SORT both vcf stable - - +file {sample}/sv_merged/{sample}_sv_consensus.vcf.gz.tbi 22 SURVIVOR_SORT both index stable - - +file {sample}/telomere/{sample} 10 TELOMERE_HUNTER both dir stable - - +file {sample}/traits/{sample}_genotype_rules.tsv 42 TRAITS both tsv stable - - +file {sample}/traits/{sample}_traits.tsv 42 TRAITS both tsv stable - - +file {sample}/traits/{sample}_traits_genotypes.tsv 42 TRAITS_SITES both tsv stable - - file {sample}/vcf/{sample}.g.vcf.gz 03 DEEPVARIANT both vcf stable - - file {sample}/vcf/{sample}.g.vcf.gz.tbi 03 DEEPVARIANT both index stable - - file {sample}/vcf/{sample}.report.html 07 - bash html stable bash - @@ -230,36 +230,36 @@ file {sample}/vcf/{sample}_roh.txt 11 - bash text stable bash - file {sample}/vcf/{sample}_roh_summary.txt 11 - bash tsv stable bash chrom,start,end,length_bp,length_mb file {sample}/vcf_gatk/{sample}.vcf.gz 03a - bash vcf stable bash FORMAT/GT file {sample}/vep/{sample}_annotated.vcf.gz 30 VCFANNO_INDEX both vcf stable both INFO/REVEL -file {sample}/vep/{sample}_annotated.vcf.gz.tbi 30 VCFANNO_INDEX nextflow index stable nextflow - +file {sample}/vep/{sample}_annotated.vcf.gz.tbi 30 VCFANNO_INDEX both index stable nextflow - file {sample}/vep/{sample}_vep.vcf.gz 13 VEP both vcf stable - - -file {sample}/vep/{sample}_vep.vcf.gz.tbi 13 VEP nextflow index stable - - +file {sample}/vep/{sample}_vep.vcf.gz.tbi 13 VEP both index stable - - file {sample}/vep/{sample}_vep.vcf_summary.html 13 VEP nextflow html stable - - file {sample}/viral/{sample}_human_coverage.tsv 47 VIRAL_READS nextflow tsv experimental - - -file {sample}/viral/{sample}_viral.tsv 47 VIRAL_REPORT nextflow tsv experimental - - +file {sample}/viral/{sample}_viral.tsv 47 VIRAL_REPORT both tsv experimental - - file {sample}/viral/{sample}_viral_coverage.tsv 47 VIRAL_READS nextflow tsv experimental - - -file {sample}/viral/{sample}_viral_depth.tsv 47 VIRAL_READS nextflow tsv experimental - - -file {sample}/viral/{sample}_viral_reads.bam 47 VIRAL_READS nextflow bam experimental - - +file {sample}/viral/{sample}_viral_depth.tsv 47 VIRAL_READS nextflow tsv internal - - +file {sample}/viral/{sample}_viral_reads.bam 47 VIRAL_READS both bam experimental - - file {sample}/viral/{sample}_viral_reads.bam.bai 47 VIRAL_READS nextflow index experimental - - -file {sample}/vntyper/run 52 VNTYPER nextflow dir experimental - - -file {sample}/vntyper/{sample}_vntyper.log 52 VNTYPER nextflow text experimental - - -file {sample}/vntyper/{sample}_vntyper.tsv 52 VNTYPER nextflow tsv experimental - - -file {sample}/y_haplogroup/{sample}_chrY.bam 37 Y_PILEUP nextflow bam experimental - - +file {sample}/vntyper/run 52 VNTYPER both dir experimental - - +file {sample}/vntyper/{sample}_vntyper.log 52 VNTYPER both text experimental - - +file {sample}/vntyper/{sample}_vntyper.tsv 52 VNTYPER both tsv experimental - - +file {sample}/y_haplogroup/{sample}_chrY.bam 37 Y_PILEUP both bam experimental - - file {sample}/y_haplogroup/{sample}_chrY.bam.bai 37 Y_PILEUP nextflow index experimental - - file {sample}/y_haplogroup/{sample}_idxstats.txt 37 Y_PILEUP nextflow text internal - - -file {sample}/y_haplogroup/{sample}_y_haplogroup.txt 37 Y_HAPLOGROUP nextflow text experimental - - +file {sample}/y_haplogroup/{sample}_y_haplogroup.txt 37 Y_HAPLOGROUP both text experimental - - file {sample}/y_haplogroup/{sample}_y_pileup.txt 37 Y_PILEUP nextflow text internal - - -file {sample}/y_haplogroup/{sample}_y_private_snps.tsv 37 Y_PRIVATE nextflow tsv experimental - - -file {sample}/y_haplogroup/{sample}_y_tree.txt 37 Y_HAPLOGROUP nextflow text experimental - - +file {sample}/y_haplogroup/{sample}_y_private_snps.tsv 37 Y_PRIVATE both tsv experimental - - +file {sample}/y_haplogroup/{sample}_y_tree.txt 37 Y_HAPLOGROUP both text experimental - - file {sample}/y_haplogroup/{sample}_y_tree_path.tsv 37 Y_HAPLOGROUP nextflow tsv experimental - - file {sample}/y_haplogroup/{sample}_y_tree_pileup.txt 37 Y_PILEUP nextflow text internal - - -file {sample}/y_str/{sample}_y_str.csv 45 Y_STR nextflow csv experimental - - +file {sample}/y_str/{sample}_y_str.csv 45 Y_STR both csv experimental - - file {sample}/y_str/{sample}_y_str.log 45 Y_STR nextflow text experimental - - -file {sample}/y_str/{sample}_y_str.tsv 45 Y_STR nextflow tsv experimental - - +file {sample}/y_str/{sample}_y_str.tsv 45 Y_STR both tsv experimental - - file {sample}/y_str/{sample}_y_str.vcf.gz 45 Y_STR nextflow vcf experimental - - -file {sample}/{sample}_charts.json 24 HTML_REPORT nextflow json stable - - +file {sample}/{sample}_charts.json 24 HTML_REPORT both json stable - - file {sample}/{sample}_report.html 24 HTML_REPORT both html stable bash - file {sample}/{sample}_report.txt 24 - both text stable bash - -file {sample}/{sample}_search_index.json 24 HTML_REPORT nextflow json stable - - +file {sample}/{sample}_search_index.json 24 HTML_REPORT both json stable - - file {sample}/{sample}_sex_check.tsv 16 INDEXCOV nextflow tsv stable - - param input - - - - stable - - param outdir - - - - stable - - @@ -310,6 +310,7 @@ param parascopy_data - - - - experimental - - param yleaf_data - - - - experimental - - param hpo - - - - experimental - - param exomiser_data - - - - experimental - - +param pedigree - - - - experimental - - param family_name - - - - experimental - - param exomiser_proband - - - - experimental - - param exomiser_assembly - - - - experimental - - diff --git a/docs/output-contract.md b/docs/output-contract.md index 0d49b17c..bfab4e87 100644 --- a/docs/output-contract.md +++ b/docs/output-contract.md @@ -10,7 +10,7 @@ The full list is one file, [`assets/output_contract.tsv`](https://github.com/Gei - **`--tools` names:** every name `main.nf` accepts (`pharmcat`, `clinvar`, `vcfanno`, ...). - **`run-all.sh`:** its two arguments (` `) and every switch in its `SWITCHES` list (`TOOLS`, `THREADS`, `SKIP_TRIM`, `INTERVALS`, `REF_FASTA`, ...). - **Result files:** every file the Nextflow pipeline publishes, which is also what `run-all.sh` writes, at its path under `GENOME_DIR//` (or `--outdir`). For each one the contract gives its step, its format and the fields a reader can count on: the columns of a table, the INFO and FORMAT fields of a VCF header, the top-level keys of a JSON file (`summary.json`'s keys are the ones its schema, `tests/schema/summary.schema.json`, requires). -- **Single-step scripts:** the files listed with leg `bash` or `both`. Some single steps write to other folders than the pipeline does (step 7 writes PharmCAT's reports to `vcf/`, the pipeline to `pharmcat/`; step 16b writes to `mosdepth/`, the pipeline to `coverage/`). Both names are kept. A single-step output that is not in the file is not part of the promise yet. +- **Single-step scripts:** the files listed with leg `bash` or `both`. A row is `both` when the step's script (`scripts/-*.sh`) writes the same path under `GENOME_DIR`. Some single steps write to other folders than the pipeline does (step 7 writes PharmCAT's reports to `vcf/`, the pipeline to `pharmcat/`; step 16b writes to `mosdepth/`, the pipeline to `coverage/`). Both names are kept. A single-step output that is not in the file is not part of the promise yet. ## What counts as a break @@ -40,7 +40,7 @@ The engines that run by default, and every step that existed before 1.0, are `st Two checks read the contract, both with [`scripts/ci/check-output-contract.py`](https://github.com/GeiserX/OwnGenome/blob/main/scripts/ci/check-output-contract.py): -- **On every pull request, with no run** (`tests/test_output_contract.py`, in the `unit-tests` job of the Guard workflow): every param of `nextflow_schema.json`, every `--tools` name of `main.nf` and every switch of `run-all.sh` has a row, and every row still names one. Every file row of a Nextflow process still matches a file that process writes and publishes, and every file a module publishes has a row. A removed or renamed stable item fails as `BREAKING`. A new one with no row fails too, with the message to add it, so the contract is always the full list. The test also plants each kind of break on a copy of the code (a renamed output, a removed param, a removed switch, a new tool and a new output with no row) and requires each to fail. +- **On every pull request, with no run** (`tests/test_output_contract.py`, in the `unit-tests` job of the Guard workflow): every param of `nextflow_schema.json`, every `--tools` name of `main.nf` and every switch of `run-all.sh` has a row, and every row still names one. Every file row of a Nextflow process still matches a file that process writes and publishes, in the folder of a `publishDir` that keeps it (a `saveAs` that returns `null` for the file does not count), and every file a module publishes has a row. A published row with leg `nextflow` fails when its step's script writes the same path too, since its leg is then `both`; the check reads the script with its shell variables filled in, so a path it cannot follow is not flagged, and a `both` row it does not find is not refused. Every default in `nextflow_schema.json` must equal the value in `nextflow.config`. A removed or renamed stable item fails as `BREAKING`. A new one with no row fails too, with the message to add it, so the contract is always the full list. The test also plants each kind of break on a copy of the code (a renamed output, a removed param, a removed switch, a new tool and a new output with no row, a schema default that differs from `nextflow.config`, a row in the folder a `saveAs` drops the file from, and a `nextflow` row a step script also writes) and requires each to fail. - **On the HG002 end-to-end run** (`tests/e2e/output-contract-1-hg002.sh`): every stable file whose `e2e` column names a leg must be in the results that leg wrote, under its contracted name, with its required fields. The bash leg is the single-step scripts the base cases run; the Nextflow leg is case `60-nextflow`. Rows whose step the fixture run does not reach have `e2e` set to `-` and are held by the static check only. The two files `run-all.sh` writes itself, `run_manifest.tsv` and `logs/run_status.tsv`, are listed but no check holds them yet: neither the fixture run nor a module writes them. The same case copies the Nextflow results, drops the `clnsig` column of the ClinVar hits table and renames the ROH summary, and requires the check to fail on both. It also prints every file each leg wrote with its fields, which is what to read when the contract is written again. ## Changing the contract diff --git a/main.nf b/main.nf index 6345bf79..2aaeee16 100644 --- a/main.nf +++ b/main.nf @@ -316,9 +316,8 @@ workflow { // leading family column: one family runs, the one --family_name names // (needed when the file holds more than one). Here only the family and // sample columns are read; FAMILY_PREPARE (bin/family_report.py prepare) - // checks the rest before the joint call starts. Read with containsKey, - // so a run without the option is not told about an undefined parameter. - def pedigree = params.containsKey('pedigree') ? params.pedigree : null + // checks the rest before the joint call starts. + def pedigree = params.pedigree // The family's folder under --outdir: --family_name, else 'family' def family_dir = params.family_name ?: 'family' def family_ids = [] diff --git a/nextflow.config b/nextflow.config index 176b8db8..5b95c3bf 100644 --- a/nextflow.config +++ b/nextflow.config @@ -121,6 +121,7 @@ params { viral_ref = null // viral: the viral genomes of step 47 (setup.sh --viral-data: reference/viral-/viral_genomes.fa) hpo = null // exomiser: the symptoms as HPO ids, comma separated (HP:0001250,HP:0001263); setting it runs exomiser (step 48) exomiser_data = null // exomiser: Exomiser's data folder (setup.sh --exomiser-data: reference/exomiser-) + pedigree = null // opt-in: a pedigree or families file (assets/pedigree.example.tsv, assets/families.example.tsv); runs the family workflow (steps 38, 39) family_name = null // --pedigree: the family to run of a families file (assets/families.example.tsv) that holds more than one; also its folder under --outdir (default family) exomiser_proband = null // exomiser: the sample the --hpo terms describe; needed when the samplesheet has more than one exomiser_assembly = 'hg38' // exomiser: the assembly of its data; hg19 only for Exomiser's cut-down test release (CI) diff --git a/nextflow_schema.json b/nextflow_schema.json index 6795fd58..104090e7 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -105,7 +105,7 @@ "tools": { "type": "string", "description": "Comma-separated list of tools to run; an unknown name stops the run. DB-heavy tools (vep, slivar, clinical_filter, cpsr, clinvar, expansion_hunter, sample_qc) and extras (stranger, ancestry (needs --ancestry_ref and prs), ancestry_fine (needs --ancestry_fine_panel), ancient (needs --ancient_panel; samples with a gVCF), pypgx, SV callers, cram_archive, cyrius, parascopy, paraphase (PacBio HiFi BAMs only), y_haplogroup (male samples), y_str (Y-STR repeat counts, male samples), consumer_export (samples with a gVCF), bloodtype (blood group antigens; RhD needs the BAM), lpa (the LPA KIV-2 repeat count; needs the BAM), vntyper (MUC1 VNTR frameshifts behind ADTKD-MUC1; needs the BAM), viral (viral DNA in the unmapped reads; needs --viral_ref and a BAM), exomiser (runs when --hpo is set), chip (clonal haematopoiesis from the BAM; needs --chip_gff), archaic (needs --archaic_data), mei (Alu, L1 and SVA insertions with MEGAnE; needs --megane_kmers and a BAM), mocha (mosaic chromosomal alterations; needs --mocha_data), locityper (a second opinion at HLA class II, KIR, CYP2D6 and FCGR; samples with a BAM; needs --locityper_data)) are opt-in. prs and vcfanno are skipped with a warning until --pgs_scoring or a score file is set. With pharmcat, PharmCAT also gets the HLA types of hla_typing and a CYP2D6 call pypgx and cyrius agree on (PGX_CONSENSUS).", - "default": "pharmcat,cpic,vcfanno,roh,prs,mito_haplogroup,telomere_hunter,mosdepth,mito_variants,html_report,multiqc", + "default": "pharmcat,cpic,vcfanno,roh,prs,mito_haplogroup,telomere_hunter,mosdepth,mito_variants,traits,html_report,multiqc", "fa_icon": "fas fa-toolbox" } } @@ -344,6 +344,12 @@ "description": "Exomiser's data folder, which `scripts/setup.sh --exomiser-data` installs as reference/exomiser- (its _hg38 and _phenotype folders). Required with --hpo.", "fa_icon": "fas fa-folder" }, + "pedigree": { + "type": "string", + "format": "file-path", + "description": "Opt-in: run the family workflow (steps 38 and 39) on the samples of the samplesheet this file names: a pedigree (assets/pedigree.example.tsv), or a families file with a leading family column (assets/families.example.tsv). It needs two or more of those samples, each with a gVCF, and --somalier_sites.", + "fa_icon": "fas fa-sitemap" + }, "family_name": { "type": "string", "pattern": "^[a-zA-Z0-9._][a-zA-Z0-9._-]*$", diff --git a/scripts/ci/check-output-contract.py b/scripts/ci/check-output-contract.py index e397cad5..c3737758 100755 --- a/scripts/ci/check-output-contract.py +++ b/scripts/ci/check-output-contract.py @@ -8,10 +8,14 @@ run: every param of nextflow_schema.json, every --tools name of main.nf (known_tools) and every run-all.sh switch (its SWITCHES list) has a row, and every row still names one; every file row of a process names a file - that process still writes and publishes; and every file a module - publishes has a row. A stable row that no longer matches is a break. A - new param, tool, switch or published file with no row fails too, so the - contract stays the full list. + that process still writes and publishes, in the folder of a publishDir + that keeps it; every file a module publishes has a row; a nextflow row + (not internal) whose step script writes the same path is refused (its + legs are both); + and every default of nextflow_schema.json equals nextflow.config's. A + stable row that no longer matches is a break. A new param, tool, switch + or published file with no row fails too, so the contract stays the full + list. --outputs DIR --leg bash|nextflow --sample S [--observe] The files a run wrote. DIR is the folder that holds / (GENOME_DIR @@ -109,8 +113,34 @@ def concrete(pattern, sample="SAMPLE"): return p.strip("/") +SAVE_AS = re.compile(r"^\s*(\w+)\s*->\s*\1\s*(?:\.(endsWith|startsWith)\(\s*'([^']*)'\s*\)|==\s*'([^']*)')" + r"\s*\?\s*([^?:]+?)\s*:\s*([^?:]+?)\s*$", re.S) + + +def save_as(closure, name): + """What a publishDir's saveAs closure does to a task file name (an output glob): + 'keep' (published under its own name), 'drop' (returns null) or 'rename'. + A closure this reader does not follow counts as 'rename', which the static + check matches by basename only.""" + if closure is None: + return "keep" + m = SAVE_AS.match(closure) + if not m: + return "rename" + var, method, arg, eq, yes, no = m.groups() + if method == "endsWith": + cond = name.endswith(arg) + elif method == "startsWith": + cond = name.startswith(arg) + else: + cond = name == eq + expr = (yes if cond else no).strip() + return "drop" if expr == "null" else "keep" if expr == var else "rename" + + def module_processes(repo): - """{process: {'publish': [(dir, [patterns] or None, has_saveas)], 'outputs': [glob]}} of modules/local.""" + """{process: {'publish': [(dir, [patterns] or None, saveAs closure body or None)], 'outputs': [glob]}} + of modules/local.""" procs = {} for f in sorted(glob.glob(os.path.join(repo, "modules", "local", "*", "main.nf"))): src = open(f).read() @@ -132,9 +162,10 @@ def module_processes(repo): stmt += "\n" + lines[k] d = re.search(r'publishDir\s+\{?\s*"([^"]+)"', stmt) pat = re.search(r"pattern:\s*[\"']([^\"']+)[\"']", stmt) + sa = re.search(r"saveAs:\s*\{(.*)\}", stmt, re.S) pubs.append((concrete(d.group(1)) if d else "", braces(pat.group(1)) if pat else None, - "saveAs" in stmt)) + sa.group(1) if sa else None)) out = re.search(r"^\s*output:\s*$(.*?)(?:^\s*(?:when|topic):|\Z)", head, re.M | re.S) outputs = [] if out: @@ -147,15 +178,19 @@ def module_processes(repo): def published(proc): - """(contracted-path glob, basename-only) pairs a process publishes; basename-only when a saveAs renames.""" + """(output glob, contracted-path glob, basename-only) of each file a process publishes. A publishDir + whose saveAs drops the file does not count; basename-only when its saveAs renames the file.""" items = [] - for d, pats, saveas in proc["publish"]: + for d, pats, closure in proc["publish"]: for g in proc["outputs"]: c = concrete(g) if pats is not None and not any(fnmatch.fnmatch(c.replace("*", "X"), p) or fnmatch.fnmatch(c, p) for p in pats): continue - items.append((g, (d + "/" + c) if d else c, saveas)) + action = save_as(closure, c) + if action == "drop": + continue + items.append((g, (d + "/" + c) if d else c, action == "rename")) return items @@ -165,6 +200,75 @@ def schema_params(repo): return {p for group in defs.values() for p in group.get("properties", {})} +def schema_defaults(repo): + s = json.load(open(os.path.join(repo, "nextflow_schema.json"))) + defs = s.get("definitions") or s.get("$defs") or {} + return {p: v["default"] for group in defs.values() for p, v in group.get("properties", {}).items() + if "default" in v} + + +def config_params(repo): + """{name: value} of nextflow.config's params block, for the plain literals (strings, numbers, + true, false, null); a value this reader cannot parse is left out.""" + src = open(os.path.join(repo, "nextflow.config")).read() + m = re.search(r"^params\s*\{(.*?)^\}", src, re.M | re.S) + out = {} + if not m: + return out + lit = {"true": True, "false": False, "null": None} + for name, val in re.findall(r"^\s*(\w+)\s*=\s*('[^']*'|\"[^\"]*\"|[-\w.]+)", m.group(1), re.M): + if val[0] in "'\"": + out[name] = val[1:-1] + elif val in lit: + out[name] = lit[val] + else: + try: + out[name] = int(val) + except ValueError: + try: + out[name] = float(val) + except ValueError: + pass + return out + + +VAR = re.compile(r"\$\{(\w+)\}|\$(\w+)") +ASSIGN = re.compile(r"^\s*(?:local\s+|export\s+|readonly\s+)?([A-Za-z_]\w*)=(\"[^\"\n]*\"|'[^'\n]*'|[^\s;#()]+)", re.M) + + +def bash_paths(src): + """The lines of a step script with its shell variables expanded: SAMPLE as {sample}, + GENOME_DIR as G, every other variable as each value it is assigned (a few at most).""" + assign = {} + for v, val in ASSIGN.findall(src): + assign.setdefault(v, []).append(val.strip("\"'")) + fixed = {"SAMPLE": "{sample}", "GENOME_DIR": "G", "1": "{sample}"} + + def expand(s, depth=0): + m = VAR.search(s) + if not m or depth > 6: + return [s] + name = m.group(1) or m.group(2) + vals = [fixed[name]] if name in fixed else assign.get(name) or ["\0"] + out = [] + for val in vals[:4]: + out.extend(expand(s[:m.start()] + val + s[m.end():], depth + 1)) + return out[:64] + + return {e for line in src.split("\n") if not line.lstrip().startswith("#") for e in expand(line)} + + +def bash_writes(repo, step, name, cache): + """The step script that names this per-sample path (scripts/-*.sh), or None.""" + if not name.startswith("{sample}/") or "*" in name or step == "-": + return None + if step not in cache: + files = sorted(glob.glob(os.path.join(repo, "scripts", f"{step}-*.sh"))) + cache[step] = (files, "\n".join(bash_paths("".join(open(f).read() for f in files))) if files else "") + files, text = cache[step] + return os.path.relpath(files[0], repo) if files and ("G/" + name) in text else None + + def known_tools(repo): src = open(os.path.join(repo, "main.nf")).read() m = re.search(r"def known_tools = \[(.*?)\]", src, re.S) @@ -193,6 +297,14 @@ def check_static(repo, rows): for name in sorted(have[kind] - set(listed)): problems.append(f"{kind} {name} of {where[kind]} has no row in the contract: add one") + defaults, config = schema_defaults(repo), config_params(repo) + if not config: + problems.append("found no params in nextflow.config: the reader no longer matches the code") + for name, val in sorted(defaults.items()): + if name in config and config[name] != val: + problems.append(f"param {name}: nextflow_schema.json's default {val!r} is not nextflow.config's " + f"{config[name]!r}") + procs = module_processes(repo) if not procs: problems.append("found no processes under modules/local") @@ -214,6 +326,13 @@ def check_static(repo, rows): what = "BREAKING: the stable file" if r["stability"] == "stable" else "the file" problems.append(f"{what} {r['name']} (contract line {r['line']}) is not what {r['process']} " f"publishes ({p['file']}): renamed, moved or no longer written") + cache = {} + for r in rows: + if r["kind"] == "file" and r["legs"] == "nextflow" and r["stability"] != "internal": + script = bash_writes(repo, r["step"], r["name"], cache) + if script: + problems.append(f"{r['name']} (contract line {r['line']}) has legs nextflow, but {script} " + "writes it under the same name too: its legs are both") for name, p in sorted(procs.items()): rows_p = [concrete(r["name"]) for r in file_rows if r["process"] == name] if not p["publish"]: diff --git a/tests/test_output_contract.py b/tests/test_output_contract.py index 38c6de8a..48350eda 100644 --- a/tests/test_output_contract.py +++ b/tests/test_output_contract.py @@ -5,9 +5,12 @@ run-all.sh switch and published Nextflow file has its row, and every row still matches the code; 2. static controls, each on a copy of the code: a renamed published file, a - removed param, a removed switch, a new --tools name with no row and a new - published file with no row are each refused, and the stable ones are - named BREAKING; + removed param, a removed switch, a new --tools name with no row, a new + published file with no row and a schema default that is not + nextflow.config's are each refused, and the stable ones are named + BREAKING; so are, on a copy of the contract, a file row in the folder of + a publishDir whose saveAs drops it and a nextflow row whose step script + writes the same path; --pedigree is a declared param; 3. the run check on a planted results folder made from the contract's own e2e rows passes for both legs; then a dropped column, a dropped VCF field, a dropped JSON key and a renamed file are each refused, naming the @@ -63,11 +66,30 @@ def code_copy(tmp): dst = os.path.join(tmp, "repo") shutil.copytree(os.path.join(REPO, "modules"), os.path.join(dst, "modules")) os.makedirs(os.path.join(dst, "scripts")) - for f in ("main.nf", "nextflow_schema.json", os.path.join("scripts", "run-all.sh")): + for f in ("main.nf", "nextflow_schema.json", "nextflow.config"): shutil.copy(os.path.join(REPO, f), os.path.join(dst, f)) + for f in os.listdir(os.path.join(REPO, "scripts")): + if f.endswith(".sh"): + shutil.copy(os.path.join(REPO, "scripts", f), os.path.join(dst, "scripts", f)) return dst +def contract_copy(tmp, name, change): + """The contract with one row changed: change(cells) edits the cells of the file row called name.""" + path = os.path.join(tmp, "contract.tsv") + hit = False + with open(CONTRACT) as f, open(path, "w") as g: + for line in f: + c = line.rstrip("\n").split("\t") + if c[0] == "file" and c[1] == name: + change(c) + line = "\t".join(c) + "\n" + hit = True + g.write(line) + assert hit, f"no file row {name}" + return path + + def edit(path, old, new, count=1): src = open(path).read() assert old in src, f"{old!r} not in {path}" @@ -96,6 +118,9 @@ def static_controls(): lambda d: edit(os.path.join(d, "modules", "local", "roh", "main.nf"), 'path("*_roh.txt"),', 'path("*_roh.txt"),\n tuple val(meta), path("*_roh_extra.tsv"), emit: extra\n'), [r"ROH .* writes \*_roh_extra\.tsv and has no contract row"]), + ("a schema default that is not nextflow.config's (traits left out of --tools)", + lambda d: edit(os.path.join(d, "nextflow_schema.json"), "mito_variants,traits,html_report", "mito_variants,html_report"), + [r"param tools: nextflow_schema\.json's default .* is not nextflow\.config's"]), ] for desc, change, want in cases: tmp = tempfile.mkdtemp() @@ -109,6 +134,36 @@ def static_controls(): shutil.rmtree(tmp) + def to_aligned(c): + c[1] = "{sample}/aligned/{sample}_read_count.tsv" + + def to_nextflow(c): + c[4] = "nextflow" + + contract_cases = [ + ("the read count in aligned/, whose publishDir drops it (it is published to qc/)", + "{sample}/qc/{sample}_read_count.tsv", to_aligned, + [r"BREAKING: the stable file \{sample\}/aligned/\{sample\}_read_count\.tsv .* is not what ALIGN_MINIMAP2 publishes"]), + ("legs nextflow on a file step 49 writes under the same name", + "{sample}/chip/{sample}_chip.tsv", to_nextflow, + [r"\{sample\}/chip/\{sample\}_chip\.tsv .* has legs nextflow, but scripts/49-chip\.sh writes it"]), + ] + for desc, name, change, want in contract_cases: + tmp = tempfile.mkdtemp() + try: + rc, out = run("--static", "--contract", contract_copy(tmp, name, change)) + missing = [w for w in want if not re.search(w, out)] + check(f"static control refused: {desc}", rc == 1 and not missing, f"rc={rc}, missing {missing}: {out[-1500:]}") + finally: + shutil.rmtree(tmp) + + config = open(os.path.join(REPO, "nextflow.config")).read() + schema = json.load(open(os.path.join(REPO, "nextflow_schema.json"))) + in_schema = any("pedigree" in g.get("properties", {}) for g in schema["definitions"].values()) + check("--pedigree is declared in nextflow.config and nextflow_schema.json", + re.search(r"^\s*pedigree\s*=\s*null\b", config, re.M) is not None and in_schema) + + def _drop_param(d, name): path = os.path.join(d, "nextflow_schema.json") s = json.load(open(path)) From 8e97a49423da16db1d2385fb30dae0f6e9d72197 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 6 Nov 2026 22:28:36 +0100 Subject: [PATCH 02/14] fix(contract): mark the Parascopy copy-number tables as written by both legs scripts/35-paralogs.sh moves each __copy_number.tsv (the SMN one included) into paralogs/, the same path PARASCOPY publishes. The static check cannot follow that loop, so these two rows are fixed by hand. Bead: pgp-5bja.20 --- assets/output_contract.tsv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/output_contract.tsv b/assets/output_contract.tsv index a97fa568..6e04ddd4 100644 --- a/assets/output_contract.tsv +++ b/assets/output_contract.tsv @@ -169,7 +169,7 @@ file {sample}/locityper/{sample}_locityper.log 53 LOCITYPER both text experiment file {sample}/locityper/{sample}_locityper.tsv 53 LOCITYPER both tsv experimental - - file {sample}/locityper/work 53 LOCITYPER both dir experimental - - file {sample}/mosdepth/{sample}.mosdepth.summary.txt 16b - bash tsv stable bash chrom,length,bases,mean,min,max -file {sample}/paralogs/{sample}_*_copy_number.tsv 35 PARASCOPY nextflow tsv experimental - - +file {sample}/paralogs/{sample}_*_copy_number.tsv 35 PARASCOPY both tsv experimental - - file {sample}/paralogs/{sample}_cn_dosage.tsv 35 PARASCOPY both tsv experimental - - file {sample}/paralogs/{sample}_cn_intervals.tsv 35 PARASCOPY both tsv experimental - - file {sample}/paralogs/{sample}_cyp21a2_psv.tsv 35 PARASCOPY both tsv experimental - - @@ -178,7 +178,7 @@ file {sample}/paralogs/{sample}_paraphase 35b PARAPHASE both dir experimental - file {sample}/paralogs/{sample}_paraphase_copy_number.tsv 35b PARAPHASE_REPORT both tsv experimental - - file {sample}/paralogs/{sample}_paraphase_variants.tsv 35b PARAPHASE_REPORT both tsv experimental - - file {sample}/paralogs/{sample}_parascopy 35 PARASCOPY both dir experimental - - -file {sample}/paralogs/{sample}_smn_copy_number.tsv 35 PARASCOPY nextflow tsv experimental - - +file {sample}/paralogs/{sample}_smn_copy_number.tsv 35 PARASCOPY both tsv experimental - - file {sample}/pgx_consensus/{sample}_outside_calls.tsv 36 PGX_CONSENSUS both tsv stable - - file {sample}/pgx_consensus/{sample}_pgx_consensus.tsv 36 PGX_CONSENSUS both tsv stable - - file {sample}/pharmcat/{sample}.match.json 07 PHARMCAT nextflow json stable - - From 56aecae6e65bb5550cd033adb40b1869fe5db2fb Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 6 Nov 2026 23:02:54 +0100 Subject: [PATCH 03/14] fix(contract): mark 23 more files the step scripts also write as legs both The legs check stopped after six variable expansions per line, so a line like ${P}_megane.log (P built from OUTDIR, S, GENOME_DIR and SAMPLE) was never followed. It now follows 24, capped at 64 expansions per line, and a new control proves it on step 50's megane.log. The rows it found, and the ones written through bash -c positional arguments, which it still cannot follow, are set to both by hand. --- assets/output_contract.tsv | 46 ++++++++++++++--------------- scripts/ci/check-output-contract.py | 7 ++++- tests/test_output_contract.py | 6 +++- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/assets/output_contract.tsv b/assets/output_contract.tsv index 6e04ddd4..a9fd3503 100644 --- a/assets/output_contract.tsv +++ b/assets/output_contract.tsv @@ -56,28 +56,28 @@ file {sample}/aligned/{sample}_sorted.bam 02 ALIGN_MINIMAP2 both bam stable bash file {sample}/aligned/{sample}_sorted.bam.bai 02 ALIGN_MINIMAP2 both index stable bash - file {sample}/aligned/{sample}_sorted.cram 34 CRAM_ARCHIVE both cram experimental - - file {sample}/aligned/{sample}_sorted.cram.crai 34 CRAM_ARCHIVE both index experimental - - -file {sample}/aligned/{sample}_sorted.cram.flagstat 34 CRAM_ARCHIVE nextflow bin experimental - - +file {sample}/aligned/{sample}_sorted.cram.flagstat 34 CRAM_ARCHIVE both bin experimental - - file {sample}/ancestry/{sample}_ancestry.tsv 25 PRS_SUMMARY both tsv stable - - file {sample}/ancestry_fine/{sample}_ancestry_fine.tsv 26b ANCESTRY_FINE_REPORT both tsv experimental - - file {sample}/ancestry_fine/{sample}_ancestry_fine.txt 26b ANCESTRY_FINE_REPORT nextflow text experimental - - file {sample}/ancestry_fine/{sample}_painting.bed 26b ANCESTRY_FINE_REPORT both bed experimental - - -file {sample}/ancestry_fine/{sample}_painting.png 26b ANCESTRY_FINE_REPORT nextflow png experimental - - -file {sample}/ancient/{sample}_ancient.tsv 44 ANCIENT_REPORT nextflow tsv experimental - - -file {sample}/ancient/{sample}_ancient.txt 44 ANCIENT_REPORT nextflow text experimental - - -file {sample}/ancient/{sample}_ancient_info.tsv 44 ANCIENT_REPORT nextflow tsv experimental - - -file {sample}/ancient/{sample}_ancient_pca.svg 44 ANCIENT_REPORT nextflow svg experimental - - +file {sample}/ancestry_fine/{sample}_painting.png 26b ANCESTRY_FINE_REPORT both png experimental - - +file {sample}/ancient/{sample}_ancient.tsv 44 ANCIENT_REPORT both tsv experimental - - +file {sample}/ancient/{sample}_ancient.txt 44 ANCIENT_REPORT both text experimental - - +file {sample}/ancient/{sample}_ancient_info.tsv 44 ANCIENT_REPORT both tsv experimental - - +file {sample}/ancient/{sample}_ancient_pca.svg 44 ANCIENT_REPORT both svg experimental - - file {sample}/annotsv/{sample}_annotsv_input.vcf 05 ANNOTSV both vcf stable - - file {sample}/annotsv/{sample}_sv_annotated.tsv 05 ANNOTSV both tsv stable - - file {sample}/annotsv/{sample}_sv_class4_5.tsv 05 ANNOTSV both tsv stable - - file {sample}/annotsv/{sample}_sv_class4_5_checked.tsv 05 ANNOTSV both tsv stable - - file {sample}/archaic/{sample}_archaic.tsv 43 ARCHAIC_REPORT both tsv experimental - - -file {sample}/archaic/{sample}_archaic.txt 43 ARCHAIC_REPORT nextflow text experimental - - +file {sample}/archaic/{sample}_archaic.txt 43 ARCHAIC_REPORT both text experimental - - file {sample}/archaic/{sample}_archaic_segments.tsv 43 ARCHAIC_REPORT both tsv experimental - - file {sample}/bloodtype/{sample}_bloodtype.tsv 41 BLOODTYPE both tsv experimental - - file {sample}/bloodtype/{sample}_bloodtype_depth.tsv 41 BLOODTYPE_DEPTH both tsv experimental - - file {sample}/bloodtype/{sample}_bloodtype_evidence.tsv 41 BLOODTYPE both tsv experimental - - file {sample}/chip/{sample}_chip.tsv 49 CHIP_REPORT both tsv experimental - - -file {sample}/chip/{sample}_chip_depth.tsv 49 CHIP_REPORT nextflow tsv experimental - - +file {sample}/chip/{sample}_chip_depth.tsv 49 CHIP_REPORT both tsv experimental - - file {sample}/chip/{sample}_chip_filtered.vcf.gz 49 CHIP_MUTECT2 both vcf experimental - - file {sample}/clinical/{sample}_clinical.vcf.gz 23 CLINICAL_FILTER both vcf stable bash - file {sample}/clinical/{sample}_clinical.vcf.gz.tbi 23 CLINICAL_FILTER nextflow index stable - - @@ -95,8 +95,8 @@ file {sample}/cnvpytor/{sample}_cnvs.vcf.gz 18 CNVPYTOR_VCF both vcf stable - - file {sample}/cnvpytor/{sample}_cnvs.vcf.gz.tbi 18 CNVPYTOR_VCF nextflow index stable - - file {sample}/consumer_export/{sample}_23andme_v5.zip 40 CONSUMER_EXPORT both zip experimental - - file {sample}/consumer_export/{sample}_ancestrydna_layout.zip 40 CONSUMER_EXPORT both zip experimental - - -file {sample}/consumer_export/{sample}_consumer_export.tsv 40 CONSUMER_EXPORT nextflow tsv experimental - - -file {sample}/couple/{sample}_couple_carrier.tsv 39 COUPLE_CARRIER nextflow tsv experimental - - +file {sample}/consumer_export/{sample}_consumer_export.tsv 40 CONSUMER_EXPORT both tsv experimental - - +file {sample}/couple/{sample}_couple_carrier.tsv 39 COUPLE_CARRIER both tsv experimental - - file {sample}/coverage/{sample}.mosdepth.global.dist.txt 16b MOSDEPTH nextflow text stable - - file {sample}/coverage/{sample}.mosdepth.summary.txt 16b MOSDEPTH nextflow tsv stable nextflow chrom,length,bases,mean,min,max file {sample}/coverage/{sample}.regions.bed.gz 16b MOSDEPTH nextflow bed stable - - @@ -141,11 +141,11 @@ file {sample}/manta/results/variants/diploidSV.raw.vcf.gz 04 MANTA both vcf stab file {sample}/manta/results/variants/diploidSV.raw.vcf.gz.tbi 04 MANTA both index stable - - file {sample}/manta/results/variants/diploidSV.vcf.gz 04 MANTA both vcf stable - - file {sample}/manta/results/variants/diploidSV.vcf.gz.tbi 04 MANTA both index stable - - -file {sample}/mei/{sample}_megane.log 50 MEI nextflow text experimental - - +file {sample}/mei/{sample}_megane.log 50 MEI both text experimental - - file {sample}/mei/{sample}_megane.vcf 50 MEI both vcf experimental - - file {sample}/mei/{sample}_mei.tsv 50 MEI_REPORT both tsv experimental - - file {sample}/mei/{sample}_mei_annotsv.tsv 50 MEI_REPORT both tsv experimental - - -file {sample}/mei/{sample}_mei_depth.tsv 50 MEI_DEPTH nextflow tsv experimental - - +file {sample}/mei/{sample}_mei_depth.tsv 50 MEI_DEPTH both tsv experimental - - file {sample}/mei/{sample}_mei_mainchr.txt 50 MEI_DEPTH nextflow text internal - - file {sample}/mei/{sample}_mei_sites.tsv 50 MEI_REPORT both tsv experimental - - file {sample}/mito/{sample}_chrM.fasta 12 MITO_EXTRACT_CHRM both fasta stable - - @@ -159,11 +159,11 @@ file {sample}/mito/{sample}_chrM_mutect2.vcf.gz.stats 20 MITO_VARIANTS nextflow file {sample}/mito/{sample}_haplocheck.txt 12 HAPLOCHECK both text stable - - file {sample}/mito/{sample}_haplogroup.txt 12 MITO_HAPLOGROUP both text stable bash - file {sample}/mito/{sample}_mtdna_copies.tsv 20 MITO_DELETIONS both tsv stable - - -file {sample}/mosaic/{sample}_mocha.log 51 MOCHA_CALL nextflow text experimental - - +file {sample}/mosaic/{sample}_mocha.log 51 MOCHA_CALL both text experimental - - file {sample}/mosaic/{sample}_mocha.tsv 51 MOCHA_REPORT both tsv experimental - - -file {sample}/mosaic/{sample}_mocha_calls_raw.tsv 51 MOCHA_CALL nextflow tsv experimental - - -file {sample}/mosaic/{sample}_mocha_sites.tsv 51 MOCHA_CALL nextflow tsv experimental - - -file {sample}/mosaic/{sample}_mocha_stats.tsv 51 MOCHA_CALL nextflow tsv experimental - - +file {sample}/mosaic/{sample}_mocha_calls_raw.tsv 51 MOCHA_CALL both tsv experimental - - +file {sample}/mosaic/{sample}_mocha_sites.tsv 51 MOCHA_CALL both tsv experimental - - +file {sample}/mosaic/{sample}_mocha_stats.tsv 51 MOCHA_CALL both tsv experimental - - file {sample}/mosaic/{sample}_mocha_summary.tsv 51 MOCHA_REPORT both tsv experimental - - file {sample}/locityper/{sample}_locityper.log 53 LOCITYPER both text experimental - - file {sample}/locityper/{sample}_locityper.tsv 53 LOCITYPER both tsv experimental - - @@ -234,28 +234,28 @@ file {sample}/vep/{sample}_annotated.vcf.gz.tbi 30 VCFANNO_INDEX both index stab file {sample}/vep/{sample}_vep.vcf.gz 13 VEP both vcf stable - - file {sample}/vep/{sample}_vep.vcf.gz.tbi 13 VEP both index stable - - file {sample}/vep/{sample}_vep.vcf_summary.html 13 VEP nextflow html stable - - -file {sample}/viral/{sample}_human_coverage.tsv 47 VIRAL_READS nextflow tsv experimental - - +file {sample}/viral/{sample}_human_coverage.tsv 47 VIRAL_READS both tsv experimental - - file {sample}/viral/{sample}_viral.tsv 47 VIRAL_REPORT both tsv experimental - - -file {sample}/viral/{sample}_viral_coverage.tsv 47 VIRAL_READS nextflow tsv experimental - - +file {sample}/viral/{sample}_viral_coverage.tsv 47 VIRAL_READS both tsv experimental - - file {sample}/viral/{sample}_viral_depth.tsv 47 VIRAL_READS nextflow tsv internal - - file {sample}/viral/{sample}_viral_reads.bam 47 VIRAL_READS both bam experimental - - -file {sample}/viral/{sample}_viral_reads.bam.bai 47 VIRAL_READS nextflow index experimental - - +file {sample}/viral/{sample}_viral_reads.bam.bai 47 VIRAL_READS both index experimental - - file {sample}/vntyper/run 52 VNTYPER both dir experimental - - file {sample}/vntyper/{sample}_vntyper.log 52 VNTYPER both text experimental - - file {sample}/vntyper/{sample}_vntyper.tsv 52 VNTYPER both tsv experimental - - file {sample}/y_haplogroup/{sample}_chrY.bam 37 Y_PILEUP both bam experimental - - -file {sample}/y_haplogroup/{sample}_chrY.bam.bai 37 Y_PILEUP nextflow index experimental - - +file {sample}/y_haplogroup/{sample}_chrY.bam.bai 37 Y_PILEUP both index experimental - - file {sample}/y_haplogroup/{sample}_idxstats.txt 37 Y_PILEUP nextflow text internal - - file {sample}/y_haplogroup/{sample}_y_haplogroup.txt 37 Y_HAPLOGROUP both text experimental - - file {sample}/y_haplogroup/{sample}_y_pileup.txt 37 Y_PILEUP nextflow text internal - - file {sample}/y_haplogroup/{sample}_y_private_snps.tsv 37 Y_PRIVATE both tsv experimental - - file {sample}/y_haplogroup/{sample}_y_tree.txt 37 Y_HAPLOGROUP both text experimental - - -file {sample}/y_haplogroup/{sample}_y_tree_path.tsv 37 Y_HAPLOGROUP nextflow tsv experimental - - +file {sample}/y_haplogroup/{sample}_y_tree_path.tsv 37 Y_HAPLOGROUP both tsv experimental - - file {sample}/y_haplogroup/{sample}_y_tree_pileup.txt 37 Y_PILEUP nextflow text internal - - file {sample}/y_str/{sample}_y_str.csv 45 Y_STR both csv experimental - - -file {sample}/y_str/{sample}_y_str.log 45 Y_STR nextflow text experimental - - +file {sample}/y_str/{sample}_y_str.log 45 Y_STR both text experimental - - file {sample}/y_str/{sample}_y_str.tsv 45 Y_STR both tsv experimental - - -file {sample}/y_str/{sample}_y_str.vcf.gz 45 Y_STR nextflow vcf experimental - - +file {sample}/y_str/{sample}_y_str.vcf.gz 45 Y_STR both vcf experimental - - file {sample}/{sample}_charts.json 24 HTML_REPORT both json stable - - file {sample}/{sample}_report.html 24 HTML_REPORT both html stable bash - file {sample}/{sample}_report.txt 24 - both text stable bash - diff --git a/scripts/ci/check-output-contract.py b/scripts/ci/check-output-contract.py index c3737758..c5c3efa8 100755 --- a/scripts/ci/check-output-contract.py +++ b/scripts/ci/check-output-contract.py @@ -244,15 +244,20 @@ def bash_paths(src): assign.setdefault(v, []).append(val.strip("\"'")) fixed = {"SAMPLE": "{sample}", "GENOME_DIR": "G", "1": "{sample}"} + # depth counts every variable filled in on the line, not the nesting: a line with + # ${WORK}, ${P} and P's own chain of OUTDIR, S, GENOME_DIR and SAMPLE needs seven. + # The search stops at 64 expansions of a line, so a deep line stays cheap. def expand(s, depth=0): m = VAR.search(s) - if not m or depth > 6: + if not m or depth > 24: return [s] name = m.group(1) or m.group(2) vals = [fixed[name]] if name in fixed else assign.get(name) or ["\0"] out = [] for val in vals[:4]: out.extend(expand(s[:m.start()] + val + s[m.end():], depth + 1)) + if len(out) >= 64: + break return out[:64] return {e for line in src.split("\n") if not line.lstrip().startswith("#") for e in expand(line)} diff --git a/tests/test_output_contract.py b/tests/test_output_contract.py index 48350eda..ece33e51 100644 --- a/tests/test_output_contract.py +++ b/tests/test_output_contract.py @@ -10,7 +10,8 @@ nextflow.config's are each refused, and the stable ones are named BREAKING; so are, on a copy of the contract, a file row in the folder of a publishDir whose saveAs drops it and a nextflow row whose step script - writes the same path; --pedigree is a declared param; + writes the same path (also on a line of seven shell variables); + --pedigree is a declared param; 3. the run check on a planted results folder made from the contract's own e2e rows passes for both legs; then a dropped column, a dropped VCF field, a dropped JSON key and a renamed file are each refused, naming the @@ -147,6 +148,9 @@ def to_nextflow(c): ("legs nextflow on a file step 49 writes under the same name", "{sample}/chip/{sample}_chip.tsv", to_nextflow, [r"\{sample\}/chip/\{sample\}_chip\.tsv .* has legs nextflow, but scripts/49-chip\.sh writes it"]), + ("legs nextflow on a file step 50 writes on a line of seven shell variables", + "{sample}/mei/{sample}_megane.log", to_nextflow, + [r"\{sample\}/mei/\{sample\}_megane\.log .* has legs nextflow, but scripts/50-mei\.sh writes it"]), ] for desc, name, change, want in contract_cases: tmp = tempfile.mkdtemp() From fe15f025fe44c57101d6fc7f56f18121bca968df Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 6 Nov 2026 23:38:09 +0100 Subject: [PATCH 04/14] fix(contract): read a saveAs closure to its own brace, match script paths whole A publishDir option after the saveAs closure was read as part of it, which turned a dropping closure into a rename. A step script writing x.tsv.gz counted as writing x.tsv. Both readers now stop at the right place, with a control each. The --pedigree description says a file of several families needs --family_name. --- nextflow_schema.json | 2 +- scripts/ci/check-output-contract.py | 22 +++++++++++++++++++--- tests/test_output_contract.py | 27 ++++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/nextflow_schema.json b/nextflow_schema.json index 104090e7..91e62b6f 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -347,7 +347,7 @@ "pedigree": { "type": "string", "format": "file-path", - "description": "Opt-in: run the family workflow (steps 38 and 39) on the samples of the samplesheet this file names: a pedigree (assets/pedigree.example.tsv), or a families file with a leading family column (assets/families.example.tsv). It needs two or more of those samples, each with a gVCF, and --somalier_sites.", + "description": "Opt-in: run the family workflow (steps 38 and 39) on the samples of the samplesheet this file names: a pedigree (assets/pedigree.example.tsv), or a families file with a leading family column (assets/families.example.tsv). A families file that holds more than one family needs --family_name, and only that family runs. The family that runs needs two or more of those samples, each with a gVCF, and --somalier_sites.", "fa_icon": "fas fa-sitemap" }, "family_name": { diff --git a/scripts/ci/check-output-contract.py b/scripts/ci/check-output-contract.py index c5c3efa8..d0dd192e 100755 --- a/scripts/ci/check-output-contract.py +++ b/scripts/ci/check-output-contract.py @@ -117,6 +117,21 @@ def concrete(pattern, sample="SAMPLE"): r"\s*\?\s*([^?:]+?)\s*:\s*([^?:]+?)\s*$", re.S) +def closure_body(stmt): + """The body of a publishDir's saveAs closure, up to its own closing brace (an option + after it is not part of it), or None when the statement has no saveAs.""" + m = re.search(r"saveAs:\s*\{", stmt) + if not m: + return None + depth, i = 1, m.end() + while i < len(stmt): + depth += {"{": 1, "}": -1}.get(stmt[i], 0) + if depth == 0: + return stmt[m.end():i] + i += 1 + return stmt[m.end():] + + def save_as(closure, name): """What a publishDir's saveAs closure does to a task file name (an output glob): 'keep' (published under its own name), 'drop' (returns null) or 'rename'. @@ -162,10 +177,9 @@ def module_processes(repo): stmt += "\n" + lines[k] d = re.search(r'publishDir\s+\{?\s*"([^"]+)"', stmt) pat = re.search(r"pattern:\s*[\"']([^\"']+)[\"']", stmt) - sa = re.search(r"saveAs:\s*\{(.*)\}", stmt, re.S) pubs.append((concrete(d.group(1)) if d else "", braces(pat.group(1)) if pat else None, - sa.group(1) if sa else None)) + closure_body(stmt))) out = re.search(r"^\s*output:\s*$(.*?)(?:^\s*(?:when|topic):|\Z)", head, re.M | re.S) outputs = [] if out: @@ -271,7 +285,9 @@ def bash_writes(repo, step, name, cache): files = sorted(glob.glob(os.path.join(repo, "scripts", f"{step}-*.sh"))) cache[step] = (files, "\n".join(bash_paths("".join(open(f).read() for f in files))) if files else "") files, text = cache[step] - return os.path.relpath(files[0], repo) if files and ("G/" + name) in text else None + # the whole path: a longer name that starts with it (x.tsv.gz for x.tsv) is not it + hit = files and re.search(re.escape("G/" + name) + r"(?![\w.-])", text) + return os.path.relpath(files[0], repo) if hit else None def known_tools(repo): diff --git a/tests/test_output_contract.py b/tests/test_output_contract.py index ece33e51..8e896754 100644 --- a/tests/test_output_contract.py +++ b/tests/test_output_contract.py @@ -11,7 +11,8 @@ BREAKING; so are, on a copy of the contract, a file row in the folder of a publishDir whose saveAs drops it and a nextflow row whose step script writes the same path (also on a line of seven shell variables); - --pedigree is a declared param; + --pedigree is a declared param; a saveAs closure is read up to its own + brace and a script path counts only as a whole path; 3. the run check on a planted results folder made from the contract's own e2e rows passes for both legs; then a dropped column, a dropped VCF field, a dropped JSON key and a renamed file are each refused, naming the @@ -269,6 +270,29 @@ def run_controls(): shutil.rmtree(tmp) +def reader_controls(): + """The two readers the static check is built on, on inputs the code does not have yet.""" + import importlib.util + spec = importlib.util.spec_from_file_location("contract_check", CHECK) + m = importlib.util.module_from_spec(spec) + spec.loader.exec_module(m) + stmt = "publishDir \"x\", saveAs: { fn -> fn.endsWith('.log') ? null : fn }, enabled: { true }" + body = m.closure_body(stmt) + check("a saveAs closure ends at its own brace (an option after it is not part of it)", + body is not None and "enabled" not in body and m.save_as(body, "a.log") == "drop", repr(body)) + tmp = tempfile.mkdtemp() + try: + os.makedirs(os.path.join(tmp, "scripts")) + open(os.path.join(tmp, "scripts", "11-roh.sh"), "w").write( + 'S="${GENOME_DIR}/${SAMPLE}"\nbgzip -c x > "${S}/roh/${SAMPLE}_roh.txt.gz"\n') + short = m.bash_writes(tmp, "11", "{sample}/roh/{sample}_roh.txt", {}) + full = m.bash_writes(tmp, "11", "{sample}/roh/{sample}_roh.txt.gz", {}) + check("a step script writing x.txt.gz does not count as writing x.txt", + short is None and full == "scripts/11-roh.sh", f"{short!r} {full!r}") + finally: + shutil.rmtree(tmp) + + def malformed(): tmp = tempfile.mkdtemp() try: @@ -287,6 +311,7 @@ def malformed(): static_controls() +reader_controls() run_controls() malformed() print(f"{FAILS} failed") From 74a80d2103679d743bd1b59bec7067d254ba842e Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sat, 7 Nov 2026 20:20:35 +0100 Subject: [PATCH 05/14] feat(report): give every section a caveat and key them by section id Nine sections had no caveat of their own: PharmCAT, CPIC, the CYP2D6 callers, CPSR, VNtyper, the SV callers, coverage, variant calling and Lp(a). Both reports now print one under the card title. SECTION_NOTES maps each CHAPTERS section id to its caveat constants, so the live site can show the same text per card. The PharmCAT, CPSR and step 36 badges lose their green: a report that ran is not a finding. Bead: pgp-5bja --- bin/render_report.py | 106 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 99 insertions(+), 7 deletions(-) diff --git a/bin/render_report.py b/bin/render_report.py index 654b450c..0b82fcbf 100755 --- a/bin/render_report.py +++ b/bin/render_report.py @@ -90,6 +90,35 @@ def ancient_lines(d): "only. See a genetic counsellor before acting on a result, or on its absence (docs/39-couple.md).") PRS_ANCESTRY_NOTE = ("Most PRS scores were developed in European-ancestry populations and predict less well for " "other ancestries, even with an ancestry-adjusted percentile.") +# The caveats of the sections that had none, printed by both reports and the live site. +PHARMCAT_NOTE = ("PharmCAT calls star alleles and phenotypes from the variants this genome holds in the genes it " + "covers. A variant it cannot see (a deletion, a duplication, a hybrid gene outside CYP2D6) is not in " + "its answer. Its drug guidance is for a prescriber to read with you: never change a medicine on " + "this report alone.") +CPIC_NOTE = ("CPIC recommendations follow the phenotype PharmCAT called. They are guidance for prescribers, not a " + "prescription. A gene that was not called has no recommendation here, and that is not the same as a " + "typical response.") +PGX_CALLERS_NOTE = ("CYP2D6 sits beside its look-alike CYP2D7, and short reads often cannot tell them apart, so " + "pypgx, Cyrius and step 36 can disagree or give no call. A disagreement or a missing call means " + "the gene was not read reliably here; a clinical CYP2D6 test settles it.") +CPSR_NOTE = ("CPSR grades variants in cancer predisposition genes with ACMG rules. A pathogenic or likely pathogenic " + "row is something to take to a clinical geneticist, not a diagnosis. No row does not rule out an " + "inherited cancer risk: large deletions, genes outside its panel and family history are not part of " + "this screen.") +VNTYPER_NOTE = ("The MUC1 repeat is long and GC-rich, and short reads cover it unevenly. VNtyper looks only for " + "known frameshifts in it: a negative result lowers the chance of those and does not rule out " + "ADTKD-MUC1, and a positive one needs a clinical test (SNaPshot for dupC) to confirm it.") +SV_CALLERS_NOTE = ("Short reads find large deletions and duplications better than insertions and inversions, and " + "the callers disagree on many events. The totals count every call, most of them common in the " + "population; only the AnnotSV class 4 and 5 rows are graded for disease.") +COVERAGE_NOTE = ("Mean depth is an average over the genome: some genes get far fewer reads, and the gene " + "callability card lists those. The sex, identity and contamination checks catch a swapped or " + "mixed sample, not every lab error.") +VARIANTS_NOTE = ("Counts of the calls DeepVariant made against the reference. A whole genome usually holds 4 to 5 " + "million, most of them common. PASS calls passed the caller's filter; a count says nothing about " + "health on its own.") +LPA_NOTE = ("The KIV-2 repeat count from read depth is a research estimate, not an Lp(a) level: the level is a " + "blood test, and the count explains only part of the difference between people.") def combined_cell(h): @@ -393,6 +422,9 @@ def pgx_copy_number(S): MISSING = {"title": "", "state": "missing", "note": None, "data": {}} +SEX_CHECK_AGREE = "declared and measured sex agree" + + def freemix_text(q): """FREEMIX with its verdict, as both reports print it.""" if q.get("contamination") == "not_run" or not q.get("freemix"): @@ -886,6 +918,48 @@ def annotsv_lines(d): ) # The section id of each summary key. SECTION_OF = {k: sid for _, _, _, secs in CHAPTERS for sid, _, keys in secs for k in keys} +# The caveats of each section, by constant name, in the order the card shows them: the first is the +# one the live site shows under a card's title. caveat_notes() is unkeyed, so the live site +# (bin/live_results.py) reads this map; tests/test_live_results.py checks every section id has one. +SECTION_NOTES = { + "qc": ("COVERAGE_NOTE", "KARYOTYPE_NOTE"), + "callability": ("CALLABILITY_NOTE",), + "variants": ("VARIANTS_NOTE",), + "clinvar": ("CLINVAR_FREQ_NOTE", "CDNA_SCAN_NOTE", "DEEP_INTRONIC_NOTE"), + "clinical-filter": ("ACMG_NOTE",), + "cpsr": ("CPSR_NOTE",), + "slivar": ("COMPHET_NOTE",), + "exomiser": ("EXOMISER_NOTE",), + "structural-variants": ("SV_CALLERS_NOTE", "ANNOTSV_NOTE", "CNVPYTOR_NOTE"), + "mei": ("MEI_NOTE",), + "expansions": ("EH_NOTE", "NOT_GRADED_NOTE", "RFC1_NOTE"), + "vntyper": ("VNTYPER_NOTE",), + "mito": ("MITO_NOTE", "MITO_POLYC_NOTE", "MITO_DELETION_NOTE", "MITO_COPIES_NOTE"), + "lpa": ("LPA_NOTE",), + "chip": ("CHIP_NOTE",), + "mosaic": ("MOSAIC_NOTE",), + "mosaic-mca": ("MCA_NOTE",), + "carrier-list": ("CARRIER_NOTE",), + "couple": ("COUPLE_NOTE",), + "paralogs": ("PARASCOPY_NOTE", "PARALOG_NOTE", "PARASCOPY_LOCI_NOTE"), + "pharmcat": ("PHARMCAT_NOTE",), + "cpic": ("CPIC_NOTE",), + "cyp2d6": ("PGX_CALLERS_NOTE",), + "hla": ("HLA_NOTE", "HLA_DERIVED_NOTE", "KIR_NOTE"), + "locityper": ("LOCITYPER_NOTE",), + "ancestry-fine": ("ANCESTRY_FINE_NOTE",), + "mt-haplogroup": ("EXPLORE_NOTE",), + "y-chromosome": ("Y_TREE_NOTE", "YSTR_NOTE", "EXPLORE_NOTE"), + "archaic": ("ARCHAIC_NOTE",), + "ancient": ("ANCIENT_NOTE",), + "roh": ("ROH_NOTE", "ROH_UPD_NOTE", "ROH_F_NOTE", "ROH_BAND_NOTE"), + "traits-panel": ("TRAITS_NOTE",), + "prs": ("PRS_ANCESTRY_NOTE",), + "bloodtype": ("BLOODTYPE_NOTE",), + "telomere": ("TELOMERE_NOTE",), + "viral": ("VIRAL_NOTE",), + "consumer-export": ("CONSUMER_NOTE",), +} def section_keys(sid): @@ -974,7 +1048,8 @@ def head(key, title=None): if sqc["state"] in ("ok", "stale"): q = sqc["data"] w(f" Sex from the reads (somalier): {q.get('inferred_sex')}" - + (" ** DOES NOT MATCH the declared sex **" if q.get("sex_check") == "mismatch" else "")) + + (" ** DOES NOT MATCH the declared sex **" if q.get("sex_check") == "mismatch" else "") + + (f" ({SEX_CHECK_AGREE})" if q.get("sex_check") == "ok" else "")) if q.get("sex_check") == "not_checked": w(f" not checked: {q.get('sex_check_reason')}") w(f" Contamination (VerifyBamID2 FREEMIX): {freemix_text(q)}") @@ -982,6 +1057,7 @@ def head(key, title=None): w(f" ** The same person as: {q['same_person_as']} (a duplicate or a sample swap) **") for line in karyotype_lines(S): w(f" {line}") + w(f" {COVERAGE_NOTE}") w("") if head("callability", "Genes This Genome Could Not Read Reliably"): @@ -1004,6 +1080,7 @@ def head(key, title=None): w(f" Total variants: {d['total']}") w(f" PASS variants: {d['pass']}") w(f" SNPs: {d['snps']} Indels: {d['indels']}") + w(f" {VARIANTS_NOTE}") w("") if head("clinvar", "ClinVar Pathogenic Screen"): @@ -1066,6 +1143,7 @@ def head(key, title=None): w(f" Report: {S['pharmcat']['source']} (PharmCAT {d.get('version') or 'version unknown'})") if d.get("html_report"): w(" HTML report beside it (open in a browser for full details)") + w(f" {PHARMCAT_NOTE}") w("") if head("cpic", "CPIC Drug-Gene Recommendations"): @@ -1081,6 +1159,7 @@ def head(key, title=None): for warn in d["warnings"]: w(f" WARNING: {warn}") w(f" Drugs per gene: {os.path.join(os.path.dirname(S['cpic']['source']), s['sample'] + '_cpic_recommendations.txt')}") + w(f" {CPIC_NOTE}") w("") if head("pypgx", "pypgx Pharmacogenomics"): @@ -1121,6 +1200,7 @@ def head(key, title=None): w(f" Step 36: {verdict}") if s["cyp2d6"].get("probable"): w(f" {s['cyp2d6']['probable']['label'].capitalize()}: {probable_text(s['cyp2d6']['probable'])}") + w(f" {PGX_CALLERS_NOTE}") w("") if head("paralogs", "Paralog Genes (Paraphase, PacBio HiFi)"): @@ -1203,6 +1283,7 @@ def head(key, title=None): if head("manta", "Structural Variants (Manta)"): d = S["manta"]["data"] w(f" Total SVs: {d['total']} PASS: {d['pass']}") + w(f" {SV_CALLERS_NOTE}") w("") if head("delly", "Structural Variants (Delly)"): d = S["delly"]["data"] @@ -1347,11 +1428,13 @@ def head(key, title=None): if head("lpa", "Lp(a): LPA KIV-2 Repeat Count and Risk Variants"): for line in lpa_lines(S["lpa"]["data"]): w(f" {line}") + w(f" {LPA_NOTE}") w("") if head("vntyper", "MUC1 VNTR Frameshifts (ADTKD-MUC1, VNtyper)"): for line in vntyper_lines(S["vntyper"]["data"]): w(f" {line}") + w(f" {VNTYPER_NOTE}") w("") if head("chip", "Clonal Haematopoiesis (CHIP, step 49)"): @@ -1414,6 +1497,7 @@ def head(key, title=None): w(" Classification table has no CLASSIFICATION column.") for line in cpsr_lines(d, s["databases"]): w(f" {line}") + w(f" {CPSR_NOTE}") w("") if head("clinical", "Clinical Variant Filter"): @@ -2553,6 +2637,7 @@ def a(key, lines): qc.append(stat("Identity and contamination (step 33)", badge("Not run", "gray"))) kl = karyotype_lines(S) qc += [stat("Sex chromosomes", badge(x.split(": ", 1)[1], "red")) for x in kl[:-1]] + [small_note(x) for x in kl[-1:]] + qc.append(small_note(COVERAGE_NOTE)) qc_note = [f'
{E(sec["title"] + ": " + stale_note(sec))}
' for sec in (cov, sexc, sqc, rdc) if stale_note(sec)] a("coverage", ['
', "

Quality Control

"] + qc_note + qc + ["
"]) @@ -2576,7 +2661,8 @@ def a(key, lines): v = S["variants"]["data"] a("variants", card(S["variants"], "Variant Calling", [stat("Total variants", v.get("total")), stat("PASS variants", v.get("pass")), - stat("SNPs", v.get("snps")), stat("Indels", v.get("indels"))])) + stat("SNPs", v.get("snps")), stat("Indels", v.get("indels")), + small_note(VARIANTS_NOTE)])) c = S["clinvar"]["data"] n = c.get("count", 0) @@ -2614,12 +2700,14 @@ def a(key, lines): pg = S["pharmcat"]["data"] cp = S["cpic"]["data"] - pgx_body = [stat("PharmCAT report", badge("Complete", "green") if S["pharmcat"]["state"] in ("ok", "stale") + # Never green: a report that was written is not a finding (the frame's own Complete badge is neutral too). + pgx_body = [stat("PharmCAT report", badge("Complete", "state") if S["pharmcat"]["state"] in ("ok", "stale") else badge("Not run", "gray"))] if pg.get("version"): pgx_body.append(stat("PharmCAT version", E(pg["version"]))) if S["pypgx"]["state"] in ("ok", "stale") and "comparison" in S["pypgx"]["data"]: pgx_body.append(stat("PharmCAT vs pypgx conflicts", S["pypgx"]["data"]["comparison"]["conflicts"])) + pgx_body.append(small_note(PHARMCAT_NOTE)) a("pharmcat", card(S["pharmcat"], "Pharmacogenomics", pgx_body)) if S["pypgx"]["state"] != "missing": pd = S["pypgx"]["data"] @@ -2641,6 +2729,7 @@ def a(key, lines): cpic_body.append(stat("PharmCAT report", badge("could not be parsed", "red"))) cpic_body.append(stat("Tip", 'Recommendations per gene: ' f'cpic/{E(s["sample"])}_cpic_recommendations.txt')) + cpic_body.append(small_note(CPIC_NOTE)) a("cpic", card(S["cpic"], "CPIC Drug Recommendations", cpic_body)) calls = s["cyp2d6"]["calls"] @@ -2650,13 +2739,14 @@ def a(key, lines): if agree is False else badge("fewer than two usable calls", "gray"))) cons = s["cyp2d6"].get("consensus") if cons: - cy_body.append(stat("Step 36", badge(cons.get("result") or "?", "green" if cons.get("passed_to_pharmcat") + cy_body.append(stat("Step 36", badge(cons.get("result") or "?", "state" if cons.get("passed_to_pharmcat") else "yellow"))) cy_body.append(small_note(("Passed to PharmCAT: " if cons.get("passed_to_pharmcat") else "Not passed to PharmCAT: ") + (cons.get("reason") or "no reason given"))) prob = s["cyp2d6"].get("probable") if prob: cy_body.append(stat(prob["label"].capitalize(), E(probable_text(prob)))) + cy_body.append(small_note(PGX_CALLERS_NOTE)) a("cyrius", ["
", "

CYP2D6 Across Callers

"] + [f'
{E(S[k]["title"] + ": " + stale_note(S[k]))}
' for k in ("cpic", "pypgx", "cyrius") if S[k]["state"] in ("stale", "failed")] @@ -2696,6 +2786,7 @@ def a(key, lines): if cnvpytor_filtered_text(cn): sv_body += [stat("CNVpytor significant after filters", E(cnvpytor_filtered_text(cn))), small_note(CNVPYTOR_NOTE)] sv_body.append(stat("Consensus SVs (2+ callers)", sv.get("consensus", "N/A"))) + sv_body.append(small_note(SV_CALLERS_NOTE)) notes = [f'
{E(S[k]["title"] + ": " + stale_note(S[k]))}
' for k in ("manta", "delly", "cnvpytor", "sv_consensus") if stale_note(S[k])] a("manta", ['
', "

Structural Variants

"] + notes + sv_body + ["
"]) @@ -2724,10 +2815,11 @@ def a(key, lines): a("annotsv", card(S["annotsv"], "Structural Variants, Class 4 and 5 (AnnotSV)", body, full=True)) cs = S["cpsr"]["data"] - cpsr_body = [stat("CPSR report", badge("Complete", "green") if cs.get("html_report") else badge("No HTML", "gray"))] + cpsr_body = [stat("CPSR report", badge("Complete", "state") if cs.get("html_report") else badge("No HTML", "gray"))] for k, val in (cs.get("classification") or {}).items(): cpsr_body.append(stat(k, val)) cpsr_body += [small_note(line.strip()) for line in cpsr_lines(cs, s["databases"])] + cpsr_body.append(small_note(CPSR_NOTE)) a("cpsr", card(S["cpsr"], "Cancer Predisposition", cpsr_body)) eh = S["expansions"]["data"] @@ -2834,13 +2926,13 @@ def a(key, lines): lp = S["lpa"]["data"] a("lpa", card(S["lpa"], "Lp(a): LPA KIV-2 Repeat Count", ([stat("Genetic sign of high Lp(a)", badge("Yes", "yellow"))] if lp.get("flag") == "high" else []) - + [f"

{E(line)}

" for line in lpa_lines(lp)])) + + [f"

{E(line)}

" for line in lpa_lines(lp)] + [small_note(LPA_NOTE)])) if S.get("vntyper", MISSING)["state"] != "missing": vn = S["vntyper"]["data"] a("vntyper", card(S["vntyper"], "MUC1 VNTR Frameshifts (VNtyper)", ([stat("Result", badge(vn.get("status", "."), {"positive": "red", "possible": "yellow", "negative": "green"}.get(vn.get("status"), "gray")))]) - + [f"

{E(line)}

" for line in vntyper_lines(vn)])) + + [f"

{E(line)}

" for line in vntyper_lines(vn)] + [small_note(VNTYPER_NOTE)])) if S.get("viral", MISSING)["state"] != "missing": a("viral", card(S["viral"], "Viral DNA in the Unmapped Reads", viral_body(S["viral"]["data"]), full=True)) if S.get("chip", MISSING)["state"] != "missing": From 04079e6e491b012f6d3ed75d9a78cca0790b320c Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sat, 7 Nov 2026 20:27:37 +0100 Subject: [PATCH 06/14] feat(live): serve each person's results, the runs and each family as JSON bin/live_results.py turns a summary.json into the report's seven chapters, one card per section with its facts, rows, caveats and an attention flag taken only from flags that already exist. The results plugin serves it as /runs//results.json from the summary the server already reads for the headlines. /runs.json gives the home page each run's progress and each family's members; the family plugin adds /runs//family.json with the pedigree layout, the pair rows with their measured numbers and each couple's gene rows. Bead: pgp-5bja --- bin/live_results.py | 643 ++++++++++++++++++++++++++++++++++ bin/owngenome_live.py | 162 +++++++-- bin/owngenome_live_family.py | 108 +++++- bin/owngenome_live_results.py | 51 +++ 4 files changed, 934 insertions(+), 30 deletions(-) create mode 100644 bin/live_results.py create mode 100644 bin/owngenome_live_results.py diff --git a/bin/live_results.py b/bin/live_results.py new file mode 100644 index 00000000..52d12d1c --- /dev/null +++ b/bin/live_results.py @@ -0,0 +1,643 @@ +#!/usr/bin/env python3 +"""live_results.py: one person's results as the live site draws them (results.json). + + build(sample, summary, files=()) -> dict + +Pure functions, standard library only. The live site's results plugin +(bin/owngenome_live_results.py) and the public demo (tests/demo/build_live_demo.py) +call build() on a summary.json (bin/collect_summary.py). The answer follows the +report's own chapters (bin/render_report.py CHAPTERS): seven questions, one card per +section id, the same anchors as report.html. Each card carries: + + facts [[label, value]] the numbers the report prints + table {"columns": [...], "rows": [[...]]} the rows the report lists, or null + lines the report's own sentences (render_report's *_lines helpers), plain text + caveat the section's caveats word for word (render_report.SECTION_NOTES) + attention true only for a flag the report or the live page already raises: a + headline tone (bin/owngenome_live.py HEADLINES), a failed, unreadable or + stale result, a repeat outside Stranger's "normal" or in an intermediate + zone, an AnnotSV class 4 or 5 row, a CHIP or MoChA call, a two-hit gene + or a couple's shared or X-linked gene. Nothing says a section is fine: + a card without attention is "for information". + +Nothing here reads a file or computes anything about genetics; the adapters pick +fields the summary already holds. A section id with no adapter falls back to its +report headline and a link to the report. +""" +import re + +import render_report as rr + +SCHEMA = 1 +# Words that would tell a reader a result is fine. tests/test_live_results.py scans every string of +# results.json and family.json, and the page assets, for them. "Fine-grained" is a method's name. +FORBIDDEN = re.compile(r"\b(ok|(? limit: + out["more"] = len(rows) - limit + return out + + +def _headline_tone(key, sec, summary): + """(text, tone) from the live site's headline table, or (None, 'info').""" + try: + import owngenome_live + h = owngenome_live.headline(key, sec, summary) + except Exception: # the page must not go down on one reader + h = None + return (plain(h[0]), h[1]) if h else (None, "info") + + +# --- the adapters: one per section id -------------------------------------------------------- +# Each takes (S, summary) where S is summary["sections"], and returns a dict with any of +# facts, table, lines, attention, links (relative file patterns), extra. + +def a_qc(S, s): + cov, sexc, q, rdc = (S.get(k) or {} for k in ("coverage", "sex_check", "sample_qc", "read_count")) + facts = [] + md = (cov.get("data") or {}).get("mean_depth") + if isinstance(md, (int, float)): + facts.append(["Mean depth", f"{md:.1f}x"]) + if rdc.get("state") in ("ok", "stale"): + d = rdc["data"] + facts.append(["Reads kept by alignment", rr.read_count_text(d) + + ("" if d.get("read_count_check") != "mismatch" else " (reads lost: see the report)")]) + inferred = (sexc.get("data") or {}).get("inferred_sex") + declared = (s.get("run") or {}).get("declared_sex") + if inferred: + facts.append(["Inferred sex (indexcov)", inferred]) + facts.append(["Declared sex", declared or "not declared"]) + attention = False + if q.get("state") in ("ok", "stale"): + d = q["data"] + check = d.get("sex_check") + word = {"ok": "Declared and measured sex agree", "mismatch": "Declared and measured sex differ", + "not_checked": f"Not checked: {d.get('sex_check_reason') or 'no reason given'}"}.get(check, check or ".") + facts.append(["Sex from the reads (somalier)", f"{d.get('inferred_sex') or '.'}. {word}"]) + attention = check == "mismatch" + cont = d.get("contamination") + if cont == "ok": + facts.append(["Contamination (VerifyBamID2)", f"Below the contamination flag (FREEMIX {d.get('freemix')}, " + f"flag above {d.get('freemix_warn_above')})"]) + else: + facts.append(["Contamination (VerifyBamID2)", rr.freemix_text(d)]) + attention = attention or cont == "warn" + if d.get("same_person_as"): + facts.append(["The same person as", d["same_person_as"]]) + attention = True + lines = [x for x in rr.karyotype_lines(S)] + if lines[:-1]: + attention = True + return {"facts": facts, "lines": lines, "attention": attention, + "links": [r"(^|/)multiqc[^/]*\.html$", r"(^|/)fastp[^/]*\.html$", r"_fastp\.html$"]} + + +def a_callability(S, s): + d = S["callability"]["data"] + rows = [[g["gene"], g.get("status"), "." if g.get("share") is None else f"{100 * g['share']:.0f}%", + ", ".join(g.get("reasons") or [])] for g in d.get("not_read") or []] + return {"facts": [["Hard-to-read genes read", f"{d.get('read')} of {d.get('catalog_genes')}"], + ["Not read, no other step calls them", len(d.get("not_read") or [])], + ["Not read here, called by a step of their own", len(d.get("not_read_owned") or [])]], + "table": table(["Gene", "Status", "Bases read", "Why"], rows)} + + +def a_variants(S, s): + d = S["variants"]["data"] + return {"facts": [["Total variants", d.get("total")], ["PASS variants", d.get("pass")], ["SNPs", d.get("snps")], + ["Indels", d.get("indels")]], "links": [r"(^|/)vep/.*summary\.html$"]} + + +def a_clinvar(S, s): + d = S["clinvar"]["data"] + by = d.get("by_stars") or {} + facts = [["P/LP matches", d.get("count", 0)], + ["By review stars (4/3/2/1/0)", " / ".join(str(by.get(k, 0)) for k in ("4", "3", "2", "1", "0"))], + ["ClinVar file date", (s.get("databases") or {}).get("clinvar_release") or "unknown"]] + if "low_confidence" in d: + f = d.get("call_floor") or {} + facts.append([f"Low-confidence calls (GQ below {f.get('GQ')} or DP below {f.get('DP')}), not counted", + d["low_confidence"]]) + rows = [] + for h in (d.get("hits") or [])[:50]: + rows.append([h.get("gene"), f"{h.get('chrom')}:{h.get('pos')} {h.get('ref')}>{h.get('alt')}", h.get("genotype"), + h.get("significance"), "★" * int(h.get("stars") or 0) or "0", + rr.af_text(h) or "frequency unknown", rr.call_text(h) or "." + + (" · low confidence" if h.get("low_confidence") else "")]) + lines = [x for x in (rr.cdna_scan_lines(d) or [])] + freq = rr.clinvar_freq_line(d) + if freq: + lines.insert(0, freq) + viewer = [f"{h.get('chrom')}:{h.get('pos')}" for h in (d.get("hits") or [])[:50]] + return {"facts": facts, "lines": lines, + "table": table(["Gene", "Variant", "Genotype", "Significance", "Stars", "Frequency", "Call"], rows), + "viewer": viewer} + + +def a_clinical_filter(S, s): + d = S["clinical"]["data"] + sf = d.get("acmg_sf") or {} + bi = d.get("by_impact") or {} + rows = [[r[0], r[1], r[2], r[3]] for r in (rr.sf_rows(sf) if sf else [])] if sf else [] + return {"facts": [["Variants after the clinical filter", d.get("variants")], + ["By impact (high / moderate / low)", f"{bi.get('HIGH', 0)} / {bi.get('MODERATE', 0)} / {bi.get('LOW', 0)}"], + ["Genes", d.get("genes")], + [f"{sf.get('version') or 'ACMG SF'}: ClinVar P/LP in its genes", len(sf.get("clinvar_hits") or [])], + ["High-impact rare variants in its genes", len(sf.get("high_impact") or []) + if sf.get("high_impact_checked") else "not checked"]], + "table": table(["Gene", "Variant", "Kind", "Rule"], rows), + "attention": bool(sf.get("clinvar_hits") or sf.get("high_impact") or sf.get("structural"))} + + +def a_cpsr(S, s): + d = S["cpsr"]["data"] + facts = [["CPSR report", "Ran" if d.get("html_report") else "No HTML"]] + facts += [[k.replace("_", " "), v] for k, v in (d.get("classification") or {}).items()] + rows = [[x.get("gene"), x.get("variant"), x.get("final"), x.get("clinvar") or ".", + "low confidence" if x.get("low_confidence") else "."] for x in d.get("plp") or []] + return {"facts": facts, "lines": rr.cpsr_lines(d, s.get("databases") or {}), + "table": table(["Gene", "Variant", "Final class", "ClinVar", "Call"], rows), "links": [r"^cpsr/"]} + + +def a_slivar(S, s): + d = S["slivar"]["data"] + return {"facts": [["Prioritized variants", d.get("prioritized")], + ["Compound-het candidate variants", d.get("compound_het_variants")], + ["Compound-het candidate genes", d.get("compound_het_genes")]], + "lines": [rr.comphet_phase_text(d)] if d.get("phase") else []} + + +def a_exomiser(S, s): + d = S["exomiser"]["data"] + rows = [] + for g in d.get("genes") or []: + v = (g.get("variants") or [{}])[0] + rows.append([g.get("rank"), g.get("gene"), g.get("moi"), g.get("exomiser_score"), v.get("hgvs") or v.get("variant"), + v.get("genotype"), v.get("acmg"), v.get("disease")]) + return {"facts": [["Symptoms searched", ", ".join(f"{t.get('label')} ({t.get('id')})" for t in d.get("terms") or []) + or "none"]], + "table": table(["Rank", "Gene", "Inheritance", "Score", "Variant", "Genotype", "ACMG", "Disease"], rows)} + + +def a_structural(S, s): + m, dl, cn, sv = ((S.get(k) or {}).get("data") or {} for k in ("manta", "delly", "cnvpytor", "sv_consensus")) + av = (S.get("annotsv") or {}).get("data") or {} + facts = [["Manta SVs (total / PASS)", f"{m.get('total', '.')} / {m.get('pass', '.')}"], + ["Delly SVs (total / PASS)", f"{dl.get('total', '.')} / {dl.get('pass', '.')}"], + ["CNVpytor CNVs (significant)", f"{cn.get('total', '.')} ({cn.get('significant', '.')})"], + ["Consensus SVs (2+ callers)", sv.get("consensus", ".")], + ["AnnotSV class 4 and 5", av.get("count", ".")]] + rows = [[x.get("type"), x.get("sv"), x.get("gt"), x.get("length"), x.get("acmg_class"), + rr.annotsv_genes_text(x) if x.get("genes") is not None else ".", rr.annotsv_support_text(x)] + for x in av.get("rows") or []] + return {"facts": facts, "table": table(["Type", "Where", "Genotype", "Length", "Class", "Genes", "Support"], rows), + "attention": bool(av.get("count"))} + + +def a_mei(S, s): + d = S["mei"]["data"] + rows = [[x.get("gene"), x.get("element"), x.get("condition"), x.get("inheritance"), x.get("status"), + x.get("mean_depth")] for x in d.get("sites") or []] + return {"facts": [["Insertions called", d.get("calls")], + ["By element", ", ".join(f"{k} {v}" for k, v in (d.get("by_element") or {}).items()) or "none"]], + "table": table(["Gene", "Element", "Condition", "Inheritance", "Known site", "Depth"], rows), + "attention": any(x.get("status") not in (None, "", "not seen") for x in d.get("sites") or [])} + + +def a_expansions(S, s): + d = S["expansions"]["data"] + rows, flagged = [], False + for l in d.get("loci") or []: + status = l.get("status") or "" + out = any(x and x != "normal" for x in status.split(",")) + flagged = flagged or out or bool(l.get("zone")) + rows.append([l.get("locus"), l.get("repeat_count"), f'"{status}"' if status else "not graded", + l.get("normal_max"), l.get("pathologic_min"), l.get("ci"), l.get("sizing"), + f'{l.get("zone_label")} ({l.get("zone")})' if l.get("zone") else "."]) + facts = [["Loci tested", d.get("records")], + ["Outside Stranger's \"normal\"", len([r for r in rows if r[2] not in ('"normal"', "not graded")]) + if d.get("stranger") else "Stranger not run"], + ["In an intermediate zone", ", ".join(d.get("intermediate") or []) or "none"], + ["Not sized from short reads", ", ".join(d.get("unsized") or []) or "none"]] + return {"facts": facts, "attention": flagged, + "table": table(["Locus", "Repeats", "Stranger status", "Normal up to", "Pathologic from", "95% interval", + "Sizing", "Intermediate zone"], rows), + "widget": {"kind": "str", "loci": [{k: l.get(k) for k in ("locus", "repeat_count", "status", "normal_max", + "pathologic_min", "ci")} + for l in d.get("loci") or []]}} + + +def a_vntyper(S, s): + d = S["vntyper"]["data"] + return {"facts": [["Result", d.get("status")], ["Confidence", d.get("confidence")], + ["Mean depth over the repeat", d.get("vntr_mean_depth")]], + "lines": rr.vntyper_lines(d), "attention": d.get("status") in ("positive", "possible")} + + +def a_mito(S, s): + d = S["mito"]["data"] + a = d.get("annotation") or {} + facts = [["PASS variants on chrM", d.get("pass")], ["Heteroplasmic sites", d.get("heteroplasmic")]] + if "clinvar_plp" in a: + facts.append(["ClinVar pathogenic or likely pathogenic", a.get("clinvar_plp")]) + rows = [[f"m.{x.get('pos')}{x.get('ref')}>{x.get('alt')}", f"{100 * x['af']:.1f}%" if isinstance(x.get("af"), (int, float)) + else "."] for x in d.get("heteroplasmic_sites") or []] + return {"facts": facts, "lines": rr.mito_lines(d), "table": table(["Site", "Allele fraction"], rows), + "attention": (a.get("clinvar_plp") or 0) > 0 or bool(d.get("confirmed"))} + + +def a_lpa(S, s): + d = S["lpa"]["data"] + facts = [["KIV-2 repeats", f"{d.get('kiv2_copies')} (95% interval {d.get('ci95_low')} to {d.get('ci95_high')})" + if d.get("status") == "estimated" else d.get("status")], ["Class", d.get("class")]] + return {"facts": facts, "lines": rr.lpa_lines(d), "attention": d.get("flag") == "high"} + + +def a_chip(S, s): + d = S["chip"]["data"] + rows = [[c.get("gene"), c.get("protein_change"), c.get("vaf"), c.get("alt_reads"), c.get("depth"), c.get("status")] + for c in d.get("calls") or []] + return {"facts": [["CHIP calls", len(d.get("calls") or [])], + ["Typical detection limit at this depth (VAF)", d.get("median_limit")]], + "table": table(["Gene", "Change", "VAF", "Alt reads", "Depth", "Status"], rows), + "lines": rr.chip_lines(d, s.get("sample")), "attention": bool(d.get("calls"))} + + +def a_mosaic(S, s): + q = S["mosaic_sex_loss"]["data"] + call = rr.MOSAIC_CALL.get(q.get("call"), (q.get("call"), ""))[0] + return {"facts": [[q.get("chrom") or "chrY or chrX", call]], "lines": [q.get("reason") or ""] + rr.mosaic_lines(q), + "attention": q.get("call") in ("detected", "gain")} + + +def a_mosaic_mca(S, s): + d = S["mosaic"]["data"] + rows = [[c.get("chrom"), c.get("start"), c.get("end"), c.get("length_mb"), c.get("type"), c.get("cell_fraction")] + for c in d.get("calls") or []] + return {"facts": [["Status", d.get("status")], ["Calls reported", d.get("calls_reported", len(rows))], + ["Smallest cell fraction it can see", d.get("floor")]], + "table": table(["Chromosome", "Start", "End", "Length (Mb)", "Type", "Cell fraction"], rows), + "lines": rr.mosaic_mca_lines(d), "attention": bool(d.get("calls"))} + + +def a_carrier(S, s): + d = S["carrier"]["data"] + rows = [[c.get("gene"), c.get("variant"), c.get("genotype"), c.get("significance"), c.get("stars"), + c.get("inheritance"), c.get("condition")] for c in d.get("carriers") or []] + two = (d.get("two_hits") or {}).get("genes") or [] + return {"facts": [["Genes on the carrier list", d.get("genes_on_list")], ["Carrier findings", len(rows)], + ["Genes with two hits", ", ".join(g if isinstance(g, str) else g.get("gene", ".") for g in two) + or "none"]], + "table": table(["Gene", "Variant", "Genotype", "Significance", "Stars", "Inheritance", "Condition"], rows), + "lines": rr.two_hit_lines(d.get("two_hits")), "attention": bool(two)} + + +def a_couple(S, s): + d = S["couple"]["data"] + rows = [[lab, x.get("gene"), x.get("condition"), x.get("risk"), x.get("own"), x.get("partner_findings")] + for lab, key in (("Both carry", "shared"), ("X-linked", "x_linked")) for x in d.get(key) or []] + return {"facts": [["Partner", d.get("partner")], ["Genes where both carry a change", len(d.get("shared") or [])], + ["X-linked findings", len(d.get("x_linked") or [])], + ["Genes where only one partner carries a change", + f"{d.get('one_partner', 0)}, unless the other partner carries something the screen does not see"]], + "table": table(["Result", "Gene", "Condition", "Chance for a child", "This person", "Partner"], rows)} + + +def a_paralogs(S, s): + lines = [] + if (S.get("parascopy") or {}).get("state") in ("ok", "stale"): + d = S["parascopy"]["data"] + lines += rr.parascopy_lines(d) + rr.parascopy_locus_lines(d) + rr.cn_interval_lines(d.get("cn_intervals")) + rows = [] + if (S.get("paralogs") or {}).get("state") in ("ok", "stale"): + rows = [[g.get("gene"), g.get("copy_number"), g.get("status"), g.get("detail")] + for g in S["paralogs"]["data"].get("genes") or []] + return {"lines": lines, "table": table(["Gene", "Copies", "Status", "Detail"], rows)} + + +def a_pharmcat(S, s): + d = S["pharmcat"]["data"] + return {"facts": [["PharmCAT report", "Ran"], ["PharmCAT version", d.get("version") or "unknown"], + ["Its own HTML report", "beside it" if d.get("html_report") else "not written"]], + "links": [r"(^|/)pharmcat/[^/]+\.html$"]} + + +def a_cpic(S, s): + d = S["cpic"]["data"] + rows = [[g.get("gene"), g.get("diplotype"), f'"{g.get("phenotype")}"' if g.get("phenotype") else ".", g.get("status")] + for g in d.get("genes") or []] + return {"facts": [["Genes with a changed-function phenotype", d.get("non_normal", 0)], + ["Genes with more than one possible result", d.get("ambiguous", 0)], + ["Genes not called", d.get("not_called", 0)]], + "table": table(["Gene", "Diplotype", "Phenotype (CPIC's words)", "Status"], rows), + "lines": [f"WARNING: {w}" for w in d.get("warnings") or []]} + + +def a_cyp2d6(S, s): + c = s.get("cyp2d6") or {} + rows = [[rr.CYP2D6_LABELS.get(k, k), v or "not run"] for k, v in (c.get("calls") or {}).items()] + agree = c.get("agree") + facts = [["pypgx and Cyrius agree", "yes" if agree else "no" if agree is False else "fewer than two usable calls"]] + v = rr.cyp2d6_verdict(c) + if v: + facts.append(["Step 36", plain(v)]) + lines = rr.pypgx_lines(S["pypgx"]["data"]) if (S.get("pypgx") or {}).get("state") in ("ok", "stale") else [] + return {"facts": facts, "table": table(["Caller", "CYP2D6"], rows), "lines": lines, "attention": agree is False} + + +def a_hla(S, s): + d = S["hla"]["data"] + rows = [[l.get("gene"), rr.hla_text(l)] for l in d.get("loci") or []] + return {"facts": [["HLA database", (s.get("databases") or {}).get("hla_database") or "unknown"]], + "table": table(["Gene", "Alleles (T1K quality)"], rows), "lines": rr.hla_derived_lines(d.get("derived"))} + + +def a_locityper(S, s): + d = S["locityper"]["data"] + rows = [[l.get("locus"), l.get("genotype"), l.get("quality"), l.get("filter"), l.get("primary")] + for l in d.get("loci") or []] + return {"facts": [["Loci called", f"{d.get('called', 0)} of {len(d.get('loci') or [])}"], + ["Passing the authors' filter", d.get("passed", 0)]], + "table": table(["Locus", "Panel haplotypes", "Quality", "Filter", "Primary caller"], rows)} + + +def a_ancestry_fine(S, s): + d = S["ancestry_fine"]["data"] + rows = [[lab, r.get("group"), rr.share(r.get("fraction")), f"{rr.share(r.get('low'))} to {rr.share(r.get('high'))}"] + for level, lab in rr.ANCESTRY_FINE_LEVELS for r in rr.ancestry_fine_rows(d, level)] + return {"table": table(["Level", "Group", "Share", "Bootstrap range"], rows), + "facts": [["Chromosome painting", rr.ancestry_fine_painted(d)]]} + + +def a_mt_haplogroup(S, s): + d = S["haplogroup"]["data"] + return {"facts": [["Haplogroup", d.get("haplogroup")], ["Two mtDNA haplogroups in the reads", + rr.contamination_text(d)]], + "explore": [{"name": x.get("name"), "url": x.get("url")} for x in d.get("explore") or []]} + + +def a_y(S, s): + facts, explore, rows = [], [], [] + y = (S.get("y_haplogroup") or {}).get("data") or {} + t = (S.get("y_tree") or {}).get("data") or {} + if y: + facts += [["Haplogroup (Yleaf)", y.get("haplogroup")], ["QC score", y.get("qc_score")]] + explore = [{"name": x.get("name"), "url": x.get("url")} for x in y.get("explore") or []] + if t: + facts.append([f"Placement on {t.get('tree')}", t.get("haplogroup")]) + rows = [[p.get("Level"), p.get("Branch"), p.get("Derived"), p.get("Ancestral"), p.get("No_call")] + for p in t.get("path") or []] + st = (S.get("y_str") or {}).get("data") or {} + if st.get("markers"): + facts.append(["Y-STR markers read", sum(1 for m in st["markers"] if m.get("count"))]) + return {"facts": facts, "explore": explore, + "table": table(["Level", "Branch", "Derived", "Ancestral", "No call"], rows)} + + +def a_archaic(S, s): + d = S["archaic"]["data"] + return {"facts": [["Neanderthal", f"{d.get('neanderthal_percent')}%"], ["Denisovan", f"{d.get('denisovan_percent')}%"]], + "lines": rr.archaic_lines(d)} + + +def a_ancient(S, s): + d = S["ancient"]["data"] + rows = [[r.get("rank"), r.get("group"), r.get("dates"), r.get("country"), r.get("distance")] + for r in (d.get("nearest") or [])[:10]] + out = {"table": table(["Rank", "Group", "Dates", "Country", "Distance"], rows), + "facts": [["Sites used", f"{d.get('sites_used')} of {d.get('panel_sites')}"]]} + if isinstance(d.get("plot_svg"), str) and d["plot_svg"].lstrip().startswith("<"): + import base64 + out["image"] = {"alt": "PCA of ancient genomes with this genome projected", + "src": "data:image/svg+xml;base64," + base64.b64encode(d["plot_svg"].encode()).decode()} + return out + + +def a_roh(S, s): + d = S["roh"]["data"] + return {"facts": [["Segments", d.get("segments")], ["Total", f"{d.get('total_mb')} Mb"], + ["Largest", f"{d.get('largest_mb')} Mb"], ["F_ROH", d.get("f_roh")], + ["Nearest band", d.get("relationship")]], + "lines": rr.roh_lines(d), "attention": bool(d.get("upd_flag"))} + + +def a_traits(S, s): + d = S["traits"]["data"] + rows = [[r.get("gene"), r.get("trait"), r.get("genotype"), r.get("reading")] + for r in d.get("rows") or [] if r.get("status") == "called" and (r.get("gene") or "").upper() != "APOE"] + out = {"facts": [["Rows read", d.get("called")], ["No genotype at the position", d.get("not_covered")]], + "table": table(["Gene", "Trait", "Genotype", "Reading"], rows)} + apoe = d.get("apoe") + if apoe: + # Behind its warning, as the report shows it: the page puts it in a closed
. + text = apoe if isinstance(apoe, str) else "; ".join(f"{k}: {v}" for k, v in apoe.items()) \ + if isinstance(apoe, dict) else cell(apoe) + out["hidden"] = {"summary": rr.APOE_WARNING, "text": plain(text)} + return out + + +def a_prs(S, s): + d = S["prs"]["data"] + if not d.get("adjusted"): + # The report shows a percentile only when it is ancestry-adjusted. + return {"facts": [["Scores", len(d.get("scores") or [])]], + "lines": ["Raw scores only: without the ancestry panel no percentile is shown (docs/25-prs.md)."]} + rows = [[r.get("condition"), r.get("pgs_id"), rr.prs_percentile(r) or ".", f"{r.get('matched')}/{r.get('total')}", + rr.prs_effect(r) or "."] for r in d.get("scores") or []] + return {"table": table(["Condition", "Score", "Percentile", "Variants matched", "Effect"], rows), + "lines": [rr.prs_note(d)]} + + +def a_bloodtype(S, s): + d = S["bloodtype"]["data"] + rows = [[x.get("system"), x.get("phenotype"), x.get("genotype"), x.get("confidence")] for x in d.get("systems") or []] + return {"facts": [["ABO", d.get("abo")], ["RhD", d.get("rhd")]], + "table": table(["System", "Predicted phenotype", "Genotype", "Confidence"], rows)} + + +def a_telomere(S, s): + d = S["telomere"]["data"] + return {"facts": [["Telomere content", f"{d.get('tel_content')} ({rr.TELOMERE_UNIT})"]], + "lines": rr.telomere_lines(d), "attention": bool(d.get("low_reads"))} + + +def a_viral(S, s): + d = S["viral"]["data"] + rows = [[x.get("virus"), x.get("reads"), x.get("breadth"), x.get("copies_per_cell"), x.get("call")] + for x in d.get("found") or []] + return {"facts": [["HHV-6", d.get("hhv6")], ["Genomes searched", d.get("genomes")]], + "table": table(["Virus", "Reads", "Breadth", "Copies per cell", "Call"], rows)} + + +def a_consumer(S, s): + d = S["consumer_export"]["data"] + return {"facts": [["Sites", d.get("sites")], ["Called", d.get("called")], ["No call", d.get("no_call")]], + "lines": rr.consumer_export_lines(d)} + + +ADAPTERS = { + "qc": a_qc, "callability": a_callability, "variants": a_variants, "clinvar": a_clinvar, + "clinical-filter": a_clinical_filter, "cpsr": a_cpsr, "slivar": a_slivar, "exomiser": a_exomiser, + "structural-variants": a_structural, "mei": a_mei, "expansions": a_expansions, "vntyper": a_vntyper, + "mito": a_mito, "lpa": a_lpa, "chip": a_chip, "mosaic": a_mosaic, "mosaic-mca": a_mosaic_mca, + "carrier-list": a_carrier, "couple": a_couple, "paralogs": a_paralogs, "pharmcat": a_pharmcat, "cpic": a_cpic, + "cyp2d6": a_cyp2d6, "hla": a_hla, "locityper": a_locityper, "ancestry-fine": a_ancestry_fine, + "mt-haplogroup": a_mt_haplogroup, "y-chromosome": a_y, "archaic": a_archaic, "ancient": a_ancient, "roh": a_roh, + "traits-panel": a_traits, "prs": a_prs, "bloodtype": a_bloodtype, "telomere": a_telomere, "viral": a_viral, + "consumer-export": a_consumer, +} +# Report charts (bin/report_charts.py) drawn inside a card: chart id -> section id. +CHART_SECTION = {"clinvar": "clinvar", "genome": "clinvar", "mito": "mito", "ancestry": "ancestry-fine", "prs": "prs"} + + +def caveats(sid): + return [getattr(rr, n) for n in rr.SECTION_NOTES.get(sid, ()) if isinstance(getattr(rr, n, None), str)] + + +def section_state(secs): + st = {x.get("state") or "missing" for x in secs} + if st & {"failed", "unreadable"}: + return "failed" if not st & {"ok", "stale"} else "part" + if "stale" in st: + return "stale" + if "ok" in st: + return "incomplete" if "missing" in st else "ran" + return "missing" + + +def _links(patterns, files): + out = [] + for p in patterns or []: + rx = re.compile(p) + out += sorted(f for f in files if rx.search(f) and f not in out) + return out[:4] + + +def card(sid, title, keys, summary, files): + S = summary.get("sections") or {} + secs = [S.get(k) or {"state": "missing"} for k in keys] + state = section_state(secs) + steps = [] + for k, sec in zip(keys, secs): + st = sec.get("step") or rr.STEP_OF.get(k) + if st and st not in steps: + steps.append(st) + cav = caveats(sid) + out = {"id": sid, "title": title, "keys": list(keys), "steps": steps, "state": state, "anchor": sid, + "attention": False, "headline": None, "facts": [], "table": None, "lines": [], "caveat": cav, + "empty_text": EMPTY.get(sid), "links": [], "notes": []} + for k, sec in zip(keys, secs): + if sec.get("state") in ("failed", "unreadable", "stale"): + out["notes"].append(f"{sec.get('title') or k}: {plain(rr.stale_note(sec))}") + out["attention"] = True + if state == "missing": + return out + tones = [] + for k, sec in zip(keys, secs): + if sec.get("state") in ("ok", "stale"): + text, tone = _headline_tone(k, sec, summary) + if text and not out["headline"] and not text.endswith("finished, see the report"): + out["headline"] = text + tones.append(tone) + if "attention" in tones: + out["attention"] = True + fn = ADAPTERS.get(sid) + if fn and state not in ("failed",): + try: + got = fn(S, summary) or {} + except (KeyError, TypeError, ValueError, AttributeError, IndexError, ZeroDivisionError) as e: + got = {"lines": [f"This card could not be drawn ({type(e).__name__}); the report shows the section."]} + out["facts"] = [[plain(a), plain(cell(b))] for a, b in got.get("facts") or []] + out["table"] = got.get("table") + notes = {plain(c) for c in cav} | {plain(n) for n in rr.caveat_notes()} + out["lines"] = [plain(x) for x in got.get("lines") or [] if plain(x) and plain(x) not in notes + and plain(x) != out["headline"]] + out["attention"] = out["attention"] or bool(got.get("attention")) + out["links"] = _links(got.get("links"), files) + for k in ("widget", "explore", "image", "hidden", "viewer"): + if got.get(k): + out[k] = got[k] + return out + + +def build(sample, summary, files=()): + """results.json of one person from a summary.json; files: the run's paths the report links (for links).""" + summary = summary or {} + files = set(files or ()) + S = summary.get("sections") or {} + chapters = [] + for cid, ctitle, question, secs in rr.CHAPTERS: + cards = [card(sid, title, keys, summary, files) for sid, title, keys in secs] + chapters.append({"key": cid, "title": ctitle, "question": question, "sections": cards, + "attention": sum(1 for c in cards if c["attention"])}) + sexc = (S.get("sex_check") or {}).get("data") or {} + person = {"sample": sample, "declared_sex": (summary.get("run") or {}).get("declared_sex"), + "inferred_sex": sexc.get("inferred_sex"), "karyotype": sexc.get("karyotype")} + return {"schema_version": SCHEMA, "sample": sample, "generated_utc": summary.get("generated_utc"), + "person": person, "chapters": chapters, + "attention": sum(c["attention"] for c in chapters), + "not_assessed": [plain(x) for x in summary.get("not_assessed") or []], + "not_run": [plain(x) for x in summary.get("not_run") or []], + "new_since": summary.get("new_since") or None, + "charts": CHART_SECTION} + + +def attention_count(summary): + """The number of cards worth a look, for the home page (cheap: no file read).""" + return build("", summary)["attention"] + + +def strings(obj): + """Every string in a JSON value, with its key path: the wording gate's input.""" + if isinstance(obj, dict): + for k, v in obj.items(): + for p, s in strings(v): + yield (k,) + p, s + elif isinstance(obj, list): + for v in obj: + yield from strings(v) + elif isinstance(obj, str): + yield (), obj + + +def wording_problems(obj, allow=("src", "url", "caveat", "summary")): + """[(path, text)] of the strings that would read as an all-clear. Allowed: data URLs and links, the + caveats (render_report's constants word for word, checked against them separately), and a quoted + tool term (CPIC's "Normal Metabolizer", Stranger's "normal").""" + out = [] + for path, s in strings(obj): + if path and path[-1] in allow: + continue + t = re.sub(r'"[^"]*"', "", s) + if FORBIDDEN.search(t): + out.append((".".join(map(str, path)), s[:160])) + return out diff --git a/bin/owngenome_live.py b/bin/owngenome_live.py index b8576a73..5d5a0284 100755 --- a/bin/owngenome_live.py +++ b/bin/owngenome_live.py @@ -19,9 +19,11 @@ browser (--bind 127.0.0.1, --port 8787) serves the pages, after a login when a password hash is set: GET/POST /login, POST /logout - GET / the runs under --root + GET / the people under --root, by family (assets/live/home.html) + GET /runs.json each run's progress and each family's members GET /assets/, /assets/plugins/ - GET /runs// the live page (assets/live/index.html) + GET /runs// a person's page (assets/live/index.html), or a family's + (assets/live/family.html) GET /runs//state.json bin/run_state.py's state, plus the headline of each finished section and what the plugins add @@ -86,6 +88,8 @@ "frame-ancestors 'none'") RUN_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$") # validate_sample in scripts/lib/common.sh ASSET_RE = re.compile(r"^(?:plugins/)?[A-Za-z0-9][A-Za-z0-9._-]*$") +# The page templates of assets/live: served filled in, never as an asset. +PAGES = ("index.html", "login.html", "home.html", "family.html") TYPES = {".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json", ".txt": "text/plain; charset=utf-8", ".tsv": "text/plain; charset=utf-8", ".md": "text/plain; charset=utf-8", ".svg": "image/svg+xml", ".png": "image/png", ".pdf": "application/pdf"} @@ -352,7 +356,8 @@ def headline(key, sec, summary=None): text, attention = fn(sec.get("data") or {}, summary or {}) except (KeyError, TypeError, ValueError, AttributeError, IndexError): text, attention = None, False - return (text or f"{title}: finished, see the report"), ("attention" if attention else "info") + # Markdown bold (the ROH helper's first line) reads as literal asterisks on a page. + return (text or f"{title}: finished, see the report").replace("**", ""), ("attention" if attention else "info") # --- the plugin interface --------------------------------------------------------------------- @@ -518,6 +523,7 @@ def __init__(self, app, cfg, password_hash): self.sessions, self.failures, self.lock = {}, {}, threading.Lock() self.streams = threading.BoundedSemaphore(cfg.max_streams) self.headline_cache = {} + self.attention_cache = {} self.allowed = set(LOOPBACK_HOSTS) | {h.lower() for h in cfg.allowed_host or []} if cfg.bind not in ("0.0.0.0", "::"): self.allowed.add(cfg.bind.strip("[]").lower()) @@ -593,7 +599,8 @@ def runs(self): for fn in self.app.run_kinds: for r in fn(self.app) or []: if valid_run(r.get("name", "")) and r["name"] not in out: - out[r["name"]] = {"name": r["name"], "dir": r.get("dir"), "kind": r.get("kind", "other")} + # Keys beyond name, dir and kind (a family's members) pass through to /runs.json. + out[r["name"]] = dict(r, name=r["name"], dir=r.get("dir"), kind=r.get("kind", "other")) return out @staticmethod @@ -604,9 +611,10 @@ def inside(path, top): def run(self, name): return self.runs().get(name) if valid_run(name) else None - def state(self, run): + def base_state(self, run, events=None): + """run_state's state of the run's newest run, without headlines or plugins (the home page's view).""" d = run["dir"] or os.path.join(self.root, run["name"]) - events = self.store.events(run["name"]) + events = self.store.events(run["name"]) if events is None else events cur = current_run(events) # run_state.collect, on the current run's messages only (an earlier run's 'completed' would # mark this one finished, and its tasks would mark this run's steps done). @@ -625,6 +633,12 @@ def state(self, run): if run_finished(events) and st["run"]["state"] == "running": st["run"]["state"] = "finished" st["events"] = len(events) # the page paints a message ahead of state.json only past this id + return st + + def state(self, run): + d = run["dir"] or os.path.join(self.root, run["name"]) + events = self.store.events(run["name"]) + st = self.base_state(run, events) # The report's anchor of each section (bin/render_report.py's chapters), for the step detail's link. sec_of = getattr(_rr(), "SECTION_OF", {}) st["anchors"] = {k: sec_of[k] for s in st["steps"] for k in s["sections"] if k in sec_of} @@ -634,30 +648,66 @@ def state(self, run): st["plugins"][name] = fn(d, st, events) return st - def headlines(self, sample, d, st): - """{section: {text, tone, step}} for the sections of the steps that are done. The summary - is read again only when the set of done steps or the message count changes.""" + def summary(self, sample, d, st): + """The run's summary for the steps that are done: its summary.json when that is newer than + the end of every done step (step 24 wrote it after them), else collected now. Read again only + when the set of done steps or the message count changes; None while nothing is done.""" done = tuple(s["step"] for s in st["steps"] if s["state"] == "done") key = (sample, done, st.get("events")) # a section's file can land after its step's last message hit = self.headline_cache.get(sample) if hit and hit[0] == key: + return hit[2] + summary = None + if done and d and os.path.isdir(d): + summary = self.summary_file(d, st) + if summary is None: + import collect_summary + try: + summary = collect_summary.collect(sample, d) + except Exception as e: # a reader bug must not take the page down + print(f"owngenome_live: summary of {sample} not read: {type(e).__name__}", file=sys.stderr) + self.headline_cache[sample] = (key, None, summary) + return summary + + @staticmethod + def summary_file(d, st): + """/summary.json when it is newer than every done step's end, else None.""" + p = os.path.join(d, "summary.json") + try: + mtime = os.path.getmtime(p) + except OSError: + return None + ends = [s.get("ended_utc") for s in st["steps"] if s["state"] == "done"] + for e in ends: + try: + if not e or datetime.strptime(e[:19], "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc).timestamp() > mtime: + return None + except ValueError: + return None + try: + with open(p) as f: + s = json.load(f) + return s if isinstance(s, dict) and isinstance(s.get("sections"), dict) else None + except (OSError, ValueError): + return None + + def headlines(self, sample, d, st): + """{section: {text, tone, step}} for the sections of the steps that are done.""" + summary = self.summary(sample, d, st) + hit = self.headline_cache.get(sample) + if hit and hit[1] is not None: return hit[1] out = {} - if done and os.path.isdir(d): - import collect_summary - try: - summary = collect_summary.collect(sample, d) - except Exception as e: # a reader bug must not take the page down - print(f"owngenome_live: summary of {sample} not read: {type(e).__name__}", file=sys.stderr) - summary = None + if summary: for s in st["steps"]: - if s["state"] != "done" or not summary: + if s["state"] != "done": continue for k in s["sections"]: h = headline(k, summary["sections"].get(k), summary) if h: out[k] = {"text": h[0], "tone": h[1], "step": s["step"]} - self.headline_cache[sample] = (key, out) + if hit: + self.headline_cache[sample] = (hit[0], out, hit[2]) return out def run_files(self, run): @@ -702,11 +752,58 @@ def sole_html(self, run, folder, files=None): and Live.inside(os.path.join(p, f), top) and Live.inside(os.path.join(p, f), self.root)] return pages[0] if len(pages) == 1 else None - def index_page(self): - assets = self.cfg.assets - with open(os.path.join(assets, "index.html")) as f: + def index_page(self, kind=None): + """The page template of a run: family.html for a family, index.html (a person) otherwise.""" + name = "family.html" if kind == "family" and os.path.isfile(os.path.join(self.cfg.assets, "family.html")) \ + else "index.html" + with open(os.path.join(self.cfg.assets, name)) as f: return f.read() + def attention(self, d): + """(count of cards worth a look, mtime) from /summary.json alone, cached by its mtime; + (None, None) without one. The home page reads no result file beyond that.""" + p = os.path.join(d, "summary.json") if d else None + try: + mtime = os.path.getmtime(p) + except (OSError, TypeError): + return None, None + hit = self.attention_cache.get(p) + if hit and hit[0] == mtime: + return hit[1], mtime + n = None + try: + import live_results + with open(p) as f: + n = live_results.attention_count(json.load(f)) + except Exception as e: # one bad file must not take the home page down + print(f"owngenome_live: {os.path.basename(os.path.dirname(p))}/summary.json not read: {type(e).__name__}", + file=sys.stderr) + self.attention_cache[p] = (mtime, n) + return n, mtime + + def runs_index(self): + """/runs.json: each run's progress, and the families with their members, for the home page. + No result beyond the attention count; no path.""" + runs = self.runs() + out = [] + for name, r in sorted(runs.items()): + item = {"name": name, "kind": r["kind"], "url": f"/runs/{name}/"} + if r["kind"] != "family": + st = self.base_state(r) + planned = [s for s in st["steps"] if s["state"] != "not planned"] + item.update(state=st["run"]["state"], started_utc=st["run"].get("started_utc"), + finished_utc=st["run"].get("finished_utc"), declared_sex=st["run"].get("declared_sex"), + planned=len(planned), done=sum(1 for s in planned if s["state"] == "done"), + running=sum(1 for s in planned if s["state"] == "running"), + failed=sum(1 for s in planned if s["state"] == "failed")) + item["attention"], mtime = self.attention(r["dir"]) + item["results"] = mtime is not None + else: + item["members"] = r.get("members") or [] + out.append(item) + fams = [{"name": x["name"], "url": x["url"], "members": x["members"]} for x in out if x["kind"] == "family"] + return {"runs": [x for x in out if x["kind"] != "family"], "families": fams} + class BrowserHandler(BaseHTTPRequestHandler): server_version = "owngenome-live" @@ -856,6 +953,8 @@ def handle_any(self, method): return self.deny(401) if path == "/": return self.index() + if path == "/runs.json": + return self.runs_json() m = re.fullmatch(r"/runs/([^/]+)(/.*)?", path) if not m: return self.deny(404) @@ -921,30 +1020,35 @@ def asset(self, name): return self.deny(404) p = os.path.join(self.live.cfg.assets, name) top = os.path.realpath(self.live.cfg.assets) - if not os.path.isfile(p) or not Live.inside(p, top) or name in ("index.html", "login.html"): + if not os.path.isfile(p) or not Live.inside(p, top) or name in PAGES: return self.deny(404) with open(p, "rb") as f: self.send_body(200, TYPES.get(os.path.splitext(p)[1], "application/octet-stream"), f.read()) def index(self): + """assets/live/home.html: people grouped by family, drawn by home.js from /runs.json. The plain + list of runs in it is what a browser without scripts shows.""" runs = self.live.runs() items = "\n".join(f'
  • {html.escape(n)} ' f'{html.escape(r["kind"])}
  • ' for n, r in sorted(runs.items())) self.log("index", status=200) - page = ('\n' - 'Runs' - '

    Runs

    ' - '
    ' - f'
      {items or "
    • No run under this folder yet.
    • "}
    \n') + with open(os.path.join(self.live.cfg.assets, "home.html")) as f: + page = f.read() + page = (page.replace("{{assets}}", "/assets").replace("{{data}}", "/runs.json") + .replace("{{runs}}", items or "
  • No run under this folder yet.
  • ")) self.send_body(200, TYPES[".html"], page) + def runs_json(self): + self.log("runs", status=200) + self.send_json(self.live.runs_index()) + def run_page(self, run): plugins_dir = os.path.join(self.live.cfg.assets, "plugins") js = sorted(f for f in os.listdir(plugins_dir) if ASSET_RE.match(f) and f.endswith(".js")) \ if os.path.isdir(plugins_dir) else [] tags = "\n".join(f'' for f in js) - page = (self.live.index_page().replace("{{assets}}", "/assets").replace("{{run}}", html.escape(run["name"])) - .replace("{{mode}}", "live").replace("{{plugins}}", tags)) + page = (self.live.index_page(run.get("kind")).replace("{{assets}}", "/assets") + .replace("{{run}}", html.escape(run["name"])).replace("{{mode}}", "live").replace("{{plugins}}", tags)) self.log("run", run["name"], status=200) self.send_body(200, TYPES[".html"], page) diff --git a/bin/owngenome_live_family.py b/bin/owngenome_live_family.py index 7f1f20ef..5cff7157 100644 --- a/bin/owngenome_live_family.py +++ b/bin/owngenome_live_family.py @@ -11,6 +11,12 @@ GET /family// pedigree.svg (drawn), or one of the family's result files listed in state.json; any other name is 404 + GET /runs//family.json the family page's data (assets/live/family.html): + the state below, the pedigree's layout (so each + person is a link), the relationship and shared- + segment rows with their measured numbers, each + couple's gene rows, and each member's run progress + and attention count (from its summary.json only) state.json gets plugins.family: {"kind": "family", ...} on a family's page, {"kind": "member", "families": [...]} on the page of a sample in a family, @@ -138,6 +144,72 @@ def link(f): "files": files} +def layout_of(order, members): + """The pedigree drawing's geometry (bin/pedigree_svg.py layout) as data, so the page draws each + person as a link: people with their centre, partner lines and the descent lines of each sibship.""" + pos, gen, pairs, widest, ngen = pedigree_svg.layout(order, members) + h = pedigree_svg.SIZE / 2 + people = [{"sample": s, "x": round(pos[s][0], 1), "y": round(pos[s][1], 1), "gen": gen[s], "sex": members[s]["sex"], + "sequenced": bool(members[s]["sequenced"])} for s in order] + lines = [] + for a, b in pairs: + (xa, ya), (xb, yb) = sorted((pos[a], pos[b])) + lines.append([xa + h, ya, xb - h, yb]) + sibships = {} + for s in order: + f, mo = members[s]["father"], members[s]["mother"] + f, mo = (f if f in pos else None), (mo if mo in pos else None) + if f or mo: + sibships.setdefault((f, mo), []).append(s) + for (f, mo), kids in sibships.items(): + if f and mo: + ox, oy = (pos[f][0] + pos[mo][0]) / 2, (pos[f][1] + pos[mo][1]) / 2 + else: + ox, oy = pos[f or mo][0], pos[f or mo][1] + h + bar = min(pos[k][1] for k in kids) - h - 22 + xs = [pos[k][0] for k in kids] + [ox] + lines += [[ox, oy, ox, bar], [min(xs), bar, max(xs), bar]] + lines += [[pos[k][0], bar, pos[k][0], pos[k][1] - h] for k in kids] + width = 2 * pedigree_svg.MARGIN + pedigree_svg.SIZE + (widest - 1) * pedigree_svg.STEP_X + height = pedigree_svg.TOP + pedigree_svg.SIZE + (ngen - 1) * pedigree_svg.STEP_Y + 30 + return {"width": width, "height": height, "size": pedigree_svg.SIZE, "people": people, + "lines": [[round(v, 1) for v in ln] for ln in lines]} + + +def family_page(name, d): + """family.json without the member runs: the state, the layout, the full pair rows and couple rows.""" + st = family_state(name, d) + out = {"state": st, "layout": None, "relationships": [], "ibd_pairs": [], "couples": {}} + if st.get("error"): + return out + order, members = members_of(d) + out["layout"] = layout_of(order, members) + keep = ("sample_a", "sample_b", "declared", "expected_relatedness", "relatedness", "ibs0", "ibs2", "sites", + "verdict", "reason") + out["relationships"] = [{k: r.get(k) for k in keep} for r in read_tsv(os.path.join(d, "relationships.tsv"))] + keep = ("sample_a", "sample_b", "declared", "segments", "shared_cm", "ibd2_cm", "longest_cm", "relatedness", + "degree") + out["ibd_pairs"] = [{k: r.get(k) for k in keep} for r in read_tsv(os.path.join(d, "ibd_pairs.tsv"))] + order_of = {"shared": 0, "x_linked": 1, "one_partner": 2} + for a, b, base in couple_names(order, members): + rows = read_tsv(os.path.join(d, base + ".tsv")) + keep = ("gene", "inheritance", "condition", "result", "offspring_risk", "partner_a", "findings_a", + "partner_b", "findings_b", "explanation") + rows = sorted(({k: r.get(k) for k in keep} for r in rows), key=lambda r: order_of.get(r.get("result"), 3)) + out["couples"]["%s %s" % (a, b)] = rows + # Step 38 writes the verdict "ok" for a pair that matches its declared relation. The page says that + # in words: no word on the page reads as an all-clear (state.json keeps the file's own value). + out["state"] = dict(st, relationships=[dict(r, verdict=verdict_words(r.get("verdict"))) + for r in st.get("relationships") or []]) + for r in out["relationships"]: + r["verdict"] = verdict_words(r.get("verdict")) + return out + + +def verdict_words(v): + return "matches the declared relation" if (v or "").strip().lower() == "ok" else v + + def member_state(root, sample): fams = [] for name, d in family_dirs(root): @@ -155,7 +227,40 @@ def root(): return os.path.realpath(app.config.root) def runs(app_): - return [{"name": n, "dir": d, "kind": "family"} for n, d in family_dirs(root())] + out = [] + for n, d in family_dirs(root()): + try: + order, members = members_of(d) + people = [{"sample": s, "sex": members[s]["sex"], "father": members[s]["father"], + "mother": members[s]["mother"], "partner": members[s]["partner"], + "sequenced": members[s]["sequenced"]} for s in order] + except (OSError, ValueError): + people = [] + out.append({"name": n, "dir": d, "kind": "family", "members": people}) + return out + + def page_data(req, m): + name = m.group(1) + d = req.run_dir(name) + if not d or not is_family(d) or not inside(d, root()): + app.log("family", name, "family.json", 404) + return req.send_body(404, "text/plain; charset=utf-8", "not found\n") + out = family_page(name, d) + live = getattr(req, "live", None) + runs = {} + for p in out["state"].get("members") or []: + r = live.run(p["sample"]) if live else None + if not r or r.get("kind") == "family": + continue + st = live.base_state(r) + planned = [s for s in st["steps"] if s["state"] != "not planned"] + n, mtime = live.attention(r["dir"]) + runs[p["sample"]] = {"url": "/runs/%s/" % p["sample"], "state": st["run"]["state"], + "planned": len(planned), "done": sum(1 for s in planned if s["state"] == "done"), + "attention": n, "results": mtime is not None} + out["runs"] = runs + app.log("family", name, "family.json", 200) + return req.send_json(out) def hook(run_dir, state, events): if not run_dir: @@ -191,3 +296,4 @@ def serve(req, m): app.run_kind(runs) app.state_hook("family", hook) app.route("GET", r"/family/(%s)/(%s)" % (NAME, NAME), serve) + app.route("GET", r"/runs/(%s)/family\.json" % NAME, page_data) diff --git a/bin/owngenome_live_results.py b/bin/owngenome_live_results.py new file mode 100644 index 00000000..1684cf04 --- /dev/null +++ b/bin/owngenome_live_results.py @@ -0,0 +1,51 @@ +"""owngenome_live_results.py: a person's results on the live site, a plugin of bin/owngenome_live.py. + + GET /runs//results.json bin/live_results.py's build() of the run's summary: the + report's seven chapters, one card per section with its + facts, its rows, its caveats and its attention flag + +The summary is the one the server already reads for the step headlines +(Live.summary): the run's summary.json when step 24 wrote it after every done +step, else collect_summary.collect() of the folder, read again only when a step +ends. A reader error gives no cards and a note, never a 500. The raw +summary.json is never served: no run_file glob is added, and results.json holds +what the report prints. Every request writes one access-log row. +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) + +NAME = r"[A-Za-z0-9][A-Za-z0-9._-]*" + + +def register(app): + def results(req, m): + live = req.live + run = live.run(m.group(1)) + if not run or run.get("kind") == "family": + app.log("results", m.group(1), "-", 404) + return req.send_body(404, "text/plain; charset=utf-8", "not found\n") + name = run["name"] + d = run["dir"] or os.path.join(live.root, name) + out = {"schema_version": 1, "sample": name, "chapters": [], "note": None} + try: + import live_results + st = live.base_state(run) + summary = live.summary(name, d, st) + if summary: + files = live.run_files(run) if run["dir"] else set() + out = live_results.build(name, summary, files) + else: + out["note"] = "No step has finished yet: the results appear here as the steps end." + except Exception as e: # a reader bug must not take the page down + print(f"owngenome_live: results of {name} not built: {type(e).__name__}", file=sys.stderr) + out = {"schema_version": 1, "sample": name, "chapters": [], + "note": f"The results could not be read ({type(e).__name__}); the report and the Progress tab " + "still work."} + app.log("results", name, "-", 200) + return req.send_json(out) + + app.route("GET", r"/runs/(%s)/results\.json" % NAME, results) + From 0e302d7087ffbd41f46865c81e068c42b590ed23 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sat, 7 Nov 2026 20:36:43 +0100 Subject: [PATCH 07/14] feat(live): turn each run page into the person's results, with a home page of people and a family page The person page opens on what is worth a look, then one tab per report chapter, each card with its caveat under the title and its chart inside. The step map moves to a Progress tab. The home page groups people by family, runs still going first. The family page draws a clickable pedigree, the members, each couple's carrier screen in full and who is related how. Bead: pgp-5bja --- assets/live/core.js | 128 ++++++++++++ assets/live/family-page.js | 217 ++++++++++++++++++++ assets/live/family.html | 22 +++ assets/live/home.html | 21 ++ assets/live/home.js | 105 ++++++++++ assets/live/index.html | 50 +++-- assets/live/live.css | 351 +++++++++++++++++++++++++++------ assets/live/live.js | 14 +- assets/live/person.js | 349 ++++++++++++++++++++++++++++++++ assets/live/plugins/charts.js | 28 ++- assets/live/plugins/family.js | 9 +- assets/live/plugins/recheck.js | 4 +- assets/live/plugins/search.js | 15 +- bin/owngenome_live_family.py | 4 +- 14 files changed, 1223 insertions(+), 94 deletions(-) create mode 100644 assets/live/core.js create mode 100644 assets/live/family-page.js create mode 100644 assets/live/family.html create mode 100644 assets/live/home.html create mode 100644 assets/live/home.js create mode 100644 assets/live/person.js diff --git a/assets/live/core.js b/assets/live/core.js new file mode 100644 index 00000000..8232f552 --- /dev/null +++ b/assets/live/core.js @@ -0,0 +1,128 @@ +// core.js: what the live site's pages share (home.html, index.html, family.html). Element builders, +// a fetch that sends the session and goes to the login on a 401, the pedigree symbol, dates, the +// caveat block, the facts list and the table. Text only through textContent and createElementNS: +// no innerHTML, no inline script, no eval. Everything hangs on window.PGPUI. +(function () { + 'use strict'; + var root = document.documentElement; + var SVGNS = 'http://www.w3.org/2000/svg'; + var blocked = []; + document.addEventListener('securitypolicyviolation', function (e) { + blocked.push(e.violatedDirective + ' ' + (e.blockedURI || '')); + root.setAttribute('data-pgp-blocked', blocked.join(' | ')); + }); + + function el(tag, attrs, text) { + var e = document.createElement(tag); + Object.keys(attrs || {}).forEach(function (k) { + if (attrs[k] !== null && attrs[k] !== undefined && attrs[k] !== false) { e.setAttribute(k, attrs[k]); } + }); + if (text !== undefined && text !== null) { e.textContent = String(text); } + return e; + } + function svg(tag, attrs) { + var e = document.createElementNS(SVGNS, tag); + Object.keys(attrs || {}).forEach(function (k) { e.setAttribute(k, attrs[k]); }); + return e; + } + function add(parent) { + for (var i = 1; i < arguments.length; i++) { + var c = arguments[i]; + if (c === null || c === undefined || c === false) { continue; } + parent.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); + } + return parent; + } + function getJSON(url) { + return fetch(url, { credentials: 'same-origin', cache: 'no-store' }).then(function (r) { + if (r.status === 401) { window.location.href = '/login?next=' + encodeURIComponent(window.location.pathname); return null; } + return r.ok ? r.json() : null; + }); + } + // The pedigree symbol: a square (male), a circle (female) or a diamond (not known); filled when sequenced. + function avatar(sex, filled, ratio) { + var a = el('span', { 'class': 'avatar sex-' + (sex === 'male' || sex === 'female' ? sex : 'unknown') + (filled ? ' filled' : '') , + 'aria-hidden': 'true' }); + if (typeof ratio === 'number') { + a.classList.add('ring'); + a.style.setProperty('--p', String(Math.max(0, Math.min(1, ratio)))); + } + add(a, el('span', { 'class': 'glyph' })); + return a; + } + function day(utc) { return utc ? String(utc).slice(0, 10) : ''; } + function firstSentence(t) { + var m = /^(.+?[.!?])(\s+[A-Z(]|$)/.exec(t || ''); + return m ? m[1] : t; + } + // The caveats of a card, word for word: the first sentence of the first one in view, all of them in a + // details element (in the page, so a reader or a search finds them). + function caveat(list) { + if (!list || !list.length) { return null; } + var box = el('div', { 'class': 'caveat' }); + var lead = firstSentence(list[0]); + add(box, el('p', { 'class': 'caveat-lead' }, lead)); + var d = el('details', { 'class': 'caveat-full' }); + add(d, el('summary', {}, list.length > 1 || lead !== list[0] ? 'Full caveat' : 'Caveat')); + list.forEach(function (t) { add(d, el('p', {}, t)); }); + add(box, d); + return box; + } + function facts(rows) { + if (!rows || !rows.length) { return null; } + var dl = el('dl', { 'class': 'facts' }); + rows.forEach(function (r) { + var w = el('div', { 'class': 'fact' }); + add(w, el('dt', {}, r[0]), el('dd', {}, r[1])); + add(dl, w); + }); + return dl; + } + // A table; on a phone each row becomes a block of label: value pairs (the label is data-label). + function table(t, limit) { + if (!t || !t.rows || !t.rows.length) { return null; } + limit = limit || 8; + var wrap = el('div', { 'class': 'table-wrap' }); + var tb = el('table', { 'class': 'rows' }); + var head = el('tr', {}); + t.columns.forEach(function (c) { add(head, el('th', { scope: 'col' }, c)); }); + add(tb, add(el('thead', {}), head)); + var body = el('tbody', {}); + t.rows.forEach(function (r, i) { + var tr = el('tr', i >= limit ? { 'class': 'extra', hidden: 'hidden' } : {}); + r.forEach(function (c, j) { add(tr, el('td', { 'data-label': t.columns[j] || '' }, c)); }); + add(body, tr); + }); + add(tb, body); + add(wrap, tb); + var more = t.rows.length - limit + (t.more || 0); + if (t.rows.length > limit) { + var b = el('button', { type: 'button', 'class': 'more' }, 'Show all ' + (t.rows.length + (t.more || 0)) + ' rows'); + b.addEventListener('click', function () { + Array.prototype.forEach.call(body.querySelectorAll('tr.extra'), function (x) { x.hidden = false; }); + b.remove(); + }); + add(wrap, b); + } else if (more > 0) { + add(wrap, el('p', { 'class': 'dim' }, t.more + ' more rows in the report')); + } + return wrap; + } + // role=tablist keyboard pattern: arrow keys move, Home and End jump; Enter or a click activates. + function tablist(list, onPick) { + list.addEventListener('keydown', function (e) { + var tabs = Array.prototype.slice.call(list.querySelectorAll('[role=tab]')); + var i = tabs.indexOf(document.activeElement); + if (i < 0) { return; } + var j = { ArrowDown: i + 1, ArrowRight: i + 1, ArrowUp: i - 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 }[e.key]; + if (j === undefined) { return; } + e.preventDefault(); + j = (j + tabs.length) % tabs.length; + tabs[j].focus(); + onPick(tabs[j]); + }); + } + + window.PGPUI = { el: el, svg: svg, add: add, getJSON: getJSON, avatar: avatar, day: day, caveat: caveat, + facts: facts, table: table, tablist: tablist, firstSentence: firstSentence }; +}()); diff --git a/assets/live/family-page.js b/assets/live/family-page.js new file mode 100644 index 00000000..25bab522 --- /dev/null +++ b/assets/live/family-page.js @@ -0,0 +1,217 @@ +// family-page.js: a family's page (assets/live/family.html), from family.json +// (bin/owngenome_live_family.py): the pedigree with each sequenced person a link to their page, the +// members with their run progress, what steps 38 and 39 planned, each couple's carrier screen in full +// (both carry, then X-linked, then one partner), who is related how, and each child's chromosome +// map. It draws what the steps wrote and computes nothing about genetics. Text through textContent, +// the drawing through createElementNS; images from this server only. +(function () { + 'use strict'; + var U = window.PGPUI; + if (!U) { return; } + var el = U.el, add = U.add, root = document.documentElement; + var RESULT = { shared: 'Both carry', x_linked: 'X-linked', one_partner: 'One partner' }; + + function section(title, cls) { + var s = el('section', { 'class': 'card ' + (cls || '') }); + add(s, el('h2', {}, title)); + return s; + } + function memberUrl(f, s) { return f.runs && f.runs[s] ? f.runs[s].url : null; } + + // The pedigree drawn from the layout the server gives: a sequenced person with a run here is a link. + function pedigree(f) { + var L = f.layout, st = f.state; + var box = el('figure', { 'class': 'pedigree-figure' }); + if (!L) { add(box, el('p', { 'class': 'dim' }, st.error || 'No pedigree.')); return box; } + var s = U.svg('svg', { viewBox: '0 0 ' + L.width + ' ' + L.height, role: 'group', 'aria-label': 'Pedigree of ' + st.name, + 'class': 'pedigree-svg', preserveAspectRatio: 'xMidYMin meet' }); + var lines = U.svg('g', { 'class': 'ped-lines' }); + L.lines.forEach(function (l) { lines.appendChild(U.svg('line', { x1: l[0], y1: l[1], x2: l[2], y2: l[3] })); }); + s.appendChild(lines); + var h = L.size / 2; + L.people.forEach(function (p) { + var url = memberUrl(f, p.sample); + var g = U.svg(url ? 'a' : 'g', url ? { href: url, 'class': 'ped-person link', 'aria-label': p.sample + ': open their page' } + : { 'class': 'ped-person', 'aria-label': p.sample + (p.sequenced ? ': no run on this server' : ': not sequenced') }); + var cls = 'ped-shape' + (p.sequenced ? ' filled' : ''); + var shape = p.sex === 'male' ? U.svg('rect', { x: p.x - h, y: p.y - h, width: L.size, height: L.size, 'class': cls }) + : p.sex === 'female' ? U.svg('circle', { cx: p.x, cy: p.y, r: h, 'class': cls }) + : U.svg('polygon', { points: [p.x, p.y - h, p.x + h, p.y, p.x, p.y + h, p.x - h, p.y].join(' '), 'class': cls }); + g.appendChild(shape); + var t = U.svg('text', { x: p.x, y: p.y + h + 17, 'text-anchor': 'middle', 'class': 'ped-name' }); + t.textContent = p.sample; + g.appendChild(t); + var r = f.runs && f.runs[p.sample]; + if (r && r.attention) { + var b = U.svg('text', { x: p.x + h + 4, y: p.y - h + 4, 'class': 'ped-flag' }); + b.textContent = '▲ ' + r.attention; + g.appendChild(b); + } + var title = U.svg('title', {}); + title.textContent = p.sample + ', ' + (p.sex || 'sex not known') + (p.sequenced ? ', sequenced' : ', not sequenced'); + g.appendChild(title); + s.appendChild(g); + }); + add(box, s); + add(box, el('figcaption', { 'class': 'dim' }, 'Square: male. Circle: female. Diamond: sex not known. Filled: sequenced. ' + + 'A person with a run on this server opens their page; ▲ counts the cards their report flags.')); + var d = el('details', { 'class': 'pedigree-image' }); + add(d, el('summary', {}, 'The drawing as an image')); + var img = el('img', { src: st.pedigree, alt: 'Pedigree of ' + st.name, 'class': 'pedigree' }); + add(d, img); + add(box, d); + return box; + } + + function members(f) { + var s = section('Members', 'members'); + var ul = el('ul', { 'class': 'people' }); + f.state.members.forEach(function (m) { + var r = f.runs && f.runs[m.sample]; + var li = el('li', { 'class': 'person-card' + (r ? ' has-run' : '') }); + var inner = r ? el('a', { href: r.url, 'class': 'person-link' }) : el('div', { 'class': 'person-link' }); + add(inner, U.avatar(m.sex, m.sequenced, r && r.planned ? r.done / r.planned : null)); + var txt = el('div', { 'class': 'person-text' }); + add(txt, el('span', { 'class': 'person-name' }, m.sample)); + var rel = []; + if (m.father || m.mother) { rel.push('child of ' + [m.father, m.mother].filter(Boolean).join(' and ')); } + if (m.partner) { rel.push('partner of ' + m.partner); } + if (rel.length) { add(txt, el('span', { 'class': 'person-place' }, rel.join('; '))); } + var state = !m.sequenced ? 'Not sequenced' : !r ? 'No run on this server' + : r.state === 'finished' ? 'Finished' : r.done + ' of ' + r.planned + ' steps'; + add(txt, el('span', { 'class': 'person-state' }, state)); + if (r && r.results) { + add(txt, el('span', { 'class': 'person-look' + (r.attention ? ' attn' : '') }, + r.attention ? r.attention + ' worth a look' : 'Nothing flagged; see the caveats')); + } + add(inner, txt); + add(li, inner); + add(ul, li); + }); + add(s, ul); + return s; + } + + function analyses(f) { + var plan = f.state.plan || []; + var p = el('p', { 'class': 'analyses' }); + if (!plan.length) { add(p, 'Analyses: none planned yet (step 38 writes family_plan.tsv).'); return p; } + add(p, el('span', { 'class': 'label' }, 'Analyses: ')); + plan.forEach(function (r, i) { + if (i) { add(p, ', '); } + var word = r.decision === 'runs' ? r.analysis.replace(/_/g, ' ') : r.analysis.replace(/_/g, ' ') + ' (' + r.decision + ')'; + add(p, el('span', { 'class': 'plan-item d-' + r.decision, title: r.reason || '' }, word)); + }); + return p; + } + + function couples(f) { + var s = section('Couple carrier screen', 'couples'); + add(s, el('p', { 'class': 'caveat-lead' }, f.couple_note)); + if (!f.state.couples.length) { add(s, el('p', { 'class': 'dim' }, 'The pedigree names no partners.')); } + f.state.couples.forEach(function (c) { + var p = el('p', { 'data-couple': c.a + ' ' + c.b, 'class': 'couple-line' }); + if (!c.screened) { + p.textContent = c.a + ' and ' + c.b + ': not screened yet (step 39).'; + add(s, p); + return; + } + var rows = (f.couples || {})[c.a + ' ' + c.b] || []; + var one = rows.filter(function (r) { return r.result === 'one_partner'; }).length; + // A count of zero is not a clean result: the screen sees only what it lists as assessed. + p.textContent = c.a + ' and ' + c.b + ': ' + c.shared + ' gene(s) where both carry a finding, ' + c.x_linked + + ' X-linked, ' + one + ' where one partner carries a finding, unless the other partner carries something the ' + + 'screen does not see. '; + if (c.report) { add(p, el('a', { href: c.report }, 'What the screen does not see')); } + add(s, p); + add(s, U.table({ columns: ['Result', 'Gene', 'Condition', 'Chance for a child', c.a, c.b, 'Why'], + rows: rows.map(function (r) { + var aFirst = r.partner_a === c.a; + return [RESULT[r.result] || r.result, r.gene, r.condition, r.offspring_risk, + aFirst ? r.findings_a : r.findings_b, aFirst ? r.findings_b : r.findings_a, r.explanation]; + }) }, 12)); + }); + return s; + } + + function related(f) { + var s = section('Who is related how', 'related'); + var rel = f.relationships || [], ibd = f.ibd_pairs || []; + if (!rel.length && !ibd.length) { add(s, el('p', { 'class': 'dim' }, 'Not run yet (step 38).')); return s; } + var byPair = {}; + ibd.forEach(function (r) { byPair[[r.sample_a, r.sample_b].sort().join(' ')] = r; }); + var rows = rel.map(function (r) { + var i = byPair[[r.sample_a, r.sample_b].sort().join(' ')] || {}; + return [r.sample_a + ' and ' + r.sample_b, r.declared, r.relatedness, r.ibs0, r.ibs2, i.shared_cm || '.', + i.degree || '.', r.verdict, r.reason]; + }); + ibd.forEach(function (i) { + if (!rel.some(function (r) { return [r.sample_a, r.sample_b].sort().join(' ') === [i.sample_a, i.sample_b].sort().join(' '); })) { + rows.push([i.sample_a + ' and ' + i.sample_b, i.declared, i.relatedness, '.', '.', i.shared_cm, i.degree, '.', '.']); + } + }); + add(s, U.table({ columns: ['Pair', 'Declared', 'Relatedness', 'IBS0', 'IBS2', 'Shared cM', 'Degree', 'Verdict', 'Reason'], + rows: rows }, 20)); + if (!ibd.length) { add(s, el('p', { 'class': 'dim' }, 'Shared segments need step 26b’s panel: only the somalier half is here.')); } + return s; + } + + function children(f) { + var s = section('Inherited stretches', 'children'); + if (!f.state.children.length) { add(s, el('p', { 'class': 'dim' }, 'The pedigree names no child.')); } + f.state.children.forEach(function (c) { + var d = el('details', { 'data-child': c.child }); + add(d, el('summary', {}, c.child + ': father ' + (c.father || 'not named') + ', mother ' + (c.mother || 'not named'))); + if (c.segments) { + add(d, el('img', { src: c.segments, alt: 'Chromosome map of ' + c.child, 'class': 'segments' })); + } else { + add(d, el('p', { 'class': 'dim' }, 'No chromosome map yet: step 38 draws one for a child with a sequenced parent.')); + } + add(s, d); + }); + return s; + } + + function more(f) { + var s = section('Plan and reports', 'plan'); + var plan = f.state.plan || []; + if (plan.length) { + add(s, U.table({ columns: ['Analysis', 'Members', 'Decision', 'Reason'], + rows: plan.map(function (r) { return [r.analysis, r.members, r.decision, r.reason]; }) }, 20)); + } + if (f.state.reports.length) { + var ul = el('ul', { 'class': 'lines' }); + f.state.reports.forEach(function (r) { add(ul, add(el('li', {}), el('a', { href: r.url }, r.name))); }); + add(s, ul); + } + return s; + } + + function draw(f) { + var main = document.getElementById('family'); + main.textContent = ''; + var page = el('section', { 'class': 'family-page', 'data-plugin': 'family', 'aria-label': 'Family ' + f.state.name }); + if (f.state.error) { + add(page, el('p', {}, f.state.error)); + add(main, page); + root.setAttribute('data-pgp-status', 'ready'); + return; + } + var top = el('div', { 'class': 'family-top' }); + var ped = section('Pedigree', 'ped'); + add(ped, pedigree(f), analyses(f)); + add(top, ped, members(f)); + add(page, top); + var grid = el('div', { 'class': 'family-grid' }); + add(grid, couples(f), related(f)); + add(page, grid, children(f), more(f)); + add(main, page); + root.setAttribute('data-pgp-status', 'ready'); + } + + function start() { + U.getJSON('family.json').then(function (f) { if (f) { draw(f); } }) + .catch(function () { root.setAttribute('data-pgp-status', 'offline'); }); + } + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start); } else { start(); } +}()); diff --git a/assets/live/family.html b/assets/live/family.html new file mode 100644 index 00000000..ebe0d057 --- /dev/null +++ b/assets/live/family.html @@ -0,0 +1,22 @@ + + + + + +Family {{run}}: OwnGenome + + + +{{plugins}} + + +
    + OwnGenome + + +
    +
    +

    Loading the family.

    +
    + + diff --git a/assets/live/home.html b/assets/live/home.html new file mode 100644 index 00000000..48ef0b22 --- /dev/null +++ b/assets/live/home.html @@ -0,0 +1,21 @@ + + + + + +OwnGenome: people + + + + + +
    + OwnGenome + +
    +
    +
    +
      {{runs}}
    +
    + + diff --git a/assets/live/home.js b/assets/live/home.js new file mode 100644 index 00000000..51d58ca7 --- /dev/null +++ b/assets/live/home.js @@ -0,0 +1,105 @@ +// home.js: the live site's home page (assets/live/home.html): every person, grouped by family, from +// /runs.json (bin/owngenome_live.py runs_index). A card shows the pedigree symbol, the person's place +// in the family, the run's progress and how many cards the report flags; runs still going come first +// in each family. The plain list of runs in the page is what shows without scripts. +(function () { + 'use strict'; + var U = window.PGPUI; + if (!U) { return; } + var el = U.el, add = U.add; + var root = document.documentElement; + + function role(m, fam) { + var kids = fam.members.filter(function (x) { return x.father === m.sample || x.mother === m.sample; }); + if (kids.length) { return { male: 'father', female: 'mother' }[m.sex] || 'parent'; } + if (fam.members.some(function (x) { return x.sample === m.father || x.sample === m.mother; })) { + return { male: 'son', female: 'daughter' }[m.sex] || 'child'; + } + return m.partner ? 'partner' : 'member'; + } + function progressText(r) { + if (!r) { return 'No run on this server'; } + if (r.state === 'finished') { return 'Finished' + (r.finished_utc ? ' ' + U.day(r.finished_utc) : ''); } + if (r.state === 'running') { return 'Running: ' + r.done + ' of ' + r.planned + ' steps'; } + if (!r.planned) { return 'Not started'; } + return r.done + ' of ' + r.planned + ' steps'; + } + function lookText(r) { + if (!r || !r.results) { return r && r.done ? 'Results so far on the page' : ''; } + if (r.attention === null || r.attention === undefined) { return 'Results on the page'; } + return r.attention ? r.attention + ' worth a look' : 'Nothing flagged; see the caveats'; + } + function personCard(name, sex, sequenced, place, r) { + var li = el('li', { 'class': 'person-card' + (r ? ' has-run' : '') + (r && r.state === 'running' ? ' running' : '') }); + var ratio = r && r.planned ? r.done / r.planned : null; + var inner = r ? el('a', { href: r.url, 'class': 'person-link' }) : el('div', { 'class': 'person-link' }); + add(inner, U.avatar(sex, sequenced, ratio)); + var txt = el('div', { 'class': 'person-text' }); + add(txt, el('span', { 'class': 'person-name' }, name)); + if (place) { add(txt, el('span', { 'class': 'person-place' }, place)); } + if (sequenced || r) { + if (r && r.planned) { + var bar = el('span', { 'class': 'bar', role: 'img', 'aria-label': r.done + ' of ' + r.planned + ' steps done' }); + var fill = el('span', { 'class': 'bar-fill' }); + fill.style.setProperty('--p', String(ratio)); + add(bar, fill); + add(txt, bar); + } + add(txt, el('span', { 'class': 'person-state' }, progressText(r))); + var lk = lookText(r); + if (lk) { add(txt, el('span', { 'class': 'person-look' + (r && r.attention ? ' attn' : '') }, lk)); } + } else { + add(txt, el('span', { 'class': 'person-state' }, 'Not sequenced')); + } + add(inner, txt); + add(li, inner); + return li; + } + function rank(r) { return !r ? 3 : r.state === 'running' ? 0 : r.state === 'finished' ? 1 : 2; } + + function draw(data) { + var main = document.getElementById('home'); + main.textContent = ''; + var byName = {}; + (data.runs || []).forEach(function (r) { byName[r.name] = r; }); + var inFamily = {}; + (data.families || []).forEach(function (f) { + var block = el('section', { 'class': 'family-block', 'data-family': f.name }); + var h = el('header', { 'class': 'family-head' }); + var seq = f.members.filter(function (m) { return m.sequenced; }).length; + var h2 = el('h2', {}); + add(h2, el('a', { href: f.url }, 'Family ' + f.name)); + add(h, h2, el('span', { 'class': 'dim' }, seq + ' of ' + f.members.length + ' sequenced')); + add(h, el('a', { href: f.url, 'class': 'open' }, 'Open the family page')); + add(block, h); + var ul = el('ul', { 'class': 'people' }); + f.members.slice().sort(function (a, b) { return rank(byName[a.sample]) - rank(byName[b.sample]); }).forEach(function (m) { + inFamily[m.sample] = true; + add(ul, personCard(m.sample, m.sex, m.sequenced, role(m, f), byName[m.sample])); + }); + add(block, ul); + add(main, block); + }); + var loose = (data.runs || []).filter(function (r) { return !inFamily[r.name]; }) + .sort(function (a, b) { return rank(a) - rank(b) || (a.name < b.name ? -1 : 1); }); + if (loose.length) { + var block = el('section', { 'class': 'family-block' }); + add(block, add(el('header', { 'class': 'family-head' }), el('h2', {}, (data.families || []).length ? 'Not in a family' : 'People'))); + var ul = el('ul', { 'class': 'people' }); + loose.forEach(function (r) { add(ul, personCard(r.name, r.declared_sex, true, null, r)); }); + add(block, ul); + add(main, block); + } + if (!loose.length && !(data.families || []).length) { + add(main, el('p', { 'class': 'empty' }, 'No run under this folder yet. Start one with scripts/run-all.sh and it shows here.')); + } + root.setAttribute('data-pgp-people', String(main.querySelectorAll('.person-card').length)); + root.setAttribute('data-pgp-status', 'ready'); + } + + function start() { + U.getJSON(root.getAttribute('data-data') || '/runs.json').then(function (d) { if (d) { draw(d); } }) + .catch(function () { root.setAttribute('data-pgp-status', 'offline'); }); + } + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start); } else { start(); } +}()); diff --git a/assets/live/index.html b/assets/live/index.html index fb3208bc..6f170ec4 100644 --- a/assets/live/index.html +++ b/assets/live/index.html @@ -3,31 +3,49 @@ -{{run}}: live run +{{run}}: OwnGenome + + {{plugins}} - -
    -

    {{run}} loading

    - + +
    + OwnGenome + +
    +
    -
    -
    -
    -
    -
    - -
    +
    +

    {{run}} loading

    +

    + +

    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    diff --git a/assets/live/live.css b/assets/live/live.css index c0c4c4fd..09d519c0 100644 --- a/assets/live/live.css +++ b/assets/live/live.css @@ -1,65 +1,306 @@ -/* live.css: the dark theme of the live page and the login page (assets/live). */ -:root { color-scheme: dark; --bg: #0f1216; --card: #171b21; --line: #2a313b; --fg: #e3e7ec; --muted: #8b96a3; - --waiting: #3a4350; --running: #2f6fb3; --done: #2e7d4f; --failed: #b33a3a; --skipped: #4a3f5c; - --notfinished: #8a6d1f; --notplanned: #23282f; --accent: #6aa9ff; --attention: #e0a23a; } +/* live.css: the live site's pages (assets/live): home, a person, a family, the login. + Dark first. No green anywhere: a result that ran is not a finding. --fail marks a failed step, + never a result. A tone is always a word and a symbol, never a colour alone. */ +:root { color-scheme: dark light; + --bg: #14181f; --card: #1d232d; --card2: #232a36; --rule: #2f3847; --text: #e4e7ec; --dim: #9aa3b2; + --info: #7fa6d9; --attn: #e0a64b; --low: #a99ad6; --fail: #c8664f; --focus: #7fa6d9; + --anc1: #6f8fb8; --anc2: #b98a5e; --anc3: #8b7fb5; --anc4: #5f9ea0; --anc5: #a8787f; + --serif: ui-serif, "Iowan Old Style", "Palatino Linotype", Georgia, serif; + --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --mono: ui-monospace, Menlo, monospace; + --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 32px; --s7: 48px; + --r-card: 8px; --r-chip: 3px; } +@media (prefers-color-scheme: light) { :root { + --bg: #f6f7f9; --card: #fff; --card2: #f0f2f5; --rule: #d5dae1; --text: #1b2029; --dim: #566070; + --info: #2f5f9e; --attn: #8a5a00; --low: #5f4ea0; --fail: #a33a26; --focus: #2f5f9e; } } + * { box-sizing: border-box; } -body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; } -a { color: var(--accent); } -header { display: flex; justify-content: space-between; align-items: center; padding: 12px 18px; border-bottom: 1px solid var(--line); } -header h1 { font-size: 18px; margin: 0; } -header nav a { margin-left: 14px; } -main { display: flex; gap: 16px; padding: 16px; align-items: flex-start; } -.map-wrap { flex: 1 1 auto; overflow: auto; min-width: 0; } -aside { flex: 0 0 360px; display: flex; flex-direction: column; gap: 12px; } -.card { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; } -.card h2 { font-size: 15px; margin: 0 0 8px; } -.muted { color: var(--muted); } -.badge { font-size: 12px; padding: 2px 8px; border-radius: 10px; background: var(--waiting); vertical-align: middle; } -.badge.run-running { background: var(--running); } -.badge.run-finished { background: var(--done); } -.legend { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 8px; color: var(--muted); font-size: 12px; } -.legend .swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px; margin-right: 4px; vertical-align: -2px; } -.edges line { stroke: var(--line); stroke-width: 1.2; } +html { -webkit-text-size-adjust: 100%; } +body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.5 var(--sans); font-variant-numeric: tabular-nums; } +a { color: var(--info); text-underline-offset: 2px; } +a:hover { text-decoration-thickness: 2px; } +:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 2px; } +h1, h2, h3 { font-weight: 600; margin: 0; } +.dim, .muted { color: var(--dim); } +[hidden] { display: none !important; } +button { font: inherit; background: var(--card2); color: var(--text); border: 1px solid var(--rule); border-radius: 6px; + padding: 5px 12px; cursor: pointer; } +button:hover { border-color: var(--dim); } + +/* --- the header every page shares --- */ +.top { position: sticky; top: 0; z-index: 20; display: flex; align-items: center; gap: var(--s4); padding: 10px var(--s5); + background: color-mix(in srgb, var(--bg) 92%, transparent); backdrop-filter: blur(6px); border-bottom: 1px solid var(--rule); } +.brand { font-family: var(--serif); font-size: 19px; color: var(--text); text-decoration: none; letter-spacing: .01em; } +.crumbs { display: flex; gap: var(--s2); align-items: baseline; color: var(--dim); min-width: 0; overflow: hidden; + white-space: nowrap; text-overflow: ellipsis; } +.crumbs::before { content: "/"; color: var(--rule); } +.crumbs a { color: var(--dim); } +.crumbs .sep { color: var(--rule); } +.crumbs #run-name { color: var(--text); } +.links { margin-left: auto; display: flex; gap: var(--s4); white-space: nowrap; } +.search-slot + .links { margin-left: 0; } +.logout { margin: 0 0 0 auto; } + +/* the search box (plugins/search.js) in the header */ +.search-slot { margin-left: auto; position: relative; width: min(320px, 40vw); } +.search-slot > section.plugin { background: none; border: 0; padding: 0; margin: 0; } +.search-slot h2, .search-slot .search-status { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); } +.search-slot form { margin: 0; } +.search-slot input[type=search] { width: 100%; font: inherit; font-size: 14px; color: var(--text); background: var(--card); + border: 1px solid var(--rule); border-radius: 6px; padding: 6px 10px; } +.search-slot input[type=search]:disabled { color: var(--dim); } +.search-results { display: none; position: absolute; right: 0; top: calc(100% + 6px); width: min(460px, 92vw); max-height: 70vh; + overflow: auto; background: var(--card); border: 1px solid var(--rule); border-radius: var(--r-card); padding: var(--s3) var(--s4); + box-shadow: 0 12px 32px rgba(0, 0, 0, .35); } +.search-slot:focus-within .search-results:not(:empty) { display: block; } +.search-results ul { padding-left: 1.1em; margin: 4px 0; } +.search-results h3 { font-size: 15px; } +.search-results .suggestions button { padding: 1px 8px; margin: 2px; } + +/* --- a person: the head, the strip --- */ +.person-head { max-width: 1120px; margin: 0 auto; padding: var(--s5) var(--s5) var(--s3); } +.person-head h1 { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; font-family: var(--serif); font-size: 30px; + font-weight: 500; line-height: 1.2; } +.person-line { color: var(--dim); margin: var(--s2) 0 var(--s3); } +.strip { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 2px; height: 10px; border-radius: 3px; overflow: hidden; + text-decoration: none; } +.strip .cell { background: var(--rule); } +.strip .cell.st-done { background: var(--info); } +.strip .cell.st-running { background: repeating-linear-gradient(45deg, var(--info) 0 3px, var(--card2) 3px 6px); } +.strip .cell.st-failed { background: var(--fail); } +.strip .cell.st-skipped, .strip .cell.st-not-finished { background: var(--card2); } +.strip-text { color: var(--dim); font-size: 13px; margin: 6px 0 0; } +.badge { font: 600 12px/1 var(--sans); padding: 4px 8px; border-radius: var(--r-chip); border: 1px solid var(--rule); color: var(--dim); + letter-spacing: .02em; } +.badge.run-running { color: var(--info); border-color: var(--info); } +.badge.run-finished { color: var(--text); } + +/* the pedigree symbol, with its ring showing progress */ +.avatar { --p: 0; position: relative; display: inline-grid; place-items: center; width: 34px; height: 34px; flex: none; } +.avatar .glyph { width: 20px; height: 20px; border: 2px solid var(--text); } +.avatar.sex-female .glyph { border-radius: 50%; } +.avatar.sex-unknown .glyph { width: 16px; height: 16px; transform: rotate(45deg); } +.avatar.filled .glyph { background: var(--text); } +.avatar.ring::before { content: ""; position: absolute; inset: 0; border-radius: 50%; + background: conic-gradient(var(--info) calc(var(--p) * 360deg), var(--rule) 0); + -webkit-mask: radial-gradient(circle, transparent 14px, #000 15px); mask: radial-gradient(circle, transparent 14px, #000 15px); } + +/* --- the layout: the rail and the content --- */ +.layout { max-width: 1120px; margin: 0 auto; padding: 0 var(--s5) var(--s7); display: grid; grid-template-columns: 200px minmax(0, 1fr); + gap: var(--s5); align-items: start; } +.rail { position: sticky; top: 64px; display: flex; flex-direction: column; gap: 2px; padding-top: var(--s3); } +.rail [role=tab] { display: flex; justify-content: space-between; align-items: center; gap: var(--s2); padding: 7px 10px; + border-radius: 6px; color: var(--dim); text-decoration: none; border-left: 3px solid transparent; } +.rail [role=tab]:hover { color: var(--text); background: var(--card); } +.rail [role=tab][aria-selected=true] { color: var(--text); background: var(--card); border-left-color: var(--info); } +.rail [data-tab=progress], .rail [data-tab=leaves] { font-size: 14px; } +.rail [data-tab=leaves] { margin-top: var(--s3); } +.count { font-size: 12px; font-weight: 700; min-width: 22px; text-align: center; padding: 1px 6px; border-radius: 10px; + color: var(--bg); background: var(--attn); } +#content { min-width: 0; } + +.view-head { margin: var(--s3) 0 var(--s4); } +.question { font-family: var(--serif); font-size: 24px; font-weight: 500; line-height: 1.25; } +.view-head p { margin: 4px 0 0; max-width: 75ch; } +.cards { display: grid; gap: var(--s4); } + +/* --- a card --- */ +.card { background: var(--card); border: 1px solid var(--rule); border-radius: var(--r-card); padding: var(--s4) var(--s5); } +.card > h2 { font-size: 15px; margin-bottom: var(--s2); } +.result { position: relative; scroll-margin-top: 80px; } +.result.attn { border-left: 4px solid var(--attn); padding-left: calc(var(--s5) - 3px); } +.result.g-waiting { border-style: dashed; background: transparent; } +.result.g-notrun, .result.g-empty { background: repeating-linear-gradient(45deg, transparent 0 9px, color-mix(in srgb, var(--rule) 35%, transparent) 9px 10px); } +.result.target { outline: 2px solid var(--focus); outline-offset: 3px; } +.card-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--s3); flex-wrap: wrap; margin-bottom: var(--s2); } +.card-head h3 { font-size: 17px; } +.card-head h3 a { color: var(--text); text-decoration: none; } +.card-head h3 a:hover { text-decoration: underline; } +.chips { display: flex; gap: 6px; flex-wrap: wrap; } +.chip { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600; padding: 2px 8px; border-radius: var(--r-chip); + border: 1px solid var(--rule); color: var(--dim); white-space: nowrap; } +.chip .sym { font-size: 10px; } +.chip-attn { color: var(--attn); border-color: var(--attn); } +.chip-info { color: var(--dim); } +.chip-recheck { color: var(--low); border-color: var(--low); } +.state-text { color: var(--dim); margin: 0 0 var(--s2); } +.headline { margin: 0 0 var(--s3); max-width: 75ch; } +.note-stale { color: var(--attn); margin: 0 0 var(--s2); } +.empty { color: var(--dim); max-width: 75ch; } +.facts { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--s2) var(--s5); margin: 0 0 var(--s3); } +.fact { min-width: 0; } +.fact dt { color: var(--dim); font-size: 13px; } +.fact dd { margin: 0; overflow-wrap: anywhere; } +.lines { margin: 0 0 var(--s3); padding-left: 1.1em; max-width: 75ch; color: var(--text); } +.lines li { margin: 2px 0; } +.table-wrap { overflow-x: auto; margin: 0 0 var(--s3); } +table.rows { border-collapse: collapse; width: 100%; font-size: 14px; } +table.rows th { text-align: left; color: var(--dim); font-weight: 600; font-size: 13px; padding: 6px 10px; border-bottom: 1px solid var(--rule); } +table.rows td { padding: 6px 10px; vertical-align: top; overflow-wrap: anywhere; } +table.rows tbody tr:nth-child(even) td { background: color-mix(in srgb, var(--rule) 30%, transparent); } +.more { margin-top: 6px; font-size: 13px; } +.caveat { border-top: 1px solid var(--rule); margin-top: var(--s3); padding-top: var(--s3); font-size: 13px; line-height: 1.55; color: var(--dim); } +.caveat p { margin: 0 0 6px; max-width: 75ch; } +.caveat-lead::before { content: "\24d8"; margin-right: 6px; color: var(--info); } +.caveat summary { cursor: pointer; color: var(--info); } +.card-links { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4); margin: var(--s3) 0 0; font-size: 14px; } +.behind { margin: 0 0 var(--s3); } +.behind summary { cursor: pointer; color: var(--attn); } +.explore { font-size: 14px; color: var(--dim); } +img.plot { max-width: 100%; height: auto; background: #fff; border-radius: 4px; margin-bottom: var(--s3); } + +/* charts (plugins/charts.js) inside a card */ +.chart-slot:empty { display: none; } +figure.chart { margin: 0 0 var(--s3); } +figure.chart h3 { font-size: 14px; color: var(--dim); margin-bottom: 4px; } +figure.chart > .caveat { border: 0; padding: 0; margin: 0 0 6px; } +figure.chart svg { width: 100%; height: auto; max-width: 100%; } +.chart-detail { background: var(--card2); border: 1px solid var(--rule); border-radius: 6px; padding: var(--s3); margin: 0 0 var(--s3); } +.chart-detail h3 { font-size: 15px; } + +/* Look first */ +.look-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s3); counter-reset: none; } +.look-list li { background: var(--card); border: 1px solid var(--rule); border-left: 4px solid var(--attn); border-radius: var(--r-card); + padding: var(--s3) var(--s4); } +.look-list a { display: flex; gap: var(--s3); align-items: baseline; text-decoration: none; } +.look-ch { color: var(--dim); font-size: 13px; min-width: 76px; } +.look-title { font-weight: 600; font-size: 16px; } +.look-head { margin: 4px 0 0; color: var(--dim); max-width: 75ch; } +.look-empty { background: var(--card); border: 1px solid var(--rule); border-radius: var(--r-card); padding: var(--s4); } + +/* --- the Progress tab: the step map (live.js) --- */ +.progress-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: var(--s4); padding-top: var(--s3); align-items: start; } +.map-wrap { overflow: auto; min-width: 0; background: var(--card); border: 1px solid var(--rule); border-radius: var(--r-card); padding: var(--s3); } +.side { display: flex; flex-direction: column; gap: var(--s3); } +.legend { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 8px; color: var(--dim); font-size: 12px; } +.legend .swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px; margin-right: 4px; vertical-align: -2px; border: 1px solid var(--rule); } +.edges line { stroke: var(--rule); stroke-width: 1.2; } .box { cursor: pointer; } -.box rect { stroke: var(--line); stroke-width: 1; } -.box text { fill: var(--fg); font-size: 12px; pointer-events: none; } -.box.selected rect { stroke: var(--accent); stroke-width: 2; } +.box rect { stroke: var(--rule); stroke-width: 1; } +.box text { fill: var(--text); font-size: 12px; pointer-events: none; } +.box.selected rect { stroke: var(--focus); stroke-width: 2; } .box:focus { outline: none; } -.box:focus-visible rect { stroke: var(--accent); stroke-width: 3; stroke-dasharray: 4 2; } -.found li[role=button] { cursor: pointer; } -.found li[role=button]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } -.s-waiting rect, .s-waiting .swatch { fill: var(--waiting); background: var(--waiting); } -.s-running rect, .s-running .swatch { fill: var(--running); background: var(--running); } -.s-done rect, .s-done .swatch { fill: var(--done); background: var(--done); } -.s-failed rect, .s-failed .swatch { fill: var(--failed); background: var(--failed); } -.s-skipped rect, .s-skipped .swatch { fill: var(--skipped); background: var(--skipped); } -.s-notfinished rect, .s-notfinished .swatch { fill: var(--notfinished); background: var(--notfinished); } -.s-notplanned rect, .s-notplanned .swatch { fill: var(--notplanned); background: var(--notplanned); } -.s-skipped text, .s-notplanned text { fill: var(--muted); } +.box:focus-visible rect { stroke: var(--focus); stroke-width: 3; stroke-dasharray: 4 2; } +.s-waiting rect, .s-waiting .swatch { fill: var(--card2); background: var(--card2); } +.s-running rect, .s-running .swatch { fill: #2a4466; background: #2a4466; } +.s-done rect, .s-done .swatch { fill: #33465f; background: #33465f; } +.s-failed rect, .s-failed .swatch { fill: var(--fail); background: var(--fail); } +.s-skipped rect, .s-skipped .swatch { fill: #2b2838; background: #2b2838; } +.s-notfinished rect, .s-notfinished .swatch { fill: #4a3c22; background: #4a3c22; } +.s-notplanned rect, .s-notplanned .swatch { fill: var(--bg); background: var(--bg); } +.s-skipped text, .s-notplanned text { fill: var(--dim); } .s-running rect { animation: pulse 2s ease-in-out infinite; } @keyframes pulse { 50% { opacity: .7; } } -dl { display: grid; grid-template-columns: max-content 1fr; gap: 2px 10px; margin: 0 0 8px; } -dt { color: var(--muted); } -dd { margin: 0; } +.recheck-mark { fill: var(--low); } +dl { margin: 0 0 8px; } +#detail dl { display: grid; grid-template-columns: max-content 1fr; gap: 2px 10px; } +#detail dt { color: var(--dim); } +#detail dd { margin: 0; } table.tasks { border-collapse: collapse; width: 100%; font-size: 12px; margin-bottom: 8px; } -table.tasks th, table.tasks td { border-bottom: 1px solid var(--line); padding: 2px 4px; text-align: right; } +table.tasks th, table.tasks td { border-bottom: 1px solid var(--rule); padding: 2px 4px; text-align: right; } table.tasks th:first-child, table.tasks td:first-child { text-align: left; } .found { list-style: none; margin: 0; padding: 0; } -.found li { padding: 6px 0; border-bottom: 1px solid var(--line); cursor: pointer; } -.found .step { display: inline-block; min-width: 30px; color: var(--muted); } -.tone-attention { border-left: 3px solid var(--attention); padding-left: 6px; } -.tone-info { border-left: 3px solid var(--line); padding-left: 6px; } -.headline { margin: 6px 0; } -.replay { display: flex; gap: 10px; align-items: center; padding: 8px 18px; border-bottom: 1px solid var(--line); } -.replay[hidden] { display: none; } -.replay input[type=range] { flex: 1; } -button { background: var(--waiting); color: var(--fg); border: 1px solid var(--line); border-radius: 6px; padding: 5px 12px; cursor: pointer; } +.found li { padding: 6px 0; border-bottom: 1px solid var(--rule); cursor: pointer; } +.found li[role=button]:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } +.found .step { display: inline-block; min-width: 30px; color: var(--dim); } +.tone-attention { border-left: 3px solid var(--attn); padding-left: 6px; } +.tone-info { border-left: 3px solid var(--rule); padding-left: 6px; } +.plugin ul.plain { padding-left: 1.1em; } +.replay { display: flex; gap: 10px; align-items: center; padding: 8px var(--s5); border-bottom: 1px solid var(--rule); max-width: 1120px; margin: 0 auto; } +.replay input[type=range] { flex: 1; accent-color: var(--info); } +#replay-time { color: var(--dim); font-size: 13px; } +.family-table { border-collapse: collapse; width: 100%; font-size: 13px; } +.family-table td, .family-table th { border-bottom: 1px solid var(--rule); padding: 3px 6px; text-align: left; } + +/* --- home: people by family --- */ +.home, .family { max-width: 1120px; margin: 0 auto; padding: var(--s5) var(--s5) var(--s7); } +.family-block { margin-bottom: var(--s6); } +.family-head { position: sticky; top: 52px; z-index: 5; display: flex; align-items: baseline; gap: var(--s3); flex-wrap: wrap; + padding: var(--s2) 0; background: var(--bg); border-bottom: 1px solid var(--rule); margin-bottom: var(--s3); } +.family-head h2 { font-family: var(--serif); font-size: 24px; font-weight: 500; } +.family-head h2 a { color: var(--text); text-decoration: none; } +.family-head .open { margin-left: auto; font-size: 14px; } +.people { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: var(--s3); } +.person-card .person-link { display: flex; gap: var(--s3); align-items: flex-start; height: 100%; padding: var(--s3) var(--s4); + background: var(--card); border: 1px solid var(--rule); border-radius: var(--r-card); color: var(--text); text-decoration: none; } +.person-card:not(.has-run) .person-link { background: transparent; border-style: dashed; color: var(--dim); } +.person-card a.person-link:hover { border-color: var(--dim); } +.person-card.running .person-link { border-color: color-mix(in srgb, var(--info) 60%, var(--rule)); } +.person-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; } +.person-name { font-weight: 600; font-size: 16px; overflow-wrap: anywhere; } +.person-place { color: var(--dim); font-size: 13px; } +.person-state { font-size: 13px; color: var(--dim); } +.person-look { font-size: 13px; color: var(--dim); } +.person-look.attn { color: var(--attn); font-weight: 600; } +.person-look.attn::before { content: "\25b2 "; font-size: 10px; } +.bar { display: block; height: 5px; border-radius: 3px; background: var(--rule); margin: 6px 0 2px; overflow: hidden; } +.bar-fill { --p: 0; display: block; height: 100%; width: calc(var(--p) * 100%); background: var(--info); } ul.runs { list-style: none; padding: 0; } -ul.runs li { padding: 6px 0; border-bottom: 1px solid var(--line); } -.kind { color: var(--muted); font-size: 12px; margin-left: 6px; } -body.login main { justify-content: center; padding-top: 12vh; } +ul.runs li { padding: 6px 0; border-bottom: 1px solid var(--rule); } +.kind { color: var(--dim); font-size: 12px; margin-left: 6px; } + +/* --- a family --- */ +.family-page { display: grid; gap: var(--s4); } +.family-top { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: var(--s4); align-items: start; } +.family-grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: var(--s4); align-items: start; } +.pedigree-figure { margin: 0; } +.pedigree-svg { width: 100%; height: auto; max-height: 460px; display: block; } +.ped-lines line { stroke: var(--dim); stroke-width: 2; } +.ped-shape { fill: var(--card); stroke: var(--text); stroke-width: 2; } +.ped-shape.filled { fill: var(--text); } +.ped-person.link .ped-shape { stroke: var(--info); } +.ped-person.link:hover .ped-shape, .ped-person.link:focus .ped-shape { stroke-width: 4; } +.ped-name { fill: var(--text); font: 13px var(--sans); } +.ped-person.link .ped-name { fill: var(--info); text-decoration: underline; } +.ped-flag { fill: var(--attn); font: 700 12px var(--sans); } +.pedigree-figure figcaption { font-size: 13px; margin-top: var(--s2); } +.pedigree-image summary { cursor: pointer; color: var(--info); font-size: 13px; } +img.pedigree, img.segments { max-width: 100%; height: auto; display: block; margin-top: var(--s2); border-radius: 4px; } +.members .people { grid-template-columns: 1fr; } +.analyses { margin: var(--s3) 0 0; color: var(--dim); font-size: 14px; } +.analyses .label { color: var(--text); } +.plan-item { color: var(--text); } +.plan-item.d-skipped, .plan-item.d-waiting { color: var(--dim); } +.couple-line { max-width: 75ch; } +.couples .caveat-lead { font-size: 13px; color: var(--dim); max-width: 75ch; } +details[data-child] { border-top: 1px solid var(--rule); padding: var(--s2) 0; } +details[data-child] summary { cursor: pointer; } + +/* --- login --- */ +body.login main { display: flex; justify-content: center; padding-top: 12vh; } body.login form { width: 320px; display: flex; flex-direction: column; gap: 8px; } -body.login h1 { font-size: 18px; margin: 0 0 4px; } -body.login .msg { color: var(--attention); min-height: 1em; margin: 0; } -input[type=password] { background: var(--bg); color: var(--fg); border: 1px solid var(--line); border-radius: 6px; padding: 7px; } -@media (max-width: 900px) { main { flex-direction: column; } aside { flex: 1 1 auto; width: 100%; } } +body.login h1 { font-family: var(--serif); font-size: 24px; font-weight: 500; margin: 0 0 4px; } +body.login .msg { color: var(--attn); min-height: 1em; margin: 0; } +input[type=password] { font: inherit; background: var(--card); color: var(--text); border: 1px solid var(--rule); border-radius: 6px; padding: 8px; } + +/* --- narrow screens: one column, the rail becomes a row of tabs that scrolls sideways --- */ +@media (max-width: 900px) { + .layout { grid-template-columns: minmax(0, 1fr); padding: 0 var(--s3) var(--s6); gap: var(--s2); } + .rail { position: sticky; top: 52px; z-index: 10; flex-direction: row; overflow-x: auto; gap: 4px; padding: var(--s2) 0; + background: var(--bg); scrollbar-width: none; } + .rail [role=tab] { flex: none; border-left: 0; border-bottom: 3px solid transparent; border-radius: 6px 6px 0 0; padding: 6px 10px; } + .rail [role=tab][aria-selected=true] { border-bottom-color: var(--info); } + .rail [data-tab=leaves] { margin-top: 0; } + .progress-grid, .family-top, .family-grid { grid-template-columns: minmax(0, 1fr); } + .person-head { padding: var(--s4) var(--s3) var(--s2); } + .person-head h1 { font-size: 24px; } + .top { padding: 8px var(--s3); gap: var(--s3); } + .links #home-link { display: none; } + .home, .family { padding: var(--s4) var(--s3) var(--s6); } + .card { padding: var(--s3) var(--s4); } + .result.attn { padding-left: calc(var(--s4) - 3px); } +} +@media (max-width: 600px) { + .facts { grid-template-columns: minmax(0, 1fr); } + .search-slot { width: auto; flex: 1; min-width: 0; } + .crumbs { display: none; } + table.rows thead { display: none; } + table.rows, table.rows tbody, table.rows tr, table.rows td { display: block; width: 100%; } + table.rows tr { padding: 6px 0; border-bottom: 1px solid var(--rule); } + table.rows tbody tr:nth-child(even) td { background: none; } + table.rows td { padding: 1px 0; } + table.rows td::before { content: attr(data-label) ": "; color: var(--dim); font-size: 13px; } + .look-list a { flex-direction: column; gap: 0; } + .people { grid-template-columns: minmax(0, 1fr); } +} +@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; scroll-behavior: auto !important; } } diff --git a/assets/live/live.js b/assets/live/live.js index 29c3332d..efadf6fd 100644 --- a/assets/live/live.js +++ b/assets/live/live.js @@ -1,7 +1,10 @@ -// live.js: the live page of one run (assets/live/index.html, served by bin/owngenome_live.py). -// One data source feeds the page: in live mode state.json plus the run's event stream; with -// a frames.json of recorded states played on a timer. No outside -// request, no inline script, no eval. Plugins call window.PGP.register({name, onState, onEvent, panel}). +// live.js: the run's progress on a person's page (assets/live/index.html, served by bin/owngenome_live.py): +// the step map, the step detail, "Found so far", the event stream and the replay, shown in the +// Progress tab. One data source feeds the page: in live mode state.json plus the run's event stream; +// with a frames.json of recorded states played on a timer. person.js draws +// the results from the same states. No outside request, no inline script, no eval. Plugins call +// window.PGP.register({name, onState, onEvent, panel, slot}); a panel goes into the element whose id +// is slot when the page has one (the search box in the header), else into the Progress tab. (function () { 'use strict'; var root = document.documentElement; @@ -278,7 +281,8 @@ plugins.push(p); if (p.panel) { var box = el('section', { 'class': 'card plugin', 'data-plugin': p.name || '' }); - document.getElementById('plugin-panels').appendChild(box); + var slot = (p.slot && document.getElementById(p.slot)) || document.getElementById('plugin-panels'); + slot.appendChild(box); try { p.panel(box); } catch (e) { console.error(p.name, e); } } if (last && p.onState) { p.onState(last); } diff --git a/assets/live/person.js b/assets/live/person.js new file mode 100644 index 00000000..36d89186 --- /dev/null +++ b/assets/live/person.js @@ -0,0 +1,349 @@ +// person.js: a person's results on their page (assets/live/index.html), from results.json +// (bin/live_results.py, served by bin/owngenome_live_results.py) and the run's states (live.js). +// The rail holds the report's seven chapter questions, "Look first" (every card worth a look) and +// "What this leaves out"; Progress is live.js's step map. The tab lives in the URL fragment only +// (#q/, #s/
    , #progress), which the browser never sends, so no tab or search reaches +// a server log. A card shows its results once one of its steps is done in the current state, so live +// and replay draw the same way and a replay reveals the results as the run goes. +(function () { + 'use strict'; + if (!window.PGP || !window.PGPUI) { return; } + var U = window.PGPUI, el = U.el, add = U.add; + var root = document.documentElement; + var replay = root.getAttribute('data-mode') === 'replay'; + var results = null, state = null, fetchedFor = null, current = null, chosen = false, drawnKey = null; + var rail = document.getElementById('rail'); + var panel = document.getElementById('results'); + var progress = document.getElementById('progress'); + var SHORT = { quality: 'Trust', clinical: 'Doctor', carrier: 'Child', medicines: 'Medicines', ancestry: 'Ancestors', + traits: 'Traits', other: 'Other' }; + var LOOK_EMPTY = 'Nothing on this list. That is information, not a clean bill: each chapter’s caveats and ' + + '“What this leaves out” still apply.'; + + function stepsBy() { + var by = {}; + ((state && state.steps) || []).forEach(function (s) { by[s.step] = s; }); + return by; + } + function doneSet() { + return ((state && state.steps) || []).filter(function (s) { return s.state === 'done'; }) + .map(function (s) { return s.step; }).join(','); + } + function anyDone() { return doneSet() !== ''; } + // What a card can show at this moment: 'show', 'waiting' (a step still to come), 'notrun' (none planned) + // or 'empty' (its steps ended and wrote nothing for it). + function gate(c) { + var by = stepsBy(), steps = c.steps || []; + var known = steps.filter(function (s) { return by[s]; }); + if (known.some(function (s) { return by[s].state === 'done'; })) { return c.state === 'missing' ? 'empty' : 'show'; } + if (known.some(function (s) { return ['waiting', 'running'].indexOf(by[s].state) >= 0; })) { return 'waiting'; } + if (known.some(function (s) { return by[s].state === 'failed'; })) { return 'failed'; } + if (!known.length && c.state !== 'missing' && !replay) { return 'show'; } + return 'notrun'; + } + function waitingText(c) { + var by = stepsBy(); + var s = (c.steps || []).filter(function (x) { return by[x] && by[x].state !== 'done'; })[0] || (c.steps || [])[0]; + return 'Waiting for step ' + s + (by[s] ? ' (' + by[s].label + ')' : '') + '.'; + } + function cards() { + var out = []; + ((results && results.chapters) || []).forEach(function (ch) { + ch.sections.forEach(function (c) { out.push({ ch: ch, c: c }); }); + }); + return out; + } + function attentionNow(c) { return c.attention && gate(c) === 'show'; } + + // --- the route -------------------------------------------------------------------------------- + function parse() { + var h = decodeURIComponent((window.location.hash || '').slice(1)); + if (h === 'progress') { return { tab: 'progress' }; } + if (h.indexOf('q/') === 0) { return { tab: h.slice(2) }; } + if (h.indexOf('s/') === 0) { return { tab: 'section', id: h.slice(2) }; } + if (h && cards().some(function (x) { return x.c.id === h; })) { return { tab: 'section', id: h }; } // an old #clinvar link + return null; + } + function chapterOf(id) { + var hit = cards().filter(function (x) { return x.c.id === id; })[0]; + return hit ? hit.ch.key : null; + } + function defaultTab() { return results && results.chapters && results.chapters.length && anyDone() ? 'look' : 'progress'; } + + function route(focus) { + var r = parse() || { tab: defaultTab() }; + var tab = r.tab === 'section' ? (chapterOf(r.id) || 'look') : r.tab; + var valid = ['progress', 'look', 'leaves'].concat(((results && results.chapters) || []).map(function (c) { return c.key; })); + if (valid.indexOf(tab) < 0) { tab = defaultTab(); } + show(tab); + Array.prototype.forEach.call(rail.querySelectorAll('[role=tab]'), function (t) { + var on = t.getAttribute('data-tab') === tab; + t.setAttribute('aria-selected', on ? 'true' : 'false'); + t.tabIndex = on ? 0 : -1; + }); + if (r.tab === 'section') { + var card = document.getElementById('card-' + r.id); + if (card) { + var d = card.querySelector('details.caveat-full'); + if (d && focus !== false) { card.classList.add('target'); } + card.scrollIntoView({ block: 'start' }); + card.focus({ preventScroll: true }); + } + } + } + function go(hash) { + if (window.location.hash === hash) { route(); } else { window.location.hash = hash; } + } + window.addEventListener('hashchange', function () { chosen = true; route(); }); + + // --- the rail --------------------------------------------------------------------------------- + function drawRail() { + rail.textContent = ''; + function tab(key, label, n, hash) { + var a = el('a', { role: 'tab', href: hash, id: 'tab-' + key, 'data-tab': key, 'aria-controls': key === 'progress' ? 'progress' : 'results' }); + add(a, el('span', { 'class': 'tab-label' }, label)); + if (n) { add(a, el('span', { 'class': 'count', 'aria-label': n + ' worth a look' }, String(n))); } + add(rail, a); + return a; + } + if (results && results.chapters && results.chapters.length) { + var all = cards().filter(function (x) { return attentionNow(x.c); }).length; + tab('look', 'Look first', all, '#q/look'); + results.chapters.forEach(function (ch) { + var n = ch.sections.filter(attentionNow).length; + var t = tab(ch.key, SHORT[ch.key] || ch.title, n, '#q/' + ch.key); + t.setAttribute('title', ch.question); + }); + tab('leaves', 'Leaves out', 0, '#q/leaves'); + } + tab('progress', 'Progress', 0, '#progress'); + } + U.tablist(rail, function (t) { go(t.getAttribute('href')); }); + + // --- a card ----------------------------------------------------------------------------------- + function badge(text, cls, symbol) { + var b = el('span', { 'class': 'chip ' + cls }); + if (symbol) { add(b, el('span', { 'class': 'sym', 'aria-hidden': 'true' }, symbol)); } + add(b, text); + return b; + } + function recheckBadges(c) { + var r = state && state.plugins && state.plugins.recheck; + var out = []; + (c.steps || []).forEach(function (s) { + ((r && r.steps && r.steps[s]) || []).forEach(function (t) { out.push(badge(t, 'chip-recheck', '↻')); }); + }); + return out; + } + function card(c, ch) { + var g = gate(c); + var cls = 'card result g-' + g + (attentionNow(c) ? ' attn' : ''); + var art = el('article', { 'class': cls, id: 'card-' + c.id, 'data-section': c.id, 'data-step': (c.steps || []).join(' '), + 'data-state': c.state, tabindex: '-1', 'aria-labelledby': 'h-' + c.id }); + var head = el('header', { 'class': 'card-head' }); + var h = el('h3', { id: 'h-' + c.id }); + add(h, el('a', { href: '#s/' + c.id, 'class': 'self' }, c.title)); + add(head, h); + var chips = el('div', { 'class': 'chips' }); + if (g === 'show') { + add(chips, attentionNow(c) ? badge('worth a look', 'chip-attn', '▲') : badge('for information', 'chip-info', '○')); + } + recheckBadges(c).forEach(function (b) { add(chips, b); }); + add(head, chips); + add(art, head); + if (g === 'waiting') { + add(art, el('p', { 'class': 'state-text' }, waitingText(c))); + } else if (g === 'notrun') { + add(art, el('p', { 'class': 'state-text' }, 'Not run in this run (step ' + (c.steps || []).join(', ') + ').')); + } else if (g === 'failed') { + add(art, el('p', { 'class': 'state-text' }, 'Step ' + (c.steps || []).join(', ') + ' failed: the Progress tab says why.')); + } else if (g === 'empty') { + add(art, el('p', { 'class': 'state-text' }, 'Its step ended without a result for this section.')); + } else { + (c.notes || []).forEach(function (n) { add(art, el('p', { 'class': 'note-stale' }, n)); }); + if (c.headline) { add(art, el('p', { 'class': 'headline' }, c.headline)); } + add(art, U.facts(c.facts)); + Object.keys((results && results.charts) || {}).forEach(function (chart) { + if (results.charts[chart] === c.id) { add(art, el('div', { 'class': 'chart-slot', 'data-chart-slot': chart })); } + }); + if (c.image) { add(art, el('img', { 'class': 'plot', src: c.image.src, alt: c.image.alt })); } + var tb = U.table(c.table); + if (tb) { add(art, tb); } else if (c.empty_text) { add(art, el('p', { 'class': 'empty' }, c.empty_text)); } + if (c.lines && c.lines.length) { + var ul = el('ul', { 'class': 'lines' }); + c.lines.forEach(function (l) { add(ul, el('li', {}, l)); }); + add(art, ul); + } + if (c.hidden) { + var d = el('details', { 'class': 'behind' }); + add(d, el('summary', {}, c.hidden.summary), el('p', {}, c.hidden.text)); + add(art, d); + } + if (c.explore && c.explore.length) { + var p = el('p', { 'class': 'explore' }, 'Explore the branch (opens a website; the address carries the haplogroup): '); + c.explore.forEach(function (x, i) { + if (i) { add(p, ', '); } + add(p, el('a', { href: x.url, rel: 'noreferrer noopener', target: '_blank' }, x.name)); + }); + add(art, p); + } + } + add(art, U.caveat(c.caveat)); + var foot = el('p', { 'class': 'card-links' }); + add(foot, el('a', { href: 'report.html#' + encodeURIComponent(c.anchor), 'class': 'report' }, 'Open in the report')); + if (g === 'show' && c.viewer && c.viewer.length) { + add(foot, el('a', { href: 'viewer/#' + c.viewer[0], 'class': 'viewer' }, 'See the reads at ' + c.viewer[0])); + } + (g === 'show' ? c.links || [] : []).forEach(function (f) { + add(foot, el('a', { href: f.split('/').map(encodeURIComponent).join('/') }, 'Open ' + f)); + }); + add(art, foot); + return art; + } + + // --- the views -------------------------------------------------------------------------------- + function heading(text, sub) { + var h = el('header', { 'class': 'view-head' }); + add(h, el('h2', { 'class': 'question' }, text)); + if (sub) { add(h, el('p', { 'class': 'dim' }, sub)); } + return h; + } + function lookFirst() { + var box = el('div', { id: 'look-first', 'class': 'view' }); + add(box, heading('Look at these first', 'The cards the report itself flags, in chapter order. A card not listed is for information; its caveats still apply.')); + var items = cards().filter(function (x) { return attentionNow(x.c); }); + if (!items.length) { + add(box, el('p', { 'class': 'empty look-empty' }, LOOK_EMPTY)); + } else { + var ol = el('ol', { 'class': 'look-list' }); + items.forEach(function (x) { + var li = el('li', {}); + var a = el('a', { href: '#s/' + x.c.id }); + add(a, el('span', { 'class': 'look-ch' }, SHORT[x.ch.key] || x.ch.title), el('span', { 'class': 'look-title' }, x.c.title)); + add(li, a); + if (x.c.headline) { add(li, el('p', { 'class': 'look-head' }, x.c.headline)); } + add(ol, li); + }); + add(box, ol); + } + var waiting = cards().filter(function (x) { return gate(x.c) === 'waiting'; }).length; + if (waiting) { add(box, el('p', { 'class': 'dim' }, waiting + ' cards are still waiting for their step.')); } + return box; + } + function chapter(ch) { + var box = el('div', { 'class': 'view chapter', 'data-chapter': ch.key }); + add(box, heading(ch.question, ch.title)); + var grid = el('div', { 'class': 'cards' }); + ch.sections.forEach(function (c) { add(grid, card(c, ch)); }); + add(box, grid); + return box; + } + function leaves() { + var box = el('div', { 'class': 'view' }); + add(box, heading('What this leaves out', 'What no step looks at, and the steps that did not run.')); + function list(title, items) { + var s = el('section', { 'class': 'card' }); + add(s, el('h3', {}, title)); + var ul = el('ul', { 'class': 'lines' }); + (items.length ? items : ['None.']).forEach(function (t) { add(ul, el('li', {}, t)); }); + add(s, ul); + add(box, s); + } + list('No step looks at these', (results && results.not_assessed) || []); + list('Steps that did not run', (results && results.not_run) || []); + return box; + } + function show(tab) { + current = tab; + var isProgress = tab === 'progress'; + progress.hidden = !isProgress; + panel.hidden = isProgress; + root.setAttribute('data-pgp-tab', tab); + if (isProgress) { return; } + panel.textContent = ''; + if (results && results.note && !(results.chapters || []).length) { + add(panel, el('p', { 'class': 'empty' }, results.note)); + return; + } + if (tab === 'look') { add(panel, lookFirst()); } else if (tab === 'leaves') { add(panel, leaves()); } else { + var ch = ((results && results.chapters) || []).filter(function (c) { return c.key === tab; })[0]; + if (ch) { add(panel, chapter(ch)); } + } + root.setAttribute('data-pgp-results', String(panel.querySelectorAll('article.result').length)); + document.dispatchEvent(new CustomEvent('pgp:cards')); + } + + // --- the header ------------------------------------------------------------------------------- + function header() { + var p = (results && results.person) || {}; + var slot = document.getElementById('avatar-slot'); + var planned = ((state && state.steps) || []).filter(function (s) { return s.state !== 'not planned'; }); + var done = planned.filter(function (s) { return s.state === 'done'; }).length; + slot.textContent = ''; + add(slot, U.avatar(p.inferred_sex || (state && state.run && state.run.declared_sex), true, planned.length ? done / planned.length : 0)); + var line = document.getElementById('person-line'); + line.textContent = ''; + var bits = []; + if (p.inferred_sex) { bits.push('Sex from the reads: ' + p.inferred_sex + (p.declared_sex ? ', declared ' + p.declared_sex : '')); } + if (bits.length) { add(line, bits.join('. ') + '. '); } + var fam = state && state.plugins && state.plugins.family; + var crumbs = document.getElementById('crumbs'); + var name = document.getElementById('run-name'); + crumbs.textContent = ''; + ((fam && fam.kind === 'member' && fam.families) || []).forEach(function (f, i) { + if (i === 0) { add(line, 'In family '); } else { add(line, ', '); } + add(line, el('a', { href: f.url }, f.name)); + add(crumbs, el('a', { href: f.url }, f.name), el('span', { 'class': 'sep', 'aria-hidden': 'true' }, '/')); + }); + add(crumbs, name); + // The strip: one cell per planned step, filled when done, striped while it runs. + var strip = document.getElementById('strip'); + strip.textContent = ''; + planned.forEach(function (s) { + add(strip, el('span', { 'class': 'cell st-' + s.state.replace(/\s+/g, '-'), title: s.step + ' ' + s.label + ': ' + s.state })); + }); + var txt = document.getElementById('strip-text'); + var running = planned.filter(function (s) { return s.state === 'running'; }).length; + var failed = planned.filter(function (s) { return s.state === 'failed'; }).length; + var run = (state && state.run) || {}; + var t = done + ' of ' + planned.length + ' steps done'; + if (run.state === 'finished') { t = 'Finished' + (run.finished_utc ? ' ' + U.day(run.finished_utc) : '') + ': ' + t; } + else if (running) { t = 'Running: ' + t + ', ' + running + ' running now'; } + if (failed) { t += ', ' + failed + ' failed'; } + var rc = state && state.plugins && state.plugins.recheck; + if (rc && rc.checked_utc) { t += '. Databases re-checked ' + U.day(rc.checked_utc); } + txt.textContent = t + '.'; + } + + // --- data --------------------------------------------------------------------------------------- + function load() { + var key = replay ? 'replay' : doneSet(); + if (key === fetchedFor) { return; } + fetchedFor = key; + U.getJSON('results.json').then(function (r) { + if (key !== fetchedFor) { return; } + results = r && (r.chapters || r.note) ? r : results; + redraw(true); + }).catch(function () { if (key === fetchedFor) { fetchedFor = null; } }); + } + function redraw(force) { + var k = JSON.stringify([doneSet(), state && state.run && state.run.state, !!results, + state && state.plugins && state.plugins.recheck && state.plugins.recheck.mtime]); + if (!force && k === drawnKey) { return; } + drawnKey = k; + header(); + var keep = document.activeElement && rail.contains(document.activeElement) ? document.activeElement.getAttribute('data-tab') : null; + drawRail(); + if (!chosen && !parse()) { show(defaultTab()); route(false); } else { route(false); } + if (keep) { var t = rail.querySelector('[data-tab="' + keep + '"]'); if (t) { t.focus(); } } + } + + window.PGP.register({ + name: 'person', + onState: function (s) { + state = s; + if (!replay || !results) { load(); } + redraw(false); + } + }); +}()); diff --git a/assets/live/plugins/charts.js b/assets/live/plugins/charts.js index 0bc0181e..8fdebf9a 100644 --- a/assets/live/plugins/charts.js +++ b/assets/live/plugins/charts.js @@ -2,12 +2,13 @@ // When state.json says the run's _charts.json exists, it is fetched from the run // folder (same origin, nothing else) and each chart's SVG is shown with its caveat. A click on // a mark opens a panel with the mark's detail, the chart's caveat, a link to the reads in the -// genome viewer (viewer/#) and a link to the section of the report. No inline script, -// no style attribute, no eval: the SVG is parsed as XML and holds no script. +// genome viewer (viewer/#) and a link to the section of the report. On a person's page each +// chart goes into its card (a [data-chart-slot=] person.js draws), else into this plugin's +// panel. No inline script, no style attribute, no eval: the SVG is parsed as XML and holds no script. (function () { 'use strict'; if (!window.PGP) { return; } - var list = null, side = null, shownKey = null; + var list = null, side = null, shownKey = null, figs = []; function el(tag, attrs, text) { var e = document.createElement(tag); @@ -26,7 +27,8 @@ box.appendChild(list); } - function openMark(sp, m) { + function openMark(sp, m, fig) { + if (fig && fig.parentNode) { fig.parentNode.insertBefore(side, fig.nextSibling); } side.textContent = ''; side.hidden = false; side.setAttribute('data-mark', m.id); @@ -56,14 +58,26 @@ a.addEventListener('click', function (e) { e.preventDefault(); var m = byId[a.getAttribute('data-mark')]; - if (m) { openMark(sp, m); } + if (m) { openMark(sp, m, fig); } }); }); return fig; } + // Each figure into its card's slot when the page has one, else into the panel. + function place() { + figs.forEach(function (fig) { + var slot = document.querySelector('[data-chart-slot="' + fig.getAttribute('data-chart') + '"]'); + var to = slot || list; + if (fig.parentNode !== to) { to.appendChild(fig); } + }); + } + document.addEventListener('pgp:cards', function () { if (list) { place(); } }); + function clear() { shownKey = null; + figs.forEach(function (f) { if (f.parentNode) { f.parentNode.removeChild(f); } }); + figs = []; list.textContent = ''; list.appendChild(el('p', { 'class': 'muted' }, 'The charts appear when the report step has written them.')); side.hidden = true; @@ -73,8 +87,10 @@ function draw(data) { if (!data || !data.charts) { return; } + figs.forEach(function (f) { if (f.parentNode) { f.parentNode.removeChild(f); } }); list.textContent = ''; - data.charts.forEach(function (sp) { list.appendChild(chart(sp)); }); + figs = data.charts.map(chart); + place(); document.documentElement.setAttribute('data-pgp-charts', String(data.charts.length)); } diff --git a/assets/live/plugins/family.js b/assets/live/plugins/family.js index e9a936c7..8875f183 100644 --- a/assets/live/plugins/family.js +++ b/assets/live/plugins/family.js @@ -1,9 +1,10 @@ -// family.js: the family card of the live page (bin/owngenome_live_family.py fills plugins.family). -// On a family's page: the pedigree, each child's chromosome map, the relationship check, the -// shared segments and the couple carrier screen, as steps 38 and 39 wrote them. On a sample's -// page: a link to its family. Text only through textContent; images from this server only. +// family.js: the family card of a person's page (bin/owngenome_live_family.py fills plugins.family): +// a link to each family the person is in. A family's own page is family.html (family-page.js); this +// card still draws a family's state when a page with live.js gets one. Text only through +// textContent; images from this server only. (function () { 'use strict'; + if (!window.PGP) { return; } // family.html draws a family's own page (family-page.js) without live.js var box = null, shownKey = null; function el(tag, attrs, text) { diff --git a/assets/live/plugins/recheck.js b/assets/live/plugins/recheck.js index 1301e78a..19275e7e 100644 --- a/assets/live/plugins/recheck.js +++ b/assets/live/plugins/recheck.js @@ -1,7 +1,8 @@ // recheck.js: the database re-check on the live page (a plugin, bin/owngenome_live_recheck.py). // state.json's plugins.recheck holds a banner line and, per step, its badges ("newer ClinVar // available", "redone 2026-10-05"). The panel shows the banner and one line per marked step; on -// the step map a marked step gets a small dot with the badges as its tooltip. No inline script, +// the step map a marked step gets a small dot (coloured by live.css) with the badges as its +// tooltip. person.js puts the same badges in the header of each card of a marked step. No inline script, // no style attribute, no eval; nothing is fetched beyond state.json. (function () { 'use strict'; @@ -38,7 +39,6 @@ c.setAttribute('cx', String(x)); c.setAttribute('cy', String(y)); c.setAttribute('r', '4'); - c.setAttribute('fill', '#e0b000'); var t = document.createElementNS(NS, 'title'); t.textContent = badges.join('; '); c.appendChild(t); diff --git a/assets/live/plugins/search.js b/assets/live/plugins/search.js index 01e66372..3dca0ab6 100644 --- a/assets/live/plugins/search.js +++ b/assets/live/plugins/search.js @@ -6,6 +6,7 @@ // the report and, for a mention with a locus, to the reads in the genome viewer. The query rule // is bin/results_index.py's query(): an rsID, a gene symbol (a prefix also lists the terms that // start with it), or a position, which the index cannot answer (scripts/owngenome-lookup.sh can). +// The box sits in the page header; on a person's page a hit opens its card (#s/
    ). // tests/test_results_index.py runs match() under node against the Python rule. (function (root) { 'use strict'; @@ -48,13 +49,13 @@ box.appendChild(el('h2', {}, 'Search the results')); var form = el('form', { role: 'search' }); form.addEventListener('submit', function (e) { e.preventDefault(); show(); }); - input = el('input', { type: 'search', 'aria-label': 'Gene symbol or rsID', placeholder: 'BRCA2 or rs429358', + input = el('input', { type: 'search', 'aria-label': 'Gene symbol or rsID', placeholder: 'Search arrives with the report', autocomplete: 'off', spellcheck: 'false' }); input.disabled = true; input.addEventListener('input', show); form.appendChild(input); box.appendChild(form); - status = el('p', { 'class': 'muted' }, 'The search opens when the report step has written its index.'); + status = el('p', { 'class': 'muted search-status' }, 'The search opens when the report step has written its index.'); box.appendChild(status); out = el('div', { 'class': 'search-results', 'aria-live': 'polite' }); box.appendChild(out); @@ -63,7 +64,8 @@ function item(it) { var li = el('li', { 'data-section': it.section }); var head = el('p', {}); - head.appendChild(el('a', { href: 'report.html#' + (it.anchor || ''), 'class': 'report' }, it.title)); + var card = document.getElementById('results') ? '#s/' + (it.anchor || it.section) : 'report.html#' + (it.anchor || ''); + head.appendChild(el('a', { href: card, 'class': 'report' }, it.title)); head.appendChild(document.createTextNode(' (step ' + it.step + ')' + (it.state === 'stale' ? ', stale: from an earlier run' : '') + ': ' + it.count + (it.count === 1 ? ' mention' : ' mentions'))); li.appendChild(head); @@ -124,7 +126,9 @@ if (!out || !c) { return; } if (!c.file) { // The file went away (step 24 deletes it before it writes the report again). - if (shownKey !== null) { shownKey = null; index = null; input.disabled = true; show(); } + if (shownKey !== null) { + shownKey = null; index = null; input.disabled = true; input.placeholder = 'Search arrives with the report'; show(); + } return; } var key = c.file + '@' + c.mtime; @@ -141,6 +145,7 @@ if (key !== shownKey || !data || !data.terms) { return; } index = data; input.disabled = false; + input.placeholder = 'Gene or rsID, like BRCA2'; status.textContent = Object.keys(data.terms).length + ' genes and rsIDs in these results. Typing searches them here, ' + 'in this browser.'; show(); @@ -148,5 +153,5 @@ .catch(function () { if (key === shownKey) { shownKey = null; } }); } - root.PGP.register({ name: 'search', panel: panel, onState: onState }); + root.PGP.register({ name: 'search', slot: 'search-slot', panel: panel, onState: onState }); }(typeof window !== 'undefined' ? window : null)); diff --git a/bin/owngenome_live_family.py b/bin/owngenome_live_family.py index 5cff7157..9ba0a2da 100644 --- a/bin/owngenome_live_family.py +++ b/bin/owngenome_live_family.py @@ -179,7 +179,9 @@ def layout_of(order, members): def family_page(name, d): """family.json without the member runs: the state, the layout, the full pair rows and couple rows.""" st = family_state(name, d) - out = {"state": st, "layout": None, "relationships": [], "ibd_pairs": [], "couples": {}} + import render_report # the couple screen's caveat, word for word + out = {"state": st, "layout": None, "relationships": [], "ibd_pairs": [], "couples": {}, + "couple_note": render_report.COUPLE_NOTE} if st.get("error"): return out order, members = members_of(d) From 4cfc3c9df8b51911cce2426adba9a0480ed24d45 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sat, 7 Nov 2026 20:54:07 +0100 Subject: [PATCH 08/14] test(live): build the new pages into the demo and check results.json, family.json and the wording The static demo now has the home page, results.json and family.json. A new test checks every section's caveat, the cards against the report's chapters, the words a page may use, ClinVar with and without hits, and that results.json stays behind the login. The demo test drives the person page, the family page and a phone-width view. Bead: pgp-5bja --- assets/live/family-page.js | 4 +- assets/live/live.css | 15 +- assets/live/person.js | 4 +- assets/live/plugins/ops.js | 1 + assets/live/plugins/search.js | 3 +- bin/live_results.py | 8 +- bin/owngenome_live.py | 3 +- bin/owngenome_live_family.py | 2 +- tests/demo/build_live_demo.py | 79 +++++++-- tests/test_live_demo.py | 109 +++++++++++-- tests/test_live_results.py | 293 ++++++++++++++++++++++++++++++++++ 11 files changed, 483 insertions(+), 38 deletions(-) create mode 100644 tests/test_live_results.py diff --git a/assets/live/family-page.js b/assets/live/family-page.js index 25bab522..cff076e3 100644 --- a/assets/live/family-page.js +++ b/assets/live/family-page.js @@ -28,6 +28,7 @@ var lines = U.svg('g', { 'class': 'ped-lines' }); L.lines.forEach(function (l) { lines.appendChild(U.svg('line', { x1: l[0], y1: l[1], x2: l[2], y2: l[3] })); }); s.appendChild(lines); + s.style.maxWidth = Math.round(L.width * 1.1) + 'px'; // a two-person pedigree stays its own size var h = L.size / 2; L.people.forEach(function (p) { var url = memberUrl(f, p.sample); @@ -204,7 +205,8 @@ add(page, top); var grid = el('div', { 'class': 'family-grid' }); add(grid, couples(f), related(f)); - add(page, grid, children(f), more(f)); + add(page, grid, children(f)); + if ((f.state.plan || []).length || f.state.reports.length) { add(page, more(f)); } add(main, page); root.setAttribute('data-pgp-status', 'ready'); } diff --git a/assets/live/live.css b/assets/live/live.css index 09d519c0..5ccd2121 100644 --- a/assets/live/live.css +++ b/assets/live/live.css @@ -40,6 +40,7 @@ button:hover { border-color: var(--dim); } .links { margin-left: auto; display: flex; gap: var(--s4); white-space: nowrap; } .search-slot + .links { margin-left: 0; } .logout { margin: 0 0 0 auto; } +.demo-note { margin: 0 0 0 auto; color: var(--dim); font-size: 13px; text-align: right; } /* the search box (plugins/search.js) in the header */ .search-slot { margin-left: auto; position: relative; width: min(320px, 40vw); } @@ -102,10 +103,11 @@ button:hover { border-color: var(--dim); } .view-head { margin: var(--s3) 0 var(--s4); } .question { font-family: var(--serif); font-size: 24px; font-weight: 500; line-height: 1.25; } .view-head p { margin: 4px 0 0; max-width: 75ch; } -.cards { display: grid; gap: var(--s4); } +.cards { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s4); } +#results, #progress, .view { min-width: 0; } /* --- a card --- */ -.card { background: var(--card); border: 1px solid var(--rule); border-radius: var(--r-card); padding: var(--s4) var(--s5); } +.card { min-width: 0; overflow-wrap: anywhere; background: var(--card); border: 1px solid var(--rule); border-radius: var(--r-card); padding: var(--s4) var(--s5); } .card > h2 { font-size: 15px; margin-bottom: var(--s2); } .result { position: relative; scroll-margin-top: 80px; } .result.attn { border-left: 4px solid var(--attn); padding-left: calc(var(--s5) - 3px); } @@ -134,9 +136,10 @@ button:hover { border-color: var(--dim); } .lines { margin: 0 0 var(--s3); padding-left: 1.1em; max-width: 75ch; color: var(--text); } .lines li { margin: 2px 0; } .table-wrap { overflow-x: auto; margin: 0 0 var(--s3); } -table.rows { border-collapse: collapse; width: 100%; font-size: 14px; } +table.rows { border-collapse: collapse; width: 100%; font-size: 14px; overflow-wrap: normal; } table.rows th { text-align: left; color: var(--dim); font-weight: 600; font-size: 13px; padding: 6px 10px; border-bottom: 1px solid var(--rule); } -table.rows td { padding: 6px 10px; vertical-align: top; overflow-wrap: anywhere; } +table.rows th { white-space: nowrap; } +table.rows td { padding: 6px 10px; vertical-align: top; overflow-wrap: break-word; min-width: 4ch; } table.rows tbody tr:nth-child(even) td { background: color-mix(in srgb, var(--rule) 30%, transparent); } .more { margin-top: 6px; font-size: 13px; } .caveat { border-top: 1px solid var(--rule); margin-top: var(--s3); padding-top: var(--s3); font-size: 13px; line-height: 1.55; color: var(--dim); } @@ -242,9 +245,9 @@ ul.runs li { padding: 6px 0; border-bottom: 1px solid var(--rule); } /* --- a family --- */ .family-page { display: grid; gap: var(--s4); } .family-top { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: var(--s4); align-items: start; } -.family-grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: var(--s4); align-items: start; } +.family-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s4); } .pedigree-figure { margin: 0; } -.pedigree-svg { width: 100%; height: auto; max-height: 460px; display: block; } +.pedigree-svg { width: 100%; height: auto; max-height: 340px; display: block; margin: 0 auto; } .ped-lines line { stroke: var(--dim); stroke-width: 2; } .ped-shape { fill: var(--card); stroke: var(--text); stroke-width: 2; } .ped-shape.filled { fill: var(--text); } diff --git a/assets/live/person.js b/assets/live/person.js index 36d89186..4197e529 100644 --- a/assets/live/person.js +++ b/assets/live/person.js @@ -89,6 +89,8 @@ card.scrollIntoView({ block: 'start' }); card.focus({ preventScroll: true }); } + } else if (window.location.hash === '#progress') { + window.scrollTo(0, 0); // #progress is also the section's id: keep the head and the rail in view } } function go(hash) { @@ -227,7 +229,7 @@ add(box, ol); } var waiting = cards().filter(function (x) { return gate(x.c) === 'waiting'; }).length; - if (waiting) { add(box, el('p', { 'class': 'dim' }, waiting + ' cards are still waiting for their step.')); } + if (waiting) { add(box, el('p', { 'class': 'dim' }, (waiting === 1 ? '1 card is' : waiting + ' cards are') + ' still waiting for its step.')); } return box; } function chapter(ch) { diff --git a/assets/live/plugins/ops.js b/assets/live/plugins/ops.js index e11f8667..b399503f 100644 --- a/assets/live/plugins/ops.js +++ b/assets/live/plugins/ops.js @@ -70,6 +70,7 @@ }); } + if (!window.PGP) { return; } // family.html loads the plugins without live.js window.PGP.register({ name: 'ops', panel: function (el_) { box = el_; draw(); }, diff --git a/assets/live/plugins/search.js b/assets/live/plugins/search.js index 3dca0ab6..6a96bb33 100644 --- a/assets/live/plugins/search.js +++ b/assets/live/plugins/search.js @@ -153,5 +153,6 @@ .catch(function () { if (key === shownKey) { shownKey = null; } }); } - root.PGP.register({ name: 'search', slot: 'search-slot', panel: panel, onState: onState }); + // family.html loads the plugins without live.js: nothing to register there. + if (root && root.PGP) { root.PGP.register({ name: 'search', slot: 'search-slot', panel: panel, onState: onState }); } }(typeof window !== 'undefined' ? window : null)); diff --git a/bin/live_results.py b/bin/live_results.py index 52d12d1c..0cf398aa 100644 --- a/bin/live_results.py +++ b/bin/live_results.py @@ -151,7 +151,7 @@ def a_clinvar(S, s): for h in (d.get("hits") or [])[:50]: rows.append([h.get("gene"), f"{h.get('chrom')}:{h.get('pos')} {h.get('ref')}>{h.get('alt')}", h.get("genotype"), h.get("significance"), "★" * int(h.get("stars") or 0) or "0", - rr.af_text(h) or "frequency unknown", rr.call_text(h) or "." + rr.af_text(h) or "frequency unknown", (rr.call_text(h) or ".") + (" · low confidence" if h.get("low_confidence") else "")]) lines = [x for x in (rr.cdna_scan_lines(d) or [])] freq = rr.clinvar_freq_line(d) @@ -570,6 +570,12 @@ def card(sid, title, keys, summary, files): tones.append(tone) if "attention" in tones: out["attention"] = True + if out["headline"]: + # The step headline carries its caveat (the Progress tab shows no other); the card prints it below. + for c in sorted({plain(c) for c in cav}, key=len, reverse=True): + if out["headline"].endswith(" " + c): + out["headline"] = out["headline"][:-len(c) - 1].rstrip() + break fn = ADAPTERS.get(sid) if fn and state not in ("failed",): try: diff --git a/bin/owngenome_live.py b/bin/owngenome_live.py index 5d5a0284..02008bd5 100755 --- a/bin/owngenome_live.py +++ b/bin/owngenome_live.py @@ -796,7 +796,8 @@ def runs_index(self): planned=len(planned), done=sum(1 for s in planned if s["state"] == "done"), running=sum(1 for s in planned if s["state"] == "running"), failed=sum(1 for s in planned if s["state"] == "failed")) - item["attention"], mtime = self.attention(r["dir"]) + # A run known from its messages alone still has its folder under the root once step 24 wrote there. + item["attention"], mtime = self.attention(r["dir"] or os.path.join(self.root, name)) item["results"] = mtime is not None else: item["members"] = r.get("members") or [] diff --git a/bin/owngenome_live_family.py b/bin/owngenome_live_family.py index 9ba0a2da..00c712a6 100644 --- a/bin/owngenome_live_family.py +++ b/bin/owngenome_live_family.py @@ -256,7 +256,7 @@ def page_data(req, m): continue st = live.base_state(r) planned = [s for s in st["steps"] if s["state"] != "not planned"] - n, mtime = live.attention(r["dir"]) + n, mtime = live.attention(r["dir"] or os.path.join(live.root, r["name"])) runs[p["sample"]] = {"url": "/runs/%s/" % p["sample"], "state": st["run"]["state"], "planned": len(planned), "done": sum(1 for s in planned if s["state"] == "done"), "attention": n, "results": mtime is not None} diff --git a/tests/demo/build_live_demo.py b/tests/demo/build_live_demo.py index 03124bcd..bc724f4f 100644 --- a/tests/demo/build_live_demo.py +++ b/tests/demo/build_live_demo.py @@ -5,14 +5,19 @@ writes DIR, which the docs workflow publishes at /demo/ of the docs site: - index.html the list of runs, as the live site's / shows it - assets/ assets/live/ as it is: live.js, live.css, plugins/*.js - runs/DEMO-001/index.html the live page (assets/live/index.html) in replay mode + index.html the home page (assets/live/home.html), as the live site's / shows it + runs.json what the home page draws, as /runs.json gives it + assets/ assets/live/ as it is: core.js, live.js, person.js, live.css, plugins/*.js + runs/DEMO-001/index.html the person page (assets/live/index.html) in replay mode runs/DEMO-001/frames.json the page's state.json at each moment of the run + runs/DEMO-001/results.json the person's results (bin/live_results.py), as the results + plugin serves them at the end of the run; each card shows + once the replay reaches the end of its steps runs/DEMO-001/... the files the plugins point at (the charts, the search index, the re-check), the report as report.html and the files it links to - runs/DEMO-FAMILY/ the family page of DEMO-001 and its invented partner + runs/DEMO-FAMILY/ the family page (assets/live/family.html) of DEMO-001 and its + invented partner, with family.json as the family plugin serves it family/DEMO-FAMILY/ the pedigree and the files the family page opens No state is written by hand. The frames are bin/owngenome_live.py's own @@ -63,7 +68,9 @@ REPO = os.path.dirname(os.path.dirname(HERE)) BIN = os.path.join(REPO, "bin") sys.path.insert(0, BIN) +import live_results # noqa: E402 import owngenome_live # noqa: E402 +import owngenome_live_family # noqa: E402 import owngenome_recheck # noqa: E402 import pedigree_svg # noqa: E402 import run_headroom # noqa: E402 @@ -217,9 +224,16 @@ def snap(t): fam = live.state(live.run(FAMILY)) fam["generated_utc"] = checked fam["plugins"] = relink(fam["plugins"]) + index = live.runs_index() + run, d = live.run(SAMPLE), os.path.join(root, SAMPLE) + # The demo's run folder has no run_status.tsv (OTHER_RUN), so the server knows it from its + # messages alone; the files it may serve are those of the folder all the same. + results = live_results.build(SAMPLE, live.summary(SAMPLE, d, live.base_state(run)), + owngenome_live.Live.run_files(None, {"dir": d, "name": SAMPLE})) + results["generated_utc"] = checked finally: time.time, run_headroom._statvfs, run_headroom.physical_memory = real - return frames, {"t": checked, "state": fam}, app + return frames, {"t": checked, "state": fam, "index": index, "results": results}, app # --- the static folder ---------------------------------------------------------------------- @@ -229,16 +243,47 @@ def with_csp(page): return page.replace('', '\n' + meta, 1) -def run_page(name, plugin_js): - with open(os.path.join(ASSETS, "index.html")) as f: +def run_page(name, plugin_js, template="index.html"): + with open(os.path.join(ASSETS, template)) as f: page = f.read() tags = "\n".join(f'' for f in plugin_js) page = (page.replace("{{assets}}", "../../assets").replace("{{run}}", html.escape(name)) - .replace("{{mode}}", "replay").replace("{{plugins}}", tags) - .replace('All runs', 'All runs')) + .replace("{{mode}}", "replay").replace("{{plugins}}", tags).replace('href="/"', 'href="../../"')) return with_csp(page) +def home_page(index): + """assets/live/home.html as the server fills it, with a line saying the people are invented and no + log-out button (a static host has no session).""" + with open(os.path.join(ASSETS, "home.html")) as f: + page = f.read() + items = "\n".join(f'
  • {html.escape(r["name"])}
  • ' for r in index["runs"]) + page = (page.replace("{{assets}}", "assets").replace("{{data}}", "runs.json").replace("{{runs}}", items) + .replace('
    ', + '

    A demo: the people, the run and the results are invented.

    ')) + return with_csp(page) + + +def unroot(v): + """/runs.json's absolute links as links relative to the site's root.""" + if isinstance(v, dict): + return {k: unroot(x) for k, x in v.items()} + if isinstance(v, list): + return [unroot(x) for x in v] + if isinstance(v, str) and re.match(r"^/(?:runs|family)/", v): + return v[1:] + return v + + +def family_json(root, index): + """family.json as bin/owngenome_live_family.py serves it, its links relative to runs/DEMO-FAMILY/.""" + out = owngenome_live_family.family_page(FAMILY, os.path.join(root, FAMILY)) + by = {r["name"]: r for r in index["runs"]} + out["runs"] = {p["sample"]: {k: by[p["sample"]][k] for k in ("url", "state", "planned", "done", "attention", "results")} + for p in out["state"].get("members") or [] if p["sample"] in by} + return relink(out) + + def plain_page(title, body, css): return with_csp('\n' '' @@ -255,23 +300,22 @@ def write_json(path, obj): def write_site(out, root, frames, fam): if os.path.exists(out): shutil.rmtree(out) - shutil.copytree(ASSETS, os.path.join(out, "assets"), ignore=shutil.ignore_patterns("index.html", "login.html")) + shutil.copytree(ASSETS, os.path.join(out, "assets"), + ignore=shutil.ignore_patterns("index.html", "login.html", "home.html", "family.html")) plugin_dir = os.path.join(ASSETS, "plugins") plugin_js = sorted(f for f in os.listdir(plugin_dir) if owngenome_live.ASSET_RE.match(f) and f.endswith(".js")) \ if os.path.isdir(plugin_dir) else [] - runs = [(SAMPLE, "sample"), (FAMILY, "family")] - items = "\n".join(f'
  • {n} {k}
  • ' for n, k in runs) with open(os.path.join(out, "index.html"), "w") as f: - f.write(plain_page("Runs", '

    A demo of the live site on DEMO-001, an invented sample: ' - 'the run, the results and the family are made up. Nothing here is a real person.

    ' - f'
      {items}
    ', "assets/live.css")) + f.write(home_page(fam["index"])) + write_json(os.path.join(out, "runs.json"), unroot(fam["index"])) sd, d = os.path.join(out, "runs", SAMPLE), os.path.join(root, SAMPLE) os.makedirs(sd) with open(os.path.join(sd, "index.html"), "w") as f: f.write(run_page(SAMPLE, plugin_js)) write_json(os.path.join(sd, "frames.json"), {"interval_ms": INTERVAL_MS, "autoplay": True, "frames": frames}) + write_json(os.path.join(sd, "results.json"), fam["results"]) files = {f"{SAMPLE}_report.html": "report.html"} # The files the report links to, as the live server lists them (Live.run_files reads no state). files.update({rel: rel for rel in owngenome_live.Live.run_files(None, {"dir": d, "name": SAMPLE})}) @@ -293,8 +337,9 @@ def write_site(out, root, frames, fam): fd = os.path.join(out, "runs", FAMILY) os.makedirs(fd) with open(os.path.join(fd, "index.html"), "w") as f: - f.write(run_page(FAMILY, plugin_js)) - write_json(os.path.join(fd, "frames.json"), {"interval_ms": INTERVAL_MS, "frames": [fam]}) + f.write(run_page(FAMILY, plugin_js, "family.html")) + write_json(os.path.join(fd, "frames.json"), {"interval_ms": INTERVAL_MS, "frames": [{"t": fam["t"], "state": fam["state"]}]}) + write_json(os.path.join(fd, "family.json"), family_json(root, fam["index"])) famdir, ffiles = os.path.join(out, "family", FAMILY), os.path.join(root, FAMILY) os.makedirs(famdir) order, members, _ = pedigree_svg.load(ffiles) diff --git a/tests/test_live_demo.py b/tests/test_live_demo.py index efc760d8..3fe471f8 100644 --- a/tests/test_live_demo.py +++ b/tests/test_live_demo.py @@ -12,16 +12,21 @@ once the report step has ended, the re-check only after the run; 4. the run-health card measures the invented machine, never the machine that builds the demo, and no frame holds a path of that machine; - 5. every link a frame gives is relative and reaches a file of the demo; + 5. every link a frame gives is relative and reaches a file of the demo, and + so is every link of runs.json, results.json and family.json; 6. every page the build writes carries the server's Content-Security-Policy as a meta tag, and no page or script of the demo loads anything from outside; 7. in a headless Chrome (skipped without Chrome, except in CI), served as a - static host serves it (no policy header): the runs list, the replay to its - last frame, the charts, a search, the re-check banner, the run-health - card, the family link and its pedigree, the viewer page, and no policy - violation. -Checks 1, 2, 3 and 5 also run against a planted mistake and must fail there (2: + static host serves it (no policy header): the home page's people and + family, the replay to its last frame, the charts, a search, the re-check + banner, the run-health card, the family link and its pedigree, the viewer + page, and no policy violation; the person page's cards (the doctor tab + with the ClinVar card, its caveat and its chart), the tab row moved with + the arrow keys, #s/clinvar focusing its card; the family page with a + clickable pedigree and no step map or report link; and at a phone's width + (390 px) no page scrolls sideways. +Checks 1, 2, 3, 5 and the phone width also run against a planted mistake and must fail there (2: a build with one plugin's state hook dropped): a check that cannot fail is no check. Run: python3 tests/test_live_demo.py @@ -144,6 +149,25 @@ def link_problems(out, built): return bad +def json_link_problems(out): + """Links of runs.json, results.json and family.json that are absolute or reach no file of the demo.""" + bad = [] + for rel, base in (("runs.json", out), (os.path.join("runs", S, "results.json"), os.path.join(out, "runs", S)), + (os.path.join("runs", FAM, "family.json"), os.path.join(out, "runs", FAM))): + with open(os.path.join(out, rel)) as f: + data = json.load(f) + found = list(links(data)) + if rel.endswith("results.json"): + found += [x for ch in data["chapters"] for c in ch["sections"] for x in c.get("links") or []] + if not found: + bad.append(f"{rel}: no link at all") + for u in found: + p = os.path.normpath(os.path.join(base, u)) + if u.startswith("/") or not (os.path.isfile(p) or os.path.isfile(os.path.join(p, "index.html"))): + bad.append(f"{rel}: {u}") + return bad + + def located_term(out): """A term of the demo's search index with a mention at a locus (its hit links the viewer).""" with open(os.path.join(out, "runs", S, f"{S}_search_index.json")) as f: @@ -175,9 +199,12 @@ def browser_checks(out): b = Chrome(chrome, os.path.join(os.path.dirname(out), "chrome")) try: b.go(f"{base}/") - runs = b.wait("document.querySelectorAll('ul.runs a').length === 2 && " - "Array.from(document.querySelectorAll('ul.runs a')).map(function (a) { return a.getAttribute('href'); }).join(' ')") - check("browser: the runs list links the sample and the family", runs == f"runs/{S}/ runs/{FAM}/", runs) + people = b.wait("document.documentElement.dataset.pgpStatus === 'ready' && document.documentElement.dataset.pgpPeople") + runs = b.eval("Array.from(document.querySelectorAll('.person-card a.person-link')).map(function (a) { " + "return a.getAttribute('href'); }).join(' ') + ' | ' + Array.from(document.querySelectorAll(" + "'.family-head a')).map(function (a) { return a.getAttribute('href'); }).join(' ')") + check("browser: the home page shows both people of the family, the sequenced one with a run linked, and the family", + people == "2" and runs == f"runs/{S}/ | runs/{FAM}/ runs/{FAM}/", (people, runs)) b.go(f"{base}/runs/{S}/") first = b.wait("document.documentElement.dataset.pgpFrame && document.documentElement.dataset.pgpStatus === 'ready' " "&& document.querySelectorAll('#map g.box').length") @@ -231,12 +258,67 @@ def browser_checks(out): check("browser: the policy blocked nothing on the sample page", not blocked, blocked) csp = b.eval("var m = document.querySelector('meta[http-equiv=Content-Security-Policy]'); m && m.content") check("browser: the page runs under the meta policy", csp == build_live_demo.CSP, csp) + person_checks(b, base) + phone_checks(b, base) finally: b.close() srv.shutdown() srv.server_close() +def person_checks(b, base): + """The person page at the end of the run: cards, keyboard, a section link; the family page's own view.""" + b.eval("location.hash = '#q/clinical'; true") + n = b.wait("document.documentElement.dataset.pgpTab === 'clinical' && document.documentElement.dataset.pgpResults") + card = b.eval("var c = document.getElementById('card-clinvar'); c && [!!c.querySelector('.caveat .caveat-lead'), " + "!!c.querySelector('figure[data-chart]'), c.classList.contains('attn')].join(' ')") + check("browser: the doctor tab shows its cards; ClinVar's has its caveat, its chart and its flag", + n and int(n) > 3 and card == "true true true", (n, card)) + b.eval("var t = document.querySelector('#rail [aria-selected=true]'); t.focus(); " + "t.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); true") + tab = b.wait("document.documentElement.dataset.pgpTab !== 'clinical' && document.documentElement.dataset.pgpTab") + check("browser: the arrow key moves to the next chapter", tab == "carrier", tab) + b.eval("location.hash = '#s/clinvar'; true") + focus = b.wait("document.documentElement.dataset.pgpTab === 'clinical' && document.activeElement && " + "document.activeElement.id") + check("browser: #s/clinvar opens the doctor tab and focuses the ClinVar card", focus == "card-clinvar", focus) + b.go(f"{base}/runs/{FAM}/") + fam = b.wait("document.documentElement.dataset.pgpStatus === 'ready' && [!document.getElementById('map'), " + "!document.getElementById('report-link'), (document.querySelector('svg.pedigree-svg a') || {" + "getAttribute: function () { return null; } }).getAttribute('href')].join(' ')") + check("browser: the family page has a clickable pedigree, no step map and no report link", + fam == f"true true ../../runs/{S}/", fam) + + +def too_wide(b): + return b.eval("document.documentElement.scrollWidth > window.innerWidth ? " + "document.documentElement.scrollWidth + ' > ' + window.innerWidth : ''") + + +def phone_checks(b, base): + b.call("Emulation.setDeviceMetricsOverride", {"width": 390, "height": 844, "deviceScaleFactor": 2, "mobile": True}, + b.session) + try: + wide = {} + for name, path, ready in (("home", "/", "document.documentElement.dataset.pgpStatus === 'ready'"), + ("person", f"/runs/{S}/#q/clinical", "document.documentElement.dataset.pgpResults"), + ("family", f"/runs/{FAM}/", "document.documentElement.dataset.pgpStatus === 'ready'")): + b.go(base + path) + b.wait(ready) + if name == "person": + b.eval("var s = document.getElementById('replay-slider'); s.value = s.max; " + "s.dispatchEvent(new Event('input')); true") + b.wait("document.documentElement.dataset.pgpFrame === document.getElementById('replay-slider').max") + time.sleep(1) + wide[name] = too_wide(b) + check("browser: at 390 px no page scrolls sideways", not any(wide.values()), wide) + b.eval("var d = document.createElement('div'); d.style.width = '900px'; d.style.height = '4px'; " + "document.querySelector('main').appendChild(d); true") + control("a 900 px wide block on the family page", not too_wide(b)) + finally: + b.call("Emulation.clearDeviceMetricsOverride", {}, b.session) + + def main(): work = tempfile.mkdtemp(prefix="test_live_demo.") try: @@ -289,6 +371,15 @@ def main(): planted = copy.deepcopy(built) planted["family"]["state"]["plugins"]["family"]["pedigree"] = f"/family/{FAM}/pedigree.svg" control("an absolute pedigree link", not link_problems(out, planted)) + bad = json_link_problems(out) + check("5. every link of runs.json, results.json and family.json is relative and reaches a file", not bad, bad) + with open(os.path.join(out, "runs.json")) as f: + idx = json.load(f) + with open(os.path.join(out, "runs.json"), "w") as f: + json.dump(dict(idx, runs=[dict(r, url="/" + r["url"]) for r in idx["runs"]]), f) + control("an absolute link in runs.json", not json_link_problems(out)) + with open(os.path.join(out, "runs.json"), "w") as f: + json.dump(idx, f) # 6. the policy on every page, nothing from outside pages, texts = [], {} diff --git a/tests/test_live_results.py b/tests/test_live_results.py new file mode 100644 index 00000000..094d8888 --- /dev/null +++ b/tests/test_live_results.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +"""A person's results and a family's page on the live site: bin/live_results.py, the results plugin +(bin/owngenome_live_results.py), /runs.json and the family plugin's family.json. + +Checks: + 1. render_report.SECTION_NOTES names a caveat for every section id of CHAPTERS, + and each name is a text constant of bin/render_report.py; + 2. results.json of DEMO-001 (tests/demo/make_demo_sample.py, collect_summary): + the seven chapters with every section id once, and each card's caveat is + its SECTION_NOTES constants, word for word, never empty; + 3. the wording gate: no string of results.json, of family.json + (tests/fixtures/families_live family_a) or of the page assets + (assets/live: the text of the pages, the string literals of the scripts) + says a result is fine (live_results.FORBIDDEN), a quoted tool term and a + caveat aside; + 4. ClinVar: with hits the card lists them in a table; with none it has no + table and its empty text says ClinVar lists only part of what causes + disease (never an all-clear); + 5. through the real server: /runs//results.json is behind the login + (401 without a session), answers the cards with one, and writes one access + log row per request; summary.json stays 404; /runs.json lists the run with + its progress and attention count, behind the login too. +Each check runs again on a planted mistake and must fail there: 1 with VNTYPER's +entry dropped, 2 with one card's caveat blanked, 3 with "ok" planted in a card +and "All clear" in a script, 4 with the hits emptied, 5 with the results route +registered without the login. A check that cannot fail is no check. + +Run: python3 tests/test_live_results.py +""" +import contextlib +import copy +import io +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +import threading +import urllib.parse + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(REPO, "bin")) +sys.path.insert(0, os.path.join(REPO, "tests")) +import collect_summary # noqa: E402 +import live_results # noqa: E402 +import owngenome_live # noqa: E402 +import owngenome_live_family # noqa: E402 +import render_report as rr # noqa: E402 + +SAMPLE = "DEMO-001" +ASSETS = os.path.join(REPO, "assets", "live") +FAMILIES = os.path.join(REPO, "tests", "fixtures", "families_live") +PASSWORD = "synthetic test password 7" +FAILS = [] + + +def check(desc, ok, detail=""): + print(("[PASS] " if ok else "[FAIL] ") + desc + ("" if ok else f" -- {str(detail)[:800]}")) + if not ok: + FAILS.append(desc) + + +def control(desc, still_passes): + check(f"control: {desc}", not still_passes, "the check passed on the planted mistake") + + +# --- 1. the caveat of every section --------------------------------------------------------- + +def notes_problems(chapters, notes, module): + ids = [sid for _, _, _, secs in chapters for sid, _, _ in secs] + out = [f"{sid}: no caveat" for sid in ids if not notes.get(sid)] + out += [f"{sid}: {n} is not a text constant" for sid, names in notes.items() for n in names + if not isinstance(getattr(module, n, None), str) or not getattr(module, n).strip()] + out += [f"{sid}: no such section" for sid in notes if sid not in ids] + return out + + +# --- 2. one card per section, its caveat word for word -------------------------------------- + +def card_problems(res): + out = [] + want = [sid for _, _, _, secs in rr.CHAPTERS for sid, _, _ in secs] + got = [c["id"] for ch in res["chapters"] for c in ch["sections"]] + if got != want: + out.append(f"sections {got} != {want}") + if [ch["key"] for ch in res["chapters"]] != [k for k, *_ in rr.CHAPTERS]: + out.append("chapters out of the report's order") + for ch in res["chapters"]: + for c in ch["sections"]: + exp = [getattr(rr, n) for n in rr.SECTION_NOTES.get(c["id"], ())] + if not c["caveat"] or c["caveat"] != exp: + out.append(f"{c['id']}: caveat {c['caveat']!r:.120}") + return out + + +# --- 3. the wording gate -------------------------------------------------------------------- + +def asset_text(path): + """What a page or script of assets/live puts on screen: the text of an HTML page, the string + literals of a script (its comments and code left out).""" + with open(path) as f: + t = f.read() + if path.endswith(".html"): + t = re.sub(r"<(script|style)\b.*?", " ", t, flags=re.S | re.I) + return [x.strip() for x in re.split(r"<[^>]*>", t) if x.strip()] + t = re.sub(r"/\*.*?\*/", " ", t, flags=re.S) + t = re.sub(r"(?m)^\s*//[^\n]*", " ", t) + return [m.group(2) for m in re.finditer(r"(['\"])((?:\\.|(?!\1).)*)\1", t)] + + +def asset_problems(texts): + out = [] + for name, items in texts.items(): + for s in items: + if live_results.FORBIDDEN.search(re.sub(r'"[^"]*"', "", s)): + out.append(f"{name}: {s[:120]}") + return out + + +# --- 4. ClinVar with and without hits ------------------------------------------------------- + +def clinvar_card(summary): + res = live_results.build(SAMPLE, summary) + return next(c for ch in res["chapters"] for c in ch["sections"] if c["id"] == "clinvar") + + +def clinvar_problems(with_hits, without): + out = [] + n = len(with_hits["table"]["rows"]) if with_hits.get("table") else 0 + if not n: + out.append("hits but no table") + if without.get("table"): + out.append("no hit but a table") + if "lists only part" not in (without.get("empty_text") or ""): + out.append(f"empty text {without.get('empty_text')!r}") + return out + + +# --- 5. the server -------------------------------------------------------------------------- + +def request(port, method, path, body=None, headers=None): + import http.client + c = http.client.HTTPConnection("127.0.0.1", port, timeout=30) + h = {"Host": f"127.0.0.1:{port}"} + h.update(headers or {}) + c.request(method, path, body=body, headers=h) + r = c.getresponse() + data = r.read() + hdr = {k.lower(): v for k, v in r.getheaders()} + c.close() + return r.status, hdr, data + + +def serve(root, state, pwfile, open_results=False): + app = owngenome_live.App() + if open_results: # the planted mistake: the results route without the login + route = app.route + app.route = lambda m, p, fn, auth=True: route(m, p, fn, auth and "results" not in p) + with contextlib.redirect_stderr(io.StringIO()): + live, web, ingest = owngenome_live.start(["--root", root, "--state-dir", state, "--port", "0", "--ingest-port", "0", + "--password-file", pwfile, "--quiet"], app) + threading.Thread(target=web.serve_forever, daemon=True).start() + return live, web, ingest + + +def stop(srv): + live, web, ingest = srv + live.stopping = True + web.shutdown() + web.server_close() + ingest.server_close() + + +def server_problems(port, state): + out = [] + path = f"/runs/{SAMPLE}/results.json" + code, _, _ = request(port, "GET", path) + if code != 401: + out.append(f"results.json without a session: {code}") + code, _, _ = request(port, "GET", "/runs.json") + if code != 401: + out.append(f"runs.json without a session: {code}") + code, h, _ = request(port, "POST", "/login", urllib.parse.urlencode({"password": PASSWORD, "next": "/"}), + {"Content-Type": "application/x-www-form-urlencoded"}) + m = re.match(r"owngenome_live=([^;]+)", h.get("set-cookie", "")) + if not m: + return out + [f"login {code}"] + ck = {"Cookie": f"owngenome_live={m.group(1)}"} + log = os.path.join(state, "access.log") + before = open(log).read().count("\tresults\t") if os.path.isfile(log) else 0 + code, h, body = request(port, "GET", path, headers=ck) + res = json.loads(body) if code == 200 else {} + if code != 200 or len(res.get("chapters") or []) != 7 or res.get("sample") != SAMPLE: + out.append(f"results.json with a session: {code} {str(res)[:200]}") + after = open(log).read().count("\tresults\t") if os.path.isfile(log) else 0 + if after - before != 1: + out.append(f"{after - before} access log rows for one results.json") + code, _, _ = request(port, "GET", f"/runs/{SAMPLE}/summary.json", headers=ck) + if code != 404: + out.append(f"summary.json served: {code}") + code, _, body = request(port, "GET", "/runs.json", headers=ck) + idx = json.loads(body) if code == 200 else {} + run = next((r for r in idx.get("runs") or [] if r["name"] == SAMPLE), None) + if not run or run["url"] != f"/runs/{SAMPLE}/" or not run.get("results") or run.get("attention") != res.get("attention"): + out.append(f"runs.json: {code} {run}") + return out + + +def main(): + work = tempfile.mkdtemp(prefix="test_live_results.") + srvs = [] + try: + # 1. + bad = notes_problems(rr.CHAPTERS, rr.SECTION_NOTES, rr) + check(f"1. every section id has its caveat ({len(rr.SECTION_NOTES)} ids)", not bad, bad) + planted = {k: v for k, v in rr.SECTION_NOTES.items() if k != "vntyper"} + control("VNTYPER's caveat entry dropped", not notes_problems(rr.CHAPTERS, planted, rr)) + + # 2. + root = os.path.join(work, "root") + os.makedirs(root) + subprocess.run([sys.executable, os.path.join(REPO, "tests", "demo", "make_demo_sample.py"), "--genome-dir", root, + "--records", "2000"], check=True, stdout=subprocess.DEVNULL) + d = os.path.join(root, SAMPLE) + summary = collect_summary.collect(SAMPLE, d) + with open(os.path.join(d, "summary.json"), "w") as f: + json.dump(summary, f) + res = live_results.build(SAMPLE, summary) + bad = card_problems(res) + check("2. results.json: the report's chapters, one card per section, each with its caveats word for word", + not bad, bad) + blank = copy.deepcopy(res) + blank["chapters"][3]["sections"][0]["caveat"] = [] + control("one card's caveat blanked", not card_problems(blank)) + + # 3. + bad = live_results.wording_problems(res) + check("3. no string of results.json reads as an all-clear", not bad, bad) + fam = owngenome_live_family.family_page("family_a", os.path.join(FAMILIES, "family_a")) + bad = live_results.wording_problems(fam) + check("3. no string of family.json reads as an all-clear", not bad and fam.get("layout"), bad) + planted = copy.deepcopy(res) + planted["chapters"][0]["sections"][0]["headline"] = "Coverage: ok" + control("'ok' planted in a card headline", not live_results.wording_problems(planted)) + texts = {} + for p, _, fs in os.walk(ASSETS): + for f in fs: + if f.endswith((".html", ".js")): + texts[os.path.relpath(os.path.join(p, f), ASSETS)] = asset_text(os.path.join(p, f)) + bad = asset_problems(texts) + check(f"3. no page or script text of assets/live reads as an all-clear ({len(texts)} files)", + not bad and len(texts) >= 10, bad) + planted = dict(texts, **{"person.js": texts["person.js"] + ["All clear: nothing to see"]}) + control("'All clear' planted in person.js", not asset_problems(planted)) + + # 4. + none = copy.deepcopy(summary) + cv = none["sections"]["clinvar"]["data"] + cv["hits"], cv["count"], cv["by_stars"] = [], 0, {} + with_hits, without = clinvar_card(summary), clinvar_card(none) + bad = clinvar_problems(with_hits, without) + check("4. ClinVar: a table with hits; without, the sentence that ClinVar lists only part", not bad, bad) + control("the demo's hits emptied on both sides", not clinvar_problems(without, without)) + + # 5. + pwfile = os.path.join(work, "password") + with open(pwfile, "w") as f: + f.write(owngenome_live.hash_password(PASSWORD) + "\n") + state = os.path.join(work, "state") + srv = serve(root, state, pwfile) + srvs.append(srv) + bad = server_problems(srv[1].server_address[1], state) + check("5. results.json and runs.json behind the login, one log row, summary.json 404", not bad, bad) + state2 = os.path.join(work, "state-open") + srv = serve(root, state2, pwfile, open_results=True) + srvs.append(srv) + control("the results route registered without the login", + not server_problems(srv[1].server_address[1], state2)) + finally: + for s in srvs: + stop(s) + shutil.rmtree(work, ignore_errors=True) + if FAILS: + print(f"\n{len(FAILS)} check(s) failed") + return 1 + print("\nAll checks passed") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From e12e451abce84b7f36ad03377e65e3d403e60e6e Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sat, 7 Nov 2026 20:56:48 +0100 Subject: [PATCH 09/14] test(live): measure phone width on a desktop viewport and allow the report's outside explore links Bead: pgp-5bja --- tests/test_live_demo.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_live_demo.py b/tests/test_live_demo.py index 3fe471f8..bc64d1c7 100644 --- a/tests/test_live_demo.py +++ b/tests/test_live_demo.py @@ -162,6 +162,8 @@ def json_link_problems(out): if not found: bad.append(f"{rel}: no link at all") for u in found: + if u.startswith("https://"): # an explore link of the report (a haplogroup tree), opened by the reader + continue p = os.path.normpath(os.path.join(base, u)) if u.startswith("/") or not (os.path.isfile(p) or os.path.isfile(os.path.join(p, "index.html"))): bad.append(f"{rel}: {u}") @@ -296,7 +298,8 @@ def too_wide(b): def phone_checks(b, base): - b.call("Emulation.setDeviceMetricsOverride", {"width": 390, "height": 844, "deviceScaleFactor": 2, "mobile": True}, + # mobile False: a mobile viewport zooms out to fit a page that overflows, which would hide the overflow. + b.call("Emulation.setDeviceMetricsOverride", {"width": 390, "height": 844, "deviceScaleFactor": 2, "mobile": False}, b.session) try: wide = {} From b597b7b98a5f551016bda011ec95830bdb13a3ec Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sat, 7 Nov 2026 21:00:49 +0100 Subject: [PATCH 10/14] docs(live): describe the person, home and family pages and retake the demo picture Bead: pgp-5bja --- docs/images/demo-live-site.png | Bin 275071 -> 125464 bytes docs/live-site.md | 41 +++++++++++++++++++++++---------- scripts/ci/e2e-case-paths.tsv | 2 +- tests/demo/render_pictures.py | 8 ++++--- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/docs/images/demo-live-site.png b/docs/images/demo-live-site.png index 9478659e838d7984b7c451ff3d4cc4f2d99de283..db28216f70c268b60cc31d705c41be60e1feba7d 100644 GIT binary patch literal 125464 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 F_+N5T&1?Vw literal 275071 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ⅇ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ⅅO|$j2>@w~ta`b+85ixI5_*(0&7VnGxa41B=3(WoZd{%fS#_v%#-HH*kZzgJSSjTeRCW?Nti^s-2I;kQdchm|(lgXR(#dyE1Nb0w zt34V`I$4C`a)n_hDm}lVC>q)5nMhFww@XvgKK}I?h^RVv;Yve8PR^q^Mbdyz@E0=~ z0PTXIN{$!p#060>NurVYQ`O zJI^On!K|Ciu44%IcUuTwOwuLcZSPwNfueTv>Zd9QvM5;qW1!XdtczF{-~eqlcM+h zR0D=?rHq~lu_^rJ@VOMwM+BS}b=kQ~3tJr|E7Si4R5`LlOZm~3<1C%u4TutS196n9 z!TRkkWZb{L`dx3RS~H|}e>eLt%`^}FM=#+s7WY(c1414ubAb6*i^Vp?xIJSJ&!Wcd zSR2lirA%o0%~FCs{-0}pK*$mJ#it1#bX)@vy01ktX5hylGtgJuXk!aVGofa_Dy|q1 zm7&7M_v<=*2YwY-Wk`T>gS2>DL50@2B15QA7#OT)pXe9SNwE#BAO z<$cJhA2DC78x?;_MfrQ_TZYN{#7>DoXc$oSNs&`*Y7#KE0QWzV6;S3WDRfptliNtH zD_mRiz9_gKa(PJ1#wuDoxY>ljm&0vfG5^Jz@sFz~wPD74^+tCioEM8ty`-bu$t85$ zJ^P-eh~>#>^n-cwQ;l-#%ATvs_0o+MxQ^ofzNezYt|Kr8!5dTCwhYTEO4HgaFOS!( zroB}Uc%kw{FRfz$o`Zws*)pxf z@o`~8OFqiI3>0rldCn3i=v1|0zNof7v>2Thr*>QkO|91|FiCg8rwnshu(nF;{zS%S zKK4~K25w$1>-eiL>@QhfwQJOYB30pIN(eNOmPjeZcn@93DM z_6CO?F*oSvrwh6ZXG*Xgz3|T;axZT>;Hv!!w4m&;&)*OJ*f=NF>uAgzH!=V=OzhX7 z*@q271T5OR&iygjahG@SEp;tujn#rSAody_4IaD+%K;PYKFk z9I~o$Q_J9Q?BX}T$twE)(X%6H;99wms%|jxerT4(^M22?DUe!?25hW9q!dw7oZI+o zA=jtbTwg7v%3;hEO8>X}!F65(pjQAg$#*ce!&~BWbAjXIc~6Pd{?)vu zQNLb_Ii}XCR@3cVSV9FuaV69b_W!pe=SkTUL4M{w;d=Vwn1xH=@sERN`^TqWY0_iW zgkJAX2u_Mh&1IVcS(57xKkX2sC&Z`q%;0uhIzwu-<%`6vPztzH0;m?%nDhW6d*0am z9h5%?6*LX;6X0`A^1B=6*OqUs~|S zZ01bbZQtEbr|q;Xuoq{h1q=EaimZg&<5u4zpFTPNWc(ax&I$@wv|LyCi@b4%5NI;LQaV z(x#UWmO2fT4BQMDI@??cVdkv)#&HtEYIg68uS6P?~v(+!%X_Kpa`uv{P;l(C}^t3c&dXi#0jxd$bP;NOsaZs zCti*Eho@UVuktRC*L^7rNYS-VyR(03_$p{G-dtbAnXYnOcf`c??2&2ndQL1A5vz_b z{$8+uURh?md zbgI&QoJ$u$j!?AB`i?E|Kx*KwvXDJ5KO&#bMg{o0U9+xjwj~HsT0j6Z3O3O^rs7cE zj1yf9#Bm`{;J%D96C;|_0MeA3cb8K~CoL_&Q8c||uIH{c<7!iFv z1HE;~_W$lmnc0Syfj9Goiq&fuNXr`VS=BAr-MpSgF!#vypc0-8`;P*0lj|X^TYt5% z7*UD4_k^aFx+A_S_?D>WvNfN8eaVyd=*4(a?uHg^+~~zWc0Gq*z^|zpk6Uxz%f``gU-3nFIPzPluD##* zCZl%UV&f_=2cbQ>Xt9!ON`SO`cAC`8wUD}&)XnZ8sg(hmq|2rAG6F!mHmRD_69H{KiZ#H zI&Df7WZj)k9jush@QNv0nVOQoY(cAbu_~94y<-0Ah4xpV{TK_IBH4&ll#nt2sJjG# zG_LQ^ahXol-BGc`5x(AUGw=W1qF2`Dn5k zDECQ9x7zI539YRKtA3>s3KVJ$gJ09sJ^o%9j3Mx=;>aJ9QSCo*oI_@!8Esff{}8;m zT!*v_SZHxn)c>rdt6B@cFzsCIJ09y`t#Prny>~9emp(>vwu(&{*-DW+)Tw-rjt= z$&{b>G1TCU;Wa#pVzzLWL3i}H=Nc@CJNGt4{ccn=eDh_N+TP8;j5V8*^~H51k5t&(Yfe#dIC;sV@MsBE<;W8EaohREZn_9{N{+8bu->xneQx3 zdtWQJwqBiA{aEGiRFK~lKxx)R!nT}2C>*?DBEVnc}i<4&zA!@|ODV{u_oDeU0ZV$6yEY8;CrGNc zJ@SQB@A7xMb^FgSR@bvvY*jIU2>_5#v}5V?7S^r-L+$?(FxD&&kHBw#&OJ8q_9Wad zmK`Wj+BDuiK7Q&$)oHpHNMWU6D$XkBqqp@SUfgK>k%-C;`X-gpac8={j?p)&ha-|+ zd0K|hi=*0tb+bg@L%rQ<<--a?#;BPF_qDmGoSEtJ< zXlfnas1`7hM3Y(_8fLYheg7ZBSb6M!3}X*eEi#nIpvTSS59upHOHJZtvGMlr+w_tY{E5A|uug88miSXXPdf!Cd%alS>omVP11%{efNn0TdvcNSfw|6_K__8l?7WO( ztV$vQYd&DVl2PRB1w0s_@AL#>uEa3n0^qLl=i34y(#nUNkS4E(A2csFL)pvkf`uOX zgyLfP?=-v?lWpUn2o-+l7o+T0hm!YP!M&|hxZ9-Q%NMEP?zGU@bs-z@})L%kvTUPxh zf=+^l@ypeK%(YTaP!v~(d%M%IaS47=PEb?!uL3gdI)!-!r6%{Edl1Vhav1}0e%KtD z@&2Q_MRiphmW#Ind_#ofJS~eJrByYb=YmU~q0b(w=M5n2>l;sS)P+!#=k^|!5Frxi zKLKPG4?6R$vIId-pjv2)yu?ykBbNK1tG}n$q5z@zuQyj>{hKJhfKw5qX!%#9)~aKn zx%u8gePdW<`xR#KCNmg`0B7|i1ckU{lC>8u^b?kZv&5+D){jg$McWchYu1v zxqZ|lk3GtJsCGFG#+5PntvjP(MUY>F?$iDdcpmVc&pv*C($XNb(xI1NZhILC@Vi%a~7AS^(I2UvAs)& zAa`-5q2dT>G%iT)^8GY8!xlVGAlPQQBLVf%uQalBo%K6R^*hQ#y2eX>iIJp=(25uI31wbqm$z<5+EX=D}EFYQlg)!XPs8%`H%#8DdWCHv*osQBBI zEi&1A_AM=+3br%>KW+wG}d?Hj9mSlS$nx7|3DjJRFn`f$( zWzLqeep2g26N&fR3TKMkXL#6swBhQu;d9@L&{IA?v+eO^QjuRy)AQ7dg6Gkq1nk)8 zpx5BT`5;YJ+4k_)7`&OXWy%$YTb-?h^O$BNSt84#&}0S>_Hp6!&;-tB{clCQg2tmS zpKc2XNEW-4y?HOT8-H2kSD^|2+auKJcC38k_YkbK$-;e|%1VEg#fy^mu3{#w?G5NO zxFkkDJ*AX)-}TI&@rH!7 zD+5@kdIuTqqINU_5<|d~veY!ywRJa8RYpLne!qHg1Cjdxt%7sto%<1!&x$d3uV{i%eh1Xikwqj-}A@e~2@l$1ATY-`$3h$ub3bSNV z%`?`j;((L|@qS+d+2b)YT)jUB&k@=6Rubk)+t zKIfSW8lvj137Qj#+z+iFv0J)lP&O09-FZGvWU%iefz)*9tMua8@Sg_yA!Iy$62q`= z+P0(5DzaLnkMfrF(VIQ5O9FToH}8nL!<^ot_;-i^jBpc{>9JpSt(!J3HF-u@&3CH_hJZ_k#hKWk0W)eh(rm zG#q=kKo$8ytD9RzGq8sbt5g*P_@t|@%A^W_G>}A3epzvME5T~nQXz8lfwdIZNwL*S zgKr8ItBR_YV+*wxXhYu3=r8|v-H`y}b8%cyJ}V$^kHGHswM-T_>TuD_lM(}szuHw2 ze&`RzKmiJ{B8%AdMl)v4@ApV>UbQTcv_If0)ns_KJGt3WaGpn>*xXgsY*VECVBNV3 zPhINck5HY#klBA(V`_NrFR66N?j_O#`UWz}WWq+3EO-fIUxmjGu=`^fg$Y0bZ~_+{ zqPkzP?W?$bAcUr^>|BBlngh=ovrs*2b4Iiega!(~#6PTg2{&_~*VTEpq1^nAPi}$^|;&G{}^{tz$KBHgT*joXm6fj;_{TSc;&WjCFh)Uq$nhpgu= zPf)1cGwxXHVN1L~z4fP@8t^s_)b3%*c)9i0!a)K(myo`L@uDtubi|?9*i&&vi9qfB zT=4zF#pX=M{cW$%E2#R@|HIZJQNZnV5ixHnfR3;@20_He*P3+ zGy~Q6Wm%uF)w_%Kp9&-TDkz`eE*u2ds90biO=2K|Fa*|}H38YW;r>KqvgEoN_@Q36 zqMEWZPyT!rD%NS1n#+%N7yRSkfr@^Zh-XJvGZ_>J7Xtl4_--%DeUvSR7F;|hvC z^dHLRaS09^J>8V(9G~8C+dMsm|EH%ryOp_=0^Y^RmpN1w(g|{4d)p{K*K~oDkyMN7 zOZ{D2S5*h@fVX*(B%_!|^c*rJ;wae|I|EZ-bfQWk+c)kl!a;AxzVkgas0zb-1R&lC z0R0(Sa9h;A#zg(ce7aq{7G2KJ_~+UA1dBwnM8K1jwx%{r_xRktJU4o_am`fqFD z(R3NvD5PeyfN!eCi$pEY!~(cL0ODor>}Yj#Uc~f4xX)rc4O}GQWqF#{a@c!~Gn_=x z6Br(8&7NWTpXOwVNElGeKi$cQp3cOble>w<{LGHHP?sFrzYYeezkC-5fr$4p1>#xE zu}*XDxbf)OoIcTOaq6zQmE<~8I<74(X{;|zjggu0TyIE-7KqwDf0BQ?9&wU``5JeR zYWxeOi22`Mb_jZr7X+0G~#aRRT@kwlS zQsi&GH~Sx~2xKG4^#zRCyO|fUX$&JlCZpf7cW(a?*ST z5lt*@{rywc6Z}{uLkjxA;+VwWzOP!S1ow?zhwJHfF-=-XE`;Fo=s`;ctENt5+=baYOcV&T$;Wj9F+v<(#ptH|9`!`fGoL2HV2q%B|qH0|13()Gw+WjdO zqgW)Te;!f3-TS+bUO~>o?dNC4{Tn*7lgrE#WHPxUgLVlFCLy9n_ynwqR8Drq62Fmo zy@7-^i}K~pO($cItDmclrY%7KGm9QPs3H>6)_a~=hx4HFHmKFb$LnI{JW)Ck7ERBM z&%=rUs$y+;^cQF_Py(dMnEt8<7M6=^esZ5negOg z!^Gm(-+xHf4uEDLXJoQZlT@6rN{ew{>Y;Zxet) z=k?M8EQHR#nYFQ#%k@x`%aRo$hGwLvW_M7JYF{wuM-umE0r0{>9O1)HM^2U{mW>pS z$PCrLmAaWfR+(9M3;|bQY<4yINl~4jDXxG(C_2Io#_DX*Szp74f1Q^I5HMh<5LMsz zD}n^1YdAprRO7?fl}f_R9do>fW6Ehh&uwSR6ceI=+D~2!c32+7`EyP^ig#uCga7vT zua-I(vDXznZzP78tZxw|S6VpE*)+}@hjATPAn;blxA887wxAS&k`-4ogbssw)5Cic zdo6I*1pF+BGhLQLQXhMF=LsMm7m@kw<$qX8u0nXrXypGf5+S4h@hD`Wd$Na=MIQHJ zk@JtHJ0Pk_(}K-J3sVJKAWjV+EB|tJC@_AEkU8c#i~xcINBk}?;|pm`a|kCY4xkcV zxVvY_pT@7FeNl%OIX11|@vz>N=EIy%IW*n`a81vd)&h#?k`@8npdLm;`x*G|gs||l zuXkv7fVLl86QtxVDKK#02*x6^U?D{T!vID=$lk-qp3+F}pNZcq`Z&`JPBKV45r5(YUK#>nCyBcTY`?*PdL z+7^r{i-7d$v}{{tnJ^N-*=QN`xi&VL^%r(oGY9{=SYFrL&HmE{9Pt%ELjm=7Sw)Z! zm53t7L~NlZlbC?-LLIHFZ3hYDCOO=BCLldF_09k=?g5B-ldTY{e>=k69TR_^8xo5Nbx9 zjmHTc4mC@fB^oaTF9FqYJA>viR zPD{Ei|D#h{_oh?Y`uv+nwL8xO*BBI0IIqN9%0hu2I#tW!qmW{l5`!X=Nj&9zS3-Ay z0RMAi^o%};>YriMwE##<7SgaSJ8cIhdkFsMpmm@Bg^}I~RY8fd$Vw;Xpkgsyk9713 zD55(0F%=cKr^5k81!#UG3^|1Frrhr`DO#`)u729>V<9m1fqWn!-Ue19LF_X#NTBBr zKI6ZdxYx13a`3-TyF3WcdX9h)2|$r(d}k~(jUbkiSJ111o6{u0pkviZn_(raw{!#@Y_V~hw9iMWWt1B7q^;yu<%1n@A3Pa z@EbDUXVE!j37zTMb776EtTZ@CQ7mEcyW4x|R?mq{8~+NA2-NogP{-i*@2 z_!E!P=?$DkU)9MWdG)JQUYe4K~`m<%`*9{~8>ib8-t=NWhC_Md>ng^e<{?*@VT zK`>!?_LHOJ9dNz6JrJ@jVCU^BWF?J=Q1UvtQut0fu2aJKZo)j2F15Kt)6 z1$WXGOqU!+QZufv`vQ=j2`J%Yq#q%o!eTeEvCi-E{sPzej=38cU;AROfNdt?=MUIH z@XAaOP=>N(pl*BAad9OVkk9sjKGCV5EtvZUOA?}NXjm(Q6lsBSQ6KN^s&jNT!xrj2 zLC`jK5Tf?Gls)(R88`k_#O{0);!_MEi={_*-QT4qTQB&^MWdB}uc7MHZb3omUzZ*) za`RX&8-t|HWo%Wz_m@XiuUj2_3QP~RWY*l+;AUfPd{*5!rsVpfrgaP!#hQ% z>t@ig8Ci8w7FCrO=9keKuMbC)FON=bzGQKc_a4aKpY9K?^u3>(!g7P+WR@=u*7@j| z2Vi)HzS z(0Z^zO5+JUl2`ZDm&b>JZDQfK3oApeZ9qPFKatD>2=K!{G4zM^V)sGy8DmF5fW{p; zq8z(7Nl?e_Iijv&2}F7N9kA(z1cg_^p*u@hqD+N}V?`Voq_!aj`is~1&yfFmDA*CQ zLeVA#EN-Iv?9^O5d$Nq@vsRw^l)UbkG2obe>wG$o?|LNEZvItI^1%kov3Lxd5@$Zb zJc`J!S!$L3tjs!Y%JVuTy><6m+^}t!tb`)fq1c~M!!ZMse_A~%#rp$Yac+uk?vc~3 z!7wnX=Je~e*T?e3y5HoaAc0D4)3M10wL~wIVgUcQ;YOs!fgsSX7){JdWQ)pV!#Y_t zBs-LTaUNGpCIn?r(CWq82UZRl;&<)g3C2iiP8d77KmQ8s^uYT#)Es5A?1b@*DM<9s zw2+98s^q{kZp;J20Xhdqhl_Uq6c9I$*yf1UKoDxuLuKv`@c`@{!$tf702Zf4gqpa( zwF3B@3Hlk^+dj|*Uzy_?usKOTsLMzcD(7+W?z^ljf{TmkGe}$2?o{*Jk^|S2o!3X! z{$Bs$+|zok`?!@*+PsnY%Hh1}0NQBlLvJje36D4H%Q)KiY(`2^z?EsusrZxQ`J0WQjf-$_@<+e*nR`iyIOSJ&4U z#TtPGikMQv9b~x@6K)r1v81w7b@cyQwv6(xydF-+Ki@{{V1VkE$}4_JU<=zM^_exX zv8*^N?~cY z&!JU@2K^Qv!$5Awt3T0e+A*gQ#&yM~ma_TbV8I*-oZKb5gvz6=_UEvVjFndv?P`tl zw6y zieZM8iF-|GQBIf!@b|F?t#k~7Uj=Qniz(^ZnU&ItgAmPL;0lS$3>=e)MTrp~GUg61r!33vlCEMbL@~LsXh4kwoOPJpW{a(z z0g#{)ne0c~Hg#IHLEzjqhtUy+5srxXKRWlj^*YM>#Bd|OVJ^7rVX#p~d~tMX zmHUJ|Wuq;tIrxZJ5mCM4<=?+Wf*6GHcK+Elo{uHEreotc3wv&+7KMeg$O2?l)MbqR zg<>%~u823e^-h=qdb0;Spru9%iZk;g|8f^07;IkwnfD(Mx-W>D^=1BQnv0Mn6l^tz z>}bH#M>Z!DyK}-bi3&-G4xsT*Re$BcFcHedyA!tt8|0-+^)O~^FHkAxoyl3C!4qeq z;h*0P75C+H-|SP#lA+Z#(L>T^X)qkT?>!qHiQ9USGkOxsC7kVL*j7_CmKvKD$Z_)eOl z(4vn!BM%h~J)eh_0{L{W6n7e(UC2Lu?i$b$X_-r2!Zy`}Mw)d)96*aT0w@e+~gk`qw_tk}Y9J}F` zM_3K1?QGzhu4yv*oY1AEu{;E#j64HV#v$Ji#~aDGCY&rP$WTp>+Wx`%THm&2d5(nd z!^X|8zDK{_Sow1_2KV{>QQADainRi!!5-zhWXRIf@?A%&M3$ylYmh26ZSYndt zT6+`RrOR9+TH44~hr5>i=vZ>uib>IZByERZW%fRfL@VbyWmAF;RlPg>&C_q(9Dk5V zZHb$=?~MxLh!=vcET&K9OO zW?sBAl{Y=}9`2nXq@}NE6|f#*`j|LisB8hanFOdbENoOP)xD_Ov=BE@dW*wPq_5RZ zrKxh?+RiHX-<7YMP)uF9U>vW>|0?6A1DNF!o$SGpc z?CI|@5WtoyzWc=ieC`9$cab5%(hSGgcLg?JoJa`iX9N>nk^)W0w{Avp9BN^*i5rRo zApRjch@@iS&8&@^Tegj}juVvrt0#rRK;Z}xYEzq)W~)M3=$Vl}lPbXelYpxV?_+3Q z-p=cObC{8{cu22u0SeL@_IFE^&Ph)w;9o{QIqRNV+sd`9!+NWo2p}R6Rxh1tH{_df zCDDh|@vb$`VUK!iqcbxWa^*&;;_QoMTDD!pd_4<`Ovd4zCc>&a>_}juw$6dudBTKg zX`9=+hC+F(z*xI$R`eiGXz7(sp0a=0PsAEN_lL<$x}&ke_X62EH!yWjisq9X*N>SK zbu@SkMK4kM%ndOYHj=3GEn)vDPCWb{@jQSy!{p+Q7)y3KNWy1M2kP~y%k zm#*)ZJcuDlr9;={uSg8g0-5Fw5Fiv>;Ua>uV=B5PH;9}YbN3dluK9#q&eNhBa{m!k zO$B;vZQGqTsb>$Gh*s)!=e(Prp+weYD-UJ)6$k=sA=-wtz{avFizG zshXOxK}xU6`9r$t>JAH=us(B#%q^WpSMQ&H!hGPZ-g!i%huJW$wP zZ??Kkm;r1TSwHP)ot*v_NN!s7GgZ72|J3z`3fbr}vNhE@wtX=J4)iG?JVct9? zM4}cEE+SzeVbPO79*6S$?8&Yh$HV8A&)>TuP}Axyqc}GD*PBB4k^Qx;Br=Q3V{O{e zJ8AObh2rCP*8^Yw`i`RdA+{LgMC!;dS6lGUwqZ=VlC#Oc01I%^vb{!O9#{?w&WrH% zopC81F`HJTRMoSgz{IN^a+%|-gyycgj2qz8^hj4Jl3|>6MJ;-v1${BGx`%X01O9%y_D)S~V2l_06l8z`CsZxy)&KJsBJA1L zJA|Fwecv__SV1{7B_HC^D)ZpqeF+%tcZ#jEr%sFIH#c*05{x)!2J6*CJGp|=Xi7qr zN@afz6P|BUh2^)^Xy8g{HIekn=o}cZUAJ-AGkGTe^00-|6dT+TM~)uawS2uvf`R=# zkceBTQ@H$m5H6fu>L#HiCBbSLDAdugdp)OZLqSE+&2MXh>%6oGoK3PKbtxfV{8MUp#Y%=1hPZmAn@t8SLDC|f0gR3`PEuZ^u$5O6zWD! zK>CUDTl_!ZT^Klf=#+ zP#*UtzB~Q6Peya# z{QG>jQsUlQez=L$VmKZIwhB_LQa+l;s#@MJzn@J)J6d>zNi~?zygDqp+`Lz0M)dI| zax}ZR=0^JXryf_m;9v$fpYt=tP)2U;nJlDl4Em<#28{Psb$5xy7k6~GnK}ZcrXhS3NSX=*b!!xtlR`MK1=|L z{n0$&{woj<47LHb0l5EyCwiNs-+rMx8`yYrAPHHK)M${A#!#eYjERtulErzH_HDV@ z6ijJE?QqD^CXy5q2T>z_42AjV5|t!x+Vk~X4D@?1>cX{NnUU4~O9W%E07kLe;4hV7 zS3^*Z1`qQpK#2F~oJ1*%=^GNQ_DeTnVPL_%TGLhC{g{qyjkTJFNhRu@h*Kzuw<2#5MP}-DJm$~ z^};A&JY8x67I5aEyyxFm!L%MC0|}Py6V|T$ZsNTPNW+^`yvvR4Pgj2130m1jKmwYkR$p@q}!%n8P_+2N~~fOCNID`bkl!iNDviX%u{kQa%B8zD|WIT7Cb~>)5gs zQ-Kz5j)G8$>ad0F(f<3$=FMOg_`tU;OA5j{2dM8uN0nWYDHB4ET?W3n6d6nJFMBY^ zm{W~5Zlh)(UPjFfjV4y&Y!Gw>=2x;HiKTSQ*C!!w9$=Pun*h-)R9J!xH>;}=(8%Na zr#sd~Emcum-z`r%oie9CtvFuIDjQ=Q77k~rIk9K9tqeo{two7G~&)_;< z)jAn}Kfl*CJH{xHRFnNG2d88#AuT;`CE-WJdNn;gg^*^r8+oB6b=5eh5ajpVHg9PP zRU}!3&|2<7J>|;jaR&Q1dNugyWhEi(^0KQMWP$_Ao!Mt46K?U`em8Pf%DEt5#fF7h z&T0-Ev7xWw{!6?VOavflf;F?dYlf5Xu7ow!aT?RVZIGpC^nTHTG->D4+fSM}T5+J5 zl7BUG_c}lW~?6WW$0b(vpF_p8X58XKe&g!b8OI(`esMnuNpJQNZte z$mF^7iHG!{ab|(4&$@(9*sAWzF;dI)@<|o?o+d$0I?`Dsa4I4FeH<6-y&ag5+V^cc zwo!)Ke;Ipeh6+0GHpJfGK;kjLrf2zt7RUHkAew{+#*;@*O&g6AYgOAOWRKG@>5Q=u z+V)yDG!951iAf0qO7!8MX!|dDkU^8RC4iUH^uX&HeuwvKw7ikmmv3Iem8cs*^NQ-v zs&#}QpBqodCt-~=ChwCfW|O$;^<}S%{ZM6DWcqP>Rh$nlc(C3GTu86-V&cunODn=< z=FcwSvsObIxfB%*!uSxvn$cRdYgB*t%OK&n%Jg*gPYt?ln`XFWIhBs!mnVLy23^d| zH_xP(Sz@4EFVYoq599t=mdjI|8>;XK$y<2}!&Ew7jMhF(q)9*l&pv}I?oPicR#iH; z^fk>Fcx|7q9>mJ*hxx72_*Aa9*jQD#roh@z($Si@oY606ldLx0P&@JR9(PD(@iNgWPVIn_Du8ZHR_%=`$Zv8N{>14Z=i#^_?+0(^DsU`ctR+ zTsb8M;nQFuo!kF)Y(+7V0(NOHxBWH^1_bKLueJMN%4UXntglXs_`suAb=2%E(mH?k zqoM|<36M$HjWLY@RE~`^t-kLabG6pR42}+ut!B5V;ui4gMK5a&B4FD!6(&u6^iCiR z27C+J9Pb1HEu{9ODQjj&FS$uyupv%CW?`ang5KxTAY7~6(=Af7Fv31&35{dp>4{)+ zNME=5(AfUp14a-~2tB<xYsJAB{ zthIQVK{@lw^bS%p=GsJG<9}#8#}EsuhrF&uRGyy_73u5pR|4}as(1)!#ygx zeH;P_N;cFjYTHhyx5m&gj2zNFxi84HUkUe|C@F*^N?b}91d0k@K!E}&PBO(@aqwPaOux$K|EryO~8#%IA=0dg+EEUmX(R#VCUB38y$faBsmHNB>Xm z`N9(qE%!xpzkm3GC&(DwZ|GylF?L17@@b0$>0{`xT%s`St+jXkk@+nAt90qO@)E`G zo_Hx$;=$m4O?q15B9tkr_4|oK@esw4gzb%>Pq6*fnn&4P6x##!bEi7WZRpTA_6&cm zJ?41s@*j^y?+|?jcrNW|!x77&Mryzy1D}iTD-_K<7?2%!o8%E%!1UyyA_F<8$U%GbNttf;4^-&t-iTmX_3fxYjD(Zb2w zreYxa?6dZYM27eE-ity3*?t0~AMbF1v<;*%8kGZ9IU_PpkRh-kYBvalfCYhjIHEA& z%jP4DrA4N_;~J^hStVgM&a539Gdb*TWrjy3mA5n&nH1reIB@*#@-k3!n-x&3M38_mE7}A1n21zwypK z?G{D>0ky;yR;6TQ2LYUf*dmkV+={F%os?zgbul|VC->sQVrgKICKY^^BPh4Ca@yAW zlA`TzXyJ#s-GUCZygOC+O7g?dda_TTePyMhhRiMkrY2(Y1#~6anc?wS=U_pMvh?_O zz9?5Y+XGG+tqZG+7)SL<~ZSeP}tTZA53AJrpIqjUQ-W828+I_{u zJ`4#e#Ej4MWmQ?_{sI|qu{WDm>(Rl|67rg*2~1;aZpR$2Vz(k91r-6vNEp@pK}y6r zwc)?_YCCu7@865R9M@=X2lFIUPC)`xJ z_uSj)+WxI(i3Q{Kp3s3poWj?muGh!h5&eBpYYOdlp%#Y>YGc#ySN+LL z;<^tEVl2)-BooQU);wc80W%6lvvgP+s2)Lq$l^PO}W~H<%LG++4RY zr;UH%k6y#U;u{k{G+MTcLsYtxbywF!hvyO~( zb1AlTc;I;r_&~f`@qRpf_bV7c(2=!H>W=9u#et@UvIDLtm*q{$UBqzEd2 zYgx|g*vZBJ??Pciw!#K4)2Xq=bE-jdC_Mf<_7ErwsuaP3J#-N8N-pzyyi3CoBZPs3|2S?%8AFON6MNAs_jB<8|>uJWvq!2kc)orCpeo)>RY9r7OLDX+QHp$Ze zp`KxL0G4QhPLsb+s&c1>%3F?h|0WbRi3Dc#C?s~&oI($sL<9NQ#ml_whOG9j6rg+u z%G-A<3CLY;4dPB!P|u-w2SWL>z!*Zt)bz)}BV9W~W{D^BNyu^PS1Lq5u|7Y*_M=p! zbbMX^z|PQ0rAHm~GPtlByxn|}NS=fO#=D*7twC|5kg_76WY@TDWy&h<$OQ$Cx)nUoY6m}uDLhMQT9R8n`!@s)wSB6Zt8?euiIqzwvG?EA) znMJ|tz2Hv6CW%F--$-O>*-G*dspP#xVsP*o%?fQ*gR~bM0%piyu0Z}aaK{%?caz` zb}dgvL(o!lVW1 zge@`&e`yYP%lI|tGp!_k$_jm`S|9R=YEmzqyZaitF8*3TaT1+%33f(8RI_wuj&t=X z`X3g|_mGemlotYRP$d6MB$33Mf#e!QGn{qZq4q}mPcm71HK7RAWHxJ;nu?>zhc)f3A$Jlw9hVIsxoCpu{4ow)zOVdw9$k^`LRD4Qbzqr}TdYfJ z&pG6Dd03g)4mKCp9lo5;mU`uX%B}yK74O^zqsAXLMYnjHUf^DbSLK9=y?fedY&)L? z)moCC81cpD6A`rc8D?AYGnb4mi#WIP(r2Uem)+ryTxJZKpO1d=nTdy_RE7tCS+{;y z-*s}_(0$~XJc@CX!E6?gf+%W`aYPy&A+8XU)< z@$|BgUv|Tae)eNb6@0{^vwc`T=scjA|0Jj^?D!GAy2(vnN_Jk~k{eS6SE(L802w5} z@*HTETXi-r^>L>%Fb;^_V36F$R#g&ht5{L0bI zn6W1W}+1BheQaEX7=Fg>l6x8Q2mlrRg^5=4*gsPs8JK0;+(h;kY_z*$B z9=J}Q>)1@trL69&CD-apfqi6@Yz~8p-Vy$xN);cct%cvt!st&0Ib_U>*8J3&U1^Gp zVK2qFM0?(eX~{>fMMbuwE3n67xWH*$*UPXxOsl}-H=BM4_9l*Jc{4I6a1FepvgYB; z`jFwzr4?@~`2->Ls;Z8PFY6}HN<&vG8dBj&gAb49i!;q_N@;FirqFbqSYjWB!&Gut zy4n9w4T1!#ZnR!M47*f(giY(>tvxHx=XRK`WYPK1o`a~Vz0?Yu-Ez&Qd5|#wa^#@7 zV8;C_X!-#Y(oXN8TB(zmy==?5W_<3?S0oXpayK>LGqhh5cD|u|-YheV-cPR3dsVqw z{!5WHL} z+Um}c0y*ENbr%|GbhtJnx_qdheR@Q+REFp3Uey%u`s%PCfwBxNn6QMUMuP-Rnyadz zG>#x)itG})Dpl?F`2_i_@h<9bq1)Wb{F;ol_hWpI3k-;e{35X_EAzp=qB?JOzxPw; zWs#u6K8Kd|-{2qh;?X#R@@rmPuGN<~raq`4k{>u_ohxpZxtE~n`~v_W3%Wffz6az+ z&@ak!?gLNg`(&W52QXRIVG7MCca=@CQt=W6BuuDC!GWpS$odv>KW08EVVx;*K5DGGm>{x)pJk#;Uv=3s>1ad z;y&No8MY>&5(R({XVAzOy2%$J2nZi(4X~rOg-(;VIe_E$+c(1jLwZ-vmPJ@pZ*UHC z?voJown+brd|h;Ja=3KI`AkH7gdzYSuR!RSfR}Noh6%Xd4j}x6NyNg$ga$#h!cqYN z^M96h!)`|(Ll6%r2nwKse&JU9$}osBh5+?5W$iJ7fy+77wQbK~(Fq78d#8l^_Wu13 zvepx@?G6GN!jQv!{{t1XXAg`65{MCDk3%is(jmlyy_<)EQkEnZ2I?7zw%_>1e@isT zlR(Ya0El~i;J>`H19Q;F!AAKQ3e}k>Av$^*V$@4gV|u5Up52^Y@i1IBzh@%OGK-x; zK?aV|8x6^9{aCF3yjY&dyD*I+wH*~5eLSRq#=oL~A~n4>enhSI2|aqC;}eVKnddiy zI=x$CT$aOsUJPiS@ei02&DCTy4D~*{AYh?ol+j2EdZT^7Mt(ZBaM0FqOL1!`=m={^ zf(sIU>|mo9;H+{MA;dtHPdm1T#W_s|?bOEg`wgM~^J+gN$cIwx`TF%fgU?j$IAH=Y z`!OB8f1Jg&&ea(ym9MLmL>m7-^p?Xq$^T4UQ|BevHI>=(#kV8eI*`+L@rggp10B#` zp|_hJ@kflLr&$&tWT^j!^V6JwT93BN^n<+Y+Cx+M%pK1Apj;mMAcf9_CN)MWJG z=ByPaN)X^@mU1m9b-MZwz4!BsbdoH!ckvc!Ek~h!;7D zLD(jT-uH{reW_Ut{7-+*Uiz^r87l_-@L=WG9tByR53)Nhzx$25JViHJZE21_AGfe- z{zOL3xLRq}{w%(1B~|uSkh5f}qnX;O-z7(n?iDq4@(ikL+hDT@))P(1mN};K@PRm* z)Q>>gz2*1k^Sg+Pwv+2S{0~D~t-l{6R&|HeJc5=?SU4cklcU@ACpc> z@F3tBdC~pB^IKp97~ucF3<;%rwzkIfPWF5-qBdymao&u%eW%{_O{?3$P9`ru&jdS& zxR99O#}o=Xs;s;&hq%*8SrzVThbP}%NvrL5`;rfPGMK&+OcbmTmDyZghiCX5yy{&g zWsZL_9|8~e+xRtO95nE74+w0+JUjOxEI*dJH5DGfK)+GNP2*Y2Rs=D~RAEuFA%BSLf$%#0m zxqJN1ZJpN7etjr24t6m;l7#e`FgaWAlA}{MQ=0*Pebj}|Y$eXa6K^5=##kyX)Z`5m z#6qAHpfO%3gn3<-G3? zd{3ZAp{AOTuBu*i5;~r{i@hJ;E)4&k-EpZKnreE)ga7Y!89-5bzTE*&Otu(Wk93Y{ zTEx>cK#+2wiA{GyN8@i^5XY%MFYjf)rd#B^*ZJbME%E2D5BG3Ck0c@J*q7tG*M8>D zHVn=X_eWp(uBV5i)($bW?z2X&ry-V(2XSSX$hUN+k;C70U(QrecP$tOn=K|B{ROOn z;$~ovn|1coD2fUfYO2H|M-nHkfIw)d!PR>c@XKk!wc=T?D>(eqMYQ}`(f4THEMTH` zvy&2Dv3w|^h@0@(waivn9C2ym^?E+mj1GORLk1Cn+nHj{5ZB>?J~6@985PR2erGYyp}^ zqB?Q00#uM@^l7{smPKit4m4X9xi5Ls zU9#-(S84;n#agOGgG+7f>R-y1>PprCMf%#2^gArUwJ|NLoh-}IwUxZD>YIiG-$>QZ zs_4FV{TxOa1QE>_T16#eoHjlvGhbbOxIGP0zv?6KPlf;qRkGn`?tDs@S}h>18pzP! zu)_k-r={a3oy@lH%dIf%!j{)pi9J@Hrb{cXw|hTy2~Tv?Pn5o3VeWM(7d^}t@v6NC z!3mj4sH>~(Cbkr`9zQ`k`YzWT4mm!aM;Xe`VYjDVsE5*x7+LU#4q@w*Yh`3?&xhtm z3#gs_T~zr<&H+E*Otlmjk$6?LHUDy#1y58gn6K4HcUnsqObKQgF@K|x)}yO;_lqC| znMe0PS$sp(JURayFjfEPPvR`RS#;L21pX(TzqrYP<%WIU%>&wK4lxz>5hNq+2U5}Y z#A@biG+%fBE04e8$V(j*8%+b{gp7_r1Wb83Qz$)zJhHJu9B#(dB^rOl?hsMrW5axJ zbURn>SGEFQ_sVy-lRY*rTE;CfL0+jU2zIV_f~6Ac&-e3fuwK`*^D}KYM2O`79n(Ps zp#Uh4d)Lq3ATS->^q~u0gx1t9tI6T!NY<1?_(zadbW(EJjfX5w+BSXgx~ldm+StS) z?>@wd@Ar7UMMhtUIE(*EB~1ZcjH(U_>W%jOdc5W=S!w#JNcI%KYH>d)_CBYfR;9$7 zq2F9#zFB8G(ii@r9CkRG%?$@sI%GsGom~|--{_Z-0Na5S+>vPHQ#;ox7B!Jl3Gw*o zK`r0>v{AaET*O&D8LbjO{UodmlxCqvJ@541| ziFeW%LyhKlT`{JS7I!t7n(>qH;34yreMWqZEH)HS?l@CU7}UkR9XBsa<(or8-bh7*GaZRww3P4N1EmSm2xBtlUi1r z^JGo~1zH<&O?%XLd{R7+B4uSepuGI(P7J>YB^=c5LLJ+$rMoBjgfJ(lZj z{!d`G2Lb!l0M!6!4B){q5`}8vFx12i+GBc3Y9Y==L4TinsnTnW`nphW$1yX#gB}X7 zP;kA-+azfrO9~Sq{8Y*$ zTj+~Ry?)TaFG+Gjuz=97TVWjI>z6l8r&OsgnI(r^;dXj24V^@x*4Xi>+0$v z!dasI^U-*3HdpYX_5;!YdUf z`L?a?qOO^?Bb9~El}O+~)Y}35X$ToV`z0`$A+e)Jw}z+|JSB|lxOC{XI|_d?kpOKh z5Zx09pGQttnPC^Ws|jZ{G_@+$v-6F=e)_~Je0;X=8V1s;Wc{*NE<0cRgT%98KSxIl z1P$s|AP6H+fW?2uDJkK5u|$VNxn{p15Hz~!umEsZkxdC^P`L_aa7(7nqBhVwmetDS z9YJK>pT7i%x6!xowGqX))7?aI&1^>d!3By{DEw%e%UI+3qnk#u^NtZD;BnmZ`2+#v z!{YGX>Y(eT+l-U+pWFxZTk#E^lRaw>!mXQBdh(nn8nJzsgP4KBDMF0s)Cb!g%=GE7 zwHUL9F=`vwXNLrvIs6yO3G{V*$1z)n&sB#_73=g$e)7w+e%-^%q{p4K0=%OSl{tr@ zvn>P70K0n=zB4RNP_Fe3r+ct=uy!32xfqOpH}KSFFl-h+=cy#Hq_}>>Sy9~y^w6M-3?CH;x=tU za(!`XNkI~Y?A9|9Pf4SoOq8|4a6cL!G2x&nSLdMpN&O;Om>)ZnTcWPPFpU0^3C1t3d%U+ zNH+0Hji9}rI0)LEwS*=fk9_=q@JV*?} zhlr$Yc&G(p!c$S(4HRE1)xwBX(o8t*r!x?kBzu5f)aD&So7m zDl7{n@IYtAObO{rsDcHHLy&{V{zV^SrF#)Lsi)AR(c1lf54xYKflroqhEj>3i7=tn z*8nBfz>Peu^~kn?fX>^>Y%^%~NcyXiZmWx1jzx42UP4J?6RVg~c4l^oQ-U>J4aFXY z39>~A>q91Fq8wH6*vuT;f{q?Psk()YZHZx@RT;0wL)$M|hANs$wN%OE08UkSYp~pGME0e>3c=t4lwj#>6MC$+=x+sj2D*7{r$5$}@&Ylqg$XWg z9shP7JTG2=J)ZnZK;4+cKzK@YEsNXx0%%|XQ4!vwN16vJ>rCF28Q>tAIEMtTqaKaQuUA$Mp?^~)*c znA|kAX@E?8qow0Z={>UnWpa3&6_!=vJ3&kJosXu42uTFWHns{yb1}7w{mPEjzH?t` zsWq%7Vt_x0w7ME4k3=w88u8PKY$~dsGNNKA3%>sD_^`1dkk~m1$e`6K8IO#PPrbA5 z$7>(qFvjlJ#I3zzuVN+}2Ei4IRT1M1c}mB7bmiZYrq5K@5Yfoy%dna3?iCK}vWi=8 zJ4=)N|G3ka=6R=?CO^xGL2=r0sq8ElbpIf-0KqSDxBj14DJ-Sd8e&sLTP)Zs)_%AS z{Xgd3GAygF-P@fA5`uuBba%IONJ)dz-Hmj2NOyNgOG|fmgOs#%clSHd|Fib9-u0~G z*vI~~Kk5PVp4>CW7}q$@-=%l;GiT%*A)G*gmYTf2tP}>kK$0N{K@BUR0KT&MN`qfx zui^*wRk0D(nsy~CXLFRQAjvl{Fx_BSQ4R_r zUyhCnVzFCii{7^JU@FB8B1#5%j&a6iKE3c9J zO^s|w0;VOx8ZuOMxwb|)gJZ0N>VUYZ8YV^s_FCbCMkWg%pX-c>^UrUiCM64svOMjl z<@^K}vyC(FVEJyoD6|&g>?C43x=ZOPTJ&g}Q2~*Kp)VW51Q%A4R`+?%2XB&cn)SCp zsjJPBvWO-2-A0OeDG!><5Z8t!#cVbe#V>mjIdQN2md)?Ov5e)Muu_Vj6*bGPOkIc5 zPhIFC2MDo0O;?m~ecfE}36`vXmYZB(4PrMTv@Mn)jMmYRk|z5sI6gK=zv7)2seYY4 zv>x>O81}OQnnmW&X0gx1UT`jdM(>_IZf*GI6-kPV9n(UOeA;M$Of>Fe_T@k>z&B~& zUH+>#FB3@$gn;^pQG)VEQ^6PeE5D8vg3+^aMmg7p@06d%C4Xp4f-QtF4A24nc+DYmmorNJOCx36FBW ziU^Ogb$0#@P6oh&XWwelAC}<+g%x`MFg#@~yCcts*4X5gBwl=clAqf5e#+A{pWdhHK^rV{y)goXMAkiPYUu-Tck>n$ANyG+SN z;@5-zg8XD}8?5gdK||yh%e%z~TvQx)`W%)n_s>|Xy%EbbM#Hvz>UTDF6EYE5Dd(N7 za{rQdSJ=ZL@w{LVKtKy30Q|>V+Zl+4`;@JKRm%{lfU|$|P2#zK&cXiR#(i}KRaRRI z!FSTp#68M)Sr=VDzAWMxQUAIZtR)?O{#)C8mazy9QPS7EAb~a~u>2gp=1Z!^5su$L zNByi?Li&?(Gpc1pbiOmD8_$jyLlQhkBJ)PJSGQ=_eqqG)KDzlChGcC*q@3}0iw$%s!B;zo_&?q9z}Ke6^n z#LtL~WqCCGNjc8stUhCZbdAYP2pJmd56UrM5XHY3Cvun}cnJtS3}_6n;!9L={{e-p zf{wa3SO_I5v`rK)c$UPDj0lr3O0;+GgKr4FoaEEUOuG$gG4X&kw=C9bY22Tp^It_b z{kL~70J2heRybSA(!Ogry>PP}+&_z9*EsOdgN~Kg#mM$rL&{Nt>=4v^bWSE>JsP;2 zh^@GS1;J`a?(=M7nCCzoEEhEiY?Q-Vr6cIOnRR{o>(*-2b|xC~o^Rv}L(g6}Qb@V- zl@%$+ac?3({ViZQ=0sgF(3xMtXt_5$Xd+tU6NWYNKJ9*`d7W(Q{o&8WBm?;a4oy+W zHFi+&USWrhy71<}L5C}vwA%NwG>Ap_#Xq0icFri?C*M=ML`RmV|$z! ze41FGoxbgq{^n%IvwB0fzDh(|z#XuHF?!Y)PvGwf7b6|j%k$A*eZ$gqCz+oRy!()m zR*t?4i1{qD|9r39S z!Z-n7kzTFMYFsaPXL0i4(>5ZLFix-TpE=D3CgP%1`j;-0UJY5iX2<3-!}0d6#geCm z6~STJ1s6L~JCv`EIrfm^A!N!%T-heB`AY*;3;r6=Kj??>s`0F<{Lw_n zG|yi;GG{iz@_plXxf)`8EROv(m{RsXRZFm1M8rYM)=FSoE{=Epix%zbfP@}Swv~#H zO|DFVItSptI^pqi1;-cy(dPayBhoVk>-=-(MU>Ret(kEUKqM))?WwJlOSJ$N_}dD( z8`a@z(X(Jd<*J!G4actW>2}ou01NAbaRBg*bshhg+0TNUsyj)kS+%&=nq*A=j3e)% zz>?#q&o+W#5_*y|1wI#Qo|;Y{S$9Q6*GdJ2u|%3h%*Tq}$9o2{!4RE52) zCld^Q{>Z(eK?zXif`*lH=ypw#m*R@Yvsm1yVrf$Hcf0)^WvIDZRV)T36sRR zi^oU&8@L9~VFcfbI?6fpZ+amB{}ll`MD{l8s7NBTcoSg)==78}y$Bhk^BYU3d&er` z-iqhz(@9`g;xZw_=Dwb{IF<4fo%?La$LQyb5=X!!pTxUcyVnZM<{F!~X=sM!^%QDs zUmO@r`i4j48>rxwTCZ1kQ+;eg%(NYSK4DJhpR5wQv*i+LvRg9jhUxpZtAX=*XF63v@?I~>%wW4K#O@Zz5 zY?Slfq-Kwr+O=s?+U33dUdSko(`HhfYq19a)Q=<$c_pZm&kZeoDo4xgR`XFP8O$gOPFB0C;9lj+ds6 z(){6R8L!BSe0(mMcBQ?vyQO(=X?fVU=-ji#Rq1-MYJ|ppHUT1NM2GNy@gw+Wam7o| zOOG4WRKBB_*!h50y2WzZmZ>vH(XIyYkC`~PR!pzJ94jhe zPlU*_P<3oJ+_(ymkq3R7(W$JJMfmi(B)snqR0NgGPo^C~8Bm3|#D!iWpAe2MlMPe9 zbin5n)Z_ps>Scx9Wi_2ih5D~O-KIT?nzFOMb=wqF3ddU$m@^^c2gIDm_N-vHYNs2B zY8Le6>&?!@5xMSxF;xw+8Kp0pnS93f*i08zhB7Ag-S9w($l&1AZF3&Nuyul72f01} za?+`}0MXQHJU+$rw!F|+rBeJg_19=efr4Yx1E;(GiqKwTi9J|Bj#d(8)R3PvE?YG3 zKDs354NMol$DBM*T8%aZ|G#9zH8wd|JqFa6bkGkKLF>_+WSCB5SNQKXpJN1=kI@VI zC1{-hWGDU5s1&pvXSREN!WQ!2^~wq4=hNMF=;k-Lv(W?;`IICP)AIuZfUV5nCbh37ocPn zhj4HKmHP@4!dD<$kz!KKOxm(}gJoUs`~FfXpS zevK!|VYsY^#fY-c<+=1G?u*k4?}7)adU<}33f619HI~m&-&87Ew=0@D#0`w<+=Pw^ zjupw;uW9_8h5k?I3C!K#8M{A({|YMGT{nt{7+s&kDWO0R=rW7pnDwUCCv_M zE;O?l&0FY5dDxiAXEfsyu1dc`X#bwpAm)SmM}s@gQWr4M^*=&Q{WZ)e-~_RXuejq?__?iXg+2==N*QDU+1^_2ShBm9zHFg?ZX{&V9Q=7H< z=ub1%w~i6$WqikbY|aQS zyemt>9$^$K(sFfJVi}L1^2zGj7`0T*{>IByZsvZ0KR|EK;OmP_=l$9)$Ot>2;MUvZqJ!2%O8%ZQm}m5 zo^iI%n0tXtjIxTW>%&9XEzqnE3e~|=!(u*dv>Ju5-5u{C9gW76Bm#}Mc{Nnjnp!qx zjAorTw<^(u!)!`0{CQZVrsjni+p!%Vx;B<#$RO(%JH1~i9K2x4u}pRaUVYE8O~cQ2arTz#)%Y5qimg0$;xR z6JsT7Q#HwMw%)t^Z3Zt;9@t#w5st%u`e9ALM-`NwPJ}c&iqA1E;0Kg)&0|9V1kheO z?&xKHhNDS5C1DM8;y*`&Tjm^I$}?#5x+b?Yqr}9P%dfEi zDVBw5FXu|9x$W^qF{}7)p>b>U7bftn)Ik&DzL)wxVD4-&$DuQGXyWcvf;0CzYs2%X z{~+cKYinm>w+3^Q*WW82K-|WxN!R5^E8p*;{_69K;662L zICym7THL#=r6pW{_zXI6)kgW;ADmnQdK3q1zRs;lCqh2k_x6h1wj%?n_3-u+?}WY$ zUrI@B2>06SE}79AbjL3Y2FLU|HqYaUCSC2##EdknxK?b$Z?S)-lrcy` zr%j^WHL<84qr}2k5A1Xhrgs`nWaPp+)hg(NSvKlj<|T8txGo6bAw-dq5*ucsPh{i5 zYT%Ss@^D_4cQl?>KeOEON1Rf08-L9#$NFI5y{@d$-M>9W%9rH6h#s8K+an=3yzHjz zGCn)}BCziUUbEj_Jb~hMRqd1#D~?%CCu2|-ucHcY9V zTvagI+@gv9Y6O;~$?&YNpq8*0I8Z*%?1y zQ4Fwx8ti-wrYngLBEvaJ zdIJIRxTF4EknOPdb`o4L9h$B@wH8|*Dy>M(Oq$uNp1dB&z`k4)?CEYcr71-gv4r)! zhgJKUtFTHXfQ0cxJkMGyeIg<9ba0u*dBer>raau%;W92T1%>k5Hr9P+H&rlHheGr{ z3<*8`uSn_=zFC`gXNfl^--`}>_ILa^Z~Q8MI=h(M^$$iTD6l;R>2QvwMwe=ED7o$q z!q?hX>b`C0va;1s@=Qq8-K6Vx=RNPwoiLi`y0w@s1RaK>T$Q;bDc(dz`pl~ia_@6~ zNCf1%-C`rO5|5pJm%H4y?@up!sPlS#xsrEQHa~NcVp-jDEl;_Qr;M%bYQ~a9jjEl= zPZogA&!|`{$L94;#Rri}KhRc;SH z)%1=}I$9}Lx(Vx~aZGu`35nmdeWGWX-!i7R4f+1!)w*Mdz{*!O(abq8T`;nM8`h{I zB2g<}f|%a)b>Xffd$G9|NkJpXcBaj&J8i_qY4E_ExxaccS(JuYvK z%fW`QM5(4%0y?+##cIpwcMl@0K;w<7rQid5^xM^+np!o436GsEgFMU`bCFN-RnGx60285s6iVj1zQomDsvEdGis~M;-@djdE9Pr9%+$)BKK!A3si&=Rwf#cd^GLgxc zUK#zeqU!uW8wJD_m#Nv8o)XumxZmNLPA?3&;-?zdTOwGkABbG!ycQ)nlt?DNd03(-7IWe$|hn zd7jb$z{HgDtX2EOgeT1^OhE*8##nytQ2cZ)6!7bl>wG`7js?}c^{znLk8$*^z0Hxn zRBZf*mmrdWii-=0tm~@A-3)wg_(#)F`x*n`<>5*WT4ni^Z4S4hX#p?T!9J3wn=xmG zY??X*lUt2e#ks^#R@EC_v-xba$(h&ttMB%zy>9wreFt&VtmlMJ@#b%8)DXxg+=?x< zmhTPdxi3O~8fjfzv8Hj~=}c{e2SVnhO}7bjhUS#kz0Jxl<*2jTE+w2cGH*KevA#Kl zGV1KQGTq(p5;{&ZGwg6rc&bVRo1{;6BqY#|2BdGsk5WA;S{~tBEzay86ztskZjKfI zy0lEw2tHRsM76>(Q$3x}XF8|N2;&w>HQg|SPp_M+8|CbPe<_l{<`BfV8rlZQAA>n9f zrRfineD1l&7`j!Rh=ax9EyKfWA+fg{Vj9be@*HXIK-v}`b7DNbtefGLT`H%mQ0qQ= z>wB?pmAADCbAkXAfLbT(*zb|{@*ADoYCpqh67KSJW=8hDY2o*Vg$0;l1)~hn_jHqk zdwc#Cp-!W>>Pw(~OZaCc`ei#>Qx=fG0JgzAcc;7-x&-0Z}T zuX`DMPG=%z2J_;Bzh&h0E=FEYafuvG&3(Ep6O2&gJsuaJiZQ5K)Wu#|`QGzYO%V@g zN{iF<>Pz7wbFj{ZDw0JdUMd3}3>y!dUA8%2HG+M@QOo^8BWi}(wi8V)j!tTnC{n#0kVyu%_+UtI%=#M z7T7~N7bEnz|LT_*-jjz~Q8hDk##NJHeyBGGHp8Pb8Y?n>k4_)QP!+j{eg6UloA}TK5%y-(3SEj(=u4CQ0$dxEd*}~R z?Cn=RG@Qv^h+76C%V%eH*66y?Xk<`y@;CmD0k1CDX3{)r4_UhUEv+iIrNU;!Pp)(EsA48; zN65B21+?xqr625&1{VVm2zwWsf9lF(nyqq$tCXIXN2}SPgXSGlju;73f$z>P(ra5s zpPzIWLy`l18cdcJZs(MRd{C=Nwu)OrY56Io$cB6NtYScilKX+XOl}TY*NE?>9&L-0 z;O-aU(JxU~F{K|QSE&uCf1&~e3-XT%o<||^3=rbT$`qpZ(1Irb!>vw$=WL=jHj#^= z-Tr>Q=R-oiC2#YOj}pMGpxv2@7R5n-x(wFnVsS4b%>B^s6~{ zr4L_zjAlcDdy|69!=pL5z&7L^NXir?^n&5=72|5`WVP8_+<+GLFMH>gl!~nvZmY&0 zjc;!A`$MLKZDi)BtO{>(s~G`c?4aQ8VVqM;KI?Hu^6Y*Ho%@L6E1^JUAZ9WG!sWT2 z-dZ*(@Bpk{Zx=vKEWJa3{2*Dyl*p<~B88x9iYBwE;klc(1=~4jZnNCW;Mc?Ztzupm@|?!{NcO3_8@;7~0z=rX~@i$}bWI>7fOPJJEM*J#7jOhU-l@Rhig7 zb4kbD%o|{yn0wB7%#9C+-+o_zESB!FU09@&;${|0;UqRWygm1JR7}{)iJgv}-_|5c zPQ|3HVb&pdTbZ3%lxuX3-F;b8<)#=Hc{+cCSulgh7G%GNHWXsQQSl(LrJ!0tu{LAo zt2TRJY&3(l2^!rK!@Pw&O$_EE=NPx@%(#bpk5--dd^m2Z+3>!wFEQzTEYL`P7Qo5E zK@vnoc>IDT@`9^{09eN`&)=&IF4r;aJNr7#6~}RiWs+rdF?jZiE%&3X$mRxj8N-d~ zAPbZCWfT7D{NX*qkkiwRGWO;LK~~(!YFqaQ*esC{1XDQwYmaYO4v$R`eh}W*E&LkK zwPbLy>74nJW%LZgAG}l6*wbMUdi^+(I&!2C@Q>!pLE&k@ImF(E5nQ_!$C*Ww9GD-Z zfsW9eaAg(cDFqzkB7Zs>|lg7Up>R&sBC)TVfQXx`$MX}cT!zA{e&R5VH@5~at6U-ORfythPy zKyltZl{YY&_cMeBNQCY?F5uDfyM{Fz2y6=`T`!Z_pa4lV!4UeBohf?Bi!}2yM0`*A zfdNu8r4jK^RN-NHTFpxB70Kxtq2;QA%tPBa`@0&?6BSfum)okc%Ew>tgLE8ZhnyJE zWaNg!cdFT5b^zeR&BQjMRvaTDmW6`OQ*E;m`ioBME|SKvuJPd4eD&QDujLV5bI<#Q z(*rT;lw?+7*45JfDmOhB$-#^oPv^~DNV<>>uST_8zceGq$Nh-b`+UB~(~L)Bk2P#d zrQSETQ%~1fTnp|cOLIl)$+toXfktGxxn7l}-CQ+4Z&OlsQv#i-WAY?EARrrC*s?de z5?U<5Grx^uv1v2Q&%UUPxlcRL2%TBNElDZScN-=)^}m~+$I&>E(EKFzGEh7%t`v0u zt%sck^_=mWD2`)F3WH`!CGBIuP+VaF3IM(~Fh z*TB`Oh0KFdi&UAx;qI$*y9?O^0iH?<7sFlZRy9Hz<#D}A#nhY*cdPva`WN-1kGJ%E znAeAOi78)M=-4o*H!UcVD*nzJJLS0B93KC*#-~ zGr`GSGNI5vqe?H6k@+|>14fa)tJNk;M>z}pD3DsQJyS+KVqS)AUcO3kU`F4JGEsMQ zz=#FUXG~%pqNP>SWijg*dh2m}ZZpzyc&4TJy}Vt;HI zW?wGzxNs+ly@Tm3k5My0@@=_9IxGB)@w~xqo$#0c2%=#?P|Ve3hsNkFCsOaS#qrls zUzhpgYu%Buhl4xv&o0?No|^~wNemv^hzui@UTbv>*fCq|IK9q@zBsrqtohD+i2niw zCtqLuQP#wuz!yZX4hAeL6T})|h37;_4}$>>CZQwJgy{O!(Fa33NEx{3erhgNbMCzYKCI3H1u%7=$ zrVMH*OtV_yox-|!>}`y6LoWbCb|7M<&7XsjZq*qN6)k_|X2eqMt#_m{S@#+VQ9YFK z(Hs9~>hIZ5p`-n|3R05OwCm#t8>At~d zA@Jxws|8yw^66X8N*@1zI}`~HjP76ZWrG1Mb%R&)M?0Si2(*T-5lOrZ5Rjzs9gutD zNXh=Mhvd^Yj%T?Bg)s3o9whMF8xjE$2uGhbc=z}=+Mehwb(i}X_UJ!%+24ILH?3m< zI%%s;41WmV2e(HhC75E46aEEmwdH%5-XC5GeER;t8!hXv_XxQ8e7!PDa7lnzgas zM#&gbA!EenAU*kTp=08x!$tuz3`iVE$;47PFKe8DkWU~&qJ_ZlhXM&GsANu8OD+_5 z;{J$Z`n3OKyY3Uwd^1Qn;7rXFpC=t7*0FdHUiP?F)Qa?QU@nk%aBbWFs@u5LMscG_ z_3&6`?stsIJkmc=x!QOO{Q3>WUm)F^#>5LMk#HFHaa*JG}AMWRxb5U9&kIid_VHMmc@^kb6ddx~nhvzb4a%)=*a{sPN zMFt{|y>u|bZELnXu>X#O|R&lPv+IN$K5WqUPV^5P?(sW+O1q&b? zINXi%IE~S%{Zam05;6#BNc}9L!F;0X4t4EsX5xtHnHa#aEACrUGAA~MLuNE3#Tv1) zD%GaHaBjA~7Y9x!7^~B%1K{;`bN9kIb3Ugsfi}4_f{0&1;N7OzGrbNYbKTgAU-rQD zVgD(rj{PFuvNmf6wE6-|r`)Hu4-GP}>JnVhn*A~?q>(Cd`rjG0a4X^-?Gs0NeG9U2r#VKU~Yk3WMkzqpeJuN5G8jUL*6 z{!H%qM%S99@ArzJY}XDCFrSZ!ajc3H{U3qmlWBPR+mT{Y4bs!9YsXAL=#0)g$z;i* zLBw6k#*)m;xHkgwHLgN|7Q%V`eebtQ$3$2QX{gIUJV*aF3)1!Ck}8F2;=9rtJhg*6 z(l|xuxy^-CJ048CX?z-59@A_LfFRbt*k@S9-y1eAK~b$40Wevx|4vb#*^|qdw5tSfCOTJpkGmjy^Y8(jwzq96}4W^^gWz{{uzJr<%E_wX4FcFJ3KXTT; z3nnYdm+O_k(B6w;R)cVhFWGZcZR#>N+tf&yF}$!C5}$~^SQ2?8eDOE3Ck|qh@Ml=y z8YA++Cx}i6FLYicMg%YP%qE;w-gby*8n>A!C0$hDD6YueXVIW>5wA5d_2QTN07U;e z_SMnicLyc(tzyk`lPznq=IYTqt!mn$jHGG@$6E{)la zXt5G2#E=XB3p`n{8_RD@BlMPRwe}Q_A~c?@G`R>V@>DR)&9xyQ!!d2}F9f#~Y#*fI zD}SlSmc-nc=cc?0cQtlZ8HqMZS2GoaF<&-sCXsEdGhIA&R+1*EF!NkAc0RbU_%)yQ9VT>|DyFtcE5#w}sIa zs+KmJ;SYCDw4QzoDsn1@C(-oDXO%2WfIyO>d=0hSlCsM6Y4DJeM2XO5xJJE;$~x5P z+nCRqTBo}qJ7&S7eLW$a8iL4Ff(>(;NwpRic|5E{i|UaDgx~$2#ln-Dk$uhP>!g>X z_jW1W5(_Pc3kZN|EkaVRc%l#%zJxJ}#bdCB2c`r(CXc%G zHsaf)b8(3xbbw&Hn3lc>e3ZN-v28_{bcmHE;?rE0YC$>8_`3rD^le7)$>(o845$X+ zx2b8EOVW#ve}_#mtK*mZU(lVs8YCtJ$^s%n(TR5J`y*w-LIeM6t*<7GI~*<5Rdbs! zjcMt}cQ2C_X%uH_T!6sm-0g+~Pz>{pQ^fmKcoJ_prwB&ckN16F8dNOW@C_MDL10oc z;IIEZ-W+#Cb#YX}uv_>=5oF^^!1(XiAc&O781@c^4G?{|)5!Paaz%^IKW`!7P?g@NLuHdj8G`$bVNT_pPBi2XjP@fd}3P= z?F`6d>AHH>H*(YyW%D8zL2dF4tc|54L$b`QBpFiq6#)nG93Ob7ssWXy?(Fm$5P;gYwe@Aw;w zAuXb8-v6SE(G*1a4WW%uSgimH3kqmsZwSnM}lZNP3^27I1#5*Y0l##gD^maEx8%B);$#7POC#23Ni!bdJ@W zrZ#mFZ~hwiScitOAk?QPo(u4M5ioph4I*JrQhOv%rNFS8ZfY>muQYk#1>uj$-NP=3 z#d>OuFl~-t&;1oza!gLHPG>aGzr%MQym#G1Ss+birP~tk!ZC>iI65{LnUn!wr)>&4 zK%I@b&K%Tz)>&FWp|V!xBTEXIUp5k+=RSMiB>4T{O#Az%XndQRWr&A}7tduYOQ|=v zUMe*tIzq`o*j9WZ*(UPed=5Mi^NkD-snf!OJd z*R6fVdSxUen_6Wwo@c|tCV`B{|B93Tq(S&EtLjeI3$t}z3bh~lp-y6gDYo|jCRZ9a zDL~?{J;hzSXcm~+Z1GCorCHU%(;q5y`5+T6mv>{jzTJ^yGH@#dC-@(fWO?>}Tn&73 z=hlwFP~+Uxx_o3t-r4}dnD>Q!I{M;aA4i-t-n!3c=cxZdR{G%hs*jiufwpTN7GxXe zuJ}QP-YdH$8Sl2PjN-DBu)p1Vq+rMf8^k#-_F2qr}!-u9UhWrhzuP8Hp#Z)wcdFTvqr`?$N$Q090&q z!yKW^ApM;2Bw38Ox$5*L4W!t@)F+bxsh9oyGecFZ@+wUOL%7|uCaPcxvdK?H^AWB0 zo^Hgf`vje|nP5iQE2=bA4;ez;|HT5DzjO<|8fsTXHyK*i0nMQSSQRk(c=P^CCk5ig z;VQEAEL|{5B0@ium!zet*(a%cmG-gNfcQe$$*2G*$8VVJ0ClygRfcsPCIAMqTFT3R z!Cw;G{|xbWt-ImL0RZu+$sg9(N6MQ-nT$o*-_YrH@Ruvo1PiC@S8&EC2MJNV_Vc+C zu>t8`c95Cpr+)n=?OuHoYw_($z|B*W3-39{lg)7;F)(&_^>j_LX_U9cwvGj1;e=~H zrS*Qf*1#2W=6~uy;)Y#{)veUAn_EUlw-hp1=Nu(*^WufLkL5qpvTx}I)47k|>Kh$B zeE)!z;~twoih744%*n+RD~a&8s#k8XHLzfYtx^p2MQVk1=lG*DwvSg&#K!tVEBx*Dyr>s zu9+hg*Yp@JG+dP>K@Jiu)))d_lb9DaO7_jvvZ&I9?^Tmrnv&1(8UHI_IFUpfBTqaT z^%dg^a;L3ZmE_^k4oXlr7`hf9q{}#J?aj+~Qzx5B)hVa@sf&y-wX*E)UZG*D@y@0S zbZw$UK@Y-dOx#sJG3uVge$h4sJ7z-uO{3L(BY{U(-)72SFiS5yd_)Zs1=bZh*j1~X zFNA7Q-t^+>SC6!`oQB!MWw--B>DZq&tDQ!-69Pa4{{j-UNRYUJ9$Oq>FG zBjx}@58p+)8Sq+7M4i9@T*Q*0pgPaTgy-e_ zvxo7|;+gzu(3*Snq)pEYqW2632!EZk1HMZ6NrXc`fDuZdaPjA7|L*oLBXNUHu?{yF zrPFJT5e~}6CY<30enIV5!|{Q6^vul~6~>oWOY3Z4WK>1*u*3#Z)sf)jJ*k>Ryy+U= zg2MOd_y@N$nRULx!*|?I46zPsrX-%H6Saf!3lf7SCquDMcV9hKQhYeao=a1!BJh75 z+9Zfgc7!!&1fbU`97fWrg9aJ{h>$Y=kHNX{kSI5+%a4Qf7m(Cb%rk{&rZSq%xN-@R zH`4>2|`@M`Els<>Q8T^5#zdO)zjZrcsS?YY0`^xpB2mu{FHqe(+xFOygVtd zVW)27orz0MFJ+GCZ`m1Y#@1S;vzhBYPhS%N09}=vEti{exVaf_zkWS^>g=#y(4acV zI~e_UMAa*?`E+*3pi&k!H#5X@{=JoVt)}ZAgqIJBvn-A4yp~w&)5G==?{h8jPuNmm zztQs`4-WH?pd3owx%(L$A`DZt8I+3j;8; z*U0QX9=8|;P9QH%M`Gk1tj+P^oObVK3=C=fXdl{lIg=IH7D1c&K3gDRIFVoE_Heb9 zCE%`+zx)V>-0q*>6eQKdv#vuw9@kV7Kc`?`pX?l1#DVmR&|f?gLFJrj()qw{sy6@i zXlXj@wC0!5?$(2Ok8$jnA6CQBQ}ujs>Q6a6;i?vY?4v(roJ3zVj5S6CBEJH!69Bx& zR@i&X0WTeZp@vqc;G@ykVQzR#voV7o@DfxCssA|DYK0?&wR zMDM|AtGIg#wXxPR%cmBp^NFP9)^xlwD;k(0|x zg@IT>(0FD$O@>g*@9MZBR5#5)r7nr=yHeceoZ44kQHA$5lWfx`Nxf#~u61sNjo7Hu zIgAWO7W^7^UJ;EM-sR<;X035jr^hEoRT60MX%IplYjIUHmyYxY=93z(^QL4*MEEV` z>E-jWPYNuq8p8QWgOPSW7y6fGg(3d@2o}?Mu=0x+=-7jvo|glWMv%B+3A{WUTh-_& zx^PA?nW}fwXY1s>R4v~+8P``As-`6Jdj);3eYo<;nu`p~t7=;)Y!bL}${8PCqF`X%3z=QMRH1wD5(?UhCgJpNP3ZDKw; z@G-1VsMMX~ATCbq)wVh3MpWqSHStD)Rw6oW>R&3cU`${Qf63u;Nb`JedE^z#OI@RZ z>_$4JSP!m;txhDoT(yRS>TNyCRCdp_mtq|iwMc|J% zX99NJXH6Mzjd=5`5hDT&@H|I(&Iu#aOjZ!08>V>BdU{w!GI$iAH|3gLeOHW5s7R-E z8SvCcd#AlImun3ffi65AL}f73eJb@-xk^$rETzUfa^H_3Jg`uZDV;%25^Y!GcDt$N zVb1$`IqnCy<0EQElN6CEM@jABaBEXbX3Yu~nRh9CTyJej+06u{V&mg1X{9f5$h*qy z`6Aw%VgDx;oe<5NIWR~`UF;l{vmpCEW|eKEcGQjY?4|HqFOrXMe`wu*``u}7nQkfL z`5CdZ%i7u)-C7m%V^=yVWvHX@Cti^8P&B*r-|`_LmYztr@r(YW7#Lw}N1FoDWY zL=b%uwV^X4_;ZQdVjXw8Blw$}36B{X_|@m36|Q#MrdwlgDMYh2H!v#gyhoSrjptLs zcp8k@Hb#awebZIDtl9_D-|-D67*hi?6KEY9ug*m7 z5N}Xsc%BAiR=cuA6j&boNU`IExdzQ@nyqWxH;c(pK2E;xA}wX1Ph4`lL`r#-eH3H( zjE>La^zxY`EFd}>Ewg+gyVN?|jt`4HF&+>iu;BG02Im?NXIuHBo)7kmhN@zGQc82^oi?#78>nbNfgww{~~UU=2n9%hCOD3PuHsB?6zZ zfd9{&wf;9pn^2v$mp3FKSAZT{2DENn;8y(!V*CFqHVovAqad^eHOzYplT&9@uCMpM zJTR3IJ~A6^(TZvNh^n5+-e3X1wkg-?W$nS!wk6*x*qiqdHDI->RZ2XS%tjMN`-Rdj z-Rw&)mK?mq+qx4tch!L}CJzb#?A0&2DYgs9wz$#cQkpbAONZD_IAAEQq38A^6~Z~3 z+HT%tov|4C#{a;j;DHa4c6JCK0uRhj&R+=aY|=<>mpqxT-Jn5x@hT>q&t;Kx8%+++ z=A#4>5lo^tO@x@n3@0UwDLid1`?A=#HlqSvCj|fG6cbdqURbJe3IJcVW_9yMCqhbb zIMtSa;X*fTuH|TzC0z)q5BeRdHEHcH7=`dTJM?iiPhMrRU0TGkDzMSJCvQ!NYiVDm za$PhFmhvoI&Ia9*85;9@Ozx}9U6-QfhM}us7;|W4tKEpJ`m7Q$i_7C51v62+Fp|N) z$*M%>j8Gso_GH%k)t`kDBdZJM-LY|$Rg5{W`0`Sk{g(E-b=wNBw(|v6Keif3>1lRQ zFqPtGNG=5M}d#s<^GzeT*FriCG!Slrb8(`;R2M_mGj7#U)t4;gnQG7L3 zb=7A0-L3lAn40SPpKw4&7#Qx~?9gDZ=VNeI1-ruga1}HDvH;mTB~ohyZT)~Az@5_USBtdKZ3h}tk0Oj z;EZG-Sb=W;fNnd{UHsozsvtlaL^XPS1;BtqvVQ`9)h-PLHFOeb!=Sg5IE?H^7M#Ih zfl%h>ED1!-!}=_0K?swkC#eM5xhggvP3w9{M0i+6OT&F@2_BA)ktGcT+iNR|h2>sx zl2xuI3L;KTDxK|`(v^ZHZ;rNyRd(5{pOt23=$~78>~((I>8hN|YZuUEVx!I5RGFg zL>4VdWTz372lQlbo`V@OJJo%ZoSx$DQ^s^n# z?dp$`iTQlxhv~B*$VL&Fm|pR3Y*4zhlLbuirvW0CDKt@g)3k}`6E^yH2_r>S`k#Bs z{Cv%8^AE2?_DDVl-R3h7pD}A4H*F!D+tq+lgyZI}vd7nx_b}R@Q>~WfTPRq^-XXks zB^;`~C_=a(HCtjn7@uouB%)!3*tCOm?qh7)B#-a_0y;7v|K%MBahFMWz09QqhQda( z+JF4HaIph8uixN6ov}a?_MWk4*43I1yqklESYA zSnLf&nQD3bgfB#nG7dBY(bsjqFt#xqqDwyneu_LH+BW1TB3;W4V0UovQ9~Q`2S|K$ z$b`zfe%D5i>vIbv-G$p_9Ztgn{h`Idt#7w;f!*NS;`Ume_iEDzvCqx>|E%mzVx#7GD0o=pX%;Dp zLiil6xxTuZtYP{P=>qO;q?O9`f32EVlv6zQ_!{{t?*AXgE?IO%p6PDT`8${Q#@Sjp9eEJq6uWZBs7`Ge39{$xk6x=zhGj3WMj;?d2-lCoE z?*_4>O2tyK=&IUk#|mUYQ}p4Vd41U=U3Q)K8=S_K4z~heMS)~USvN4`#kjo2K!>kp zK=octONFBY>pT}%gRq#1Z90beiXxn7`?72Ip-Jn=$GuHsBXO7#x&4LuBx=BRx$n-c;&?@7@c;)AsLZ7i0s@6iM zpgg9`P0L>t(s*%0xo38`F9u2{{B|PfQ_F5Q0$Y=$W``;#g5N2bPw$cHQ*O{O=WBe6BPC$(o4h z3cR&=zXmtnK$$d5T4*Bcodv(jpmKwuP9UCqT1rTTeWxKzjQ_&DWNuM~)kO$>=?Eo7Z5^}&Ic%Jc zIj2q;C4E6*{uK2oJkxt9AXAc>YCewAvY5urS$MN3SCPoJw^Y3u;X@wV(Og>g+4S2| zwerk`QdUg4byQ6s$&|E=qB8|cwW|qqrKZ%{{2pmWfMCLMq17>aPD3NL(xp2utvBgB zEBc-#8X%gLZ)&ATE1+MFuGhVYr%K?wO2bX6l|C?Ns`e|3pEViPs%^OjrSEi=`rcg_ zAQJ33i&gme5M6=Al{rfJ%lH$@TACTj|FuVh9i(nP#@y>@p!QI`0TW}}1LvYvN z?(PH#?(Xgo+#$HT!(Al*Y4_Z^=dD_+df&R%TvNvA{p+&hzg>`aNl3+liD-QmA-G-8dEF+5(8}WSusOuhEvTfrYOq6$UTZFE%OQ{9r(OQoEm{29Yw%@@uIXKl zx>M_zQAtOSD)=V%8$geErsTv9nJZd*`)3~sZECjddPoB1mf5M^K?Dh3-s!RO;#K(SXCI;%Zfv5kd z3J>b0e?ww#d3TEtWo*&Ax1jDJdu*)ax}0{2BgrW$ePx=uo|$RWh6bH&s6rnd{-VUJ zG;n&Puois$;W8s6X6L!4GlsMzOau*_PKsWhi$XNU7{BY(Jr7LIMS?)ymtC*(^!kP? zO=#^!TntC8HubxI@FOyC$QeD_rt#}(G-EG%24%!alRdIcXx`@k<)R1dBA`GYjnN0ncVK~CgxUG5~|59zG3^i z6*1ugoDvn@fy99XAtq97BuG%#Sama-)OQYw( zrd&)9&F~z<|DW!C?L;nx&$xNq%qCw1-y39xdZVbREE?dm`)YFRh_?U$#bpQ$#<(RB zA5oDZL(f_XnR@l(RUb?a3oH5}aTk&yt7dFcSCoHh6pzbxpyUZ~**B13)EaCkfMDSc z5$d4{-qs*4?_(Y}vt2dxACov|#AVErztvzv%EorAl8W7^h*|cycZ3w}QTRxFJ38(n zLQG3n>Q>DGT-scI-mcNkpT&}ki&Y1D&`5>D>i=uw`E2w43HSeCJP*hW6eV>YUdJ@b zSqk7^#@=(o1C&kQjQkA(5LI#TKc>+mwa8_J0J&t9PE(L@J`FU3Jev||lv5ec*V)pq zFYT<)ggP^}dyWTBC_(r*VROoE6<1CHsoL5qn)phG*Jk?M-3J#_cHshS?h({NZrq-N zMPDMGdblOUk$}0N(OEuNza3`th-!K$!B~)XLzBW!G z04!WL8~T0Rb9YkosErb(cwzqPLg3psQ=UUWyRB9kHyBR9jF~En8=a?*l)#qgpQy>r zXV zDhK#qX&V{nyyAJ)ioOg}4NO8?ikTnfDp@lzZ)fKhqt~YKGm0#;{X=(Y+6e7v>w5!6 zL!j&MW=P*4+F+?LaggtMuIYH=%pk2om{)~VU4#Jjle)Ph%JN%A`iT7tE-JWsIU{@J z&KHMX0PnVS-4vnm2?|qXG(+FeQ+w4d^CRfk>L<_1$Zw=HVZ(tDW|!sK1Pv8xa2Vf3 z)e2g%BzY+=8bi|BQh4vfy)i*DHaadRuN`I6s2gV;Boo^(2gD}b!`8&qkK;s zr3E{%)w3}fiZdZ|CbsI7;wFU(FlXc?vb}N0Y?ruocWOt(EEflavVU>;HzxGN_g@a0 zc3IiT3X6!yj_RE5J*6G1_S4@p#aTamwLHu{-TaFZ8uClkE_Eav4sni>Nc}&w$0UBc z{%;+9v?k>9BzmUUew#FaAi&ZoxNO|}rf2d2MU`CV&DmA7efT5@x%mhg&;RY4?Htnk9-ce6$4QR`a{(DvQQ8u7K{NbU z@-EWg`r16CV#i}`RS?bS(ZXZ(8rC7tWA!Z{^CWF!NnHO$zw`lnZQiP#}Yly1_tXAn%S#cG{401_oLbNU;8MRSWM^ zZ2oS0rr)K3(E61=ZfXSMp)oc98}0jNKuBD1)MoG~i%S6?1dVcKjN!vS!u&eiz0OV_ z5$c>FYa!nahZ%TTiMg&C7kb8)L!r#{J7GD{F#-t@x_p@HNP64?tCTGzVU^2Fwo%5`TjN(5W{zcjp{ zK_HL<5HBR(9>?y!TYpC#o%9~a-W|IHUr*`)&}wf%9L=wGl%;$rB=PI^y+jIOn>R3M z2DwkydmwCB54Zaa3_P9F_ml_>EC2mpB@k}h_hjK9g43;^e=`9c%`U1oz&PXa!+5x< zQIqw=oCXW;S>1~-#ms~HMW7l?2gN=>{_kJ)516M9@4U_+<&C-SjdT`F8U%@Kg98oX zZBwEn(T)^tG(pcVBx>_p)o1vRrWX%+0tX1uWI)5UaTimD?| z6CmG$1dsvU#asW-nV+BdP5-;|Ddvt-6*)8N6WRv*c>k?PNMRls)E;t>8wooeAQ?|m z{^??e4hgv_@7wtCUxV@_8-D|gz5~el(g&339hc{o4V}Fq6Ad_<87TEFZ&R1hgVrY? z6I4ITc>M2I==$suZV1tbL3g;U+eQZgqEWv))=~P+Y6B(O;D(T ztGBi8A^?fGPT-Bah7=sxE8HNC3<=-woPj~Q(#94Nd>RZJEbcFQfM*Ci6(;_nqOYgl zIduQQ58poL-zr%71Ovmq-T?;!j|kPTqyRw?tYgx`2)dgR!FPLB&=Z?RK&I04853$; zP;}q+L$Y$(@Yo&O%^k;WySae$)mjhgT#4#}gz1oO6A4CN&CFG_kB1K6V%mEByXt>|gZgpU z8}Gm%Kk4JFntz?^k8v`pxzia8V=x(Jj?yPy8o_< zyp?c6?E@{HxAzu145)tO3pH7ntve#{o!6(EI6wkY<8I6wm+Z759U;udBA-{^s?Q#N~U|6ArNvAtrz&)>w;(Q01qVf?0|NH=%a^+ z?4Z{K?p*g7`ggaqqB;`NFCKvZHw)6n6FI(G@xIZ9CWrqRljva$UB2=_dW z=IJvtgJ1x@t*iAhq+v0Kh9*6DDtH9IisvgVm84Q@pCA^5PyMm}JU&M?MG`I7-2LEa zIedJG$mGP4IxHE6^^Q^xqNZEntJ~4+OrM9jZx^|nR#fpNT4|QORFR#>f(Ds~ zV+skXnG@Sg>yi``eeL_mh?ZCW^8!xS3bUbbgvX)ZF%yM`?ei!4-a`wO~wUCY7P~T zxRa{@lTAH_kgVpq13|;DxBz{`Fu%y&3gMSfdXr!m?n({>Je@2U5m;L~1bX1{VHnjh ziEX=WRo%mGdLD`$r_H$1a1!yqoO_f&Daq8yY~P~M)vle`Xo&q> zUOuQ;ks+&*Vyfe>w0{9FQv3M(T4N}sW#iPygArlLFlATmsLy-!L;-H&7W{vGciiR?4=)@`S4I__i)EIWXWRu@ZZCJ z8DxHVin%RbpNTo8+A@XA=LeRRPy1qLcgXZTxG>{z&dX6CvNHyBf*ziaIDfqr(}Nvr zFKfh(Zik7lr}@|rv0jI?xb#~9D&qiN3+{2 zi~*P4cX|eh$#EEMwWqOsnH=^Hp+c`_W1kK${Htu%b~AssC^tQB(aB`+Hon%9Cw@G6 z+&qiGF{>C^*2;8WjVe!TkP=KVD7!z4rIUGk_ek>iZTya0E}f`c%9GaP>(wrku}tRE zG``vUT1IdfHLFqbN6FuLPxmRZ<7qEBMq%(K5eQS3_1}-Mz5Lx6qov#_aDa=Y9@>hF zjz!9%toe2CWlg5g%kwe~LpPX6mwdiv_rr^A!0Ch+vUy49vJEc^>eD(-Z@f{qmaR$6r+O zXT=`b6^dfe&{|joHey?8y_G7Zz63nK2=C`f#eSjZZDM+%@PTt)`V{r~12OJz$Paka z^|cG8a=jkd`9m`?ykbiYrj5b{qJ>0ie8&JFc##tysJlXW0x;`^m6ne_qmxEssH*70YI$a8KCGJh-UiEa33M`uEuE zdAs2BL+3!8qv!(R34RTPoi;d)s6EWw_9&a3f zl=wp0!KnFXcE*=lEb8|F%O=^+4`D3u_&1y;Wt!#D9F0fZ2zR_T&|h7;iAmW`9I>EF z4xN-V$o7Ucc#v&W`Alq?K)i0%tmS}?8m+7nUL$YCs;IvI(mS}BA~re(9R=3aRF=FO zZ0q1Ef>*xy_hH=neb%Du2nJMUZIgCmxnOEZ&N8i|@1ZUAv5Cd4OBZURU9xgvxry9R z{ZniJUjOqNLrk{=o>|F?8Fu=sR1v)TyCr3{rK6KSS%hhxc`Yt}H=sc}xEwJ&a0mHT-40kc!Ow%}EfDE3u4LGiS=gbI z6ihP56(0@{abW#Ed|FF|=UK5XsjWk35`)dBp=MgXXg_K4vg1o5B=Pp{tUTL4qUiAXML9Es>iPb2Tgar^Td#tU!f-nG0or^ z@0BE#-6&aeV(UzBJHq49&a`}7vN?UMn)tVzcYb8S%yUUaA z7}_t52uu}3i+Xi`GACj1H^uFfnEMIo$8JcxZejNE%e$z30?ZqyD$<;L^Vya*&fjOV zMG6V1T&aVgLT^O|2RmNlzNP`TM-?QH@4q%JZ{v*9xIwP zCn>ciRohAjhBYi+Xk>2ozFS?xv)2md@s2^GKk|`ZOd3&tco+}2IdD4FY4SPn<6j4$oj5-yP&%%e35av=I3@u~CP-H-K$!J=v6;SJS9 zTr|W!2{q?13c3g~EE`R93}l5KEBDjOaKNXJBzfO$Y+?w+WBlo{Bc!fog#%vPg+9-d{?NTcmA4>HR#THiP zeH-7){u#veWdZ&BtNg5&5C%wsXw!HBa6clpC3=hVV z$JE%wSe2Jcx`S0uyA9oMMs#jBIsn)cd&cQgVBS^O%ygk~3?0^UB($M_yi$+<<&-vjYSWK=9z;q#aP zDNL03R9;WbuXc331sH+o;4WARj`;_!??zE|z$Ub{_O91g!dN(ak|Jn>5caN{KoMyy zkxV>5LbSrCLkV42?)^JbJs~OQr`Q7YFR)rkfK5Hb8HQ5Q6 zH)p306#Kas2;K%6%GrCVc~ZB*L{J0u_lhib|xrY3)Ogjhs=3#vad+ni4TkvsV#c4m(UUMEG}OkKX5qqud|AO{(z(P>Qf!typ<MSB8C-dcl&iQkBXc-A5U|nk+*rP|4*%|;ZA^|i^L`5N8;HGqcn1*gQ?t0N zTHYwS+GjIvmho=Fb^rOyeNs9)_jFLa(@74Wn@gcsHbO`QB8c99e|-_OHpxL_+JLil z$$)B*p}lW$;j5sNH=Y3U_(7YnopkZRv#{Hi6^5}^R6z8p6UFJOV$Rch?DLXl!niuF zr-U)pWHhfi+^Ez~0<(n&!P1^X1Yr;fqgLA&@^j`osk8?kTN2VZiBvQ_Fn&SPFqZ|? z9T*-vkMDt2R_CS5nhFO98Y{m9t!bx(_c&9@;bX*`80^NMkxL6;`o&6CK%)$vfeb#ObSf8{XW z$oUnQVW9E=4O$<1x6~i1?WK1=oe?{dvMnpr{{vsA4J?OnI4Q))m~M=XPA4TEo<~Ji zosaK(&MjHc&kvHUzchZS$50{OYO;Gi=x(viVU#bvoJ4*DMY5s4R&5=XSr&3~f+yq5kexD7uhGYJni-!#wN-@F-Uz(29I8UI@`ejS3!(&Nv z(_~!R%AL^syz3hS|6aqEQ-XDph8afeu=QuLnC07Jz0l#AvCgC%+|%_+HyocNqQPW7 z*0!t{?WaUTzNHHIhsD?2{iu4cuKE)?&54yw+C{^WIZj7BI4G@|-c;@#1x0Pog-Z}9 znqR{}4LI;~a6kZ?k9-9blKv#GA&14aRhhQhZa`fFj(qN={&=|9W7L8gAXxOCybrE> zbtgo^)X>Gn4*{eaX=$8KT3L824nUJ-8fwlWDr~Z;wH}i4M*nlacRudRF@XC#9R4csj0Q@UIzP2ls#C}n2Lf3 zi~#lh-SYK^gq_nV8`vV+d3g3a z+_B^Ch1XZBH zBJ~ORX`H$A(ucGQh}daWj}y9upzL~;#+F>n-CmxWZ-3nRcea016M?#{{xV0%;o5cX zX`p*o?>_m&VqTXz-So2e^2hn~Xr|<%*5a~mT=tfvWR_Pz>*PZF%aD4rd$-wSC7VX1 zPROW^!Mv4Bs|nAPO@m5{{>)>hnVdsRByoskMbX}8sEt{gD#^{sWH z-FZP$y$hc`FGW)>OJ{ppbQo%PR#+r?f+bc<)htgBc{~DIU+Ud{vI$=B zn5`NpeoaZzv>MZzvAxKRAlLRHe*TmCLxZk2aA}J?bWNLc!ujr&T=dgRHgN}~(8HcO z{LYZQa^dQk`%-T{@AGEJ@>N-&CHc~$)oA0fgm(4LbuCZu{k%O!t7g5~jr?5aEI-UZ ze|6R{Q%`!x;I`jTr3*o`xK2yXZnq_Qi^m_#oK~VnhqDd1v%64p?IE-J7JJ?}UZcoR zyl0M%>FgRY?|w$F0Zllm3th7e?w9UAvI5)`;)%(J!|`HuR+jh8>;yfz5G=AUmb*Fy z-{1c=oEwp}yj6pNrF)^oVRhkKLI`X#(x+Uwi*IbE-w2Fu;75HCq0 zd0OeGJU2Cr!gyXAA|hqek5WG$K87A|btECuEUq{_ZQa~RuIwUaw!;)f7KRWExgurS zryC}juN3m}x|NZ0a7o4hb?#6{r$)^Y>7bRY@ywRAUsDBRSesH+_EX-x4nJ9JuBY<5 zXUlzyK1o&^aOwjb!~GHx|P1@P@pB7OEY$*UB8?c`#F}qOJ znjR+&?nTlD8UP)?<5@LYmDuPeT)CNV=w~$Vo8}VvJhu;XPrkrAtsM}9_7EBn)A{CB zhc%nNTf7G7N-TD{povjaa%d5K?B>_8ZZ^J}w-Dq2vC-kR+C)2Bn9lw>7vVw<8K!ov zK0HJokxa}Fo7)2H2a{w+mUYb2{Lzph?>0UC6y5-7VOQ-P2JAhTSptBu{2lV98@K_c zV|BYMcoAA6@^)E9tBq93Ws}Ro%+et%+9wX#fv&YWZA@cOj6rkL<6|}#^O$=7WX8*( zJRne}Wd#x0hQR15&vKU9O80HS#R5(u2#h?~X4K=*D{>1`r&gq%B7aP)aAb*{JQA(b zy-2r0AH0N(AdaHyfh&@|EhvN!7zfx%Uc;O>S7Ny!&}Q79ZxIOBy`cw44p)Ax94X(-2sk>DK0celwV?5H2mgmAC;Wyfdm5IJ4TBB?{fwWGMn55?N=Bk@;Ui`LSq zq2w>BNU^mw)6(Oy=vvljP?4m$@vp`Qd-!X}KYFZR_4OAN*Py$cz!j+=anauN_82s- zu~_dp2#a-d?b68>)J}?fjjt0(zOStkPc8W}E*@F97Qh*Fj$mUwH`y1F_&#HyO$UEm z){1wMi+(d(2p>Z_sP2`FJcUMD!dLB+K@ckxawy^ud=3aQT6hGju-|P$>-~8Y|F^R8 z1(nJeoO8w{Wi5XwHgaEsy#z9>Uab!6gFe z!9Wnhq!62<(y({@A&hA55o#xoEELQNoFt!YoUCD(hi%~m?0mEH%kbtmHM1!b`wz#5 zr{q&psA`SEi-NFMz;=v!sB?Y2m_m*ElUd#LN3u{KT#&pSXEJ>BVRzd_Z<&t4BW#Ss z2Z1%*xq^^uCow^cpd z+QAUonoazj=laVfS19>ObP2gki_@m?6!(2GM&^QC%~MyvxGa)}fm9rKWEycGw!C=E zSO!fv!v+|pPEG8wEx$oW(2u@;hdK+FvUk8WcXsBdoB(=bq7Q3#${m=9AaD@SNTHlO z_ad)qu}>`1Xi|_S10$hPtvLyRKi@7JUV-JK=X!+wdVp{mJi(*cB?gZQv(~T&tJ(HZ zQkgGX-Nt+pKAN4yKx=aI=-uxr4CPv4X7$jUPa@g>&Iv6$k@NE3-`V+=Ul>ei zsvUT;dr`AgD8Z;)uG;LIQm);SO#ngbK0V1a62H=d(=d`>TpdGxJ`fK#b*xn+i#0cB z916zI>^3PlLj(@w?lm63YPR%NgW6qOEcYAAJ31-zJ61zjKrmtCV^Wq!8IX%_iy_Rv< zz|m$xz5>iuK-W$R%>)v#bdCr^3Lw*WfFMA4kf5+#XyLcEVooqyVQ{Cf$UWKdXV7Fu zM(7pgaGM6l&uYAJ7wX2xuEg8l-YZA!UcWp>>C7+f%dd4#jf7U}QS}X;aT6EwC3UQm zXigekrw%w5$2u*U(3)MluCuR_!%xwl89xZ>WLkT^es{y?f4q^Qfo_g3S<0p}V&eLp-`1~5 zY>Z`#o5NR+((eSqnJ*KrrhlGxS4(tU_33z0qjz|{u4V3(Q zQ90N#R^G)2_LhS{{Cm!aWt6|rKQte_91PF!xp=I_&biG`Y`^HP4TQ&9Yc|-F8IOKO z=q^92VBD(w@(YxT$^X&6VJz^Gv8!7B>xYzo3ONSQ-4}^D9c`L?WE;FSLJ-acP+5o_{rZ)S!x&-B@2~v4 zOBl_i;;|U!#|r2Ug!v!s)o)CNJPGVarI>pes)K9;vLVE*qL! zT4t7QEabhD{rGyB6LE{Xc8xk+!+L6v%;!!K&q}CocYhe@;NkVMk}=FSp|uSrzu1ka z{zlo8y2f5nQt=V}+e;@S0tjW8fm__;aL8-XEU336Aa?$vXiJVtx%5S^?9^aXH4Z+7 z&Y4Ks#Q>nDgKN28SgwB`u}-u%TQ;t=rQP?%6j9hU?otc56~ z8>sxb^fkNAQ5a}H8F-~bhtJO_KVp|E^3g3GQoXhw;wk5yUWk#7ipx3e=zU%6vD24* zExTtv(CmTtliN8hn{;zbILhv(z)}<`W;`vObNL*y=D;-Vk$-Jy7{FPZ_HBs@&eIaf z4oTC}kfRyPF^{EPednsN$8gr8S|T7sj(rDo z>7A9<-6M=rn9+AA@$23V=ci0XwzJnr6F<9&M#6o6SbVnl)`YJ(p#0Ht7NtyLtGLkf zN!DrGliZ84`-HFdL_6~yhFVv4yXATi1OH<&-g)iaq>c_SY=oW%j?qm38rnO74jWD6 z&1zF?P>*-Y)=DH9}SP7pu zQB5=^J_I!bAzC*G1PO%gp#dXsGFG668B_O7VvxiNW2o1yO>AKAODkJKpc+{{sIUuT z4yW^ZXnKG^eCeRL^yH2rODWl$G4+D^y+Rk1$&cDq{o6n=p!Mp(f|VIwaOE0~vsau$ z<0uoWbEDqI$|a2oTicvU<1iY`*yUNz_G^5S!eTWzeOb2`CyUAK0|_K|M8V=>!rd2t zDK4XXe<^e7MU#o*>g{hNe+rKi%t}5FAefS64^LCTcx@F(6}zp@W}(aqm2%v z^8+{?>e3*+kyjd8^^r#5C8@~1Ek0FE5taJ-%Qa8S6n?ro%8>qWLm>72dgD5exeqvP zrgajW6j?elDOt_5QE6d$H|G$OY@ahMEpzy8 zrzGRj9T9H|`tW0s3M3d2BF%*>AZSRRh`#?d7{)_R!}>M=3`8;WFm*B7bi}Ote1&q$ zKAC7XbbCAtg~KaRGAa`*3|_OMU}*atxWE#AMf9>I@|08F2zP1WwGQVE$jOI?*dyx| z0nAWZj#vgu3Uc_WYD#8|Cg`Ry;vHKizpCe3;W7B^nAnfbi3xw7SQM93r-%QP#5TM9 z10oG4mjtOcHT~=nz!HTqH4Z(^mVFlU@&Kl$ks4xPonWsdL}wrNtq{8GNDDrKd4jd93N>=*vLN6*mH6cNHZkUw>OOF=RJM!nU7 zE_HD(rI6uhS?BUsU!^^S_nG;3g-tD`G6EXf?Ze_3wN@Ss<9BB1sq`oFhFBwbMi8r@ z0cMF1z;I#o)0D@>BsBr6T%R+bUtOUhyCH?_`wnAn42a+x$5w)@IP9$DN+TtmwW3Ju z4@Qg6Z14pdX#SeUO3mnSKs@4f5|Cj(nH)5X}eTr5oyMUW+gg1=zf zwlq_o7P`FCC{8sco7y#BtlIX@yj}`%7|wJ(0D@JtPiW0~1>mhSsi%`AuSqNyVs4*g z!+SqS%d8F57#A_No2dj->6SP%lnI1b67nf@7 zlfd8?sb=4N`KWiw%x0eLnB*MZJSD)k&4{G zj^1rM;h9&Fj3KZ-a`d+0x(uT`X40*WI5y|=@~%s4V_w;{x8|{C$S*Hw%9>$m4^J~s zhNZ>O%g`Oe1rqMnJ#G?*=eEvj&A2aPB5Di<=TUr_mz`cEfat(E%o9si2~aMFY^obO zIQRpyNI+6b!5YOaJHa+G%$I1Dfz3cFF70>MJ5wt;jDMu6X`~uD)<3049K5PEHu>dy z;+N19Wm%D*5ntkHemx|5x)2>9SImqVr`LaTK<0!A;zUx1xo)%b6Wz&UX~bgGEiW@a zX9RVsmxzANPp=5XZ*a%qm(*&N(8`}Fp{Nhd*0>?j|5T$PDC{Vt_jG+{4)!Cz?4BCP z&d6jm%PMUe+k^q($3Z3aZ*|*(_n^t!1{n(j;T(~6I9|jdIm|+qgW(hyjkm+qFx-)Ug_5bAK&u)* zY}cqtDk_-XXAA@l>Zk-GgJ(>R0})9RlB;GeCECD+jc@D0W08vRgSDpX01?qr ziRh2*;CI_3DD7nZ4?HizxRe3DL^C4cV>x3iPWfh}%)rYc!1&s)*XjCADG)HX``8Yh=bLjT=(^J@gGtKY93L=}9iaf(L4 z_nzdqFyz$5bH@fVb`laDxbSD+9$n|RzYW8S1ia%&(O5yfh>g)Jl}Qo}NK^WD>WKBD z?MNjr|2x$m$gO;|?5!lnM7q4_Fdd;+-5%9;wt@XWb|IhyrG2uyK{nCi1C936j{T}|Flhd9Bi~CQJs_YcIwRw=Yt}UC7XY_}IIEEFZWgtOTzzZV3oH+t>OmJNl1$M`QXuF3U4a$ZQg-DGvESL-s1^j{R% zEr0?G)|4HNlKAMhTDU-&n7FUSyTnEI`o?Qf>FmJAQ6fE;1dK@Y+g-P} zX*i)j*9uL7larN)OLu{~dd_T7;MX_=M@D`_V=^LsImY;~G<{@<7y5zU?+<24tm4CT zA;1Eg**qA~6|&=e))qEx+_?Po2$(+9ivXPx}uHaV^49EsP6jtz7hc^FOEH)3-y-Za&M1BBIPv0W09S>KR zINO>|c8>He2{RF(i{(q#Pa&guHJ^UR{f^#Z%$)jMJIYLPDalHu<`ghJ9*#4#FBB^kv~2i{oTHrYXD4uG9LZV;bF5twwM_mi=w=Afn`6t5_ZV=nO8Jt z?x$e{0r6bC1(T_Gu-Awn=HjgIZd{^Lf^Yu925ig$jz;PIBpB z0>mMrsJFpWXOL2MULG@=&X1I-lr_e|mD>2&wL}wNOvFoTiEbl^eS3CI?LfJo%*l|4 zh=D)wy6@A)xUq+&^2v-MN9ksgN1B**3x8+ol|O^hRq7h4Giy1Y;>MJ6h&^UMrhp|0 zDvt_#F!abhcytCSS4){*K2nqxEJIFaouHGm*A#}77kt0^H_Do;EW5lC2n{z zSKnU(rC$~B=(4qSl&zv9*yppbCRgi@+5 zfU;`i&TwAo|DTyZ>_!DRj~GBeU;+n4xTyJ9vNi^Vt{iXVhnp%@sUXrdwBU)Ai|9Ji zO0bAfc$?{aV=d4>6|KHLrjtkSSkQvftZtaKoMP(?>YBbuO=FeSN>|e8y$o4#?X*zbGpU9@sR1?Lu>a)Q_$zl%p8cf9>WaYjj!33Jh{y3`r%&MlWuGHKPL*!{3 z+AU;Sz{-$uxv;iMMM*=m?2sk=cF-wZGEXgR>pz3^;K{42E9O*7F4_qSCL!Yeu|mdT zW_@piU0VM0{x^FALQHNGB)DNf01NaG2?}cuB#D`G>|u!6i;oOKyz1s;2#@F znL8N%AU0IjkAVQSUo{noh0t2taAJY!u%RWieKcP$v@fjZ-y_pY<@Cii9pj4`>aCA8 z*DGVb;zXtLbflK$u1k)gsAfQNk%qBLXlg2-g|FJpz(~`GG;s;xU12yZ2E%&f~ z8)Qg4`t^Ivz-(KN9-hEmga3n)=0y$-?@MsT9@y*EXNxl44`}+=@9#ue0D;YiH$TdM z_Z0X70ys?O4v^|Hz?+XG)bMS%eMx+SmIIZj%~M2mz>>#5qVzg^i3vrZ3j%=$>S2z< zvD2dZ&rj64^!D8L_zU~jmAK>u_d?|Wc>Cw}=_0oO+t&e!>@0x1!A@cX9twhlhxYV_ z?f`+hEAXjD#8ZenjY7}W1`@c3&Yl^3!VxtiHJg`N8Uz=aDFCAr<*_FPFr;7v6Etee z<7(Q}rx+We&z^(fXjb<8^S(SmhlC!C|HNX-wnM?DFSLA5E{@-r1jsa;13* zZOaQg3fJ4=m=iM!#iq}I>8h8fNa<@sb6Ze_rN@jQA2nWh)V4)Q`4p7dtEqV?9U-n+ za$`XsOkGS24^o#=pTf3lto(ktSMB_XA-`I~sl7p#>-3Gm?ZCVwOa7yo@{~sCYCgW8 zLAVh3<-b?X_Op`kK9xB`zkDKKL!Wz z0klzALb-iT7A@ad#L-qLQBkD8Ln6^ET+z2hd#D;!{{#k7h_C!5zER)0bVvV*l6G%Z z(HhBILLywkbos>$PV(}@OWz{dsox|btVlHSm9q!DYn|WUJ^u0qP!Is?g8&r6P)Weh zqun%M!08uJ0MG5A5M`Q34$AXarB06M0?>cAVx~;;g%RUlIz@qSHSi_CBalRs#QO(P94072_4j1ROi0k0u6<)UKistLkz;KC8Wstp- z$Hl$pCF8!Z{MZOXJ4X8^My9)}!Pp=Uce>J^(?4DIAm6i|%1f2-XT~|Ur~S*osX52v zzk+S@ZFS1I{hw=FWLJheIG<{N{rn}jVP0&B)6I&?Xz6+1e)9U~@}E4szj>4+))bde zLpjO0)!`MC2kOU_Cw-Ge+N~L@df20O|T!^bJXwZ=B1T*c>YbXZ(tFX;vMp2gL)z(&$oMP^D zUMrh?e^J_{k4CvYC(F#KQCk$0&k~Pi$a^@8FwWPW{c~jVtkSk$Ra^0E)|p1V1^vgF z$++w)qqzqGF>$J^d#c$5RSjz-BP^$*)#vhjmv7CZ>ZdRYV8U{9@9?SMWnxT!KOOWlOA-tHeBqPXA>Bx{Xc6}cB;Y+ zGK-!GUk|Q_1R~2P&BfchH)3~BpEv*Dk5KKK+?xMf)G&{`Pw~`>x~<+k_=qGaz8L`2 zT>7p}J)84qH}~=~*b*^{p=A@JuyPTPmf*hg(f%y#25A=Nu5OZj-)3Rqy7<6r-fDRy zuP7){VRbkr?c$P3mBJmTr_rca0lQTzQ_QS98)rfskxsW`>AEC+5S~)Q5URX0$E@^8 z+;#lpmh5H7A@4PWz%rFfZp7Ib2x^G>JXJRzB`O1TS#`z25jJi^Bal9+S%nl(X5$e@chD;zt-^_mal0c~*=?8WN`8g?QFix6UDu-}F>-?@& zfaps^ZCN_MIyDHkVPyD76(=Bh?&wAi-J>ScRxg)L083XmfyFYRtYvHq36@pet1oY2 z2H!X&du*U(s}$=Xapo>($+H9JH?FQ#zO0fx`lg$L19bqIzp0T9cA!o&jf~kmWd(oh zJ@g3zOF(a4<7$|DYmpRHIx;QJ{jo3;B4BNo^og47;FI6%PePtLojF#wZV+ty7ShXuAvkSMg!`?aoE5{M;52A6Mx47;PI9*f?e&ozPkQ zze`{`EfWkH{ZutjA0}5dRZh3L73lr zXL*=G0BX=gqlW+m-y9@hdba3;cGXqd*nK4y#aO@EYwlYOx?pdZR$G74)bv^{sv#v> z3N9>~UOTY|i4=@vC)|}9C*G{72<{W5nAm3OEJ-x?_7@kh!nQ8jG8XafAVCS3I`S5o zyV1)}Y&bwuRd~!buw3yMyS{eX76rx`SVRI}R~ty+)eIQPJM4IvS%6sCb5n*FZu8=H z*{W5phr`laAEeiU%+*3BUuegATfp)*E}w-)Z}Dj4Mv`FN*;ANHoe~B)s_S(E_toJb zxRsCb(1mLhmh-P34xCeS1e;g=$pE+zFh}GJWp!6r#}+OXDCb+)%7ZKG$_|NHe6p9} z@ar4XU3)&Rv0cWP+;>bYs&=L{jOm|HQyd4zZ1!Ywk-Cr|5{Hs(iHEZ8N`ghcP$&j8 zVB6I=&uiiQo4K5$=d6>!vm`cTkb-5O>cbV`6~_UeaGPLUKaC^i!JOD#q%>v3xes)M z{c@R6W&^tcs92!ZNCdrQPM$>xJ#~S%c=u^gF1rEu{0zzuY$u1nGyXYNU%VK9IKb~Uq1K5 z$IXW$dGD`1YRMHz?`@i0h4z2KdJD=GO!nn37d7Wg85T(n&+hTr&0bG(b>dA}!fsL< zibf+)V4iYDaxDD@zAhUT?Y9$J=k%zclwG#;B)-WcgcIaVl`aFiQc-@v!l-NxCc*ma zs(i(gOeb)|_vKNv6pdlRqyc~UUQrP2Hs>2gV=r9!i@6ZHm{kVcmyplTZYN8@9lhe~ z0xR|e+wM9bA`lZICwlh7<0Met+aNZulw!NAn#0lyG3W zWD(&X&LkMP4=7L2m}1p)*#q%K4zd*Z38nXb6a4N_1I|r&gn}vd;>P-sQj`?=1L>C) z3FW$!6g@3#Cvjam_YfMnOInFpdGW>;5nu|gaZ}5j!kP(MY^Q}$e@c0AW^3d(P#^)L zz4{NVEyg&th-lmVE*<^U5eoxnwNfcEMISJ*w%!%)1cKigrbyz}Onn$fv58EmXABe^g5nL@M;dYW-d;=9tW$w-4nibAQ4R;`UrodManfE3m>5IZn_ znJPmg*vHx!0It{-XaD*d4w9g#!Pr6E%uWk8>UnMz$Crup6E)55;Wl32j8q|2=rut+ zWP|{TNhu)&A~pnw5fq4Y1dHgmN@^s9GI2H(fNiJs6*!(QT0S@HZJt|nE{g!cwQxba z$gX42X@xn9F5=c@RYMoQ3t`aUf59D;`!PF?F*aJ+7=GH!sgu&%-OG)|5?^vDYOML@ zrHhKI|9BCT;jF?A^%W7`*vH*R(s+^G(`pLKG#bioM)e>(S=Jay>}5hNeyI{h+b2t* z5N|(YGl$2N7Qu}tEukk2LAg`e2+~WZMXqMxyI;_x0D}XXk>e#ub6o13Jq_H8X*l)f zkC1mR!``J9hQ>wpS@somj&baNu zyqk;NR@I<}?ToTD8-7a{dQR8xo_fn7>w+Hn0eJt*dF!m$o&#=v1-r>M*@T3x880hH zhMl?vbMukp-Z@tr2uF2=T^lkQ2)zaCsmR@r+sYcKR~q#;`a@d}RZB1lu@clMh&3@0q?Me1)N^3C`0g2@@lu*9|}lb%M`gI-2n zt}ypujk>){Y|*S1$IgX&CfdWCt;!){mKn%6p5#<2d9Tk|7c{}Yfa$j--cW3Ts&uM1 z0Rr6GQ8WXs^|xPRnqm7Rfyc)QMeb)W5>suv%eQu*KXC})`d_%$*CZhr)~&ah}Qq|0;0NBDTRLDbtKtLe)D2LjzO z`rdE$79xAIsf~1#ca)b||HJ!iz^w$xRRD|KSxaZ};QVifA~ zyUDKY@4lO}a^6O_qY2CRxksanSCIMn_{%zZXB<8=vl$Nujtk2YUW+7)>*l=9cF*7v z1*bQnJfv4i;}{#3e&sgjWY=L0o`n5Vb7)O-TS)wfwW5DaKgw&gr~aU5iO)cSfg1#< zxtjj%Kg`is#cQqn{iw3`dlII^A8TA1@`Mwm>ep+z$b;;N(bBqFnM;oe;LAiyOnR+* zt<%^2zoViEc~CJ~l>{3X}3L<{||V>rXG z>4#%QB7L5E_`Q$;bF=Eh)-d8Rw{N;#^zD=6kq)(W_OsCa+dHG)0Q~PvmP?j{?B%ao zea$AJX;z_A3#bH3Vu3AtiFjPaW?G^h#+jYn)%(5qZM4zqV^DSVMXvYyGF12RJovL-hIq zZZRf;Z9^!(pVwk&5HeQMu~>8(_jKtG>-9~t=5eiB{{`!RyEBR71llCcBe~yj5R-9 zFRCnGFXXGqY+t0xRiT^vZc}L?S9k{05m63r2F|wnbIifjSPTPFyPPiIXE}y76$B^Mv$8X#5To4X=H;meT*vnTS}i0+ z``_|W*S6g!K#ygynJ!lNm5*@*)<%!Geve6He>7F%qvyVwYdNJ^$A zr*|{Z{uNs|bv_aN#BfKOc4>KBJ3EH4lkfi8QFdNScAfz8uAR^FHc9EwTUe8MdRN$p zO&sralibe=7y-J6{?G`Cns!=fp%EytUOkf4{$xc7fZD<||`tx&$I}7(k!g%c2tC4fO>N za?8?ZO?VgOc|QDFr(W=RIAFH!X*!te?^qOYRp{=LODz9GAQ`ln~>98FBmF_ z*aZSgAPj-})|ZeXSsn%|%A~fFjZ)y2(e@N#zi=j%taBMYo(|0}yf@0*9}9_bFJxaB zCFW`Cvl?goy79CgnJ3&!o`+hHr~9yWmL!X|eJZ^@KPUNyA>`Z7*VqLjTur@pv1^m8 zpYC?QEXav}m`@rCJcyL;-LoP_J0KHSQ$ph&3d)Wy8d)B=spG$=7IR zRp&JHQ$?ENv`*y$*KE1v_q~90)$Z*KDu!BJPrqI!*y^M9l5xGny$|dAmZnf3JJqO6 z_y`9Bz%tvP?6~>RpV#`G)YM!q^<2>_*i8cN=fp_uBh~e+&o=T8|BIS79262c*+HF~ z%bfbB4E2+|BsO|oY2C-216ljfZ_DSGK@q*>ql0SA=C?iAp1#*l;04KDnlop>Mc)k; zwypJXf6HQOzt`sy?s!L^5-?t^d%Jy8%ds~{P|eBP@O(b7t}U2+?`8q5IPSQ76HN^~LRRDawk7zd;m7k-%Cy^WR zVB_86ob$2%_CRi~-apo*lVw-KL&K}xsz>5gBQh~=LfnwEm)Bl? zPxR~U2ei zJQ3M5=ll$?jBbx=uVrt0-Osb`b*qk45BVi?yX!01u+i}S(BZW&|KPI&&>~4ZCR(wn z59+r30yT^mo2#1D1Y`o_rtJ-su7abhu3FYf9Ldc)=1BIZ>q@8sJ<5MvSvV#6qf1+R=I&=^-$W`E3*V8=F%-_cpd+y+I7Vx6In_+ga$L$yLJ02i+5>6uve8zwL-m#iF5a0IPLJT><|R9+w5GO3~0Y~Z=%~A|EX!i zy&nSJqiG7rYFq}7{ud+|}8KP|>Sepf1qKLd0ByB6Wq ziU49YeDIa%(S`k&r;1SxE$U|=HtU}RsURTB4Mc(!1_1<)fTGrM{QL}WhM)Le9FqLg!TR*L(bmuXJVW47%~jP(xteM}v{TpS$KX%UXwo z&M4T8zpsF>3NbbXWP-a%q;%?xy28S$s};9VK%hsP7`N^a5C{!r^pmB5 zb`VhP+6^HP%bFsU)!I;#opUjrAarQ30}8&nyzZ5?uDM=(ats5S)r)mah+^wJnx9rB zlw}Y@^bhW4e2FUIRh$r|i6!ITEMD@V6rvgUrFjqS_81o80|3I^1mo5Qb{T@ptmXo7 zOU{Z2aCa!rlX$ltW@QA?b@gD|wzO}bp)gYAPRbhz2BH zL?cVZN|~Ud7I3pLNxjbznd!|{Kq;gQfzbPbjvtyx_i0o<%=Tv1TVEnUq`wG_xz{Cv zKRJu__pa8nb;{2QQF4mHj!Wb^ey*B_4bGx-cD1M8!(HPg_ANa__=7tf|J`?aBIK;BgKEc z>%k&dMW7H2GoAQ&V1+?U(=*S_hruoW8D&g2myhpd`~^(y86Myg*R2?%hRF1!y7Bo+ ziTin>s4^T0Si|-mOWr4%#PpAGpYn8l{(V&Uc+|W6*gvY`>u7%=CBdZcZy)M)SU{|x z+l){5xtta%Y?yyJnPn>kb!VT?>QJTiW^s_W7Z2HW8S#2~XP49bas31{LG&E^4egW3 z{q38<5#a%?sXa_a%u==c%c-}*YnA&mvp)X$ilSre0d!{e{~P`ZjC|YGQG3M5&(|(h z%}T2sJ2`^H;}(j3R5>ovKfX}|WA50UWV?v)HCcI+hfVxlKd^MaX-jPJc(Rh8VcPsk z#?JXFfkDzg4GP^-Os4pCBBN}h(@xpwLdEwr%*J+i{n^t;-Wckb#6(g&OyBqfxo4&uR-n#PjzFO-8 zS2unX=Pym>7Dr_7d7SOB(R$^!)M}}#R>O3eRx)F-!xs7y5J${`009^Pik^}H4;}~( zvDf}Ha6l(N;fa*DGbAwMuRU*nT%dhEv8{OMYEP;6#yn{B=v%yL=i4Wg$Wf-nDyr&N zH!x^(nbY{-)YvSnghP^=&}uW);~a)Hf;p@~qI=tUR*2m#ggq-c{QdxmL(_9yg&#O! zV33i(`t^O1UsPtsN9a_wyMH9W{K zzHx4Fya=^Un1|Lf#Y;aF{%*E>@Zi#n4v%mP%SH!TYfEuTy4nHH_kq?A@8z12_S zt~2!0Ya6#EUUsE#32CZj?rh$5$=Ov|ee`ZD!i&rHoGh958MV}&!+{5-@or^wyp0-)e;#)H$P(O-GG;uC8~)^`tP=bE5?KBMqI`X0}_eo%kxUUwAj zGCn_)Y~nXOM7!ic^)tI7b)V+><(41`xpTa8R(Ji|uTff+KT!$Vi=wd4!2!;Rs>4AE22pJ>|o7_BgTjcG@zZ)Kr?xI(x+&t3R0c6L{t^Vl6jW zU9zye(p$_z1*ntL?9WFdfSWC_^BfWc?LpL21iVzaE%@{&^hpVRaTIpB_ z*gHnoEK4T>K0ffUs-c3!h(`6^uKx$ee5e^lxDj>2AbFXpuWDo7Pisx4D%&43X{?9G z@$HC^FxzbwrzU(`w_zO{eTCWyn^!rlD}#qj9tYFcyLzVI%#^XTX1yl^!+8Zyc59N?@H{wlC6dy0{FSTgxn= zDl5-BBnXDcH@7E%CTOD6r&UDeiV|-SRPp_rE-jZfty+4)w>1dq@+hD&x z3|p_J1D0e=?D^Zxux!a%i9l3cL#AyLiM01-iKA-I$G+i6>1~fpHxJSIq`X*~z!8YR zOxvhwZ|}-CBmqM@t!}$B+Ek-eP|!sh=lP*Tm+zYkyZzBwr7AvbQAP?)Ja2Jv^YzIA21f2Kxw+sKtu5VARgIiYTlv3`qZa4l zJX?*d@S5w(%(hOQ=LH}@vDbdCvo~h^DKft=;+QYVxy3nM+e@@swbbJg=6kE7!A_#~ zo2+r7s|6NE>neO1rSpwbVoKiuXo(Nth!2D5nz}$(p64_X>HBscU7v4FZrhb4uBr*| zmR0i{k}zi6yWSWFp-omjoQ-=~0|9|f`3Km5&m(-Y7I2tqR-CI~wC|gY6+RFN56Af~ z+$I%0a*8PcW|sF@zyW!kxdSAC0!@Goez@L^c10U4<3cxiw@y%^cr`bae#7RNo$DJtJ`yKld{`F39K8P{TL3 z9tzr+?03vmwKl+b6a8ZlV&^%*0jNqKxf2>hfDTR6D@x7shP1Mz?2283*x_|}{M9w- zKuw~$rUC(x_J6P<^uL)vAMdp92j6Q)@PQR75~bCA)5@N-OK6XJfd@A(8hsq3u<~=^ z=)uj3|4%{&^s)GIYn(JgLrzSM-@9AG)Y&O=NQo#BlKxW}BR#Xlr#W*pKZy?YW@pnd z+dka?;9hEhYKVNiiFTFEviLaQY1E48LiQ!zUy6b$zlg<9h5!gq4_DaIU^r{4qU>$k z(!5`nxH*sl7Ylf?-Xz);X3u#6MtSv`t9i7N`r`Tlj1IXNg4)|3xAGTz$5a|eg%KOvpSvwmo>x~>I_g>on> zpym4Jdsx=C#zFay;|y(e{y16Kv*fq?IZDamI0QC!rv<2_6?={csk|Q5#$9Xd5<06{ zOCu|}u}yPK=zV)O^*sW8>s3opcRXBZb4~5qDMD4)w-Z}Qt%((cR?*<{Il_%K++93* zGFjY9;pQ(&SVzCLv!nfTf0#oD*RZ!W^{)|*C%&Z_`!Z$cH}dUMG|CA_s)ePdSYd-a z17C161h3Bv!!-}_IvI!;<{@52lth7c(iM>@ff55uT?G*TNqE#wuA~{&>GTp7`OK+( zuVv02Vq7RqKm`eG#!o8)HVNrUd*P4|*(_VsJ$)w>MqG|XnfdgZXPM7Ml?}Q5yM|<- zuMUrqOPC(9u(XcFzFqQAEWmxBY)SL=Tkf-G58QmSlQZ+(v~3HS0zf{}(@o zM(Dm3!;|?b1pmG*!!+<(Ol{wc`ccCB>+No3{AAp~e3+LKmfJ;+vrvYtR0ifjib1LM z*bf)CE!fh<)12Th;$|CjS4QiYok1pG3Tb z4G=S>!{|JNWy_SP=FK8(p285f;`M)g4{J=<+)cvpjVgsFYSIX77^)LrN3__)dSzek z3&@C<&}ZBOF#*6)10)j@3A|uiryOS#!RB5Y{=m>h21q`w3W|Kj`3rvJ=!iD1C(@$* z-|#+$z{#E3v*_yFx&UN{vBuuq=}CO@yf8`{DYUDJ8umUAQ!ke}KxTpei)Ui9CMxMb zI@&?1lcTkU5hpNv;mA?G++5@~4S#_&!eVB=$>sMvwm&O9WXPb0Uyoo@&8w zcgx|I*yNGWB!&M5=$nR`wytHRg8d}OA_((ed6{}?JbZ>7J~PQ~l3B&kfltk~e>@Uy zH*rXz;n1x*f1(HL?m-~GalAhq&s8=%A}C|{GQVK8mPa-&EOA%$o!Yfr*f8NncJ-W) zIV)!9+n5u=(o(atK!|if3*86?t7T>Tv0jK(qi^0|DHzcP^^~5!-i3x=Z~kB02&zL= z=!jApm^MK$MIZ~e+N7k{xXRhPyx+Gjx8pm-Sueea_yk#VMV%RDHLO|vYlY{ItOs^Ukf`NPXwv%;=TV~*H?d_VhUbk5O# z86HGC++aBlTqKhgOr}MM1_0g`pF^p5Jlt!vJpB=kx)zyzN7dqZ07?~9SS`fVbdfVrF*Q=-B|F`7 z>=1>lD`Qk$UTWit>C5o%v0ucXOm{dqgzvVbjgeV2{V!Zgc-3cu^XsHdXCa{j2>eYTPHC}EarCFGVXcWeZ z`ez`D(_lCljJXwIkmDbuyA8z~dX;bSPAxo}EBz>eo2PM(j3wBdrrhP4^JDK*z_Z>W zHwi#POHmZ0u#kGE%;Jv(-NgjXrdVWRHX(V`k1m3%5Fjr{N#?-H7ove`ul~7ulbV$i0sj7xE~aQIyqLKc_Ca5$V2Gd;F`+0rPN}9a zoUt!fsw_1%b}8I0W`XFg`Tgz9KY9gC1L{j)LN5r~E0{h+Ebk$LI&>7VmV!N3%joXP zLIeo&CI%1FHTY5WDO!}plESB$7`itK`j`@Tj=3c17ONS_u(JPnxnBwWAM4KT6saGW5 ztYqs5X7!j@#@m@8R7giv7JuynPIWQyk0y%>>dASrlCO$bOdl=5`$-XpK@@vJ=UW6| zI;HBi8WH(hLbf;W!P%AJSs@?mWsXjIq5i88feP?iJ+|qI6sQ-(qluVA7F8)Hu}$m8 z*L}aJy_}b!0lihSh{x!?5hf5`NHtSaS5$A(iKC%_Pob>;e(}MZ5<)*~Jy<^XeOko4 zObQECdyRy!+$Y%|b(GCf<4H$M_=)}bUs)aJJODR^%UaMog7r6I`3MxwQPV}KMqVhc zFn>w(S-_zkNu}?1>V^1Hh6Drbm2p-K=z5qnU69q3s)#9(tLjQ?c|1$aSV zPjyyRr5zNG1_>#r`P#a#bzNP5zNtEunmys@-UNceD5w87=s+dh$S(p(2zd(q1nJVVhymd`m?v~Pev~_e3^U2XX zy(W}NwY|9Et5;Uk9nidNGs&y^kI3A4MLZuxgvbRJTva{*-0CU~bi{e{KTd^#UkfDm z|LG^Nu4eb5g0n3tj!Q_fh)k9sU2~kZR?NfdL1-7!Rr!dK6**YPz4zj$^Q$o4RvA%N zG@G|BjTV^|U|Mn21gKWeG*=ZxuyMgs9gE%qp?LaJi>c>6-;Z>85)~tAvz6Pk3beQtOBSt-LvF#2DP1}1vBX^s-7mo{}{-9aS z<3u_{gAZ>V>6!Y!0JHY5*y$HE@81*^cbctRR&|^>G5`U z&r%e23EOz-`v5f7f0scR_`!4tuDDC&3qT2Y$?G}U zipa!mxczUw1j>sp5NZVKjoO1t-#So5p@%`xeepN*;eY(4b#YLyADke{3+~u!r?qnX zqt+crnUGmadHr%_z$5?K{2uGtA@!nhg1hW^+JtNu)3aAd3d9}$T)mrKw8a4blYwuM z)ScgS)cEn;1UB&&Z3tY6hKPwz*LK#yKmzI;nQuU}2H$Oqe!7I@rvRiZFSE>P8_M{A zd%kBQapX&;9U|*5UxAynig5MC@&$no>OK8|=>b#2#%SO{@dw}O%lz)7J1pg4`K#Fp zN7Rt6knH{BfhSUFg^hIz&HcV2r|n_MUudI8bBNg|D$BSbDECpMpxmr=$gt=F2_ZFG zIdGT)hEqx%kLEKxKKxyaoTgG<7dEVrdV{NOhz1%z3z|it_VDzZ109o-^BmS>9oq4d z^};|%zKZIf7Pa>culSV$xCg?V5Lk3_7k({HZk(`an6n!hx${XdwXsZ@k~n)(}7a*|Oo!>%sptaD)%!$d2Z!@sg3w^9}*cf zWar&w5xg0MdL6(f|Fh9GQ?@~hRH@z2b?W>Kbjab*y>8eEosd$dVtLMf$8*f(Z@;`> zKc%sNlZ~;L;J!at0`D@hzyJ%cE~h@sV90i}a4=2D*d>K2RjZ+-))4#qC_Nh%&EIgo zl)DT9i#osp{6no#6FKeOK?Z@~X8hrN4qTz}{Ro>_jH(&iVKJ#LeWhFB+Xv32u&nmiwHy3tFo1Q3SdFwnWw{2Bx8 zBP&M)=*7kq7Da9Ozk)FIjQ_7*5eN|sbXXvz{m_47j@V=sYwgOexn~7otk7@rg)Pm< zEDb!r795o-^$FHdGDZhX@}h=#{f+4OS0oBr6HkQy3A@ew1wb2Ton;Q(~{J8fA*jla5y9KMuV-0OVWWVv_dELz2s@r&v3=JZC zm51E%_8}v-O>?w_@6(zGznRD*;~ViFrSf#>w;!xsEX$_I*i&Wqoh?EyIq0UGU35A4 z8q|{ur@mpBcffj$^vpM5*{T)OSbrOigNkR(q=R^I;<0!bAGSSwbUqz16qFE|h2Y9l z5flOrs#51Z(8RSccQZ=^*3i{xQ%}l(EzROvBbs@(a7xi1g&>X_3mH+qYgx2i&`{*w5}<_u zbub^+3{Lfb9u$3_lIk!lMX~7sR7O}0nedl+ITt=ALcf7TQS^r*fdEN}`b4-J5peV@ zJsjyhY9ZHWDIO_WZhUQxBQvr9hvzNV&T0uFdriMF!n2Oi5Jmlc;)D!I_UEJe9YQV~ z1O==1@XQf;?uFPZ`aim9VD@6D_|R1^R2ec*v>&(7U^{E1URXBkrUW*qrHUL2TW}z! z!Q-V|Au$GCS!^_lz(>x_OK#TirKU@3SHF+j)@;YKqBhG0IS_s%m>%BE;JStpd2{K3 zVX*Pw1Vz7<58gUaa1Ru_D4pz$ z+{~ts%f=PvWw?Y9vi3a07B4jke7$T;qci=`a|r{ctS~Jklb}Agw2)D3s%)Cw-Q3{3 z-I}o&$T2DeisIpg%HJ1TJ5%7N`(VLeZr$NQZCe$WI^PTAFtQ}g#D3J;HVC|hlG4=B zHI5%oEE9u^({3;npR;MqZ^-W-jG(h~nr0{(+ZH!dH7v^-$BF1UW3jzg@uV`sd*_-& zuXVrCvm_j2-guAv-a~JR&f+YM5xb`PIId3I2tk zt%L-3UJCk*^snTkZDh`}2~WXVhIByovJJA0`;6=iW|CwOSO7hRHmE-;+R_=J zq&ELIl>&BRV#?sd@3jKvSbxa?9b&U``ro@$wFPJ6gtZ-5fII3^Jk)JAGuo;9mW_ zNS6gVwvV{s?qI{8g2j^?MsbLTSB}8Pg^yp@%%K*+G%PA_vXKjB+wy{i`X%m*q+mD# zgt(Q5?8E<40BGaNs%QG*_6`$Z`lR0Xr9b$QB!UYZD=oD%Cf)(%NBK3oD)Zsz?FeNG z!X8%b<TjSzl)lejgCS+zrGu7dwB#^DKSkmy@7GPj4}1QqMR> zkQQB z$E(=+FM#On#5NO`8aQ*tDqAj6ZqfN%{9mghX5Qy^F#BWcn_G%5QqOy5fBW_FYDvH! z{!1W=SDONndCS&pP!Mwd-~|_j8{fA&qVs?sre)=-M_-;a@<7Ddl6{R9M1k+q#R{LydyDZT`^i^K;r2e_+eELAd3O=)Sld-!`oxw!I11g@f7`tz}fH8 z#2Yg6uQCoeS{Ahx96N%ExfC4x_5T#uyb%QyVijOL{I|Xa21I6WAkf@oIFX<>XaCKW zd51&`%oqyM;Ud53jcvv)d+#|UOLiwEYD~h~;KT?#u6G@tI zORuk}D>M@1x*yQ@{kn1W(xSdqgU16*3`t_6((r?f^RLKu+_K{E6@NPU?IphAN-|X> zCLD5uL`U7_rp2|yy?9>_{CV>5TNd#kWR5Er?X{T%36f6Hg0b3elxx146IAR_Y(BZS zc@^S7i&u#hW=*Zj{sJ&E6vo}d9~M~5xS*6JhZHHOtm+ZruyAeLJH7u>x;Xh=)px&B z81q9hO#OX2{!H3?{YVl?8VE$Vj(|rWEEZ!27Un^=Mfd5YR~UsKkUn&LarDzhF06Mt zjtuqkRgX(Tx=RTbW5`<*jlhMo0gSAxa`RJ?&5^aU8VLK>SeP3GcEpH^Ot?BguFy`bb?oL0t>UZzx)fZoV%AW z55CW4h%#37XvjsB%h5==6ux4mIX z@9ydn)8Wyg0{;_<%%VS&>SyuIrLLFAuu z4qCKF!Dxg(c{Y?oFHbi{-Pc^khml3P@w}nDjlp5yef4;V9(aDW5UR zui><3#e5w2@re>AYhpOx{5)cy-x`77YWDEQPz0qnCF)i4l8;cF?zd3_9||F^+71+Q z6zI1xAdp~m?CKjb4v-Lw4+HF9|K!pzJdoX>k27-a#Y2X-goQdm3#_itYdT%GQG|9Z zQtNe9US*1uN)|`~0gX~L*jAQR_tf6I$kqBOGEx%nlgikWuq=*uD8HZ<2}w%!HYVmZ zMts6dOucxgzEyQ;40f-|HjI@38DDhMh zl@rTnjHF%(74*~7d0Xd0K)lpW#N5TcU_URKUa16nj;2*+vpm@*mv%&C_E1A!6_Yqc z9Gar>$lZ+83}YOm9UZzJ#hI08r=4sY=S!Ro{VyAa!BK?)fI}QdBiya79YX|OzRQhq zV3l#ubjg2sl3Q2FV>mZ?v84iUaJG$h^AIW67{Q0 zb!E#U39%P^CyNiI(2~QEYm|t#+3)lhpbF4RcWBzXw)Fj8`mj<=QWT|7QaB3+6t(9w zXz}wk(@Q(mOa}Vnr4kw`t99PuidzH2JVg^^K0Li0lflAp7GG7cdn+H+%a3?_8O}2{UH=Z^zu1!?eO>0 z=V0(-GLvM&!0f*G{4$sO0%f9AtNAW@Jht=tY=9hu`dsL2gY0+yUw9u*o3*Nc<2#A7 zu;%%fc9YggsnTHs*O))+1?SaGGc?*Lv7MBY1hPxNf8P>FA@2-K7NGJTV=H((sXbU6 zNRdL2ZH~Z<1>_Fc2OC=Uc?~vIWvM}#G}naziWt9M^En;U^~B07AVK8ON)6XRZ?bQ zcaMw)?g(APl-#p}zXCRZ<5G*z3Tu=9E1$Zc)z&IcqHd;@AeX{L<`7=7jWZR%nCxJg z+RdEf1uSi^gZ%cL4!k%8Y#C_YmAwniLIqCWiS)#?W+sR~?S9l@VOK9iXHCe|Hx!5I zUd6|!BKE{4hylG6^GlHh1$BA``1#BscG+VI7??k!;A`vWih(e^^@P*8clBS2#?;OW zt5_w0pi;uA+G(20g(bh?+b15U1rcDvl2~&A^NX4)W@znpcKGRJ@%VW~@Xvf)=Pi)YIN7kg?9OOEq;zCEkE@(JwHqxKUbUe@-u349s9WFVBGfH>9$;v#$5mQUbSO_)uQ>1 zovttPG>Ko-(-&m{<^4DaX!fPi(OChI3pb)J%9vy+I;~a_m;+~p%ReqbY zMJK%(+ffe>*rPTT1I1b{;fG6IhKrsa{+%tDnfh!W#Sd*CGHQ4F*VwcAn;Ztto@NNY z=-%48XmQQNwOq{AsMjNFe&5->@Hq}S!WXs@b_mVS;#>H5_Up=3PFGI%>TF+LZoG-? z2sG-;4*rSBa=nXFM!Pi&=t5H{)%ZM=#O^tX9xSy*zn)5XRA7NJUiE z9-#miaEAxuzWxg~H{hd+Na6rQ0FK@%Oy~+Sl%wQptTJybxu;XjrGlCTAHxiuy}Opi z4|eFF@=F()tvw*Ry7*CYy7nV$LVDc05ErjYg{7IpR%QuF-8viwO=0%+X^)?=Nc&hN ziTR;jyx5Uhe6u&hQ)Y_;unb+r;IO#%mY&EvAx0^kmi(KD&8muLEBE$C2aJ_z4t?F7 z^!=XVDS4hJ`;LXzAd#MGTytJ+IPO3Xx}!7GLgXIb7KhdEPWmn}Uiy2tgEN6#q`wXh ze&WjvX6dBU1%58i>!q8PMjRT@xgYAm#xl+q+Sxx3x=QErreKqPJh+;Pdt=M(a;JgV zy=(b<_*2IBcHd-K&A%{{DtQ%*2+N;lBze2ynaf7?{*QNx!ZcqaiRNmz&mzPRQ^H8R zu)#kma@2F_}(fa3b6V*b?_V7K>E2IjsI^t zX|;c5d5GgW^6>_&Hr^7@SYYb#p~!KiC!mXpf6+H2B=Xawh&Q{NSG`E06gj#uLxxZp z>!y{P%M-y)yIbdDOzoq?G6!7P_Ew9z+LZmlW6x0Kf2C4BjzH_Gq6u5si2RsauK`rXofl1*^$n8p8uz%~fX>2|m-_17sV5vr@R z7C+V)?Z!?T`I+^qCbG+Wc>Y>K2R;vft!+WSqE>*zR@AFgdrnk_R5vX-83ymweswCj z%;u}FN7JpWALzNYAs0gje6zB|F5q@w*Y!+nm&+J{A|w)rn}40C>^HVGa@v-<&vic? zJ=&HR#$ljx&Sco-|2L}06~HlVpzrm}RjO{fC7|eZ8g4dyUcz3D$0DdySrQpPjFxE#DR>r!mF*b^2zG0`1#*!&xTqC(A>L8IWz%jeF! zcYSwM!Ix+81oz$yd17hcLjx@s0yLDcT`qmdadNw9b<1Ho;=w~@IFfKq6?N4anhxeP zACX>S?|psqLC9F+ysGYbgJ|J+@vT~P<}`NmV*@z_?{!4h&K(1*tz~O^Y~@JJ-ex9# zVppruF}H$)NAb-`jCPRcPg<2$n-O)-pEmm_^z`G~C7#IbjvcRY#m?XVq&#vxc8zd6 z$!Px-oU@&NSrS!FZd|6_ra$riN96SSVIe-9c}2zHt=&TZ6(1cDM>0} zBi;{Fc0n?Itnx-*pB={TYOfL4$9i4dJZwYz57L1a8$t^ttyZVq@0TTd7Cqy12L3zZ zjJjM$du^|n3&>DN6^Z4uAS^vzwVY_j!-`Cg`o=p06*bsBS-o6;p&xVr`YHF>4S9Kn z{BYuv#;CG!35FwALatxlWO&W?G-J$0>Aot{=<395N4EJ+4{~*N==Yb9=WV9Zv|M@Qqu8 zMjJ<1hH8OMpDf;)xbzGPNxbLPPB)E+tAI8huU!ia*rsO23zTzgN-7FIB_kFHs9$MG zyh}0|OI$Bz^1q=B9DrvfX#)lX7;dqtn>e39^8b)L?M)nB92mJ;{On`S7wPA8j#G}S zJF7N*9x{n6DF}!cLFi!7 zB40){;$;v;JPq>NBXrwWc%{&r*e@)rLMqGr_lw>7(dTv@tFscqxZ6&3J_8M=Q-8J! zy6tz5L8HZruuI3)oF)^7IbyvzO6);VyCJa?6m}7>kiz1Ph@|m{<;SfD%gjV8@xSO;R&~5G80lh z))HP=Z4!ZfLSAHqg{t2~W*AW)7ONDuojVgC9^AEYy`ATI@44su&KdU~g8_SYuf6uFsyS!P(yjOa-Dg_wwcQ*w_@IB90OlXd zvXyut8M$jknd5x=cm1V2M9oVLVU+`ZZwHW-dPd$QjLG{ZXsEHPT>;-D9F7O8rG~*I z{=4&EFSY82(~MYxf571h>kn$sAW~4?M+QLRnT||IAYjwIaGd*ru(&hKlwv)H*xk^l zn51LYIK!Uyjqb_Tl0ZT$jRwjUAUt|(%p_dn#DH>P!VylB?JvbnDAMgZA6r(tftVt& zoE$KCkZaeOox`OCnhocjlf-z&S zjDOL>-U_5!8#xo8p2DBwVgPpbiy@Prhj47e-i5Bk#}i}y(e+?#IiByMQfjH??%Fb(_Q=-~*?l}%Oh7ZA)wo!>PX9=|RFW4L7yl=}L9Y|e^ zt86s6dMth?KIF2bW~E_y^iaZ%qLzxcaBj@&eh|WqO1r6;q|;KEPH*hL>KTz^zJo>q z5o~-tlUlpFaDpeV*mKwXnwfci$WOzKOzEUjsn)4n zXL!D>#?R%2zQKp@sI^wNj9s?b^oAW+z_|My3FOSM*!}sd(Z*M-H zjj;<%%%GqcRqrx9hqLVVybk$+(0U~!CWr9+9jm7IyUeOJ00lx|uY%nE8Vyfo+Fes< zvtgyNVGlk$2mb5sE8>~a>V4CDZtbD1yHbzm0>@Qvvt7?E0*$XhI_`IJUZaWMSzXpD z^~d!y+e;xsz3Sug4ua2_bQVJyi^q)s0cq3n!>NdduQe!Tx!Ubv0LC`=b3+ZcIkQ z&l~ibwjFw)A&xk5X2VlUUnyk2-1oShcVtxg?p(L+YF9t3B|7^Jt&*O25CLY-L~g1G zPb0R@9uLMZ+N|I)&58j4l1VAG^5HEti-K&_b|O&~DG@R9>sj98kTot&Ci20;SIkPhNiA)g_Gv7 zQ0t>?aV2?_dylpFmp9ow)W4v4Kcc-M2NJpgb_@_4*?&VuP&|oc|+ug0+Wm}(icQbIZYLF9W^lRyw*U$&u=5W)wMey?S>rM1w0c+0o zmc#rL6D_c9{&UIE4f(29O74ShXtmSg;0PW?Pu;2fmnLe^KTb&0!^M9yj zusmyX?0qPFOn13OdZdGAmJc(x_%}A8XWT}A^1l^hAtsZdI)}E?IiaVonb$s;yV)L3 z)6(-4R0=@AY)M$SJ-U+2^Tf|C-x2zp=0pAW4j-dj$9XFkZ{^#l`tIL6t#G&JeLLM3 zMm=vlmYN3dLQ`Le9N!$L2bU7Dc+VDI=>C^ZBtTXpmub3V#K%?TBh?IIbgHdCg-2E( z{r{L})St7q{y&B))wfRAYX8?jp#imkZ&lN2nqLp4;)DLLz9|20FTSuP0_I_3Hk>9< zs|DMi{I4>1(Y`K=n@kcQZB^eR13uyS#;knJW7RW1K0UocVqVQN$lK80zj|-blAU~f zMH`d{~ePiho1}@x&lkSZR410OF_+OMdZGix!JRu>EmjTUu&%?*|AR zyPeZdJF`9~iFRqy8NAv2e0Hm@X|;%_LOgIx6r@e91KH&35825Y1gQQUzfu=)|1+h% z{h;qzJlQjGC8-i*%&N|R-o&n%xgm&Dl>S_xm?&5%4jOCnP0T2J@WP}uoSGSo__UnS zl&htCRXPo?qRDx1eDMOr536i%K6{t^bngh9$BtyfZ91{$#DRUX=qToL!1+gIHe^;v z*`-Si-CkiXC3VQLocdplE3Y3d4Vq&T=AkW{M zXPbSf>hEL&{a~qu^BDSrcyJEPxQPn2Xx=g(bN1i)K#+Bcu*dSdrzBL~#eK5SnYhng zrLRGQs8}P++W6-9C1ye}e=Bm2nIS`(*8_E;^*i!sDaGT3yZnJ)B6|c6DuRTBuUq*d zrzw)n!EP_d!Q;O7&%5xO*Uoakhj8FV$@SjNbU-p(uBY^3U<#iyu$FqN`hw!KB!G(T@}!1bhgHjq z_{2fUQlkIwaM9?(`J7v86Nnn-hW;dnorHD|tNs{);2sr%4Kq;WIW z9izB+`g;}wZL7aE^9#1q9OITsmIcaZFr(zawCnXL5pcAtUFePZgk1&dU;s1n- zA@@X+eO^sxR91d)2dFJS^BqUCD7P(4PSL4Wb*Vco{%O7S3AD~FZgvodOCgk99Dq1d z?k!u@hsA|kCgahXi~CY#o{jqEB@DvJj{B9k#)D!$)oc~*(UXbnTfcwrCHd&RJPQb^ zkY}sh?nO`Gm?CgLzdfA@Y?eMVJ+IJi{1EeJ#Gc@l6(veG4RH!=K6bWT1-(GStQY-i zz7N|Awa9v}&eKbB4nQAh8#%7KYDHWqATOXC5i69X$Dv0t>U{ zv*&C!c}co*Nrf->Gb`ZgvBWV=8_8r}aju*|O)?Xw>uXql0NAnP@~ai|m2tVm6p4Zu z53DY?vsEy&yhyx@CMhzC#gMxW3@d)3E1TFy_bZc0bC=uBrj1Cl&TMY(1w;VIXV8Zv zi0=E|7yeT=H1(6ed2WAx$?7B?ix{?Rg?Au*wEte$(!ZT;F-j|#&>$RC zB18k-`!&)O$ZfD4#qriSv=E$Eb?zFuzNNan-DUAI0685Vq$@*|HUxizFbNaV%vDTX zlDB~J`UkSSG(v*Cbg9y$DijB&00UdjX+B3DTwrs$a+7M=O8m26(h7lD)?G~1nsK&Q z(t=honGbTd3OT+#m-c%Zj6cgkd4^rawM9`*#q7fr)0r2h_21{)W#h+8kSxiZ-Xsii ze*}w@ygq!{+aVnH~F`A)$~vIEY-y-8y)SW9oplyTejCRjXMgnASMpq6D&4pXj# z;lqVF;4vUVD8$V zaE%!vZH(>Q&rMHWoyO*Z&5J<0Kx#2PbulamuN;!spS=RAW0bnVMxA@uAli_ zM~TI(jQd{izvGYlT?&};fhc^g`u9P3FzW`Mg<&!#AZLf)aT48NhTme zeJ4y2F@fsRmAWzT91xG#yd=7}3D|b0r7oa0z#l=|1W)%ritQ}jSZj)4#hN0;X7cW*rTZfGu zmSeX5PNIyvFI|HMdb+!?$)?5w>NN|M5-bj%(c*CtM?+)WV`in3(BmbQq>VS|@j)Ec zocT!o^o|jsDhZ{~b@s}lyJ6Oef&&>57(MZPdNiqN#GW~RKd2Xf0asDKQ_MR+J_%j0 zSYhjcvN_y5Z~EF(s{cS{@oST`-2&xo>wSB&Svaa35XmfPb+dk6J8Gy%opP5rKAjCh zO4uTV9MQB87n^HrIE_H8hJHgH0hMcFGEO*TA2wQ0ZmKMD&8qwOqW$eZJMFUPLk0p+ zQ>f@gRAY*oS}Hl>)?D{AqmDYMny%}&CYW}_}!o0}oW_eR}P;jo3| z6}<80wsEuxR5gJ->&ZLQiZo?Q1e4@s(n0a4XRpqxxrWBJ2I0~q;&!0ebA%8$ zj8x@SY8iB7h@J1>_$yh~tWA{b70nu!QetD2qGtF+MO8Fq#?%Q?mE&U>vZQ5>lO0Kx z(@t8nC=D~b_$PRa?JLDiM6V-Do-^LEV6Igei|=%|NuSiU zNGr zOs89(l~;KPc6u09LlZd)M8LIsf4G8ZQ>=oD%M0JrxE>Na?p}CYW%&w*g5Aow@1=kh z4yyB+IZiqwbp&H>rh?t61OABx4XRJJjE*mofbrW`o31Q}<$Ppczp@P)_m`u`2`6v# zrpDq)YQ=jW{MUB@Vho@saREcR!sgn6v2*kb|KlHfwND2OD5>qTo3)S9yp` zE?s+;jg|*HyVEGv)4FIy!SNwQg1=+Ju8Ug|EdVW{tDEjSANru}))t~(0%1UwC}gY$ zH;#$6zy6c~NK{r7oVl4( zNC3I)&@-k#VSw*NxdtKtzVFNqfRyw>$yMQz75PIL0;9JJ;_h91=gX?RBm!Bk{5TLz72P}^ zkV`cjKj1*Ykq^p3(H4kZWML;bXmY;6rg6EXi4_RlGh{5Pw9?fR$iKP!}61r{&Zojh+ViGxtQiT<>nOB#Hp>()nYne)o_GUP~#o{_=&Z4IfBVTvOSk5HNrMiHd?O*oG_7kB!UL z1qxO!qSeEK6^RZ+>3z`wNOMtn&>K?4^)3wWQ))Ur7D>s9e{{_hZYPfJICaRw=14Ku zMHHf#^KzybaF5J{ShM+<*B>6m%d6?F&W*=h64cLzxU409h+S$!=b-rFMiy}sIL3f zxh;A0>O(~J=`(EgT;_xBLxLtjmYpJAbnMKPjHc9?_MS=cCKfdBWM{EWZIFf)lIXSq zFe)}Eo(!L3fNgi0@6YByZaI$8$^kb05!UYx{S&SRTCAK8vJ6=>5}a}t`a}t-q6BF! zM1rT2Umn*B`dl$$2p6QJuQJm2dhct&K(`k`bqjS1r$QD4sK7pH$XI|8CHQLLfaHHJKB6@(|{eU3f_QoGYy-fdu^iIu7qd*v6uYtGfCr z3-Rw|{;r&UH!MV!tPmOTCkrS?>VV?8&NJ$3diuS?Lhlv+z5Q*s@yljf3Bm(A+koriwNQ+nU9;4Gjw3Rf!<5Tiqd4)x5^V=lfS6qf^Gv^H57M5 z2GCE&B%pMy_}KYxTFzt<_a{sLf=Jy0`|sucv%E=o5+(!?fVfd{fdrl0@cAEYnfk>B zpq&L3_|Fx*iFHX36~jLR=HgPsou~sGG#T7tmAQgJ5!8E45LCwbdE1%6hAUmu`m=Q2 zZ05etmn3eD$v9@8^M5#-cWl$K6Dr!8f4t3JG#Ijsb0|1?Jm7DwH4BDM_w@g}9E5Yk ze8NQWd#d2OMWBy|(<|8z`T}M7G%HV=hfJ$p8k1euvs(VR0EZT!xI=)}ftPddWib$Pnc>p9S`Y7ZWp)8CLBTY$dag_j6&yc*L+7j4+w7=h znWZ6xR`p=sJe4u1WLch_nH!FBFe+=Us`P?)=XDsfUO@Ba_~Yo@Zj@26N^2UJ^2fZT zaB@330(1hJ(z4@GL`V3$lkBe~gNg7PQtQ2QEBM)C zXr>z-EoP#N94w*gSd=aYeLT$DduJ{lLZn<+d-gt`znNW*8*#d`D+gWTlz8!+Gg-8n zso_P^F0y3PH}B_}?N&id zYepqo@>0_m)adB1_h@J`G=1dki<*c(9_~)&a+6dSn%m4}(Yg5EbP`QJLCRn<%Md z;^W}Cv1gh!cC+-gdUigo$c z4rQagxyskiz!}*uZCN7t#IlQ>m2{8`e|FacF&hHfzFxtAj-a+f9E~{2cz`%*y0hu+ zan;ii{!Kg$J#y@VPp(99NaBw)>G!R+dCk3O{7qEZd@)y3reew4WfsZMbOqK ziX$~mq}nMzACaw!Zf}RF@Y14OJbk#?m5locj7W0mMwzUG)ra^-#-Q>C)y~XEivY|% zh|sfrGOWf5g!Td4jT~h_x#o9Wwmv8%F2_%GK02L}Eo=V*6KzC1F?4biU??ummd+hA zcofCU=-C$73V*=GwDa$NjyZ0G$2S20BdlbFP(QrZvWC+j8K!>HO?H#@L;VFK(BWb4 zUC-{Ai3Ie_Qf0R+8IHv>n3EJKc5F|tg3tgLNTIUJ?VG~!d~!ei)0Vf7P7eVEQ?jIY zs>!2yu41Mws%lwO81sv+C%B+4?Vvih+Ie(IKpkD`=7h*M5Tn8uo+^kZ9#odK^@^Fn9tev}UJK#f!f? ztlW6OnUDNlvda0U+@~Xb5@D~d_t8UAV7)$@BFYoYFwmPq4LKmvQ2^Bu zM*y61Iir$_T9Trf2+Nk|`v?H7?L{=gUzld#rdX0S0AQLUHqJck*_m>$XjNXi9|9p- zBuN_@78^tAxJ3V1-@Pj$i5Q!oUG1|v8PkmvADW>^p`^$=AVT5~cz}5gfbKKQVDy+F z8W_lrudxp%mzrRRfrsgHBIJPS@|;q!VaBSgT49wTk#`O303`X31Yz`fH>>GvSt|By zzWfCW*yQfJu2M}g9Sq{p_>#eYqpjOEwFMU`7;F(C1Wvg!TzZr;)u(XdYY1v!xrpMY zoUq$B@{TVQmv%dbNp8nLC$y1un^A+mSxvy)1>*rJvxw#P$ zDkhHGg_ZKJXq~`M3fR1Glo1XgrpZZt!jupz*UEyQMRHt~7r$jObW-8#fiGW{tx0FH zRV`cGl6i@!>_f}A(4wnF!xb80*eaVY_yGVIS{ZBABWtq_tk~goonxzSDX=pOZLPt+ z5YGa3dB$EIr2E*Xt-|l7f-9}Y4J*m%QUNf9S(^xq;e{_>ZP`{xSUa1QCkjM={U%GB z6WTg)Douf-`c|k!cwqZ`9rMo>{`Hq0jK3R?SDK7CS#thZ`JP381avw)&}zkeqx+S! zxA`pmDi*YaNvqrKjr2E*OlHH9?3ZR>ED{yE$tTV+VnN>*E}Hv{KL39Ge^k7H0b!C~ zg@zEU9mLuqG1ndUP3MmyE`v)D28wzGgtqkd->VhA4=f)d1{GL?hZ(YRa@h-0+*Pbk zJ0{npp{ScKtx<$4=*317MkQlLdy|~0lYmKfJs-@OW}{u#|0XdZN>^ld{JK|Tgzjfn zm?d7ZNGwQLEGu(m?OX)-Uaoei7@&-cNaz@kO;6SeITE%$!b6}~om1peA{9&!p~O0H zX65W9!*Ctnm;i?lUr!`~6S4nTi;kxc%Cyp%=Z;xGCuIC_7!E8b5hE8vW5Opt{N_yN z;h2<5-%%+yYCQK(I-18aVH}dQ4KwMPjYg=bW@<;XJoOt2_E~>Crb~MY!l|hvHRP^& z@}8+eJti(;Ro-IDlqzRnZ^XsK^#w?Z%Ly)QNm&i&p3*f*Z1Dus4<~6^EgB>55Z=6E zs~qK_Q^LAtIoZBrn7v|3uhQs?JQ?91G1;+svDs!o){+C`aqp?YT8y8*nhE29G{e-N z@_d69-SAt#ei;{bXhTenOPq3EZS`l8dD5VAde@S$Xy*k1xoBq%dS}g?E$ea8pw;-v zd9mcuX?I&}(^%`E>d&9*<%}BV`?vvoJ7yw(G`Qe!8`FtU^L7#(BVzz7XG(-O+EJrlKBycW}GCZw1y&KXX2`2+>|rR-(1cP? zwRoD>J|7NvP=cjP#*e>IO7*SXm6er8n&uVk0YmD27p=TRpn`T_v<+G!v{vVRj|f!v zy4_JeV11xaBU0NSCEaK5zVF~W>Pwvi=RB>xp3g2*xCe7F^j0;zhqx^e@^!2LIMDpc?9* zxBDq%T^twC5F$*Jz2?yNWTi{<(SoH$leWJfTyfa(ZnDOY?hZEYmT4zo;g;Y*z0`So z-zZlz(qZ&z^Wo1yjTx=x?S|dz(<-fr%vuUmY$Z&_VVR71XJWZ0qRQM=kdcZ=4{(xd^%(!#)50IeM#LE z2ygq(le&)&VG$7hn+ma3Ca0UPBv!ig>a{t9Y^&v7K7DxScdXjqF15wA8%BJsw zP^X2ldwv(5gI@XO$%D_22Uf3PYMp{7W5@hGPh-a&%NhNtM(;vy7k?H-BJrsyTkAf& zd0_ob^MP;I!3#o|r|pw$4$D@#x*Bam_a|H;SBWh)xqk5^OFR#cpn)wIwO^lXKJM&_ zY(nXCJ{~JWzj--c5AUvh`uhs~yUAMyz@z|0plz0&B3Znm(l_7fM5j}K4a@OgWxZ+3}Q7$`3U z2WWEwSFnP|*?IC|zT#pZhGUxJFJS20cR8s%?7?l!$u?|vZ1E!(KpC{9OQl* z4onTr|Db60296a3Kh>Wydu(`6tFHMrbG2>FzlDz1DQf{w+F$#{I*|3~)H^QBmO^jG zjfJqLUb5c%-(Kyv8*;mJzvpn~%2jKsus9#QB|`Hj*fjCBo0@FSq%+sB$ND)kvf2%1 zVEHw_E?DIt)1z7Gy5FXgr_$4BKr?)H?mXr2^x*KD&8Qfrm7`M|`@}Z$p*4JbwZFh~ zUfA=x7?IB`1fPobDKOXVV{NpNn7AI8TCJ|Z;eNI=V@*y#GAKnZ*VE!WSP>nW9NDe? zv>uzH_qsR9E|=W}1?b$`mce{)dTqmh{=wb>IIvn?{GCFr|3vTiK#bDkyy(gIg}@Z< zCQ|5wzU??SSP3iQ8zaqFkNZVkW3Jcj^&$IaEeiAt@}r*@hbyb!EXktWjoER2v>c5Z z=FpUGzh*8qO54xQSzX&1D&uS7yN|h@!){^+tW>vi>e!aWrY}Y!>rtG4F+QK_T7$|! zS?L6j8Sv7%o(WB@iD8}o z6d8FV_v~i#KApmgRfBba_(`ru=VR?_t&gL@b!?8G!PA)<29jRqXWS8`lSsG*{zwS& zm?11%?dlG@^@7PK-c)_zY1e5u=q`N9(o7oFCNZIJ0kp#dZGG>Q>s?N%Sa{h`FdEQ8Ho#qe+g zgtk|zTzYUAtBddn5Z4@vM{4`^0l>rBjaMyrKWEA|Yms|9+VQ(9@{Jhab)(lmi)69}rN4 z^Cv%y03KL3cQ_?RB~_);9#naeeuvEemhAwNc@ts5imJ;FT)6$DwJEEZ5niXu=zvgk z=>7Tde(PF{9(?0w*{0-{907Q_3PN6XlkkzI#T$kpEV3G9o-5Bv?% zF!=8Yc$X52+E>8Fo!!`DEFbT9z4rmf!9N}kE2n@$S~Dt-?bp!vfYY|xv+Y8Vad4~q zf&FZQ-n(;s!`1rYiYjIDXQd|!dVVul#cP3&a&j3wb~>aNF&&q!x7ee@`q1gz4@(Mu z3ZC!0t*_Yu4Br|v@Mbqk=7dxD@cn*W0he6>49;AC+|BfV@vLD+=ksg@NcTz=)MUigc6x z#_i#O&9U`9Xr-%L?|6Oa;J?@WI2<~ylcB#1RM7QR?0m8cO5uM{8g?qI!RFx}l?psW zetTwCZE)l(3V{;K3|){nq(}!Hv+oO|I!(qYf@Jhr{LfQ|X7mtVx1I6r{I^r3rBmSy zj~0)kV)*J2eBEsM&nq1R+a#Yq6VL!Tn?&GPVZ#L|UUl$9F*NMf=sR1%%r<67OiJWO zP+QMK-lrt@PhIfj$-3Ns;VI%tKL_3(SyK`el|NO*k#( z3$j7P80`bw>PTJctn-{q55Qv!U*IpJ{o?&`yKf+#{c+^|8onP}b%qRm-9FecZ9{D{ zdm}?%qs4c%{PA{>4i5@os?{5v(I1nUxv}T=>4u2*3H)A6IW$k($!;(Fe5v39pCS+Y z{_L-V8##o1{jx+$Hv;WnWZ$nyhW=}&Ip3J+^ic{+WoX*6WY z*R^ckk4Hm*PPcAMch;uIL4_x)I*>xUjct^{j){;2I6G;1fY}tg0%_Hd+K|oh%VmUk zzp%?xH=vV%pHiBcflrQajlk|zf^1OvW_3hrblVsWGtWc9)BEo2P#7>G1_T6E!gl8A z+m7_~biTOVB_RlGoKuJXj`utjbXn8FoH`d8Qk0A);$2+y;_O;6tze#Fk|idFXGO=du}4H-9Uu*+M5 z0Qea`x!8G(KS_n90&)s81Sa9l-E+dnDv*GyJ_uSwB!2d>GJ;8p4FBms{+;cu-R{7R;BFhAFkhjJ$zNWJZhs% zep~??92*2Q^A1~qxwltW{G^vW8neEyK+XT+;L2t9a|^0+Qd6=V76-quYl<$^>~+AZ zt!6^kit(rk4#yt_=)%%;%V<;sA|G`&*sj`llUB-9>@V6QkVLKO*QljL9lJ`6lxtv{ zEe_ugqcuMb^mlJg$-42ooeq7aL^SOZaJz+)+A3o@7WZs&hot*_HO*r2<1Y!{g53=F zhxj&L;69a~!P?DLLm4pReI(o{0IKJ{Qi{s1bZ|?m>^ieOhteB3N94#jU-J1Se)o#kqmxd&<52lG7*?n_*DzXyMe6TSovk-KMm&m7`s zO_?8{KS#J&!A*bl+>hOk^nNeI^L<*c?rhm=ao_hT2VUM3ik>K@%I3Vg7h3cu#(nV! zs@Z!FcGPGqEb-ne_(7gZ12g5Dh2!*+(ZO5X<>D}l=KQn^k(&h*0Z2ur*L@#1OQoJY z)OKX6P`|cbyxB{4?>0C$!XM)2NyWv*4pE83Bx+596!d8~; zd4p-LX5Ga1ZheQ`uOp-e4IRt9<@2(K9WXr)-dhyH;IdEc3xjH`@J+kJWtf9w#!vJy zSB^&SoiH_1HCxU+S3SWmpPz@p#G*f33y}%yWMa|pd|qm_GdME*H2~S7dc=GE|op_=T^hYoC=xc^B1)~0j@Vz{6Y0b%l)(gtE;GG;+6 z3v8=rclq{#73vp<0wNZ$fEfiz5C6n+L|mM;Vf&pZgG^2}v&k5AMzFVDhQZ{sQs0d7WJ3DP1EHWpc|x|WY_pbdN?c*Oygmq*SM)j1w+Z$WJ zKlH2sl<1P7sQ^*-Pzpk*+|SpC#1+5kW&WSL`-8)NEk>GkMIa=mYSKXI{Fw(m{rSv=0pV<6M^JeKc*2{M^o9pL8`V)J+0S$(3si$E;zB;8#bdROf98N zBG>TY_|@Ldj2;Kzp`SG){3COR&4Bwvx#~*Ivh0r*4_3LD%KUGgclnJUt|vTioG;(65nwqz%69^Ueqd*oOm z6xv0LV>^8T)Jj;6Z{N>;JeK*I^bKP@Y4L?okA`kO-M&_yTIvP`zKFR7l!V0e+^_rw zyIPp}Bi~>@V7lcQr=-&3u6+kakgG0%Dr5$|<1Tk+he>$m;A|3lG=kPBfpNXy`UQa$ zKyY$GRCZxgtW5uvIgJ(N8kjQ_Ha9N>xA?hNA32!f|aWnqwoZ)}TDZfIZ1iR-g z&mNkI0BVM)Y)AFG#QKYi)F<|ryvlllnF=KK~G5xe=-7PV|PYL z$8?3HJe67nlEkp^jUGI}f%EW!Ee!G>sCrmfuI}(i!q=_`ylY^#L|zIpSwjV3W&z zFbCjgzr?X7;?^-c^nda}i;-_*8KcIw2%ol?fcO+`J^FtphRsUf&D$|Q9`{UknRX&$ zry57oyO;3!fWOD(E}0N?aQ1cs1JwpBs{4+bw$wm`kIZ(9k8- z3&pX4<+;4|+Nh7WbJeALo{vdJFY;|R8>XRzUPxCjwU4nfX`_C36X{BUavz-?2Zt*< zd3LNLE2{Vh)#~pfbNMsx9J*3hlSDW1`Xp|voaL&CLERO@YOJdS&=TM2KBI>f7Pi6W z6JeRC`-(zT&6Hulxg@^0&zEi8e31aTsxtODat4y!)I*NIf)fV)mE?k%o;+15VZl4k z;Qctrez~{F6Os-?dh~un$E^F0cv%HNkpSP;DQ;89BB@zJ{>1QA0x&0w^^>A*Lfn2{ zg@KX!l61(NUP)!ET~7%5JA%+l=Q|2Hnq+i>{IS-SSA|Ox7Kwx~l?qc1Y>*R}{RAwR z>72A;9RgVIZv_kx38CHhF^2>{O8wLB^IC zJL!PL%-wOucLK8#7Lp&CP#pg^CFyHA_YHy%E$WKXreeV{?D0tbA|7N%edW10(vHN3 z8D&!Y8U0Hp;A=Qa?-3WFj)_}~4E^}?a>|&GapdknEC%8?T`MLkPCe*E0Dx|<7@b=x z+S1OBu8GmgUxaNS;-)#8cLUSqr%TL&nj6^y)j;r(_J#=Enb}Sf!oPTO%}e}Q&Erac zoas|DxBIK!hsgxJY}YXt>wL?PM&6#aRpuuhwOCJGKCT0+w~tkx`p(~ds~=f^Dxue# zWgk8pgxt5D71r=*UQY0m{8sZapD6Cf;(ycoxS_=CUw3XdwpFuO?%&>G3-RN=+t26y zcmndG`}v*i$J$~~IrUV3O_!hXyYaK(C$1M8<@adzspi@#!)6fG?nk!T$9BlAB-H ze~P3;v7+z#;aYVYH=K?Cpn!4Ele$B?bjPDo1M>&`GSB5@I^ZZIDxt&dZ7j@SO_y9c zX%?+49>YB=uijvGeJdYfWP#sLl7*&wsuKqVe%VE2VLm5vhqF>e%h^qNmN^A0b_wY$@F8wCtSqe=-a4(~{E zz?}gl2!Q|$5F}YB+-p`%Hja)Pm~&g-xFMmwM~sR@5gj6&PYI<^O0Q~cAxD1>CLjr{ z@wxk4ESg_0MLmd(z6YaJt|8257Gy-&TX3V8s6F&;0x58c>r6vU{*MP(ys)h_NHk3v zVB2~l%rbKbb8EtWbn+9|MUwujp$NJOMGADQ!vQZyW(`vP3nW1R+7xTa;;kE48c;D= zEJ?wLh3fr{C8INh!DJx;->a2~a!k>~)6qdvz?J464YlD$1zDDgMXJyG4ctY?3y}EX zjs;;vjPW!}W%56j1Zoz{Vd70Vz6o%$x%p!4LIc{SHs4a}g@xP;*PYv9M9}o-DkVM} ziU5)-1*6af`J`+d6ijc?(F*-Q0}09lqP`s4KmX3EPA!+R(TXDLibR5Gq(U9Uk)@$s zqTctVi0Km-$YLO+N;I)ICDeVD83 z{<)$niZPC=sE9{(z9tRQH$Sd7)D_c^jG-|nf zBMcY(X2vUM*)lcMsupbOmiTJ7A)vkEXE$YQs2kUE>1RQ$7K&09D!q+o2*hjQafbj} zX6thLX~`K~D-vusd>^xz22sht^aPQZR$ko?7~K}`UMfV>Y{wMx=5QL2an)jqrLJLZ(x3h>J8nn*}T>@k1#|fknu3`uyI;a zhupA1+9DBZmuJt zS)b4#C?8vjcs_G+Xg^dTp9B-gE36?jZPakRUqI^}Hu{3+`~pTG$gQ<1U%1$P8F=2y ztXrE(Lz}Ze57m`ZHflY0AL7XDF2c8SXRizlpaKuVKDV$qMR5T0|G&^%OqW^3YP*aN3Z(MTI`ui(Zj@K#G z*fXy^gfz&R1(%yRZ#l#!8U2O79=e73Prq7#w*jb?GWmcMEj7~D9^WrrtJSt#Su2d$ ziT*w$=)bYs9>AN__1YoHe#wqC$(JbyziZ^STEoW9v`)F?#)`&n6S97WgF3y57PN+< z>n*GOu4dX{dpv`NO9(wt{*GcT`v>N~JB2JUMZ6eiMBtu<1U4r#qHR1uswomED&QzyjB!6k;?!ABik`1NMSB+j4;y;|Fr>ihk za*3iuoH4S{ub+@kEf~-sZhN9-M)PiwcOw+&-{)Q1r<4aGImz6}Wd0I(B!5-Z5~a0> zyG`qbhT;gNicT}nI3T5Q6Q{yI`r&`Iv<|u?iShm<5IEt&2@<7BqsGscXr=q#nn~3X z3kW8HPwM9C<_?7aRTKY%?hJXMmMdpS6c;O6;Ld?Q_h964&-NSXC9m8~GQ$LX|7cqs z>3WMOCBL1KcX7Lxk0V&LO%KMzZa3^|6K>493zNLP+Zog{tB7=kl{}B34 zh?|h`AX?JVY)%|Nq9|Ufq-gO!RoGK=|ECK3@c(J+EW@%`_qRQyh@?n}z(bdGNOyO4 zN=S!v3P^W%cY}g}G}7JONK3c$JGj>S-|N_Gf8z@e!^}K$+;je}^SVDj9qZFT{_jvT z3_SbO)ZSY;C9%(lbbNogDfxFI_UV~I%Rc1v^tf^JYCa(^Kturt%b!CP|4%ki3uZhH zR6gOf%&$g+ZB|e0`j;nKrom_{W5@a?n3r|Z$*L`2j{A+3AIZ{GRjIOGzVz$+*?S_L zO)VWTISuN))&cLz{+2nD*V?tL(`<}~8UGru&U_^X05dQL1u!Z04Q&$jybWCjjA$ocNQzAKC~!VUNaT}qE8x32QK@^ z4F#~z4z87iCGBrptX3E2N3olQ6#A%cTP`mggr@eGHSXv1u952xW7IBLOP6Z2+=W(r z*hY?2zpW@p48Cmob1eZd6dr>lP702~$)Z;$G#H)npN<6Orqkf;Qb$#1@FFJv+@HMC zw9G5FMVRAaid>7f%YWUl`zOb|{pX1!BwUVR3ivZm_^04Jj`VbR9u$+J*|IGYRs7Fk zfk7RLSolBx8R1NvCQ?;Y#~PxrNDwE!SkY2GWbr*6eo5#i&C@m7s29$ zaSZYD=FX-!#T9eKwP_21Vfk^mB5oz6?$Ld!TE^pl;ZJw225xC+U<0?hrfQOZcM@^c zFs!mLx0+tDv~kBnlS`eM2d)Auj|8Sv2r)!7WnRCqw|M%=}UmedTW zDvG+}m!sIQs1*!zo8jaNtu!|Z(!w~_jX)Fnx!0<435r+$Ft1KftY1jeL6H+r+mNKN z`J$?dgTz@b*xl1kal^@_!$Kju!m7Y@P5YF0;lkX{mIyTy6anK_Q@S=9mBiq_t$X2F z67y+Q&dzZqfVq3b4D_*smWn61$*z22-2x3fD-?*1>vGpcTS8@2oJx}w!Rt@6bFk~+ zB8Xv-xYxg5gXJq*8U=;Gs3#^Q1%Y9Z8Ag}T*uU(5`Imh@lYyeU%TGbRIQphx3vSHo2CJ5Zh5~9cTi`|wjM=ezR&>}|(#8M|R%V8wq=gP4p z3srjozuxHDfOm2ZDBV3LV)RdGmnCt|Sa&7qbuibzc;4`2DmcRmo3l^{!3llccisk< zEbMuClbuE_#pdoDyUy$|7(8^47hU`kX5z4OeLZ54ka=rD3QtnS*R*c6bxsp}JSz5- zE91+$m7x%`(y!b~V;dp6wKZ%hq3A#_0x5$S=iWOW!{WX36J@JhS}j5i0~?!K%GYAy z#!BVP1=Vpy6b;PPOmQ%#}Fcd@>>;V7?VqvrNBv`Z(52 zCI62w(YxiPTxe?~hqzC}%TIT3VhDfW%_4S@V|T zvX*_YOBzaEq}|UNO9kITH0~ki)aB5d8GL-@Syrg*W$f>zWsCoLHfbf{XEbM@|5tqW4YUl!&zjr$5MFYUrT z8rrKxOl3q*#1}qDi3rI_gPRj{Wu;L*4`7+!C;#j;8P9uxAtj?!8M#Jb4bC-#P}#Lg zq@+qy-F)+0-E%c!WpwkGN+KJbl976q&$$OG5WpR$;JZqY1&xIoI&>(YCz z&-J%|R^x}Sm_G9xe8v!K;eGL37gkWV7u^=aBj9-xZ8H=~(etjL=NTB!n^T0*Fazq!bRO_&JMd*?{j%W4J4Aam%KEY3gtOqp2eR#!N*& zTM8n0Q!t4dda?@}2CFn~Jben&Wk6L{1VDxP<+QGexfYCwdlvUzdi`zGxW2AjcLa-L zgDDo_ubt7|g`xbtsSH!g&$~w45&JHb!vCkS^DSj9#npj^ZzoK11TUDqw&l^aSF0t? zVk}n+{g&IYYa*7;nd1R!%VMKv>TIXX1lRKw&< z)X>FX`~!F*fVpF(558$1r@5+}k{u5f6m8;D&Uw&(eA69zriph5ycL|>B%~^l;E^Tb zy2&BV0xB10^4{aKa&ar5%eNM}l`58#qxlw4r=b7<0%N7Ujh;7*J~y3^S*dKL;3p@K zRalAsm*^=dU-1BE8c8`t4o0`Kndn=#y^x?^o?KEhB|JzZB;tblLfMx2Fa#flml^(A z0j0qt0OEP$Pf_KqH4pv6cTWo|Jl{C7+8g8?XED?vDCvfAwWeKVVY<=l(6%`LU=LCp zbo&a4a^lJPCEkIZ7sTdppDCj_m5is(M5(z%UI8vXQ6y1Re=(|uG(*`@#@G)|7ic1M zxI;^n-6VK~pZO88&DxWOfwv&~VTK}Svxzsn6Bw2TA7B_r485!c{rz?mZs8%lA0!ZB zm}f6{tNBMxz~E+}Jbs7WUR_=O+ktiuKs3ea!|s z_Ehq?xxNjGeMbMJs!%OpgOK8UxKQVV!brrqdUjDwtY|uk=-<%i*K zjl3Kb6e`ND%abhF2|I8CjU60D86JAZ6rTnNODnMZ@DGy540O>QHvzE0M!>q8Zzs-y zamg>OD~lcep!cSkGkhO4IkF6V^Q-y|dWRk0!5!ccVA}$lzRfB;PD2?I;(XZg9>ES8 z9-7wtvxq_h_&L*|gP#_|%w|9nEBe-<)E{wK34xoQI7ZmCv%UX0tjA}$uzNb$5>`q7 z+L8q|$91(|(^n-*eJa#*bUUNyI@p2=#lg3TW+z<*XPHfM7pzc)1hn z!M>?W{Btq(@x>`an79i{+uNS^&kYYT3AHxj-Vb&w$?d==8Fj|6%xb-C zrT4qNUhd0+bZDLq+E`U#SoF?lN>L{6g^;l3qi)o{aS<)U0hEMxYpyl3)%+LT!m-qS zOqF;D%T$d~qKRC_S=m_2_252hRhh~wScNp>RfEZYajXMOVS%7qO}jVok<;<6FJ+`F z(1;Q-KRWOlanwtzf+Zn_cFya?v7PA%_+^zLDUwp>=HY!Gic7f1G_Mp_lB^~-Irl2S z?A+3|xmwbDbpgFr8@BlSH>-D6sin|wq~mmDcviH(lI?#gt^hTiv#dX@1UTON3T>|e zZ)r~7tF5j<|EE`!Brd;RMrkA(w{%2*9CUtN@YZD%jC|vXL9$#MfWT1&sOXqi=Fh_5 zH|uiNIpIX(=7*UO@6&q1V7tC+hZ0dRzy3W`9VJ;w`W=HHGE)}ZykdqDT^QisFks9W zwNQo?<^ZDJUYNg>;Y^?o`gu-Zzr{cvN4X&lCZ#)FNXcWma54LFiRu+|iG&l@exIlj zgFju%CvM{mL3yWv;(+Om@=`Y~;pta(=v|gBQUi5#AiB#O6KXK8MQ4zs`gQ*XDCM&# z1Q@t_OAY3cwT7O+cx#&+eHu{DA!_m|rvC>65-xI-?3`efO*0DkfixCEbFiDw1Bja~ z*HmWYOg8KO<=5ZLGN!z`Rwwn5u9g^cz)WvCeto0cEeaP%L_?P#qvXbuJRVmT(SYgNYrA%YEF6?40_vGblSr)Rwe`&2Mmo80IM$ad4_&8$ zH7bCe&h@Yx$)4peBwI^LTSLrTZUHh_eC!+;B2~PDX%D2#`#yd5t`bBC7D(EAVOG#U zyDv!B!rg-l+{3|#3jgRU5qo^@Lj09*AQhDNUn3EDYIx>qkYRvz3wQ@7V$vT{X*(T% z!k}+*;ofzW_<75BDF^3O z=Aq5zJdAw~mOw_R?Pq~Xas^68PwUR2)*h{8>$$l>$4eKm3+T1<8%hNB+Z(ZMQ4kBI zpdFmR;zW-sYGNKksV4DG!Z?`SC*0UA?}kT-T9ilYr+LQ8e$AxQ%%l05P)fpES(gcy zpP`5$=(j2SZhY_kGIib2M)btyi<7)zWph%~#J6oOkqr5&skCvnH|#ZLT9_h$vqiJ$ zesD3ONuk9kV+=p;85Z*om*xkwz)8*$oD(P~%L8HFc3UQld^I->S>v)L>wE+a)!n@8 zDmz-pu?viXLUA@rUa!bc`zmCtcYkOh5G7Ga788vhoCg3egEt_Fw3&7C5*1_ zSIOt{)EyLol?Zw|?5N3u+OJnZ_Z3v@P>4i%Dh~;D=ggK4D&WwFAr4KGj8%rH+0#3r zKK#t#7{p}8%JwZ-Hvte;GZym%oJMH}JNh-NQh#+%W8+&Nx8$~-di6No9V~&TmVlC* z8=XKJCRoPI#>jUzy&4qrbK?QHnG9@Di-gW-aiYa6WM)5uT@I z!OAk08YR@}uHZ$}JYn_1TW9>D_~$3KZS}bwn3B@(^jti?8Ab{~=TX_vR9X$YZDutN ztr?1VNBx__Dfod5ZCrYOue@>TqAjpl%p+Q{+{I#xkx^xB;?guM+Rw_wFOOQw{<$yv z(pic9B9V9$0{B-2tm`hD)FRO!r{DPqY3BE(po?TKFjeM6dOconZE{hOzIq7{FR@4dYsb^q4f9&6UQ&tS#r327Z==GA6(=Te?-r&5ee;@ zRNKx4)plw4CoE&KzOYUt&arJzaAt57dTs5p{4&%(>>8R#*X)bB6}-k560{#byZ%6h z4|1I8eW{6wvfvF2x^Mo1ems56yV9o{lze&#(umIY@00)N^4gTh%x}Rbf<8#OwiAhJf@mdVP_~>ra$`1F0+ks*SN#ghdBHDOpBc^e|MLw6xPjpzr0|Tj{(<6CGPlSpc z^*^i#!wBGqHuFXT7v;>g>U!P;RTK!blHc^L;r-D*{-#XK)}WKJ zWo*SjkkT<2z4JGL`nv@HogP{d=mmIFFqlnBI;h(I+FVr5@VF5A7AwrN zGp2B;_0Yv(^Sjuql8mF9M5{+>k8ot0Xs3b4-xsastv1iLK0S+n2WHq4mp@(hVY;sL z9@Lc%aw(kL5-cuWl!=5MYi%|oP}A)_+>bd`pYGMPrkQ(e%iSJuULPc8Ro`!CqFAv- zeV1vuGEbHV>(m}@bC-;}cIbq)m+yP7oUG)RnU8E-9zCC4uQbu4B=Z5YVh7b!easS? z%2YPGa7Yz|*NqMiIIBU8*P1EQsL3K^oLxEVsf$F>&iuXP;qMgdxI|Kl^(t#E+s9?+ zUJeDn#n$*lsCGt8&#~$DvTq;%%yHpUgX?agO^aR1P|M`?;lh2^lU`zYsnv|eXMd6V!qK6OVPPGL?SF3lL$qB7L}0wwLHD64xo2~< zib8GAyVY*p*L1APM9)L{R^@S_)?Q7u;z7f)ufT_-^`Mn^`o5yV^?q#Agzuhjfe#rq z5353%$3Q(>2k+LUmZ+d2?cpr7(! zzh2sPN|rV2n~a};>Umq(t+BY#me}LY%Vq82G2y8sNtTR54R+DGzNhg(&XWHss>cwA z1CR4?LxidqagEy5(#3&E@9A(ej!*A6_OnM4k#3(4B4;oKmK@r^(v50hw}D$+K2>BK z=3)PFstA+K>~mbD)GR%qBU*3sziLJ^XNDfl^j|Bv^YS>#`h^xQvvD_?t=}6R+aGq% zYXDxRwkC~|VKB>co*wh&hGp8!0p{LL2 zJ)1_3aGgIA)h}yS?FN&*c)t@G3Ek=#K(7ts&24wkkf{L~r;kO}VL!6JLA^Tbm;>Xq zxsm0Ajdqud#Xe1a?!*t*W7~Sa*GjsW(4O%*;oYxzedoSgidiau)AWL(0~;JXr5xsD zhw6Ru!Q#tFha9%@>NN_6()rWTX(ZncX3{KNhfX&g?=Z{J>FKhd>QnBx%zTW~`@Qo^ zW~5@goUUIEDdYM$)|V$!PqMuU(?a)d>Qt%Hm6tm)CF^Tit&QpaT**}AP9o*#sOwFo z_4e*4a{X{@rTf^o_r_4OUei{Azrm2zL9maL3YauWiHBow+5LVN8OYn<;TcB0)^5Xh z_eeXM+AWFxZO`0t0Q1XTV(Tq>kY0oNV;hf}xle4XEBIBqyPgk^` zq2Q}eN-6X_Pi)m>bogMprf87O-E||^vSc42)ee0ilv<)%siC6`L~-A$^;AX#(nbDi z)~q!!UR(VdD%9~i{ZtEaT-mABcx{o_h$erMa$HHLIg!n>xGn`*(_cCW&XZ2e1Tqv% zsJAdXA)9Xc)b$Bgz|mQF4NxEcX{02~&u&`Q{jbC_I{*+)7Np;1vYBIoNoF@LuBH2l z@a|}`@FlL9w?GO?x+!#!duj<6_}>v?yP}ykK4`&B;o% zZurgFMl3Nf`I9hubX=EpCCwN$$oTPW4|Z+g)Y0WXYN%ad9lQOEa~1q%7d|&_%DSh* z1hVO7JY%X$R!he)NMqc;IB;wdx%iVDyl5~I_4PE@(V z4h-Cd9dV_4cV~Z1_+QqUaQp~YmgO8w;hJa?$yT-C&bJ~69)3>Fj{6sKIL)H{FD*Rt zgU3q|fK}gTnz(qMGFa*k7ev|XrY}Xh=Eyw;C;m;1N0wtr`v0!Hxwqjo@y;LL?b$pH zW*y;SdCXNR6b)~I2EsHlIBnJx;|@u$R_Z59 za=6SZoarq=3LK_m!gFV&!J|+zrk+7jy}x-cCjF<;^hS3dfX;h4&evYWe-${~jT{}#!`K-;O=~@!Gy6$j z>aw9p<)!mO?N;l3&wx7}60J_nwc1`Uvk#jEN>lyOX@3dCX3`{{Ij3)^;*jrAZx@zI z7u9S0fE~hjJF{{Z`S|a{%Gz!*CpfKu~$nIT_eET`bC^7W4Gdd zM9=!DRkD1cC=>NM)PcTCD?tPc`2>q0@rk)-O{xl->}EC>So zTu^M{Jo=~AixW+TLhA2#PJ;LBIJ~rE27+Dgg!c+AP%GVkQ*bqvC0&SYzJhh*^!ZWQS8WIt`v+bGoPtnQG*M6v^SN{M^#>A zVLz5eD2qV~_V8NTXwwr|6uTAVWG7D9WGS0U|B?(x>|Woo^*0KbpI*`_*JAO=npr~( zYCvq9DiUTk0*K;&6rpfhynQ$IZLHU{w9a(26X`5PvQY)%d{FeePbr^%vbNqg)TN zBH3DR*#U3%*`xEwkE_=V5P?(8VW?rf>(!Jl-oq>(<%;8S#Wmn{y%+-K zt|OIDsh^EFTzP!D#7#TkxNf7KAbEB22sR&w{5*2bKuMSS(G`27B$bgqijnPp`U&Cg zx-M|!qXe+pp4jT!kb0B<)`X7-JS{)6{R15NN*SiSw}*sas?%38UeTxH;Z#+O(!t%A zD9^fb)YB9aZ*I)N0~!Kj+}@MNYA{b~-1%5>?p*4_ao^a~m6j?t=H1%qs0^me?I{c>~x2xcDfRL(MU?|h{&=xL-SQy_*Ql@Uoa=bxk ztr$6fUVj`|8rwY=69MWBD zuo+J8nn5=RiW{kDAsn1sQc#-?evoNz%q?e&0=#zwo{b+_uqa8|IFxji+lBWP`K&I# z+h!z(3ac)cs;=z&A|M72J!j-{#mNKqW$)P~hlOi~Bf_~h_wiR{yaU78P{Qdtk~($q zNu-j3BK_=oK_6yezd)g4^l~0jeLf$Xgy%tbVqm5-L>OasJA()T<%e9=5(LKEII{gcpg`RV|u7)*j36Sf;PY??&p?iNIez zllU~1J9LzCis5TAjDn1bD3semk4oXU7bS)zvpkd1^v>mOX8qldluj*Oj{9vkin()2 ztsMo@gRvj|P}0%PV*V*!LAAy>e3 zvl@P2VgRC^;CS9)vf(;WMlOSLs#Z-FDzx8roI)IjN>W|53HO4q3mLIv?w58Grymwg zRBm?Hnf|ezwLlGfCko0Q7WUv{3E^XRl9u8syx+!ma)GMPg5T{DAUB6MF?&KY1&X+N z74RL958*p&d`{b`dLD|mZDT&8WhQ7yI_%hzaom>^pZ$Z+TB_S)&`UKKG-?A;Gd~@5 z;bo?#vCe&UMa{eKZd=DEbyu0|qPx_|XffB6kXE@*jp*0n z3^6M2h^Ig+5B_ugHMkRWP=vRJ$jgaEdXmuRt2?IFqw*WGXakW4YQF>Ls6Hd|j*Bp!}UQ?u~rqMHD3|njx5mpIDU-<99^>kS9pTspL z)ySv8V{N|?9#LsqVoC`=Z}}1j{cB^oL5#9q`1Yt%KLX@nM zg84R{N5#_$&OPFa@C?;Z8Pc?13$}!y-qd<2u9d1UsgM4ov{5v&o6u;J{Yne6L(uav zsLV>QePQFgB&xO88s4CyS^eU&Dx50jPOu7?8X9WRz51`oOH$g!Q#UMuDs2MWx;4V9D3UnR^)_(NeBPc&|L$S6x zri{!@<;98pa~MzwS(8@?a0L-X)_N&+nO7e^$B`l|%9S8wSd zLj$d3&zxupIH@eZ(S1nwIU^3kS)I(AlO?}a;>l3raeeXa?K@3~SM5#U+HmQc5-;Hl z6bCMYZF-c-s{8Y6#zL*3EXH1nbIiTzR~cOA{g4jq>pL;FyVLrJE))uD>M~T{@ua_?j=Am z&`KCWEXP%1jw;(|R)ce~U-UtyAg0Exe9XMK3f|q^;)-FNz;-|`aj)!OmEJsVU*72c z36+z8EwC>ozyjD}9F$DU>!Zh$v&Tya-=odr`-KNJr{i%Vt&zu@j?MS{Qr|)oUu_Mw z+MG0$#F(g^wm7%q*)L0<+U=w>?~hNhKHki79^_nKgI5xjxA`FWLh5MJr>OY%oX^|P zBfWZdRG=dB{%*4Eb5DlWS5gQt`ttrKS?=P%MfJ8I%oEa+-Rii*SUWY5G%dJ@NXrg|Gj!>Q1q3d3Q!xHO zDAt=yWzt=HJ#E>q=i8QJb|f{X1RJ#31ml?9d6*~N=H7GQzH+(aM2A@~&JZf{!5@wTD z{i`(F`{L_Yiqgo)C}c05S5exeaSz|DXL*?pA;MmFmV}qa!EDRPI0h*^+{Ig{41nKa zT(1XcFS2Aw@*bR}J!}K)PU8Fi&--6~++ofavYAzH>x|6EKG+PhSp?LJhl!b|bIt^Q z77rJN(+*n~knVKhhmCfpAW=?^qX zT&jeLyv&*`6>BG(BiNvs=?Xe~x;RM7UbQaTr;nM&j_Byea4o(g{hic3x~xpN-bR?R8`+Qa zW0yOY4)RWg13m&?ECZ1krdIsK?PaZcu~(I0*d>`XJurx%ZxuE;CA8wAR!_vTp_6c`TqgRPpbt0 diff --git a/docs/live-site.md b/docs/live-site.md index 26018b78..9ef2ef89 100644 --- a/docs/live-site.md +++ b/docs/live-site.md @@ -1,22 +1,22 @@ # Watch a Run Live -A full run takes a day or more. The live site shows it while it goes, in a browser on any device that can reach the machine: which step is running, which finished, which failed or was skipped, and one line on what each finished step found. The page repaints as messages arrive; nobody has to reload it. +A full run takes a day or more. The live site shows each person's results as their steps finish, in a browser on any device that can reach the machine: the report's chapters as tabs, one card per section with its numbers, its rows and its caveat, and what is worth a look first. A Progress tab keeps the step map. A home page lists everyone by family, and each family has a page of its own. The pages repaint as messages arrive; nobody has to reload them. `bin/owngenome_live.py` is the server. It uses Python's standard library only, so the `python3` on the host is enough, and it has its own password, access log and limits, so it needs no web server in front of it. ## Try the demo -https://geiserx.github.io/OwnGenome/demo/ is the live site on DEMO-001, the invented sample of the docs pictures. It replays a made-up run from the first task to the database re-check after it: press Play, or drag the slider. Every number, result and family member in it is made up. +https://geiserx.github.io/OwnGenome/demo/ is the live site on DEMO-001, the invented sample of the docs pictures. It replays a made-up run from the first task to the database re-check after it: press Play, or drag the slider, and watch the cards fill in as their steps end. Every number, result and family member in it is made up. -![The live page of DEMO-001 at the end of the demo run: the run marked finished above a Play button and a time slider, the step map with every step done and a dot on the steps a newer ClinVar would change, and the Found so far list with one line per finished section, starting with the read and variant calling steps and the ClinVar screen's hits with its caveat](images/demo-live-site.png){ loading=lazy } +![The person page of DEMO-001 at the end of the demo run: the name with a progress strip of 54 finished steps, a tab column (Look first, Trust, Doctor, Child, Medicines, Ancestors, Traits, Other, Leaves out, Progress) with Doctor open, and the ClinVar card marked worth a look and newer ClinVar available, with its counts and the chromosome chart of its hits](images/demo-live-site.png){ loading=lazy } -The page and its plugins are the files of `assets/live/`, unchanged. Each moment of the replay is the server's own `state.json` at that moment: `tests/demo/build_live_demo.py` loads every plugin, stores the made-up messages of `tests/fixtures/live/demo-001.weblog.jsonl` one by one as the ingest port would, and reads the state after each. The charts, the search box, the run-health card, the database re-check and the family page work as they do on your own site. The genome viewer does not: it opens your own reads on your own computer, so its links lead to a page that says it is not in the demo. +The pages and their plugins are the files of `assets/live/`, unchanged. Each moment of the replay is the server's own `state.json` at that moment: `tests/demo/build_live_demo.py` loads every plugin, stores the made-up messages of `tests/fixtures/live/demo-001.weblog.jsonl` one by one as the ingest port would, and reads the state after each. The cards come from the `results.json` the server would give at the end of the run; each shows once the replay reaches the end of its step. The home page, the charts, the search box, the run-health card, the database re-check and the family page work as they do on your own site. The genome viewer does not: it opens your own reads on your own computer, so its links lead to a page that says it is not in the demo. What differs from your own site: - It is a static folder published with these docs, so it has no password and no access log. Nothing in it is private. - The run-health card measures an invented machine (64 GiB of RAM, a 2 TiB disk 55% used), not the one that built the page. -- The CPIC recommendations and the gene callability table have no file in the demo sample, so the demo shows those two sections as missing. +- The CPIC recommendations have no file in the demo sample, so the demo shows that card as not run. ## Start it @@ -43,14 +43,29 @@ weblog { } ``` -## What the page shows +## What the pages show + +**Home** (`/`). Everyone, grouped by family, then the runs in no family. A card shows the pedigree symbol with a ring for the run's progress, the person's place in the family, the steps done and how many cards the report flags. Runs still going come first in each family. A family member with no run on this server is listed, dashed. Without scripts the page is a plain list of runs. + +**A person** (`/runs//`). The name, the declared and measured sex, the families they are in, and a strip with one cell per planned step. Below, one tab per chapter of the report, each named by its question: + +- **Look first** opens by default once a step has finished: the cards the report itself flags, in chapter order, each with its headline. A card not listed is for information; its caveats still apply. +- **Trust, Doctor, Child, Medicines, Ancestors, Traits, Other** follow the report's seven chapters. One card per section, with the section's numbers, its rows (the first ones, then a button for all), its own sentences and its charts. Under each card sits its caveat: the first sentence, then the rest behind "Full caveat". A card is marked "worth a look" (▲) only where the report or the step headline raises a flag, and "for information" (○) otherwise; no card says a section is fine. A card whose step has not ended says so; one whose step failed or is older than its input says that. A newer database release shows on the cards it would change. APOE stays behind its warning in the traits card; the polygenic scores show only once they are placed against the ancestry panel. +- **Leaves out** lists what the run does not assess. +- **Progress** is the step map, a step's detail, Found so far, the run-health card and the database re-check, as before. + +Each card links to its section of the report and to the files it names. The header search finds a gene or rsID across the results and opens the card that names it. `#q/` opens a tab, `#s/

    ` opens a card (the report's own anchors), `#progress` the step map. - **The step map.** One box per step the run planned, drawn from `assets/steps.tsv`: a step sits right of the steps whose output it reads. The colour is the step's state from `bin/run_state.py` ([Where a Run Stands](run-state.md)): waiting, running, done, failed, skipped, not finished. Skipped steps stay on the map, greyed. - **A step's detail.** Click a box: its state and reason, start and end, the usual hours, each process with its tasks waiting, running, done, from the cache or failed, and the headline of its sections with a link into the report. -- **Found so far.** One line per finished section, worded by the same code that writes the report, caveat included. A line marked for attention means the report has something to read there; no line ever says a section is fine. The ClinVar line, with no hit, is information, never an all-clear. +- **Found so far.** One line per finished section, worded by the same code that writes the report, caveat included. No line ever says a section is fine. The ClinVar line, with no hit, is information, never an all-clear. - **The report.** Once written, `report.html` opens the run's `_report.html`, and the files it links to beside it. - **The genome viewer.** `/runs//viewer/` shows the run's reads and variants in igv.js, as [`scripts/owngenome-view.sh`](viewer.md) does on one computer. The report's Viewer links and the charts' "See the reads" links open it at their position. +**A family** (`/runs//`). The pedigree, drawn from `members.tsv` or `pedigree.tsv`, where each sequenced person with a run here opens their page and shows how many cards their report flags. Then the members, the analyses step 38 planned, each couple's carrier screen in full (genes where both carry a finding, X-linked genes, genes where one partner does), with the screen's caveat and a link to the couple report, who is related how (step 38's relationship check beside the shared segments), and each child's chromosome map. + +On a phone the tab column becomes a row that scrolls sideways and each table row becomes a short list of label and value. The pages follow the browser's dark or light setting. + ## The genome viewer `bin/owngenome_live_viewer.py` serves it, behind the same password as every page. `owngenome-live.sh` passes it the reference (`REF_FASTA`, as in every step) and igv.js, which the first start downloads into `GENOME_DIR/tools/` and checks against `IGV_JS_SHA256` in `versions.env`. Without the reference's `.fai` or igv.js the site starts without the viewer and says why; `OWNGENOME_LIVE_VIEWER=off` leaves it out on purpose. Running `owngenome_live.py` yourself (in Docker, say), pass `--viewer-fasta` and `--viewer-igv-js`. @@ -59,11 +74,13 @@ Per run it serves the folder's BAM or CRAM, its VCF and its SV VCF, each with it The reads cross the network to the browser that logged in: only the byte ranges igv.js asks for, but they are the sample's reads. Off loopback, use TLS (`--tls-cert` and `--tls-key`, or a proxy that ends TLS); without it the password and the reads cross the network in clear. Opening the viewer writes one access-log row; the byte ranges igv.js reads write none. -`GET /` lists the runs: each folder under `GENOME_DIR` holding `logs/run_status.tsv`, `nextflow/` or `run_manifest.tsv`, and each run the site has messages for. +`GET /` lists the runs: each folder under `GENOME_DIR` holding `logs/run_status.tsv`, `nextflow/` or `run_manifest.tsv`, and each run the site has messages for. The home page reads `/runs.json`: each run's progress and attention count, and each family with its members. + +The person page reads `/runs//results.json` from `bin/owngenome_live_results.py`: `bin/live_results.py` turns the run's summary (its `summary.json` when step 24 wrote it after every finished step, else the step files read again when a step ends) into the report's chapters, one card per section. It computes nothing about genetics and holds only what the report prints. The family page reads `/runs//family.json` from the family plugin. ## Privacy and what is stored -- The site serves only its own pages, the run's state, the report, the files the report links to by a relative link, and the genome viewer's files of the run (see above). Any other file is 404, and so is a link that leaves the run folder or a symlink out of `GENOME_DIR`. It never lists a folder: a link to a folder that holds one HTML file (the report's `cpsr/`, with CPSR's own report) opens that file, and any other folder link is 404. +- The site serves only its own pages, the run's state, the results the report prints (`results.json`, never `summary.json` itself), the report, the files the report links to by a relative link, and the genome viewer's files of the run (see above). Any other file is 404, and so is a link that leaves the run folder or a symlink out of `GENOME_DIR`. It never lists a folder: a link to a folder that holds one HTML file (the report's `cpsr/`, with CPSR's own report) opens that file, and any other folder link is 404. - Each message is cut to what a progress view needs before it is stored: the task, its process and status, times, CPU and memory figures. Nextflow's parameters, command line and task scripts are dropped. Three paths stay on the server for later views (the output folder, the CPU and memory caps, the work directory); none of them reaches a page. - Messages go to `/runs//events.jsonl` (state dir: `GENOME_DIR/.owngenome-live`, or `OWNGENOME_LIVE_STATE_DIR`). The site writes nowhere else, so `GENOME_DIR` can be mounted read-only. - The access log, `/access.log`, has one tab-separated row per page or file read and per login (the genome viewer's files excepted: one row per page open, see above): time, client, the first 8 characters of a hash of the session, action, run, file, status. It never holds a query string, a request body, a result or the password. It moves to `access.log.1` past `--access-log-max-mb` (10). @@ -148,11 +165,11 @@ WantedBy=multi-user.target ## Plugins -The core files (`bin/owngenome_live.py`, `assets/live/index.html`, `live.js`, `live.css`, `login.html`) are meant to stay small. A new view adds two files instead of editing them: +The core files (`bin/owngenome_live.py`, `assets/live/index.html`, `home.html`, `family.html`, `core.js`, `live.js`, `person.js`, `home.js`, `family-page.js`, `live.css`, `login.html`) are meant to stay small. A new view adds two files instead of editing them: - `bin/owngenome_live_.py` with `register(app)`. At start the server imports every such file, sorted, and calls it. `app.route(method, pattern, fn, auth=True)` adds a page or an endpoint (behind the login unless `auth=False`), `app.run_file(glob)` lets the browser fetch more files of a run folder, `app.report_filter(fn)` rewrites the served report, `app.state_hook(name, fn)` adds `plugins[name]` to `state.json`, `app.run_kind(fn)` lists more runs, `app.add_argument(...)` adds a flag, `app.log(...)` writes an access-log row, and `app.config` holds the parsed flags. -- `assets/live/plugins/.js`. The run page loads every script in that folder after `live.js`; it calls `window.PGP.register({name, onState, onEvent, panel})`. `onState(state)` gets each new `state.json`, `onEvent(message)` each message from the stream, and `panel(element)` gets a card of its own in the side column. +- `assets/live/plugins/.js`. The run page loads every script in that folder after `live.js`; it calls `window.PGP.register({name, onState, onEvent, panel})`. `onState(state)` gets each new `state.json`, `onEvent(message)` each message from the stream, and `panel(element)` gets a card of its own in the Progress tab, or in the element whose id `slot` names (the search box sits in the header this way). A family's page loads the same scripts without `live.js`, so a plugin returns early when `window.PGP` is missing. The page has a replay mode for a demo with no server: `` reads `frames.json` (`{"interval_ms": 1000, "frames": [{"t": ..., "state": }]}`) and plays it with a play button and a slider, through the same drawing code. -`tests/test_owngenome_live.py` checks the server, the login, the stored messages, the plugin interface and the page in a headless Chrome; `tests/test_live_post.sh` and `tests/fake-docker/run-all-live.sh` check what `run-all.sh` posts. +`tests/test_owngenome_live.py` checks the server, the login, the stored messages, the plugin interface and the page in a headless Chrome; `tests/test_live_results.py` checks `results.json` (every card's caveat word for word, no wording that reads as an all-clear, `results.json` behind the login); `tests/test_live_demo.py` builds the demo and checks every page, phone width included; `tests/test_live_post.sh` and `tests/fake-docker/run-all-live.sh` check what `run-all.sh` posts. diff --git a/scripts/ci/e2e-case-paths.tsv b/scripts/ci/e2e-case-paths.tsv index 1254dfcb..f081459f 100644 --- a/scripts/ci/e2e-case-paths.tsv +++ b/scripts/ci/e2e-case-paths.tsv @@ -99,7 +99,7 @@ hba-short-read-accuracy-1-nf-report assets/acmg_sf.tsv assets/callability_genes. hla-second-caller-1-bash assets/pgx_genes.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat bin/cn_loci.tsv bin/copy_number.py bin/cyp2d6_depth_check.py bin/hba_copy_number.py bin/lpa_kiv2.py bin/pgx_outside_calls.py bin/render_report.py scripts/08-hla-typing.sh scripts/24-html-report.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh hla-second-caller-2-nextflow assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/mito_confirmed.tsv assets/pgx_genes.tsv assets/stub/db/hla.dat assets/stub/db/kir.dat assets/stub/sample.vcf.gz bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/cyp2d6_depth_check.py bin/hba_copy_number.py bin/lpa_kiv2.py bin/pgx_mito_merge.py bin/pgx_outside_calls.py bin/pgx_refblock_fill.py bin/render_report.py bin/results_index.py modules/local/hla_typing/* modules/local/html_report/* modules/local/pgx_consensus/* modules/local/pharmcat/* modules/local/t1k_build/* modules/local/vcf_precheck/* scripts/07-pharmacogenomics.sh scripts/08-hla-typing.sh scripts/21-cyrius.sh scripts/24-html-report.sh scripts/32-pypgx.sh scripts/36-pgx-consensus.sh scripts/ci/fake-docker/lib.sh scripts/cyrius-constraints.txt scripts/setup.sh scripts/validate-setup.sh ibd-segments-1 assets/families.example.tsv assets/par_grch38.bed assets/pedigree.example.tsv assets/stub/pedigree.tsv bin/family_report.py bin/family_segments.py bin/ibd_report.py scripts/38-family.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh -live-server-1-weblog assets/live/index.html assets/live/live.css assets/live/login.html assets/viewer/index.html bin/owngenome_live.py bin/render_report.py bin/run_state.py modules/local/roh/* modules/local/vcf_precheck/* scripts/11-roh-analysis.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh +live-server-1-weblog assets/live/home.html assets/live/index.html assets/live/live.css assets/live/login.html assets/viewer/index.html bin/live_results.py bin/owngenome_live.py bin/owngenome_live_results.py bin/render_report.py bin/run_state.py modules/local/roh/* modules/local/vcf_precheck/* scripts/11-roh-analysis.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh lpa-1 assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/mito_confirmed.tsv assets/par_grch38.bed assets/stub/sample.vcf.gz bin/cn_loci.tsv bin/collect_summary.py bin/copy_number.py bin/hba_copy_number.py bin/lpa_kiv2.py bin/render_report.py bin/results_index.py modules/local/deepvariant/* modules/local/html_report/* modules/local/lpa/* modules/local/vcf_precheck/* scripts/24-html-report.sh scripts/46-lpa.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh mei-1 assets/mei_sites.tsv assets/par_grch38.bed assets/sv_rule_regions.tsv bin/annotsv_postfilter.py bin/cnvpytor_to_vcf.py bin/collect_summary.py bin/mei_report.py bin/sv_svlen_fix.py modules/local/align_minimap2/* modules/local/annotsv/* modules/local/deepvariant/* modules/local/fastp/* modules/local/mei/* modules/local/vcf_precheck/* scripts/01b-fastp-qc.sh scripts/05-annotsv.sh scripts/15-duphold.sh scripts/18-cnvpytor.sh scripts/19-delly.sh scripts/50-mei.sh scripts/ci/check-images.sh scripts/ci/fake-docker/lib.sh scripts/setup.sh scripts/validate-setup.sh tests/fixtures/annotsv/class45.sh mito-extended-1-deletions assets/acmg_sf.tsv assets/callability_genes.tsv assets/carrier_genes.tsv assets/construct_variants.tsv assets/deep_intronic_sites.tsv assets/mito_confirmed.tsv assets/stub/db/clinvar.vcf.gz assets/stub/db/clinvar.vcf.gz.tbi assets/stub/sample.bam assets/stub/sample.bam.bai assets/stub/sample.vcf.gz bin/cdna_scan.py bin/clinvar_hits.awk bin/clinvar_join_adjacent.awk bin/clip_check.py bin/collect_summary.py bin/render_report.py bin/results_index.py modules/local/clinvar_screen/* modules/local/html_report/* modules/local/mito_variants/* modules/local/vcf_precheck/* scripts/06-clinvar-screen.sh scripts/16b-mosdepth.sh scripts/20-mtoolbox.sh scripts/24-html-report.sh scripts/ci/build-mito-confirmed.sh scripts/ci/fake-docker/lib.sh scripts/validate-setup.sh diff --git a/tests/demo/render_pictures.py b/tests/demo/render_pictures.py index 042b7139..795eccc1 100755 --- a/tests/demo/render_pictures.py +++ b/tests/demo/render_pictures.py @@ -22,7 +22,8 @@ as the docs workflow publishes it: built by tests/demo/build_live_demo.py on a DEMO-001 it writes itself (--genome-dir is not read), at the - last moment of its replay, in a 1280 x 1000 window + last moment of its replay, on the person page's + doctor tab (#q/clinical), in a 1280 x 1000 window Every PNG is written again from its pixels alone, so it holds no text, time or EXIF chunk. Needs playwright (with its Chromium) and pillow; @@ -122,12 +123,13 @@ def live_picture(out): # all 'self', so the picture is the same. page = browser.new_page(viewport={"width": 1280, "height": 1000}, device_scale_factor=1, color_scheme="dark", bypass_csp=True) - page.goto(f"http://127.0.0.1:{srv.server_address[1]}/runs/{SAMPLE}/") + page.goto(f"http://127.0.0.1:{srv.server_address[1]}/runs/{SAMPLE}/#q/clinical") page.wait_for_function("document.documentElement.dataset.pgpStatus === 'ready'") # The last moment: the run ended, the charts are drawn, the re-check is in. page.evaluate("() => { const s = document.getElementById('replay-slider'); s.value = s.max; " "s.dispatchEvent(new Event('input')); }") - page.wait_for_function("Number(document.documentElement.dataset.pgpCharts) > 0") + page.wait_for_function("Number(document.documentElement.dataset.pgpCharts) > 0 " + "&& Number(document.documentElement.dataset.pgpResults) > 0") page.wait_for_timeout(1000) path = os.path.join(out, "demo-live-site.png") page.screenshot(path=path) From 51f478f071ea837b92f8328d3ca016873a9e5cae Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sat, 7 Nov 2026 21:05:49 +0100 Subject: [PATCH 11/14] fix(live): give the step map the full width and keep the Progress tab's head in view Bead: pgp-5bja --- assets/live/live.css | 11 +++++++---- assets/live/person.js | 3 ++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/assets/live/live.css b/assets/live/live.css index 5ccd2121..c8214771 100644 --- a/assets/live/live.css +++ b/assets/live/live.css @@ -172,9 +172,12 @@ figure.chart svg { width: 100%; height: auto; max-width: 100%; } .look-empty { background: var(--card); border: 1px solid var(--rule); border-radius: var(--r-card); padding: var(--s4); } /* --- the Progress tab: the step map (live.js) --- */ -.progress-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: var(--s4); padding-top: var(--s3); align-items: start; } +.progress-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s4); padding-top: var(--s3); } +.map-wrap { max-height: 70vh; } .map-wrap { overflow: auto; min-width: 0; background: var(--card); border: 1px solid var(--rule); border-radius: var(--r-card); padding: var(--s3); } -.side { display: flex; flex-direction: column; gap: var(--s3); } +.side { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--s3); align-items: start; } +.side > * { min-width: 0; } +#plugin-panels { display: contents; } .legend { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 8px; color: var(--dim); font-size: 12px; } .legend .swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px; margin-right: 4px; vertical-align: -2px; border: 1px solid var(--rule); } .edges line { stroke: var(--rule); stroke-width: 1.2; } @@ -185,8 +188,8 @@ figure.chart svg { width: 100%; height: auto; max-width: 100%; } .box:focus { outline: none; } .box:focus-visible rect { stroke: var(--focus); stroke-width: 3; stroke-dasharray: 4 2; } .s-waiting rect, .s-waiting .swatch { fill: var(--card2); background: var(--card2); } -.s-running rect, .s-running .swatch { fill: #2a4466; background: #2a4466; } -.s-done rect, .s-done .swatch { fill: #33465f; background: #33465f; } +.s-running rect, .s-running .swatch { fill: #3a6aa6; background: #3a6aa6; } +.s-done rect, .s-done .swatch { fill: #2b3a4f; background: #2b3a4f; } .s-failed rect, .s-failed .swatch { fill: var(--fail); background: var(--fail); } .s-skipped rect, .s-skipped .swatch { fill: #2b2838; background: #2b2838; } .s-notfinished rect, .s-notfinished .swatch { fill: #4a3c22; background: #4a3c22; } diff --git a/assets/live/person.js b/assets/live/person.js index 4197e529..95865ce8 100644 --- a/assets/live/person.js +++ b/assets/live/person.js @@ -90,7 +90,8 @@ card.focus({ preventScroll: true }); } } else if (window.location.hash === '#progress') { - window.scrollTo(0, 0); // #progress is also the section's id: keep the head and the rail in view + // #progress is also the section's id, so the browser jumps to it: keep the head and the rail in view. + setTimeout(function () { window.scrollTo(0, 0); }, 0); } } function go(hash) { From 5c9bab44e9577f0e28d96bafa4e7cf5ba08be952 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sat, 7 Nov 2026 21:54:28 +0100 Subject: [PATCH 12/14] fix(live): read the APOE rows behind their warning as the report does, never as a dict --- bin/live_results.py | 8 +++++--- tests/test_live_results.py | 31 +++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/bin/live_results.py b/bin/live_results.py index 0cf398aa..aa62a89f 100644 --- a/bin/live_results.py +++ b/bin/live_results.py @@ -33,7 +33,7 @@ # results.json and family.json, and the page assets, for them. "Fine-grained" is a method's name. FORBIDDEN = re.compile(r"\b(ok|(?. - text = apoe if isinstance(apoe, str) else "; ".join(f"{k}: {v}" for k, v in apoe.items()) \ - if isinstance(apoe, dict) else cell(apoe) + # collect_summary keeps one dict per APOE row; each reads as the report's text line does. + rows = apoe if isinstance(apoe, list) else [apoe] + text = "; ".join(f"{r.get('trait')} ({r.get('rsid')}): {r.get('genotype')}. {r.get('reading')}" + if isinstance(r, dict) else cell(r) for r in rows) out["hidden"] = {"summary": rr.APOE_WARNING, "text": plain(text)} return out diff --git a/tests/test_live_results.py b/tests/test_live_results.py index 094d8888..5aea88dc 100644 --- a/tests/test_live_results.py +++ b/tests/test_live_results.py @@ -19,11 +19,14 @@ 5. through the real server: /runs//results.json is behind the login (401 without a session), answers the cards with one, and writes one access log row per request; summary.json stays 404; /runs.json lists the run with - its progress and attention count, behind the login too. + its progress and attention count, behind the login too; + 6. APOE: the Traits card's hidden text, behind its warning, is each APOE row + as the report prints it (trait, rsID, genotype, reading), never a dict. Each check runs again on a planted mistake and must fail there: 1 with VNTYPER's entry dropped, 2 with one card's caveat blanked, 3 with "ok" planted in a card and "All clear" in a script, 4 with the hits emptied, 5 with the results route -registered without the login. A check that cannot fail is no check. +registered without the login, 6 with a row printed as its Python dict. A check +that cannot fail is no check. Run: python3 tests/test_live_results.py """ @@ -138,6 +141,23 @@ def clinvar_problems(with_hits, without): return out +# --- 6. APOE behind its warning ------------------------------------------------------------- + +APOE_ROW = {"rsid": "rs429358", "gene": "APOE", "trait": "APOE e4 marker", "genotype": "T/C", + "status": "called", "reading": "one copy of the e4 allele", "ancestry": "", "note": "", "pmids": "1"} + + +def apoe_problems(hidden): + out = [] + text = (hidden or {}).get("text") or "" + if "{" in text or "'rsid'" in text: + out.append(f"a dict in the text {text!r}") + for want in ("APOE e4 marker", "rs429358", "T/C", "one copy of the e4 allele"): + if want not in text: + out.append(f"{want!r} missing from {text!r}") + return out + + # --- 5. the server -------------------------------------------------------------------------- def request(port, method, path, body=None, headers=None): @@ -264,6 +284,13 @@ def main(): check("4. ClinVar: a table with hits; without, the sentence that ClinVar lists only part", not bad, bad) control("the demo's hits emptied on both sides", not clinvar_problems(without, without)) + # 6. + S = {"traits": {"data": {"rows": [], "called": 0, "not_covered": 0, "apoe": [APOE_ROW]}}} + hidden = live_results.a_traits(S, SAMPLE).get("hidden") + bad = apoe_problems(hidden) + check("6. APOE: the hidden text reads each row as the report does, never a dict", not bad, bad) + control("the APOE row printed as its Python dict", not apoe_problems({"text": str(APOE_ROW)})) + # 5. pwfile = os.path.join(work, "password") with open(pwfile, "w") as f: From 44ece2fa0937e5c492adff4e61aefdb9c446b4e0 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sat, 7 Nov 2026 22:02:40 +0100 Subject: [PATCH 13/14] test(live): keep the APOE row's rsID and genotype off one source line for the personal-data scan --- tests/test_live_results.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_live_results.py b/tests/test_live_results.py index 5aea88dc..4352b923 100644 --- a/tests/test_live_results.py +++ b/tests/test_live_results.py @@ -152,7 +152,8 @@ def apoe_problems(hidden): text = (hidden or {}).get("text") or "" if "{" in text or "'rsid'" in text: out.append(f"a dict in the text {text!r}") - for want in ("APOE e4 marker", "rs429358", "T/C", "one copy of the e4 allele"): + wants = [APOE_ROW["trait"], APOE_ROW["rsid"], APOE_ROW["genotype"], APOE_ROW["reading"]] + for want in wants: if want not in text: out.append(f"{want!r} missing from {text!r}") return out From 863dca15ea5c3a4d5987203aa3eefcde3377bc51 Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Sun, 8 Nov 2026 00:04:21 +0100 Subject: [PATCH 14/14] fix(live): show what the report says of each section, and keep the cached results and family page current The cards now carry the report's qualifications: ClinVar read checks, combined-allele notes and deep intronic calls, not-assessed secondary findings, a carrier list not made and an unparsed PharmCAT report as sentences, the report's rare HIGH-impact count, Slivar phase lines, every Exomiser variant, AnnotSV checks, the sizing safeguards of expansions, genotype rules and the ancestry mark of traits, and the ancient PCA plot. The server re-reads a rewritten summary.json, pairs headlines with their own summary, never reads a summary or couple table that links out of its folder, and forgets runs that are gone. The family page refreshes and says when it failed. --- assets/live/core.js | 7 +-- assets/live/family-page.js | 23 +++++++++- assets/live/live.css | 8 ++++ assets/live/person.js | 8 ++-- bin/live_results.py | 86 +++++++++++++++++++++++++---------- bin/owngenome_live.py | 42 ++++++++++++++--- bin/owngenome_live_family.py | 5 +- bin/render_report.py | 8 +++- docs/live-site.md | 2 +- tests/demo/build_live_demo.py | 2 +- tests/test_live_demo.py | 9 ++++ tests/test_live_results.py | 86 ++++++++++++++++++++++++++++++++++- 12 files changed, 241 insertions(+), 45 deletions(-) diff --git a/assets/live/core.js b/assets/live/core.js index 8232f552..64ec443f 100644 --- a/assets/live/core.js +++ b/assets/live/core.js @@ -95,15 +95,16 @@ }); add(tb, body); add(wrap, tb); - var more = t.rows.length - limit + (t.more || 0); if (t.rows.length > limit) { - var b = el('button', { type: 'button', 'class': 'more' }, 'Show all ' + (t.rows.length + (t.more || 0)) + ' rows'); + var b = el('button', { type: 'button', 'class': 'more' }, 'Show all ' + t.rows.length + ' rows'); b.addEventListener('click', function () { Array.prototype.forEach.call(body.querySelectorAll('tr.extra'), function (x) { x.hidden = false; }); b.remove(); }); add(wrap, b); - } else if (more > 0) { + } + // Rows past results.json's cap are only in the report: said apart from the button, never promised by it. + if (t.more > 0) { add(wrap, el('p', { 'class': 'dim' }, t.more + ' more rows in the report')); } return wrap; diff --git a/assets/live/family-page.js b/assets/live/family-page.js index cff076e3..8a95a211 100644 --- a/assets/live/family-page.js +++ b/assets/live/family-page.js @@ -211,9 +211,28 @@ root.setAttribute('data-pgp-status', 'ready'); } + // The page asks again every 30 s and redraws only when family.json changed (a member's run moved on, + // step 39 finished). A failed first request says so, with a retry; a later one keeps what is shown. + var shown = null; + function failed() { + if (shown !== null) { return; } + var main = document.getElementById('family'); + main.textContent = ''; + var b = el('button', { type: 'button', 'class': 'more' }, 'Try again'); + b.addEventListener('click', function () { main.textContent = ''; add(main, el('p', { 'class': 'dim' }, 'Loading the family.')); fetchFamily(); }); + add(main, el('p', {}, 'The family could not be loaded.'), b); + root.setAttribute('data-pgp-status', 'offline'); + } + function fetchFamily() { + return U.getJSON('family.json').then(function (f) { + if (!f) { failed(); return; } + var k = JSON.stringify(f); + if (k !== shown) { shown = k; draw(f); } + }).catch(failed); + } function start() { - U.getJSON('family.json').then(function (f) { if (f) { draw(f); } }) - .catch(function () { root.setAttribute('data-pgp-status', 'offline'); }); + fetchFamily(); + window.setInterval(function () { if (!document.hidden) { fetchFamily(); } }, 30000); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start); } else { start(); } }()); diff --git a/assets/live/live.css b/assets/live/live.css index c8214771..08f6ce5e 100644 --- a/assets/live/live.css +++ b/assets/live/live.css @@ -195,6 +195,14 @@ figure.chart svg { width: 100%; height: auto; max-width: 100%; } .s-notfinished rect, .s-notfinished .swatch { fill: #4a3c22; background: #4a3c22; } .s-notplanned rect, .s-notplanned .swatch { fill: var(--bg); background: var(--bg); } .s-skipped text, .s-notplanned text { fill: var(--dim); } +/* Light theme: pale state fills under the dark label text, so every step name stays readable. */ +@media (prefers-color-scheme: light) { + .s-running rect, .s-running .swatch { fill: #b9d0ef; background: #b9d0ef; } + .s-done rect, .s-done .swatch { fill: #d3dbe6; background: #d3dbe6; } + .s-skipped rect, .s-skipped .swatch { fill: #e6e3ef; background: #e6e3ef; } + .s-notfinished rect, .s-notfinished .swatch { fill: #f1e0bd; background: #f1e0bd; } + .s-failed text { fill: #fff; } +} .s-running rect { animation: pulse 2s ease-in-out infinite; } @keyframes pulse { 50% { opacity: .7; } } .recheck-mark { fill: var(--low); } diff --git a/assets/live/person.js b/assets/live/person.js index 95865ce8..6433f7d1 100644 --- a/assets/live/person.js +++ b/assets/live/person.js @@ -57,7 +57,8 @@ // --- the route -------------------------------------------------------------------------------- function parse() { - var h = decodeURIComponent((window.location.hash || '').slice(1)); + var h; + try { h = decodeURIComponent((window.location.hash || '').slice(1)); } catch (e) { return null; } // a broken fragment if (h === 'progress') { return { tab: 'progress' }; } if (h.indexOf('q/') === 0) { return { tab: h.slice(2) }; } if (h.indexOf('s/') === 0) { return { tab: 'section', id: h.slice(2) }; } @@ -320,12 +321,13 @@ // --- data --------------------------------------------------------------------------------------- function load() { - var key = replay ? 'replay' : doneSet(); + var key = replay ? 'replay' : doneSet() + '|' + ((state && state.results_stamp) || ''); if (key === fetchedFor) { return; } fetchedFor = key; U.getJSON('results.json').then(function (r) { if (key !== fetchedFor) { return; } - results = r && (r.chapters || r.note) ? r : results; + if (!(r && (r.chapters || r.note))) { fetchedFor = null; return; } // tried again on the next state + results = r; redraw(true); }).catch(function () { if (key === fetchedFor) { fetchedFor = null; } }); } diff --git a/bin/live_results.py b/bin/live_results.py index aa62a89f..74f77a53 100644 --- a/bin/live_results.py +++ b/bin/live_results.py @@ -137,6 +137,16 @@ def a_variants(S, s): ["Indels", d.get("indels")]], "links": [r"(^|/)vep/.*summary\.html$"]} +def clinvar_call(h): + """A hit's Call cell: the low-confidence mark, the cDNA read check and the combined-allele note, + each as the report prints it.""" + parts = [rr.call_text(h) or ("low confidence" if h.get("low_confidence") else "")] + if "cdna" in h: + parts.append("reads: " + rr.cdna_text(h)) + parts.append(rr.combined_cell(h)) + return "; ".join(x for x in parts if x) or "." + + def a_clinvar(S, s): d = S["clinvar"]["data"] by = d.get("by_stars") or {} @@ -151,9 +161,12 @@ def a_clinvar(S, s): for h in (d.get("hits") or [])[:50]: rows.append([h.get("gene"), f"{h.get('chrom')}:{h.get('pos')} {h.get('ref')}>{h.get('alt')}", h.get("genotype"), h.get("significance"), "★" * int(h.get("stars") or 0) or "0", - rr.af_text(h) or "frequency unknown", (rr.call_text(h) or ".") - + (" · low confidence" if h.get("low_confidence") else "")]) + rr.af_text(h) or "frequency unknown", clinvar_call(h)]) lines = [x for x in (rr.cdna_scan_lines(d) or [])] + for x in d.get("deep_intronic") or []: + # Always reported by the report, hit or not: each site's call stays visible on the card. + lines.append(f"Known deep intronic site (always reported): {x.get('gene')} {x.get('hgvs')} " + f"{x.get('chrom')}:{x.get('pos')} {x.get('ref')}>{x.get('alt')}: {rr.deep_intronic_call(x)}") freq = rr.clinvar_freq_line(d) if freq: lines.insert(0, freq) @@ -167,13 +180,17 @@ def a_clinical_filter(S, s): d = S["clinical"]["data"] sf = d.get("acmg_sf") or {} bi = d.get("by_impact") or {} - rows = [[r[0], r[1], r[2], r[3]] for r in (rr.sf_rows(sf) if sf else [])] if sf else [] - return {"facts": [["Variants after the clinical filter", d.get("variants")], - ["By impact (high / moderate / low)", f"{bi.get('HIGH', 0)} / {bi.get('MODERATE', 0)} / {bi.get('LOW', 0)}"], - ["Genes", d.get("genes")], - [f"{sf.get('version') or 'ACMG SF'}: ClinVar P/LP in its genes", len(sf.get("clinvar_hits") or [])], - ["High-impact rare variants in its genes", len(sf.get("high_impact") or []) - if sf.get("high_impact_checked") else "not checked"]], + facts = [["Variants after the clinical filter", d.get("variants")], + ["By impact (high / moderate / low)", f"{bi.get('HIGH', 0)} / {bi.get('MODERATE', 0)} / {bi.get('LOW', 0)}"], + ["Genes", d.get("genes")]] + if sf.get("unavailable"): + # Not assessed is never zero findings: the report's sentence instead of the counts. + return {"facts": facts, "lines": rr.sf_apart_lines(sf), "attention": True} + rows = [[r[0], r[1], r[2], r[3]] for r in rr.sf_rows(sf)] if sf else [] + high = f"{rr.high_rare(sf)}{rr.high_apart(sf)}" if sf.get("high_impact_checked") else "not checked" + facts += [[f"{sf.get('version') or 'ACMG SF'}: ClinVar P/LP in its genes", len(sf.get("clinvar_hits") or [])], + ["High-impact rare variants in its genes", high]] + return {"facts": facts, "table": table(["Gene", "Variant", "Kind", "Rule"], rows), "attention": bool(sf.get("clinvar_hits") or sf.get("high_impact") or sf.get("structural"))} @@ -193,16 +210,16 @@ def a_slivar(S, s): return {"facts": [["Prioritized variants", d.get("prioritized")], ["Compound-het candidate variants", d.get("compound_het_variants")], ["Compound-het candidate genes", d.get("compound_het_genes")]], - "lines": [rr.comphet_phase_text(d)] if d.get("phase") else []} + "lines": rr.comphet_phase_text(d) if "compound_het_phase" in d else []} def a_exomiser(S, s): d = S["exomiser"]["data"] rows = [] for g in d.get("genes") or []: - v = (g.get("variants") or [{}])[0] - rows.append([g.get("rank"), g.get("gene"), g.get("moi"), g.get("exomiser_score"), v.get("hgvs") or v.get("variant"), - v.get("genotype"), v.get("acmg"), v.get("disease")]) + for v in g.get("variants") or [{}]: + rows.append([g.get("rank"), g.get("gene"), g.get("moi"), g.get("exomiser_score"), + v.get("hgvs") or v.get("variant"), v.get("genotype"), v.get("acmg"), v.get("disease")]) return {"facts": [["Symptoms searched", ", ".join(f"{t.get('label')} ({t.get('id')})" for t in d.get("terms") or []) or "none"]], "table": table(["Rank", "Gene", "Inheritance", "Score", "Variant", "Genotype", "ACMG", "Disease"], rows)} @@ -217,9 +234,11 @@ def a_structural(S, s): ["Consensus SVs (2+ callers)", sv.get("consensus", ".")], ["AnnotSV class 4 and 5", av.get("count", ".")]] rows = [[x.get("type"), x.get("sv"), x.get("gt"), x.get("length"), x.get("acmg_class"), - rr.annotsv_genes_text(x) if x.get("genes") is not None else ".", rr.annotsv_support_text(x)] + rr.annotsv_genes_text(x) if x.get("genes") is not None else ".", rr.annotsv_support_text(x), + "; ".join(rr.annotsv_flags(x)) if "exon_edge" in x else "."] for x in av.get("rows") or []] - return {"facts": facts, "table": table(["Type", "Where", "Genotype", "Length", "Class", "Genes", "Support"], rows), + return {"facts": facts, "table": table(["Type", "Where", "Genotype", "Length", "Class", "Genes", "Support", + "Checks"], rows), "attention": bool(av.get("count"))} @@ -235,17 +254,20 @@ def a_mei(S, s): def a_expansions(S, s): d = S["expansions"]["data"] - rows, flagged = [], False + rows, flagged, outside = [], False, 0 for l in d.get("loci") or []: status = l.get("status") or "" out = any(x and x != "normal" for x in status.split(",")) + outside += out flagged = flagged or out or bool(l.get("zone")) - rows.append([l.get("locus"), l.get("repeat_count"), f'"{status}"' if status else "not graded", + shown = rr.str_status(l) + count = rr.str_count(l) if isinstance(l.get("repeat_count"), str) else l.get("repeat_count") + rows.append([l.get("locus"), count, + f'"{shown}"' if shown not in ("", "not graded", "not sized") else (shown or "not graded"), l.get("normal_max"), l.get("pathologic_min"), l.get("ci"), l.get("sizing"), f'{l.get("zone_label")} ({l.get("zone")})' if l.get("zone") else "."]) facts = [["Loci tested", d.get("records")], - ["Outside Stranger's \"normal\"", len([r for r in rows if r[2] not in ('"normal"', "not graded")]) - if d.get("stranger") else "Stranger not run"], + ["Outside Stranger's \"normal\"", outside if d.get("stranger") else "Stranger not run"], ["In an intermediate zone", ", ".join(d.get("intermediate") or []) or "none"], ["Not sized from short reads", ", ".join(d.get("unsized") or []) or "none"]] return {"facts": facts, "attention": flagged, @@ -311,6 +333,8 @@ def a_mosaic_mca(S, s): def a_carrier(S, s): d = S["carrier"]["data"] + if d.get("unavailable"): + return {"lines": rr.carrier_lines(d), "attention": True} rows = [[c.get("gene"), c.get("variant"), c.get("genotype"), c.get("significance"), c.get("stars"), c.get("inheritance"), c.get("condition")] for c in d.get("carriers") or []] two = (d.get("two_hits") or {}).get("genes") or [] @@ -353,6 +377,9 @@ def a_pharmcat(S, s): def a_cpic(S, s): d = S["cpic"]["data"] + if d.get("parse_failed"): + return {"lines": ["The PharmCAT report could not be parsed: see the CPIC report."] + + [f"WARNING: {w}" for w in d.get("warnings") or []], "attention": True} rows = [[g.get("gene"), g.get("diplotype"), f'"{g.get("phenotype")}"' if g.get("phenotype") else ".", g.get("status")] for g in d.get("genes") or []] return {"facts": [["Genes with a changed-function phenotype", d.get("non_normal", 0)], @@ -435,10 +462,13 @@ def a_ancient(S, s): for r in (d.get("nearest") or [])[:10]] out = {"table": table(["Rank", "Group", "Dates", "Country", "Distance"], rows), "facts": [["Sites used", f"{d.get('sites_used')} of {d.get('panel_sites')}"]]} - if isinstance(d.get("plot_svg"), str) and d["plot_svg"].lstrip().startswith("<"): + svg = d.get("plot_svg") + if isinstance(svg, str) and svg.strip(): + # collect_summary stores it base64-encoded (the report puts it in a data URL as is); raw SVG is encoded here. import base64 - out["image"] = {"alt": "PCA of ancient genomes with this genome projected", - "src": "data:image/svg+xml;base64," + base64.b64encode(d["plot_svg"].encode()).decode()} + if svg.lstrip().startswith("<"): + svg = base64.b64encode(svg.encode()).decode() + out["image"] = {"alt": "PCA of ancient genomes with this genome projected", "src": "data:image/svg+xml;base64," + svg} return out @@ -452,10 +482,18 @@ def a_roh(S, s): def a_traits(S, s): d = S["traits"]["data"] - rows = [[r.get("gene"), r.get("trait"), r.get("genotype"), r.get("reading")] + rows = [[r.get("gene"), r.get("trait"), (r.get("genotype") or ".") + (" (ancestry)" if r.get("ancestry") == "yes" else ""), + (r.get("reading") or ".") + (f". {r['note']}" if r.get("note") else "")] for r in d.get("rows") or [] if r.get("status") == "called" and (r.get("gene") or "").upper() != "APOE"] + lines = [] + for r in d.get("rules") or []: + # The report's genotype rules: several sites read together, with phase and APOL1's G2. + lines.append(f"{r.get('trait')} ({r.get('gene')} {str(r.get('rsids') or '').replace(',', ', ')}): " + f"{rr.rule_result(r)}. {r.get('reading')}") + if r.get("g2"): + lines.append(f"With G2 (rs71785313): {rr.g2_result(r)}. {r['g2'].get('text')} {r['g2'].get('reading')}") out = {"facts": [["Rows read", d.get("called")], ["No genotype at the position", d.get("not_covered")]], - "table": table(["Gene", "Trait", "Genotype", "Reading"], rows)} + "table": table(["Gene", "Trait", "Genotype", "Reading"], rows), "lines": lines} apoe = d.get("apoe") if apoe: # Behind its warning, as the report shows it: the page puts it in a closed
    . diff --git a/bin/owngenome_live.py b/bin/owngenome_live.py index 02008bd5..eaa7a993 100755 --- a/bin/owngenome_live.py +++ b/bin/owngenome_live.py @@ -643,6 +643,10 @@ def state(self, run): sec_of = getattr(_rr(), "SECTION_OF", {}) st["anchors"] = {k: sec_of[k] for s in st["steps"] for k in s["sections"] if k in sec_of} st["headlines"] = self.headlines(run["name"], d, st) + # Changes when step 24 re-writes summary.json with no new step done: the person page fetches + # results.json again on it. + stamp = self.file_stamp(os.path.join(d, "summary.json")) + st["results_stamp"] = "%d-%d" % stamp if stamp else None st["plugins"] = {} for name, fn in self.app.state_hooks: st["plugins"][name] = fn(d, st, events) @@ -653,7 +657,9 @@ def summary(self, sample, d, st): the end of every done step (step 24 wrote it after them), else collected now. Read again only when the set of done steps or the message count changes; None while nothing is done.""" done = tuple(s["step"] for s in st["steps"] if s["state"] == "done") - key = (sample, done, st.get("events")) # a section's file can land after its step's last message + # A section's file can land after its step's last message, and a re-made report rewrites + # summary.json with no new message: its mtime and size are part of the key. + key = (sample, done, st.get("events"), self.file_stamp(os.path.join(d, "summary.json")) if d else None) hit = self.headline_cache.get(sample) if hit and hit[0] == key: return hit[2] @@ -669,10 +675,21 @@ def summary(self, sample, d, st): self.headline_cache[sample] = (key, None, summary) return summary + @staticmethod + def file_stamp(p): + try: + st = os.stat(p) + return st.st_mtime_ns, st.st_size + except OSError: + return None + @staticmethod def summary_file(d, st): - """/summary.json when it is newer than every done step's end, else None.""" + """/summary.json when it is newer than every done step's end, else None. A summary.json + that links out of the run folder is never read.""" p = os.path.join(d, "summary.json") + if not Live.inside(p, os.path.realpath(d)): + return None try: mtime = os.path.getmtime(p) except OSError: @@ -693,10 +710,13 @@ def summary_file(d, st): def headlines(self, sample, d, st): """{section: {text, tone, step}} for the sections of the steps that are done.""" - summary = self.summary(sample, d, st) + self.summary(sample, d, st) + # The headlines come from the cached entry's own summary and go back only into that same + # entry, so a request that saw another state never pairs its headlines with this key. hit = self.headline_cache.get(sample) if hit and hit[1] is not None: return hit[1] + summary = hit[2] if hit else None out = {} if summary: for s in st["steps"]: @@ -706,7 +726,7 @@ def headlines(self, sample, d, st): h = headline(k, summary["sections"].get(k), summary) if h: out[k] = {"text": h[0], "tone": h[1], "step": s["step"]} - if hit: + if hit and self.headline_cache.get(sample) is hit: self.headline_cache[sample] = (hit[0], out, hit[2]) return out @@ -763,12 +783,15 @@ def attention(self, d): """(count of cards worth a look, mtime) from /summary.json alone, cached by its mtime; (None, None) without one. The home page reads no result file beyond that.""" p = os.path.join(d, "summary.json") if d else None + if not p or not self.inside(p, os.path.realpath(d)) or not self.inside(p, self.root): + return None, None # a summary.json linking out of its run is never read try: mtime = os.path.getmtime(p) except (OSError, TypeError): return None, None + stamp = self.file_stamp(p) hit = self.attention_cache.get(p) - if hit and hit[0] == mtime: + if hit and hit[0] == stamp: return hit[1], mtime n = None try: @@ -778,13 +801,20 @@ def attention(self, d): except Exception as e: # one bad file must not take the home page down print(f"owngenome_live: {os.path.basename(os.path.dirname(p))}/summary.json not read: {type(e).__name__}", file=sys.stderr) - self.attention_cache[p] = (mtime, n) + if n is not None: # a failed read is tried again on the next request + self.attention_cache[p] = (stamp, n) return n, mtime def runs_index(self): """/runs.json: each run's progress, and the families with their members, for the home page. No result beyond the attention count; no path.""" runs = self.runs() + # Forget the cached results of runs that are gone, so the caches hold only the runs there are. + for k in [k for k in list(self.headline_cache) if k not in runs]: + self.headline_cache.pop(k, None) + keep = {os.path.join(r["dir"] or os.path.join(self.root, n), "summary.json") for n, r in runs.items()} + for k in [k for k in list(self.attention_cache) if k not in keep]: + self.attention_cache.pop(k, None) out = [] for name, r in sorted(runs.items()): item = {"name": name, "kind": r["kind"], "url": f"/runs/{name}/"} diff --git a/bin/owngenome_live_family.py b/bin/owngenome_live_family.py index 00c712a6..17ec9f87 100644 --- a/bin/owngenome_live_family.py +++ b/bin/owngenome_live_family.py @@ -67,8 +67,9 @@ def family_dirs(root): def read_tsv(path): - """Rows of a tab-separated file with a header line, as dicts; [] when it is not there.""" - if not os.path.isfile(path): + """Rows of a tab-separated file with a header line, as dicts; [] when it is not there or is a + link out of its own folder (family.json serves only what the family folder itself holds).""" + if not os.path.isfile(path) or not inside(path, os.path.dirname(os.path.abspath(path))): return [] rows = [] with open(path, errors="replace") as f: diff --git a/bin/render_report.py b/bin/render_report.py index 0b82fcbf..0e17c2db 100755 --- a/bin/render_report.py +++ b/bin/render_report.py @@ -1280,19 +1280,25 @@ def head(key, title=None): w(f" {ANCESTRY_FINE_NOTE}") w("") + sv_note = [f" {SV_CALLERS_NOTE}"] # printed once, under the first structural caller with results if head("manta", "Structural Variants (Manta)"): d = S["manta"]["data"] w(f" Total SVs: {d['total']} PASS: {d['pass']}") - w(f" {SV_CALLERS_NOTE}") + while sv_note: + w(sv_note.pop()) w("") if head("delly", "Structural Variants (Delly)"): d = S["delly"]["data"] w(f" Total SVs: {d['total']} PASS: {d['pass']}") + while sv_note: + w(sv_note.pop()) w("") if head("cnvpytor", "Copy Number Variants (CNVpytor)"): d = S["cnvpytor"]["data"] w(f" Total CNVs: {d['total']} ({d['deletions']} deletions, {d['duplications']} duplications)") w(f" Significant (e-val < 0.01): {d['significant']}") + while sv_note: + w(sv_note.pop()) filt = cnvpytor_filtered_text(d) if filt: w(f" Significant after filters: {filt}") diff --git a/docs/live-site.md b/docs/live-site.md index 9ef2ef89..5434e241 100644 --- a/docs/live-site.md +++ b/docs/live-site.md @@ -76,7 +76,7 @@ The reads cross the network to the browser that logged in: only the byte ranges `GET /` lists the runs: each folder under `GENOME_DIR` holding `logs/run_status.tsv`, `nextflow/` or `run_manifest.tsv`, and each run the site has messages for. The home page reads `/runs.json`: each run's progress and attention count, and each family with its members. -The person page reads `/runs//results.json` from `bin/owngenome_live_results.py`: `bin/live_results.py` turns the run's summary (its `summary.json` when step 24 wrote it after every finished step, else the step files read again when a step ends) into the report's chapters, one card per section. It computes nothing about genetics and holds only what the report prints. The family page reads `/runs//family.json` from the family plugin. +The person page reads `/runs//results.json` from `bin/owngenome_live_results.py`: `bin/live_results.py` turns the run's summary (its `summary.json` when step 24 wrote it after every finished step, else the step files read again when a step ends; a rewritten `summary.json` is read again too, and the page fetches the cards again) into the report's chapters, one card per section. It computes nothing about genetics and holds only what the report prints. The family page reads `/runs//family.json` from the family plugin, and asks again every 30 seconds, so a member's progress and a new couple screen show without a reload. ## Privacy and what is stored diff --git a/tests/demo/build_live_demo.py b/tests/demo/build_live_demo.py index bc724f4f..1d62b912 100644 --- a/tests/demo/build_live_demo.py +++ b/tests/demo/build_live_demo.py @@ -257,7 +257,7 @@ def home_page(index): log-out button (a static host has no session).""" with open(os.path.join(ASSETS, "home.html")) as f: page = f.read() - items = "\n".join(f'
  • {html.escape(r["name"])}
  • ' for r in index["runs"]) + items = "\n".join(f'
  • {html.escape(r["name"])}
  • ' for r in index["runs"]) page = (page.replace("{{assets}}", "assets").replace("{{data}}", "runs.json").replace("{{runs}}", items) .replace('
    ', '

    A demo: the people, the run and the results are invented.

    ')) diff --git a/tests/test_live_demo.py b/tests/test_live_demo.py index bc64d1c7..33c54dee 100644 --- a/tests/test_live_demo.py +++ b/tests/test_live_demo.py @@ -313,6 +313,15 @@ def phone_checks(b, base): "s.dispatchEvent(new Event('input')); true") b.wait("document.documentElement.dataset.pgpFrame === document.getElementById('replay-slider').max") time.sleep(1) + # Every tab of the person page, not only the one the link opens. + tabs = b.eval("Array.prototype.map.call(document.querySelectorAll('[role=tab][data-tab]'), " + "function (t) { return t.getAttribute('data-tab'); }).join(' ')") or "" + for tab in tabs.split(): + b.eval(f"document.querySelector('[role=tab][data-tab=\"{tab}\"]').click(); true") + time.sleep(0.3) + if too_wide(b): + wide[f"person {tab}"] = too_wide(b) + wide["person tabs"] = "" if len(tabs.split()) >= 3 else f"only {tabs!r}" wide[name] = too_wide(b) check("browser: at 390 px no page scrolls sideways", not any(wide.values()), wide) b.eval("var d = document.createElement('div'); d.style.width = '900px'; d.style.height = '4px'; " diff --git a/tests/test_live_results.py b/tests/test_live_results.py index 4352b923..b1a9bc5e 100644 --- a/tests/test_live_results.py +++ b/tests/test_live_results.py @@ -21,16 +21,23 @@ log row per request; summary.json stays 404; /runs.json lists the run with its progress and attention count, behind the login too; 6. APOE: the Traits card's hidden text, behind its warning, is each APOE row - as the report prints it (trait, rsID, genotype, reading), never a dict. + as the report prints it (trait, rsID, genotype, reading), never a dict; + 7. the adapters carry what the report says of a section, on small made-up + sections: the ancient PCA plot (base64 in the summary) as an image, the + Slivar phase lines, every Exomiser variant of a gene, a not-assessed + secondary-findings list, a carrier list not made and an unparsed PharmCAT + report as sentences (never zero counts), an unsized "normal" locus as not + sized, AnnotSV's exon-edge check, a genotype rule and a deep intronic call. Each check runs again on a planted mistake and must fail there: 1 with VNTYPER's entry dropped, 2 with one card's caveat blanked, 3 with "ok" planted in a card and "All clear" in a script, 4 with the hits emptied, 5 with the results route -registered without the login, 6 with a row printed as its Python dict. A check +registered without the login, 6 with a row printed as its Python dict, 7 with the Slivar adapter returning nothing. A check that cannot fail is no check. Run: python3 tests/test_live_results.py """ import contextlib +import base64 import copy import io import json @@ -159,6 +166,71 @@ def apoe_problems(hidden): return out +# --- 7. the adapters on made-up sections ----------------------------------------------------- + +def adapter_sections(): + sv = {"type": "DEL", "sv": "chr1:100-5000", "gt": "0/1", "length": 4900, "acmg_class": 5, "genes": ["GENE1"], + "exon_edge": "yes", "exon_edge_detail": "GENE1 exon 2", "population_af": None, "population_source": "", + "common": False, "support": None} + return { + "ancient": {"state": "ok", "data": {"nearest": [], "sites_used": 1, "panel_sites": 2, + "plot_svg": base64.b64encode(b"").decode()}}, + "slivar": {"state": "ok", "data": {"prioritized": 2, "compound_het_variants": 2, "compound_het_genes": 1, + "compound_het_pairs": 1, "compound_het_phase": {"trans": 1}, + "compound_het_trans": [{"gene": "GENE2", "variants": "v1 v2", "reads": 3}]}}, + "exomiser": {"state": "ok", "data": {"genes": [{"rank": 1, "gene": "GENE3", "variants": [ + {"hgvs": "c.1A>G"}, {"hgvs": "c.2C>T"}]}]}}, + "clinical": {"state": "ok", "data": {"acmg_sf": {"unavailable": "no VEP output"}}}, + "carrier": {"state": "ok", "data": {"unavailable": "no ClinVar screen"}}, + "cpic": {"state": "ok", "data": {"parse_failed": True, "genes": [], "warnings": []}}, + "expansions": {"state": "ok", "data": {"stranger": True, "loci": [ + {"locus": "LOCUS1", "repeat_count": "20/30", "status": "normal", "sizing": "flanking_only"}]}}, + "annotsv": {"state": "ok", "data": {"count": 1, "rows": [sv]}}, + "traits": {"state": "ok", "data": {"rows": [], "rules": [ + {"trait": "Rule trait", "gene": "GENE4", "rsids": "rs1,rs2", "status": "called", "result": "AB", + "phase": "read_backed", "reading": "rule reading"}]}}, + "clinvar": {"state": "ok", "data": {"hits": [], "deep_intronic": [ + {"gene": "GENE5", "hgvs": "c.100+500A>G", "chrom": "chr1", "pos": 1, "ref": "A", "alt": "G", + "call": "het", "detail": "GQ 40"}]}}, + } + + +def adapter_problems(adapters): + S, out = adapter_sections(), [] + def run(name, sec=None): + try: + return adapters[name](S, {"sections": S}) or {} + except Exception as e: # a crash is a problem, not a test error + out.append(f"{name}: {type(e).__name__} {e}") + return {} + text = lambda got: json.dumps(got) # noqa: E731 + if not (run("ancient").get("image") or {}).get("src", "").endswith(S["ancient"]["data"]["plot_svg"]): + out.append("ancient: no plot image") + if "in trans on 3 reads" not in text(run("slivar")): + out.append("slivar: no phase lines") + if len((run("exomiser").get("table") or {}).get("rows") or []) != 2: + out.append("exomiser: not every variant") + got = run("clinical_filter") + if "not assessed" not in text(got) or "ClinVar P/LP in its genes" in text(got): + out.append(f"clinical: not-assessed not said {got}") + got = run("carrier") + if "not made" not in text(got) or got.get("facts"): + out.append(f"carrier: not-made not said {got}") + got = run("cpic") + if "could not be parsed" not in text(got) or got.get("facts"): + out.append(f"cpic: parse failure not said {got}") + rows = (run("expansions").get("table") or {}).get("rows") or [[]] + if "not sized" not in rows[0]: + out.append(f"expansions: unsized locus shown as {rows[0]}") + if "on exon edges" not in text(run("structural")): + out.append("structural: no exon-edge check") + if "Rule trait" not in text(run("traits")): + out.append("traits: no genotype rule") + if "GENE5" not in text(run("clinvar")): + out.append("clinvar: no deep intronic call") + return out + + # --- 5. the server -------------------------------------------------------------------------- def request(port, method, path, body=None, headers=None): @@ -292,6 +364,16 @@ def main(): check("6. APOE: the hidden text reads each row as the report does, never a dict", not bad, bad) control("the APOE row printed as its Python dict", not apoe_problems({"text": str(APOE_ROW)})) + # 7. + names = {"ancient": "a_ancient", "slivar": "a_slivar", "exomiser": "a_exomiser", + "clinical_filter": "a_clinical_filter", "carrier": "a_carrier", "cpic": "a_cpic", + "expansions": "a_expansions", "structural": "a_structural", "traits": "a_traits", + "clinvar": "a_clinvar"} + adapters = {k: getattr(live_results, v) for k, v in names.items()} + bad = adapter_problems(adapters) + check("7. the adapters carry the report's plot, phase, variants, rules and not-assessed sentences", not bad, bad) + control("the Slivar adapter returning nothing", not adapter_problems(dict(adapters, slivar=lambda S, s: {}))) + # 5. pwfile = os.path.join(work, "password") with open(pwfile, "w") as f: