just_dna_enricher.civic_citations¶
just_dna_enricher.civic_citations ¶
RM160 — the citations a CIViC variant carries that no dated file can reach, and the canary on them.
civic build reads a dated bulk release; RM169 widened that as far as a dated file can go. The one
thing left over is structural: the wider basis comes from a VCF, a VCF record needs a POS, and CIViC
publishes no GRCh37 coordinate for a variant it names as a class of event or as a legacy notation. So
the submitted evidence attached to those variants is published on exactly one surface, the GraphQL
API, and ten records' citations are unreachable from every file the builder reads. Variant 1955 is the
worked case: its only evidence for the numbering convention that would settle its identity is EID
9969 (PMID 12202531, free full text), submitted, in the API and in no file.
What lands, and where. A recovered citation is a studies.csv row — the authored table that holds
an (rsid, pmid) evidence link — and nothing writes literature.csv here. That is the pairing the
format already has: literature.csv is the derived article table, produced by the literature pass
from the PMIDs studies.csv names, and an article row nothing cites is dropped from the artifact
(@uncited-literature-dropped). Writing one here would either duplicate that pass or add a row the
compiler discards; drafting the citing row and letting literature fill the article is the shape that
works in both directions.
Three ways a module row reaches a CIViC variant id, and the third exists because the first two miss
the motivating class. The snapshot's own coordinate join is the ordinary route, through
clinical.comparison_plan so this lane asks about the same alleles the other CIViC leg does. The
curated name-identity table (civic_identities) is how a variant whose identity CIViC publishes only
inside its name is reachable at all. Neither reaches variant 1955 — it is one of the two records the
identity round could not resolve, which is why its citations mattered in the first place — so
--variant-id asks about a variant id directly and writes a module-level citation row, which
StudyRow has permitted since RM47: a row may name no variant and ground the module instead.
status rides as confidence/confidence_unit, unconverted. CIViC's own instrument named rather
than translated into a house grade, so an accepted row and a submitted row are not the same row once
both are in the file. Where several evidence items cite one paper and their statuses disagree, the
confidence is withheld rather than picked: the value is unknown, and unknown is never written down
as an answer.
Rejected evidence is not drafted. status: ALL returns items CIViC's editors threw out, and a
citation whose every item is rejected is content the source itself has repudiated — counted with its
own reason, never silently dropped, and never written into a module as though the source stood behind
it. Where a rejected item sits beside a live one for the same paper, the live ones decide the row.
The pin, and why it is on the SourceRow. Every drafted row records when the API was asked and on
what basis — fetched_at and dataset on the (civic, literature) row — rather than restating a
timestamp inside each conclusion. A prose timestamp would put the moment of the read into
content_signature, and a moment is not a claim about a variant. The pin records the ask that first
put a recovered citation into this module: merge_sources_csv is never-clobber by design, so a later
run adding more rows does not move it. That is a floor on not asked since, and
check_evidence_status_currency below is what closes the gap — it re-asks and reports what moved.
The canary never gates. A source re-curating its own evidence is not an authoring error
(@a-source-recuring-is-not-a-strict-matter), so both findings warn in both modes. It is also a
different question from dataset_currency, which asks which release a table came from; this one
asks whether a per-item judgement has moved, and the two currency findings stay apart.
CivicCitationsError ¶
Bases: RuntimeError
This lane could not do its job — an unreadable licence table, a spec it cannot write into.
Its own type rather than the client's: a caller of the pass is told to catch this, and letting
CivicApiError out would make an outage and a broken module indistinguishable at the handler
(@client-exception-contract). The API's own failures never reach here — they are recorded per
subject as unreachable, which is the withhold rather than a raise.
CivicSubject
dataclass
¶
CivicSubject(
variant_id: int,
route: str,
rsid: str | None = None,
chrom: str | None = None,
start: int | None = None,
ref: str | None = None,
civic_name: str | None = None,
)
One CIViC variant this run can ask about, and the module row it was reached from.
The identity cells are the authored row's, not the snapshot's: they are what the drafted
citation carries, so a re-run matches on the same signature and appends nothing. A requested
subject carries none of them — it grounds the module rather than a variant.
identity_cells
property
¶
The identity a drafted citation carries — rsID, or the coordinate, never both.
A study row must carry the same identity its variant row got, which is the rule
clinvar_draft states in its own comment after the compiler's orphan check found it on the
first real panel. It matters twice here. A row written with rsID and coordinate has a
different match_on signature from the rsID-only rows civic_draft and clinvar_draft
write, so one lane's citation would not recognise the other's and the file would grow a
second row under the same (variant_key, pmid) — duplicate_study_citation on every module
that ran both. And the pair would state a locus twice where the model already derives one.
study_key
property
¶
The variant_key a citation drafted for this subject will carry, or None.
The same derivation StudyRow.variant_key runs, over the same cells, so the canary joins on
what the model computes rather than on a tuple of raw cells this module spells its own way.
None for a requested subject: it names no variant, so its citations ground the module.
restate ¶
CIViC variant 1955 (VHL P71fs (c.211insT)) — what a line puts in front of an author.
CivicCitationsResult
dataclass
¶
CivicCitationsResult(
subjects: list[CivicSubject] = list(),
unmapped_rows: int = 0,
unreachable: dict[int, str] = dict(),
citations_seen: int = 0,
withheld: dict[str, int] = dict(),
confidence_withheld: int = 0,
reports: list[DraftReport] = list(),
warnings: list[str] = list(),
sources: list[SourceRow] = list(),
offline: bool = False,
)
What one civic citations run asked, drafted and withheld.
asked
property
¶
Subjects the API really answered about — the denominator any count here is out of.
RecoveredCitation
dataclass
¶
One PubMed citation a CIViC variant carries, and every live item behind it.
status
property
¶
The one status every live item agrees on, or None when they do not.
Withheld rather than resolved. Two items that disagree about whether an editor has signed this paper's evidence off state two facts, and picking one would publish a guess in a column whose whole purpose is to carry the source's own word.
conclusion ¶
The prose beside the row. Deterministic, and deliberately carrying no timestamp.
When the API was asked is on the SourceRow; putting it here would fold the moment of a read
into content_signature, where only claims belong.
Source code in enricher/src/just_dna_enricher/civic_citations.py
MovedCitation
dataclass
¶
MovedCitation(
code: str,
subject: CivicSubject,
pmid: str,
recorded: str | None,
current: str | None,
)
One recorded citation whose answer at CIViC has moved since it was drafted.
EvidenceStatusCheck
dataclass
¶
EvidenceStatusCheck(
recorded: int = 0,
subjects: int = 0,
findings: list[MovedCitation] = list(),
not_re_askable: int = 0,
unreachable: dict[int, str] = dict(),
skip: str | None = None,
)
What the canary compared, and what it found. Never a stand-in for a check that could not run.
skip is the closed reason this run put no question, or None when it did. A dataclass with a
reason rather than a bare None return, because there are four ways not to run here and they are
cleared by four different things — a verdict with several arms owes a reason with the same arms
(@answered-is-not-absent).
detail ¶
The sentence beside the machine key, on every run including the empty one.
Source code in enricher/src/just_dna_enricher/civic_citations.py
read_module ¶
The two authored/injected tables this lane joins on, loaded the way every other reader does.
The build re-stamp is not optional (@restamp-for-build). VariantRow._freeze_identity runs
at construction, where the module's yaml is not in scope, so every row comes back keyed for
GRCh38; the compiler fixes that after load at both of its sites and enrich at its own. A reader
that skipped it would join a GRCh37 module's rows against a table keyed differently and silently
match nothing.
genome_build is the caller's, not this function's: enrich already holds the module's declared
build and the CLI reads it with enrich.spec_genome_build, and importing that here would make the
two modules import each other.
A missing table is an empty list rather than an error: a module with no resolution.csv has
nothing this lane can place, which the caller reports as a subject it could not map.
Source code in enricher/src/just_dna_enricher/civic_citations.py
read_studies ¶
The module's citation table, or [] when it carries none — and never fatal.
A table that will not parse is logged and read as empty rather than raised, because the one caller
is a read-only check folded into a pass that has other work to do: enrich would otherwise
start failing on a module whose studies.csv it never used to open, and the compiler is where an
invalid authored table is diagnosed. litvar._read_table takes the same line for the same reason.
Source code in enricher/src/just_dna_enricher/civic_citations.py
plan_subjects ¶
plan_subjects(
variants: Sequence[VariantRow],
resolution_rows: Sequence[ResolutionRow],
*,
reference: Path | None,
requested: Iterable[int] = (),
) -> tuple[list[CivicSubject], int]
(subjects, authored rows that mapped to no CIViC variant).
Matching goes through comparison_plan, the same resolved-coordinate route the ClinVar
cross-check and the CIViC refutation leg use, so every CIViC question in this tier is asked about
the same alleles — never about an rsID, which is position-level and would ask about a locus rather
than an allele.
The snapshot is consulted first and the curated table second, because the snapshot is the source speaking for itself and the curated table is this workspace's reading of a name. A row matching both gets the snapshot's answer and the route says so.
Source code in enricher/src/just_dna_enricher/civic_citations.py
draft_civic_citations ¶
draft_civic_citations(
spec_dir: Path,
*,
variants: Sequence[VariantRow],
resolution_rows: Sequence[ResolutionRow],
reference: Path | None,
requested: Iterable[int] = (),
client: CivicApiClient | None = None,
offline: bool = False,
dry_run: bool = False,
) -> CivicCitationsResult
Append the citations a CIViC variant carries and the local basis does not.
One request per subject by construction — evidenceItems takes a single variantId — which
is why this fits enrich-time tooling and would not fit civic build: a builder batching it
would be one request per variant of the whole database, and its output could not be pinned to a
dated release. Batching it into the builder is the first repair anyone proposes and it is the
bargain RM160's shape 3 refused.
--offline refuses rather than answering empty: every subject is recorded as unreachable with the
offline reason, and no row is written. Nobody asked and the source has nothing more are
different facts (@unreachable-not-absent).
Source code in enricher/src/just_dna_enricher/civic_citations.py
469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 | |
recorded_civic_citations ¶
variant_key → {pmid: recorded status} for every row this lane wrote.
Keyed on confidence_unit, which is the instrument name and therefore the only cell that says a
status came from CIViC. A row whose confidence was withheld carries no unit and is not a subject:
there is no recorded judgement to compare against, and inventing one would be the check answering
its own question.
Joined on StudyRow.variant_key — the model's own derivation over the same cells — rather than on
a tuple of raw columns this module spells its own way. A row may legitimately name its variant by
rsID where another names it by coordinate, and a raw compare would read those as two subjects.
Source code in enricher/src/just_dna_enricher/civic_citations.py
grounding_civic_citations ¶
How many rows this lane wrote that name no variant — the --variant-id route's output.
They ground the module rather than a variant, which is the only shape that reaches a record CIViC publishes no identity for. The cost is that no later run can map one back to a CIViC variant id, and that is a fact about the check's reach: counted and published, never read as agreement.
Source code in enricher/src/just_dna_enricher/civic_citations.py
cited_pmids ¶
variant_key → every PMID the module cites there, whatever wrote the row.
Deliberately wider than recorded_civic_citations, and the width is the whole point: the
added-a-citation arm asks whether the module has a row for a paper CIViC now returns, and a row
written by draft-panel, by hand, or by this lane with its confidence withheld is a row all the
same. Keying that arm on the narrow set would report CIViC now carries PMID X and this module has
no row for it about a citation sitting in the file, on every lap
(@a-set-that-silences-is-narrower-than-one-that-raises).
Source code in enricher/src/just_dna_enricher/civic_citations.py
check_evidence_status_currency ¶
check_evidence_status_currency(
variants: Sequence[VariantRow],
resolution_rows: Sequence[ResolutionRow],
studies: Sequence[StudyRow],
*,
reference: Path | None,
client: CivicApiClient | None = None,
offline: bool = False,
) -> EvidenceStatusCheck
Re-ask CIViC about the citations this module recorded from it, and report what has moved.
Three moves, two codes. An item accepted since the draft and one rejected since are the same
finding on the status axis with the same remedy — read what CIViC now says about a row already in
the file — while a citation CIViC has added since is a different sentence with a different remedy,
which is re-running civic citations (@warning-code-names-the-finding).
Reports and repairs nothing (@enrichment-is-validation), and never escalates: a source
re-curating its own evidence is a fact about CIViC, and rewriting an authored cell from a live read
is the one thing an enricher pass may not do.
A run that could not put the question says which of four reasons applies rather than reporting a comparison of zeros — a check that could not run is not a check that passed.
Source code in enricher/src/just_dna_enricher/civic_citations.py
720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 | |