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:
Jorijn van der Graaf 2026-10-31 06:48:55 +01:00
commit 741e3a656d
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
70 changed files with 30565 additions and 21 deletions

View file

@ -205,6 +205,46 @@ namespace Crafter::Review::Dyn {
return Encode(std::u32string_view(decoded).substr(static_cast<std::size_t>(from), static_cast<std::size_t>(to - from)));
}
std::pair<char32_t, std::size_t> CodePointAt(std::string_view text, std::size_t at) {
return Next(text, at);
}
std::pair<std::size_t, std::size_t> SliceBounds(std::size_t size, std::int64_t start, std::optional<std::int64_t> end) {
const auto count = static_cast<std::int64_t>(size);
auto clip = [count](std::int64_t index) {
if (index < 0) index += count;
return std::clamp<std::int64_t>(index, 0, count);
};
const std::int64_t from = clip(start);
const std::int64_t to = end ? clip(*end) : count;
return {static_cast<std::size_t>(from), static_cast<std::size_t>(std::max(from, to))};
}
namespace {
template <typename Predicate>
std::size_t RunOf(std::string_view text, std::size_t at, Predicate matches) {
std::size_t end = at;
while (end < text.size()) {
const auto [c, used] = Next(text, end);
if (!matches(c)) break;
end += used;
}
return end - at;
}
}
std::size_t DecimalRun(std::string_view text, std::size_t at) {
return RunOf(text, at, [](char32_t c) { return IsDecimal(c); });
}
std::size_t SpaceRun(std::string_view text, std::size_t at) {
return RunOf(text, at, [](char32_t c) { return IsSpace(c); });
}
std::size_t NonSpaceRun(std::string_view text, std::size_t at) {
return RunOf(text, at, [](char32_t c) { return !IsSpace(c); });
}
bool IsSpace(char32_t c) {
return std::ranges::binary_search(Unicode::Space, c);
}
@ -230,6 +270,10 @@ namespace Crafter::Review::Dyn {
return InRuns(Unicode::Printable, c);
}
bool IsWord(char32_t c) {
return InRuns(Unicode::Word, c);
}
char IgnoreCaseLetter(char32_t c) {
if (c >= 'a' && c <= 'z') return static_cast<char>(c);
if (c >= 'A' && c <= 'Z') return static_cast<char>(c - 'A' + 'a');
@ -413,21 +457,305 @@ namespace Crafter::Review::Dyn {
return out;
}
std::string LJust(std::string_view text, std::size_t width) {
const std::size_t length = Length(text);
std::string out(text);
if (length < width) out.append(width - length, ' ');
return out;
}
std::string ReadText(const std::filesystem::path& path) {
std::ifstream in(path, std::ios::binary);
if (!in) throw std::runtime_error(std::format("[Errno 2] No such file or directory: {}", Repr(std::string_view(path.string()))));
const std::string bytes((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
for (std::size_t at = 0; at < bytes.size();) {
const auto [c, used] = Next(bytes, at);
if (c == 0xFFFD && (used != 3 || std::string_view(bytes).substr(at, 3) != "\xEF\xBF\xBD")) {
throw std::runtime_error(std::format("'utf-8' codec can't decode byte 0x{:02x} in position {}: invalid utf-8 ({})",
static_cast<std::uint8_t>(bytes[at]), at, path.string()));
}
at += used;
if (!in || std::filesystem::is_directory(path)) {
throw Error("FileNotFoundError", std::format("[Errno 2] No such file or directory: {}", Repr(std::string_view(path.string()))));
}
const std::string bytes((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
if (const std::optional<std::string> error = Utf8DecodeError(bytes)) throw Error("UnicodeDecodeError", *error);
return UniversalNewlines(bytes);
}
std::optional<std::string> Utf8DecodeError(std::string_view bytes) {
// The decoder's reasons: a bad lead byte is "invalid start byte"; the
// k-th continuation byte out of range is "invalid continuation byte"
// over k bytes; the input ending inside a sequence is "unexpected end
// of data" over the rest of it.
for (std::size_t at = 0; at < bytes.size();) {
const auto lead = static_cast<std::uint8_t>(bytes[at]);
std::size_t need = 0;
std::uint8_t low = 0x80;
std::uint8_t high = 0xBF;
if (lead < 0x80) {
++at;
continue;
}
if (lead >= 0xC2 && lead <= 0xDF) {
need = 1;
} else if (lead >= 0xE0 && lead <= 0xEF) {
need = 2;
if (lead == 0xE0) low = 0xA0;
if (lead == 0xED) high = 0x9F;
} else if (lead >= 0xF0 && lead <= 0xF4) {
need = 3;
if (lead == 0xF0) low = 0x90;
if (lead == 0xF4) high = 0x8F;
}
std::string_view reason;
std::size_t length = 1;
if (need == 0) {
reason = "invalid start byte";
} else {
for (std::size_t k = 1; k <= need; ++k) {
if (at + k >= bytes.size()) {
reason = "unexpected end of data";
length = bytes.size() - at;
break;
}
const auto next = static_cast<std::uint8_t>(bytes[at + k]);
if (next < low || next > high) {
reason = "invalid continuation byte";
length = k;
break;
}
low = 0x80;
high = 0xBF;
}
}
if (reason.empty()) {
at += need + 1;
continue;
}
if (length == 1) return std::format("'utf-8' codec can't decode byte 0x{:02x} in position {}: {}", lead, at, reason);
return std::format("'utf-8' codec can't decode bytes in position {}-{}: {}", at, at + length - 1, reason);
}
return std::nullopt;
}
namespace {
struct JsonFailure {
std::string_view message;
std::size_t position;
};
bool IsJsonSpace(char32_t c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}
bool IsJsonDigit(char32_t c) {
return c >= '0' && c <= '9';
}
std::int32_t HexDigit(char32_t c) {
if (c >= '0' && c <= '9') return static_cast<std::int32_t>(c - '0');
if (c >= 'a' && c <= 'f') return static_cast<std::int32_t>(c - 'a' + 10);
if (c >= 'A' && c <= 'F') return static_cast<std::int32_t>(c - 'A' + 10);
return -1;
}
// A JSON scanner that fails where and why the recorded messages say,
// step for step: a string, an object, an array, a number, then any
// value. "Expecting value" is a position where no value starts.
class JsonScanner {
public:
explicit JsonScanner(std::u32string_view text) : s(text) {}
std::optional<JsonFailure> Value(std::size_t idx, std::size_t& next) {
if (idx >= s.size()) return JsonFailure{"Expecting value", idx};
switch (s[idx]) {
case '"':
return String(idx + 1, next);
case '{':
return Object(idx + 1, next);
case '[':
return Array(idx + 1, next);
case 'n':
if (Word(idx, U"null", next)) return std::nullopt;
break;
case 't':
if (Word(idx, U"true", next)) return std::nullopt;
break;
case 'f':
if (Word(idx, U"false", next)) return std::nullopt;
break;
case 'N':
if (Word(idx, U"NaN", next)) return std::nullopt;
break;
case 'I':
if (Word(idx, U"Infinity", next)) return std::nullopt;
break;
case '-':
if (Word(idx, U"-Infinity", next)) return std::nullopt;
break;
default:
break;
}
return Number(idx, next);
}
private:
std::u32string_view s;
bool Word(std::size_t idx, std::u32string_view word, std::size_t& next) const {
if (s.substr(idx, word.size()) != word) return false;
next = idx + word.size();
return true;
}
std::size_t SkipSpace(std::size_t idx) const {
while (idx < s.size() && IsJsonSpace(s[idx])) ++idx;
return idx;
}
std::optional<JsonFailure> String(std::size_t end, std::size_t& next) const {
const std::size_t begin = end - 1;
const std::size_t len = s.size();
while (true) {
std::size_t at = end;
char32_t c = 0;
for (; at < len; ++at) {
c = s[at];
if (c == '"' || c == '\\') break;
if (c <= 0x1F) return JsonFailure{"Invalid control character at", at};
}
if (at >= len) return JsonFailure{"Unterminated string starting at", begin};
++at;
if (c == '"') {
next = at;
return std::nullopt;
}
if (at == len) return JsonFailure{"Unterminated string starting at", begin};
c = s[at];
if (c != 'u') {
end = at + 1;
if (!std::u32string_view(U"\"\\/bfnrt").contains(c)) return JsonFailure{"Invalid \\escape", end - 2};
continue;
}
++at;
end = at + 4;
if (end >= len) return JsonFailure{"Invalid \\uXXXX escape", at - 1};
std::uint32_t unit = 0;
for (; at < end; ++at) {
const std::int32_t digit = HexDigit(s[at]);
if (digit < 0) return JsonFailure{"Invalid \\uXXXX escape", end - 5};
unit = (unit << 4) | static_cast<std::uint32_t>(digit);
}
if (unit >= 0xD800 && unit <= 0xDBFF && end + 6 < len && s[at] == '\\' && s[at + 1] == 'u') {
at += 2;
end += 6;
std::uint32_t second = 0;
for (; at < end; ++at) {
const std::int32_t digit = HexDigit(s[at]);
if (digit < 0) return JsonFailure{"Invalid \\uXXXX escape", end - 5};
second = (second << 4) | static_cast<std::uint32_t>(digit);
}
if (second < 0xDC00 || second > 0xDFFF) end -= 6;
}
}
}
std::optional<JsonFailure> Object(std::size_t idx, std::size_t& next) {
idx = SkipSpace(idx);
if (idx >= s.size() || s[idx] != '}') {
while (true) {
if (idx >= s.size() || s[idx] != '"') return JsonFailure{"Expecting property name enclosed in double quotes", idx};
std::size_t after = 0;
if (std::optional<JsonFailure> failure = String(idx + 1, after)) return failure;
idx = SkipSpace(after);
if (idx >= s.size() || s[idx] != ':') return JsonFailure{"Expecting ':' delimiter", idx};
idx = SkipSpace(idx + 1);
if (std::optional<JsonFailure> failure = Value(idx, after)) return failure;
idx = SkipSpace(after);
if (idx < s.size() && s[idx] == '}') break;
if (idx >= s.size() || s[idx] != ',') return JsonFailure{"Expecting ',' delimiter", idx};
const std::size_t comma = idx;
idx = SkipSpace(idx + 1);
if (idx < s.size() && s[idx] == '}') return JsonFailure{"Illegal trailing comma before end of object", comma};
}
}
next = idx + 1;
return std::nullopt;
}
std::optional<JsonFailure> Array(std::size_t idx, std::size_t& next) {
idx = SkipSpace(idx);
if (idx >= s.size() || s[idx] != ']') {
while (true) {
std::size_t after = 0;
if (std::optional<JsonFailure> failure = Value(idx, after)) return failure;
idx = SkipSpace(after);
if (idx < s.size() && s[idx] == ']') break;
if (idx >= s.size() || s[idx] != ',') return JsonFailure{"Expecting ',' delimiter", idx};
const std::size_t comma = idx;
idx = SkipSpace(idx + 1);
if (idx < s.size() && s[idx] == ']') return JsonFailure{"Illegal trailing comma before end of array", comma};
}
}
next = idx + 1;
return std::nullopt;
}
std::optional<JsonFailure> Number(std::size_t start, std::size_t& next) const {
const std::size_t len = s.size();
std::size_t idx = start;
if (s[idx] == '-') {
++idx;
if (idx >= len) return JsonFailure{"Expecting value", start};
}
if (s[idx] >= '1' && s[idx] <= '9') {
++idx;
while (idx < len && IsJsonDigit(s[idx])) ++idx;
} else if (s[idx] == '0') {
++idx;
} else {
return JsonFailure{"Expecting value", start};
}
if (idx + 1 < len && s[idx] == '.' && IsJsonDigit(s[idx + 1])) {
idx += 2;
while (idx < len && IsJsonDigit(s[idx])) ++idx;
}
if (idx + 1 < len && (s[idx] == 'e' || s[idx] == 'E')) {
const std::size_t exponent = idx;
++idx;
if (idx + 1 < len && (s[idx] == '-' || s[idx] == '+')) ++idx;
while (idx < len && IsJsonDigit(s[idx])) ++idx;
if (!IsJsonDigit(s[idx - 1])) idx = exponent;
}
next = idx;
return std::nullopt;
}
};
}
std::optional<std::string> JsonDecodeError(std::string_view text) {
const std::u32string s = Decode(text);
JsonScanner scanner(s);
std::size_t start = 0;
while (start < s.size() && IsJsonSpace(s[start])) ++start;
std::size_t end = 0;
std::optional<JsonFailure> failure;
if (s.starts_with(U'\uFEFF')) {
failure = JsonFailure{"Unexpected UTF-8 BOM (decode using utf-8-sig)", 0};
} else {
failure = scanner.Value(start, end);
}
if (!failure) {
while (end < s.size() && IsJsonSpace(s[end])) ++end;
if (end != s.size()) failure = JsonFailure{"Extra data", end};
}
if (!failure) return std::nullopt;
const std::size_t pos = failure->position;
const std::u32string_view before = std::u32string_view(s).substr(0, pos);
const auto line = std::ranges::count(before, U'\n') + 1;
const std::size_t lastNewline = before.rfind(U'\n');
const std::size_t column = lastNewline == std::u32string_view::npos ? pos + 1 : pos - lastNewline;
return std::format("{}: line {} column {} (char {})", failure->message, line, column, pos);
}
Json LoadJsonFile(const std::filesystem::path& path) {
const std::string text = ReadText(path);
if (const std::optional<std::string> error = JsonDecodeError(text)) throw Error("JSONDecodeError", *error);
return ParseLoose(text);
}
std::string UniversalNewlines(std::string_view text) {
if (!text.contains('\r')) return std::string(text);
std::string out;
@ -933,6 +1261,29 @@ namespace Crafter::Review::Dyn {
return parts;
}
std::string UrlUnsplit(std::string_view scheme, std::string_view netloc, std::string_view path, std::string_view query, std::string_view fragment) {
// The schemes that get `//` before an empty netloc.
constexpr std::array<std::string_view, 27> UsesNetloc = {
"", "ftp", "http", "gopher", "nntp", "telnet", "imap", "wais", "file", "mms", "https", "shttp", "snews", "prospero",
"rtsp", "rtsps", "rtspu", "rsync", "svn", "svn+ssh", "sftp", "nfs", "git", "git+ssh", "ws", "wss", "itms-services",
};
bool hasNetloc = !netloc.empty();
if (!hasNetloc && !scheme.empty() && std::ranges::contains(UsesNetloc, scheme) && (path.empty() || path.starts_with('/'))) {
hasNetloc = true;
}
std::string url(path);
if (hasNetloc) {
if (!url.empty() && !url.starts_with('/')) url.insert(0, "/");
url = std::format("//{}{}", netloc, url);
} else if (url.starts_with("//")) {
url.insert(0, "//");
}
if (!scheme.empty()) url = std::format("{}:{}", scheme, url);
if (!query.empty()) url = std::format("{}?{}", url, query);
if (!fragment.empty()) url = std::format("{}#{}", url, fragment);
return url;
}
std::string Unquote(std::string_view text) {
if (!text.contains('%')) return std::string(text);
// Percent sequences become bytes; runs of bytes are decoded as UTF-8
@ -1058,4 +1409,203 @@ namespace Crafter::Review::Dyn {
std::string ErrnoText(std::int32_t code) {
return std::format("[Errno {}] {}", code, std::strerror(code));
}
Error::Error(std::string_view kind, const std::string& message) : std::runtime_error(message), kind(kind) {}
const std::string& Error::Kind() const {
return kind;
}
bool Truthy(const Json& value) {
switch (value.type()) {
case Json::value_t::null:
return false;
case Json::value_t::boolean:
return value.get<bool>();
case Json::value_t::number_integer:
return value.get<std::int64_t>() != 0;
case Json::value_t::number_unsigned:
return value.get<std::uint64_t>() != 0;
case Json::value_t::number_float:
// NaN is true: it is not 0.0.
return value.get<double>() != 0.0;
case Json::value_t::string:
return !value.get_ref<const std::string&>().empty();
case Json::value_t::array:
case Json::value_t::object:
return !value.empty();
default:
return true;
}
}
const Json& Or(const Json& value, const Json& fallback) {
return Truthy(value) ? value : fallback;
}
const Json& EmptyDict() {
static const Json empty = Json::object();
return empty;
}
const Json& EmptyList() {
static const Json empty = Json::array();
return empty;
}
bool Has(const Json& dict, std::string_view key) {
return dict.is_object() && dict.find(key) != dict.end();
}
const Json& Get(const Json& dict, std::string_view key) {
static const Json none;
if (!dict.is_object()) throw Error("AttributeError", std::format("'{}' object has no attribute 'get'", TypeName(dict)));
const auto found = dict.find(key);
return found == dict.end() ? none : *found;
}
const Json& At(const Json& dict, std::string_view key) {
if (dict.is_object()) {
const auto found = dict.find(key);
if (found == dict.end()) throw Error("KeyError", Repr(key));
return *found;
}
if (dict.is_array()) throw Error("TypeError", "list indices must be integers or slices, not str");
if (dict.is_string()) throw Error("TypeError", "string indices must be integers, not 'str'");
throw Error("TypeError", std::format("'{}' object is not subscriptable", TypeName(dict)));
}
std::string_view TypeName(const Json& value) {
switch (value.type()) {
case Json::value_t::null:
return "NoneType";
case Json::value_t::boolean:
return "bool";
case Json::value_t::number_integer:
case Json::value_t::number_unsigned:
return "int";
case Json::value_t::number_float:
return "float";
case Json::value_t::string:
return "str";
case Json::value_t::array:
return "list";
case Json::value_t::object:
return "dict";
default:
return "bytes";
}
}
bool IsInt(const Json& value) {
return value.is_boolean() || value.is_number_integer();
}
bool IsNumber(const Json& value) {
return value.is_boolean() || value.is_number();
}
std::string Str(const Json& value) {
if (value.is_string()) return value.get<std::string>();
return ReprValue(value);
}
std::string StrOr(const Json& value, std::string_view fallback) {
return Truthy(value) ? Str(value) : std::string(fallback);
}
namespace {
// An int of a JSON value: a bool is 0 or 1. Unsigned values beyond
// the signed range keep their own type in Equal().
std::int64_t IntOf(const Json& value) {
if (value.is_boolean()) return value.get<bool>() ? 1 : 0;
if (value.is_number_unsigned()) return static_cast<std::int64_t>(value.get<std::uint64_t>());
return value.get<std::int64_t>();
}
// An integer equals a float exactly, never by rounding the integer to
// a double.
bool IntEqualsFloat(const Json& integer, double f) {
if (!std::isfinite(f) || f != std::floor(f)) return false;
if (integer.is_number_unsigned() && integer.get<std::uint64_t>() > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())) {
if (f < 0.0 || f >= 18446744073709551616.0) return false;
return static_cast<std::uint64_t>(f) == integer.get<std::uint64_t>();
}
if (f < -9223372036854775808.0 || f >= 9223372036854775808.0) return false;
return static_cast<std::int64_t>(f) == IntOf(integer);
}
}
bool Equal(const Json& a, const Json& b) {
if (IsNumber(a) && IsNumber(b)) {
if (a.is_number_float() && b.is_number_float()) return a.get<double>() == b.get<double>();
if (a.is_number_float()) return IntEqualsFloat(b, a.get<double>());
if (b.is_number_float()) return IntEqualsFloat(a, b.get<double>());
const bool bigA = a.is_number_unsigned() && a.get<std::uint64_t>() > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max());
const bool bigB = b.is_number_unsigned() && b.get<std::uint64_t>() > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max());
if (bigA || bigB) return bigA && bigB && a.get<std::uint64_t>() == b.get<std::uint64_t>();
return IntOf(a) == IntOf(b);
}
if (a.type() != b.type()) return false;
if (a.is_array()) {
if (a.size() != b.size()) return false;
for (std::size_t i = 0; i < a.size(); ++i) {
if (!Equal(a[i], b[i])) return false;
}
return true;
}
if (a.is_object()) {
if (a.size() != b.size()) return false;
for (auto it = a.begin(); it != a.end(); ++it) {
const auto other = b.find(it.key());
if (other == b.end() || !Equal(it.value(), *other)) return false;
}
return true;
}
return a == b;
}
bool In(const Json& item, const Json& container) {
if (container.is_array()) {
return std::ranges::any_of(container, [&](const Json& element) { return Equal(item, element); });
}
if (container.is_object()) {
if (item.is_array() || item.is_object()) throw Error("TypeError", std::format("unhashable type: '{}'", TypeName(item)));
return item.is_string() && container.find(item.get_ref<const std::string&>()) != container.end();
}
if (container.is_string()) {
if (!item.is_string()) throw Error("TypeError", std::format("'in <string>' requires string as left operand, not {}", TypeName(item)));
return container.get_ref<const std::string&>().contains(item.get_ref<const std::string&>());
}
throw Error("TypeError", std::format("argument of type '{}' is not a container or iterable", TypeName(container)));
}
Json Add(const Json& a, const Json& b) {
if (!IsNumber(a) || !IsNumber(b)) {
throw Error("TypeError", std::format("unsupported operand type(s) for +: '{}' and '{}'", TypeName(a), TypeName(b)));
}
if (a.is_number_float() || b.is_number_float()) {
auto asDouble = [](const Json& value) { return value.is_number_float() ? value.get<double>() : static_cast<double>(IntOf(value)); };
return Json(asDouble(a) + asDouble(b));
}
return Json(IntOf(a) + IntOf(b));
}
double Sum(std::span<const double> values) {
if (values.empty()) return 0.0;
// sum() starts from the int 0; 0 + x is x (and 0.0 for -0.0).
double total = 0.0 + values.front();
double compensation = 0.0;
for (const double x : values.subspan(1)) {
const double t = total + x;
if (std::fabs(total) >= std::fabs(x)) {
compensation += (total - t) + x;
} else {
compensation += (x - t) + total;
}
total = t;
}
if (compensation != 0.0 && std::isfinite(compensation)) total += compensation;
return total;
}
}