`crafter-review sql` hands the SQL to the driver's broker named in REVIEW_GATES_DB; the broker runs it as one transaction, rolled back, and streams the lines back with the exit code last. The client speaks protocol 3.0 over Crafter.Network's TCP client: cleartext, MD5 and SCRAM-SHA-256, one Parse per statement, the transaction status as the backstop. The splitter, the refusals, the table layout and every message work on code points; connect and socket errors read "[Errno 111] Connection refused" or "timed out". `crafter-review totp` prints "123456 (valid 17s)". No sleep after the last connect attempt; TOTP reads the clock once; a broker token that is no string gets the NO DATABASE answer and the exit mark. The goldens add the splitter, the comment stripping, the fingerprint, the refusals, the first words, the headline, the cell, table, error and notice layouts, the run's refusals and limits and 40 TOTP codes (2774 checks now). tests/PgWire runs the helper over a Postgres stand-in and compares its output with the goldens, line for line, plus the broker's protocol. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
269 lines
13 KiB
C++
269 lines
13 KiB
C++
// SPDX-License-Identifier: AGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
module Crafter.Review:Driver_impl;
|
|
import :Driver;
|
|
import :Core;
|
|
import :Text;
|
|
import :Platform;
|
|
import :Process;
|
|
import :Inventory;
|
|
import :Config;
|
|
import :Freeze;
|
|
import :PgWire;
|
|
import :Totp;
|
|
import std;
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace Crafter::Review {
|
|
namespace {
|
|
constexpr std::string_view Usage =
|
|
R"(usage: crafter-review [--config FILE] {run,freeze,slots,config} ...
|
|
crafter-review {seats,inventory,sql,totp,init,doc,browser,stop} ...
|
|
crafter-review --version
|
|
|
|
The pre-push review pipeline: adversarial lenses, one claude process per
|
|
inventory unit, against a frozen commit range.)";
|
|
|
|
std::int32_t PrintVersion() {
|
|
std::println("{}", DriverLine());
|
|
try {
|
|
std::println("home {}", Home().string());
|
|
for (const auto& [name, sha] : CharterHashes()) std::println("charter {} {}", name, sha);
|
|
} catch (const std::exception& e) {
|
|
std::println("home not found: {}", e.what());
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// A usage error: exit 3, the preflight's code, for every command that
|
|
// cannot start.
|
|
std::int32_t UsageError(std::string_view usage, std::string_view prog, std::string_view message) {
|
|
std::println(std::cerr, "{}\n{}: error: {}", usage, prog, message);
|
|
return 3;
|
|
}
|
|
|
|
// die(): the preflight's message and exit 3.
|
|
std::int32_t Die(std::string_view message) {
|
|
std::println(std::cerr, "preflight: {}", message);
|
|
return 3;
|
|
}
|
|
|
|
// ---- the option parser these commands use ---------------------------------
|
|
struct OptionSpec {
|
|
std::string_view name;
|
|
// A value follows (`--x V` or `--x=V`); else a store_true flag.
|
|
bool takesValue;
|
|
std::string_view metavar;
|
|
std::string_view help;
|
|
};
|
|
|
|
struct Parsed {
|
|
std::map<std::string, std::string, std::less<>> values;
|
|
std::set<std::string, std::less<>> flags;
|
|
bool help = false;
|
|
std::string error;
|
|
|
|
std::optional<std::string> Value(std::string_view name) const {
|
|
const auto found = values.find(name);
|
|
if (found == values.end()) return std::nullopt;
|
|
return found->second;
|
|
}
|
|
bool Flag(std::string_view name) const {
|
|
return flags.contains(name);
|
|
}
|
|
};
|
|
|
|
std::string UsageLine(std::string_view prog, std::span<const OptionSpec> specs) {
|
|
std::string line = std::format("usage: {} [-h]", prog);
|
|
for (const OptionSpec& spec : specs) {
|
|
line += spec.takesValue ? std::format(" [{} {}]", spec.name, spec.metavar) : std::format(" [{}]", spec.name);
|
|
}
|
|
return line;
|
|
}
|
|
|
|
std::string Help(std::string_view prog, std::string_view description, std::span<const OptionSpec> specs) {
|
|
std::string text = std::format("{}\n\n{}\n\noptions:\n -h, --help show this help message and exit\n", UsageLine(prog, specs), description);
|
|
for (const OptionSpec& spec : specs) {
|
|
const std::string left = spec.takesValue ? std::format("{} {}", spec.name, spec.metavar) : std::string(spec.name);
|
|
text += left.size() < 20 ? std::format(" {:<20} {}\n", left, spec.help) : std::format(" {}\n {}\n", left, spec.help);
|
|
}
|
|
return text;
|
|
}
|
|
|
|
// `--opt value`, `--opt=value`, any unique prefix of a long option, the
|
|
// last of a repeated option, every unrecognized argument named at the
|
|
// end.
|
|
Parsed ParseOptions(std::span<const std::string_view> args, std::span<const OptionSpec> specs) {
|
|
Parsed parsed;
|
|
std::vector<std::string> unrecognized;
|
|
for (std::size_t i = 0; i < args.size(); ++i) {
|
|
const std::string_view arg = args[i];
|
|
if (arg == "-h") {
|
|
parsed.help = true;
|
|
continue;
|
|
}
|
|
if (!arg.starts_with("--") || arg == "--") {
|
|
unrecognized.emplace_back(arg);
|
|
continue;
|
|
}
|
|
const std::size_t equals = arg.find('=');
|
|
const std::string_view name = arg.substr(0, equals);
|
|
if (name.size() > 2 && std::string_view("--help").starts_with(name) && std::ranges::none_of(specs, [&](const OptionSpec& s) { return s.name.starts_with(name); })) {
|
|
parsed.help = true;
|
|
continue;
|
|
}
|
|
std::vector<const OptionSpec*> matches;
|
|
for (const OptionSpec& spec : specs) {
|
|
if (spec.name == name) {
|
|
matches = {&spec};
|
|
break;
|
|
}
|
|
if (spec.name.starts_with(name)) matches.push_back(&spec);
|
|
}
|
|
if (matches.empty()) {
|
|
unrecognized.emplace_back(arg);
|
|
continue;
|
|
}
|
|
if (matches.size() > 1) {
|
|
std::vector<std::string> names;
|
|
for (const OptionSpec* spec : matches) names.emplace_back(spec->name);
|
|
parsed.error = std::format("ambiguous option: {} could match {}", name, Dyn::Join(names, ", "));
|
|
return parsed;
|
|
}
|
|
const OptionSpec& spec = *matches.front();
|
|
const std::string key(spec.name.substr(2));
|
|
if (!spec.takesValue) {
|
|
if (equals != std::string_view::npos) {
|
|
parsed.error = std::format("argument {}: ignored explicit argument {}", spec.name, Dyn::Repr(arg.substr(equals + 1)));
|
|
return parsed;
|
|
}
|
|
parsed.flags.insert(key);
|
|
continue;
|
|
}
|
|
if (equals != std::string_view::npos) {
|
|
parsed.values[key] = std::string(arg.substr(equals + 1));
|
|
} else if (i + 1 < args.size() && !(args[i + 1].starts_with('-') && args[i + 1].size() > 1)) {
|
|
parsed.values[key] = std::string(args[++i]);
|
|
} else {
|
|
parsed.error = std::format("argument {}: expected one argument", spec.name);
|
|
return parsed;
|
|
}
|
|
}
|
|
if (!unrecognized.empty()) parsed.error = std::format("unrecognized arguments: {}", Dyn::Join(unrecognized, " "));
|
|
return parsed;
|
|
}
|
|
|
|
// The consumer config, loaded before every command but `config`:
|
|
// warnings on stderr, a problem stops it.
|
|
std::optional<Json> LoadConfig(const std::optional<std::string>& configPath) {
|
|
Config::Loaded loaded = Config::Load(fs::current_path(), configPath ? fs::path(*configPath) : fs::path());
|
|
for (const Json& warning : Dyn::Or(Dyn::Get(loaded.config, "_warnings"), Dyn::EmptyList())) std::println(std::cerr, "config: warning: {}", warning.get<std::string>());
|
|
if (!loaded.problems.empty()) {
|
|
Die(std::format("config: {}", Dyn::Join(loaded.problems, "; ")));
|
|
return std::nullopt;
|
|
}
|
|
return std::move(loaded.config);
|
|
}
|
|
|
|
constexpr std::array<OptionSpec, 9> FreezeOptions = {{
|
|
{"--repo", true, "REPO", "the repository (toplevel of a checkout; default: the working directory)"},
|
|
{"--slug", true, "SLUG", "the artifact dir name: .review-gates/push/<slug>/"},
|
|
{"--out", true, "OUT", "the artifact dir (overrides --slug)"},
|
|
{"--base", true, "BASE", "the range base; default @{upstream}, else merge-base with the configured base branch"},
|
|
{"--replace", false, "", "re-freeze into an out dir that already holds patches (gate-state/ is kept) and supersede this repo's worktrees"},
|
|
{"--worktree", false, "", "check out a detached worktree of HEAD under .review-gates/trees/ for the lenses (printed as --trees)"},
|
|
{"--require-signed", false, "", "an unsigned commit fails instead of warning"},
|
|
{"--date", true, "DATE", "the run date (default today)"},
|
|
{"--verify", false, "", "push-time check: HEAD, a clean tree and a fresh export still match freeze.json"},
|
|
}};
|
|
|
|
std::int32_t FreezeCommand(std::span<const std::string_view> args, const std::optional<std::string>& configPath) {
|
|
constexpr std::string_view Prog = "crafter-review freeze";
|
|
const Parsed parsed = ParseOptions(args, FreezeOptions);
|
|
if (parsed.help) {
|
|
std::print("{}", Help(Prog, "cut the frozen commit-range artifact for the push gates (process/prepush.md Gate 0)", FreezeOptions));
|
|
return 0;
|
|
}
|
|
if (!parsed.error.empty()) return UsageError(UsageLine(Prog, FreezeOptions), Prog, parsed.error);
|
|
const std::optional<Json> config = LoadConfig(configPath);
|
|
if (!config) return 3;
|
|
Freeze::Arguments arguments;
|
|
arguments.repo = parsed.Value("repo");
|
|
arguments.slug = parsed.Value("slug");
|
|
arguments.out = parsed.Value("out");
|
|
arguments.base = parsed.Value("base");
|
|
arguments.replace = parsed.Flag("replace");
|
|
arguments.worktree = parsed.Flag("worktree");
|
|
arguments.requireSigned = parsed.Flag("require-signed");
|
|
arguments.date = parsed.Value("date").value_or(Platform::TodayIso());
|
|
arguments.verify = parsed.Flag("verify");
|
|
// `%G?` starts gpg, which can leave a daemon behind: as a subreaper
|
|
// this process inherits it, and takes it down on the way out.
|
|
Process::BecomeSubreaper();
|
|
const std::int32_t code = Freeze::Main(arguments, *config);
|
|
Process::KillDescendants();
|
|
return code;
|
|
}
|
|
}
|
|
|
|
std::int32_t Main(std::span<const std::string_view> args) {
|
|
const std::span<const std::string_view> rest = args.empty() ? args : args.subspan(1);
|
|
// The supervisor first and alone: it is single-threaded and has to
|
|
// stay so, and it runs between the driver and claude, not as a
|
|
// command anyone types.
|
|
if (!rest.empty() && rest.front() == Process::SuperviseVerb) return Process::SuperviseMain(rest.subspan(1));
|
|
Platform::Init();
|
|
// The options before the command: --config FILE or --config=FILE, and
|
|
// any unique prefix of a long option.
|
|
std::optional<std::string> configPath;
|
|
std::size_t at = 0;
|
|
while (at < rest.size() && rest[at].starts_with("--") && rest[at] != "--") {
|
|
const std::string_view arg = rest[at];
|
|
const std::size_t equals = arg.find('=');
|
|
const std::string_view name = arg.substr(0, equals);
|
|
if (name.size() > 2 && std::string_view("--config").starts_with(name)) {
|
|
if (equals != std::string_view::npos) {
|
|
configPath = std::string(arg.substr(equals + 1));
|
|
} else if (at + 1 < rest.size()) {
|
|
configPath = std::string(rest[++at]);
|
|
} else {
|
|
return UsageError(Usage, "crafter-review", "argument --config: expected one argument");
|
|
}
|
|
} else if (name.size() > 2 && std::string_view("--version").starts_with(name)) {
|
|
return PrintVersion();
|
|
} else if (name.size() > 2 && std::string_view("--help").starts_with(name)) {
|
|
std::println("{}", Usage);
|
|
return 0;
|
|
} else {
|
|
return UsageError(Usage, "crafter-review", std::format("unrecognized arguments: {}", arg));
|
|
}
|
|
++at;
|
|
}
|
|
if (at < rest.size() && rest[at] == "-h") {
|
|
std::println("{}", Usage);
|
|
return 0;
|
|
}
|
|
if (at >= rest.size()) return UsageError(Usage, "crafter-review", "a command is required");
|
|
const std::string_view command = rest[at];
|
|
const std::span<const std::string_view> after = rest.subspan(at + 1);
|
|
try {
|
|
// The helpers a lens runs: they read neither the home nor the
|
|
// config, only their arguments, stdin and REVIEW_GATES_DB.
|
|
if (command == "sql") return PgWire::SqlMain(after);
|
|
if (command == "totp") return Totp::Main(after);
|
|
if (command == "inventory" && !configPath) return Inventory::Main(after);
|
|
if (command == "config") {
|
|
if (!after.empty()) return UsageError(Usage, "crafter-review", std::format("unrecognized arguments: {}", after.front()));
|
|
return Config::Main(fs::current_path(), configPath ? fs::path(*configPath) : fs::path());
|
|
}
|
|
if (command == "freeze") return FreezeCommand(after, configPath);
|
|
} catch (const Dyn::Error& error) {
|
|
// An error no command catches: its message, exit 1.
|
|
std::println(std::cerr, "crafter-review: {}: {}", error.Kind(), error.what());
|
|
return 1;
|
|
}
|
|
return UsageError(Usage, "crafter-review", std::format("argument cmd: invalid choice: '{}'", command));
|
|
}
|
|
}
|