feat(config): every lens off until the repository turns it on

Jorijn: "all lenses off by default and opt in only". Config::Defaults()
gives every lens run: false; a config that turns on none - a missing
`lenses` key included - has the one problem that names the lenses, so
config, freeze and run all stop on it; the template ships every lens off,
init says to turn lenses on, and the "no lens runs" preflight names the
cause. A consumer config without a `lenses` key now needs one, as the CI
smoke test's does.

Proven with the lens commit before it: the suite (26 passed), and the
dry-run comparison recorded there.
This commit is contained in:
Jorijn van der Graaf 2026-10-31 20:09:07 +01:00
commit 101471d81a
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
15 changed files with 513 additions and 269 deletions

View file

@ -120,7 +120,9 @@ namespace Crafter::Review::Config {
Json Defaults() {
Json lenses = Json::object();
for (const Plan::Lens& lens : Plan::Lenses) lenses[Plan::LensName(lens.file)] = {{"run", true}, {"environment", lens.env}};
// 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").
for (const Plan::Lens& lens : Plan::Lenses) lenses[Plan::LensName(lens.file)] = {{"run", false}, {"environment", lens.env}};
return {
// The integration branch a range is measured against (e.g.
// origin/dev): merge-base HEAD <base>.
@ -143,8 +145,8 @@ namespace Crafter::Review::Config {
// 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 (false: left out of every run of this repository)
// and environment (Plan::Environments).
// Per lens: run (off until true: every lens is opt-in) and
// environment (Plan::Environments).
{"lenses", lenses},
// The database under test, for every lens whose environment
// reaches it. The URL reaches a db pass's broker, never a prompt,
@ -237,7 +239,13 @@ namespace Crafter::Review::Config {
if (!Dyn::In(Dyn::Get(entry, "environment"), environments)) out.push_back(std::format("lenses.{}.environment must be one of {}", it.key(), EnvironmentNames(", ")));
}
auto runs = [](const Json& entry) { return entry.is_object() && Dyn::Get(entry, "run") == true; };
if (!lenses.empty() && !std::ranges::any_of(lenses, runs)) out.emplace_back("lenses: every lens is off - at least one must run");
// 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, ", ")));
}
for (const Plan::Lens& lens : Plan::Lenses) {
const Json& entry = Dyn::Get(lenses, Plan::LensName(lens.file));
if (!lens.empirical || !runs(entry)) continue;