The tool source generated from the schema, the home with its view, the shared and per-process environments, the permission rules, the commands, the warm-up under the supervisor, the event stream and the classification, its patterns as hand-written scanners. The goldens record the answer for each, a warm-up against a fake opencode included. A timed-out supervised command now gets SIGTERM, so the supervisor takes its tree down (SIGKILL 10 s later); any other child gets SIGKILL. :Json gains DumpCompact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
5.4 KiB
C++
124 lines
5.4 KiB
C++
// 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);
|
|
}
|
|
|
|
// No whitespace, ASCII only, the keys in their order. What the MCP config
|
|
// and OpenCode's inline agent config go to their programs as.
|
|
inline std::string DumpCompact(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);
|
|
}
|
|
|
|
// What another program wrote — a line of the claude stream, a shared
|
|
// file — read leniently: an escaped lone surrogate ("\ud800" with no low
|
|
// half after it), which nlohmann refuses, is read as U+FFFD.
|
|
inline Json ParseLoose(std::string_view text) {
|
|
try {
|
|
return Json::parse(text);
|
|
} catch (const nlohmann::json::parse_error&) {
|
|
auto hexAt = [&](std::size_t at) -> std::int32_t {
|
|
if (at + 4 > text.size()) return -1;
|
|
std::int32_t value = 0;
|
|
for (std::size_t i = at; i < at + 4; ++i) {
|
|
const char c = text[i];
|
|
const std::int32_t digit = c >= '0' && c <= '9' ? c - '0' : (c >= 'a' && c <= 'f' ? c - 'a' + 10 : (c >= 'A' && c <= 'F' ? c - 'A' + 10 : -1));
|
|
if (digit < 0) return -1;
|
|
value = value * 16 + digit;
|
|
}
|
|
return value;
|
|
};
|
|
std::string fixed;
|
|
bool changed = false;
|
|
for (std::size_t at = 0; at < text.size();) {
|
|
if (text[at] != '\\' || at + 1 >= text.size()) {
|
|
fixed.push_back(text[at++]);
|
|
continue;
|
|
}
|
|
if (text[at + 1] != 'u') {
|
|
fixed.append(text.substr(at, 2));
|
|
at += 2;
|
|
continue;
|
|
}
|
|
const std::int32_t unit = hexAt(at + 2);
|
|
const bool high = unit >= 0xD800 && unit <= 0xDBFF;
|
|
const bool low = unit >= 0xDC00 && unit <= 0xDFFF;
|
|
if (high && at + 12 <= text.size() && text[at + 6] == '\\' && text[at + 7] == 'u' && hexAt(at + 8) >= 0xDC00 && hexAt(at + 8) <= 0xDFFF) {
|
|
fixed.append(text.substr(at, 12));
|
|
at += 12;
|
|
} else if (high || low) {
|
|
fixed += "\\ufffd";
|
|
changed = true;
|
|
at += 6;
|
|
} else {
|
|
fixed.append(text.substr(at, 2));
|
|
at += 2;
|
|
}
|
|
}
|
|
if (!changed) throw;
|
|
return Json::parse(fixed);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|