Crafter.Review/interfaces/Crafter.Review-Run.cppm
Jorijn van der Graaf 339f4b9b40
feat(modes): the kernel modes - a patch series and a mailing-list reply
Beside the push gates, crafter-review runs the pre-send review of a kernel
contributor's workbench: --mode series over a format-patch export with its
cover letter, --mode reply over a mail draft. The inventory partitions a
cover letter, a plain mail and the camera review's shape; ten kernel lenses
join the table with their gates (structure, code, artifact truth) and the
persona gate - one maintainer-persona instance per --reviewers entry, its
whole pass gating its blocks and pruning the units that are not theirs. A
series' code units are keyed on the code like the push gates'; every text
unit on its text, the references it cites and the anchor when it is dated.
The references an artifact cites are prefetched through utilities/lore.py
into the gate-state, with a digest in the run layer, and a persona's
calibration map is its lore listing. The kernel schema carries the persona
vocabulary, the kernel note kinds, the predicted review and the not-mine
list; the matrix the persona holes, the thin and the modelled units; gates
clean without a persona pass is GATES CLEAN. A reply's gate-state sits
beside the draft and is read once; a kernel mode needs --subsystem,
defaults the effort to max and the trees to work/linux,work/milos-linux,
and excuses no unit from the holes.

The kernel lenses and the common text ship verbatim; the table lists them
after the push gates' lenses and the config template holds them off, so a
repository that never turns them on changes nothing. Prompts, keys and plans
were checked byte for byte against the pipeline these modes replace, on its
own artifacts: two replies and a series, every system prompt, user prompt,
job and reuse key identical.
2026-10-31 22:56:12 +01:00

317 lines
16 KiB
C++

// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
export module Crafter.Review:Run;
import std;
import :Json;
import :Plan;
import :Pool;
import :SeatPool;
import :Memory;
import :PgWire;
import :State;
import :Platform;
import :Refs;
// Run: the driver for a commit range about to be pushed
// (process/prepush.md), and for the kernel modes - a mailing-list reply or
// a patch series about to be sent. One claude process per inventory unit
// per lens, whole passes where a lens has one (each gating its block
// passes), fail-closed coverage per unit, the matrix, per-unit reuse across
// hashes, a seat pool.
//
// Gates (commits): 1 repo-better-solution (approach) -> 2 repo-functional
// (the range exercised in the running application) -> 3 repo-bug-prover
// and repo-security -> 4 the owner. SEND-READY = push-ready. The kernel
// modes run their own gates (Plan::Gates), then the persona gate for every
// reviewer given; gates clean without a persona pass is GATES CLEAN, not
// SEND-READY. Exit codes: 0 SEND-READY, 1 FIX FIRST, 2 INCOMPLETE / PARTIAL
// / GATES CLEAN / CONFIRMING (gates clean, code units still owe clean reads
// - run the same line again), 3 preflight.
//
// A Run is built empty and filled field by field, as the tests fill one, so
// its attributes are public; Main builds and runs the real one.
export namespace Crafter::Review::Run {
using Log = std::function<void(const std::string&)>;
// `crafter-review run`'s options: the dump records them under `args`, in
// this order.
struct Arguments {
std::optional<std::string> config;
std::string mode = "commits";
// The journal subsystem: required in the kernel modes (its ledger,
// captures and threads are the lenses' evidence); optional in
// commits mode.
std::optional<std::string> subsystem;
std::string artifact;
std::optional<std::string> hash;
// The reviewers a persona reads as, comma-separated (the kernel
// modes).
std::string reviewers;
std::optional<std::string> dispositions;
std::string base;
std::string date;
std::optional<std::string> trees;
std::string contextPaths;
std::optional<std::string> only;
std::optional<std::string> without;
// --units, split on commas.
std::optional<std::set<std::string>> units;
bool warm = false;
// A cap on the passes in flight on this machine, every driver's
// counted; none by default (2026-10-31, Jorijn: "there is no need to
// do a global cap anymore, the cap should be 128 per available
// account"): each account's own cap is the limit.
std::optional<std::int64_t> concurrency;
std::string effort;
std::int64_t reads = Plan::Reads;
bool noReuse = false;
bool noSalvage = false;
// pruned: a persona's block passes skip the units its whole pass
// marked not-mine; all: every unit.
std::string personaBlocks = "pruned";
bool dryRun = false;
std::optional<std::string> claudeBin;
std::string models;
std::optional<std::string> scratch;
std::optional<std::string> seats;
std::optional<std::string> envFile;
bool noProbe = false;
std::optional<double> apiBudget;
std::string harness = "claude";
std::optional<std::string> variant;
std::optional<std::string> opencodeBin;
};
// vars(a) without `cmd`, the units sorted.
Json ArgsJson(const Arguments& a);
// die(): "preflight: <message>" on stderr, exit 3.
class Preflight : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
// (ok, detail): does the application under test answer at <url><path>?
// Below 500 is ok.
std::pair<bool, std::string> AppReady(std::string_view url, std::string_view path = "/", double timeout = 10);
// The machine's memory as SlotsPlan reads it (the tests give their own).
struct Machine {
std::function<std::optional<std::int64_t>()> available;
std::function<std::int64_t()> reserve;
};
// `slots`: how many stacks to bring up - as many as free memory holds,
// and no more than the range's browser passes can use at once - and the
// commands for the ones not up yet. (want, commands).
std::pair<std::int64_t, std::vector<std::string>> SlotsPlan(const Json& cfg, const std::filesystem::path& repo, const std::optional<std::filesystem::path>& artifact = std::nullopt,
const std::optional<std::filesystem::path>& scratchRoot = std::nullopt, const Log& log = {}, const Machine& machine = {});
// ({version: path} of the migrations at the range HEAD, the range's added
// or changed migrations, {path: fingerprint at HEAD} of those).
using MigrationsHead = std::tuple<std::map<std::string, std::string>, std::vector<std::string>, std::map<std::string, std::string>>;
using QueryResult = std::pair<std::vector<std::string>, std::vector<std::vector<std::optional<std::string>>>>;
class Run {
public:
Run();
Run(Arguments a, Json cfg);
~Run();
Run(const Run&) = delete;
Run& operator=(const Run&) = delete;
// The whole run: the gates, the dump, the summary; the exit code.
std::int32_t Execute();
// A signal or `crafter-review stop`: every running pass killed, the
// pool aborted, and the stop stays in force for the gates after.
void Stop(const std::string& why, std::int32_t signal = 0);
// The file `crafter-review stop` writes, once the scratch dir is
// known.
std::optional<std::filesystem::path> StopFile() const;
// The handler a signal reaches: false before gate 0 set it up (the
// signal then ends the process).
bool Ready() const;
// ---- gate 0 and its preflights ---------------------------------------
void Gate0();
std::map<std::string, std::string> AppLenses(std::span<const std::string> lensFiles) const;
std::map<std::string, std::string> DbLenses(std::span<const std::string> lensFiles) const;
void InstrumentPreflight(std::span<const std::string> lensFiles);
static std::optional<std::string> MigrationVersion(std::string_view file);
MigrationsHead MigrationsAtHead();
// (problem, note): the migrations at the range HEAD against the ones
// the database applied.
std::pair<std::optional<std::string>, std::optional<std::string>> MigrationsDrift(const std::string& url);
std::unique_ptr<Pool::Stacks> LoadStacks();
std::string StacksLine() const;
std::vector<Json> DbStacks(std::span<const std::string> lensFiles) const;
void StackPreflight(std::span<const std::string> lensFiles);
std::optional<std::string> LoginMissing(const Json& stack) const;
std::optional<std::string> TreeDrift();
// ---- prompts ---------------------------------------------------------------
// (appended system prompt file, stdin user message) of a pass.
std::pair<std::filesystem::path, std::string> PromptFor(const Json& job);
std::optional<std::string> CacheKey(Json& job);
bool WarmProbe(const Json& job) const;
// ---- reuse -----------------------------------------------------------------
const Plan::CommitCode& RangeCode();
std::optional<Json> ReuseWhole(const std::string& name, Json& job);
std::optional<std::string> ReuseKey(const Json& job, const std::optional<std::string>& environment = std::nullopt);
std::vector<std::string> OtherKeys(const Json& job, const std::optional<std::string>& key);
std::optional<std::pair<Json, Json>> Salvage(const Json& job);
Json DirEnvironments(const std::filesystem::path& dir);
static std::int64_t DirRound(const std::filesystem::path& dir);
std::int64_t ReadsNeeded(const Json& job) const;
// ---- one process, a gate -------------------------------------------------
std::pair<Json, Json> ExecJob(const Json& job, std::int64_t attempt);
Json PartFrom(const Json& job, const Json& rec, Json raw);
static Json DryPart(const Json& job);
std::vector<Json> RunGate(std::string_view title, std::vector<std::string> lensFiles);
// (lens file, persona) per lens instance: a persona's instance reads
// as that reviewer.
std::vector<Json> RunGate(std::string_view title, std::vector<std::pair<std::string, std::string>> specs);
// The kernel modes (reply, series).
bool Kernel() const;
std::unique_ptr<Pool::Admission> Admission(std::span<const std::string> lensFiles);
std::optional<Json> Resumable(const std::string& name, const std::string& lensFile, std::string_view title);
void Checkpoint(std::string_view title);
std::vector<Json> PrepareBlocks(const std::string& name, std::vector<Json> blocks, std::map<std::string, std::vector<Json>>& parts);
// ---- the end ---------------------------------------------------------------
std::int32_t Finish(std::string overall, const Json& stoppedAt, Json incomplete);
Json BuildDump(std::string_view overall, const Json& stoppedAt, const Json& incomplete);
Json DryReport();
bool Storable(const Json& rec) const;
Json StoreState();
void Message(const std::string& message);
// ---- the run's state -----------------------------------------------------
Arguments a;
Json cfg = Json::object();
std::vector<std::string> off;
// {lens file: environment}, in the config's lens order: the dump, the
// resume file and the scratch dir's marker write it in that order.
Json environments = Json::object();
// The working directory: the config, the env file and the slots are
// its, the lens processes run in it, and every path logged is relative
// to it.
std::filesystem::path repo;
// The repository under review, from the artifact's freeze record (the
// working directory when it has none): the prompt names it, and the
// calibration is kept under it.
std::filesystem::path reviewed;
std::vector<std::string> logs;
double t0 = 0;
std::optional<std::filesystem::path> logFile;
std::vector<Json> records;
std::vector<Json> results;
std::map<std::string, std::int64_t> lensLayersBytes;
std::vector<Json> jobsRun;
std::map<std::string, std::string> summaries;
// The kernel modes: the reviewers with a persona, their calibration
// maps, the references the artifact cites and their digest, the
// anchor unit's text, and whether the persona gate passed.
std::vector<std::string> reviewers;
std::map<std::string, std::string> corpora;
Refs::Table refUrls;
Json refIndex = Json::object();
std::string refDigest;
std::string anchorText;
bool personaRan = false;
std::optional<std::string> abort;
Pool::Scheduler* sched = nullptr;
std::optional<Plan::CommitCode> commitCode;
std::int64_t x = 1;
std::int64_t round = 1;
bool newRound = false;
Json owed = Json::object();
std::map<std::string, Json> wholeParts;
std::optional<std::string> dbChecked;
std::optional<bool> dbBypass;
std::optional<std::string> dbRole;
std::unique_ptr<PgWire::Broker> broker;
std::unique_ptr<Pool::Stacks> stacks;
bool slotMode = false;
std::vector<std::string> slotProblems;
std::unique_ptr<SeatPool::Board> board;
std::unique_ptr<State::RunLock> runLock;
std::unique_ptr<Platform::LifetimeLock> lifetime;
std::vector<std::string> trees;
std::vector<std::string> contextPaths;
std::string hash;
std::string h8;
std::string head;
std::filesystem::path gateDir;
Json units = Json::array();
std::vector<std::string> lines;
bool testMode = false;
std::filesystem::path scratch;
std::filesystem::path dumpPath;
std::vector<std::string> notReviewed;
std::vector<Json> dispEntries;
std::vector<Json> dispRows;
std::vector<Json> inForce;
std::filesystem::path statePath;
Json state = State::EmptyState();
std::vector<std::filesystem::path> salvageDirs;
std::filesystem::path resumePath;
Json resume;
std::map<std::string, std::string> charters;
std::map<std::string, std::string> charterSha;
std::string systemText;
// {sha: path}, in the order first written.
std::vector<std::pair<std::string, std::filesystem::path>> systemPaths;
// {lens: the shas of its system files}, lenses in the order first met.
std::vector<std::pair<std::string, std::set<std::string>>> systemByLens;
std::string runLayer;
std::optional<std::string> loginFixture;
std::string appBlock;
std::map<std::string, std::string> schemas;
std::unique_ptr<Memory::Calibration> calibration;
std::unique_ptr<Memory::Sampler> sampler;
std::unique_ptr<Pool::Runner> runner;
Json apiCfg = Json::object();
std::unique_ptr<SeatPool::Ledger> ledger;
std::vector<Json> seats;
std::map<std::filesystem::path, Json> envMarkers;
std::set<std::pair<std::string, std::string>> envSkips;
std::map<std::pair<std::string, std::string>, MigrationsHead> migCache;
// The log every line goes to (the tests put their own here).
Log log;
// ---- the seams a test replaces ---------------------------------------------
// Each starts as the real one; the methods call through these.
std::function<std::pair<bool, std::string>(std::string_view url, std::string_view path)> appReady;
std::function<std::pair<bool, std::string>(std::string_view url)> probe;
std::function<QueryResult(std::string_view url, std::string_view sql)> query;
std::function<std::optional<bool>(std::string_view url)> bypassesRls;
std::function<MigrationsHead()> migrationsAtHead;
std::function<std::pair<std::optional<std::string>, std::optional<std::string>>(const std::string& url)> migrationsDrift;
std::function<std::optional<std::string>()> treeDrift;
std::function<std::pair<std::filesystem::path, std::string>(const Json& job)> promptFor;
std::function<std::pair<Json, Json>(const Json& job, const std::string& prompt, const std::filesystem::path& systemPath, std::string_view schema, std::int64_t maxTurns, std::int64_t timeoutMin,
bool persist)>
runPass;
std::function<std::optional<Json>(const std::string& name, const std::string& lensFile, std::string_view title)> resumable;
std::function<void(std::span<const std::string> lensFiles)> instrumentPreflight;
private:
// What exec_job's threads share with the pool's: the system files,
// the lens layers, the jobs run.
mutable std::recursive_mutex shared;
mutable std::mutex logLock;
std::optional<std::string> stopped;
std::atomic<bool> ready = false;
std::optional<std::filesystem::path> stopFile;
};
// The `run` command: argv parsed by the CLI; the exit code.
std::int32_t Main(Arguments a, const Json& cfg);
// What SIGINT, SIGTERM and SIGHUP do (Process::InstallSignalThread):
// stop the run once gate 0 is through; before that, and for every other
// command, the process ends, its descendants with it.
void OnSignal(std::int32_t signal);
}