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>
2026-10-31 06:48:55 +01:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
|
|
|
|
|
|
export module Crafter.Review:Config;
|
|
|
|
|
import std;
|
|
|
|
|
import :Json;
|
|
|
|
|
|
|
|
|
|
// The consumer's configuration: `.review-gates/config.json` at the
|
|
|
|
|
// repository root (`--config` names another file). It holds what the
|
|
|
|
|
// pipeline must know about THIS repository — who may author its commits,
|
|
|
|
|
// which branch a range is measured against, which lenses run and the
|
|
|
|
|
// environment each gets, how its application is started and reached, where
|
|
|
|
|
// its local database answers, which paths carry no runtime behaviour — and
|
|
|
|
|
// nothing about any one review. A missing file is the defaults; an unknown
|
|
|
|
|
// key or a malformed value is a preflight failure, and a retired key a
|
|
|
|
|
// warning. The config stays the JSON it was read as, keys in their order.
|
|
|
|
|
export namespace Crafter::Review::Config {
|
|
|
|
|
constexpr std::string_view ConfigDir = ".review-gates";
|
|
|
|
|
constexpr std::string_view ConfigFile = ".review-gates/config.json";
|
|
|
|
|
constexpr std::string_view SlotFile = "slot.json";
|
|
|
|
|
|
|
|
|
|
// DEFAULTS, a fresh copy each call.
|
|
|
|
|
Json Defaults();
|
|
|
|
|
|
|
|
|
|
struct Loaded {
|
|
|
|
|
Json config;
|
|
|
|
|
std::vector<std::string> problems;
|
|
|
|
|
};
|
|
|
|
|
// The defaults, the file's values merged over them, and every problem
|
|
|
|
|
// Validate finds. A retired key is no problem: it is dropped, and
|
|
|
|
|
// config["_warnings"] says so and what replaced it — a key the pipeline
|
|
|
|
|
// stopped reading must not stop a consumer's freeze or run (2026-10-22).
|
|
|
|
|
// `path` empty: <repo>/.review-gates/config.json.
|
|
|
|
|
Loaded Load(const std::filesystem::path& repo, const std::filesystem::path& path = {});
|
|
|
|
|
std::vector<std::string> Validate(const Json& config);
|
|
|
|
|
// The config for a dump: the database URL with its password masked, no
|
|
|
|
|
// `_` bookkeeping keys.
|
|
|
|
|
Json Redacted(const Json& config);
|
|
|
|
|
// The emails the freeze accepts: the configured list, else the
|
|
|
|
|
// repository's git identity.
|
|
|
|
|
std::vector<std::string> Authors(const Json& config, const std::filesystem::path& repo);
|
|
|
|
|
std::string GitUserEmail(const std::filesystem::path& repo);
|
feat(config): modes - a mode is an artifact kind, the lenses it runs and its effort
The config defines the modes (modes.<name>): the kind of artifact the mode
reviews - reply, series or commits, the three shipped partitions; a kind
is a mode of its own name - the lenses it runs, in its kind's gate order,
and the effort it runs at, else the kind's default (max for a reply or a
series, xhigh for a commit range). `run --mode` names any of them, the
dump records mode and kind, and everything that decided by the mode's
name - the partition and hash, the prompts, the schemas, the disposition
and reuse-key rules, what the CLI requires - decides by the kind.
Every lens stays off until a mode lists it; lenses.<name>.run is retired
with a warning that says so, and a lens of another kind, a name that is
no lens, a lens listed twice and an unknown effort are config problems.
The defaults run no lens; the template ships the three kinds as modes
with every lens of their kind at the kind's effort, to trim (the owner's
ruling of 2026-11-01), and init says so as it writes it - a kept config
is the repository's own, and only one that lists no lens gets a step.
The readers of the retired key follow: the CI smoke config, the shipped
skill, the procedure (its description of the config as well as its
mechanics), the manifest template, the run's messages and the comments
that named the three modes. The run lock, the run numbers and the dump
names are the kind's, so one full run per hash holds across the modes
of a kind and their rounds count on together; the dump says which mode
ran. The database broker starts for a lens the mode runs, as every other
reader of the lenses off reads them. A named mode runs through the CLI
in the kernel-modes test.
2026-11-01 01:31:40 +01:00
|
|
|
// The lens files no mode lists (what runs nowhere: the slots and init).
|
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>
2026-10-31 06:48:55 +01:00
|
|
|
std::vector<std::string> LensesOff(const Json& config);
|
|
|
|
|
// {lens file: environment} for every lens of the table, as the config
|
|
|
|
|
// holds it (validated elsewhere).
|
|
|
|
|
Json Environments(const Json& config);
|
|
|
|
|
|
|
|
|
|
struct Slots {
|
|
|
|
|
// pool.Stacks entries {id, appUrl, apiUrls, dbUrl, login, slot: true},
|
|
|
|
|
// in slot order.
|
|
|
|
|
Json stacks = Json::array();
|
|
|
|
|
std::vector<std::string> problems;
|
|
|
|
|
};
|
|
|
|
|
// The slots that are up: slots.dir/<n>/slot.json.
|
|
|
|
|
Slots SlotStacks(const Json& config, const std::filesystem::path& repo);
|
|
|
|
|
// slots.start or slots.stop for the slot numbers given, {n} filled.
|
|
|
|
|
std::vector<std::string> SlotCommands(const Json& config, std::string_view which, std::span<const std::int64_t> slots);
|
|
|
|
|
// The first login fixture that exists, relative to the repository root.
|
|
|
|
|
std::optional<std::string> LoginFixture(const Json& config, const std::filesystem::path& repo);
|
feat(config): lens sections - a repository's own text appended to a shipped charter
A lens has one charter, the one this pipeline ships; what a repository
knows that is not general to the lens - its platform and layers, the
record it keeps its decisions in, what its users already have, the
calibration entries that are its own - goes in a file the config names
for the lens (lenses.<name>.section), and Charter appends it after one
blank line. The charter sha in every reuse key covers the combined text.
The config refuses a path outside the repository - absolute, or
climbing out with `..` - by the check slots.dir had, now one both
settings share; a file missing or one the run could not use (a dangling
link, unreadable, not UTF-8) is a problem too, checked through the
reader Charter uses (Prompt::ReadSection), so a section is never
dropped in silence, and a name that is no lens is the unknown key it
always was. Nothing lists a directory, so what lies beside the files is
no concern of the config's: a directory listed would have to tolerate or
refuse every foreign entry. The run logs which charters got one.
The better-solution charter gains a consumer's general entry
(2026-10-31: a mechanism reworked a second time in one submission is the
signal to derive the fix at every layer the data crosses). The procedure
names the section wherever it names the charter: among the config's keys,
in the lens inputs, in the reuse rule (an edit to either re-runs the lens)
and in the feedback loop, which routes a repository's own lesson to its
section and a general one to the shipped charter.
2026-11-01 01:30:22 +01:00
|
|
|
// The repository's own lens sections (`lenses.<name>.section`, resolved
|
|
|
|
|
// against the repository root), keyed by lens file: each a file the
|
|
|
|
|
// config names inside the repository (a relative path, no `..` part).
|
|
|
|
|
std::map<std::string, std::filesystem::path> LensSectionsOf(const Json& config, const std::filesystem::path& repo);
|
|
|
|
|
// What is wrong with those files: one missing, or one
|
|
|
|
|
// Prompt::ReadSection cannot use.
|
|
|
|
|
std::vector<std::string> SectionProblems(const Json& config, const std::filesystem::path& repo);
|
feat(config): modes - a mode is an artifact kind, the lenses it runs and its effort
The config defines the modes (modes.<name>): the kind of artifact the mode
reviews - reply, series or commits, the three shipped partitions; a kind
is a mode of its own name - the lenses it runs, in its kind's gate order,
and the effort it runs at, else the kind's default (max for a reply or a
series, xhigh for a commit range). `run --mode` names any of them, the
dump records mode and kind, and everything that decided by the mode's
name - the partition and hash, the prompts, the schemas, the disposition
and reuse-key rules, what the CLI requires - decides by the kind.
Every lens stays off until a mode lists it; lenses.<name>.run is retired
with a warning that says so, and a lens of another kind, a name that is
no lens, a lens listed twice and an unknown effort are config problems.
The defaults run no lens; the template ships the three kinds as modes
with every lens of their kind at the kind's effort, to trim (the owner's
ruling of 2026-11-01), and init says so as it writes it - a kept config
is the repository's own, and only one that lists no lens gets a step.
The readers of the retired key follow: the CI smoke config, the shipped
skill, the procedure (its description of the config as well as its
mechanics), the manifest template, the run's messages and the comments
that named the three modes. The run lock, the run numbers and the dump
names are the kind's, so one full run per hash holds across the modes
of a kind and their rounds count on together; the dump says which mode
ran. The database broker starts for a lens the mode runs, as every other
reader of the lenses off reads them. A named mode runs through the CLI
in the kernel-modes test.
2026-11-01 01:31:40 +01:00
|
|
|
// A mode as the config defines it (modes.<name>, 2026-11-01): the
|
|
|
|
|
// artifact kind it reviews, the lens files it runs in the table's order,
|
|
|
|
|
// and the effort it runs at when set. nullopt: no such mode, or one
|
|
|
|
|
// whose kind or lens list cannot be read; lenient on the rest Validate
|
|
|
|
|
// refuses (a lens of another kind dropped, an unknown effort kept),
|
|
|
|
|
// which no command reaches: each stops on the config's problems first.
|
|
|
|
|
struct Mode {
|
|
|
|
|
std::string name;
|
|
|
|
|
std::string kind;
|
|
|
|
|
std::vector<std::string> lensFiles;
|
|
|
|
|
std::optional<std::string> effort;
|
|
|
|
|
};
|
|
|
|
|
std::optional<Mode> ModeOf(const Json& config, std::string_view name);
|
|
|
|
|
// The configured mode names, in the config's order.
|
|
|
|
|
std::vector<std::string> ModeNames(const Json& config);
|
|
|
|
|
// The lens files of the mode's kind that the mode does not list.
|
|
|
|
|
std::vector<std::string> LensesOff(const Json& config, std::string_view mode);
|
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>
2026-10-31 06:48:55 +01:00
|
|
|
// The example a consumer starts from (process/templates/config.json is
|
|
|
|
|
// this, rendered).
|
|
|
|
|
Json Example();
|
|
|
|
|
|
|
|
|
|
// `crafter-review [--config F] config`: the merged config, its warnings
|
|
|
|
|
// and its problems; exit 1 when there is a problem.
|
|
|
|
|
std::int32_t Main(const std::filesystem::path& repo, const std::filesystem::path& path);
|
|
|
|
|
}
|