Sprint 281: DAP Hardening (steps 1933–1937)

DAPRequestValidator, DAPCapabilityNegotiator, DAPEventBroadcaster,
DAPBreakpointHitDispatcher — 26/26 tests pass.
Integration step 1937: Python+Rust DAP session round-trip verified.
Also adds allBreakpoints() to ASTNodeBreakpointMapper.
Metrics: 8 whetstone calls, 8853 tokens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill
2026-03-01 21:45:53 -07:00
parent b38f57ed1c
commit 99cc6319c0
12 changed files with 756 additions and 0 deletions

View File

@@ -59,6 +59,14 @@ public:
void clear() { bp_.clear(); }
// All breakpoints across every node and language.
std::vector<Breakpoint> allBreakpoints() const {
std::vector<Breakpoint> result;
result.reserve(bp_.size());
for (const auto& [key, bp] : bp_) result.push_back(bp);
return result;
}
private:
std::map<std::pair<std::string, std::string>, Breakpoint> bp_;
};

View File

@@ -0,0 +1,47 @@
#pragma once
#include "ASTNodeBreakpointMapper.h"
#include "DebugAdapterRouter.h"
#include <string>
#include <vector>
namespace whetstone {
struct HitResult {
std::string language;
std::string astNodeId;
int line = 0;
bool dispatched = false;
};
class DAPBreakpointHitDispatcher {
public:
// Find the first active breakpoint at source:line, set router to its language.
HitResult dispatch(const std::string& source, int line,
const ASTNodeBreakpointMapper& mapper,
DebugAdapterRouter& router) const {
for (const auto& bp : mapper.allBreakpoints()) {
if (bp.active && bp.source == source && bp.line == line) {
router.setActiveLanguage(bp.language);
return {bp.language, bp.astNodeId, bp.line, true};
}
}
return {"", "", line, false};
}
// Return a HitResult for every active breakpoint at source:line.
// Router ends at the language of the last match.
std::vector<HitResult> dispatchAll(const std::string& source, int line,
const ASTNodeBreakpointMapper& mapper,
DebugAdapterRouter& router) const {
std::vector<HitResult> results;
for (const auto& bp : mapper.allBreakpoints()) {
if (bp.active && bp.source == source && bp.line == line) {
router.setActiveLanguage(bp.language);
results.push_back({bp.language, bp.astNodeId, bp.line, true});
}
}
return results;
}
};
} // namespace whetstone

View File

@@ -0,0 +1,52 @@
#pragma once
#include <map>
#include <set>
#include <string>
#include <vector>
namespace whetstone {
class DAPCapabilityNegotiator {
public:
void registerAdapter(const std::string& language,
const std::vector<std::string>& supportedCommands) {
auto& s = adapters_[language];
for (const auto& cmd : supportedCommands)
s.insert(cmd);
}
bool supports(const std::string& language, const std::string& command) const {
auto it = adapters_.find(language);
if (it == adapters_.end()) return false;
return it->second.count(command) > 0;
}
std::vector<std::string> supportedCommands(const std::string& language) const {
auto it = adapters_.find(language);
if (it == adapters_.end()) return {};
return {it->second.begin(), it->second.end()};
}
// All languages that support the given command.
std::vector<std::string> languagesFor(const std::string& command) const {
std::vector<std::string> result;
for (const auto& [lang, cmds] : adapters_)
if (cmds.count(command)) result.push_back(lang);
return result;
}
// Returns preferred language if it supports command,
// else the first registered language that does, else "".
std::string selectAdapter(const std::string& command,
const std::string& preferredLanguage) const {
if (supports(preferredLanguage, command)) return preferredLanguage;
for (const auto& [lang, cmds] : adapters_)
if (cmds.count(command)) return lang;
return "";
}
private:
std::map<std::string, std::set<std::string>> adapters_;
};
} // namespace whetstone

View File

@@ -0,0 +1,59 @@
#pragma once
#include <nlohmann/json.hpp>
#include <functional>
#include <map>
#include <string>
#include <vector>
namespace whetstone {
class DAPEventBroadcaster {
public:
using EventHandler = std::function<void(const nlohmann::json&)>;
// Subscribe to events of eventType. Returns a subscription id.
int subscribe(const std::string& eventType, EventHandler handler) {
int id = nextId_++;
subs_.push_back({id, eventType, std::move(handler)});
return id;
}
// Remove the subscription with the given id.
void unsubscribe(int id) {
subs_.erase(
std::remove_if(subs_.begin(), subs_.end(),
[id](const Sub& s){ return s.id == id; }),
subs_.end());
}
// Call all handlers registered for eventType, passing body.
void broadcast(const std::string& eventType, const nlohmann::json& body) const {
for (const auto& s : subs_)
if (s.eventType == eventType) s.handler(body);
}
// Broadcast using event["event"] as the type, event itself as the body.
void broadcastAll(const nlohmann::json& event) const {
if (!event.contains("event") || !event["event"].is_string()) return;
broadcast(event["event"].get<std::string>(), event);
}
int subscriberCount(const std::string& eventType) const {
int n = 0;
for (const auto& s : subs_)
if (s.eventType == eventType) ++n;
return n;
}
private:
struct Sub {
int id;
std::string eventType;
EventHandler handler;
};
std::vector<Sub> subs_;
int nextId_ = 1;
};
} // namespace whetstone

View File

@@ -0,0 +1,78 @@
#pragma once
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
namespace whetstone {
struct DAPValidationResult {
bool valid = true;
std::string errorCode;
std::string errorMessage;
};
class DAPRequestValidator {
public:
// Validate a single DAP message.
// Checks: type in {request,response,event}, seq is integer >=1,
// request/response have non-empty command, event has non-empty event field.
DAPValidationResult validate(const nlohmann::json& msg) const {
// type
if (!msg.contains("type") || !msg["type"].is_string()) {
return {false, "invalid_type", "type must be a string"};
}
const std::string type = msg["type"];
if (type != "request" && type != "response" && type != "event") {
return {false, "unknown_type",
"type must be request, response, or event; got: " + type};
}
// seq
if (!msg.contains("seq") || !msg["seq"].is_number_integer()) {
return {false, "missing_seq", "seq must be an integer"};
}
if (msg["seq"].get<int>() < 1) {
return {false, "invalid_seq", "seq must be >= 1"};
}
// command / event field
if (type == "request" || type == "response") {
if (!msg.contains("command") || !msg["command"].is_string()
|| msg["command"].get<std::string>().empty()) {
return {false, "missing_command",
"request/response must have a non-empty command"};
}
} else { // event
if (!msg.contains("event") || !msg["event"].is_string()
|| msg["event"].get<std::string>().empty()) {
return {false, "missing_event",
"event message must have a non-empty event field"};
}
}
return {true, "", ""};
}
// Validate a batch of messages.
std::vector<DAPValidationResult> validateBatch(
const std::vector<nlohmann::json>& msgs) const {
std::vector<DAPValidationResult> results;
results.reserve(msgs.size());
for (const auto& m : msgs)
results.push_back(validate(m));
return results;
}
// Static type predicates (do not validate seq/command).
static bool isRequest(const nlohmann::json& msg) {
return msg.contains("type") && msg["type"] == "request";
}
static bool isResponse(const nlohmann::json& msg) {
return msg.contains("type") && msg["type"] == "response";
}
static bool isEvent(const nlohmann::json& msg) {
return msg.contains("type") && msg["type"] == "event";
}
};
} // namespace whetstone

View File

@@ -0,0 +1,21 @@
#pragma once
// Sprint 281 Integration Summary
// Steps 19331937: DAP Hardening
//
// 1933: DAPRequestValidator — validate DAP message shape
// 1934: DAPCapabilityNegotiator — per-adapter command registry
// 1935: DAPEventBroadcaster — subscribe/broadcast DAP events
// 1936: DAPBreakpointHitDispatcher — route hit to correct language adapter
// 1937: Integration — Python+Rust DAP session round-trip
#include <string>
namespace whetstone {
struct Sprint281IntegrationSummary {
int stepsCompleted = 5;
bool success = true;
std::string sprintName() const { return "Sprint 281: DAP Hardening"; }
};
} // namespace whetstone