tempo-silence-is-abstention: the beat is the lower median of cast votes only; a Persona turn admits a Pulse only for a vote, a withdrawal or a nonzero urgency
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
743cbb8b43
commit
acee839b4b
6 changed files with 31 additions and 21 deletions
|
|
@ -586,7 +586,7 @@ pub fn poll(organs: &Organs, config: &Config) -> Result<Outcome> {
|
|||
rows.push(row);
|
||||
}
|
||||
|
||||
// The tempo is the tally over every Mind of the roster, braked or not; a Mind with no vote or no readable mind counts as the default beat.
|
||||
// The tempo is the tally over the votes cast by the Minds of the roster, braked or not; a Mind with no vote or no readable mind abstains and is not counted.
|
||||
let votes: Vec<Option<u32>> = rows.iter().map(|row| row.read.as_ref().and_then(|read| read.vote)).collect();
|
||||
let beat_s = tempo::tally(&votes);
|
||||
|
||||
|
|
|
|||
|
|
@ -689,7 +689,7 @@ fn a_null_vote_withdraws_the_standing_one_and_an_urgency_only_pulse_leaves_it()
|
|||
rig.poll();
|
||||
let withdrawn = rig.store.traces.borrow().last().cloned().unwrap();
|
||||
assert_eq!(withdrawn.instances[ANN].vote_beat_s, None, "an explicit null withdraws it");
|
||||
assert_eq!(withdrawn.tempo_s, 900, "she counts as silent again");
|
||||
assert_eq!(withdrawn.tempo_s, 900, "she abstains again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ pub fn reply_schema(rooms: &[String], campaigns: &[String]) -> String {
|
|||
}
|
||||
},
|
||||
"start_self_run": start,
|
||||
"tempo_vote_beat_s": { "description": "Omit this key to leave your standing vote unchanged. A number votes that many seconds per beat. null only withdraws your vote, so you count as silent again.", "anyOf": [{ "type": "null" }, { "type": "integer", "minimum": TEMPO_VOTE_MIN_S, "maximum": TEMPO_VOTE_MAX_S }] },
|
||||
"tempo_vote_beat_s": { "description": "Omit this key to leave your standing vote unchanged. A number votes that many seconds per beat. null only withdraws your vote, so you abstain again and are not counted.", "anyOf": [{ "type": "null" }, { "type": "integer", "minimum": TEMPO_VOTE_MIN_S, "maximum": TEMPO_VOTE_MAX_S }] },
|
||||
"urgency_permille": { "anyOf": [{ "type": "null" }, { "type": "integer", "minimum": 0, "maximum": URGENCY_MAX_PERMILLE }] }
|
||||
}
|
||||
})
|
||||
|
|
@ -692,7 +692,7 @@ fn one_turn(
|
|||
}
|
||||
}
|
||||
}
|
||||
if reply.tempo_vote_beat_s.is_some() || reply.urgency_permille.is_some() {
|
||||
if reply.tempo_vote_beat_s.is_some() || reply.urgency_permille.is_some_and(|urgency| urgency > 0) {
|
||||
outputs.push(TraceItem { provenance: "her own".into(), room: String::new(), text: format!("pulse tempo_vote_beat_s={:?} urgency_permille={:?}", reply.tempo_vote_beat_s, reply.urgency_permille) });
|
||||
if !admit_pulse(organs, run, turn, &reply)? {
|
||||
organs.trace.record(&record(outputs, "failed: pulse".into()))?;
|
||||
|
|
@ -832,7 +832,7 @@ fn admit_pulse(organs: &Organs, run: &PipelineRef, turn: u32, reply: &PersonaRep
|
|||
/// The tempo the Minds hold between them, from the newest `heartbeat.cc` trace:
|
||||
/// the tally, and each Mind vote and declared urgency, her own marked.
|
||||
fn render_tempo(view: Option<&dyn TempoView>, instance: &Slug) -> String {
|
||||
let guidance = "The swarm beat is the lower median of every Mind newest vote. You vote with tempo_vote_beat_s (60 to 86400 seconds per beat; omit the key to leave your standing vote as it is, and send null to withdraw it so you count as silent again) and declare your own urgency with urgency_permille (0 to 1000, null for none); an urgency lasts until your next turn, so use it as etiquette, not as a habit.";
|
||||
let guidance = "The swarm beat is the lower median of the votes cast: a Mind with no standing vote abstains and is not counted, and with no vote cast the beat is 900 s. A vote only needs changing when conditions change; leave it standing otherwise. You vote with tempo_vote_beat_s (60 to 86400 seconds per beat; omit the key to leave your standing vote as it is, and send null to withdraw it and abstain) and declare your own urgency with urgency_permille (0 to 1000, null for none); an urgency lasts until your next turn, so use it as etiquette, not as a habit.";
|
||||
let latest: Option<PollTrace> = view.and_then(|view| view.latest().ok().flatten());
|
||||
let Some(trace) = latest else { return format!("Tempo: not yet read. {guidance}") };
|
||||
let mut out = format!("Tempo: the beat is {} s. {guidance}\nMinds:\n", trace.tempo_s);
|
||||
|
|
|
|||
|
|
@ -1143,6 +1143,14 @@ fn an_explicit_null_vote_is_a_pulse_that_withdraws_and_an_omitted_vote_is_no_vot
|
|||
assert!(silent.mind.ids(K::Pulse).is_empty(), "no vote key and no urgency is no pulse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zero_urgency_alone_is_no_pulse() {
|
||||
let rig = Rig::new(vec![operator_says(1, "go")], &[voting(None, json!(0))]);
|
||||
let Outcome::Served { failure, .. } = rig.go() else { panic!("served") };
|
||||
assert_eq!(failure, None);
|
||||
assert!(rig.mind.ids(K::Pulse).is_empty(), "urgency 0 declares nothing");
|
||||
}
|
||||
|
||||
/// A Self run of hers, closed Recorded with `rationale`.
|
||||
fn self_run_closed_with(rig: &Rig, label: &str, rationale: &str) {
|
||||
rig.mind.committed(vec![run(label, RunTurn::SelfRun, RunOperator::Mind, &[])]);
|
||||
|
|
|
|||
|
|
@ -9,26 +9,25 @@
|
|||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
/// The beat, in seconds, when no Mind has voted.
|
||||
/// The beat, in seconds, when no vote is cast.
|
||||
pub const DEFAULT_BEAT_S: u32 = 900;
|
||||
|
||||
/// How many orders of magnitude a full urgency may multiply a Mind's wait by.
|
||||
/// Interim, per `swarm-shares-log-bound`, until the swarm's governance sets it.
|
||||
pub const LOG_BOUND_ORDERS: f64 = 1.0;
|
||||
|
||||
/// The swarm's tempo (ruling `eureka-body:ruling:tempo-silent-means-default`):
|
||||
/// the lower median over every Mind in the roster, each her newest vote in seconds
|
||||
/// per beat. A Mind with no standing vote, or whose mind cannot be read, is
|
||||
/// `None` and counts as `DEFAULT_BEAT_S`. The lower median moves below the
|
||||
/// default only when at least half the roster, rounded up, votes at or below it
|
||||
/// (ruling `eureka-body:ruling:tempo-lower-median-stands`): on a roster of one or
|
||||
/// two a single Mind decides, on the live eight it takes four.
|
||||
/// The swarm's tempo (ruling `eureka-body:ruling:tempo-silence-is-abstention`):
|
||||
/// the lower median of the votes actually cast, each a Mind's newest standing
|
||||
/// vote in seconds per beat. Silence is abstention: a Mind with no standing vote
|
||||
/// (never voted, or withdrew) or whose mind cannot be read is `None` and is not
|
||||
/// counted, so the beat follows those who care to vote. With no vote cast it is
|
||||
/// `DEFAULT_BEAT_S`. On one voter that voter decides; on two the lower of them.
|
||||
pub fn tally(votes: &[Option<u32>]) -> u32 {
|
||||
let mut sorted: Vec<u32> = votes.iter().map(|vote| vote.unwrap_or(DEFAULT_BEAT_S)).collect();
|
||||
sorted.sort_unstable();
|
||||
match sorted.len() {
|
||||
let mut cast: Vec<u32> = votes.iter().flatten().copied().collect();
|
||||
cast.sort_unstable();
|
||||
match cast.len() {
|
||||
0 => DEFAULT_BEAT_S,
|
||||
n => sorted[(n - 1) / 2],
|
||||
n => cast[(n - 1) / 2],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,10 +52,13 @@ fn the_tally_is_the_lower_median_and_one_outlier_cannot_move_it() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn a_silent_or_unreadable_mind_counts_as_the_default_beat() {
|
||||
let silent = |voters: usize, vote: u32, total: usize| -> Vec<Option<u32>> { (0..total).map(|i| (i < voters).then_some(vote)).collect() };
|
||||
assert_eq!(tally(&silent(1, 60, 8)), 900, "one voter among seven silent Minds cannot move the beat");
|
||||
assert_eq!(tally(&silent(5, 60, 8)), 60, "a majority of the roster moves it");
|
||||
fn silence_is_abstention_and_only_cast_votes_are_counted() {
|
||||
let mut eight = vec![None; 8];
|
||||
eight[0] = Some(60);
|
||||
assert_eq!(tally(&eight), 60, "one voter among eight silent Minds decides");
|
||||
assert_eq!(tally(&[None; 8]), DEFAULT_BEAT_S, "none cast is the default");
|
||||
assert_eq!(tally(&[Some(3600), None, Some(60)]), 60, "two cast: the lower median");
|
||||
assert_eq!(tally(&[Some(3600), Some(1800), None, None, Some(60)]), 1800, "abstainers do not dilute the median");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue