Crafter.Review/interfaces/Crafter.Review-Run.cppm
Jorijn van der Graaf dd0929b705
test(run): the store rules, stacks and memory, and the review fixes, check by check
StoreRules (23 checks), StacksMemory (24) and ReviewFixes (19), each a
test of its own.

Run and SlotsPlan now have seams that start as the real ones: appReady,
probe, query, bypassesRls, migrationsAtHead, migrationsDrift, treeDrift,
promptFor, runPass, resumable, instrumentPreflight, and a Machine for
SlotsPlan. MigrationsAtHead is split out of MigrationsDrift for it; the
HEAD side is still worked out once per tree and base.

The Postgres stand-in and the raw broker call move to tests/Common, shared
by PgWire and ReviewFixes. ReviewFixes runs this binary as `sql`, and a
real server that resets every connection makes the startup message either
fail to go out or never get its answer - a DbError either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 09:39:53 +01:00

276 lines
14 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;
// Run: the driver for a commit range about to be pushed
// (process/prepush.md). 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: 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. Exit codes: 0
// SEND-READY, 1 FIX FIRST, 2 INCOMPLETE / PARTIAL / 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";
std::string artifact;
std::optional<std::string> hash;
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;
std::int64_t concurrency = 128;
std::string effort;
std::int64_t reads = Plan::Reads;
bool noReuse = false;
bool noSalvage = false;
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);
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();
std::filesystem::path repo;
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;
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);
}