// 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 Statuses(std::span rows) { std::map out; for (const Json& row : rows) out[KeyOf(Dyn::At(row, "id"))] = Dyn::At(row, "status").get(); 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(); } std::string JoinStrings(const Json& list, std::string_view separator) { std::vector 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()); } return Dyn::Join(parts, separator); } std::size_t Len(const Json& value) { return value.is_string() ? Dyn::Length(value.get_ref()) : 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 Load(std::string_view path) { if (path.empty()) return {}; const std::optional 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 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 Classify(std::span entries, std::string_view hash, const fs::path& artifact, std::string_view mode) { std::vector 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() && Dyn::Truthy(Dyn::Get(entry, "anchors"))) { bool ok = true; std::vector 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 InForce(std::span entries, std::span rows) { const std::map status = Statuses(rows); std::vector 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 inForce, std::span 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 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 status = Statuses(rows); std::vector lines; for (const Json* entry : visible) { const Json& d = *entry; std::vector 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); } }