// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® export module Crafter.Review:Freeze; import std; import :Json; // `crafter-review freeze`: cut the frozen commit-range artifact for the push // gates — identity, AI trailers by the consumer's rule, a clean tree, no // merges, a fast-forward — then `git format-patch` it, hash it, and // optionally check out a detached worktree the lenses read; with --verify, // prove at push time that the repository still is what was reviewed. // // freeze.json records gitVersion, the git that exported the patches: // format-patch ends every patch with its version, so a fresh export under // another git hashes differently, and --verify says so when that is why. export namespace Crafter::Review::Freeze { // .review-gates/trees: inside the consumer's checkout, gitignored, so no // --add-dir is needed for the lenses to read a worktree there. constexpr std::string_view Worktrees = ".review-gates/trees"; constexpr std::string_view PushDir = ".review-gates/push"; // `git --version` without its "git version " prefix; "" when git does not // answer. std::string GitVersion(); struct Commit { std::string sha; std::string author; std::string committer; std::string sig; std::string subject; std::string body; }; // base..HEAD, oldest first. Throws Dyn::Error (RuntimeError) with git's // message. std::vector CommitsIn(const std::filesystem::path& repo, std::string_view base); // The tracked changes that count, and the ones the config allows // (allowDirty): `XY path` lines of git status --porcelain. std::pair, std::vector> DirtyPaths(const std::filesystem::path& repo, std::span allowDirty = {}); struct Base { std::string sha; std::optional upstream; std::string how; std::string failure; }; // An explicit --base wins; with rangeFrom 'base' the merge-base of HEAD and // the configured branch (the whole pull request); else the branch's // @{upstream} (the push delta), else that merge-base (a new branch that // tracks nothing yet). Base ResolveBase(const std::filesystem::path& repo, std::string_view base, const std::optional& configBase, std::string_view rangeFrom); struct Options { std::string base; bool replace = false; bool worktree = false; bool requireSigned = false; // The emails author and committer must be among; none: not checked. std::optional> authors; std::string aiTrailers = "forbid"; std::vector allowDirty; std::optional configBase; std::string rangeFrom = "upstream"; std::string date; std::filesystem::path cwd; }; // The report: ok, repo, out, date, failures, warnings, then base, baseFrom, // upstream, head, authors, aiTrailers, commits, files, hash, worktree as // they come. Nothing is exported while a check fails; every check is // listed, not just the first. Json Run(const std::filesystem::path& repo, const std::filesystem::path& out, const Options& options); // Push-time check: HEAD is the frozen head, the tree is clean (the allowed // paths aside), a fresh export hashes to the frozen hash, and the artifact // dir still does too. The problems; none: it matches. std::vector Verify(const std::filesystem::path& repo, const std::filesystem::path& out, std::span allowDirty = {}); // The exact `run` command for a freeze report: built from its fields, never // retyped (2026-09-29). std::string RunLine(const Json& report, std::string_view outRel, std::string_view trees); // The tree the lenses read for a frozen artifact, as its record says: the // worktree (relative to the directory freeze ran from), else the // repository when it is not `cwd`, else `.`. std::string TreeOf(const Json& record, const std::filesystem::path& cwd); // What an artifact's freeze.json says it reviews: the repository, absolute // (`cwd` when the artifact has no record, or its record names none), and // the tree (TreeOf). A record that is not JSON throws. struct Reviewed { std::filesystem::path repo; std::string tree; }; Reviewed ReviewedBy(const std::filesystem::path& artifact, const std::filesystem::path& cwd); struct Arguments { std::optional repo; std::optional slug; std::optional out; std::optional base; bool replace = false; bool worktree = false; bool requireSigned = false; std::string date; bool verify = false; }; // `crafter-review freeze ...` with the consumer's config loaded: 0, 1 for // a --verify MISMATCH, 3 for a failed check. std::int32_t Main(const Arguments& arguments, const Json& config); }