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

@ -87,11 +87,11 @@ echo '{"type":"result","subtype":"success","is_error":false,"result":"ok","total
const Process::Captured first = Self({"init"}, r);
const std::string skill = Read(r / ".claude" / "skills" / "prepush" / "SKILL.md");
const std::size_t frontmatter = shipped.find("\n---\n", 3) + 5;
Check("init: the config from the template, the artifact and worktree dirs ignored, the skill with init's marker after its frontmatter, the next steps",
Check("init: the config from the template (every lens off), the artifact and worktree dirs ignored, the skill with init's marker after its frontmatter, the next steps (turn lenses on; no browser line while none drives the application)",
first.returnCode == 0 && Read(r / ".review-gates" / "config.json") == Read(checkout / "process" / "templates" / "config.json")
&& Read(r / ".gitignore") == ".review-gates/push/\n.review-gates/trees/\n" && skill.starts_with(shipped.substr(0, frontmatter))
&& skill.substr(frontmatter).starts_with(marker) && skill.ends_with(shipped.substr(frontmatter)) && first.out.contains(" config - until it reports no problem")
&& first.out.contains(" browser install - "),
&& first.out.contains("turn lenses on in .review-gates/config.json") && !first.out.contains(" browser install - "),
first.out + first.err);
Write(r / ".review-gates" / "config.json", R"({"base": "origin/dev"})");
Write(r / ".gitignore", "node_modules/\n/.review-gates/push\n");

View file

@ -42,16 +42,21 @@ int main() {
fs::create_directories(td);
auto load = [&] { return Config::Load(td); };
Config::Loaded loaded = load();
Check("no file: the defaults, no problem, _path None", loaded.problems.empty() && loaded.config.at("aiTrailers") == "forbid"
&& loaded.config.at("functional").at("appUrl") == "http://localhost:8080" && loaded.config.at("_path").is_null());
auto noLensOn = [](const std::vector<std::string>& problems) { return problems.size() == 1 && problems[0].starts_with("lenses: no lens is on"); };
Check("no file: the defaults, _path None, and the one problem of a repository that turns no lens on (2026-10-31)", noLensOn(loaded.problems) && loaded.config.at("aiTrailers") == "forbid"
&& loaded.config.at("functional").at("appUrl") == "http://localhost:8080" && loaded.config.at("_path").is_null(), Show(loaded.problems));
fs::create_directories(td / ".review-gates");
const fs::path p = td / ".review-gates" / "config.json";
auto put = [&](const Json& value) { std::ofstream(p, std::ios::binary) << DumpSpaced(value); };
auto bad = [&](const Json& value) {
// A config under test turns the bug-prover on unless it says which lenses
// run itself: every lens is off by default, and a config with none on is
// a problem of its own (checked below).
auto bad = [&](Json value) {
if (!value.contains("lenses")) value["lenses"] = {{"repo-bug-prover", {{"run", true}}}};
put(value);
return load().problems;
};
put({{"base", "origin/dev"}, {"aiTrailers", "allow"}, {"allowDirty", {"supabase/config.toml"}},
put({{"base", "origin/dev"}, {"aiTrailers", "allow"}, {"allowDirty", {"supabase/config.toml"}}, {"lenses", {{"repo-bug-prover", {{"run", true}}}}},
{"functional", {{"exclude", {"**/*.test.ts"}}, {"readyPath", "/health"}, {"extraTools", {"Bash(redis-cli:*)", "mcp__x__y"}}}}});
loaded = load();
Check("the file merges over the defaults, nested too", loaded.problems.empty() && loaded.config.at("base") == "origin/dev" && loaded.config.at("aiTrailers") == "allow"
@ -59,7 +64,8 @@ int main() {
Show(loaded.problems));
Check("rangeFrom 'base' without a base branch is a problem; 'sideways' is one too", Any(bad({{"rangeFrom", "base"}}), "needs a base")
&& Any(bad({{"rangeFrom", "sideways"}, {"base", "origin/dev"}}), "rangeFrom must be"));
std::vector<std::string> problems = bad({{"bogus", 1}, {"functional", {{"nope", 2}, {"appUrl", "ftp://x"}}}, {"aiTrailers", "maybe"}, {"authors", {"nomail"}}});
std::vector<std::string> problems = bad({{"bogus", 1}, {"functional", {{"nope", 2}, {"appUrl", "ftp://x"}}}, {"aiTrailers", "maybe"}, {"authors", {"nomail"}},
{"lenses", {{"repo-functional", {{"run", true}}}}}});
Check("unknown keys and bad values are each a problem", problems.size() == 5 && Any(problems, "bogus") && Any(problems, "functional.nope") && Any(problems, "appUrl")
&& Any(problems, "aiTrailers") && Any(problems, "authors"), Show(problems));
problems = bad({{"functional", {{"extraTools", {"rm -rf /"}}, {"mcp", {{"args", "x"}}}}}});
@ -75,8 +81,10 @@ int main() {
Check("--config to a missing file is a problem", Any(Config::Load(td, td / "none.json").problems, "no such file"));
put(Json::object());
loaded = load();
bool allRun = true;
for (const Json& entry : loaded.config.at("lenses")) allRun = allRun && entry.at("run") == true;
bool noneRun = true;
for (const Json& entry : loaded.config.at("lenses")) noneRun = noneRun && entry.at("run") == false;
std::vector<std::string> everyLens;
for (const Plan::Lens& lens : Plan::Lenses) everyLens.emplace_back(lens.file);
bool defaultsMatch = true;
std::set<std::string> names;
for (const Plan::Lens& lens : Plan::Lenses) {
@ -87,25 +95,31 @@ int main() {
std::set<std::string> defaultNames;
for (auto it = defaults.at("lenses").begin(); it != defaults.at("lenses").end(); ++it) defaultNames.insert(it.key());
const Json environments = {{"repo-better-solution.md", "none"}, {fn, "browser"}, {"repo-bug-prover.md", "none"}, {"repo-security.md", "none"}, {Plan::FactsLens, "none"}};
Check("lenses: the defaults run all five with none/browser/none/none/none - plan.LENSES' env; the table names every lens of plan.LENSES", loaded.problems.empty() && allRun
&& Config::LensesOff(loaded.config).empty() && Dyn::Equal(Config::Environments(loaded.config), environments) && defaultNames == names && defaultsMatch, Show(loaded.problems));
put({{"lenses", {{"repo-functional", {{"run", false}}}, {"repo-bug-prover", {{"environment", "browser"}}}}}});
Check("lenses: every lens is off by default (2026-10-31, opt-in only) with plan.LENSES' env - none/browser/none/none/none - and a config that turns none on has the one problem that says so; the table names every lens of plan.LENSES",
noLensOn(loaded.problems) && noneRun && Config::LensesOff(loaded.config) == everyLens && Dyn::Equal(Config::Environments(loaded.config), environments) && defaultNames == names && defaultsMatch,
Show(loaded.problems));
put({{"lenses", {{"repo-functional", {{"run", false}}}, {"repo-bug-prover", {{"run", true}, {"environment", "browser"}}}}}});
loaded = load();
Check("lenses: run=false merges and lists the lens off in gate order; a static lens may take the browser", loaded.problems.empty()
&& Config::LensesOff(loaded.config) == std::vector<std::string>{fn} && Config::Environments(loaded.config).at("repo-bug-prover.md") == "browser"
std::vector<std::string> allButProver;
for (const Plan::Lens& lens : Plan::Lenses) {
if (lens.file != "repo-bug-prover.md") allButProver.emplace_back(lens.file);
}
Check("lenses: run=true merges and every lens not turned on is listed off in gate order; a static lens may take the browser", loaded.problems.empty()
&& Config::LensesOff(loaded.config) == allButProver && Config::Environments(loaded.config).at("repo-bug-prover.md") == "browser"
&& loaded.config.at("lenses").at("repo-functional").at("environment") == "browser", Show(loaded.problems));
problems = bad({{"lenses", {{"nope", {{"run", true}}}, {"repo-better-solution", {{"run", "yes"}, {"environment", "http"}}}}}});
problems = bad({{"lenses", {{"nope", {{"run", true}}}, {"repo-better-solution", {{"run", "yes"}, {"environment", "http"}}}, {"repo-bug-prover", {{"run", true}}}}}});
Check("lenses: an unknown lens is an unknown key; run must be a bool; environment must be one of none, browser, supabase, browser+supabase", problems.size() == 3
&& Any(problems, "lenses.nope") && Any(problems, "run must be") && Any(problems, "one of none, browser, supabase, browser+supabase"), Show(problems));
problems = bad({{"lenses", {{"repo-functional", {{"environment", "none"}}}}}});
problems = bad({{"lenses", {{"repo-functional", {{"run", true}, {"environment", "none"}}}}}});
Check("the functional lens running with no application under test is a problem; off with none is fine; an unknown environment is reported once",
problems.size() == 1 && problems[0].contains("empirical") && bad({{"lenses", {{"repo-functional", {{"run", false}, {"environment", "none"}}}}}}).empty()
&& bad({{"lenses", {{"repo-functional", {{"environment", "http"}}}}}}).size() == 1, Show(problems));
problems = bad({{"lenses", {{"repo-functional", {{"environment", "supabase"}}}}}});
problems.size() == 1 && problems[0].contains("empirical")
&& bad({{"lenses", {{"repo-functional", {{"run", false}, {"environment", "none"}}}, {"repo-bug-prover", {{"run", true}}}}}}).empty()
&& bad({{"lenses", {{"repo-functional", {{"run", true}, {"environment", "http"}}}}}}).size() == 1, Show(problems));
problems = bad({{"lenses", {{"repo-functional", {{"run", true}, {"environment", "supabase"}}}}}});
Check("the database alone is no application under test for the functional lens; browser+supabase is; a static lens may take either, or the database alone",
problems.size() == 1 && problems[0].contains("empirical") && problems[0].contains("browser, browser+supabase")
&& bad({{"lenses", {{"repo-functional", {{"environment", "browser+supabase"}}}, {"repo-bug-prover", {{"environment", "supabase"}}},
{"repo-better-solution", {{"environment", "browser+supabase"}}}}}}).empty(), Show(problems));
&& bad({{"lenses", {{"repo-functional", {{"run", true}, {"environment", "browser+supabase"}}}, {"repo-bug-prover", {{"run", true}, {"environment", "supabase"}}},
{"repo-better-solution", {{"run", true}, {"environment", "browser+supabase"}}}}}}).empty(), Show(problems));
loaded = load();
Check("supabase: the defaults are the local stack's Postgres (54322), no start/stop, no notes",
Dyn::Equal(loaded.config.at("supabase"), {{"dbUrl", "postgresql://postgres:postgres@127.0.0.1:54322/postgres"}, {"start", nullptr}, {"stop", nullptr}, {"notes", ""}}));
@ -117,9 +131,10 @@ int main() {
&& bad({{"supabase", {{"dbUrl", "postgresql://u@[::1]:5432/x"}}}}).empty() && Any(bad({{"supabase", {{"start", {"bash", "x"}}}}}), "supabase.start")
&& Any(bad({{"supabase", {{"nope", 1}}}}), "supabase.nope"));
Check("functional.appUrl is checked only while a lens that runs drives the application (plan.ENVIRONMENTS item 4)",
Any(bad({{"functional", {{"appUrl", "ftp://x"}}}}), "appUrl")
&& bad({{"functional", {{"appUrl", "ftp://x"}}}, {"lenses", {{"repo-functional", {{"run", false}}}, {"repo-bug-prover", {{"environment", "supabase"}}}}}}).empty()
&& Any(bad({{"functional", {{"appUrl", "ftp://x"}}}, {"lenses", {{"repo-functional", {{"run", false}}}, {"repo-bug-prover", {{"environment", "browser+supabase"}}}}}}), "appUrl"));
Any(bad({{"functional", {{"appUrl", "ftp://x"}}}, {"lenses", {{"repo-functional", {{"run", true}}}}}}), "appUrl")
&& !Any(bad({{"functional", {{"appUrl", "ftp://x"}}}}), "appUrl")
&& bad({{"functional", {{"appUrl", "ftp://x"}}}, {"lenses", {{"repo-functional", {{"run", false}}}, {"repo-bug-prover", {{"run", true}, {"environment", "supabase"}}}}}}).empty()
&& Any(bad({{"functional", {{"appUrl", "ftp://x"}}}, {"lenses", {{"repo-functional", {{"run", false}}}, {"repo-bug-prover", {{"run", true}, {"environment", "browser+supabase"}}}}}}), "appUrl"));
problems = bad({{"functional", {{"extraTools", {"Bash(psql:*)", "Bash(psql -h 127.0.0.1:*)", "Bash(psqlx:*)", "Bash(npm run e2e:*)", "mcp__x__y"}}}}});
Check("functional.extraTools: an entry pool.DENY overrides is a problem (psql commits to the shared database; npm is a package manager); a mere prefix twin is not",
problems.size() == 1 && problems[0].contains("Bash(psql:*)") && problems[0].contains("Bash(psql -h 127.0.0.1:*)") && problems[0].contains("Bash(npm run e2e:*)")
@ -129,16 +144,17 @@ int main() {
const Json rc = Config::Redacted(withPassword);
Check("redacted: the dump's copy of the config masks the database password and drops _path", rc.at("supabase").at("dbUrl") == "postgresql://postgres:***@127.0.0.1:54322/postgres"
&& !DumpSpaced(rc).contains("hunter2") && !rc.contains("_path"));
problems = bad({{"lenses", {{"repo-functional", false}}}});
Check("a bare false under lenses is a shape problem, never an exception; lenses_off and environments fall back to the defaults", problems.size() == 1
&& problems[0].contains("must be an object") && Config::LensesOff(load().config).empty() && Config::Environments(load().config).at(fn) == "browser", Show(problems));
problems = bad({{"lenses", {{"repo-functional", false}, {"repo-bug-prover", {{"run", true}}}}}});
Check("a bare false under lenses is a shape problem, never an exception; lenses_off and environments fall back to the defaults (off, browser)", problems.size() == 1
&& problems[0].contains("must be an object") && Config::LensesOff(load().config) == allButProver && Config::Environments(load().config).at(fn) == "browser", Show(problems));
Json allOff = Json::object();
for (auto it = defaults.at("lenses").begin(); it != defaults.at("lenses").end(); ++it) allOff[it.key()] = {{"run", false}};
problems = bad({{"lenses", allOff}});
Check("every lens off is a problem", problems.size() == 1 && problems[0].contains("every lens is off"), Show(problems));
Check("every lens off is the problem that names them", problems.size() == 1 && problems[0].contains("no lens is on") && problems[0].contains("repo-facts"), Show(problems));
const Json ex = Config::Example();
Check("the example validates and is what process/templates/config.json holds", Config::Validate(ex).empty()
&& Dyn::Equal(Dyn::LoadJsonFile(fs::current_path() / "process" / "templates" / "config.json"), ex));
Check("the example is what process/templates/config.json holds, every lens off, and its only problem says to turn one on", noLensOn(Config::Validate(ex))
&& Dyn::Equal(Dyn::LoadJsonFile(fs::current_path() / "process" / "templates" / "config.json"), ex)
&& std::ranges::all_of(ex.at("lenses"), [](const Json& entry) { return entry.at("run") == false; }), Show(Config::Validate(ex)));
const fs::path repo = td / "r";
fs::create_directories(repo);
Process::Run({"git", "-C", repo.string(), "init", "-q"});

File diff suppressed because it is too large Load diff

View file

@ -366,8 +366,8 @@ namespace {
fs::create_directories(td / ".review-gates");
fs::copy_file(fs::current_path() / "process" / "templates" / "config.json", td / ".review-gates" / "config.json");
const Config::Loaded loaded = Config::Load(td);
Check("template: process/templates/config.json, copied as the README says, loads with no problem and no warning - unknown keys included (finding 2)",
loaded.problems.empty() && loaded.config.at("_warnings").empty(), Show(loaded.problems));
Check("template: process/templates/config.json, copied as the README says, loads with no warning and no problem but the one that says to turn a lens on - unknown keys included (finding 2; every lens off since 2026-10-31)",
loaded.problems.size() == 1 && loaded.problems[0].starts_with("lenses: no lens is on") && loaded.config.at("_warnings").empty(), Show(loaded.problems));
}
fs::remove_all(base);
return Failures ? 1 : 0;

View file

@ -196,15 +196,16 @@ echo "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false,\"result\"
{"GIT_COMMITTER_NAME", "Me"}, {"GIT_COMMITTER_EMAIL", "me@example.com"}, {"CRAFTER_REVIEW_HOME", checkout.string()},
{"REVIEW_GATES_BOARD", (base / "board.json").string()}, {"FAKE_DIR", fakeDir.string()}});
// A repository with one commit to review, the functional lens off (it
// needs a running application) and the commit-truth lens off (it gets
// the last scenario).
// A repository with one commit to review, the three static code lenses
// on (every lens is off until a config turns it on; the functional
// lens needs a running application, the commit-truth lens gets the
// last scenario).
fs::create_directories(repo);
Git(repo, {"init", "-q", "-b", "main"});
Git(repo, {"config", "user.email", "me@example.com"});
Git(repo, {"config", "user.name", "Me"});
Write(repo / ".gitignore", ".review-gates/push/\n.review-gates/trees/\n");
Write(repo / ".review-gates" / "config.json", R"({"lenses": {"repo-functional": {"run": false}, "repo-facts": {"run": false}}})");
Write(repo / ".review-gates" / "config.json", R"({"lenses": {"repo-better-solution": {"run": true}, "repo-bug-prover": {"run": true}, "repo-security": {"run": true}}})");
Write(repo / "README.md", "a greeter\n");
Git(repo, {"add", "-A"});
Git(repo, {"commit", "-q", "-m", "base"});
@ -421,14 +422,14 @@ echo "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false,\"result\"
Check("an error escaping a full run (a reserve that is no number) ends it with its ValueError, exit 1",
badReserve.returnCode == 1 && badReserve.err.contains("crafter-review: ValueError: could not convert string to float: 'x'"), badReserve.err);
// The commit-truth lens (2026-10-31), alone: a config that turns off
// every other lens (through --config: the tracked config file must
// stay as committed, or the tree is dirty) runs Gate 4 - its three text
// The commit-truth lens (2026-10-31), alone: a config that turns on
// only it (through --config: the tracked config file must stay as
// committed, or the tree is dirty) runs Gate 4 - its three text
// blocks and its two slot passes (the stand-in answers a SLOT PASS
// too) - push-ready, with the code units, the per-commit slots and
// the functional lens's slot listed as not reviewed.
{
Write(base / "facts.json", R"({"lenses": {"repo-better-solution": {"run": false}, "repo-functional": {"run": false}, "repo-bug-prover": {"run": false}, "repo-security": {"run": false}}})");
Write(base / "facts.json", R"({"lenses": {"repo-facts": {"run": true}}})");
const std::int64_t before = calls();
std::vector<std::string> truthArgs = runArgs({"--no-reuse", "--no-salvage"});
truthArgs.insert(truthArgs.begin() + 1, {"--config", (base / "facts.json").string()});

View file

@ -51,14 +51,16 @@ int main() {
Write(p, DumpSpaced(d));
return Config::Load(td).problems;
};
auto sl = [](const Json& kw) { return Json {{"slots", kw}}; };
// Every lens is off by default: a config under test turns the
// bug-prover on, or the one problem it would have is that none runs.
auto sl = [](const Json& kw) { return Json {{"slots", kw}, {"lenses", {{"repo-bug-prover", {{"run", true}}}}}}; };
Check("config: slots default to none; a dir inside the repository, start/stop with {n}, and the measured memMb with it; anything else is a problem",
Dyn::Equal(Config::Defaults().at("slots"), {{"dir", nullptr}, {"start", nullptr}, {"stop", nullptr}, {"memMb", nullptr}, {"startMb", nullptr}})
&& AnyContains(bad(sl({{"dir", "s"}, {"memMb", 1}, {"startMb", 0}})), "slots.startMb")
&& bad(sl({{"dir", ".local-stack/slots"}, {"start", "bash s.sh full --slot {n}"}, {"stop", "bash s.sh stop --slot {n}"}, {"memMb", 1500}})).empty()
&& AnyContains(bad(sl({{"dir", "/abs"}, {"memMb", 1}})), "inside the repository") && AnyContains(bad(sl({{"dir", "s"}, {"start", "bash s.sh"}, {"memMb", 1}})), "{n}")
&& AnyContains(bad(sl({{"dir", "s"}})), "slots.memMb") && AnyContains(bad(sl({{"dir", "s"}, {"memMb", true}})), "slots.memMb") && bad({{"slots", {{"nope", 1}}}}).size() == 1);
Write(p, R"({"functional": {"concurrency": 4}})");
&& AnyContains(bad(sl({{"dir", "s"}})), "slots.memMb") && AnyContains(bad(sl({{"dir", "s"}, {"memMb", true}})), "slots.memMb") && bad(sl({{"nope", 1}})).size() == 1);
Write(p, R"({"functional": {"concurrency": 4}, "lenses": {"repo-bug-prover": {"run": true}}})");
const Config::Loaded cr = Config::Load(td);
const Config::Loaded cr2 = Config::Load("/", p);
const Json& warnings = cr.config.at("_warnings");
@ -341,6 +343,7 @@ int main() {
Write(td / ".local-stack" / "slots" / "1" / "slot.json", R"({"appUrl": "http://localhost:20180", "apiUrls": [], "dbUrl": null, "login": null})");
Json cfg = Config::Load(td).config;
cfg["slots"] = {{"dir", ".local-stack/slots"}, {"start", "up --slot {n}"}, {"stop", nullptr}, {"memMb", 1500}, {"startMb", 6000}};
cfg["lenses"]["repo-functional"]["run"] = true;
cfg["lenses"]["repo-functional"]["environment"] = "browser+supabase";
std::vector<std::string> out;
const Run::Machine machine = {.available = [] { return std::optional<std::int64_t>(20'000); }, .reserve = [] { return std::int64_t {4000}; }};