Crafter.Review/implementations/Crafter.Review-Config.cpp

663 lines
36 KiB
C++
Raw Permalink Normal View History

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®
module Crafter.Review:Config_impl;
import :Config;
import :Json;
import :Text;
import :Core;
import :Plan;
import :Tools;
import :PgWire;
import :Process;
import :Platform;
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
import :Prompt;
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
import std;
namespace fs = std::filesystem;
namespace Crafter::Review::Config {
namespace {
// Keys a consumer's config may still carry, with what replaced them.
struct Retired {
std::string_view section;
std::string_view key;
std::string why;
};
std::vector<Retired> RetiredKeys() {
return {{
"functional", "concurrency",
std::format("passes run as many at once as free memory allows, each at its environment's measured cost, and a pass that drives "
"the application holds a stack of its own (2026-10-22); for more of them at once bring up more stacks "
"(slots.*, `{} slots`)", Invocation()),
}};
}
std::string EnvironmentNames(std::string_view separator) {
std::vector<std::string> names;
for (const Plan::Environment& environment : Plan::Environments) names.emplace_back(environment.name);
return Dyn::Join(names, separator);
}
Json EnvironmentTable() {
Json table = Json::object();
for (const Plan::Environment& environment : Plan::Environments) table[std::string(environment.name)] = true;
return table;
}
bool AllStrings(const Json& list) {
return list.is_array() && std::ranges::all_of(list, [](const Json& item) { return item.is_string(); });
}
// ^(Bash\([^()]+:\*\)|mcp__[A-Za-z0-9_-]+(__[A-Za-z0-9_-]+)?)$; `$`
// also matches before a final newline.
bool IsToolEntry(std::string_view tool) {
std::string_view body = tool;
if (body.ends_with('\n')) body.remove_suffix(1);
if (body.starts_with("Bash(") && body.ends_with(":*)") && body.size() > 8) {
const std::string_view prefix = body.substr(5, body.size() - 8);
return !prefix.contains('(') && !prefix.contains(')');
}
if (body.starts_with("mcp__") && body.size() > 5) {
return std::ranges::all_of(body.substr(5), [](char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-';
});
}
return false;
}
// re.match(r'^https?://', text)
bool IsHttpUrl(std::string_view text) {
return text.starts_with("http://") || text.starts_with("https://");
}
// An integer or a float, not a bool.
bool IsRealNumber(const Json& value) {
return value.is_number();
}
bool Positive(const Json& value) {
return value.is_number_float() ? value.get<double>() > 0.0 : (value.is_number_unsigned() ? value.get<std::uint64_t>() > 0 : value.get<std::int64_t>() > 0);
}
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's defaults: its kind (null for a name that is no kind - the
// config says which), no lens, the kind's effort.
Json ModeDefaults(std::string_view kind) {
return {{"kind", kind.empty() ? Json() : Json(std::string(kind))}, {"lenses", Json::array()}, {"effort", nullptr}};
}
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> Merge(Json& destination, const Json& source, const std::string& path) {
std::vector<std::string> problems;
for (auto it = source.begin(); it != source.end(); ++it) {
const std::string& key = it.key();
if (!Dyn::Has(destination, key)) {
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 of the repository's own name: its defaults, then
// the config's values over them.
if (path == "modes.") {
destination[key] = ModeDefaults("");
} else {
problems.push_back(std::format("unknown key {}{}", path, key));
continue;
}
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
}
Json& target = destination[key];
if (target.is_object() && it.value().is_object()) {
std::ranges::move(Merge(target, it.value(), std::format("{}{}.", path, key)), std::back_inserter(problems));
} else {
target = it.value();
}
}
return problems;
}
// The config's entry for the lens, else its default one.
const Json& LensEntry(const Json& config, std::string_view lensFile) {
static const Json defaults = Defaults()["lenses"];
const Json& lenses = Dyn::Get(config, "lenses");
const std::string name = Plan::LensName(lensFile);
const Json* entry = lenses.is_object() ? &Dyn::Get(lenses, name) : nullptr;
return entry && entry->is_object() ? *entry : defaults.at(name);
}
// A config or a slot file as JSON: the value, or the text that goes
// into the "not JSON (...)" problem.
std::variant<Json, std::string> LoadJson(const fs::path& path) {
try {
return Dyn::LoadJsonFile(path);
} catch (const std::exception& error) {
return std::string(error.what());
}
}
}
Json Defaults() {
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
// Each lens's environment, and its section: a file of the repository's
// own appended to its shipped charter (null: none). Which lenses run is
// the modes'.
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
Json lenses = Json::object();
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
for (const Plan::Lens& lens : Plan::Lenses) lenses[Plan::LensName(lens.file)] = {{"environment", lens.env}, {"section", nullptr}};
// Every lens is off until a mode lists it (2026-10-31, Jorijn: "all
// lenses off by default and opt in only"): the three artifact kinds
// are modes of their own name with no lens; a mode of any name is a
// kind, the lenses it runs and the effort it runs at (2026-11-01).
Json modes = Json::object();
for (const std::string_view kind : Plan::Kinds) modes[std::string(kind)] = ModeDefaults(kind);
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
return {
// The integration branch a range is measured against (e.g.
// origin/dev): merge-base HEAD <base>.
{"base", nullptr},
// upstream: the push delta @{upstream}..HEAD, the base branch only
// when HEAD tracks nothing; base: always merge-base HEAD
// <base>..HEAD, the whole pull request.
{"rangeFrom", "upstream"},
// Author and committer emails the freeze accepts; null: the
// repository's `git config user.email`.
{"authors", nullptr},
// forbid | allow: Assisted-by / Co-Authored-By trailers in the
// range.
{"aiTrailers", "forbid"},
// An unsigned commit fails the freeze (else a warning).
{"requireSigned", false},
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 modes: {<name>: {kind, lenses, effort}}.
{"modes", modes},
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
// Tracked paths the local stack rewrites in place while the lenses
// run.
{"allowDirty", Json::array()},
// The commands Gate 0 runs by hand, in order (documentation for the
// skill; the driver runs none of them).
{"gate0", Json::array()},
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
// Per lens: its environment (Plan::Environments); which lenses
// run is the modes'.
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
{"lenses", lenses},
// The database under test, for every lens whose environment
// reaches it. The URL reaches a db pass's broker, never a prompt,
// and the dump and the log show it with the password masked.
{"supabase", {{"dbUrl", "postgresql://postgres:postgres@127.0.0.1:54322/postgres"}, {"start", nullptr}, {"stop", nullptr}, {"notes", ""}}},
// Stacks side by side (2026-10-22): a pass that drives an
// application holds one of its own.
{"slots", {{"dir", nullptr}, {"start", nullptr}, {"stop", nullptr}, {"memMb", nullptr}, {"startMb", nullptr}}},
// The application under test, for every lens whose environment
// reaches one.
{"functional", {
{"appUrl", "http://localhost:8080"},
{"readyPath", "/"},
{"apiUrls", Json::array()},
{"start", nullptr},
{"stop", nullptr},
{"login", {{"fixtures", Json::array()}, {"notes", ""}}},
{"exclude", Json::array()},
{"extraTools", Json::array()},
{"mcp", {{"command", "npx"}, {"args", nullptr}}},
{"notes", ""},
}},
};
}
Loaded Load(const fs::path& repo, const fs::path& path) {
Loaded loaded{Defaults(), {}};
Json& config = loaded.config;
const fs::path p = path.empty() ? repo / ConfigFile : path;
std::vector<std::string> warnings;
if (fs::exists(p)) {
std::variant<Json, std::string> read = LoadJson(p);
if (std::holds_alternative<std::string>(read)) {
loaded.problems.push_back(std::format("{}: not JSON ({})", p.string(), std::get<std::string>(read)));
return loaded;
}
Json data = std::move(std::get<Json>(read));
if (!data.is_object()) {
loaded.problems.push_back(std::format("{}: the top level must be an object", p.string()));
return loaded;
}
for (const Retired& retired : RetiredKeys()) {
const Json& section = Dyn::Get(data, retired.section);
if (section.is_object() && Dyn::Has(section, retired.key)) {
warnings.push_back(std::format("{}.{} is no longer read - remove it from {}: {}", retired.section, retired.key, p.filename().string(), retired.why));
data[std::string(retired.section)].erase(std::string(retired.key));
}
}
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
// lenses.<name>.run is retired (2026-11-01): a mode lists what runs.
if (const Json& given = Dyn::Get(data, "lenses"); given.is_object()) {
std::vector<std::string> retiredRuns;
for (auto it = given.begin(); it != given.end(); ++it) {
if (it.value().is_object() && Dyn::Has(it.value(), "run")) retiredRuns.push_back(it.key());
}
for (const std::string& name : retiredRuns) {
warnings.push_back(std::format("lenses.{}.run is no longer read - remove it from {}: a mode lists the lenses it runs (modes.<mode>.lenses)", name, p.filename().string()));
data["lenses"][name].erase("run");
}
}
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
loaded.problems = Merge(config, data, "");
} else if (!path.empty()) {
loaded.problems.push_back(std::format("{}: no such file", p.string()));
}
config["_path"] = fs::exists(p) ? Json(p.string()) : Json();
Json warningList = Json::array();
for (const std::string& warning : warnings) warningList.push_back(warning);
config["_warnings"] = std::move(warningList);
std::ranges::move(Validate(config), std::back_inserter(loaded.problems));
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
std::ranges::move(SectionProblems(config, repo), std::back_inserter(loaded.problems));
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
return loaded;
}
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
namespace {
// A path setting resolved against the repository root (slots.dir,
// lenses.<name>.section): a relative path that stays inside it - not
// blank, no leading `/`, no `..` part (2026-11-01: one check, so the
// settings cannot drift apart).
bool InsideRepository(const Json& value) {
if (!value.is_string()) return false;
const std::string& dir = value.get_ref<const std::string&>();
const std::vector<std::string> parts = Dyn::Split(Dyn::Replace(dir, "\\", "/"), "/");
return !Dyn::Strip(dir).empty() && !dir.starts_with('/') && !std::ranges::contains(parts, std::string(".."));
}
}
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> Validate(const Json& config) {
std::vector<std::string> out;
const Json& trailers = Dyn::At(config, "aiTrailers");
if (trailers != "forbid" && trailers != "allow") out.emplace_back("aiTrailers must be 'forbid' or 'allow'");
const Json& rangeFrom = Dyn::At(config, "rangeFrom");
if (rangeFrom != "upstream" && rangeFrom != "base") out.emplace_back("rangeFrom must be 'upstream' or 'base'");
if (rangeFrom == "base" && !Dyn::Truthy(Dyn::At(config, "base"))) out.emplace_back("rangeFrom 'base' needs a base branch");
const Json& authors = Dyn::At(config, "authors");
if (!authors.is_null() && (!authors.is_array() || !std::ranges::all_of(authors, [](const Json& a) {
return a.is_string() && a.get_ref<const std::string&>().contains('@');
}))) {
out.emplace_back("authors must be null or a list of email addresses");
}
for (const std::string_view key : {"allowDirty", "gate0"}) {
if (!AllStrings(Dyn::At(config, key))) out.push_back(std::format("{} must be a list of strings", key));
}
const Json environments = EnvironmentTable();
Json lenses = Dyn::At(config, "lenses");
if (!lenses.is_object()) {
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
out.push_back(std::format("lenses must be an object: {{<lens name>: {{environment: {}}}}}", EnvironmentNames("|")));
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
lenses = Json::object();
}
for (auto it = lenses.begin(); it != lenses.end(); ++it) {
const Json& entry = it.value();
if (!entry.is_object()) {
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
out.push_back(std::format("lenses.{} must be an object {{environment: {}}}", it.key(), EnvironmentNames("|")));
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
continue;
}
if (!Dyn::In(Dyn::Get(entry, "environment"), environments)) out.push_back(std::format("lenses.{}.environment must be one of {}", it.key(), EnvironmentNames(", ")));
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
const Json& section = Dyn::Get(entry, "section");
if (!section.is_null() && !InsideRepository(section)) out.push_back(std::format("lenses.{}.section must be null or a file inside the repository (a relative path)", it.key()));
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
}
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 modes (2026-11-01): each an artifact kind and the lenses it
// runs, at an effort of its own when set. Every lens is off until a
// mode lists it.
auto joined = [](std::span<const std::string_view> items, std::string_view separator) {
std::vector<std::string> strings(items.begin(), items.end());
return Dyn::Join(strings, separator);
};
std::set<std::string> listed;
const Json& modes = Dyn::At(config, "modes");
if (!modes.is_object()) {
out.push_back(std::format("modes must be an object: {{<mode name>: {{kind: {}, lenses: [<lens name>, ...], effort: {}|null}}}}", joined(Plan::Kinds, "|"), joined(Plan::Efforts, "|")));
} else {
for (auto it = modes.begin(); it != modes.end(); ++it) {
const std::string& name = it.key();
const Json& mode = it.value();
if (!mode.is_object()) {
out.push_back(std::format("modes.{} must be an object {{kind, lenses, effort}}", name));
continue;
}
const Json& kindValue = Dyn::Get(mode, "kind");
std::string kind;
if (std::ranges::contains(Plan::Kinds, std::string_view(name))) {
if (!kindValue.is_null() && kindValue != name) out.push_back(std::format("modes.{}.kind must be {} (the mode of a kind's name is that kind) or null", name, name));
kind = name;
} else if (kindValue.is_string() && std::ranges::contains(Plan::Kinds, std::string_view(kindValue.get_ref<const std::string&>()))) {
kind = kindValue.get<std::string>();
} else {
out.push_back(std::format("modes.{}.kind must name the artifact kind the mode reviews ({})", name, joined(Plan::Kinds, ", ")));
}
const Json& lensList = Dyn::Get(mode, "lenses");
if (!AllStrings(lensList)) {
out.push_back(std::format("modes.{}.lenses must be a list of lens names", name));
} else if (!kind.empty()) {
const std::vector<std::string> ofKind = Plan::KindLenses(kind);
std::vector<std::string> ofKindNames;
for (const std::string& lf : ofKind) ofKindNames.push_back(Plan::LensName(lf));
std::set<std::string> seen;
for (const Json& item : lensList) {
const std::string lf = std::format("{}.md", item.get<std::string>());
if (!std::ranges::contains(ofKind, lf)) {
out.push_back(std::format("modes.{}: {} is no lens of a {} ({})", name, Dyn::Str(item), kind, Dyn::Join(ofKindNames, ", ")));
} else if (!seen.insert(lf).second) {
out.push_back(std::format("modes.{}: {} is listed twice", name, Dyn::Str(item)));
} else {
listed.insert(lf);
}
}
}
const Json& effort = Dyn::Get(mode, "effort");
if (!effort.is_null() && !(effort.is_string() && std::ranges::contains(Plan::Efforts, std::string_view(effort.get_ref<const std::string&>())))) {
out.push_back(std::format("modes.{}.effort must be one of {} or null (the kind's default: {} for a reply or a series, {} for a commit range)", name, joined(Plan::Efforts, ", "),
Plan::KernelEffort, Plan::Effort));
}
}
if (listed.empty()) {
std::vector<std::string> names;
for (const Plan::Lens& lens : Plan::Lenses) names.push_back(Plan::LensName(lens.file));
out.push_back(std::format("modes: no mode lists a lens - every lens is off until a mode lists it in modes.<mode>.lenses ({})", Dyn::Join(names, ", ")));
}
}
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
auto runs = [&](std::string_view lensFile) { return listed.contains(std::string(lensFile)); };
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
for (const Plan::Lens& lens : Plan::Lenses) {
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
if (!lens.empirical || !runs(lens.file)) continue;
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
const Json& entry = Dyn::Get(lenses, Plan::LensName(lens.file));
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
if (!entry.is_object()) continue;
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
const Json& name = Dyn::Get(entry, "environment");
if (!Dyn::In(name, environments)) continue;
if (!Plan::FindEnvironment(name.get_ref<const std::string&>())->app) {
std::vector<std::string> withApp;
for (const Plan::Environment& environment : Plan::Environments) {
if (environment.app) withApp.emplace_back(environment.name);
}
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
out.push_back(std::format("lenses.{}: an empirical lens needs an environment with an application under test ({}) - or no mode lists it", Plan::LensName(lens.file),
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
Dyn::Join(withApp, ", ")));
}
}
const Json& supabase = Dyn::At(config, "supabase");
if (!supabase.is_object()) {
out.emplace_back("supabase must be an object {dbUrl, start, stop, notes}");
} else {
if (const std::optional<std::string> problem = PgWire::CheckUrl(Dyn::Get(supabase, "dbUrl"))) out.push_back(std::format("supabase.dbUrl: {}", *problem));
for (const std::string_view key : {"start", "stop"}) {
const Json& value = Dyn::Get(supabase, key);
if (!value.is_null() && !value.is_string()) out.push_back(std::format("supabase.{} must be null or a command line", key));
}
if (!Dyn::Get(supabase, "notes").is_string()) out.emplace_back("supabase.notes must be text");
}
const Json& functional = Dyn::At(config, "functional");
if (!functional.is_object()) {
out.emplace_back("functional must be an object");
return out;
}
// The application's URL matters only to a lens that drives it.
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
const bool drivesApp = std::ranges::any_of(Plan::Lenses, [&](const Plan::Lens& lens) {
if (!runs(lens.file)) return false;
const Json& entry = Dyn::Get(lenses, Plan::LensName(lens.file));
if (!entry.is_object()) return false;
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
const Json& name = Dyn::Get(entry, "environment");
if (name.is_array() || name.is_object()) throw Dyn::Error("TypeError", std::format("unhashable type: '{}'", Dyn::TypeName(name)));
const Plan::Environment* environment = name.is_string() ? Plan::FindEnvironment(name.get_ref<const std::string&>()) : nullptr;
return environment && environment->app;
});
if (drivesApp && !IsHttpUrl(Dyn::StrOr(Dyn::At(functional, "appUrl"), ""))) out.emplace_back("functional.appUrl must be an http(s) URL");
if (!AllStrings(Dyn::At(functional, "exclude"))) out.emplace_back("functional.exclude must be a list of globs");
const Json& extraTools = Dyn::At(functional, "extraTools");
if (!extraTools.is_array() || !std::ranges::all_of(extraTools, [](const Json& x) { return x.is_string() && IsToolEntry(x.get_ref<const std::string&>()); })) {
out.emplace_back("functional.extraTools entries must be Bash(<prefix>:*) or mcp__<server>[__<tool>]");
} else {
std::vector<std::string> dead;
for (const Json& tool : extraTools) {
if (Tools::Denied(tool.get_ref<const std::string&>())) dead.push_back(tool.get<std::string>());
}
if (!dead.empty()) {
out.push_back(std::format("functional.extraTools: {} - denied to every pass (pool.DENY wins over an allowlist); "
"a lens reaches the database through the supabase environment, every call rolled back", Dyn::Join(dead, ", ")));
}
}
const Json& login = Dyn::At(functional, "login");
if (!login.is_object() || !(Dyn::Has(login, "fixtures") ? Dyn::At(login, "fixtures").is_array() : true)) out.emplace_back("functional.login must be {fixtures: [paths], notes: text}");
const Json& slots = Dyn::At(config, "slots");
if (!slots.is_object()) {
out.emplace_back("slots must be an object {dir, start, stop, memMb, startMb}");
} else if (!Dyn::Get(slots, "dir").is_null()) {
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
if (!InsideRepository(Dyn::At(slots, "dir"))) out.emplace_back("slots.dir must be null or a directory inside the repository (a relative path)");
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
for (const std::string_view key : {"start", "stop"}) {
const Json& value = Dyn::Get(slots, key);
if (!value.is_null() && (!value.is_string() || !value.get_ref<const std::string&>().contains("{n}"))) {
out.push_back(std::format("slots.{} must be null or a command line with {{n}}, the slot number", key));
}
}
const Json& memMb = Dyn::Get(slots, "memMb");
if (!IsRealNumber(memMb) || !Positive(memMb)) {
out.push_back(std::format("slots.memMb must be the measured memory of one slot in MB (a number > 0): `{} slots` sizes the slots by it", Invocation()));
}
const Json& startMb = Dyn::Get(slots, "startMb");
if (!startMb.is_null() && (!IsRealNumber(startMb) || !Positive(startMb))) {
out.emplace_back("slots.startMb must be null or the measured peak of one slot coming up in MB (a number > 0)");
}
}
const Json& args = Dyn::Get(Dyn::At(functional, "mcp"), "args");
if (!args.is_null() && !AllStrings(args)) out.emplace_back("functional.mcp.args must be null or a list of strings");
// A path found on the host, such as /usr/bin/npx, is not there inside
// the Flatpak, whose /usr is the Sdk's.
const Json& command = Dyn::Get(Dyn::At(functional, "mcp"), "command");
if (command.is_string() && command.get<std::string>().starts_with('/')) {
std::error_code error;
const fs::path file(command.get<std::string>());
const bool executable = fs::is_regular_file(file, error) && (fs::status(file, error).permissions() & (fs::perms::owner_exec | fs::perms::group_exec | fs::perms::others_exec)) != fs::perms::none;
if (!executable) {
out.emplace_back(std::format("functional.mcp.command {} is no program here{}", Dyn::Repr(command.get<std::string>()),
Platform::InSandbox() ? " (inside the Flatpak /usr is the Sdk's: name it `npx`, the bundled one)" : ""));
}
}
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
return out;
}
Json Redacted(const Json& config) {
Json copy = Json::object();
for (auto it = config.begin(); it != config.end(); ++it) {
if (!it.key().starts_with('_')) copy[it.key()] = it.value();
}
if (Dyn::Get(copy, "supabase").is_object() && Dyn::Get(copy["supabase"], "dbUrl").is_string()) {
copy["supabase"]["dbUrl"] = PgWire::Redact(copy["supabase"]["dbUrl"].get<std::string>());
}
return copy;
}
std::string GitUserEmail(const fs::path& repo) {
const Process::Captured run = Process::Run({"git", "-C", repo.string(), "config", "user.email"});
return run.returnCode == 0 ? Dyn::Strip(Dyn::UniversalNewlines(Dyn::DecodeReplace(run.out))) : std::string();
}
std::vector<std::string> Authors(const Json& config, const fs::path& repo) {
const Json& authors = Dyn::Get(config, "authors");
if (Dyn::Truthy(authors)) {
std::vector<std::string> out;
for (const Json& author : authors) out.push_back(Dyn::Str(author));
return out;
}
const std::string email = GitUserEmail(repo);
if (email.empty()) return {};
return {email};
}
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
std::map<std::string, fs::path> LensSectionsOf(const Json& config, const fs::path& repo) {
// Only a file inside the repository, by the rule Validate refuses the
// rest by: fs::path's `/` would let an absolute one replace the root.
std::map<std::string, fs::path> out;
for (const Plan::Lens& lens : Plan::Lenses) {
const Json& section = Dyn::Get(LensEntry(config, lens.file), "section");
if (InsideRepository(section)) out.emplace(std::string(lens.file), repo / section.get<std::string>());
}
return out;
}
std::vector<std::string> SectionProblems(const Json& config, const fs::path& repo) {
// Through the reader the run itself uses (Prompt::ReadSection): a
// section the run could not use is a problem here, never a section
// left out in silence or an error in the run's Gate 0.
std::vector<std::string> out;
for (const auto& [lensFile, file] : LensSectionsOf(config, repo)) {
const std::string where = std::format("lenses.{}.section: {}", Plan::LensName(lensFile), Dyn::Str(Dyn::Get(LensEntry(config, lensFile), "section")));
const Prompt::Section section = Prompt::ReadSection(file);
if (!section.problem.empty()) out.push_back(std::format("{} {}", where, section.problem));
else if (!section.present) out.push_back(std::format("{} does not exist", where));
}
return out;
}
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
std::optional<Mode> ModeOf(const Json& config, std::string_view name) {
const Json& modes = Dyn::Get(config, "modes");
if (!modes.is_object() || !Dyn::Has(modes, name)) return std::nullopt;
const Json& mode = Dyn::At(modes, name);
if (!mode.is_object()) return std::nullopt;
Mode out{std::string(name), {}, {}, std::nullopt};
const Json& kind = Dyn::Get(mode, "kind");
if (std::ranges::contains(Plan::Kinds, name)) {
out.kind = std::string(name);
} else if (kind.is_string() && std::ranges::contains(Plan::Kinds, std::string_view(kind.get_ref<const std::string&>()))) {
out.kind = kind.get<std::string>();
} else {
return std::nullopt;
}
const Json& list = Dyn::Get(mode, "lenses");
if (!AllStrings(list)) return std::nullopt;
std::set<std::string> wanted;
for (const Json& item : list) wanted.insert(std::format("{}.md", item.get<std::string>()));
for (const std::string& lf : Plan::KindLenses(out.kind)) {
if (wanted.contains(lf)) out.lensFiles.push_back(lf);
}
const Json& effort = Dyn::Get(mode, "effort");
if (effort.is_string() && !effort.get_ref<const std::string&>().empty()) out.effort = effort.get<std::string>();
return out;
}
std::vector<std::string> ModeNames(const Json& config) {
std::vector<std::string> out;
const Json& modes = Dyn::Get(config, "modes");
if (!modes.is_object()) return out;
for (auto it = modes.begin(); it != modes.end(); ++it) out.push_back(it.key());
return out;
}
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) {
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
std::set<std::string> listed;
for (const std::string& name : ModeNames(config)) {
if (const std::optional<Mode> mode = ModeOf(config, name)) listed.insert(mode->lensFiles.begin(), mode->lensFiles.end());
}
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> out;
for (const Plan::Lens& lens : Plan::Lenses) {
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
if (!listed.contains(std::string(lens.file))) out.emplace_back(lens.file);
}
return out;
}
std::vector<std::string> LensesOff(const Json& config, std::string_view modeName) {
std::vector<std::string> out;
const std::optional<Mode> mode = ModeOf(config, modeName);
if (!mode) return out;
for (const std::string& lf : Plan::KindLenses(mode->kind)) {
if (!std::ranges::contains(mode->lensFiles, lf)) out.push_back(lf);
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
}
return out;
}
Json Environments(const Json& config) {
Json out = Json::object();
for (const Plan::Lens& lens : Plan::Lenses) {
const Json& entry = LensEntry(config, lens.file);
out[std::string(lens.file)] = Dyn::Has(entry, "environment") ? Dyn::At(entry, "environment") : Json("none");
}
return out;
}
Slots SlotStacks(const Json& config, const fs::path& repo) {
Slots slots;
const Json& dir = Dyn::Get(Dyn::Or(Dyn::Get(config, "slots"), Dyn::EmptyDict()), "dir");
if (!Dyn::Truthy(dir) || !fs::is_directory(repo / dir.get<std::string>())) return slots;
const std::string d = dir.get<std::string>();
std::vector<std::string> names;
for (const fs::directory_entry& entry : fs::directory_iterator(repo / d)) names.push_back(entry.path().filename().string());
std::ranges::stable_sort(names, {}, [](const std::string& name) { return Dyn::Natural(name); });
for (const std::string& n : names) {
const fs::path file = repo / d / n / SlotFile;
if (!fs::is_regular_file(file)) continue;
const std::string rel = Dyn::JoinPath(Dyn::JoinPath(d, n), SlotFile);
std::variant<Json, std::string> read = LoadJson(file);
if (std::holds_alternative<std::string>(read)) {
slots.problems.push_back(std::format("{}: not JSON ({})", rel, std::get<std::string>(read)));
continue;
}
const Json& s = std::get<Json>(read);
if (!s.is_object() || !IsHttpUrl(Dyn::StrOr(Dyn::Get(s, "appUrl"), ""))) {
slots.problems.push_back(std::format("{}: no appUrl", rel));
continue;
}
const Json& api = Dyn::Or(Dyn::Get(s, "apiUrls"), Dyn::EmptyList());
if (!AllStrings(api)) {
slots.problems.push_back(std::format("{}: apiUrls must be a list of URLs", rel));
continue;
}
const Json& db = Dyn::Get(s, "dbUrl");
if (!db.is_null()) {
if (const std::optional<std::string> problem = PgWire::CheckUrl(db)) {
slots.problems.push_back(std::format("{}: dbUrl: {}", rel, *problem));
continue;
}
}
const Json& login = Dyn::Get(s, "login");
if (!login.is_null() && (!login.is_string() || !fs::exists(repo / login.get<std::string>()))) {
slots.problems.push_back(std::format("{}: login {} is not a file", rel, Dyn::ReprValue(login)));
continue;
}
slots.stacks.push_back({
{"id", std::format("slot {}", n)},
{"appUrl", Dyn::RStrip(Dyn::At(s, "appUrl").get<std::string>(), "/")},
{"apiUrls", api},
{"dbUrl", db},
{"login", login},
{"slot", true},
});
}
return slots;
}
std::vector<std::string> SlotCommands(const Json& config, std::string_view which, std::span<const std::int64_t> slots) {
const Json& command = Dyn::Get(Dyn::Or(Dyn::Get(config, "slots"), Dyn::EmptyDict()), which);
if (!Dyn::Truthy(command)) return {};
std::vector<std::string> out;
for (const std::int64_t n : slots) out.push_back(Dyn::Replace(command.get<std::string>(), "{n}", std::to_string(n)));
return out;
}
std::optional<std::string> LoginFixture(const Json& config, const fs::path& repo) {
const Json& login = Dyn::Or(Dyn::Get(Dyn::At(config, "functional"), "login"), Dyn::EmptyDict());
for (const Json& fixture : Dyn::Or(Dyn::Get(login, "fixtures"), Dyn::EmptyList())) {
if (fs::exists(repo / fixture.get<std::string>())) return fixture.get<std::string>();
}
return std::nullopt;
}
Json Example() {
Json example = Defaults();
example["base"] = "origin/main";
example["authors"] = {"you@example.com"};
example["aiTrailers"] = "forbid";
example["gate0"] = {"make check"};
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 three kinds as modes, each with every lens of its kind at the
// kind's effort: trim each to what the repository runs.
for (const std::string_view kind : Plan::Kinds) {
Json names = Json::array();
for (const std::string& lf : Plan::KindLenses(kind)) names.push_back(Plan::LensName(lf));
example["modes"][std::string(kind)]["lenses"] = names;
example["modes"][std::string(kind)]["effort"] = std::string(Plan::DefaultEffort(kind));
}
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
Json& functional = example["functional"];
functional["appUrl"] = "http://localhost:8080";
functional["start"] = "make run";
functional["stop"] = "make stop";
functional["login"] = {{"fixtures", {".local-stack/test-user.json"}}, {"notes", "the fixture holds a TOTP-verified login"}};
functional["exclude"] = {"**/*.test.*", "**/*.spec.*", "docs/**", ".github/**", "*.md"};
return example;
}
std::int32_t Main(const fs::path& repo, const fs::path& path) {
const Loaded loaded = Load(repo, path);
Json shown = Redacted(loaded.config);
// The database password masked, as in the dump.
shown["_path"] = Dyn::Get(loaded.config, "_path");
std::println("{}", DumpFile(shown));
for (const Json& warning : Dyn::Or(Dyn::Get(loaded.config, "_warnings"), Dyn::EmptyList())) std::println("warning: {}", Dyn::Str(warning));
for (const std::string& problem : loaded.problems) std::println("problem: {}", problem);
return loaded.problems.empty() ? 0 : 1;
}
}