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

@ -148,7 +148,7 @@ jobs:
git config user.email ci@catcrafts.net
printf ".review-gates/push/\n.review-gates/trees/\n" > .gitignore
mkdir .review-gates
printf "{\"lenses\": {\"repo-functional\": {\"run\": false}}}\n" > .review-gates/config.json
printf "{\"lenses\": {\"repo-better-solution\": {\"run\": true}, \"repo-bug-prover\": {\"run\": true}, \"repo-security\": {\"run\": true}, \"repo-facts\": {\"run\": true}}}\n" > .review-gates/config.json
git add -A
git commit -q -m base
base=$(git rev-parse HEAD)

View file

@ -1,6 +1,6 @@
# Crafter Review
The pre-push review pipeline: adversarial lenses review a frozen commit range, one `claude --print` process per inventory unit, before the range is pushed. Five lenses in a fixed order: the approach, the range exercised in the running application through the Playwright MCP server, side by side the static bug proof and the security review, and last the commit truth of the messages and the documentation. The repository's config says which of them run and what each gets: the browser, the local Supabase database, both, or neither.
The pre-push review pipeline: adversarial lenses review a frozen commit range, one `claude --print` process per inventory unit, before the range is pushed. Five lenses in a fixed order: the approach, the range exercised in the running application through the Playwright MCP server, side by side the static bug proof and the security review, and last the commit truth of the messages and the documentation. Every lens is off until the repository's config turns it on, and the config says what each gets: the browser, the local Supabase database, both, or neither.
| Gate | Lens | Question |
|---|---|---|
@ -43,7 +43,7 @@ crafter-review config # edit .review-gates/config.json until it prints no
```
`init` does four things:
- it writes `.review-gates/config.json` from the template when there is none;
- it writes `.review-gates/config.json` from the template when there is none - every lens off: turn on the ones this repository runs;
- it adds `.review-gates/push/` and `.review-gates/trees/` to `.gitignore`;
- it writes the `/prepush` skill to `.claude/skills/prepush/SKILL.md`, which a later `init` refreshes (the line after its frontmatter marks it as init's);
- it prints the next steps.
@ -52,7 +52,7 @@ It writes nothing in a subdirectory, in your home directory (`--force` when that
`.review-gates/config.json` is committed. It says what the pipeline must know about the repository:
- who may author its commits, and which branch a range is measured against;
- which lenses run and the environment each gets (`none`, `browser`, `supabase` or `browser+supabase`);
- which lenses run (every lens is off until `lenses.<name>.run` is true) and the environment each gets (`none`, `browser`, `supabase` or `browser+supabase`);
- how its application is started and reached: one stack, or several side by side (`slots`);
- where its local database answers;
- which paths carry no runtime behaviour.

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;

View file

@ -842,7 +842,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: set base, then what the lenses' environments need)");
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)");
}
// The artifacts and the worktrees stay out of git.
const fs::path ignore = cwd / ".gitignore";
@ -887,6 +887,7 @@ 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");
if (browser) std::println(" {} browser install - a lens drives the application in Chromium", Invocation());
return 0;
}

View file

@ -1720,7 +1720,9 @@ namespace Crafter::Review::Run {
const std::vector<Plan::Gate> gates = Plan::GatesFor(only, without, off);
if (gates.empty()) {
std::vector<std::string> why;
if (!off.empty()) {
if (off.size() == Plan::Lenses.size()) {
why.emplace_back("the config turns on no lens (every lens is off until lenses.<name>.run = true turns it on)");
} else if (!off.empty()) {
std::vector<std::string> names;
for (const std::string& lf : off) names.push_back(Plan::LensName(lf));
why.push_back(std::format("the config turns off {}", Dyn::Join(names, ", ")));

View file

@ -30,7 +30,7 @@
<release version="1.0.0" date="2026-10-31">
<description>
<p>First release: the pre-push review gates in C++26, shipped as a Flatpak.</p>
<p>The commit-truth lens, repo-facts, reads the commit messages and the documentation of the range as Gate 4.</p>
<p>Every lens is off until the repository's config turns it on. The commit-truth lens, repo-facts, reads the commit messages and the documentation of the range as Gate 4.</p>
</description>
</release>
</releases>

View file

@ -6,7 +6,8 @@ the pull request it opens) is the next release. The `/prepush` skill drives
this file. The mechanics: freeze → hash → gates → the owner; five lenses,
the functional one as Gate 2 (Jorijn, 2026-10-15), the security lens beside
the bug-prover in Gate 3 (2026-10-22), the commit-truth lens as Gate 4
(2026-10-31).
(2026-10-31), every one of them off until the repository's config turns it
on (2026-10-31).
Nothing is pushed without the owner's explicit approval of the exact frozen
hash. Local commits parked unpushed are fine and normal.
@ -136,7 +137,9 @@ freeze accepts; null = `git config user.email`), `aiTrailers` (`forbid` or
`allow`), `requireSigned`, `allowDirty` (tracked
files the local stack rewrites in place while the lenses run), `gate0` (the
commands Gate 0 runs by hand, for the skill), `lenses` (per lens, by its
name: `run` — `false` leaves the lens out of every run of this repository:
name: `run` — every lens is off until it is `true` here (opt-in only,
2026-10-31); a lens the config does not turn on is out of every run of this
repository:
its gate disappears, the units only it reads are listed as not reviewed,
never as holes, and the run stays a full run; the ruling of record for a
repository with nothing to exercise, where `--without` is a per-run ruling
@ -308,7 +311,8 @@ diffstats — and the units of documentation files (`*.md`), each
heading-delimited part of a hunk on its own, and it asks `whole:order` and
`whole:absent` in slot passes of its own; a documentation file's diff header
and a part the commit writes nothing in are a mechanical `n/a`. A lens the
config turns off (`lenses.<name>.run = false`) reads nothing: the units only it would have read (its whole slot —
config leaves off (every lens is off until `lenses.<name>.run = true`) reads
nothing: the units only it would have read (its whole slot —
`whole:behaviour` for the functional lens; the prose for the commit-truth
lens) are listed under `coverage.notReviewed`, never as holes, and the
summary says so. The functional lens plans no pass on a diff header, on a
@ -606,6 +610,10 @@ repository.
5-hour column. Then (Jorijn: "there is no need to do a global cap anymore,
the cap should be 128 per available account") the machine-wide width
went: `--concurrency` caps only when given.
- 2026-10-31 — **the commit-truth lens as Gate 4** (Jorijn: "fix that in
Crafter.Review"). The owner's gate is 5. Its firing proof in this pipeline
is owed (the owner's go).
- 2026-10-31 — **every lens off by default, opt-in only; the commit-truth
lens as Gate 4** (Jorijn: "make it so all lenses are off by default and
it's opt in only"; and of the commit-truth lens, "fix that in
Crafter.Review"). A config that
turns on no lens is a problem `config` names and `freeze` and `run` stop
on; `init` says to turn lenses on; the owner's gate is 5. The commit-truth
lens's firing proof in this pipeline is owed (the owner's go).

View file

@ -1,6 +1,6 @@
---
name: prepush
description: Pre-push gates for a repository set up for crafter-review (`crafter-review init`). Runs the pipeline's procedure end to end — Gate 0 build/tests + freeze (identity, trailers by the config's rule, clean tree, no merges, fast-forward), the application and the database under test brought up from the frozen tree, the driver run of the lenses the config enables (repo-better-solution → repo-functional with the Playwright MCP → repo-bug-prover and repo-security side by side → repo-facts, the commit truth) via crafter-review, the fix/rerun loop, the owner's approval of the hash, verify, push. Nothing is pushed unreviewed or unapproved.
description: Pre-push gates for a repository set up for crafter-review (`crafter-review init`). Runs the pipeline's procedure end to end — Gate 0 build/tests + freeze (identity, trailers by the config's rule, clean tree, no merges, fast-forward), the application and the database under test brought up from the frozen tree, the driver run of the lenses the config turns on - every lens is off until it does (repo-better-solution → repo-functional with the Playwright MCP → repo-bug-prover and repo-security side by side → repo-facts, the commit truth) via crafter-review, the fix/rerun loop, the owner's approval of the hash, verify, push. Nothing is pushed unreviewed or unapproved.
---
Follow the pipeline's procedure, `prepush.md`, EXACTLY — read it now, in
@ -14,7 +14,8 @@ of running it:
(`git log --oneline @{u}..HEAD`, or `$(git merge-base HEAD <config base>)..HEAD`
for a branch that tracks nothing yet), the slug (the branch or release
name), and the config: `flatpak run net.catcrafts.CrafterReview config`
must print no problem.
must print no problem (every lens is off until `lenses.<name>.run = true`
turns it on: turn on the ones this repository runs).
2. **Gate 0**: run the config's `gate0` commands on that HEAD and keep their
output. Then freeze:

View file

@ -12,23 +12,23 @@
],
"lenses": {
"repo-better-solution": {
"run": true,
"run": false,
"environment": "none"
},
"repo-functional": {
"run": true,
"run": false,
"environment": "browser"
},
"repo-bug-prover": {
"run": true,
"run": false,
"environment": "none"
},
"repo-security": {
"run": true,
"run": false,
"environment": "none"
},
"repo-facts": {
"run": true,
"run": false,
"environment": "none"
}
},

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}; }};