bake: name the five ways the extension can fail to answer, and move the landing verdict into a pure, tab-provable classifier

A no-answer stays a failure; it now says whether the extension never ran the landing, answered a stale generation, spoke a wire this build cannot read, cleared the request, or refused it.
This commit is contained in:
2026-08-02 06:38:04 -04:00
parent 6e937b9c61
commit 962ab64ef0
8 changed files with 373 additions and 48 deletions
+47
View File
@@ -2,6 +2,8 @@
#include "core/wire/bake_wire.h"
#include <utility>
#include "core/version/app_version.h"
#include "core/wire/wire.h"
@@ -122,4 +124,49 @@ std::optional<BakeOutcome> decodeBakeOutcome(const std::string& wire) {
return outcome;
}
BakeAnswer classifyBakeAnswer(const std::optional<std::string>& raw,
const BakeRequest& sent) {
BakeAnswer answer;
if (!raw || raw->empty()) {
answer.kind = BakeAnswerKind::Cleared;
return answer;
}
if (std::optional<BakeOutcome> outcome = decodeBakeOutcome(*raw)) {
answer.kind = outcome->generation == sent.generation
? BakeAnswerKind::Answered
: BakeAnswerKind::ForeignOutcome;
answer.outcome = std::move(outcome);
return answer;
}
// Still a request: whether it is OURS is what separates "nothing read this key" from
// "another instance overwrote it" — a persisted instanceGuid is copyable, so two
// instances CAN name one key.
if (const std::optional<BakeRequest> req = decodeBakeRequest(*raw)) {
answer.kind =
*req == sent ? BakeAnswerKind::Unanswered : BakeAnswerKind::ForeignRequest;
return answer;
}
answer.kind = BakeAnswerKind::Undecodable;
return answer;
}
BakeScanVerdict classifyBakeScan(const BakeScanContext& session, const BakeScanKey& key,
std::int64_t nowSec) {
// Not a request: an outcome the writing instance has not collected yet, or a value
// from a build we do not read. The writer owns clearing its own key.
if (!key.decoded) return BakeScanVerdict::Ignore;
// Either direction, so a clock moved backwards is caught too.
const std::int64_t age = nowSec - key.generation;
if (age > kMaxRequestAgeSeconds || age < -kMaxRequestAgeSeconds)
return BakeScanVerdict::ClearStale;
// Three tabs have to agree before anything may land: the tab the request was found in,
// the tab the session's book/ledger last loaded, and the tab a persist will write into.
// Landing on a disagreement writes one tab's bake into another's bank.
const bool landable = session.sessionHasLoadedProject &&
session.loadedProjectIsActive && key.inLoadedProject;
return landable ? BakeScanVerdict::Land : BakeScanVerdict::RefuseWrongProject;
}
} // namespace reasampler::wire