vortexVote.js is a small, dependency-free JS/Bun module that implements three configurable threshold-gating decision rules — a way to programmatically decide whether an action should proceed, based on collected "votes" (from people, services, or checks), gated by different levels of strictness depending on how risky the action is.
It's not a distributed consensus protocol (no networking, no fault tolerance) — it's a rules engine for "should this thing happen?" decisions, borrowing consensus vocabulary as a design metaphor.
Built around a No Silent Failure (NSF) philosophy: every function returns a discriminated Result (Ok/Err) rather than throwing unpredictably or quietly coercing bad input into a business outcome. A malformed call and a legitimate "no" vote are never allowed to look the same.
📄 For the formal analysis this README summarizes — comparison against Paxos/Raft/PBFT, a full gap history, and per-path derivations — see the full whitepaper.
| Path | Structure | Rule |
|---|---|---|
| Fast (3-4-1/3) | 3 primary voters → 4 stabilizers | 3 primaries must agree, then just 1/3 of stabilizers need to accept (threshold scales with stabilizer count). Cheap and quick. |
| Middle (3-6-9) | 3 seed voters → 6 validators | Seed quorum required, then a 9-round audit of validators — needs 7 rounds to pass. Supports a pollFn for genuinely independent per-round observations. |
| Slow (7-4-?) | 4 structural gates → 7 vote states → entropy buffer | All 4 gates must clear, any single escalate vote forces human review, and an entropy buffer ('?') can force a deliberate delay before a majority vote even counts. |
Each path is a pure, total function
P : V₁ × V₂ × ... × Vₖ → Result⟨Decision, VortexVoteError⟩
Decision = { outcome: Outcome, details: object }
over fixed-arity vote vectors Vᵢ. "Total" means every reachable input — including malformed or under-length vote arrays — produces a defined Result, never an unhandled exception. Ok({ outcome: ABORT, ... }) and Err(VortexVoteError) are deliberately distinct channels: the former is a legitimate business result (votes were well-formed, quorum genuinely wasn't reached); the latter is a caller/programmer error (wrong shape, wrong arity, unrecognized enum value).
The module models neither Byzantine fault tolerance (vote authenticity is assumed, not verified) nor liveness (no retry, timeout, or leader election) — it's a deterministic evaluation over an already-collected, already-trusted snapshot of votes. See the whitepaper's §5 Consensus Theory table for how each path maps onto (and diverges from) its nearest classical analogue.
| Outcome | Emitted by | Meaning |
|---|---|---|
COMMIT / SAFE_COMMIT |
fast · middle · slow | Positive terminal outcome — the gated action proceeds. |
ABORT |
fast · middle · slow | Quorum/majority stage failed outright. |
ROLLBACK |
fast | Primary quorum passed but stabilizer acceptance fell below threshold. |
RECONCILE |
middle | Seed quorum passed but cyclic-audit pass count fell below threshold. |
HOLD |
slow | Structural gates not all clear — highest-priority block. |
HUMAN_REVIEW |
slow | Any agent voted escalate — a single dissenter is sufficient, regardless of other votes. |
DELAY_AND_AUDIT |
slow | Entropy buffer (?) triggered — deliberate hesitation, independent of vote content. |
runVote dispatches to one of the three paths, each of which validates its input at the boundary before any business logic runs:
flowchart TD
A(["runVote(path, input)"]) --> B{"path ∈ VotePath?"}
B -- "no" --> EP["Err · INVALID_PATH"]
B -- "fast" --> F[["fastPath()"]]
B -- "middle" --> M[["middlePath()"]]
B -- "slow" --> S[["slowPath()"]]
F --> RES{{"Result⟨Decision, VortexVoteError⟩"}}
M --> RES
S --> RES
RES -- "ok: true" --> OK["Ok({ outcome, details })"]
RES -- "ok: false" --> ERR["Err(VortexVoteError)"]
classDef err fill:#fdf0f0,stroke:#8b1a1a,color:#8b1a1a;
classDef result fill:#eef4fb,stroke:#2e5fa3,color:#1a2744;
class EP,ERR err;
class RES,OK result;
A cascade, not a single quorum: strict unanimity among 3 primaries (cheap to fail early) feeds a comparatively weak 1/3-of-stabilizers plurality check, which is what actually governs commit/rollback.
flowchart TD
Start(["fastPath(primaryVotes, stabilizerVotes)"]) --> V1{"validateVoteArray\nprimaryVotes, min 3"}
V1 -- invalid --> E1["Err · INVALID_VOTE_ARRAY / INVALID_VOTE_ENTRY"]
V1 -- valid --> V2{"validateVoteArray\nstabilizerVotes, min 4"}
V2 -- invalid --> E1
V2 -- valid --> Q{"hasQuorum(primary₃, min 3)\nunanimity gate"}
Q -- no --> ABORT["Ok · ABORT\nno primary quorum"]
Q -- yes --> T["threshold = ⌈stabilizers.length × 1/3⌉"]
T --> C{"acceptCount ≥ threshold?\nplurality gate"}
C -- yes --> COMMIT["Ok · COMMIT"]
C -- no --> ROLLBACK["Ok · ROLLBACK"]
classDef err fill:#fdf0f0,stroke:#8b1a1a,color:#8b1a1a;
classDef neg fill:#fdf0f0,stroke:#8b1a1a,color:#8b1a1a;
classDef pos fill:#f0faf2,stroke:#1a5c2a,color:#1a5c2a;
class E1 err;
class ABORT,ROLLBACK neg;
class COMMIT pos;
Seed quorum gates entry into a 9-round cyclic audit of the 6 validators. Without a pollFn, each round re-evaluates the same static snapshot, so passes is deterministically 0 or 9; supplying pollFn(round, validators) gives each round a genuinely independent observation, letting passes land anywhere in [0, 9].
flowchart TD
Start(["middlePath(seedVotes, validatorVotes, opts)"]) --> V1{"validateVoteArray\nseedVotes, min 3"}
V1 -- invalid --> E1["Err"]
V1 -- valid --> V2{"validateVoteArray\nvalidatorVotes, min 6"}
V2 -- invalid --> E1
V2 -- valid --> Q{"hasQuorum(seeds₃, min 3)"}
Q -- no --> ABORT["Ok · ABORT\nno seed quorum"]
Q -- yes --> AUD["runCyclicAudit(validators₆, auditCount=9, pollFn?)"]
AUD --> RND["for round in 0..9:\nsnapshot = pollFn?.(round, validators) ?? validators\npass if acceptCount(snapshot) > snapshot.length / 2"]
RND --> TALLY["passes = count(passing rounds)"]
TALLY --> C{"passes ≥ passThreshold\n(default 7)?"}
C -- yes --> COMMIT["Ok · COMMIT"]
C -- no --> RECONCILE["Ok · RECONCILE"]
classDef err fill:#fdf0f0,stroke:#8b1a1a,color:#8b1a1a;
classDef neg fill:#fdf0f0,stroke:#8b1a1a,color:#8b1a1a;
classDef def fill:#fff8ee,stroke:#8b5a00,color:#8b5a00;
classDef pos fill:#f0faf2,stroke:#1a5c2a,color:#1a5c2a;
class E1 err;
class ABORT neg;
class RECONCILE def;
class COMMIT pos;
Not a quorum system — an ordered rule list, closer to a firewall ruleset than a vote count. The check order is the specification: structural integrity dominates a lone dissenter's escalation veto, which dominates the entropy buffer, which dominates the plain majority tally. Reordering these checks is a breaking behavioral change, not a refactor.
flowchart TD
Start(["slowPath({ stateVotes, gateChecks, entropyBufferTriggered })"]) --> V1{"validateGateArray\ngateChecks, ≥4 booleans"}
V1 -- invalid --> E1["Err · INVALID_GATE_ARRAY"]
V1 -- valid --> V2{"validateStateVotes\nstateVotes ∈ 7 canonical VoteState"}
V2 -- invalid --> E2["Err · INVALID_STATE_VALUE"]
V2 -- valid --> G{"allGatesClear(4/4)?"}
G -- no --> HOLD["Ok · HOLD\nsafety precondition — unconditional block"]
G -- yes --> ESC{"any vote === ESCALATE?"}
ESC -- yes --> HR["Ok · HUMAN_REVIEW\nliberal veto — one dissenter wins"]
ESC -- no --> ENT{"entropyBufferTriggered?"}
ENT -- yes --> DAA["Ok · DELAY_AND_AUDIT\nunconditional deliberate delay"]
ENT -- no --> MAJ{"majorityYes(stateVotes)?"}
MAJ -- yes --> SC["Ok · SAFE_COMMIT"]
MAJ -- no --> AB["Ok · ABORT\nno majority yes"]
classDef err fill:#fdf0f0,stroke:#8b1a1a,color:#8b1a1a;
classDef neg fill:#fdf0f0,stroke:#8b1a1a,color:#8b1a1a;
classDef def fill:#fff8ee,stroke:#8b5a00,color:#8b5a00;
classDef esc fill:#eef4fb,stroke:#2e5fa3,color:#1a2744;
classDef pos fill:#f0faf2,stroke:#1a5c2a,color:#1a5c2a;
class E1,E2 err;
class HOLD,DAA def;
class HR esc;
class SC pos;
class AB neg;
- Fast — rolling out feature flags, ephemeral routing/traffic changes, QoS policy tweaks — anything low-risk and instantly reversible.
- Middle — billing threshold changes, infra auto-scaling decisions, non-emergency resource governance — needs redundancy but isn't an emergency.
- Slow — DNS root key rotation, CA revocation, or any irreversible action that should require out-of-band human verification before it happens.
In short: it's a CI/CD or ops gate — drop it in front of a deploy pipeline, a config-change approval flow, or an automated agent's "should I take this action" check, picking the path that matches how dangerous the action is.
No dependencies. Copy vortexVote.js into your project, or clone the repo:
bun add ./vortexVote.js # local moduleRequires Bun (or any modern JS runtime that supports ES modules).
Every path function returns a Result: { ok: true, value } on success, { ok: false, error } on a validation failure (malformed input — a programmer/caller error, not a business decision).
import { runVote, VotePath, VoteState, isOk, unwrap } from "./vortexVote.js";
// FAST PATH — 3 primaries, 4 stabilizers
const fast = runVote(VotePath.FAST, {
primaryVotes: [{ vote: "accept" }, { vote: "accept" }, { vote: "accept" }],
stabilizerVotes: [{ vote: "accept" }, { vote: "accept" }, { vote: "reject" }, { vote: "reject" }],
});
if (isOk(fast)) {
console.log(fast.value); // { outcome: "COMMIT", details: { acceptCount: 2, threshold: 2 } }
}
// MIDDLE PATH — 3 seeds, 6 validators, optional live pollFn for genuine
// per-round independence in the 9-round cyclic audit
const middle = runVote(VotePath.MIDDLE, {
seedVotes: [{ vote: "accept" }, { vote: "accept" }, { vote: "accept" }],
validatorVotes: [
{ vote: "accept" }, { vote: "accept" }, { vote: "accept" },
{ vote: "accept" }, { vote: "reject" }, { vote: "reject" },
],
// pollFn: (round, validators) => rePollLiveAgents(validators),
});
// SLOW PATH — 7 discrete states, 4 gates, entropy buffer
const slow = runVote(VotePath.SLOW, {
stateVotes: [VoteState.YES, VoteState.YES, VoteState.MAYBE],
gateChecks: [true, true, true, true],
entropyBufferTriggered: false,
});
// Malformed input is caught, not silently absorbed:
const bad = runVote(VotePath.FAST, { primaryVotes: undefined, stabilizerVotes: [] });
// bad.ok === false
// bad.error.code === "INVALID_VOTE_ARRAY"
// `unwrap` throws on Err — use only at an outermost edge (CLI entrypoint, etc.)
const decision = unwrap(fast);Run the built-in demo directly with Bun:
bun run vortexVote.js| Export | Signature | Returns |
|---|---|---|
fastPath |
(primaryVotes, stabilizerVotes) |
Result<{outcome, details}, VortexVoteError> |
middlePath |
(seedVotes, validatorVotes, {passThreshold=7, pollFn}?) |
Result<{outcome, details}, VortexVoteError> |
slowPath |
({stateVotes, gateChecks, entropyBufferTriggered}) |
Result<{outcome, details}, VortexVoteError> |
runCyclicAudit |
(validators, auditCount=9, pollFn?) |
{ passes, rounds } |
allGatesClear |
(gateChecks) |
boolean |
majorityYes |
(stateVotes) |
boolean |
runVote |
(path, input) |
dispatches to one of the above |
Ok / Err / isOk / isErr / unwrap |
— | Result helpers |
VortexVoteError |
class | typed error with .code and .context |
ErrorCode |
frozen enum | INVALID_VOTE_ARRAY · INVALID_VOTE_ENTRY · INVALID_GATE_ARRAY · INVALID_STATE_VALUE · INVALID_PATH |
Outcome |
frozen enum | ABORT · COMMIT · ROLLBACK · RECONCILE · HOLD · HUMAN_REVIEW · DELAY_AND_AUDIT · SAFE_COMMIT |
VoteState |
frozen enum | yes · no · maybe · wait · escalate · reject · audit |
VotePath |
frozen enum | fast · middle · slow |
bun test34 tests covering quorum/threshold/gate-ordering business logic, validation-error paths, the cyclic-audit independence fix, the Result/VortexVoteError machinery itself, and the boolean-vote shorthand.
- All exported decision functions are pure and total: every reachable input — valid or malformed — produces a defined
Result, never an unhandled exception or an undefined branch. - No silent failure: malformed input (wrong shape, wrong arity, an unrecognized enum value) returns
Err(VortexVoteError)and is never coerced into a business outcome likeABORT. A caller bug and a legitimate "no" vote are always distinguishable. - No phase locks: every function is stateless — no shared mutable state, no ordering requirement across calls. The 9-round cyclic audit accepts an optional
pollFnso each round can be a genuinely independent observation rather than a repeated check of a static snapshot. - The Slow Path's check order (gates → escalation → entropy → majority) is the specification — reordering it changes real-world behavior and should be treated as a breaking change, not a refactor.
- This module trusts its inputs' authenticity completely — it validates shape and type, not who cast the vote. There is no Byzantine fault tolerance at the vote-object layer; authenticate votes before they reach this module if that matters for your use case.
Every point above traces back to a specific gap identified and resolved in the whitepaper's §6 Known Gaps — the fast-path arity coupling, cyclic-audit independence, guard-chain ordering, Byzantine-trust disclosure, and the Err/Ok(ABORT) conflation all originated there.
MIT