feat(lenses): the commit-truth lens, repo-facts, as Gate 4
Every claim in every commit message, and in the documentation the range adds or changes, proven or gone. The lens's block kinds are text, doc and slot: a message unit is keyed on its text, the base and the code up to its commit (MessageDeps); a documentation unit on its file, its normalised text, the base and the code of the whole range (DocKeyParts); a diffstat on its text alone; whole:order and whole:absent are asked in slot passes of its own, never stored. A documentation file's diff header and a part the commit writes nothing in are a mechanical n/a. Budgets 60/40 for a text or doc block, 50/35 for a slot; one read. Gate 4, after the proofs, so the messages and documents are read once the code they describe has settled; the owner's gate is 5. The goldens that follow from the lens table are derived by tests/Goldens/derive.py, which asserts that the recorded table still reproduces the recorded entries before it writes. Proven, with the opt-in commit after this one: the suite (26 passed); a dry run of a scratch range (2 commits, a README with sections) by the previous binary and this one with the three static lenses on and the rest off, 25 prompts and 4 system files byte for byte; with repo-facts on as well, those files unchanged and 10 passes and 1 system file added. The lens's firing proof in this pipeline is owed (the owner's go).
This commit is contained in:
parent
61ee4602f4
commit
febbfc3fcc
19 changed files with 2564 additions and 646 deletions
|
|
@ -163,6 +163,16 @@ namespace Crafter::Review::Plan {
|
|||
bool Contains(std::span<const std::string> list, std::string_view item) {
|
||||
return std::ranges::contains(list, item);
|
||||
}
|
||||
|
||||
// any(l[:1] in ('+', '-') and l[1:].strip() for l in code_norm(text).split('\n')):
|
||||
// does the commit add or remove a line with something on it in this
|
||||
// part of a documentation hunk?
|
||||
bool WritesIn(std::string_view text) {
|
||||
for (const std::string& line : Dyn::Split(CodeNorm(text), "\n")) {
|
||||
if ((line.starts_with('+') || line.starts_with('-')) && !Dyn::Strip(line.substr(1)).empty()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ModelTier>& Models() {
|
||||
|
|
@ -193,6 +203,7 @@ namespace Crafter::Review::Plan {
|
|||
{"Gate 1 — Approach", {"repo-better-solution.md"}},
|
||||
{"Gate 2 — Functional", {FunctionalLens}},
|
||||
{"Gate 3 — Proof", {"repo-bug-prover.md", "repo-security.md"}},
|
||||
{"Gate 4 — Commit truth", {FactsLens}},
|
||||
};
|
||||
return gates;
|
||||
}
|
||||
|
|
@ -213,8 +224,11 @@ namespace Crafter::Review::Plan {
|
|||
|
||||
Budget BudgetFor(std::string_view kind, std::string_view unitKind, std::string_view lensFile) {
|
||||
if (kind == "whole") return lensFile == FunctionalLens ? Budget{220, 120} : Budget{180, 105};
|
||||
if (unitKind != "code") throw Dyn::Error("KeyError", Dyn::Repr(std::format("block:{}", unitKind)));
|
||||
return lensFile == FunctionalLens ? Budget{90, 60} : Budget{60, 40};
|
||||
if (kind == "slot") return {50, 35};
|
||||
if (unitKind == "code") return lensFile == FunctionalLens ? Budget{90, 60} : Budget{60, 40};
|
||||
if (unitKind == "doc") return {60, 40};
|
||||
if (unitKind == "text") return lensFile == FactsLens ? Budget{60, 40} : Budget{50, 30};
|
||||
throw Dyn::Error("KeyError", Dyn::Repr(std::format("block:{}", unitKind)));
|
||||
}
|
||||
|
||||
std::string_view DefaultEffort() {
|
||||
|
|
@ -251,8 +265,9 @@ namespace Crafter::Review::Plan {
|
|||
return path && IsDocPath(*path);
|
||||
}
|
||||
|
||||
// The lens is not consulted (yet): no lens of this pipeline reads a
|
||||
// documentation file since the facts lens was removed.
|
||||
// Every lens of this pipeline is a repo lens: the units of a documentation
|
||||
// file are "doc" for each of them - the commit-truth lens reads them, the
|
||||
// code lenses do not.
|
||||
std::string_view KindFor(std::string_view, const Json& unit) {
|
||||
const std::string_view kind = UnitKind(unit);
|
||||
return kind == "code" && IsDocUnit(Dyn::At(unit, "id").get_ref<const std::string&>()) ? "doc" : kind;
|
||||
|
|
@ -361,6 +376,38 @@ namespace Crafter::Review::Plan {
|
|||
if (!unitsFilter.empty() && !Contains(unitsFilter, id)) continue;
|
||||
const std::string text = UnitText(lines, unit);
|
||||
const std::optional<std::string> path = UnitPath(id);
|
||||
if (kind == "slot") {
|
||||
// A lens with no whole pass asks each of its own slots in a
|
||||
// pass of its own; another lens's whole slot is not its.
|
||||
if (!std::ranges::contains(lens.slots, std::string_view(id))) continue;
|
||||
const Budget budget = BudgetFor("slot", "slot", lensFile);
|
||||
plan.blocks.push_back({
|
||||
{"label", std::format("{}:slot:{}", name, id)},
|
||||
{"lens", name},
|
||||
{"lensFile", lensFile},
|
||||
{"kind", "slot"},
|
||||
{"unit", id},
|
||||
{"unitKind", "slot"},
|
||||
{"expectedIds", {id}},
|
||||
{"unitSha", Sha(text)},
|
||||
{"model", ModelFor(lens.model)},
|
||||
{"maxTurns", budget.maxTurns},
|
||||
{"timeoutMin", budget.timeoutMin},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (kind == "doc" && id.ends_with("#hdr")) {
|
||||
// The diff header of a documentation file names the file, its
|
||||
// blobs and its mode: no sentence, no claim.
|
||||
plan.blocks.push_back(Mechanical(name, lensFile, unit, text, "mechanical: diff header of a documentation file"));
|
||||
continue;
|
||||
}
|
||||
if (kind == "doc" && !WritesIn(text)) {
|
||||
// A part of a hunk that holds only context (the lines above a
|
||||
// section the commit adds): nothing was written here.
|
||||
plan.blocks.push_back(Mechanical(name, lensFile, unit, text, "mechanical: context only - the commit adds and removes no line in this part"));
|
||||
continue;
|
||||
}
|
||||
if (!lens.perCommitSlots && (id.ends_with(":outside") || id.ends_with(":absent"))) {
|
||||
// The functional lens judges adjacent flows and what is missing
|
||||
// in its whole pass, not per commit.
|
||||
|
|
@ -570,9 +617,54 @@ namespace Crafter::Review::Plan {
|
|||
std::format("functional:{}", ident));
|
||||
}
|
||||
|
||||
std::pair<std::string, std::vector<std::string>> DocKeyParts(std::string_view unitId, const CommitCode& cc) {
|
||||
const std::size_t colon = unitId.find(':');
|
||||
if (colon == std::string_view::npos) throw Dyn::Error("ValueError", "not enough values to unpack (expected 2, got 1)");
|
||||
const std::string_view rest = unitId.substr(colon + 1);
|
||||
const std::size_t hash = rest.rfind('#');
|
||||
return {std::format("doc:{}", hash == std::string_view::npos ? rest : rest.substr(0, hash)), {std::format("base={}", cc.base), std::format("range={}", cc.range)}};
|
||||
}
|
||||
|
||||
std::vector<std::string> MessageDeps(std::string_view unitId, const CommitCode& cc) {
|
||||
// ^p(\d+):(subject|msg)$; `$` also matches before a final newline.
|
||||
const std::optional<std::size_t> prefix = PkPrefix(unitId);
|
||||
if (!prefix) return {};
|
||||
std::string_view name = unitId.substr(*prefix);
|
||||
if (name.ends_with('\n')) name.remove_suffix(1);
|
||||
if (name != "subject" && name != "msg") return {};
|
||||
const std::string_view number = unitId.substr(1, *prefix - 2);
|
||||
const std::optional<std::int64_t> k = Dyn::ParseInt(number);
|
||||
if (!k) throw Dyn::Error("ValueError", Dyn::IntError(number));
|
||||
const auto upto = cc.upto.find(*k);
|
||||
if (upto == cc.upto.end()) throw Dyn::Error("KeyError", std::to_string(*k));
|
||||
return {std::format("base={}", cc.base), std::format("upto={}", upto->second)};
|
||||
}
|
||||
|
||||
std::optional<std::string> DocReuseKey(const Json& job, std::string_view lensFile, std::string_view unitText, const CommitCode& cc,
|
||||
std::span<const std::string> inForceIds, std::string_view charterSha, std::int32_t promptVersion,
|
||||
std::string_view environment) {
|
||||
auto [ident, deps] = DocKeyParts(Dyn::At(job, "unit").get_ref<const std::string&>(), cc);
|
||||
std::ranges::move(EnvDeps(lensFile, environment), std::back_inserter(deps));
|
||||
return ReuseKey(job, lensFile, CodeNorm(unitText), deps, {inForceIds.begin(), inForceIds.end()}, charterSha, promptVersion, ident);
|
||||
}
|
||||
|
||||
std::optional<std::string> TextReuseKey(const Json& job, std::string_view lensFile, std::string_view unitText, const CommitCode& cc,
|
||||
std::span<const std::string> inForceIds, std::string_view charterSha, std::int32_t promptVersion,
|
||||
std::string_view environment) {
|
||||
std::vector<std::string> deps = MessageDeps(Dyn::At(job, "unit").get_ref<const std::string&>(), cc);
|
||||
std::ranges::move(EnvDeps(lensFile, environment), std::back_inserter(deps));
|
||||
return ReuseKey(job, lensFile, unitText, deps, {inForceIds.begin(), inForceIds.end()}, charterSha, promptVersion);
|
||||
}
|
||||
|
||||
std::optional<std::string> BlockReuseKey(const Json& job, std::string_view lensFile, std::string_view unitText, const CommitCode& cc,
|
||||
std::span<const std::string> inForceIds, std::string_view charterSha, std::int32_t promptVersion,
|
||||
std::string_view environment) {
|
||||
// A slot pass and a whole pass have no block key (ReuseKey's own
|
||||
// rule, before any id is parsed: a `whole:*` id has no commit number).
|
||||
if (Dyn::At(job, "kind") != "block") return std::nullopt;
|
||||
const Json& unitKind = Dyn::Get(job, "unitKind");
|
||||
if (unitKind == "doc") return DocReuseKey(job, lensFile, unitText, cc, inForceIds, charterSha, promptVersion, environment);
|
||||
if (unitKind == "text") return TextReuseKey(job, lensFile, unitText, cc, inForceIds, charterSha, promptVersion, environment);
|
||||
if (LensOf(lensFile).key == "functional") {
|
||||
return FunctionalReuseKey(job, lensFile, unitText, cc, inForceIds, charterSha, promptVersion, environment);
|
||||
}
|
||||
|
|
@ -657,7 +749,9 @@ namespace Crafter::Review::Plan {
|
|||
const auto layer = lensLayerBytes.find(lens);
|
||||
const double lensTokens = static_cast<double>(layer == lensLayerBytes.end() ? 3000 : layer->second) / tpb;
|
||||
const double tail = static_cast<double>(tailBytes) / tpb;
|
||||
const std::int64_t turns = kind == "whole" ? 40 : (Dyn::At(job, "lensFile") == FunctionalLens ? 25 : 10);
|
||||
const Json& unitKind = Dyn::Get(job, "unitKind");
|
||||
const std::int64_t turns = kind == "whole" ? 40 : kind == "slot" ? 10 : Dyn::At(job, "lensFile") == FunctionalLens ? 25
|
||||
: unitKind == "code" ? 10 : (Dyn::At(job, "lensFile") == FactsLens ? 12 : 8);
|
||||
if (seen.insert({lens, kind}).second) cacheWrite += prefix + lensTokens;
|
||||
cacheWrite += tail + static_cast<double>(turns * 3000);
|
||||
std::vector<double> reads;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,25 @@ namespace Crafter::Review::Prompt {
|
|||
"Judge the in-repository users and callers of what this commit changes, outside its own diff - at the base (`git -C <tree> show <base>:<path>`) and at HEAD.";
|
||||
const std::string_view HintAbsent =
|
||||
"Judge what a reviewer of this repository expects in this commit and does not find: a commit body at all (a body-less commit has no pK:msg unit), a test, a version or changelog bump, a doc or packaging update, an error path, a migration for existing installs.";
|
||||
const std::string_view TextBlockHintRepo =
|
||||
"Within this unit, verify every claim against this commit's diff and the repository at HEAD and at the base; a device, field or network claim against the evidence paths INPUTS names, or report it unverifiable.";
|
||||
const std::string_view DocBlockHintRepo =
|
||||
"This unit is documentation the commit adds or changes: a diff hunk of a documentation file, or one heading-delimited part of it. Judge the lines the commit adds (\"+\"): every claim in them must hold for the repository as THIS commit leaves it "
|
||||
"(`git -C <tree> show <sha>:<path>`, the sha on this patch's `From` line) and, where the document at HEAD still carries it, at HEAD too - a later commit of the range may have made it false without updating it; a device, field or network claim against the evidence paths INPUTS names, or report it unverifiable. "
|
||||
"Context lines are context and removed lines are not claims, except a context sentence this commit's own change makes false.";
|
||||
|
||||
std::string_view SlotDuty(std::string_view lensFile, std::string_view unitId) {
|
||||
if (lensFile != Plan::FactsLens) return {};
|
||||
if (unitId == "whole:order") {
|
||||
return "; for this lens that means each message describes its own commit alone - nothing a message claims is delivered by a later commit of the range, and no message describes the state a later commit creates";
|
||||
}
|
||||
if (unitId == "whole:absent") {
|
||||
return "; for this lens that means what the range as a whole implies but no message states - a behaviour change no message mentions, a version bump without its changelog line - "
|
||||
"and documentation the range leaves stale: a sentence of the repository's README or other documentation at HEAD, written by the range or older, that the range's code makes false "
|
||||
"(a behaviour, an option, a path or a command described as it was before), or a list in it that something the range adds belongs in and is missing from";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
namespace {
|
||||
// An optional string as the prompt prints it: None when absent.
|
||||
|
|
@ -202,12 +221,14 @@ namespace Crafter::Review::Prompt {
|
|||
const std::string id = Dyn::Str(Dyn::At(u, "id"));
|
||||
if (kind == "slot") {
|
||||
const std::string description = Dyn::Replace(Text(Dyn::At(u, "excerpt")), "FIXED SLOT: ", "");
|
||||
return std::format("SCOPE - SLOT PASS, unit \"{}\": this unit has no lines; it stands for a question about the whole artifact: {}. Answer it for your lens. Coverage: exactly this one unit.\n{}",
|
||||
id, description, GenericVocab);
|
||||
return std::format("SCOPE - SLOT PASS, unit \"{}\": this unit has no lines; it stands for a question about the whole artifact: {}. Answer it for your lens{}. Coverage: exactly this one unit.\n{}",
|
||||
id, description, SlotDuty(lensFile, id), GenericVocab);
|
||||
}
|
||||
const Json& range = Dyn::At(u, "lines");
|
||||
const std::int64_t a = IntItem(range, 0);
|
||||
const std::int64_t b = IntItem(range, 1);
|
||||
const Json& unitKind = Dyn::Get(job, "unitKind");
|
||||
const bool prose = unitKind == "text" || unitKind == "doc";
|
||||
std::string_view hint;
|
||||
if (id.ends_with(":outside")) {
|
||||
hint = HintOutside;
|
||||
|
|
@ -215,10 +236,15 @@ namespace Crafter::Review::Prompt {
|
|||
hint = HintAbsent;
|
||||
} else if (lensFile == Plan::FunctionalLens) {
|
||||
hint = FunctionalBlockHint;
|
||||
} else if (unitKind == "text") {
|
||||
hint = TextBlockHintRepo;
|
||||
} else if (unitKind == "doc") {
|
||||
hint = DocBlockHintRepo;
|
||||
}
|
||||
const std::string text = Plan::UnitText(lines, u);
|
||||
const std::string scope = std::format("SCOPE - BLOCK PASS, unit \"{}\" [lines {}-{}]: judge ONLY this unit, with your full attention by your charter - a hunk against the standard - reading the rest of the artifact only as context. Do not re-litigate the approach as a whole{}. Coverage: exactly this one unit.",
|
||||
id, a, b, wholeSummary.empty() ? "" : " - the whole pass of this lens concluded as stated above");
|
||||
const std::string scope = std::format("SCOPE - BLOCK PASS, unit \"{}\" [lines {}-{}]: judge ONLY this unit, with your full attention by your charter - {} - reading the rest of the artifact only as context. Do not re-litigate the approach as a whole{}. Coverage: exactly this one unit.",
|
||||
id, a, b, prose ? "a claim at its source, a sentence against its proof obligation" : "a hunk against the standard",
|
||||
wholeSummary.empty() ? "" : " - the whole pass of this lens concluded as stated above");
|
||||
return std::format("{}{}\n\nUNIT TEXT (verbatim, lines {}-{}):\n{}\n\n{}", scope, hint.empty() ? std::string() : std::format("\n{}", hint), a, b, text, GenericVocab);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -498,8 +498,7 @@ namespace Crafter::Review::Run {
|
|||
}
|
||||
const std::vector<std::string> exclude = Strings(cfg.at("functional").at("exclude"));
|
||||
notReviewed = Plan::NotReviewed(units, lines, running, exclude);
|
||||
log(std::format("inventory: {} units ({}); {} units no lens that runs reads (text, documentation, the units of a lens the config turns off)", units.size(), Dyn::RelPath(invPath.string(), repo.string()),
|
||||
notReviewed.size()));
|
||||
log(std::format("inventory: {} units ({}); {} units no lens that runs reads", units.size(), Dyn::RelPath(invPath.string(), repo.string()), notReviewed.size()));
|
||||
if (a.units) {
|
||||
std::set<std::string> known;
|
||||
for (const Json& u : units) known.insert(Dyn::Str(u.at("id")));
|
||||
|
|
|
|||
Loading…
Reference in a new issue