diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 10effbd..ae61765 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -235,13 +235,19 @@ you anything tends not to get run.
## Documentation
-The website is plain HTML in `docs/` (`index.html`, `reference.html` and
-`assets/`), with no build step: open `docs/index.html` to see a change. The
-documentation workflow runs `tools/check_site.py`, which fails on a dead link or
-anchor, an option `recore --help` does not have, an option missing from the
-reference, a default that differs from the binary's, or an example run whose
-counts differ from the page's, instead of a stale page somebody finds six
-months later. The long-form pages are markdown under `docs/` and read on GitHub.
+The website is plain HTML in `docs/` (`index.html`, `how-it-works.html`,
+`reference.html` and `assets/`): open `docs/index.html` to see a change. The
+drawings of How it works (`assets/how.js`) decide with a copy of
+`evidence_state` and `matrix_policy_corrects` from `src/pipeline.rs`; change
+them together. The Docs tab is the markdown under `docs/`, turned into pages by
+`tools/build_docs.py`; after editing a `.md` file, run it and commit what it
+writes. The documentation workflow runs `tools/check_site.py`, which fails on a
+documentation page that no longer matches its markdown, a dead link or anchor,
+an option `recore --help` does not have, an option missing from the reference,
+a default that differs from the binary's, a walkthrough that decides
+differently from the code, or an example run whose counts differ from the
+page's, instead of a stale page somebody finds six months later. The markdown
+also reads on GitHub as it is.
`docs/assets/example-report.html` is the tutorial run's own output, linked from
the [Run report](../docs/guide/report.md) page. If you change the viewer, that
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 3fecd4a..e67b9ae 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -2,15 +2,18 @@ name: Documentation
# Checks the website in docs/ against the code it describes.
#
-# The site is plain HTML with no build step (docs/index.html, reference.html and
-# assets/), so there is nothing to build or deploy: GitHub Pages can serve the
-# folder as it is, with Settings -> Pages -> "Deploy from a branch" -> main ->
-# /docs, once the repository is public or the organisation is on a paid plan.
-# What can still go wrong is drift, and tools/check_site.py fails on it: a dead
-# link or anchor, an option the pages show that `recore --help` does not have,
-# an option or a default the reference gets wrong, and an example run whose
-# counts are not the ones the page promises. That last part runs the "Try it"
-# commands for real, which is why this needs minimap2 and a build.
+# The site is plain HTML with no build step (docs/index.html, how-it-works.html,
+# reference.html and assets/), so there is nothing to build or deploy: GitHub
+# Pages can serve the folder as it is, with Settings -> Pages -> "Deploy from a
+# branch" -> main -> /docs, once the repository is public or the organisation is
+# on a paid plan. The Docs tab is built from the markdown by tools/build_docs.py
+# and committed. What can still go wrong is drift, and tools/check_site.py fails
+# on it: a documentation page that no longer matches its markdown, a dead link
+# or anchor, an option the pages show that `recore --help` does not have, an
+# option or a default the reference gets wrong, a walkthrough that decides
+# differently from src/pipeline.rs, and an example run whose counts are not the
+# ones the page promises. That last part runs the "Try it" commands for real,
+# which is why this needs minimap2 and a build.
#
# The workflow and job names are what the Dependabot auto-merge gate waits for
# ("Documentation" / "Build"); tools/check_automerge_gate.py fails CI if they
@@ -27,6 +30,7 @@ on:
- "Cargo.lock"
- "benchmarks/make_tutorial_dataset.py"
- "tools/check_site.py"
+ - "tools/build_docs.py"
- ".github/workflows/docs.yml"
pull_request:
paths:
@@ -36,6 +40,7 @@ on:
- "Cargo.lock"
- "benchmarks/make_tutorial_dataset.py"
- "tools/check_site.py"
+ - "tools/build_docs.py"
- ".github/workflows/docs.yml"
workflow_dispatch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b736bda..e0a5f08 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -104,6 +104,15 @@ and Recore uses semantic versioning.
embedded report follows; pointing at a cell of the opening matrix says what
happened to it; the policy section shows which of ten found genes each policy
puts back; and GitHub is a button in the header, the opening and the end.
+ A Docs tab carries every long page in the site's design, with a page list,
+ contents that follow the reading, previous and next links, a search box over
+ all of them and an edit link to GitHub; `tools/build_docs.py` builds the pages
+ from the markdown, which stays the source and still reads on GitHub. Code
+ boxes are light in the light theme. A How it works page follows one empty
+ cell through seven animated drawings, from the search to what gets written
+ down: pick what the search finds, whether the neighbours agree and the
+ policy, and the drawings decide by the same rules as the code, which
+ `tools/check_site.py` holds them to.
- `publication_status` in `orthology_evidence.tsv` follows `matrix_action`.
Evidence the matrix policy or a quality floor kept out reads `held_out_policy`
or `held_out_quality` instead of a `corrected...` value, and a strict
diff --git a/README.md b/README.md
index 4db86ca..5443557 100644
--- a/README.md
+++ b/README.md
@@ -97,14 +97,17 @@ with eleven false absences planted in it and walks through the run.
The quickest way in is the website in [`docs/`](docs/index.html): an install
line, a command builder for your pangenome tool, a one-minute example, the
-example report to click through, and one reference page with every option,
-output file and cell state. It is plain HTML with no build step, so open
-`docs/index.html` from a clone; once the repository is public, GitHub Pages can
-serve it straight from `main` and `/docs`. `tools/check_site.py` keeps it
-honest in CI: every option it shows exists, the defaults match `recore --help`,
-and the example run gives the numbers the page promises.
-
-The long versions are markdown files in this repository and read here on GitHub.
+example report to click through, an illustrated walk through how one empty cell
+is decided, a reference page with every option, output file and cell state, and
+a Docs tab with every page below, searchable. It is plain HTML with no build
+step, so open `docs/index.html` from a clone; once the repository is public,
+GitHub Pages can serve it straight from `main` and `/docs`. The Docs pages are
+built from the markdown by `tools/build_docs.py`, and `tools/check_site.py`
+keeps the site honest in CI: the pages match their markdown, every option it
+shows exists, the defaults match `recore --help`, the walkthrough decides by
+the code's rules, and the example run gives the numbers the page promises.
+
+The same pages are markdown files in this repository and read here on GitHub.
| Start here | |
|---|---|
diff --git a/docs/about/citation.html b/docs/about/citation.html
new file mode 100644
index 0000000..7b20cdb
--- /dev/null
+++ b/docs/about/citation.html
@@ -0,0 +1,125 @@
+
+
+
Ruiz-Rodriguez P, et al. (2026). Recore: post-hoc rescue and quality control of pangenome presence/absence matrices. In preparation.
+
@misc{recore,
+ author = {Ruiz-Rodriguez, Paula and others},
+ title = {Recore: post-hoc rescue and quality control of pangenome
+ presence/absence matrices},
+ year = {2026},
+ note = {In preparation},
+ howpublished = {\url{https://github.com/PathoGenOmics-Lab/recore}}
+}
+
Please also cite the tools Recore depends on for the run you report:
+
minimap2, for every sequence search: Li H. (2018). Minimap2: pairwise alignment for nucleotide sequences. Bioinformatics 34(18):3094-3100.
MMseqs2, if you used --nsc-engine mmseqs2: Steinegger M, Soeding J. (2017). MMseqs2 enables sensitive protein sequence searching for the analysis of massive data sets. Nature Biotechnology 35:1026-1028.
the pangenome tool that produced the input matrix, for example Panaroo: Tonkin-Hill G, et al. (2020). Producing polished prokaryotic pangenomes with the Panaroo pipeline. Genome Biology 21:180.
The end-to-end test needs minimap2 on PATH. It skips silently without one, which is not what you want in CI, so CI exports the guard that turns a skip into a failure, and you should too:
+
RECORE_REQUIRE_MINIMAP2=1 cargo test --locked --all-targets
+
The benchmarks are not part of CI. Run them by hand when a change could move the numbers:
The report's assets are inlined into the binary at compile time from src/pipeline/report_assets/:
+
File
Holds
report.css
the whole stylesheet
explorer.js
the phylogeny, matrix and synteny viewer
report-ui.js
theme, drawer, sortable tables, tooltips
+
Two rules the report has to keep:
+
No remote assets. It has to open from a results directory with no network. The integration test walks every link, script, img, iframe and use element and fails if any of them loads from a URL. Links to the project are fine; loading a resource is not.
Nothing may assume the whole matrix fits. Every pane draws only its visible window onto a viewport-sized canvas. A change that iterates all cells per frame will be fine on the test fixture and unusable at 30,000 clusters.
+
To look at a change, generate a report and open it over HTTP. Opening the file directly with file:// works, but some browsers block the inline script and you get an empty workspace with no error.
The website is docs/index.html, docs/how-it-works.html and docs/reference.html, styled by docs/assets/site.css and driven by docs/assets/site.js: plain files, so opening docs/index.html in a browser shows a change. Keep the front page short. It is there to get someone running in two minutes; the details belong in the documentation.
+
The drawings of How it works are docs/assets/how.js. They decide with a copy of evidence_state and matrix_policy_corrects from src/pipeline.rs, so a change to those rules is a change there too; tools/check_site.py fails if the policy table, a state, a rescue type, a matrix action or a column no longer matches the code.
+
The Docs tab is these markdown pages. tools/build_docs.py turns each one into an .html page next to it, with the site's header, sidebar, contents and search, and writes docs/documentation.html and the search index. The pages are committed, so after editing a .md file, rebuild them:
+
python3 tools/build_docs.py
+
A new page is a line in PAGES at the top of that script.
That fails on a documentation page that no longer matches its markdown, a dead link or anchor, an option the site mentions that recore --help does not have, an option missing from the reference, a default that differs from the binary's, an em-dash, and an example run whose counts differ from the ones the page shows.
+
docs/assets/example-report.html is the report the website and the Run report page link to. It is the tutorial run's own output, so regenerate it whenever the viewer changes:
The two logo variants and the report's dark-mode colour overrides all come from one source file, so regenerate those together rather than editing either by hand:
Open an issue at github.com/PathoGenOmics-Lab/recore/issues. For a wrong call, the most useful thing to include is the row from orthology_evidence.tsv: it carries the evidence state, the adaptive guard floors, the confidence tier and the risk flags, which together explain why Recore decided what it did.
+
+
+
+
+
diff --git a/docs/about/contributing.md b/docs/about/contributing.md
index 2fbb602..f455f6e 100644
--- a/docs/about/contributing.md
+++ b/docs/about/contributing.md
@@ -57,11 +57,28 @@ python3 -m http.server 8000 --directory out/
## Documentation
-The website is `docs/index.html` and `docs/reference.html`, styled by
-`docs/assets/site.css` and driven by `docs/assets/site.js`: plain files with no
-build step, so opening `docs/index.html` in a browser shows a change. Keep it
-short. It is there to get someone running in two minutes; the details belong in
-the markdown pages it links to.
+The website is `docs/index.html`, `docs/how-it-works.html` and
+`docs/reference.html`, styled by `docs/assets/site.css` and driven by
+`docs/assets/site.js`: plain files, so opening `docs/index.html` in a browser
+shows a change. Keep the front page short. It is there to get someone running in
+two minutes; the details belong in the documentation.
+
+The drawings of How it works are `docs/assets/how.js`. They decide with a copy
+of `evidence_state` and `matrix_policy_corrects` from `src/pipeline.rs`, so a
+change to those rules is a change there too; `tools/check_site.py` fails if the
+policy table, a state, a rescue type, a matrix action or a column no longer
+matches the code.
+
+The Docs tab is these markdown pages. `tools/build_docs.py` turns each one into
+an `.html` page next to it, with the site's header, sidebar, contents and
+search, and writes `docs/documentation.html` and the search index. The pages
+are committed, so after editing a `.md` file, rebuild them:
+
+```bash
+python3 tools/build_docs.py
+```
+
+A new page is a line in `PAGES` at the top of that script.
Check it the way CI does:
@@ -70,7 +87,8 @@ cargo build
python3 tools/check_site.py --recore target/debug/recore --run-tutorial
```
-That fails on a dead link or anchor, an option the site mentions that
+That fails on a documentation page that no longer matches its markdown, a
+dead link or anchor, an option the site mentions that
`recore --help` does not have, an option missing from the reference, a default
that differs from the binary's, an em-dash, and an example run whose counts
differ from the ones the page shows.
diff --git a/docs/assets/how.js b/docs/assets/how.js
new file mode 100644
index 0000000..945d710
--- /dev/null
+++ b/docs/assets/how.js
@@ -0,0 +1,775 @@
+/* How Recore works: seven illustrated steps for one empty cell, driven by what
+ the search finds, whether the neighbours agree, and the matrix policy. The
+ decisions follow evidence_state and matrix_policy_corrects in src/pipeline.rs. */
+(function () {
+ "use strict";
+
+ var stage = document.querySelector("[data-stage]");
+ if (!stage) return;
+
+ var NS = "http://www.w3.org/2000/svg";
+ var reduceMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+
+ // `type` is the rescue type's name; `written` is how rescue_type reads in
+ // orthology_evidence.tsv (tools/check_site.py holds both to the code).
+ var SCENARIOS = [
+ { id: "annotated", label: "The whole gene, annotated", type: "DROPPED", written: "DROPPED", identity: 99.6, coverage: 100 },
+ { id: "unannotated", label: "The whole gene, nothing annotated", type: "MISSING_CDS", written: "MISSING_CDS", identity: 99.4, coverage: 100 },
+ { id: "split", label: "The same gene, in another cluster", type: "SPLIT", written: "SPLIT:cluster_Y", identity: 99.2, coverage: 100 },
+ { id: "stop", label: "The gene, with a stop codon", type: "PSEUDOGENE", written: "PSEUDOGENE:1stops", identity: 99.1, coverage: 100 },
+ { id: "pieces", label: "The gene in two pieces", type: "FRAGMENTED", written: "FRAGMENTED:2frags:96%cov", identity: 99.0, coverage: 96 },
+ { id: "paralog", label: "A related gene", type: "PARALOG", written: "PARALOG:cluster_Z", identity: 92.4, coverage: 97 },
+ { id: "none", label: "Nothing", type: "", written: "", identity: 0, coverage: 0 }
+ ];
+
+ var CORRECTS = {
+ strict: ["STRICT_PRESENT", "ALIAS_CLUSTER"],
+ balanced: ["STRICT_PRESENT", "ALIAS_CLUSTER", "SEQUENCE_ONLY"],
+ sensitive: ["STRICT_PRESENT", "ALIAS_CLUSTER", "SEQUENCE_ONLY", "PARTIAL_HOMOLOGY", "PSEUDOGENE"]
+ };
+
+ var state = { step: 0, scenario: "annotated", synteny: "agree", policy: "balanced" };
+ var timers = [];
+ var playTimer = null;
+
+ function scenario() {
+ return SCENARIOS.filter(function (s) {
+ return s.id === state.scenario;
+ })[0];
+ }
+
+ function evidenceState() {
+ var sc = scenario();
+ if (sc.id === "none") return "ABSENT";
+ if (state.synteny === "differ") return "REJECTED_CONTEXT";
+ var agree = state.synteny === "agree";
+ switch (sc.id) {
+ case "annotated":
+ return agree ? "STRICT_PRESENT" : "SEQUENCE_ONLY";
+ case "unannotated":
+ return agree ? "STRICT_PRESENT" : "MISSING_CDS_UNVALIDATED";
+ case "split":
+ return "ALIAS_CLUSTER";
+ case "stop":
+ return "PSEUDOGENE";
+ case "pieces":
+ return "PARTIAL_HOMOLOGY";
+ default:
+ return agree ? "STRICT_PRESENT" : "HOMOLOG_PARALOG";
+ }
+ }
+
+ function rejected() {
+ var s = evidenceState();
+ return s === "REJECTED_CONTEXT" || (s === "HOMOLOG_PARALOG" && state.synteny !== "agree");
+ }
+
+ function outcome() {
+ var s = evidenceState();
+ if (s === "ABSENT") return "absent";
+ return CORRECTS[state.policy].indexOf(s) !== -1 ? "putback" : "kept";
+ }
+
+ function matrixAction() {
+ if (outcome() === "putback") return "correct_presence";
+ return rejected() ? "audit_only" : "audit_only_policy";
+ }
+
+ /* ── SVG helpers ── */
+
+ function el(tag, attrs, parent) {
+ var node = document.createElementNS(NS, tag);
+ Object.keys(attrs || {}).forEach(function (k) {
+ node.setAttribute(k, attrs[k]);
+ });
+ if (parent) parent.appendChild(node);
+ return node;
+ }
+
+ function text(parent, x, y, content, attrs) {
+ var a = attrs || {};
+ a.x = x;
+ a.y = y;
+ var t = el("text", a, parent);
+ t.textContent = content;
+ return t;
+ }
+
+ // A gene drawn as an arrow pointing right.
+ function gene(parent, x, y, w, cls, label) {
+ var h = 30;
+ var g = el("g", { class: "gene " + cls }, parent);
+ el("path", { d: "M" + x + " " + y + "h" + (w - 12) + "l12 " + h / 2 + "l-12 " + h / 2 + "h-" + (w - 12) + "z" }, g);
+ if (label) text(g, x + (w - 6) / 2, y + h / 2 + 5, label, { class: "gene-label", "text-anchor": "middle" });
+ return g;
+ }
+
+ // Elements enter with a CSS animation after `delay` seconds.
+ function enter(node, kind, delay) {
+ node.classList.add("in-" + kind);
+ node.style.animationDelay = (delay || 0) + "s";
+ return node;
+ }
+
+ function later(ms, fn) {
+ timers.push(setTimeout(fn, reduceMotion ? 0 : ms));
+ }
+
+ function animate(node, frames, options) {
+ if (reduceMotion || !node.animate) {
+ var last = frames[frames.length - 1];
+ Object.keys(last).forEach(function (k) {
+ if (k !== "offset" && k !== "easing") node.style[k] = last[k];
+ });
+ return;
+ }
+ node.animate(frames, Object.assign({ fill: "forwards", easing: "cubic-bezier(.4,0,.2,1)" }, options));
+ }
+
+ function countUp(node, target, suffix, ms) {
+ if (reduceMotion) {
+ node.textContent = target.toFixed(1) + suffix;
+ return;
+ }
+ var start = performance.now();
+ function tick(now) {
+ var t = Math.min(1, (now - start) / ms);
+ node.textContent = (target * (1 - Math.pow(1 - t, 3))).toFixed(1) + suffix;
+ if (t < 1 && node.isConnected) requestAnimationFrame(tick);
+ }
+ requestAnimationFrame(tick);
+ }
+
+ // Rendered width of a text node, which grows with the phone font sizes.
+ function measure(node, fallback) {
+ try {
+ var n = node.getComputedTextLength();
+ return n > 0 ? n : fallback;
+ } catch (e) {
+ return fallback;
+ }
+ }
+
+ function badge(parent, x, y, label, cls) {
+ var g = el("g", { class: "badge-svg " + (cls || "") }, parent);
+ var rect = el("rect", { y: y - 19, height: 38, rx: 19 }, g);
+ var t = text(g, x, y + 6, label, { "text-anchor": "middle" });
+ var w = Math.max(90, measure(t, label.length * 10.5) + 28);
+ rect.setAttribute("x", x - w / 2);
+ rect.setAttribute("width", w);
+ return g;
+ }
+
+ /* ── The seven scenes ── */
+
+ function sceneMatrix(svg) {
+ var cols = ["A", "B", "X", "C", "D", "E", "F"];
+ var x0 = 186;
+ var y0 = 88;
+ var cw = 58;
+ var ch = 42;
+ text(svg, x0 + (cols.length * cw) / 2, 40, "genes", { class: "axis", "text-anchor": "middle" });
+ text(svg, 70, y0 + (5 * ch) / 2, "genomes", { class: "axis", "text-anchor": "middle", transform: "rotate(-90 70 " + (y0 + (5 * ch) / 2) + ")" });
+ cols.forEach(function (c, i) {
+ var t = text(svg, x0 + i * cw + cw / 2 - 3, y0 - 14, c === "X" ? "gene X" : c, { class: "col-label" + (c === "X" ? " is-x" : ""), "text-anchor": "middle" });
+ enter(t, "fade", 0.1);
+ });
+ var empty = { "2,2": "target", "4,6": "gap", "1,5": "gap", "3,0": "gap" };
+ for (var r = 0; r < 5; r++) {
+ var label = text(svg, x0 - 18, y0 + r * ch + ch / 2 + 2, "G" + (r + 1), { class: "row-label" + (r === 2 ? " is-x" : ""), "text-anchor": "end" });
+ enter(label, "fade", 0.1 + r * 0.06);
+ for (var c = 0; c < cols.length; c++) {
+ var kind = empty[r + "," + c];
+ var cell = el("rect", {
+ x: x0 + c * cw,
+ y: y0 + r * ch,
+ width: cw - 8,
+ height: ch - 8,
+ rx: 6,
+ class: "m-cell " + (kind === "target" ? "m-target" : kind ? "m-empty" : "m-present")
+ }, svg);
+ enter(cell, "pop", 0.15 + r * 0.07 + c * 0.02);
+ }
+ }
+ var cx = x0 + 2 * cw + (cw - 8) / 2;
+ var cy = y0 + 2 * ch + (ch - 8) / 2;
+ enter(text(svg, cx, cy + 7, "?", { class: "m-question", "text-anchor": "middle" }), "pop", 1.0);
+ var note = text(svg, x0 + (cols.length * cw) / 2 - 4, y0 + 5 * ch + 34, "gene X in G3: absent?", { class: "callout-text", "text-anchor": "middle" });
+ enter(note, "fade", 1.3);
+ }
+
+ function sceneSequence(svg) {
+ var file = el("g", { class: "file" }, svg);
+ el("path", { d: "M60 92h92l26 26v150h-118z" }, file);
+ el("path", { class: "file-fold", d: "M152 92v26h26" }, file);
+ for (var i = 0; i < 6; i++) {
+ el("rect", { class: "file-line" + (i === 2 ? " is-x" : ""), x: 76, y: 134 + i * 18, width: i % 2 ? 66 : 84, height: 6, rx: 3 }, file);
+ }
+ text(svg, 119, 300, "pan_genome_reference.fa", { class: "small-label", "text-anchor": "middle" });
+ enter(file, "fade", 0);
+
+ var moving = el("g", {}, svg);
+ gene(moving, 300, 150, 300, "gx", "gene X");
+ animate(moving, [{ transform: "translate(-200px, 40px) scale(.5)", opacity: 0 }, { transform: "translate(0, 0) scale(1)", opacity: 1 }], { duration: 1100, delay: 300 });
+
+ var bases = ["a", "c", "g", "t"];
+ var strip = el("g", { class: "bases" }, svg);
+ for (var k = 0; k < 36; k++) {
+ var b = el("rect", { class: "base base-" + bases[(k * 7 + 3) % 4], x: 300 + k * 8.3, y: 206, width: 6.5, height: 22, rx: 2 }, strip);
+ enter(b, "pop", 1.2 + k * 0.025);
+ }
+ enter(text(svg, 450, 262, "gene X, 1,281 bp", { class: "small-label", "text-anchor": "middle" }), "fade", 2.0);
+ var arrow = el("path", { class: "flow-arrow", d: "M190 180 C 230 180, 250 170, 290 168", pathLength: 1 }, svg);
+ enter(arrow, "draw", 0.4);
+ }
+
+ function sceneSearch(svg) {
+ var sc = scenario();
+ text(svg, 330, 34, "genome G3's assembly", { class: "axis", "text-anchor": "middle" });
+ var cell = el("rect", { class: "bact", x: 120, y: 56, width: 420, height: 270, rx: 135 }, svg);
+ enter(cell, "fade", 0);
+ var cx = 330;
+ var cy = 191;
+ var r = 92;
+ el("circle", { class: "chromosome", cx: cx, cy: cy, r: r }, svg);
+ // Genes along the chromosome, as coloured arcs.
+ var palette = ["ga", "gb", "gc", "gd", "gp", "ga", "gc", "gb", "gd", "gp", "gc", "ga"];
+ palette.forEach(function (cls, i) {
+ var a0 = (i / palette.length) * Math.PI * 2 - Math.PI / 2;
+ var a1 = a0 + (Math.PI * 2) / palette.length - 0.14;
+ el("path", {
+ class: "arc " + cls,
+ d: "M" + (cx + r * Math.cos(a0)) + " " + (cy + r * Math.sin(a0)) + " A " + r + " " + r + " 0 0 1 " + (cx + r * Math.cos(a1)) + " " + (cy + r * Math.sin(a1))
+ }, svg);
+ });
+ var stopAt = sc.id === "none" ? 360 : sc.id === "paralog" ? 125 : 228;
+ var rad = ((stopAt - 90) * Math.PI) / 180;
+ var spot = { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
+
+ var lens = el("g", { class: "lens" }, svg);
+ el("line", { x1: cx + 18, y1: cy - r + 18, x2: cx + 40, y2: cy - r + 40 }, lens);
+ el("circle", { cx: cx, cy: cy - r, r: 24 }, lens);
+ lens.style.transformOrigin = cx + "px " + cy + "px";
+ lens.style.transformBox = "view-box";
+ animate(lens, [{ transform: "rotate(0deg)" }, { transform: "rotate(" + stopAt + "deg)" }], { duration: 2600, delay: 300, easing: "cubic-bezier(.45,.05,.3,1)" });
+
+ var panel = el("g", { class: "panel" }, svg);
+ el("rect", { x: 562, y: 110, width: 150, height: 150, rx: 14 }, panel);
+ text(panel, 580, 140, "identity", { class: "small-label" });
+ var idText = text(panel, 580, 170, "0.0%", { class: "big-num" });
+ text(panel, 580, 204, "coverage", { class: "small-label" });
+ var covText = text(panel, 580, 234, "0.0%", { class: "big-num" });
+ enter(panel, "fade", 0.2);
+
+ later(2950, function () {
+ if (sc.id === "none") {
+ idText.textContent = "< 90%";
+ covText.textContent = "< 80%";
+ enter(badge(svg, 330, 352, "no hit above the thresholds", "is-muted"), "pop", 0);
+ return;
+ }
+ var glow = el("circle", { class: "glow", cx: spot.x, cy: spot.y, r: 18 }, svg);
+ enter(glow, "pulse", 0);
+ countUp(idText, sc.identity, "%", 900);
+ countUp(covText, sc.coverage, "%", 900);
+ enter(badge(svg, 330, 352, sc.id === "paralog" ? "a match, elsewhere" : "a match", "is-good"), "pop", 0.4);
+ });
+ }
+
+ function sceneLook(svg) {
+ var sc = scenario();
+ var y = 222;
+ el("line", { class: "contig", x1: 30, y1: y + 15, x2: 690, y2: y + 15 }, svg);
+ var paralog = sc.id === "paralog";
+ var left = paralog ? [["ge", "E"], ["gf", "F"]] : [["ga", "A"], ["gb", "B"]];
+ var right = paralog ? [["gg", "G"], ["gh", "H"]] : [["gc", "C"], ["gd", "D"]];
+ enter(gene(svg, 50, y, 96, left[0][0], left[0][1]), "fade", 0);
+ enter(gene(svg, 158, y, 96, left[1][0], left[1][1]), "fade", 0.05);
+ enter(gene(svg, 462, y, 96, right[0][0], right[0][1]), "fade", 0.1);
+ enter(gene(svg, 570, y, 96, right[1][0], right[1][1]), "fade", 0.15);
+
+ var lx = 280;
+ var lw = 156;
+ var label = "";
+ if (sc.id === "annotated") {
+ enter(gene(svg, lx, y, lw, "gp", "CDS, no cluster"), "fade", 0.2);
+ label = "a CDS the pangenome tool left out";
+ } else if (sc.id === "stop") {
+ enter(gene(svg, lx, y, lw, "gp", ""), "fade", 0.2);
+ label = "the gene is there, with a stop codon inside";
+ } else if (sc.id === "unannotated") {
+ var bracket = el("path", { class: "bracket", d: "M" + lx + " " + (y + 34) + "v10h" + lw + "v-10" }, svg);
+ enter(bracket, "fade", 0.2);
+ enter(text(svg, lx + lw / 2, y + 66, "no CDS annotated here", { class: "small-label", "text-anchor": "middle" }), "fade", 0.3);
+ label = "the gene is there, the GFF missed it";
+ } else if (sc.id === "split" || paralog) {
+ enter(gene(svg, lx, y, lw, "go", paralog ? "cluster Z" : "cluster Y"), "fade", 0.2);
+ label = paralog ? "a related gene of another family" : "the same gene, filed under another cluster";
+ } else if (sc.id === "pieces") {
+ enter(gene(svg, lx, y, 62, "gp", "1"), "fade", 0.2);
+ var is = el("g", { class: "is-el" }, svg);
+ el("rect", { x: lx + 64, y: y - 4, width: 28, height: 38, rx: 5 }, is);
+ text(is, lx + 78, y + 21, "IS", { "text-anchor": "middle" });
+ enter(is, "pop", 0.3);
+ enter(gene(svg, lx + 94, y, 62, "gp", "2"), "fade", 0.25);
+ label = "one gene, broken by an insertion";
+ } else {
+ label = "nothing there reaches 90% identity";
+ }
+
+ // The query drops onto what it matched.
+ var q = el("g", {}, svg);
+ gene(q, lx, 92, lw, "gx", "gene X");
+ if (sc.id === "none") {
+ animate(q, [{ transform: "translate(0,-60px)", opacity: 0 }, { transform: "translate(0,0)", opacity: 0.45 }], { duration: 700 });
+ var cross = el("g", { class: "cross" }, svg);
+ el("path", { d: "M" + (lx + lw / 2 - 14) + " 150 l28 28 M" + (lx + lw / 2 + 14) + " 150 l-28 28" }, cross);
+ enter(cross, "pop", 0.8);
+ } else {
+ animate(q, [{ transform: "translate(0,-60px)", opacity: 0 }, { transform: "translate(0,0)", opacity: 1 }], { duration: 700 });
+ // Each guide joins a point of the query to where it lands. The pieces
+ // split the query in the middle, around the insertion.
+ var mid = lx + lw / 2;
+ var guides = sc.id === "pieces" ? [[lx, lx], [mid, lx + 62], [mid, lx + 94], [lx + lw, lx + lw]] : [[lx, lx], [lx + lw, lx + lw]];
+ guides.forEach(function (pair, k) {
+ enter(el("line", { class: "guide", x1: pair[0], y1: 126, x2: pair[1], y2: y - 4, pathLength: 1 }, svg), "draw", 0.6 + k * 0.08);
+ });
+ enter(text(svg, lx + lw + 14, 112, sc.identity.toFixed(1) + "% identical", { class: "small-label" }), "fade", 0.9);
+ if (sc.id === "stop") {
+ var stopSign = el("g", { class: "stop-sign" }, svg);
+ var sx = lx + 112;
+ el("polygon", { points: [sx - 6, y + 3, sx + 6, y + 3, sx + 12, y + 9, sx + 12, y + 21, sx + 6, y + 27, sx - 6, y + 27, sx - 12, y + 21, sx - 12, y + 9].join(" ") }, stopSign);
+ text(stopSign, sx, y + 18, "stop", { "text-anchor": "middle" });
+ enter(stopSign, "pop", 1.1);
+ enter(el("rect", { class: "stop-tick", x: sx - 2, y: 92, width: 4, height: 30 }, svg), "pop", 1.1);
+ }
+ }
+ enter(badge(svg, 360, 330, sc.id === "none" ? "no rescue" : sc.type, sc.id === "none" ? "is-muted" : ""), "pop", 1.2);
+ enter(text(svg, 360, 372, label, { class: "small-label", "text-anchor": "middle" }), "fade", 1.3);
+ }
+
+ // The fading lives on an inner group: the entrance animation sets the
+ // outer group's opacity and would undo it.
+ function track(svg, y, genes, faint, dx) {
+ var g = el("g", { class: "track" }, svg);
+ var body = el("g", faint ? { class: "faint" } : {}, g);
+ var shift = dx || 0;
+ el("line", { class: "contig", x1: 60 + shift, y1: y + 15, x2: 660 + shift, y2: y + 15 }, body);
+ var xs = [80, 190, 300, 450, 560];
+ genes.forEach(function (spec, i) {
+ gene(body, xs[i] + shift, y, i === 2 ? 140 : 96, spec[0], spec[1]);
+ });
+ return { g: g, xs: xs };
+ }
+
+ function sceneNeighbours(svg) {
+ var sc = scenario();
+ var none = sc.id === "none";
+ var differ = state.synteny === "differ";
+ var off = state.synteny === "off";
+ var hitCls = sc.id === "split" || sc.id === "paralog" ? "go" : sc.id === "unannotated" ? "gx-ghost" : "gp";
+ var top = differ ? [["ge", "E"], ["gf", "F"], [hitCls, "hit"], ["gg", "G"], ["gh", "H"]] : [["ga", "A"], ["gb", "B"], [hitCls, "hit"], ["gc", "C"], ["gd", "D"]];
+ text(svg, 60, 326, "the genomes that have gene X", { class: "axis" });
+ var faintAll = off || none;
+ track(svg, 266, [["ga", ""], ["gb", ""], ["gx", ""], ["gc", ""], ["gd", ""]], true, 10);
+ var b = track(svg, 256, [["ga", "A"], ["gb", "B"], ["gx", "gene X"], ["gc", "C"], ["gd", "D"]], faintAll);
+ enter(b.g, "fade", 0.15);
+ if (none) {
+ // No hit in G3, so there is no place to look around.
+ enter(badge(svg, 360, 150, "nothing found, nothing to compare", "is-muted"), "pop", 0.4);
+ return;
+ }
+ text(svg, 60, 66, "G3, around the hit", { class: "axis" });
+ var t = track(svg, 84, top, faintAll);
+ enter(t.g, "fade", 0);
+ if (off) {
+ enter(badge(svg, 360, 190, "not compared without --synteny", "is-muted"), "pop", 0.4);
+ return;
+ }
+ [0, 1, 3, 4].forEach(function (i, k) {
+ var x = t.xs[i] + 44;
+ var path = el("path", {
+ class: differ ? "link link-bad" : "link link-good",
+ d: "M" + x + " 120 C " + x + " 170, " + x + " 200, " + x + " 250",
+ pathLength: 1
+ }, svg);
+ enter(path, "draw", 0.4 + k * 0.18);
+ });
+ enter(badge(svg, 360, 190, differ ? "different neighbours: another locus" : "same neighbours: same locus", differ ? "is-bad" : "is-good"), "pop", 1.2);
+ }
+
+ function sceneDecide(svg) {
+ var s = evidenceState();
+ var out = outcome();
+ var chip = el("g", { class: "chip-svg" }, svg);
+ el("rect", { x: 22, y: 168, width: 196, height: 44, rx: 12 }, chip);
+ text(chip, 120, 196, s, { "text-anchor": "middle", class: s.length > 18 ? "long" : "" });
+ enter(chip, "pop", 0);
+ var gates = { strict: 300, balanced: 420, sensitive: 540 };
+ Object.keys(gates).forEach(function (p) {
+ var gx = gates[p];
+ var on = p === state.policy;
+ var allowed = CORRECTS[p].indexOf(s) !== -1;
+ var g = el("g", { class: "gate" + (on ? " is-on" : "") + (allowed ? " allows" : "") }, svg);
+ text(g, gx, 96, p, { "text-anchor": "middle" });
+ el("rect", { class: "frame", x: gx - 40, y: 112, width: 80, height: 150, rx: 8 }, g);
+ var door = el("rect", { class: "door", x: gx - 34, y: 118, width: 68, height: 138, rx: 5 }, g);
+ enter(g, "fade", 0.1);
+ if (on && out === "putback") {
+ // The door rolls up inside its frame.
+ door.style.transformBox = "fill-box";
+ door.style.transformOrigin = "50% 0%";
+ later(1300, function () {
+ animate(door, [{ transform: "scaleY(1)" }, { transform: "scaleY(0.05)" }], { duration: 600 });
+ });
+ }
+ });
+ var target = gates[state.policy];
+ var token = el("circle", { class: "token", cx: 218, cy: 190, r: 11 }, svg);
+ token.style.opacity = 0;
+ if (out === "absent") {
+ enter(badge(svg, 420, 330, "nothing found: the cell stays absent", "is-muted"), "pop", 0.6);
+ return;
+ }
+ var put = out === "putback";
+ var result = el("g", { class: "result" }, svg);
+ el("path", { class: "flow-arrow", d: "M" + target + " 268 V 294 M" + (target - 6) + " 287 L " + target + " 294 L " + (target + 6) + " 287", pathLength: 1 }, result);
+ el("rect", { class: "r-cell " + (put ? "r-put" : "r-kept"), x: target - 24, y: 300, width: 48, height: 48, rx: 8 }, result);
+ // The label goes on whichever side of the cell has room.
+ var room = target < 500;
+ var lines = put ? ["present", "in the corrected matrix"] : ["kept out,", "with the reason"];
+ var anchor = { "text-anchor": room ? "start" : "end" };
+ text(result, room ? target + 40 : target - 40, 318, lines[0], Object.assign({ class: "small-label result-head" }, anchor));
+ text(result, room ? target + 40 : target - 40, 342, lines[1], Object.assign({ class: "small-label" }, anchor));
+ result.style.opacity = 0;
+ var toGate = target - 218;
+ animate(token, [{ transform: "translate(0,0)", opacity: 1 }, { transform: "translate(" + toGate + "px, 0)", opacity: 1 }], { duration: 1000, delay: 300 });
+ if (put) {
+ later(1900, function () {
+ animate(token, [{ transform: "translate(" + toGate + "px, 0)", opacity: 1 }, { transform: "translate(" + toGate + "px, 134px)", opacity: 0 }], { duration: 700 });
+ animate(result, [{ opacity: 0 }, { opacity: 1 }], { duration: 500, delay: 400 });
+ });
+ } else {
+ later(1400, function () {
+ var gate = svg.querySelector(".gate.is-on .door");
+ if (gate) animate(gate, [{ transform: "translateX(0)" }, { transform: "translateX(-5px)" }, { transform: "translateX(5px)" }, { transform: "translateX(0)" }], { duration: 400, fill: "none" });
+ animate(token, [{ transform: "translate(" + toGate + "px, 0)", opacity: 1 }, { transform: "translate(" + toGate + "px, 134px)", opacity: 0 }], { duration: 700, delay: 350 });
+ animate(result, [{ opacity: 0 }, { opacity: 1 }], { duration: 500, delay: 800 });
+ });
+ }
+ }
+
+ function doc(svg, x, y, w, h, title) {
+ var g = el("g", { class: "doc" }, svg);
+ el("rect", { class: "doc-bg", x: x, y: y, width: w, height: h, rx: 12 }, g);
+ text(g, x + 14, y + 26, title, { class: "doc-title" });
+ return g;
+ }
+
+ function sceneWrite(svg) {
+ var s = evidenceState();
+ var out = outcome();
+ var sc = scenario();
+ var d1 = doc(svg, 20, 60, 214, 220, "corrected matrix");
+ var d2 = doc(svg, 252, 60, 214, 220, "states matrix");
+ var d3 = doc(svg, 484, 60, 216, 220, "orthology_evidence.tsv");
+ [d1, d2].forEach(function (d, k) {
+ var x0 = (k ? 252 : 20) + 28;
+ for (var r = 0; r < 4; r++) {
+ for (var c = 0; c < 5; c++) {
+ var isTarget = r === 2 && c === 2;
+ var cls = "m-cell m-present";
+ if (isTarget) cls = k === 0 ? (out === "putback" ? "m-cell m-put" : "m-cell m-empty") : "m-cell m-state";
+ if (!isTarget && (r * 5 + c) % 7 === 3) cls = "m-cell m-empty";
+ el("rect", { class: cls, x: x0 + c * 32, y: 92 + r * 32, width: 26, height: 26, rx: 5 }, d);
+ }
+ }
+ });
+ text(d1, 127, 252, out === "putback" ? "G3, gene X: present" : "G3, gene X: absent", { class: "small-label", "text-anchor": "middle" });
+ text(d2, 359, 252, s, { class: "small-label state-label" + (s.length > 18 ? " long" : ""), "text-anchor": "middle" });
+ if (s === "ABSENT") {
+ text(d3, 592, 140, "no row:", { class: "small-label", "text-anchor": "middle" });
+ text(d3, 592, 164, "nothing was found", { class: "small-label", "text-anchor": "middle" });
+ } else {
+ var rows = [
+ ["gene_cluster", "gene_X"],
+ ["genome", "G3"],
+ ["rescue_type", sc.written],
+ ["evidence_state", s],
+ ["matrix_action", matrixAction()]
+ ];
+ rows.forEach(function (row, k) {
+ text(d3, 498, 104 + k * 30, row[0], { class: "tsv-key" });
+ text(d3, 498, 119 + k * 30, row[1], { class: "tsv-val" });
+ });
+ }
+ enter(d1, "rise", 0);
+ enter(d2, "rise", 0.25);
+ enter(d3, "rise", 0.5);
+ var hl = el("rect", { class: "hl", x: 112, y: 152, width: 30, height: 30, rx: 7 }, svg);
+ enter(hl, "pulse", 1.0);
+ enter(badge(svg, 360, 330, "nothing is thrown away", "is-good"), "pop", 1.2);
+ }
+
+ var STEPS = [
+ {
+ title: "The matrix says absent",
+ kicker: "Step 1",
+ scene: sceneMatrix,
+ text: function () {
+ return "Your pangenome tool marked gene X absent in genome G3. Every empty cell like this one is a question Recore asks the assembly.";
+ }
+ },
+ {
+ title: "Take the gene's sequence",
+ kicker: "Step 2",
+ scene: sceneSequence,
+ text: function () {
+ return "Recore takes gene X's representative sequence from the pan-genome reference. With --multi-allele it takes every allele, so a diverged copy is not missed.";
+ }
+ },
+ {
+ title: "Search the assembly",
+ kicker: "Step 3",
+ scene: sceneSearch,
+ text: function () {
+ var base = "minimap2 searches G3's assembly. A hit counts if it is at least 90% identical and covers 80% of the gene (--min-identity, --min-coverage).";
+ return scenario().id === "none" ? base + " Here nothing reaches them: the cell was searched, and the gene is not there." : base;
+ }
+ },
+ {
+ title: "Look at what is there",
+ kicker: "Step 4",
+ scene: sceneLook,
+ text: function () {
+ return {
+ annotated: "The hit sits on a CDS the annotation has, but that the pangenome tool left out of every cluster. Recore calls it DROPPED.",
+ unannotated: "The whole gene is in the assembly, but the GFF has no CDS there. Recore calls it MISSING_CDS.",
+ split: "The hit sits on a gene of another cluster, at 99% identity: the same gene, split between two clusters. Recore calls it SPLIT.",
+ stop: "The gene is there, but a stop codon interrupts it: a pseudogene. Recore reads the CDS in its own frame and counts the stops.",
+ pieces: "The gene is in two pieces, here broken by an insertion sequence, but the pieces cover it in order. Recore calls it FRAGMENTED.",
+ paralog: "The hit is a related gene of another family, 92% identical, at another place in the genome. Recore calls it PARALOG: a lookalike, not the missing gene.",
+ none: "Nothing in G3 is close enough to gene X. That is a real absence, and the states file says it was searched."
+ }[scenario().id];
+ }
+ },
+ {
+ title: "Check the neighbours",
+ kicker: "Step 5",
+ scene: sceneNeighbours,
+ control: "synteny",
+ text: function () {
+ var base = "With --synteny, Recore compares the genes around the hit with the genes around gene X in the genomes that have it.";
+ if (scenario().id === "none") return base + " Here there is no hit to compare.";
+ if (state.synteny === "off") return base + " Without it, the neighbours are not compared, and the evidence has to stand on the sequence alone.";
+ if (state.synteny === "differ") return base + " Different neighbours mean a copy somewhere else: the candidate is rejected and kept as audit evidence.";
+ return base + " The same neighbours mean the same place in the genome: strong evidence it is gene X.";
+ }
+ },
+ {
+ title: "Decide",
+ kicker: "Step 6",
+ scene: sceneDecide,
+ control: "policy",
+ text: function () {
+ var s = evidenceState();
+ var out = outcome();
+ var p = "" + state.policy + "";
+ var sentence = "The evidence is " + s + ". --matrix-policy decides which evidence becomes presence in the corrected matrix.";
+ if (out === "absent") return sentence + " Nothing was found, so the cell stays absent.";
+ if (out === "putback") {
+ var guard = state.policy === "sensitive" && (s === "PSEUDOGENE" || s === "PARTIAL_HOMOLOGY") ? ", if it also passes the quality guard" : "";
+ return sentence + " Under " + p + " this one goes in" + guard + ": the cell becomes present.";
+ }
+ var anyPolicy = Object.keys(CORRECTS).some(function (k) {
+ return CORRECTS[k].indexOf(s) !== -1;
+ });
+ if (!anyPolicy) return sentence + " No policy takes " + s + ": under all three it stays out, written down with the reason.";
+ return sentence + " Under " + p + " it stays out, written down with the reason.";
+ }
+ },
+ {
+ title: "Write it down",
+ kicker: "Step 7",
+ scene: sceneWrite,
+ text: function () {
+ return "Every decision is written with its evidence: the corrected matrix for your next tool, a state for every cell, and one row per decision in orthology_evidence.tsv. Kept-out evidence is not lost.";
+ }
+ }
+ ];
+
+ /* ── Page ── */
+
+ var stepsList = document.querySelector("[data-steps]");
+ var kicker = document.querySelector("[data-kicker]");
+ var title = document.querySelector("[data-step-title]");
+ var body = document.querySelector("[data-step-text]");
+ var control = document.querySelector("[data-control]");
+ var progress = document.querySelector("[data-progress]");
+ var prevButton = document.querySelector("[data-prev]");
+ var nextButton = document.querySelector("[data-next]");
+ var playButton = document.querySelector("[data-play]");
+ var replayButton = document.querySelector("[data-replay]");
+ var scenarioBox = document.querySelector("[data-scenarios]");
+ var svgTitle = document.getElementById("stage-title");
+ var svgDesc = document.getElementById("stage-desc");
+
+ SCENARIOS.forEach(function (sc) {
+ var b = document.createElement("button");
+ b.type = "button";
+ b.textContent = sc.label;
+ b.setAttribute("aria-pressed", sc.id === state.scenario ? "true" : "false");
+ b.addEventListener("click", function () {
+ stopPlay();
+ state.scenario = sc.id;
+ // A lookalike from elsewhere has, by definition, other neighbours.
+ state.synteny = sc.id === "paralog" ? "differ" : "agree";
+ scenarioBox.querySelectorAll("button").forEach(function (other) {
+ other.setAttribute("aria-pressed", other === b ? "true" : "false");
+ });
+ render();
+ });
+ scenarioBox.appendChild(b);
+ });
+
+ STEPS.forEach(function (step, i) {
+ var li = document.createElement("li");
+ var b = document.createElement("button");
+ b.type = "button";
+ b.innerHTML = '' + (i + 1) + "" + step.title + "";
+ b.addEventListener("click", function () {
+ stopPlay();
+ go(i);
+ });
+ li.appendChild(b);
+ stepsList.appendChild(li);
+ });
+
+ function segmented(options, current, onPick, label) {
+ var wrap = document.createElement("div");
+ wrap.className = "seg";
+ wrap.setAttribute("role", "group");
+ wrap.setAttribute("aria-label", label);
+ options.forEach(function (opt) {
+ var b = document.createElement("button");
+ b.type = "button";
+ b.innerHTML = opt[1];
+ b.setAttribute("aria-pressed", opt[0] === current ? "true" : "false");
+ b.addEventListener("click", function () {
+ stopPlay();
+ onPick(opt[0]);
+ render();
+ });
+ wrap.appendChild(b);
+ });
+ return wrap;
+ }
+
+ function render() {
+ var step = STEPS[state.step];
+ Array.prototype.forEach.call(stage.querySelectorAll(":scope > :not(title):not(desc)"), function (n) {
+ n.remove();
+ });
+ timers.forEach(clearTimeout);
+ timers = [];
+ var scene = el("g", { class: "scene" }, stage);
+ step.scene(scene);
+
+ kicker.textContent = step.kicker + " of " + STEPS.length;
+ title.textContent = step.title;
+ body.innerHTML = step.text();
+ svgTitle.textContent = step.kicker + ": " + step.title;
+ svgDesc.textContent = body.textContent;
+ control.innerHTML = "";
+ if (step.control === "synteny" && scenario().id !== "none") {
+ control.appendChild(
+ segmented(
+ [["agree", "Neighbours agree"], ["differ", "Neighbours differ"], ["off", "Not checked (no --synteny)"]],
+ state.synteny,
+ function (v) {
+ state.synteny = v;
+ },
+ "Neighbours"
+ )
+ );
+ }
+ if (step.control === "policy" && outcome() !== "absent") {
+ control.appendChild(
+ segmented(
+ [["strict", "strict"], ["balanced", "balanced"], ["sensitive", "sensitive"]],
+ state.policy,
+ function (v) {
+ state.policy = v;
+ },
+ "Matrix policy"
+ )
+ );
+ }
+ stepsList.querySelectorAll("button").forEach(function (b, i) {
+ if (i === state.step) b.setAttribute("aria-current", "step");
+ else b.removeAttribute("aria-current");
+ b.classList.toggle("done", i < state.step);
+ });
+ progress.style.width = ((state.step + 1) / STEPS.length) * 100 + "%";
+ prevButton.disabled = state.step === 0;
+ nextButton.textContent = state.step === STEPS.length - 1 ? "Start again" : "Next";
+ }
+
+ function go(i) {
+ state.step = (i + STEPS.length) % STEPS.length;
+ render();
+ }
+
+ function stopPlay() {
+ if (playTimer) clearInterval(playTimer);
+ playTimer = null;
+ playButton.textContent = "Play all";
+ }
+
+ prevButton.addEventListener("click", function () {
+ stopPlay();
+ go(state.step - 1);
+ });
+ nextButton.addEventListener("click", function () {
+ stopPlay();
+ go(state.step + 1);
+ });
+ replayButton.addEventListener("click", function () {
+ stopPlay();
+ render();
+ });
+ playButton.addEventListener("click", function () {
+ if (playTimer) {
+ stopPlay();
+ return;
+ }
+ playButton.textContent = "Pause";
+ go(0);
+ playTimer = setInterval(function () {
+ if (state.step === STEPS.length - 1) {
+ stopPlay();
+ return;
+ }
+ go(state.step + 1);
+ }, 5200);
+ });
+ document.addEventListener("keydown", function (event) {
+ if (/input|textarea|select/i.test(document.activeElement.tagName)) return;
+ if (event.key === "ArrowRight") {
+ stopPlay();
+ go(state.step + 1);
+ } else if (event.key === "ArrowLeft") {
+ stopPlay();
+ go(state.step - 1);
+ }
+ });
+
+ // Labels and pills are sized for the font of the moment, so redraw when
+ // the phone sizes switch on or off.
+ if (window.matchMedia) {
+ var phone = window.matchMedia("(max-width: 600px)");
+ if (phone.addEventListener) phone.addEventListener("change", render);
+ }
+
+ render();
+})();
diff --git a/docs/assets/search-index.js b/docs/assets/search-index.js
new file mode 100644
index 0000000..1e8ba3a
--- /dev/null
+++ b/docs/assets/search-index.js
@@ -0,0 +1,1029 @@
+/* Generated by tools/build_docs.py: the documentation, section by section, for the search box. */
+window.RECORE_DOCS_INDEX = [
+{
+"page": "Tutorial",
+"section": "",
+"url": "getting-started/tutorial.html",
+"text": "A complete run on a small dataset with known problems planted in it, so you can check that what Recore reports is what is actually there. You need minimap2 on PATH and Recore installed (see Installation). Nothing is downloaded: the dataset is generated by a script in the repository."
+},
+{
+"page": "Tutorial",
+"section": "1. Build the dataset",
+"url": "getting-started/tutorial.html#1-build-the-dataset",
+"text": "Twelve MTBC-style genomes across six lineages, with an L8 outgroup, and twenty-four gene clusters: Eleven cells are marked absent in the matrix that should not be. They are there on purpose, in four flavours: Rv0003 3 the full-length gene is in the assembly, the matrix says absent Rv0012 3 the same, in a different cluster Rv0006 2 the gene is in the assembly, but the GFF has no CDS there Rv0009 3 (all of L2) the gene is there with an internal stop codon Plus three cases that must not be corrected: Rv3810 is genuinely absent everywhere but L8, Rv0018c has only a diverged copy in L4_AS00404, and"
+},
+{
+"page": "Tutorial",
+"section": "2. Run Recore",
+"url": "getting-started/tutorial.html#2-run-recore",
+"text": "The lines that matter in the log: Read that against what was planted: 50 gaps is every empty cell in the matrix, not just the planted ones. 11 skipped are the Rv3810 cells. It sits in one genome out of twelve, which is under the 10% --min-prevalence floor, so it is never searched. 11 rescues is exactly the 11 planted cells, split into DROPPED 6 (Rv0003 and Rv0012), MISSING_CDS 2 (Rv0006) and PSEUDOGENE 3 (Rv0009). PARALOG: 0: the diverged copy in L4_AS00404 is 85% identical, under the 90% --min-identity bar, so it produced nothing at all. It must not, and it did not."
+},
+{
+"page": "Tutorial",
+"section": "3. Eleven found, eight corrected",
+"url": "getting-started/tutorial.html#3-eleven-found-eight-corrected",
+"text": "Eleven cells had evidence; eight reached the binary matrix. The gap is the whole point of the balanced policy: a gene with an internal stop codon is evidence of a disrupted homologue, not a clean presence call, so Rv0009 stays out of the binary matrix and stays visible everywhere else. orthology_evidence.tsv says so per cell: Rv0003 (x3) DROPPED STRICT_PRESENT correct_presence Rv0012 (x3) DROPPED STRICT_PRESENT correct_presence Rv0006 (x2) MISSING_CDS STRICT_PRESENT correct_presence Rv0009 (x3) PSEUDOGENE:1stops PSEUDOGENE audit_only_policy STRICT_PRESENT on Rv0006 is worth a second look. The "
+},
+{
+"page": "Tutorial",
+"section": "4. What the policy and the evidence each decide",
+"url": "getting-started/tutorial.html#4-what-the-policy-and-the-evidence-each-decide",
+"text": "Run the same data six ways and the two levers separate cleanly: strict 8 0 balanced 8 6 sensitive 11 9 Three things fall out of that table: strict without annotation context corrects nothing. It only accepts same-locus evidence, and without synteny none of the eight correctable cells has any: six come through as SEQUENCE_ONLY and the two Rv0006 cells as MISSING_CDS_UNVALIDATED, and strict takes neither. (The other three are PSEUDOGENE either way.) Strict is not \"balanced but fussier\", it is a different question. balanced takes full-length sequence, but not an unconfirmed locus. The six Rv0003 "
+},
+{
+"page": "Tutorial",
+"section": "5. Read the multistate matrix",
+"url": "getting-started/tutorial.html#5-read-the-multistate-matrix",
+"text": "The binary matrix is what downstream tools consume. The multistate matrix is what you read: Those three columns are L1_AS00101, L1_AS00102 and L2_AS00201. Rv0003 shows STRICT_PRESENT where its false absences were planted; Rv0009 is untouched in the two L1 genomes and PSEUDOGENE in the first L2 one. Rv3810 was never searched, being under the prevalence floor, and says so instead of reading as an absence Recore had checked. Nothing was thrown away and nothing was quietly upgraded. The thirteen states are listed in Evidence states."
+},
+{
+"page": "Tutorial",
+"section": "6. Open the report",
+"url": "getting-started/tutorial.html#6-open-the-report",
+"text": "Open tutorial_out/recore_report.html in a browser. It is one self-contained file, so double-clicking it works (checked in Chrome and in Safari's engine), and so does copying it off a cluster. Things to try, in order: Press Cmd+K and type Rv0009. The matrix jumps to that column. The three L2 genomes are the only ones not marked present, and the inspector shows the mechanism. Set tip colour to lineage from the sliders icon in the phylogeny header. The L2 clade lights up as one block, which is what a clade-restricted signal looks like. Find Rv1180. Only the three L2 tips carry it. This one is a g"
+},
+{
+"page": "Tutorial",
+"section": "7. What changed, in one line",
+"url": "getting-started/tutorial.html#7-what-changed-in-one-line",
+"text": "Three clusters that were missing from at least one genome are now found in all twelve. They were always there; the matrix was wrong about them. That is the entire job."
+},
+{
+"page": "Tutorial",
+"section": "Next",
+"url": "getting-started/tutorial.html#next",
+"text": "Matrix policies, to decide which policy your own run should use. Output files, for the columns of everything in tutorial_out/. Inputs, for getting your own data to match up."
+},
+{
+"page": "Inputs",
+"section": "",
+"url": "getting-started/inputs.html",
+"text": "gene_presence_absence.csv yes Panaroo-style matrix: metadata columns followed by one column per genome. Roary, PPanGGOLiN and PanTA write this layout, and PIRATE's own table is read too (see below) pan_genome_reference.fa yes Cluster representative sequences; FASTA IDs must match matrix cluster names genome FASTAs yes One assembly per matrix genome column GFF annotations optional, recommended Enables annotation-aware SPLIT, START_VARIANT, PARALOG, pseudogene and synteny classification reference genome name optional Used by --reference-genome to audit whether clusters are absent from a chosen r"
+},
+{
+"page": "Inputs",
+"section": "Which files each pangenome tool gives you",
+"url": "getting-started/inputs.html#which-files-each-pangenome-tool-gives-you",
+"text": "Pass -p the matrix and -r the representative sequences, and -a the same GFF files the tool was run on, so the gene IDs in the matrix cells are the ones in the annotations. Each pair was checked on real output of the version shown. Panaroo gene_presence_absence.csv pan_genome_reference.fa Roary gene_presence_absence.csv pan_genome_reference.fa PPanGGOLiN (2.3.2) matrix.csv, from ppanggolin all or ppanggolin write_pangenome --csv all_nucleotide_families.fasta, from ppanggolin fasta --gene_families all PanTA (1.0.1) gene_presence_absence.csv representative_clusters_nucl.fasta PIRATE (1.0.5) PIRAT"
+},
+{
+"page": "Inputs",
+"section": "Name matching",
+"url": "getting-started/inputs.html#name-matching",
+"text": "This is where most failed runs come from. Matrix genome columns should match FASTA and GFF file stems, or contain the same embedded ASxxxxx identifier. Pan-reference FASTA cluster names must match the Gene column of the matrix. GFF3 attributes are percent-decoded before matching, so ID, Name, gene, locus_tag, aliases, products and notes all reach the matcher with their real values. An encoded locus tag no longer silently misses its matrix entry. Tree tips are matched to matrix genomes by name. Genomes with no matching tip are ignored, and Recore reports that explicitly rather than dropping the"
+},
+{
+"page": "Inputs",
+"section": "What is rejected before the pipeline starts",
+"url": "getting-started/inputs.html#what-is-rejected-before-the-pipeline-starts",
+"text": "Recore validates the inputs before any expensive search, so a malformed run fails in seconds rather than after an hour of minimap2: empty presence/absence matrices; empty pan-reference FASTA files; duplicate genome names; duplicate cluster names."
+},
+{
+"page": "Inputs",
+"section": "Choosing the optional inputs",
+"url": "getting-started/inputs.html#choosing-the-optional-inputs",
+"text": "Use GFFs whenever you have them. Without annotations Recore can still detect full-length sequence evidence and some partial or deletion evidence, but it cannot reliably distinguish SPLIT, START_VARIANT and PARALOG by annotation overlap, and synteny validation is not available at all. --synteny: Use it with GFFs whenever paralogy or multi-locus clusters are plausible. It is the check that keeps a paralogous hit from becoming a presence call. --multi-allele: Use it when clusters have divergent alleles, so a diverged true allele is not mistaken for a different gene. --min-prevalence: 0.0 for exha"
+},
+{
+"page": "Parameters",
+"section": "",
+"url": "guide/parameters.html",
+"text": ""
+},
+{
+"page": "Parameters",
+"section": "Required",
+"url": "guide/parameters.html#required",
+"text": "-p, --presence-absence Panaroo gene_presence_absence.csv -r, --pan-reference Panaroo pan_genome_reference.fa -g, --genomes Directory of genome FASTA files (.fasta, .fa, .fna, also .gz)"
+},
+{
+"page": "Parameters",
+"section": "Optional",
+"url": "guide/parameters.html#optional",
+"text": "-a, --annotations none Directory of GFF3 annotation files. Required for SPLIT/START_VARIANT classification and synteny validation. -o, --output recore_output/ Output directory -t, --threads 4 Number of parallel threads (for rayon classification and minimap2) Recore matches FASTA and GFF files by exact matrix column name first, then by an embedded ASxxxxx identifier. For single-contig genomes, GFF seqids are normalized internally to the FASTA contig name so --synteny can work when a GFF uses contig_1 or L4_AS00146 but the FASTA contig is AS00146. Numeric parameters are validated before the pipe"
+},
+{
+"page": "Parameters",
+"section": "Alignment",
+"url": "guide/parameters.html#alignment",
+"text": "--preset asm5 minimap2 preset. asm5 for sequences ≤5% divergence (MTBC, closely related strains). asm20 for up to 20% divergence (more divergent species). --min-identity 90.0 Minimum % nucleotide identity to consider a hit. For MTBC, the default (90%) is appropriate: real genes have >99%. Lowering this increases PARALOG noise. --min-coverage 80.0 Minimum % of reference sequence that must be covered by a hit (single hit or merged fragments). --multi-allele false Use all alleles per cluster as queries. More sensitive for clusters with high intra-cluster diversity, but slower. Panaroo produces mu"
+},
+{
+"page": "Parameters",
+"section": "Filtering",
+"url": "guide/parameters.html#filtering",
+"text": "--min-prevalence 0.1 Minimum fraction of genomes where a cluster must be present to attempt rescue. Clusters present in fewer genomes are likely truly accessory and skipped. Set to 0.0 to attempt rescue of all gaps."
+},
+{
+"page": "Parameters",
+"section": "Rescue options",
+"url": "guide/parameters.html#rescue-options",
+"text": "--synteny false Validate each rescue by adaptive 1-to-5-neighbour flanking context against reference genomes. Requires --annotations. Boosts synteny-consistent rescues (×1.2), reports quantitative synteny metrics, and rejects explicit synteny failures from the corrected matrix while writing them to synteny_rejected_report.tsv. Independently, PARALOG candidates without explicit synteny support are rejected to paralog_rejected_report.tsv. --write-corrected-gff false Write a corrected_gff/*.recore.gff3 file for each genome with a matrix-corrected rescue, with those CDS features appended. Original"
+},
+{
+"page": "Parameters",
+"section": "Nucleotide Similarity Clustering",
+"url": "guide/parameters.html#nucleotide-similarity-clustering",
+"text": "--nscluster false Enable NSC. Detects and merges Panaroo clusters that are nearly identical at the DNA level but split at the protein level. --nsc-engine minhash Clustering engine: minhash (built-in, approximate, no dependencies) or mmseqs2 (exact, requires MMseqs2 in PATH). --nsc-kmer-size 21 k-mer size for MinHash sketching. Larger values are more specific (fewer false positives). --nsc-threshold 0.8 Maximum containment threshold for MinHash. Pairs above this are merged. --nsc-min-seq-id 0.9 Minimum sequence identity for MMseqs2 linclust (0.0-1.0). --nsc-coverage 0.8 Minimum bidirectional co"
+},
+{
+"page": "Parameters",
+"section": "Logging",
+"url": "guide/parameters.html#logging",
+"text": "-v, --verbose false Enable DEBUG-level log output. Shows GFF loading details, synteny decisions (including why individual rescues pass/fail), per-genome missing FASTA warnings, and the top 10 rescues."
+},
+{
+"page": "Parameters",
+"section": "Parameter Guide by Organism",
+"url": "guide/parameters.html#parameter-guide-by-organism",
+"text": ""
+},
+{
+"page": "Parameters",
+"section": "M. tuberculosis complex (MTBC)",
+"url": "guide/parameters.html#m-tuberculosis-complex-mtbc",
+"text": "MTBC has >99.95% average nucleotide identity across strains. This has two consequences: Real genes have very high identity; PARALOGs (90-94%) are nearly always noise PE/PPE genes (~170 families with high mutual similarity) are a major source of cross-matches Recommended command: Why --min-prevalence 0.0: In MTBC, even rare accessory clusters can have real false absences due to annotation differences. 0.0 ensures nothing is skipped. Why asm5: Strains are <5% divergent. asm5 is faster and more accurate at this range. Why mmseqs2 for NSC: More precise than MinHash for detecting the redundant anno"
+},
+{
+"page": "Parameters",
+"section": "More divergent bacteria / pan-genome with 95% ANI",
+"url": "guide/parameters.html#more-divergent-bacteria--pan-genome-with-95-ani",
+"text": "Why asm20: Allows up to 20% divergence, which is needed when comparing strains from the same species but different lineages. Why --min-prevalence 0.05: Rare accessory clusters (present in <5% of genomes) generate excessive cross-match noise across the matrix."
+},
+{
+"page": "Parameters",
+"section": "Threshold Effects",
+"url": "guide/parameters.html#threshold-effects",
+"text": ""
+},
+{
+"page": "Parameters",
+"section": "--min-identity (alignment filter)",
+"url": "guide/parameters.html#--min-identity-alignment-filter",
+"text": "Affects PARALOG counts most strongly. If you see an unexpectedly large number of PARALOGs: Check if --min-identity was set below 90% by mistake For MTBC: real genes should be >99% identical; PARALOGs at 90-94% are noise Increasing to 95% eliminates most PE/PPE cross-matches but may miss a few rare variants"
+},
+{
+"page": "Parameters",
+"section": "--min-coverage (coverage filter)",
+"url": "guide/parameters.html#--min-coverage-coverage-filter",
+"text": "Controls the boundary between PSEUDOGENE:PartialDeletion (60-80%) and no rescue (<60%): Increasing to 90%: only rescues nearly complete genes; misses short pseudogenes Decreasing to 60%: rescues more partial genes but increases false positives"
+},
+{
+"page": "Parameters",
+"section": "--min-prevalence (gap filter)",
+"url": "guide/parameters.html#--min-prevalence-gap-filter",
+"text": "Affects total rescues and PARALOG rate: 0.0: search all gaps (maximum sensitivity, higher PARALOG noise) 0.05: skip clusters present in <5% of genomes 0.1 (default): skip clusters present in <10% of genomes For core/soft-core analysis, --min-prevalence 0.0 is recommended. For reducing runtime on large datasets, 0.05 is a reasonable compromise."
+},
+{
+"page": "Parameters",
+"section": "--nsc-threshold (NSC similarity)",
+"url": "guide/parameters.html#--nsc-threshold-nsc-similarity",
+"text": "Controls how aggressively redundant clusters are merged: 0.8 (default): merge if 80% of one cluster's k-mers are contained in the other 0.9: stricter, fewer merges 0.7: more aggressive, may merge genuinely distinct paralogs For MTBC, prefer MMseqs2 with --nsc-min-seq-id 0.9 over MinHash, as it uses exact alignment identity."
+},
+{
+"page": "Matrix policies",
+"section": "",
+"url": "guide/matrix-policies.html",
+"text": "Recore always records every piece of detected evidence in the audit files. The matrix policy controls one thing only: which accepted evidence is written into the binary corrected matrix. Nothing is ever thrown away by a policy. A cell that strict declines to correct is still in gene_presence_absence_recore_states.csv with its state, and still in orthology_evidence.tsv with the reason."
+},
+{
+"page": "Matrix policies",
+"section": "Choosing one",
+"url": "guide/matrix-policies.html#choosing-one",
+"text": "Paper-ready binary PA matrix balanced strict same-locus calls, alias clusters, and full-length sequence evidence pseudogenes, partial homology, context-rejected hits Maximum conservatism strict only strict same-locus and alias-cluster evidence full sequence-only hits without annotation or synteny support Exploratory rescue sensitive accepted strict, alias, sequence-only, partial and pseudogene evidence that passes the quality guard rejected contexts, weak accepted evidence, and homologous or paralogous traps balanced is the recommendation for anything that will be published. It rescues the fal"
+},
+{
+"page": "Matrix policies",
+"section": "The adaptive orthology guard",
+"url": "guide/matrix-policies.html#the-adaptive-orthology-guard",
+"text": "Policy is not the only gate. Every accepted rescue is also scored by an adaptive guard that combines: sequence identity and query coverage; locus and synteny evidence; ORF integrity; the evidence state; cluster prevalence; representative sequence length. Rare clusters and very short clusters have to clear a higher sequence bar, because they are the easiest to over-call from weak homology. Same-locus evidence such as a passing synteny context can override that stricter sequence floor for STRICT_PRESENT and ALIAS_CLUSTER calls, but weak sequence-only evidence is held back as audit_only_quality. "
+},
+{
+"page": "Matrix policies",
+"section": "Reporting it",
+"url": "guide/matrix-policies.html#reporting-it",
+"text": "The wording that matches what balanced actually does: We used Recore with the balanced matrix policy, which rescues full-length false absences while retaining partial or disrupted homologues as multistate audit-only evidence. If a specific cluster needs its own sentence, orthology_evidence.tsv carries per-cluster wording guidance, and the run report shows it in the inspector under recommended wording."
+},
+{
+"page": "Output files",
+"section": "",
+"url": "guide/outputs.html",
+"text": "All output files are written to the directory specified by -o (default: recore_output/). Two guarantees hold for every table here. TSV outputs are written as single-line records: tabs, line breaks and null characters inside annotation-derived text are normalised to spaces, so a GFF product, note or alias cannot corrupt a downstream table. And when an output directory is reused, Recore clears the known optional reports before writing the new run, so a stale reference_*, rejected-evidence, NSC, synteny or corrected-GFF file from a previous run cannot be mistaken for a current result."
+},
+{
+"page": "Output files",
+"section": "rescue_report.tsv",
+"url": "guide/outputs.html#rescue_reporttsv",
+"text": "Primary output. One row per rescued gene. Tab-separated."
+},
+{
+"page": "Output files",
+"section": "Columns",
+"url": "guide/outputs.html#columns",
+"text": "gene_cluster string Panaroo cluster name (from CSV Gene column) genome string Genome name (matches CSV column header and FASTA filename stem) rescue_type string See rescue types identity float % nucleotide identity of the best hit (0-100) coverage float % of reference sequence covered by hit(s) (0-100) contig string Contig name in the genome FASTA start int Start coordinate, 1-based end int End coordinate, 1-based inclusive strand char + or - confidence float Combined quality score (0-1.2+); see algorithm synteny string PASS or empty (not assessed). FAIL rows are written to synteny_rejected_re"
+},
+{
+"page": "Output files",
+"section": "Example",
+"url": "guide/outputs.html#example",
+"text": ""
+},
+{
+"page": "Output files",
+"section": "Filtering suggestions",
+"url": "guide/outputs.html#filtering-suggestions",
+"text": ""
+},
+{
+"page": "Output files",
+"section": "synteny_rejected_report.tsv",
+"url": "guide/outputs.html#synteny_rejected_reporttsv",
+"text": "Written only when --synteny is enabled and one or more candidate rescues have explicitly incompatible flanking context. The schema is identical to rescue_report.tsv, but these rows are audit records: they are not added to gene_presence_absence_recore.csv and are not written to corrected GFF output. This file is useful for reviewing high-identity paralog/cross-family hits that look like sequence rescues but fail the local genomic-context check."
+},
+{
+"page": "Output files",
+"section": "paralog_rejected_report.tsv",
+"url": "guide/outputs.html#paralog_rejected_reporttsv",
+"text": "Written only when one or more PARALOG candidates lack explicit synteny support. The schema is identical to rescue_report.tsv, but these rows are audit records: they are not added to gene_presence_absence_recore.csv and are not written to corrected GFF output. This is the conservative guardrail for multi-copy families: sequence similarity alone is not allowed to rescue a missing gene when the annotation says the hit belongs to a different cluster."
+},
+{
+"page": "Output files",
+"section": "orthology_evidence.tsv",
+"url": "guide/outputs.html#orthology_evidencetsv",
+"text": "Written for every run. One row per accepted rescue plus every rejected synteny or paralog candidate. decision accepted or rejected orthology_status Conservative interpretation, e.g. accepted_synteny, accepted_boundary_variant, rejected_incompatible_context, or rejected_paralog_without_synteny evidence_state Explicit evidence class: STRICT_PRESENT, ALIAS_CLUSTER, SEQUENCE_ONLY, MISSING_CDS_UNVALIDATED, PARTIAL_HOMOLOGY, PSEUDOGENE, HOMOLOG_PARALOG, or REJECTED_CONTEXT matrix_action correct_presence for accepted rescues, audit_only_quality for accepted evidence that fails the adaptive guard, aud"
+},
+{
+"page": "Output files",
+"section": "reference_genome_audit.tsv",
+"url": "guide/outputs.html#reference_genome_audittsv",
+"text": "Written only when --reference-genome is provided. The value can be an exact matrix column name or an embedded ASxxxxx identifier. gene_cluster Panaroo cluster name reference_genome Resolved matrix column used as the audited reference reference_absence_class new_to_reference, reference_like_or_present, or reference_like_ambiguous final_publication_status Whether to keep in a strict reference-absent set, remove, or review reference_status Detailed reason: matrix-present, clean absent, strict rescue, alias, partial-like, pseudogene-like, paralog-like, or incompatible-context evidence matrix_actio"
+},
+{
+"page": "Output files",
+"section": "reference_cluster_events.tsv",
+"url": "guide/outputs.html#reference_cluster_eventstsv",
+"text": "Written only when --reference-genome is provided and Recore finds reference-absent clusters with strong sequence evidence to a reference-present cluster. This file is audit-only: it never merges clusters and never changes the binary corrected matrix. gene_cluster Cluster absent from the audited reference target_reference_cluster Best reference-present cluster matched by k-mer seed plus global alignment target_reference_gene Gene ID carried by the audited reference for the target cluster reference_event_class same_gene_full_length, short_in_frame_allele, long_in_frame_allele, frameshift_or_trun"
+},
+{
+"page": "Output files",
+"section": "evolutionary_events.tsv",
+"url": "guide/outputs.html#evolutionary_eventstsv",
+"text": "Written for every run. This file is audit-only: it does not merge clusters and does not change gene_presence_absence_recore.csv. When --tree is provided, Recore parses the rooted Newick tree, matches tips to matrix columns by exact name or embedded ASxxxxx identifier, and applies Fitch parsimony to the corrected binary state for each cluster. gene_cluster Panaroo cluster name n_present_corrected, prevalence_corrected Presence after accepted matrix corrections and NSC presence updates absent_not_searched, not_searched_reason How many of the cluster's absences in the corrected matrix were never "
+},
+{
+"page": "Output files",
+"section": "cluster_locus_context.tsv",
+"url": "guide/outputs.html#cluster_locus_contexttsv",
+"text": "Written when GFF annotations are loaded. One row per observed locus context for each cluster. gene_cluster Panaroo cluster name context_rank Rank within the cluster by number of genomes n_genomes Number of genomes supporting this context fraction_of_mapped_contexts Fraction among genomes where the cluster gene ID was resolved in the GFF upstream_context Nearest upstream cluster names, joined by ; NA if no mapped context downstream_context Nearest downstream cluster names, joined by ; NA if no mapped context example_genomes Up to five genome:gene examples"
+},
+{
+"page": "Output files",
+"section": "cluster_locus_summary.tsv",
+"url": "guide/outputs.html#cluster_locus_summarytsv",
+"text": "Written when GFF annotations are loaded. One row per cluster. gene_cluster Panaroo cluster name n_present Number of genomes where the cluster is present in the input matrix n_context_mapped Present genomes where the cell gene ID was found in the GFF n_unresolved_gene_ids Present genomes where the cell gene ID could not be resolved in the GFF n_unique_contexts Number of distinct upstream/downstream context signatures dominant_context_count Number of genomes in the largest context dominant_context_fraction Dominant context count divided by mapped contexts; blank when no context could be mapped i"
+},
+{
+"page": "Output files",
+"section": "cluster_synteny_blocks.tsv",
+"url": "guide/outputs.html#cluster_synteny_blockstsv",
+"text": "Written when GFF annotations are loaded. This is an audit-only synteny-block summary using up to five upstream and five downstream cluster neighbours. When a reference genome is supplied, each block also stores a representative gene-level neighbourhood and the best matching reference/H37Rv neighbourhood inferred from shared flanking clusters, even when the focal cluster itself is absent from the reference. gene_cluster Panaroo cluster name block_rank Rank by number of supporting genomes n_genomes, fraction_of_present Support for this block among genomes where the cluster is present upstream_bl"
+},
+{
+"page": "Output files",
+"section": "gene_presence_absence_recore.csv",
+"url": "guide/outputs.html#gene_presence_absence_recorecsv",
+"text": "Drop-in replacement for Panaroo's gene_presence_absence.csv. Identical format and column order."
+},
+{
+"page": "Output files",
+"section": "Changes from the original",
+"url": "guide/outputs.html#changes-from-the-original",
+"text": "Rescued cells: Empty cells (gene absent) become RESCUED:contig:start-end where coordinates are 1-based. Matrix-policy filter: --matrix-policy controls which accepted rescues become binary presence. strict corrects only STRICT_PRESENT/ALIAS_CLUSTER; balanced also corrects full-length SEQUENCE_ONLY; sensitive also permits partial/disrupted accepted evidence. In all modes, the adaptive orthology-quality guard can keep weak accepted evidence out of the binary matrix. NSC-merged cells (only if --nscluster): Cells where a merged cluster would have added presence become NSC_MERGED. Metadata columns u"
+},
+{
+"page": "Output files",
+"section": "Compatibility",
+"url": "guide/outputs.html#compatibility",
+"text": "This file can be used directly as input to downstream tools that accept Panaroo's format: distree for phylogenetic distance analysis fstic for allele frequency-based FST calculations Custom R/Python scripts expecting Panaroo output."
+},
+{
+"page": "Output files",
+"section": "gene_presence_absence_recore.Rtab",
+"url": "guide/outputs.html#gene_presence_absence_recorertab",
+"text": "Written for every run, in the same pass as gene_presence_absence_recore.csv, so the two agree on every cell. It is the corrected matrix as 1 and 0 in Panaroo's gene_presence_absence.Rtab layout: a Gene column, then one column per genome, tab-separated. A cell is 1 when the corrected CSV has anything in it (input presence, RESCUED:... or NSC_MERGED), and clusters merged away by NSC are left out, as in the CSV. This is the file pyseer's --pres, and most R code written for Panaroo, reads."
+},
+{
+"page": "Output files",
+"section": "gene_presence_absence_recore_states.csv",
+"url": "guide/outputs.html#gene_presence_absence_recore_statescsv",
+"text": "Written for every run. Same row/column layout as the corrected matrix, but each genome cell stores the biological interpretation rather than a binary presence call. Possible states: PRESENT_INPUT: present in the original matrix. ABSENT: searched, and no evidence under current thresholds. NOT_SEARCHED_LOW_PREVALENCE: never searched, because the cluster is in fewer genomes than --min-prevalence. NOT_SEARCHED_NO_CLUSTER_SEQUENCE: never searched, because the cluster has no sequence in the pan-genome reference. NOT_SEARCHED_NO_FASTA: never searched, because the genome has no FASTA file. STRICT_PRES"
+},
+{
+"page": "Output files",
+"section": "cluster_relationships.tsv",
+"url": "guide/outputs.html#cluster_relationshipstsv",
+"text": "Written for every run. The file may contain only the header when no inter-cluster relationships are detected. This table summarizes reproducible evidence that one Panaroo cluster is related to another. It is intended as an audit layer: Recore reports relationships and recommendations, but only nucleotide similarity clustering (--nscluster) changes the output matrix automatically."
+},
+{
+"page": "Output files",
+"section": "Columns",
+"url": "guide/outputs.html#columns-1",
+"text": "query_cluster Cluster being rescued or reviewed related_cluster Cluster linked by SPLIT/PARALOG or NSC evidence relationship_type split, paralog, annotation_match_cluster, or nucleotide_similarity_cluster evidence_type Specific rescue type or nscluster n_genomes_supporting Number of genomes supporting the relationship synteny_support pass, fail, mixed, or not_tested identity_* Minimum, median and maximum nucleotide identity across supporting rescues coverage_* Minimum, median and maximum query coverage across supporting rescues recommendation Conservative interpretation: collapse_alias, review"
+},
+{
+"page": "Output files",
+"section": "Interpretation",
+"url": "guide/outputs.html#interpretation",
+"text": "collapse_alias: near-complete, high-identity, synteny-supported evidence that the query is not an independent cluster. review_alias_without_full_synteny: high sequence support, but synteny is absent, mixed, or not tested. review_annotation_match: the rescued locus overlaps a feature already assigned to another cluster; inspect as a possible naming or boundary issue. review_paralog_keep_distinct_by_default: related sequence family member; keep distinct unless additional evidence supports collapse."
+},
+{
+"page": "Output files",
+"section": "cluster_components.tsv / cluster_component_members.tsv",
+"url": "guide/outputs.html#cluster_componentstsv--cluster_component_memberstsv",
+"text": "Written for every run. These tables convert pairwise relationships into connected components, so split clusters, annotation-matched clusters, NSC members, and paralog-linked clusters can be reviewed as one unit. cluster_components.tsv contains one row per non-singleton component: component_id Stable component label for the run n_clusters Number of clusters in the component relation_types Relationship classes connecting the component recommendation Component-level action, such as review_or_collapse_aliases or review_component_keep_paralogs_distinct member_clusters Semicolon-separated cluster na"
+},
+{
+"page": "Output files",
+"section": "genome_qc.tsv",
+"url": "guide/outputs.html#genome_qctsv",
+"text": "Written for every run. One row per genome with rescue and annotation burden metrics. genome Matrix genome column fasta_found, gff_found Whether matching FASTA/GFF inputs were available gff_contig_remapped_records Number of GFF records remapped to FASTA contig names gaps_passing_filter Missing cells searched or eligible for search after prevalence filtering accepted_rescues, matrix_corrected_rescues, nsc_added_presence Evidence and matrix-correction burden by genome synteny_rejected, paralog_rejected Rejected evidence by genome qc_status, qc_flags pass, review, or fail plus semicolon-separated "
+},
+{
+"page": "Output files",
+"section": "summary.txt",
+"url": "guide/outputs.html#summarytxt",
+"text": "Human-readable run summary. This one is from the tutorial run: The gap lines add up. Every empty cell of the input matrix is in Total gaps, and each one is accounted for exactly once: Skipped (prevalence) in clusters under --min-prevalence; never searched Gaps skipped (no seq) in clusters with no sequence in the pan-genome reference; shown only when there are some Gaps passing filter everything else Gaps skipped (FASTA) of those, in genomes with no FASTA file; shown only when there are some Gaps searched of those, actually searched with minimap2 Matrix-corrected is the number of cells written "
+},
+{
+"page": "Output files",
+"section": "Interpreting core genome changes",
+"url": "guide/outputs.html#interpreting-core-genome-changes",
+"text": "Core +3 in the tutorial: Rv0003, Rv0006 and Rv0012 were missing from a few genomes and are now found in all of them, core genes the matrix had excluded. Rv0009 stays out, because balanced keeps pseudogenes out of the binary matrix. With --nscluster the soft-core can shrink: clusters that looked soft-core on their own may be redundant copies of another cluster and are merged into it."
+},
+{
+"page": "Output files",
+"section": "run_manifest.json",
+"url": "guide/outputs.html#run_manifestjson",
+"text": "Machine-readable run manifest written for every run. It records: tool and tool_version; schema_version (recore.outputs.v7); created_utc, the time the run finished writing outputs, and command_line, the exact invocation; input paths used for the run; key parameters, including matrix policy, prevalence threshold, synteny, NSC, reference genome, tree, and mobile-element settings; headline counts such as clusters, genomes, searched gaps, direct rescue corrections, NSC-merged corrections, rejected synteny/paralog candidates, and NSC groups. Use this file when archiving outputs for a manuscript or c"
+},
+{
+"page": "Output files",
+"section": "nscluster_report.tsv",
+"url": "guide/outputs.html#nscluster_reporttsv",
+"text": "Written only if --nscluster is enabled and at least one merge was found. ns_cluster NS cluster ID (NS_0, NS_1, ...) representative Name of the representative Panaroo cluster (highest prevalence) n_members Total number of clusters in the group (including representative) members Semicolon-separated list of all member cluster names"
+},
+{
+"page": "Output files",
+"section": "Example",
+"url": "guide/outputs.html#example-1",
+"text": "These clusters had nearly identical nucleotide sequences but were split by Panaroo due to amino acid-level differences (typically from upstream annotation artifacts)."
+},
+{
+"page": "Output files",
+"section": "corrected_gff/*.recore.gff3",
+"url": "guide/outputs.html#corrected_gffrecoregff3",
+"text": "Written only when --write-corrected-gff is enabled. For each genome with at least one rescue that corrected the matrix, Recore writes a GFF3 file under corrected_gff/; evidence kept out of the matrix (held_out_policy, held_out_quality) is not written. If an original GFF was supplied with --annotations, its records are preserved and Recore appends rescued CDS features, on the GFF's own seqid even when that seqid had to be mapped onto a differently named FASTA contig. If the original GFF contains a ##FASTA section, rescue features are inserted before ##FASTA and the FASTA tail is preserved after"
+},
+{
+"page": "Output files",
+"section": "rescued_genes.fna / rescued_genes.faa",
+"url": "guide/outputs.html#rescued_genesfna--rescued_genesfaa",
+"text": "Written only when --write-rescued-fasta is enabled: the sequence of every cell the binary matrix gained, for functional annotation (eggNOG-mapper, InterProScan, BLAST). Record IDs are the feature IDs of corrected_gff/*.recore.gff3, and the description carries the genome, the cluster, the locus, the rescue type and the phase: rescued_genes.fna has the nucleotide span read in the gene's direction (reverse-complemented on the minus strand). rescued_genes.faa translates it from the phase the alignment fixes, with the bacterial code (NCBI table 11); a first codon that is the gene's own start reads "
+},
+{
+"page": "Output files",
+"section": "recore_report.html",
+"url": "guide/outputs.html#recore_reporthtml",
+"text": "Written only when --write-html-report is enabled. This is a self-contained browser report for run review and manuscript-facing QC. It summarizes: input matrix size, searchable gaps, accepted evidence, corrected matrix cells, rejected synteny/paralog evidence, and core/soft-core changes; orthology actions and confidence tiers from orthology_evidence.tsv; evolutionary priorities and dominant mechanisms from evolutionary_events.tsv; per-genome QC status from genome_qc.tsv; the top candidate clusters ranked by evolutionary_signal_score; direct links to the exact output files used to generate the s"
+},
+{
+"page": "Output files",
+"section": "Coordinate System",
+"url": "guide/outputs.html#coordinate-system",
+"text": "All coordinates in Recore output are 1-based (standard for bioinformatics formats like GFF3, VCF, GenBank): rescue_report.tsv: start column is 1-based gene_presence_absence_recore.csv: RESCUED:contig:start-end coordinates are 1-based minimap2 internally uses 0-based half-open coordinates (PAF format). Recore converts these to 1-based for all output."
+},
+{
+"page": "Output files",
+"section": "Log Output",
+"url": "guide/outputs.html#log-output",
+"text": "Recore writes structured log output to stderr. Use -v / --verbose for debug-level detail."
+},
+{
+"page": "Output files",
+"section": "Standard output (INFO level)",
+"url": "guide/outputs.html#standard-output-info-level",
+"text": "This is the tutorial run from Tutorial, with timestamps removed:"
+},
+{
+"page": "Output files",
+"section": "Verbose output (DEBUG level, with -v)",
+"url": "guide/outputs.html#verbose-output-debug-level-with--v",
+"text": ""
+},
+{
+"page": "Output files",
+"section": "Common debug messages and their meaning",
+"url": "guide/outputs.html#common-debug-messages-and-their-meaning",
+"text": "ref gene 'xxx_pseudo' not found in GFF of genome idx=N Panaroo refound gene ID has no counterpart in GFF (expected for refound_* genes) Upstream gene 'Rv2650c' not found in any Panaroo cluster Flanking gene was cleaned from the pangenome; synteny cannot be assessed Synteny skip: no flanking genes mapped Neither upstream nor downstream gene has a cluster assignment; rescue is kept but synteny_ok = None X genomes with gaps have no FASTA file (skipped) Genome names in CSV don't match FASTA filenames; check naming"
+},
+{
+"page": "The report",
+"section": "",
+"url": "guide/report.html",
+"text": "--write-html-report writes recore_report.html into the output directory. It is a single file with no external requests: it opens from a results directory on a cluster node with no network, and it keeps working when you email it to a co-author. Open an example report{ .md-button .md-button--primary target=_blank } That one is the output of the tutorial run: twelve genomes, twenty-four clusters, eleven planted false absences. It is the whole file, opened straight from this page, which is the point of it being self-contained. Without --tree the workspace still opens: the rows follow the matrix or"
+},
+{
+"page": "The report",
+"section": "The workspace",
+"url": "guide/report.html#the-workspace",
+"text": "The viewer is the report. Four regions share the window: Phylogeny the matched tips, in the order the matrix rows use Corrected matrix one column per cluster, one row per genome, coloured by evidence state or by mechanism Inspector the selected cluster, or the selected genome Unique syntenies the flanking-gene context for the selected cluster, full width along the bottom Every seam between them is draggable, and the sizes are remembered in the browser for the next report you open."
+},
+{
+"page": "The report",
+"section": "Finding things",
+"url": "guide/report.html#finding-things",
+"text": "Press Cmd+K (or Ctrl+K, or /) to open the find field. It searches clusters and genomes at once, ranking prefix matches on the name above name matches above annotation matches. Enter opens the result: a cluster becomes the selected column, a genome scrolls the panes to its tip and opens its card. The arrow buttons beside the field step through the visible columns one at a time, and ← / → do the same from the keyboard. Shift with them jumps ten."
+},
+{
+"page": "The report",
+"section": "The tree",
+"url": "guide/report.html#the-tree",
+"text": "The sliders icon in the phylogeny header opens everything that shapes the tree. Shape. Phylogram draws branch lengths to scale; cladogram ignores them and shows topology only. A scale bar under the pane gives the phylogram its units. Root. Every ancestral reading in the report depends on where the tree is rooted, so it is an explicit choice: the L8 outgroup when one is present, the tree as supplied, or the midpoint of the longest tip-to-tip path. Alt and a click on any tip roots on that genome instead. Tip colour. By the selected cluster's state, by MTBC lineage, by mechanism, by rescue burden"
+},
+{
+"page": "The report",
+"section": "The matrix",
+"url": "guide/report.html#the-matrix",
+"text": "Each cell is one cluster in one genome, in one of six states: input grey present in the input matrix corrected blue absent in the input; Recore found the gene and corrected the cell NSC orange absent in the input; near-identical clusters were merged and the merged cluster is present (only when NSC ran) kept out crimson Recore found sequence evidence, but the matrix policy, a quality floor or a synteny or paralog guard kept it out of the corrected matrix absent pale absent; Recore searched the gap and found no evidence not searched pale, hatched absent in the input and never searched: the clust"
+},
+{
+"page": "The report",
+"section": "Comparing two genomes",
+"url": "guide/report.html#comparing-two-genomes",
+"text": "Two genomes far apart in the tree are far apart in the matrix. Pin to compare in the genome card, or P, keeps a genome's row in a band under the column header, aligned with every column whatever the scroll; its name sits level with it in the tree pane's header, marked A or B, and its tip gets a blue ring. Pinning a third genome unpins the oldest. Hover a cell of the band for its state; click it to open that cell. With two genomes pinned, the genome card compares them over every cluster of the corrected matrix: how many are in both, only in A, only in B and in neither, and the clusters that dif"
+},
+{
+"page": "The report",
+"section": "Synteny",
+"url": "guide/report.html#synteny",
+"text": "Press S or use the layers button to show the synteny band. It draws the top unique five-neighbour blocks for the selected cluster, at most 10 per cluster, with gene arrows keeping strand and proportional length. Uniqueness is defined by the ordered displayed gene architecture, ignoring internal cluster gaps, coordinate differences and length differences. Each row shows one representative locus. Selecting a row marks its carrier tips on the tree. Row 2 is the inferred reference context from shared neighbours, and it is clickable. Clicking any gene arrow jumps to that cluster in the matrix. Shar"
+},
+{
+"page": "The report",
+"section": "Run details",
+"url": "guide/report.html#run-details",
+"text": "D or the info button opens the run details. They start with what you need to write the run up: Using these results: a methods paragraph built from the run's own settings and counts, with a button that copies it; the caveats the evidence calls for (cells kept out of the matrix, corrections whose span is not whole codons, rejected candidates, synteny or the tree being off); and the parameters, the command line and the time the report was written. Changed cells: every evidence row, corrections first, with a button that copies them as tab-separated text and one that downloads recore_changed_cells."
+},
+{
+"page": "The report",
+"section": "Getting figures out",
+"url": "guide/report.html#getting-figures-out",
+"text": "The download button writes recore_matrix.png and, when the synteny band is open, recore_synteny.svg. The PNG is drawn for the page, not copied from the screen: the tree, the lineage strip, the column names and the matrix share one geometry, so every tip sits on its row, at three times the on-screen size, always in the light palette and without the hover crosshair or the selection marks. Below the matrix come a key for the cell states, the tree's scale bar and a caption with what is shown, the root, the matrix policy and the Recore version. When the whole matrix fits in 4,000 by 4,000 screen pi"
+},
+{
+"page": "The report",
+"section": "Keyboard shortcuts",
+"url": "guide/report.html#keyboard-shortcuts",
+"text": "Press ? for this list in the report itself. A switch at its top turns the one-letter shortcuts off, for example when they fire by accident with speech input; Cmd+K and the arrows keep working, and the choice is remembered in that browser. Cmd+K / Ctrl+K / / find a cluster or genome ← → previous / next cluster Shift + ← → jump ten clusters S show or hide the synteny panel D open the run details E export the view F fullscreen T switch theme C phylogram or cladogram R reroot at the midpoint Z fit the matrix to the window P pin or unpin the open genome, to compare two Cmd / Ctrl + wheel, or pinch "
+},
+{
+"page": "The report",
+"section": "Scale",
+"url": "guide/report.html#scale",
+"text": "The matrix ships as compact per-cluster state sets rather than one character per cell, and each pane draws only its visible window onto a viewport-sized canvas. A 10,000 genome by 30,000 cluster matrix, 300 million cells, is a 17 MB file that reaches interactive in about 130 ms and re-renders in single-digit milliseconds at every zoom level. The workspace is embedded up to 20,000 matched tips and 1.2 billion cells. Above either limit the report says so and points at gene_presence_absence_recore_states.csv for the full matrix, rather than shipping something a browser cannot open."
+},
+{
+"page": "The report",
+"section": "If you did not use --write-html-report",
+"url": "guide/report.html#if-you-did-not-use---write-html-report",
+"text": "benchmarks/recore_report.py builds a compact Markdown and HTML report (recore_report.md and recore_compact_report.html) from an existing output directory. It refuses to replace an interactive recore_report.html unless given --force."
+},
+{
+"page": "Algorithm",
+"section": "",
+"url": "how-it-works/algorithm.html",
+"text": ""
+},
+{
+"page": "Algorithm",
+"section": "The pipeline at a glance",
+"url": "how-it-works/algorithm.html#the-pipeline-at-a-glance",
+"text": "InputsPA matrix + pan-ref FASTA + assemblies + optional GFF Find matrix gaps Batch minimap2primary indexed target Exact no-CDS passGFF + synteny mode Classify evidencefull, missing CDS, split, fragment, pseudogene, paralog Orthology support? Accepted evidence Rejected audit-only Matrix policy Multistate matrixand evidence tables Corrected binary matrix Optional auditsreference genome + NSC + tree + mobile elements Reports + optional corrected GFF The corrected binary matrix is not the only output. Recore also keeps the evidence trail that explains why a cell was corrected, retained as audit-on"
+},
+{
+"page": "Algorithm",
+"section": "Overview",
+"url": "how-it-works/algorithm.html#overview",
+"text": "Recore rescues falsely absent genes from pangenome matrices produced by Panaroo (or Roary). It takes a gene presence/absence CSV, the pan-genome reference FASTA, individual genome FASTAs, and (optionally) GFF annotations, then identifies genes that are physically present in genomes but missing from the matrix. The pipeline runs in 12 sequential phases. Each phase is deterministic given the same inputs and thread count."
+},
+{
+"page": "Algorithm",
+"section": "Phase 1: Input Parsing",
+"url": "how-it-works/algorithm.html#phase-1-input-parsing",
+"text": ""
+},
+{
+"page": "Algorithm",
+"section": "Presence/absence matrix",
+"url": "how-it-works/algorithm.html#presenceabsence-matrix",
+"text": "Parses Panaroo's gene_presence_absence.csv. The CSV has 13-15 metadata columns (Gene, Annotation, No. isolates, etc.) followed by one column per genome. Each cell is either empty (gene absent) or contains semicolon-separated gene IDs. The matrix is stored as a PresenceAbsence struct with encapsulated access via newtype indices (ClusterIdx, GenomeIdx) to prevent index confusion."
+},
+{
+"page": "Algorithm",
+"section": "Pan-genome reference",
+"url": "how-it-works/algorithm.html#pan-genome-reference",
+"text": "Parses Panaroo's pan_genome_reference.fa. Each entry has format >clusterName~~~genomeName description. The ~~~ separator is stripped to extract the cluster name. Single-allele mode (default): keeps only the longest allele per cluster. Multi-allele mode (--multi-allele): keeps all alleles, increasing sensitivity at the cost of more minimap2 queries."
+},
+{
+"page": "Algorithm",
+"section": "Genome FASTAs",
+"url": "how-it-works/algorithm.html#genome-fastas",
+"text": "Scans the genomes directory for files with extensions .fasta, .fa, .fna (also .gz variants). File stems become genome names matched against CSV column headers."
+},
+{
+"page": "Algorithm",
+"section": "GFF annotations (optional)",
+"url": "how-it-works/algorithm.html#gff-annotations-optional",
+"text": "Parses GFF3 files matching genome names. Extracts CDS features only: contig, start, end, strand, gene ID. Coordinates are converted from GFF3's 1-based closed to 0-based half-open (matching minimap2 PAF output). Supports .gff, .gff3, and gzipped variants. GFF3 percent-encoded attributes are decoded before identifier, alias, product, and note matching."
+},
+{
+"page": "Algorithm",
+"section": "Cross-validation",
+"url": "how-it-works/algorithm.html#cross-validation",
+"text": "Before proceeding, validates: Genome and cluster names are safe for path construction (no ../, /, \\0) The matrix and pan-reference FASTA are not empty Genome names and cluster names are unique Warns about clusters in CSV but not in reference (and vice versa) Warns about genomes without matching FASTA files"
+},
+{
+"page": "Algorithm",
+"section": "Phase 2: Gap Identification",
+"url": "how-it-works/algorithm.html#phase-2-gap-identification",
+"text": "Iterates over every (cluster, genome) pair. A gap is any pair where the gene is absent. Gaps are filtered by prevalence: if a cluster is present in fewer than --min-prevalence fraction of genomes (default 10%), its gaps are skipped. The rationale is that ultra-rare clusters are likely truly accessory, and searching for them in hundreds of genomes generates cross-match noise. The remaining gaps are grouped by genome for batch processing."
+},
+{
+"page": "Algorithm",
+"section": "Phase 3: GFF Index Construction",
+"url": "how-it-works/algorithm.html#phase-3-gff-index-construction",
+"text": "For each genome with a GFF file, builds a GffIndex: Genes sorted by (contig, start position) Per-contig range index for O(log n) overlap queries via binary search (partition_point) Every name of every CDS (its gene ID, then its aliases) stored once, back to back in one buffer, with a sorted table of name hashes for lookup by name Products and notes read once to recognise mobile elements, then dropped A CDS costs 32 bytes plus its names, so the index grows with a genome's gene count and the length of its names, not with how many maps refer to them. The index supports: overlapping(contig, start,"
+},
+{
+"page": "Algorithm",
+"section": "Phase 4: Gene-to-Cluster Maps",
+"url": "how-it-works/algorithm.html#phase-4-gene-to-cluster-maps",
+"text": "For each genome with an annotation, builds a map from gene ID → ClusterIdx. This maps every gene ID found in the CSV cells to its Panaroo cluster assignment. Panaroo appends suffixes to gene IDs that don't exist in the original GFF: _pseudo for pseudogene annotations _dup_N for duplicate gene copies The map indexes both the original ID and stripped versions to maximize GFF matching success. The map is then read into one cluster for each name of the genome's GffIndex (4 bytes each, a GeneClusters) and dropped. Every later lookup is for a name the annotation uses, so this answers each one as the"
+},
+{
+"page": "Algorithm",
+"section": "Phase 5: Search (minimap2)",
+"url": "how-it-works/algorithm.html#phase-5-search-minimap2",
+"text": "This is the computational core. Each genome that has gaps is searched for its own absent clusters, one minimap2 run per genome, in parallel across --threads."
+},
+{
+"page": "Algorithm",
+"section": "5a. One genome at a time",
+"url": "how-it-works/algorithm.html#5a-one-genome-at-a-time",
+"text": "A genome's queries are only the clusters absent from it, and its target is only its own assembly. Genomes that carry a cluster never compete with the genome where it is missing. That isolation is the point. Concatenating every genome into one target looks cheaper, but minimap2's secondary budget (-N) and its secondary-to-primary ratio (-p) then apply across all genomes at once. Once more than about fifty genomes carry a gene, their full-length copies use up the budget and the gap genome's hit is dropped; a gap genome's weaker copy, or the two pieces of a gene split across contigs, fall under h"
+},
+{
+"page": "Algorithm",
+"section": "5b. Query preparation",
+"url": "how-it-works/algorithm.html#5b-query-preparation",
+"text": "The cluster sequences a genome needs are written to a temporary FASTA. In multi-allele mode, alleles are named clusterName~~~ALLELE~0, clusterName~~~ALLELE~1, etc."
+},
+{
+"page": "Algorithm",
+"section": "5c. Target preparation",
+"url": "how-it-works/algorithm.html#5c-target-preparation",
+"text": "The genome's FASTA is read by Recore's own reader (plain or gzipped) and written to a temporary file in normalised form: each header is cut to its contig name, line endings are normalised. minimap2 is given that copy rather than the user's file, so the aligner and the rest of the pipeline agree on what the assembly contains. Inputs that would otherwise give confident wrong answers fail here, naming the file and line: sequence before the first > header, an empty or non-UTF-8 contig name, and a contig name used twice in one assembly."
+},
+{
+"page": "Algorithm",
+"section": "5d. Alignment",
+"url": "how-it-works/algorithm.html#5d-alignment",
+"text": "Every pass runs with base-level alignment, so identity and coverage are exact: -c: base-level alignment. Without it minimap2 reports chain-based match counts, which read a 99.6% identical gene as about 93%, and every identity threshold would be comparing against that. -s 40: the asm presets ask for a peak alignment score of 200, which with -c drops every gene shorter than about 200 bp. --end-bonus 40: beats the asm5 mismatch penalty, so one SNP near either end of a gene does not clip the alignment short of the gene's end. --secondary=yes -N 500 -p 0.5: report secondary alignments, needed for s"
+},
+{
+"page": "Algorithm",
+"section": "5e. Hit filtering",
+"url": "how-it-works/algorithm.html#5e-hit-filtering",
+"text": "Each hit is assigned to its cluster through the query name and filtered by minimum identity (--min-identity, default 90%). Identity is exact, so the threshold means what it says. Result: a map from (genome, cluster) to its hits."
+},
+{
+"page": "Algorithm",
+"section": "Phase 6: Hit Classification",
+"url": "how-it-works/algorithm.html#phase-6-hit-classification",
+"text": "Each (cluster, genome) gap with hits is classified into one of 9 rescue types. Classification follows three paths:"
+},
+{
+"page": "Algorithm",
+"section": "Path 1: Single full-coverage hit (≥ min_coverage)",
+"url": "how-it-works/algorithm.html#path-1-single-full-coverage-hit--min_coverage",
+"text": "The best hit (by query length, then score) covers ≥80% of the reference sequence (default --min-coverage). Classification depends on GFF annotation overlap: Before classification, Recore builds a conservative gene-to-cluster index from the matrix and the GFF. If one alias of a CDS maps unambiguously to a cluster, its other aliases (locus_tag, gene, Name, protein_id, old locus tags) are mapped to the same cluster. Ambiguous aliases that point to multiple clusters are left unresolved. No GFF provided DROPPED identity × coverage GFF loaded, no substantial CDS overlaps hit, identity ≥95% MISSING_C"
+},
+{
+"page": "Algorithm",
+"section": "Path 2: Conservative fragment-chain assembly (same allele, contig, and strand)",
+"url": "how-it-works/algorithm.html#path-2-conservative-fragment-chain-assembly-same-allele-contig-and-strand",
+"text": "If no single hit has sufficient coverage, hits are first grouped by query allele, query length, target contig, and strand. Recore builds the best co-linear chain within each group and uses only the best chain. This prevents independent alleles or paralogous hits from being merged into a false fragmented gene. If the best chain reaches --min-coverage with ≥2 fragments: Non-co-linear chains are not rescued as fragments. If the whole chain lies inside one compatible CDS, Recore classifies it through the single-CDS annotation logic (DROPPED, START_VARIANT, SPLIT, or PARALOG) instead of calling FRA"
+},
+{
+"page": "Algorithm",
+"section": "Path 3: Partial deletion pseudogene",
+"url": "how-it-works/algorithm.html#path-3-partial-deletion-pseudogene",
+"text": "If the best hit has ≥90% identity but only 60-80% coverage (below min_coverage), the gene is classified as PSEUDOGENE:PartialDeletion. Multi-fragment coverage is also considered."
+},
+{
+"page": "Algorithm",
+"section": "If none of the paths match → no rescue (gene is truly absent)",
+"url": "how-it-works/algorithm.html#if-none-of-the-paths-match--no-rescue-gene-is-truly-absent",
+"text": ""
+},
+{
+"page": "Algorithm",
+"section": "Phase 7: Internal Stop Codon Scanning",
+"url": "how-it-works/algorithm.html#phase-7-internal-stop-codon-scanning",
+"text": "DROPPED and MISSING_CDS rescues are post-processed for pseudogene detection. For each genome with at least one sequence-only rescue: Load genome FASTA contigs on-demand (not preloaded for all genomes) If the rescue sits on an annotated CDS, extract that CDS and read it in its own frame, from its start codon to its stop, counting stops before the last codon. That is the reading frame this genome's annotation asserts, and it ends at this genome's own stop. Otherwise extract the aligned region, reverse-complement it on the minus strand, and read it in the frame the alignment fixes. The cluster's "
+},
+{
+"page": "Algorithm",
+"section": "Phase 8: Synteny Validation",
+"url": "how-it-works/algorithm.html#phase-8-synteny-validation",
+"text": "When --synteny is enabled and GFF files are loaded, each rescue is validated against the genomic context of reference genomes where the gene IS present."
+},
+{
+"page": "Algorithm",
+"section": "Algorithm",
+"url": "how-it-works/algorithm.html#algorithm-1",
+"text": "For the rescued gene at position (contig, start, end) in the target genome: Build side-specific context windows around the rescued locus using 1, 2, 3, 4, and 5 upstream/downstream CDS neighbours. If the rescue substantially overlaps an annotated CDS, that CDS is used as the centre so short start-site differences and overlapping CDS boundaries do not shift the context. Otherwise the locus midpoint is used, which is important for missing-CDS calls with only tiny overlaps into adjacent genes. For every reference genome where this cluster IS present, in deterministic genome order: a. Read every g"
+},
+{
+"page": "Algorithm",
+"section": "Confidence adjustment",
+"url": "how-it-works/algorithm.html#confidence-adjustment",
+"text": "Synteny PASS: confidence × 1.2 (boost) Synteny FAIL: confidence × 0.6, then reject from the corrected matrix No flanking genes found: no adjustment (synteny_ok = None)"
+},
+{
+"page": "Algorithm",
+"section": "Phase 9: Conflict Resolution",
+"url": "how-it-works/algorithm.html#phase-9-conflict-resolution",
+"text": "Multiple rescues can overlap on the genome (e.g., a DROPPED and a PARALOG rescue at the same locus). Conflicts are resolved by: Sort rescues by (genome, contig, start position) For substantial overlaps on the same contig (≥50% of the shorter interval): keep the one with higher confidence Single-pass O(n) sweep with a removal bitmap"
+},
+{
+"page": "Algorithm",
+"section": "Phase 10: Nucleotide Similarity Clustering (NSC)",
+"url": "how-it-works/algorithm.html#phase-10-nucleotide-similarity-clustering-nsc",
+"text": "Optional (--nscluster). Detects Panaroo clusters that are distinct at the protein level but nearly identical at the nucleotide level (annotation-level redundancy)."
+},
+{
+"page": "Algorithm",
+"section": "MinHash engine (default, built-in)",
+"url": "how-it-works/algorithm.html#minhash-engine-default-built-in",
+"text": "Sketch: Bottom-k MinHash with 128 hashes per cluster. Uses canonical k-mers (min of forward and reverse-complement hash) with FNV-1a hashing over upper-cased bases, so soft-masked sequence sketches like the rest. Multi-allele clusters are concatenated with N-spacers to prevent cross-allele k-mers. Candidates: an index from hash value to the sketches holding it. Two clusters are candidates when their sketches share at least two values; a value held by more than 500 sketches (a repeat) is ignored. Positional bands over sorted bottom-k sketches, used before, rarely line up between a gene and a ne"
+},
+{
+"page": "Algorithm",
+"section": "MMseqs2 engine (optional, exact)",
+"url": "how-it-works/algorithm.html#mmseqs2-engine-optional-exact",
+"text": "Uses MMseqs2 linclust for linear-time clustering with alignment-based identity: createdb → linclust → createtsv Connected-component clustering mode Bidirectional coverage requirement Automatic temp directory cleanup"
+},
+{
+"page": "Algorithm",
+"section": "Phase 11: Output Generation",
+"url": "how-it-works/algorithm.html#phase-11-output-generation",
+"text": ""
+},
+{
+"page": "Algorithm",
+"section": "rescue_report.tsv",
+"url": "how-it-works/algorithm.html#rescue_reporttsv",
+"text": "One row per rescue event. Columns: gene_cluster, genome, rescue_type, identity, coverage, contig, start (1-based), end, strand, confidence, synteny, quantitative synteny evidence (synteny_score, support count, reference context count, selected neighbour window, upstream/downstream matches), ORF integrity signal, annotation_match, plus fragment-chain evidence columns for multi-hit rescues."
+},
+{
+"page": "Algorithm",
+"section": "synteny_rejected_report.tsv",
+"url": "how-it-works/algorithm.html#synteny_rejected_reporttsv",
+"text": "Written only when --synteny rejects at least one candidate. It uses the same schema as rescue_report.tsv, but these rows are audit records only and are not inserted into gene_presence_absence_recore.csv."
+},
+{
+"page": "Algorithm",
+"section": "paralog_rejected_report.tsv",
+"url": "how-it-works/algorithm.html#paralog_rejected_reporttsv",
+"text": "Written only when PARALOG candidates lack explicit synteny support. It uses the same schema as rescue_report.tsv, but these rows are audit records only and are not inserted into gene_presence_absence_recore.csv."
+},
+{
+"page": "Algorithm",
+"section": "orthology_evidence.tsv",
+"url": "how-it-works/algorithm.html#orthology_evidencetsv",
+"text": "One row per accepted rescue plus rejected synteny or paralog candidate. This table makes the decision layer explicit: accepted/rejected status, orthology status, evidence state (STRICT_PRESENT, ALIAS_CLUSTER, SEQUENCE_ONLY, PARTIAL_HOMOLOGY, PSEUDOGENE, HOMOLOG_PARALOG, REJECTED_CONTEXT), matrix action, publication status, annotation-matched cluster, synteny state, quantitative synteny evidence, ORF integrity signal, confidence, coordinates, and rejection reason. Before an accepted rescue enters the binary matrix, Recore also applies an adaptive orthology-quality guard. The guard raises the re"
+},
+{
+"page": "Algorithm",
+"section": "reference_genome_audit.tsv",
+"url": "how-it-works/algorithm.html#reference_genome_audittsv",
+"text": "Written when --reference-genome is provided. The reference can be an exact matrix column or an embedded ASxxxxx identifier. For every cluster, Recore reports whether the reference matrix already contains it, whether Recore rescued reference evidence, whether only rejected/paralog/partial evidence exists, or whether no evidence was found. The reference_absence_class column is deliberately simple: new_to_reference: no reference evidence under the current thresholds. reference_like_or_present: already present or rescued in the audited reference. reference_like_ambiguous: partial, pseudogene, para"
+},
+{
+"page": "Algorithm",
+"section": "evolutionary_events.tsv",
+"url": "how-it-works/algorithm.html#evolutionary_eventstsv",
+"text": "Written for every run. Recore combines the corrected matrix state, reference event audit, rescue classifications, synteny rejections, paralog rejections, cluster locus context, optional GFF mobile-element labels, and optional Newick tree evidence into one interpretation table. When --tree is provided, Recore performs binary Fitch parsimony per cluster: tips are matched to matrix columns by exact name or embedded ASxxxxx ID, and gains/losses are counted by a top-down assignment consistent with the Fitch state sets. If the root state is ambiguous under Fitch, Recore now reports ancestral_state=a"
+},
+{
+"page": "Algorithm",
+"section": "cluster_locus_context.tsv / cluster_locus_summary.tsv",
+"url": "how-it-works/algorithm.html#cluster_locus_contexttsv--cluster_locus_summarytsv",
+"text": "Written when GFF annotations are loaded. Recore builds a two-gene upstream and two-gene downstream context window for each present gene ID that can be resolved in the GFF. The context table lists every observed cluster-side context; the summary table flags clusters as stable single-locus, mostly stable with minor context noise, possible multi-locus/paralog, or insufficient context. Synteny validation uses an adaptive 1-to-5-neighbour model, but this summary remains fixed-width so locus stability can be compared across clusters."
+},
+{
+"page": "Algorithm",
+"section": "cluster_synteny_blocks.tsv",
+"url": "how-it-works/algorithm.html#cluster_synteny_blockstsv",
+"text": "Written when GFF annotations are loaded. This audit-only table summarizes wider five-neighbour upstream/downstream blocks for every resolvable present cluster. Unique blocks are collapsed by ordered displayed gene/locus identity: internal cluster gaps, representative coordinates, and gene lengths do not create additional blocks. One representative locus and the full carrier-genome set are kept for each unique architecture. It is intended for reviewing translocations, tandem duplications, or alternative local contexts without changing the correction matrix. If a reference genome is known, Recor"
+},
+{
+"page": "Algorithm",
+"section": "genome_qc.tsv",
+"url": "how-it-works/algorithm.html#genome_qctsv",
+"text": "Written for every run. Recore summarizes each genome's FASTA/GFF availability, GFF contig remapping, searchable gaps, accepted rescues, matrix corrections, NSC-added presence, synteny rejections, and paralog rejections. The qc_flags column highlights genomes that may need assembly or annotation review before interpreting noisy presence/absence patterns. Successful GFF contig remapping is kept as an informational flag and does not by itself move a genome from pass to review."
+},
+{
+"page": "Algorithm",
+"section": "cluster_components.tsv / cluster_component_members.tsv",
+"url": "how-it-works/algorithm.html#cluster_componentstsv--cluster_component_memberstsv",
+"text": "Written for every run. Recore converts pairwise cluster relationships into connected components. Edges come from split-cluster evidence, annotation-match evidence, NSC merges, and paralog evidence. The component tables make it clear whether a cluster should be reviewed as a possible alias/redundant component or as part of a paralog family that should remain distinct by default. Component membership is mirrored in evolutionary_events.tsv."
+},
+{
+"page": "Algorithm",
+"section": "gene_presence_absence_recore.csv",
+"url": "how-it-works/algorithm.html#gene_presence_absence_recorecsv",
+"text": "Drop-in replacement for Panaroo's CSV. Changes: Rescued genes: cell value RESCUED:contig:start-end (1-based coordinates) Binary correction is controlled by --matrix-policy (strict, balanced, sensitive) plus the adaptive orthology-quality guard NSC-merged genes: cell value NSC_MERGED Clusters absorbed by NSC are removed (rows dropped) Metadata columns (No. isolates, No. sequences) updated to reflect rescues"
+},
+{
+"page": "Algorithm",
+"section": "gene_presence_absence_recore_states.csv",
+"url": "how-it-works/algorithm.html#gene_presence_absence_recore_statescsv",
+"text": "Same shape as the corrected matrix, but genome cells are states rather than binary calls: PRESENT_INPUT, ABSENT, STRICT_PRESENT, ALIAS_CLUSTER, SEQUENCE_ONLY, PARTIAL_HOMOLOGY, PSEUDOGENE, HOMOLOG_PARALOG, or REJECTED_CONTEXT, and NOT_SEARCHED_LOW_PREVALENCE, NOT_SEARCHED_NO_CLUSTER_SEQUENCE or NOT_SEARCHED_NO_FASTA for a gap that was never searched, so it is not mistaken for one searched and found empty. This is the preferred output for plotting or reviewing ambiguous biology without forcing it into a clean 0/1 matrix."
+},
+{
+"page": "Algorithm",
+"section": "summary.txt",
+"url": "how-it-works/algorithm.html#summarytxt",
+"text": "Human-readable summary with gap counts, accepted rescues, matrix-corrected rescues under the selected policy, rescue type breakdown, core/soft-core genome changes, synteny stats, and NSC stats."
+},
+{
+"page": "Algorithm",
+"section": "nscluster_report.tsv (if NSC enabled)",
+"url": "how-it-works/algorithm.html#nscluster_reporttsv-if-nsc-enabled",
+"text": "One row per NS cluster: cluster ID, representative name, member count, semicolon-separated member names."
+},
+{
+"page": "Algorithm",
+"section": "Phase 12: Core Genome Recalculation",
+"url": "how-it-works/algorithm.html#phase-12-core-genome-recalculation",
+"text": "After all rescues and NSC merges, the core and soft-core genome sizes are recalculated: Core (strict): clusters present in ALL genomes (including rescues and NSC unions) Soft-core (≥95%): clusters present in ≥95% of genomes NSC-merged clusters are excluded from the count (their presences are absorbed into representatives)."
+},
+{
+"page": "Algorithm",
+"section": "Rescue Type Reference",
+"url": "how-it-works/algorithm.html#rescue-type-reference",
+"text": "DROPPED Gene present in genome but cleaned by Panaroo graph cleaning 1.0× (base) MISSING_CDS Gene sequence present at a locus with no substantial overlapping CDS in the GFF 1.0× (base) SPLIT Gene assigned to wrong Panaroo cluster (≥95% identity to another cluster's gene) 1.1× FRAGMENTED Gene broken into ≥2 fragments with combined coverage ≥80% and a large biological gap 0.8× ALIGNMENT_SPLIT Gene reconstructed by ≥2 small co-linear alignments without biological break evidence 0.9× START_VARIANT Same gene with different start codon (≤300bp length difference) 0.95× PARALOG Cross-match to a relate"
+},
+{
+"page": "Algorithm",
+"section": "Computational Complexity",
+"url": "how-it-works/algorithm.html#computational-complexity",
+"text": "Parsing O(clusters × genomes) O(clusters × genomes) Gap filtering O(clusters × genomes) O(gaps) GFF indexing O(genes × log(genes)) per genome O(genes) per genome minimap2 O(queries × target_size) O(streaming), PAF not buffered Classification O(hits) with rayon parallelism O(rescues) Internal stops O(DROPPED_rescues × hit_length) O(genome) per genome Synteny O(rescues × neighbour_windows × present_cluster_copies) O(contexts per cluster) per rescue Conflict resolution O(rescues × log(rescues)) O(rescues) NSC MinHash O(clusters × seq_length) + O(candidates) O(clusters × sketch_size) NSC MMseqs2 O"
+},
+{
+"page": "Evidence states",
+"section": "",
+"url": "how-it-works/evidence-states.html",
+"text": "The multistate matrix, gene_presence_absence_recore_states.csv, has the same shape as the input presence/absence matrix but stores a biological state per cell instead of a 0 or a 1. It is the file to read for audit and for figures; the binary matrix is the file to feed downstream tools. PRESENT_INPUT present in the original matrix already present ABSENT searched, and no accepted evidence found no NOT_SEARCHED_LOW_PREVALENCE never searched: the cluster is in fewer genomes than --min-prevalence no NOT_SEARCHED_NO_CLUSTER_SEQUENCE never searched: the cluster has no sequence in the pan-genome refe"
+},
+{
+"page": "Evidence states",
+"section": "Absent or not searched",
+"url": "how-it-works/evidence-states.html#absent-or-not-searched",
+"text": "ABSENT is a finding: Recore searched that gap and nothing cleared the thresholds. A gap outside the search is not, and says why instead. Under the default --min-prevalence of 0.1 that is every gap of a cluster in fewer than one genome in ten, which in most pangenomes is most of the empty cells. Before this distinction both read ABSENT. When two reasons apply, the cluster's is written, in the order the pipeline filters gaps: prevalence, then a missing cluster sequence, then a missing FASTA. That is the order of the gap lines in summary.txt, which count the same cells except those NSC later fill"
+},
+{
+"page": "Evidence states",
+"section": "Reading the states in the report",
+"url": "how-it-works/evidence-states.html#reading-the-states-in-the-report",
+"text": "The HTML viewer collapses these thirteen states into the six a matrix can actually be read in: input PRESENT_INPUT corrected the states corrected by the active policy NSC presence merged in from a nucleotide-redundant cluster kept out evidence the policy, a quality floor or a guard kept out of the matrix absent ABSENT and the other states that are not corrected not searched, hatched the three NOT_SEARCHED_* states Switching the matrix to mechanism colouring instead shows the evolutionary call from evolutionary_events.tsv, which answers a different question: not \"is it there?\" but \"what kind of"
+},
+{
+"page": "Evidence states",
+"section": "Which policy corrects which state",
+"url": "how-it-works/evidence-states.html#which-policy-corrects-which-state",
+"text": "See Matrix policies for the full table. In short: strict corrects only STRICT_PRESENT and ALIAS_CLUSTER; balanced adds SEQUENCE_ONLY; sensitive also admits partial and pseudogene evidence that clears the adaptive guard."
+},
+{
+"page": "Rescue types",
+"section": "",
+"url": "how-it-works/rescue-types.html",
+"text": "Recore classifies each rescued gene into one of 9 types based on the alignment evidence and GFF annotation context. Examples below show the core rescue_report.tsv columns; see Output files for the full schema, including fragment evidence columns."
+},
+{
+"page": "Rescue types",
+"section": "DROPPED",
+"url": "how-it-works/rescue-types.html#dropped",
+"text": "What it means: The gene is physically present in the genome but is absent from the matrix, typically because graph cleaning dropped it or because annotation transfer never placed it. Panaroo's graph-based cleaning can over-aggressively trim genes that are divergent or connected to fragmented paths in the de Bruijn graph. Evidence: Full-coverage hit (≥80%) with no overlapping GFF gene assigned to a different cluster. Confidence: identity × coverage (base confidence, unmodified) Example in rescue_report.tsv:"
+},
+{
+"page": "Rescue types",
+"section": "MISSING_CDS",
+"url": "how-it-works/rescue-types.html#missing_cds",
+"text": "What it means: The gene sequence is present in the assembly, but the supplied GFF has no substantial CDS feature at that locus. This catches annotation omissions rather than Panaroo-only matrix cleaning errors. Evidence: Full-coverage hit (≥80%) with ≥95% identity, GFF annotations loaded, and no CDS overlapping more than half of both the hit span and the annotated CDS. Tiny boundary overlaps with adjacent genes are allowed. Confidence: identity × coverage (base confidence, unmodified) Matrix behavior: A MISSING_CDS candidate is corrected only when --synteny gives PASS, which upgrades it to STR"
+},
+{
+"page": "Rescue types",
+"section": "SPLIT",
+"url": "how-it-works/rescue-types.html#split",
+"text": "What it means: The gene locus is present and annotated, but Panaroo assigned it to a different cluster. This typically happens when a gene has subtle sequence variation that caused it to cluster separately from the canonical reference. Evidence: Full-coverage hit (≥80%) with a GFF gene at that locus that belongs to a different Panaroo cluster, with ≥95% nucleotide identity. Confidence: identity × coverage × 1.1 (slight boost: evidence is strong) Example: The value after SPLIT: is the cluster that actually contains the annotation."
+},
+{
+"page": "Rescue types",
+"section": "FRAGMENTED",
+"url": "how-it-works/rescue-types.html#fragmented",
+"text": "What it means: The gene is broken into multiple biological pieces in this genome. Common causes are assembly breaks (contig boundaries), IS element insertions, or large indels that split the reading frame. Evidence: A co-linear chain of ≥2 hits from the same query allele, target contig, and strand whose merged coverage reaches ≥80% of the reference, with a large query or target gap (≥50 bp). Non-co-linear chains are not rescued as FRAGMENTED, and chains contained inside one compatible CDS are classified through the single-CDS annotation logic instead. A gene split across two contigs is FRAGMEN"
+},
+{
+"page": "Rescue types",
+"section": "ALIGNMENT_SPLIT",
+"url": "how-it-works/rescue-types.html#alignment_split",
+"text": "What it means: The gene is reconstructed by multiple minimap2 alignments, but the evidence does not support a biological gene break. This is usually an alignment or representation split rather than a functional disruption. Evidence: A co-linear multi-hit chain reaches the coverage threshold, but the query and target gaps are small (<50 bp), no frameshift-like gap is detected, and the chain is not better explained as a single-CDS annotation match. Confidence: avg_identity × total_coverage × 0.9 Example: These events are reported separately so downstream analyses can avoid treating technical spl"
+},
+{
+"page": "Rescue types",
+"section": "START_VARIANT",
+"url": "how-it-works/rescue-types.html#start_variant",
+"text": "What it means: The gene is present with the correct sequence but starts at a different position than the reference annotation. Alternative start codons (GTG, TTG) and upstream ATGs can lead to start site disagreements between annotators. Evidence: A hit covering ≥90% of the query with ≥98% identity, overlapping an annotated CDS on the same strand that agrees with the cluster's representative at the stop (within one codon) and differs from it at the start by 1 to 299 bp. The representative's start and stop are placed on the contig from the alignment, counting the query bases it left unaligned a"
+},
+{
+"page": "Rescue types",
+"section": "PARALOG",
+"url": "how-it-works/rescue-types.html#paralog",
+"text": "What it means: The hit is a related gene from a different family (a paralog), not the missing gene itself. Common in high-GC organisms with large multi-copy gene families (PE/PPE in M. tuberculosis, IS elements). Evidence: Full-coverage hit with a GFF gene at that locus belonging to a different cluster, with <95% identity. Confidence: identity × coverage × 0.7 (reduced: this is a cross-match, not a true rescue) Matrix behavior: PARALOG candidates are audit-only unless synteny explicitly passes. Without synteny=PASS, they are written to paralog_rejected_report.tsv and are not inserted into gene"
+},
+{
+"page": "Rescue types",
+"section": "PSEUDOGENE:InternalStop",
+"url": "how-it-works/rescue-types.html#pseudogeneinternalstop",
+"text": "What it means: The gene sequence is present with good coverage but contains one or more premature stop codons (TAA, TAG, TGA) in the reading frame. This is a functional pseudogene. Evidence: Full-coverage hit reclassified after scanning the genome FASTA sequence. When the rescue sits on an annotated CDS, that CDS is read in its own frame from its own start to its own stop, so a premature stop cannot hide in another frame and a representative that reads through its stop does not turn this genome's canonical stop into an internal one. Without an annotated CDS, the aligned region is reverse-compl"
+},
+{
+"page": "Rescue types",
+"section": "PSEUDOGENE:Frameshift",
+"url": "how-it-works/rescue-types.html#pseudogeneframeshift",
+"text": "What it means: The gene is fragmented in a pattern consistent with a frameshift mutation: fragments have high identity but are separated by small gaps whose size is not divisible by 3. Evidence: ≥2 fragments with average identity ≥95%, gaps between fragments ≤50bp, and at least one gap whose size mod 3 ≠ 0. Confidence: avg_identity × total_coverage × 0.5 Example:"
+},
+{
+"page": "Rescue types",
+"section": "PSEUDOGENE:PartialDeletion",
+"url": "how-it-works/rescue-types.html#pseudogenepartialdeletion",
+"text": "What it means: The gene is truncated: only 60-80% of the reference sequence is present with high identity. This is consistent with a partial deletion of one end of the gene. Evidence: Hit with ≥90% identity but coverage 60-80% (below --min-coverage, above 60%). Confidence: identity × coverage × 0.4 (lowest: truncated gene, evidence is weakest) Example:"
+},
+{
+"page": "Rescue types",
+"section": "Confidence Score",
+"url": "how-it-works/rescue-types.html#confidence-score",
+"text": "The confidence score combines alignment quality and rescue type into a single value: Where: identity_fraction = identity / 100 coverage_fraction = coverage / 100 type_modifier = see table above (1.0 for DROPPED, down to 0.4 for PartialDeletion) synteny_modifier = 1.2 if PASS, 1.0 if not assessed. Explicit FAIL candidates are penalized for audit and rejected from the corrected matrix when --synteny is enabled. A value close to 1.0 indicates a high-confidence rescue. Values below ~0.5 should be interpreted with caution."
+},
+{
+"page": "Rescue types",
+"section": "Rescue Type Priority in Conflict Resolution",
+"url": "how-it-works/rescue-types.html#rescue-type-priority-in-conflict-resolution",
+"text": "When two rescues overlap on the genome, the one with higher confidence wins. Because of the type modifiers, the natural priority order is: This prevents a PARALOG cross-match from overwriting a genuine DROPPED rescue at the same locus."
+},
+{
+"page": "Evolutionary events",
+"section": "",
+"url": "how-it-works/evolutionary-events.html",
+"text": "evolutionary_events.tsv is an interpretation layer, not a new correction policy. Its job is to answer a different question from the corrected matrix: The corrected matrix asks: is there enough evidence to mark this cluster as present in this genome? The evolutionary audit asks: what biological or technical process best explains the cluster-level pattern after correction? The two answers are deliberately kept separate. A cluster can remain a real matrix cluster while the recommended wording says \"reference-gene derivative\", \"paralog/duplication signal\", \"fragmented homology\", or \"review before "
+},
+{
+"page": "Evolutionary events",
+"section": "Evidence used",
+"url": "how-it-works/evolutionary-events.html#evidence-used",
+"text": "Each molecular_mechanism call combines: corrected cluster state after accepted rescues and optional NSC support; reference-genome audit and reference-derived cluster events; local locus-context stability and synteny/paralog rejection evidence; optional tree parsimony, including explicit ambiguous-root reporting; optional sequence-based mobile-element support; connected components of related clusters, so paralogous, redundant, split, or annotation-linked clusters are interpreted together. Tree tips are matched to matrix columns by exact name or embedded ASxxxxx identifier. If only a subset of t"
+},
+{
+"page": "Evolutionary events",
+"section": "Decision order",
+"url": "how-it-works/evolutionary-events.html#decision-order",
+"text": "The mechanism call is a deterministic evidence ladder. Direct molecular/locus evidence has priority over a visually attractive tree pattern, because a clean clade can still be caused by a reference allele, paralog collapse, gene fragmentation, or annotation boundaries. Reference-derived event check. If reference_cluster_events.tsv links the cluster to a gene in the audited reference, Recore first decides whether the cluster is a full-length/same-gene allele, a short/long in-frame allele, a frameshift/truncation/extension, or partial reference homology. If the same signal is also paralogous or "
+},
+{
+"page": "Evolutionary events",
+"section": "Mechanism meanings",
+"url": "how-it-works/evolutionary-events.html#mechanism-meanings",
+"text": "ancestral_retained_no_disruption The corrected cluster is broadly present and the tree pattern supports ancestral retention. Conserved ancestral presence in the analysed set; not a novel acquisition. clean_insertion_or_acquisition_candidate The cluster is absent from the audited reference, present in a coherent lineage/clade, and lacks stronger reference/paralog/fragmentation/artifact explanations. Best candidate for reference-absent accessory acquisition or insertion; validate flanks before claiming a novel locus. reference_or_lineage_loss_candidate The reference is absent, but the inferred r"
+},
+{
+"page": "Evolutionary events",
+"section": "How to use the calls",
+"url": "how-it-works/evolutionary-events.html#how-to-use-the-calls",
+"text": "Use phylogenetic_pattern to describe where the signal sits on the tree. Use molecular_mechanism to describe what process is the safest interpretation. Use recommended_wording for manuscript text, because it is written to avoid overclaiming. For manuscript highlighting, the safest candidates are usually clusters with: clean_insertion_or_acquisition_candidate, reference_or_lineage_loss_candidate, or deletion_loss_candidate; coherent phylogenetic_pattern with low/moderate blink flag; high carrier_mrca_purity or dollo_origin_purity; no paralog/component warning; compatible synteny and clear ORF in"
+},
+{
+"page": "Evolutionary events",
+"section": "Calls that rest on gaps Recore never searched",
+"url": "how-it-works/evolutionary-events.html#calls-that-rest-on-gaps-recore-never-searched",
+"text": "Gains and losses are made of absences: a gain needs the gene missing in the rest of the tree, a loss needs it missing where it was lost. A gap outside the search (a cluster below --min-prevalence, a cluster with no sequence to search with, or a genome with no FASTA) keeps the input matrix's call, so a call built on such absences is only as good as that call. Under the default prevalence floor that is every gain on a cluster in fewer than one genome in ten. absent_not_searched counts those absences and not_searched_reason says why (low_prevalence, no_cluster_sequence or no_fasta). For the calls"
+},
+{
+"page": "Evolutionary events",
+"section": "Where the fields live",
+"url": "how-it-works/evolutionary-events.html#where-the-fields-live",
+"text": "Every field named on this page is a column of evolutionary_events.tsv; its schema is in Output files. The report colours the matrix by molecular_mechanism when you ask it to, and its mechanism cards double as column filters: see Run report."
+},
+{
+"page": "Limits and speed",
+"section": "",
+"url": "how-it-works/limits.html",
+"text": ""
+},
+{
+"page": "Limits and speed",
+"section": "What Recore does not claim",
+"url": "how-it-works/limits.html#what-recore-does-not-claim",
+"text": "Recore is an audit and correction layer, not an oracle. It does not claim that every homologous sequence is a functional gene. It does not turn pseudogenes or partial homologues into clean binary presence under the recommended balanced policy. It does not prove ancestry or biological novelty by itself. Evolutionary labels are audit interpretations, to be read alongside reference evidence, phylogeny, synteny and external validation. It does not replace manual review for complex paralogous families, repetitive elements, or collapsed assemblies. It cannot recover evidence that is absent from the "
+},
+{
+"page": "Limits and speed",
+"section": "Ambiguity is reported as ambiguity",
+"url": "how-it-works/limits.html#ambiguity-is-reported-as-ambiguity",
+"text": "With --tree, Fitch parsimony root states that come out ambiguous are reported as ambiguous rather than being forced into a single ancestral story. Carrier-MRCA purity and Dollo-like origin and loss metrics are there to separate a compact clade-associated event from a blinking pattern that happens to have the same count. If tree tips do not cover every matrix genome, the unmatched genomes are ignored and Recore says so explicitly."
+},
+{
+"page": "Limits and speed",
+"section": "Compared with Panaroo refinding",
+"url": "how-it-works/limits.html#compared-with-panaroo-refinding",
+"text": "Timing during graph cleaning post-hoc on the final matrix Aligner BLAST-style refinding batch minimap2 Output binary found / not found binary matrix plus multistate audit Classification limited dropped, split, fragment, start variant, paralog, pseudogene Pseudogene handling not explicit stops, frameshifts, partial deletions Paralog control limited synteny and context rejection Cluster redundancy not addressed optional nucleotide similarity clustering Reference-specific audit no yes, via --reference-genome Corrected GFF export no optional The two are not alternatives. Panaroo refinding happens "
+},
+{
+"page": "Limits and speed",
+"section": "Benchmark results",
+"url": "how-it-works/limits.html#benchmark-results",
+"text": "Recore includes a synthetic benchmark that creates known false absences, true absences, and partial pseudogenes, then computes precision/recall/F1 from the actual corrected matrices. Current synthetic benchmark: strict 0 0 5 NA 0.000 0.000 balanced 5 0 0 1.000 1.000 1.000 sensitive 5 0 0 1.000 1.000 1.000 Interpretation: strict is conservative in annotation-free settings. balanced recovers full-length false absences without converting partial pseudogenes into binary presence. sensitive allows partial/disrupted evidence at the policy layer, but the adaptive quality guard still blocks weak pseud"
+},
+{
+"page": "Limits and speed",
+"section": "Run time and memory",
+"url": "how-it-works/limits.html#run-time-and-memory",
+"text": "Measured with benchmarks/search_scaling.py on an Apple M4 Pro (14 cores, 24 GB) with 8 threads and minimap2 2.31: synthetic MTBC-sized genomes (one 4.9 Mb contig and 4,000 genes each), 80 matrix gaps per genome of which 40 are false absences, GFFs and --synteny. The GFFs give each CDS an ID and a locus tag; the last two columns are the same runs with PGAP-style GFFs (--rich-gff), six names per CDS plus a product and a note. 100 8,000 2.4 s 1.3 s 0.24 GB 2.2 s 0.32 GB 200 16,000 4.7 s 2.8 s 0.30 GB 4.2 s 0.43 GB 400 32,000 10.7 s 6.3 s 0.38 GB 8.8 s 0.64 GB 800 64,000 21.4 s 13.5 s 0.56 GB 20.7"
+},
+{
+"page": "Limits and speed",
+"section": "Reproducible review",
+"url": "how-it-works/limits.html#reproducible-review",
+"text": "Three Python utilities ship in benchmarks/ for checking a run rather than trusting it: self_benchmark.py: Masks known-present cells, reruns Recore, and estimates dataset-specific masked-positive recall plus score calibration bins. This measures the tool on your data rather than on a synthetic set. compare_runs.py: Compares two output directories and labels every matrix call unchanged, new, lost or changed. Use it when changing a policy or a threshold. recore_report.py: Builds a compact Markdown or HTML report for an output directory that was generated without --write-html-report. The synthetic"
+},
+{
+"page": "Citation",
+"section": "",
+"url": "about/citation.html",
+"text": "If you use Recore, please cite: Ruiz-Rodriguez P, et al. (2026). Recore: post-hoc rescue and quality control of pangenome presence/absence matrices. In preparation. Please also cite the tools Recore depends on for the run you report: minimap2, for every sequence search: Li H. (2018). Minimap2: pairwise alignment for nucleotide sequences. Bioinformatics 34(18):3094-3100. MMseqs2, if you used --nsc-engine mmseqs2: Steinegger M, Soeding J. (2017). MMseqs2 enables sensitive protein sequence searching for the analysis of massive data sets. Nature Biotechnology 35:1026-1028. the pangenome tool that "
+},
+{
+"page": "Citation",
+"section": "Contact",
+"url": "about/citation.html#contact",
+"text": "Paula Ruiz-Rodriguez, paula.ruiz.rodriguez@csic.es PathoGenOmics Lab, github.com/PathoGenOmics-Lab"
+},
+{
+"page": "Citation",
+"section": "License",
+"url": "about/citation.html#license",
+"text": "GNU General Public License v3.0. See LICENSE."
+},
+{
+"page": "Contributing",
+"section": "",
+"url": "about/contributing.html",
+"text": ""
+},
+{
+"page": "Contributing",
+"section": "Development checks",
+"url": "about/contributing.html#development-checks",
+"text": "Exactly what CI runs, in the order it runs, is three commands: The end-to-end test needs minimap2 on PATH. It skips silently without one, which is not what you want in CI, so CI exports the guard that turns a skip into a failure, and you should too: The benchmarks are not part of CI. Run them by hand when a change could move the numbers:"
+},
+{
+"page": "Contributing",
+"section": "Working on the HTML report",
+"url": "about/contributing.html#working-on-the-html-report",
+"text": "The report's assets are inlined into the binary at compile time from src/pipeline/report_assets/: report.css the whole stylesheet explorer.js the phylogeny, matrix and synteny viewer report-ui.js theme, drawer, sortable tables, tooltips Two rules the report has to keep: No remote assets. It has to open from a results directory with no network. The integration test walks every link, script, img, iframe and use element and fails if any of them loads from a URL. Links to the project are fine; loading a resource is not. Nothing may assume the whole matrix fits. Every pane draws only its visible wi"
+},
+{
+"page": "Contributing",
+"section": "Documentation",
+"url": "about/contributing.html#documentation",
+"text": "The website is docs/index.html, docs/how-it-works.html and docs/reference.html, styled by docs/assets/site.css and driven by docs/assets/site.js: plain files, so opening docs/index.html in a browser shows a change. Keep the front page short. It is there to get someone running in two minutes; the details belong in the documentation. The drawings of How it works are docs/assets/how.js. They decide with a copy of evidence_state and matrix_policy_corrects from src/pipeline.rs, so a change to those rules is a change there too; tools/check_site.py fails if the policy table, a state, a rescue type, a"
+},
+{
+"page": "Contributing",
+"section": "Reporting a problem",
+"url": "about/contributing.html#reporting-a-problem",
+"text": "Open an issue at github.com/PathoGenOmics-Lab/recore/issues. For a wrong call, the most useful thing to include is the row from orthology_evidence.tsv: it carries the evidence state, the adaptive guard floors, the confidence tier and the risk flags, which together explain why Recore decided what it did."
+}
+];
diff --git a/docs/assets/site.css b/docs/assets/site.css
index 48e9230..db3f839 100644
--- a/docs/assets/site.css
+++ b/docs/assets/site.css
@@ -24,10 +24,15 @@
--accent-soft: #14243b;
--on-accent: #0a1220;
--code-bg: #090a0d;
+ --code-border: #23262e;
--code-ink: #e6e8ef;
--code-muted: #8b90a0;
--code-flag: #7cc4ff;
--code-value: #f4c27a;
+ --copy-border: rgba(255, 255, 255, 0.16);
+ --copy-bg: rgba(255, 255, 255, 0.06);
+ --copy-hover: rgba(255, 255, 255, 0.14);
+ --copy-done: #8ef0bf;
--present: #5d6676;
--rescue: #4b9bff;
--nsc: #ff9a2e;
@@ -59,7 +64,16 @@
--accent-hover: #0849a0;
--accent-soft: #e8f0fc;
--on-accent: #ffffff;
- --code-bg: #12141b;
+ --code-bg: #f4f5f7;
+ --code-border: #e1e3e8;
+ --code-ink: #1d2230;
+ --code-muted: #6b7182;
+ --code-flag: #0b5cc4;
+ --code-value: #9a4f00;
+ --copy-border: rgba(16, 18, 27, 0.14);
+ --copy-bg: rgba(255, 255, 255, 0.8);
+ --copy-hover: #ffffff;
+ --copy-done: #0f7a52;
--present: #b3bbc7;
--rescue: #0f6fd6;
--nsc: #e07b00;
@@ -694,7 +708,7 @@ p {
background: var(--code-bg);
color: var(--code-ink);
border-radius: var(--radius-sm);
- border: 1px solid color-mix(in srgb, var(--code-bg) 80%, var(--line-strong));
+ border: 1px solid var(--code-border);
}
.code pre {
@@ -708,7 +722,7 @@ p {
/* A one-line command that would hide under the copy button wraps instead;
what is copied is the text, not the wrapping. */
-.code.wrap pre {
+.code.one-line pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
@@ -734,8 +748,8 @@ p {
position: absolute;
top: 10px;
right: 10px;
- border: 1px solid rgba(255, 255, 255, 0.16);
- background: rgba(255, 255, 255, 0.06);
+ border: 1px solid var(--copy-border);
+ background: var(--copy-bg);
color: var(--code-ink);
font: 600 0.78rem var(--font);
padding: 6px 10px;
@@ -744,12 +758,37 @@ p {
}
.copy:hover {
- background: rgba(255, 255, 255, 0.14);
+ background: var(--copy-hover);
}
.copy.done {
- color: #8ef0bf;
- border-color: rgba(142, 240, 191, 0.4);
+ color: var(--copy-done);
+ border-color: currentColor;
+}
+
+.code-title {
+ font: 600 0.75rem var(--font);
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--code-muted);
+ padding: 10px 18px 0;
+}
+
+.code-title + pre {
+ padding-top: 8px;
+}
+
+kbd {
+ display: inline-block;
+ min-width: 1.6em;
+ padding: 0.05em 0.45em;
+ border: 1px solid var(--line-strong);
+ border-bottom-width: 2px;
+ border-radius: 6px;
+ background: var(--bg-subtle);
+ font: 600 0.8em var(--mono);
+ text-align: center;
+ color: var(--ink);
}
.out {
@@ -846,6 +885,12 @@ p {
color: var(--on-accent);
}
+.seg button[aria-pressed="true"] code {
+ background: color-mix(in srgb, var(--on-accent) 14%, transparent);
+ border-color: transparent;
+ color: inherit;
+}
+
.opt {
display: grid;
grid-template-columns: auto 1fr;
@@ -1613,7 +1658,9 @@ td.wrap-code code {
}
.deep a span {
+ display: block;
font-size: 0.86rem;
+ line-height: 1.45;
color: var(--muted);
}
@@ -1626,3 +1673,1076 @@ td.wrap-code code {
transition: none;
}
}
+
+/* ── Documentation pages ── */
+
+.docs-layout {
+ display: grid;
+ grid-template-columns: 230px minmax(0, 1fr) 210px;
+ gap: 44px;
+ padding-top: 32px;
+ padding-bottom: 72px;
+}
+
+.docs-home {
+ grid-template-columns: 230px minmax(0, 1fr);
+}
+
+@media (max-width: 1180px) {
+ .docs-layout {
+ grid-template-columns: 210px minmax(0, 1fr);
+ }
+
+ .docs-toc {
+ display: none;
+ }
+}
+
+@media (max-width: 860px) {
+ .docs-layout,
+ .docs-home {
+ grid-template-columns: minmax(0, 1fr);
+ gap: 18px;
+ padding-top: 18px;
+ }
+}
+
+.docs-nav,
+.docs-toc {
+ position: sticky;
+ top: 80px;
+ align-self: start;
+ font-size: 0.92rem;
+}
+
+/* A sticky box stacks on its own, so the sidebar sits above the text it
+ spreads its search results over. */
+.docs-nav {
+ z-index: 10;
+}
+
+/* The page list and the contents scroll on their own; the search box does
+ not, so its results can spread over the page instead of being cut off. */
+.docs-menu,
+.docs-toc {
+ max-height: calc(100vh - 150px);
+ overflow: auto;
+}
+
+.docs-toc {
+ max-height: calc(100vh - 96px);
+}
+
+@media (max-width: 860px) {
+ .docs-nav {
+ position: static;
+ }
+
+ .docs-menu {
+ max-height: none;
+ }
+}
+
+.docs-search {
+ position: relative;
+ margin-bottom: 16px;
+}
+
+.docs-search input {
+ width: 100%;
+ font: 0.92rem var(--font);
+ padding: 9px 12px;
+ border: 1px solid var(--line-strong);
+ border-radius: 9px;
+ background: var(--bg-elev);
+ color: var(--ink);
+}
+
+.docs-results {
+ position: absolute;
+ z-index: 20;
+ left: 0;
+ right: -160px;
+ top: calc(100% + 6px);
+ background: var(--bg-elev);
+ border: 1px solid var(--line-strong);
+ border-radius: 10px;
+ box-shadow: var(--shadow);
+ padding: 6px;
+ max-height: 60vh;
+ overflow: auto;
+}
+
+@media (max-width: 860px) {
+ .docs-results {
+ right: 0;
+ }
+}
+
+.docs-results a {
+ display: block;
+ padding: 8px 10px;
+ border-radius: 7px;
+ text-decoration: none;
+ color: var(--ink);
+}
+
+.docs-results a:hover,
+.docs-results a:focus,
+.docs-results a.active {
+ background: var(--bg-subtle);
+ outline: none;
+}
+
+.docs-results b {
+ display: block;
+ font-size: 0.9rem;
+}
+
+.docs-results small {
+ display: block;
+ color: var(--muted);
+ font-size: 0.8rem;
+ line-height: 1.35;
+ margin-top: 2px;
+}
+
+.docs-results mark {
+ background: color-mix(in srgb, var(--accent) 30%, transparent);
+ color: inherit;
+ border-radius: 3px;
+}
+
+.docs-results .none {
+ padding: 8px 10px;
+ color: var(--muted);
+ font-size: 0.88rem;
+}
+
+.docs-menu summary {
+ display: none;
+}
+
+@media (max-width: 860px) {
+ .docs-menu {
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ background: var(--bg-elev);
+ padding: 4px 14px 8px;
+ }
+
+ .docs-menu summary {
+ display: list-item;
+ cursor: pointer;
+ font-weight: 650;
+ padding: 8px 0;
+ }
+}
+
+.docs-section {
+ font-size: 0.75rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--muted);
+ margin: 16px 0 6px;
+}
+
+.docs-nav ul,
+.docs-toc ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.docs-nav li a {
+ display: block;
+ padding: 5px 10px;
+ border-radius: 7px;
+ color: var(--ink-soft);
+ text-decoration: none;
+}
+
+.docs-nav li a:hover {
+ background: var(--bg-subtle);
+ color: var(--ink);
+}
+
+.docs-nav li a[aria-current="page"] {
+ background: var(--accent-soft);
+ color: var(--accent);
+ font-weight: 650;
+}
+
+.docs-toc p {
+ font-size: 0.75rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--muted);
+ margin-bottom: 8px;
+}
+
+.docs-toc li a {
+ display: block;
+ padding: 3px 0 3px 10px;
+ border-left: 2px solid var(--line);
+ color: var(--muted);
+ text-decoration: none;
+ line-height: 1.35;
+}
+
+.docs-toc li.toc-3 a {
+ padding-left: 22px;
+ font-size: 0.86rem;
+}
+
+.docs-toc li a:hover {
+ color: var(--ink);
+}
+
+.docs-toc li a.active {
+ border-left-color: var(--accent);
+ color: var(--ink);
+}
+
+/* Long-form text. */
+
+.prose {
+ min-width: 0;
+ max-width: 780px;
+}
+
+.prose .crumb {
+ font-size: 0.85rem;
+ color: var(--muted);
+ margin-bottom: 8px;
+}
+
+.prose .crumb a {
+ color: var(--muted);
+}
+
+.prose h1 {
+ font-size: clamp(2rem, 4vw, 2.6rem);
+ font-weight: 800;
+ letter-spacing: -0.03em;
+ margin-bottom: 18px;
+}
+
+.prose h2 {
+ font-size: 1.5rem;
+ margin: 2.2em 0 0.7em;
+}
+
+.prose h3 {
+ font-size: 1.15rem;
+ margin: 1.8em 0 0.6em;
+}
+
+.prose h4,
+.prose h5,
+.prose h6 {
+ font-size: 1rem;
+ margin: 1.5em 0 0.5em;
+}
+
+.prose h1,
+.prose h2,
+.prose h3,
+.prose h4 {
+ scroll-margin-top: 80px;
+ /* Headings that are file names have no spaces to break at. */
+ overflow-wrap: anywhere;
+}
+
+.prose .anchor {
+ margin-left: 8px;
+ color: var(--line-strong);
+ text-decoration: none;
+ opacity: 0;
+ font-weight: 500;
+}
+
+.prose h2:hover .anchor,
+.prose h3:hover .anchor,
+.prose h4:hover .anchor,
+.prose .anchor:focus {
+ opacity: 1;
+}
+
+.prose p,
+.prose ul,
+.prose ol,
+.prose blockquote,
+.prose .code,
+.prose .table-wrap,
+.prose .flow,
+.prose .figure {
+ margin: 0 0 1.1em;
+}
+
+.prose ul,
+.prose ol {
+ padding-left: 1.4em;
+}
+
+.prose li {
+ margin: 0.3em 0;
+}
+
+.prose li > ul,
+.prose li > ol,
+.prose li > .code {
+ margin: 0.5em 0;
+}
+
+.prose hr {
+ border: 0;
+ border-top: 1px solid var(--line);
+ margin: 2em 0;
+}
+
+.prose blockquote {
+ border-left: 3px solid var(--accent);
+ background: var(--bg-subtle);
+ border-radius: 0 10px 10px 0;
+ padding: 12px 16px;
+ color: var(--ink-soft);
+}
+
+.prose blockquote p:last-child {
+ margin-bottom: 0;
+}
+
+.prose .table-wrap {
+ font-size: 0.9rem;
+}
+
+.prose td code {
+ white-space: normal;
+}
+
+.prose .figure img {
+ max-width: 100%;
+ height: auto;
+}
+
+.only-light {
+ display: none;
+}
+
+:root[data-theme="light"] .only-light {
+ display: inline;
+}
+
+:root[data-theme="light"] .only-dark {
+ display: none;
+}
+
+.prose .edit {
+ margin-top: 2.5em;
+ font-size: 0.9rem;
+}
+
+.prose .edit a {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.prose .edit svg {
+ width: 16px;
+ height: 16px;
+}
+
+.pager {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 14px;
+ margin-top: 18px;
+}
+
+.pager a {
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ background: var(--bg-elev);
+ padding: 12px 16px;
+ text-decoration: none;
+ color: var(--ink);
+ font-weight: 650;
+}
+
+.pager a:hover {
+ border-color: var(--accent);
+}
+
+.pager a span {
+ display: block;
+ font-size: 0.78rem;
+ font-weight: 600;
+ color: var(--muted);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.pager .next {
+ grid-column: 2;
+ text-align: right;
+}
+
+/* A mermaid flowchart, as its steps. */
+
+.flow {
+ list-style: none;
+ padding: 0 !important;
+ counter-reset: step;
+}
+
+.flow-step {
+ position: relative;
+ padding-left: 34px;
+ margin: 0 0 14px !important;
+}
+
+.flow-step::before {
+ counter-increment: step;
+ content: counter(step);
+ position: absolute;
+ left: 0;
+ top: 8px;
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ display: grid;
+ place-items: center;
+ font: 700 0.72rem var(--font);
+ color: #fff;
+ background: linear-gradient(135deg, #0274b8, var(--brand-c));
+}
+
+.flow-box {
+ display: inline-block;
+ border: 1px solid var(--line-strong);
+ background: var(--bg-elev);
+ border-radius: 10px;
+ padding: 8px 12px;
+}
+
+.flow-decision .flow-box {
+ border-style: dashed;
+ border-color: var(--accent);
+}
+
+.flow-box b {
+ display: block;
+}
+
+.flow-box span {
+ display: block;
+ font-size: 0.85rem;
+ color: var(--muted);
+}
+
+.flow-next {
+ list-style: none;
+ padding: 4px 0 0 12px !important;
+ margin: 0 !important;
+ font-size: 0.88rem;
+ color: var(--ink-soft);
+}
+
+.flow-next li {
+ margin: 2px 0;
+}
+
+.flow-next i {
+ color: var(--muted);
+}
+
+.docs-group {
+ font-size: 1.1rem !important;
+ margin: 1.6em 0 0.7em !important;
+}
+
+/* ── How it works: the illustrated walk through one cell ── */
+
+.nav-wide {
+ white-space: nowrap;
+}
+
+@media (max-width: 1060px) {
+ .nav .nav-wide {
+ display: none;
+ }
+}
+
+.how-hero h1 {
+ font-size: clamp(2.1rem, 5vw, 3.2rem);
+}
+
+.how {
+ padding: 8px 0 24px;
+}
+
+.how-pick {
+ margin: 18px 0 22px;
+}
+
+.how-layout {
+ display: grid;
+ grid-template-columns: 250px minmax(0, 1fr);
+ gap: 28px;
+ align-items: start;
+}
+
+@media (max-width: 900px) {
+ .how-layout {
+ grid-template-columns: minmax(0, 1fr);
+ }
+}
+
+.how-steps {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: grid;
+ gap: 6px;
+ position: sticky;
+ top: 84px;
+}
+
+.how-steps button {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ text-align: left;
+ font: 600 0.95rem var(--font);
+ color: var(--muted);
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 10px;
+ padding: 9px 10px;
+ cursor: pointer;
+}
+
+.how-steps button:hover {
+ color: var(--ink);
+ background: var(--bg-subtle);
+}
+
+.how-steps .n {
+ display: inline-grid;
+ place-items: center;
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ border: 2px solid var(--line-strong);
+ font-size: 0.8rem;
+ flex: none;
+}
+
+.how-steps button.done .n {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+
+.how-steps button[aria-current="step"] {
+ color: var(--ink);
+ background: var(--bg-elev);
+ border-color: var(--line);
+}
+
+.how-steps button[aria-current="step"] .n {
+ background: linear-gradient(135deg, #0274b8, var(--brand-c));
+ border-color: transparent;
+ color: #fff;
+}
+
+@media (max-width: 900px) {
+ .how-steps {
+ position: static;
+ grid-auto-flow: column;
+ grid-auto-columns: max-content;
+ overflow-x: auto;
+ padding-bottom: 4px;
+ }
+
+ .how-steps button span:not(.n) {
+ display: none;
+ }
+
+ .how-steps button {
+ padding: 4px;
+ }
+}
+
+.how-stage-card {
+ background: var(--bg-elev);
+ border: 1px solid var(--line);
+ border-radius: 16px;
+ overflow: hidden;
+ box-shadow: var(--shadow);
+}
+
+.how-progress {
+ height: 4px;
+ background: var(--bg-subtle);
+}
+
+.how-progress i {
+ display: block;
+ height: 100%;
+ width: 0;
+ background: linear-gradient(90deg, var(--grad-a), var(--grad-c));
+ transition: width 0.4s ease;
+}
+
+.stage {
+ display: block;
+ width: 100%;
+ height: auto;
+ background:
+ radial-gradient(70% 90% at 50% 30%, color-mix(in srgb, var(--brand-b) 10%, transparent), transparent 75%),
+ var(--bg-elev);
+ border-bottom: 1px solid var(--line);
+ font-family: var(--font);
+}
+
+.how-text {
+ padding: 20px 24px 6px;
+}
+
+.how-kicker {
+ font-size: 0.8rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--muted);
+}
+
+.how-text h2 {
+ font-size: 1.5rem;
+ margin: 6px 0 8px;
+}
+
+.how-text p {
+ color: var(--ink-soft);
+ max-width: 62ch;
+}
+
+.how-control:not(:empty) {
+ margin-top: 14px;
+}
+
+.how-nav {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 16px 24px 20px;
+ flex-wrap: wrap;
+}
+
+.how-nav [data-next] {
+ margin-left: auto;
+}
+
+.how-nav button:disabled {
+ opacity: 0.45;
+ cursor: default;
+}
+
+.how-note {
+ margin-top: 16px;
+ max-width: 80ch;
+}
+
+/* Things in the drawings. */
+
+.stage text {
+ fill: var(--ink);
+}
+
+.stage .axis {
+ fill: var(--muted);
+ font-size: 15px;
+ font-weight: 600;
+}
+
+.stage .small-label {
+ fill: var(--muted);
+ font-size: 15px;
+}
+
+.stage .col-label,
+.stage .row-label {
+ fill: var(--muted);
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.stage .is-x {
+ fill: var(--rescue);
+}
+
+.stage .big-num {
+ font-size: 26px;
+ font-weight: 750;
+}
+
+.stage .gene path {
+ stroke: color-mix(in srgb, var(--ink) 25%, transparent);
+ stroke-width: 1;
+}
+
+.stage .gene-label {
+ font-size: 14px;
+ font-weight: 650;
+ fill: #0b0d12;
+}
+
+.stage .gx path { fill: var(--rescue); }
+.stage .gx .gene-label { fill: #fff; }
+.stage .ga path { fill: #3fb6a8; }
+.stage .gb path { fill: #e2a03f; }
+.stage .gc path { fill: #9a8cf8; }
+.stage .gd path { fill: #ec7fae; }
+.stage .ge path { fill: #8fa88a; }
+.stage .gf path { fill: #b49b7a; }
+.stage .gg path { fill: #7d93b3; }
+.stage .gh path { fill: #a98bb0; }
+.stage .go path { fill: var(--nsc); }
+
+.stage .gp path {
+ fill: var(--bg-subtle);
+ stroke: var(--line-strong);
+ stroke-width: 2;
+}
+
+.stage .gp .gene-label {
+ fill: var(--ink-soft);
+}
+
+.stage .gx-ghost path {
+ fill: transparent;
+ stroke: var(--rescue);
+ stroke-dasharray: 5 4;
+ stroke-width: 2;
+}
+
+.stage .gx-ghost .gene-label {
+ fill: var(--rescue);
+}
+
+.stage .contig {
+ stroke: var(--line-strong);
+ stroke-width: 4;
+ stroke-linecap: round;
+}
+
+.stage .m-present { fill: var(--present); }
+.stage .m-empty { fill: var(--absent); stroke: var(--absent-stroke); }
+.stage .m-put { fill: var(--rescue); }
+.stage .m-state { fill: var(--accent-soft); stroke: var(--accent); stroke-width: 2; }
+
+.stage .m-target {
+ fill: var(--absent);
+ stroke: var(--accent);
+ stroke-width: 3;
+ stroke-dasharray: 6 4;
+ animation: target-pulse 1.6s ease-in-out infinite;
+}
+
+.stage .m-question {
+ fill: var(--accent);
+ font-size: 22px;
+ font-weight: 800;
+}
+
+.stage .callout path {
+ fill: none;
+ stroke: var(--muted);
+ stroke-width: 2;
+ stroke-dasharray: 4 5;
+}
+
+.stage .callout-text {
+ fill: var(--ink);
+ font-size: 18px;
+ font-weight: 700;
+}
+
+.stage .file path {
+ fill: var(--bg-subtle);
+ stroke: var(--line-strong);
+ stroke-width: 2;
+}
+
+.stage .file .file-fold {
+ fill: none;
+}
+
+.stage .file-line {
+ fill: var(--line-strong);
+}
+
+.stage .base-a { fill: #3fb6a8; }
+.stage .base-c { fill: #5aa8ff; }
+.stage .base-g { fill: #e2a03f; }
+.stage .base-t { fill: #ec7fae; }
+
+.stage .flow-arrow,
+.stage .guide {
+ fill: none;
+ stroke: var(--accent);
+ stroke-width: 2;
+ stroke-dasharray: 1;
+ stroke-dashoffset: 0;
+}
+
+.stage .guide {
+ stroke-dasharray: 1;
+ opacity: 0.7;
+}
+
+.stage .bact {
+ fill: color-mix(in srgb, var(--grad-a) 10%, var(--bg-elev));
+ stroke: color-mix(in srgb, var(--grad-a) 55%, transparent);
+ stroke-width: 3;
+}
+
+.stage .chromosome {
+ fill: none;
+ stroke: var(--line-strong);
+ stroke-width: 3;
+ stroke-dasharray: 2 6;
+}
+
+.stage .arc {
+ fill: none;
+ stroke-width: 12;
+ stroke-linecap: round;
+}
+
+.stage .arc.ga { stroke: #3fb6a8; }
+.stage .arc.gb { stroke: #e2a03f; }
+.stage .arc.gc { stroke: #9a8cf8; }
+.stage .arc.gd { stroke: #ec7fae; }
+.stage .arc.gp { stroke: var(--line-strong); }
+
+.stage .lens circle {
+ fill: color-mix(in srgb, var(--bg-elev) 55%, transparent);
+ stroke: var(--ink);
+ stroke-width: 4;
+}
+
+.stage .lens line {
+ stroke: var(--ink);
+ stroke-width: 7;
+ stroke-linecap: round;
+}
+
+.stage .glow {
+ fill: none;
+ stroke: var(--rescue);
+ stroke-width: 4;
+}
+
+.stage .panel rect {
+ fill: var(--bg-subtle);
+ stroke: var(--line);
+}
+
+.stage .badge-svg rect {
+ fill: var(--accent-soft);
+ stroke: var(--accent);
+ stroke-width: 1.5;
+}
+
+.stage .badge-svg text {
+ fill: var(--accent);
+ font-size: 16px;
+ font-weight: 700;
+}
+
+.stage .badge-svg.is-good rect { fill: color-mix(in srgb, var(--ok) 16%, var(--bg-elev)); stroke: var(--ok); }
+.stage .badge-svg.is-good text { fill: var(--ok); }
+.stage .badge-svg.is-bad rect { fill: color-mix(in srgb, var(--kept) 14%, var(--bg-elev)); stroke: var(--kept); }
+.stage .badge-svg.is-bad text { fill: var(--kept); }
+.stage .badge-svg.is-muted rect { fill: var(--bg-subtle); stroke: var(--line-strong); }
+.stage .badge-svg.is-muted text { fill: var(--muted); }
+
+.stage .bracket {
+ fill: none;
+ stroke: var(--muted);
+ stroke-width: 2;
+ stroke-dasharray: 5 4;
+}
+
+.stage .is-el rect { fill: #8b5cf6; }
+.stage .is-el text { fill: #fff; font-size: 13px; font-weight: 800; }
+
+.stage .cross path {
+ stroke: var(--kept);
+ stroke-width: 5;
+ stroke-linecap: round;
+}
+
+.stage .stop-sign polygon { fill: var(--kept); }
+.stage .stop-sign text { fill: #fff; font-size: 8px; font-weight: 800; }
+.stage .stop-tick { fill: var(--kept); }
+
+.stage .track .faint {
+ opacity: 0.4;
+}
+
+.stage .link {
+ fill: none;
+ stroke-width: 3;
+ stroke-dasharray: 1;
+ stroke-dashoffset: 0;
+}
+
+.stage .link-good { stroke: var(--ok); }
+.stage .link-bad { stroke: var(--kept); stroke-dasharray: 0.04 0.03; }
+
+.stage .chip-svg rect {
+ fill: var(--accent-soft);
+ stroke: var(--accent);
+ stroke-width: 2;
+}
+
+.stage .chip-svg text {
+ fill: var(--accent);
+ font: 700 14px var(--mono);
+}
+
+.stage .chip-svg text.long,
+.stage .state-label.long {
+ font-size: 12px;
+}
+
+.stage .gate text {
+ fill: var(--muted);
+ font: 700 15px var(--mono);
+}
+
+.stage .gate .frame {
+ fill: none;
+ stroke: var(--line-strong);
+ stroke-width: 3;
+}
+
+.stage .gate .door {
+ fill: var(--bg-subtle);
+ stroke: var(--line);
+}
+
+.stage .gate.allows .door {
+ fill: color-mix(in srgb, var(--rescue) 16%, var(--bg-subtle));
+}
+
+.stage .gate.is-on text { fill: var(--accent); }
+.stage .gate.is-on .frame { stroke: var(--accent); }
+.stage .gate:not(.is-on) > * { opacity: 0.45; }
+
+.stage .r-put { fill: var(--rescue); }
+
+.stage .result-head {
+ fill: var(--ink);
+ font-weight: 700;
+}
+
+.stage .r-kept {
+ fill: var(--absent);
+ stroke: var(--kept);
+ stroke-width: 3;
+}
+
+.stage .doc .doc-bg {
+ fill: var(--bg-subtle);
+ stroke: var(--line-strong);
+}
+
+.stage .token {
+ fill: var(--accent);
+}
+
+.stage .file-line.is-x {
+ fill: var(--rescue);
+}
+
+.stage .doc .doc-title {
+ font: 700 14px var(--mono);
+ fill: var(--ink-soft);
+}
+
+.stage .state-label {
+ font-family: var(--mono);
+ fill: var(--accent);
+ font-weight: 700;
+}
+
+.stage .tsv-key {
+ font: 600 11px var(--mono);
+ fill: var(--muted);
+}
+
+.stage .tsv-val {
+ font: 700 13px var(--mono);
+ fill: var(--ink);
+}
+
+.stage .hl {
+ fill: none;
+ stroke: var(--accent);
+ stroke-width: 3;
+}
+
+/* On a phone the drawing is about half its drawn size: give it the whole
+ width and larger labels. */
+@media (max-width: 600px) {
+ .how-stage-card {
+ margin: 0 -24px;
+ border-radius: 0;
+ border-left: 0;
+ border-right: 0;
+ }
+
+ .stage .small-label,
+ .stage .axis,
+ .stage .gate text {
+ font-size: 18px;
+ }
+
+ .stage .gene-label { font-size: 16px; }
+ .stage .badge-svg text { font-size: 19px; }
+ .stage .chip-svg text { font-size: 16px; }
+ .stage .col-label,
+ .stage .row-label { font-size: 19px; }
+ .stage .callout-text { font-size: 21px; }
+}
+
+/* How things arrive. */
+
+.stage .in-fade { animation: in-fade 0.6s ease both; }
+.stage .in-pop { animation: in-pop 0.45s cubic-bezier(.3,1.4,.6,1) both; transform-box: fill-box; transform-origin: center; }
+.stage .in-rise { animation: in-rise 0.6s ease both; }
+.stage .in-draw { animation: in-draw 0.9s ease both; }
+.stage .in-pulse { animation: in-pulse 1.4s ease-in-out 3 both; transform-box: fill-box; transform-origin: center; }
+
+@keyframes in-fade { from { opacity: 0; } to { opacity: 1; } }
+@keyframes in-pop { from { opacity: 0; transform: scale(0.6); } to { opacity: 1; transform: scale(1); } }
+@keyframes in-rise { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: translateY(0); } }
+@keyframes in-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
+@keyframes in-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.45; transform: scale(1.25); } }
+@keyframes target-pulse { 0%, 100% { stroke-opacity: 1; } 50% { stroke-opacity: 0.35; } }
+
+@media (prefers-reduced-motion: reduce) {
+ .stage *,
+ .stage *::before {
+ animation: none !important;
+ transition: none !important;
+ }
+}
diff --git a/docs/assets/site.js b/docs/assets/site.js
index 148c661..092374d 100644
--- a/docs/assets/site.js
+++ b/docs/assets/site.js
@@ -142,10 +142,12 @@
if (source) addCopy(button, source);
});
- // Every other code block gets a button too.
+ // Every other code block gets a button too. A one-line block wraps rather
+ // than run under the button; a block of lines keeps its layout.
document.querySelectorAll(".code").forEach(function (block) {
- if (block.querySelector(".copy")) return;
var pre = block.querySelector("pre");
+ if (pre && pre.textContent.trim().indexOf("\n") === -1) block.classList.add("one-line");
+ if (block.querySelector(".copy")) return;
if (!pre) return;
var button = document.createElement("button");
button.type = "button";
@@ -586,6 +588,171 @@
});
});
+ /* ── Documentation: search, page contents, menu ── */
+
+ var searchBox = document.querySelector("[data-docs-search]");
+ var resultsBox = document.querySelector("[data-docs-results]");
+ if (searchBox && resultsBox && window.RECORE_DOCS_INDEX) {
+ // The index lists URLs from the docs root; this page may sit a folder down.
+ var indexScript = document.querySelector('script[src$="assets/search-index.js"]');
+ var root = indexScript ? indexScript.getAttribute("src").replace("assets/search-index.js", "") : "";
+ var active = -1;
+
+ function escapeHtml(text) {
+ return text.replace(/&/g, "&").replace(//g, ">");
+ }
+
+ // Words match where a word starts, so "min" finds --min-prevalence and
+ // not the middle of "becoming".
+ function startsWord(w, flags) {
+ return new RegExp("(^|[^a-z0-9])(" + w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", flags);
+ }
+
+ function highlight(text, words) {
+ var safe = escapeHtml(text);
+ words.forEach(function (w) {
+ safe = safe.replace(startsWord(w, "ig"), "$1$2");
+ });
+ return safe;
+ }
+
+ function snippet(text, word) {
+ var at = text.toLowerCase().indexOf(word);
+ if (at < 0) return text.slice(0, 140);
+ var start = Math.max(0, at - 60);
+ return (start > 0 ? "\u2026" : "") + text.slice(start, start + 150) + (start + 150 < text.length ? "\u2026" : "");
+ }
+
+ function search(query) {
+ var words = query.toLowerCase().split(/\s+/).filter(function (w) {
+ return w.length > 1;
+ });
+ if (!words.length) return [words, []];
+ var hits = [];
+ window.RECORE_DOCS_INDEX.forEach(function (entry) {
+ var page = entry.page.toLowerCase();
+ var section = entry.section.toLowerCase();
+ var text = entry.text.toLowerCase();
+ var score = 0;
+ for (var k = 0; k < words.length; k++) {
+ var pattern = startsWord(words[k], "i");
+ var inSection = pattern.test(section);
+ var inPage = pattern.test(page);
+ var inText = pattern.test(text);
+ if (!inSection && !inPage && !inText) return;
+ score += (inSection ? 5 : 0) + (inPage ? 3 : 0) + (inText ? 1 : 0);
+ }
+ hits.push({ entry: entry, score: score });
+ });
+ hits.sort(function (a, b) {
+ return b.score - a.score;
+ });
+ return [words, hits.slice(0, 8)];
+ }
+
+ function render() {
+ var found = search(searchBox.value.trim());
+ var words = found[0];
+ var hits = found[1];
+ active = -1;
+ if (!words.length) {
+ resultsBox.hidden = true;
+ return;
+ }
+ resultsBox.hidden = false;
+ if (!hits.length) {
+ resultsBox.innerHTML = '
Nothing matches. Try another word.
';
+ return;
+ }
+ resultsBox.innerHTML = hits
+ .map(function (hit) {
+ var e = hit.entry;
+ var title = e.section ? e.page + " \u203a " + e.section : e.page;
+ return (
+ '' + highlight(title, words) + "" +
+ highlight(snippet(e.text, words[0]), words) + ""
+ );
+ })
+ .join("");
+ }
+
+ function move(step) {
+ var links = resultsBox.querySelectorAll("a");
+ if (!links.length) return;
+ active = (active + step + links.length) % links.length;
+ links.forEach(function (link, k) {
+ link.classList.toggle("active", k === active);
+ });
+ links[active].scrollIntoView({ block: "nearest" });
+ }
+
+ searchBox.addEventListener("input", render);
+ searchBox.addEventListener("keydown", function (event) {
+ if (event.key === "ArrowDown") {
+ event.preventDefault();
+ move(1);
+ } else if (event.key === "ArrowUp") {
+ event.preventDefault();
+ move(-1);
+ } else if (event.key === "Enter") {
+ var links = resultsBox.querySelectorAll("a");
+ var pick = links[active >= 0 ? active : 0];
+ if (pick) window.location.href = pick.getAttribute("href");
+ } else if (event.key === "Escape") {
+ searchBox.value = "";
+ resultsBox.hidden = true;
+ }
+ });
+ document.addEventListener("click", function (event) {
+ if (!event.target.closest(".docs-search")) resultsBox.hidden = true;
+ });
+ // "/" jumps to the search box, as on most documentation sites.
+ document.addEventListener("keydown", function (event) {
+ if (event.key === "/" && !/input|textarea|select/i.test(document.activeElement.tagName)) {
+ event.preventDefault();
+ searchBox.focus();
+ }
+ });
+ }
+
+ // The page contents follow the reader down the page.
+ var tocLinks = Array.prototype.slice.call(document.querySelectorAll(".docs-toc a"));
+ if (tocLinks.length) {
+ var targets = tocLinks
+ .map(function (link) {
+ return document.getElementById(link.getAttribute("href").slice(1));
+ })
+ .filter(Boolean);
+ var ticking = false;
+ function markCurrent() {
+ ticking = false;
+ var current = 0;
+ targets.forEach(function (heading, k) {
+ if (heading.getBoundingClientRect().top < 120) current = k;
+ });
+ tocLinks.forEach(function (link, k) {
+ link.classList.toggle("active", k === current);
+ });
+ }
+ window.addEventListener(
+ "scroll",
+ function () {
+ if (!ticking) {
+ ticking = true;
+ requestAnimationFrame(markCurrent);
+ }
+ },
+ { passive: true }
+ );
+ markCurrent();
+ }
+
+ // On a phone the page list starts folded, so the page itself comes first.
+ var docsMenu = document.querySelector(".docs-menu");
+ if (docsMenu && window.matchMedia && window.matchMedia("(max-width: 860px)").matches) {
+ docsMenu.open = false;
+ }
+
/* ── Reference filter ── */
document.querySelectorAll("[data-filter]").forEach(function (input) {
diff --git a/docs/documentation.html b/docs/documentation.html
new file mode 100644
index 0000000..6e75bbf
--- /dev/null
+++ b/docs/documentation.html
@@ -0,0 +1,131 @@
+
+
+
+
+
+Documentation | Recore
+
+
+
+
+
+
+Skip to the content
+
+
+
Panaroo-style matrix: metadata columns followed by one column per genome. Roary, PPanGGOLiN and PanTA write this layout, and PIRATE's own table is read too (see below)
pan_genome_reference.fa
yes
Cluster representative sequences; FASTA IDs must match matrix cluster names
genome FASTAs
yes
One assembly per matrix genome column
GFF annotations
optional, recommended
Enables annotation-aware SPLIT, START_VARIANT, PARALOG, pseudogene and synteny classification
reference genome name
optional
Used by --reference-genome to audit whether clusters are absent from a chosen reference
phylogenetic tree
optional
Newick, for --tree: parsimony on the corrected state, and the phylogeny pane of the HTML report
mobile-element FASTA
optional
For --mobile-element-reference: sequence-based IS support instead of product names
Pass -p the matrix and -r the representative sequences, and -a the same GFF files the tool was run on, so the gene IDs in the matrix cells are the ones in the annotations. Each pair was checked on real output of the version shown.
+
Tool
-p
-r
Panaroo
gene_presence_absence.csv
pan_genome_reference.fa
Roary
gene_presence_absence.csv
pan_genome_reference.fa
PPanGGOLiN (2.3.2)
matrix.csv, from ppanggolin all or ppanggolin write_pangenome --csv
all_nucleotide_families.fasta, from ppanggolin fasta --gene_families all
PanTA (1.0.1)
gene_presence_absence.csv
representative_clusters_nucl.fasta
PIRATE (1.0.5)
PIRATE.gene_families.tsv
representative_sequences.ffn
+
For PIRATE, give recore its own table, not the output of PIRATE_to_roary.pl: that script names every row allele__family, which matches no sequence in PIRATE's representative FASTA. The table is read into Panaroo's layout, so the corrected files are the same as for a Panaroo run, and a fission written (locus1:locus2) counts its pieces as one gene. PanTA writes its gene IDs as sample-contig-gene; recore matches them to the GFF by the gene part.
Matrix genome columns should match FASTA and GFF file stems, or contain the same embedded ASxxxxx identifier.
Pan-reference FASTA cluster names must match the Gene column of the matrix.
GFF3 attributes are percent-decoded before matching, so ID, Name, gene, locus_tag, aliases, products and notes all reach the matcher with their real values. An encoded locus tag no longer silently misses its matrix entry.
Tree tips are matched to matrix genomes by name. Genomes with no matching tip are ignored, and Recore reports that explicitly rather than dropping them quietly.
Use GFFs whenever you have them. Without annotations Recore can still detect full-length sequence evidence and some partial or deletion evidence, but it cannot reliably distinguish SPLIT, START_VARIANT and PARALOG by annotation overlap, and synteny validation is not available at all.
+
--synteny: Use it with GFFs whenever paralogy or multi-locus clusters are plausible. It is the check that keeps a paralogous hit from becoming a presence call.
--multi-allele: Use it when clusters have divergent alleles, so a diverged true allele is not mistaken for a different gene.
--min-prevalence: 0.0 for exhaustive audits where every cluster matters. A higher value such as 0.05 cuts rare-cluster noise in large publication runs.
--preset: asm5 for assemblies within about 5% divergence, which is faster. asm20 for up to 20%, which is safer across a broader species range.
Twelve MTBC-style genomes across six lineages, with an L8 outgroup, and twenty-four gene clusters:
+
tutorial_data/
+├── gene_presence_absence.csv # what the pangenome pipeline thought it saw
+├── pan_genome_reference.fa # one representative sequence per cluster
+├── genomes/ # one assembly per genome
+├── annotations/ # one GFF3 per genome
+└── species.tree # Newick, matching the lineage structure
+
Eleven cells are marked absent in the matrix that should not be. They are there on purpose, in four flavours:
+
Cluster
Genomes
What was planted
Rv0003
3
the full-length gene is in the assembly, the matrix says absent
Rv0012
3
the same, in a different cluster
Rv0006
2
the gene is in the assembly, but the GFF has no CDS there
Rv0009
3 (all of L2)
the gene is there with an internal stop codon
+
Plus three cases that must not be corrected: Rv3810 is genuinely absent everywhere but L8, Rv0018c has only a diverged copy in L4_AS00404, and Rv1180/Rv1181 are carried by L2 alone.
50 gaps is every empty cell in the matrix, not just the planted ones.
11 skipped are the Rv3810 cells. It sits in one genome out of twelve, which is under the 10% --min-prevalence floor, so it is never searched.
11 rescues is exactly the 11 planted cells, split into DROPPED 6 (Rv0003 and Rv0012), MISSING_CDS 2 (Rv0006) and PSEUDOGENE 3 (Rv0009).
PARALOG: 0: the diverged copy in L4_AS00404 is 85% identical, under the 90% --min-identity bar, so it produced nothing at all. It must not, and it did not.
Eleven cells had evidence; eight reached the binary matrix. The gap is the whole point of the balanced policy: a gene with an internal stop codon is evidence of a disrupted homologue, not a clean presence call, so Rv0009 stays out of the binary matrix and stays visible everywhere else.
STRICT_PRESENT on Rv0006 is worth a second look. The gene was in the assembly with no CDS annotated over it, which on its own is only MISSING_CDS_UNVALIDATED. --synteny checked the flanking genes, found the locus where it should be, and that promoted the call.
Run the same data six ways and the two levers separate cleanly:
+
for policy in strict balanced sensitive; do
+ for flags in "--synteny" ""; do
+ recore -p tutorial_data/gene_presence_absence.csv \
+ -r tutorial_data/pan_genome_reference.fa \
+ -g tutorial_data/genomes -a tutorial_data/annotations \
+ $flags --multi-allele --matrix-policy "$policy" \
+ -t 4 -o "out_${policy}" >/dev/null 2>&1
+ printf "%-10s %-8s " "$policy" "${flags:---none}"
+ grep "^Matrix-corrected:" "out_${policy}/summary.txt"
+ done
+done
+
Policy
With --synteny
Without
strict
8
0
balanced
8
6
sensitive
11
9
+
Three things fall out of that table:
+
strict without annotation context corrects nothing. It only accepts same-locus evidence, and without synteny none of the eight correctable cells has any: six come through as SEQUENCE_ONLY and the two Rv0006 cells as MISSING_CDS_UNVALIDATED, and strict takes neither. (The other three are PSEUDOGENE either way.) Strict is not "balanced but fussier", it is a different question.
balanced takes full-length sequence, but not an unconfirmed locus. The six Rv0003 and Rv0012 cells sit on annotated CDSs, so they are corrected either way; what changes is the provenance, STRICT_PRESENT with synteny and SEQUENCE_ONLY without. The two Rv0006 cells are a gene with no CDS annotated over it. Only synteny can say the locus is where the gene belongs, so without it balanced holds them back as audit-only.
sensitive with synteny reaches 11. The extra three are the Rv0009 pseudogenes. That is the policy saying "a disrupted homologue counts as presence", which is a claim you have to be willing to make in writing. Without synteny it still takes them, and still holds back Rv0006.
Those three columns are L1_AS00101, L1_AS00102 and L2_AS00201. Rv0003 shows STRICT_PRESENT where its false absences were planted; Rv0009 is untouched in the two L1 genomes and PSEUDOGENE in the first L2 one. Rv3810 was never searched, being under the prevalence floor, and says so instead of reading as an absence Recore had checked. Nothing was thrown away and nothing was quietly upgraded. The thirteen states are listed in Evidence states.
Open tutorial_out/recore_report.html in a browser. It is one self-contained file, so double-clicking it works (checked in Chrome and in Safari's engine), and so does copying it off a cluster.
+
Things to try, in order:
+
Press Cmd+K and type Rv0009. The matrix jumps to that column. The three L2 genomes are the only ones not marked present, and the inspector shows the mechanism.
Set tip colour to lineage from the sliders icon in the phylogeny header. The L2 clade lights up as one block, which is what a clade-restricted signal looks like.
Find Rv1180. Only the three L2 tips carry it. This one is a genuine clade acquisition rather than an artefact, and the inspector says so.
Press S to open the synteny band. The flanking genes are stable across genomes, which is why the Rv0006 locus could be validated.
Three clusters that were missing from at least one genome are now found in all twelve. They were always there; the matrix was wrong about them. That is the entire job.
Recore always records every piece of detected evidence in the audit files. The matrix policy controls one thing only: which accepted evidence is written into the binary corrected matrix.
+
Nothing is ever thrown away by a policy. A cell that strict declines to correct is still in gene_presence_absence_recore_states.csv with its state, and still in orthology_evidence.tsv with the reason.
full sequence-only hits without annotation or synteny support
Exploratory rescue
sensitive
accepted strict, alias, sequence-only, partial and pseudogene evidence that passes the quality guard
rejected contexts, weak accepted evidence, and homologous or paralogous traps
+
balanced is the recommendation for anything that will be published. It rescues the false absences that motivated the tool without letting a disrupted homologue become a clean presence call.
+
Reach for strict when the downstream analysis is sensitive to false positives in a way that outweighs the false absences, for example a core-genome definition that feeds a phylogeny.
+
Reach for sensitive when you are exploring, and read the multistate matrix alongside it rather than the binary one.
Policy is not the only gate. Every accepted rescue is also scored by an adaptive guard that combines:
+
sequence identity and query coverage;
locus and synteny evidence;
ORF integrity;
the evidence state;
cluster prevalence;
representative sequence length.
+
Rare clusters and very short clusters have to clear a higher sequence bar, because they are the easiest to over-call from weak homology. Same-locus evidence such as a passing synteny context can override that stricter sequence floor for STRICT_PRESENT and ALIAS_CLUSTER calls, but weak sequence-only evidence is held back as audit_only_quality.
+
The guard shows its work in orthology_evidence.tsv:
+
Column
Meaning
adaptive_identity_floor
the identity this cluster had to clear
adaptive_coverage_floor
the coverage this cluster had to clear
adaptive_sequence_pass
whether the sequence evidence cleared both
orthology_confidence_score
combined score
orthology_confidence_tier
the score bucketed for reading
risk_flags
what made this call risky
+
So a cell that was not corrected under balanced can be traced to either the policy or the guard, and the two are distinguishable.
The wording that matches what balanced actually does:
+
We used Recore with the balanced matrix policy, which rescues full-length false absences while retaining partial or disrupted homologues as multistate audit-only evidence.
+
If a specific cluster needs its own sentence, orthology_evidence.tsv carries per-cluster wording guidance, and the run report shows it in the inspector under recommended wording.
All output files are written to the directory specified by -o (default: recore_output/).
+
Two guarantees hold for every table here. TSV outputs are written as single-line records: tabs, line breaks and null characters inside annotation-derived text are normalised to spaces, so a GFF product, note or alias cannot corrupt a downstream table. And when an output directory is reused, Recore clears the known optional reports before writing the new run, so a stale reference_*, rejected-evidence, NSC, synteny or corrected-GFF file from a previous run cannot be mistaken for a current result.
Best upstream/downstream cluster matches observed in the selected assessment
orf_integrity_signal
string
in_frame_span, out_of_frame_span_review, fragmented_orf, disrupted_orf, or unknown
annotation_match
string
GFF gene ID that overlapped the rescue region, if any
fragment_n
int
Number of fragments in the selected multi-hit chain, if applicable
fragment_query_gap_bp
int
Total query-space gap across the selected fragment chain
fragment_target_gap_bp
int
Total target-space gap across the selected fragment chain
fragment_colinear
bool
Whether fragments are co-linear on the target
fragment_same_cds
bool
Whether the chain is contained in one compatible CDS
fragment_note
string
Fragment interpretation, such as alignment_split, biological_gap, frameshift_gap, or same_cds_alignment_split
+
Fields that do not apply to a row (the fragment columns of a single-hit rescue, an annotation match where none overlapped) are written empty, and booleans are TRUE or FALSE.
Written only when --synteny is enabled and one or more candidate rescues have explicitly incompatible flanking context. The schema is identical to rescue_report.tsv, but these rows are audit records: they are not added to gene_presence_absence_recore.csv and are not written to corrected GFF output.
+
This file is useful for reviewing high-identity paralog/cross-family hits that look like sequence rescues but fail the local genomic-context check.
Written only when one or more PARALOG candidates lack explicit synteny support. The schema is identical to rescue_report.tsv, but these rows are audit records: they are not added to gene_presence_absence_recore.csv and are not written to corrected GFF output.
+
This is the conservative guardrail for multi-copy families: sequence similarity alone is not allowed to rescue a missing gene when the annotation says the hit belongs to a different cluster.
correct_presence for accepted rescues, audit_only_quality for accepted evidence that fails the adaptive guard, audit_only_policy for accepted evidence excluded by policy, or audit_only for rejected candidates
publication_status
How the row can be used in a paper; it follows matrix_action, see the values below
evidence_strength
high, moderate, review, or low
recommended_wording
Short text for how to describe the event in downstream tables/manuscripts
Per-cluster sequence guard used before a rescue can enter the binary matrix; rare or short clusters require stronger evidence unless same-locus context supports the rescue
Quantitative adaptive synteny evidence, same meaning as rescue_report.tsv
orf_integrity_signal
ORF-level signal, same meaning as rescue_report.tsv
annotation_cluster
Cluster assigned to the overlapping annotation, when known
rejection_reason
Reason rejected, e.g. synteny_fail or paralog_without_synteny
fragment_note
Fragment interpretation, if applicable
+
publication_status takes one of these values:
+
Value
Meaning
publication_ready_correction
Corrected; strict same-locus or cluster-alias evidence over a whole number of codons
corrected_review
Corrected; strict evidence whose span is not a whole number of codons, or another state that needs a look
corrected_but_review_for_publication
Corrected; sequence-only, partial or pseudogene evidence that a sensitive policy let into the matrix
corrected_paralog_review
Corrected; the hit may be a paralog
held_out_policy
Accepted evidence the matrix policy kept out of the corrected matrix
held_out_quality
Accepted evidence an adaptive quality floor kept out of the corrected matrix
audit_only_not_corrected
Rejected by the synteny or paralog guard
+
Only the first four describe a corrected cell, so a value starting with corrected or publication always means the cell is present in gene_presence_absence_corrected.csv.
+
Use this table as the primary audit trail for why a candidate entered, or did not enter, the corrected matrix.
Evidence fields when reference-like sequence evidence exists
+
This file is meant for reference-absence claims: keep new_to_reference for strictly absent clusters, remove reference_like_or_present, and manually review reference_like_ambiguous. The publication_confidence field uses clean, complex_paralog, event_derived, fragmented, pseudogene, unresolved, or not_reference_absent. complex_paralog is used when the exact cluster remains absent from the reference but discarded/subthreshold sequence evidence points to a partial or paralogous reference-like copy. event_derived is used when the exact cluster remains separate in the matrix but its representative sequence is best explained as a reference-gene allele or fragment. Accepted sequence-only evidence that fails the adaptive orthology-quality guard is kept as reference_like_ambiguous with publication_confidence=unresolved.
Written only when --reference-genome is provided and Recore finds reference-absent clusters with strong sequence evidence to a reference-present cluster. This file is audit-only: it never merges clusters and never changes the binary corrected matrix.
+
Column
Description
gene_cluster
Cluster absent from the audited reference
target_reference_cluster
Best reference-present cluster matched by k-mer seed plus global alignment
target_reference_gene
Gene ID carried by the audited reference for the target cluster
reference_event_class
same_gene_full_length, short_in_frame_allele, long_in_frame_allele, frameshift_or_truncated_reference_homology, frameshift_or_extension_reference_homology, or partial_reference_homology
identity, query_coverage, reference_coverage
Global-alignment summary between the cluster representative and the matched reference-cluster representative
Alignment and seed evidence used to choose the best event
matrix_recommendation
Currently keep_separate_cluster_annotate_event
interpretation
Publication-facing wording for the event
+
Rows in this file also appear in reference_genome_audit.tsv as EVENT_DERIVED_REFERENCE_CLUSTER with final_publication_status=flag_reference_event_derived_cluster and publication_confidence=event_derived.
Written for every run. This file is audit-only: it does not merge clusters and does not change gene_presence_absence_recore.csv. When --tree is provided, Recore parses the rooted Newick tree, matches tips to matrix columns by exact name or embedded ASxxxxx identifier, and applies Fitch parsimony to the corrected binary state for each cluster.
+
Column
Description
gene_cluster
Panaroo cluster name
n_present_corrected, prevalence_corrected
Presence after accepted matrix corrections and NSC presence updates
absent_not_searched, not_searched_reason
How many of the cluster's absences in the corrected matrix were never searched, and why: low_prevalence (the cluster is below --min-prevalence), no_cluster_sequence, or no_fasta (some genomes have no FASTA); 0 and blank when every absence was searched
tree_matched_tips
Number of tree tips matched to matrix genomes; blank when no tree is provided
reference_state
present, absent, or not_assessed for the requested --reference-genome
high, moderate, low, or not_assessed, based on tree pattern, root-state ambiguity, MRCA purity, and parsimony changes
phylogenetic_blink_flag
low_blink, moderate_blink, high_blink, or not_assessed; high blink marks clusters with singleton/dispersed/homoplastic behaviour that should be filtered or reviewed before highlighting
Dollo-like penalized model using the smallest carrier MRCA as the single-gain origin; broad origins, many losses, root ambiguity, and repeated independent gains reduce the score
ancestral_state, root_state_confidence
Parsimony root state: present, absent, ambiguous, or unknown, plus whether the Fitch root assignment is unambiguous
Whether all carrier tips form one pure carrier-only clade, and the largest carrier-only clade fraction
carrier_mrca_tips, carrier_mrca_purity
Size and carrier purity of the smallest tree clade containing all carrier tips; low purity means a single-gain story would require many internal losses
molecular_mechanism
Best conservative mechanism call: reference-derived allele, IS interruption, pseudogenization, fragmentation, duplication/paralog expansion, context shift, acquisition/insertion candidate, deletion/loss candidate, homoplastic/unstable signal, ancestral retention, or unresolved
Evidence summary, integrated prioritization, and publication-facing interpretation. With --nscluster, mechanism_detail also carries nsc_representative on the cluster that receives the merged support and nsc_member_merged_into_representative on each redundant member. A gain or loss call that rests on absences never searched carries absences_not_searched (none searched, and one support level lower) or absences_not_searched=N/M (some), and its wording says so; see Evolutionary events
Connected-component context for split, annotation-matched, NSC, and paralog-linked clusters
is_interruption_supported, mobile_element_*
Nearby mobile element signal. mobile_element_source=sequence comes from --mobile-element-reference alignments and is name-agnostic; mobile_element_source=annotation is the fallback from GFF labels/products/notes. IS calls are conservative and require sequence support or an insertion-sequence-like label within 250 bp of a fragmented/disrupted rescue
Locus-context stability and paralog/multilocus risk summary
synteny_mean_score, synteny_pass_fraction
Cluster-level summary of quantitative synteny support across assessed rescue evidence
orf_integrity_summary, rescue_evidence_counts
ORF signals and per-cluster accepted rescue-type summary
+
Use this file as the top-level evolutionary interpretation table. The most important guardrail is that phylogenetic_pattern and molecular_mechanism are deliberately separate: a clade-associated presence can be an acquisition, a reference loss, a duplicated/paralogous copy, or an event-derived allele depending on the sequence, synteny, GFF, and reference evidence.
Written when GFF annotations are loaded. One row per cluster.
+
Column
Description
gene_cluster
Panaroo cluster name
n_present
Number of genomes where the cluster is present in the input matrix
n_context_mapped
Present genomes where the cell gene ID was found in the GFF
n_unresolved_gene_ids
Present genomes where the cell gene ID could not be resolved in the GFF
n_unique_contexts
Number of distinct upstream/downstream context signatures
dominant_context_count
Number of genomes in the largest context
dominant_context_fraction
Dominant context count divided by mapped contexts; blank when no context could be mapped
interpretation
stable_single_locus, stable_single_locus_minor_noise, mostly_stable_review_minor_contexts, possible_multilocus_or_paralog, or insufficient_context
+
Clusters flagged as possible_multilocus_or_paralog are not automatically changed, but they should be reviewed before treating accessory-gene calls as biological.
Written when GFF annotations are loaded. This is an audit-only synteny-block summary using up to five upstream and five downstream cluster neighbours. When a reference genome is supplied, each block also stores a representative gene-level neighbourhood and the best matching reference/H37Rv neighbourhood inferred from shared flanking clusters, even when the focal cluster itself is absent from the reference.
+
Column
Description
gene_cluster
Panaroo cluster name
block_rank
Rank by number of supporting genomes
n_genomes, fraction_of_present
Support for this block among genomes where the cluster is present
upstream_block_5, downstream_block_5
Cluster-name signatures for the wider flanking block
block_interpretation
single_synteny_block, dominant_block_minor_alternatives, or alternative_synteny_block
example_genomes
Up to five genome:gene examples
carrier_genomes
All genomes carrying that exact block signature
representative_*
Genome, gene ID, contig, 1-based coordinates, strand, and length for one representative focal gene
representative_block_genes
Semicolon-separated gene records for the representative block as cluster,gene_id,strand,contig,start,end,length
reference_block_genes
Same gene-record encoding for the inferred reference context; includes an __GAP__ marker when the focal cluster is absent between shared neighbours
Matrix-policy filter: --matrix-policy controls which accepted rescues become binary presence. strict corrects only STRICT_PRESENT/ALIAS_CLUSTER; balanced also corrects full-length SEQUENCE_ONLY; sensitive also permits partial/disrupted accepted evidence. In all modes, the adaptive orthology-quality guard can keep weak accepted evidence out of the binary matrix.
NSC-merged cells (only if --nscluster): Cells where a merged cluster would have added presence become NSC_MERGED.
Metadata columns updated: No. isolates, No. sequences, and Avg sequences per isolate are recomputed from the final cells. Multicopy cells separated by ; count as multiple sequences but one isolate.
NSC clusters removed: Panaroo clusters that were merged by NSC into a representative cluster are dropped entirely (their rows are absent from the output).
Written for every run, in the same pass as gene_presence_absence_recore.csv, so the two agree on every cell. It is the corrected matrix as 1 and 0 in Panaroo's gene_presence_absence.Rtab layout: a Gene column, then one column per genome, tab-separated. A cell is 1 when the corrected CSV has anything in it (input presence, RESCUED:... or NSC_MERGED), and clusters merged away by NSC are left out, as in the CSV. This is the file pyseer's --pres, and most R code written for Panaroo, reads.
Written for every run. Same row/column layout as the corrected matrix, but each genome cell stores the biological interpretation rather than a binary presence call.
+
Possible states:
+
PRESENT_INPUT: present in the original matrix.
ABSENT: searched, and no evidence under current thresholds.
NOT_SEARCHED_LOW_PREVALENCE: never searched, because the cluster is in fewer genomes than --min-prevalence.
NOT_SEARCHED_NO_CLUSTER_SEQUENCE: never searched, because the cluster has no sequence in the pan-genome reference.
NOT_SEARCHED_NO_FASTA: never searched, because the genome has no FASTA file.
STRICT_PRESENT: accepted same-locus rescue.
ALIAS_CLUSTER: accepted split/alias or NSC-compatible cluster evidence.
SEQUENCE_ONLY: full-length sequence evidence without context/annotation support.
PARTIAL_HOMOLOGY: accepted fragmented or alignment-split evidence.
PSEUDOGENE: accepted disrupted sequence evidence.
HOMOLOG_PARALOG: rejected or ambiguous paralog-like evidence.
Use this file when a binary matrix would overstate biology, especially for pseudogenes, partial genes, PE/PPE-like families, or publication figures that need to show uncertainty explicitly. A NOT_SEARCHED_* cell is absent because the input matrix says so; Recore did not test it. When a cell has two reasons, the cluster's is written, in the order of the gap lines in summary.txt.
Written for every run. The file may contain only the header when no inter-cluster relationships are detected.
+
This table summarizes reproducible evidence that one Panaroo cluster is related to another. It is intended as an audit layer: Recore reports relationships and recommendations, but only nucleotide similarity clustering (--nscluster) changes the output matrix automatically.
Written for every run. These tables convert pairwise relationships into connected components, so split clusters, annotation-matched clusters, NSC members, and paralog-linked clusters can be reviewed as one unit.
+
cluster_components.tsv contains one row per non-singleton component:
+
Column
Description
component_id
Stable component label for the run
n_clusters
Number of clusters in the component
relation_types
Relationship classes connecting the component
recommendation
Component-level action, such as review_or_collapse_aliases or review_component_keep_paralogs_distinct
member_clusters
Semicolon-separated cluster names
+
cluster_component_members.tsv contains one row per member cluster:
+
Column
Description
gene_cluster
Panaroo cluster name
component_id, component_size
Component membership
component_role
nsc_representative, nsc_member, related_cluster_member, or paralog_component_member
relation_types, recommendation
Same component evidence and review action
+
These components are also reported in evolutionary_events.tsv so manuscript highlighting can deprioritize redundant aliases or paralog components.
The gap lines add up. Every empty cell of the input matrix is in Total gaps, and each one is accounted for exactly once:
+
Line
Gaps it counts
Skipped (prevalence)
in clusters under --min-prevalence; never searched
Gaps skipped (no seq)
in clusters with no sequence in the pan-genome reference; shown only when there are some
Gaps passing filter
everything else
Gaps skipped (FASTA)
of those, in genomes with no FASTA file; shown only when there are some
Gaps searched
of those, actually searched with minimap2
+
Matrix-corrected is the number of cells written into the corrected matrix as presence, and Remaining gaps is the number of cells still empty in it, both counted from the file as it is written. Without --nscluster, Total gaps - Matrix-corrected = Remaining gaps. With --nscluster a NSC merged cells line appears under Matrix-corrected, rows merged into a representative are no longer in the file, and the summary ends with a Nucleotide similarity clustering block.
Core +3 in the tutorial: Rv0003, Rv0006 and Rv0012 were missing from a few genomes and are now found in all of them, core genes the matrix had excluded. Rv0009 stays out, because balanced keeps pseudogenes out of the binary matrix.
With --nscluster the soft-core can shrink: clusters that looked soft-core on their own may be redundant copies of another cluster and are merged into it.
Machine-readable run manifest written for every run. It records:
+
tool and tool_version;
schema_version (recore.outputs.v7);
created_utc, the time the run finished writing outputs, and command_line, the exact invocation;
input paths used for the run;
key parameters, including matrix policy, prevalence threshold, synteny, NSC, reference genome, tree, and mobile-element settings;
headline counts such as clusters, genomes, searched gaps, direct rescue corrections, NSC-merged corrections, rejected synteny/paralog candidates, and NSC groups.
+
Use this file when archiving outputs for a manuscript or comparing runs across parameter settings.
These clusters had nearly identical nucleotide sequences but were split by Panaroo due to amino acid-level differences (typically from upstream annotation artifacts).
Written only when --write-corrected-gff is enabled.
+
For each genome with at least one rescue that corrected the matrix, Recore writes a GFF3 file under corrected_gff/; evidence kept out of the matrix (held_out_policy, held_out_quality) is not written. If an original GFF was supplied with --annotations, its records are preserved and Recore appends rescued CDS features, on the GFF's own seqid even when that seqid had to be mapped onto a differently named FASTA contig. If the original GFF contains a ##FASTA section, rescue features are inserted before ##FASTA and the FASTA tail is preserved after them. If no original GFF is available for a rescued genome, the file contains only the appended rescue features.
+
Appended rescue features use source Recore, type CDS, and include:
+
Attribute
Description
gene_cluster
Panaroo/Recore cluster name
rescue_type
Rescue classification
identity
Nucleotide identity (%)
coverage
Query coverage (%)
confidence
Recore confidence score
synteny
PASS, FAIL, or NA
annotation_match
Original GFF feature ID overlapped by the rescue, if any
fragment_n
Number of fragments in the selected multi-hit chain, if applicable
fragment_query_gap_bp
Total query-space gap across the fragment chain
fragment_target_gap_bp
Total target-space gap across the fragment chain
fragment_note
Fragment interpretation, if applicable
+
These GFFs are designed as auditable corrected annotations. If a rescue overlaps an existing feature, the original feature is preserved and the appended Recore feature records the rescue evidence explicitly.
Written only when --write-rescued-fasta is enabled: the sequence of every cell the binary matrix gained, for functional annotation (eggNOG-mapper, InterProScan, BLAST). Record IDs are the feature IDs of corrected_gff/*.recore.gff3, and the description carries the genome, the cluster, the locus, the rescue type and the phase:
rescued_genes.fna has the nucleotide span read in the gene's direction (reverse-complemented on the minus strand). rescued_genes.faa translates it from the phase the alignment fixes, with the bacterial code (NCBI table 11); a first codon that is the gene's own start reads M whichever start it is, and the final stop is left off. A rescue built from several alignments has no single reading frame, so it is in the .fna with phase=unknown and not in the .faa.
orthology actions and confidence tiers from orthology_evidence.tsv;
evolutionary priorities and dominant mechanisms from evolutionary_events.tsv;
per-genome QC status from genome_qc.tsv;
the top candidate clusters ranked by evolutionary_signal_score;
direct links to the exact output files used to generate the summary.
+
When --tree is supplied and the matched tree/matrix is small enough for a responsive browser view, the report also embeds a phylogeny plus corrected matrix explorer. The explorer prunes the tree to matched matrix tips, keeps matrix rows in tree order, lets the user search/select clusters, recolors tree tips dynamically by the selected cluster state or evolutionary mechanism, and lets users click matrix columns to change the selected cluster. The matrix can also be recolored by evolutionary mechanism, with a legend explaining each observed process from evolutionary_events.tsv. MTBC-style genome names can be colored by lineage with the mycolorsTB Nature palette; in lineage mode, monophyletic branches are colored too. Clicking mechanism cards adds or removes mechanisms from the matrix-column filter, and selected-cluster details show the reference cell/gene or the reference-derived event source when available. Dense views can be zoomed with the row and column sliders in each pane header, with the mouse wheel over the tree or the column header, and in fullscreen. The viewer's own page documents it in full: see Run report. When cluster_synteny_blocks.tsv is present, the viewer can open a full-width synteny band under the matrix, drawing up to ten ranked unique five-neighbour blocks for the selected gene cluster. The rows are unique by ordered displayed gene/locus architecture, ignoring internal cluster gaps, length differences, and coordinate differences; each row stores one representative locus and all carrier genomes for that architecture. The band is hidden by default and opens from the layers button or the S key. Once visible, the seam above it is draggable and a scale slider expands or compresses the gene-arrow layout. The selected block marks its carrier genomes on the tree, the clickable H37Rv/reference row marks the reference tip, the focal cluster is highlighted, and clicking any gene arrow jumps to that cluster in the matrix. Gene arrows carry strand orientation and proportional length. Labels prefer reference locus IDs where available, and each gene tooltip includes cluster, locus, coordinates, strand, and length. Shared neighbouring clusters are connected only between adjacent synteny rows.
+
The embedded explorer is intentionally size-limited. Larger runs keep the static audit summary and report why the explorer was omitted, rather than writing a sluggish HTML file.
+
The HTML report is an audit dashboard, not a replacement for the TSV outputs. Use the TSV files for exact filtering, statistics, and downstream figures.
[DEBUG recore::rescue] Synteny skip: no flanking genes mapped to clusters for rescue cluster=3940 genome=75
+[DEBUG recore::rescue] Upstream gene 'L1_AS00366_Rv2650c' not found in any Panaroo cluster
+[DEBUG recore::pipeline] Top rescues (first 10):
+[DEBUG recore::pipeline] group_2044 in A1_AS00349 → Dropped (id=100.0%, cov=100.0%, conf=1.20, synteny=Some(true))
Directory of GFF3 annotation files. Required for SPLIT/START_VARIANT classification and synteny validation.
-o, --output
recore_output/
Output directory
-t, --threads
4
Number of parallel threads (for rayon classification and minimap2)
+
Recore matches FASTA and GFF files by exact matrix column name first, then by an embedded ASxxxxx identifier. For single-contig genomes, GFF seqids are normalized internally to the FASTA contig name so --synteny can work when a GFF uses contig_1 or L4_AS00146 but the FASTA contig is AS00146.
+
Numeric parameters are validated before the pipeline starts: percentages must be finite values between 0 and 100, fractions between 0 and 1, thread count and NSC k-mer size must be at least 1, and --nsc-engine must be minhash or mmseqs2.
minimap2 preset. asm5 for sequences ≤5% divergence (MTBC, closely related strains). asm20 for up to 20% divergence (more divergent species).
--min-identity
90.0
Minimum % nucleotide identity to consider a hit. For MTBC, the default (90%) is appropriate: real genes have >99%. Lowering this increases PARALOG noise.
--min-coverage
80.0
Minimum % of reference sequence that must be covered by a hit (single hit or merged fragments).
--multi-allele
false
Use all alleles per cluster as queries. More sensitive for clusters with high intra-cluster diversity, but slower. Panaroo produces multiple alleles in pan_genome_reference.fa.
Minimum fraction of genomes where a cluster must be present to attempt rescue. Clusters present in fewer genomes are likely truly accessory and skipped. Set to 0.0 to attempt rescue of all gaps.
Validate each rescue by adaptive 1-to-5-neighbour flanking context against reference genomes. Requires --annotations. Boosts synteny-consistent rescues (×1.2), reports quantitative synteny metrics, and rejects explicit synteny failures from the corrected matrix while writing them to synteny_rejected_report.tsv. Independently, PARALOG candidates without explicit synteny support are rejected to paralog_rejected_report.tsv.
--write-corrected-gff
false
Write a corrected_gff/*.recore.gff3 file for each genome with a matrix-corrected rescue, with those CDS features appended. Original GFF records are preserved when --annotations is provided.
--write-rescued-fasta
false
Write rescued_genes.fna and rescued_genes.faa: the sequence of every cell the binary matrix gained, read in the gene's direction, and its protein (bacterial code, table 11) where the reading frame is known. IDs match the features of --write-corrected-gff.
--search-cache
none
Directory to keep minimap2 results in and reuse them from. A rerun that changes the matrix policy, coverage, synteny or the prevalence floor aligns only the sequences it has not seen before; a changed genome, query sequence, preset, --min-identity or minimap2 version is searched afresh. Results are identical with and without it.
--write-html-report
false
Write a self-contained recore_report.html with headline QC, correction counts, evolutionary mechanisms, top candidate clusters, and links to the underlying audit files.
--matrix-policy
balanced
Controls which accepted rescues can enter the binary corrected matrix. strict corrects only STRICT_PRESENT and ALIAS_CLUSTER; balanced also corrects full-length SEQUENCE_ONLY; sensitive also corrects partial/disrupted accepted evidence for exploratory analyses. In all modes, the adaptive orthology-quality guard can keep weak accepted evidence as audit_only_quality.
--reference-genome
none
Audit one genome column/reference background by exact matrix name or embedded ASxxxxx identifier. Runs a reference-only shadow search and writes reference_genome_audit.tsv with new_to_reference, reference_like_or_present, and reference_like_ambiguous classifications, plus publication_confidence labels for clean, complex/paralogous, event-derived, fragmented, pseudogene, and unresolved reference evidence. Also writes reference_cluster_events.tsv for reference-absent clusters whose representative sequence is best explained as an allele, fragment, or partial homologue of a reference gene. These rows are audit-only and do not change the binary matrix.
--tree
none
Optional rooted Newick tree. When provided, Recore uses Fitch parsimony on the corrected presence/absence state to add ancestral state, root-state confidence, gain/loss counts, carrier-MRCA purity, Dollo-like fit, monophyly, and phylogenetic pattern calls to evolutionary_events.tsv. Tip names can match matrix columns exactly or by embedded ASxxxxx identifiers.
--mobile-element-reference
none
Optional nucleotide FASTA of mobile elements, insertion sequences, or transposase loci. Recore aligns these sequences to genomes with rescue/disruption evidence and reports nearby hits in evolutionary_events.tsv as mobile_element_source=sequence, so IS support can be sequence-based rather than dependent on GFF product names.
--mobile-element-min-identity
85.0
Minimum identity for sequence-based mobile-element hits.
--mobile-element-min-coverage
50.0
Minimum query coverage for sequence-based mobile-element hits.
--mobile-element-window
1000
Maximum distance in bp from a rescue/disruption interval to report mobile-element support. IS interruption calls still require close evidence within 250 bp.
Why --min-prevalence 0.0: In MTBC, even rare accessory clusters can have real false absences due to annotation differences. 0.0 ensures nothing is skipped.
+
Why asm5: Strains are <5% divergent. asm5 is faster and more accurate at this range.
+
Why mmseqs2 for NSC: More precise than MinHash for detecting the redundant annotation clusters common in MTBC pangenome analyses.
+
+
More divergent bacteria / pan-genome with 95% ANI#
--write-html-report writes recore_report.html into the output directory. It is a single file with no external requests: it opens from a results directory on a cluster node with no network, and it keeps working when you email it to a co-author.
That one is the output of the tutorial run: twelve genomes, twenty-four clusters, eleven planted false absences. It is the whole file, opened straight from this page, which is the point of it being self-contained.
+
Without --tree the workspace still opens: the rows follow the matrix order, the tree pane only lists the genomes, and a notice above the workspace says so. Ancestral readings, tree colouring by subtree and rerooting need a real tree.
+
If the workspace cannot start (a browser that cannot hold the embedded data, for instance), the report says why in place of the workspace, and D still opens the run details.
Press Cmd+K (or Ctrl+K, or /) to open the find field. It searches clusters and genomes at once, ranking prefix matches on the name above name matches above annotation matches. Enter opens the result: a cluster becomes the selected column, a genome scrolls the panes to its tip and opens its card.
+
The arrow buttons beside the field step through the visible columns one at a time, and ← / → do the same from the keyboard. Shift with them jumps ten.
The sliders icon in the phylogeny header opens everything that shapes the tree.
+
Shape. Phylogram draws branch lengths to scale; cladogram ignores them and shows topology only. A scale bar under the pane gives the phylogram its units.
+
Root. Every ancestral reading in the report depends on where the tree is rooted, so it is an explicit choice: the L8 outgroup when one is present, the tree as supplied, or the midpoint of the longest tip-to-tip path. Alt and a click on any tip roots on that genome instead.
+
Tip colour. By the selected cluster's state, by MTBC lineage, by mechanism, by rescue burden, by absence burden, or by any correction. For MTBC-style names such as L4_AS00146, lineage colouring uses the mycolorsTB palette and colours monophyletic branches as well as tips.
+
A plain click on a tip opens that genome in the inspector: its lineage, its state in the selected cluster, its counts across all clusters, and its distance from the root.
+
The burden modes draw a ramp from the line colour to the state colour, with a hollow ring for zero, and a key under the top bar gives the ramp's maximum.
Each cell is one cluster in one genome, in one of six states:
+
State
Colour
Meaning
input
grey
present in the input matrix
corrected
blue
absent in the input; Recore found the gene and corrected the cell
NSC
orange
absent in the input; near-identical clusters were merged and the merged cluster is present (only when NSC ran)
kept out
crimson
Recore found sequence evidence, but the matrix policy, a quality floor or a synteny or paralog guard kept it out of the corrected matrix
absent
pale
absent; Recore searched the gap and found no evidence
not searched
pale, hatched
absent in the input and never searched: the cluster is below --min-prevalence or has no sequence to search with, or the genome has no FASTA
+
Input presence is the quiet background on purpose, so what Recore found is what draws the eye at any zoom. The five colours stay distinguishable under deutan and protan colour vision, and not-searched cells add a hatch rather than a sixth colour. The legend in the matrix header only lists the states the run has, and drops to swatches when the pane is too narrow for the labels; each swatch still names its state on hover.
+
Absent or not searched. An absent cell is a gap Recore searched and found nothing in. A hatched cell is a gap it never searched, so its absence is the input's call and not a finding. Under the default --min-prevalence of 0.1 every cluster in fewer than one genome in ten is hatched. Its tooltip and cell card say why it was not searched, the cluster card says so for a whole cluster, and in the tree a tip with a dashed ring is a genome the selected cluster was not searched in.
+
Cell evidence. Hover a cell for its genome, cluster and state; for a corrected or kept-out cell the tooltip adds the rescue type, identity and coverage. Click a cell for the full row from orthology_evidence.tsv at the top of the inspector: the locus, the synteny and ORF checks, whether it went into the matrix and why not, and its publication status. The cluster card lists every evidence row of the cluster, corrections first; clicking one opens that cell and brings its tip into view.
+
Mechanism colour. In mechanism mode the input cells take the colour of their cluster's mechanism family (retained, gain, loss, disruption, paralog or context change, artifact or unresolved), while corrected and kept-out cells keep their own colours. The legend key opens the full list, with counts and filters.
+
The sliders icon in the matrix header holds cell colour, column order, the reference-absent filter, the searched-clusters filter and the mechanism filter. Searched clusters only hides the clusters whose gaps were never searched and refits the columns, leaving the part of the matrix Recore actually audited.
+
Column order. Input matrix order, prevalence, mechanism, number of corrected cells, or cluster name.
+
Mechanism filter. The matrix menu lists the mechanisms in view as a checklist, with each one's description in its tip. Ticking one shows only its clusters and refits the columns to the pane; active filters appear as chips beside the matrix count, and a click on a chip removes it. Download TSV saves the clusters in view with their counts of input, corrected, NSC, kept-out, absent and not-searched cells.
+
Corrections by lineage. For MTBC-style genome names the cluster card shows, per lineage, how many genomes were corrected or had evidence kept out, out of the lineage's genomes.
+
Zoom. The slider in each pane header drives its own axis, in equal ratios from end to end. A plain wheel always scrolls. Cmd (or Ctrl) and the wheel zoom columns, which is also what a trackpad pinch does; Alt and the wheel zoom rows. Each notch is a factor of 1.25 and the cell under the pointer stays under the pointer. Press Z to fit the data back to the window.
+
Press Z to see the whole matrix at once. When that makes cells smaller than a pixel, the matrix is drawn as density: each pixel shows the share of present cells it covers, between the absent and present colours, and is tinted blue where any cell in it was corrected (crimson where evidence was kept out), more strongly the larger that share. The hatch carries on in density, as strong as the share of not-searched cells in the pixel. Zoom in on a region to get back to single cells.
+
Above 40 columns a strip under the matrix shows the mean prevalence of the clusters at each position, with a blue mark where any of them has a changed cell, and the current viewport outlined. Clicking it jumps.
+
The open genome's row stays outlined in the tree and the matrix after the pointer moves on, so a genome found from the find field is still easy to spot.
Two genomes far apart in the tree are far apart in the matrix. Pin to compare in the genome card, or P, keeps a genome's row in a band under the column header, aligned with every column whatever the scroll; its name sits level with it in the tree pane's header, marked A or B, and its tip gets a blue ring. Pinning a third genome unpins the oldest. Hover a cell of the band for its state; click it to open that cell.
+
With two genomes pinned, the genome card compares them over every cluster of the corrected matrix: how many are in both, only in A, only in B and in neither, and the clusters that differ, each one a click away. When a difference rests on a gap that was never searched, the card says so, because then the input matrix, not Recore, says the gene is missing. Show only the clusters that differ filters the matrix to them and refits the columns; a chip beside the matrix count takes the filter off again.
Press S or use the layers button to show the synteny band. It draws the top unique five-neighbour blocks for the selected cluster, at most 10 per cluster, with gene arrows keeping strand and proportional length.
+
Uniqueness is defined by the ordered displayed gene architecture, ignoring internal cluster gaps, coordinate differences and length differences. Each row shows one representative locus.
+
Selecting a row marks its carrier tips on the tree.
Row 2 is the inferred reference context from shared neighbours, and it is clickable.
Clicking any gene arrow jumps to that cluster in the matrix.
Shared neighbouring clusters are joined only between adjacent rows.
+
This needs cluster_synteny_blocks.tsv, which means GFFs were loaded.
D or the info button opens the run details. They start with what you need to write the run up:
+
Using these results: a methods paragraph built from the run's own settings and counts, with a button that copies it; the caveats the evidence calls for (cells kept out of the matrix, corrections whose span is not whole codons, rejected candidates, synteny or the tree being off); and the parameters, the command line and the time the report was written.
Changed cells: every evidence row, corrections first, with a button that copies them as tab-separated text and one that downloads recore_changed_cells.tsv. Clicking a row opens that cell in the matrix.
+
Below come the run statistics, the dominant mechanisms, the top candidate clusters (the first ten, then Show all) and every file in the output directory with its row count. The links open the files beside the report, so they work while the report sits in its output directory.
+
The genome card shows each genome's QC status and flags from genome_qc.tsv, and tips can be coloured by QC status.
The download button writes recore_matrix.png and, when the synteny band is open, recore_synteny.svg.
+
The PNG is drawn for the page, not copied from the screen: the tree, the lineage strip, the column names and the matrix share one geometry, so every tip sits on its row, at three times the on-screen size, always in the light palette and without the hover crosshair or the selection marks. Below the matrix come a key for the cell states, the tree's scale bar and a caption with what is shown, the root, the matrix policy and the Recore version. When the whole matrix fits in 4,000 by 4,000 screen pixels it is drawn in full; otherwise the window on screen is drawn and the caption names its rows and columns.
+
The SVG carries every colour and font as a plain attribute, with no stylesheet or CSS variables, so illustration software and PDF tools draw it as the page does.
+
Cmd+P prints the figure described above for the whole matrix (in the light palette, whatever the screen is in), then the run details as a document with every candidate. The live panes are swapped for the figure only while printing.
Press ? for this list in the report itself. A switch at its top turns the one-letter shortcuts off, for example when they fire by accident with speech input; Cmd+K and the arrows keep working, and the choice is remembered in that browser.
The matrix ships as compact per-cluster state sets rather than one character per cell, and each pane draws only its visible window onto a viewport-sized canvas. A 10,000 genome by 30,000 cluster matrix, 300 million cells, is a 17 MB file that reaches interactive in about 130 ms and re-renders in single-digit milliseconds at every zoom level.
+
The workspace is embedded up to 20,000 matched tips and 1.2 billion cells. Above either limit the report says so and points at gene_presence_absence_recore_states.csv for the full matrix, rather than shipping something a browser cannot open.
benchmarks/recore_report.py builds a compact Markdown and HTML report (recore_report.md and recore_compact_report.html) from an existing output directory. It refuses to replace an interactive recore_report.html unless given --force.
Seven steps from an empty cell to a decision. Pick what the search finds, step through, and watch what happens to the cell.
+
+
+
+
+
+
+
What does the search find in the assembly?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
A simplified walk through one cell. The real run does this for every empty cell, in parallel, and also merges clusters that are the same DNA, audits a reference genome and calls evolutionary events. Every rule, in the Algorithm page.
+
+
+
+
+
+
Go deeper
+
The same steps, with every threshold and every rule.
Optional auditsreference genome + NSC + tree + mobile elements
→ Reports + optional corrected GFF
Reports + optional corrected GFF
+
The corrected binary matrix is not the only output. Recore also keeps the evidence trail that explains why a cell was corrected, retained as audit-only, or rejected.
Recore rescues falsely absent genes from pangenome matrices produced by Panaroo (or Roary). It takes a gene presence/absence CSV, the pan-genome reference FASTA, individual genome FASTAs, and (optionally) GFF annotations, then identifies genes that are physically present in genomes but missing from the matrix.
+
The pipeline runs in 12 sequential phases. Each phase is deterministic given the same inputs and thread count.
Parses Panaroo's gene_presence_absence.csv. The CSV has 13-15 metadata columns (Gene, Annotation, No. isolates, etc.) followed by one column per genome. Each cell is either empty (gene absent) or contains semicolon-separated gene IDs.
+
The matrix is stored as a PresenceAbsence struct with encapsulated access via newtype indices (ClusterIdx, GenomeIdx) to prevent index confusion.
Parses Panaroo's pan_genome_reference.fa. Each entry has format >clusterName~~~genomeName description. The ~~~ separator is stripped to extract the cluster name.
+
Single-allele mode (default): keeps only the longest allele per cluster. Multi-allele mode (--multi-allele): keeps all alleles, increasing sensitivity at the cost of more minimap2 queries.
Scans the genomes directory for files with extensions .fasta, .fa, .fna (also .gz variants). File stems become genome names matched against CSV column headers.
Iterates over every (cluster, genome) pair. A gap is any pair where the gene is absent.
+
Gaps are filtered by prevalence: if a cluster is present in fewer than --min-prevalence fraction of genomes (default 10%), its gaps are skipped. The rationale is that ultra-rare clusters are likely truly accessory, and searching for them in hundreds of genomes generates cross-match noise.
+
The remaining gaps are grouped by genome for batch processing.
For each genome with an annotation, builds a map from gene ID → ClusterIdx. This maps every gene ID found in the CSV cells to its Panaroo cluster assignment.
+
Panaroo appends suffixes to gene IDs that don't exist in the original GFF:
+
_pseudo for pseudogene annotations
_dup_N for duplicate gene copies
+
The map indexes both the original ID and stripped versions to maximize GFF matching success.
+
The map is then read into one cluster for each name of the genome's GffIndex (4 bytes each, a GeneClusters) and dropped. Every later lookup is for a name the annotation uses, so this answers each one as the map did. The genomes are done in parallel.
This is the computational core. Each genome that has gaps is searched for its own absent clusters, one minimap2 run per genome, in parallel across --threads.
A genome's queries are only the clusters absent from it, and its target is only its own assembly. Genomes that carry a cluster never compete with the genome where it is missing.
+
That isolation is the point. Concatenating every genome into one target looks cheaper, but minimap2's secondary budget (-N) and its secondary-to-primary ratio (-p) then apply across all genomes at once. Once more than about fifty genomes carry a gene, their full-length copies use up the budget and the gap genome's hit is dropped; a gap genome's weaker copy, or the two pieces of a gene split across contigs, fall under half the carriers' score and are cut. Searched alone, a genome's hits only compete with its own other copies, and each run indexes one assembly rather than the whole pangenome.
The cluster sequences a genome needs are written to a temporary FASTA. In multi-allele mode, alleles are named clusterName~~~ALLELE~0, clusterName~~~ALLELE~1, etc.
The genome's FASTA is read by Recore's own reader (plain or gzipped) and written to a temporary file in normalised form: each header is cut to its contig name, line endings are normalised. minimap2 is given that copy rather than the user's file, so the aligner and the rest of the pipeline agree on what the assembly contains. Inputs that would otherwise give confident wrong answers fail here, naming the file and line: sequence before the first > header, an empty or non-UTF-8 contig name, and a contig name used twice in one assembly.
Every pass runs with base-level alignment, so identity and coverage are exact:
+
-c: base-level alignment. Without it minimap2 reports chain-based match counts, which read a 99.6% identical gene as about 93%, and every identity threshold would be comparing against that.
-s 40: the asm presets ask for a peak alignment score of 200, which with -c drops every gene shorter than about 200 bp.
--end-bonus 40: beats the asm5 mismatch penalty, so one SNP near either end of a gene does not clip the alignment short of the gene's end.
--secondary=yes -N 500 -p 0.5: report secondary alignments, needed for split and paralog detection. The budget is per genome.
Preset asm5 (default) or asm20: controls alignment sensitivity.
Each hit is assigned to its cluster through the query name and filtered by minimum identity (--min-identity, default 90%). Identity is exact, so the threshold means what it says.
Each (cluster, genome) gap with hits is classified into one of 9 rescue types. Classification follows three paths:
+
Path 1: Single full-coverage hit (≥ min_coverage)#
+
The best hit (by query length, then score) covers ≥80% of the reference sequence (default --min-coverage). Classification depends on GFF annotation overlap:
+
Before classification, Recore builds a conservative gene-to-cluster index from the matrix and the GFF. If one alias of a CDS maps unambiguously to a cluster, its other aliases (locus_tag, gene, Name, protein_id, old locus tags) are mapped to the same cluster. Ambiguous aliases that point to multiple clusters are left unresolved.
+
Condition
Type
Confidence
No GFF provided
DROPPED
identity × coverage
GFF loaded, no substantial CDS overlaps hit, identity ≥95%
MISSING_CDS
identity × coverage
No GFF gene overlaps hit
DROPPED
identity × coverage
Overlapping gene belongs to a different cluster, identity ≥ 95%
SPLIT
base × 1.1
Overlapping gene belongs to a different cluster, identity < 95%
PARALOG
base × 0.7
Overlapping gene not in any cluster
DROPPED
identity × coverage
+
MISSING_CDS is used only when the alignment is full-length and no CDS overlaps more than half of both the hit span and the annotated CDS. Short boundary overlaps with adjacent genes are ignored so a real missing annotation is not swallowed by a neighboring feature.
+
After initial classification, DROPPED hits undergo start variant detection: if the hit has ≥98% identity and a GFF gene within 300bp length difference overlaps ≥80% of the hit, it's reclassified as START_VARIANT.
+
Path 2: Conservative fragment-chain assembly (same allele, contig, and strand)#
+
If no single hit has sufficient coverage, hits are first grouped by query allele, query length, target contig, and strand. Recore builds the best co-linear chain within each group and uses only the best chain. This prevents independent alleles or paralogous hits from being merged into a false fragmented gene.
+
If the best chain reaches --min-coverage with ≥2 fragments:
+
Non-co-linear chains are not rescued as fragments.
If the whole chain lies inside one compatible CDS, Recore classifies it through the single-CDS annotation logic (DROPPED, START_VARIANT, SPLIT, or PARALOG) instead of calling FRAGMENTED.
If average identity is ≥95% and a small gap is not divisible by 3, the event is classified as PSEUDOGENE:Frameshift.
If the chain has a large query or target gap (≥50 bp), the event is classified as FRAGMENTED.
Otherwise, the event is classified as ALIGNMENT_SPLIT: the gene is reconstructed by multiple alignments, but the evidence does not support a biological break.
+
Fragment-chain evidence is written to rescue_report.tsv for auditability.
If the best hit has ≥90% identity but only 60-80% coverage (below min_coverage), the gene is classified as PSEUDOGENE:PartialDeletion. Multi-fragment coverage is also considered.
+
If none of the paths match → no rescue (gene is truly absent)#
DROPPED and MISSING_CDS rescues are post-processed for pseudogene detection. For each genome with at least one sequence-only rescue:
+
Load genome FASTA contigs on-demand (not preloaded for all genomes)
If the rescue sits on an annotated CDS, extract that CDS and read it in its own frame, from its start codon to its stop, counting stops before the last codon. That is the reading frame this genome's annotation asserts, and it ends at this genome's own stop.
Otherwise extract the aligned region, reverse-complement it on the minus strand, and read it in the frame the alignment fixes. The cluster's representative begins at its start codon, so an alignment that starts q bases into the representative puts the first whole codon (3 - q mod 3) mod 3 bases into the region. Count internal stops (TAA, TAG, TGA) before the last codon.
Only when neither is known (a rescue built from several alignments) scan all three frames and keep the frame with the fewest.
If stops are found, reclassify as PSEUDOGENE:InternalStop, confidence × 0.5
+
The minimum over three frames is only the last resort: a premature stop in the true frame can hide behind another frame that happens to be stop-free, which G/C-ending codons make common. An unannotated pseudogene of that kind used to be inserted into the matrix as present.
+
The same alignment start sets the phase of the rescue's CDS feature in corrected_gff/.
When --synteny is enabled and GFF files are loaded, each rescue is validated against the genomic context of reference genomes where the gene IS present.
For the rescued gene at position (contig, start, end) in the target genome:
+
Build side-specific context windows around the rescued locus using 1, 2, 3, 4, and 5 upstream/downstream CDS neighbours. If the rescue substantially overlaps an annotated CDS, that CDS is used as the centre so short start-site differences and overlapping CDS boundaries do not shift the context. Otherwise the locus midpoint is used, which is important for missing-CDS calls with only tiny overlaps into adjacent genes.
For every reference genome where this cluster IS present, in deterministic genome order: a. Read every gene ID in the matrix cell, not only the first one. This keeps multi-copy/paralogous clusters from being represented by an arbitrary copy. b. Find each reference gene in the GFF (try exact ID, then strip _pseudo/_dup_N suffixes). c. Build the same side-specific context window around each resolved reference gene.
Score each target/reference context comparison. Nearest neighbours contribute most, distal neighbours contribute less, upstream/downstream sides are scored separately, and each reference neighbour can be matched only once.
A reference context is compatible when its score is at least 0.50 and it satisfies the side rule: if both sides are comparable, at least one upstream and one downstream cluster must match; if only one side is comparable, that side must match.
If three or fewer reference genomes have context, one compatible context can validate the rescue. If more than three reference genomes have context, compatibility must be observed in at least two genomes. This prevents a single noisy/paralogous context or several duplicated copies in one genome from validating a rescue in a large dataset.
Recore keeps the best assessment across neighbour windows, preferring passing assessments, then higher score, then the smallest informative window.
Otherwise -> FAIL.
+
Explicit FAIL candidates are written to synteny_rejected_report.tsv and are not used to correct the output matrix. Rescues with no mappable flanking context remain in rescue_report.tsv with synteny empty, and so do rescues whose context no genome carrying the cluster can be compared with (for example when those genomes have no GFF): a missing comparison is not an incompatible one.
+
PARALOG candidates are more conservative: if they do not have explicit synteny=PASS, they are written to paralog_rejected_report.tsv and are not inserted into the corrected matrix.
+
MISSING_CDS candidates follow the same conservative matrix rule: synteny=PASS upgrades them to STRICT_PRESENT; without synteny support they are kept as MISSING_CDS_UNVALIDATED audit evidence and are not corrected by any matrix policy.
Optional (--nscluster). Detects Panaroo clusters that are distinct at the protein level but nearly identical at the nucleotide level (annotation-level redundancy).
Sketch: Bottom-k MinHash with 128 hashes per cluster. Uses canonical k-mers (min of forward and reverse-complement hash) with FNV-1a hashing over upper-cased bases, so soft-masked sequence sketches like the rest. Multi-allele clusters are concatenated with N-spacers to prevent cross-allele k-mers.
Candidates: an index from hash value to the sketches holding it. Two clusters are candidates when their sketches share at least two values; a value held by more than 500 sketches (a repeat) is ignored. Positional bands over sorted bottom-k sketches, used before, rarely line up between a gene and a near copy or a fragment of it.
Containment: For candidate pairs, compute max Jaccard containment (max of containment(A,B) and containment(B,A)). This is more appropriate than Jaccard similarity when genes differ in length. Containment of A in B is estimated over A's hash values no larger than B's largest, which is as far as a full sketch of B can answer; counting the rest as misses put a gene's own first half at about 0.5.
Clustering: Union-Find with path compression and union-by-rank. Pairs above threshold (default 0.8) are merged.
Representative selection: Within each merged group, the cluster with highest prevalence becomes the representative. Others are removed from the output, and their presences are unioned into the representative. The union is taken over the corrected matrix, not the input one, so the representative can receive merged presence support that Recore itself rescued in a member cluster.
Written only when --synteny rejects at least one candidate. It uses the same schema as rescue_report.tsv, but these rows are audit records only and are not inserted into gene_presence_absence_recore.csv.
Written only when PARALOG candidates lack explicit synteny support. It uses the same schema as rescue_report.tsv, but these rows are audit records only and are not inserted into gene_presence_absence_recore.csv.
One row per accepted rescue plus rejected synteny or paralog candidate. This table makes the decision layer explicit: accepted/rejected status, orthology status, evidence state (STRICT_PRESENT, ALIAS_CLUSTER, SEQUENCE_ONLY, PARTIAL_HOMOLOGY, PSEUDOGENE, HOMOLOG_PARALOG, REJECTED_CONTEXT), matrix action, publication status, annotation-matched cluster, synteny state, quantitative synteny evidence, ORF integrity signal, confidence, coordinates, and rejection reason.
+
Before an accepted rescue enters the binary matrix, Recore also applies an adaptive orthology-quality guard. The guard raises the required sequence identity/coverage for rare or very short clusters, while allowing strong same-locus evidence such as passing synteny to support strict/alias calls. The guard writes adaptive_identity_floor, adaptive_coverage_floor, adaptive_sequence_pass, orthology_confidence_score, orthology_confidence_tier, and risk_flags. Accepted evidence that passes the selected --matrix-policy but fails this guard is kept as audit_only_quality.
Written when --reference-genome is provided. The reference can be an exact matrix column or an embedded ASxxxxx identifier. For every cluster, Recore reports whether the reference matrix already contains it, whether Recore rescued reference evidence, whether only rejected/paralog/partial evidence exists, or whether no evidence was found. The reference_absence_class column is deliberately simple:
+
new_to_reference: no reference evidence under the current thresholds.
reference_like_or_present: already present or rescued in the audited reference.
reference_like_ambiguous: partial, pseudogene, paralog, or rejected-context evidence; review before calling novel to the reference.
+
The reference audit also runs an exact reference-only shadow search and keeps a shadow copy of candidates before conflict resolution. This prevents partial/paralogous evidence from disappearing simply because it was correctly excluded from the binary matrix. Such rows are reported as flag_reference_partial_paralog_evidence with publication_confidence=complex_paralog: the exact cluster can remain absent, but the absence should not be described as a clean absence of the broader sequence family.
+
Accepted reference-like rescue evidence is still passed through the adaptive orthology-quality guard used by the corrected matrix. Weak sequence-only hits below the dataset-aware identity/coverage floors remain reference_like_ambiguous rather than being promoted to reference_like_or_present.
+
The reference audit also compares reference-absent cluster representatives against reference-present cluster representatives. Recore seeds candidate pairs with canonical 15-mers, globally aligns the best candidates, and classifies high-confidence matches as full-length same-gene sequence, short/long in-frame alleles, frameshift-like length changes, or partial reference homology. These events are written to reference_cluster_events.tsv and mirrored in reference_genome_audit.tsv as EVENT_DERIVED_REFERENCE_CLUSTER with publication_confidence=event_derived. They are deliberately audit-only: Recore keeps the cluster as a separate matrix column but prevents downstream tables from calling it a clean novel gene relative to the reference.
Written for every run. Recore combines the corrected matrix state, reference event audit, rescue classifications, synteny rejections, paralog rejections, cluster locus context, optional GFF mobile-element labels, and optional Newick tree evidence into one interpretation table.
+
When --tree is provided, Recore performs binary Fitch parsimony per cluster: tips are matched to matrix columns by exact name or embedded ASxxxxx ID, and gains/losses are counted by a top-down assignment consistent with the Fitch state sets. If the root state is ambiguous under Fitch, Recore now reports ancestral_state=ambiguous and avoids turning that arbitrary tie-break into an ancestral-loss claim. This yields phylogenetic patterns such as ancestral retention, ancestral loss, lineage-specific gain, recurrent gain/loss, homoplastic gain/loss, ambiguous gain/loss, and singleton/noise-like signals.
+
The molecular mechanism call is separate from the tree pattern. Recore gives priority to direct sequence/locus evidence: reference-derived alleles from reference_cluster_events.tsv, IS-supported interruptions from GFF labels, pseudogene and fragmentation evidence, paralog/duplication signals, context shifts, acquisition/insertion candidates, deletion/loss candidates, and unresolved accessory variation. These calls are audit-only and are intended for manual review and manuscript wording, not automatic binary correction.
+
The table also reports a phylogenetic_blink_flag, an evolutionary_signal_score, and a highlight_priority. These combine tree coherence, mechanism support, synteny, locus-context stability, ORF integrity, and cluster-component risk so highly dispersed or singleton-like clusters are easy to filter before manuscript highlighting.
+
Recore also reports a Dollo-like penalized model. It finds the smallest carrier MRCA, reports its size and carrier purity, and counts the minimum loss events inside that MRCA. A compact, high-purity carrier clade is treated as more plausible than a root-spanning pattern that would require many independent losses. Root ambiguity and repeated independent gains add dollo_violation_count and reduce penalized_phylo_score. This is deliberately an interpretation aid rather than a binary correction rule.
+
Mobile-element support has two tiers. If --mobile-element-reference is provided, Recore aligns that nucleotide FASTA of mobile elements, IS, or transposase loci against genomes with rescue/disruption evidence and reports nearby hits as mobile_element_source=sequence. This path is agnostic to GFF feature names. If no sequence database hit is available, Recore falls back to annotation labels (ID, aliases, product, Note) containing terms such as IS names, insertion sequence, transposase, phage, integrase, or recombinase. IS_interruption_supported is deliberately conservative: it requires a fragmented or disrupted rescue plus close mobile-element evidence within 250 bp.
Written when GFF annotations are loaded. Recore builds a two-gene upstream and two-gene downstream context window for each present gene ID that can be resolved in the GFF. The context table lists every observed cluster-side context; the summary table flags clusters as stable single-locus, mostly stable with minor context noise, possible multi-locus/paralog, or insufficient context. Synteny validation uses an adaptive 1-to-5-neighbour model, but this summary remains fixed-width so locus stability can be compared across clusters.
Written when GFF annotations are loaded. This audit-only table summarizes wider five-neighbour upstream/downstream blocks for every resolvable present cluster. Unique blocks are collapsed by ordered displayed gene/locus identity: internal cluster gaps, representative coordinates, and gene lengths do not create additional blocks. One representative locus and the full carrier-genome set are kept for each unique architecture. It is intended for reviewing translocations, tandem duplications, or alternative local contexts without changing the correction matrix. If a reference genome is known, Recore indexes every boundary between reference genes and chooses the boundary sharing the largest number of flanking clusters with each observed block. This provides a reference/H37Rv context row even for clusters absent from the reference, so users can inspect the neighbouring genes that define the likely insertion, deletion, disruption, or context-shift slot.
Written for every run. Recore summarizes each genome's FASTA/GFF availability, GFF contig remapping, searchable gaps, accepted rescues, matrix corrections, NSC-added presence, synteny rejections, and paralog rejections. The qc_flags column highlights genomes that may need assembly or annotation review before interpreting noisy presence/absence patterns. Successful GFF contig remapping is kept as an informational flag and does not by itself move a genome from pass to review.
Written for every run. Recore converts pairwise cluster relationships into connected components. Edges come from split-cluster evidence, annotation-match evidence, NSC merges, and paralog evidence. The component tables make it clear whether a cluster should be reviewed as a possible alias/redundant component or as part of a paralog family that should remain distinct by default. Component membership is mirrored in evolutionary_events.tsv.
Same shape as the corrected matrix, but genome cells are states rather than binary calls: PRESENT_INPUT, ABSENT, STRICT_PRESENT, ALIAS_CLUSTER, SEQUENCE_ONLY, PARTIAL_HOMOLOGY, PSEUDOGENE, HOMOLOG_PARALOG, or REJECTED_CONTEXT, and NOT_SEARCHED_LOW_PREVALENCE, NOT_SEARCHED_NO_CLUSTER_SEQUENCE or NOT_SEARCHED_NO_FASTA for a gap that was never searched, so it is not mistaken for one searched and found empty. This is the preferred output for plotting or reviewing ambiguous biology without forcing it into a clean 0/1 matrix.
Human-readable summary with gap counts, accepted rescues, matrix-corrected rescues under the selected policy, rescue type breakdown, core/soft-core genome changes, synteny stats, and NSC stats.
The multistate matrix, gene_presence_absence_recore_states.csv, has the same shape as the input presence/absence matrix but stores a biological state per cell instead of a 0 or a 1. It is the file to read for audit and for figures; the binary matrix is the file to feed downstream tools.
+
State
Meaning
Corrected in the binary matrix under balanced
PRESENT_INPUT
present in the original matrix
already present
ABSENT
searched, and no accepted evidence found
no
NOT_SEARCHED_LOW_PREVALENCE
never searched: the cluster is in fewer genomes than --min-prevalence
no
NOT_SEARCHED_NO_CLUSTER_SEQUENCE
never searched: the cluster has no sequence in the pan-genome reference
no
NOT_SEARCHED_NO_FASTA
never searched: the genome has no FASTA file
no
STRICT_PRESENT
same-locus evidence from annotation or synteny
yes
ALIAS_CLUSTER
the same biological locus assigned to another cluster
yes
SEQUENCE_ONLY
full-length sequence evidence, without stronger annotation context
yes
MISSING_CDS_UNVALIDATED
full-length sequence in a locus with no CDS annotation, and no synteny support
no
PARTIAL_HOMOLOGY
partial homologous sequence
no
PSEUDOGENE
disrupted gene evidence
no
HOMOLOG_PARALOG
a homologous or paralogous hit
no
REJECTED_CONTEXT
candidate rejected by context or paralog checks
no
+
The last five rows are the point of the tool. Under balanced, none of them becomes a presence call, and all of them stay visible with the reason attached.
ABSENT is a finding: Recore searched that gap and nothing cleared the thresholds. A gap outside the search is not, and says why instead. Under the default --min-prevalence of 0.1 that is every gap of a cluster in fewer than one genome in ten, which in most pangenomes is most of the empty cells. Before this distinction both read ABSENT.
+
When two reasons apply, the cluster's is written, in the order the pipeline filters gaps: prevalence, then a missing cluster sequence, then a missing FASTA. That is the order of the gap lines in summary.txt, which count the same cells except those NSC later filled or merged away.
The HTML viewer collapses these thirteen states into the six a matrix can actually be read in:
+
Cell
States
input
PRESENT_INPUT
corrected
the states corrected by the active policy
NSC
presence merged in from a nucleotide-redundant cluster
kept out
evidence the policy, a quality floor or a guard kept out of the matrix
absent
ABSENT and the other states that are not corrected
not searched, hatched
the three NOT_SEARCHED_* states
+
Switching the matrix to mechanism colouring instead shows the evolutionary call from evolutionary_events.tsv, which answers a different question: not "is it there?" but "what kind of event is this?".
See Matrix policies for the full table. In short: strict corrects only STRICT_PRESENT and ALIAS_CLUSTER; balanced adds SEQUENCE_ONLY; sensitive also admits partial and pseudogene evidence that clears the adaptive guard.
evolutionary_events.tsv is an interpretation layer, not a new correction policy. Its job is to answer a different question from the corrected matrix:
+
The corrected matrix asks: is there enough evidence to mark this cluster as present in this genome?
The evolutionary audit asks: what biological or technical process best explains the cluster-level pattern after correction?
+
The two answers are deliberately kept separate. A cluster can remain a real matrix cluster while the recommended wording says "reference-gene derivative", "paralog/duplication signal", "fragmented homology", or "review before claiming novel acquisition".
corrected cluster state after accepted rescues and optional NSC support;
reference-genome audit and reference-derived cluster events;
local locus-context stability and synteny/paralog rejection evidence;
optional tree parsimony, including explicit ambiguous-root reporting;
optional sequence-based mobile-element support;
connected components of related clusters, so paralogous, redundant, split, or annotation-linked clusters are interpreted together.
+
Tree tips are matched to matrix columns by exact name or embedded ASxxxxx identifier. If only a subset of tips matches, evolutionary counts and patterns are calculated on the matched subset and the run summary/log warns about the incomplete match.
+
The tree is used for topology-based interpretation. Recore first scores the corrected presence/absence pattern on the matched tree using binary parsimony and a Dollo-like single-origin model. This produces fields such as phylogenetic_pattern, phylogenetic_blink_flag, carrier_mrca_purity, dollo_origin_purity, and penalized_phylo_score. Branch lengths are preserved for the HTML viewer, but the mechanism call itself is conservative and is driven primarily by evidence type, locus context, and topology.
The mechanism call is a deterministic evidence ladder. Direct molecular/locus evidence has priority over a visually attractive tree pattern, because a clean clade can still be caused by a reference allele, paralog collapse, gene fragmentation, or annotation boundaries.
+
Reference-derived event check. If reference_cluster_events.tsv links the cluster to a gene in the audited reference, Recore first decides whether the cluster is a full-length/same-gene allele, a short/long in-frame allele, a frameshift/truncation/extension, or partial reference homology. If the same signal is also paralogous or multi-locus, it is treated as duplication_or_paralog_expansion.
Mobile-element disruption check. If a disrupted or fragmented rescue has nearby mobile-element evidence within the conservative window, Recore calls IS_interruption_supported. With --mobile-element-reference, this is sequence-based and name-agnostic; without it, Recore falls back to annotation labels such as insertion sequence, transposase, integrase, phage, or recombinase terms.
ORF integrity check. Pseudogene evidence is called pseudogenization; fragmented or split homology is called gene_fragmentation_or_structural_break.
Paralog and locus-context check. Explicit paralog evidence, unstable locus context, or multi-locus behaviour becomes duplication_or_paralog_expansion.
Synteny rejection check. Strong sequence similarity in an incompatible local context becomes translocation_or_context_shift_candidate, because it may represent a real sequence at the wrong locus, a translocation, or a paralog/annotation problem.
Annotation/redundancy check. Split genes, start-boundary shifts, or NSC representative/member relationships become annotation_or_clustering_artifact.
Reference-absent clade check. Only after the previous traps are ruled out, a cluster absent from the reference and concentrated in a lineage/clade can become clean_insertion_or_acquisition_candidate.
Reference loss check. If the reference is absent but the inferred root state is present, Recore prefers reference_or_lineage_loss_candidate over "gain everywhere else".
Tree loss/homoplasy/retention check. Ancestral or recurrent losses become deletion_loss_candidate; highly recurrent gain/loss patterns become unstable_or_homoplastic_accessory_signal; conserved ancestral presence becomes ancestral_retained_no_disruption.
Fallback. If the available matrix, reference, synteny, mobile-element, and tree evidence do not resolve the process, Recore reports unresolved_accessory_variation.
The corrected cluster is broadly present and the tree pattern supports ancestral retention.
Conserved ancestral presence in the analysed set; not a novel acquisition.
clean_insertion_or_acquisition_candidate
The cluster is absent from the audited reference, present in a coherent lineage/clade, and lacks stronger reference/paralog/fragmentation/artifact explanations.
Best candidate for reference-absent accessory acquisition or insertion; validate flanks before claiming a novel locus.
reference_or_lineage_loss_candidate
The reference is absent, but the inferred root/background state is present.
More consistent with loss in the reference or reference lineage than independent gain in all other genomes.
deletion_loss_candidate
Tree pattern suggests ancestral presence with one or more losses.
Candidate deletion/loss event; inspect flanking genes, breakpoints, and mobile elements.
IS_interruption_supported
Fragmented/disrupted evidence is close to mobile-element evidence.
Candidate IS/transposase/mobile-element-mediated interruption; not automatically intact gene presence.
pseudogenization
Rescue evidence indicates frameshift, internal stop, or pseudogene-like disruption.
Homologous sequence exists, but should be described as disrupted unless additional evidence proves function.
gene_fragmentation_or_structural_break
Evidence is split, fragmented, or alignment-split.
Partial homology or local structural break; review before using as gain/loss evidence.
duplication_or_paralog_expansion
Paralog evidence, multi-locus context, or copy-number-like behaviour is detected.
Copy-number/paralog/cluster-definition complexity; avoid treating as one clean locus without manual locus review.
reference_gene_allelic_derivative
The cluster is best explained as same-gene, short in-frame, or long in-frame reference-derived sequence.
Event-derived allele/version of a reference gene; not a completely new gene.
reference_gene_disruption_or_truncation
Reference-like sequence has frameshift, truncation, or extension evidence.
Reference-gene-derived disrupted/truncated form; interpret with ORF integrity.
partial_reference_homology
The representative sequence partially matches a reference gene.
Some homology to reference exists; do not call "absent from reference" as if it were wholly novel.
translocation_or_context_shift_candidate
Sequence evidence exists but synteny/local context is incompatible.
Real sequence may be in a different context, translocated, paralogous, or annotation-driven; review locus manually.
annotation_or_clustering_artifact
Split/start-boundary evidence, NSC redundancy, or cluster-component evidence explains the signal.
Likely cluster definition, boundary, or redundancy issue rather than a biological gene gain/loss.
unstable_or_homoplastic_accessory_signal
The tree shows repeated appearances/disappearances or high blinking.
Could be real mobility/recombination, but also annotation/assembly/clustering noise; deprioritize for headline claims.
unresolved_accessory_variation
Evidence is insufficient or conflicting after all checks.
Keep visible for audit; do not use as a strong mechanistic claim without external validation.
Use phylogenetic_pattern to describe where the signal sits on the tree. Use molecular_mechanism to describe what process is the safest interpretation. Use recommended_wording for manuscript text, because it is written to avoid overclaiming.
+
For manuscript highlighting, the safest candidates are usually clusters with:
+
clean_insertion_or_acquisition_candidate, reference_or_lineage_loss_candidate, or deletion_loss_candidate;
coherent phylogenetic_pattern with low/moderate blink flag;
high carrier_mrca_purity or dollo_origin_purity;
no paralog/component warning;
compatible synteny and clear ORF integrity;
absent_not_searched of 0, so every absence the call rests on was searched (see below).
+
Clusters with duplication_or_paralog_expansion, annotation_or_clustering_artifact, translocation_or_context_shift_candidate, or unstable_or_homoplastic_accessory_signal can still be biologically interesting, but they should be presented as review/complex-locus signals until the locus is inspected manually. Clusters with reference_gene_* or partial_reference_homology should not be described as completely novel reference-absent genes, even if the original pangenome matrix marked them as absent from the reference.
Gains and losses are made of absences: a gain needs the gene missing in the rest of the tree, a loss needs it missing where it was lost. A gap outside the search (a cluster below --min-prevalence, a cluster with no sequence to search with, or a genome with no FASTA) keeps the input matrix's call, so a call built on such absences is only as good as that call. Under the default prevalence floor that is every gain on a cluster in fewer than one genome in ten.
+
absent_not_searched counts those absences and not_searched_reason says why (low_prevalence, no_cluster_sequence or no_fasta). For the calls that rest on absences (clean_insertion_or_acquisition_candidate, reference_or_lineage_loss_candidate, deletion_loss_candidate, unstable_or_homoplastic_accessory_signal and unresolved_accessory_variation):
+
when none of the absences behind the call were searched (for a reference loss, when the reference genome's was not), support_level drops one level, mechanism_detail gains absences_not_searched, and recommended_wording says so. The lower support lowers evolutionary_signal_score, and with it highlight_priority;
when only some were not searched, mechanism_detail gains absences_not_searched=N/M and the wording says how many, but the searched absences still carry the call.
+
Calls made from rescue evidence (pseudogenes, fragments, paralogs, reference events) do not rest on absences and are left as they are.
Every field named on this page is a column of evolutionary_events.tsv; its schema is in Output files. The report colours the matrix by molecular_mechanism when you ask it to, and its mechanism cards double as column filters: see Run report.
Recore is an audit and correction layer, not an oracle.
+
It does not claim that every homologous sequence is a functional gene.
It does not turn pseudogenes or partial homologues into clean binary presence under the recommended balanced policy.
It does not prove ancestry or biological novelty by itself. Evolutionary labels are audit interpretations, to be read alongside reference evidence, phylogeny, synteny and external validation.
It does not replace manual review for complex paralogous families, repetitive elements, or collapsed assemblies.
It cannot recover evidence that is absent from the assembly sequence.
+
This is exactly why every run writes both a corrected binary matrix and audit-grade evidence tables. The binary matrix is what a downstream tool consumes; the evidence tables are what a reviewer reads.
With --tree, Fitch parsimony root states that come out ambiguous are reported as ambiguous rather than being forced into a single ancestral story. Carrier-MRCA purity and Dollo-like origin and loss metrics are there to separate a compact clade-associated event from a blinking pattern that happens to have the same count.
+
If tree tips do not cover every matrix genome, the unmatched genomes are ignored and Recore says so explicitly.
The two are not alternatives. Panaroo refinding happens while the graph is being built; Recore runs afterwards on whatever matrix you ended up with, from any pipeline that writes a Panaroo-style matrix.
Recore includes a synthetic benchmark that creates known false absences, true absences, and partial pseudogenes, then computes precision/recall/F1 from the actual corrected matrices.
+
+
Current synthetic benchmark:
+
Policy
TP
FP
FN
Precision
Recall
F1
strict
0
0
5
NA
0.000
0.000
balanced
5
0
0
1.000
1.000
1.000
sensitive
5
0
0
1.000
1.000
1.000
+
Interpretation:
+
strict is conservative in annotation-free settings.
balanced recovers full-length false absences without converting partial pseudogenes into binary presence.
sensitive allows partial/disrupted evidence at the policy layer, but the adaptive quality guard still blocks weak pseudogene-like evidence in this benchmark.
No policy converts a homologous/paralogous trap into binary presence unless the evidence has already been promoted to same-locus support by synteny.
The biological event benchmark adds a small GFF/synteny truth set. It checks that a same-locus missing CDS is rescued, an identical paralog in the wrong context is rejected, and the evolutionary audit table contains quantitative synteny, ORF, phylogenetic-reliability, and paralog-risk columns.
+
python3 benchmarks/biological_event_benchmark.py
+
The synthetic benchmark validates policy behavior on matrix ground truth; the biological benchmark validates the locus-context guardrails. Neither is a species-specific biological truth benchmark.
Measured with benchmarks/search_scaling.py on an Apple M4 Pro (14 cores, 24 GB) with 8 threads and minimap2 2.31: synthetic MTBC-sized genomes (one 4.9 Mb contig and 4,000 genes each), 80 matrix gaps per genome of which 40 are false absences, GFFs and --synteny. The GFFs give each CDS an ID and a locus tag; the last two columns are the same runs with PGAP-style GFFs (--rich-gff), six names per CDS plus a product and a note.
+
Genomes
Gaps searched
Run time
Warm --search-cache
Peak memory
PGAP-style GFFs: run time
PGAP-style GFFs: peak memory
100
8,000
2.4 s
1.3 s
0.24 GB
2.2 s
0.32 GB
200
16,000
4.7 s
2.8 s
0.30 GB
4.2 s
0.43 GB
400
32,000
10.7 s
6.3 s
0.38 GB
8.8 s
0.64 GB
800
64,000
21.4 s
13.5 s
0.56 GB
20.7 s
1.05 GB
+
Every planted false absence was recovered, except one at 100 genomes that synteny rejected because the random deletions had changed its neighbourhood.
+
Memory grows by about 0.5 MB per genome, or 1 MB with PGAP-style GFFs, nearly all of it the annotation synteny reads for every genome: 5,000 genomes like these would need about 2.5 GB, or 5.5 GB with PGAP-style GFFs. Each name is stored once per genome and the gene-to-cluster lookup keeps one cluster per name; when every name was a String in several maps, memory grew by 3.4 MB per genome (6.2 MB with PGAP-style GFFs). Time grows with the genome count, about 0.027 s per genome, and the minimap2 search is about 9 of the 21 seconds at 800 genomes. The rest is synteny, and the audit tables in cluster_synteny_blocks.tsv, whose size follows the number of distinct gene neighbourhoods. These synthetic genomes are a worst case for that: every gap reshuffles the context of ten neighbours, and at 800 genomes the table reached 2.3 GB. Assemblies with conserved synteny write far less, and the table is written a bounded chunk of clusters at a time, so its size does not show in the memory.
+
--search-cache removes the search from a rerun (21 seconds to 13.5 at 800 genomes), so it pays off most where the search is the expensive part: many gaps per genome (--min-prevalence 0), --multi-allele, or larger genomes. The cache directory can be deleted at any time; the next run simply searches again.
Three Python utilities ship in benchmarks/ for checking a run rather than trusting it:
+
self_benchmark.py: Masks known-present cells, reruns Recore, and estimates dataset-specific masked-positive recall plus score calibration bins. This measures the tool on your data rather than on a synthetic set.
compare_runs.py: Compares two output directories and labels every matrix call unchanged, new, lost or changed. Use it when changing a policy or a threshold.
recore_report.py: Builds a compact Markdown or HTML report for an output directory that was generated without --write-html-report.
+
The synthetic benchmark itself, which creates known false absences, true absences and partial pseudogenes and then computes precision, recall and F1 from the actual corrected matrices, is benchmarks/synthetic_benchmark.py.
What it means: The gene is physically present in the genome but is absent from the matrix, typically because graph cleaning dropped it or because annotation transfer never placed it. Panaroo's graph-based cleaning can over-aggressively trim genes that are divergent or connected to fragmented paths in the de Bruijn graph.
+
Evidence: Full-coverage hit (≥80%) with no overlapping GFF gene assigned to a different cluster.
What it means: The gene sequence is present in the assembly, but the supplied GFF has no substantial CDS feature at that locus. This catches annotation omissions rather than Panaroo-only matrix cleaning errors.
+
Evidence: Full-coverage hit (≥80%) with ≥95% identity, GFF annotations loaded, and no CDS overlapping more than half of both the hit span and the annotated CDS. Tiny boundary overlaps with adjacent genes are allowed.
Matrix behavior: A MISSING_CDS candidate is corrected only when --synteny gives PASS, which upgrades it to STRICT_PRESENT. Without synteny support it remains MISSING_CDS_UNVALIDATED audit evidence and is not inserted into the binary corrected matrix.
What it means: The gene locus is present and annotated, but Panaroo assigned it to a different cluster. This typically happens when a gene has subtle sequence variation that caused it to cluster separately from the canonical reference.
+
Evidence: Full-coverage hit (≥80%) with a GFF gene at that locus that belongs to a different Panaroo cluster, with ≥95% nucleotide identity.
+
Confidence:identity × coverage × 1.1 (slight boost: evidence is strong)
What it means: The gene is broken into multiple biological pieces in this genome. Common causes are assembly breaks (contig boundaries), IS element insertions, or large indels that split the reading frame.
+
Evidence: A co-linear chain of ≥2 hits from the same query allele, target contig, and strand whose merged coverage reaches ≥80% of the reference, with a large query or target gap (≥50 bp). Non-co-linear chains are not rescued as FRAGMENTED, and chains contained inside one compatible CDS are classified through the single-CDS annotation logic instead.
+
A gene split across two contigs is FRAGMENTED too: two pieces, in order along the gene, each reaching its contig's end on the side where the gene carries on. The contigs may be assembled in either orientation. Such a rescue reports the larger piece's coordinates, and its fragment_note reads split_across_contigs:<other contig>.
+
A gene across the origin of a circular contig is not fragmented, and is not reported as such. A circular chromosome or plasmid is assembled as a line cut somewhere, and a gene over the cut comes back as two pieces on the same contig and strand: the first runs off one end and the second carries on from the other. Recore joins them into one gene and classifies it like a single full-length hit at the larger piece's locus (usually MISSING_CDS, since annotators tend to miss these genes), with the coverage and identity of both pieces. The rescue reports the larger piece's coordinates, and its fragment_note reads wraps_origin:<start>-<end>, the span of the other piece.
What it means: The gene is reconstructed by multiple minimap2 alignments, but the evidence does not support a biological gene break. This is usually an alignment or representation split rather than a functional disruption.
+
Evidence: A co-linear multi-hit chain reaches the coverage threshold, but the query and target gaps are small (<50 bp), no frameshift-like gap is detected, and the chain is not better explained as a single-CDS annotation match.
What it means: The gene is present with the correct sequence but starts at a different position than the reference annotation. Alternative start codons (GTG, TTG) and upstream ATGs can lead to start site disagreements between annotators.
+
Evidence: A hit covering ≥90% of the query with ≥98% identity, overlapping an annotated CDS on the same strand that agrees with the cluster's representative at the stop (within one codon) and differs from it at the start by 1 to 299 bp. The representative's start and stop are placed on the contig from the alignment, counting the query bases it left unaligned at each end, so trimmed alignment ends are not mistaken for a different start. A difference at the stop end is not a start variant: that is what a representative which reads through its stop looks like.
+
Confidence:identity × 0.95 (the only rescue type whose score does not multiply in coverage)
+57bp means this genome's annotated CDS starts 57 bp upstream of where the cluster's representative starts; a negative value means it starts downstream.
What it means: The hit is a related gene from a different family (a paralog), not the missing gene itself. Common in high-GC organisms with large multi-copy gene families (PE/PPE in M. tuberculosis, IS elements).
+
Evidence: Full-coverage hit with a GFF gene at that locus belonging to a different cluster, with <95% identity.
+
Confidence:identity × coverage × 0.7 (reduced: this is a cross-match, not a true rescue)
+
Matrix behavior: PARALOG candidates are audit-only unless synteny explicitly passes. Without synteny=PASS, they are written to paralog_rejected_report.tsv and are not inserted into gene_presence_absence_recore.csv.
Note for MTBC analyses: A large fraction of PARALOGs in tuberculosis data come from the PE/PPE superfamily, which comprises ~170 genes with high nucleotide similarity. These are not rescued genes; they are correctly absent.
What it means: The gene sequence is present with good coverage but contains one or more premature stop codons (TAA, TAG, TGA) in the reading frame. This is a functional pseudogene.
+
Evidence: Full-coverage hit reclassified after scanning the genome FASTA sequence. When the rescue sits on an annotated CDS, that CDS is read in its own frame from its own start to its own stop, so a premature stop cannot hide in another frame and a representative that reads through its stop does not turn this genome's canonical stop into an internal one. Without an annotated CDS, the aligned region is reverse-complemented if on the minus strand and scanned in all three frames, and the frame with the fewest stops is reported.
What it means: The gene is fragmented in a pattern consistent with a frameshift mutation: fragments have high identity but are separated by small gaps whose size is not divisible by 3.
+
Evidence: ≥2 fragments with average identity ≥95%, gaps between fragments ≤50bp, and at least one gap whose size mod 3 ≠ 0.
What it means: The gene is truncated: only 60-80% of the reference sequence is present with high identity. This is consistent with a partial deletion of one end of the gene.
+
Evidence: Hit with ≥90% identity but coverage 60-80% (below --min-coverage, above 60%).
type_modifier = see table above (1.0 for DROPPED, down to 0.4 for PartialDeletion)
synteny_modifier = 1.2 if PASS, 1.0 if not assessed. Explicit FAIL candidates are penalized for audit and rejected from the corrected matrix when --synteny is enabled.
+
A value close to 1.0 indicates a high-confidence rescue. Values below ~0.5 should be interpreted with caution.