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