Skip to content

Commit 86f9279

Browse files
stackdumpclaude
andcommitted
Each poll choice gets its own vote transition in the Petri net
Instead of a single castVote transition with a choice parameter, the poll model now has one vote:X transition per choice, each connected to its own tally:X place and to the shared nullifiers place. This reflects the ZK proof structure where each choice is a distinct firing path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2027bc1 commit 86f9279

1 file changed

Lines changed: 55 additions & 15 deletions

File tree

public/bitwrap.js

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -398,40 +398,80 @@ function injectPollChoices(model, poll) {
398398
const m = JSON.parse(JSON.stringify(model)); // deep clone
399399
const choices = poll.choices;
400400

401-
// Remove the generic "tallies" place
401+
// Remove the generic castVote transition and tallies place
402+
delete m.transitions['castVote'];
402403
delete m.places['tallies'];
403404

404-
// Add a place for each choice, arranged in a row below castVote
405-
const castVote = m.transitions['castVote'] || { x: 300, y: 50 };
406-
const startX = castVote.x - ((choices.length - 1) * 80) / 2;
405+
// Remove all arcs involving castVote or tallies
406+
m.arcs = m.arcs.filter(a =>
407+
a.source !== 'castVote' && a.target !== 'castVote' &&
408+
a.source !== 'tallies' && a.target !== 'tallies'
409+
);
410+
411+
// Layout: choices fan out from a central point
412+
const cx = 300, topY = 40;
413+
const spacing = Math.max(120, 400 / choices.length);
414+
const startX = cx - ((choices.length - 1) * spacing) / 2;
407415

408416
choices.forEach((name, i) => {
409-
const placeId = 'tally:' + name;
410-
m.places[placeId] = {
411-
x: Math.round(startX + i * 80),
412-
y: castVote.y + 180,
417+
const x = Math.round(startX + i * spacing);
418+
419+
// Each choice gets its own vote transition (square)
420+
m.transitions['vote:' + name] = {
421+
x: x,
422+
y: topY,
423+
'@type': 'Transition',
424+
};
425+
426+
// Each choice gets its own tally place (circle)
427+
m.places['tally:' + name] = {
428+
x: x,
429+
y: topY + 140,
413430
initial: [0],
414431
'@type': 'Place',
415432
offset: 0,
416433
capacity: [null],
417434
};
418-
});
419435

420-
// Replace castVote→tallies arc with one arc per choice
421-
m.arcs = m.arcs.filter(a => !(a.source === 'castVote' && a.target === 'tallies'));
422-
choices.forEach((name) => {
436+
// vote:choice → tally:choice (records the vote)
423437
m.arcs.push({
424-
source: 'castVote',
438+
source: 'vote:' + name,
425439
target: 'tally:' + name,
426440
weight: [1],
427441
inhibitTransition: false,
428442
'@type': 'Arrow',
429443
});
444+
445+
// vote:choice → nullifiers (prevents double voting)
446+
m.arcs.push({
447+
source: 'vote:' + name,
448+
target: 'nullifiers',
449+
weight: [1],
450+
inhibitTransition: false,
451+
'@type': 'Arrow',
452+
});
430453
});
431454

432-
// Update model name to poll title
433-
m.name = poll.title || m.name;
455+
// Reposition remaining places below the choices
456+
const bottomY = topY + 300;
457+
const remaining = Object.keys(m.places).filter(k => !k.startsWith('tally:'));
458+
remaining.forEach((id, i) => {
459+
const rx = Math.round(cx - ((remaining.length - 1) * 100) / 2 + i * 100);
460+
m.places[id].x = rx;
461+
m.places[id].y = bottomY;
462+
});
434463

464+
// Reposition createPoll and closePoll
465+
if (m.transitions['createPoll']) {
466+
m.transitions['createPoll'].x = cx - 150;
467+
m.transitions['createPoll'].y = bottomY - 80;
468+
}
469+
if (m.transitions['closePoll']) {
470+
m.transitions['closePoll'].x = cx + 150;
471+
m.transitions['closePoll'].y = bottomY - 80;
472+
}
473+
474+
m.name = poll.title || m.name;
435475
return m;
436476
}
437477

0 commit comments

Comments
 (0)