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

@ -33,6 +33,19 @@ export namespace Crafter::Review::Dyn {
// The code points from start to end: a negative index counts from the
// end, and both are clipped to the text.
std::string Slice(std::string_view text, std::int64_t start, std::optional<std::int64_t> end = std::nullopt);
// The code point at byte `at` and the bytes it takes: U+FFFD and the
// length of the invalid sequence where the bytes are no UTF-8.
std::pair<char32_t, std::size_t> CodePointAt(std::string_view text, std::size_t at);
// [start, end) of a slice of `size` items: a negative index counts from
// the end, and both are clipped to the sequence.
std::pair<std::size_t, std::size_t> SliceBounds(std::size_t size, std::int64_t start, std::optional<std::int64_t> end = std::nullopt);
// ---- regex classes, for the hand-written scanners -----------------------
// The bytes of the run at `at` of regex \d (Unicode decimal digits), \s
// (Unicode whitespace) or \S characters.
std::size_t DecimalRun(std::string_view text, std::size_t at);
std::size_t SpaceRun(std::string_view text, std::size_t at);
std::size_t NonSpaceRun(std::string_view text, std::size_t at);
// ---- characters ----------------------------------------------------------
bool IsSpace(char32_t c);
@ -41,6 +54,8 @@ export namespace Crafter::Review::Dyn {
// The digit's value, -1 for a character that is not a decimal digit.
std::int32_t DecimalValue(char32_t c);
bool IsPrintable(char32_t c);
// regex \w: what \b takes for a word character.
bool IsWord(char32_t c);
// The lowercase ASCII letter `c` matches case-insensitively, or '\0':
// ASCII letters, and the four code points outside ASCII that fold onto
// one (İ ı → i, ſ → s, K → k).
@ -70,14 +85,27 @@ export namespace Crafter::Review::Dyn {
std::size_t Count(std::string_view text, std::string_view sub);
std::string Replace(std::string_view text, std::string_view from, std::string_view to);
std::string Join(std::span<const std::string> parts, std::string_view separator);
// f'{text:<width}' / text.ljust(width): padded with spaces to `width`
// code points, where std::format would count display columns.
std::string LJust(std::string_view text, std::size_t width);
// ---- reading text files ----------------------------------------------------
// A text file, read whole: strict UTF-8 — an invalid byte throws
// std::runtime_error naming the file and the offset — and universal
// newlines.
// A text file, read whole: strict UTF-8 — invalid bytes throw Error
// ("UnicodeDecodeError") with Utf8DecodeError()'s message — and universal
// newlines. A file that cannot be opened throws Error ("FileNotFoundError").
std::string ReadText(const std::filesystem::path& path);
// \r\n and a lone \r become \n, as text mode reads them.
std::string UniversalNewlines(std::string_view text);
// The message for bytes that are not UTF-8 — "'utf-8' codec can't decode
// byte 0xff in position 3: invalid start byte" — nullopt for valid UTF-8.
std::optional<std::string> Utf8DecodeError(std::string_view bytes);
// The message for text that is no JSON document, with the scanner's
// reason and position ("Expecting ',' delimiter: line 3 column 5 (char
// 21)"); nullopt for a document LoadJsonFile() takes.
std::optional<std::string> JsonDecodeError(std::string_view text);
// A JSON file: read as text, then parsed. Throws Error with the message a
// "not JSON (...)" problem carries: the decode error, or the JSON error.
Json LoadJsonFile(const std::filesystem::path& path);
// ---- numbers ------------------------------------------------------------------
// A float as its shortest digits that round-trip; an exponent below 1e-4
@ -127,6 +155,9 @@ export namespace Crafter::Review::Dyn {
// std::invalid_argument with a ValueError message for unbalanced or
// invalid brackets.
SplitUrl UrlSplit(std::string_view url);
// The five parts joined back into a URL: an empty query or fragment gets
// no '?' or '#'.
std::string UrlUnsplit(std::string_view scheme, std::string_view netloc, std::string_view path, std::string_view query, std::string_view fragment);
// unquote(text): %XX sequences decoded as UTF-8, bad bytes replaced.
std::string Unquote(std::string_view text);
@ -152,4 +183,63 @@ export namespace Crafter::Review::Dyn {
// ---- errno texts -------------------------------------------------------------------------
// "[Errno 111] Connection refused": an errno as a message prints it.
std::string ErrnoText(std::int32_t code);
// ---- exceptions ------------------------------------------------------------------------------
// Data the code does not expect — a key that is absent, a value of the
// wrong type, a lookup in something that is no object — throws Error
// named for the case (KeyError, TypeError, AttributeError): a caller
// catches the names it expects, and anything else fails loudly.
class Error : public std::runtime_error {
public:
Error(std::string_view kind, const std::string& message);
const std::string& Kind() const;
private:
std::string kind;
};
// ---- JSON values ---------------------------------------------------------------------------------
// The driver's data — units, jobs, results, dispositions, the config, the
// claude stream — stays JSON from end to end, so every key keeps its place
// and its unknown neighbours. These are the operations on it.
//
// Truthiness: null, false, 0, 0.0, "", [] and {} are false.
bool Truthy(const Json& value);
// `value or fallback`. A reference to one of the two, so neither may be a
// temporary: name the fallback, or use EmptyDict()/EmptyList().
const Json& Or(const Json& value, const Json& fallback);
const Json& Or(Json&& value, const Json& fallback) = delete;
const Json& Or(const Json& value, Json&& fallback) = delete;
// The shared {} and [] that `x or {}` and `x or []` fall back to.
const Json& EmptyDict();
const Json& EmptyList();
// Whether an object has the key; false for anything that is no object.
bool Has(const Json& dict, std::string_view key);
// The key's value, null when it is absent; Error (AttributeError) on
// something that is no object.
const Json& Get(const Json& dict, std::string_view key);
// The key's value; Error (KeyError) when it is absent.
const Json& At(const Json& dict, std::string_view key);
// The type name messages use: dict, list, str, int, float, bool, NoneType.
std::string_view TypeName(const Json& value);
// An integer, a bool included.
bool IsInt(const Json& value);
// An integer or a float, a bool included.
bool IsNumber(const Json& value);
// A string as it is, anything else as ReprValue() prints it.
std::string Str(const Json& value);
// Str() of the value, or of the fallback when the value is falsy.
std::string StrOr(const Json& value, std::string_view fallback);
// a == b: numbers by value (True == 1 == 1.0), dicts whatever their key
// order, a str never equal to a number.
bool Equal(const Json& a, const Json& b);
// Membership: an element of a list, a key of an object, a substring of a
// string (Error when the item is no string there).
bool In(const Json& item, const Json& container);
// a + b on numbers: int + int stays an int, a float makes a float, a bool
// counts as 0 or 1; Error (TypeError) for anything else.
Json Add(const Json& a, const Json& b);
// A total of floats by Neumaier's compensated summation, so it comes out
// to the same last bit as the goldens record it.
double Sum(std::span<const double> values);
}