// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® export module Crafter.Review:Inventory; import std; import :Json; // The partition of a frozen artifact into review units for the coverage // contract: a commit range (a `git format-patch` export, the commits mode), // a kernel patch series (the same export with a cover letter, the series // mode) or a mailing-list reply (the reply mode). It is MECHANICAL: no agent // decides what the units are. Every non-blank line of the artifact belongs // to exactly one unit, and the inventory fails when that does not hold. // Fixed slots give what is NOT in the text a row of the coverage matrix too. // // Reply mode units (ids): // nl:intro the author's own paragraphs before the first per-file header // nl: the paragraphs under the header naming that patch file // anchor the dated anchor line(s) opening the block // RN reversal paragraphs ("R2. ...") // :N numbered item N of section (camss, ov13b10, dts, x) // :Na lettered sub-item a of item N // x:N bullet N of the cross-series section // refs the reference list // whole:* fixed slots (WholeReply) // A plain on-list mail (no "Dates: every" anchor) is partitioned // generically instead (PartitionMail): // hdr the header block (From/To/Cc/Subject/In-Reply-To/References) // quote:N the N-th quoted block, with its "On ... wrote:" line // para:N the N-th paragraph of our own text // trailers a paragraph made only of "Tag-Name: value" lines // Series mode units: // cover:header the cover letter's header, cover:N its paragraph N // pK:... as below, every hunk whole (no documentation split) // whole:* fixed slots (WholeSeries) // Commits mode units (ids): // pK:subject commit K's subject // pK:msg commit K's message body (before the trailers), when not empty // pK:trailers commit K's trailer block, when not empty // pK:diffstat commit K's diffstat // pK:#hdr the diff header of in commit K (mode, rename, blobs) // pK:#H hunk H of in commit K; a hunk of a documentation file // that holds more than one heading-delimited part is cut into // #H.1, #H.2, ... (DocParts) // pK:outside fixed slot: the in-repository users and callers of what // commit K changes, outside its diff // pK:absent fixed slot: what a reviewer expects in commit K and does not // find // whole:* fixed slots (WholeCommits) export namespace Crafter::Review::Inventory { struct WholeSlot { std::string_view id; std::string_view description; }; constexpr std::array WholeReply = {{ {"whole:route", "the decision or route the mail recommends, and whether its consequences are carried through every item that depends on it"}, {"whole:asks", "the total set of actions the mail asks of the reader — is it legible as one list, free of contradictions between items"}, {"whole:absent", "what this reviewer would expect the mail to say and it does not — a precedent, a test, a dependency, a recipient, a question left open"}, }}; constexpr std::array WholeSeries = {{ {"whole:order", "patch ordering, bisectability, declared dependencies and base"}, {"whole:cover-vs-patches", "the cover's claims against what the patches actually do, including testing statements"}, {"whole:recipients", "To/Cc against get_maintainer and the people who reviewed the same construct recently"}, {"whole:absent", "what this reviewer would expect the series to contain and it does not"}, }}; constexpr std::array WholeCommits = {{ {"whole:order", "commit ordering and bisectability — each commit a self-contained step that leaves the repository consistent (no later commit in the range repairs an earlier one, nothing is used before the commit that adds it), the declared base"}, {"whole:absent", "what a reviewer of this repository expects the range to contain and it does not — a test, a version/changelog bump, a doc or packaging update, a migration for existing installs, a message for a body-less commit"}, {"whole:behaviour", "the user-visible behaviour of the range, exercised end to end in the running application — every behaviour the code and the messages promise, as a user sees it; the flows the range touches and the adjacent flows that share what it changes; what a user expects the range to deliver and does not get"}, }}; // [a, b) is one hunk of a Markdown file, lines[a] its @@ line: its // partition at the headings on the new side of the diff, outside fenced // code blocks, once the part before holds anything but headings. std::vector> DocParts(std::span lines, std::size_t a, std::size_t b); // The first non-blank line, stripped and cut to 90 code points. std::string Excerpt(std::span lines); struct Series { // Every patch's lines, one after the other: what unit lines number. std::vector lines; // Units ({id, lines, excerpt}), the whole slots not yet among them. Json units; }; // A format-patch export, a cover letter (0000-) as cover:header and // cover:N paragraphs; splitDocs cuts a documentation hunk at its // headings (the commits mode only); kernel words the per-patch slots // as the series mode does (a patch in a tree, not a commit in a // repository). Series PartitionSeries(const std::filesystem::path& dir, bool splitDocs = true, bool kernel = false); // A plain on-list mail: the header block, each quoted block with its // attribution line, each paragraph of our own text, a trailer paragraph. Series PartitionMail(std::string_view text); // The camera review's shape when the text has its dated anchor // ("Dates: every"), else PartitionMail. Series PartitionReply(std::string_view text); // A partition that broke the invariant. class InvariantError : public std::runtime_error { public: using std::runtime_error::runtime_error; }; struct Built { // What `inventory commits ` prints: {mode, artifact, hash8, // units, invariant, count}. Json out; std::vector lines; }; // `artifact` is kept as it is spelled: the inventory records it so. mode: // reply (a draft file), series or commits (a dir of *.patch). Built Build(std::string_view mode, std::string_view artifact); // `crafter-review inventory reply|series|commits `: the partition // as indented JSON on stdout. Exit 1 on a broken invariant or a bad // command line. std::int32_t Main(std::span args); }