gnomancer 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. gnomancer-0.3.1/LICENSE +21 -0
  2. gnomancer-0.3.1/PKG-INFO +211 -0
  3. gnomancer-0.3.1/README.md +186 -0
  4. gnomancer-0.3.1/gnomancer/__init__.py +2 -0
  5. gnomancer-0.3.1/gnomancer/__main__.py +2 -0
  6. gnomancer-0.3.1/gnomancer/cli.py +155 -0
  7. gnomancer-0.3.1/gnomancer/clinvar.py +133 -0
  8. gnomancer-0.3.1/gnomancer/context.py +32 -0
  9. gnomancer-0.3.1/gnomancer/data/haplogroup_context.tsv +72 -0
  10. gnomancer-0.3.1/gnomancer/data/markers.tsv +115 -0
  11. gnomancer-0.3.1/gnomancer/data/rCRS_NC_012920.1.fasta +239 -0
  12. gnomancer-0.3.1/gnomancer/haplogroups.py +130 -0
  13. gnomancer-0.3.1/gnomancer/load.py +67 -0
  14. gnomancer-0.3.1/gnomancer/mcp_server.py +200 -0
  15. gnomancer-0.3.1/gnomancer/panel.py +141 -0
  16. gnomancer-0.3.1/gnomancer/paths.py +47 -0
  17. gnomancer-0.3.1/gnomancer/pharmcat.py +120 -0
  18. gnomancer-0.3.1/gnomancer/qc.py +60 -0
  19. gnomancer-0.3.1/gnomancer/report.py +141 -0
  20. gnomancer-0.3.1/gnomancer/report_html.py +325 -0
  21. gnomancer-0.3.1/gnomancer/roh.py +60 -0
  22. gnomancer-0.3.1/gnomancer/setup_cmd.py +97 -0
  23. gnomancer-0.3.1/gnomancer/summary.py +181 -0
  24. gnomancer-0.3.1/gnomancer/yleaf.py +71 -0
  25. gnomancer-0.3.1/gnomancer.egg-info/PKG-INFO +211 -0
  26. gnomancer-0.3.1/gnomancer.egg-info/SOURCES.txt +35 -0
  27. gnomancer-0.3.1/gnomancer.egg-info/dependency_links.txt +1 -0
  28. gnomancer-0.3.1/gnomancer.egg-info/entry_points.txt +3 -0
  29. gnomancer-0.3.1/gnomancer.egg-info/requires.txt +10 -0
  30. gnomancer-0.3.1/gnomancer.egg-info/top_level.txt +1 -0
  31. gnomancer-0.3.1/pyproject.toml +32 -0
  32. gnomancer-0.3.1/setup.cfg +4 -0
  33. gnomancer-0.3.1/tests/test_annotations.py +71 -0
  34. gnomancer-0.3.1/tests/test_markers.py +26 -0
  35. gnomancer-0.3.1/tests/test_mcp.py +59 -0
  36. gnomancer-0.3.1/tests/test_pipeline.py +78 -0
  37. gnomancer-0.3.1/tests/test_summary.py +28 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gnomancer contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: gnomancer
3
+ Version: 0.3.1
4
+ Summary: Local, private deep dive into a consumer raw DNA file (AncestryDNA, 23andMe, MyHeritage, FTDNA, LivingDNA, VCF): QC, haplogroups, traits, pharmacogenomics, one HTML report.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/codebase/gnomancer
7
+ Project-URL: Issues, https://github.com/codebase/gnomancer/issues
8
+ Keywords: dna,genetics,23andme,ancestrydna,haplogroup,pharmacogenomics,clinvar,mcp
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: snps>=2.9
17
+ Requires-Dist: pandas>=2.0
18
+ Requires-Dist: numpy>=1.24
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=8; extra == "dev"
21
+ Requires-Dist: mcp>=2; extra == "dev"
22
+ Provides-Extra: mcp
23
+ Requires-Dist: mcp>=2; extra == "mcp"
24
+ Dynamic: license-file
25
+
26
+ # gnomancer
27
+
28
+ **https://gnomancer.com**
29
+
30
+ **A private, local deep dive into your raw DNA file.** One command turns an AncestryDNA, 23andMe, MyHeritage,
31
+ FamilyTreeDNA, LivingDNA or VCF export into a readable HTML report: file quality, Y and mitochondrial lineages,
32
+ traits, carrier sites and pharmacogenomics, each with plain-language context and honest limits.
33
+
34
+ Nothing is uploaded. The only network traffic is the one-time download of public tools and reference files.
35
+
36
+ ```bash
37
+ pipx install gnomancer # or: pip install gnomancer (Python 3.10+; Java 11+ for haplogroups and PharmCAT)
38
+ gnomancer setup # downloads the open-source tools and reference files once (~250 MB) into ~/.gnomancer
39
+ gnomancer ~/Downloads/AncestryDNA.zip
40
+ open AncestryDNA_analysis/REPORT.html
41
+ ```
42
+
43
+ No Python yet? `curl -fsSL https://gnomancer.com/install.sh | bash` does all of it on macOS/Linux/WSL.
44
+
45
+ Not sure how to get the file? [gnomancer.com/get-your-data](https://gnomancer.com/get-your-data/) has the steps for every vendor.
46
+ Full install notes, including Windows: [gnomancer.com/install](https://gnomancer.com/install/).
47
+
48
+ **Use it from your AI assistant.** gnomancer is also an MCP server, so Claude Desktop, Claude Code, Cursor and others can run it and answer questions about your report, all locally:
49
+
50
+ ```bash
51
+ claude mcp add gnomancer -- ~/gnomancer/.venv/bin/gnomancer serve # Claude Code
52
+ ```
53
+
54
+ Claude Desktop (`claude_desktop_config.json`):
55
+ ```json
56
+ { "mcpServers": { "gnomancer": { "command": "/Users/you/gnomancer/.venv/bin/gnomancer", "args": ["serve"] } } }
57
+ ```
58
+
59
+ Tools: `analyze`, `summary`, `section`, `lookup`, `narrative_prompt` / `set_narrative`, and (only on request) `clinvar_findings`,
60
+ `pharmacogenomics`. The raw file never leaves the machine; only derived results reach the assistant. The repo also doubles
61
+ as a Claude plugin (`skills/gnomancer/SKILL.md`).
62
+
63
+ ![demo report](docs/demo-report.png)
64
+
65
+ ## What you get
66
+
67
+ `<input>_analysis/`
68
+
69
+ | File | Contents |
70
+ |---|---|
71
+ | `REPORT.html` | The report: self-contained, light/dark, printable, searchable marker tables |
72
+ | `REPORT.md` | The same content as Markdown |
73
+ | `summary.json` | Everything machine-readable |
74
+ | `chromosome-qc.tsv`, `roh.tsv`, `marker-panel.tsv` | Raw tables |
75
+ | `clinvar-hits.tsv`, `pharmcat-calls.tsv` | ClinVar and PharmCAT results |
76
+ | `yhaplo/`, `haplogrep3/`, `pharmcat/` | Raw tool outputs |
77
+
78
+ Report sections:
79
+
80
+ 0. **In plain language / At a glance** – a narrative summary (yours or Claude's) and an automatic digest: who the data says you are, what is worth knowing, what is reassuring, sensible next steps
81
+ 1. **Overview** – vendor, build, sex, lineages, blood group, quality flags
82
+ 2. **Quality** – no-call rate, heterozygosity per chromosome, independent sex inference, contamination flags
83
+ 3. **Runs of homozygosity** – parental relatedness and founder-population signal
84
+ 4. **Lineages** – Y haplogroup via [yhaplo](https://github.com/23andMe/yhaplo) with a second opinion from [Yleaf](https://github.com/genid/Yleaf) on current YFull/FTDNA/ISOGG trees; mtDNA via [Haplogrep3](https://github.com/genepi/haplogrep3); each with population context
85
+ 5. **Multi-site readings** – ABO blood group, lactase persistence, TAS2R38 taster haplotype, APOE (collapsed by default)
86
+ 6. **Marker panel** – ~110 curated sites: ancestry-informative, traits, health, carrier, pharmacogenomics
87
+ 7. **ClinVar scan** – every typed site checked against [ClinVar](https://www.ncbi.nlm.nih.gov/clinvar/); pathogenic alleles listed with a plausibility score (strand-ambiguous and homozygous-rare calls are flagged as probable array artifacts)
88
+ 8. **PharmCAT** – star-allele diplotypes and CPIC phenotypes for 18 pharmacogenes via [PharmCAT](https://github.com/PharmGKB/PharmCAT), with per-gene coverage so you can see how much of each gene the chip actually typed
89
+ 9. **Ancestry estimate** (`--ancestry`) – 1000 Genomes population classification via [ezancestry](https://github.com/arvkevi/ezancestry)
90
+ 10. **Limits** – what a genotyping array cannot tell you
91
+
92
+ Health and carrier sections are collapsed by default so that people who would rather not see, say, their APOE
93
+ genotype are not shown it by accident.
94
+
95
+ ## Vendors and quirks
96
+
97
+ Parsing is delegated to [`snps`](https://github.com/apriha/snps), which detects the vendor from the header and
98
+ normalizes everything. Quirks that are handled:
99
+
100
+ | Vendor | Quirk | Handling |
101
+ |---|---|---|
102
+ | AncestryDNA | CRLF line endings; chromosomes coded 23=X, 24=Y, 25=PAR, 26=MT; two allele columns; no-call `0`; indels `I`/`D`; diploid-looking `GG` on Y and MT | normalized to X/Y/PAR/MT, haploid single letters, NaN no-calls |
103
+ | AncestryDNA | V1 (~700k sites, 2012–2016) and V2 (~665k, 2016+) type different sites | report marks `not on array` per site |
104
+ | 23andMe | single genotype column; `--` no-call; haploid `A`; internal `i3000001` IDs; v3/v4/v5 chips differ (v5 dropped many PGx sites) | handled; missing sites reported |
105
+ | MyHeritage, FamilyTreeDNA | quoted CSV `RSID,CHROMOSOME,POSITION,RESULT` | handled |
106
+ | FamilyTreeDNA (old) | build 36 exports | remapped to 37; mtDNA call skipped (build 36 MT is not rCRS) |
107
+ | LivingDNA, Genes for Good, tellmeGen, Circle | tab/CSV variants | handled |
108
+ | Build 38 exports, VCF | | remapped to GRCh37 (chain file downloaded once) |
109
+ | Any | occasional probes reported on the reverse strand | panel auto-flips unambiguous SNPs and flags A/T and C/G ones |
110
+ | Any | MT numbering is rCRS (NC_012920) on builds 37/38 | diffed against the bundled rCRS |
111
+
112
+ ## The marker panel
113
+
114
+ `gnomancer/data/markers.tsv` is a plain TSV anyone can extend:
115
+
116
+ ```
117
+ rsid category gene label ref effect interp0 interp1 interp2 note
118
+ ```
119
+
120
+ `ref`/`effect` are the two alleles on the **GRCh37 forward strand**. `effect` is the allele that gets counted and
121
+ `interp0/1/2` is shown for 0/1/2 copies. Categories: `ancestry`, `trait`, `health`, `carrier`, `pgx`.
122
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the sourcing rules.
123
+
124
+ Haplogroup context lives in `gnomancer/data/haplogroup_context.tsv`, matched by prefix so a call like `R-M269 / R1b1a2a1a2`
125
+ shows the chain R → R1b → R-M269.
126
+
127
+ ## MCP server
128
+
129
+ `gnomancer serve` speaks MCP over stdio. Registry manifest: `server.json`; publishing steps: `docs/mcp-registry.md`.
130
+
131
+ ## Options
132
+
133
+ ```
134
+ gnomancer FILE [--out DIR] [--markers TSV] [--no-y] [--no-yleaf] [--no-mt] [--no-clinvar] [--no-pharmcat] [--ancestry]
135
+ [--summary summary.md] [--ai]
136
+ ```
137
+
138
+ **Plain-language summary.** The report opens with an automatic "at a glance" block built from the results. For a narrative on
139
+ top of that, every run also writes `summary-prompt.md`: paste it to whatever AI assistant you use (Claude, ChatGPT, a local
140
+ model), save the answer as `summary.md`, and re-run with `--summary summary.md`. Or pass `--ai` to have Claude write it
141
+ directly (`pip install anthropic`, credentials via `ANTHROPIC_API_KEY` or `ant auth login`). `--ai` is the one path that sends
142
+ anything off your machine: the structured results in `summary-prompt.md`, never the raw genotype file.
143
+
144
+ `--ancestry` runs [ezancestry](https://github.com/arvkevi/ezancestry) for a 1000 Genomes population classification.
145
+ ezancestry and Yleaf need Python ≤3.11; `setup.sh` builds a side venv (`.venv-ancestry`) for both automatically when
146
+ `python3.11` is on the PATH, and the CLI uses it when present. First runs download reference data (ezancestry models;
147
+ Yleaf's hg19 reference, ~900 MB).
148
+
149
+ ## Tools used under the hood
150
+
151
+ | Tool | What it contributes | Source |
152
+ |---|---|---|
153
+ | `snps` | vendor detection, normalization, build remapping | github.com/apriha/snps |
154
+ | yhaplo | Y haplogroup (ISOGG 2016 tree) | github.com/23andMe/yhaplo |
155
+ | Haplogrep3 + PhyloTree 17 | mtDNA haplogroup | github.com/genepi/haplogrep3 |
156
+ | ClinVar | clinically classified variants at every typed site | ncbi.nlm.nih.gov/clinvar |
157
+ | PharmCAT | CPIC/PharmGKB star-allele calling | github.com/PharmGKB/PharmCAT |
158
+ | ezancestry | 1000 Genomes population classification | github.com/arvkevi/ezancestry |
159
+ | Yleaf | second-opinion Y haplogroup on current YFull / FTDNA / ISOGG trees | github.com/genid/Yleaf |
160
+ | rCRS (NC_012920.1) | mitochondrial reference | NCBI |
161
+
162
+ Evaluated and not used (yet): PLINK 2 (nothing an array file needs that is not already here), ADMIXTURE (needs a multi-GB reference panel),
163
+ pgsc_calc polygenic scores (arrays need imputation first), SNPedia (non-commercial license, no bulk use).
164
+
165
+ ## Try it without your own data
166
+
167
+ ```bash
168
+ .venv/bin/python examples/make_demo.py demo # synthetic AncestryDNA / 23andMe / MyHeritage files
169
+ .venv/bin/gnomancer demo/demo_AncestryDNA.txt
170
+ ```
171
+
172
+ ## Limits, stated plainly
173
+
174
+ - A genotyping array covers roughly 0.02% of the genome at pre-chosen common sites. It is not a sequence.
175
+ - Negative at a site ≠ gene is normal. BRCA1/2, CFTR and similar are covered at a handful of sites only.
176
+ - Positive at a rare site is often wrong: 40% of variants from consumer raw data were false positives when a clinical
177
+ lab re-tested them ([Tandy-Connor et al. 2018](https://doi.org/10.1038/gim.2018.38)). The ClinVar section scores each hit for plausibility for this reason.
178
+ - CYP2D6 copy number, full HLA typing and rare variants are invisible.
179
+ - Y and mtDNA resolution stops at the few thousand / few hundred sites typed.
180
+ - Admixture percentages need a reference panel; the vendor's own estimate is the easy source.
181
+ - **This is not medical advice.** Confirm anything that matters with a clinical-grade test and a clinician or genetic counselor.
182
+
183
+ ## Website
184
+
185
+ `site/` is the landing page at https://gnomancer.com, served as a Cloudflare Worker with static assets (`wrangler.toml`).
186
+ Deploy with `npx wrangler deploy` (Node 22+, `wrangler login` once). It is static: no cookies, no analytics.
187
+
188
+ ## Development
189
+
190
+ ```bash
191
+ ./setup.sh # venv, editable install, tools
192
+ .venv/bin/pytest
193
+ python -m build # sdist + wheel; releases publish to PyPI from GitHub Actions (trusted publishing)
194
+ ```
195
+
196
+ CI runs the tests and builds a demo report on every push. Real raw-data files and `*_analysis/` folders are
197
+ git-ignored; never commit them.
198
+
199
+ ## Related tools
200
+
201
+ Local: [`apriha/lineage`](https://github.com/apriha/lineage) (shared segments between kits),
202
+ [PLINK 2](https://github.com/chrchang/plink-ng), ADMIXTURE + 1000 Genomes, [SNPedia](https://www.snpedia.com).
203
+ Web (uploads your data): GEDmatch, James Lick's mthap, Morley's Y-SNP predictor, Promethease, Genetic Genie.
204
+
205
+ ## License
206
+
207
+ MIT. Haplogroup tools and references have their own licenses: yhaplo (23andMe), Haplogrep3 (GENEPI Innsbruck),
208
+ PhyloTree, ISOGG.
209
+
210
+ <!-- registry ownership proof -->
211
+ mcp-name: io.github.codebase/gnomancer
@@ -0,0 +1,186 @@
1
+ # gnomancer
2
+
3
+ **https://gnomancer.com**
4
+
5
+ **A private, local deep dive into your raw DNA file.** One command turns an AncestryDNA, 23andMe, MyHeritage,
6
+ FamilyTreeDNA, LivingDNA or VCF export into a readable HTML report: file quality, Y and mitochondrial lineages,
7
+ traits, carrier sites and pharmacogenomics, each with plain-language context and honest limits.
8
+
9
+ Nothing is uploaded. The only network traffic is the one-time download of public tools and reference files.
10
+
11
+ ```bash
12
+ pipx install gnomancer # or: pip install gnomancer (Python 3.10+; Java 11+ for haplogroups and PharmCAT)
13
+ gnomancer setup # downloads the open-source tools and reference files once (~250 MB) into ~/.gnomancer
14
+ gnomancer ~/Downloads/AncestryDNA.zip
15
+ open AncestryDNA_analysis/REPORT.html
16
+ ```
17
+
18
+ No Python yet? `curl -fsSL https://gnomancer.com/install.sh | bash` does all of it on macOS/Linux/WSL.
19
+
20
+ Not sure how to get the file? [gnomancer.com/get-your-data](https://gnomancer.com/get-your-data/) has the steps for every vendor.
21
+ Full install notes, including Windows: [gnomancer.com/install](https://gnomancer.com/install/).
22
+
23
+ **Use it from your AI assistant.** gnomancer is also an MCP server, so Claude Desktop, Claude Code, Cursor and others can run it and answer questions about your report, all locally:
24
+
25
+ ```bash
26
+ claude mcp add gnomancer -- ~/gnomancer/.venv/bin/gnomancer serve # Claude Code
27
+ ```
28
+
29
+ Claude Desktop (`claude_desktop_config.json`):
30
+ ```json
31
+ { "mcpServers": { "gnomancer": { "command": "/Users/you/gnomancer/.venv/bin/gnomancer", "args": ["serve"] } } }
32
+ ```
33
+
34
+ Tools: `analyze`, `summary`, `section`, `lookup`, `narrative_prompt` / `set_narrative`, and (only on request) `clinvar_findings`,
35
+ `pharmacogenomics`. The raw file never leaves the machine; only derived results reach the assistant. The repo also doubles
36
+ as a Claude plugin (`skills/gnomancer/SKILL.md`).
37
+
38
+ ![demo report](docs/demo-report.png)
39
+
40
+ ## What you get
41
+
42
+ `<input>_analysis/`
43
+
44
+ | File | Contents |
45
+ |---|---|
46
+ | `REPORT.html` | The report: self-contained, light/dark, printable, searchable marker tables |
47
+ | `REPORT.md` | The same content as Markdown |
48
+ | `summary.json` | Everything machine-readable |
49
+ | `chromosome-qc.tsv`, `roh.tsv`, `marker-panel.tsv` | Raw tables |
50
+ | `clinvar-hits.tsv`, `pharmcat-calls.tsv` | ClinVar and PharmCAT results |
51
+ | `yhaplo/`, `haplogrep3/`, `pharmcat/` | Raw tool outputs |
52
+
53
+ Report sections:
54
+
55
+ 0. **In plain language / At a glance** – a narrative summary (yours or Claude's) and an automatic digest: who the data says you are, what is worth knowing, what is reassuring, sensible next steps
56
+ 1. **Overview** – vendor, build, sex, lineages, blood group, quality flags
57
+ 2. **Quality** – no-call rate, heterozygosity per chromosome, independent sex inference, contamination flags
58
+ 3. **Runs of homozygosity** – parental relatedness and founder-population signal
59
+ 4. **Lineages** – Y haplogroup via [yhaplo](https://github.com/23andMe/yhaplo) with a second opinion from [Yleaf](https://github.com/genid/Yleaf) on current YFull/FTDNA/ISOGG trees; mtDNA via [Haplogrep3](https://github.com/genepi/haplogrep3); each with population context
60
+ 5. **Multi-site readings** – ABO blood group, lactase persistence, TAS2R38 taster haplotype, APOE (collapsed by default)
61
+ 6. **Marker panel** – ~110 curated sites: ancestry-informative, traits, health, carrier, pharmacogenomics
62
+ 7. **ClinVar scan** – every typed site checked against [ClinVar](https://www.ncbi.nlm.nih.gov/clinvar/); pathogenic alleles listed with a plausibility score (strand-ambiguous and homozygous-rare calls are flagged as probable array artifacts)
63
+ 8. **PharmCAT** – star-allele diplotypes and CPIC phenotypes for 18 pharmacogenes via [PharmCAT](https://github.com/PharmGKB/PharmCAT), with per-gene coverage so you can see how much of each gene the chip actually typed
64
+ 9. **Ancestry estimate** (`--ancestry`) – 1000 Genomes population classification via [ezancestry](https://github.com/arvkevi/ezancestry)
65
+ 10. **Limits** – what a genotyping array cannot tell you
66
+
67
+ Health and carrier sections are collapsed by default so that people who would rather not see, say, their APOE
68
+ genotype are not shown it by accident.
69
+
70
+ ## Vendors and quirks
71
+
72
+ Parsing is delegated to [`snps`](https://github.com/apriha/snps), which detects the vendor from the header and
73
+ normalizes everything. Quirks that are handled:
74
+
75
+ | Vendor | Quirk | Handling |
76
+ |---|---|---|
77
+ | AncestryDNA | CRLF line endings; chromosomes coded 23=X, 24=Y, 25=PAR, 26=MT; two allele columns; no-call `0`; indels `I`/`D`; diploid-looking `GG` on Y and MT | normalized to X/Y/PAR/MT, haploid single letters, NaN no-calls |
78
+ | AncestryDNA | V1 (~700k sites, 2012–2016) and V2 (~665k, 2016+) type different sites | report marks `not on array` per site |
79
+ | 23andMe | single genotype column; `--` no-call; haploid `A`; internal `i3000001` IDs; v3/v4/v5 chips differ (v5 dropped many PGx sites) | handled; missing sites reported |
80
+ | MyHeritage, FamilyTreeDNA | quoted CSV `RSID,CHROMOSOME,POSITION,RESULT` | handled |
81
+ | FamilyTreeDNA (old) | build 36 exports | remapped to 37; mtDNA call skipped (build 36 MT is not rCRS) |
82
+ | LivingDNA, Genes for Good, tellmeGen, Circle | tab/CSV variants | handled |
83
+ | Build 38 exports, VCF | | remapped to GRCh37 (chain file downloaded once) |
84
+ | Any | occasional probes reported on the reverse strand | panel auto-flips unambiguous SNPs and flags A/T and C/G ones |
85
+ | Any | MT numbering is rCRS (NC_012920) on builds 37/38 | diffed against the bundled rCRS |
86
+
87
+ ## The marker panel
88
+
89
+ `gnomancer/data/markers.tsv` is a plain TSV anyone can extend:
90
+
91
+ ```
92
+ rsid category gene label ref effect interp0 interp1 interp2 note
93
+ ```
94
+
95
+ `ref`/`effect` are the two alleles on the **GRCh37 forward strand**. `effect` is the allele that gets counted and
96
+ `interp0/1/2` is shown for 0/1/2 copies. Categories: `ancestry`, `trait`, `health`, `carrier`, `pgx`.
97
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the sourcing rules.
98
+
99
+ Haplogroup context lives in `gnomancer/data/haplogroup_context.tsv`, matched by prefix so a call like `R-M269 / R1b1a2a1a2`
100
+ shows the chain R → R1b → R-M269.
101
+
102
+ ## MCP server
103
+
104
+ `gnomancer serve` speaks MCP over stdio. Registry manifest: `server.json`; publishing steps: `docs/mcp-registry.md`.
105
+
106
+ ## Options
107
+
108
+ ```
109
+ gnomancer FILE [--out DIR] [--markers TSV] [--no-y] [--no-yleaf] [--no-mt] [--no-clinvar] [--no-pharmcat] [--ancestry]
110
+ [--summary summary.md] [--ai]
111
+ ```
112
+
113
+ **Plain-language summary.** The report opens with an automatic "at a glance" block built from the results. For a narrative on
114
+ top of that, every run also writes `summary-prompt.md`: paste it to whatever AI assistant you use (Claude, ChatGPT, a local
115
+ model), save the answer as `summary.md`, and re-run with `--summary summary.md`. Or pass `--ai` to have Claude write it
116
+ directly (`pip install anthropic`, credentials via `ANTHROPIC_API_KEY` or `ant auth login`). `--ai` is the one path that sends
117
+ anything off your machine: the structured results in `summary-prompt.md`, never the raw genotype file.
118
+
119
+ `--ancestry` runs [ezancestry](https://github.com/arvkevi/ezancestry) for a 1000 Genomes population classification.
120
+ ezancestry and Yleaf need Python ≤3.11; `setup.sh` builds a side venv (`.venv-ancestry`) for both automatically when
121
+ `python3.11` is on the PATH, and the CLI uses it when present. First runs download reference data (ezancestry models;
122
+ Yleaf's hg19 reference, ~900 MB).
123
+
124
+ ## Tools used under the hood
125
+
126
+ | Tool | What it contributes | Source |
127
+ |---|---|---|
128
+ | `snps` | vendor detection, normalization, build remapping | github.com/apriha/snps |
129
+ | yhaplo | Y haplogroup (ISOGG 2016 tree) | github.com/23andMe/yhaplo |
130
+ | Haplogrep3 + PhyloTree 17 | mtDNA haplogroup | github.com/genepi/haplogrep3 |
131
+ | ClinVar | clinically classified variants at every typed site | ncbi.nlm.nih.gov/clinvar |
132
+ | PharmCAT | CPIC/PharmGKB star-allele calling | github.com/PharmGKB/PharmCAT |
133
+ | ezancestry | 1000 Genomes population classification | github.com/arvkevi/ezancestry |
134
+ | Yleaf | second-opinion Y haplogroup on current YFull / FTDNA / ISOGG trees | github.com/genid/Yleaf |
135
+ | rCRS (NC_012920.1) | mitochondrial reference | NCBI |
136
+
137
+ Evaluated and not used (yet): PLINK 2 (nothing an array file needs that is not already here), ADMIXTURE (needs a multi-GB reference panel),
138
+ pgsc_calc polygenic scores (arrays need imputation first), SNPedia (non-commercial license, no bulk use).
139
+
140
+ ## Try it without your own data
141
+
142
+ ```bash
143
+ .venv/bin/python examples/make_demo.py demo # synthetic AncestryDNA / 23andMe / MyHeritage files
144
+ .venv/bin/gnomancer demo/demo_AncestryDNA.txt
145
+ ```
146
+
147
+ ## Limits, stated plainly
148
+
149
+ - A genotyping array covers roughly 0.02% of the genome at pre-chosen common sites. It is not a sequence.
150
+ - Negative at a site ≠ gene is normal. BRCA1/2, CFTR and similar are covered at a handful of sites only.
151
+ - Positive at a rare site is often wrong: 40% of variants from consumer raw data were false positives when a clinical
152
+ lab re-tested them ([Tandy-Connor et al. 2018](https://doi.org/10.1038/gim.2018.38)). The ClinVar section scores each hit for plausibility for this reason.
153
+ - CYP2D6 copy number, full HLA typing and rare variants are invisible.
154
+ - Y and mtDNA resolution stops at the few thousand / few hundred sites typed.
155
+ - Admixture percentages need a reference panel; the vendor's own estimate is the easy source.
156
+ - **This is not medical advice.** Confirm anything that matters with a clinical-grade test and a clinician or genetic counselor.
157
+
158
+ ## Website
159
+
160
+ `site/` is the landing page at https://gnomancer.com, served as a Cloudflare Worker with static assets (`wrangler.toml`).
161
+ Deploy with `npx wrangler deploy` (Node 22+, `wrangler login` once). It is static: no cookies, no analytics.
162
+
163
+ ## Development
164
+
165
+ ```bash
166
+ ./setup.sh # venv, editable install, tools
167
+ .venv/bin/pytest
168
+ python -m build # sdist + wheel; releases publish to PyPI from GitHub Actions (trusted publishing)
169
+ ```
170
+
171
+ CI runs the tests and builds a demo report on every push. Real raw-data files and `*_analysis/` folders are
172
+ git-ignored; never commit them.
173
+
174
+ ## Related tools
175
+
176
+ Local: [`apriha/lineage`](https://github.com/apriha/lineage) (shared segments between kits),
177
+ [PLINK 2](https://github.com/chrchang/plink-ng), ADMIXTURE + 1000 Genomes, [SNPedia](https://www.snpedia.com).
178
+ Web (uploads your data): GEDmatch, James Lick's mthap, Morley's Y-SNP predictor, Promethease, Genetic Genie.
179
+
180
+ ## License
181
+
182
+ MIT. Haplogroup tools and references have their own licenses: yhaplo (23andMe), Haplogrep3 (GENEPI Innsbruck),
183
+ PhyloTree, ISOGG.
184
+
185
+ <!-- registry ownership proof -->
186
+ mcp-name: io.github.codebase/gnomancer
@@ -0,0 +1,2 @@
1
+ """gnomancer: vendor-agnostic analysis of consumer raw DNA files."""
2
+ __version__ = "0.3.1"
@@ -0,0 +1,2 @@
1
+ from .cli import main
2
+ main()
@@ -0,0 +1,155 @@
1
+ """Command-line entry point: python -m gnomancer <raw file> [--out DIR]"""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from . import __version__
10
+ from .load import load
11
+ from .qc import chromosome_table, summary
12
+ from .roh import find_roh, interpret
13
+ from .panel import load_markers, evaluate, combos
14
+ from .haplogroups import call_y, call_mt
15
+ from .report import write
16
+ from . import clinvar as clinvar_mod
17
+ from . import pharmcat as pharmcat_mod
18
+ from . import yleaf as yleaf_mod
19
+ from . import summary as summary_mod
20
+ from .report_html import write_html
21
+
22
+ from .paths import DATA, ancestry_python
23
+
24
+
25
+ def run_ezancestry(inp: Path, loaded) -> dict | None:
26
+ """Run ezancestry in a separate interpreter (it needs Python <=3.11) if .venv-ancestry exists, else in-process."""
27
+ import json as _json
28
+ import subprocess
29
+ code = ("import json,sys\nfrom snps import SNPs\nr=SNPs(sys.argv[1]).predict_ancestry()\n"
30
+ "print(json.dumps({k:v for k,v in r.items() if k!='ezancestry_df'}, default=str))")
31
+ venv_py = ancestry_python()
32
+ py = str(venv_py) if venv_py else sys.executable
33
+ try:
34
+ p = subprocess.run([py, "-c", code, str(inp)], capture_output=True, text=True, timeout=1800)
35
+ if p.returncode != 0:
36
+ raise RuntimeError(p.stderr.strip().splitlines()[-1] if p.stderr.strip() else "unknown error")
37
+ return _json.loads(p.stdout.strip().splitlines()[-1])
38
+ except Exception as e: # noqa: BLE001
39
+ loaded.notes.append(f"ezancestry failed: {e}")
40
+ return None
41
+
42
+
43
+ def main(argv=None):
44
+ argv = list(sys.argv[1:] if argv is None else argv)
45
+ if argv[:1] == ["serve"]:
46
+ from .mcp_server import main as serve
47
+ return serve()
48
+ if argv[:1] == ["setup"]:
49
+ from .setup_cmd import run as setup
50
+ return setup(argv[1:])
51
+ ap = argparse.ArgumentParser(prog="gnomancer", description="Analyze a consumer raw DNA file locally. Subcommands: `gnomancer setup` (download tools), `gnomancer serve` (MCP server).")
52
+ ap.add_argument("input", help="raw data file (.txt/.csv/.tsv/.vcf, optionally .zip/.gz)")
53
+ ap.add_argument("--out", default=None, help="output directory (default: <input>_analysis)")
54
+ ap.add_argument("--markers", default=str(DATA / "markers.tsv"), help="marker panel TSV")
55
+ ap.add_argument("--no-y", action="store_true", help="skip Y haplogroup")
56
+ ap.add_argument("--no-mt", action="store_true", help="skip mtDNA haplogroup")
57
+ ap.add_argument("--ancestry", action="store_true", help="run ezancestry population prediction (uses .venv-ancestry if present)")
58
+ ap.add_argument("--no-clinvar", action="store_true", help="skip the ClinVar scan")
59
+ ap.add_argument("--no-yleaf", action="store_true", help="skip the Yleaf second-opinion Y call")
60
+ ap.add_argument("--no-pharmcat", action="store_true", help="skip PharmCAT")
61
+ ap.add_argument("--summary", default=None, help="Markdown/text file with a narrative summary (e.g. written by your AI agent) to show at the top of the report")
62
+ ap.add_argument("--ai", action="store_true", help="have Claude write the narrative summary (sends the structured results, not the raw file, to the Anthropic API; needs credentials)")
63
+ ap.add_argument("--version", action="version", version=__version__)
64
+ a = ap.parse_args(argv)
65
+ run_args = argv
66
+
67
+ inp = Path(a.input)
68
+ out = Path(a.out) if a.out else inp.with_name(inp.stem.replace(".", "_") + "_analysis")
69
+ out.mkdir(parents=True, exist_ok=True)
70
+
71
+ print(f"[1/6] loading {inp} ...", file=sys.stderr)
72
+ loaded = load(str(inp))
73
+ df = loaded.df
74
+ print(f" {loaded.source}, build {loaded.build}, {loaded.count:,} SNPs, sex {loaded.sex or '?'}", file=sys.stderr)
75
+
76
+ print("[2/6] quality metrics ...", file=sys.stderr)
77
+ ct = chromosome_table(df)
78
+ qc = summary(df, ct, loaded.sex)
79
+ ct.to_csv(out / "chromosome-qc.tsv", sep="\t", index=False)
80
+
81
+ print("[3/6] runs of homozygosity ...", file=sys.stderr)
82
+ roh = find_roh(df)
83
+ roh.to_csv(out / "roh.tsv", sep="\t", index=False)
84
+ roh_text = interpret(roh)
85
+
86
+ print("[4/6] marker panel ...", file=sys.stderr)
87
+ markers = load_markers(Path(a.markers))
88
+ panel = evaluate(df, markers)
89
+ panel.to_csv(out / "marker-panel.tsv", sep="\t", index=False)
90
+ combo = combos(df)
91
+
92
+ is_male = qc["sex_inferred"] == "Male"
93
+ y = dict(error="skipped (sample is not male)" if not is_male else "skipped (--no-y)")
94
+ if is_male and not a.no_y:
95
+ print("[5/6] Y haplogroup (yhaplo) ...", file=sys.stderr)
96
+ y = call_y(df, out)
97
+ if not a.no_yleaf:
98
+ print(" second opinion on current trees (Yleaf) ...", file=sys.stderr)
99
+ y["yleaf"] = yleaf_mod.run(df, out)
100
+ mt = dict(error="skipped (--no-mt)")
101
+ if not a.no_mt:
102
+ print("[5/6] mtDNA haplogroup (haplogrep3) ...", file=sys.stderr)
103
+ mt = call_mt(df, out, loaded.build_original)
104
+
105
+ cv = dict(available=False, hits=None, note="skipped (--no-clinvar)")
106
+ if not a.no_clinvar:
107
+ print("[6/8] ClinVar scan of every typed site ...", file=sys.stderr)
108
+ cv = clinvar_mod.scan(df)
109
+ if cv["available"] and cv["hits"] is not None and not cv["hits"].empty:
110
+ cv["hits"].to_csv(out / "clinvar-hits.tsv", sep="\t", index=False)
111
+ cv_text = clinvar_mod.interpret(cv) if not a.no_clinvar else cv["note"]
112
+ pc = dict(available=False, calls=None, error="skipped (--no-pharmcat)", stats={})
113
+ if not a.no_pharmcat:
114
+ print("[7/8] PharmCAT pharmacogenomics ...", file=sys.stderr)
115
+ pc = pharmcat_mod.run(df, out)
116
+ if pc.get("calls") is not None and not pc["calls"].empty:
117
+ pc["calls"].to_csv(out / "pharmcat-calls.tsv", sep="\t", index=False)
118
+ pc_text = pharmcat_mod.interpret(pc) if not a.no_pharmcat else pc["error"]
119
+ ancestry = None
120
+ if a.ancestry:
121
+ print("[8/8] ancestry prediction (ezancestry) ...", file=sys.stderr)
122
+ ancestry = run_ezancestry(inp, loaded)
123
+
124
+ glance = summary_mod.at_a_glance(loaded, qc, roh, roh_text, combo, y, mt, ancestry, panel, cv, pc)
125
+ prompt = summary_mod.build_prompt(glance, combo, y, mt, ancestry, panel, cv, pc, qc)
126
+ summary_mod.write_prompt_file(out, prompt)
127
+ narrative = None
128
+ if a.summary:
129
+ narrative = Path(a.summary).read_text().strip()
130
+ elif a.ai:
131
+ print(" narrative summary (Claude) ...", file=sys.stderr)
132
+ try:
133
+ narrative = summary_mod.ai_summary(prompt)
134
+ (out / "summary.md").write_text(narrative + "\n")
135
+ except Exception as e: # noqa: BLE001
136
+ loaded.notes.append(f"AI summary failed: {e}")
137
+ extra = dict(clinvar=cv, clinvar_text=cv_text, pharmcat=pc, pharmcat_text=pc_text, glance=glance, narrative=narrative)
138
+ rep = write(out / "REPORT.md", loaded, qc, ct, roh, roh_text, panel, combo, y, mt, ancestry, extra)
139
+ html = write_html(out / "REPORT.html", loaded, qc, ct, roh, roh_text, panel, combo, y, mt, ancestry, input_name=inp.name, extra=extra)
140
+ json.dump(dict(input=str(inp.resolve()), args=[str(Path(x).resolve()) if Path(x).exists() else x for x in run_args],
141
+ report_html=str((out / "REPORT.html").resolve()), report_md=str((out / "REPORT.md").resolve()), glance=glance,
142
+ source=loaded.source, build=loaded.build, count=loaded.count, qc=qc,
143
+ roh_total_mb=float(roh.length_mb.sum()) if not roh.empty else 0.0, y=y, mt=mt,
144
+ combos=combo, ancestry=ancestry, notes=loaded.notes,
145
+ clinvar=dict(sites_in_clinvar=cv.get("sites_in_clinvar"), pathogenic_sites_typed=cv.get("pathogenic_sites_typed"),
146
+ hits=(cv["hits"].to_dict("records") if cv.get("hits") is not None and not cv["hits"].empty else [])),
147
+ pharmcat=dict(stats={k: v for k, v in pc.get("stats", {}).items() if k != "genes"}, error=pc.get("error"),
148
+ calls=(pc["calls"].to_dict("records") if pc.get("calls") is not None and not pc["calls"].empty else []))),
149
+ open(out / "summary.json", "w"), indent=1, default=str)
150
+ print(f"\nReport: {html}\n {rep}", file=sys.stderr)
151
+ if y.get("haplogroup"):
152
+ print(f"Y: {y['haplogroup']} ", end="", file=sys.stderr)
153
+ if mt.get("haplogroup"):
154
+ print(f"MT: {mt['haplogroup']}", end="", file=sys.stderr)
155
+ print(file=sys.stderr)