// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module; #include "../lib/json.hpp" export module Crafter.Review:Json; import std; export namespace Crafter::Review { // nlohmann/json, vendored unmodified at lib/json.hpp. ordered_json keeps an // object's keys in the order they were read or inserted, and a duplicate // key keeps its first position and its last value. That is what lets a // driver rewrite the files every driver on the machine shares — the seat // board, the ledger, unit-state.json — with every key in place, the ones // it does not know about included. using Json = nlohmann::ordered_json; // Indented by one space per level, one key or item per line, \uXXXX // escapes for everything outside ASCII: every file this pipeline writes. A // string that is not valid UTF-8 has its bad bytes replaced rather than // throwing in the middle of a write. // // A float from 1e15 up to 1e16 prints with an exponent (AGENTS.md). // Readers parse it either way, and none of the files depends on it. inline std::string DumpFile(const Json& value) { return value.dump(1, ' ', true, Json::error_handler_t::replace); } // Keys in code-point order, no whitespace, ASCII only. The // structured-output schema goes to claude in this form and its bytes are // part of the prompt cache, so they must not move by a byte (the goldens // hold them); dedupe keys use it too. inline std::string DumpCompactSorted(const Json& value) { // Converted to nlohmann::json, which keeps objects in a std::map: that // sorts the keys by byte, and byte order is code-point order in UTF-8. const nlohmann::json sorted(value); return sorted.dump(-1, ' ', true, nlohmann::json::error_handler_t::replace); } // On one line, with ", " and ": " between items: where a reader matches // on the bytes, such as the first line of an opencode output, `{"type": // "review_gates_meta", …`, which salvage looks for. inline std::string DumpSpaced(const Json& value) { if (value.is_object()) { std::string out = "{"; bool first = true; for (const auto& [key, child] : value.items()) { if (!first) out += ", "; first = false; out += Json(key).dump(-1, ' ', true, Json::error_handler_t::replace); out += ": "; out += DumpSpaced(child); } return std::format("{}}}", out); } if (value.is_array()) { std::string out = "["; bool first = true; for (const Json& child : value) { if (!first) out += ", "; first = false; out += DumpSpaced(child); } return std::format("{}]", out); } return value.dump(-1, ' ', true, Json::error_handler_t::replace); } }