diff --git a/README.md b/README.md
index 7bee2a0..8c9aa00 100644
--- a/README.md
+++ b/README.md
@@ -158,7 +158,7 @@ Every query and annotated VCF reports:
| `N_HET` | Heterozygous PASS carriers |
| `N_HOM_ALT` | Homozygous-alt PASS carriers |
| `N_HOM_REF` | Samples trusted as homozygous reference |
-| `N_FAIL` | Carriers with FILTER ≠ PASS (excluded from AC/AN) |
+| `N_FAIL` | Samples with a failed call (FILTER ≠ PASS) at the position (excluded from AC/AN) |
| `N_NO_COVERAGE` | Non-carriers on a partially-covered tech without sufficient evidence to call hom-ref |
VCF INFO field names use the `AFQUERY_` prefix (e.g. `AFQUERY_AF`, `AFQUERY_N_NO_COVERAGE`).
diff --git a/docs/advanced/coverage-evidence.md b/docs/advanced/coverage-evidence.md
index 9e94460..bc2b10a 100644
--- a/docs/advanced/coverage-evidence.md
+++ b/docs/advanced/coverage-evidence.md
@@ -27,9 +27,8 @@ At a multi-allelic site, eligible samples that carry only another ALT allele at
the position fall in none of these categories for this allele, so the sum is
lower than `n_eligible` by exactly that number of samples.
-Samples in `N_NO_COVERAGE` remain in `eligible` and contribute to `AN` (just
-like `N_FAIL`), so AC/AN/AF stay conservative — the field never inflates
-allele frequencies. Two rules always hold:
+Samples in `N_NO_COVERAGE` remain in `eligible` and contribute to `AN`, so
+AC/AN/AF stay conservative — the field never inflates allele frequencies. Two rules always hold:
- **Carriers are never reclassified.** A sample with a `het`, `hom`, or
`fail` call at the position stays in its category. `N_NO_COVERAGE` only
@@ -55,6 +54,11 @@ position. They run at query time, so no database rebuild is needed.
When both flags are >0, both must hold (AND). The default `0` disables the
gate.
+`afquery annotate` applies the same gates to input alleles that are not in the
+database. At a position where no sample has any call, no tech has evidence, so
+under any active gate the non-carriers of partially-covered techs are reported
+as `N_NO_COVERAGE`.
+
!!! tip
If your VCFs do not carry `FORMAT/DP` or `FORMAT/GQ`, these are the
flags you want. They are the cheapest option and apply to any database.
@@ -102,7 +106,7 @@ coverage decision is baked in.
| `--min-dp D` | Minimum `FORMAT/DP` per carrier. |
| `--min-gq G` | Minimum `FORMAT/GQ` per carrier. |
| `--min-qual Q` | Minimum VCF `QUAL` per carrier. |
-| `--min-covered K`| Per partially-covered tech, the position is "trusted" only if at least K of its carriers pass the quality thresholds. Non-carriers of failing positions are recorded as `N_NO_COVERAGE`. The gate is evaluated at build time for each ALT allele separately; samples carrying another allele at the position are never counted as `N_NO_COVERAGE`. |
+| `--min-covered K`| Per partially-covered tech, the position is "trusted" only if at least K of its carriers pass the quality thresholds. Non-carriers of failing positions are recorded as `N_NO_COVERAGE`. Carriers of any ALT allele at the position count, so every allele of a multi-allelic site gets the same verdict; samples carrying another allele there are never counted as `N_NO_COVERAGE`. |
A carrier counts as quality-passing only if **all** active thresholds hold
(unset thresholds are simply ignored). At least one of these flags must be
diff --git a/docs/advanced/debugging-results.md b/docs/advanced/debugging-results.md
index 03834c4..49e7a13 100644
--- a/docs/advanced/debugging-results.md
+++ b/docs/advanced/debugging-results.md
@@ -42,13 +42,13 @@ A variant you expect to find is not in the database:
### 4. Unexpected N_FAIL > 0
-`N_FAIL > 0` means some eligible samples had a call with `FILTER≠PASS` at this position. These samples are excluded from AC, but they remain eligible and still count in AN. This is usually benign (1–2 samples), but a high N_FAIL warrants investigation:
+`N_FAIL > 0` means some eligible samples had a call with `FILTER≠PASS` at this position. These samples are left out of AC and AN. This is usually benign (1–2 samples), but a high N_FAIL warrants investigation:
| N_FAIL relative to n_eligible | Likely cause |
|---|---|
| 1–2 samples | Isolated low-quality calls — not concerning |
| > 5% of n_eligible | Systematic sequencing artifact at this site |
-| All eligible samples | Site-wide QC failure — AF=0 but variant is present in source VCFs |
+| All eligible samples | Site-wide QC failure — AN=0 and AF is `NA`, although the variant is present in source VCFs |
To inspect a site with high N_FAIL, query with `--format json` to see all fields:
diff --git a/docs/advanced/filter-pass-tracking.md b/docs/advanced/filter-pass-tracking.md
index 11d03ca..a80b333 100644
--- a/docs/advanced/filter-pass-tracking.md
+++ b/docs/advanced/filter-pass-tracking.md
@@ -1,106 +1,77 @@
# FILTER=PASS Tracking
-AFQuery tracks variants that are called but fail quality filters (`FILTER≠PASS`). This allows distinguishing between "variant not present" and "variant present but low quality" in your cohort.
+AFQuery keeps the calls that fail quality filters (`FILTER≠PASS`) instead of discarding them. A failed call tells you the position was looked at but the sample's genotype there is unknown, which is different both from "variant not present" and from "variant present".
---
-## Background: VCF FILTER Field
+## What counts as a failed call
-In VCF format, the FILTER column indicates whether a variant call passed quality filters:
+A sample has a failed call at a position when its VCF holds, for any ALT allele there:
-- `PASS` or `.` (missing) — the variant passed all filters
-- Any other value (e.g., `LowQual`, `VQSRTrancheSNP99.90to100.00`) — the variant failed one or more filters
+- a call carrying the ALT allele with `FILTER` other than `PASS` or `.`;
+- a missing genotype (`./.`) at a site that failed a filter; or
+- calls that carry more alleles than the sample has at the position, such as two ALT alleles on chrX in a male. These are detected when the VCF is ingested, which logs a warning (see [Genotypes inconsistent with ploidy](ploidy-and-sex-chroms.md#genotypes-inconsistent-with-ploidy)).
-AFQuery's default behavior:
-
-- Only `FILTER=PASS` calls contribute to **AC** (and therefore to **AF**). This is always enforced.
-- **AN** is not affected by the filter — it counts every eligible sample at the position, failed calls included.
+These samples are stored in the variant's `fail_bitmap` and reported as `N_FAIL`. A homozygous-reference call (`0/0`) is never stored, so at a failed site it would count as hom-ref; [`normalize_vcf.sh`](../getting-started/preprocessing.md) avoids this by masking every genotype at a failed site as missing.
---
-## fail_bitmap
-
-AFQuery stores a third bitmap per variant alongside `het_bitmap` and `hom_bitmap`:
-
-- **`fail_bitmap`** — bit set for each sample whose call at this site has `FILTER≠PASS`. In practice this is two cases: a sample that carries the alt allele but whose call failed a filter, and a sample with a missing genotype (`./.`) at a site that itself failed a filter.
+## How failed calls are counted
-What this means for a sample in `fail_bitmap`:
-
-- Its alt alleles are **not** counted in AC, so it does not raise AF.
-- It is still an eligible sample, so it **is** counted in AN. The failed call lowers AF by sitting in the denominator without contributing to the numerator.
-- Its count is exposed separately as `N_FAIL`.
-
----
+A failed call is judged per **position**, not per allele: a sample with a failed call for any ALT allele at a position has no reliable genotype there. For every allele at that position, the sample:
-## Database Creation
+- adds nothing to **AC**, even if another record at the position called it with `FILTER=PASS`;
+- is left out of **AN**, which counts only the alleles of samples with a usable call;
+- is counted in **N_FAIL**, which therefore has the same value on every line of a multi-allelic site;
+- stays in **n_eligible**, so `N_HET + N_HOM_ALT + N_HOM_REF + N_FAIL + N_NO_COVERAGE` still accounts for every eligible sample (see [Multi-allelic sites](../getting-started/understanding-output.md#multi-allelic-sites) for the one exception).
-The `fail_bitmap` is always written, regardless of build options:
-
-```bash
-afquery create-db --manifest manifest.tsv --output-dir ./db/ --genome-build GRCh38
-```
-
----
-
-## Querying N_FAIL
-
-### CLI
-
-The `N_FAIL` count is shown automatically in query output:
+Leaving failed calls out of AN keeps AF an estimate over the genotypes that were actually observed. At every position, AN equals the reference alleles plus the AC of all its ALT alleles.
```bash
afquery query --db ./db/ --locus chr1:925952
```
```
-chr1:925952 G>A AC=142 AN=2742 AF=0.0518 n_eligible=1371 N_HET=138 N_HOM_ALT=2 N_HOM_REF=1224 N_FAIL=7
+chr1:925952 G>A AC=142 AN=2728 AF=0.0521 n_eligible=1371 N_HET=138 N_HOM_ALT=2 N_HOM_REF=1224 N_FAIL=7 N_NO_COVERAGE=0
```
-`N_FAIL=7` means 7 eligible samples had a call with FILTER≠PASS at this site. They are part of `n_eligible` (and of AN), so the genotype counts still add up: 138 + 2 + 1224 + 7 = 1371.
+The 7 samples with a failed call are part of `n_eligible` (138 + 2 + 1224 + 7 = 1371) but not of AN: 2 × (1371 − 7) = 2728.
-### Python API
+### When every eligible sample failed
-```python
-results = db.query("chr1", pos=925952)
-for r in results:
- print(f"AC={r.AC} AN={r.AN} FAIL={r.N_FAIL}")
- if r.N_FAIL > 0:
- print(f" Warning: {r.N_FAIL} samples have low-quality calls at this site")
-```
+If all eligible samples have a failed call at a position, AN is 0 and AF is undefined: `NA` in text and TSV output, `null` in JSON and the Python API, an empty cell in `dump`, and `.` in `AFQUERY_AF`. `N_FAIL` still reports how many samples failed.
-`N_FAIL` is always an `int` (default `0`).
+---
-### Identifying specific FAIL samples
+## Counting failed calls as reference
-To see which individual samples have FAIL status at a position, use `variant-info`:
+`--fail-as-ref` (`fail_as_ref=True` in the Python API) keeps samples with a failed call in AN, counting them as homozygous reference. It is available on `query`, `annotate` and `dump`, and changes only AN and AF: the failed samples still add nothing to AC and are still reported in `N_FAIL`.
```bash
-afquery variant-info --db ./db/ --locus chr1:925952
+afquery query --db ./db/ --locus chr1:925952 --fail-as-ref
```
-Each carrier row shows its `filter` column as `PASS` or `FAIL`, along with sample metadata (technology, phenotype codes). This helps pinpoint whether failures cluster in a specific technology or sample group. See [Variant Info](../guides/variant-info.md) for full options.
+```
+chr1:925952 G>A AC=142 AN=2742 AF=0.0518 n_eligible=1371 N_HET=138 N_HOM_ALT=2 N_HOM_REF=1224 N_FAIL=7 N_NO_COVERAGE=0
+```
----
+Because every failed sample is then treated as a non-carrier, the AF is never higher than the default one, which makes it a conservative lower estimate when a site has many failures. AN then no longer equals the reference alleles plus the AC of all ALT alleles.
-## VCF Annotation
+---
-Among the INFO fields `afquery annotate` writes, one reports the failed-call count:
+## Finding the failed samples
-| Field | Type | Description |
-|-------|------|-------------|
-| `AFQUERY_N_FAIL` | Integer | Eligible samples with `FILTER≠PASS` at this variant |
+`afquery variant-info` lists each sample with a failed call at the position as genotype `alt` with filter `FAIL`, along with its technology and phenotypes. This shows whether failures cluster in one technology or sample group. See [Variant Info](../guides/variant-info.md).
```bash
-afquery annotate --db ./db/ --input variants.vcf --output annotated.vcf
+afquery variant-info --db ./db/ --locus chr1:925952
```
-See [Understanding Output](../getting-started/understanding-output.md#vcf-annotation-fields) for the full set of `AFQUERY_*` fields.
-
---
-## PASS-Only Enforcement
+## VCF annotation
-PASS-only counting is always enforced and cannot be turned off. It applies to the numerator only: a failed call never adds an alt allele to AC, but the sample stays in the eligible set and so remains in AN. The result is a deliberately conservative AF — a failed carrier weighs on the denominator without lifting the numerator, which is the safe direction for clinical and research use.
+`afquery annotate` writes the same per-position values: `AFQUERY_AN` excludes failed calls and `AFQUERY_N_FAIL` counts each failed sample once per site. See [Understanding Output](../getting-started/understanding-output.md#vcf-annotation-fields) for the full set of `AFQUERY_*` fields.
---
diff --git a/docs/advanced/ploidy-and-sex-chroms.md b/docs/advanced/ploidy-and-sex-chroms.md
index 36acbcf..2157fe3 100644
--- a/docs/advanced/ploidy-and-sex-chroms.md
+++ b/docs/advanced/ploidy-and-sex-chroms.md
@@ -76,7 +76,7 @@ afquery query --db ./db/ --locus chrY:2787758 --sex female
# chrY:2787758 — no results (AN=0 for all variants)
afquery query --db ./db/ --locus chrY:2787758 --sex male
-# chrY:2787758 C>T AC=3 AN=856 AF=0.0035 n_eligible=856 N_HET=0 N_HOM_ALT=3 N_HOM_REF=853 N_FAIL=0
+# chrY:2787758 C>T AC=3 AN=856 AF=0.0035 n_eligible=856 N_HET=0 N_HOM_ALT=3 N_HOM_REF=853 N_FAIL=0 N_NO_COVERAGE=0
```
### chrX non-PAR
@@ -107,10 +107,10 @@ For every query result at a biallelic site, the following identity holds:
**N_HET + N_HOM_ALT + N_HOM_REF + N_FAIL + N_NO_COVERAGE = n_eligible**
-This can be used to validate results. At a multi-allelic site, eligible samples that carry only another ALT allele at the position fall in none of these categories, so the sum is lower by exactly that number of samples (see [Multi-allelic sites](../getting-started/understanding-output.md#multi-allelic-sites)). N_HOM_REF is the number of eligible samples that are homozygous reference (i.e., carry no ALT allele at the position and passed quality filters). N_NO_COVERAGE is 0 unless a coverage-evidence filter is active — see [Coverage Evidence](coverage-evidence.md).
+This can be used to validate results. At a multi-allelic site, eligible samples that carry only another ALT allele at the position fall in none of these categories, so the sum is lower by exactly that number of samples (see [Multi-allelic sites](../getting-started/understanding-output.md#multi-allelic-sites)). N_HOM_REF is the number of eligible samples that are homozygous reference (i.e., carry no ALT allele at the position and have no failed call there). N_NO_COVERAGE is 0 unless a coverage-evidence filter is active — see [Coverage Evidence](coverage-evidence.md).
!!! note "Mutual exclusivity"
- N_HET, N_HOM_ALT, N_HOM_REF, N_FAIL, and N_NO_COVERAGE are mutually exclusive. A sample with a non-ref allele but FILTER≠PASS is counted in N_FAIL only — it does not appear in N_HET or N_HOM_ALT. Likewise, N_HOM_REF counts only PASS-filtered samples.
+ N_HET, N_HOM_ALT, N_HOM_REF, N_FAIL, and N_NO_COVERAGE are mutually exclusive. A sample with a failed call (FILTER≠PASS) for any allele at the position is counted in N_FAIL only — it does not appear in N_HET, N_HOM_ALT or N_HOM_REF, and is left out of AC and AN.
### chrX non-PAR
@@ -140,6 +140,15 @@ chrM is haploid for all samples (both sexes contribute AN=1):
!!! warning "N_HET is always 0 on haploid regions"
On chrY, chrM, and chrX non-PAR for males, all carriers are counted in N_HOM_ALT. N_HET is 0 because haploid samples have only one allele copy — there is no heterozygous state. This is correct behavior, not a bug.
+### Genotypes inconsistent with ploidy
+
+Ploidy comes from each sample's sex and the position, not from how the VCF writes the genotype: a male carrier on chrX non-PAR counts as one allele whether its GT is `1`, `0/1` or `1/1`. Calls that carry more alleles than the sample has cannot be counted that way:
+
+- on a haploid position, two different ALT alleles (e.g. GT=`1/2` on chrM, or on chrX non-PAR in a male);
+- on a diploid position, more than two copies across the records at that position (e.g. `1/1` for one ALT and `0/1` for another).
+
+`create-db` and `update-db --add-samples` record every call of the sample at such a position as a failed call, so it counts in `N_FAIL` and is left out of AC and AN, and log a warning naming the sample and the positions. Running `bcftools +fixploidy` during [preprocessing](../getting-started/preprocessing.md) avoids most of these cases on chrX.
+
---
## Sex Filter Interaction
diff --git a/docs/faq.md b/docs/faq.md
index 0fdab9b..f15e0b3 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -153,7 +153,7 @@ On chrX non-PAR positions, males contribute AN=1 and females contribute AN=2. Wh
## What does N_FAIL mean in query output?
-`N_FAIL` is the count of eligible samples whose call had `FILTER≠PASS` at the position. Shown as `N_FAIL=N` in text output. These samples are excluded from AC, but they stay eligible and still count toward AN. See [FILTER=PASS Tracking](advanced/filter-pass-tracking.md).
+`N_FAIL` is the count of eligible samples whose call had `FILTER≠PASS` at the position. Shown as `N_FAIL=N` in text output. These samples are left out of AC and AN, because their genotype there is unknown. See [FILTER=PASS Tracking](advanced/filter-pass-tracking.md).
---
diff --git a/docs/getting-started/concepts.md b/docs/getting-started/concepts.md
index fb1d5be..be5b2df 100644
--- a/docs/getting-started/concepts.md
+++ b/docs/getting-started/concepts.md
@@ -9,7 +9,7 @@ For each variant, AFQuery stores a compressed bitset recording which samples car
```
Samples → Metadata → Filters → Eligible Bitmap
Variants → Genotypes → het/hom Bitmaps
-Query: (eligible bitmap) AND (carrier bitmap) → AC; count(eligible) → AN; AC/AN → AF
+Query: (eligible bitmap) AND (carrier bitmap) → AC; count(eligible − failed) → AN; AC/AN → AF
```
```mermaid
@@ -25,7 +25,7 @@ flowchart LR
end
subgraph Result
- R["AC = popcount(eligible AND carrier) AN = popcount(eligible) × ploidy AF = AC / AN"]
+ R["AC = popcount(eligible AND carrier) AN = popcount(eligible − failed) × ploidy AF = AC / AN"]
end
S --> E
@@ -55,7 +55,7 @@ For a detailed discussion of the methodological gaps AFQuery addresses, includin
For a given variant (chromosome, position, ref, alt):
- **AC** (Allele Count) — number of copies of the alt allele observed across eligible samples
-- **AN** (Allele Number) — total number of alleles examined (2× diploid samples, 1× haploid)
+- **AN** (Allele Number) — total number of alleles examined (2× diploid samples, 1× haploid). Samples with a failed call (`FILTER≠PASS`) at the position are left out, as their genotype there is unknown; they are reported as `N_FAIL` (see [FILTER=PASS Tracking](../advanced/filter-pass-tracking.md))
- **AF** (Allele Frequency) — `AC / AN` (None when AN = 0)
A heterozygous carrier contributes AC=1; a homozygous alt carrier contributes AC=2.
@@ -130,7 +130,7 @@ For each variant row, AFQuery stores per-sample genotype information as [Roaring
- **`het_bitmap`** — sample is heterozygous (`GT=0/1`) with `FILTER=PASS`
- **`hom_bitmap`** — sample is homozygous alt (`GT=1/1`, or `GT=1` on haploid regions) with `FILTER=PASS`
-- **`fail_bitmap`** — the sample's call at this site has `FILTER≠PASS`
+- **`fail_bitmap`** — the sample's call at this site failed (`FILTER≠PASS`, or a genotype inconsistent with its ploidy); see [FILTER=PASS Tracking](../advanced/filter-pass-tracking.md)
Each sample has a stable integer ID (0-indexed). The bit position in the bitmap equals the sample ID. Databases built with coverage-quality filters carry two additional bitmaps — see [Data Model](../reference/data-model.md#parquet-schema).
@@ -214,7 +214,7 @@ When planning phenotype codes, consider:
---
-PASS-only ingestion is always enforced. See [FILTER=PASS Tracking](../advanced/filter-pass-tracking.md) for details.
+Only `FILTER=PASS` calls count towards AC. Failed calls are stored too, and their samples are reported as `N_FAIL` and left out of AN. See [FILTER=PASS Tracking](../advanced/filter-pass-tracking.md) for details.
---
diff --git a/docs/getting-started/preprocessing.md b/docs/getting-started/preprocessing.md
index 08e6f64..05e74ec 100644
--- a/docs/getting-started/preprocessing.md
+++ b/docs/getting-started/preprocessing.md
@@ -29,16 +29,13 @@ Multi-allelic variants and complex indels can be represented in multiple equival
### Ploidy correction for sex chromosomes
-Male samples should have haploid genotype calls at chrX non-PAR regions (GT=`1`, not GT=`0/1` or `1/1`). Many variant callers output diploid genotypes for males at chrX by default. Without ploidy correction:
-
-- Males at chrX are counted as diploid → AN is inflated by up to 50%
-- AF estimates for X-linked variants are systematically underestimated
+Male samples should have haploid genotype calls at chrX non-PAR regions (GT=`1`, not GT=`0/1` or `1/1`). Many variant callers output diploid genotypes for males at chrX by default. AFQuery takes ploidy from the sample's sex, so a diploid-coded male call still counts as one allele; but a male call carrying two different ALT alleles (`1/2`) cannot be counted at all and is recorded as a failed call, with a warning during ingestion (see [Genotypes inconsistent with ploidy](../advanced/ploidy-and-sex-chroms.md#genotypes-inconsistent-with-ploidy)). `bcftools +fixploidy` rewrites these genotypes as haploid calls.
`bcftools +fixploidy` corrects genotype ploidy based on a sex file.
### Filtering and stripping
-- **Non-PASS genotypes**: Masking them as missing ensures that low-quality calls do not inflate AC. AFQuery tracks these as N_FAIL.
+- **Non-PASS genotypes**: Masking every genotype at a failed site as missing ensures that low-quality calls do not inflate AC, and that no sample there is taken for hom-ref. AFQuery tracks these samples as N_FAIL and leaves them out of AN.
- **Homozygous reference calls**: Removing ref/ref genotypes reduces file size and speeds ingestion; they contribute AC=0 and are not needed
- **INFO fields**: Stripping INFO reduces file size and speeds ingestion. Additionally, malformed or non-standard INFO fields produced by some variant callers can break downstream parsing; stripping them pre-emptively prevents these errors.
- **FORMAT fields**: Only `GT`, `DP`, and `GQ` are preserved. `GT` is the genotype required by AFQuery for all queries. `DP` and `GQ` are read by the [coverage-evidence](../advanced/coverage-evidence.md) quality flags (`afquery create-db --min-dp / --min-gq / --min-qual / --min-covered`). VCFs without `DP`/`GQ` are still valid; their carriers simply contribute no quality evidence to the cohort. All other FORMAT fields (`PL`, `AD`, etc.) are dropped to reduce file size.
diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md
index 1a3ace0..ff5f224 100644
--- a/docs/getting-started/quickstart.md
+++ b/docs/getting-started/quickstart.md
@@ -70,7 +70,7 @@ afquery query --db ./my_db/ --locus chr1:925952
Example output:
```
-chr1:925952 G>A AC=2 AN=6 AF=0.3333 n_eligible=3 N_HET=2 N_HOM_ALT=0 N_HOM_REF=1 N_FAIL=0
+chr1:925952 G>A AC=2 AN=6 AF=0.3333 n_eligible=3 N_HET=2 N_HOM_ALT=0 N_HOM_REF=1 N_FAIL=0 N_NO_COVERAGE=0
```
Filter to female samples with a specific phenotype:
@@ -134,7 +134,7 @@ The output VCF gains INFO fields (see [Annotate a VCF](../guides/annotate-vcf.md
| `AFQUERY_N_HET` | A (per ALT) | Heterozygous sample count |
| `AFQUERY_N_HOM_ALT` | A (per ALT) | Homozygous alt sample count |
| `AFQUERY_N_HOM_REF` | A (per ALT) | Homozygous ref sample count |
-| `AFQUERY_N_FAIL` | 1 (per site) | Samples with FILTER≠PASS |
+| `AFQUERY_N_FAIL` | 1 (per site) | Samples with a failed call (FILTER≠PASS), left out of AN |
| `AFQUERY_N_NO_COVERAGE` | A (per ALT) | Eligible samples lacking coverage evidence (0 unless a coverage-evidence filter is active) |
---
diff --git a/docs/getting-started/tutorial.md b/docs/getting-started/tutorial.md
index df85551..f33e5ff 100644
--- a/docs/getting-started/tutorial.md
+++ b/docs/getting-started/tutorial.md
@@ -74,7 +74,7 @@ afquery query --db ./demo_db/ --locus chr1:925952
Example output:
```
-chr1:925952 G>A AC=6 AN=20 AF=0.3000 n_eligible=10 N_HET=4 N_HOM_ALT=1 N_HOM_REF=5 N_FAIL=0
+chr1:925952 G>A AC=6 AN=20 AF=0.3000 n_eligible=10 N_HET=4 N_HOM_ALT=1 N_HOM_REF=5 N_FAIL=0 N_NO_COVERAGE=0
```
### Reading the output
@@ -107,15 +107,10 @@ afquery query --db ./demo_db/ --locus chr1:946000
Example output:
```
-chr1:946000 T>C AC=2 AN=20 AF=0.1000 n_eligible=10 N_HET=2 N_HOM_ALT=0 N_HOM_REF=6 N_FAIL=2
+chr1:946000 T>C AC=2 AN=16 AF=0.1250 n_eligible=10 N_HET=2 N_HOM_ALT=0 N_HOM_REF=6 N_FAIL=2 N_NO_COVERAGE=0
```
-Here, 2 samples were flagged as `LowQual` in their source VCF at this position. N_FAIL samples are:
-
-- **Counted in AN** — they are eligible and contribute to the denominator
-- **Excluded from AC** — their alleles are not counted in the numerator
-
-This makes AF a conservative estimate: `AF=2/20=0.1000` even though 2 additional samples may carry the variant. Verify: 10 = 2+0+6+2 ✓.
+Here, 2 samples were flagged as `LowQual` in their source VCF at this position. Their genotype there is unknown, so they are left out of both AC and AN: `AN=2×(10−2)=16` and `AF=2/16=0.1250`. They remain eligible, so the accounting identity still holds: 10 = 2+0+6+2+0 ✓. See [FILTER=PASS Tracking](../advanced/filter-pass-tracking.md).
!!! tip
N_FAIL > 0 is a signal to inspect source VCF quality at this site. A high N_FAIL relative to n_eligible may indicate a systematic sequencing artifact. See [Understanding Output](understanding-output.md) for thresholds.
@@ -131,9 +126,9 @@ afquery query --db ./demo_db/ --region chr1:900000-950000
Example output:
```
-chr1:925100 C>T AC=4 AN=20 AF=0.2000 n_eligible=10 N_HET=4 N_HOM_ALT=0 N_HOM_REF=6 N_FAIL=0
-chr1:925952 G>A AC=6 AN=20 AF=0.3000 n_eligible=10 N_HET=4 N_HOM_ALT=1 N_HOM_REF=5 N_FAIL=0
-chr1:946000 T>C AC=2 AN=20 AF=0.1000 n_eligible=10 N_HET=2 N_HOM_ALT=0 N_HOM_REF=6 N_FAIL=2
+chr1:925100 C>T AC=4 AN=20 AF=0.2000 n_eligible=10 N_HET=4 N_HOM_ALT=0 N_HOM_REF=6 N_FAIL=0 N_NO_COVERAGE=0
+chr1:925952 G>A AC=6 AN=20 AF=0.3000 n_eligible=10 N_HET=4 N_HOM_ALT=1 N_HOM_REF=5 N_FAIL=0 N_NO_COVERAGE=0
+chr1:946000 T>C AC=2 AN=16 AF=0.1250 n_eligible=10 N_HET=2 N_HOM_ALT=0 N_HOM_REF=6 N_FAIL=2 N_NO_COVERAGE=0
```
---
@@ -181,7 +176,7 @@ afquery query --db ./demo_db/ --locus chr1:925952 --sex female
Example output:
```
-chr1:925952 G>A AC=3 AN=10 AF=0.3000 n_eligible=5 N_HET=3 N_HOM_ALT=0 N_HOM_REF=2 N_FAIL=0
+chr1:925952 G>A AC=3 AN=10 AF=0.3000 n_eligible=5 N_HET=3 N_HOM_ALT=0 N_HOM_REF=2 N_FAIL=0 N_NO_COVERAGE=0
```
Now query male samples:
@@ -193,7 +188,7 @@ afquery query --db ./demo_db/ --locus chr1:925952 --sex male
Example output:
```
-chr1:925952 G>A AC=3 AN=10 AF=0.3000 n_eligible=5 N_HET=1 N_HOM_ALT=1 N_HOM_REF=3 N_FAIL=0
+chr1:925952 G>A AC=3 AN=10 AF=0.3000 n_eligible=5 N_HET=1 N_HOM_ALT=1 N_HOM_REF=3 N_FAIL=0 N_NO_COVERAGE=0
```
AN drops from 20 to 10 in both cases because only 5 samples are eligible. The AF happens to be identical here (0.3000), but the genotype distributions differ: the male group has one homozygous-alt carrier while the female group has three heterozygous carriers.
@@ -211,7 +206,7 @@ afquery query --db ./demo_db/ --locus chr1:925952 --phenotype E11.9
Example output:
```
-chr1:925952 G>A AC=5 AN=8 AF=0.6250 n_eligible=4 N_HET=3 N_HOM_ALT=1 N_HOM_REF=0 N_FAIL=0
+chr1:925952 G>A AC=5 AN=8 AF=0.6250 n_eligible=4 N_HET=3 N_HOM_ALT=1 N_HOM_REF=0 N_FAIL=0 N_NO_COVERAGE=0
```
Exclude control samples:
@@ -223,7 +218,7 @@ afquery query --db ./demo_db/ --locus chr1:925952 --phenotype ^control
Example output:
```
-chr1:925952 G>A AC=6 AN=12 AF=0.5000 n_eligible=6 N_HET=4 N_HOM_ALT=1 N_HOM_REF=1 N_FAIL=0
+chr1:925952 G>A AC=6 AN=12 AF=0.5000 n_eligible=6 N_HET=4 N_HOM_ALT=1 N_HOM_REF=1 N_FAIL=0 N_NO_COVERAGE=0
```
The `^` prefix means "exclude". Excluding controls removes 4 samples, leaving 6. See [Sample Filtering](../guides/sample-filtering.md) for the full syntax.
@@ -241,7 +236,7 @@ afquery query --db ./demo_db/ --locus chr1:925952 --tech wgs
Example output:
```
-chr1:925952 G>A AC=4 AN=8 AF=0.5000 n_eligible=4 N_HET=2 N_HOM_ALT=1 N_HOM_REF=1 N_FAIL=0
+chr1:925952 G>A AC=4 AN=8 AF=0.5000 n_eligible=4 N_HET=2 N_HOM_ALT=1 N_HOM_REF=1 N_FAIL=0 N_NO_COVERAGE=0
```
Now query each WES technology separately:
@@ -251,7 +246,7 @@ afquery query --db ./demo_db/ --locus chr1:925952 --tech wes_v1
```
```
-chr1:925952 G>A AC=1 AN=6 AF=0.1667 n_eligible=3 N_HET=1 N_HOM_ALT=0 N_HOM_REF=2 N_FAIL=0
+chr1:925952 G>A AC=1 AN=6 AF=0.1667 n_eligible=3 N_HET=1 N_HOM_ALT=0 N_HOM_REF=2 N_FAIL=0 N_NO_COVERAGE=0
```
```bash
@@ -259,7 +254,7 @@ afquery query --db ./demo_db/ --locus chr1:925952 --tech wes_v2
```
```
-chr1:925952 G>A AC=1 AN=6 AF=0.1667 n_eligible=3 N_HET=1 N_HOM_ALT=0 N_HOM_REF=2 N_FAIL=0
+chr1:925952 G>A AC=1 AN=6 AF=0.1667 n_eligible=3 N_HET=1 N_HOM_ALT=0 N_HOM_REF=2 N_FAIL=0 N_NO_COVERAGE=0
```
`chr1:925952` falls inside the `chr1:900000-950000` capture region shared by both WES kits. All 3 wes_v1 samples and all 3 wes_v2 samples are covered, so AN=6 (3 samples × 2 alleles) for both.
@@ -273,7 +268,7 @@ afquery query --db ./demo_db/ --locus chr1:1399914 --tech wgs
```
```
-chr1:1399914 G>T AC=2 AN=8 AF=0.2500 n_eligible=4 N_HET=2 N_HOM_ALT=0 N_HOM_REF=2 N_FAIL=0
+chr1:1399914 G>T AC=2 AN=8 AF=0.2500 n_eligible=4 N_HET=2 N_HOM_ALT=0 N_HOM_REF=2 N_FAIL=0 N_NO_COVERAGE=0
```
```bash
@@ -307,7 +302,7 @@ afquery query \
Example output:
```
-chr1:925952 G>A AC=1 AN=2 AF=0.5000 n_eligible=1 N_HET=1 N_HOM_ALT=0 N_HOM_REF=0 N_FAIL=0
+chr1:925952 G>A AC=1 AN=2 AF=0.5000 n_eligible=1 N_HET=1 N_HOM_ALT=0 N_HOM_REF=0 N_FAIL=0 N_NO_COVERAGE=0
```
Only one sample meets all three criteria (DEMO_001: female, wgs, E11.9). With n_eligible=1, AN=2 (one diploid sample on an autosome) and AC=1 (heterozygous carrier).
@@ -325,18 +320,18 @@ afquery annotate \
--output ./annotated_demo.vcf
```
-The output VCF gains `AFQUERY_AC`, `AFQUERY_AN`, `AFQUERY_AF`, `AFQUERY_N_HET`, `AFQUERY_N_HOM_ALT`, `AFQUERY_N_HOM_REF`, and `AFQUERY_N_FAIL` INFO fields. Inspect the chr1 variants:
+The output VCF gains `AFQUERY_AC`, `AFQUERY_AN`, `AFQUERY_AF`, `AFQUERY_N_HET`, `AFQUERY_N_HOM_ALT`, `AFQUERY_N_HOM_REF`, `AFQUERY_N_FAIL`, and `AFQUERY_N_NO_COVERAGE` INFO fields. Inspect the chr1 variants:
```bash
-grep -v "^##" ./annotated_demo.vcf | grep "^chr1" | head -5
+grep -v "^##" ./annotated_demo.vcf | grep "^chr1" | head -3
```
Example output (columns: CHROM, POS, ID, REF, ALT, QUAL, FILTER, INFO, FORMAT, DEMO_001):
```
-chr1 925100 . C T 50 PASS AFQUERY_AC=4;AFQUERY_AN=20;AFQUERY_AF=0.2;AFQUERY_N_HET=4;AFQUERY_N_HOM_ALT=0;AFQUERY_N_HOM_REF=6;AFQUERY_N_FAIL=0 GT 0/1
-chr1 925952 . G A 50 PASS AFQUERY_AC=6;AFQUERY_AN=20;AFQUERY_AF=0.3;AFQUERY_N_HET=4;AFQUERY_N_HOM_ALT=1;AFQUERY_N_HOM_REF=5;AFQUERY_N_FAIL=0 GT 0/1
-chr1 946000 . T C 50 PASS AFQUERY_AC=2;AFQUERY_AN=20;AFQUERY_AF=0.1;AFQUERY_N_HET=2;AFQUERY_N_HOM_ALT=0;AFQUERY_N_HOM_REF=6;AFQUERY_N_FAIL=2 GT 0/1
+chr1 925100 . C T 50 PASS AFQUERY_AC=4;AFQUERY_AN=20;AFQUERY_AF=0.2;AFQUERY_N_HET=4;AFQUERY_N_HOM_ALT=0;AFQUERY_N_HOM_REF=6;AFQUERY_N_FAIL=0;AFQUERY_N_NO_COVERAGE=0 GT 0/1
+chr1 925952 . G A 50 PASS AFQUERY_AC=6;AFQUERY_AN=20;AFQUERY_AF=0.3;AFQUERY_N_HET=4;AFQUERY_N_HOM_ALT=1;AFQUERY_N_HOM_REF=5;AFQUERY_N_FAIL=0;AFQUERY_N_NO_COVERAGE=0 GT 0/1
+chr1 946000 . T C 50 PASS AFQUERY_AC=2;AFQUERY_AN=16;AFQUERY_AF=0.125;AFQUERY_N_HET=2;AFQUERY_N_HOM_ALT=0;AFQUERY_N_HOM_REF=6;AFQUERY_N_FAIL=2;AFQUERY_N_NO_COVERAGE=0 GT 0/1
```
`AFQUERY_N_FAIL=2` for `chr1:946000` matches the query output from Step 4. The annotated values reflect cohort-wide frequencies across all 10 samples, not just DEMO_001.
@@ -362,12 +357,12 @@ head -6 demo_dump.csv
Example output:
```
-chrom,pos,ref,alt,AC,AN,AF,N_HET,N_HOM_ALT,N_HOM_REF,N_FAIL
-chr1,925100,C,T,4,20,0.2,4,0,6,0
-chr1,925952,G,A,6,20,0.3,4,1,5,0
-chr1,946000,T,C,2,20,0.1,2,0,6,2
-chr1,1225000,A,G,5,20,0.25,5,0,5,0
-chr1,1399914,G,T,2,8,0.25,2,0,2,0
+chrom,pos,ref,alt,AC,AN,AF,N_HET,N_HOM_ALT,N_HOM_REF,N_FAIL,N_NO_COVERAGE
+chr1,925100,C,T,4,20,0.2,4,0,6,0,0
+chr1,925952,G,A,6,20,0.3,4,1,5,0,0
+chr1,946000,T,C,2,16,0.125,2,0,6,2,0
+chr1,1225000,A,G,5,20,0.25,5,0,5,0,0
+chr1,1399914,G,T,2,8,0.25,2,0,2,0,0
```
Note that `chr1:1399914` has AN=8 even in the full-cohort dump — technology-aware AN is applied automatically (WES samples are excluded for positions outside their capture regions).
diff --git a/docs/getting-started/understanding-output.md b/docs/getting-started/understanding-output.md
index b7d7781..4d31b50 100644
--- a/docs/getting-started/understanding-output.md
+++ b/docs/getting-started/understanding-output.md
@@ -9,14 +9,14 @@ This page explains what each field in AFQuery output means and how to interpret
| Field | Type | Description |
|-------|------|-------------|
| **AC** | int | Allele count — number of alt allele copies in eligible samples |
-| **AN** | int | Allele number — total alleles examined (adjusted for ploidy and eligible samples) |
-| **AF** | float | Allele frequency — `AC / AN`. `None` when AN=0 |
+| **AN** | int | Allele number — alleles of the eligible samples with a usable call at this position, adjusted for ploidy. Samples in `N_FAIL` are left out (counted as hom-ref with `--fail-as-ref`) |
+| **AF** | float | Allele frequency — `AC / AN`. `NA` (text/TSV) or `null` (JSON) when AN=0 |
| **N_HET** | int | Number of eligible samples heterozygous for the alt allele (GT=0/1) |
| **N_HOM_ALT** | int | Number of eligible samples homozygous for the alt allele (GT=1/1 or GT=1). Includes haploid carriers on sex chromosomes and chrM. See [Ploidy](../advanced/ploidy-and-sex-chroms.md#genotype-counting). |
| **N_HOM_REF** | int | Number of eligible samples homozygous reference (GT=0/0 or GT=0). Samples carrying another ALT allele at the same position are not counted (see [Multi-allelic sites](#multi-allelic-sites)). |
| **n_eligible** | int | Number of eligible samples — those passing the sex/phenotype/tech filters *and* covered at this position |
-| **N_FAIL** | int | Number of eligible samples whose call at this position had FILTER≠PASS. These samples are counted *only* in N_FAIL — not in N_HET, N_HOM_ALT, or N_HOM_REF — but they stay eligible and still count toward AN. |
-| **N_NO_COVERAGE** | int | Number of eligible samples whose tech lacks coverage evidence at this position. Excluded from `N_HOM_REF` to keep AC/AN conservative. Always `0` unless a coverage-evidence filter is active. See [Coverage Evidence](../advanced/coverage-evidence.md). |
+| **N_FAIL** | int | Number of eligible samples with a failed call (FILTER≠PASS) for any allele at this position — the same value for every allele there. These samples are counted *only* in N_FAIL, not in N_HET, N_HOM_ALT or N_HOM_REF, and are left out of AC and AN. See [FILTER=PASS Tracking](../advanced/filter-pass-tracking.md). |
+| **N_NO_COVERAGE** | int | Number of eligible samples whose tech lacks coverage evidence at this position. Excluded from `N_HOM_REF`; they still count in AN. Always `0` unless a coverage-evidence filter is active. See [Coverage Evidence](../advanced/coverage-evidence.md). |
---
@@ -74,11 +74,14 @@ afquery query --db ./db/ --locus chr1:925952 --ref G --alt A --format json
### AN=0 and AF=None
-AN=0 means no eligible samples have coverage at this position. This happens when:
+AN=0 means no eligible sample has a usable call at this position. This happens when:
- All eligible samples are WES or panels and the position is outside capture regions
- The phenotype/sex/technology filter excludes all samples
- The chromosome is not in the database
+- Every eligible sample has a failed call there (`N_FAIL` = `n_eligible`)
+
+In the first three cases `query` returns no line for the position; in the last one it reports the variant with AN=0.
!!! warning "AN=0 does not mean the variant is absent"
@@ -94,6 +97,11 @@ reference for this allele, so it appears in none of `N_HET`, `N_HOM_ALT`,
`n_eligible` at biallelic sites; at a multi-allelic site they fall short by the
number of such samples. AC, AN and AF are not affected.
+AN and `N_FAIL` describe the position, so they are the same on every line of a
+multi-allelic site, and AN equals the reference alleles plus the AC of all its
+ALT alleles. A sample with a failed call for any of the alleles is counted in
+`N_FAIL` on every line.
+
A sample with two different ALT alleles (`GT=1/2`) is counted as heterozygous on
both lines.
@@ -124,7 +132,7 @@ AC=0 with a high AN (e.g., AN=4000) means the variant was **genuinely not observ
### N_FAIL > 0
-When N_FAIL > 0, some eligible samples had a non-PASS filter at this position in their source VCF. A high N_FAIL relative to AN may indicate a problematic site (e.g., systematic sequencing artifacts). Consider filtering positions with more than 10% of failing (`N_FAIL / (AN / 2) > 0.1`).
+When N_FAIL > 0, some eligible samples had a non-PASS filter at this position in their source VCF, and AF is computed over the remaining samples. A high N_FAIL relative to `n_eligible` may indicate a problematic site (e.g., systematic sequencing artifacts). Consider filtering positions where more than 10% of eligible samples failed (`N_FAIL / n_eligible > 0.1`).
---
@@ -135,12 +143,12 @@ When using `afquery annotate`, the following INFO fields are added to each varia
| INFO field | Number | Description |
|-----------|--------|-------------|
| `AFQUERY_AC` | A (per ALT) | Allele count — one value per ALT allele |
-| `AFQUERY_AN` | 1 (per site) | Allele number — shared across all ALT alleles |
-| `AFQUERY_AF` | A (per ALT) | Allele frequency — one value per ALT allele |
+| `AFQUERY_AN` | 1 (per site) | Allele number — shared across all ALT alleles; excludes samples in `AFQUERY_N_FAIL` |
+| `AFQUERY_AF` | A (per ALT) | Allele frequency — one value per ALT allele; `.` when `AFQUERY_AN=0` |
| `AFQUERY_N_HET` | A (per ALT) | Heterozygous sample count per ALT allele |
| `AFQUERY_N_HOM_ALT` | A (per ALT) | Homozygous alt sample count per ALT allele |
| `AFQUERY_N_HOM_REF` | A (per ALT) | Homozygous ref sample count per ALT allele, excluding carriers of other ALT alleles at the position |
-| `AFQUERY_N_FAIL` | 1 (per site) | Fail sample count — shared across all ALT alleles |
+| `AFQUERY_N_FAIL` | 1 (per site) | Samples with a failed call for any ALT allele at the position, each counted once |
| `AFQUERY_N_NO_COVERAGE` | A (per ALT) | Eligible samples whose tech lacks coverage evidence at this position. Always `0` unless a coverage-evidence filter is active. See [Coverage Evidence](../advanced/coverage-evidence.md). |
!!! note "Multi-allelic sites"
diff --git a/docs/guides/annotate-vcf.md b/docs/guides/annotate-vcf.md
index e9d3b4a..f3c1f89 100644
--- a/docs/guides/annotate-vcf.md
+++ b/docs/guides/annotate-vcf.md
@@ -21,13 +21,13 @@ afquery annotate \
| Field | Type | Number | Description |
|-------|------|--------|-------------|
| `AFQUERY_AC` | Integer | A (per ALT) | Allele count in eligible samples |
-| `AFQUERY_AN` | Integer | 1 (per site) | Allele number (total alleles examined) |
-| `AFQUERY_AF` | Float | A (per ALT) | Allele frequency (`AC / AN`) |
+| `AFQUERY_AN` | Integer | 1 (per site) | Allele number (total alleles examined, excluding samples in `AFQUERY_N_FAIL`) |
+| `AFQUERY_AF` | Float | A (per ALT) | Allele frequency (`AC / AN`); `.` when `AFQUERY_AN=0` |
| `AFQUERY_N_HET` | Integer | A (per ALT) | Heterozygous sample count |
| `AFQUERY_N_HOM_ALT` | Integer | A (per ALT) | Homozygous alt sample count |
| `AFQUERY_N_HOM_REF` | Integer | A (per ALT) | Homozygous ref sample count, excluding carriers of other ALT alleles at the position (including alleles stored in the database but absent from the input record) |
-| `AFQUERY_N_FAIL` | Integer | 1 (per site) | Eligible samples whose call had FILTER≠PASS. Excluded from AC, but still counted in AN. Mutually exclusive with N_HET/N_HOM_ALT/N_HOM_REF. |
-| `AFQUERY_N_NO_COVERAGE` | Integer | A (per ALT) | Eligible samples whose tech lacks coverage evidence at this position. Excluded from `N_HOM_REF` to keep AC/AN conservative. Always `0` unless a coverage-evidence filter is active. See [Coverage Evidence](../advanced/coverage-evidence.md). |
+| `AFQUERY_N_FAIL` | Integer | 1 (per site) | Eligible samples with a failed call (FILTER≠PASS) for any ALT allele at the position, each counted once. Excluded from AC and AN. Mutually exclusive with N_HET/N_HOM_ALT/N_HOM_REF. |
+| `AFQUERY_N_NO_COVERAGE` | Integer | A (per ALT) | Eligible samples whose tech lacks coverage evidence at this position. Excluded from `N_HOM_REF`; still counted in AN. Always `0` unless a coverage-evidence filter is active. See [Coverage Evidence](../advanced/coverage-evidence.md). |
!!! note "Multi-allelic sites"
Number=A fields have one value per ALT allele (comma-separated for multi-allelic sites). Number=1 fields are shared across all ALT alleles at the same position.
diff --git a/docs/guides/create-database.md b/docs/guides/create-database.md
index 6e48a08..99fccb1 100644
--- a/docs/guides/create-database.md
+++ b/docs/guides/create-database.md
@@ -93,7 +93,7 @@ afquery create-db --manifest manifest.tsv --output-dir ./db/ --genome-build GRCh
## FILTER=PASS Behavior
-Only `FILTER=PASS` calls (or calls with no FILTER field) contribute to AC, and therefore to AF. AN is not affected — it counts every eligible sample, failed calls included. Calls that fail a filter are tracked in `fail_bitmap` and surfaced as `N_FAIL`. PASS-only counting is always enforced — there is currently no CLI option to change this behaviour.
+Only `FILTER=PASS` calls (or calls with no FILTER field) contribute to AC. Calls that fail a filter are stored in `fail_bitmap`; at query time those samples are left out of AC and AN and reported as `N_FAIL`. Genotypes that carry more alleles than the sample's ploidy (e.g. `1/2` on chrX in a male) are stored the same way, and ingestion logs a warning listing them.
See [FILTER=PASS Tracking](../advanced/filter-pass-tracking.md) for details.
diff --git a/docs/guides/dump-export.md b/docs/guides/dump-export.md
index c8ddf10..2368609 100644
--- a/docs/guides/dump-export.md
+++ b/docs/guides/dump-export.md
@@ -70,6 +70,8 @@ chrom,pos,ref,alt,AC,AN,AF,N_HET,N_HOM_ALT,N_HOM_REF,N_FAIL,N_NO_COVERAGE
`N_NO_COVERAGE` is always emitted but is `0` unless a coverage-evidence
filter is active (see [Coverage Evidence](../advanced/coverage-evidence.md)).
+`AF` and every `AF_` column are left empty when the matching `AN` is 0,
+i.e. when no eligible sample of that group has a usable call at the position.
=== "--by-sex"
diff --git a/docs/guides/query.md b/docs/guides/query.md
index 5b7d064..3f83dc3 100644
--- a/docs/guides/query.md
+++ b/docs/guides/query.md
@@ -151,7 +151,7 @@ hom-ref. With standard variant-only VCFs that assumption can be wrong: a missing
position may simply mean the sample was not sequenced deeply enough at that locus.
Three optional flags let you trade hom-ref aggressiveness for confidence. Samples
that fall below a threshold are reported in **N_NO_COVERAGE** instead of N_HOM_REF
-(they remain in `eligible` and `AN`, like `N_FAIL`).
+(they remain in `eligible` and `AN`).
| Flag | Meaning |
|------|---------|
diff --git a/docs/guides/variant-info.md b/docs/guides/variant-info.md
index f0953ae..cea5429 100644
--- a/docs/guides/variant-info.md
+++ b/docs/guides/variant-info.md
@@ -141,7 +141,7 @@ When `--ref` and `--alt` are specified, the `variant` block contains the actual
|---|---|
| `het` | Heterozygous carrier, FILTER=PASS |
| `hom` | Homozygous alt carrier, FILTER=PASS |
-| `alt` | Non-ref carrier with FILTER≠PASS (ploidy unknown) |
+| `alt` | Failed call (FILTER≠PASS) for any allele at this position; genotype unknown. Listed for every allele of the position, as in `N_FAIL` |
| `no_coverage` | Sample's tech lacks coverage evidence at this position; not a carrier. Only appears when a coverage-evidence filter (`--min-pass`, `--min-observed`, `--min-quality-evidence`, or build-time `--min-covered`) is active. The FILTER column is empty (text/tsv) or `null` (JSON) — `PASS`/`FAIL` does not apply because there is no call. See [Coverage Evidence](../advanced/coverage-evidence.md). |
---
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
index 4f7d707..100da2e 100644
--- a/docs/reference/cli.md
+++ b/docs/reference/cli.md
@@ -70,6 +70,7 @@ Exactly one of `--locus`, `--region`, or `--from-file` must be provided.
| `--min-pass` | INTEGER | `0` | Min PASS carriers (`het\|hom`) per partially-covered tech for hom-ref to be assumed. Non-carriers move to `N_NO_COVERAGE` if a tech falls below the threshold. (0 = disabled) |
| `--min-observed` | INTEGER | `0` | Min any-VCF entries (`het\|hom\|fail`) per partially-covered tech for hom-ref to be assumed. (0 = disabled) |
| `--min-quality-evidence` | INTEGER | `0` | Min quality-passing carriers per partially-covered tech. Requires a database built with `--min-dp`, `--min-gq`, `--min-qual`, or `--min-covered`. (0 = disabled) |
+| `--fail-as-ref` | flag | False | Count samples with a failed call at the position (`N_FAIL`) as hom-ref in AN instead of leaving them out. See [FILTER=PASS Tracking](../advanced/filter-pass-tracking.md#counting-failed-calls-as-reference). |
| `--no-warn` | flag | False | Suppress warnings for unknown phenotypes, technologies, and chromosomes |
---
@@ -95,6 +96,7 @@ afquery annotate [OPTIONS]
| `--min-pass` | INTEGER | `0` | Min PASS carriers per partially-covered tech for hom-ref to be assumed (0 = disabled) |
| `--min-observed` | INTEGER | `0` | Min any-VCF entries per partially-covered tech (0 = disabled) |
| `--min-quality-evidence` | INTEGER | `0` | Min quality-passing carriers per partially-covered tech. Requires a database built with `--min-dp`, `--min-gq`, `--min-qual`, or `--min-covered`. (0 = disabled) |
+| `--fail-as-ref` | flag | False | Count samples with a failed call at the position (`N_FAIL`) as hom-ref in AN instead of leaving them out. See [FILTER=PASS Tracking](../advanced/filter-pass-tracking.md#counting-failed-calls-as-reference). |
| `--no-warn` | flag | False | Suppress warnings for unknown phenotypes, technologies, and chromosomes |
The annotated VCF gains `AFQUERY_AC`, `AFQUERY_AN`, `AFQUERY_AF`,
@@ -131,6 +133,7 @@ afquery dump [OPTIONS]
| `--min-pass` | INTEGER | `0` | Min PASS carriers per partially-covered tech for hom-ref to be assumed (0 = disabled) |
| `--min-observed` | INTEGER | `0` | Min any-VCF entries per partially-covered tech (0 = disabled) |
| `--min-quality-evidence` | INTEGER | `0` | Min quality-passing carriers per partially-covered tech. Requires a database built with `--min-dp`, `--min-gq`, `--min-qual`, or `--min-covered`. (0 = disabled) |
+| `--fail-as-ref` | flag | False | Count samples with a failed call at the position (`N_FAIL`) as hom-ref in AN instead of leaving them out. See [FILTER=PASS Tracking](../advanced/filter-pass-tracking.md#counting-failed-calls-as-reference). |
CSV output adds an `N_NO_COVERAGE` column (and per-group variants
`N_NO_COVERAGE_