Skip to content
This repository was archived by the owner on Jun 15, 2026. It is now read-only.

Commit aac9f95

Browse files
committed
v1.6.1: zygosity column, CADD fixes, methylation panel, config UX (#44)
Closes #31, #41, #42, #43, #45.
1 parent e9ce281 commit aac9f95

19 files changed

Lines changed: 388 additions & 41 deletions

‎CHANGELOG.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,37 @@
22

33
All notable changes are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
44

5+
## [1.6.1]
6+
7+
### Added
8+
- **Zygosity column in all report outputs (#41).** Every annotation row
9+
now shows `Heterozygous`, `Homozygous`, or `No Call` — derived from
10+
the genotype call. Appears in HTML, terminal, and JSON reports.
11+
Functional-medicine `+/−` notation deferred until the risk allele
12+
field lands (v2.1+).
13+
- **CADD PHRED styling in HTML reports (#42).** CADD scores are now
14+
color-coded by deleteriousness tier: red (≥30, top 0.1%), orange
15+
(≥20, top 1%), gray (10–20, top 10%), gray (<10, no tooltip).
16+
Tooltip shows the percentile tier on hover for scores ≥10.
17+
- **Config file path in `config get` / `config set` output (#43).**
18+
Both commands now print the resolved config file path, so users know
19+
which file is being read or written.
20+
21+
### Changed
22+
- **JSON schema version bumped to 4.** New `zygosity` field on every
23+
annotation. Diff between v1–v4 reports still works.
24+
- **Methylation panel expanded to 34 genes (#31).** Added ACAT1, DHFR,
25+
GNMT, MAOA, NOS3, SUOX, VDR. Sorted alphabetically.
26+
27+
### Fixed
28+
- **CADD multi-allelic scoring bug (#45).** At multi-allelic positions,
29+
`_enrich_cadd` max-reduced CADD PHRED across all alts, stamping the
30+
highest score regardless of which allele the user carries. Now looks
31+
up the score for the user's specific allele (direct match preferred
32+
over complement). Biallelic sites were unaffected.
33+
- **Test protocol AM column name.** `FULL_TEST_PROTOCOL.md` referenced
34+
"AM Score" but the actual HTML header is "AM".
35+
536
## [1.6.0]
637

738
### Added
@@ -1529,6 +1560,7 @@ All notable changes are documented here. Format follows [Keep a Changelog](https
15291560
- GitHub Actions CI matrix on Python 3.11 and 3.12.
15301561

15311562

1563+
[1.6.1]: https://github.com/dial481/allelix/compare/v1.6.0...v1.6.1
15321564
[1.6.0]: https://github.com/dial481/allelix/compare/v1.5.3...v1.6.0
15331565
[1.5.3]: https://github.com/dial481/allelix/compare/v1.5.2...v1.5.3
15341566
[1.5.2]: https://github.com/dial481/allelix/compare/v1.5.1...v1.5.2

‎allelix/cli.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,6 +1306,7 @@ def config_get(data_dir: Path | None, key: str | None) -> None:
13061306
cfg = load_config(resolved)
13071307

13081308
if key is None:
1309+
console.print(f"[dim]Config: {resolved / 'config.toml'}[/dim]")
13091310
click.echo(_serialize(cfg))
13101311
return
13111312

@@ -1397,6 +1398,7 @@ def config_set(data_dir: Path | None, key: str, value: str) -> None:
13971398
)
13981399

13991400
save_config(resolved, cfg)
1401+
console.print(f"[dim]Config: {resolved / 'config.toml'}[/dim]")
14001402
console.print(f"[green]Set {key} = {val_lower}[/green]")
14011403

14021404

‎allelix/models.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,13 @@ class Annotation:
105105
am_pathogenicity: float | None = None
106106
am_class: str = ""
107107
cadd_phred: float | None = None
108+
109+
@property
110+
def zygosity(self) -> str:
111+
"""Classify the genotype call as Heterozygous, Homozygous, or No Call."""
112+
if NO_CALL_MARKER in self.genotype_match:
113+
return "No Call"
114+
parts = self.genotype_match.split("/")
115+
if len(parts) != 2:
116+
return "Homozygous" if len(set(self.genotype_match)) == 1 else "Heterozygous"
117+
return "Heterozygous" if parts[0] != parts[1] else "Homozygous"

‎allelix/reports/_pipeline.py‎

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,28 @@ def rollup_gwas_duplicates(annotations: list[Annotation]) -> list[Annotation]:
231231
return survivors
232232

233233

234+
def _lookup_user_allele(
235+
user_alt: str,
236+
coords: list[tuple[str, int, str, str]],
237+
scores: dict[tuple[str, int, str, str], float],
238+
resolve_strand: Callable[[str, str, str], str | None],
239+
) -> float | None:
240+
"""Find the CADD score for a specific user allele at a multi-allelic site.
241+
242+
Prefers a direct allele match over a complement (minus-strand) match
243+
to avoid false positives where the complement of the user's allele
244+
coincidentally equals a different alt at the same position.
245+
"""
246+
for chrom, pos, ref, alt in coords:
247+
if user_alt == alt:
248+
return scores.get((chrom, pos, ref, alt))
249+
for chrom, pos, ref, alt in coords:
250+
resolved = resolve_strand(user_alt, ref, alt)
251+
if resolved is not None and resolved == alt:
252+
return scores.get((chrom, pos, ref, alt))
253+
return None
254+
255+
234256
def _enrich_cadd(
235257
annotations: list[Annotation],
236258
gnomad: GnomadAnnotator,
@@ -260,18 +282,16 @@ def _enrich_cadd(
260282
coords = coord_map.get(a.rsid)
261283
if not coords:
262284
continue
263-
best: float | None = None
264-
for chrom, pos, ref, alt in coords:
265-
if a.alt:
266-
resolved = resolve_strand(a.alt, ref, alt)
267-
if resolved is None:
268-
continue
269-
score = scores.get((chrom, pos, ref, resolved))
270-
else:
285+
if a.alt:
286+
score = _lookup_user_allele(a.alt, coords, scores, resolve_strand)
287+
a.cadd_phred = score
288+
else:
289+
best: float | None = None
290+
for chrom, pos, ref, alt in coords:
271291
score = scores.get((chrom, pos, ref, alt))
272-
if score is not None and (best is None or score > best):
273-
best = score
274-
a.cadd_phred = best
292+
if score is not None and (best is None or score > best):
293+
best = score
294+
a.cadd_phred = best
275295

276296

277297
def run_analysis(

‎allelix/reports/diff.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
from allelix.models import Annotation
2727

28-
_SUPPORTED_SCHEMA_VERSIONS = {"1", "2", "3"}
28+
_SUPPORTED_SCHEMA_VERSIONS = {"1", "2", "3", "4"}
2929

3030

3131
@dataclass

‎allelix/reports/html.py‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@
108108
.am-benign { color: #2e7d32; font-weight: 600; }
109109
.am-ambiguous { color: #f9a825; font-weight: 600; }
110110
.am-score { color: #999; }
111+
.cadd-high { color: #c62828; font-weight: 600; }
112+
.cadd-med { color: #e65100; font-weight: 600; }
113+
.cadd-low { color: #999; }
111114
tr.diff-new { background: #e8f5e9; }
112115
tr.diff-changed { background: #fff3e0; }
113116
tr.diff-removed { background: #fafafa; text-decoration: line-through; color: #999; }
@@ -306,7 +309,15 @@ def _format_cadd(score: float | None) -> str:
306309
"""Format a CADD PHRED score for display."""
307310
if score is None:
308311
return "—"
309-
return f"{score:.1f}"
312+
if score >= 30:
313+
css, tip = "cadd-high", "top 0.1% most deleterious"
314+
elif score >= 20:
315+
css, tip = "cadd-med", "top 1% most deleterious"
316+
else:
317+
css, tip = "cadd-low", "top 10% most deleterious" if score >= 10 else ""
318+
if tip:
319+
return f'<span class="{css}" title="{tip}">{score:.1f}</span>'
320+
return f'<span class="{css}">{score:.1f}</span>'
310321

311322

312323
def _row_html(
@@ -346,6 +357,7 @@ def _row_html(
346357
f'<span class="bar" style="width: {bar_width}px;"></span>'
347358
f"{a.magnitude:.1f}</td>"
348359
f"<td>{_escape(a.genotype_match)}</td>"
360+
f"<td>{_escape(a.zygosity)}</td>"
349361
f"{freq_td}"
350362
f"{am_td}"
351363
f"{cadd_td}"
@@ -510,7 +522,7 @@ def _th(label: str) -> str:
510522
"<table>"
511523
"<thead><tr>"
512524
f"{_th('rsID')}{_th('Gene')}{_th('Source')}{_th('Significance')}"
513-
f"{review_th}{_th('Magnitude')}{_th('Genotype')}"
525+
f"{review_th}{_th('Magnitude')}{_th('Genotype')}{_th('Zygosity')}"
514526
f"{freq_th}"
515527
f"{am_th}"
516528
f"{cadd_th}"

‎allelix/reports/json_report.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Output schema (versioned via `schema_version`):
66
77
{
8-
"schema_version": "3",
8+
"schema_version": "4",
99
"allelix_version": "1.1.0",
1010
"generated_at": "2026-05-11T12:34:56+00:00",
1111
"regulatory_notice": "...",
@@ -52,12 +52,13 @@
5252
from allelix.reports.diff import DiffResult
5353

5454

55-
SCHEMA_VERSION = "3"
55+
SCHEMA_VERSION = "4"
5656

5757

5858
def _annotation_dict(a: Annotation) -> dict:
5959
"""Serialize an annotation, adding AM caveat for non-protein sources."""
6060
d = {k: v for k, v in asdict(a).items() if k != "is_must_include"}
61+
d["zygosity"] = a.zygosity
6162
if a.am_pathogenicity is not None and a.source == "pharmgkb":
6263
d["am_caveat"] = "protein structure impact only"
6364
return d

‎allelix/reports/methylation.py‎

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,39 @@
1212
# Folate / one-carbon / methylation cycle genes. Add via PR + ADR if expanding.
1313
METHYLATION_PANEL_GENES: frozenset[str] = frozenset(
1414
{
15-
"MTHFR",
16-
"MTR",
17-
"MTRR",
18-
"COMT",
19-
"CBS",
15+
"ACAT1",
16+
"AHCY",
2017
"BHMT",
2118
"BHMT2",
22-
"MTHFD1",
23-
"MTHFD1L",
24-
"AHCY",
25-
"MAT1A",
26-
"MAT2A",
27-
"MAT2B",
28-
"TCN1",
29-
"TCN2",
30-
"FUT2",
31-
"GSTM1",
32-
"GSTP1",
19+
"CBS",
20+
"COMT",
21+
"DHFR",
3322
"DNMT1",
3423
"DNMT3A",
3524
"DNMT3B",
36-
"PEMT",
3725
"FOLR1",
3826
"FOLR2",
39-
"SLC19A1",
27+
"FUT2",
28+
"GNMT",
29+
"GSTM1",
30+
"GSTP1",
31+
"MAOA",
32+
"MAT1A",
33+
"MAT2A",
34+
"MAT2B",
35+
"MTHFD1",
36+
"MTHFD1L",
37+
"MTHFR",
38+
"MTR",
39+
"MTRR",
40+
"NOS3",
41+
"PEMT",
4042
"SHMT1",
4143
"SHMT2",
44+
"SLC19A1",
45+
"SUOX",
46+
"TCN1",
47+
"TCN2",
48+
"VDR",
4249
}
4350
)

‎allelix/reports/terminal.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ def _print_table(filtered: list[Annotation], console: Console) -> None:
164164
table.add_column("Review Status", style="dim")
165165
table.add_column("Magnitude", justify="right")
166166
table.add_column("Genotype", no_wrap=True)
167+
table.add_column("Zygosity", no_wrap=True)
167168
if has_freq:
168169
table.add_column("Freq", justify="right", no_wrap=True)
169170
if has_am:
@@ -181,6 +182,7 @@ def _print_table(filtered: list[Annotation], console: Console) -> None:
181182
a.review_status or "—",
182183
f"{a.magnitude:.1f}",
183184
a.genotype_match,
185+
a.zygosity,
184186
]
185187
if has_freq:
186188
row.append(_format_freq(a.allele_frequency))
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# ADR-0033: Schema Version Bump Policy
2+
3+
**Status:** Accepted
4+
**Date:** 2026-06-11
5+
**Supersedes:** The schema-version carve-out in ADR-0032 (Context section,
6+
"JSON schema version stays at 3").
7+
8+
## Context
9+
10+
ADR-0032 stated that additive optional fields (specifically `cadd_phred`)
11+
do not require a schema version bump. v1.6.1 adds `zygosity`, also an
12+
additive optional field, and bumps the schema to version 4. The two
13+
decisions are inconsistent.
14+
15+
## Decision
16+
17+
Every new field emitted in JSON report output bumps the schema version,
18+
even if the field is optional and additive. This gives consumers a
19+
reliable signal that the output shape changed.
20+
21+
The `cadd_phred` addition in v1.6.0 should have bumped the schema from
22+
2 to 3 under this policy. It did not (ADR-0032 carved it out). That
23+
ship has sailed — v1.6.0 emits schema version 3 with `cadd_phred`
24+
present, and changing it retroactively would break existing consumers.
25+
Going forward, all new fields bump.
26+
27+
## Consequences
28+
29+
- Any commit that adds, removes, or renames a field in the JSON report
30+
output must increment `_SCHEMA_VERSION` and update
31+
`_SUPPORTED_SCHEMA_VERSIONS` in the diff loader.
32+
- The diff loader's supported-versions set grows monotonically.
33+
- ADR-0032's body is not edited (ADR immutability). This ADR supersedes
34+
its schema-version stance.

0 commit comments

Comments
 (0)