Question nav: a followup is offered only once it is earned
The 0.3.34 rule listed every followup "further down" from the current page, so the front door offered the thank-you pages before anything was submitted, and a followup one typed in offered the ones beyond it. The chain half was weaker than it looked too: it matched the question the chain answered, which is a front door anyway. Now a followup is offered only when the visitor has earned it: their verified chain answered the alternative that leads there (the chain index records the alternative from here on; older nodes fall back to every recording alternative of the answered question, and nothing beyond it), or it hangs off an earned page by a link that records nothing. A page behind an answer they have not given is never offered. The walk ends at any front door, so "back to the start" never fans out. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
366ff51a0a
commit
e67e841f8c
+18
-20
@@ -2033,18 +2033,19 @@ pub async fn get_site() -> Result<SiteConfig, ServerFnError> {
|
||||
/// Every question the visitor may be offered from `current`, as (id,
|
||||
/// name) - the question nav's data. Front-door pages the visitor
|
||||
/// qualifies for are always in (an owner session sees the gated desks
|
||||
/// too); `followup` pages and ended events only when they are on the
|
||||
/// visitor's path - the page their verified `?chain=` answered - or
|
||||
/// further down from `current` (see `content::outcomes_reachable_from`).
|
||||
/// A chain unlocks the followup it came from, never every followup on
|
||||
/// the site; an unverifiable hash unlocks nothing.
|
||||
/// too). A `followup` page or an ended event is in only when the
|
||||
/// visitor has earned it: their verified `?chain=` answered the
|
||||
/// alternative that leads there, or it is reachable from an earned
|
||||
/// page over links that record nothing (see `content::earned_followups`).
|
||||
/// Nothing is offered that sits behind an answer they have not given;
|
||||
/// an unverifiable hash unlocks nothing.
|
||||
#[server(endpoint = "list_qualifying_questions")]
|
||||
pub async fn list_qualifying_questions(
|
||||
current: String,
|
||||
chain: Option<String>,
|
||||
) -> Result<Vec<(String, String)>, ServerFnError> {
|
||||
use crate::auth::{User, SESSION_USER_KEY};
|
||||
use crate::content::{is_qualified, outcomes_reachable_from, Question};
|
||||
use crate::content::{earned_followups, is_qualified, unlocked_by, Question};
|
||||
use crate::server::AppState;
|
||||
|
||||
let state = expect_context::<AppState>();
|
||||
@@ -2056,26 +2057,23 @@ pub async fn list_qualifying_questions(
|
||||
|
||||
let now_ms = chrono::Utc::now().timestamp_millis();
|
||||
let questions = state.questions.load();
|
||||
// The one page the chain verifiably answered - the same lookup a
|
||||
// What the chain verifiably answered - the same lookup a
|
||||
// `requires_chain` gate does, so a forged or foreign hash resolves
|
||||
// to nothing relevant.
|
||||
let answered: Option<String> = match &chain {
|
||||
Some(hash) => crate::chain::lookup_node(&state.jetstream, hash)
|
||||
.await
|
||||
.map(|node| node.question_id),
|
||||
None => None,
|
||||
// to nothing - and therefore where it sent the visitor.
|
||||
let seeds: Vec<String> = match &chain {
|
||||
Some(hash) => match crate::chain::lookup_node(&state.jetstream, hash).await {
|
||||
Some(node) => unlocked_by(&questions, &node.question_id, node.alternative.as_deref()),
|
||||
None => Vec::new(),
|
||||
},
|
||||
None => Vec::new(),
|
||||
};
|
||||
// An ended event page is a followup from then on.
|
||||
let is_outcome = |q: &Question| q.is_followup() || q.event_ended_at(now_ms);
|
||||
let reach = outcomes_reachable_from(&questions, ¤t, &is_outcome);
|
||||
let earned = earned_followups(&questions, ¤t, &seeds, &is_outcome);
|
||||
let mut out: Vec<(String, String)> = questions
|
||||
.values()
|
||||
.filter(|q| is_qualified(user.as_ref(), q))
|
||||
.filter(|q| {
|
||||
!is_outcome(q)
|
||||
|| answered.as_deref() == Some(q.id.as_str())
|
||||
|| reach.contains(&q.id)
|
||||
})
|
||||
.filter(|q| !is_outcome(q) || earned.contains(&q.id))
|
||||
// A live event is announced in the header instead.
|
||||
.filter(|q| !q.event_live_at(now_ms))
|
||||
// A dynamic page has no URL of its own to link to.
|
||||
@@ -2225,7 +2223,7 @@ pub async fn submit_answer(
|
||||
// Index the node so requires_chain pages can verify lineage.
|
||||
// Best-effort: the NATS event above is the durable record.
|
||||
if let Err(e) =
|
||||
crate::chain::record_node(&state.jetstream, &chain_hash, &question_id, timestamp_ms).await
|
||||
crate::chain::record_node(&state.jetstream, &chain_hash, &question_id, &alternative, timestamp_ms).await
|
||||
{
|
||||
tracing::error!("failed to index chain node: {e}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user