Abstimmung: mehrere Agenten pruefen unabhaengig und stimmen blind ab (#702)
A unit can put operations to a blind vote instead of its lead. Not opinions are counted but independent checks enforced. - UnitConfig.votes: operations, voters, approveAt, rejectAt, quorum, hours, perDay - set only by a human (OrgAdministration.SetVotes). - Core/Staging/Votes: routes a covered staged call before the lead; one task per voter with the frozen call and nothing else; cast_vote needs reasoning and evidence. Counted as soon as the result is settled; in between and without the quorum a human decides. The proposer does not vote, voters that read foreign content get no ballot, changes to agents never go to a vote. - Escalation tool: list_ballots, cast_vote. - Approvals view shows each vote with every ballot and the voters' costs; organisation view sets the rule and lists the votes held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d834e5ac38
commit
a5dd503d9b
@@ -2637,6 +2637,16 @@ button.org-einheit .org-name {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* #702: a vote beside the call — every ballot with its reasoning and evidence. */
|
||||
.staging-stimmen {
|
||||
list-style: none;
|
||||
margin: 8px 0 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* #693: the lead's spot checks and the measures waiting for the human. */
|
||||
.org-stichproben h4 {
|
||||
margin: 14px 0 6px;
|
||||
|
||||
@@ -138,6 +138,20 @@
|
||||
"org.measure.security": "Sicherheitsverdacht",
|
||||
"org.measureStatus.accepted": "angenommen",
|
||||
"org.measureStatus.dismissed": "abgelehnt",
|
||||
"org.votes": "Abstimmungen",
|
||||
"org.votesHint": "Operationen, die mehrere Agenten vor der Ausführung unabhängig prüfen und blind abstimmen — statt der Leitung. Stimmberechtigte wählen, die auf verschiedene Daten sehen (Konten, Lager, Sortiment): Es geht um unabhängige Prüfungen, nicht um eine Mehrheit. Wer beantragt, stimmt nicht mit; wer fremde Inhalte liest, bekommt keinen Stimmzettel; eine fehlende Stimme ist kein Ja. Zwischen Annahme und Ablehnung und ohne Quorum entscheidest du. Jede Stimme kostet einen Lauf.",
|
||||
"org.votesVoters": "Stimmberechtigte (ein Agent je Zeile)",
|
||||
"org.votesApproveAt": "Angenommen ab Ja-Stimmen",
|
||||
"org.votesRejectAt": "Abgelehnt bei höchstens Ja-Stimmen",
|
||||
"org.votesQuorum": "Mindestens abgegebene Stimmen (0: wie für Annahme)",
|
||||
"org.votesHours": "Stunden für die Abstimmung",
|
||||
"org.votesHeld": "Abgehaltene Abstimmungen",
|
||||
"org.voteWhen": "Wann",
|
||||
"org.voteOperation": "Aufruf",
|
||||
"org.voteTally": "Stimmen",
|
||||
"org.voteTallyText": "{yes} ja · {no} nein · {missing} fehlen",
|
||||
"org.voteOutcome": "Ergebnis",
|
||||
"org.voteCost": "Kosten der Abstimmenden",
|
||||
"org.ceilingInherited": "Von oben begrenzt auf: {tools}",
|
||||
"org.settings": "Sammelwerte",
|
||||
"org.settingsHint": "Werden nach einer Vorschau in die Agenten geschrieben. Die Einheit merkt sich den Wert nur als Vorgabe für neue Mitglieder — zur Laufzeit übertrumpft nichts einen Agenten.",
|
||||
@@ -659,6 +673,14 @@
|
||||
"staging.rejected": "Aufruf wurde abgelehnt.",
|
||||
"staging.rejectFailed": "Ablehnen fehlgeschlagen: {error}",
|
||||
"staging.withLead": "Die Leitung {lead} darf dies bis {until} entscheiden, im Rahmen dessen, was ihre Einheit erlaubt. Du kannst vorher entscheiden; danach liegt es allein bei dir.",
|
||||
"staging.voteRunning": "Abstimmung #{id} läuft bis {until}: {yes} ja, {no} nein, {missing} ausstehend. Du kannst vorher entscheiden.",
|
||||
"staging.voteDone": "Abstimmung #{id}: {outcome} — {yes} ja, {no} nein, {missing} fehlen.",
|
||||
"staging.voteRule": "Angenommen ab {approve} Ja, abgelehnt bei höchstens {reject}, mindestens {quorum} Stimmen",
|
||||
"staging.voteCost": "Läufe der Abstimmenden: ${cost}",
|
||||
"staging.voteYes": "ja",
|
||||
"staging.voteNo": "nein",
|
||||
"staging.voteMissing": "fehlt",
|
||||
"staging.voteEvidence": "Beleg: {evidence}",
|
||||
"staging.leadsTitle": "Freigaben durch Leitungen",
|
||||
"staging.leadsHint": "Was Leitungen in den letzten 30 Tagen an deiner Stelle freigegeben haben. Eine Leitung, die alles freigibt, verursacht nur Kosten — sieh dir ihre Entscheidungen an.",
|
||||
"staging.leadName": "Leitung",
|
||||
|
||||
@@ -138,6 +138,20 @@
|
||||
"org.measure.security": "Security suspicion",
|
||||
"org.measureStatus.accepted": "accepted",
|
||||
"org.measureStatus.dismissed": "dismissed",
|
||||
"org.votes": "Votes",
|
||||
"org.votesHint": "Operations that several agents check independently and vote on blind before they run — instead of the lead. Choose voters that look at different data (accounts, stock, range): independent checks are the point, not a majority. Whoever proposes does not vote; voters that read content from outside get no ballot; a missing vote is no yes. Between approval and rejection, and without the quorum, you decide. Every voter costs a run.",
|
||||
"org.votesVoters": "Voters (one agent per line)",
|
||||
"org.votesApproveAt": "Approved from yes votes",
|
||||
"org.votesRejectAt": "Rejected at yes votes or fewer",
|
||||
"org.votesQuorum": "Votes needed at least (0: as for approval)",
|
||||
"org.votesHours": "Hours for the vote",
|
||||
"org.votesHeld": "Votes held",
|
||||
"org.voteWhen": "When",
|
||||
"org.voteOperation": "Call",
|
||||
"org.voteTally": "Votes",
|
||||
"org.voteTallyText": "{yes} yes · {no} no · {missing} missing",
|
||||
"org.voteOutcome": "Outcome",
|
||||
"org.voteCost": "Cost of the voters",
|
||||
"org.ceilingInherited": "Limited from above to: {tools}",
|
||||
"org.settings": "Unit-wide values",
|
||||
"org.settingsHint": "Written into the agents after a preview. The unit keeps the value only as a default for new members — nothing overrides an agent at run time.",
|
||||
@@ -659,6 +673,14 @@
|
||||
"staging.rejected": "Call was rejected.",
|
||||
"staging.rejectFailed": "Rejection failed: {error}",
|
||||
"staging.withLead": "Its lead {lead} may decide this until {until}, within the limits its unit grants. You can decide it first; after that it is yours alone.",
|
||||
"staging.voteRunning": "Vote #{id} runs until {until}: {yes} yes, {no} no, {missing} outstanding. You can decide it first.",
|
||||
"staging.voteDone": "Vote #{id}: {outcome} — {yes} yes, {no} no, {missing} missing.",
|
||||
"staging.voteRule": "Approved from {approve} yes, rejected at {reject} or fewer, at least {quorum} votes",
|
||||
"staging.voteCost": "voters' runs: ${cost}",
|
||||
"staging.voteYes": "yes",
|
||||
"staging.voteNo": "no",
|
||||
"staging.voteMissing": "missing",
|
||||
"staging.voteEvidence": "Evidence: {evidence}",
|
||||
"staging.leadsTitle": "Approvals by leads",
|
||||
"staging.leadsHint": "What leads approved instead of you in the last 30 days. A lead that approves everything does nothing but cost — look at its decisions.",
|
||||
"staging.leadName": "Lead",
|
||||
|
||||
@@ -369,6 +369,7 @@ function unitPanel(unit) {
|
||||
cards.push(approvalsCard(unit));
|
||||
cards.push(doctrineCard(unit));
|
||||
cards.push(inspectionCard(unit));
|
||||
cards.push(votesCard(unit));
|
||||
cards.push(settingsCard(unit));
|
||||
|
||||
// Members.
|
||||
@@ -504,6 +505,69 @@ function doctrineCard(unit) {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* #702: What the unit puts to a blind vote, who votes, and the thresholds. A vote counts
|
||||
* independent checks, not opinions — the voters should look at different data. The votes held
|
||||
* stand below, with every ballot.
|
||||
*/
|
||||
function votesCard(unit) {
|
||||
const current = unit.votes;
|
||||
const ops = erstelleElement("textarea", { rows: 2, placeholder: "Database.insert" });
|
||||
ops.value = (current?.operations ?? []).join("\n");
|
||||
const voters = erstelleElement("textarea", { rows: 3, placeholder: "accounting\nlogistics\nmarketing" });
|
||||
voters.value = (current?.voters ?? []).join("\n");
|
||||
const number = (value, min) => erstelleElement("input", { type: "number", min: String(min), step: "1", value: String(value) });
|
||||
const approveAt = number(current?.approveAt ?? 3, 1);
|
||||
const rejectAt = number(current?.rejectAt ?? 1, 0);
|
||||
const quorum = number(current?.quorum ?? 0, 0);
|
||||
const hours = erstelleElement("input", { type: "number", min: "0.25", step: "0.25", value: String(current?.hours ?? 4) });
|
||||
const perDay = number(current?.perDay ?? 10, 0);
|
||||
const held = erstelleElement("div", { klasse: "org-stichproben" });
|
||||
|
||||
(async () => {
|
||||
let list = [];
|
||||
try { list = await api(`/organisation/units/${encodeURIComponent(unit.id)}/votes`); } catch { return; }
|
||||
if (!list.length) return;
|
||||
held.append(erstelleElement("h4", { text: t("org.votesHeld") }));
|
||||
held.append(erstelleElement("table", { klasse: "daten-tabelle" },
|
||||
erstelleElement("thead", {}, erstelleElement("tr", {},
|
||||
...[t("org.voteWhen"), t("org.voteOperation"), t("org.voteTally"), t("org.voteOutcome"), t("org.voteCost")].map((h) => erstelleElement("th", { text: h })))),
|
||||
erstelleElement("tbody", {}, ...list.map((v) => erstelleElement("tr", {},
|
||||
erstelleElement("td", { text: formatiereDatumZeit(v.createdAt) }),
|
||||
erstelleElement("td", { text: `${v.operation} · ${v.proposer}` }),
|
||||
erstelleElement("td", { text: t("org.voteTallyText", { yes: v.yes, no: v.no, missing: v.missing }) }),
|
||||
erstelleElement("td", { text: v.outcomeText }),
|
||||
erstelleElement("td", { text: v.costUsd != null ? `$${v.costUsd.toFixed(4)}` : "—" }))))));
|
||||
})();
|
||||
|
||||
const lines = (el) => el.value.split(/[\n,]/).map((o) => o.trim()).filter(Boolean);
|
||||
return erstelleKarte(t("org.votes"), [
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.votesHint") }),
|
||||
erstelleElement("div", { klasse: "formular-raster" },
|
||||
erstelleElement("label", { text: t("org.approvalsOperations") }), ops,
|
||||
erstelleElement("label", { text: t("org.votesVoters") }), voters,
|
||||
erstelleElement("label", { text: t("org.votesApproveAt") }), approveAt,
|
||||
erstelleElement("label", { text: t("org.votesRejectAt") }), rejectAt,
|
||||
erstelleElement("label", { text: t("org.votesQuorum") }), quorum,
|
||||
erstelleElement("label", { text: t("org.votesHours") }), hours,
|
||||
erstelleElement("label", { text: t("org.approvalsPerDay") }), perDay),
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "klein", text: t("common.save"),
|
||||
onclick: () => send(`/organisation/units/${encodeURIComponent(unit.id)}/votes`, "PUT", {
|
||||
operations: lines(ops),
|
||||
voters: lines(voters),
|
||||
approveAt: Number(approveAt.value),
|
||||
rejectAt: Number(rejectAt.value),
|
||||
quorum: Number(quorum.value),
|
||||
hours: Number(hours.value),
|
||||
perDay: Number(perDay.value),
|
||||
reason: reason()
|
||||
})
|
||||
}),
|
||||
held
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* #693: The lead's spot checks. How often it looks at the runs of the agents it leads, what
|
||||
* came of the last checks, and the measures it proposed — which only a human decides. Nothing
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
PFADE,
|
||||
erstelleKarte,
|
||||
erstelleModal,
|
||||
erstellePlakette,
|
||||
formatiereDatumZeit,
|
||||
zeigeMeldung
|
||||
} from "../modules/dom.js";
|
||||
@@ -21,6 +22,28 @@ function menschWartet(anfrage) {
|
||||
return !anfrage.withLead;
|
||||
}
|
||||
|
||||
/**
|
||||
* #702: A vote on the call — the tally, and each ballot with its reasoning and evidence. The
|
||||
* voters never see this; the human decides with all of it in view when the vote is undecided.
|
||||
*/
|
||||
function voteBox(vote, running) {
|
||||
const kopf = running
|
||||
? t("staging.voteRunning", { id: vote.id, until: formatiereDatumZeit(vote.until), yes: vote.yes, no: vote.no, missing: vote.missing })
|
||||
: t("staging.voteDone", { id: vote.id, outcome: vote.outcomeText, yes: vote.yes, no: vote.no, missing: vote.missing });
|
||||
const regeln = t("staging.voteRule", { approve: vote.approveAt, reject: vote.rejectAt, quorum: vote.quorum })
|
||||
+ (vote.costUsd != null ? " · " + t("staging.voteCost", { cost: vote.costUsd.toFixed(4) }) : "");
|
||||
return erstelleElement("div", { klasse: "hinweis-box staging-abstimmung" },
|
||||
erstelleElement("strong", { text: kopf }),
|
||||
erstelleElement("div", { klasse: "gedaempft text-klein", text: regeln }),
|
||||
erstelleElement("ul", { klasse: "staging-stimmen" }, ...vote.ballots.map((b) => erstelleElement("li", {},
|
||||
erstellePlakette(
|
||||
b.choice === "yes" ? t("staging.voteYes") : b.choice === "no" ? t("staging.voteNo") : t("staging.voteMissing"),
|
||||
b.choice === "yes" ? "erfolg" : b.choice === "no" ? "gefahr" : "neutral"),
|
||||
erstelleElement("strong", { text: ` ${b.voter}` }),
|
||||
b.choice ? erstelleElement("div", { klasse: "text-klein", text: b.reasoning }) : "",
|
||||
b.choice ? erstelleElement("div", { klasse: "gedaempft text-klein", text: t("staging.voteEvidence", { evidence: b.evidence }) }) : ""))));
|
||||
}
|
||||
|
||||
export async function holeWartendeAnzahl() {
|
||||
try {
|
||||
const liste = await api("/staging");
|
||||
@@ -145,7 +168,10 @@ async function ladeFreigaben() {
|
||||
const teile = [datenliste, argBox];
|
||||
|
||||
// #687: A lead decides it within its unit's limits — the human can still decide first.
|
||||
if (anfrage.withLead) {
|
||||
// #702: Or a blind vote does; its ballots stand beside the call.
|
||||
if (anfrage.vote) {
|
||||
teile.unshift(voteBox(anfrage.vote, !!anfrage.withLead));
|
||||
} else if (anfrage.withLead) {
|
||||
teile.unshift(erstelleElement("div", {
|
||||
klasse: "hinweis-box",
|
||||
text: t("staging.withLead", { lead: anfrage.withLead, until: formatiereDatumZeit(anfrage.leadUntil) })
|
||||
|
||||
Reference in New Issue
Block a user