feat(pipeline): the pure pipeline, checked against its goldens
The modules that turn a frozen range into passes and passes into a verdict: :Schema, :Tools (the DENY, git and base lists, a leaf, so the config and the pool both read them), :Plan, :Artifact, :Inventory, :Dispositions, :State, :Normalize, :Stream, :Prompt, :Config, and the URL half of :PgWire. Units, jobs, results, the config and the claude stream stay JSON from end to end, so every key keeps its place; :Text gives JSON values truthiness, get, ==, membership and Str(), and throws Dyn::Error under a name for each kind of unexpected data. Exactness where the bytes are seen: - the stream classifiers are hand-written scanners with case-insensitive folds (the Kelvin sign is a k), Unicode \b and \s, and `.` stopping at \n; the SQL call detection takes `<exe> sql` with any path before it; - config problems carry the JSON scanner's and the UTF-8 decoder's own texts, positions included; - a float total is Neumaier's compensated sum, and the build passes -ffp-contract=off: -march=native would fuse a*b+c; - `crafter-review inventory` and `config` print what the goldens record, byte for byte (the fixture and 14 configs, broken ones included); a message names this binary as the command to run. tests/Goldens: the recorded answers over the fixtures next to them (the commits fixture, a rich versioned series with CRLF, a folded subject, Markdown headings in and out of fences, the code-key rewrites); the suite compares 2304 results as DumpFile writes them, so a key out of order fails too. All agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b02de3ceff
commit
741e3a656d
70 changed files with 30565 additions and 21 deletions
282
implementations/Crafter.Review-State.cpp
Normal file
282
implementations/Crafter.Review-State.cpp
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
// 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 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);
|
||||
}
|
||||
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) {
|
||||
// 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);
|
||||
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)) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue