Crafter.Debug/project.cpp

234 lines
9.9 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Four configurations:
// Crafter.Debug.Core what the app and the client share (values, codecs,
// the protocol, discovery, build info)
// Crafter.Debug.Cli the client's code, a library so tests can run it
feat(core): Value, reflection, codecs and results, shared by app and client Crafter.Debug.Core is what the app side and the crafter-debug client both need, without the session: - Value: a JSON value with its own parser and printer, so no consumer interface sees a JSON library. Integers stay integers and whole doubles stay doubles; NaN and the infinities, which JSON cannot spell, are written as strings rather than hidden, because a debugger must not lose a NaN; invalid UTF-8 is replaced so output always parses. Objects keep insertion order, the order an agent reads. - Reflect: aggregate members and names through structured-binding packs and source_location, taken from Crafter.DB, plus scoped-enum names by probing a range of values. Self-tested with static_asserts so a compiler that changes function_name()'s shape fails the build. - Codec<T>: encode, decode and describe for bool, integers, floating point, strings, enums, optional, vector, array, string-keyed maps, Value, Required<T> and every aggregate. Decode reports every problem in a call with its path and, for a typo, the likely intended name, and never throws: a bad call cannot take an app down, which a wrongly typed parameter could in 3DForts' hand-written from_json. - Result, Error and Async: handler results, including ones that take frames to arrive, so callers need no ready:false polling. project.cpp now builds Crafter.Debug.Core as its own library, used by both the app library and the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
// crafter.debug-cli the client, crafter-debug
// Crafter.Debug what an app links (returned), depending on the core
// and the client
feat(core): Value, reflection, codecs and results, shared by app and client Crafter.Debug.Core is what the app side and the crafter-debug client both need, without the session: - Value: a JSON value with its own parser and printer, so no consumer interface sees a JSON library. Integers stay integers and whole doubles stay doubles; NaN and the infinities, which JSON cannot spell, are written as strings rather than hidden, because a debugger must not lose a NaN; invalid UTF-8 is replaced so output always parses. Objects keep insertion order, the order an agent reads. - Reflect: aggregate members and names through structured-binding packs and source_location, taken from Crafter.DB, plus scoped-enum names by probing a range of values. Self-tested with static_asserts so a compiler that changes function_name()'s shape fails the build. - Codec<T>: encode, decode and describe for bool, integers, floating point, strings, enums, optional, vector, array, string-keyed maps, Value, Required<T> and every aggregate. Decode reports every problem in a call with its path and, for a typo, the likely intended name, and never throws: a bad call cannot take an app down, which a wrongly typed parameter could in 3DForts' hand-written from_json. - Result, Error and Async: handler results, including ones that take frames to arrive, so callers need no ready:false polling. project.cpp now builds Crafter.Debug.Core as its own library, used by both the app library and the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
// The client is one of the library's dependencies only so that a consumer's
// debug build compiles it in the same pass as the app; an executable adds no
// link flags to what depends on it. Consumers load this file through
// Debug::Use (tooling/), which also writes their launcher.
feat(transport): serve the session over QUIC on loopback A Session with listen = true (the default) now listens on 127.0.0.1 with the ALPN crafter-debug/1 and writes its discovery record (port, token, protocol, source id, client path), which goes first when it ends. One request per stream the client opens: the request and a FIN in, zero or more stream lines and then the reply and a FIN out, one JSON object per line. It holds no thread while nothing arrives. Crafter.Network delivers a new stream on the pool, its bytes on msquic's worker through OnRecieve, and replies leave through SendAsync, which never waits. The handler, which takes the session's mutex, runs on a pool task rather than inside the stream's callback, so no msquic worker ever waits on the session, and the session's threads only send, which Network allows under any lock. Stream wrappers are let go outside every lock their callbacks take. The rules head the implementation. Limits: a request over 1 MiB is answered with an error, not buffered, and a log follower more than 8 MiB behind is reset. quit --force exits 200 ms after its reply was taken, from a thread of its own, since "sent" means msquic has the bytes, not that they left. A client killed without closing is let go at the 10 s idle timeout. The Session refuses a pool without a worker: Network hands it new streams there, and with Start(1) nothing would run them. The partition exports nothing, so an app's view of the module is unchanged and it does not see Crafter.Network's types. Crafter.Network comes from forgejo, unpinned, with the args unchanged; --local takes ../Crafter.Network, and $CRAFTER_DEBUG_NETWORK any project.cpp, for the branch the callback API is on until it lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:06:56 +01:00
//
// The library and the client talk QUIC through Crafter.Network, which comes
// from forgejo, or with --local from the checkout beside this one.
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
namespace {
// The configurations this file makes besides the one it returns, at
// stable addresses for the pointers in `dependencies`. One set per load,
// not statics: a static would keep the first load's args for every load.
std::deque<Configuration> owned;
// This file's directory. A configuration a project file makes for itself
// keeps the path it is given, and "./" would later resolve against the
// consumer's directory instead of this one.
fs::path Root() {
return fs::path(std::source_location::current().file_name()).parent_path();
}
feat(core): Value, reflection, codecs and results, shared by app and client Crafter.Debug.Core is what the app side and the crafter-debug client both need, without the session: - Value: a JSON value with its own parser and printer, so no consumer interface sees a JSON library. Integers stay integers and whole doubles stay doubles; NaN and the infinities, which JSON cannot spell, are written as strings rather than hidden, because a debugger must not lose a NaN; invalid UTF-8 is replaced so output always parses. Objects keep insertion order, the order an agent reads. - Reflect: aggregate members and names through structured-binding packs and source_location, taken from Crafter.DB, plus scoped-enum names by probing a range of values. Self-tested with static_asserts so a compiler that changes function_name()'s shape fails the build. - Codec<T>: encode, decode and describe for bool, integers, floating point, strings, enums, optional, vector, array, string-keyed maps, Value, Required<T> and every aggregate. Decode reports every problem in a call with its path and, for a typo, the likely intended name, and never throws: a bad call cannot take an app down, which a wrongly typed parameter could in 3DForts' hand-written from_json. - Result, Error and Async: handler results, including ones that take frames to arrive, so callers need no ready:false polling. project.cpp now builds Crafter.Debug.Core as its own library, used by both the app library and the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
feat(transport): serve the session over QUIC on loopback A Session with listen = true (the default) now listens on 127.0.0.1 with the ALPN crafter-debug/1 and writes its discovery record (port, token, protocol, source id, client path), which goes first when it ends. One request per stream the client opens: the request and a FIN in, zero or more stream lines and then the reply and a FIN out, one JSON object per line. It holds no thread while nothing arrives. Crafter.Network delivers a new stream on the pool, its bytes on msquic's worker through OnRecieve, and replies leave through SendAsync, which never waits. The handler, which takes the session's mutex, runs on a pool task rather than inside the stream's callback, so no msquic worker ever waits on the session, and the session's threads only send, which Network allows under any lock. Stream wrappers are let go outside every lock their callbacks take. The rules head the implementation. Limits: a request over 1 MiB is answered with an error, not buffered, and a log follower more than 8 MiB behind is reset. quit --force exits 200 ms after its reply was taken, from a thread of its own, since "sent" means msquic has the bytes, not that they left. A client killed without closing is let go at the 10 s idle timeout. The Session refuses a pool without a worker: Network hands it new streams there, and with Start(1) nothing would run them. The partition exports nothing, so an app's view of the module is unchanged and it does not see Crafter.Network's types. Crafter.Network comes from forgejo, unpinned, with the args unchanged; --local takes ../Crafter.Network, and $CRAFTER_DEBUG_NETWORK any project.cpp, for the branch the callback API is on until it lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:06:56 +01:00
// Crafter.Network, with the args unchanged: a consumer that also uses
// it passes the same ones, so both resolve to one configuration rather
// than one module built twice.
Configuration* Network(std::span<const std::string_view> args) {
const std::vector<std::string> forwarded(args.begin(), args.end());
if (std::ranges::find(args, std::string_view("--local")) != args.end()) {
return LocalProject({ .projectFile = Root().parent_path() / "Crafter.Network" / "project.cpp", .args = forwarded });
}
return GitProject({
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Network.git" },
.args = forwarded,
});
}
2026-11-01 01:40:59 +01:00
// Tests of the core, linked against it alone.
constexpr std::array<std::string_view, 5> coreTests = {
feat(core): Value, reflection, codecs and results, shared by app and client Crafter.Debug.Core is what the app side and the crafter-debug client both need, without the session: - Value: a JSON value with its own parser and printer, so no consumer interface sees a JSON library. Integers stay integers and whole doubles stay doubles; NaN and the infinities, which JSON cannot spell, are written as strings rather than hidden, because a debugger must not lose a NaN; invalid UTF-8 is replaced so output always parses. Objects keep insertion order, the order an agent reads. - Reflect: aggregate members and names through structured-binding packs and source_location, taken from Crafter.DB, plus scoped-enum names by probing a range of values. Self-tested with static_asserts so a compiler that changes function_name()'s shape fails the build. - Codec<T>: encode, decode and describe for bool, integers, floating point, strings, enums, optional, vector, array, string-keyed maps, Value, Required<T> and every aggregate. Decode reports every problem in a call with its path and, for a typo, the likely intended name, and never throws: a bad call cannot take an app down, which a wrongly typed parameter could in 3DForts' hand-written from_json. - Result, Error and Async: handler results, including ones that take frames to arrive, so callers need no ready:false polling. project.cpp now builds Crafter.Debug.Core as its own library, used by both the app library and the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
"ShouldRoundTripJson",
"ShouldRoundTripAggregates",
"ShouldReportDecodeErrorsWithPaths",
"ShouldDescribeDefaultsAndRequired",
"ShouldDiscoverLiveInstances",
feat(core): Value, reflection, codecs and results, shared by app and client Crafter.Debug.Core is what the app side and the crafter-debug client both need, without the session: - Value: a JSON value with its own parser and printer, so no consumer interface sees a JSON library. Integers stay integers and whole doubles stay doubles; NaN and the infinities, which JSON cannot spell, are written as strings rather than hidden, because a debugger must not lose a NaN; invalid UTF-8 is replaced so output always parses. Objects keep insertion order, the order an agent reads. - Reflect: aggregate members and names through structured-binding packs and source_location, taken from Crafter.DB, plus scoped-enum names by probing a range of values. Self-tested with static_asserts so a compiler that changes function_name()'s shape fails the build. - Codec<T>: encode, decode and describe for bool, integers, floating point, strings, enums, optional, vector, array, string-keyed maps, Value, Required<T> and every aggregate. Decode reports every problem in a call with its path and, for a typo, the likely intended name, and never throws: a bad call cannot take an app down, which a wrongly typed parameter could in 3DForts' hand-written from_json. - Result, Error and Async: handler results, including ones that take frames to arrive, so callers need no ready:false polling. project.cpp now builds Crafter.Debug.Core as its own library, used by both the app library and the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
};
2026-11-01 01:40:59 +01:00
// Tests of the app library.
feat(session): serve requests without blocking a thread, pause and step The app's end of the connection, independent of the transport, which reaches it through Detail::Exchange: one request's way back, safe from any thread and never blocking. - What needs only the session's own state (hello, status, list, log, pause, resume) is answered on the transport thread. What touches the app (calls, get, set, trace, screenshot) queues for the next Pump and runs on the app's thread between frames. Waits and steps reply from the Pump that sees them satisfied, events from the thread that emits them. No request ever holds a thread while it waits. - A request whose timeout passes before Pump is answered Timeout and is guaranteed not to have run, so an agent never wonders whether a command went through; an Async result arrives on the Pump it is ready; requests left when the Session ends are answered ShuttingDown. - Advance() for frame loops and Paused()/TakeSteps() for fixed-step loops hold what the app gates on them; pausing an app that gates nothing is refused instead of silently doing nothing. pause --when stops the frame its condition holds; pause --on-check stops before the next Advance in the frame a check fails, optionally raising SIGTRAP for an attached debugger. - A wait that times out says what its condition's paths were; a log follower gets matching entries in order, from the backlog on, and is dropped when it cannot keep up. - Request parameters go through Codec, so a mistyped request gets the same did-you-mean replies a mistyped command does. The session sets PR_SET_PTRACER_ANY so a debugger the agent starts can attach. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:53:15 +01:00
constexpr std::array<std::string_view, 7> libraryTests = {
2026-11-01 01:40:59 +01:00
"ShouldInvokeCommands",
"ShouldResolveWatchPaths",
"ShouldRecordTimeline",
"ShouldEvaluateConditions",
feat(session): serve requests without blocking a thread, pause and step The app's end of the connection, independent of the transport, which reaches it through Detail::Exchange: one request's way back, safe from any thread and never blocking. - What needs only the session's own state (hello, status, list, log, pause, resume) is answered on the transport thread. What touches the app (calls, get, set, trace, screenshot) queues for the next Pump and runs on the app's thread between frames. Waits and steps reply from the Pump that sees them satisfied, events from the thread that emits them. No request ever holds a thread while it waits. - A request whose timeout passes before Pump is answered Timeout and is guaranteed not to have run, so an agent never wonders whether a command went through; an Async result arrives on the Pump it is ready; requests left when the Session ends are answered ShuttingDown. - Advance() for frame loops and Paused()/TakeSteps() for fixed-step loops hold what the app gates on them; pausing an app that gates nothing is refused instead of silently doing nothing. pause --when stops the frame its condition holds; pause --on-check stops before the next Advance in the frame a check fails, optionally raising SIGTRAP for an attached debugger. - A wait that times out says what its condition's paths were; a log follower gets matching entries in order, from the backlog on, and is dropped when it cannot keep up. - Request parameters go through Codec, so a mistyped request gets the same did-you-mean replies a mistyped command does. The session sets PR_SET_PTRACER_ANY so a debugger the agent starts can attach. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:53:15 +01:00
"ShouldAnswerRequests",
"ShouldPauseAndStep",
"ShouldWaitTraceAndFollow",
2026-11-01 01:40:59 +01:00
};
std::string ReadFile(const fs::path& path) {
std::ifstream in(path, std::ios::binary);
return std::string((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
}
// FNV-1a over the path and bytes of every source, in a fixed order: what
// the app and the client compare to tell they came from the same code.
std::string SourceId() {
std::vector<fs::path> files;
for (const std::string_view dir : { "interfaces", "implementations" }) {
std::error_code ec;
for (const fs::directory_entry& entry : fs::recursive_directory_iterator(Root() / dir, ec)) {
if (entry.is_regular_file() && (entry.path().extension() == ".cppm" || entry.path().extension() == ".cpp")) files.push_back(entry.path());
}
}
std::ranges::sort(files);
std::uint64_t hash = 14695981039346656037ULL;
const auto mix = [&](std::string_view bytes) {
for (const char c : bytes) {
hash ^= static_cast<std::uint8_t>(c);
hash *= 1099511628211ULL;
}
};
for (const fs::path& file : files) {
mix(file.lexically_relative(Root()).generic_string());
mix(ReadFile(file));
}
return std::format("{:016x}", hash);
}
void WriteIfChanged(const fs::path& file, const std::string& text) {
if (fs::exists(file) && ReadFile(file) == text) return;
fs::create_directories(file.parent_path());
std::ofstream(file, std::ios::binary | std::ios::trunc) << text;
}
// Generates the definitions of :BuildInfo for this variant of the core and
// returns the source to compile, without its extension.
fs::path GenerateBuildInfo(const Configuration& core, const Configuration& client) {
const fs::path source = Root() / "build" / "generated" / core.VariantId() / "Crafter.Debug.Core-BuildInfo";
const fs::path clientPath = client.BinDir() / client.outputName;
WriteIfChanged(fs::path(source).concat(".cpp"), std::format(
"// Generated by Crafter.Debug's project.cpp whenever the sources or the client's\n"
"// path change. Not to be edited.\n"
"module Crafter.Debug.Core:BuildInfo_impl;\n"
"import std;\n"
"import :BuildInfo;\n"
"\n"
"std::string_view Crafter::Debug::Detail::SourceId() {{\n"
" return \"{}\";\n"
"}}\n"
"\n"
"std::string_view Crafter::Debug::Detail::ClientPath() {{\n"
" return R\"crafter-debug({})crafter-debug\";\n"
"}}\n",
SourceId(), clientPath.string()));
return source;
}
}
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
Configuration cfg;
cfg.path = "./";
cfg.name = "Crafter.Debug";
cfg.outputName = "Crafter.Debug";
cfg.type = ConfigurationType::LibraryStatic;
ApplyStandardArgs(cfg, args);
feat(core): Value, reflection, codecs and results, shared by app and client Crafter.Debug.Core is what the app side and the crafter-debug client both need, without the session: - Value: a JSON value with its own parser and printer, so no consumer interface sees a JSON library. Integers stay integers and whole doubles stay doubles; NaN and the infinities, which JSON cannot spell, are written as strings rather than hidden, because a debugger must not lose a NaN; invalid UTF-8 is replaced so output always parses. Objects keep insertion order, the order an agent reads. - Reflect: aggregate members and names through structured-binding packs and source_location, taken from Crafter.DB, plus scoped-enum names by probing a range of values. Self-tested with static_asserts so a compiler that changes function_name()'s shape fails the build. - Codec<T>: encode, decode and describe for bool, integers, floating point, strings, enums, optional, vector, array, string-keyed maps, Value, Required<T> and every aggregate. Decode reports every problem in a call with its path and, for a typo, the likely intended name, and never throws: a bad call cannot take an app down, which a wrongly typed parameter could in 3DForts' hand-written from_json. - Result, Error and Async: handler results, including ones that take frames to arrive, so callers need no ready:false polling. project.cpp now builds Crafter.Debug.Core as its own library, used by both the app library and the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
feat(transport): serve the session over QUIC on loopback A Session with listen = true (the default) now listens on 127.0.0.1 with the ALPN crafter-debug/1 and writes its discovery record (port, token, protocol, source id, client path), which goes first when it ends. One request per stream the client opens: the request and a FIN in, zero or more stream lines and then the reply and a FIN out, one JSON object per line. It holds no thread while nothing arrives. Crafter.Network delivers a new stream on the pool, its bytes on msquic's worker through OnRecieve, and replies leave through SendAsync, which never waits. The handler, which takes the session's mutex, runs on a pool task rather than inside the stream's callback, so no msquic worker ever waits on the session, and the session's threads only send, which Network allows under any lock. Stream wrappers are let go outside every lock their callbacks take. The rules head the implementation. Limits: a request over 1 MiB is answered with an error, not buffered, and a log follower more than 8 MiB behind is reset. quit --force exits 200 ms after its reply was taken, from a thread of its own, since "sent" means msquic has the bytes, not that they left. A client killed without closing is let go at the 10 s idle timeout. The Session refuses a pool without a worker: Network hands it new streams there, and with Start(1) nothing would run them. The partition exports nothing, so an app's view of the module is unchanged and it does not see Crafter.Network's types. Crafter.Network comes from forgejo, unpinned, with the args unchanged; --local takes ../Crafter.Network, and $CRAFTER_DEBUG_NETWORK any project.cpp, for the branch the callback API is on until it lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:06:56 +01:00
Configuration* network = Network(args);
feat(core): Value, reflection, codecs and results, shared by app and client Crafter.Debug.Core is what the app side and the crafter-debug client both need, without the session: - Value: a JSON value with its own parser and printer, so no consumer interface sees a JSON library. Integers stay integers and whole doubles stay doubles; NaN and the infinities, which JSON cannot spell, are written as strings rather than hidden, because a debugger must not lose a NaN; invalid UTF-8 is replaced so output always parses. Objects keep insertion order, the order an agent reads. - Reflect: aggregate members and names through structured-binding packs and source_location, taken from Crafter.DB, plus scoped-enum names by probing a range of values. Self-tested with static_asserts so a compiler that changes function_name()'s shape fails the build. - Codec<T>: encode, decode and describe for bool, integers, floating point, strings, enums, optional, vector, array, string-keyed maps, Value, Required<T> and every aggregate. Decode reports every problem in a call with its path and, for a typo, the likely intended name, and never throws: a bad call cannot take an app down, which a wrongly typed parameter could in 3DForts' hand-written from_json. - Result, Error and Async: handler results, including ones that take frames to arrive, so callers need no ready:false polling. project.cpp now builds Crafter.Debug.Core as its own library, used by both the app library and the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
Configuration& core = owned.emplace_back();
core.path = Root();
core.name = "Crafter.Debug.Core";
core.outputName = "Crafter.Debug.Core";
core.type = ConfigurationType::LibraryStatic;
ApplyStandardArgs(core, args);
feat(core): Value, reflection, codecs and results, shared by app and client Crafter.Debug.Core is what the app side and the crafter-debug client both need, without the session: - Value: a JSON value with its own parser and printer, so no consumer interface sees a JSON library. Integers stay integers and whole doubles stay doubles; NaN and the infinities, which JSON cannot spell, are written as strings rather than hidden, because a debugger must not lose a NaN; invalid UTF-8 is replaced so output always parses. Objects keep insertion order, the order an agent reads. - Reflect: aggregate members and names through structured-binding packs and source_location, taken from Crafter.DB, plus scoped-enum names by probing a range of values. Self-tested with static_asserts so a compiler that changes function_name()'s shape fails the build. - Codec<T>: encode, decode and describe for bool, integers, floating point, strings, enums, optional, vector, array, string-keyed maps, Value, Required<T> and every aggregate. Decode reports every problem in a call with its path and, for a typo, the likely intended name, and never throws: a bad call cannot take an app down, which a wrongly typed parameter could in 3DForts' hand-written from_json. - Result, Error and Async: handler results, including ones that take frames to arrive, so callers need no ready:false polling. project.cpp now builds Crafter.Debug.Core as its own library, used by both the app library and the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
// The client, built for the library's target. Not through
// ApplyStandardArgs: a consumer's --lib would make it a library.
Configuration& client = owned.emplace_back();
client.path = Root();
client.name = "crafter.debug-cli";
client.outputName = "crafter-debug";
client.type = ConfigurationType::Executable;
client.target = cfg.target;
client.march = cfg.march;
client.mtune = cfg.mtune;
client.sysroot = cfg.sysroot;
client.debug = cfg.debug;
// The client's code as a library, so tests can run it in process.
Configuration& cli = owned.emplace_back();
cli.path = Root();
cli.name = "Crafter.Debug.Cli";
cli.outputName = "Crafter.Debug.Cli";
cli.type = ConfigurationType::LibraryStatic;
cli.target = cfg.target;
cli.march = cfg.march;
cli.mtune = cfg.mtune;
cli.sysroot = cfg.sysroot;
cli.debug = cfg.debug;
cli.dependencies = { &core, network };
client.dependencies = { &cli };
{
std::array<fs::path, 8> interfaces = {
"interfaces/Crafter.Debug.Core-Value",
"interfaces/Crafter.Debug.Core-Reflect",
"interfaces/Crafter.Debug.Core-Codec",
"interfaces/Crafter.Debug.Core-Result",
"interfaces/Crafter.Debug.Core-Protocol",
"interfaces/Crafter.Debug.Core-BuildInfo",
"interfaces/Crafter.Debug.Core-Discovery",
"interfaces/Crafter.Debug.Core",
};
std::array<fs::path, 5> implementations = {
"implementations/Crafter.Debug.Core-Value",
"implementations/Crafter.Debug.Core-Codec",
"implementations/Crafter.Debug.Core-Protocol",
"implementations/Crafter.Debug.Core-Discovery",
GenerateBuildInfo(core, client),
};
core.GetInterfacesAndImplementations(interfaces, implementations);
}
{
std::array<fs::path, 3> interfaces = { "interfaces/Crafter.Debug.Cli-Client", "interfaces/Crafter.Debug.Cli-Quic", "interfaces/Crafter.Debug.Cli" };
std::array<fs::path, 2> implementations = { "implementations/Crafter.Debug.Cli-Client", "implementations/Crafter.Debug.Cli-Quic" };
cli.GetInterfacesAndImplementations(interfaces, implementations);
}
{
std::array<fs::path, 0> interfaces = {};
std::array<fs::path, 1> implementations = { "implementations/main" };
client.GetInterfacesAndImplementations(interfaces, implementations);
}
feat(core): Value, reflection, codecs and results, shared by app and client Crafter.Debug.Core is what the app side and the crafter-debug client both need, without the session: - Value: a JSON value with its own parser and printer, so no consumer interface sees a JSON library. Integers stay integers and whole doubles stay doubles; NaN and the infinities, which JSON cannot spell, are written as strings rather than hidden, because a debugger must not lose a NaN; invalid UTF-8 is replaced so output always parses. Objects keep insertion order, the order an agent reads. - Reflect: aggregate members and names through structured-binding packs and source_location, taken from Crafter.DB, plus scoped-enum names by probing a range of values. Self-tested with static_asserts so a compiler that changes function_name()'s shape fails the build. - Codec<T>: encode, decode and describe for bool, integers, floating point, strings, enums, optional, vector, array, string-keyed maps, Value, Required<T> and every aggregate. Decode reports every problem in a call with its path and, for a typo, the likely intended name, and never throws: a bad call cannot take an app down, which a wrongly typed parameter could in 3DForts' hand-written from_json. - Result, Error and Async: handler results, including ones that take frames to arrive, so callers need no ready:false polling. project.cpp now builds Crafter.Debug.Core as its own library, used by both the app library and the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
feat(transport): serve the session over QUIC on loopback A Session with listen = true (the default) now listens on 127.0.0.1 with the ALPN crafter-debug/1 and writes its discovery record (port, token, protocol, source id, client path), which goes first when it ends. One request per stream the client opens: the request and a FIN in, zero or more stream lines and then the reply and a FIN out, one JSON object per line. It holds no thread while nothing arrives. Crafter.Network delivers a new stream on the pool, its bytes on msquic's worker through OnRecieve, and replies leave through SendAsync, which never waits. The handler, which takes the session's mutex, runs on a pool task rather than inside the stream's callback, so no msquic worker ever waits on the session, and the session's threads only send, which Network allows under any lock. Stream wrappers are let go outside every lock their callbacks take. The rules head the implementation. Limits: a request over 1 MiB is answered with an error, not buffered, and a log follower more than 8 MiB behind is reset. quit --force exits 200 ms after its reply was taken, from a thread of its own, since "sent" means msquic has the bytes, not that they left. A client killed without closing is let go at the 10 s idle timeout. The Session refuses a pool without a worker: Network hands it new streams there, and with Start(1) nothing would run them. The partition exports nothing, so an app's view of the module is unchanged and it does not see Crafter.Network's types. Crafter.Network comes from forgejo, unpinned, with the args unchanged; --local takes ../Crafter.Network, and $CRAFTER_DEBUG_NETWORK any project.cpp, for the branch the callback API is on until it lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:06:56 +01:00
cfg.dependencies = { &core, &client, network };
feat(core): Value, reflection, codecs and results, shared by app and client Crafter.Debug.Core is what the app side and the crafter-debug client both need, without the session: - Value: a JSON value with its own parser and printer, so no consumer interface sees a JSON library. Integers stay integers and whole doubles stay doubles; NaN and the infinities, which JSON cannot spell, are written as strings rather than hidden, because a debugger must not lose a NaN; invalid UTF-8 is replaced so output always parses. Objects keep insertion order, the order an agent reads. - Reflect: aggregate members and names through structured-binding packs and source_location, taken from Crafter.DB, plus scoped-enum names by probing a range of values. Self-tested with static_asserts so a compiler that changes function_name()'s shape fails the build. - Codec<T>: encode, decode and describe for bool, integers, floating point, strings, enums, optional, vector, array, string-keyed maps, Value, Required<T> and every aggregate. Decode reports every problem in a call with its path and, for a typo, the likely intended name, and never throws: a bad call cannot take an app down, which a wrongly typed parameter could in 3DForts' hand-written from_json. - Result, Error and Async: handler results, including ones that take frames to arrive, so callers need no ready:false polling. project.cpp now builds Crafter.Debug.Core as its own library, used by both the app library and the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
{
feat(transport): serve the session over QUIC on loopback A Session with listen = true (the default) now listens on 127.0.0.1 with the ALPN crafter-debug/1 and writes its discovery record (port, token, protocol, source id, client path), which goes first when it ends. One request per stream the client opens: the request and a FIN in, zero or more stream lines and then the reply and a FIN out, one JSON object per line. It holds no thread while nothing arrives. Crafter.Network delivers a new stream on the pool, its bytes on msquic's worker through OnRecieve, and replies leave through SendAsync, which never waits. The handler, which takes the session's mutex, runs on a pool task rather than inside the stream's callback, so no msquic worker ever waits on the session, and the session's threads only send, which Network allows under any lock. Stream wrappers are let go outside every lock their callbacks take. The rules head the implementation. Limits: a request over 1 MiB is answered with an error, not buffered, and a log follower more than 8 MiB behind is reset. quit --force exits 200 ms after its reply was taken, from a thread of its own, since "sent" means msquic has the bytes, not that they left. A client killed without closing is let go at the 10 s idle timeout. The Session refuses a pool without a worker: Network hands it new streams there, and with Start(1) nothing would run them. The partition exports nothing, so an app's view of the module is unchanged and it does not see Crafter.Network's types. Crafter.Network comes from forgejo, unpinned, with the args unchanged; --local takes ../Crafter.Network, and $CRAFTER_DEBUG_NETWORK any project.cpp, for the branch the callback API is on until it lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:06:56 +01:00
std::array<fs::path, 6> interfaces = {
2026-11-01 01:40:59 +01:00
"interfaces/Crafter.Debug-Registry",
"interfaces/Crafter.Debug-Timeline",
"interfaces/Crafter.Debug-Condition",
feat(session): serve requests without blocking a thread, pause and step The app's end of the connection, independent of the transport, which reaches it through Detail::Exchange: one request's way back, safe from any thread and never blocking. - What needs only the session's own state (hello, status, list, log, pause, resume) is answered on the transport thread. What touches the app (calls, get, set, trace, screenshot) queues for the next Pump and runs on the app's thread between frames. Waits and steps reply from the Pump that sees them satisfied, events from the thread that emits them. No request ever holds a thread while it waits. - A request whose timeout passes before Pump is answered Timeout and is guaranteed not to have run, so an agent never wonders whether a command went through; an Async result arrives on the Pump it is ready; requests left when the Session ends are answered ShuttingDown. - Advance() for frame loops and Paused()/TakeSteps() for fixed-step loops hold what the app gates on them; pausing an app that gates nothing is refused instead of silently doing nothing. pause --when stops the frame its condition holds; pause --on-check stops before the next Advance in the frame a check fails, optionally raising SIGTRAP for an attached debugger. - A wait that times out says what its condition's paths were; a log follower gets matching entries in order, from the backlog on, and is dropped when it cannot keep up. - Request parameters go through Codec, so a mistyped request gets the same did-you-mean replies a mistyped command does. The session sets PR_SET_PTRACER_ANY so a debugger the agent starts can attach. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:53:15 +01:00
"interfaces/Crafter.Debug-Session",
feat(transport): serve the session over QUIC on loopback A Session with listen = true (the default) now listens on 127.0.0.1 with the ALPN crafter-debug/1 and writes its discovery record (port, token, protocol, source id, client path), which goes first when it ends. One request per stream the client opens: the request and a FIN in, zero or more stream lines and then the reply and a FIN out, one JSON object per line. It holds no thread while nothing arrives. Crafter.Network delivers a new stream on the pool, its bytes on msquic's worker through OnRecieve, and replies leave through SendAsync, which never waits. The handler, which takes the session's mutex, runs on a pool task rather than inside the stream's callback, so no msquic worker ever waits on the session, and the session's threads only send, which Network allows under any lock. Stream wrappers are let go outside every lock their callbacks take. The rules head the implementation. Limits: a request over 1 MiB is answered with an error, not buffered, and a log follower more than 8 MiB behind is reset. quit --force exits 200 ms after its reply was taken, from a thread of its own, since "sent" means msquic has the bytes, not that they left. A client killed without closing is let go at the 10 s idle timeout. The Session refuses a pool without a worker: Network hands it new streams there, and with Start(1) nothing would run them. The partition exports nothing, so an app's view of the module is unchanged and it does not see Crafter.Network's types. Crafter.Network comes from forgejo, unpinned, with the args unchanged; --local takes ../Crafter.Network, and $CRAFTER_DEBUG_NETWORK any project.cpp, for the branch the callback API is on until it lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:06:56 +01:00
"interfaces/Crafter.Debug-Transport",
2026-11-01 01:40:59 +01:00
"interfaces/Crafter.Debug",
};
feat(transport): serve the session over QUIC on loopback A Session with listen = true (the default) now listens on 127.0.0.1 with the ALPN crafter-debug/1 and writes its discovery record (port, token, protocol, source id, client path), which goes first when it ends. One request per stream the client opens: the request and a FIN in, zero or more stream lines and then the reply and a FIN out, one JSON object per line. It holds no thread while nothing arrives. Crafter.Network delivers a new stream on the pool, its bytes on msquic's worker through OnRecieve, and replies leave through SendAsync, which never waits. The handler, which takes the session's mutex, runs on a pool task rather than inside the stream's callback, so no msquic worker ever waits on the session, and the session's threads only send, which Network allows under any lock. Stream wrappers are let go outside every lock their callbacks take. The rules head the implementation. Limits: a request over 1 MiB is answered with an error, not buffered, and a log follower more than 8 MiB behind is reset. quit --force exits 200 ms after its reply was taken, from a thread of its own, since "sent" means msquic has the bytes, not that they left. A client killed without closing is let go at the 10 s idle timeout. The Session refuses a pool without a worker: Network hands it new streams there, and with Start(1) nothing would run them. The partition exports nothing, so an app's view of the module is unchanged and it does not see Crafter.Network's types. Crafter.Network comes from forgejo, unpinned, with the args unchanged; --local takes ../Crafter.Network, and $CRAFTER_DEBUG_NETWORK any project.cpp, for the branch the callback API is on until it lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:06:56 +01:00
std::array<fs::path, 5> implementations = {
2026-11-01 01:40:59 +01:00
"implementations/Crafter.Debug-Registry",
"implementations/Crafter.Debug-Timeline",
"implementations/Crafter.Debug-Condition",
feat(session): serve requests without blocking a thread, pause and step The app's end of the connection, independent of the transport, which reaches it through Detail::Exchange: one request's way back, safe from any thread and never blocking. - What needs only the session's own state (hello, status, list, log, pause, resume) is answered on the transport thread. What touches the app (calls, get, set, trace, screenshot) queues for the next Pump and runs on the app's thread between frames. Waits and steps reply from the Pump that sees them satisfied, events from the thread that emits them. No request ever holds a thread while it waits. - A request whose timeout passes before Pump is answered Timeout and is guaranteed not to have run, so an agent never wonders whether a command went through; an Async result arrives on the Pump it is ready; requests left when the Session ends are answered ShuttingDown. - Advance() for frame loops and Paused()/TakeSteps() for fixed-step loops hold what the app gates on them; pausing an app that gates nothing is refused instead of silently doing nothing. pause --when stops the frame its condition holds; pause --on-check stops before the next Advance in the frame a check fails, optionally raising SIGTRAP for an attached debugger. - A wait that times out says what its condition's paths were; a log follower gets matching entries in order, from the backlog on, and is dropped when it cannot keep up. - Request parameters go through Codec, so a mistyped request gets the same did-you-mean replies a mistyped command does. The session sets PR_SET_PTRACER_ANY so a debugger the agent starts can attach. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:53:15 +01:00
"implementations/Crafter.Debug-Session",
feat(transport): serve the session over QUIC on loopback A Session with listen = true (the default) now listens on 127.0.0.1 with the ALPN crafter-debug/1 and writes its discovery record (port, token, protocol, source id, client path), which goes first when it ends. One request per stream the client opens: the request and a FIN in, zero or more stream lines and then the reply and a FIN out, one JSON object per line. It holds no thread while nothing arrives. Crafter.Network delivers a new stream on the pool, its bytes on msquic's worker through OnRecieve, and replies leave through SendAsync, which never waits. The handler, which takes the session's mutex, runs on a pool task rather than inside the stream's callback, so no msquic worker ever waits on the session, and the session's threads only send, which Network allows under any lock. Stream wrappers are let go outside every lock their callbacks take. The rules head the implementation. Limits: a request over 1 MiB is answered with an error, not buffered, and a log follower more than 8 MiB behind is reset. quit --force exits 200 ms after its reply was taken, from a thread of its own, since "sent" means msquic has the bytes, not that they left. A client killed without closing is let go at the 10 s idle timeout. The Session refuses a pool without a worker: Network hands it new streams there, and with Start(1) nothing would run them. The partition exports nothing, so an app's view of the module is unchanged and it does not see Crafter.Network's types. Crafter.Network comes from forgejo, unpinned, with the args unchanged; --local takes ../Crafter.Network, and $CRAFTER_DEBUG_NETWORK any project.cpp, for the branch the callback API is on until it lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:06:56 +01:00
"implementations/Crafter.Debug-Transport",
2026-11-01 01:40:59 +01:00
};
feat(core): Value, reflection, codecs and results, shared by app and client Crafter.Debug.Core is what the app side and the crafter-debug client both need, without the session: - Value: a JSON value with its own parser and printer, so no consumer interface sees a JSON library. Integers stay integers and whole doubles stay doubles; NaN and the infinities, which JSON cannot spell, are written as strings rather than hidden, because a debugger must not lose a NaN; invalid UTF-8 is replaced so output always parses. Objects keep insertion order, the order an agent reads. - Reflect: aggregate members and names through structured-binding packs and source_location, taken from Crafter.DB, plus scoped-enum names by probing a range of values. Self-tested with static_asserts so a compiler that changes function_name()'s shape fails the build. - Codec<T>: encode, decode and describe for bool, integers, floating point, strings, enums, optional, vector, array, string-keyed maps, Value, Required<T> and every aggregate. Decode reports every problem in a call with its path and, for a typo, the likely intended name, and never throws: a bad call cannot take an app down, which a wrongly typed parameter could in 3DForts' hand-written from_json. - Result, Error and Async: handler results, including ones that take frames to arrive, so callers need no ready:false polling. project.cpp now builds Crafter.Debug.Core as its own library, used by both the app library and the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
cfg.GetInterfacesAndImplementations(interfaces, implementations);
}
// Tests only when this is the project being built: a consumer's load of
// this file has no use for them.
if (IsTopLevelProject()) {
2026-11-01 01:40:59 +01:00
for (const std::string_view test : coreTests) cfg.AddTest(test).Dependencies({ &core });
for (const std::string_view test : libraryTests) cfg.AddTest(test).Dependencies({ &cfg });
cfg.AddTest("ShouldRunClientCommands").Dependencies({ &cfg, &cli });
cfg.AddTest("ShouldServeOverQuic").Dependencies({ &cfg, &cli });
feat(core): Value, reflection, codecs and results, shared by app and client Crafter.Debug.Core is what the app side and the crafter-debug client both need, without the session: - Value: a JSON value with its own parser and printer, so no consumer interface sees a JSON library. Integers stay integers and whole doubles stay doubles; NaN and the infinities, which JSON cannot spell, are written as strings rather than hidden, because a debugger must not lose a NaN; invalid UTF-8 is replaced so output always parses. Objects keep insertion order, the order an agent reads. - Reflect: aggregate members and names through structured-binding packs and source_location, taken from Crafter.DB, plus scoped-enum names by probing a range of values. Self-tested with static_asserts so a compiler that changes function_name()'s shape fails the build. - Codec<T>: encode, decode and describe for bool, integers, floating point, strings, enums, optional, vector, array, string-keyed maps, Value, Required<T> and every aggregate. Decode reports every problem in a call with its path and, for a typo, the likely intended name, and never throws: a bad call cannot take an app down, which a wrongly typed parameter could in 3DForts' hand-written from_json. - Result, Error and Async: handler results, including ones that take frames to arrive, so callers need no ready:false polling. project.cpp now builds Crafter.Debug.Core as its own library, used by both the app library and the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:31:58 +01:00
}
return cfg;
}