Crafter.Review/implementations/Crafter.Review-Dispositions.cpp

158 lines
8.8 KiB
C++
Raw Normal View History

feat(pipeline): the pure pipeline, checked against its goldens The modules that turn a frozen range into passes and passes into a verdict: :Schema, :Tools (the DENY, git and base lists, a leaf, so the config and the pool both read them), :Plan, :Artifact, :Inventory, :Dispositions, :State, :Normalize, :Stream, :Prompt, :Config, and the URL half of :PgWire. Units, jobs, results, the config and the claude stream stay JSON from end to end, so every key keeps its place; :Text gives JSON values truthiness, get, ==, membership and Str(), and throws Dyn::Error under a name for each kind of unexpected data. Exactness where the bytes are seen: - the stream classifiers are hand-written scanners with case-insensitive folds (the Kelvin sign is a k), Unicode \b and \s, and `.` stopping at \n; the SQL call detection takes `<exe> sql` with any path before it; - config problems carry the JSON scanner's and the UTF-8 decoder's own texts, positions included; - a float total is Neumaier's compensated sum, and the build passes -ffp-contract=off: -march=native would fuse a*b+c; - `crafter-review inventory` and `config` print what the goldens record, byte for byte (the fixture and 14 configs, broken ones included); a message names this binary as the command to run. tests/Goldens: the recorded answers over the fixtures next to them (the commits fixture, a rich versioned series with CRLF, a folded subject, Markdown headings in and out of fences, the code-key rewrites); the suite compares 2304 results as DumpFile writes them, so a key out of order fails too. All agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 06:48:55 +01:00
// 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 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.";
}
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::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 (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", "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::vector<const Json*> visible;
for (const Json& entry : inForce) {
const Json& lenses = Dyn::Get(entry, "lenses");
if (!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. Nothing outside this list discharges anything.", hash);
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 Gate 4, it does not block", Len(Dyn::At(d, "reaffirmed")));
}
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"), Rules);
}
}