OpenLikwid (the demo at https://openlikwid.org) is my attempt at building governance infrastructure that doesn’t accidentally reinvent the comment section.
But today I don’t want to write about the platform.
I want to talk about three ideas that keep showing up any time you try to build “decision-making software” that doesn’t fall over the moment reality walks into the room:
- Liquid delegation (trust as a graph)
- Condorcet (preference aggregation as pairwise comparisons)
- Schulze (how to survive cycles without pretending they don’t exist)
And I want to keep this grounded in code — specifically: what Likwid actually does today, what it carefully avoids claiming, and what still belongs in the “designed, not yet wired” bucket.
🔗1) Liquid delegation is a trust graph, not a vibe
The pitch for liquid delegation is usually presented as:
- You can vote directly.
- Or you can delegate your vote to someone you trust.
- And you can revoke it whenever you want.
All true.
The deeper (and more useful) framing is: liquid delegation is a dynamic, weighted graph.
- Nodes are people.
- Directed edges are delegations (“I trust you to vote for me in this scope”).
- Each edge has constraints: scope and revocability.
- Each edge can carry a fractional weight.
In Likwid, you can see that model directly in the schema:
- Delegations live in
delegationswith scopes:global,community,topic,proposal. - They’re revocable (
is_active,revoked_at). - They can be fractional (
weightisDECIMAL(5,4); the API validates0 < weight <= 1).
There’s also a design choice I like because it’s operationally honest:
- When you create a new delegation with the same scope + target (same community/topic/proposal reference), Likwid deactivates the previous one.
So the graph stays “one active outgoing edge per scope+target” for each user, instead of becoming a messy parallel universe of overlapping delegations.
🔗Cycles: the bug you don’t want to debug in production
The first failure mode everyone points out is also the first one you should prevent:
- A delegates to B
- B delegates to C
- C delegates back to A
This is not a philosophical problem. It’s a runtime problem.
Likwid prevents this at the database layer: a trigger (check_delegation_cycle) walks the chain up to a max depth of 20 and rejects inserts/updates that would create a cycle.
That’s important because it’s not “best effort application logic.” It’s an invariant.
🔗2) Condorcet: the world where preferences fight in pairs
Condorcet methods start from a simple move:
Instead of asking:
- “Which option got the most votes?”
they ask:
- “If we compare options two at a time, which one does the group prefer?”
This gives you a pairwise preference matrix:
d[i][j] = number of voters who prefer option i over option j
If there exists an option that beats every other option head-to-head, that’s the Condorcet winner.
Great. In a perfect world, you pick it and go home.
🔗The Condorcet paradox (a.k.a. “rock-paper-scissors, but for grown-ups”)
The problem is: the pairwise comparisons can form a cycle.
- A beats B
- B beats C
- C beats A
Now what?
You can’t fix this by “being more democratic.” The cycle is a property of aggregated preferences.
This is the point where a lot of systems quietly panic and either:
- fall back to a simpler rule, or
- invent a tie-breaker that magically turns into a political argument
Schulze is one of the cleanest ways out.
🔗3) Schulze, computed like you mean it
Schulze keeps the Condorcet worldview (pairwise comparisons), but changes the meaning of “beats.”
It doesn’t only care about direct victories.
It cares about the strongest path between options.
If you imagine the pairwise matrix as a directed graph, the strength of a path is the minimum edge strength along that path — and the strongest path is the path that maximizes that minimum.
That’s a very computer-science sentence, so here’s the translation:
- A direct win with strength 60 is good.
- But an indirect route A → X → Y → B can still matter, if every step of that route has decent support.
🔗The computational core
In Likwid, the Schulze implementation lives in backend/src/voting/schulze.rs.
In terms of integration, the current codebase has the Schulze tally implemented and exposed in the “results” path (it’s one of the methods handled by the voting results endpoint), while the vote-casting endpoint for ranked ballots is still strict about accepting ranked ballots only for IRV (ranked_choice). In other words: the computational core is there and testable today; wiring it end-to-end for real proposals is mostly API/UX plumbing.
It does three key things:
- Build
d(pairwise preferences) from ranked ballots. - Build
p(strongest paths) using a Floyd–Warshall-style dynamic program. - Derive a winner + a ranking, and return method-specific details.
A detail that matters for real ballots: unranked options.
In the current implementation:
- Ranked beats unranked
- Unranked loses to ranked
- Both unranked = no preference
That’s a pragmatic rule: you can submit partial rankings without creating nonsense pairwise assertions.
🔗Complexity (so you know where the cliffs are)
If you have:
noptionsmballots
Then:
- Building
disO(m * n^2) - Floyd–Warshall is
O(n^3)
This is fine when n is “a menu of options” and not “every possible future.”
And in governance, most decisions worth using Schulze for tend to be dozens of options at most — not thousands.
🔗A note about rankings (being honest about what the code does)
In Likwid, the returned “score” is currently the number of pairwise wins according to the strongest-path matrix p.
That is:
- Option i gets one point for every option j such that
p[i][j] > p[j][i]. - Options are sorted by that win count.
This is a reasonable summary for display, and the API also returns pairwise_matrix and strongest_paths so you can inspect the underlying structure.
If we want a stricter Schulze ranking procedure (with explicit tie handling and a more formal ordering), that’s a well-scoped improvement — but the core computation is already the “correct shape”: pairwise preferences + strongest paths.
🔗4) “Computationally sound” also means auditable
If the output is: “trust me, the algorithm says so,” you didn’t build governance infrastructure.
You built a black box.
Likwid leans hard toward auditability in two ways:
- It returns method-specific details (e.g. matrices for Schulze).
- It has a database layer for reproducibility and audit snapshots.
There’s a vote_audit table and an archival mechanism (plugin_wasm_archive) meant to tie results to the algorithm version that produced them.
This is one of those design choices that looks boring until it saves you from a real dispute.
🔗5) What’s intentionally not claimed (yet)
There are parts of the liquid delegation story that exist in the schema, but aren’t fully wired into the vote-casting flow yet.
For example:
delegated_votesexists as a place to record votes cast on behalf of someone else.delegation_chainsexists as a cache for transitive delegation resolution.
But the current vote-casting endpoints primarily record the direct voter’s ballot (approval / ranked / STAR / quadratic) using a pseudonymous voting_identity.
That’s fine — it just means that when I talk about liquid delegation here, I’m describing the trust-graph substrate and the invariants (scopes, weights, cycle prevention), not claiming that every downstream “delegate votes for you automatically” path is already end-to-end.
🔗Closing thought: graphs + matrices are friendlier than they sound
Liquid delegation and Condorcet-style methods sometimes get treated as “too academic.”
But the moment you translate them into the right mental models — graphs and matrices — they become less mystical and more like what they are:
- structured ways to preserve information
- tools to make paradoxes explicit
- and algorithms you can actually implement, test, and audit
Which, for a governance platform, is the whole point.