// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module; #include #include #include #include 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(); if (value.is_boolean()) return value.get() ? 1.0 : 0.0; if (value.is_number_unsigned()) return static_cast(value.get()); if (value.is_number_integer()) return static_cast(value.get()); 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()) : 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(); } // f'{value:(), 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 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 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(step); } } std::optional 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 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 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(5 * output.get()) : 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 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 kind = Dyn::Or(Dyn::Get(dump, "kind"), Dyn::Get(dump, "mode")); if (kind == "reply" || kind == "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 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"); } }