From 0e0983f2782e61ec5f4bcd5aefd306924ce7f3a4 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 1 Nov 2026 01:31:40 +0100 Subject: [PATCH] fix(run): the dry run's re-slice reads the line prefix to its bar The re-slice self-check stripped six characters off every inlined artifact line - `0001| ` - and so failed from line 10000 on, where the number has five digits. The prefix is read to its bar now; a test freezes a 10,050-line file and dry-runs it. --- implementations/Crafter.Review-Run.cpp | 6 +++- tests/Runs/main.cpp | 50 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/implementations/Crafter.Review-Run.cpp b/implementations/Crafter.Review-Run.cpp index cf6117d..2478f86 100644 --- a/implementations/Crafter.Review-Run.cpp +++ b/implementations/Crafter.Review-Run.cpp @@ -2241,7 +2241,11 @@ namespace Crafter::Review::Run { const std::string after = runLayer.substr(at + marker.size()); const std::string body = after.substr(0, after.find("\n\nINVENTORY - ")); const std::vector artLines = Dyn::Split(body, "\n"); - for (std::size_t i = 1; i < artLines.size(); ++i) art.push_back(Dyn::Slice(artLines[i], 6)); + // The line number prefix (`0001| `, five digits from line 10000 on - the self-review of 2026-11-01 found the fixed width). + for (std::size_t i = 1; i < artLines.size(); ++i) { + const std::size_t bar = artLines[i].find("| "); + art.push_back(bar == std::string::npos ? artLines[i] : artLines[i].substr(bar + 2)); + } } for (const Json& u : units) { const Json& range = u.at("lines"); diff --git a/tests/Runs/main.cpp b/tests/Runs/main.cpp index e04cb2e..af536b7 100644 --- a/tests/Runs/main.cpp +++ b/tests/Runs/main.cpp @@ -525,6 +525,56 @@ echo "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false,\"result\" Check("stop between gates: the next gate's pool starts aborted - none of its passes runs, and the abort is the run's", ran == 0 && g.abort && g.abort->starts_with("stopped by crafter-review stop") && res.size() == 1 && !res[0].at("coverageMissing").empty(), g.abort.value_or("None")); } + // The dry run's re-slice reads the artifact back from the run layer + // by stripping the line-number prefix - `0001| `, five digits from + // line 10000 on. A fixed six characters was wrong there: the first + // artifact past 9999 lines, the pipeline's own review of 2026-11-01, + // refused itself at preflight. + { + const fs::path big = base / "big"; + fs::create_directories(big / ".review-gates"); + Write(big / ".review-gates" / "config.json", R"({"modes": {"commits": {"lenses": ["repo-bug-prover"]}}})"); + Git(big, {"init", "-q"}); + Git(big, {"config", "user.email", "me@example.com"}); + Git(big, {"config", "user.name", "Me"}); + Write(big / "README.md", "# big\n"); + Git(big, {"add", "."}); + Git(big, {"commit", "-q", "-m", "base"}); + const std::string bigBase = Dyn::Strip(Git(big, {"rev-parse", "HEAD"}).out); + std::string wide; + for (std::int32_t i = 0; i < 10'050; ++i) wide += std::format("line {}\n", i); + Write(big / "wide.txt", wide); + Git(big, {"add", "."}); + Git(big, {"commit", "-q", "-m", "add a wide file\n\nTen thousand and fifty lines."}); + const fs::path bigFake = base / "big-fake"; + Write(bigFake, "#!/bin/sh\nexit 1\n"); + fs::permissions(bigFake, fs::perms::owner_all); + const fs::path bigOut = base / "big-out"; + const Process::Captured bigFreeze = Process::Run({Platform::SelfExe().string(), "freeze", "--out", bigOut.string(), "--base", bigBase}, big, std::chrono::seconds(60), {}, Env); + Json bigRecord; + try { + bigRecord = Dyn::LoadJsonFile(bigOut / "freeze.json"); + } catch (const std::exception&) { + } + const std::string bigHash = bigRecord.is_object() ? Text(bigRecord.at("hash")) : std::string(); + std::string patchText; + std::error_code bigError; + for (fs::directory_iterator it(bigOut, bigError), end; !bigError && it != end; it.increment(bigError)) { + if (it->path().extension() == ".patch") patchText = Platform::ReadFile(it->path()).value_or(""); + } + const fs::path bigScratch = base / "big-scratch"; + const Process::Captured bigRun = Process::Run({Platform::SelfExe().string(), "run", "--artifact", bigOut.string(), "--hash", bigHash, "--base", bigBase, "--date", "2026-11-01", "--claude-bin", + bigFake.string(), "--scratch", bigScratch.string(), "--seats", "local", "--no-probe", "--dry-run"}, + big, std::chrono::seconds(120), {}, Env); + Json bigDry; + try { + bigDry = Dyn::LoadJsonFile(bigScratch / std::format("{}-run-1-dry", bigHash.substr(0, 8)) / "dry-run.json"); + } catch (const std::exception&) { + } + Check("an artifact past 9999 lines: the dry run's re-slice of the run layer's artifact matches every unit (the prefix is read to its bar, not six characters)", + bigFreeze.returnCode == 0 && std::ranges::count(patchText, '\n') > 10'000 && bigRun.returnCode == 2 && bigDry.is_object() && Dyn::Truthy(bigDry.at("dryRun").at("resliceOk")), + std::format("freeze {} run {}: {}{}", bigFreeze.returnCode, bigRun.returnCode, Dyn::Slice(bigRun.out, -600), bigRun.err)); + } fs::remove_all(base); return Failures ? 1 : 0; }