> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirobody.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 基因数据

> 原始基因型上传如何变成位点检出结果和 CPIC 药物-基因覆盖情况，以及这两个工具有意保留的边界。

export const OssSource = ({path, lang = "en"}) => {
  const href = "https://github.com/thetahealth/mirobody/blob/c1aae297b4f3fad5cb87c5fc5bffc5163f7758c9/" + path;
  return <p className="text-sm text-gray-500 dark:text-gray-400">
      {lang === "zh" ? "对应 mirobody " : "For mirobody "}
      <code>1.5.3</code>
      {lang === "zh" ? " · 源文件 " : " · source "}
      <a href={href}>
        <code>{path}</code>
      </a>
    </p>;
};

<OssSource path="docs/genetics.zh-CN.md" lang="zh" />

Mirobody 把上传的原始基因型文件当作事实保存：每个位点一条检出结果，并记下得出
这条结果时，对参考基因组版本、等位基因和链方向做过哪些核对。读这些事实的工具有两个：
`query_genetic_data` 按位点、基因或染色体区间查，`query_pharmacogenomics` 查
CPIC 的药物–基因覆盖情况。范围是有意收窄的：

* **位点。** 随包的位点索引收录十二个药物基因区域里的 489 个公开 dbSNP 155
  Common SNV，并映射到两个参考基因组版本。上传文件里的其余行原样保存，标为
  `unresolved`：按 rsID 或原始坐标仍然查得到，但索引核对不了的，就不会给出基因型。
* **药物。** 回答会说明 CPIC 定义位点里哪些已检出、哪些缺失、哪些未检出，表型
  一律返回 `not_determined`。判定星号等位基因（star allele）需要相位、拷贝数和
  全部定义位点，而芯片导出文件没法可靠地提供其中任何一项。
* **疾病。** 罕见致病位点的检出结果，永远不会被转成疾病结论。

基因型没有测量时间、单位，也谈不上趋势。原始基因型上传走单独的一套存储和上面这
两个工具；基因检测**报告**仍然走观测值（observation）流程。位点缺失也好，未检出
（no-call）也好，都不等于结果正常。

<h2 id="统一的表示方式">
  统一的表示方式
</h2>

不同厂商的消费级导出文件，解析之后都落到同一种位点／检出结构上：**已知时的 dbSNP
rsID、染色体、核实过的 GRCh37/GRCh38 坐标、REF/ALT、VCF 风格的 GT、检出状态、
合子状态（zygosity）、可选的 HGNC 基因符号，以及上传时的原始写法和来源**。
REF/ALT/GT 的定义来自 [VCF 规范](https://github.com/samtools/hts-specs)；
[dbSNP](https://www.ncbi.nlm.nih.gov/snp/) 标识 RefSNP 位点；
[HGNC](https://hgnc.genenames.org/) 负责人类基因命名。芯片给出的一对等位基因，
只有在位点索引支持它的参考基因组版本、坐标和链方向时，才会变成 GT。VCF 可以自带
REF/ALT/GT，但这并不意味着位点索引核实过整个文件。

这和主诉用 ICPC-3、检验指标用 LOINC 是同一个目的：让数据能互通。不同的是，基因型
没有一个对等的单一编码，它的身份还取决于参考基因组版本和等位基因。导出成
[FHIR Genomics Reporting STU3](https://hl7.org/fhir/uv/genomics-reporting/STU3/index.html)
时，映射好的变异信息以 LOINC 观测码和组件码承载。导出的是一个有边界的 Variant
Observation 集合，并不声称符合这份实施指南里的每一个 profile。

随 wheel 一起发布了一小套取自公开数据的[开发者样例](https://github.com/thetahealth/mirobody/blob/c1aae297b4f3fad5cb87c5fc5bffc5163f7758c9/mirobody/testing/genomics/README.md)：
12 个基因上的 13 个核实过的检出结果，写成五种厂商的文本／CSV 格式，以及 GRCh37、
GRCh38 两版 VCF 和它的 gzip、BGZF、ZIP 版本，另附一个公开的未检出样例和一个女性
X 染色体样例。`canonical.json` 是解析后应当得到的统一结果，`manifest.json` 钉住了
每个文件和它的来源。参与者的原始导出文件和全基因组文件都不进包。

<h2 id="上传与存储">
  上传与存储
</h2>

基因文件处理器靠列表头认文件，不看厂商写在文件开头的说明文字。它接受 WeGene、
23andMe 的单列基因型，Ancestry 的两列等位基因，MyHeritage、FTDNA 的 CSV，还有
几种公开 `snps` 测试样例里的格式，以及 VCF、gzip、BGZF 和 ZIP。一个 ZIP 里可以放
一份 VCF，外加有大小上限的 BED/TXT 附带文件；里面要是有多个基因型文件，就分不清
该用哪个，会被拒绝。多样本 VCF 也会被拒绝，因为上传时没有任何信息能说明哪个样本
是这个人。Illumina TOP 链的检出结果，没有对应的芯片清单（manifest）时保持
`unresolved`。只在正文里提到 rsID 的文件，仍然留在文档流程里。VCF 里非标准的
contig 名称按原始染色体文本保存；规范区间查询和 VCF 导出会跳过它们，不会凭空做
坐标转换。

解析器把 Ancestry 的 23/24/25/26 映射成 X/Y/PAR/MT，把 FTDNA 的 0/XY 映射成 PAR。
`--`、`00` 和 VCF 里缺失的等位基因都记为 `no_call`，原始写法照样保留。每一行都对照
一份带版本号的只读 dbSNP 索引做标准化：染色体和坐标必须对得上某个已知的参考基因组
版本，REF/ALT 和链方向也必须支持这条检出，才会赋予 VCF GT。有歧义或相互矛盾的
检出记为 `unresolved`。导入时，文件声明的参考基因组版本，和根据位点匹配推断出来的
版本分开记录。`unknown` 是合法结果：参与投票的位点少于 20 个，或者没有哪个版本
拿到 90% 的票，都记为 `unknown`，所以随包的 13 位点样例总是得到这个结果。用 X/Y
推断性别至少需要 1,000 个 X 检出；过了这个门槛，女性 Y 染色体上的未检出才会记为
`not_applicable`。推断出性别之后，男性非 PAR 区 X/Y 上两个等位基因不同的检出记为
`unresolved / haploid_conflict`；PAR 位置不确定时，多等位基因的 GT 记为
`unresolved / par_unknown`。PAR 区的检出仍按二倍体处理。生效集合的 `n_called`
在这些修正之后、在激活事务内部统计。

上传先写进一个 `loading` 状态的基因型集合。只有所有批次都成功、存下的行数和解析
出的行数对得上，才用一个事务替换掉之前的生效集合。哪个批次失败了，旧集合照样能查。
数据库保证每人只有一个生效集合、每个集合里每个 rsID 只有一行。重新上传是替换生效
集合，不会再多存一份。随包网页端的"数据"页会显示上传状态和生效集合的统计数字。

<h3 id="从-151-升级">
  从 1.5.1 升级
</h3>

新工具不读旧的 `th_series_data_genetic` 表。升级后的部署，在执行 `32_genomics.sql`
之后（或者以 `BOOTSTRAP_SCHEMA=true` 启动之后）运行 `mirobody migrate-genotypes`。
这个命令给每个人挑出最近的一份旧文件来源，每个 rsID 留最新的一行，并且只在这个人
还没有生效集合时才创建一个。重复运行是安全的。`--user` 只处理一个人，
`--max-users` 限制一次处理的人数。旧数据行会保留，方便回滚或审计。迁移过来的检出
保留原始值，但 `build_detected=unknown`、没有 GT、`call_status=unresolved`，明确的
未检出除外。想要标准化的结果，请本人重新上传原始导出文件；迁移永远不会去猜参考
等位基因或链方向。

<h2 id="随包的位点索引">
  随包的位点索引
</h2>

`mirobody/res/genomics/genotype_sites.sqlite3` 收录十二个药物基因区域里的 489 个
公开 dbSNP 155 Common SNV，它们在 GRCh37 和 GRCh38 上的染色体和 REF/ALT 都一致。
文件 86,016 字节，随 wheel 和 sdist 一起发布。它的版本标签是
`dbsnp-b155-common-pgx-candidate`：前半截是构建脚本的发布名，后缀是构建时的输入
范围。`candidate` 表示位点是从公开标记列表和 CPIC 定义位点里挑出来的；单位点的
解析样例用的是 `sample`。每次上传都会把这个标签记成自己的 `site_table_version`。
[NOTICE](https://github.com/thetahealth/mirobody/blob/c1aae297b4f3fad5cb87c5fc5bffc5163f7758c9/mirobody/res/genomics/genotype_sites.NOTICE) 记录了来源 URL 和哈希，
[构建脚本](https://github.com/thetahealth/mirobody/blob/c1aae297b4f3fad5cb87c5fc5bffc5163f7758c9/benchmarks/genomics/README.md)能用钉住的、校验过哈希的输入重建出
这个文件。

这些位点是在十二个区域里，按两份公开 PGP 导出文件和 CPIC 定义表里出现的 rsID
挑出来的。对照这几份来源，索引覆盖了 PGP 23andMe v5 导出 625,705 个 rsID 里的
262 个、PGP AncestryDNA v2 导出 677,436 个里的 340 个，以及 CPIC 定义表 992 个
rsID 里的 41 个。这两份导出是实际观察到的标记集合，不是厂商的芯片清单；这些比例
是一个十二区域索引该有的比例，不能拿来描述消费级芯片的覆盖率。索引里没有 WeGene/GSA
的标记集、indel 定义、合并 rsID 的历史，也没有罕见变异面板。索引之外的芯片行，只以
原始的 `unresolved` 行出现，不会带 GT。按基因查询只能查到带 CPIC 基因标签的那
41 个位点。

索引映射不了的行，也会完整保留。公开的 Ancestry v2 导出，27,206 行 X/Y/PAR/MT
全部保存（25,242/1,665/36/263），PAR 查询用显式的原始坐标模式能找到它们，但不声称
属于哪个参考基因组版本。VCF 上传自带 REF/ALT/GT，所以全基因组 VCF 会把列出的检出
全部存下来：两份公开的 BGZF 全基因组 VCF 分别存了 4,741,304 行和 5,017,551 行。
这些检出描述的是文件本身，索引并没有核实过。导入耗时、存储大小和完整的公开语料
结果，见[基准测试说明](https://github.com/thetahealth/mirobody/blob/c1aae297b4f3fad5cb87c5fc5bffc5163f7758c9/benchmarks/genomics/README.md)。

<h2 id="读取与导出">
  读取与导出
</h2>

`query_genetic_data` 不带选择条件时返回生效集合的概况；否则从下面几种里选一种：
rsID（最多 50 个）、HGNC 基因，或者一个有边界的 GRCh37/GRCh38 区间。区间也可以用
`build=raw`，按上传文件的原始位置找，不声称属于哪个参考基因组版本；PAR 区的行只能
用这种模式查。每次最多返回 100 行，附带来源、参考基因组版本、检出状态和截断
说明。区间查询时，`position` 用的是所请求版本的坐标，`query_build`、`raw_position`、
`pos37` 和 `pos38` 把坐标的来历交代清楚。Agent 和需要认证的 MCP 接口用的是同一套
服务。按基因搜索需要索引里有这个基因标签，按指定版本搜索区间需要映射好的坐标。
`build=raw` 的区间查询能找到没映射上的上传位置，同时不声称属于哪个版本；没映射上
的行也能按 rsID 找到。读取关爱圈成员的数据需要授权。这个工具不推断疾病风险。

`GET /api/v1/genomics/active-set` 给"数据"页提供统计数字和来源信息。
`GET /api/v1/genomics/export.vcf?build=GRCh38`（或 `GRCh37`）只把生效集合里已映射、
可靠的检出以流式导出。文件头写明有多少行因为 unresolved、未映射或不适合导出而被
省略，并记录上传格式、厂商、声明和检测到的参考基因组版本，以及标准化器和位点目录的
版本。contig 先按数字顺序排，再接 X、Y 和 MT。PharmCAT 3.4.0 读取导出的 VCF 时没有
任何警告。只凭两个公开的 CYP2C19 位点，它的 Named Allele Matcher 只能列出几种候选
双倍型（diplotype），这也正是 Mirobody 自己从不给出双倍型的原因。

`GET /api/v1/genomics/export.fhir.json?rsids=rs4244285&rsids=rs4986893` 按常规的
API 响应格式，返回一个有边界的 FHIR STU3 Variant Observation 集合。每个位点单独写一个
`rsids` 参数，最多 50 个；用逗号拼在一起的写法会被拒绝。`build` 默认是 `GRCh38`。
它读取当前生效的上传数据，授权要求和基因型工具相同，并返回 `missing_rsids` 和
`omitted_rsids`。只有已映射、有坐标和 REF/ALT 的已检出或未检出位点才能被表示。
公开的双位点上传，九种格式经过这个导出都能还原出真值。

<h2 id="药物基因组学">
  药物基因组学
</h2>

`query_pharmacogenomics` 按精确的 CPIC 通用药名或 HGNC 基因匹配；不带条件时，检查
当前生效的用药计划。它读取固定在 v1.60.0 版本的 CPIC 数据中 A/B 级的药物–基因关系，
统计生效上传里各个定义位置已检出、缺失和未检出的数量。指南的证据等级不等于这个人的表型。
工具只做到覆盖统计这一步：表型返回 `not_determined`，因为星号等位基因需要相位、
拷贝数和全部定义位点；它也从不给处方建议。CPIC 读取模块只有在双倍型是芯片之外确认
过的情况下，才会把它映射成表型。随包的 [CPIC NOTICE](https://github.com/thetahealth/mirobody/blob/c1aae297b4f3fad5cb87c5fc5bffc5163f7758c9/mirobody/res/genomics/cpic-v1.60.0.NOTICE)
记录了这个固定版本的来源、提取文件和许可。

`mirobody fetch cpic --version v1.59.1` 把一个确切版本的公开 CPIC 发布下载到
`CPIC_DIR`（默认 `~/.mirobody/cpic`），把里面的 COPY 段当作纯数据解析校验（不执行其中的
SQL），再原子地装好提取出的表。`--version latest` 会解析出抓取时发布了数据库 dump 的最新官方版本。
需要的话，可以把 `CPIC_SOURCE_URL` 设成镜像的基础地址。把 `CPIC_VERSION` 设成某个
已安装的确切版本，或者设成 `latest` 选用**本地已安装**的最新提取文件，之后的查询
就用它；设成 `bundled` 则始终用经过测试、随包附带的 v1.60.0。抓取一个版本并不会
启用它。查询在请求时读取所选的版本，每次药物基因组学回答都按请求现算，不会存下来。
固定了哈希的公开 v1.59.1、v1.60.0 两个 dump，已经通过抓取、校验和版本选择测试。

罕见致病位点的芯片检出结果，不会被转成疾病结论。缺失的位点、未检出和冲突，都不能
被当成正常来表示。

<h2 id="公开数据验证与隐私">
  公开数据验证与隐私
</h2>

[公开数据生成器](https://github.com/thetahealth/mirobody/blob/c1aae297b4f3fad5cb87c5fc5bffc5163f7758c9/benchmarks/genomics/generate_public_formats.py)用 SHA-256 钉住
一份 1000 Genomes HG00096 真值文件和 PharmCAT 3.4 的位点文件，再用同样的两个检出
生成九个上传文件，包括 gzip、BGZF、带附带文件的 ZIP，以及两个参考基因组版本。
[端到端检查](https://github.com/thetahealth/mirobody/blob/c1aae297b4f3fad5cb87c5fc5bffc5163f7758c9/benchmarks/genomics/e2e_public_truth.py)经过真实的 WebSocket 上传、
生效集合替换、rsID／基因／区间 MCP 查询、CPIC 覆盖、GRCh37/38 VCF 与 FHIR 导出，
以及真实的 Agent 工具调用，来检验随包的位点索引。公开的 rs4244285 VCF 里 ALT 只写了
`A`，而索引里的 dbSNP 位点列的是 `A,C,T`；标准化器会把它的 GT 下标换算到索引的
等位基因顺序。这是一项流程检查，不能当作全芯片准确性的证据。另一项检查只把这些公开
真值行从 1.5.1 的表里迁移过来。任何人自己的基因型导出或个人健康文档，都不应该出现
在提交进仓库的测试数据里。

在同一份公开真值上，还有一套 40 题的 Agent 评测，检查工具选择和违禁表述。Qwen 在
40 次首轮回答里有 37 次选对了工具，自动的违禁表述告警一次也没触发。三次失误都是
笼统的"没有问题"式回复，算作无效回答。它衡量的是两个位点上的工具选择，不是回答在
临床上对不对。评测语料和回答都不放在仓库里。

系统按人分开存储基因数据。工具回复会限制基因型的行数；原始基因型文件被排除在
Agent 的 `/uploads/` 和 `/library/` 文档挂载之外，作为聊天消息附件上传时也一样。
以前被误标成报告的旧数据行，会按基因型表头或压缩包类型筛出来，关联到基因型集合的
行也会被排除。存储的文本缺失时，读取和下载还会检查原始字节。"数据"页只拿到汇总
数字。模型回答问题时可能看到有边界的工具结果，所以托管部署必须考虑用的是哪家模型
服务商，以及相应的知情同意要求。[在线隐私检查](https://github.com/thetahealth/mirobody/blob/c1aae297b4f3fad5cb87c5fc5bffc5163f7758c9/benchmarks/genomics/check_filesystem_privacy.py)
覆盖普通、gzip、zip 三种聊天附件的归类、数据行的持久化，以及两个文档挂载点。每次
调用模型之前，守卫都会统计本轮基因工具返回的行数，并在检查点回放时去掉上一轮的
基因工具结果和依赖它的回答。一次隔离环境下的两轮实测记录了三个模型边界：每一行
可见的基因型都能对应到本轮的工具结果，上一轮的数据行和依赖它的回答在回放时都被
去掉了。DeepAgents 单独的摘要器和历史转储，拿到的也是脱敏后的基因型结果，上下文
溢出恢复时同样如此；同步和异步两条路径都有用公开数据写的回归测试。Agent 里禁用了
通用子代理（general-purpose subagent）。做过基因查询之后，Agent 会拒绝读写草稿
文件，但文档和档案挂载点仍然可以只读访问。
