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.
This commit is contained in:
Jorijn van der Graaf 2026-11-01 01:31:40 +01:00
commit 0e0983f278
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
2 changed files with 55 additions and 1 deletions

View file

@ -2241,7 +2241,11 @@ namespace Crafter::Review::Run {
const std::string after = runLayer.substr(at + marker.size()); const std::string after = runLayer.substr(at + marker.size());
const std::string body = after.substr(0, after.find("\n\nINVENTORY - ")); const std::string body = after.substr(0, after.find("\n\nINVENTORY - "));
const std::vector<std::string> artLines = Dyn::Split(body, "\n"); const std::vector<std::string> 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) { for (const Json& u : units) {
const Json& range = u.at("lines"); const Json& range = u.at("lines");

View file

@ -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", 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")); 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); fs::remove_all(base);
return Failures ? 1 : 0; return Failures ? 1 : 0;
} }