Desk wire: the showability gate no longer shadows a stored answer's replay
answer_desk's (answerer, request_id) lookup is now bound_answer, pub(crate) in desk.rs, and Mind::desk_answer asks it so the gate runs only when nothing is stored. Finding desk-wire-gate-shadows-replay. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0a9d69679c
commit
905cdd6793
2 changed files with 38 additions and 10 deletions
|
|
@ -142,6 +142,15 @@ fn counts_under(answer: &PipelineDeskAnswer, current: &Sha256Hex) -> bool {
|
|||
matches!(answer.outcome, DeskOutcome::Counted | DeskOutcome::Revision { .. }) && answer.options_hash == *current
|
||||
}
|
||||
|
||||
/// The answer already stored for `(answerer, request_id)`, if any: the one retry lookup, which
|
||||
/// `answer_desk` runs before it reads the fork and the door runs before its showability gate.
|
||||
pub(crate) fn bound_answer<'a>(docs: &'a Docs<'_>, answerer: &Short, request_id: &RequestId) -> Option<(&'a str, &'a PipelineDeskAnswer)> {
|
||||
docs.of_kind(PipelineKind::DeskAnswer).find_map(|(key, document)| match document {
|
||||
PipelineDocument::DeskAnswer(held) if held.answerer == *answerer && held.request_id == *request_id => Some((key, held)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The answer that counts for `fork` now: the lowest-sequence answer whose stored
|
||||
/// outcome is Counted or Revision under the card's current options hash and that no
|
||||
/// Retraction names (R4; an answer under an older hash stops counting). Derived on
|
||||
|
|
@ -318,11 +327,7 @@ impl<S: MindStore> Mind<S> {
|
|||
// the fork is read, so a retry gets its stored outcome without re-validating and even if the
|
||||
// card has since changed. A malformed request_id binds no record and meets the probe below.
|
||||
// The key is (answerer, request_id).
|
||||
let bound = docs.of_kind(PipelineKind::DeskAnswer).find_map(|(key, document)| match document {
|
||||
PipelineDocument::DeskAnswer(held) if held.answerer == stamp.answerer && held.request_id == request.request_id => Some((key, held)),
|
||||
_ => None,
|
||||
});
|
||||
if let Some((key, held)) = bound {
|
||||
if let Some((key, held)) = bound_answer(&docs, &stamp.answerer, &request.request_id) {
|
||||
if held.body_sha256 != body_sha256 {
|
||||
return Err(MindRefusal::DeskRequestIdConflict { request_id: request.request_id.0.clone() });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ use eureka_pipeline::{
|
|||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::desk::{DeskAnswerOutcome, DeskAnswerRequest, DeskBody, DeskStamp, counted_answer};
|
||||
use crate::desk::{DeskAnswerOutcome, DeskAnswerRequest, DeskBody, DeskStamp, bound_answer, counted_answer};
|
||||
use crate::docs::Docs;
|
||||
use crate::mind::Mind;
|
||||
use crate::refusal::MindRefusal;
|
||||
|
|
@ -323,10 +323,11 @@ impl<S: MindStore> Mind<S> {
|
|||
Err(refused) => return Ok((DeskResponse::NotStored(refused), false)),
|
||||
};
|
||||
let image = self.image()?;
|
||||
let unshowable = matches!(
|
||||
Docs::at(image, image.head).find(PipelineKind::Question, &sent.fork.0),
|
||||
Some(PipelineDocument::Question(question)) if !showable(question, deciders)
|
||||
);
|
||||
let docs = Docs::at(image, image.head);
|
||||
// A request already stored replays whatever the list is now: the lookup is `answer_desk`'s,
|
||||
// asked here only so the gate does not shadow it.
|
||||
let unshowable = bound_answer(&docs, &subject, &sent.request_id).is_none()
|
||||
&& matches!(docs.find(PipelineKind::Question, &sent.fork.0), Some(PipelineDocument::Question(question)) if !showable(question, deciders));
|
||||
if unshowable {
|
||||
return Ok((DeskResponse::NotStored(fork_unknown()), false));
|
||||
}
|
||||
|
|
@ -886,6 +887,28 @@ mod tests {
|
|||
assert_eq!(answered(ask(&mut mind, "x", asked).0).0.outcome, "Counted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stored_answer_replays_whatever_the_decider_list_is_now() {
|
||||
let mut mind = desk();
|
||||
let hash = hash_of(&mind, FORK);
|
||||
let request = wire(FORK, &hash);
|
||||
let (stored, replayed) = ask(&mut mind, "x", request.clone());
|
||||
assert!(!replayed);
|
||||
let before = head(&mind);
|
||||
// The list Bifrost passes is empty now; the stored request still gets its stored answer.
|
||||
let (again, replayed) = ask_as(&mut mind, "x", &[], request.clone());
|
||||
assert!(replayed);
|
||||
assert_eq!(encode(&again), encode(&stored));
|
||||
assert_eq!(head(&mind), before);
|
||||
// Another subject with the same bytes has stored nothing: the gate refuses it.
|
||||
let (other, replayed) = ask_as(&mut mind, "y", &[], request);
|
||||
assert_eq!((not_stored(other).code, replayed), (ForkRefusalCode::ForkUnknown, false));
|
||||
// And a request nobody stored is fork_unknown while the list is empty.
|
||||
let fresh = wire(FORK, &hash);
|
||||
assert_eq!(not_stored(ask_as(&mut mind, "x", &[], fresh).0).code, ForkRefusalCode::ForkUnknown);
|
||||
assert_eq!((head(&mind), stored_count(&mind, FORK)), (before, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn showable_needs_two_options_text_depends_and_a_decider() {
|
||||
let D::Question(plain) = question("Q9", &["A", "B"], "A") else { unreachable!() };
|
||||
|
|
|
|||
Loading…
Reference in a new issue