feat(modes): the kernel modes - a patch series and a mailing-list reply
Beside the push gates, crafter-review runs the pre-send review of a kernel contributor's workbench: --mode series over a format-patch export with its cover letter, --mode reply over a mail draft. The inventory partitions a cover letter, a plain mail and the camera review's shape; ten kernel lenses join the table with their gates (structure, code, artifact truth) and the persona gate - one maintainer-persona instance per --reviewers entry, its whole pass gating its blocks and pruning the units that are not theirs. A series' code units are keyed on the code like the push gates'; every text unit on its text, the references it cites and the anchor when it is dated. The references an artifact cites are prefetched through utilities/lore.py into the gate-state, with a digest in the run layer, and a persona's calibration map is its lore listing. The kernel schema carries the persona vocabulary, the kernel note kinds, the predicted review and the not-mine list; the matrix the persona holes, the thin and the modelled units; gates clean without a persona pass is GATES CLEAN. A reply's gate-state sits beside the draft and is read once; a kernel mode needs --subsystem, defaults the effort to max and the trees to work/linux,work/milos-linux, and excuses no unit from the holes. The kernel lenses and the common text ship verbatim; the table lists them after the push gates' lenses and the config template holds them off, so a repository that never turns them on changes nothing. Prompts, keys and plans were checked byte for byte against the pipeline these modes replace, on its own artifacts: two replies and a series, every system prompt, user prompt, job and reuse key identical.
This commit is contained in:
parent
4fc15fe1c2
commit
339f4b9b40
46 changed files with 5732 additions and 362 deletions
|
|
@ -13,15 +13,21 @@ namespace fs = std::filesystem;
|
|||
|
||||
namespace Crafter::Review::Inventory {
|
||||
namespace {
|
||||
constexpr std::string_view Usage = R"(Partition a frozen commit range (a `git format-patch` export) into review units for the coverage contract.
|
||||
constexpr std::string_view Usage = R"(Partition a frozen artifact into review units for the coverage contract.
|
||||
|
||||
The inventory is MECHANICAL: no agent decides what the units are. Every non-blank line of the artifact belongs to
|
||||
exactly one unit, and the tool fails if that invariant does not hold. Fixed slots are added so that what is NOT in
|
||||
the text still has a row in the coverage matrix.
|
||||
|
||||
crafter-review inventory commits <dir-with-*.patch> > gate-state/inventory-<hash8>.json
|
||||
crafter-review inventory reply <draft.txt> > gate-state/inventory-<hash8>.json
|
||||
crafter-review inventory series <dir-with-*.patch> > gate-state/inventory-<hash8>.json
|
||||
crafter-review inventory commits <dir-with-*.patch> > gate-state/inventory-<hash8>.json (own-repo push: git format-patch of a range)
|
||||
|
||||
Units (ids):
|
||||
Reply mode units: nl:intro, nl:<file>, anchor, RN, <sec>:N, <sec>:Na, x:N, refs, whole:* - or, for a plain mail
|
||||
without the dated anchor: hdr, quote:N, para:N, trailers, whole:*. Series mode: cover:header, cover:N and the
|
||||
units below (every hunk whole), whole:* from the series slots.
|
||||
|
||||
Commits mode units (ids):
|
||||
pK:subject commit K's subject
|
||||
pK:msg commit K's message body (before the trailers) - only when non-empty
|
||||
pK:trailers commit K's trailer block - only when non-empty
|
||||
|
|
@ -79,6 +85,74 @@ Units (ids):
|
|||
return {{"id", std::move(id)}, {"lines", {first, last}}, {"excerpt", std::move(excerpt)}};
|
||||
}
|
||||
|
||||
// ^On .+ wrote:$ on a stripped line: an attribution.
|
||||
bool IsAttribution(std::string_view stripped) {
|
||||
return stripped.starts_with("On ") && stripped.ends_with(" wrote:") && stripped.size() > 10 && !stripped.contains('\n');
|
||||
}
|
||||
|
||||
// ^[A-Z][A-Za-z-]+: \S : a trailer line of a mail (Tested-by: ...).
|
||||
bool IsMailTrailer(std::string_view line) {
|
||||
if (line.empty() || line[0] < 'A' || line[0] > 'Z') return false;
|
||||
std::size_t at = 1;
|
||||
while (at < line.size() && ((line[at] >= 'A' && line[at] <= 'Z') || (line[at] >= 'a' && line[at] <= 'z') || line[at] == '-')) ++at;
|
||||
if (at < 2 || !line.substr(at).starts_with(": ")) return false;
|
||||
return at + 2 < line.size() && !Dyn::IsSpace(Dyn::CodePointAt(line, at + 2).first);
|
||||
}
|
||||
|
||||
// ^[\w.-]+/\d{4}-[^\s]+\.patch$ : a bare patch path, the header of a
|
||||
// per-file group of the camera review.
|
||||
bool IsPatchPath(std::string_view text) {
|
||||
std::size_t at = 0;
|
||||
std::size_t head = 0;
|
||||
while (at < text.size()) {
|
||||
const auto [c, used] = Dyn::CodePointAt(text, at);
|
||||
if (!(Dyn::IsWord(c) || c == '.' || c == '-')) break;
|
||||
at += used;
|
||||
++head;
|
||||
}
|
||||
if (head == 0 || at >= text.size() || text[at] != '/') return false;
|
||||
++at;
|
||||
if (Dyn::DecimalRun(text, at) != 4) return false;
|
||||
at += 4;
|
||||
if (at >= text.size() || text[at] != '-') return false;
|
||||
++at;
|
||||
const std::size_t rest = Dyn::NonSpaceRun(text, at);
|
||||
if (rest <= 6 || at + rest != text.size()) return false;
|
||||
return text.ends_with(".patch");
|
||||
}
|
||||
|
||||
// ^R\d+\. and ^\d+\. : a reversal or a numbered item; the id is the
|
||||
// text before the dot.
|
||||
std::optional<std::string> NumberedHead(std::string_view line, bool reversal) {
|
||||
std::size_t at = 0;
|
||||
if (reversal) {
|
||||
if (!line.starts_with('R')) return std::nullopt;
|
||||
at = 1;
|
||||
}
|
||||
const std::size_t digits = Dyn::DecimalRun(line, at);
|
||||
if (digits == 0 || !line.substr(at + digits).starts_with(". ")) return std::nullopt;
|
||||
return std::string(line.substr(0, at + digits));
|
||||
}
|
||||
|
||||
// ^\s+[a-z]\. \S : a lettered sub-item; the letter.
|
||||
std::optional<char> LetteredHead(std::string_view line) {
|
||||
const std::size_t spaces = Dyn::SpaceRun(line, 0);
|
||||
if (spaces == 0 || spaces + 3 >= line.size()) return std::nullopt;
|
||||
const char letter = line[spaces];
|
||||
if (letter < 'a' || letter > 'z' || !line.substr(spaces + 1).starts_with(". ")) return std::nullopt;
|
||||
if (Dyn::IsSpace(Dyn::CodePointAt(line, spaces + 3).first)) return std::nullopt;
|
||||
return letter;
|
||||
}
|
||||
|
||||
// ^\[1\] https?://
|
||||
bool IsFirstReference(std::string_view line) {
|
||||
return line.starts_with("[1] http://") || line.starts_with("[1] https://");
|
||||
}
|
||||
|
||||
std::span<const std::string> Range(const std::vector<std::string>& lines, std::size_t from, std::size_t to) {
|
||||
return std::span<const std::string>(lines).subspan(from, to - from);
|
||||
}
|
||||
|
||||
std::string ReprInts(std::span<const std::int64_t> values) {
|
||||
std::string out = "[";
|
||||
for (std::size_t i = 0; i < values.size(); ++i) out += std::format("{}{}", i ? ", " : "", values[i]);
|
||||
|
|
@ -119,7 +193,7 @@ Units (ids):
|
|||
return "";
|
||||
}
|
||||
|
||||
Series PartitionSeries(const fs::path& dir, bool splitDocs) {
|
||||
Series PartitionSeries(const fs::path& dir, bool splitDocs, bool kernel) {
|
||||
Series series;
|
||||
series.units = Json::array();
|
||||
Json& units = series.units;
|
||||
|
|
@ -129,7 +203,8 @@ Units (ids):
|
|||
const std::vector<std::string> lines = Dyn::Split(text, "\n");
|
||||
const auto count = static_cast<std::int64_t>(lines.size());
|
||||
const std::optional<std::int64_t> number = Artifact::PatchNumber(name);
|
||||
const std::string pid = std::format("p{}", number ? *number : -1);
|
||||
const std::int64_t k = number ? *number : -1;
|
||||
const std::string pid = k == 0 ? std::string("cover") : std::format("p{}", k);
|
||||
const std::int64_t base = offset;
|
||||
std::optional<std::int64_t> subject;
|
||||
for (std::int64_t i = 0; i < count; ++i) {
|
||||
|
|
@ -155,6 +230,26 @@ Units (ids):
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (pid == "cover") {
|
||||
// The header as one unit, the paragraphs of the body.
|
||||
units.push_back(Unit("cover:header", base + 1, base + body0, subject ? Dyn::Slice(Dyn::Strip(lines[static_cast<std::size_t>(*subject)]), 0, 90) : std::string()));
|
||||
std::int64_t i = body0;
|
||||
std::int64_t pn = 0;
|
||||
while (i < count) {
|
||||
if (Dyn::Strip(lines[static_cast<std::size_t>(i)]).empty()) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
std::int64_t j = i;
|
||||
while (j < count && !Dyn::Strip(lines[static_cast<std::size_t>(j)]).empty()) ++j;
|
||||
++pn;
|
||||
units.push_back(Unit(std::format("cover:{}", pn), base + i + 1, base + j, Excerpt(Lines(lines, i, j))));
|
||||
i = j;
|
||||
}
|
||||
series.lines.insert(series.lines.end(), lines.begin(), lines.end());
|
||||
offset += count;
|
||||
continue;
|
||||
}
|
||||
units.push_back(Unit(std::format("{}:subject", pid), base + 1, base + body0,
|
||||
subject ? Dyn::Slice(Dyn::Strip(lines[static_cast<std::size_t>(*subject)]), 0, 90) : std::string()));
|
||||
std::int64_t t = diff0;
|
||||
|
|
@ -216,19 +311,189 @@ Units (ids):
|
|||
++i;
|
||||
}
|
||||
closeHunk(count);
|
||||
units.push_back(Unit(std::format("{}:outside", pid), 0, 0, "FIXED SLOT: in-repository users/callers of what this commit changes, outside its diff"));
|
||||
units.push_back(Unit(std::format("{}:absent", pid), 0, 0, "FIXED SLOT: what a reviewer expects in this commit and does not find"));
|
||||
units.push_back(Unit(std::format("{}:outside", pid), 0, 0,
|
||||
kernel ? "FIXED SLOT: in-tree users/callers of what this patch changes, outside its diff" : "FIXED SLOT: in-repository users/callers of what this commit changes, outside its diff"));
|
||||
units.push_back(Unit(std::format("{}:absent", pid), 0, 0,
|
||||
kernel ? "FIXED SLOT: what a reviewer expects in this patch and does not find" : "FIXED SLOT: what a reviewer expects in this commit and does not find"));
|
||||
series.lines.insert(series.lines.end(), lines.begin(), lines.end());
|
||||
offset += count;
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
Series PartitionMail(std::string_view text) {
|
||||
Series out;
|
||||
out.lines = Dyn::Split(text, "\n");
|
||||
out.units = Json::array();
|
||||
const std::vector<std::string>& lines = out.lines;
|
||||
const std::size_t n = lines.size();
|
||||
std::size_t hdrEnd = n;
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
if (Dyn::Strip(lines[i]).empty()) {
|
||||
hdrEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hdrEnd > 0) out.units.push_back(Unit("hdr", 1, static_cast<std::int64_t>(hdrEnd), Excerpt(Range(lines, 0, hdrEnd))));
|
||||
std::size_t i = hdrEnd;
|
||||
std::int64_t q = 0;
|
||||
std::int64_t pn = 0;
|
||||
std::int64_t tr = 0;
|
||||
while (i < n) {
|
||||
if (Dyn::Strip(lines[i]).empty()) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
std::size_t j = i;
|
||||
std::string uid;
|
||||
if (lines[i].starts_with('>') || IsAttribution(Dyn::Strip(lines[i]))) {
|
||||
// A quoted block: the attribution line (if any) and every
|
||||
// following quoted line; a blank line inside the quote is
|
||||
// part of it only when a quoted line follows.
|
||||
while (j < n && (lines[j].starts_with('>') || IsAttribution(Dyn::Strip(lines[j])) || (Dyn::Strip(lines[j]).empty() && j + 1 < n && lines[j + 1].starts_with('>')))) ++j;
|
||||
++q;
|
||||
uid = std::format("quote:{}", q);
|
||||
} else {
|
||||
while (j < n && !Dyn::Strip(lines[j]).empty() && !lines[j].starts_with('>')) ++j;
|
||||
if (std::all_of(lines.begin() + static_cast<std::ptrdiff_t>(i), lines.begin() + static_cast<std::ptrdiff_t>(j), [](const std::string& l) { return IsMailTrailer(l); })) {
|
||||
++tr;
|
||||
uid = tr == 1 ? std::string("trailers") : std::format("trailers:{}", tr);
|
||||
} else {
|
||||
++pn;
|
||||
uid = std::format("para:{}", pn);
|
||||
}
|
||||
}
|
||||
out.units.push_back(Unit(uid, static_cast<std::int64_t>(i) + 1, static_cast<std::int64_t>(j), Excerpt(Range(lines, i, j))));
|
||||
i = j;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Series PartitionReply(std::string_view text) {
|
||||
Series out;
|
||||
out.lines = Dyn::Split(text, "\n");
|
||||
out.units = Json::array();
|
||||
const std::vector<std::string>& lines = out.lines;
|
||||
const std::size_t n = lines.size();
|
||||
// The block starts at the dated anchor line; a mail without one is a
|
||||
// plain reply.
|
||||
std::optional<std::size_t> anchor;
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
if (lines[i].starts_with("Dates: every")) {
|
||||
anchor = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!anchor) return PartitionMail(text);
|
||||
const std::size_t blk = *anchor;
|
||||
// The author's part: paragraphs grouped by the patch file they
|
||||
// discuss - a group starts at a paragraph that is a bare path ending
|
||||
// in .patch; everything before the first such header is nl:intro.
|
||||
struct Group {
|
||||
std::string id;
|
||||
std::size_t start;
|
||||
std::size_t end;
|
||||
};
|
||||
std::vector<Group> groups;
|
||||
std::size_t i = 0;
|
||||
while (i < blk) {
|
||||
if (Dyn::Strip(lines[i]).empty()) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
std::size_t j = i;
|
||||
while (j < blk && !Dyn::Strip(lines[j]).empty()) ++j;
|
||||
const std::string first = Dyn::Strip(lines[i]);
|
||||
if (IsPatchPath(first) && j - i == 1) {
|
||||
groups.push_back({std::format("nl:{}", first), i, j});
|
||||
} else if (!groups.empty()) {
|
||||
groups.back().end = j;
|
||||
} else {
|
||||
groups.push_back({"nl:intro", i, j});
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
for (const Group& g : groups) out.units.push_back(Unit(g.id, static_cast<std::int64_t>(g.start) + 1, static_cast<std::int64_t>(g.end), Excerpt(Range(lines, g.start, g.end))));
|
||||
// The anchor paragraph.
|
||||
std::size_t j = blk;
|
||||
while (j < n && !Dyn::Strip(lines[j]).empty()) ++j;
|
||||
out.units.push_back(Unit("anchor", static_cast<std::int64_t>(blk) + 1, static_cast<std::int64_t>(j), Excerpt(Range(lines, blk, j))));
|
||||
// The rest of the block: reversals, sections, items, sub-items,
|
||||
// bullets, refs.
|
||||
std::optional<std::size_t> refsStart;
|
||||
for (std::size_t r = 0; r < n; ++r) {
|
||||
if (IsFirstReference(lines[r])) {
|
||||
refsStart = r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!refsStart) throw InvariantError("StopIteration");
|
||||
static const std::map<std::string, std::string> Sections = {{"camss-milos", "camss"}, {"ov13b10-of", "ov13b10"}, {"milos-dts-camss", "dts"}, {"across the three series", "x"}};
|
||||
std::optional<std::string> sec;
|
||||
std::optional<std::string> item;
|
||||
std::int64_t bullet = 0;
|
||||
std::vector<std::pair<std::size_t, std::string>> starts;
|
||||
i = j;
|
||||
while (i < *refsStart) {
|
||||
const std::string& l = lines[i];
|
||||
const std::string s = Dyn::Strip(l);
|
||||
if (s.empty()) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
const std::string low = Dyn::Lower(s);
|
||||
const bool indented = l.starts_with(' ');
|
||||
if (Sections.contains(low) && !indented) {
|
||||
sec = Sections.at(low);
|
||||
item.reset();
|
||||
bullet = 0;
|
||||
starts.emplace_back(i, std::format("{}:header", *sec));
|
||||
} else if (const std::optional<std::string> r = NumberedHead(l, true)) {
|
||||
starts.emplace_back(i, *r);
|
||||
} else if (l.starts_with("Reversals of the")) {
|
||||
starts.emplace_back(i, "reversals:header");
|
||||
} else if (const std::optional<std::string> num = NumberedHead(l, false); num && sec) {
|
||||
item = *num;
|
||||
starts.emplace_back(i, std::format("{}:{}", *sec, *item));
|
||||
} else if (const std::optional<char> letter = LetteredHead(l); letter && sec && item) {
|
||||
starts.emplace_back(i, std::format("{}:{}{}", *sec, *item, *letter));
|
||||
} else if (l.starts_with("- ") && sec == "x") {
|
||||
++bullet;
|
||||
starts.emplace_back(i, std::format("x:{}", bullet));
|
||||
} else if (!indented && starts.empty()) {
|
||||
starts.emplace_back(i, std::format("para:{}", i + 1));
|
||||
} else if (!indented && !NumberedHead(l, false) && !NumberedHead(l, true) && Dyn::Strip(lines[i - 1]).empty() && !sec) {
|
||||
starts.emplace_back(i, std::format("para:{}", i + 1));
|
||||
}
|
||||
++i;
|
||||
}
|
||||
for (std::size_t k = 0; k < starts.size(); ++k) {
|
||||
const auto& [a, uid] = starts[k];
|
||||
std::size_t b = k + 1 < starts.size() ? starts[k + 1].first : *refsStart;
|
||||
while (b > a && Dyn::Strip(lines[b - 1]).empty()) --b;
|
||||
out.units.push_back(Unit(uid, static_cast<std::int64_t>(a) + 1, static_cast<std::int64_t>(b), Excerpt(Range(lines, a, b))));
|
||||
}
|
||||
std::size_t b = n;
|
||||
while (b > *refsStart && Dyn::Strip(lines[b - 1]).empty()) --b;
|
||||
out.units.push_back(Unit("refs", static_cast<std::int64_t>(*refsStart) + 1, static_cast<std::int64_t>(b), Excerpt(Range(lines, *refsStart, b))));
|
||||
return out;
|
||||
}
|
||||
|
||||
Built Build(std::string_view mode, std::string_view artifact) {
|
||||
if (mode != "commits") throw InvariantError(std::format("inventory: mode {} is not supported here (commits only)", Dyn::Repr(mode)));
|
||||
Series series = PartitionSeries(fs::path(artifact), true);
|
||||
if (mode != "reply" && mode != "series" && mode != "commits") throw InvariantError(std::format("inventory: mode {} is not reply, series or commits", Dyn::Repr(mode)));
|
||||
Series series;
|
||||
std::string h8;
|
||||
std::span<const WholeSlot> whole;
|
||||
if (mode == "reply") {
|
||||
series = PartitionReply(Dyn::ReadText(fs::path(artifact)));
|
||||
h8 = Artifact::Sha256File(fs::path(artifact)).substr(0, 8);
|
||||
whole = WholeReply;
|
||||
} else {
|
||||
series = PartitionSeries(fs::path(artifact), mode == "commits", mode == "series");
|
||||
h8 = Artifact::ArtifactHash("commits", fs::path(artifact)).substr(0, 8);
|
||||
whole = mode == "commits" ? std::span<const WholeSlot>(WholeCommits) : std::span<const WholeSlot>(WholeSeries);
|
||||
}
|
||||
Json& units = series.units;
|
||||
const std::string h8 = Artifact::ArtifactHash("commits", fs::path(artifact)).substr(0, 8);
|
||||
std::map<std::int64_t, std::string> owner;
|
||||
for (const Json& unit : units) {
|
||||
const std::int64_t a = Dyn::At(unit, "lines")[0].get<std::int64_t>();
|
||||
|
|
@ -251,7 +516,7 @@ Units (ids):
|
|||
const std::span<const std::int64_t> first(uncovered.data(), std::min<std::size_t>(uncovered.size(), 10));
|
||||
throw InvariantError(std::format("INVARIANT: {} non-blank lines in no unit, first: {}", uncovered.size(), ReprInts(first)));
|
||||
}
|
||||
for (const WholeSlot& slot : WholeCommits) units.push_back(Unit(std::string(slot.id), 0, 0, std::format("FIXED SLOT: {}", slot.description)));
|
||||
for (const WholeSlot& slot : whole) units.push_back(Unit(std::string(slot.id), 0, 0, std::format("FIXED SLOT: {}", slot.description)));
|
||||
std::map<std::string, std::int32_t> seen;
|
||||
for (const Json& unit : units) ++seen[Dyn::At(unit, "id").get<std::string>()];
|
||||
Json duplicates = Json::array();
|
||||
|
|
@ -274,7 +539,7 @@ Units (ids):
|
|||
}
|
||||
|
||||
std::int32_t Main(std::span<const std::string_view> args) {
|
||||
if (args.size() != 2 || args[0] != "commits") {
|
||||
if (args.size() != 2 || (args[0] != "reply" && args[0] != "series" && args[0] != "commits")) {
|
||||
std::print(std::cerr, "{}", Usage);
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue