forked from yvv/decision
Fusion de la tête du chantier (924a40d : lots B2 et F) dans le lot D
Les deux côtés gardés partout. Aux coutures : - une objection (redit / contredit) porte sur deux articles, jamais sur un module ; un ordre ne se corrige pas, ni par son auteur ni par un autre ; une coquille du titre suit le chemin de F (l'auteur du titre, un administrateur pour le titre de départ, ou 3 jours), et documents.title suit à l'application ; - votable() couvre les modules ; leurs totaux passent par shown_tally, à l'écran et dans document.json ; - « Modules à voter » ne compte plus une version retenue par une objection (test vu rouge) ; la liste du groupe la compte toujours parmi les propositions en cours ; - le panier : une fonction pour les articles et les modules, avec les versions retenues de F ; un nouvel article se propose dans une section, à sa fin, et peut répondre à une objection ; - migrations 0007 (modules) puis 0008 (objections) ; - C25 et C26 supportent un groupe d'essai où une voix suffit (dès 16 membres). Boucle : fmt, clippy -D warnings, cargo test (227), svelte-check 0, vitest 55, build, e2e 35 / 35 (C1–C33 et M1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,12 @@ pub async fn require_open(state: &AppState, user: &IdentityRow, group: &GroupRow
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Does this identity open the group now (see `trustin_access::opens`)?
|
||||
pub async fn opens(state: &AppState, user: &IdentityRow, group: &GroupRow) -> AppResult<bool> {
|
||||
let has = granted(state, &group.slug, &user.id).await?;
|
||||
Ok(trustin_access::opens(&user.to_identity(), &group.to_group(), has))
|
||||
}
|
||||
|
||||
/// May this identity vote in the group (see `trustin_access::votes_in`)?
|
||||
pub async fn may_vote(state: &AppState, user: &IdentityRow, group: &GroupRow) -> AppResult<bool> {
|
||||
let has = granted(state, &group.slug, &user.id).await?;
|
||||
@@ -259,7 +265,9 @@ pub struct DocState {
|
||||
pub module_kinds: HashMap<String, ModuleKind>,
|
||||
pub proposals: Vec<ProposalRow>,
|
||||
pub tallies: HashMap<String, Tally>,
|
||||
/// Versions held back by a standing objection: not votable, never entering into force.
|
||||
pub blocked: HashSet<String>,
|
||||
/// Standing objections touching each version, either side.
|
||||
pub signal_counts: HashMap<String, i64>,
|
||||
pub electorate: u32,
|
||||
pub electorate_source: &'static str,
|
||||
@@ -344,32 +352,9 @@ pub async fn load_state(state: &AppState, doc_id: &str) -> AppResult<DocState> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct SignalRow {
|
||||
proposal_id: String,
|
||||
target_proposal_id: String,
|
||||
}
|
||||
let signals = sqlx::query_as::<_, SignalRow>(
|
||||
"SELECT s.proposal_id, s.target_proposal_id FROM signals s JOIN proposals p ON p.id = s.proposal_id
|
||||
JOIN units u ON u.id = p.unit_id WHERE u.doc_id = ? AND s.lifted_at IS NULL",
|
||||
)
|
||||
.bind(doc_id)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
let in_force: HashSet<&str> = units.iter().filter_map(|u| u.in_force_id.as_deref()).collect();
|
||||
let mut blocked = HashSet::new();
|
||||
let mut signal_counts: HashMap<String, i64> = HashMap::new();
|
||||
for s in &signals {
|
||||
*signal_counts.entry(s.proposal_id.clone()).or_default() += 1;
|
||||
*signal_counts.entry(s.target_proposal_id.clone()).or_default() += 1;
|
||||
// Two texts that contradict each other never stand in force together.
|
||||
if in_force.contains(s.target_proposal_id.as_str()) {
|
||||
blocked.insert(s.proposal_id.clone());
|
||||
}
|
||||
if in_force.contains(s.proposal_id.as_str()) {
|
||||
blocked.insert(s.target_proposal_id.clone());
|
||||
}
|
||||
}
|
||||
// A standing objection holds back the latest arrival of its pair (never a version
|
||||
// in force): not votable, and it cannot enter into force (ADR 0010).
|
||||
let (blocked, signal_counts) = crate::objections::held(state, doc_id, &units).await?;
|
||||
let (electorate, electorate_source) = electorate(state, &group).await?;
|
||||
let measure = participation::stored(state, &group.slug).await?;
|
||||
let participation = Participation::of(
|
||||
@@ -410,6 +395,25 @@ impl DocState {
|
||||
self.tallies.get(id).copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The totals shown to people: today the live count, as `tally`. The one
|
||||
/// place where a publication by steps would plug in — on the chain, who
|
||||
/// voted and when is public, and live totals tell what (the secret of the
|
||||
/// vote, review of 74f96be, a decision that waits for Yvv). Adoption reads
|
||||
/// `tally`; the published texts (`publish.rs`) would follow this one.
|
||||
pub fn shown_tally(&self, id: &str) -> Tally {
|
||||
self.tally(id)
|
||||
}
|
||||
|
||||
/// How far the threshold is, as shown: on `shown_tally`.
|
||||
pub fn shown_evaluation(&self, unit: &UnitRow, p: &ProposalRow) -> Option<Evaluation> {
|
||||
self.scale().evaluate(self.shown_tally(&p.id), self.voting_level(unit, p)).ok()
|
||||
}
|
||||
|
||||
/// Open to a vote: open or in force, and not held back by an objection.
|
||||
pub fn votable(&self, p: &ProposalRow) -> bool {
|
||||
matches!(p.status.as_str(), "open" | "in_force") && !self.blocked.contains(&p.id)
|
||||
}
|
||||
|
||||
/// The module a unit is, if it is one.
|
||||
pub fn module_of(&self, unit_id: &str) -> Option<ModuleKind> {
|
||||
self.module_kinds.get(unit_id).copied()
|
||||
@@ -627,7 +631,7 @@ fn proposal_json(
|
||||
me: &str,
|
||||
) -> serde_json::Value {
|
||||
let (mine, ballot) = mine;
|
||||
let eval = st.evaluation(unit, p);
|
||||
let eval = st.shown_evaluation(unit, p);
|
||||
let (kind, target) = match p.kind() {
|
||||
// A module's version says what it holds (ADR 0009).
|
||||
ProposalKind::Text if matches!(p.kind.as_str(), "order" | "title") => (p.kind.as_str(), None),
|
||||
@@ -646,11 +650,12 @@ fn proposal_json(
|
||||
"revision": p.revision, "createdAt": p.created_at, "updatedAt": p.updated_at,
|
||||
"submittedAt": p.submitted_at, "enteredAt": p.entered_at, "confirmedAt": p.confirmed_at,
|
||||
"seedMinimal": p.status == "in_force" && p.entered_at.is_none() && p.confirmed_at.is_none() && p.origin == "seed",
|
||||
"tally": st.tally(&p.id),
|
||||
"tally": st.shown_tally(&p.id),
|
||||
"evaluation": eval,
|
||||
"votedAtInertia": voted_at,
|
||||
"blocked": st.blocked.contains(&p.id),
|
||||
"signals": st.signal_counts.get(&p.id).copied().unwrap_or(0),
|
||||
"votable": st.votable(p),
|
||||
"objections": st.signal_counts.get(&p.id).copied().unwrap_or(0),
|
||||
"myVote": mine.map(|v| v.choice.as_str()),
|
||||
"myVoteStale": mine.is_some_and(|v| v.revision != p.revision),
|
||||
"myVoteChain": mine.is_some_and(|v| v.source == "chain"),
|
||||
|
||||
Reference in New Issue
Block a user