Step 532: add deterministic retry and escalation protocol
This commit is contained in:
@@ -3667,4 +3667,13 @@ target_link_libraries(step531_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step532_test tests/step532_test.cpp)
|
||||
target_include_directories(step532_test PRIVATE src)
|
||||
target_link_libraries(step532_test PRIVATE
|
||||
nlohmann_json::nlohmann_json
|
||||
unofficial::tree-sitter::tree-sitter
|
||||
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||
|
||||
129
editor/src/RetryEscalationProtocol.h
Normal file
129
editor/src/RetryEscalationProtocol.h
Normal file
@@ -0,0 +1,129 @@
|
||||
#pragma once
|
||||
// Step 532: Retry/Escalation Protocol for Constraint Failures
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "ConstraintViolationDiagnostics.h"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
struct RetryAttempt {
|
||||
int attemptNumber = 0;
|
||||
bool success = false;
|
||||
std::vector<std::string> appliedFixes;
|
||||
ConstraintDiagnosticPacket diagnostics;
|
||||
};
|
||||
|
||||
struct RetryEscalationInput {
|
||||
ConstraintDiagnosticPacket initialDiagnostics;
|
||||
int retryBudget = 2;
|
||||
};
|
||||
|
||||
struct RetryEscalationResult {
|
||||
bool resolved = false;
|
||||
bool escalated = false;
|
||||
int attemptsUsed = 0;
|
||||
std::vector<RetryAttempt> attempts;
|
||||
ConstraintDiagnosticPacket finalDiagnostics;
|
||||
json escalationPacket;
|
||||
};
|
||||
|
||||
class RetryEscalationProtocol {
|
||||
public:
|
||||
static RetryEscalationResult run(const RetryEscalationInput& input) {
|
||||
RetryEscalationResult result;
|
||||
result.finalDiagnostics = input.initialDiagnostics;
|
||||
|
||||
if (input.initialDiagnostics.ok) {
|
||||
result.resolved = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (hasNonRetryable(input.initialDiagnostics)) {
|
||||
result.escalated = true;
|
||||
result.escalationPacket = makeEscalationPacket(input.initialDiagnostics,
|
||||
result.attempts,
|
||||
"non_retryable_violation");
|
||||
return result;
|
||||
}
|
||||
|
||||
int budget = input.retryBudget < 0 ? 0 : input.retryBudget;
|
||||
ConstraintDiagnosticPacket current = input.initialDiagnostics;
|
||||
|
||||
for (int attemptNo = 1; attemptNo <= budget; ++attemptNo) {
|
||||
RetryAttempt attempt;
|
||||
attempt.attemptNumber = attemptNo;
|
||||
attempt.diagnostics = applyDeterministicRepair(current, attempt.appliedFixes);
|
||||
attempt.success = attempt.diagnostics.ok;
|
||||
result.attempts.push_back(attempt);
|
||||
result.attemptsUsed = attemptNo;
|
||||
current = attempt.diagnostics;
|
||||
|
||||
if (current.ok) {
|
||||
result.resolved = true;
|
||||
result.finalDiagnostics = current;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
result.finalDiagnostics = current;
|
||||
result.escalated = true;
|
||||
result.escalationPacket = makeEscalationPacket(current,
|
||||
result.attempts,
|
||||
"retry_budget_exhausted");
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
static bool hasNonRetryable(const ConstraintDiagnosticPacket& packet) {
|
||||
for (const auto& v : packet.violations) {
|
||||
if (!v.retryable) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static ConstraintDiagnosticPacket applyDeterministicRepair(
|
||||
const ConstraintDiagnosticPacket& current,
|
||||
std::vector<std::string>& appliedFixes) {
|
||||
ConstraintDiagnosticPacket next;
|
||||
for (const auto& violation : current.violations) {
|
||||
if (isAutoCorrectable(violation)) {
|
||||
appliedFixes.push_back("drop-symbol:" + violation.field);
|
||||
continue;
|
||||
}
|
||||
next.violations.push_back(violation);
|
||||
}
|
||||
|
||||
next.ok = next.violations.empty();
|
||||
next.recommendedAction = next.ok ? "proceed"
|
||||
: (hasNonRetryable(next) ? "escalate" : "retry");
|
||||
return next;
|
||||
}
|
||||
|
||||
static bool isAutoCorrectable(const ConstraintViolation& violation) {
|
||||
return violation.retryable &&
|
||||
(violation.code == "out_of_scope_symbol" ||
|
||||
violation.code == "transient_scope_miss");
|
||||
}
|
||||
|
||||
static json makeEscalationPacket(const ConstraintDiagnosticPacket& finalDiag,
|
||||
const std::vector<RetryAttempt>& attempts,
|
||||
const std::string& reason) {
|
||||
json packet;
|
||||
packet["reason"] = reason;
|
||||
packet["finalDiagnostics"] = finalDiag.toJson();
|
||||
packet["attempts"] = json::array();
|
||||
for (const auto& a : attempts) {
|
||||
packet["attempts"].push_back({
|
||||
{"attemptNumber", a.attemptNumber},
|
||||
{"success", a.success},
|
||||
{"appliedFixes", a.appliedFixes},
|
||||
{"diagnostics", a.diagnostics.toJson()}
|
||||
});
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
};
|
||||
171
editor/tests/step532_test.cpp
Normal file
171
editor/tests/step532_test.cpp
Normal file
@@ -0,0 +1,171 @@
|
||||
// Step 532: Retry/Escalation Protocol (12 tests)
|
||||
|
||||
#include "RetryEscalationProtocol.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
static ConstraintViolation mkViolation(const std::string& code,
|
||||
const std::string& field,
|
||||
bool retryable) {
|
||||
return ConstraintViolation{code, code + " message", field, retryable};
|
||||
}
|
||||
|
||||
static ConstraintDiagnosticPacket mkPacket(
|
||||
const std::vector<ConstraintViolation>& violations) {
|
||||
ConstraintDiagnosticPacket p;
|
||||
p.violations = violations;
|
||||
p.ok = violations.empty();
|
||||
p.recommendedAction = p.ok ? "proceed" : "escalate";
|
||||
return p;
|
||||
}
|
||||
|
||||
void test_ok_packet_resolves_without_retry() {
|
||||
TEST(ok_packet_resolves_without_retry);
|
||||
RetryEscalationInput input{mkPacket({}), 2};
|
||||
auto result = RetryEscalationProtocol::run(input);
|
||||
CHECK(result.resolved, "ok packet should resolve");
|
||||
CHECK(!result.escalated, "ok packet should not escalate");
|
||||
CHECK(result.attemptsUsed == 0, "no attempts should be used");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_non_retryable_violation_escalates_immediately() {
|
||||
TEST(non_retryable_violation_escalates_immediately);
|
||||
RetryEscalationInput input{mkPacket({mkViolation("illegal_operation", "delete", false)}), 3};
|
||||
auto result = RetryEscalationProtocol::run(input);
|
||||
CHECK(!result.resolved, "non-retryable should not resolve");
|
||||
CHECK(result.escalated, "non-retryable should escalate");
|
||||
CHECK(result.attemptsUsed == 0, "should not spend retry attempts");
|
||||
CHECK(result.escalationPacket["reason"] == "non_retryable_violation", "wrong escalation reason");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_retryable_out_of_scope_resolves_in_one_retry() {
|
||||
TEST(retryable_out_of_scope_resolves_in_one_retry);
|
||||
RetryEscalationInput input{mkPacket({mkViolation("out_of_scope_symbol", "ghost", true)}), 2};
|
||||
auto result = RetryEscalationProtocol::run(input);
|
||||
CHECK(result.resolved, "retryable out-of-scope should resolve");
|
||||
CHECK(!result.escalated, "resolved packet should not escalate");
|
||||
CHECK(result.attemptsUsed == 1, "should resolve on first retry");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_mixed_retryable_and_non_retryable_escalates_without_retry() {
|
||||
TEST(mixed_retryable_and_non_retryable_escalates_without_retry);
|
||||
RetryEscalationInput input{mkPacket({
|
||||
mkViolation("out_of_scope_symbol", "ghost", true),
|
||||
mkViolation("forbidden_symbol", "unsafeGlobal", false)
|
||||
}), 2};
|
||||
auto result = RetryEscalationProtocol::run(input);
|
||||
CHECK(result.escalated, "mixed packet should escalate");
|
||||
CHECK(result.attemptsUsed == 0, "mixed packet should not retry");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_retry_budget_exhaustion_escalates() {
|
||||
TEST(retry_budget_exhaustion_escalates);
|
||||
RetryEscalationInput input{mkPacket({mkViolation("retryable_unknown", "x", true)}), 2};
|
||||
auto result = RetryEscalationProtocol::run(input);
|
||||
CHECK(!result.resolved, "unknown retryable should remain unresolved");
|
||||
CHECK(result.escalated, "budget exhaustion should escalate");
|
||||
CHECK(result.attemptsUsed == 2, "should consume full retry budget");
|
||||
CHECK(result.escalationPacket["reason"] == "retry_budget_exhausted", "wrong exhaustion reason");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_escalation_packet_contains_attempt_history() {
|
||||
TEST(escalation_packet_contains_attempt_history);
|
||||
RetryEscalationInput input{mkPacket({mkViolation("retryable_unknown", "x", true)}), 1};
|
||||
auto result = RetryEscalationProtocol::run(input);
|
||||
CHECK(result.escalationPacket.contains("attempts"), "packet should contain attempts");
|
||||
CHECK(result.escalationPacket["attempts"].size() == 1, "attempt count should match retries");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_protocol_is_deterministic_for_same_input() {
|
||||
TEST(protocol_is_deterministic_for_same_input);
|
||||
RetryEscalationInput input{mkPacket({mkViolation("retryable_unknown", "x", true)}), 2};
|
||||
auto a = RetryEscalationProtocol::run(input);
|
||||
auto b = RetryEscalationProtocol::run(input);
|
||||
CHECK(a.attemptsUsed == b.attemptsUsed, "attempt counts should match");
|
||||
CHECK(a.escalated == b.escalated, "escalation decision should be deterministic");
|
||||
CHECK(a.finalDiagnostics.violations.size() == b.finalDiagnostics.violations.size(),
|
||||
"final diagnostics should match");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_multiple_correctable_retryables_resolve_together() {
|
||||
TEST(multiple_correctable_retryables_resolve_together);
|
||||
RetryEscalationInput input{mkPacket({
|
||||
mkViolation("out_of_scope_symbol", "ghostA", true),
|
||||
mkViolation("transient_scope_miss", "ghostB", true)
|
||||
}), 2};
|
||||
auto result = RetryEscalationProtocol::run(input);
|
||||
CHECK(result.resolved, "correctable retryables should resolve");
|
||||
CHECK(result.attemptsUsed == 1, "should resolve in one deterministic pass");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_final_diagnostics_retry_when_unresolved_retryable() {
|
||||
TEST(final_diagnostics_retry_when_unresolved_retryable);
|
||||
RetryEscalationInput input{mkPacket({mkViolation("retryable_unknown", "x", true)}), 1};
|
||||
auto result = RetryEscalationProtocol::run(input);
|
||||
CHECK(!result.finalDiagnostics.ok, "final diagnostics should remain failing");
|
||||
CHECK(result.finalDiagnostics.recommendedAction == "retry", "final diagnostics should request retry");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_zero_retry_budget_escalates_for_retryable_failure() {
|
||||
TEST(zero_retry_budget_escalates_for_retryable_failure);
|
||||
RetryEscalationInput input{mkPacket({mkViolation("out_of_scope_symbol", "ghost", true)}), 0};
|
||||
auto result = RetryEscalationProtocol::run(input);
|
||||
CHECK(result.escalated, "zero budget should escalate unresolved failure");
|
||||
CHECK(result.attemptsUsed == 0, "no retries should be used");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_attempt_records_applied_fixes_for_correctable_failure() {
|
||||
TEST(attempt_records_applied_fixes_for_correctable_failure);
|
||||
RetryEscalationInput input{mkPacket({mkViolation("out_of_scope_symbol", "ghost", true)}), 2};
|
||||
auto result = RetryEscalationProtocol::run(input);
|
||||
CHECK(!result.attempts.empty(), "attempt history expected");
|
||||
CHECK(result.attempts[0].appliedFixes.size() == 1, "one fix should be recorded");
|
||||
CHECK(result.attempts[0].appliedFixes[0] == "drop-symbol:ghost", "fix metadata mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_escalation_packet_contains_final_diagnostics() {
|
||||
TEST(escalation_packet_contains_final_diagnostics);
|
||||
RetryEscalationInput input{mkPacket({mkViolation("retryable_unknown", "x", true)}), 1};
|
||||
auto result = RetryEscalationProtocol::run(input);
|
||||
CHECK(result.escalationPacket.contains("finalDiagnostics"), "packet should include final diagnostics");
|
||||
CHECK(result.escalationPacket["finalDiagnostics"]["ok"] == false,
|
||||
"final diagnostics in packet should indicate failure");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 532: Retry/Escalation Protocol\n";
|
||||
|
||||
test_ok_packet_resolves_without_retry(); // 1
|
||||
test_non_retryable_violation_escalates_immediately(); // 2
|
||||
test_retryable_out_of_scope_resolves_in_one_retry(); // 3
|
||||
test_mixed_retryable_and_non_retryable_escalates_without_retry(); // 4
|
||||
test_retry_budget_exhaustion_escalates(); // 5
|
||||
test_escalation_packet_contains_attempt_history(); // 6
|
||||
test_protocol_is_deterministic_for_same_input(); // 7
|
||||
test_multiple_correctable_retryables_resolve_together(); // 8
|
||||
test_final_diagnostics_retry_when_unresolved_retryable(); // 9
|
||||
test_zero_retry_budget_escalates_for_retryable_failure(); // 10
|
||||
test_attempt_records_applied_fixes_for_correctable_failure(); // 11
|
||||
test_escalation_packet_contains_final_diagnostics(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
38
progress.md
38
progress.md
@@ -8851,3 +8851,41 @@ post-apply contract expectations.
|
||||
- `editor/src/ContractDeltaChecker.h` within header-size limit (`149` <= `600`)
|
||||
- `editor/tests/step531_test.cpp` within test-file size guidance (`200` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
### Step 532: Retry/Escalation Protocol for Constraint Failures
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Implements deterministic retry/escalation behavior for constraint failures,
|
||||
including automatic correction for retryable scope failures and full failure
|
||||
packet escalation when non-retryable or unresolved conditions remain.
|
||||
|
||||
**Files added:**
|
||||
- `editor/src/RetryEscalationProtocol.h` - retry/escalation module:
|
||||
- immediate pass-through for already-valid diagnostics
|
||||
- immediate escalation for non-retryable violations
|
||||
- deterministic repair pass for correctable retryable failures
|
||||
- retry-budget control with attempt history
|
||||
- machine-readable escalation packet with reason/final diagnostics/attempts
|
||||
- `editor/tests/step532_test.cpp` - 12 tests covering:
|
||||
- resolved/no-retry path
|
||||
- immediate escalation for non-retryable and mixed failures
|
||||
- successful retry for correctable failures
|
||||
- retry-budget exhaustion behavior
|
||||
- escalation packet structure and attempt history
|
||||
- deterministic repeatability guarantees
|
||||
- zero-budget edge case
|
||||
- applied-fix metadata capture
|
||||
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` - `step532_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `cmake -S editor -B editor/build-native` - PASS
|
||||
- `cmake --build editor/build-native --target step532_test step531_test` - PASS
|
||||
- `./editor/build-native/step532_test` - PASS (12/12)
|
||||
- `./editor/build-native/step531_test` - PASS (12/12) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/RetryEscalationProtocol.h` within header-size limit (`129` <= `600`)
|
||||
- `editor/tests/step532_test.cpp` within test-file size guidance (`171` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
Reference in New Issue
Block a user