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.
This commit is contained in:
parent
6cf15919db
commit
d5e54ffeb3
25 changed files with 1277 additions and 711 deletions
|
|
@ -81,13 +81,25 @@ namespace Crafter::Review::Config {
|
|||
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);
|
||||
}
|
||||
|
||||
// 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}};
|
||||
}
|
||||
|
||||
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)) {
|
||||
problems.push_back(std::format("unknown key {}{}", path, key));
|
||||
continue;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
Json& target = destination[key];
|
||||
if (target.is_object() && it.value().is_object()) {
|
||||
|
|
@ -120,12 +132,17 @@ namespace Crafter::Review::Config {
|
|||
}
|
||||
|
||||
Json Defaults() {
|
||||
// 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'.
|
||||
Json lenses = Json::object();
|
||||
// Every lens is off until the repository's config turns it on
|
||||
// (2026-10-31, Jorijn: "all lenses off by default and opt in only");
|
||||
// its section is a file of the repository's own appended to its
|
||||
// shipped charter (null: none).
|
||||
for (const Plan::Lens& lens : Plan::Lenses) lenses[Plan::LensName(lens.file)] = {{"run", false}, {"environment", lens.env}, {"section", nullptr}};
|
||||
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);
|
||||
return {
|
||||
// The integration branch a range is measured against (e.g.
|
||||
// origin/dev): merge-base HEAD <base>.
|
||||
|
|
@ -142,14 +159,16 @@ namespace Crafter::Review::Config {
|
|||
{"aiTrailers", "forbid"},
|
||||
// An unsigned commit fails the freeze (else a warning).
|
||||
{"requireSigned", false},
|
||||
// The modes: {<name>: {kind, lenses, effort}}.
|
||||
{"modes", modes},
|
||||
// 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()},
|
||||
// Per lens: run (off until true: every lens is opt-in) and
|
||||
// environment (Plan::Environments).
|
||||
// Per lens: its environment (Plan::Environments); which lenses
|
||||
// run is the modes'.
|
||||
{"lenses", lenses},
|
||||
// The database under test, for every lens whose environment
|
||||
// reaches it. The URL reaches a db pass's broker, never a prompt,
|
||||
|
|
@ -198,6 +217,17 @@ namespace Crafter::Review::Config {
|
|||
data[std::string(retired.section)].erase(std::string(retired.key));
|
||||
}
|
||||
}
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
loaded.problems = Merge(config, data, "");
|
||||
} else if (!path.empty()) {
|
||||
loaded.problems.push_back(std::format("{}: no such file", p.string()));
|
||||
|
|
@ -243,31 +273,84 @@ namespace Crafter::Review::Config {
|
|||
const Json environments = EnvironmentTable();
|
||||
Json lenses = Dyn::At(config, "lenses");
|
||||
if (!lenses.is_object()) {
|
||||
out.push_back(std::format("lenses must be an object: {{<lens name>: {{run: bool, environment: {}}}}}", EnvironmentNames("|")));
|
||||
out.push_back(std::format("lenses must be an object: {{<lens name>: {{environment: {}}}}}", EnvironmentNames("|")));
|
||||
lenses = Json::object();
|
||||
}
|
||||
for (auto it = lenses.begin(); it != lenses.end(); ++it) {
|
||||
const Json& entry = it.value();
|
||||
if (!entry.is_object()) {
|
||||
out.push_back(std::format("lenses.{} must be an object {{run: bool, environment: {}}}", it.key(), EnvironmentNames("|")));
|
||||
out.push_back(std::format("lenses.{} must be an object {{environment: {}}}", it.key(), EnvironmentNames("|")));
|
||||
continue;
|
||||
}
|
||||
if (!Dyn::Get(entry, "run").is_boolean()) out.push_back(std::format("lenses.{}.run must be true or false", it.key()));
|
||||
if (!Dyn::In(Dyn::Get(entry, "environment"), environments)) out.push_back(std::format("lenses.{}.environment must be one of {}", it.key(), EnvironmentNames(", ")));
|
||||
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()));
|
||||
}
|
||||
auto runs = [](const Json& entry) { return entry.is_object() && Dyn::Get(entry, "run") == true; };
|
||||
// Every lens is off until the repository turns it on: a config that
|
||||
// turns on none has nothing to run.
|
||||
if (std::ranges::none_of(Plan::Lenses, [&](const Plan::Lens& lens) { return runs(Dyn::Get(lenses, Plan::LensName(lens.file))); })) {
|
||||
std::vector<std::string> names;
|
||||
for (const Plan::Lens& lens : Plan::Lenses) names.push_back(Plan::LensName(lens.file));
|
||||
out.push_back(std::format("lenses: no lens is on - every lens is off until lenses.<name>.run = true turns it on for this repository ({})", Dyn::Join(names, ", ")));
|
||||
// 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, ", ")));
|
||||
}
|
||||
}
|
||||
auto runs = [&](std::string_view lensFile) { return listed.contains(std::string(lensFile)); };
|
||||
for (const Plan::Lens& lens : Plan::Lenses) {
|
||||
if (!lens.empirical || !runs(lens.file)) continue;
|
||||
const Json& entry = Dyn::Get(lenses, Plan::LensName(lens.file));
|
||||
if (!lens.empirical || !runs(entry)) continue;
|
||||
if (!entry.is_object()) continue;
|
||||
const Json& name = Dyn::Get(entry, "environment");
|
||||
if (!Dyn::In(name, environments)) continue;
|
||||
if (!Plan::FindEnvironment(name.get_ref<const std::string&>())->app) {
|
||||
|
|
@ -275,7 +358,7 @@ namespace Crafter::Review::Config {
|
|||
for (const Plan::Environment& environment : Plan::Environments) {
|
||||
if (environment.app) withApp.emplace_back(environment.name);
|
||||
}
|
||||
out.push_back(std::format("lenses.{}: an empirical lens needs an environment with an application under test ({}) - or turn it off", Plan::LensName(lens.file),
|
||||
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),
|
||||
Dyn::Join(withApp, ", ")));
|
||||
}
|
||||
}
|
||||
|
|
@ -296,8 +379,10 @@ namespace Crafter::Review::Config {
|
|||
return out;
|
||||
}
|
||||
// The application's URL matters only to a lens that drives it.
|
||||
const bool drivesApp = std::ranges::any_of(lenses, [&](const Json& entry) {
|
||||
if (!runs(entry)) return false;
|
||||
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;
|
||||
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;
|
||||
|
|
@ -410,10 +495,58 @@ namespace Crafter::Review::Config {
|
|||
return out;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
std::vector<std::string> LensesOff(const Json& config) {
|
||||
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());
|
||||
}
|
||||
std::vector<std::string> out;
|
||||
for (const Plan::Lens& lens : Plan::Lenses) {
|
||||
if (Dyn::Get(LensEntry(config, lens.file), "run") == false) out.emplace_back(lens.file);
|
||||
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);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
@ -500,6 +633,14 @@ namespace Crafter::Review::Config {
|
|||
example["authors"] = {"you@example.com"};
|
||||
example["aiTrailers"] = "forbid";
|
||||
example["gate0"] = {"make check"};
|
||||
// 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));
|
||||
}
|
||||
Json& functional = example["functional"];
|
||||
functional["appUrl"] = "http://localhost:8080";
|
||||
functional["start"] = "make run";
|
||||
|
|
|
|||
|
|
@ -267,12 +267,11 @@ inventory unit, against a frozen commit range.)";
|
|||
{"--verify", false, "", "push-time check: HEAD, a clean tree and a fresh export still match freeze.json"},
|
||||
}};
|
||||
|
||||
constexpr std::array<std::string_view, 3> Modes = {"reply", "series", "commits"};
|
||||
constexpr std::array<std::string_view, 2> Harnesses = {"claude", "opencode"};
|
||||
constexpr std::array<std::string_view, 2> PersonaBlocks = {"pruned", "all"};
|
||||
|
||||
const std::vector<OptionSpec>& RunOptions() {
|
||||
static const std::string effort = std::format("reasoning effort of every lens process (default {})", Plan::Effort);
|
||||
static const std::string effort = std::format("reasoning effort of every lens process (default: the mode's, else {} for a reply or a series and {} for a commit range)", Plan::KernelEffort, Plan::Effort);
|
||||
static const std::string reads =
|
||||
std::format("clean reads a code unit of the static code lenses needs before its verdict is reused, one per round (default {})", Plan::Reads);
|
||||
static const std::string models = [] {
|
||||
|
|
@ -282,7 +281,7 @@ inventory unit, against a frozen commit range.)";
|
|||
}();
|
||||
static const std::string variant = std::format("opencode harness: the model's reasoning variant (default {})", OpenCode::DefaultVariant);
|
||||
static const std::vector<OptionSpec> specs = {
|
||||
{"--mode", true, "MODE", "", Kind::String, Modes, false, true},
|
||||
{"--mode", true, "MODE", "a mode of the config (modes.<name>: its artifact kind, the lenses it runs, its effort; default commits)"},
|
||||
{"--subsystem", true, "SUBSYSTEM", "journal subsystem: required for reply and series; optional in commits mode (names the ledger and captures the lenses may read)"},
|
||||
{"--artifact", true, "ARTIFACT", "the frozen artifact: a dir of *.patch (.review-gates/push/<slug>/, a series), or the reply draft file"},
|
||||
{"--hash", true, "HASH", ""},
|
||||
|
|
@ -336,6 +335,12 @@ inventory unit, against a frozen commit range.)";
|
|||
Run::Arguments a;
|
||||
a.config = configPath;
|
||||
a.mode = parsed.Value("mode").value_or("commits");
|
||||
const std::optional<Config::Mode> mode = Config::ModeOf(*config, a.mode);
|
||||
if (!mode) {
|
||||
return Die(std::format("--mode {}: no such mode in the config (modes: {}) - a mode is an artifact kind and the lenses it runs, modes.<name> in .review-gates/config.json", a.mode,
|
||||
Dyn::Join(Config::ModeNames(*config), ", ")));
|
||||
}
|
||||
a.kind = mode->kind;
|
||||
a.subsystem = parsed.Value("subsystem");
|
||||
a.artifact = parsed.Value("artifact").value_or("");
|
||||
if (a.artifact.empty()) return UsageError(UsageLine(Prog, specs), Prog, "the following arguments are required: --artifact");
|
||||
|
|
@ -369,8 +374,8 @@ inventory unit, against a frozen commit range.)";
|
|||
if (*a.concurrency < 1) return UsageError(UsageLine(Prog, specs), Prog, "argument --concurrency: a number from 1 up (leave it out for no cap)");
|
||||
}
|
||||
a.effort = parsed.Value("effort").value_or("");
|
||||
if (a.effort.empty()) a.effort = std::string(Plan::DefaultEffort(a.mode));
|
||||
if (a.mode == "commits") {
|
||||
if (a.effort.empty()) a.effort = mode->effort.value_or(std::string(Plan::DefaultEffort(a.kind)));
|
||||
if (a.kind == "commits") {
|
||||
if (!a.reviewers.empty()) return Die("commits mode has no persona gate (the owner is the last gate): drop --reviewers");
|
||||
} else {
|
||||
if (!a.subsystem || a.subsystem->empty()) return Die(std::format("{} mode: --subsystem is required", a.mode));
|
||||
|
|
@ -391,10 +396,10 @@ inventory unit, against a frozen commit range.)";
|
|||
a.variant = parsed.Value("variant");
|
||||
if (!a.variant || a.variant->empty()) a.variant = a.harness == "opencode" ? std::optional<std::string>(std::string(OpenCode::DefaultVariant)) : std::nullopt;
|
||||
a.opencodeBin = parsed.Value("opencode-bin");
|
||||
a.reads = Plan::ReadsFor(a.mode);
|
||||
a.reads = Plan::ReadsFor(a.kind);
|
||||
if (const std::optional<std::string> reads = parsed.Value("reads")) {
|
||||
const std::int64_t n = *Dyn::ParseInt(*reads);
|
||||
if (n < 1 || (a.mode == "reply" && n != 1)) return Die("--reads: a number from 1 up; a reply has no code units and is read once");
|
||||
if (n < 1 || (a.kind == "reply" && n != 1)) return Die("--reads: a number from 1 up; a reply has no code units and is read once");
|
||||
a.reads = n;
|
||||
}
|
||||
for (const std::string& part : Dyn::Split(a.models, ",")) {
|
||||
|
|
@ -857,7 +862,7 @@ inventory unit, against a frozen commit range.)";
|
|||
} else {
|
||||
fs::create_directories(config.parent_path());
|
||||
std::ofstream(config, std::ios::binary) << Platform::ReadFile(Home() / "process" / "templates" / "config.json").value_or("{}\n");
|
||||
std::println(" wrote .review-gates/config.json (the template: every lens off - turn on the ones this repository runs, set base, then what their environments need)");
|
||||
std::println(" wrote .review-gates/config.json (the template: the three kinds as modes with every lens of their kind - trim each to what this repository runs, set base, then what the lenses' environments need)");
|
||||
}
|
||||
// The artifacts and the worktrees stay out of git.
|
||||
const fs::path ignore = cwd / ".gitignore";
|
||||
|
|
@ -902,7 +907,10 @@ inventory unit, against a frozen commit range.)";
|
|||
if (!std::ranges::contains(off, it.key()) && environment && !environment->mcp.empty()) browser = true;
|
||||
}
|
||||
std::println("\nnext:\n {} config - until it reports no problem", Invocation());
|
||||
if (off.size() == Plan::Lenses.size()) std::println(" turn lenses on in .review-gates/config.json (lenses.<name>.run = true) - every lens is off until the repository says which run");
|
||||
// Only a config that runs no lens has a lens step left: the
|
||||
// template init wrote says to trim itself, on its own line, and
|
||||
// a kept config that lists lenses is the repository's own.
|
||||
if (off.size() == Plan::Lenses.size()) std::println(" list the lenses each mode runs in .review-gates/config.json (modes.<mode>.lenses) - every lens is off until a mode lists it");
|
||||
if (browser) std::println(" {} browser install - a lens drives the application in Chromium", Invocation());
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,12 +187,12 @@ namespace Crafter::Review::Plan {
|
|||
throw Dyn::Error("KeyError", Dyn::Repr(tier));
|
||||
}
|
||||
|
||||
bool IsKernelMode(std::string_view mode) {
|
||||
return mode == "reply" || mode == "series";
|
||||
bool IsKernelKind(std::string_view kind) {
|
||||
return kind == "reply" || kind == "series";
|
||||
}
|
||||
|
||||
std::int32_t ReadsFor(std::string_view mode) {
|
||||
return mode == "reply" ? ReplyReads : Reads;
|
||||
std::int32_t ReadsFor(std::string_view kind) {
|
||||
return kind == "reply" ? ReplyReads : Reads;
|
||||
}
|
||||
|
||||
bool IsLens(std::string_view file) {
|
||||
|
|
@ -206,7 +206,7 @@ namespace Crafter::Review::Plan {
|
|||
throw Dyn::Error("KeyError", Dyn::Repr(file));
|
||||
}
|
||||
|
||||
const std::vector<Gate>& Gates(std::string_view mode) {
|
||||
const std::vector<Gate>& Gates(std::string_view kind) {
|
||||
static const std::vector<Gate> commits = {
|
||||
{"Gate 1 — Approach", {"repo-better-solution.md"}},
|
||||
{"Gate 2 — Functional", {FunctionalLens}},
|
||||
|
|
@ -221,10 +221,10 @@ namespace Crafter::Review::Plan {
|
|||
{"Gate 2 — Code", {"code-quality.md", "minimalism.md", "bug-prover.md"}},
|
||||
{"Gate 3 — Artifact truth", {"facts.md", "consistency.md", "process-docs.md", "etiquette.md", "conciseness.md"}},
|
||||
};
|
||||
if (mode == "reply") return reply;
|
||||
if (mode == "series") return series;
|
||||
if (mode == "commits") return commits;
|
||||
throw Dyn::Error("KeyError", Dyn::Repr(mode));
|
||||
if (kind == "reply") return reply;
|
||||
if (kind == "series") return series;
|
||||
if (kind == "commits") return commits;
|
||||
throw Dyn::Error("KeyError", Dyn::Repr(kind));
|
||||
}
|
||||
|
||||
const Environment* FindEnvironment(std::string_view name) {
|
||||
|
|
@ -257,8 +257,8 @@ namespace Crafter::Review::Plan {
|
|||
return Effort;
|
||||
}
|
||||
|
||||
std::string_view DefaultEffort(std::string_view mode) {
|
||||
return IsKernelMode(mode) ? KernelEffort : Effort;
|
||||
std::string_view DefaultEffort(std::string_view kind) {
|
||||
return IsKernelKind(kind) ? KernelEffort : Effort;
|
||||
}
|
||||
|
||||
bool IsBareHeader(const Json& unit, std::string_view text) {
|
||||
|
|
@ -357,12 +357,12 @@ namespace Crafter::Review::Plan {
|
|||
return Dyn::Replace(lensFile, ".md", "");
|
||||
}
|
||||
|
||||
std::vector<std::string> ModeLenses(std::string_view mode) {
|
||||
std::vector<std::string> KindLenses(std::string_view kind) {
|
||||
std::vector<std::string> out;
|
||||
for (const Gate& gate : Gates(mode)) {
|
||||
for (const Gate& gate : Gates(kind)) {
|
||||
for (const std::string_view lens : gate.lenses) out.emplace_back(lens);
|
||||
}
|
||||
if (IsKernelMode(mode)) out.emplace_back(PersonaLens);
|
||||
if (IsKernelKind(kind)) out.emplace_back(PersonaLens);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
|
@ -370,9 +370,9 @@ namespace Crafter::Review::Plan {
|
|||
return GatesFor("commits", only, without, off);
|
||||
}
|
||||
|
||||
std::vector<Gate> GatesFor(std::string_view mode, std::span<const std::string> only, std::span<const std::string> without, std::span<const std::string> off) {
|
||||
std::vector<Gate> GatesFor(std::string_view kind, std::span<const std::string> only, std::span<const std::string> without, std::span<const std::string> off) {
|
||||
std::vector<Gate> out;
|
||||
for (const Gate& gate : Gates(mode)) {
|
||||
for (const Gate& gate : Gates(kind)) {
|
||||
Gate kept{gate.title, {}};
|
||||
for (const std::string_view lens : gate.lenses) {
|
||||
if (!only.empty() && !Contains(only, lens)) continue;
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ namespace Crafter::Review::Run {
|
|||
return {
|
||||
{"config", opt(a.config)},
|
||||
{"mode", a.mode},
|
||||
{"kind", a.kind},
|
||||
{"subsystem", opt(a.subsystem)},
|
||||
{"artifact", a.artifact},
|
||||
{"hash", opt(a.hash)},
|
||||
|
|
@ -331,12 +332,12 @@ namespace Crafter::Review::Run {
|
|||
a = std::move(given);
|
||||
cfg = std::move(config);
|
||||
// The lens files the config turns off, and the environment of each.
|
||||
off = Config::LensesOff(cfg);
|
||||
off = Config::LensesOff(cfg, a.mode);
|
||||
environments = Config::Environments(cfg);
|
||||
}
|
||||
|
||||
bool Run::Kernel() const {
|
||||
return Plan::IsKernelMode(a.mode);
|
||||
return Plan::IsKernelKind(a.kind);
|
||||
}
|
||||
|
||||
Run::~Run() {
|
||||
|
|
@ -380,10 +381,10 @@ namespace Crafter::Review::Run {
|
|||
// ---- Gate 0 -------------------------------------------------------------------------
|
||||
void Run::Gate0() {
|
||||
if (!fs::exists(a.artifact)) Die(std::format("artifact {} does not exist", a.artifact));
|
||||
hash = Artifact::ArtifactHash(a.mode, a.artifact);
|
||||
hash = Artifact::ArtifactHash(a.kind, a.artifact);
|
||||
if (a.hash && !a.hash->empty() && *a.hash != hash) Die(std::format("--hash {} does not match the artifact ({})", Dyn::Slice(*a.hash, 0, 12), hash.substr(0, 12)));
|
||||
h8 = hash.substr(0, 8);
|
||||
if (a.mode == "commits") {
|
||||
if (a.kind == "commits") {
|
||||
// The lenses read the live checkout named first in --trees: it
|
||||
// must be the range HEAD the artifact was cut from, and clean -
|
||||
// anything else is evidence the review never saw (re-checked
|
||||
|
|
@ -398,11 +399,11 @@ namespace Crafter::Review::Run {
|
|||
}
|
||||
// A reply's gate-state sits beside the draft; a directory artifact's
|
||||
// inside it.
|
||||
gateDir = Dyn::JoinPath(a.mode == "reply" ? Dyn::DirName(a.artifact) : a.artifact, "gate-state");
|
||||
gateDir = Dyn::JoinPath(a.kind == "reply" ? Dyn::DirName(a.artifact) : a.artifact, "gate-state");
|
||||
fs::create_directories(gateDir);
|
||||
Inventory::Built built;
|
||||
try {
|
||||
built = Inventory::Build(a.mode, a.artifact);
|
||||
built = Inventory::Build(a.kind, a.artifact);
|
||||
} catch (const Inventory::InvariantError& e) {
|
||||
Die(e.what());
|
||||
} catch (const Dyn::Error& e) {
|
||||
|
|
@ -430,19 +431,23 @@ namespace Crafter::Review::Run {
|
|||
// same, clobber the resume file and buy every pass twice. The lock is
|
||||
// the kernel's: a killed driver releases it, no lens child inherits it.
|
||||
// A test or dry run takes none but numbers itself past a live holder.
|
||||
// The lock, the run numbers and the dump names are the kind's: the
|
||||
// gate-state is shared by every mode of a kind, so one full run per
|
||||
// hash holds across them and their rounds count on together; the dump
|
||||
// says which mode ran (2026-11-01, the first self-review).
|
||||
Json holder;
|
||||
if (!a.dryRun && !testMode) {
|
||||
try {
|
||||
runLock = std::make_unique<State::RunLock>(gateDir, a.mode, h8);
|
||||
runLock = std::make_unique<State::RunLock>(gateDir, a.kind, h8);
|
||||
} catch (const State::RunLocked& e) {
|
||||
const std::string how = Platform::InSandbox() ? std::format("wait for it or stop it (`{} stop <its scratch dir>`)", Invocation()) : std::string("wait for it or kill it");
|
||||
Die(std::format("a full run on this hash is already live in {}: {} - {}; a dead holder releases the lock", Dyn::RelPath(gateDir.string(), repo.string()), e.what(), how));
|
||||
}
|
||||
} else {
|
||||
const std::optional<Json> found = State::LockHolder(gateDir, a.mode, h8);
|
||||
const std::optional<Json> found = State::LockHolder(gateDir, a.kind, h8);
|
||||
if (found) holder = *found;
|
||||
}
|
||||
std::int64_t runN = State::NextRunNumber(gateDir, a.mode, h8);
|
||||
std::int64_t runN = State::NextRunNumber(gateDir, a.kind, h8);
|
||||
if (Dyn::Truthy(holder) && Dyn::Truthy(Dyn::Get(holder, "run_n"))) runN = std::max(runN, Int(holder.at("run_n")) + 1);
|
||||
const fs::path scratchRoot = ScratchRoot(a.scratch);
|
||||
// A run killed before its first checkpoint left no dump but did leave
|
||||
|
|
@ -460,7 +465,7 @@ namespace Crafter::Review::Run {
|
|||
// run that bought nothing (no outputs in its dir) would otherwise have
|
||||
// its number taken again and its dump written over.
|
||||
auto dumped = [&](std::int64_t n) {
|
||||
return fs::exists(gateDir / std::format("{}-{}-run-{}.json", a.mode, h8, n)) || fs::exists(gateDir / std::format("{}-{}-run-{}-test.json", a.mode, h8, n));
|
||||
return fs::exists(gateDir / std::format("{}-{}-run-{}.json", a.kind, h8, n)) || fs::exists(gateDir / std::format("{}-{}-run-{}-test.json", a.kind, h8, n));
|
||||
};
|
||||
while (!a.dryRun) {
|
||||
const fs::path candidate = scratchRoot / std::format("{}-run-{}", h8, runN);
|
||||
|
|
@ -488,7 +493,7 @@ namespace Crafter::Review::Run {
|
|||
const std::lock_guard guard(shared);
|
||||
stopFile = scratch / "stop";
|
||||
}
|
||||
dumpPath = a.dryRun ? scratch / "dry-run.json" : gateDir / std::format("{}-{}-run-{}{}.json", a.mode, h8, runN, testMode ? "-test" : "");
|
||||
dumpPath = a.dryRun ? scratch / "dry-run.json" : gateDir / std::format("{}-{}-run-{}{}.json", a.kind, h8, runN, testMode ? "-test" : "");
|
||||
if (runLock) {
|
||||
runLock->WriteInfo({{"pid", Platform::PublishedPid()},
|
||||
{"since", Platform::LocalTime("%Y-%m-%dT%H:%M:%S")},
|
||||
|
|
@ -497,18 +502,18 @@ namespace Crafter::Review::Run {
|
|||
{"run_n", runN}});
|
||||
}
|
||||
logFile = scratch / "log.txt";
|
||||
log(std::format("crafter-review {} prompt v{}: {} {} @ {}", Version(), PromptVersion, a.mode, Dyn::RelPath(a.artifact, repo.string()), hash));
|
||||
log(std::format("crafter-review {} prompt v{}: {}{} {} @ {}", Version(), PromptVersion, a.mode, a.mode == a.kind ? std::string() : std::format(" (a {})", a.kind), Dyn::RelPath(a.artifact, repo.string()), hash));
|
||||
std::error_code canonical;
|
||||
if (a.mode == "commits" && fs::weakly_canonical(reviewed, canonical) != fs::weakly_canonical(repo, canonical)) log(std::format("repository under review: {} (read at {})", reviewed.string(), trees.front()));
|
||||
if (a.kind == "commits" && fs::weakly_canonical(reviewed, canonical) != fs::weakly_canonical(repo, canonical)) log(std::format("repository under review: {} (read at {})", reviewed.string(), trees.front()));
|
||||
log(std::format("config: {}", Dyn::Truthy(Dyn::Get(cfg, "_path")) ? Dyn::Str(cfg.at("_path")) : std::string("defaults (no .review-gates/config.json)")));
|
||||
for (const Json& w : Dyn::Or(Dyn::Get(cfg, "_warnings"), Dyn::EmptyList())) log(std::format("config: warning: {}", Dyn::Str(w)));
|
||||
// The mode's lenses: the others are another mode's and say nothing
|
||||
// about this run.
|
||||
const std::vector<std::string> modeLenses = Plan::ModeLenses(a.mode);
|
||||
const std::vector<std::string> modeLenses = Plan::KindLenses(a.kind);
|
||||
std::vector<std::string> lensParts;
|
||||
for (const std::string& lf : modeLenses) {
|
||||
const std::string name = Plan::LensName(lf);
|
||||
lensParts.push_back(std::ranges::contains(off, lf) ? std::format("{} OFF (lenses.{}.run = false)", name, name) : std::format("{} ({})", name, Dyn::Str(Dyn::At(environments, lf))));
|
||||
lensParts.push_back(std::ranges::contains(off, lf) ? std::format("{} OFF (not in modes.{}.lenses)", name, a.mode) : std::format("{} ({})", name, Dyn::Str(Dyn::At(environments, lf))));
|
||||
}
|
||||
log(std::format("lenses: {}", Dyn::Join(lensParts, ", ")));
|
||||
std::vector<std::string> running;
|
||||
|
|
@ -534,7 +539,7 @@ namespace Crafter::Review::Run {
|
|||
if (!unknown.empty()) Die(std::format("--units names ids not in the inventory: {}", Dyn::Join(unknown, ", ")));
|
||||
}
|
||||
dispEntries = Dispositions::Load(a.dispositions.value_or(""));
|
||||
dispRows = Dispositions::Classify(dispEntries, hash, a.artifact, a.mode);
|
||||
dispRows = Dispositions::Classify(dispEntries, hash, a.artifact, a.kind);
|
||||
inForce = Dispositions::InForce(dispEntries, dispRows);
|
||||
std::vector<std::string> rowParts;
|
||||
for (const Json& r : dispRows) rowParts.push_back(std::format("{}:{}", Dyn::Str(r.at("id")), Dyn::Str(r.at("status"))));
|
||||
|
|
@ -543,7 +548,7 @@ namespace Crafter::Review::Run {
|
|||
// The references the artifact cites, prefetched into the
|
||||
// gate-state (a dry run reads an existing index and fetches
|
||||
// nothing), and each reviewer's calibration map.
|
||||
const std::string text = a.mode == "reply" ? Dyn::ReadText(fs::path(a.artifact)) : Dyn::Join(lines, "\n");
|
||||
const std::string text = a.kind == "reply" ? Dyn::ReadText(fs::path(a.artifact)) : Dyn::Join(lines, "\n");
|
||||
refUrls = Refs::ParseRefs(text);
|
||||
const fs::path refsDir = gateDir / "refs";
|
||||
const fs::path indexPath = gateDir / std::format("refs-{}.json", h8);
|
||||
|
|
@ -633,10 +638,10 @@ namespace Crafter::Review::Run {
|
|||
charters[lf] = Prompt::Charter(lf);
|
||||
charterSha[lf] = Plan::Sha(charters[lf]);
|
||||
}
|
||||
systemText = Prompt::SystemText(a.mode);
|
||||
systemText = Prompt::SystemText(a.kind);
|
||||
systemPaths.clear();
|
||||
systemByLens.clear();
|
||||
runLayer = Kernel() ? Prompt::RunLayerKernel(a.mode, Dyn::RelPath(a.artifact, repo.string()), a.subsystem.value_or(""), a.base, reviewers, a.date, trees, hash, lines, units, refDigest, contextPaths)
|
||||
runLayer = Kernel() ? Prompt::RunLayerKernel(a.kind, Dyn::RelPath(a.artifact, repo.string()), a.subsystem.value_or(""), a.base, reviewers, a.date, trees, hash, lines, units, refDigest, contextPaths)
|
||||
: Prompt::RunLayer(Dyn::RelPath(a.artifact, repo.string()), a.base, a.date, trees, hash, lines, units, contextPaths, Basename(reviewed), a.subsystem.value_or(""));
|
||||
// The application under test: the login fixture, the TOTP helper.
|
||||
const Json& fcfg = cfg.at("functional");
|
||||
|
|
@ -644,7 +649,7 @@ namespace Crafter::Review::Run {
|
|||
stacks = LoadStacks();
|
||||
const Json first = *stacks->First();
|
||||
appBlock = Prompt::AppBlock(fcfg, loginFixture, Pool::TotpCommand(loginFixture), slotMode);
|
||||
schemas = {{"block", Schema::SchemaJson("block", a.mode)}, {"whole", Schema::SchemaJson("whole", a.mode)}};
|
||||
schemas = {{"block", Schema::SchemaJson("block", a.kind)}, {"whole", Schema::SchemaJson("whole", a.kind)}};
|
||||
// Memory: every pass's tree measured, its peak recorded per repository
|
||||
// and environment; the budget per gate.
|
||||
calibration = std::make_unique<Memory::Calibration>(scratchRoot / "memory.json", reviewed, DefaultCosts());
|
||||
|
|
@ -659,11 +664,12 @@ namespace Crafter::Review::Run {
|
|||
});
|
||||
}
|
||||
// The database URLs stay here: a db pass gets the broker's address and
|
||||
// its stack's token.
|
||||
// its stack's token. Only a lens this mode runs counts: `off` is the
|
||||
// mode's own kind's, as every reader of it reads it.
|
||||
bool anyDb = false;
|
||||
for (auto it = environments.begin(); it != environments.end(); ++it) {
|
||||
const Plan::Environment* e = Plan::FindEnvironment(Dyn::Str(it.value()));
|
||||
anyDb = anyDb || (e && e->db && !std::ranges::contains(off, it.key()));
|
||||
anyDb = anyDb || (e && e->db && std::ranges::contains(modeLenses, it.key()) && !std::ranges::contains(off, it.key()));
|
||||
}
|
||||
if (!a.dryRun && anyDb) {
|
||||
broker = std::make_unique<PgWire::Broker>();
|
||||
|
|
@ -1183,8 +1189,8 @@ namespace Crafter::Review::Run {
|
|||
// ---- reuse --------------------------------------------------------------------------
|
||||
const Plan::CommitCode& Run::RangeCode() {
|
||||
if (!commitCode) {
|
||||
commitCode = Plan::CommitCodeOf(Artifact::PatchTexts(a.artifact), a.mode == "series");
|
||||
if (a.mode == "series" && !a.base.empty()) {
|
||||
commitCode = Plan::CommitCodeOf(Artifact::PatchTexts(a.artifact), a.kind == "series");
|
||||
if (a.kind == "series" && !a.base.empty()) {
|
||||
// A kernel export carries a base-commit line only when it was
|
||||
// cut with --base, and then in the cover letter: the
|
||||
// submission base the run was given is part of every code
|
||||
|
|
@ -1200,7 +1206,7 @@ namespace Crafter::Review::Run {
|
|||
// a part, re-normalised against today's dispositions, with its summary
|
||||
// for the block passes. nullopt: the whole pass runs.
|
||||
const std::string lensFile = Dyn::Str(job.at("lensFile"));
|
||||
if (a.mode != "commits") {
|
||||
if (a.kind != "commits") {
|
||||
// Only the push gates reuse a whole pass; a kernel whole pass
|
||||
// runs on every hash.
|
||||
job["reuseKey"] = nullptr;
|
||||
|
|
@ -1268,7 +1274,7 @@ namespace Crafter::Review::Run {
|
|||
// rule, 2026-10-06.6); every text unit on its text and what it
|
||||
// cites: the [n] URLs, the anchor when it is dated, the R-units
|
||||
// it names.
|
||||
if (a.mode == "series" && Dyn::Get(job, "unitKind") == "code") return Plan::CodeReuseKey(job, lensFile, text, RangeCode(), ids, charterSha.at(lensFile), PromptVersion, env);
|
||||
if (a.kind == "series" && Dyn::Get(job, "unitKind") == "code") return Plan::CodeReuseKey(job, lensFile, text, RangeCode(), ids, charterSha.at(lensFile), PromptVersion, env);
|
||||
return Plan::ReuseKey(job, lensFile, text, Plan::TextDeps(text, units, lines, refUrls, anchorText), ids, charterSha.at(lensFile), PromptVersion);
|
||||
}
|
||||
return Plan::BlockReuseKey(job, lensFile, text, RangeCode(), ids, charterSha.at(lensFile), PromptVersion, env);
|
||||
|
|
@ -1428,12 +1434,12 @@ namespace Crafter::Review::Run {
|
|||
const std::lock_guard guard(shared);
|
||||
if (sched) sched->Abort(reason);
|
||||
};
|
||||
const std::string now = Artifact::ArtifactHash(a.mode, a.artifact);
|
||||
const std::string now = Artifact::ArtifactHash(a.kind, a.artifact);
|
||||
if (now != hash) {
|
||||
abortPool(std::format("artifact changed during the run ({} != {})", now.substr(0, 8), h8));
|
||||
return {{{"label", job.at("label")}, {"status", "error"}, {"reason", "artifact changed during the run - aborted"}}, nullptr};
|
||||
}
|
||||
if (const std::optional<std::string> drift = a.mode == "commits" ? treeDrift() : std::nullopt) {
|
||||
if (const std::optional<std::string> drift = a.kind == "commits" ? treeDrift() : std::nullopt) {
|
||||
abortPool(std::format("evidence drift during the run ({})", *drift));
|
||||
return {{{"label", job.at("label")}, {"status", "error"}, {"reason", std::format("{} - aborted", *drift)}}, nullptr};
|
||||
}
|
||||
|
|
@ -1849,11 +1855,11 @@ namespace Crafter::Review::Run {
|
|||
}
|
||||
reviewers = CommaList(a.reviewers);
|
||||
if (!reviewers.empty() && std::ranges::contains(off, std::string(Plan::PersonaLens))) {
|
||||
Die(std::format("--reviewers names {} but the config turns off the persona lens (lenses.{}.run = false): turn it on, or drop --reviewers", Dyn::Join(reviewers, ", "),
|
||||
Die(std::format("--reviewers names {} but modes.{}.lenses does not list the persona lens ({}): list it, or drop --reviewers", Dyn::Join(reviewers, ", "), a.mode,
|
||||
Plan::LensName(Plan::PersonaLens)));
|
||||
}
|
||||
Gate0();
|
||||
const std::vector<std::string> modeLenses = Plan::ModeLenses(a.mode);
|
||||
const std::vector<std::string> modeLenses = Plan::KindLenses(a.kind);
|
||||
std::vector<std::string> only;
|
||||
if (a.only && !a.only->empty()) {
|
||||
only = CommaList(*a.only);
|
||||
|
|
@ -1872,7 +1878,7 @@ namespace Crafter::Review::Run {
|
|||
if (std::ranges::contains(off, lf)) isOff.push_back(lf);
|
||||
}
|
||||
if (!isOff.empty()) {
|
||||
Die(std::format("--only names {}, which the config turns off (lenses.{}.run = false): the config is the record - edit it for a test run", Dyn::Join(isOff, ", "), Plan::LensName(isOff.front())));
|
||||
Die(std::format("--only names {}, which modes.{}.lenses does not list: the config is the record - edit it for a test run", Dyn::Join(isOff, ", "), a.mode));
|
||||
}
|
||||
}
|
||||
std::vector<std::string> without;
|
||||
|
|
@ -1890,7 +1896,7 @@ namespace Crafter::Review::Run {
|
|||
if (!foreign.empty()) Die(std::format("--without names lens file(s) the {} mode never runs: {}", a.mode, Dyn::Join(foreign, ", ")));
|
||||
log(std::format("without: {} - left out of this run by owner ruling (recorded in the dump's args); the run stays a full run", Dyn::Join(without, ", ")));
|
||||
}
|
||||
const std::vector<Plan::Gate> gates = Plan::GatesFor(a.mode, only, without, off);
|
||||
const std::vector<Plan::Gate> gates = Plan::GatesFor(a.kind, only, without, off);
|
||||
if (gates.empty()) {
|
||||
std::vector<std::string> why;
|
||||
std::vector<std::string> offHere;
|
||||
|
|
@ -1898,7 +1904,7 @@ namespace Crafter::Review::Run {
|
|||
if (std::ranges::contains(modeLenses, lf)) offHere.push_back(lf);
|
||||
}
|
||||
if (offHere.size() == modeLenses.size()) {
|
||||
why.emplace_back("the config turns on no lens (every lens is off until lenses.<name>.run = true turns it on)");
|
||||
why.emplace_back(std::format("modes.{}.lenses lists no lens of this kind (every lens is off until a mode lists it)", a.mode));
|
||||
} else if (!offHere.empty()) {
|
||||
std::vector<std::string> names;
|
||||
for (const std::string& lf : offHere) names.push_back(Plan::LensName(lf));
|
||||
|
|
@ -2008,7 +2014,7 @@ namespace Crafter::Review::Run {
|
|||
owedReads += o.at("reads").get<std::int64_t>();
|
||||
}
|
||||
overall = std::format("CONFIRMING - NOT SEND-READY (gates clean; {} code unit(s) owe {} clean read(s) to reach {}: run the same line again)", owedUnits, owedReads, x);
|
||||
} else if (personaRan || a.mode == "commits") {
|
||||
} else if (personaRan || a.kind == "commits") {
|
||||
// commits: no persona gate - the gates clean = push-ready (the
|
||||
// owner is the last gate).
|
||||
overall = "SEND-READY";
|
||||
|
|
@ -2020,7 +2026,7 @@ namespace Crafter::Review::Run {
|
|||
|
||||
std::int32_t Run::Finish(std::string overall, const Json& stoppedAt, Json incomplete) {
|
||||
if (broker) broker->Close();
|
||||
if (Artifact::ArtifactHash(a.mode, a.artifact) != hash) {
|
||||
if (Artifact::ArtifactHash(a.kind, a.artifact) != hash) {
|
||||
overall = "INCOMPLETE - NOT SEND-READY (artifact changed during the run)";
|
||||
incomplete.push_back({{"gate", "end"}, {"reason", "artifact hash changed during the run"}});
|
||||
}
|
||||
|
|
@ -2121,7 +2127,7 @@ namespace Crafter::Review::Run {
|
|||
const Json& db = Dyn::Get(s, "dbUrl");
|
||||
stackList.push_back({{"id", s.at("id")}, {"appUrl", s.at("appUrl")}, {"dbUrl", Dyn::Truthy(db) ? Json(PgWire::Redact(Dyn::Str(db))) : Json()}, {"login", Dyn::Get(s, "login")}});
|
||||
}
|
||||
const std::vector<std::string> modeLenses = Plan::ModeLenses(a.mode);
|
||||
const std::vector<std::string> modeLenses = Plan::KindLenses(a.kind);
|
||||
Json lensesOff = Json::array();
|
||||
for (const std::string& lf : off) {
|
||||
if (std::ranges::contains(modeLenses, lf)) lensesOff.push_back(Plan::LensName(lf));
|
||||
|
|
@ -2144,6 +2150,7 @@ namespace Crafter::Review::Run {
|
|||
Json dump = {
|
||||
{"driver", DriverLine()},
|
||||
{"mode", a.mode},
|
||||
{"kind", a.kind},
|
||||
{"artifact", Dyn::RelPath(a.artifact, repo.string())},
|
||||
{"hash", hash},
|
||||
{"date", a.date},
|
||||
|
|
@ -2388,7 +2395,7 @@ namespace Crafter::Review::Run {
|
|||
// second store of the same run, or the relaunch of an unfinished run,
|
||||
// adds nothing. The persona pass is recorded once the persona gate
|
||||
// passed.
|
||||
const Json read = a.mode == "reply" ? Json() : Json({{"hash", h8}, {"round", round}, {"run", Basename(dumpPath)}});
|
||||
const Json read = a.kind == "reply" ? Json() : Json({{"hash", h8}, {"round", round}, {"run", Basename(dumpPath)}});
|
||||
const Json persona = personaRan ? Json({{"hash", hash}, {"reviewers", Array(reviewers)}, {"date", a.date}}) : Json();
|
||||
const std::vector<std::string> drops(dirty.begin(), dirty.end());
|
||||
Json merged = State::SaveState(statePath, sets, drops, read, persona);
|
||||
|
|
|
|||
|
|
@ -250,8 +250,8 @@ namespace Crafter::Review::State {
|
|||
}
|
||||
const Json& coverage = Dyn::Or(Dyn::Get(dump, "coverage"), Dyn::EmptyDict());
|
||||
if (Dyn::Truthy(coverage)) {
|
||||
const Json& mode = Dyn::Get(dump, "mode");
|
||||
if (mode == "reply" || mode == "series") {
|
||||
const Json kind = Dyn::Or(Dyn::Get(dump, "kind"), Dyn::Get(dump, "mode"));
|
||||
if (kind == "reply" || kind == "series") {
|
||||
lines.push_back(std::format("matrix: {} units x {} lenses; holes {}, persona holes {}, thin {}, modelled {}, units with a defect verdict {}", Dyn::Str(Dyn::At(coverage, "units")),
|
||||
Len(Dyn::At(coverage, "lenses")), Len(Dyn::At(coverage, "holes")), Len(Dyn::Or(Dyn::Get(coverage, "personaHoles"), Dyn::EmptyList())),
|
||||
Len(Dyn::Or(Dyn::Get(coverage, "thin"), Dyn::EmptyList())), Len(Dyn::Or(Dyn::Get(coverage, "modelled"), Dyn::EmptyList())), Len(Dyn::At(coverage, "defects"))));
|
||||
|
|
|
|||
Loading…
Reference in a new issue