--- phase: 06-validation plan: 01 type: execute wave: 1 depends_on: [] files_modified: - src/usher_pipeline/scoring/negative_controls.py - src/usher_pipeline/scoring/validation.py - src/usher_pipeline/scoring/__init__.py autonomous: true must_haves: truths: - "Housekeeping genes are compiled as a curated negative control set with source provenance" - "Negative control validation shows housekeeping genes rank below 50th percentile median" - "Positive control validation reports recall@k metrics at k=10%, 20%, top-100" - "Known genes achieve >70% recall in top 10% of scored candidates" artifacts: - path: "src/usher_pipeline/scoring/negative_controls.py" provides: "Housekeeping gene compilation and negative control validation" exports: ["HOUSEKEEPING_GENES_CORE", "compile_housekeeping_genes", "validate_negative_controls"] - path: "src/usher_pipeline/scoring/validation.py" provides: "Enhanced positive control validation with recall@k and per-source breakdown" exports: ["validate_known_gene_ranking", "compute_recall_at_k", "generate_validation_report"] - path: "src/usher_pipeline/scoring/__init__.py" provides: "Updated exports including negative control functions" contains: "validate_negative_controls" key_links: - from: "src/usher_pipeline/scoring/negative_controls.py" to: "DuckDB scored_genes table" via: "PERCENT_RANK window function query" pattern: "PERCENT_RANK.*ORDER BY composite_score" - from: "src/usher_pipeline/scoring/validation.py" to: "src/usher_pipeline/scoring/known_genes.py" via: "compile_known_genes import" pattern: "from usher_pipeline.scoring.known_genes import" --- Implement negative control validation with housekeeping genes and enhance positive control validation with recall@k metrics. Purpose: Negative controls ensure the scoring system does not indiscriminately rank all genes high (complementing the existing positive control validation). Enhanced positive control metrics (recall@k) provide the specific ">70% in top 10%" measurement required by success criteria. Output: Two modules -- negative_controls.py (new) and enhanced validation.py (updated) -- ready for integration into the comprehensive validation report (Plan 03). @/Users/gbanyan/.claude/get-shit-done/workflows/execute-plan.md @/Users/gbanyan/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/06-validation/06-RESEARCH.md @src/usher_pipeline/scoring/validation.py @src/usher_pipeline/scoring/known_genes.py @src/usher_pipeline/scoring/quality_control.py @src/usher_pipeline/scoring/__init__.py @src/usher_pipeline/persistence/duckdb_store.py Task 1: Create negative control validation module with housekeeping genes src/usher_pipeline/scoring/negative_controls.py Create `src/usher_pipeline/scoring/negative_controls.py` with: 1. **HOUSEKEEPING_GENES_CORE** frozenset constant containing 13 curated housekeeping genes: RPL13A, RPL32, RPLP0, GAPDH, ACTB, B2M, HPRT1, TBP, SDHA, PGK1, PPIA, UBC, YWHAZ. Include inline comments grouping by function (ribosomal, metabolic, transcription/reference). 2. **compile_housekeeping_genes() -> pl.DataFrame** function returning DataFrame with columns: - gene_symbol (str) - source (str): "literature_validated" for all - confidence (str): "HIGH" for all Follow the exact same pattern as `compile_known_genes()` in known_genes.py. 3. **validate_negative_controls(store: PipelineStore, percentile_threshold: float = 0.50) -> dict** function: - Register housekeeping genes as temporary DuckDB table `_housekeeping_genes` - Use the same PERCENT_RANK window function pattern as `validate_known_gene_ranking()` in validation.py - Query: join ranked_genes CTE with _housekeeping_genes on gene_symbol - INVERTED validation logic: `validation_passed = median_percentile < percentile_threshold` - Return dict with keys: total_expected, total_in_dataset, median_percentile, top_quartile_count, in_high_tier_count, validation_passed, housekeeping_gene_details (top 20 by percentile ASC) - Clean up temp table after query - Use structlog logger with info/warning levels matching validation.py patterns 4. **generate_negative_control_report(metrics: dict) -> str** function: - Follow the exact formatting pattern from generate_validation_report() in validation.py - Show gene table with Score, Percentile, headers - Include interpretation text for pass/fail Use structlog, polars, duckdb imports matching existing scoring module patterns. Import PipelineStore from usher_pipeline.persistence.duckdb_store. Run: `cd /Users/gbanyan/Project/usher-exploring && python -c "from usher_pipeline.scoring.negative_controls import HOUSEKEEPING_GENES_CORE, compile_housekeeping_genes, validate_negative_controls, generate_negative_control_report; df = compile_housekeeping_genes(); print(f'Housekeeping genes: {df.height}'); assert df.height == 13; assert set(df.columns) == {'gene_symbol', 'source', 'confidence'}; print('OK')"` exits 0 negative_controls.py exists with 13 curated housekeeping genes, compile function returns correct DataFrame structure, validate function uses PERCENT_RANK with inverted threshold logic, report function generates human-readable output. Task 2: Enhance positive control validation with recall@k metrics src/usher_pipeline/scoring/validation.py, src/usher_pipeline/scoring/__init__.py **In validation.py**, add the following functions (do NOT modify existing functions, only ADD): 1. **compute_recall_at_k(store: PipelineStore, k_values: list[int] | None = None) -> dict** function: - Default k_values: [100, 500, 1000, 2000] (absolute counts) - Also compute recall at percentage thresholds: top 5%, 10%, 20% of scored genes - Query scored_genes ordered by composite_score DESC (WHERE NOT NULL) - For each k: count how many known genes (from compile_known_genes, deduplicated on gene_symbol) appear in top-k - Recall@k = found_in_top_k / total_known_unique - Return dict with: recalls_absolute (dict mapping k -> recall float), recalls_percentage (dict mapping pct_string -> recall float), total_known_unique (int), total_scored (int) - Use structlog for logging results 2. **validate_positive_controls_extended(store: PipelineStore, percentile_threshold: float = 0.75) -> dict** function: - Call existing validate_known_gene_ranking(store, percentile_threshold) to get base metrics - Call compute_recall_at_k(store) to get recall metrics - Add per-source breakdown: compute median percentile separately for "omim_usher" and "syscilia_scgs_v2" genes - Per-source query: same PERCENT_RANK CTE but filter JOIN by source - Return dict combining base metrics + recall_at_k + per_source_breakdown (dict mapping source -> {median_percentile, count, top_quartile_count}) - This is the "full" positive control validation for Phase 6 **In __init__.py**, add exports for: compute_recall_at_k, validate_positive_controls_extended, and also add imports/exports for negative_controls module: HOUSEKEEPING_GENES_CORE, compile_housekeeping_genes, validate_negative_controls, generate_negative_control_report. Run: `cd /Users/gbanyan/Project/usher-exploring && python -c "from usher_pipeline.scoring import compute_recall_at_k, validate_positive_controls_extended, HOUSEKEEPING_GENES_CORE, compile_housekeeping_genes, validate_negative_controls, generate_negative_control_report; print('All imports OK')"` exits 0 validation.py has compute_recall_at_k and validate_positive_controls_extended functions. __init__.py exports all new functions from both negative_controls.py and updated validation.py. Recall@k computes at both absolute and percentage thresholds. Per-source breakdown separates OMIM from SYSCILIA metrics. - `python -c "from usher_pipeline.scoring.negative_controls import HOUSEKEEPING_GENES_CORE; assert len(HOUSEKEEPING_GENES_CORE) == 13"` -- housekeeping genes compiled - `python -c "from usher_pipeline.scoring import validate_negative_controls, compute_recall_at_k, validate_positive_controls_extended"` -- all functions importable - `python -c "from usher_pipeline.scoring.negative_controls import compile_housekeeping_genes; df = compile_housekeeping_genes(); assert 'gene_symbol' in df.columns and 'source' in df.columns"` -- DataFrame structure correct - negative_controls.py creates housekeeping gene set and validates they rank low (inverted threshold) - validation.py compute_recall_at_k measures recall at multiple k values including percentage-based thresholds - validate_positive_controls_extended combines percentile + recall + per-source metrics - All new functions exported from scoring.__init__ After completion, create `.planning/phases/06-validation/06-01-SUMMARY.md`