Crafter.Review/implementations/Crafter.Review-State.cpp
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

290 lines
16 KiB
C++

// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include <cerrno>
#include <fcntl.h>
#include <sys/file.h>
#include <unistd.h>
module Crafter.Review:State_impl;
import :State;
import :Json;
import :Text;
import :Platform;
import std;
namespace fs = std::filesystem;
namespace Crafter::Review::State {
namespace {
std::string Formatted(double value, std::int32_t precision) {
return std::format("{:.{}f}", value, precision);
}
double AsDouble(const Json& value) {
if (value.is_number_float()) return value.get<double>();
if (value.is_boolean()) return value.get<bool>() ? 1.0 : 0.0;
if (value.is_number_unsigned()) return static_cast<double>(value.get<std::uint64_t>());
if (value.is_number_integer()) return static_cast<double>(value.get<std::int64_t>());
throw Dyn::Error("TypeError", std::format("unsupported operand type(s) for /: '{}' and 'float'", Dyn::TypeName(value)));
}
// `value or 0` for a number that is summed.
Json OrZero(const Json& value) {
return Dyn::Truthy(value) ? value : Json(0);
}
// The key's value, or the fallback only when the key is absent.
const Json& GetOr(const Json& dict, std::string_view key, const Json& fallback) {
return Dyn::Has(dict, key) ? Dyn::At(dict, key) : fallback;
}
std::size_t Len(const Json& value) {
return value.is_string() ? Dyn::Length(value.get_ref<const std::string&>()) : value.size();
}
// A string, or Error (TypeError): only a string is concatenated.
const std::string& Text(const Json& value) {
if (!value.is_string()) throw Dyn::Error("TypeError", std::format("can only concatenate str (not \"{}\") to str", Dyn::TypeName(value)));
return value.get_ref<const std::string&>();
}
// f'{value:<width}' of a str.
std::string Padded(const Json& value, std::size_t width) {
if (!value.is_string()) throw Dyn::Error("TypeError", std::format("unsupported format string passed to {}.__format__", Dyn::TypeName(value)));
return Dyn::LJust(value.get_ref<const std::string&>(), width);
}
}
Json EmptyState() {
return {{"version", 1}, {"entries", Json::object()}, {"personaPass", nullptr}};
}
Json LoadState(const fs::path& path) {
if (!path.empty() && fs::exists(path)) {
try {
return Json::parse(Dyn::ReadText(path));
} catch (const std::exception&) {
// A damaged file is the empty state.
}
}
return EmptyState();
}
Json MergeState(const Json& disk, const Json& sets, std::span<const std::string> drops, const Json& read, const Json& persona) {
const bool ok = disk.is_object() && Dyn::Get(disk, "entries").is_object();
Json state = {
{"version", 1},
{"entries", ok ? Dyn::At(disk, "entries") : Json::object()},
{"personaPass", ok ? Dyn::Get(disk, "personaPass") : Json()},
};
Json& entries = state["entries"];
for (const std::string& key : drops) entries.erase(key);
for (auto it = sets.begin(); it != sets.end(); ++it) {
if (std::ranges::contains(drops, it.key())) continue;
Json entry = it.value();
if (!entry.is_object()) throw Dyn::Error("TypeError", std::format("'{}' object is not iterable", Dyn::TypeName(entry)));
if (!read.is_null() && !Dyn::Equal(Dyn::Get(entry, "kind"), Json("whole"))) {
// One clean read per round: a second store of the same run, or
// the relaunch of an unfinished run, adds nothing.
const Json& previous = Dyn::Get(entries, it.key());
Json reads = Json::array();
if (Dyn::Truthy(previous)) {
const Json& stored = Dyn::Get(previous, "reads");
if (Dyn::Truthy(stored)) {
reads = stored;
} else {
const Json& hash = Dyn::Get(previous, "hash");
const std::string head = Dyn::Truthy(hash) ? Dyn::Slice(Text(hash), 0, 8) : std::string();
reads.push_back({{"hash", head}, {"round", 0}, {"run", Dyn::Get(previous, "run")}});
}
}
const bool counted = std::ranges::any_of(reads, [&](const Json& x) {
return Dyn::Equal(Dyn::Get(x, "hash"), Dyn::At(read, "hash")) && Dyn::Equal(Dyn::Get(x, "round"), Dyn::At(read, "round"));
});
if (!counted) reads.push_back(read);
entry["reads"] = std::move(reads);
}
entries[it.key()] = std::move(entry);
}
if (!persona.is_null()) state["personaPass"] = persona;
return state;
}
void SaveWhole(const fs::path& path, const Json& state) {
const fs::path temp = std::format("{}.tmp", path.string());
{
std::ofstream out(temp, std::ios::binary | std::ios::trunc);
if (!out) throw std::runtime_error(std::format("{}: cannot write", temp.string()));
out << DumpFile(state);
}
fs::rename(temp, path);
}
Json SaveState(const fs::path& path, const Json& sets, std::span<const std::string> drops, const Json& read, const Json& persona) {
// Without the lock the write is still made; a failed write throws.
const Platform::FileLock lock(std::format("{}.lock", path.string()));
Json merged = MergeState(LoadState(path), sets, drops, read, persona);
Platform::WriteAtomic(path, DumpFile(merged), 0666);
return merged;
}
fs::path LockPath(const fs::path& gateDir, std::string_view mode, std::string_view hash8) {
return gateDir / std::format(".run-{}-{}.lock", mode, hash8);
}
Json ReadLockInfo(const fs::path& path) {
try {
const std::string text = Dyn::ReadText(path);
const Json info = Dyn::Strip(text).empty() ? Json::object() : Json::parse(text);
return info.is_object() ? info : Json::object();
} catch (const std::exception&) {
return Json::object();
}
}
std::string Describe(const Json& info) {
if (!Dyn::Truthy(Dyn::Get(info, "pid"))) return "holder not identified yet (it has just started)";
const Json unknown = "?";
return std::format("pid {} since {}, run {}, scratch {}", Dyn::Str(Dyn::At(info, "pid")), Dyn::Str(GetOr(info, "since", unknown)),
Dyn::Str(GetOr(info, "run_n", unknown)), Dyn::Str(GetOr(info, "scratch", unknown)));
}
RunLock::RunLock(const fs::path& gateDir, std::string_view mode, std::string_view hash8) {
const fs::path path = LockPath(gateDir, mode, hash8);
fd = open(path.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0644);
if (fd < 0) throw std::runtime_error(std::format("{}: {}", Dyn::ErrnoText(errno), Dyn::Repr(std::string_view(path.string()))));
if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
const Json info = ReadLockInfo(path);
close(fd);
fd = -1;
throw RunLocked(Describe(info));
}
}
RunLock::~RunLock() {
if (fd >= 0) close(fd);
}
void RunLock::WriteInfo(const Json& info) {
const std::string text = DumpSpaced(info);
if (lseek(fd, 0, SEEK_SET) < 0 || ftruncate(fd, 0) != 0) return;
std::size_t written = 0;
while (written < text.size()) {
const ssize_t step = write(fd, text.data() + written, text.size() - written);
if (step < 0) {
if (errno == EINTR) continue;
return;
}
written += static_cast<std::size_t>(step);
}
}
std::optional<Json> LockHolder(const fs::path& gateDir, std::string_view mode, std::string_view hash8) {
const fs::path path = LockPath(gateDir, mode, hash8);
const std::int32_t handle = open(path.c_str(), O_RDONLY | O_CLOEXEC);
if (handle < 0) return std::nullopt;
std::optional<Json> holder;
if (flock(handle, LOCK_EX | LOCK_NB) == 0) {
flock(handle, LOCK_UN);
} else {
holder = ReadLockInfo(path);
}
close(handle);
return holder;
}
std::int64_t NextRunNumber(const fs::path& gateDir, std::string_view mode, std::string_view hash8) {
std::int64_t n = 1;
while (fs::exists(gateDir / std::format("{}-{}-run-{}.json", mode, hash8, n)) || fs::exists(gateDir / std::format("{}-{}-run-{}-test.json", mode, hash8, n))) ++n;
return n;
}
Json UsageTotals(std::span<const Json> records) {
Json totals = {
{"input", 0}, {"cacheCreation", 0}, {"cacheRead", 0}, {"output", 0}, {"thinking", 0},
{"turns", 0}, {"processes", 0}, {"costUsd", 0.0}, {"denials", 0},
};
const Json zero = 0;
for (const Json& record : records) {
const Json& usage = Dyn::Or(Dyn::Get(record, "usage"), Dyn::EmptyDict());
for (const std::string_view key : {"input", "cacheCreation", "cacheRead", "output", "thinking"}) {
totals[std::string(key)] = Dyn::Add(totals[std::string(key)], OrZero(GetOr(usage, key, zero)));
}
totals["costUsd"] = Dyn::Add(totals["costUsd"], OrZero(Dyn::Get(record, "costUsd")));
totals["denials"] = Dyn::Add(totals["denials"], Json(Len(Dyn::Or(Dyn::Get(record, "denials"), Dyn::EmptyList()))));
totals["turns"] = Dyn::Add(totals["turns"], OrZero(GetOr(record, "turns", zero)));
const Json& status = Dyn::Get(record, "status");
const bool counted = !status.is_null() && status != "dry-run" && status != "reused";
totals["processes"] = Dyn::Add(totals["processes"], Json(counted ? 1 : 0));
}
// input + 1.25 * cacheCreation + 0.1 * cacheRead + 5 * output: 5 * an
// integer stays an integer until it meets the float, so the dump
// prints it the same.
const Json& output = totals["output"];
const double five = output.is_number_integer() ? static_cast<double>(5 * output.get<std::int64_t>()) : 5.0 * AsDouble(output);
double costUnits = AsDouble(totals["input"]) + 1.25 * AsDouble(totals["cacheCreation"]);
costUnits = costUnits + 0.1 * AsDouble(totals["cacheRead"]);
costUnits = costUnits + five;
totals["costUnits"] = costUnits;
return totals;
}
std::string Summary(const Json& dump) {
std::vector<std::string> lines;
std::string overall = std::format("overall: {}", Dyn::Str(Dyn::At(dump, "overall")));
if (Dyn::Truthy(Dyn::Get(dump, "stoppedAt"))) overall += std::format(" (stopped at {})", Dyn::Str(Dyn::At(dump, "stoppedAt")));
lines.push_back(std::move(overall));
for (const Json& result : Dyn::At(dump, "results")) {
const Json& parts = Dyn::Or(Dyn::Get(result, "parts"), Dyn::EmptyList());
const auto reused = std::ranges::count_if(parts, [](const Json& part) { return Dyn::Truthy(Dyn::Get(part, "reused")); });
std::string line = std::format(" {} {} ", Padded(Dyn::At(result, "lens"), 28), Padded(Dyn::At(result, "verdict"), 12));
if (Dyn::Truthy(Dyn::Get(result, "resumed"))) line += std::format("[resumed {}] ", Text(Dyn::At(result, "resumed")));
line += std::format("findings {} (undischarged {}), notes {}, passes {}", Len(Dyn::At(result, "findings")), Dyn::Str(Dyn::At(result, "undischarged")),
Len(Dyn::At(result, "notes")), Len(parts));
if (reused) line += std::format(" ({} reused)", reused);
if (Dyn::Truthy(Dyn::Get(result, "coverageMissing"))) line += std::format(", missing {}", Len(Dyn::At(result, "coverageMissing")));
if (Dyn::Truthy(Dyn::Get(result, "stoppedEarly"))) line += std::format(" <- {}", Text(Dyn::At(result, "stoppedEarly")));
lines.push_back(std::move(line));
}
const Json& coverage = Dyn::Or(Dyn::Get(dump, "coverage"), Dyn::EmptyDict());
if (Dyn::Truthy(coverage)) {
const Json& mode = Dyn::Get(dump, "mode");
if (mode == "reply" || mode == "series") {
lines.push_back(std::format("matrix: {} units x {} lenses; holes {}, persona holes {}, thin {}, modelled {}, units with a defect verdict {}", Dyn::Str(Dyn::At(coverage, "units")),
Len(Dyn::At(coverage, "lenses")), Len(Dyn::At(coverage, "holes")), Len(Dyn::Or(Dyn::Get(coverage, "personaHoles"), Dyn::EmptyList())),
Len(Dyn::Or(Dyn::Get(coverage, "thin"), Dyn::EmptyList())), Len(Dyn::Or(Dyn::Get(coverage, "modelled"), Dyn::EmptyList())), Len(Dyn::At(coverage, "defects"))));
} else {
lines.push_back(std::format("matrix: {} units x {} lenses; holes {}, not reviewed by any lens that runs (messages, diffstats, documentation, the units of a lens the config turns off) {}, units with a defect verdict {}",
Dyn::Str(Dyn::At(coverage, "units")), Len(Dyn::At(coverage, "lenses")), Len(Dyn::At(coverage, "holes")),
Len(Dyn::Or(Dyn::Get(coverage, "notReviewed"), Dyn::EmptyList())), Len(Dyn::At(coverage, "defects"))));
}
}
const Json& usageAll = Dyn::Has(dump, "usage") ? Dyn::At(dump, "usage") : Dyn::EmptyDict();
const Json& usage = Dyn::Or(Dyn::Get(usageAll, "run"), Dyn::EmptyDict());
if (Dyn::Truthy(usage)) {
const Json zero = 0;
lines.push_back(std::format("usage: {} processes, {} turns, cache write {}M, cache read {}M, output {}K, cost-units {}M, list cost ${}, denied calls {}, wall {} min, prefix hit rate {}",
Dyn::Str(Dyn::At(usage, "processes")), Dyn::Str(Dyn::At(usage, "turns")),
Formatted(AsDouble(Dyn::At(usage, "cacheCreation")) / 1e6, 2), Formatted(AsDouble(Dyn::At(usage, "cacheRead")) / 1e6, 2),
Formatted(AsDouble(Dyn::At(usage, "output")) / 1e3, 0), Formatted(AsDouble(Dyn::At(usage, "costUnits")) / 1e6, 2),
Formatted(AsDouble(Dyn::At(usage, "costUsd")), 2), Dyn::Str(Dyn::At(usage, "denials")),
Formatted(AsDouble(GetOr(dump, "wallMin", zero)), 0), Dyn::Str(Dyn::Get(usageAll, "prefixHitRate"))));
}
const Json& reads = Dyn::Get(dump, "reads");
if (Dyn::Truthy(reads)) {
std::vector<std::string> owed;
const Json& owedBy = Dyn::At(reads, "owed");
for (auto it = owedBy.begin(); it != owedBy.end(); ++it) {
owed.push_back(std::format("{} {} unit(s) / {} read(s)", it.key(), Dyn::Str(Dyn::At(it.value(), "units")), Dyn::Str(Dyn::At(it.value(), "reads"))));
}
std::string owedText = Dyn::Join(owed, "; ");
if (owedText.empty()) owedText = "none";
lines.push_back(std::format("reads: x = {}, round {} on this hash{}; read this run: {}; owed: {}", Dyn::Str(Dyn::At(reads, "x")), Dyn::Str(Dyn::At(reads, "round")),
Dyn::Truthy(Dyn::Get(reads, "continued")) ? " (continued)" : "", Dyn::Str(Dyn::Or(Dyn::Get(reads, "readThisRun"), Dyn::EmptyDict())), owedText));
}
lines.push_back(std::format("dump: {}", Dyn::Str(Dyn::At(dump, "dumpPath"))));
return Dyn::Join(lines, "\n");
}
}