// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include #include #include // One populated result object, rendered more than once. // // The report used to be printed as the writer walked the results, which fixed the order of the file // to the order of the pipeline and made a summary at the top impossible - the warnings that decide // the verdict are produced by the sections the verdict has to sit above. BuildReportDocument() // therefore populates this document, and a renderer walks it afterwards: the text renderer in // ResultReport.cpp reads ONLY from here and prints nothing on its own. A second rendering is a // second walk over the same document, never a second hand-maintained emitter. // The TYPE of a pathology, from a closed vocabulary a consumer can switch on, carried beside the // open free-text description that says what actually happened to this crystal. One code per warning // PRODUCER: a code appears in PATHOLOGY_FLAGS if and only if its warning fired, so lighting an // indicator is one grep and needs no prose parsing. Add a producer, add a code here. namespace PathologyCode { constexpr const char *NO_LATTICE = "NO_LATTICE"; // nothing indexed constexpr const char *INDEXING_AMBIGUITY = "INDEXING_AMBIGUITY"; // alternative indexing unresolved constexpr const char *SYMMETRY_AMBIGUITY = "SYMMETRY_AMBIGUITY"; // the data do not pick a point group constexpr const char *CENTERING_UNTESTED = "CENTERING_UNTESTED"; // centring from the metric only constexpr const char *UNUSABLE_MERGE = "UNUSABLE_MERGE"; // the merged data carry no signal constexpr const char *LOW_COMPLETENESS = "LOW_COMPLETENESS"; // too little of reciprocal space constexpr const char *SWEEP_GAPS = "SWEEP_GAPS"; // ranges that delivered much less constexpr const char *GONIO_SCALE = "GONIO_SCALE"; // the stage rotation is mis-scaled constexpr const char *SPINDLE_CAP = "SPINDLE_CAP"; // the mounting cost unique data constexpr const char *ANISOTROPY = "ANISOTROPY"; // strongly direction-dependent constexpr const char *TWINNING = "TWINNING"; // twinning indicated constexpr const char *MODEL_HAND = "MODEL_HAND"; // data and model in opposite hands constexpr const char *MODEL_NOT_VALIDATED = "MODEL_NOT_VALIDATED"; // a model was given, not usable constexpr const char *REFERENCE_MISMATCH = "REFERENCE_MISMATCH"; // the reference MTZ does not match; free flags not inherited constexpr const char *CANCELLED = "CANCELLED"; // the run did not finish constexpr const char *RESOLUTION_FIT = "RESOLUTION_FIT"; // the CC1/2 fall-off is not a fall-off constexpr const char *FLIGHT_PATH = "FLIGHT_PATH"; // the assumed flight-path medium matters here constexpr const char *PSEUDO_TRANSLATION = "PSEUDO_TRANSLATION"; // translational pseudo-symmetry indicated // Both warnings under this code say the same thing to a consumer - a translation the cell in use // does not declare is present in the data, so the centring or the cell is wrong - and differ only // in how exact it is. ANISOTROPY and RESOLUTION_FIT are already shared this way. constexpr const char *LATTICE_TRANSLATION = "LATTICE_TRANSLATION"; // a translation the cell does not declare constexpr const char *HARMONIC_CONTAMINATION = "HARMONIC_CONTAMINATION"; // the beam carries a higher harmonic constexpr const char *SUPERCELL_POSSIBLE = "SUPERCELL_POSSIBLE"; // Bragg-like intensity at half-integer positions constexpr const char *MULTIPLE_LATTICES = "MULTIPLE_LATTICES"; // a second lattice carries much of the spot intensity constexpr const char *GEOMETRY_NOT_CONVERGED = "GEOMETRY_NOT_CONVERGED"; // the geometry walk ran out of rounds constexpr const char *SCALING_NOT_CONVERGED = "SCALING_NOT_CONVERGED"; // the per-frame scales hit their cap still moving constexpr const char *POWDER_RINGS = "POWDER_RINGS"; // non-ice powder rings hold many spots constexpr const char *ICE_RINGS = "ICE_RINGS"; // hexagonal-ice rings hold many spots constexpr const char *BEAM_CENTRE_OFF = "BEAM_CENTRE_OFF"; // the file's beam centre is wrong for this geometry constexpr const char *OVERLOADS = "OVERLOADS"; // saturated pixels took reflections constexpr const char *OVERLAPS = "OVERLAPS"; // neighbouring spots share signal pixels } // A closed type plus an open description. The type is what machinery switches on; the text is what a // person reads, and it is free-form on purpose - the vocabulary must not have to grow a code every // time a sentence gains a number. struct ReportWarning { std::string code; std::string text; // Advisories about the pipeline's own internals rather than about this crystal. They still fire, // still count in the developer report, and are kept out of the default one - a warning nobody can // act on is how users learn to skip the warnings. bool developer_only = false; }; // One line of the SUMMARY's pathology checklist, for a rendering that lights an indicator per // pathology rather than reading the summary's prose. PRESENT exactly when one of `codes` is in // PATHOLOGY_FLAGS; ABSENT when it was looked for and did not fire; UNDETERMINED when it could not be // measured on this run. `text` is the summary row's own words, `detail` the warnings that fired. struct ReportCheck { enum class Status { Present, Absent, Undetermined }; std::string label; std::vector codes; Status status = Status::Undetermined; std::string text; std::string detail; }; // A value with its type kept, so a rendering other than text does not have to parse the string back. // `text` is always the exact spelling the text report prints, because that spelling is the interface. struct ReportValue { enum class Type { Text, Integer, Real, Boolean, Enumerated }; Type type = Type::Text; std::string text; double real = 0.0; int64_t integer = 0; bool boolean = false; std::vector domain; // Enumerated only: the closed vocabulary this value comes from }; // Content that is a table in every rendering. The pre-formatted text lines and the typed cells carry // the same content: the text renderer prints the former, another rendering reads the latter. struct ReportTable { std::vector columns; std::string text_header; std::vector text_rows; std::vector> cells; }; struct ReportEntry { enum class Kind { Key, Prose, Blank, Table }; Kind kind = Kind::Key; std::string key; // Key ReportValue value; // Key std::string prose; // Prose: a paragraph, already line-wrapped for the text rendering ReportTable table; // Table // Written only when the developer report was asked for. The document always holds everything; // which rendering shows what is the renderer's decision, so nothing is lost by building once. bool developer_only = false; // The other half of the same idea: a short paragraph that exists only because the long one it // summarises was moved out. Printing both would say the same thing twice. bool default_only = false; }; struct ReportSection { std::string title; // empty for the header block, which has no banner of its own std::vector entries; bool developer_only = false; }; // The whole report as data. Sections in the order they are written; warnings and the verdict live // here rather than in a section, because the SUMMARY section is composed from them after every other // section has been built. struct ReportDocument { std::vector sections; std::vector warnings; std::string verdict; // closed: OK | WARNINGS | UNUSABLE | FAILED std::string verdict_text; // one to three sentences, free text std::vector pathology_flags; // PathologyCode::*, deduplicated, in first-seen order std::vector checks; // the SUMMARY's pathologies, in the order it lists them // The SUMMARY's one-line facts as (label, value), in the order it lists them - what the CLI shows // at the end of a run, in the report's own words. std::vector> summary_rows; };