From 71dba5690ff9d7b7d4dc66added96d0e6cfb6d9a Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sat, 31 Oct 2026 06:56:09 +0100 Subject: [PATCH] test(pipeline): the checks of the pure pipeline Stream, Normalize, Merge, Dispositions, Config, CodeKeys and the pure half of CommitsMode, each a test of its own (131 checks). The fixture rewrites are checked in (code-keys//, commits3/), so the suite reads fixtures and edits nothing. The allowlist, command-line and MCP checks of CommitsMode come with the pool they test. Among them: this binary's ` sql` spelling counts as a SQL call, and a result line with a raw U+2028 in a JSON string is still the result line. Co-Authored-By: Claude Opus 5.5 --- project.cpp | 9 +- tests/CodeKeys/main.cpp | 180 ++ tests/CommitsMode/main.cpp | 271 ++ tests/Config/main.cpp | 147 + tests/Dispositions/main.cpp | 46 + .../0001-add-the-greeting-module.patch | 30 + .../0002-greet-by-name-and-document-it.patch | 55 + .../fixture/commits3/0003-add-tests.patch | 24 + tests/Goldens/fixture/pipeline.json | 2774 +++++++++++++++++ tests/Merge/main.cpp | 76 + tests/Normalize/main.cpp | 70 + tests/Stream/main.cpp | 97 + 12 files changed, 3778 insertions(+), 1 deletion(-) create mode 100644 tests/CodeKeys/main.cpp create mode 100644 tests/CommitsMode/main.cpp create mode 100644 tests/Config/main.cpp create mode 100644 tests/Dispositions/main.cpp create mode 100644 tests/Goldens/fixture/commits3/0001-add-the-greeting-module.patch create mode 100644 tests/Goldens/fixture/commits3/0002-greet-by-name-and-document-it.patch create mode 100644 tests/Goldens/fixture/commits3/0003-add-tests.patch create mode 100644 tests/Merge/main.cpp create mode 100644 tests/Normalize/main.cpp create mode 100644 tests/Stream/main.cpp diff --git a/project.cpp b/project.cpp index 7c28329..049a986 100644 --- a/project.cpp +++ b/project.cpp @@ -64,12 +64,19 @@ namespace { // One per directory under tests/, each an ordinary main() that links the // library: exit 0 passes, nonzero fails, 77 skips. - constexpr std::array Tests = { + constexpr std::array Tests = { "Crypto", "Text", "Platform", "Process", "Goldens", + "Stream", + "Normalize", + "Merge", + "Dispositions", + "Config", + "CommitsMode", + "CodeKeys", }; // The version the binary reports: CI sets it from the tag, a checkout diff --git a/tests/CodeKeys/main.cpp b/tests/CodeKeys/main.cpp new file mode 100644 index 0000000..f13f075 --- /dev/null +++ b/tests/CodeKeys/main.cpp @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// Code keys: a static code pass is keyed on its code, a functional pass on +// its code AND the range. Every rewrite of the commits fixture is checked in +// under tests/Goldens/fixture/code-keys//. + +import std; +import Crafter.Review; +namespace fs = std::filesystem; +using namespace Crafter::Review; + +namespace { + std::int32_t Failures = 0; + const std::string Fn(Plan::FunctionalLens); + + void Check(std::string_view name, bool cond, std::string_view detail = {}) { + std::println("{}{}{}", cond ? " ok " : " FAIL ", name, !cond && !detail.empty() ? std::format(" ({})", detail) : std::string()); + if (!cond) ++Failures; + } + + using Keys = std::map; + + Keys CodeKeys(const fs::path& d, const std::string& lens = "repo-bug-prover.md", std::string_view env = {}) { + const Inventory::Built built = Inventory::Build("commits", d.string()); + const Plan::CommitCode cc = Plan::CommitCodeOf(Artifact::PatchTexts(d)); + Keys out; + for (const Json& u : built.out.at("units")) { + const std::string id = u.at("id").get(); + if (Plan::KindFor(lens, u) != "code") continue; + if (lens == Fn && (id.ends_with(":outside") || id.ends_with(":absent") || id.ends_with("#hdr"))) continue; + const Json job = {{"kind", "block"}, {"unit", id}, {"unitKind", "code"}}; + out[id] = Plan::BlockReuseKey(job, lens, Plan::UnitText(built.lines, u), cc, {}, "c", 1, env).value_or(""); + } + return out; + } + + std::optional WholeKey(const fs::path& d, const std::string& lens = "repo-better-solution.md", std::string_view env = {}) { + return Plan::WholeCodeKey({{"kind", "whole"}, {"unit", nullptr}}, lens, Plan::CommitCodeOf(Artifact::PatchTexts(d)), {}, "c", 1, env); + } + + std::set Changed(const Keys& now, const Keys& before) { + std::set out; + for (const auto& [id, key] : before) { + if (now.at(id) != key) out.insert(id); + } + return out; + } + + bool AllChanged(const Keys& now, const Keys& before) { + return Changed(now, before).size() == before.size(); + } + + std::string Show(const std::set& ids) { + Json out = Json::array(); + for (const std::string& id : ids) out.push_back(id); + return Dyn::ReprValue(out); + } +} + +int main() { + std::println("reuse keys: a static code pass is keyed on its code; a functional pass on its code AND the range (fixtures/commits and rewrites of it)"); + const fs::path fixture = fs::current_path() / "tests" / "Goldens" / "fixture"; + const fs::path src = fixture / "commits"; + auto rewrite = [&](std::string_view name) { return fixture / "code-keys" / name; }; + const Keys base = CodeKeys(src); + const Keys fbase = CodeKeys(src, Fn); + const Keys sbase = CodeKeys(src, "repo-security.md"); + std::set baseValues; + std::set sbaseValues; + for (const auto& [id, key] : base) baseValues.insert(key); + for (const auto& [id, key] : sbase) sbaseValues.insert(key); + const bool keysDiffer = std::ranges::all_of(base, [&](const auto& entry) { return sbase.contains(entry.first) && sbase.at(entry.first) != entry.second; }); + Check("the security lens keys the same code units as the bug-prover, every key its own (the lens file is in the key): a verdict of one is never evidence for the other", + std::ranges::equal(std::views::keys(sbase), std::views::keys(base)) && keysDiffer && sbaseValues.size() == baseValues.size()); + std::set slots; + for (const auto& [id, key] : base) { + if (id.ends_with(":outside") || id.ends_with(":absent")) slots.insert(id); + } + std::set fbaseValues; + for (const auto& [id, key] : fbase) fbaseValues.insert(key); + const std::set functionalUnits = {"p1:hello.py#1", "p2:hello.py#1", "p2:hello.py#2"}; + std::set shared; + std::ranges::set_intersection(baseValues, fbaseValues, std::inserter(shared, shared.begin())); + Check("fixture: every static code unit has a key (4 slots, hunks, headers), the functional units are the hunks alone, no doc unit, no shared key", + std::ranges::all_of(base, [](const auto& entry) { return !entry.second.empty(); }) && slots.size() == 4 && base.contains("p2:hello.py#hdr") + && std::set(std::views::keys(fbase).begin(), std::views::keys(fbase).end()) == functionalUnits + && std::ranges::none_of(base, [](const auto& entry) { return entry.first.contains(".md#"); }) && shared.empty()); + + const fs::path rw = rewrite("rewrite"); + Check("a rewrite that leaves the code alone (ids, counter, blobs, line numbers, git version, a reworded message) -> every key the same, static and functional", + CodeKeys(rw) == base && CodeKeys(rw, Fn) == fbase && WholeKey(rw) == WholeKey(src) && WholeKey(rw, Fn) == WholeKey(src, Fn) + && Artifact::ArtifactHash("commits", rw) != Artifact::ArtifactHash("commits", src)); + const fs::path code = rewrite("code"); + std::set diff = Changed(CodeKeys(code), base); + std::set expected = slots; + expected.insert("p2:hello.py#1"); + Check("one changed line in p2:hello.py#1 -> static: that hunk and the four slots, nothing else", diff == expected, Show(diff)); + Check("...functional: EVERY unit is asked again (the range is in its key)", AllChanged(CodeKeys(code, Fn), fbase)); + Check("...and both whole passes", WholeKey(code) != WholeKey(src) && WholeKey(code, Fn) != WholeKey(src, Fn)); + const Keys ctx = CodeKeys(rewrite("context")); + Check("a changed context line re-keys its hunk", ctx.at("p2:hello.py#1") != base.at("p2:hello.py#1") && ctx.at("p2:hello.py#2") == base.at("p2:hello.py#2")); + const fs::path nobody = rewrite("nobody"); + Check("p2 loses its body -> p2:absent alone (static); nothing functional", Changed(CodeKeys(nobody), base) == std::set{"p2:absent"} && CodeKeys(nobody, Fn) == fbase); + const fs::path bse = rewrite("base"); + Check("another base -> every key changes, static, functional and both wholes", AllChanged(CodeKeys(bse), base) && AllChanged(CodeKeys(bse, Fn), fbase) && WholeKey(bse) != WholeKey(src)); + const fs::path mode = rewrite("mode"); + expected = slots; + expected.insert("p2:hello.py#hdr"); + Check("a mode change re-keys the file header (and the slots), not the hunks; the functional hunks follow the range", Changed(CodeKeys(mode), base) == expected + && AllChanged(CodeKeys(mode, Fn), fbase)); + const fs::path docs = rewrite("docs"); + Check("a reworded README sentence -> no key changes anywhere (documentation is nobody's code)", CodeKeys(docs) == base && CodeKeys(docs, Fn) == fbase && WholeKey(docs) == WholeKey(src)); + const fs::path nodoc = rewrite("nodoc"); + Check("p2 drops its README hunk -> p2's two slots and the whole passes are asked again, no hunk", Changed(CodeKeys(nodoc), base) == std::set{"p2:absent", "p2:outside"} + && WholeKey(nodoc) != WholeKey(src) && CodeKeys(nodoc, Fn) == fbase); + + // The environment in the keys (plan.ENVIRONMENTS item 1). + const Keys sb = CodeKeys(src, "repo-bug-prover.md", "supabase"); + const Keys fb = CodeKeys(src, Fn, "browser+supabase"); + Check("environment: a lens in its default environment keeps the key every stored verdict was built with (none for the static lenses, browser for the functional lens)", + CodeKeys(src, "repo-bug-prover.md", "none") == base && CodeKeys(src, Fn, "browser") == fbase && WholeKey(src, "repo-better-solution.md", "none") == WholeKey(src) + && WholeKey(src, Fn, "browser") == WholeKey(src, Fn) && Plan::EnvDeps("repo-bug-prover.md").empty() && Plan::EnvDeps(Fn, "browser").empty() + && Plan::EnvDeps(Fn, "browser+supabase") == std::vector{"env=browser+supabase"}); + std::set everyKey; + for (const Plan::Environment& environment : Plan::Environments) { + for (const auto& [id, key] : CodeKeys(src, "repo-bug-prover.md", environment.name)) everyKey.insert(key); + } + Check("environment: any other environment re-keys every unit, block and whole, static and functional - each environment its own keys, none shared", + AllChanged(sb, base) && AllChanged(fb, fbase) && WholeKey(src, "repo-better-solution.md", "supabase") != WholeKey(src) + && WholeKey(src, Fn, "browser+supabase") != WholeKey(src, Fn) && everyKey.size() == Plan::Environments.size() * base.size()); + expected = slots; + expected.insert("p2:hello.py#1"); + Check("environment: the code rules hold inside an environment - a rewrite that leaves the code alone keeps every supabase key, a changed line re-keys that hunk and the slots alone", + CodeKeys(rw, "repo-bug-prover.md", "supabase") == sb && CodeKeys(rw, Fn, "browser+supabase") == fb + && WholeKey(rw, "repo-better-solution.md", "supabase") == WholeKey(src, "repo-better-solution.md", "supabase") + && Changed(CodeKeys(code, "repo-bug-prover.md", "supabase"), sb) == expected); + + const Plan::CommitCode cc = Plan::CommitCodeOf(Artifact::PatchTexts(src)); + Check("whole key: both wholes have keys and differ; a block never; the bug-prover has none", WholeKey(src) && WholeKey(src, Fn) && WholeKey(src) != WholeKey(src, Fn) + && !WholeKey(src, "repo-bug-prover.md") && !Plan::WholeCodeKey({{"kind", "block"}, {"unit", "x"}}, "repo-better-solution.md", cc, {}, "c", 1)); + const Json job = {{"kind", "whole"}, {"unit", nullptr}}; + const std::vector w1 = {"w1"}; + Json withModel = job; + withModel["model"] = "m"; + const std::set> wholes = { + WholeKey(src), Plan::WholeCodeKey(job, "repo-better-solution.md", cc, w1, "c", 1), Plan::WholeCodeKey(job, "repo-better-solution.md", cc, {}, "c2", 1), + Plan::WholeCodeKey(job, "repo-better-solution.md", cc, {}, "c", 2), Plan::WholeCodeKey(withModel, "repo-better-solution.md", cc, {}, "c", 1), + }; + Check("whole key: dispositions, the charter, the prompt version and the model are in it", wholes.size() == 5); + Plan::CommitCode two; + two.base = "b"; + two.range = "r"; + two.code = {{3, Plan::Sha("")}, {4, Plan::Sha("")}}; + two.body = {{3, true}, {4, true}}; + two.docs = {{3, {"h"}}, {4, {"h"}}}; + two.upto = {{3, "u"}, {4, "u"}}; + Plan::CommitCode renamed = two; + renamed.docs = {{3, {"h2"}}, {4, {"h"}}}; + Check("slot keys: two documentation-only commits do not share a slot key (the position is in it); a renamed doc file re-asks the slots of its commit", + Plan::CodeKeyParts("p3:absent", two) != Plan::CodeKeyParts("p4:absent", two) + && Plan::CodeKeyParts("p3:outside", two) != Plan::CodeKeyParts("p3:outside", renamed) && Plan::CodeKeyParts("p3:absent", two) != Plan::CodeKeyParts("p3:absent", renamed)); + Check("a hunk is keyed on its file and text, not on its number or its commit; the functional ident is its own", Plan::CodeKeyParts("p2:hello.py#2", cc) == Plan::CodeKeyParts("p7:hello.py#5", cc) + && Plan::CodeKeyParts("p2:hello.py#2", cc).first != Plan::CodeKeyParts("p2:other.py#2", cc).first + && Plan::CodeKeyParts("p2:hello.py#hdr", cc).first != Plan::CodeKeyParts("p2:hello.py#1", cc).first); + const std::vector parts = {"repo-bug-prover.md", "U", Plan::Sha("T"), Plan::Sha("d"), "w1", "cs", "1", "", "max"}; + Check("reuse key: the pre-2026-10-02 formula with the effort slot frozen at max", Plan::ReuseKey({{"kind", "block"}, {"unit", "U"}}, "repo-bug-prover.md", "T", {"d"}, {"w1"}, "cs", 1) + == Plan::Sha(Dyn::Join(parts, "|")).substr(0, 16) && Plan::ReuseKeyEffortSlot == "max"); + Check("code_norm leaves a hunk body alone", Plan::CodeNorm("@@ -1,2 +1,2 @@ f\n-- \n+index 1..2\n+@@ -1 +1 @@") == "@@ f\n-- \n+index 1..2\n+@@ -1 +1 @@"); + const Json e = {{"unit", "p2:hello.py#2"}, {"coverage", {{"item", "p2:hello.py#2"}, {"verdict", "clear"}, {"basis", "b"}}}, {"findings", Json::array()}, + {"notes", {{{"note", "n"}, {"items", {"p2:hello.py#2", "p2:hello.py#1"}}}}}}; + const Json r = Plan::Rebind(e, "p2:hello.py#3"); + Json v = {{"verdict", "SEND-READY"}, {"findings", r.at("findings")}, {"notes", r.at("notes")}, {"coverage", {r.at("coverage")}}}; + const std::vector ids = {"p2:hello.py#3"}; + const Json n = Normalize::NormalizeOutput(v, {}, "repo-bug-prover.md", "H", Json::array({{{"id", "p2:hello.py#3"}}}), ids); + Check("rebind: the stored result under the unit's id of today, and it covers the renumbered unit", r.at("unit") == "p2:hello.py#3" + && r.at("notes").at(0).at("items") == Json::array({"p2:hello.py#3"}) && e.at("coverage").at("item") == "p2:hello.py#2" && n.at("verdict") == "SEND-READY" + && n.at("coverageMissing").empty()); + return Failures ? 1 : 0; +} diff --git a/tests/CommitsMode/main.cpp b/tests/CommitsMode/main.cpp new file mode 100644 index 0000000..dcb4352 --- /dev/null +++ b/tests/CommitsMode/main.cpp @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// Commits mode: the inventory, the gates, the plans (the functional lens's +// n/a), the prompts, over the commits fixture. The allowlist, command line +// and MCP checks sit with the pool they test. + +import std; +import Crafter.Review; +namespace fs = std::filesystem; +using namespace Crafter::Review; + +namespace { + std::int32_t Failures = 0; + const std::string Fn(Plan::FunctionalLens); + + void Check(std::string_view name, bool cond, std::string_view detail = {}) { + std::println("{}{}{}", cond ? " ok " : " FAIL ", name, !cond && !detail.empty() ? std::format(" ({})", detail) : std::string()); + if (!cond) ++Failures; + } + + std::set Set(const std::vector& values) { + return {values.begin(), values.end()}; + } + + std::string Show(const std::set& values) { + Json out = Json::array(); + for (const std::string& value : values) out.push_back(value); + return Dyn::ReprValue(out); + } + + const Json& Unit(const Json& units, std::string_view id) { + for (const Json& unit : units) { + if (unit.at("id") == id) return unit; + } + throw std::runtime_error(std::format("no unit {}", id)); + } + + const Json& Job(const std::vector& jobs, std::string_view kind, std::string_view unit = {}) { + for (const Json& job : jobs) { + if (job.at("kind") == kind && (unit.empty() || job.at("unit") == unit)) return job; + } + throw std::runtime_error(std::format("no {} job {}", kind, unit)); + } + + std::vector>> Gates(const std::vector& gates) { + std::vector>> out; + for (const Plan::Gate& gate : gates) out.emplace_back(std::string(gate.title), std::vector(gate.lenses.begin(), gate.lenses.end())); + return out; + } + + std::vector Titles(const std::vector& gates) { + std::vector out; + for (const Plan::Gate& gate : gates) out.emplace_back(gate.title); + return out; + } + + // re.findall(r'(?i)\b(exploit\w*|attack\w*|...)\b', text): the words that + // start with one of the stems. + std::vector Offensive(std::string_view text) { + constexpr std::array Stems = {"exploit", "attack", "payload", "malicious", "weaponi", "hack", "pwn", "backdoor", "intrusion", "penetrat"}; + const std::u32string decoded = Dyn::Decode(text); + std::vector found; + std::size_t at = 0; + while (at < decoded.size()) { + if (!Dyn::IsWord(decoded[at])) { + ++at; + continue; + } + std::size_t end = at; + while (end < decoded.size() && Dyn::IsWord(decoded[end])) ++end; + const std::u32string_view word = std::u32string_view(decoded).substr(at, end - at); + for (const std::string_view stem : Stems) { + if (word.size() < stem.size()) continue; + bool match = true; + for (std::size_t i = 0; i < stem.size(); ++i) match = match && Dyn::IgnoreCaseLetter(word[i]) == stem[i]; + if (match) { + found.push_back(Dyn::Encode(word)); + break; + } + } + at = end; + } + return found; + } +} + +int main() { + std::println("commits mode: inventory, gates, plans (the functional lens's n/a), prompts (fixtures/commits)"); + SetHome(fs::current_path()); + const fs::path fixture = fs::current_path() / "tests" / "Goldens" / "fixture"; + const fs::path d = fixture / "commits"; + const Inventory::Built built = Inventory::Build("commits", d.string()); + const Json& units = built.out.at("units"); + const std::vector& lines = built.lines; + std::vector ids; + for (const Json& u : units) ids.push_back(u.at("id").get()); + const std::set idSet = Set(ids); + Check("body-less p1: no msg/trailers units, subject/diffstat/hunk present", !idSet.contains("p1:msg") && !idSet.contains("p1:trailers") && idSet.contains("p1:subject") + && idSet.contains("p1:diffstat") && idSet.contains("p1:hello.py#1")); + Check("p2: msg unit, no trailers, three hunks over two files", idSet.contains("p2:msg") && !idSet.contains("p2:trailers") && idSet.contains("p2:README.md#1") + && idSet.contains("p2:hello.py#1") && idSet.contains("p2:hello.py#2")); + Check("the three whole slots", idSet.contains("whole:order") && idSet.contains("whole:absent") && idSet.contains("whole:behaviour") && !idSet.contains("whole:recipients")); + Check("body-less commit: diffstat unit starts at the --- line", Plan::UnitText(lines, Unit(units, "p1:diffstat")).starts_with("---")); + std::string all; + for (const std::string& name : Artifact::PatchFiles(d)) all += *Platform::ReadFile(d / name); + Check("artifact_hash = sha256 of the sorted concatenation; patch_head = the From line of the last patch", Artifact::ArtifactHash("commits", d) == Sha256Hex(all) + && Artifact::PatchHead(d) == std::string(40, '2')); + const auto g = Gates(Plan::GatesFor()); + std::vector gateNames; + for (const auto& [title, lenses] : g) gateNames.push_back(Dyn::Split(title, " — ")[0]); + Check("gates: 1 approach, 2 functional, 3 proof - the bug-prover and the security lens side by side (2026-10-22), the owner stays Gate 4", + gateNames == std::vector{"Gate 1", "Gate 2", "Gate 3"} && g[0].second == std::vector{"repo-better-solution.md"} && g[1].second == std::vector{Fn} + && g[2].second == std::vector{"repo-bug-prover.md", "repo-security.md"}); + const std::vector onlyFn = {Fn}; + const std::vector security = {"repo-security.md"}; + std::set lensSet; + for (const Plan::Lens& lens : Plan::Lenses) lensSet.emplace(lens.file); + using GateList = std::vector>>; + Check("mode_lenses = the four; --only keeps, --without drops, an emptied gate disappears; Gate 3 with one of its lenses dropped keeps the other", + Set(Plan::ModeLenses()) == lensSet && Gates(Plan::GatesFor(onlyFn)) == GateList{{"Gate 2 — Functional", {Fn}}} + && Titles(Plan::GatesFor({}, onlyFn)) == std::vector{"Gate 1 — Approach", "Gate 3 — Proof"} && Plan::GatesFor(onlyFn, onlyFn).empty() + && Gates(Plan::GatesFor({}, security)).back() == std::pair>{"Gate 3 — Proof", {"repo-bug-prover.md"}} + && Gates(Plan::GatesFor(security)) == GateList{{"Gate 3 — Proof", {"repo-security.md"}}}); + const std::vector threeOff = {"repo-better-solution.md", "repo-bug-prover.md", "repo-security.md"}; + const std::vector bugOff = {"repo-bug-prover.md"}; + Check("lenses off by config: gates_for drops the lens and its gate; --only cannot bring it back", Titles(Plan::GatesFor({}, {}, onlyFn)) == std::vector{"Gate 1 — Approach", "Gate 3 — Proof"} + && Plan::GatesFor(onlyFn, {}, onlyFn).empty() && Gates(Plan::GatesFor({}, {}, threeOff)) == GateList{{"Gate 2 — Functional", {Fn}}} + && Gates(Plan::GatesFor({}, {}, bugOff)).back() == std::pair>{"Gate 3 — Proof", {"repo-security.md"}}); + const std::vector allLenses(lensSet.begin(), lensSet.end()); + const std::set nrAll = Set(Plan::NotReviewed(units, lines, Plan::ModeLenses())); + Check("not reviewed (every lens runs): the text units and the documentation units", nrAll == std::set{"p1:subject", "p1:diffstat", "p2:subject", "p2:msg", "p2:diffstat", "p2:README.md#hdr", "p2:README.md#1"}, + Show(nrAll)); + std::vector statics; + for (const Plan::Lens& lens : Plan::Lenses) { + if (lens.file != Fn) statics.emplace_back(lens.file); + } + std::set plusBehaviour = nrAll; + plusBehaviour.insert("whole:behaviour"); + std::set plusStatic = nrAll; + plusStatic.insert({"whole:order", "whole:absent", "p1:outside", "p1:absent", "p2:outside", "p2:absent", "p1:hello.py#hdr", "p2:hello.py#hdr"}); + Check("not reviewed is derived from the lenses that run: the functional lens off adds whole:behaviour; only the functional lens adds the static slots and the file headers (a mechanical n/a is no reading)", + Set(Plan::NotReviewed(units, lines, statics)) == plusBehaviour && Set(Plan::NotReviewed(units, lines, onlyFn)) == plusStatic, Show(Set(Plan::NotReviewed(units, lines, onlyFn)))); + std::set codeIds; + for (const std::string& id : ids) { + if (Plan::UnitKindOf(id) == "code" && !id.contains(".md#")) codeIds.insert(id); + } + auto unitsOf = [](const std::vector& jobs) { + std::set out; + for (const Json& job : jobs) out.insert(job.at("unit").get()); + return out; + }; + const Plan::LensPlan approach = Plan::PlanLens("repo-better-solution.md", units, lines); + std::vector expected = {approach.whole.at("expectedIds").at(0).get(), approach.whole.at("expectedIds").at(1).get()}; + std::ranges::sort(expected); + Check("repo-better-solution: whole pass over order+absent, every code unit incl. outside/absent, no doc unit", !approach.whole.is_null() + && expected == std::vector{"whole:absent", "whole:order"} && approach.whole.at("expectedIds").size() == 2 && unitsOf(approach.blocks) == codeIds + && std::ranges::all_of(approach.blocks, [](const Json& j) { return j.at("kind") == "block"; })); + const Plan::LensPlan prover = Plan::PlanLens("repo-bug-prover.md", units, lines); + Check("repo-bug-prover: no whole pass, the same code units", prover.whole.is_null() && unitsOf(prover.blocks) == codeIds && codeIds.contains("p2:hello.py#hdr")); + const Plan::LensPlan sec = Plan::PlanLens("repo-security.md", units, lines); + const Plan::Lens& s = Plan::LensOf("repo-security.md"); + const Plan::Lens& b = Plan::LensOf("repo-bug-prover.md"); + const bool sameShape = s.gate == b.gate && s.whole == b.whole && std::ranges::equal(s.slots, b.slots) && std::ranges::equal(s.block, b.block) && s.model == b.model && s.reuse == b.reuse + && s.wholeReuse == b.wholeReuse && s.reads == b.reads && s.key == b.key && s.empirical == b.empirical && s.perCommitSlots == b.perCommitSlots && s.env == b.env; + Check("repo-security: the bug-prover's shape - no whole pass, the same code units, static blocks keyed on the code, three reads, none by default, not empirical", + sec.whole.is_null() && unitsOf(sec.blocks) == codeIds && sameShape && s.gate == 3 + && std::ranges::all_of(sec.blocks, [](const Json& j) { return j.at("maxTurns") == 60 && j.at("timeoutMin") == 40 && j.at("model") == Plan::ModelFor("opus"); })); + const std::vector exclude = {"**/*.test.*", "docs/**"}; + const Plan::LensPlan functional = Plan::PlanLens(Fn, units, lines, {}, exclude); + std::map kinds; + for (const Json& j : functional.blocks) kinds[j.at("unit").get()] = j.at("kind").get(); + const std::map wantKinds = {{"p1:hello.py#hdr", "auto-na"}, {"p1:hello.py#1", "block"}, {"p2:hello.py#hdr", "auto-na"}, {"p2:hello.py#1", "block"}, {"p2:hello.py#2", "block"}}; + Check("repo-functional: whole pass over whole:behaviour only; hunks as blocks; file headers n/a; no per-commit slots; no doc unit", !functional.whole.is_null() + && functional.whole.at("expectedIds") == Json::array({"whole:behaviour"}) && kinds == wantKinds); + const Json& functionalBlock = Job(functional.blocks, "block"); + Check("functional budgets: block 90/60, whole 220/120; static block 60/40, whole 180/105", functionalBlock.at("maxTurns") == 90 && functionalBlock.at("timeoutMin") == 60 + && functional.whole.at("maxTurns") == 220 && functional.whole.at("timeoutMin") == 120 && prover.blocks.at(0).at("maxTurns") == 60 && prover.blocks.at(0).at("timeoutMin") == 40 + && approach.whole.at("maxTurns") == 180 && approach.whole.at("timeoutMin") == 105); + + // A third commit touching a test file: excluded for the functional lens, + // a block for the static ones. + const Inventory::Built three = Inventory::Build("commits", (fixture / "commits3").string()); + const std::vector testsGlob = {"tests/**"}; + std::map> f3; + for (const Json& j : Plan::PlanLens(Fn, three.out.at("units"), three.lines, {}, testsGlob).blocks) { + if (j.at("unit").get().starts_with("p3:")) f3[j.at("unit").get()] = {j.at("kind").get(), Dyn::Get(j, "basis").is_string() ? j.at("basis").get() : ""}; + } + std::map s3; + for (const Json& j : Plan::PlanLens("repo-bug-prover.md", three.out.at("units"), three.lines).blocks) { + if (j.at("unit").get().starts_with("p3:")) s3[j.at("unit").get()] = j.at("kind").get(); + } + Check("functional.exclude: the test hunk is a mechanical n/a naming the glob; the static lens still reads it", f3["p3:tests/test_hello.py#1"].first == "auto-na" + && f3["p3:tests/test_hello.py#1"].second.contains("tests/**") && s3["p3:tests/test_hello.py#1"] == "block" && !f3.contains("p3:outside") && s3.contains("p3:outside")); + const std::vector testTs = {"**/*.test.ts"}; + const std::vector github = {".github/**"}; + Check("excluded_by: ** prefixes match at the root too", Plan::ExcludedBy("a.test.ts", testTs) == "**/*.test.ts" && Plan::ExcludedBy("src/a.test.ts", testTs) + && !Plan::ExcludedBy("src/a.ts", testTs) && Plan::ExcludedBy(".github/x.yml", github)); + + // Prompts. + const std::string st = Prompt::SystemText(); + Check("system text: commit range; the browser tools belong to a lens with an APPLICATION UNDER TEST block, not to the functional lens by name", st.contains("commit range") + && st.contains("mcp__playwright__") && st.contains("APPLICATION UNDER TEST block") && !st.contains("The functional lens alone") && !st.contains("The functional lens acts")); + const std::string h = Artifact::ArtifactHash("commits", d); + const std::vector dot = {"."}; + const std::string rl = Prompt::RunLayer(".review-gates/push/x", "abc123", "2026-10-15", dot, h, lines, units, {}, "myrepo"); + Check("run layer: INPUTS (tree `.`, base, no review record), the artifact with line numbers, the inventory", rl.contains("myrepo") && rl.contains("`.` is the working directory") + && !Dyn::Lower(rl).contains("ledger") && rl.contains("\nARTIFACT (frozen, hash ") && rl.contains("\n\nINVENTORY - ") && rl.contains("\n0001| ")); + const std::vector worktree = {".review-gates/trees/r-1234"}; + const std::vector captures = {"captures/"}; + const std::string rl2 = Prompt::RunLayer("x", "", "2026-10-15", worktree, h, lines, units, captures, "r"); + Check("run layer: the context paths and a worktree named", rl2.contains("captures/") && rl2.contains(".review-gates/trees/r-1234") && rl2.contains("base-commit line")); + const std::string totp = "/app/bin/crafter-review totp cypress/fixtures/created-user.json"; + const std::string ab = Prompt::AppBlock({{"appUrl", "http://localhost:8080"}, {"apiUrls", {"http://127.0.0.1:54321"}}, {"login", {{"notes", "TOTP enrolled"}}}, {"notes", "leave billing alone"}}, + "cypress/fixtures/created-user.json", totp); + Check("app block: this lens drives it (no lens named); the URL, the origins, the fixture path (not its content), the exact TOTP command, the notes, the shared-stack rules", + ab.contains("this lens drives it") && !ab.contains("every other lens") && ab.contains("http://localhost:8080") && ab.contains("127.0.0.1:54321") + && ab.contains("cypress/fixtures/created-user.json") && ab.contains(std::format("`{}`", totp)) && ab.contains("TOTP enrolled") && ab.contains("leave billing alone") + && ab.contains("not reset between passes") && ab.contains("browser_take_screenshot") && ab.contains("browser_snapshot before every action")); + const std::string ab2 = Prompt::AppBlock({{"appUrl", "http://localhost:1"}}); + Check("app block without a fixture says so", ab2.contains("no fixture was found")); + const std::string tailApproach = Prompt::Tail(approach.whole, units, lines); + const std::string tailFunctional = Prompt::Tail(functional.whole, units, lines); + Check("whole tails: the approach text lists order+absent; the functional text lists behaviour and names the APPLICATION UNDER TEST", tailApproach.contains("for THIS repository") + && tailApproach.contains("whole:order") && tailFunctional.contains("as a USER") && tailFunctional.contains("whole:behaviour") && !tailFunctional.contains("whole:order")); + Check("block tails: the functional hint on a functional block, none on a static hunk, the repo hints on the slots", + Prompt::Tail(functionalBlock, units, lines).contains("judges ONE hunk in the running application") && !Prompt::Tail(Job(prover.blocks, "block", "p2:hello.py#1"), units, lines).contains("judges ONE hunk") + && Prompt::Tail(Job(prover.blocks, "block", "p1:absent"), units, lines).contains("body-less commit") && Prompt::Tail(Job(prover.blocks, "block", "p1:outside"), units, lines).contains("users and callers")); + const std::string ll = Prompt::LensLayer(Fn, "CHARTER TEXT", "DISPOSITIONS BLOCK", ab, "SUMMARY"); + const std::string sp = Prompt::SystemPrompt(st, rl, ll); + const std::string up = Prompt::UserPrompt(tailFunctional, {{"maxTurns", 220}}); + Check("lens layer order: charter, application block, dispositions, the whole summary; system prompt = common + run + lens; user prompt = tail + budget line, no run layer", + ll.find("CHARTER TEXT") < ll.find("APPLICATION UNDER TEST") && ll.find("APPLICATION UNDER TEST") < ll.find("DISPOSITIONS BLOCK") && ll.find("DISPOSITIONS BLOCK") < ll.find("SUMMARY") + && sp.starts_with(std::format("{}{}{}{}", st, Prompt::SepRun, rl, Prompt::SepLens)) && sp.ends_with(ll) && up.contains("Tool-call budget for this pass: 220") + && !up.contains(rl.substr(0, 200))); + const std::string sql = "/app/bin/crafter-review sql"; + const std::string dbb = Prompt::DbBlock({{"dbUrl", "postgresql://postgres:hunter2@127.0.0.1:54322/postgres"}, {"notes", "the scan tables are seeded"}}, sql, "postgres", false, + "it answers (x); its 3 applied migrations are the range HEAD's", true); + const std::string dba = Prompt::DbBlock({{"notes", ""}}, sql, "postgres", true); + Check("db block: the exact helper command and its heredoc, one transaction rolled back, nothing shared, the refused statements, the API roles, the limits, the evidence, the owner's notes - never the URL or its password", + dbb.starts_with("DATABASE UNDER TEST") && dbb.contains(std::format("`{} <<'SQL'`", sql)) && dbb.contains("rolls back at its end") && dbb.contains("not by the other passes") + && dbb.contains("COMMIT") && dbb.contains("ROLLBACK TO are fine") && dbb.contains("set local role authenticated") && dbb.contains("request.jwt.claims") && dbb.contains("service_role") + && dbb.contains("statement_timeout 20 s") && dbb.contains("NO DATABASE") && dbb.contains("the scan tables are seeded") && !dbb.contains("hunter2") && !dbb.contains("54322") + && !dbb.contains("reaches the application") && dba.contains("Nothing you write here reaches the application") + && dbb.contains("Checked by the driver before this gate: it answers (x); its 3 applied migrations are the range HEAD's.") && dba.contains("Checked by the driver before this gate: nothing (a dry run).") + && dbb.contains("the role `postgres`, which bypasses row-level security.") && dba.contains("the role `postgres`.") + && Prompt::DbBlock(Json::object(), sql, "reviewer", false, std::nullopt, false).contains("the role `reviewer`, which row-level security applies to.")); + const std::string lld = Prompt::LensLayer(Fn, "CHARTER TEXT", "DISPOSITIONS BLOCK", ab, "SUMMARY", dbb); + const std::vector layered = {std::format("YOUR CHARTER (process/lenses/{}), verbatim:\n\nCHARTER TEXT", Fn), ab, "DISPOSITIONS BLOCK", + "The whole pass of this lens on this hash concluded (the constraints your block pass rests on):\nSUMMARY"}; + Check("lens layer with the database: charter, application block, database block, dispositions, summary; without one it is byte for byte what it was", + lld.find("CHARTER TEXT") < lld.find("APPLICATION UNDER TEST") && lld.find("APPLICATION UNDER TEST") < lld.find("DATABASE UNDER TEST") + && lld.find("DATABASE UNDER TEST") < lld.find("DISPOSITIONS BLOCK") && lld.find("DISPOSITIONS BLOCK") < lld.find("SUMMARY") + && Prompt::LensLayer(Fn, "CHARTER TEXT", "DISPOSITIONS BLOCK", ab, "SUMMARY", "") == ll && ll == Dyn::Join(layered, "\n\n")); + Check("system text: a lens with a DATABASE UNDER TEST block runs SQL there, rolled back, and has the exact command it prints", + st.contains("DATABASE UNDER TEST block runs SQL in that database, in transactions the pipeline rolls back") && st.contains("exact SQL command that block prints")); + Check("prompt version 1; the charters exist and the common text has the placeholder filled", PromptVersion == 1 + && std::ranges::all_of(Plan::Lenses, [](const Plan::Lens& lens) { return fs::exists(fs::current_path() / "process" / "lenses" / lens.file); }) && !st.contains("{{")); + // The security charter is the owner's defensive review of their own code + // (2026-10-22: a charter that reads as a request to do harm is declined by + // the process, and a declined pass is NOT RETURNED). + const std::string charter = Prompt::Charter("repo-security.md"); + const std::vector banned = Offensive(charter); + Check("the security charter: no offensive vocabulary; the throwaway local copy, the smallest step, no tooling built, refutations name the control, the per-commit slots, one residual-risk on the null path", + banned.empty() && charter.contains("throwaway local copy") && charter.contains("smallest step") && charter.contains("no tooling built") && charter.contains("Refutations name the control") + && charter.contains("`pK:absent`") && charter.contains("`pK:outside`") && charter.contains("exactly one\n `residual-risk`") && charter.starts_with("# Lens: repo-security") + && charter.contains("Calibration (append dated entries at the bottom)"), Dyn::Join(banned, ", ")); + Check("the security charter names what the other lenses own and tells its blocks to route it as a note, never to suppress it", charter.contains("`repo-better-solution`") + && charter.contains("`repo-bug-prover`") && charter.contains("`repo-functional`") && charter.contains("instead of suppressing it")); + return Failures ? 1 : 0; +} diff --git a/tests/Config/main.cpp b/tests/Config/main.cpp new file mode 100644 index 0000000..7b1a994 --- /dev/null +++ b/tests/Config/main.cpp @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// The consumer config: the defaults, the merge, unknown keys, validation, +// the lenses (run, environment), authors, the login fixture, the example. + +#include +import std; +import Crafter.Review; +namespace fs = std::filesystem; +using namespace Crafter::Review; + +namespace { + std::int32_t Failures = 0; + + void Check(std::string_view name, bool cond, std::string_view detail = {}) { + std::println("{}{}{}", cond ? " ok " : " FAIL ", name, !cond && !detail.empty() ? std::format(" ({})", detail) : std::string()); + if (!cond) ++Failures; + } + + bool Any(const std::vector& problems, std::string_view part) { + return std::ranges::any_of(problems, [&](const std::string& p) { return p.contains(part); }); + } + + std::string Show(const std::vector& problems) { + Json out = Json::array(); + for (const std::string& p : problems) out.push_back(p); + return Dyn::ReprValue(out); + } +} + +int main() { + std::println("config: defaults, merge, unknown keys, validation, lenses (run, environment), authors, login fixture, the example"); + // A git call of a test reads no global config: this machine's signs + // every commit with the owner's key. + setenv("GIT_CONFIG_GLOBAL", "/dev/null", 1); + setenv("GIT_CONFIG_SYSTEM", "/dev/null", 1); + SetHome(fs::current_path()); + const std::string fn(Plan::FunctionalLens); + const fs::path td = fs::temp_directory_path() / std::format("crafter-review-config-{}", Platform::PublishedPid()); + fs::remove_all(td); + 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()); + 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) { + put(value); + return load().problems; + }; + put({{"base", "origin/dev"}, {"aiTrailers", "allow"}, {"allowDirty", {"supabase/config.toml"}}, + {"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" + && loaded.config.at("functional").at("readyPath") == "/health" && loaded.config.at("functional").at("appUrl") == "http://localhost:8080" && loaded.config.at("_path") == p.string(), + 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 problems = bad({{"bogus", 1}, {"functional", {{"nope", 2}, {"appUrl", "ftp://x"}}}, {"aiTrailers", "maybe"}, {"authors", {"nomail"}}}); + 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"}}}}}}); + Check("extraTools must be Bash(:*) or mcp__...; mcp.args a list", problems.size() == 2, Show(problems)); + std::ofstream(p, std::ios::binary) << "{not json"; + Check("not JSON -> one problem", load().problems.size() == 1); + 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 defaultsMatch = true; + std::set names; + for (const Plan::Lens& lens : Plan::Lenses) { + names.insert(Plan::LensName(lens.file)); + defaultsMatch = defaultsMatch && Config::Defaults().at("lenses").at(Plan::LensName(lens.file)).at("environment") == lens.env; + } + const Json defaults = Config::Defaults(); + std::set 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"}}; + Check("lenses: the defaults run all four with none/browser/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"}}}}}}); + 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{fn} && 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"}}}}}}); + 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"}}}}}}); + 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"}}}}}}); + 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)); + 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", ""}})); + problems = bad({{"supabase", {{"dbUrl", "postgresql://postgres:secret@db.abcdefgh.supabase.co:5432/postgres"}}}}); + Check("supabase.dbUrl: a hosted database is refused (this machine only), and the message does not echo the password", problems.size() == 1 + && problems[0].contains("not this machine") && !problems[0].contains("secret"), Show(problems)); + Check("supabase.dbUrl: http:// is no database URL; localhost and ::1 are this machine; start/stop must be command lines; an unknown key is reported", + Any(bad({{"supabase", {{"dbUrl", "http://127.0.0.1:54321"}}}}), "postgres://") && bad({{"supabase", {{"dbUrl", "postgres://u:p@localhost:6543/x"}}}}).empty() + && 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")); + 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:*)") + && !problems[0].contains("psqlx") && problems[0].contains("supabase environment"), Show(problems)); + Json withPassword = load().config; + withPassword["supabase"] = {{"dbUrl", "postgresql://postgres:hunter2@127.0.0.1:54322/postgres"}, {"start", nullptr}, {"stop", nullptr}, {"notes", ""}}; + 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)); + 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)); + 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)); + const fs::path repo = td / "r"; + fs::create_directories(repo); + Process::Run({"git", "-C", repo.string(), "init", "-q"}); + Process::Run({"git", "-C", repo.string(), "config", "user.email", "me@example.com"}); + Check("authors: the configured list, else git config user.email", Config::Authors({{"authors", {"a@b.c"}}}, repo) == std::vector{"a@b.c"} + && Config::Authors({{"authors", nullptr}}, repo) == std::vector{"me@example.com"}); + std::ofstream(td / "u.json", std::ios::binary) << "{}"; + Check("login_fixture: the first candidate that exists", Config::LoginFixture({{"functional", {{"login", {{"fixtures", {"nope.json", "u.json"}}}}}}}, td) == "u.json" + && !Config::LoginFixture({{"functional", {{"login", {{"fixtures", Json::array()}}}}}}, td)); + fs::remove_all(td); + return Failures ? 1 : 0; +} diff --git a/tests/Dispositions/main.cpp b/tests/Dispositions/main.cpp new file mode 100644 index 0000000..3e6de70 --- /dev/null +++ b/tests/Dispositions/main.cpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// Dispositions: in force, carried, lapsed, and the prompt block. + +import std; +import Crafter.Review; +namespace fs = std::filesystem; +using namespace Crafter::Review; + +namespace { + std::int32_t Failures = 0; + + void Check(std::string_view name, bool cond, std::string_view detail = {}) { + std::println("{}{}{}", cond ? " ok " : " FAIL ", name, !cond && !detail.empty() ? std::format(" ({})", detail) : std::string()); + if (!cond) ++Failures; + } +} + +int main() { + std::println("dispositions: in-force / carried / lapsed, the prompt block"); + const fs::path td = fs::temp_directory_path() / std::format("crafter-review-dispositions-{}", Platform::PublishedPid()); + fs::create_directories(td); + std::ofstream(td / "0001-a.patch", std::ios::binary) << "x\nANCHOR TEXT\ny\nANCHOR TEXT\n"; + const std::vector e = { + {{"id", "a"}, {"hash", "H1"}}, + {{"id", "b"}, {"hash", "H0"}, {"carry", true}, {"anchors", {{{"file", "0001-a.patch"}, {"text", "ANCHOR TEXT"}, {"count", 2}}}}}, + {{"id", "c"}, {"hash", "H0"}, {"carry", true}, {"anchors", {{{"file", "0001-a.patch"}, {"text", "ANCHOR TEXT"}, {"count", 1}}}}}, + {{"id", "d"}, {"hash", "H0"}}, + }; + std::map rows; + for (const Json& row : Dispositions::Classify(e, "H1", td)) rows[row.at("id").get()] = row.at("status").get(); + const std::map expected = {{"a", "in-force"}, {"b", "carried"}, {"c", "lapsed"}, {"d", "lapsed"}}; + Check("hash match in-force, count match carried, count mismatch lapsed, no carry lapsed", rows == expected); + const std::vector inf = Dispositions::InForce(e, Dispositions::Classify(e, "H1", td)); + Check("in_force = in-force + carried", inf.size() == 2 && inf[0].at("id") == "a" && inf[1].at("id") == "b"); + const std::string blk = Dispositions::Block(inf, Dispositions::Classify(e, "H1", td), "repo-bug-prover.md", "H1"); + Check("block text names the carried entry, nothing outside the register discharges, Gate 4 named", blk.contains("CARRIED to this hash") && blk.contains("- a [") + && blk.contains("Nothing outside this list") && blk.contains("Gate 4") && !blk.contains("ledger")); + const std::vector e2 = {{{"id", "r"}, {"hash", "H1"}, {"kind", "refuted"}, {"lenses", {"repo-functional.md"}}}}; + const std::vector rowsR = {{{"id", "r"}, {"status", "in-force"}}}; + Check("refuted entry hidden from other lenses, shown to its own", Dispositions::Block(e2, rowsR, "repo-bug-prover.md", "H1").contains("(none)") + && Dispositions::Block(e2, rowsR, Plan::FunctionalLens, "H1").contains("- r [")); + fs::remove_all(td); + return Failures ? 1 : 0; +} diff --git a/tests/Goldens/fixture/commits3/0001-add-the-greeting-module.patch b/tests/Goldens/fixture/commits3/0001-add-the-greeting-module.patch new file mode 100644 index 0000000..cf0e850 --- /dev/null +++ b/tests/Goldens/fixture/commits3/0001-add-the-greeting-module.patch @@ -0,0 +1,30 @@ +From 1111111111111111111111111111111111111111 Mon Sep 17 00:00:00 2001 +From: Jorijn van der Graaf +Date: Wed, 24 Sep 2026 10:00:00 +0200 +Subject: [PATCH 1/3] add the greeting module + +--- + hello.py | 9 +++++++++ + 1 file changed, 9 insertions(+) + create mode 100644 hello.py + +diff --git a/hello.py b/hello.py +new file mode 100644 +index 0000000..a1b2c3d +--- /dev/null ++++ b/hello.py +@@ -0,0 +1,9 @@ ++"""Greetings.""" ++ ++ ++def greet(): ++ return "hello" ++ ++ ++def farewell(): ++ return "bye" + +base-commit: 0000000000000000000000000000000000000000 +-- +2.51.0 + diff --git a/tests/Goldens/fixture/commits3/0002-greet-by-name-and-document-it.patch b/tests/Goldens/fixture/commits3/0002-greet-by-name-and-document-it.patch new file mode 100644 index 0000000..7863ce6 --- /dev/null +++ b/tests/Goldens/fixture/commits3/0002-greet-by-name-and-document-it.patch @@ -0,0 +1,55 @@ +From 2222222222222222222222222222222222222222 Mon Sep 17 00:00:00 2001 +From: Jorijn van der Graaf +Date: Wed, 24 Sep 2026 10:05:00 +0200 +Subject: [PATCH 2/3] greet by name and document it + +The greeting takes a name now and the farewell mirrors it; the README +documents both calls. Callers that pass nothing still get the old text. + +The format follows the greeting spec [1]. + +[1] https://example.org/greeting-spec +--- + README.md | 4 ++++ + hello.py | 8 ++++++-- + 2 files changed, 10 insertions(+), 2 deletions(-) + create mode 100644 README.md + +diff --git a/README.md b/README.md +new file mode 100644 +index 0000000..d4e5f6a +--- /dev/null ++++ b/README.md +@@ -0,0 +1,4 @@ ++# hello ++ ++Call `greet(name)` or `farewell(name)`; without a name both return the ++plain text. +diff --git a/hello.py b/hello.py +index a1b2c3d..b7c8d9e 100644 +--- a/hello.py ++++ b/hello.py +@@ -1,9 +1,13 @@ + """Greetings.""" + + +-def greet(): +- return "hello" ++def greet(name=None): ++ if name is None: ++ return "hello" ++ return "hello " + name + + +@@ -7,3 +11,5 @@ def greet(): +-def farewell(): +- return "bye" ++def farewell(name=None): ++ if name is None: ++ return "bye" ++ return "bye " + name + +base-commit: 0000000000000000000000000000000000000000 +-- +2.51.0 + diff --git a/tests/Goldens/fixture/commits3/0003-add-tests.patch b/tests/Goldens/fixture/commits3/0003-add-tests.patch new file mode 100644 index 0000000..e28ccc2 --- /dev/null +++ b/tests/Goldens/fixture/commits3/0003-add-tests.patch @@ -0,0 +1,24 @@ +From 3333333333333333333333333333333333333333 Mon Sep 17 00:00:00 2001 +From: Jorijn van der Graaf +Date: Wed, 24 Sep 2026 10:10:00 +0200 +Subject: [PATCH 3/3] add tests + +Tests for greet. +--- + tests/test_hello.py | 3 +++ + 1 file changed, 3 insertions(+) + create mode 100644 tests/test_hello.py + +diff --git a/tests/test_hello.py b/tests/test_hello.py +new file mode 100644 +index 0000000..1111111 +--- /dev/null ++++ b/tests/test_hello.py +@@ -0,0 +1,3 @@ ++from hello import greet ++def test_greet(): ++ assert greet("x") == "hello x" + +base-commit: 0000000000000000000000000000000000000000 +-- +2.51.0 diff --git a/tests/Goldens/fixture/pipeline.json b/tests/Goldens/fixture/pipeline.json index f35d1d4..91ab279 100644 --- a/tests/Goldens/fixture/pipeline.json +++ b/tests/Goldens/fixture/pipeline.json @@ -4967,6 +4967,2780 @@ "listUsd": 56.34 } }, + "commits3": { + "dir": "tests/Goldens/fixture/commits3", + "inventory": { + "mode": "commits", + "artifact": "tests/Goldens/fixture/commits3", + "hash8": "6bf22897", + "units": [ + { + "id": "p1:subject", + "lines": [ + 1, + 5 + ], + "excerpt": "Subject: [PATCH 1/3] add the greeting module" + }, + { + "id": "p1:diffstat", + "lines": [ + 6, + 10 + ], + "excerpt": "---" + }, + { + "id": "p1:hello.py#hdr", + "lines": [ + 11, + 15 + ], + "excerpt": "diff --git a/hello.py b/hello.py" + }, + { + "id": "p1:hello.py#1", + "lines": [ + 16, + 31 + ], + "excerpt": "@@ -0,0 +1,9 @@" + }, + { + "id": "p1:outside", + "lines": [ + 0, + 0 + ], + "excerpt": "FIXED SLOT: in-repository users/callers of what this commit changes, outside its diff" + }, + { + "id": "p1:absent", + "lines": [ + 0, + 0 + ], + "excerpt": "FIXED SLOT: what a reviewer expects in this commit and does not find" + }, + { + "id": "p2:subject", + "lines": [ + 32, + 36 + ], + "excerpt": "Subject: [PATCH 2/3] greet by name and document it" + }, + { + "id": "p2:msg", + "lines": [ + 37, + 42 + ], + "excerpt": "The greeting takes a name now and the farewell mirrors it; the README" + }, + { + "id": "p2:diffstat", + "lines": [ + 43, + 48 + ], + "excerpt": "---" + }, + { + "id": "p2:README.md#hdr", + "lines": [ + 49, + 53 + ], + "excerpt": "diff --git a/README.md b/README.md" + }, + { + "id": "p2:README.md#1", + "lines": [ + 54, + 58 + ], + "excerpt": "@@ -0,0 +1,4 @@" + }, + { + "id": "p2:hello.py#hdr", + "lines": [ + 59, + 62 + ], + "excerpt": "diff --git a/hello.py b/hello.py" + }, + { + "id": "p2:hello.py#1", + "lines": [ + 63, + 74 + ], + "excerpt": "@@ -1,9 +1,13 @@" + }, + { + "id": "p2:hello.py#2", + "lines": [ + 75, + 87 + ], + "excerpt": "@@ -7,3 +11,5 @@ def greet():" + }, + { + "id": "p2:outside", + "lines": [ + 0, + 0 + ], + "excerpt": "FIXED SLOT: in-repository users/callers of what this commit changes, outside its diff" + }, + { + "id": "p2:absent", + "lines": [ + 0, + 0 + ], + "excerpt": "FIXED SLOT: what a reviewer expects in this commit and does not find" + }, + { + "id": "p3:subject", + "lines": [ + 88, + 92 + ], + "excerpt": "Subject: [PATCH 3/3] add tests" + }, + { + "id": "p3:msg", + "lines": [ + 93, + 93 + ], + "excerpt": "Tests for greet." + }, + { + "id": "p3:diffstat", + "lines": [ + 94, + 98 + ], + "excerpt": "---" + }, + { + "id": "p3:tests/test_hello.py#hdr", + "lines": [ + 99, + 103 + ], + "excerpt": "diff --git a/tests/test_hello.py b/tests/test_hello.py" + }, + { + "id": "p3:tests/test_hello.py#1", + "lines": [ + 104, + 112 + ], + "excerpt": "@@ -0,0 +1,3 @@" + }, + { + "id": "p3:outside", + "lines": [ + 0, + 0 + ], + "excerpt": "FIXED SLOT: in-repository users/callers of what this commit changes, outside its diff" + }, + { + "id": "p3:absent", + "lines": [ + 0, + 0 + ], + "excerpt": "FIXED SLOT: what a reviewer expects in this commit and does not find" + }, + { + "id": "whole:order", + "lines": [ + 0, + 0 + ], + "excerpt": "FIXED SLOT: commit ordering and bisectability \u2014 each commit a self-contained step that leaves the repository consistent (no later commit in the range repairs an earlier one, nothing is used before the commit that adds it), the declared base" + }, + { + "id": "whole:absent", + "lines": [ + 0, + 0 + ], + "excerpt": "FIXED SLOT: what a reviewer of this repository expects the range to contain and it does not \u2014 a test, a version/changelog bump, a doc or packaging update, a migration for existing installs, a message for a body-less commit" + }, + { + "id": "whole:behaviour", + "lines": [ + 0, + 0 + ], + "excerpt": "FIXED SLOT: the user-visible behaviour of the range, exercised end to end in the running application \u2014 every behaviour the code and the messages promise, as a user sees it; the flows the range touches and the adjacent flows that share what it changes; what a user expects the range to deliver and does not get" + } + ], + "invariant": "every non-blank line belongs to exactly one unit; fixed slots have lines [0,0]", + "count": 26 + }, + "lines": [ + "From 1111111111111111111111111111111111111111 Mon Sep 17 00:00:00 2001", + "From: Jorijn van der Graaf ", + "Date: Wed, 24 Sep 2026 10:00:00 +0200", + "Subject: [PATCH 1/3] add the greeting module", + "", + "---", + " hello.py | 9 +++++++++", + " 1 file changed, 9 insertions(+)", + " create mode 100644 hello.py", + "", + "diff --git a/hello.py b/hello.py", + "new file mode 100644", + "index 0000000..a1b2c3d", + "--- /dev/null", + "+++ b/hello.py", + "@@ -0,0 +1,9 @@", + "+\"\"\"Greetings.\"\"\"", + "+", + "+", + "+def greet():", + "+ return \"hello\"", + "+", + "+", + "+def farewell():", + "+ return \"bye\"", + "", + "base-commit: 0000000000000000000000000000000000000000", + "-- ", + "2.51.0", + "", + "", + "From 2222222222222222222222222222222222222222 Mon Sep 17 00:00:00 2001", + "From: Jorijn van der Graaf ", + "Date: Wed, 24 Sep 2026 10:05:00 +0200", + "Subject: [PATCH 2/3] greet by name and document it", + "", + "The greeting takes a name now and the farewell mirrors it; the README", + "documents both calls. Callers that pass nothing still get the old text.", + "", + "The format follows the greeting spec [1].", + "", + "[1] https://example.org/greeting-spec", + "---", + " README.md | 4 ++++", + " hello.py | 8 ++++++--", + " 2 files changed, 10 insertions(+), 2 deletions(-)", + " create mode 100644 README.md", + "", + "diff --git a/README.md b/README.md", + "new file mode 100644", + "index 0000000..d4e5f6a", + "--- /dev/null", + "+++ b/README.md", + "@@ -0,0 +1,4 @@", + "+# hello", + "+", + "+Call `greet(name)` or `farewell(name)`; without a name both return the", + "+plain text.", + "diff --git a/hello.py b/hello.py", + "index a1b2c3d..b7c8d9e 100644", + "--- a/hello.py", + "+++ b/hello.py", + "@@ -1,9 +1,13 @@", + " \"\"\"Greetings.\"\"\"", + " ", + " ", + "-def greet():", + "- return \"hello\"", + "+def greet(name=None):", + "+ if name is None:", + "+ return \"hello\"", + "+ return \"hello \" + name", + " ", + " ", + "@@ -7,3 +11,5 @@ def greet():", + "-def farewell():", + "- return \"bye\"", + "+def farewell(name=None):", + "+ if name is None:", + "+ return \"bye\"", + "+ return \"bye \" + name", + "", + "base-commit: 0000000000000000000000000000000000000000", + "-- ", + "2.51.0", + "", + "", + "From 3333333333333333333333333333333333333333 Mon Sep 17 00:00:00 2001", + "From: Jorijn van der Graaf ", + "Date: Wed, 24 Sep 2026 10:10:00 +0200", + "Subject: [PATCH 3/3] add tests", + "", + "Tests for greet.", + "---", + " tests/test_hello.py | 3 +++", + " 1 file changed, 3 insertions(+)", + " create mode 100644 tests/test_hello.py", + "", + "diff --git a/tests/test_hello.py b/tests/test_hello.py", + "new file mode 100644", + "index 0000000..1111111", + "--- /dev/null", + "+++ b/tests/test_hello.py", + "@@ -0,0 +1,3 @@", + "+from hello import greet", + "+def test_greet():", + "+ assert greet(\"x\") == \"hello x\"", + "", + "base-commit: 0000000000000000000000000000000000000000", + "-- ", + "2.51.0", + "" + ], + "hash": "6bf22897cbe59ecd75b655833bdbb0d08a5bbaf72b949ed884d62954e9ba20ba", + "head": "3333333333333333333333333333333333333333", + "patchFiles": [ + "0001-add-the-greeting-module.patch", + "0002-greet-by-name-and-document-it.patch", + "0003-add-tests.patch" + ], + "patchParts": { + "1": [ + false, + "diff --git a/hello.py b/hello.py\nnew file mode 100644\nindex\n--- /dev/null\n+++ b/hello.py\n@@\n+\"\"\"Greetings.\"\"\"\n+\n+\n+def greet():\n+ return \"hello\"\n+\n+\n+def farewell():\n+ return \"bye\"", + "0000000000000000000000000000000000000000", + [], + "Subject: add the greeting module\n" + ], + "2": [ + true, + "diff --git a/hello.py b/hello.py\nindex 100644\n--- a/hello.py\n+++ b/hello.py\n@@\n \"\"\"Greetings.\"\"\"\n \n \n-def greet():\n- return \"hello\"\n+def greet(name=None):\n+ if name is None:\n+ return \"hello\"\n+ return \"hello \" + name\n \n \n@@ def greet():\n-def farewell():\n- return \"bye\"\n+def farewell(name=None):\n+ if name is None:\n+ return \"bye\"\n+ return \"bye \" + name", + "0000000000000000000000000000000000000000", + [ + "diff --git a/README.md b/README.md\nnew file mode 100644\nindex\n--- /dev/null\n+++ b/README.md" + ], + "Subject: greet by name and document it\n\nThe greeting takes a name now and the farewell mirrors it; the README\ndocuments both calls. Callers that pass nothing still get the old text.\n\nThe format follows the greeting spec [1].\n\n[1] https://example.org/greeting-spec" + ], + "3": [ + true, + "diff --git a/tests/test_hello.py b/tests/test_hello.py\nnew file mode 100644\nindex\n--- /dev/null\n+++ b/tests/test_hello.py\n@@\n+from hello import greet\n+def test_greet():\n+ assert greet(\"x\") == \"hello x\"", + "0000000000000000000000000000000000000000", + [], + "Subject: add tests\n\nTests for greet." + ] + }, + "commitCode": { + "body": { + "1": false, + "2": true, + "3": true + }, + "code": { + "1": "5e4094c730dff2915cee33a9d1d680636270e348327ebf2ab43b1fc2008878dd", + "2": "b2cc3b53017cdb202788417d40ea0ea7e411003697f966bb111f77ac27244a80", + "3": "9ae03b452c1dceef52357cf229ad1ca20a4b2dfc1b3dde89b9d67cd47183609b" + }, + "docs": { + "1": [], + "2": [ + "diff --git a/README.md b/README.md\nnew file mode 100644\nindex\n--- /dev/null\n+++ b/README.md" + ], + "3": [] + }, + "msg": { + "1": "d428e3cc45b34df47803c58e86f7401e33837d926fb7086901e44520f43617fd", + "2": "3d74e7a9c2bf88fadeee2e858ba663efba838d8e8a1351970c0efff9788fc6bf", + "3": "08f6f0f7b6f3bc71741bc095cf4a84f921833f9d8f813b916df132ecb3f42add" + }, + "range": "bd4ac080833e50f3532594f38f22e98a04e969fdf6ecefe6026fc203dcfda9e5", + "upto": { + "1": "23558a4551ddbad0bbf92e6dd39dffd4a8a87680bb625cc8b0b1f9c9a7916fd9", + "2": "3180e59826dc6dad783dbae9c2fe2c156748d482c4d443fbca0903cd36739182", + "3": "bd4ac080833e50f3532594f38f22e98a04e969fdf6ecefe6026fc203dcfda9e5" + }, + "base": "0000000000000000000000000000000000000000" + }, + "units": [ + { + "id": "p1:subject", + "kind": "text", + "doc": false, + "path": null, + "text": "From 1111111111111111111111111111111111111111 Mon Sep 17 00:00:00 2001\nFrom: Jorijn van der Graaf \nDate: Wed, 24 Sep 2026 10:00:00 +0200\nSubject: [PATCH 1/3] add the greeting module\n", + "norm": "From 1111111111111111111111111111111111111111 Mon Sep 17 00:00:00 2001\nFrom: Jorijn van der Graaf \nDate: Wed, 24 Sep 2026 10:00:00 +0200\nSubject: [PATCH 1/3] add the greeting module" + }, + { + "id": "p1:diffstat", + "kind": "text", + "doc": false, + "path": null, + "text": "---\n hello.py | 9 +++++++++\n 1 file changed, 9 insertions(+)\n create mode 100644 hello.py\n", + "norm": "---\n hello.py | 9 +++++++++\n 1 file changed, 9 insertions(+)\n create mode 100644 hello.py" + }, + { + "id": "p1:hello.py#hdr", + "kind": "code", + "doc": false, + "path": "hello.py", + "text": "diff --git a/hello.py b/hello.py\nnew file mode 100644\nindex 0000000..a1b2c3d\n--- /dev/null\n+++ b/hello.py", + "norm": "diff --git a/hello.py b/hello.py\nnew file mode 100644\nindex\n--- /dev/null\n+++ b/hello.py" + }, + { + "id": "p1:hello.py#1", + "kind": "code", + "doc": false, + "path": "hello.py", + "text": "@@ -0,0 +1,9 @@\n+\"\"\"Greetings.\"\"\"\n+\n+\n+def greet():\n+ return \"hello\"\n+\n+\n+def farewell():\n+ return \"bye\"\n\nbase-commit: 0000000000000000000000000000000000000000\n-- \n2.51.0\n\n", + "norm": "@@\n+\"\"\"Greetings.\"\"\"\n+\n+\n+def greet():\n+ return \"hello\"\n+\n+\n+def farewell():\n+ return \"bye\"" + }, + { + "id": "p1:outside", + "kind": "code", + "doc": false, + "path": null, + "text": "", + "norm": "" + }, + { + "id": "p1:absent", + "kind": "code", + "doc": false, + "path": null, + "text": "", + "norm": "" + }, + { + "id": "p2:subject", + "kind": "text", + "doc": false, + "path": null, + "text": "From 2222222222222222222222222222222222222222 Mon Sep 17 00:00:00 2001\nFrom: Jorijn van der Graaf \nDate: Wed, 24 Sep 2026 10:05:00 +0200\nSubject: [PATCH 2/3] greet by name and document it\n", + "norm": "From 2222222222222222222222222222222222222222 Mon Sep 17 00:00:00 2001\nFrom: Jorijn van der Graaf \nDate: Wed, 24 Sep 2026 10:05:00 +0200\nSubject: [PATCH 2/3] greet by name and document it" + }, + { + "id": "p2:msg", + "kind": "text", + "doc": false, + "path": null, + "text": "The greeting takes a name now and the farewell mirrors it; the README\ndocuments both calls. Callers that pass nothing still get the old text.\n\nThe format follows the greeting spec [1].\n\n[1] https://example.org/greeting-spec", + "norm": "The greeting takes a name now and the farewell mirrors it; the README\ndocuments both calls. Callers that pass nothing still get the old text.\n\nThe format follows the greeting spec [1].\n\n[1] https://example.org/greeting-spec" + }, + { + "id": "p2:diffstat", + "kind": "text", + "doc": false, + "path": null, + "text": "---\n README.md | 4 ++++\n hello.py | 8 ++++++--\n 2 files changed, 10 insertions(+), 2 deletions(-)\n create mode 100644 README.md\n", + "norm": "---\n README.md | 4 ++++\n hello.py | 8 ++++++--\n 2 files changed, 10 insertions(+), 2 deletions(-)\n create mode 100644 README.md" + }, + { + "id": "p2:README.md#hdr", + "kind": "code", + "doc": true, + "path": "README.md", + "text": "diff --git a/README.md b/README.md\nnew file mode 100644\nindex 0000000..d4e5f6a\n--- /dev/null\n+++ b/README.md", + "norm": "diff --git a/README.md b/README.md\nnew file mode 100644\nindex\n--- /dev/null\n+++ b/README.md" + }, + { + "id": "p2:README.md#1", + "kind": "code", + "doc": true, + "path": "README.md", + "text": "@@ -0,0 +1,4 @@\n+# hello\n+\n+Call `greet(name)` or `farewell(name)`; without a name both return the\n+plain text.", + "norm": "@@\n+# hello\n+\n+Call `greet(name)` or `farewell(name)`; without a name both return the\n+plain text." + }, + { + "id": "p2:hello.py#hdr", + "kind": "code", + "doc": false, + "path": "hello.py", + "text": "diff --git a/hello.py b/hello.py\nindex a1b2c3d..b7c8d9e 100644\n--- a/hello.py\n+++ b/hello.py", + "norm": "diff --git a/hello.py b/hello.py\nindex 100644\n--- a/hello.py\n+++ b/hello.py" + }, + { + "id": "p2:hello.py#1", + "kind": "code", + "doc": false, + "path": "hello.py", + "text": "@@ -1,9 +1,13 @@\n \"\"\"Greetings.\"\"\"\n \n \n-def greet():\n- return \"hello\"\n+def greet(name=None):\n+ if name is None:\n+ return \"hello\"\n+ return \"hello \" + name\n \n ", + "norm": "@@\n \"\"\"Greetings.\"\"\"\n \n \n-def greet():\n- return \"hello\"\n+def greet(name=None):\n+ if name is None:\n+ return \"hello\"\n+ return \"hello \" + name\n \n " + }, + { + "id": "p2:hello.py#2", + "kind": "code", + "doc": false, + "path": "hello.py", + "text": "@@ -7,3 +11,5 @@ def greet():\n-def farewell():\n- return \"bye\"\n+def farewell(name=None):\n+ if name is None:\n+ return \"bye\"\n+ return \"bye \" + name\n\nbase-commit: 0000000000000000000000000000000000000000\n-- \n2.51.0\n\n", + "norm": "@@ def greet():\n-def farewell():\n- return \"bye\"\n+def farewell(name=None):\n+ if name is None:\n+ return \"bye\"\n+ return \"bye \" + name" + }, + { + "id": "p2:outside", + "kind": "code", + "doc": false, + "path": null, + "text": "", + "norm": "" + }, + { + "id": "p2:absent", + "kind": "code", + "doc": false, + "path": null, + "text": "", + "norm": "" + }, + { + "id": "p3:subject", + "kind": "text", + "doc": false, + "path": null, + "text": "From 3333333333333333333333333333333333333333 Mon Sep 17 00:00:00 2001\nFrom: Jorijn van der Graaf \nDate: Wed, 24 Sep 2026 10:10:00 +0200\nSubject: [PATCH 3/3] add tests\n", + "norm": "From 3333333333333333333333333333333333333333 Mon Sep 17 00:00:00 2001\nFrom: Jorijn van der Graaf \nDate: Wed, 24 Sep 2026 10:10:00 +0200\nSubject: [PATCH 3/3] add tests" + }, + { + "id": "p3:msg", + "kind": "text", + "doc": false, + "path": null, + "text": "Tests for greet.", + "norm": "Tests for greet." + }, + { + "id": "p3:diffstat", + "kind": "text", + "doc": false, + "path": null, + "text": "---\n tests/test_hello.py | 3 +++\n 1 file changed, 3 insertions(+)\n create mode 100644 tests/test_hello.py\n", + "norm": "---\n tests/test_hello.py | 3 +++\n 1 file changed, 3 insertions(+)\n create mode 100644 tests/test_hello.py" + }, + { + "id": "p3:tests/test_hello.py#hdr", + "kind": "code", + "doc": false, + "path": "tests/test_hello.py", + "text": "diff --git a/tests/test_hello.py b/tests/test_hello.py\nnew file mode 100644\nindex 0000000..1111111\n--- /dev/null\n+++ b/tests/test_hello.py", + "norm": "diff --git a/tests/test_hello.py b/tests/test_hello.py\nnew file mode 100644\nindex\n--- /dev/null\n+++ b/tests/test_hello.py" + }, + { + "id": "p3:tests/test_hello.py#1", + "kind": "code", + "doc": false, + "path": "tests/test_hello.py", + "text": "@@ -0,0 +1,3 @@\n+from hello import greet\n+def test_greet():\n+ assert greet(\"x\") == \"hello x\"\n\nbase-commit: 0000000000000000000000000000000000000000\n-- \n2.51.0\n", + "norm": "@@\n+from hello import greet\n+def test_greet():\n+ assert greet(\"x\") == \"hello x\"" + }, + { + "id": "p3:outside", + "kind": "code", + "doc": false, + "path": null, + "text": "", + "norm": "" + }, + { + "id": "p3:absent", + "kind": "code", + "doc": false, + "path": null, + "text": "", + "norm": "" + }, + { + "id": "whole:order", + "kind": "slot", + "doc": false, + "path": null, + "text": "", + "norm": "" + }, + { + "id": "whole:absent", + "kind": "slot", + "doc": false, + "path": null, + "text": "", + "norm": "" + }, + { + "id": "whole:behaviour", + "kind": "slot", + "doc": false, + "path": null, + "text": "", + "norm": "" + } + ], + "notReviewed": { + "all": [ + "p1:subject", + "p1:diffstat", + "p2:subject", + "p2:msg", + "p2:diffstat", + "p2:README.md#hdr", + "p2:README.md#1", + "p3:subject", + "p3:msg", + "p3:diffstat" + ], + "static": [ + "p1:subject", + "p1:diffstat", + "p2:subject", + "p2:msg", + "p2:diffstat", + "p2:README.md#hdr", + "p2:README.md#1", + "p3:subject", + "p3:msg", + "p3:diffstat", + "whole:behaviour" + ], + "functional": [ + "p1:subject", + "p1:diffstat", + "p1:hello.py#hdr", + "p1:outside", + "p1:absent", + "p2:subject", + "p2:msg", + "p2:diffstat", + "p2:README.md#hdr", + "p2:README.md#1", + "p2:hello.py#hdr", + "p2:outside", + "p2:absent", + "p3:subject", + "p3:msg", + "p3:diffstat", + "p3:tests/test_hello.py#hdr", + "p3:tests/test_hello.py#1", + "p3:outside", + "p3:absent", + "whole:order", + "whole:absent" + ] + }, + "plans": { + "repo-better-solution.md": { + "whole": { + "label": "repo-better-solution:whole", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "whole", + "unit": null, + "expectedIds": [ + "whole:order", + "whole:absent" + ], + "model": "claude-opus-5-5", + "maxTurns": 180, + "timeoutMin": 105 + }, + "blocks": [ + { + "label": "repo-better-solution:block:p1:hello.py#hdr", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p1:hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p1:hello.py#hdr" + ], + "unitSha": "6570ee137060f18d456b9b64f93087e3686684254c7e3f6315184de5823c9d8d", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p1:hello.py#1", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p1:hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p1:hello.py#1" + ], + "unitSha": "5aa93b494045c41acf995d776f069eb2877e3394f12f3840c46105061fd50b7b", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p1:outside", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p1:outside", + "unitKind": "code", + "expectedIds": [ + "p1:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p1:absent", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p1:absent", + "unitKind": "code", + "expectedIds": [ + "p1:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p2:hello.py#hdr", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p2:hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#hdr" + ], + "unitSha": "92d8d033aedfeacedc28db66ccdac1139b545078d4e7c0004d0e4f8b3114293e", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p2:hello.py#1", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p2:hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#1" + ], + "unitSha": "06a9d50520c4013c7c28388601386d3750fb5de449e1e697951328bebc7edcc7", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p2:hello.py#2", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p2:hello.py#2", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#2" + ], + "unitSha": "5ef2924a54ff3400a57ae09c1216fde775659f1919241f5114c2cc3014566783", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p2:outside", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p2:outside", + "unitKind": "code", + "expectedIds": [ + "p2:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p2:absent", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p2:absent", + "unitKind": "code", + "expectedIds": [ + "p2:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p3:tests/test_hello.py#hdr", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p3:tests/test_hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p3:tests/test_hello.py#hdr" + ], + "unitSha": "b3272418d51329d527e8f71a250f2ef3aaffa8c3c8472dabd0794012247df743", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p3:tests/test_hello.py#1", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p3:tests/test_hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p3:tests/test_hello.py#1" + ], + "unitSha": "fbb38ed45f23f435d83fef525222e28d9bd3cf40b8f00bc27d49418bfa46a44e", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p3:outside", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p3:outside", + "unitKind": "code", + "expectedIds": [ + "p3:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p3:absent", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p3:absent", + "unitKind": "code", + "expectedIds": [ + "p3:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + } + ] + }, + "repo-better-solution.md:exclude": { + "whole": { + "label": "repo-better-solution:whole", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "whole", + "unit": null, + "expectedIds": [ + "whole:order", + "whole:absent" + ], + "model": "claude-opus-5-5", + "maxTurns": 180, + "timeoutMin": 105 + }, + "blocks": [ + { + "label": "repo-better-solution:block:p1:hello.py#hdr", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p1:hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p1:hello.py#hdr" + ], + "unitSha": "6570ee137060f18d456b9b64f93087e3686684254c7e3f6315184de5823c9d8d", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p1:hello.py#1", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p1:hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p1:hello.py#1" + ], + "unitSha": "5aa93b494045c41acf995d776f069eb2877e3394f12f3840c46105061fd50b7b", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p1:outside", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p1:outside", + "unitKind": "code", + "expectedIds": [ + "p1:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p1:absent", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p1:absent", + "unitKind": "code", + "expectedIds": [ + "p1:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p2:hello.py#hdr", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p2:hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#hdr" + ], + "unitSha": "92d8d033aedfeacedc28db66ccdac1139b545078d4e7c0004d0e4f8b3114293e", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p2:hello.py#1", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p2:hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#1" + ], + "unitSha": "06a9d50520c4013c7c28388601386d3750fb5de449e1e697951328bebc7edcc7", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p2:hello.py#2", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p2:hello.py#2", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#2" + ], + "unitSha": "5ef2924a54ff3400a57ae09c1216fde775659f1919241f5114c2cc3014566783", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p2:outside", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p2:outside", + "unitKind": "code", + "expectedIds": [ + "p2:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p2:absent", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p2:absent", + "unitKind": "code", + "expectedIds": [ + "p2:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p3:tests/test_hello.py#hdr", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p3:tests/test_hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p3:tests/test_hello.py#hdr" + ], + "unitSha": "b3272418d51329d527e8f71a250f2ef3aaffa8c3c8472dabd0794012247df743", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p3:tests/test_hello.py#1", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p3:tests/test_hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p3:tests/test_hello.py#1" + ], + "unitSha": "fbb38ed45f23f435d83fef525222e28d9bd3cf40b8f00bc27d49418bfa46a44e", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p3:outside", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p3:outside", + "unitKind": "code", + "expectedIds": [ + "p3:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-better-solution:block:p3:absent", + "lens": "repo-better-solution", + "lensFile": "repo-better-solution.md", + "kind": "block", + "unit": "p3:absent", + "unitKind": "code", + "expectedIds": [ + "p3:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + } + ] + }, + "repo-functional.md": { + "whole": { + "label": "repo-functional:whole", + "lens": "repo-functional", + "lensFile": "repo-functional.md", + "kind": "whole", + "unit": null, + "expectedIds": [ + "whole:behaviour" + ], + "model": "claude-opus-5-5", + "maxTurns": 220, + "timeoutMin": 120 + }, + "blocks": [ + { + "label": "repo-functional:na:p1:hello.py#hdr", + "lens": "repo-functional", + "lensFile": "repo-functional.md", + "kind": "auto-na", + "unit": "p1:hello.py#hdr", + "expectedIds": [ + "p1:hello.py#hdr" + ], + "unitSha": "6570ee137060f18d456b9b64f93087e3686684254c7e3f6315184de5823c9d8d", + "basis": "mechanical: a diff header (mode, rename, new or deleted file) carries no behaviour of its own; the hunks of the file do" + }, + { + "label": "repo-functional:block:p1:hello.py#1", + "lens": "repo-functional", + "lensFile": "repo-functional.md", + "kind": "block", + "unit": "p1:hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p1:hello.py#1" + ], + "unitSha": "5aa93b494045c41acf995d776f069eb2877e3394f12f3840c46105061fd50b7b", + "model": "claude-opus-5-5", + "maxTurns": 90, + "timeoutMin": 60 + }, + { + "label": "repo-functional:na:p2:hello.py#hdr", + "lens": "repo-functional", + "lensFile": "repo-functional.md", + "kind": "auto-na", + "unit": "p2:hello.py#hdr", + "expectedIds": [ + "p2:hello.py#hdr" + ], + "unitSha": "92d8d033aedfeacedc28db66ccdac1139b545078d4e7c0004d0e4f8b3114293e", + "basis": "mechanical: a diff header (mode, rename, new or deleted file) carries no behaviour of its own; the hunks of the file do" + }, + { + "label": "repo-functional:block:p2:hello.py#1", + "lens": "repo-functional", + "lensFile": "repo-functional.md", + "kind": "block", + "unit": "p2:hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#1" + ], + "unitSha": "06a9d50520c4013c7c28388601386d3750fb5de449e1e697951328bebc7edcc7", + "model": "claude-opus-5-5", + "maxTurns": 90, + "timeoutMin": 60 + }, + { + "label": "repo-functional:block:p2:hello.py#2", + "lens": "repo-functional", + "lensFile": "repo-functional.md", + "kind": "block", + "unit": "p2:hello.py#2", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#2" + ], + "unitSha": "5ef2924a54ff3400a57ae09c1216fde775659f1919241f5114c2cc3014566783", + "model": "claude-opus-5-5", + "maxTurns": 90, + "timeoutMin": 60 + }, + { + "label": "repo-functional:na:p3:tests/test_hello.py#hdr", + "lens": "repo-functional", + "lensFile": "repo-functional.md", + "kind": "auto-na", + "unit": "p3:tests/test_hello.py#hdr", + "expectedIds": [ + "p3:tests/test_hello.py#hdr" + ], + "unitSha": "b3272418d51329d527e8f71a250f2ef3aaffa8c3c8472dabd0794012247df743", + "basis": "mechanical: a diff header (mode, rename, new or deleted file) carries no behaviour of its own; the hunks of the file do" + }, + { + "label": "repo-functional:block:p3:tests/test_hello.py#1", + "lens": "repo-functional", + "lensFile": "repo-functional.md", + "kind": "block", + "unit": "p3:tests/test_hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p3:tests/test_hello.py#1" + ], + "unitSha": "fbb38ed45f23f435d83fef525222e28d9bd3cf40b8f00bc27d49418bfa46a44e", + "model": "claude-opus-5-5", + "maxTurns": 90, + "timeoutMin": 60 + } + ] + }, + "repo-functional.md:exclude": { + "whole": { + "label": "repo-functional:whole", + "lens": "repo-functional", + "lensFile": "repo-functional.md", + "kind": "whole", + "unit": null, + "expectedIds": [ + "whole:behaviour" + ], + "model": "claude-opus-5-5", + "maxTurns": 220, + "timeoutMin": 120 + }, + "blocks": [ + { + "label": "repo-functional:na:p1:hello.py#hdr", + "lens": "repo-functional", + "lensFile": "repo-functional.md", + "kind": "auto-na", + "unit": "p1:hello.py#hdr", + "expectedIds": [ + "p1:hello.py#hdr" + ], + "unitSha": "6570ee137060f18d456b9b64f93087e3686684254c7e3f6315184de5823c9d8d", + "basis": "mechanical: a diff header (mode, rename, new or deleted file) carries no behaviour of its own; the hunks of the file do" + }, + { + "label": "repo-functional:block:p1:hello.py#1", + "lens": "repo-functional", + "lensFile": "repo-functional.md", + "kind": "block", + "unit": "p1:hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p1:hello.py#1" + ], + "unitSha": "5aa93b494045c41acf995d776f069eb2877e3394f12f3840c46105061fd50b7b", + "model": "claude-opus-5-5", + "maxTurns": 90, + "timeoutMin": 60 + }, + { + "label": "repo-functional:na:p2:hello.py#hdr", + "lens": "repo-functional", + "lensFile": "repo-functional.md", + "kind": "auto-na", + "unit": "p2:hello.py#hdr", + "expectedIds": [ + "p2:hello.py#hdr" + ], + "unitSha": "92d8d033aedfeacedc28db66ccdac1139b545078d4e7c0004d0e4f8b3114293e", + "basis": "mechanical: a diff header (mode, rename, new or deleted file) carries no behaviour of its own; the hunks of the file do" + }, + { + "label": "repo-functional:block:p2:hello.py#1", + "lens": "repo-functional", + "lensFile": "repo-functional.md", + "kind": "block", + "unit": "p2:hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#1" + ], + "unitSha": "06a9d50520c4013c7c28388601386d3750fb5de449e1e697951328bebc7edcc7", + "model": "claude-opus-5-5", + "maxTurns": 90, + "timeoutMin": 60 + }, + { + "label": "repo-functional:block:p2:hello.py#2", + "lens": "repo-functional", + "lensFile": "repo-functional.md", + "kind": "block", + "unit": "p2:hello.py#2", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#2" + ], + "unitSha": "5ef2924a54ff3400a57ae09c1216fde775659f1919241f5114c2cc3014566783", + "model": "claude-opus-5-5", + "maxTurns": 90, + "timeoutMin": 60 + }, + { + "label": "repo-functional:na:p3:tests/test_hello.py#hdr", + "lens": "repo-functional", + "lensFile": "repo-functional.md", + "kind": "auto-na", + "unit": "p3:tests/test_hello.py#hdr", + "expectedIds": [ + "p3:tests/test_hello.py#hdr" + ], + "unitSha": "b3272418d51329d527e8f71a250f2ef3aaffa8c3c8472dabd0794012247df743", + "basis": "mechanical: a diff header (mode, rename, new or deleted file) carries no behaviour of its own; the hunks of the file do" + }, + { + "label": "repo-functional:na:p3:tests/test_hello.py#1", + "lens": "repo-functional", + "lensFile": "repo-functional.md", + "kind": "auto-na", + "unit": "p3:tests/test_hello.py#1", + "expectedIds": [ + "p3:tests/test_hello.py#1" + ], + "unitSha": "fbb38ed45f23f435d83fef525222e28d9bd3cf40b8f00bc27d49418bfa46a44e", + "basis": "mechanical: no runtime behaviour - the file matches functional.exclude (tests/**)" + } + ] + }, + "repo-bug-prover.md": { + "whole": null, + "blocks": [ + { + "label": "repo-bug-prover:block:p1:hello.py#hdr", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p1:hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p1:hello.py#hdr" + ], + "unitSha": "6570ee137060f18d456b9b64f93087e3686684254c7e3f6315184de5823c9d8d", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p1:hello.py#1", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p1:hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p1:hello.py#1" + ], + "unitSha": "5aa93b494045c41acf995d776f069eb2877e3394f12f3840c46105061fd50b7b", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p1:outside", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p1:outside", + "unitKind": "code", + "expectedIds": [ + "p1:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p1:absent", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p1:absent", + "unitKind": "code", + "expectedIds": [ + "p1:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p2:hello.py#hdr", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p2:hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#hdr" + ], + "unitSha": "92d8d033aedfeacedc28db66ccdac1139b545078d4e7c0004d0e4f8b3114293e", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p2:hello.py#1", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p2:hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#1" + ], + "unitSha": "06a9d50520c4013c7c28388601386d3750fb5de449e1e697951328bebc7edcc7", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p2:hello.py#2", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p2:hello.py#2", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#2" + ], + "unitSha": "5ef2924a54ff3400a57ae09c1216fde775659f1919241f5114c2cc3014566783", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p2:outside", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p2:outside", + "unitKind": "code", + "expectedIds": [ + "p2:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p2:absent", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p2:absent", + "unitKind": "code", + "expectedIds": [ + "p2:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p3:tests/test_hello.py#hdr", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p3:tests/test_hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p3:tests/test_hello.py#hdr" + ], + "unitSha": "b3272418d51329d527e8f71a250f2ef3aaffa8c3c8472dabd0794012247df743", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p3:tests/test_hello.py#1", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p3:tests/test_hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p3:tests/test_hello.py#1" + ], + "unitSha": "fbb38ed45f23f435d83fef525222e28d9bd3cf40b8f00bc27d49418bfa46a44e", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p3:outside", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p3:outside", + "unitKind": "code", + "expectedIds": [ + "p3:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p3:absent", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p3:absent", + "unitKind": "code", + "expectedIds": [ + "p3:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + } + ] + }, + "repo-bug-prover.md:exclude": { + "whole": null, + "blocks": [ + { + "label": "repo-bug-prover:block:p1:hello.py#hdr", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p1:hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p1:hello.py#hdr" + ], + "unitSha": "6570ee137060f18d456b9b64f93087e3686684254c7e3f6315184de5823c9d8d", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p1:hello.py#1", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p1:hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p1:hello.py#1" + ], + "unitSha": "5aa93b494045c41acf995d776f069eb2877e3394f12f3840c46105061fd50b7b", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p1:outside", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p1:outside", + "unitKind": "code", + "expectedIds": [ + "p1:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p1:absent", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p1:absent", + "unitKind": "code", + "expectedIds": [ + "p1:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p2:hello.py#hdr", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p2:hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#hdr" + ], + "unitSha": "92d8d033aedfeacedc28db66ccdac1139b545078d4e7c0004d0e4f8b3114293e", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p2:hello.py#1", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p2:hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#1" + ], + "unitSha": "06a9d50520c4013c7c28388601386d3750fb5de449e1e697951328bebc7edcc7", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p2:hello.py#2", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p2:hello.py#2", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#2" + ], + "unitSha": "5ef2924a54ff3400a57ae09c1216fde775659f1919241f5114c2cc3014566783", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p2:outside", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p2:outside", + "unitKind": "code", + "expectedIds": [ + "p2:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p2:absent", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p2:absent", + "unitKind": "code", + "expectedIds": [ + "p2:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p3:tests/test_hello.py#hdr", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p3:tests/test_hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p3:tests/test_hello.py#hdr" + ], + "unitSha": "b3272418d51329d527e8f71a250f2ef3aaffa8c3c8472dabd0794012247df743", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p3:tests/test_hello.py#1", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p3:tests/test_hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p3:tests/test_hello.py#1" + ], + "unitSha": "fbb38ed45f23f435d83fef525222e28d9bd3cf40b8f00bc27d49418bfa46a44e", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p3:outside", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p3:outside", + "unitKind": "code", + "expectedIds": [ + "p3:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-bug-prover:block:p3:absent", + "lens": "repo-bug-prover", + "lensFile": "repo-bug-prover.md", + "kind": "block", + "unit": "p3:absent", + "unitKind": "code", + "expectedIds": [ + "p3:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + } + ] + }, + "repo-security.md": { + "whole": null, + "blocks": [ + { + "label": "repo-security:block:p1:hello.py#hdr", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p1:hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p1:hello.py#hdr" + ], + "unitSha": "6570ee137060f18d456b9b64f93087e3686684254c7e3f6315184de5823c9d8d", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p1:hello.py#1", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p1:hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p1:hello.py#1" + ], + "unitSha": "5aa93b494045c41acf995d776f069eb2877e3394f12f3840c46105061fd50b7b", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p1:outside", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p1:outside", + "unitKind": "code", + "expectedIds": [ + "p1:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p1:absent", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p1:absent", + "unitKind": "code", + "expectedIds": [ + "p1:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p2:hello.py#hdr", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p2:hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#hdr" + ], + "unitSha": "92d8d033aedfeacedc28db66ccdac1139b545078d4e7c0004d0e4f8b3114293e", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p2:hello.py#1", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p2:hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#1" + ], + "unitSha": "06a9d50520c4013c7c28388601386d3750fb5de449e1e697951328bebc7edcc7", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p2:hello.py#2", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p2:hello.py#2", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#2" + ], + "unitSha": "5ef2924a54ff3400a57ae09c1216fde775659f1919241f5114c2cc3014566783", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p2:outside", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p2:outside", + "unitKind": "code", + "expectedIds": [ + "p2:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p2:absent", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p2:absent", + "unitKind": "code", + "expectedIds": [ + "p2:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p3:tests/test_hello.py#hdr", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p3:tests/test_hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p3:tests/test_hello.py#hdr" + ], + "unitSha": "b3272418d51329d527e8f71a250f2ef3aaffa8c3c8472dabd0794012247df743", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p3:tests/test_hello.py#1", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p3:tests/test_hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p3:tests/test_hello.py#1" + ], + "unitSha": "fbb38ed45f23f435d83fef525222e28d9bd3cf40b8f00bc27d49418bfa46a44e", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p3:outside", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p3:outside", + "unitKind": "code", + "expectedIds": [ + "p3:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p3:absent", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p3:absent", + "unitKind": "code", + "expectedIds": [ + "p3:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + } + ] + }, + "repo-security.md:exclude": { + "whole": null, + "blocks": [ + { + "label": "repo-security:block:p1:hello.py#hdr", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p1:hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p1:hello.py#hdr" + ], + "unitSha": "6570ee137060f18d456b9b64f93087e3686684254c7e3f6315184de5823c9d8d", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p1:hello.py#1", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p1:hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p1:hello.py#1" + ], + "unitSha": "5aa93b494045c41acf995d776f069eb2877e3394f12f3840c46105061fd50b7b", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p1:outside", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p1:outside", + "unitKind": "code", + "expectedIds": [ + "p1:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p1:absent", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p1:absent", + "unitKind": "code", + "expectedIds": [ + "p1:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p2:hello.py#hdr", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p2:hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#hdr" + ], + "unitSha": "92d8d033aedfeacedc28db66ccdac1139b545078d4e7c0004d0e4f8b3114293e", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p2:hello.py#1", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p2:hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#1" + ], + "unitSha": "06a9d50520c4013c7c28388601386d3750fb5de449e1e697951328bebc7edcc7", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p2:hello.py#2", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p2:hello.py#2", + "unitKind": "code", + "expectedIds": [ + "p2:hello.py#2" + ], + "unitSha": "5ef2924a54ff3400a57ae09c1216fde775659f1919241f5114c2cc3014566783", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p2:outside", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p2:outside", + "unitKind": "code", + "expectedIds": [ + "p2:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p2:absent", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p2:absent", + "unitKind": "code", + "expectedIds": [ + "p2:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p3:tests/test_hello.py#hdr", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p3:tests/test_hello.py#hdr", + "unitKind": "code", + "expectedIds": [ + "p3:tests/test_hello.py#hdr" + ], + "unitSha": "b3272418d51329d527e8f71a250f2ef3aaffa8c3c8472dabd0794012247df743", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p3:tests/test_hello.py#1", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p3:tests/test_hello.py#1", + "unitKind": "code", + "expectedIds": [ + "p3:tests/test_hello.py#1" + ], + "unitSha": "fbb38ed45f23f435d83fef525222e28d9bd3cf40b8f00bc27d49418bfa46a44e", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p3:outside", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p3:outside", + "unitKind": "code", + "expectedIds": [ + "p3:outside" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + }, + { + "label": "repo-security:block:p3:absent", + "lens": "repo-security", + "lensFile": "repo-security.md", + "kind": "block", + "unit": "p3:absent", + "unitKind": "code", + "expectedIds": [ + "p3:absent" + ], + "unitSha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "model": "claude-opus-5-5", + "maxTurns": 60, + "timeoutMin": 40 + } + ] + } + }, + "keys": { + "repo-better-solution.md|None|": { + "p1:hello.py#hdr": "86ab66ff2958b265", + "p1:hello.py#1": "ec26d6d187afb316", + "p1:outside": "4f91e01270480f8a", + "p1:absent": "3969dad9a7dc4a5a", + "p2:hello.py#hdr": "a417d84e2b67bfe2", + "p2:hello.py#1": "0f33e5ffd1e6a50f", + "p2:hello.py#2": "c01ef3d95ca10364", + "p2:outside": "e73394e549bab720", + "p2:absent": "d372676b68d51084", + "p3:tests/test_hello.py#hdr": "fbc230de2560afd1", + "p3:tests/test_hello.py#1": "4eb6c3132a63607b", + "p3:outside": "b6b9d5c0f6e850e2", + "p3:absent": "bcfb3b97e0b19661" + }, + "repo-better-solution.md|None|w2,w1": { + "p1:hello.py#hdr": "36670e2ce1b7eb0f", + "p1:hello.py#1": "e9644e2e2e18cf96", + "p1:outside": "912b9a024fe5286b", + "p1:absent": "9223c3d0da98976f", + "p2:hello.py#hdr": "a87f050384779c6a", + "p2:hello.py#1": "4f69bb8a5e31b2f4", + "p2:hello.py#2": "b12fbc2be307f79d", + "p2:outside": "6bd255541a40a262", + "p2:absent": "bc0b51adb3e5d7b4", + "p3:tests/test_hello.py#hdr": "1610490e25516283", + "p3:tests/test_hello.py#1": "d11dd8f1d70b8c79", + "p3:outside": "e0281aa8b0d210a2", + "p3:absent": "16844d4867fe0bd8" + }, + "repo-better-solution.md|none|": { + "p1:hello.py#hdr": "86ab66ff2958b265", + "p1:hello.py#1": "ec26d6d187afb316", + "p1:outside": "4f91e01270480f8a", + "p1:absent": "3969dad9a7dc4a5a", + "p2:hello.py#hdr": "a417d84e2b67bfe2", + "p2:hello.py#1": "0f33e5ffd1e6a50f", + "p2:hello.py#2": "c01ef3d95ca10364", + "p2:outside": "e73394e549bab720", + "p2:absent": "d372676b68d51084", + "p3:tests/test_hello.py#hdr": "fbc230de2560afd1", + "p3:tests/test_hello.py#1": "4eb6c3132a63607b", + "p3:outside": "b6b9d5c0f6e850e2", + "p3:absent": "bcfb3b97e0b19661" + }, + "repo-better-solution.md|none|w2,w1": { + "p1:hello.py#hdr": "36670e2ce1b7eb0f", + "p1:hello.py#1": "e9644e2e2e18cf96", + "p1:outside": "912b9a024fe5286b", + "p1:absent": "9223c3d0da98976f", + "p2:hello.py#hdr": "a87f050384779c6a", + "p2:hello.py#1": "4f69bb8a5e31b2f4", + "p2:hello.py#2": "b12fbc2be307f79d", + "p2:outside": "6bd255541a40a262", + "p2:absent": "bc0b51adb3e5d7b4", + "p3:tests/test_hello.py#hdr": "1610490e25516283", + "p3:tests/test_hello.py#1": "d11dd8f1d70b8c79", + "p3:outside": "e0281aa8b0d210a2", + "p3:absent": "16844d4867fe0bd8" + }, + "repo-better-solution.md|browser|": { + "p1:hello.py#hdr": "fff47e4c275ea6f2", + "p1:hello.py#1": "0d7a5e474c36e5a6", + "p1:outside": "bde319a0b918d2cc", + "p1:absent": "b7571f407ad6ca1e", + "p2:hello.py#hdr": "047cc6e32a2fd314", + "p2:hello.py#1": "86412122797f3f0e", + "p2:hello.py#2": "f60e61f76c5b095d", + "p2:outside": "d36a860511c05584", + "p2:absent": "88832eda36b07430", + "p3:tests/test_hello.py#hdr": "743029a3d6fd6b29", + "p3:tests/test_hello.py#1": "fb64a7fc0a403f8c", + "p3:outside": "7b24fe62d497d149", + "p3:absent": "05768513cf263bfd" + }, + "repo-better-solution.md|browser|w2,w1": { + "p1:hello.py#hdr": "1f971ccd4e7ecf15", + "p1:hello.py#1": "ed444189cb643f79", + "p1:outside": "04935c7df40e303b", + "p1:absent": "2c6ee48517b155a9", + "p2:hello.py#hdr": "5bbff120e727329f", + "p2:hello.py#1": "71c5602e4377cfaf", + "p2:hello.py#2": "be0b053840982e59", + "p2:outside": "c5cc24d26319a532", + "p2:absent": "999f5e69c60825c2", + "p3:tests/test_hello.py#hdr": "410d4b79ea242a44", + "p3:tests/test_hello.py#1": "6e23b9d264270751", + "p3:outside": "5b0754cd856508b3", + "p3:absent": "d47bed890a980611" + }, + "repo-better-solution.md|supabase|": { + "p1:hello.py#hdr": "7853dd63d954cc84", + "p1:hello.py#1": "ba09b829961ee739", + "p1:outside": "acd576bcdd39c803", + "p1:absent": "4e24babbe57d907e", + "p2:hello.py#hdr": "e46fa5e269834fcc", + "p2:hello.py#1": "ea4cfb46da7181f9", + "p2:hello.py#2": "5b1df01cd6b523de", + "p2:outside": "55f52c0e94527c01", + "p2:absent": "5211f87235fb8618", + "p3:tests/test_hello.py#hdr": "184a143156354351", + "p3:tests/test_hello.py#1": "bbdaae2f800a5d7a", + "p3:outside": "05060f8921d773d3", + "p3:absent": "076e49634bba3f50" + }, + "repo-better-solution.md|supabase|w2,w1": { + "p1:hello.py#hdr": "62cd9eca96dcc583", + "p1:hello.py#1": "739ff25d602eb71b", + "p1:outside": "5d6a3cebc4967919", + "p1:absent": "0dde585dbbd3c4c4", + "p2:hello.py#hdr": "d7b1dddf80d83c3a", + "p2:hello.py#1": "27df4b787b60a9d4", + "p2:hello.py#2": "2aa6b9c2cb01183b", + "p2:outside": "43710a8bcf5807c9", + "p2:absent": "cbe5598a0a43befb", + "p3:tests/test_hello.py#hdr": "728f1a00d090da4d", + "p3:tests/test_hello.py#1": "b681f96459625c00", + "p3:outside": "8d529517a40d6602", + "p3:absent": "699530897a9e3786" + }, + "repo-better-solution.md|browser+supabase|": { + "p1:hello.py#hdr": "0675cb34344724f8", + "p1:hello.py#1": "42481f747ea8179a", + "p1:outside": "c2fd6d8828511490", + "p1:absent": "1751ae96c3fb3456", + "p2:hello.py#hdr": "9dfc809c6a17b1a9", + "p2:hello.py#1": "db1e87dd2b6cb432", + "p2:hello.py#2": "1419bb43a3c3e131", + "p2:outside": "0f9763f5bdfb6b25", + "p2:absent": "4aae0dbd51c582a4", + "p3:tests/test_hello.py#hdr": "35b3f7b4a2e94d3b", + "p3:tests/test_hello.py#1": "cdf570fa23aaecdb", + "p3:outside": "3f0b5f210a1d8fa3", + "p3:absent": "ecfa7108a2780fdd" + }, + "repo-better-solution.md|browser+supabase|w2,w1": { + "p1:hello.py#hdr": "91dc6f9859c04d33", + "p1:hello.py#1": "6e1b5f2014981798", + "p1:outside": "a90b4e4dd4ff791f", + "p1:absent": "8fd10cac81f30334", + "p2:hello.py#hdr": "9049e09530281e8a", + "p2:hello.py#1": "bc234fc19914a0ba", + "p2:hello.py#2": "0b585800f434b6a0", + "p2:outside": "9e0a532aa8500fdc", + "p2:absent": "6be10a1e92eeefb5", + "p3:tests/test_hello.py#hdr": "223e9417f51e8e8e", + "p3:tests/test_hello.py#1": "7924cec8fde1ac13", + "p3:outside": "5209fb9cb789da13", + "p3:absent": "4f9ea8c3624ecfa4" + }, + "repo-functional.md|None|": { + "p1:hello.py#1": "aae0edda09efda89", + "p2:hello.py#1": "cdfe6f72e7b06df1", + "p2:hello.py#2": "564c534a808c4be1" + }, + "repo-functional.md|None|w2,w1": { + "p1:hello.py#1": "8af10533218e4d1a", + "p2:hello.py#1": "a9fb5a72511aa4c0", + "p2:hello.py#2": "12614a96e715ed53" + }, + "repo-functional.md|none|": { + "p1:hello.py#1": "5c81f7f419305fc1", + "p2:hello.py#1": "26ab9146bb1202ea", + "p2:hello.py#2": "6cc05a3914228068" + }, + "repo-functional.md|none|w2,w1": { + "p1:hello.py#1": "41587e0c7fd6f06b", + "p2:hello.py#1": "1e5811adf9cb6ce2", + "p2:hello.py#2": "cded9c2e933b2465" + }, + "repo-functional.md|browser|": { + "p1:hello.py#1": "aae0edda09efda89", + "p2:hello.py#1": "cdfe6f72e7b06df1", + "p2:hello.py#2": "564c534a808c4be1" + }, + "repo-functional.md|browser|w2,w1": { + "p1:hello.py#1": "8af10533218e4d1a", + "p2:hello.py#1": "a9fb5a72511aa4c0", + "p2:hello.py#2": "12614a96e715ed53" + }, + "repo-functional.md|supabase|": { + "p1:hello.py#1": "ac7523530a22f662", + "p2:hello.py#1": "4cea0535f56cfa08", + "p2:hello.py#2": "9514cc62830449d6" + }, + "repo-functional.md|supabase|w2,w1": { + "p1:hello.py#1": "6c99dbebf9b446e7", + "p2:hello.py#1": "12e093e41e0159a3", + "p2:hello.py#2": "4c6a84a8776ff6cc" + }, + "repo-functional.md|browser+supabase|": { + "p1:hello.py#1": "e27e4a16fe52c163", + "p2:hello.py#1": "d37cbc4d3fdbe578", + "p2:hello.py#2": "36bfdfd3cba3234b" + }, + "repo-functional.md|browser+supabase|w2,w1": { + "p1:hello.py#1": "fa37bf5af24480ce", + "p2:hello.py#1": "b4aec92aa631aa9b", + "p2:hello.py#2": "434653e51e242f85" + }, + "repo-bug-prover.md|None|": { + "p1:hello.py#hdr": "03748a78cb579d13", + "p1:hello.py#1": "a7dff8c8d7f38f1b", + "p1:outside": "695b3b18772edf4c", + "p1:absent": "1ee4ad004025ebfe", + "p2:hello.py#hdr": "51766e1b2606d9a9", + "p2:hello.py#1": "f3a6017d642dfa74", + "p2:hello.py#2": "49433e0a44f1e71f", + "p2:outside": "8a85d9cebfe0cc10", + "p2:absent": "9eba93e23a7bcc58", + "p3:tests/test_hello.py#hdr": "804719a981b994b8", + "p3:tests/test_hello.py#1": "0b494af3870e58c8", + "p3:outside": "963c87a9f6e0dfae", + "p3:absent": "9dfe80907ebdc8c9" + }, + "repo-bug-prover.md|None|w2,w1": { + "p1:hello.py#hdr": "69614a0e68791d39", + "p1:hello.py#1": "70c378dd1d9e78b3", + "p1:outside": "f9b34844a98096b7", + "p1:absent": "3caec537f8d24034", + "p2:hello.py#hdr": "a566f6aacea7915b", + "p2:hello.py#1": "c9ed60db4aaec5d1", + "p2:hello.py#2": "f0859a7099323e5d", + "p2:outside": "27920a5a2d50861f", + "p2:absent": "94138b54ef63fc93", + "p3:tests/test_hello.py#hdr": "99c6252ca99e9fac", + "p3:tests/test_hello.py#1": "5c330aa77375ed86", + "p3:outside": "1513da6115ae96de", + "p3:absent": "543f6495fab61042" + }, + "repo-bug-prover.md|none|": { + "p1:hello.py#hdr": "03748a78cb579d13", + "p1:hello.py#1": "a7dff8c8d7f38f1b", + "p1:outside": "695b3b18772edf4c", + "p1:absent": "1ee4ad004025ebfe", + "p2:hello.py#hdr": "51766e1b2606d9a9", + "p2:hello.py#1": "f3a6017d642dfa74", + "p2:hello.py#2": "49433e0a44f1e71f", + "p2:outside": "8a85d9cebfe0cc10", + "p2:absent": "9eba93e23a7bcc58", + "p3:tests/test_hello.py#hdr": "804719a981b994b8", + "p3:tests/test_hello.py#1": "0b494af3870e58c8", + "p3:outside": "963c87a9f6e0dfae", + "p3:absent": "9dfe80907ebdc8c9" + }, + "repo-bug-prover.md|none|w2,w1": { + "p1:hello.py#hdr": "69614a0e68791d39", + "p1:hello.py#1": "70c378dd1d9e78b3", + "p1:outside": "f9b34844a98096b7", + "p1:absent": "3caec537f8d24034", + "p2:hello.py#hdr": "a566f6aacea7915b", + "p2:hello.py#1": "c9ed60db4aaec5d1", + "p2:hello.py#2": "f0859a7099323e5d", + "p2:outside": "27920a5a2d50861f", + "p2:absent": "94138b54ef63fc93", + "p3:tests/test_hello.py#hdr": "99c6252ca99e9fac", + "p3:tests/test_hello.py#1": "5c330aa77375ed86", + "p3:outside": "1513da6115ae96de", + "p3:absent": "543f6495fab61042" + }, + "repo-bug-prover.md|browser|": { + "p1:hello.py#hdr": "afdca0199b27a253", + "p1:hello.py#1": "78336c743c76a36d", + "p1:outside": "0d36c6cd7f7b0142", + "p1:absent": "709760acc1ca0dd2", + "p2:hello.py#hdr": "59599d2ac61b242c", + "p2:hello.py#1": "9e21cc6a69235aee", + "p2:hello.py#2": "3882f168c4ebac34", + "p2:outside": "563d2d1ac27c062b", + "p2:absent": "39e5ec36d5f601de", + "p3:tests/test_hello.py#hdr": "0f7f2a9854bb1cb1", + "p3:tests/test_hello.py#1": "5a2252d0042573ba", + "p3:outside": "547981787ef2bf4d", + "p3:absent": "1ae5233c869bdc63" + }, + "repo-bug-prover.md|browser|w2,w1": { + "p1:hello.py#hdr": "8c5f92efd6734de2", + "p1:hello.py#1": "eaa4d035d3756873", + "p1:outside": "7ec2636b645602e9", + "p1:absent": "3bdd4ac0209b15bb", + "p2:hello.py#hdr": "cf68e2a996323b2d", + "p2:hello.py#1": "6230589a2d427edf", + "p2:hello.py#2": "0bd97116776fa680", + "p2:outside": "837b94d9db8aa64a", + "p2:absent": "7274206a354b1112", + "p3:tests/test_hello.py#hdr": "1091ba6f9f5a7620", + "p3:tests/test_hello.py#1": "5d0e28398cda7b35", + "p3:outside": "2f70463f9177510f", + "p3:absent": "4440d384ccfbc0c7" + }, + "repo-bug-prover.md|supabase|": { + "p1:hello.py#hdr": "63263d900d2c65f1", + "p1:hello.py#1": "4671303b5e9ca442", + "p1:outside": "db945886600c0586", + "p1:absent": "abcc01bc694f7ed0", + "p2:hello.py#hdr": "5a7de3de6c4722e7", + "p2:hello.py#1": "06c949d8e589f2b0", + "p2:hello.py#2": "47bd28ac4bbe3b85", + "p2:outside": "04a5aabc056a1e07", + "p2:absent": "3f1ecd9c879684b3", + "p3:tests/test_hello.py#hdr": "e663228540bc34f1", + "p3:tests/test_hello.py#1": "e3439d88e83cd0f4", + "p3:outside": "b06dbf94185beb39", + "p3:absent": "8931002b46003cc5" + }, + "repo-bug-prover.md|supabase|w2,w1": { + "p1:hello.py#hdr": "e56aef32618efd03", + "p1:hello.py#1": "432c4c57e46328a4", + "p1:outside": "628de6903447f817", + "p1:absent": "d15d14de4d9e6537", + "p2:hello.py#hdr": "b37b4104e9c9b37b", + "p2:hello.py#1": "431149c73cb004f2", + "p2:hello.py#2": "bb4ededf8b91a08d", + "p2:outside": "81b654f7f1c0b84c", + "p2:absent": "20bdd50324bd9423", + "p3:tests/test_hello.py#hdr": "c70289f81d0ae015", + "p3:tests/test_hello.py#1": "cd4e14141083a442", + "p3:outside": "7cc182edfc3904ba", + "p3:absent": "318a6c37860fad91" + }, + "repo-bug-prover.md|browser+supabase|": { + "p1:hello.py#hdr": "3f5c1f4b889921c6", + "p1:hello.py#1": "75fa3f79f9d49e7f", + "p1:outside": "f8a6d62c247fd834", + "p1:absent": "00b83cf0a800b1fe", + "p2:hello.py#hdr": "b24ae4069d3ff455", + "p2:hello.py#1": "9ccbedaface54965", + "p2:hello.py#2": "e57c83fb7f880f24", + "p2:outside": "11cb331ef2c84ff8", + "p2:absent": "3b7296762c915b2e", + "p3:tests/test_hello.py#hdr": "7e1d6248af105582", + "p3:tests/test_hello.py#1": "f133055cf867c529", + "p3:outside": "8baae9a95491af9e", + "p3:absent": "f3e2347814381852" + }, + "repo-bug-prover.md|browser+supabase|w2,w1": { + "p1:hello.py#hdr": "0898d2d9fcca51e2", + "p1:hello.py#1": "9c458732b74e0689", + "p1:outside": "c70e39ddd50aca39", + "p1:absent": "429391ada0c8b52c", + "p2:hello.py#hdr": "38905bd56abc814b", + "p2:hello.py#1": "3cbdb039a5ea5881", + "p2:hello.py#2": "f53536520d0b00d5", + "p2:outside": "c86794c17cdd41b2", + "p2:absent": "07ef0edc5708ccba", + "p3:tests/test_hello.py#hdr": "63b5b89cc23e5cd1", + "p3:tests/test_hello.py#1": "f89b074437c970a6", + "p3:outside": "cebd95e5e25c2e3e", + "p3:absent": "8ed4fe948d765b6b" + }, + "repo-security.md|None|": { + "p1:hello.py#hdr": "a71245b173df7f3d", + "p1:hello.py#1": "dbc855da5b721df7", + "p1:outside": "959dfc5aa9222811", + "p1:absent": "99af41c408f6c0aa", + "p2:hello.py#hdr": "cca4149ce4011a99", + "p2:hello.py#1": "75a13104d24919d0", + "p2:hello.py#2": "198366c3fd70bf62", + "p2:outside": "ba1a3d1208852bac", + "p2:absent": "464fff73d5ed3c6b", + "p3:tests/test_hello.py#hdr": "4d1ca3389beddffc", + "p3:tests/test_hello.py#1": "18a65f5a40ed4cd3", + "p3:outside": "1b130a82487d3f65", + "p3:absent": "a48bf1999c708bd8" + }, + "repo-security.md|None|w2,w1": { + "p1:hello.py#hdr": "72a4514d7b184da0", + "p1:hello.py#1": "aa75702cdd4e67cd", + "p1:outside": "a6ee3355061a3694", + "p1:absent": "a88743327761fe8b", + "p2:hello.py#hdr": "c13dc1c146fadb30", + "p2:hello.py#1": "b2d30d223811f90d", + "p2:hello.py#2": "7ba0232229f30442", + "p2:outside": "37c4deeb637294ce", + "p2:absent": "2ccaa146bff72b8a", + "p3:tests/test_hello.py#hdr": "cbddd9a00edeae33", + "p3:tests/test_hello.py#1": "d27497be1e3ee0c8", + "p3:outside": "33c757f16ccf2c6f", + "p3:absent": "5235164c6f5d64c1" + }, + "repo-security.md|none|": { + "p1:hello.py#hdr": "a71245b173df7f3d", + "p1:hello.py#1": "dbc855da5b721df7", + "p1:outside": "959dfc5aa9222811", + "p1:absent": "99af41c408f6c0aa", + "p2:hello.py#hdr": "cca4149ce4011a99", + "p2:hello.py#1": "75a13104d24919d0", + "p2:hello.py#2": "198366c3fd70bf62", + "p2:outside": "ba1a3d1208852bac", + "p2:absent": "464fff73d5ed3c6b", + "p3:tests/test_hello.py#hdr": "4d1ca3389beddffc", + "p3:tests/test_hello.py#1": "18a65f5a40ed4cd3", + "p3:outside": "1b130a82487d3f65", + "p3:absent": "a48bf1999c708bd8" + }, + "repo-security.md|none|w2,w1": { + "p1:hello.py#hdr": "72a4514d7b184da0", + "p1:hello.py#1": "aa75702cdd4e67cd", + "p1:outside": "a6ee3355061a3694", + "p1:absent": "a88743327761fe8b", + "p2:hello.py#hdr": "c13dc1c146fadb30", + "p2:hello.py#1": "b2d30d223811f90d", + "p2:hello.py#2": "7ba0232229f30442", + "p2:outside": "37c4deeb637294ce", + "p2:absent": "2ccaa146bff72b8a", + "p3:tests/test_hello.py#hdr": "cbddd9a00edeae33", + "p3:tests/test_hello.py#1": "d27497be1e3ee0c8", + "p3:outside": "33c757f16ccf2c6f", + "p3:absent": "5235164c6f5d64c1" + }, + "repo-security.md|browser|": { + "p1:hello.py#hdr": "79f5fda94ab59861", + "p1:hello.py#1": "c39999ec7614a9d0", + "p1:outside": "8d558b989aa15972", + "p1:absent": "1aa2d6ebe99e866f", + "p2:hello.py#hdr": "ef82ef65d8d37ef9", + "p2:hello.py#1": "b7c6b54d3ed90ef5", + "p2:hello.py#2": "3b1e1d98c9725cd3", + "p2:outside": "7dd9a61c68b9463c", + "p2:absent": "7e0edca1334a39e4", + "p3:tests/test_hello.py#hdr": "fc077b47f659da5b", + "p3:tests/test_hello.py#1": "19451b2f9897821c", + "p3:outside": "576e4eda256d2aa0", + "p3:absent": "58c29259d1e960b4" + }, + "repo-security.md|browser|w2,w1": { + "p1:hello.py#hdr": "3e7678bec100a256", + "p1:hello.py#1": "cdafbabdee3f24cb", + "p1:outside": "a41ab6fd840b398f", + "p1:absent": "604b39b9f9bbccfe", + "p2:hello.py#hdr": "882ecbdcabdb8a42", + "p2:hello.py#1": "d8ee6d8da3494f3d", + "p2:hello.py#2": "b7ea9903ee812745", + "p2:outside": "aaa096f8e8948450", + "p2:absent": "5f1725e2e4bac509", + "p3:tests/test_hello.py#hdr": "029705e04607e5f5", + "p3:tests/test_hello.py#1": "9c2eee1d86afba88", + "p3:outside": "29757d8f977a9def", + "p3:absent": "b029790da06dad50" + }, + "repo-security.md|supabase|": { + "p1:hello.py#hdr": "1031e3fc854cb455", + "p1:hello.py#1": "93d2ba4eb660b34c", + "p1:outside": "0ce47a1fc5db3d0f", + "p1:absent": "1a65a8b7609b37d6", + "p2:hello.py#hdr": "10dd546048a8638f", + "p2:hello.py#1": "7ab90011284bc894", + "p2:hello.py#2": "7e56bbd7e5794066", + "p2:outside": "7af2454d278e60f5", + "p2:absent": "d76a284de36557bb", + "p3:tests/test_hello.py#hdr": "138ff5bc44f4f2b8", + "p3:tests/test_hello.py#1": "4f8b72ec0408870b", + "p3:outside": "3d9d08843cbccfde", + "p3:absent": "cd74e0a0ae93c5ce" + }, + "repo-security.md|supabase|w2,w1": { + "p1:hello.py#hdr": "90aaec4cde671b79", + "p1:hello.py#1": "10ae72358e3934e3", + "p1:outside": "dc4f9a512a64062b", + "p1:absent": "e97aa795a57c97fe", + "p2:hello.py#hdr": "a730e6bada223dbf", + "p2:hello.py#1": "60a1cc8459e245a0", + "p2:hello.py#2": "9c7321d32e0d89ea", + "p2:outside": "6576521b21b92e9b", + "p2:absent": "28304c4ddee848f3", + "p3:tests/test_hello.py#hdr": "01be5f8fa766e5ef", + "p3:tests/test_hello.py#1": "5737b1e18afff6f4", + "p3:outside": "73b3bd53e37cff23", + "p3:absent": "dcd4736015b55b94" + }, + "repo-security.md|browser+supabase|": { + "p1:hello.py#hdr": "4df87eb84d1a5e44", + "p1:hello.py#1": "a68f0093f5f0f712", + "p1:outside": "293732b413b0f345", + "p1:absent": "927b67a31cad0502", + "p2:hello.py#hdr": "ad9c969e5bf3a71f", + "p2:hello.py#1": "a48ebe0859f3f2e6", + "p2:hello.py#2": "9feab9c11d95c772", + "p2:outside": "fba520cde71900ff", + "p2:absent": "915b8825d4463f5f", + "p3:tests/test_hello.py#hdr": "cb6826cd5c5d05c1", + "p3:tests/test_hello.py#1": "705b1145eaf9d8a6", + "p3:outside": "80de109d094b06ff", + "p3:absent": "9ad0f230af0129ae" + }, + "repo-security.md|browser+supabase|w2,w1": { + "p1:hello.py#hdr": "f8fb515b397eb8d1", + "p1:hello.py#1": "0afd7c37fdd1ead4", + "p1:outside": "e4dfc010762c87e1", + "p1:absent": "78293bcd7bbf3108", + "p2:hello.py#hdr": "fde2c8c4ec7dd42e", + "p2:hello.py#1": "bea84db46b688ed0", + "p2:hello.py#2": "a6e838ffcd99063b", + "p2:outside": "45b88908d476db09", + "p2:absent": "8577de2d5c880382", + "p3:tests/test_hello.py#hdr": "7dad1e3b1f28f79e", + "p3:tests/test_hello.py#1": "a2a3c13278bccd7c", + "p3:outside": "f2d82580bf816585", + "p3:absent": "0a403cf8f0e7dfa9" + } + }, + "wholeKeys": { + "repo-better-solution.md|None|": "2043742bea851ffc", + "repo-better-solution.md|None|w2,w1": "f988e9ac4b2b664a", + "repo-better-solution.md|none|": "2043742bea851ffc", + "repo-better-solution.md|none|w2,w1": "f988e9ac4b2b664a", + "repo-better-solution.md|browser|": "9a899f278f3e6a1a", + "repo-better-solution.md|browser|w2,w1": "293be7b51bfead01", + "repo-better-solution.md|supabase|": "9ec5532c7162220d", + "repo-better-solution.md|supabase|w2,w1": "4244b762fdd38a5f", + "repo-better-solution.md|browser+supabase|": "d890fa91eb6c98cc", + "repo-better-solution.md|browser+supabase|w2,w1": "8f2632bffc9e29b5", + "repo-functional.md|None|": "5d369ba6d41a285b", + "repo-functional.md|None|w2,w1": "44b04b0e35f8f094", + "repo-functional.md|none|": "88ff4c7c427122b1", + "repo-functional.md|none|w2,w1": "14f6c3831def97ce", + "repo-functional.md|browser|": "5d369ba6d41a285b", + "repo-functional.md|browser|w2,w1": "44b04b0e35f8f094", + "repo-functional.md|supabase|": "c4caae7b106051ea", + "repo-functional.md|supabase|w2,w1": "39650b35a0646e53", + "repo-functional.md|browser+supabase|": "9be9049036f492fc", + "repo-functional.md|browser+supabase|w2,w1": "64d6eafe3106698d", + "repo-bug-prover.md|None|": null, + "repo-bug-prover.md|None|w2,w1": null, + "repo-bug-prover.md|none|": null, + "repo-bug-prover.md|none|w2,w1": null, + "repo-bug-prover.md|browser|": null, + "repo-bug-prover.md|browser|w2,w1": null, + "repo-bug-prover.md|supabase|": null, + "repo-bug-prover.md|supabase|w2,w1": null, + "repo-bug-prover.md|browser+supabase|": null, + "repo-bug-prover.md|browser+supabase|w2,w1": null, + "repo-security.md|None|": null, + "repo-security.md|None|w2,w1": null, + "repo-security.md|none|": null, + "repo-security.md|none|w2,w1": null, + "repo-security.md|browser|": null, + "repo-security.md|browser|w2,w1": null, + "repo-security.md|supabase|": null, + "repo-security.md|supabase|w2,w1": null, + "repo-security.md|browser+supabase|": null, + "repo-security.md|browser+supabase|w2,w1": null + }, + "codeKeyParts": { + "p1:hello.py#hdr": [ + "code:hello.py#hdr", + [ + "base=0000000000000000000000000000000000000000" + ] + ], + "p1:hello.py#1": [ + "code:hello.py#hunk", + [ + "base=0000000000000000000000000000000000000000" + ] + ], + "p1:outside": [ + "code:outside", + [ + "base=0000000000000000000000000000000000000000", + "commit=1:5e4094c730dff2915cee33a9d1d680636270e348327ebf2ab43b1fc2008878dd", + "range=bd4ac080833e50f3532594f38f22e98a04e969fdf6ecefe6026fc203dcfda9e5", + "docs=" + ] + ], + "p1:absent": [ + "code:absent", + [ + "base=0000000000000000000000000000000000000000", + "commit=1:5e4094c730dff2915cee33a9d1d680636270e348327ebf2ab43b1fc2008878dd", + "range=bd4ac080833e50f3532594f38f22e98a04e969fdf6ecefe6026fc203dcfda9e5", + "docs=", + "body=no" + ] + ], + "p2:README.md#hdr": [ + "code:README.md#hdr", + [ + "base=0000000000000000000000000000000000000000" + ] + ], + "p2:README.md#1": [ + "code:README.md#hunk", + [ + "base=0000000000000000000000000000000000000000" + ] + ], + "p2:hello.py#hdr": [ + "code:hello.py#hdr", + [ + "base=0000000000000000000000000000000000000000" + ] + ], + "p2:hello.py#1": [ + "code:hello.py#hunk", + [ + "base=0000000000000000000000000000000000000000" + ] + ], + "p2:hello.py#2": [ + "code:hello.py#hunk", + [ + "base=0000000000000000000000000000000000000000" + ] + ], + "p2:outside": [ + "code:outside", + [ + "base=0000000000000000000000000000000000000000", + "commit=2:b2cc3b53017cdb202788417d40ea0ea7e411003697f966bb111f77ac27244a80", + "range=bd4ac080833e50f3532594f38f22e98a04e969fdf6ecefe6026fc203dcfda9e5", + "docs=diff --git a/README.md b/README.md\nnew file mode 100644\nindex\n--- /dev/null\n+++ b/README.md" + ] + ], + "p2:absent": [ + "code:absent", + [ + "base=0000000000000000000000000000000000000000", + "commit=2:b2cc3b53017cdb202788417d40ea0ea7e411003697f966bb111f77ac27244a80", + "range=bd4ac080833e50f3532594f38f22e98a04e969fdf6ecefe6026fc203dcfda9e5", + "docs=diff --git a/README.md b/README.md\nnew file mode 100644\nindex\n--- /dev/null\n+++ b/README.md", + "body=yes" + ] + ], + "p3:tests/test_hello.py#hdr": [ + "code:tests/test_hello.py#hdr", + [ + "base=0000000000000000000000000000000000000000" + ] + ], + "p3:tests/test_hello.py#1": [ + "code:tests/test_hello.py#hunk", + [ + "base=0000000000000000000000000000000000000000" + ] + ], + "p3:outside": [ + "code:outside", + [ + "base=0000000000000000000000000000000000000000", + "commit=3:9ae03b452c1dceef52357cf229ad1ca20a4b2dfc1b3dde89b9d67cd47183609b", + "range=bd4ac080833e50f3532594f38f22e98a04e969fdf6ecefe6026fc203dcfda9e5", + "docs=" + ] + ], + "p3:absent": [ + "code:absent", + [ + "base=0000000000000000000000000000000000000000", + "commit=3:9ae03b452c1dceef52357cf229ad1ca20a4b2dfc1b3dde89b9d67cd47183609b", + "range=bd4ac080833e50f3532594f38f22e98a04e969fdf6ecefe6026fc203dcfda9e5", + "docs=", + "body=yes" + ] + ] + }, + "estimate": { + "cacheRead": 51821078.37837833, + "cacheWrite": 2132795.1351351333, + "output": 684000.0, + "processes": 45, + "costUnits": 11268101.75675675, + "listUsd": 56.34 + } + }, "code-keys/rewrite": { "dir": "tests/Goldens/fixture/code-keys/rewrite", "inventory": { diff --git a/tests/Merge/main.cpp b/tests/Merge/main.cpp new file mode 100644 index 0000000..630153d --- /dev/null +++ b/tests/Merge/main.cpp @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// Merging a lens's parts: the dedupe, the cross-unit rule, the strongest +// verdict, missing units; the matrix with units nobody reviews. + +import std; +import Crafter.Review; +using namespace Crafter::Review; + +namespace { + std::int32_t Failures = 0; + + void Check(std::string_view name, bool cond, std::string_view detail = {}) { + std::println("{}{}{}", cond ? " ok " : " FAIL ", name, !cond && !detail.empty() ? std::format(" ({})", detail) : std::string()); + if (!cond) ++Failures; + } + + // {item: verdict} of a merged result. + std::map Verdicts(const Json& merged) { + std::map out; + for (const Json& c : merged.at("coverage")) out[c.at("item").get()] = c.at("verdict").get(); + return out; + } + + Json With(Json value, const Json& changes) { + value.update(changes); + return value; + } +} + +int main() { + std::println("merge_parts: dedupe, cross-unit rule, strongest verdict, missing units; the matrix with not-reviewed units"); + const std::string lens = "repo-bug-prover.md"; + const Json note = {{"kind", "info"}, {"items", {"A", "B"}}, {"note", "shared"}}; + const Json found = {{"finding", "x"}, {"items", {"A", "B"}}, {"discharged", true}, {"dischargedBy", "w1"}}; + const std::vector parts = { + {{"partLabel", "a"}, {"expectedIds", {"A"}}, {"findings", {found}}, {"notes", {note, note}}, {"coverage", {{{"item", "A"}, {"verdict", "clear"}}}}}, + {{"partLabel", "b"}, {"expectedIds", {"B"}}, {"findings", {found}}, {"notes", {note}}, {"coverage", {{{"item", "B"}, {"verdict", "clear"}}}}}, + }; + Json m = Normalize::MergeParts("L", lens, parts, {}); + Check("one note and one finding after the merge, verdict unchanged", m.at("notes").size() == 1 && m.at("findings").size() == 1 && m.at("verdict") == "SEND-READY"); + const Json pa = {{"partLabel", "L:block:A"}, {"partKind", "block"}, {"partUnit", "A"}, {"verdict", "FIX FIRST"}, + {"findings", {{{"finding", "B contradicts A"}, {"evidence", "e"}, {"proposedAction", "p"}, {"items", {"A", "B"}}, {"discharged", false}}}}, + {"notes", Json::array()}, {"coverage", {{{"item", "A"}, {"verdict", "defect"}, {"basis", "b"}}}}, {"coverageMissing", Json::array()}, {"undischarged", 1}}; + const Json pb = {{"partLabel", "L:block:B"}, {"partKind", "block"}, {"partUnit", "B"}, {"verdict", "SEND-READY"}, {"findings", Json::array()}, {"notes", Json::array()}, + {"coverage", {{{"item", "B"}, {"verdict", "clear"}, {"basis", "checked"}}}}, {"coverageMissing", Json::array()}}; + m = Normalize::MergeParts("L", lens, std::vector{pa, pb}, {}); + Json basisOfB; + for (const Json& c : m.at("coverage")) { + if (c.at("item") == "B") basisOfB = c; + } + const bool raised = std::ranges::any_of(m.at("normalizeLogs"), [](const Json& l) { return l.get().contains("raised to defect"); }); + Check("B raised to defect by the finding citing it; lens FIX FIRST", basisOfB.at("verdict") == "defect" && basisOfB.at("basis").get().starts_with("cited by finding") + && m.at("verdict") == "FIX FIRST" && raised); + Json dischargedFinding = pa.at("findings").at(0); + dischargedFinding["discharged"] = true; + dischargedFinding["dischargedBy"] = "w1"; + const Json pc = With(pa, {{"findings", {dischargedFinding}}, {"coverage", {{{"item", "A"}, {"verdict", "clear"}, {"basis", "discharged"}}}}, {"verdict", "SEND-READY"}, {"undischarged", 0}}); + Check("a discharged finding does not raise B", Verdicts(Normalize::MergeParts("L", lens, std::vector{pc, pb}, {}))["B"] == "clear"); + const std::vector missingA = {"A"}; + const Json m3 = Normalize::MergeParts("L", lens, std::vector{pb}, missingA); + Check("missing unit -> coverageMissing + contract violation + verdict NOT RETURNED", m3.at("coverageMissing") == Json::array({"A"}) && !m3.at("contractViolations").empty() + && m3.at("verdict") == "NOT RETURNED"); + const Json pd = With(pb, {{"coverage", {{{"item", "B"}, {"verdict", "immaterial"}, {"basis", "w"}}}}}); + Check("strongest verdict per unit wins (immaterial > clear)", Verdicts(Normalize::MergeParts("L", lens, std::vector{pb, pd}, {}))["B"] == "immaterial"); + const Json result = Normalize::MergeParts("L", lens, std::vector{pa, pb}, {}); + const std::vector notReviewed = {"p1:msg"}; + const Json mx = Normalize::Matrix(Json::array({{{"id", "A"}}, {{"id", "B"}}, {{"id", "C"}}, {{"id", "p1:msg"}}}), std::vector{result}, notReviewed); + std::set defects; + for (auto it = mx.at("defects").begin(); it != mx.at("defects").end(); ++it) defects.insert(it.key()); + Check("matrix: a unit without an entry is MISSING and a hole; a not-reviewed unit is listed, never a hole; defects per unit", + mx.at("matrix").at("C").at("L").at("verdict") == "MISSING" && mx.at("holes") == Json::array({"C"}) && mx.at("notReviewed") == Json::array({"p1:msg"}) + && defects == std::set{"A", "B"}, DumpCompactSorted(mx.at("holes"))); + return Failures ? 1 : 0; +} diff --git a/tests/Normalize/main.cpp b/tests/Normalize/main.cpp new file mode 100644 index 0000000..c4e7f70 --- /dev/null +++ b/tests/Normalize/main.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// Normalising a pass: the synthesized finding, prefix resolution, discharge, +// contests, the vocabulary. + +import std; +import Crafter.Review; +using namespace Crafter::Review; + +namespace { + std::int32_t Failures = 0; + + void Check(std::string_view name, bool cond, std::string_view detail = {}) { + std::println("{}{}{}", cond ? " ok " : " FAIL ", name, !cond && !detail.empty() ? std::format(" ({})", detail) : std::string()); + if (!cond) ++Failures; + } + + bool AnyKind(const Json& violations, std::string_view part) { + return std::ranges::any_of(violations, [&](const Json& v) { return v.at("kind").get().contains(part); }); + } +} + +int main() { + std::println("normalize: synthesized finding, prefix resolution, discharge, contests, vocabulary"); + const Json inv = Json::array({{{"id", "p1:x.py#1"}}, {{"id", "p1:src/components/a.tsx#1"}}, {{"id", "p1:src/components/a.tsx#2"}}}); + std::vector ids; + for (const Json& u : inv) ids.push_back(u.at("id").get()); + const std::string lens = "repo-bug-prover.md"; + const Json first = Json::array({inv[0]}); + auto run = [&](Json v, const std::vector& inForce, std::string_view lensFile = "repo-bug-prover.md") { + return Normalize::NormalizeOutput(v, inForce, lensFile, "h", first, ids); + }; + const Json n = run({{"verdict", "SEND-READY"}, {"findings", Json::array()}, {"notes", Json::array()}, {"coverage", {{{"item", "p1:x.py#1"}, {"verdict", "defect"}, {"basis", "x"}}}}}, {}); + Check("defect without finding -> synthesized undischarged finding, FIX FIRST", n.at("verdict") == "FIX FIRST" && !n.at("findings").empty() + && Dyn::Truthy(Dyn::Get(n.at("findings").at(0), "synthesized")) && n.at("undischarged") == 1); + Check("declared SEND-READY over it -> contract violation", AnyKind(n.at("contractViolations"), "failed closed")); + Check("prefix: a unique prefix resolves, an ambiguous one stays", Normalize::ResolveId("p1:x", ids) == ids[0] + && Normalize::ResolveId("p1:src/components/a.tsx#", ids) == "p1:src/components/a.tsx#"); + const Json n2 = run({{"verdict", "SEND-READY"}, {"findings", Json::array()}, {"notes", Json::array()}, {"coverage", {{{"item", "p1:x"}, {"verdict", "clear"}, {"basis", "b"}}}}}, {}); + Check("abbreviated coverage id resolved -> no MISSING", n2.at("coverageMissing").empty() && n2.at("coverage").at(0).at("item") == ids[0]); + const Json n3 = run({{"verdict", "SEND-READY"}, {"findings", Json::array()}, {"notes", Json::array()}, {"coverage", Json::array()}}, {}); + Check("no coverage entry -> coverageMissing (NOT RETURNED)", n3.at("coverageMissing") == Json::array({ids[0]}) && !n3.at("contractViolations").empty()); + const std::vector d = {{{"id", "w1"}, {"kind", "waiver-owner"}, {"hash", "h"}}}; + auto finding = [&](const Json& extra) { + Json f = {{"finding", "f"}, {"evidence", "e"}, {"proposedAction", "p"}, {"items", {ids[0]}}}; + f.update(extra); + return f; + }; + const Json n4 = run({{"verdict", "SEND-READY"}, {"findings", {finding({{"dischargedBy", "w1"}})}}, {"notes", Json::array()}, + {"coverage", {{{"item", ids[0]}, {"verdict", "clear"}, {"basis", "discharged by w1"}}}}}, d); + Check("dischargedBy an in-force id -> discharged, SEND-READY", n4.at("verdict") == "SEND-READY" && n4.at("findings").at(0).at("discharged") == true); + const Json n5 = run({{"verdict", "SEND-READY"}, {"findings", {finding({{"dischargedBy", "nope"}})}}, {"notes", Json::array()}, + {"coverage", {{{"item", ids[0]}, {"verdict", "clear"}, {"basis", "b"}}}}}, d); + Check("dischargedBy an unknown id -> undischarged, FIX FIRST", n5.at("verdict") == "FIX FIRST" && n5.at("findings").at(0).at("discharged") == false); + const Json n6 = run({{"verdict", "FIX FIRST"}, {"findings", {finding({{"contests", {{"id", "w1"}, {"why", "w"}}}})}}, {"notes", Json::array()}, + {"coverage", {{{"item", ids[0]}, {"verdict", "defect"}, {"basis", "b"}}}}}, d); + Check("contest of an unsettled entry blocks", n6.at("verdict") == "FIX FIRST" && !n6.at("contests").empty() && n6.at("contests").at(0).at("outcome").get().contains("CONTESTED")); + const std::vector d2 = {{{"id", "w1"}, {"kind", "waiver-owner"}, {"hash", "h"}, {"reaffirmed", {{{"hash", "h"}}}}}}; + const Json n7 = run({{"verdict", "FIX FIRST"}, {"findings", {finding({{"contests", {{"id", "w1"}, {"why", "w"}}}})}}, {"notes", Json::array()}, + {"coverage", {{{"item", ids[0]}, {"verdict", "clear"}, {"basis", "b"}}}}}, d2); + Check("contest of a reaffirmed entry is recorded, finding discharged (silent veto logged)", n7.at("verdict") == "SEND-READY" && n7.at("findings").at(0).at("discharged") == true + && AnyKind(n7.at("contractViolations"), "silent veto")); + const Json n8 = run({{"verdict", "SEND-READY"}, {"findings", Json::array()}, + {"notes", {{{"kind", "unverified"}, {"note", "x"}, {"evidence", "e"}, {"action", "a"}, {"items", {ids[0]}}}}}, + {"coverage", {{{"item", ids[0]}, {"verdict", "n/a"}, {"basis", "unverified: login failed"}}}}}, {}, Plan::FunctionalLens); + Check("functional: n/a with an unverified note is a pass of the contract (not clear, not a hole in the pass)", n8.at("verdict") == "SEND-READY" && n8.at("coverageMissing").empty() + && std::ranges::contains(Schema::NoteKinds, std::string_view("unverified")) && !std::ranges::contains(Schema::NoteKinds, std::string_view("disposition-review"))); + return Failures ? 1 : 0; +} diff --git a/tests/Stream/main.cpp b/tests/Stream/main.cpp new file mode 100644 index 0000000..2616310 --- /dev/null +++ b/tests/Stream/main.cpp @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// The claude stream: its envelopes. + +import std; +import Crafter.Review; +namespace fs = std::filesystem; +using namespace Crafter::Review; + +namespace { + std::int32_t Failures = 0; + + void Check(std::string_view name, bool cond, std::string_view detail = {}) { + std::println("{}{}{}", cond ? " ok " : " FAIL ", name, !cond && !detail.empty() ? std::format(" ({})", detail) : std::string()); + if (!cond) ++Failures; + } + + // The result line mutated, or dropped when the mutation returns nullopt. + std::string Variant(const std::string& text, const std::function(Json)>& mutate) { + std::vector out; + for (const std::string& line : Dyn::SplitLines(text)) { + if (line.starts_with('{') && Dyn::Replace(line, " ", "").contains("\"type\":\"result\"")) { + const std::optional changed = mutate(Json::parse(line)); + if (!changed) continue; + out.push_back(DumpSpaced(*changed)); + } else { + out.push_back(line); + } + } + return Dyn::Join(out, "\n"); + } + + std::function(Json)> Failed(const Json& update) { + return [update](Json o) -> std::optional { + o.update(update); + o.erase("structured_output"); + return o; + }; + } + + std::string Status(const std::string& text, std::optional code, std::string_view err = {}) { + return Stream::Classify(Stream::ParseStream(text), code, err).first; + } +} + +int main() { + std::println("stream envelopes"); + const std::string text = Dyn::ReadText(fs::current_path() / "tests" / "Goldens" / "fixture" / "stream-success.jsonl"); + const Json p = Stream::ParseStream(text); + Check("success: classify ok", Stream::Classify(p, 0, "") == std::pair{"ok", ""}); + const std::optional so = Stream::StructuredOutput(p.at("result")); + Check("success: structured_output parsed", so && so->at("verdict") == "SEND-READY" && so->at("coverage").at(0).at("item") == "R1"); + Check("success: init parsed", Dyn::Truthy(p.at("init")) && Dyn::Get(p.at("init"), "mcp_servers") == Json::array()); + Check("success: usage from the result line + firstTurn", p.at("turns") == 2 && p.at("firstTurn").at("cache_creation_input_tokens").get() > 0 + && p.at("usage").at("output").get() > 0 && !p.at("costUsd").is_null() && p.at("denials") == Json::array()); + const Json w = Stream::Windows(p.at("rateLimit")); + Check("success: rate windows as percent", w.contains("five_hour") && w.at("five_hour").at("utilization").get() > 0 && w.at("five_hour").at("utilization").get() < 100 + && Dyn::Truthy(w.at("seven_day").at("resetsAt")), DumpCompactSorted(w)); + Check("error_max_turns -> error", Status(Variant(text, Failed({{"subtype", "error_max_turns"}, {"is_error", true}, {"result", "Reached max turns (1)"}})), 1) == "error"); + Check("429 -> limit", Status(Variant(text, Failed({{"subtype", "error_during_execution"}, {"is_error", true}, {"api_error_status", 429}, {"result", "API Error: 429 rate_limit_error"}})), 1) == "limit"); + Check("401 -> auth", Status(Variant(text, Failed({{"subtype", "error_during_execution"}, {"is_error", true}, {"api_error_status", 401}, {"result", "API Error: 401 authentication_error"}})), 1) == "auth"); + const std::string noResult = Variant(text, [](Json) -> std::optional { return std::nullopt; }); + Check("403 in stderr, no result -> auth", Status(noResult, 1, "Error: 403 Forbidden") == "auth"); + Check("no result line -> error", Status(noResult, 1) == "error"); + Check("no result line + usage limit text -> limit", Status(noResult, 1, "You're out of extra usage") == "limit"); + Check("timeout flag / exit 124 -> timeout", Stream::Classify(p, std::nullopt, "", true).first == "timeout" && Stream::Classify(p, 124, "").first == "timeout"); + const std::string doubled = Variant(text, [](Json o) -> std::optional { + o["structured_output"] = DumpSpaced(o.at("structured_output")); + return o; + }); + Check("double-encoded structured_output decoded", Stream::StructuredOutput(Stream::ParseStream(doubled).at("result")) == so); + const std::string inResult = Variant(text, [](Json o) -> std::optional { + const Json inner = o.at("structured_output"); + o.erase("structured_output"); + o["result"] = DumpSpaced(inner); + return o; + }); + Check("structured output inside result text decoded", Stream::StructuredOutput(Stream::ParseStream(inResult).at("result")) == so); + const std::string noFindings = Variant(text, [](Json o) -> std::optional { + o["structured_output"] = {{"verdict", "SEND-READY"}}; + return o; + }); + Check("missing findings array -> error (no structured output)", Status(noFindings, 0) == "error"); + Check("credit balance too low -> credits", Status(Variant(text, Failed({{"subtype", "error_during_execution"}, {"is_error", true}, {"api_error_status", 400}, + {"result", "API Error: 400 Your credit balance is too low to access the Anthropic API."}})), 1) == "credits"); + + // The helper's spelling is a SQL call wherever it stands, and a stream + // line is cut at \n only: a raw U+2028 in a JSON string stays inside its + // line. + Check("this binary's helper is a SQL call, alone, after a path, in a compound command; another verb is not", + Stream::IsSqlCall("/app/bin/crafter-review sql <<'SQL'\nselect 1;\nSQL") && Stream::IsSqlCall("cd . && crafter-review sql 'select 1'") + && Stream::IsSqlCall("/x/bin/crafter.review-exe-v/crafter-review sql") && !Stream::IsSqlCall("/app/bin/crafter-review sqlx") && !Stream::IsSqlCall("/app/bin/crafter-review totp f.json")); + const std::string split = "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false,\"result\":\"a\u2028b\",\"structured_output\":{\"verdict\":\"SEND-READY\",\"findings\":[],\"notes\":[],\"coverage\":[]}}"; + Check("a result line with a raw U+2028 in a string is still the result line", Stream::Classify(Stream::ParseStream(split), 0, "").first == "ok"); + return Failures ? 1 : 0; +}