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.
180 lines
11 KiB
C++
180 lines
11 KiB
C++
// SPDX-License-Identifier: AGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
module Crafter.Review:Dispositions_impl;
|
|
import :Dispositions;
|
|
import :Json;
|
|
import :Text;
|
|
import :Platform;
|
|
import :Plan;
|
|
import std;
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace Crafter::Review::Dispositions {
|
|
namespace {
|
|
// A map key, as text: ids are compared by value and type.
|
|
std::string KeyOf(const Json& value) {
|
|
return DumpCompactSorted(value);
|
|
}
|
|
|
|
std::map<std::string, std::string> Statuses(std::span<const Json> rows) {
|
|
std::map<std::string, std::string> out;
|
|
for (const Json& row : rows) out[KeyOf(Dyn::At(row, "id"))] = Dyn::At(row, "status").get<std::string>();
|
|
return out;
|
|
}
|
|
|
|
// A string, or Error (TypeError): only a string is concatenated.
|
|
const std::string& Text(const Json& value) {
|
|
if (!value.is_string()) throw Dyn::Error("TypeError", std::format("can only concatenate str (not \"{}\") to str", Dyn::TypeName(value)));
|
|
return value.get_ref<const std::string&>();
|
|
}
|
|
|
|
std::string JoinStrings(const Json& list, std::string_view separator) {
|
|
std::vector<std::string> parts;
|
|
for (const Json& item : list) {
|
|
if (!item.is_string()) throw Dyn::Error("TypeError", std::format("sequence item {}: expected str instance, {} found", parts.size(), Dyn::TypeName(item)));
|
|
parts.push_back(item.get<std::string>());
|
|
}
|
|
return Dyn::Join(parts, separator);
|
|
}
|
|
|
|
std::size_t Len(const Json& value) {
|
|
return value.is_string() ? Dyn::Length(value.get_ref<const std::string&>()) : value.size();
|
|
}
|
|
|
|
constexpr std::string_view Rules =
|
|
"Rules: if you detect a finding one of these entries names, STILL REPORT IT and set dischargedBy to its id and its unit id in items:[] - detection never changes, only disposition, and Gate 4 must see it. "
|
|
"Before setting dischargedBy, VERIFY that entry's grounds against its cited evidence (the repository, the evidence paths when INPUTS names them, the running application for the functional lens). "
|
|
"If the grounds do not survive, return the finding WITHOUT dischargedBy and set contests {id, why} with the evidence - a contest is a claim and will itself be verified. "
|
|
"Record that verification inside the finding's evidence field. For a refuted entry you agree with, return a note kind=refutation-verified with its id instead of a finding. "
|
|
"An entry with no matching finding of yours: ignore it - nothing is owed for it.";
|
|
// The kernel pipeline's rules, where the owner is Gate 5 and the
|
|
// evidence is the ledger, the captures, lore and the tree.
|
|
constexpr std::string_view KernelRules =
|
|
"Rules: if you detect a finding one of these entries names, STILL REPORT IT and set dischargedBy to its id and its unit id in items:[] - detection never changes, only disposition, and Gate 5 must see it. "
|
|
"Before setting dischargedBy, VERIFY that entry's grounds against its cited evidence (the ledger, captures/, lore via utilities/lore.py, the tree). "
|
|
"If the grounds do not survive, return the finding WITHOUT dischargedBy and set contests {id, why} with the evidence - a contest is a claim and will itself be verified. "
|
|
"Record that verification inside the finding's evidence field - do NOT emit disposition-review notes (that kind belongs to the persona lens). "
|
|
"For a refuted entry you agree with, return a note kind=refutation-verified with its id instead of a finding. An entry with no matching finding of yours: ignore it - nothing is owed for it.";
|
|
constexpr std::string_view PersonaRules =
|
|
"Rules: for EVERY entry above except kind gate0, return one note kind=disposition-review with its id, wouldComment (would this reviewer comment on the item as the artifact stands?) and why. "
|
|
"If one of your own findings is the item an entry names, report it with dischargedBy set to that id and its unit id in items:[] - it does not block.";
|
|
|
|
bool IsKernelLens(std::string_view lensFile) {
|
|
return Plan::IsLens(lensFile) && Plan::LensOf(lensFile).kernel;
|
|
}
|
|
}
|
|
|
|
std::vector<Json> Load(std::string_view path) {
|
|
if (path.empty()) return {};
|
|
const std::optional<std::string> text = Platform::ReadFile(fs::path(path));
|
|
if (!text) throw Dyn::Error("FileNotFoundError", std::format("[Errno 2] No such file or directory: {}", Dyn::Repr(path)));
|
|
const Json document = Json::parse(*text);
|
|
std::vector<Json> out;
|
|
auto keep = [&](const Json& entry) {
|
|
if (Dyn::Truthy(entry) && Dyn::Truthy(Dyn::Get(entry, "id"))) out.push_back(entry);
|
|
};
|
|
if (document.is_array()) {
|
|
for (const Json& entry : document) keep(entry);
|
|
} else {
|
|
keep(document);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::vector<Json> Classify(std::span<const Json> entries, std::string_view hash, const fs::path& artifact, std::string_view mode) {
|
|
std::vector<Json> rows;
|
|
for (const Json& entry : entries) {
|
|
if (Dyn::Equal(Dyn::Get(entry, "hash"), Json(hash))) {
|
|
rows.push_back({{"id", Dyn::At(entry, "id")}, {"status", "in-force"}, {"detail", "hash match"}});
|
|
continue;
|
|
}
|
|
const Json& carry = Dyn::Get(entry, "carry");
|
|
if (mode != "reply" && carry.is_boolean() && carry.get<bool>() && Dyn::Truthy(Dyn::Get(entry, "anchors"))) {
|
|
bool ok = true;
|
|
std::vector<std::string> details;
|
|
for (const Json& anchor : Dyn::At(entry, "anchors")) {
|
|
const std::string& file = Text(Dyn::At(anchor, "file"));
|
|
const fs::path path = fs::is_directory(artifact) ? artifact / Dyn::BaseName(file) : fs::path(file);
|
|
std::string text;
|
|
try {
|
|
text = Dyn::ReadText(path);
|
|
} catch (const std::exception&) {
|
|
ok = false;
|
|
details.push_back(std::format("{}: unreadable", file));
|
|
continue;
|
|
}
|
|
const std::size_t found = Dyn::Count(text, Text(Dyn::At(anchor, "text")));
|
|
const Json& recorded = Dyn::Get(anchor, "count");
|
|
details.push_back(std::format("{}: recorded {} found {}", file, Dyn::Str(recorded), found));
|
|
if (!Dyn::Equal(Json(found), recorded)) ok = false;
|
|
}
|
|
rows.push_back({{"id", Dyn::At(entry, "id")}, {"status", ok ? "carried" : "lapsed"}, {"detail", Dyn::Join(details, "; ")}});
|
|
} else {
|
|
rows.push_back({{"id", Dyn::At(entry, "id")}, {"status", "lapsed"},
|
|
{"detail", mode == "reply" ? "hash mismatch; no carry in reply mode - re-bind the entry to this hash" : "hash mismatch; carry disabled or no anchors"}});
|
|
}
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
std::vector<Json> InForce(std::span<const Json> entries, std::span<const Json> rows) {
|
|
const std::map<std::string, std::string> status = Statuses(rows);
|
|
std::vector<Json> out;
|
|
for (const Json& entry : entries) {
|
|
const auto found = status.find(KeyOf(Dyn::At(entry, "id")));
|
|
if (found != status.end() && (found->second == "in-force" || found->second == "carried")) out.push_back(entry);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::string Block(std::span<const Json> inForce, std::span<const Json> rows, std::string_view lensFile, std::string_view hash, std::string_view subsystem) {
|
|
const bool isPersona = lensFile == Plan::PersonaLens;
|
|
const bool kernel = IsKernelLens(lensFile);
|
|
const std::string_view ownerGate = kernel ? "Gate 5" : "Gate 4";
|
|
std::vector<const Json*> visible;
|
|
for (const Json& entry : inForce) {
|
|
const Json& lenses = Dyn::Get(entry, "lenses");
|
|
if (isPersona || !Dyn::Equal(Dyn::Get(entry, "kind"), Json("refuted")) || (Dyn::Truthy(lenses) && Dyn::In(Json(lensFile), lenses))) visible.push_back(&entry);
|
|
}
|
|
const std::string head = std::format("DISPOSITIONS IN FORCE at hash {} - the register is the authority.{}", hash,
|
|
subsystem.empty() ? std::string(" Nothing outside this list discharges anything.")
|
|
: std::format(" A waiver row in journal/{}/ledger.md for your lens that is NOT listed here is LAPSED for this hash and discharges nothing.", subsystem));
|
|
if (visible.empty()) return std::format("{}\n(none)", head);
|
|
const std::map<std::string, std::string> status = Statuses(rows);
|
|
std::vector<std::string> lines;
|
|
for (const Json* entry : visible) {
|
|
const Json& d = *entry;
|
|
std::vector<std::string> anchorTexts;
|
|
if (Dyn::Has(d, "anchors")) {
|
|
for (const Json& anchor : Dyn::At(d, "anchors")) {
|
|
anchorTexts.push_back(std::format("{} x{}", Dyn::Str(Dyn::At(anchor, "file")), Dyn::Str(Dyn::Get(anchor, "count"))));
|
|
}
|
|
}
|
|
std::string anchors = Dyn::Join(anchorTexts, ", ");
|
|
if (anchors.empty()) anchors = "none";
|
|
const auto found = status.find(KeyOf(Dyn::At(d, "id")));
|
|
const bool carried = found != status.end() && found->second == "carried";
|
|
std::string tag = std::format("{} [{}", Dyn::Str(Dyn::At(d, "id")), Dyn::Str(Dyn::Get(d, "kind")));
|
|
if (Dyn::Truthy(Dyn::Get(d, "by"))) tag += std::format(", by {}", Text(Dyn::At(d, "by")));
|
|
if (Dyn::Truthy(Dyn::Get(d, "boundAt"))) tag += std::format(", bound {}", Text(Dyn::At(d, "boundAt")));
|
|
if (carried) tag += ", CARRIED to this hash";
|
|
if (Dyn::Truthy(Dyn::Get(d, "lenses"))) tag += std::format(", lenses: {}", JoinStrings(Dyn::At(d, "lenses"), " "));
|
|
tag += "]";
|
|
std::string line = std::format("- {}\n finding: {}\n grounds: {}", tag, Dyn::StrOr(Dyn::Get(d, "finding"), "(unnamed)"),
|
|
Dyn::StrOr(Dyn::Get(d, "grounds"), "(none recorded)"));
|
|
if (Dyn::Truthy(Dyn::Get(d, "supersedes"))) line += std::format("\n supersedes: {}", Text(Dyn::At(d, "supersedes")));
|
|
line += std::format("\n anchors: {}", anchors);
|
|
if (Dyn::Truthy(Dyn::Get(d, "reaffirmed"))) {
|
|
line += std::format("\n REAFFIRMED by the owner {}x - final for this binding; a further contest is recorded for {}, it does not block", Len(Dyn::At(d, "reaffirmed")), ownerGate);
|
|
}
|
|
if (Dyn::Truthy(Dyn::Get(d, "contestRejected"))) {
|
|
line += std::format("\n contest REJECTED {}x after re-checking the cited fact - a repeat contest without new evidence is recorded, it does not block",
|
|
Len(Dyn::At(d, "contestRejected")));
|
|
}
|
|
lines.push_back(std::move(line));
|
|
}
|
|
return std::format("{}\n{}\n{}", head, Dyn::Join(lines, "\n"), isPersona ? PersonaRules : kernel ? KernelRules : Rules);
|
|
}
|
|
}
|