Lot V, interface : les types du contrat (tallyPending, tallySimulated, myVoteState, vote.simulated, mode des bulletins, publication.waitsFor, messages « ballot ») ; i18n des états (simulé, écarté par une objection, messages d'un vote écarté) en fr, en, es ; la boîte « Messages » dit les votes écartés ; « Tes bulletins » marque un vote simulé

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 05:49:57 +01:00
co-authored by Claude Opus 5.5
parent e8e9633b75
commit f62347cc6b
8 changed files with 111 additions and 16 deletions
+11 -1
View File
@@ -255,6 +255,10 @@ export const en: Messages = {
'vote.mine.chain': 'recorded on the Ğ1 chain',
'vote.mine.saved': 'saved',
'vote.mine.pending': 'Waiting for the Ğ1 chain: {choice}.',
'vote.mine.simulated': 'simulated',
'vote.simulated.note': 'The Ğ1 chain is not open yet: your vote is simulated. It counts as a vote, marked “simulated”, until it is erased when the chain opens.',
'vote.tally.simulated.one': 'of which {n} simulated',
'vote.tally.simulated.other': 'of which {n} simulated',
'vote.mine.refused': 'Your last vote was not counted: {reason}',
'vote.mine.expired': 'Your last vote was not seen on the Ğ1 chain in time: it does not count.',
'vote.mine.receipts': 'Your ballots and receipts',
@@ -269,6 +273,7 @@ export const en: Messages = {
'ballot.reason.not_member': 'the wallet that signed is not that of a member of the Ğ1 web of trust.',
'ballot.reason.no_identity': 'the wallet that signed is linked to no Ğ1 identity.',
'ballot.reason.transfer': 'the transfer to the vote account is missing, or its amount is too low.',
'ballot.reason.objection': 'an objection was holding this version back when the Ğ1 chain confirmed your vote; your earlier vote stands.',
'basket.title': 'Voting basket',
'basket.intro': 'Choose article by article, a whole section or the whole document: it all goes in a single gesture.',
'basket.doc': 'The whole document',
@@ -336,6 +341,7 @@ export const en: Messages = {
'ballots.lines.other': '{n} votes',
'ballots.status.pending': 'Waiting for the Ğ1 chain',
'ballots.status.confirmed': 'Counted for the Ğ1 identity “{name}”',
'ballots.status.simulated': 'Simulated: counted for “{name}” until the Ğ1 chain opens',
'ballots.status.refused': 'Not counted: {reason}',
'ballots.status.expired': 'Not seen on the Ğ1 chain in time: it does not count',
'ballots.block': 'block {n}',
@@ -475,7 +481,7 @@ export const en: Messages = {
'objection.respond.contredit': 'Reword',
'objection.linked': 'This proposal answers an objection: once submitted, the person who raised it lifts it or reiterates it.',
'messages.title': 'Messages',
'messages.intro': 'What waits for a gesture from you: an objection to answer, a typo fix to validate. Each message carries its deadline; a message dealt with asks nothing more.',
'messages.intro': 'What waits for a gesture from you: an objection to answer, a typo fix to validate. Each message carries its deadline; a message dealt with asks nothing more. You also read here what became of a vote that was not counted.',
'messages.none': 'No messages for now.',
'messages.readAll': 'Mark all as read',
'messages.read': 'Mark as read',
@@ -484,12 +490,16 @@ export const en: Messages = {
'messages.open': 'See it in the text',
'messages.topic.objection': 'Objection',
'messages.topic.typo': 'Typo',
'messages.topic.ballot': 'Vote',
'messages.objection_raised.redit': '{name} raises an objection: your version “{held}” repeats “{other}”. Until it is lifted, nobody can vote for it any more. To answer, change the article it repeats.',
'messages.objection_raised.contredit': '{name} raises an objection: your version “{held}” contradicts “{other}”. Until it is lifted, nobody can vote for it any more. To answer, reword it.',
'messages.objection_reiterated': '{name} reiterates their objection: nobody can vote for “{held}” yet. Answer again.',
'messages.objection_answered': 'Your objection on “{held}” has been answered. Lift it if the answer suits you, or reiterate it. With no answer from you, it is lifted.',
'messages.typo_pending.author': '{name} suggests a typo fix in your version “{title}”.',
'messages.typo_pending.admin': '{name} suggests a typo fix in “{title}”, a seed text: an administrator validates it.',
'messages.ballot_expired': 'Your vote of {date} ({articles}) was not seen on the Ğ1 chain within 24 hours: it is set aside. You can vote again.',
'messages.ballot_refused': 'Your vote of {date} ({articles}) was not counted: {reason} You can vote again.',
'messages.ballot_objection': 'Your “for” on {articles} is set aside: an objection was holding this version back when the Ğ1 chain confirmed your vote. Your earlier vote stands.',
'messages.respond': 'Answer',
'messages.outcome.answered': 'answered',
'messages.outcome.reiterated': 'objection reiterated',
+11 -1
View File
@@ -255,6 +255,10 @@ export const es: Messages = {
'vote.mine.chain': 'inscrito en la cadena Ğ1',
'vote.mine.saved': 'registrado',
'vote.mine.pending': 'Esperando a la cadena Ğ1: {choice}.',
'vote.mine.simulated': 'simulado',
'vote.simulated.note': 'La cadena Ğ1 aún no está abierta: tu voto es simulado. Cuenta como un voto, marcado «simulado», hasta que se borre al abrirse la cadena.',
'vote.tally.simulated.one': 'de ellos {n} simulado',
'vote.tally.simulated.other': 'de ellos {n} simulados',
'vote.mine.refused': 'Tu último voto no se contó: {reason}',
'vote.mine.expired': 'Tu último voto no se vio a tiempo en la cadena Ğ1: no cuenta.',
'vote.mine.receipts': 'Tus papeletas y recibos',
@@ -269,6 +273,7 @@ export const es: Messages = {
'ballot.reason.not_member': 'el monedero que firmó no es el de un miembro de la red de confianza Ğ1.',
'ballot.reason.no_identity': 'el monedero que firmó no está vinculado a ninguna identidad Ğ1.',
'ballot.reason.transfer': 'falta la transferencia a la cuenta de los votos, o su importe es demasiado bajo.',
'ballot.reason.objection': 'una objeción retenía esta versión cuando la cadena Ğ1 confirmó tu voto; tu voto anterior se mantiene.',
'basket.title': 'Cesta de votación',
'basket.intro': 'Elige artículo por artículo, una sección entera o todo el documento: todo sale en un solo gesto.',
'basket.doc': 'Todo el documento',
@@ -336,6 +341,7 @@ export const es: Messages = {
'ballots.lines.other': '{n} votos',
'ballots.status.pending': 'Esperando a la cadena Ğ1',
'ballots.status.confirmed': 'Contado para la identidad Ğ1 «{name}»',
'ballots.status.simulated': 'Simulado: contado para «{name}» hasta que se abra la cadena Ğ1',
'ballots.status.refused': 'No contado: {reason}',
'ballots.status.expired': 'No visto a tiempo en la cadena Ğ1: no cuenta',
'ballots.block': 'bloque {n}',
@@ -475,7 +481,7 @@ export const es: Messages = {
'objection.respond.contredit': 'Reformular',
'objection.linked': 'Esta propuesta responde a una objeción: una vez enviada, la persona que la hizo la levanta o la reitera.',
'messages.title': 'Mensajes',
'messages.intro': 'Lo que espera un gesto tuyo: una objeción que responder, una corrección de errata que validar. Cada mensaje lleva su plazo; un mensaje tratado ya no pide nada.',
'messages.intro': 'Lo que espera un gesto tuyo: una objeción que responder, una corrección de errata que validar. Cada mensaje lleva su plazo; un mensaje tratado ya no pide nada. Aquí lees también qué fue de un voto que no se contó.',
'messages.none': 'Ningún mensaje por ahora.',
'messages.readAll': 'Marcar todo como leído',
'messages.read': 'Marcar como leído',
@@ -484,12 +490,16 @@ export const es: Messages = {
'messages.open': 'Verlo en el texto',
'messages.topic.objection': 'Objeción',
'messages.topic.typo': 'Errata',
'messages.topic.ballot': 'Voto',
'messages.objection_raised.redit': '{name} hace una objeción: tu versión «{held}» repite «{other}». Hasta que se levante, nadie puede votar a favor. Para responder, modifica el artículo que repite.',
'messages.objection_raised.contredit': '{name} hace una objeción: tu versión «{held}» contradice «{other}». Hasta que se levante, nadie puede votar a favor. Para responder, reformúlala.',
'messages.objection_reiterated': '{name} reitera su objeción: nadie puede votar aún a favor de «{held}». Responde de nuevo.',
'messages.objection_answered': 'Tu objeción sobre «{held}» recibió una respuesta. Levántala si te convence, o reitérala. Sin respuesta tuya, se levanta.',
'messages.typo_pending.author': '{name} propone una corrección de errata en tu versión «{title}».',
'messages.typo_pending.admin': '{name} propone una corrección de errata en «{title}», un texto del seed: un administrador la valida.',
'messages.ballot_expired': 'Tu voto del {date} ({articles}) no se vio en la cadena Ğ1 en 24 horas: queda descartado. Puedes votar de nuevo.',
'messages.ballot_refused': 'Tu voto del {date} ({articles}) no se contó: {reason} Puedes votar de nuevo.',
'messages.ballot_objection': 'Tu «a favor» sobre {articles} queda descartado: una objeción retenía esta versión cuando la cadena Ğ1 confirmó tu voto. Tu voto anterior se mantiene.',
'messages.respond': 'Responder',
'messages.outcome.answered': 'respuesta dada',
'messages.outcome.reiterated': 'objeción reiterada',
+11 -1
View File
@@ -259,6 +259,10 @@ export const fr = {
'vote.mine.chain': 'inscrit sur la chaîne Ğ1',
'vote.mine.saved': 'enregistré',
'vote.mine.pending': 'En attente de la chaîne Ğ1 : {choice}.',
'vote.mine.simulated': 'simulé',
'vote.simulated.note': "La chaîne Ğ1 n'est pas encore ouverte : ton vote est simulé. Il compte comme un vote, marqué « simulé », jusqu'à son effacement à l'ouverture de la chaîne.",
'vote.tally.simulated.one': 'dont {n} simulé',
'vote.tally.simulated.other': 'dont {n} simulés',
'vote.mine.refused': "Ton dernier vote n'a pas été compté : {reason}",
'vote.mine.expired': "Ton dernier vote n'a pas été vu sur la chaîne Ğ1 à temps : il ne compte pas.",
'vote.mine.receipts': 'Tes bulletins et reçus',
@@ -273,6 +277,7 @@ export const fr = {
'ballot.reason.not_member': "le portefeuille qui a signé n'est pas celui d'un membre de la toile de confiance Ğ1.",
'ballot.reason.no_identity': "le portefeuille qui a signé n'est rattaché à aucune identité Ğ1.",
'ballot.reason.transfer': 'le virement vers le compte des votes manque, ou son montant est trop faible.',
'ballot.reason.objection': "une objection retenait cette version quand la chaîne Ğ1 a confirmé ton vote ; ton vote d'avant reste.",
'basket.title': 'Panier de vote',
'basket.intro': 'Choisis article par article, une section entière ou tout le document : tout part en un seul geste.',
'basket.doc': 'Tout le document',
@@ -340,6 +345,7 @@ export const fr = {
'ballots.lines.other': '{n} votes',
'ballots.status.pending': 'En attente de la chaîne Ğ1',
'ballots.status.confirmed': "Compté pour l'identité Ğ1 « {name} »",
'ballots.status.simulated': "Simulé : compté pour « {name} » jusqu'à l'ouverture de la chaîne Ğ1",
'ballots.status.refused': 'Pas compté : {reason}',
'ballots.status.expired': 'Pas vu sur la chaîne Ğ1 à temps : il ne compte pas',
'ballots.block': 'bloc {n}',
@@ -479,7 +485,7 @@ export const fr = {
'objection.respond.contredit': 'Reformuler',
'objection.linked': "Cette proposition répond à une objection : une fois soumise, la personne qui l'a faite la lève ou la réitère.",
'messages.title': 'Messages',
'messages.intro': 'Ce qui attend un geste de toi : une objection à laquelle répondre, une correction de coquille à valider. Chaque message porte son échéance ; un message traité ne demande plus rien.',
'messages.intro': "Ce qui attend un geste de toi : une objection à laquelle répondre, une correction de coquille à valider. Chaque message porte son échéance ; un message traité ne demande plus rien. Tu y lis aussi ce qu'il advient d'un vote qui n'a pas été compté.",
'messages.none': "Aucun message pour l'instant.",
'messages.readAll': 'Tout marquer comme lu',
'messages.read': 'Marquer comme lu',
@@ -488,12 +494,16 @@ export const fr = {
'messages.open': 'Voir dans le texte',
'messages.topic.objection': 'Objection',
'messages.topic.typo': 'Coquille',
'messages.topic.ballot': 'Vote',
'messages.objection_raised.redit': "{name} fait une objection : ta version « {held} » redit « {other} ». Jusqu'à sa levée, personne ne peut plus voter pour elle. Pour répondre, modifie l'article visé.",
'messages.objection_raised.contredit': "{name} fait une objection : ta version « {held} » contredit « {other} ». Jusqu'à sa levée, personne ne peut plus voter pour elle. Pour répondre, reformule-la.",
'messages.objection_reiterated': '{name} réitère son objection : personne ne peut encore voter pour « {held} ». Réponds à nouveau.',
'messages.objection_answered': 'Ton objection sur « {held} » a reçu une réponse. Lève-la si elle te convient, ou réitère-la. Sans réponse de ta part, elle est levée.',
'messages.typo_pending.author': '{name} propose une correction de coquille dans ta version « {title} ».',
'messages.typo_pending.admin': '{name} propose une correction de coquille dans « {title} », un texte du seed : un administrateur la valide.',
'messages.ballot_expired': "Ton vote du {date} ({articles}) n'a pas été vu sur la chaîne Ğ1 en 24 heures : il est écarté. Tu peux voter à nouveau.",
'messages.ballot_refused': "Ton vote du {date} ({articles}) n'a pas été compté : {reason} Tu peux voter à nouveau.",
'messages.ballot_objection': "Ton « pour » sur {articles} est écarté : une objection retenait cette version quand la chaîne Ğ1 a confirmé ton vote. Ton vote d'avant reste.",
'messages.respond': 'Répondre',
'messages.outcome.answered': 'réponse donnée',
'messages.outcome.reiterated': 'objection réitérée',
+47 -5
View File
@@ -117,7 +117,14 @@ export interface Proposal {
seedMinimal: boolean;
/** A seed proposal (ADR 0011): open, put forward by the seed to start the vote, not in force. */
seedProposal?: boolean;
/** The totals shown: recorded, pending and simulated votes, each intention in place of
* the vote its session sees as its own (ADR 0012). */
tally: Tally;
/** The part of `tally` still without proof (an intention pending on the chain). */
tallyPending: Tally;
/** The part of `tally` that comes from the simulation (ADR 0012). */
tallySimulated: Tally;
/** On the totals shown (`tally`), the quorum included (ADR 0013). */
evaluation: Evaluation | null;
votedAtInertia: Inertia;
/** Held back by a standing objection: the latest arrival of the two (never a text in force). */
@@ -126,11 +133,17 @@ export interface Proposal {
votable: boolean;
/** Standing objections around this version. */
objections: number;
/** What the person sees as her vote now: her latest gesture, an intention pending included. */
myVote: 'for' | 'against' | null;
/** Where « ton vote » stands (ADR 0012): `recorded` — proven by the chain, or a vote of
* the server in a managed group; `pending` — an intention waiting for the chain;
* `simulated` — a vote of the simulation, while the chain is closed. */
myVoteState: 'recorded' | 'pending' | 'simulated' | null;
myVoteStale: boolean;
/** « Ton vote » was cast on the Ğ1 chain (confirmed). */
myVoteChain: boolean;
/** A ballot of this session not counted yet: pending on the chain, or refused, or let go. */
/** A ballot of this session not counted yet: pending on the chain, or refused, or let go —
* or its *for* set aside by an objection (`refused`, reason `objection`, ADR 0012). */
myBallot: MyBallot | null;
translations: Partial<Record<Lang, Translation>>;
/** An order version: the order it gives its section now. */
@@ -142,7 +155,7 @@ export type Choice = 'for' | 'against' | 'none';
export interface MyBallot {
id: string;
status: 'pending' | 'refused' | 'expired';
reason: 'not_member' | 'no_identity' | 'transfer' | null;
reason: 'not_member' | 'no_identity' | 'transfer' | 'objection' | null;
choice: Choice;
}
@@ -162,8 +175,12 @@ export interface ChainSettings {
/** How this session votes in the group of the document, and whether it may now. */
export interface VoteAccess {
/** `server` too while the chain is closed: the vote is then simulated (`simulated`). */
way: 'server' | 'chain';
open: boolean;
/** The chain is closed: the vote leaves by a write of the server, marked as simulated,
* and counts until an act « supprime » sets it aside (ADR 0012). */
simulated: boolean;
reason: 'members_only' | 'chain_off' | null;
chain: ChainSettings | null;
}
@@ -181,6 +198,8 @@ export interface Ballot {
voter: string | null;
entered: Entered[];
expiresAt: string;
/** `simulated`: a ballot of the simulation, proven by the server, never on the chain. */
mode: 'chain' | 'simulated';
}
/** A ballot of the person's history (`GET /api/ballots`): never anyone else's. */
@@ -195,6 +214,7 @@ export interface BallotEntry {
lines: number;
createdAt: string;
settledAt: string | null;
mode: Ballot['mode'];
}
export interface Unit {
@@ -234,6 +254,9 @@ export interface Publication {
doneAt?: string | null;
commit?: string | null;
attempts?: number;
/** Why it is suspended: no article in force yet (`text`, ADR 0011), or something in force
* entered by simulated votes that the proven votes do not carry yet (`proven`, ADR 0012). */
waitsFor?: 'text' | 'proven' | null;
}
export interface DocView {
@@ -345,11 +368,29 @@ export interface TypoFix {
version?: { id: string; title: string; status: Proposal['status']; unitId: string; code: string | null };
}
/** A message of one's box: about an objection or a typo fix, with its gestures now. */
/** A ballot set aside, as a message tells it (ADR 0012): line by line, what became of it. */
export interface BallotNotice {
id: string;
status: Ballot['status'];
reason: MyBallot['reason'];
mode: Ballot['mode'];
createdAt: string;
settledAt: string | null;
lines: { proposalId: string; unitId: string; code: string; title: string; choice: Choice; outcome: 'counted' | 'superseded' | 'objection' | null }[];
}
/** A message of one's box: about an objection, a typo fix or a ballot set aside, with its gestures now. */
export interface Message {
id: string;
topic: 'objection' | 'typo';
kind: 'objection_raised' | 'objection_answered' | 'objection_reiterated' | 'typo_pending';
topic: 'objection' | 'typo' | 'ballot';
kind:
| 'objection_raised'
| 'objection_answered'
| 'objection_reiterated'
| 'typo_pending'
| 'ballot_expired'
| 'ballot_refused'
| 'ballot_objection';
cycle: number;
createdAt: string;
dueAt: string | null;
@@ -361,4 +402,5 @@ export interface Message {
doc: { id: string; groupSlug: string; slug: string; title: LangMap };
objection?: Objection;
typo?: TypoFix;
ballot?: BallotNotice;
}
+5 -2
View File
@@ -27,6 +27,8 @@
function standing(b: BallotEntry): string {
if (b.status === 'pending') return t('ballots.status.pending');
// A vote of the simulation (ADR 0012): counted for the session, never on the chain.
if (b.status === 'confirmed' && b.mode === 'simulated') return t('ballots.status.simulated', { name: b.voter ?? '' });
if (b.status === 'confirmed') return t('ballots.status.confirmed', { name: b.voter ?? '' });
if (b.status === 'refused') return t('ballots.status.refused', { reason: t(`ballot.reason.${b.reason ?? 'transfer'}`) });
return t('ballots.status.expired');
@@ -62,7 +64,7 @@
{#each data.value.ballots as b (b.id)}
<li class="card block ballot">
<p class="small faint">{i18n.date(b.createdAt)} · {titleOf(b.doc.title)} · {i18n.plural('ballots.lines', b.lines)}</p>
<p class="state {b.status}">
<p class="state {b.status}" class:simulated={b.mode === 'simulated'}>
{standing(b)}{#if b.block !== null}<span class="faint"> · {t('ballots.block', { n: i18n.number(b.block) })}</span>{/if}
</p>
<code class="comment">{b.comment}</code>
@@ -159,7 +161,8 @@
.state.confirmed {
color: var(--moss);
}
.state.pending {
.state.pending,
.state.confirmed.simulated {
color: var(--water);
}
.state.refused,
+22 -2
View File
@@ -2,7 +2,8 @@
// The box « Messages » of trustinWallet, in the application (2026-10-31, lot F,
// ADR 0010): what waits for a gesture of the person — an objection to answer,
// to lift or to reiterate, a typo fix to validate or refuse —, each with its
// deadline. A message dealt with offers no gesture any more. No mail.
// deadline. A message dealt with offers no gesture any more. No mail. A ballot set
// aside (ADR 0012) is told here too, without gesture: one votes again in the text.
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { api } from '$lib/api';
@@ -44,10 +45,27 @@
await reload();
}
/** The lines of a ballot a message is about: those set aside by an objection, or all. */
function linesOf(m: Message) {
const lines = m.ballot?.lines ?? [];
const held = lines.filter((l) => l.outcome === 'objection');
return m.kind === 'ballot_objection' && held.length ? held : lines;
}
/** The text of a message, in the language of the reader, from its subject. */
function textOf(m: Message): string {
const o = m.objection;
const fix = m.typo;
const b = m.ballot;
if (b) {
const articles = linesOf(m)
.map((l) => l.code)
.join(', ');
const date = i18n.date(b.createdAt);
if (m.kind === 'ballot_expired') return t('messages.ballot_expired', { date, articles });
if (m.kind === 'ballot_refused') return t('messages.ballot_refused', { date, articles, reason: t(`ballot.reason.${b.reason ?? 'transfer'}`) });
return t('messages.ballot_objection', { articles });
}
if (o) {
const held = o.heldVersion?.title ?? '';
if (m.kind === 'objection_raised') return t(`messages.objection_raised.${o.kind}`, { name: o.objector ?? '', held, other: o.other.title ?? '' });
@@ -73,6 +91,8 @@
function hrefOf(m: Message): string {
const v = m.objection?.heldVersion ?? m.typo?.version ?? null;
const base = `/g/${m.doc.groupSlug}/docs/${m.doc.slug}`;
const line = linesOf(m)[0];
if (line) return `${base}?${panelSearch({ tab: 'details', unitId: line.unitId, proposalId: line.proposalId }, null)}`;
if (!v) return base;
return `${base}?${panelSearch({ tab: 'details', unitId: v.unitId ?? null, proposalId: v.id }, null)}`;
}
@@ -149,7 +169,7 @@
<p class="text">{textOf(m)}</p>
{#if m.objection?.reason && m.kind !== 'objection_answered'}<p class="reason">{m.objection.reason}</p>{/if}
{#if m.typo}<DiffText parts={condense(m.typo.diff)} />{/if}
{#if m.done}
{#if m.done && m.topic !== 'ballot'}
<p class="outcome">{outcomeOf(m)}</p>
{:else if m.dueAt}
<p class="due">{t('messages.due', { date: i18n.date(m.dueAt) })}</p>
+2 -2
View File
@@ -6,8 +6,8 @@ function prop(id: string, unitId: string, over: Partial<Proposal> = {}): Proposa
return {
id, unitId, kind: 'text', inertiaTarget: null, title: unitId, body: `texte ${id}`, status: 'open', origin: 'seed',
label: null, author: null, mine: false, revision: 1, createdAt: '', updatedAt: '', submittedAt: null, enteredAt: null,
confirmedAt: null, seedMinimal: false, tally: { for: 0, against: 0 }, evaluation: null, votedAtInertia: 3,
blocked: false, votable: true, objections: 0, myVote: null, myVoteStale: false, myVoteChain: false, myBallot: null, translations: {},
confirmedAt: null, seedMinimal: false, tally: { for: 0, against: 0 }, tallyPending: { for: 0, against: 0 }, tallySimulated: { for: 0, against: 0 }, evaluation: null, votedAtInertia: 3,
blocked: false, votable: true, objections: 0, myVote: null, myVoteState: null, myVoteStale: false, myVoteChain: false, myBallot: null, translations: {},
...over,
};
}
+2 -2
View File
@@ -6,9 +6,9 @@ function prop(id: string, unitId: string, over: Partial<Proposal> = {}): Proposa
return {
id, unitId, kind: 'text', inertiaTarget: null, title: unitId, body: `texte ${id}`, status: 'open', origin: 'seed',
label: null, author: null, mine: false, revision: 1, createdAt: '', updatedAt: '', submittedAt: null, enteredAt: null,
confirmedAt: null, seedMinimal: false, tally: { for: 0, against: 0 },
confirmedAt: null, seedMinimal: false, tally: { for: 0, against: 0 }, tallyPending: { for: 0, against: 0 }, tallySimulated: { for: 0, against: 0 },
evaluation: { cast: 0, required: 50, reached: false, missing: 50, requiredRatio: 1, wellRated: false },
votedAtInertia: 3, blocked: false, votable: true, objections: 0, myVote: null, myVoteStale: false, myVoteChain: false, myBallot: null, translations: {},
votedAtInertia: 3, blocked: false, votable: true, objections: 0, myVote: null, myVoteState: null, myVoteStale: false, myVoteChain: false, myBallot: null, translations: {},
...over,
};
}