A formal analysis of vortexVote.js — a dependency-free Bun/ES module implementing three
threshold-quorum decision procedures (3-4-1/3, 3-6-9, 7-4-?) for gating operations by risk class,
from ephemeral feature flags to irreversible infrastructure changes. This edition documents the
Result-typed, NSF-compliant release; gaps identified in §6 are marked with their current status.
vortexVote.js implements three parameterized threshold-quorum decision procedures over discrete-valued vote arrays: a Fast Path (3-4-1/3) for ephemeral, low-risk operations; a Middle Path (3-6-9) for balanced-redundancy governance decisions; and a Slow Path (7-4-?) for irreversible, emergency-class actions gated behind structural integrity checks and human escalation. Each path is a pure, deterministic, total function — every reachable input produces one of eight terminal outcomes, with no unhandled branch.
This paper treats the module as an object of formal analysis rather than a marketing artifact. We define the input/output model for each path, compare its structure against classical distributed-consensus concepts (unanimity gates, plurality thresholds, Byzantine repeated sampling, veto-weighted rule chains), and identify concrete gaps between the module's documented intent and its current implementation — most notably in the Middle Path's cyclic-audit routine, which is currently deterministic across all rounds rather than exhibiting genuine round-to-round independence.
The conclusion is explicit in the module's own header comment and reaffirmed here: this is a consensus metaphor — a business-rule threshold engine borrowing distributed-systems vocabulary — suitable for gating deploy pipelines, config changes, and human-review triggers, and not a substitute for a fault-tolerant consensus protocol where Byzantine behavior, network partition, or liveness guarantees must be formally proven.
Let a vote be an element of the loosely-typed set {accept, reject, true, false, yes},
which the module treats as a coerced boolean via hasQuorum and inline filter predicates. Each
path P is a function
P : V₁ × V₂ × ... × Vₖ → Outcome × Details
where each Vᵢ is a fixed-arity vote vector, and Outcome ∈
{ ABORT, COMMIT, ROLLBACK, RECONCILE, HOLD, HUMAN_REVIEW, DELAY_AND_AUDIT, SAFE_COMMIT }
A central structural property holds across all three paths: they are total functions.
Every reachable input — including malformed or under-length vote arrays — produces a defined outcome,
never an exception or an undefined branch (hasQuorum and allGatesClear both
return false on missing or short arrays, rather than throwing). This is a minimal but
real correctness guarantee: closer to exhaustiveness in the state-machine sense than to
liveness in the CAP-theorem sense, but it does mean the decision procedure cannot stall in an
undefined state.
Positive terminal outcome — the gated action proceeds.
Quorum stage failed outright before any downstream check ran.
Quorum passed but stabilizer acceptance fell below threshold.
Quorum passed but cyclic-audit pass count fell below threshold.
Structural gates not all clear — highest-priority block.
Any agent voted ESCALATE — liberal veto, single dissenter wins.
Entropy buffer ('?') triggered — deliberate hesitation, independent of vote content.
Gates clear, no escalation, no entropy trigger, but no majority yes.
quorum(primary, k=3) ∧ |{accept ∈ stabilizers}| ≥ ⌈4·(1/3)⌉
Structurally, this is a conjunction of a unanimity gate and a plurality gate — not a
single quorum system but a cascade of two. The first stage, hasQuorum(primary, 3) called
with min = length, is effectively unanimity among the 3 primary voters — a strictly
stronger requirement than the majority quorum (⌈n/2⌉+1) used in classical systems such as
Paxos. The second stage relaxes the bar to ⌈4/3⌉ = 2, i.e., a plurality rather than
a majority.
This asymmetry — a strict unanimity gate feeding a comparatively weak downstream check — is the formal expression of the design intent: "fast, but only for low-stakes decisions." The expensive check (unanimous primaries) is cheap to fail early; the cheap check (2-of-4 stabilizers) is what actually governs the commit/rollback boundary.
The threshold Math.ceil(4 * (1/3)) is a roundabout way of writing 2, and it is
computed against the literal constant 4, not against stabilizers.length. A
caller passing more than 4 stabilizers still gets a threshold of 2 — the arity and the threshold formula
are coupled by convention, not by code. Worth an assertion or an explicit stabilizers.length
reference if the arity is ever expected to vary.
quorum(seeds, k=3) ∧ passes(auditRounds=9, majority(validators)) ≥ 7
This is the most analytically interesting path. runCyclicAudit re-evaluates the
same static validators array across 9 iterations:
for (let i = 0; i < auditCount; i++) { const acceptCount = validators.filter( (v) => v && (v.vote === "accept" || v.vote === true) ).length; rounds.push(acceptCount > validators.length / 2); }
Since validators never mutates between iterations, all 9 rounds are deterministically
identical — the result is passes ∈ {0, 9}, never anything in between, under the
current implementation. In a genuine cyclic-audit design — repeated sampling under noise, or re-polling
live agents whose votes can plausibly flip between rounds — one would expect a distribution over
passes ∈ [0, 9]. As written, the function is a majority check dressed as an iterative
protocol: the loop currently has no independent source of variance per round.
Single majority evaluation of a static snapshot, repeated 9 times with identical inputs. Output space collapses to two points: full pass (9/9) or full fail (0/9). The "9 rounds" language in the interface is not yet backed by 9 independent observations.
Each round re-polls live validators (or samples from a distribution over their possible states),
so passes can land anywhere in [0,9] and the 7-of-9 threshold does
meaningful work as a noise-tolerance parameter rather than a no-op.
Unlike the Fast and Middle paths, the Slow Path is best modeled as an ordered rule list
— closer to a firewall ruleset or a cond expression than to a quorum system. The order in
which its four checks run is the specification:
INPUT: { stateVotes, gateChecks, entropyBufferTriggered }
│
▼
┌─────────────────────────┐ fail ┌────────┐
│ allGatesClear(4/4)? ├─────────▶│ HOLD │ ← safety precondition, unconditional block
└───────────┬─────────────┘ └────────┘
pass│
▼
┌─────────────────────────┐ yes ┌────────────────┐
│ any vote === ESCALATE? ├─────────▶│ HUMAN_REVIEW │ ← liberal veto: one dissenter wins
└───────────┬─────────────┘ └────────────────┘
no│
▼
┌─────────────────────────┐ yes ┌───────────────────┐
│ entropyBufferTriggered? ├─────────▶│ DELAY_AND_AUDIT │ ← unconditional deliberate delay
└───────────┬─────────────┘ └───────────────────┘
no│
▼
┌──────────────────────────┐ yes ┌───────────────┐
│ majorityYes(stateVotes)? ├─────────▶│ SAFE_COMMIT │
└───────────┬──────────────┘ └───────────────┘
no│
▼
ABORT
The priority order encodes a threat model: structural integrity failures are treated as more urgent than
a lone dissenter, which is itself more urgent than "just delay," which is itself more urgent than
counting votes. The escalation veto is notably stronger than typical Byzantine quorum protections — those
usually require ⌊n/3⌋+1 dissenters to block an action, whereas here a single
agent voting ESCALATE is sufficient to force human review, regardless of how many other
agents voted YES.
This ordering is a legitimate stance for irreversible actions, but it is currently implicit in code rather than documented as a decision. A reordering — e.g., checking the entropy buffer before the escalation veto — would change real-world behavior nontrivially and should be treated as a breaking change requiring its own review, not a refactor.
Classical consensus protocols (Paxos, Raft, PBFT) are judged against two properties: safety
(nothing bad happens — no two conflicting values are ever chosen) and liveness (something
good eventually happens, given enough correct, live nodes). vortexVote.js does not model
liveness at all — there is no retry, timeout, or leader-election notion anywhere in the module. Its
"safety" is really just deterministic evaluation of an already-collected, already-trusted snapshot of
votes: it assumes the vote arrays are honest, with no Byzantine fault tolerance. A maliciously constructed
{vote: "accept"} object is indistinguishable from a genuine one at this layer.
| Path | Structure | Nearest classical analogue | Key gap vs. that analogue |
|---|---|---|---|
fastPath3-4-1/3 |
Unanimity gate → plurality gate | Two-phase commit (loosely) | Threshold hardcoded to arity 4, not relative to stabilizers.length |
middlePath3-6-9 |
Quorum gate → repeated majority | Byzantine repeated sampling | Rounds are non-independent; passes is always 0 or 9, never in between |
slowPath7-4-? |
Ordered guard chain with veto | Rule-based access control / circuit breaker | Not a quorum system at all — ordering of checks is the semantics, and is undocumented as a design decision |
The honest characterization, consistent with the module's own header comment, is that
vortexVote.js is a business-rule threshold engine borrowing consensus vocabulary
— well suited to gating deploy pipelines, config changes, or human-review triggers, and not a substitute
for a protocol offering formal distributed-systems guarantees.
Resolved: the commit threshold is now computed as Math.ceil(stabilizers.length * (1/3))
against the actual validated array, not a hardcoded literal.
Resolved: runCyclicAudit now accepts an optional pollFn(round, validators).
Supplying one gives each of the 9 rounds a genuinely independent snapshot, so passes can
land anywhere in [0,9]. Without a pollFn, behavior remains deterministic —
this was an additive fix, not a breaking change.
Resolved: the priority order (gates → escalation → entropy → majority) is now stated explicitly in the module's own header comment as a design decision, not left to be inferred from control flow.
Out of scope for this module by design, and correctly disclosed in the header comment — but worth re-stating at every call site that feeds this module: it validates vote shape, not vote authenticity.
Resolved (post-whitepaper addition): every path now validates input shape, arity, and enum
membership at the boundary and returns Err(VortexVoteError) on failure — a distinct
channel from the Ok({ outcome: ABORT, ... }) a caller gets from a legitimate quorum or
vote-count failure. See §7 for the updated model.
§1's formal model is updated in the release to reflect a stricter contract:
P : V₁ × V₂ × ... × Vₖ → Result<Decision, VortexVoteError>
Result<T,E> = Ok(T) | Err(E)
Decision = { outcome: Outcome, details: object }
The distinction that matters: Ok({ outcome: ABORT, ... }) is a legitimate business result —
the votes were well-formed and a quorum genuinely wasn't reached. Err(VortexVoteError) is a
caller error — malformed shape, wrong arity, or an unrecognized enum value. GAP 05 (§6) was
exactly this conflation in the pre-release version; the two channels are now structurally distinct, so a
consuming system can retry/alert differently for each.
| Error code | Raised by | Meaning |
|---|---|---|
INVALID_VOTE_ARRAY | all three paths | vote array missing, wrong type, or below required arity |
INVALID_VOTE_ENTRY | fast, middle, slow | a vote entry isn't a recognized {vote: accept|reject|true|false} shape |
INVALID_GATE_ARRAY | slow | gateChecks missing, too short, or containing a non-boolean |
INVALID_STATE_VALUE | slow | a stateVotes entry isn't one of the 7 canonical VoteState values |
INVALID_PATH | runVote | the requested path isn't fast, middle, or slow |
Every function in the module is now a total function over a strictly wider domain: it was already total
over well-formed input (§1), and is now total over all input, because malformed input routes to
a typed Err instead of either throwing unpredictably or silently degrading into a business
outcome. No input shape can produce an unhandled exception under normal operation.
The module's own vocabulary — "no phase locks," "no silent failure," "fail fast," "total function" — borrows from several distinct bodies of literature: software error-handling patterns, distributed-systems fault models, and (once, loosely) electrical engineering. This section cross-checks each borrowed term against its source definition and states plainly which ones the code actually earns, which are deliberate departures, and which don't apply at all. Recording a negative result here is treated as equally load-bearing as a positive one — the same standard §6 already applies to the module's own claims.
| Concept | Where in this module | Status |
|---|---|---|
| Fail-fast | Every path validates shape, arity, and enum membership before any quorum or threshold logic runs (§3 of the source). A malformed call never reaches business logic. | confirmed |
| Defensive programming | validateVoteArray, validateGateArray, validateStateVotes — every externally supplied array and enum value is shape-checked at the boundary. |
confirmed |
| Exception handling (Result-type substitute) | Internally, nothing throws — Ok/Err is used in place of exceptions for control flow. unwrap() is the single, explicitly documented point where an Err becomes a thrown exception, and only at an outermost caller's discretion. |
confirmed |
| Undefined behavior — avoided by construction | JavaScript has no UB in the C/C++ sense, so this is an analogy, not a literal claim: every reachable input, valid or malformed, maps to exactly one defined branch (§1's totality property). There is no input shape that falls through unhandled. | confirmed (by analogy) |
| Robustness principle (Postel's Law) | validateStateVotes rejects an unrecognized value like "yess" with Err(INVALID_STATE_VALUE) instead of liberally coercing it. See callout below. |
deliberate departure |
| Graceful degradation | RECONCILE, HOLD, and DELAY_AND_AUDIT are non-terminal, non-crashing outcomes a caller can act on, rather than a thrown error or a forced commit. |
confirmed (loose analogy) |
| Circuit breaker design pattern | allGatesClear() failing routes the Slow Path straight to HOLD, functioning as an open-circuit block on further evaluation. See caveat below. |
partial analogy |
Postel's Law ("be liberal in what you accept") is the industry-standard default, but this module inverts it
on purpose for stateVotes: GAP 05 (§6) was exactly the failure mode that liberal acceptance
produces — a typo'd vote silently degrading into an ordinary ABORT, indistinguishable from a
genuine "no." Strict validation was the fix. This is a considered exception to Postel's Law, not an
unawareness of it — liberal acceptance is still the right default for most integration boundaries.
allGatesClear() has no memory across calls: no trip counter, no half-open probe state, no reset
timer. Every invocation re-evaluates gateChecks from scratch. It reproduces the pattern's
effect (block downstream work when upstream integrity has failed) without its state machine — worth
flagging so "circuit breaker" isn't read as a claim of the full pattern.
These were checked against the source and are simply not present — consistent with, not contradicting, the liveness disclaimer already made in §5 (no retry, timeout, or leader election anywhere in the module).
| Concept | Finding |
|---|---|
| Timeout (computing) | No deadline or timer is enforced anywhere. entropyBufferTriggered is a caller-supplied boolean the module reacts to — it does not itself run a clock or a wait. |
| Keepalive | No heartbeat or liveness signal exists. The module has no way to distinguish a vote source that is slow from one that is permanently gone. |
| Futures and promises | Every exported function, including a caller-supplied pollFn, is called and used synchronously. A pollFn that genuinely re-polls live agents over a network would need to return a Promise, which the current runCyclicAudit does not await — a real constraint on v1.0, not yet a documented GAP. |
The module header's "NO PHASE LOCKS" principle (and the README's identical language) describes the absence of shared mutable state and cross-call ordering requirements — a software-engineering property. It is not a technical reference to phase-locking, the phase-locked loop, injection locking, or the lock-in amplifier — the electrical-engineering concepts the term is borrowed from. None of this module's functions have an oscillator, a frequency reference, or a signal-synchronization property. Consistent with how the project's "vortex math" (3-6-9) framing is already disclosed as metaphor rather than mechanism (Abstract, §5), no literal EE claim should be read into this naming.
validateStateVotes checks membership via
VALID_VOTE_STATES = new Set(Object.values(VoteState)), giving average-case O(1) lookups instead
of an O(n) array scan. The set is fixed at 7 short string keys, so
hash collision
behavior is cited here only to rule it out as a concern, not because it is one — there is no attacker-controlled
key space at this layer.
.filter, .some, .every; see also
JS syntax — arrow functions)
and ordinary if/for
control flow —
no exotic or version-specific language features anywhere in the module.
The following were considered and don't describe anything this module does. They're recorded so the absence reads as a checked decision, not an oversight.
Outcome, VoteState, VotePath,
ErrorCode) is Object.freeze, a direct mutation lock — not a Proxy-based
interception layer.
SharedArrayBuffer, and no
cross-thread shared mutable state. The "no shared mutable state" property already claimed (design notes, §1)
is a purity and testability guarantee about ordinary synchronous JS call semantics — it is not a claim about
immunity to speculative-execution timing side-channels, which is a distinct, hardware-level concern this
module has no surface area for one way or the other.
| 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 / Outcome / VoteState / VotePath | frozen enums | — |
export function fastPath(primaryVotes, stabilizerVotes) { const primaryCheck = validateVoteArray(primaryVotes, 3, "primaryVotes"); if (isErr(primaryCheck)) return primaryCheck; const stabilizerCheck = validateVoteArray(stabilizerVotes, 4, "stabilizerVotes"); if (isErr(stabilizerCheck)) return stabilizerCheck; const primary = takeFirst(primaryCheck.value, 3); if (!hasQuorum(primary, 3)) { return Ok({ outcome: Outcome.ABORT, details: { reason: "no primary quorum" } }); } const stabilizers = takeFirst(stabilizerCheck.value, 4); const acceptCount = countAccepts(stabilizers); // threshold scales with actual arity — fixes GAP 01 const threshold = Math.ceil(stabilizers.length * (1 / 3)); return acceptCount >= threshold ? Ok({ outcome: Outcome.COMMIT, details: { acceptCount, threshold } }) : Ok({ outcome: Outcome.ROLLBACK, details: { acceptCount, threshold } }); }
export function slowPath({ stateVotes, gateChecks, entropyBufferTriggered = false }) { const gateCheckResult = validateGateArray(gateChecks); if (isErr(gateCheckResult)) return gateCheckResult; const stateVoteResult = validateStateVotes(stateVotes); if (isErr(stateVoteResult)) return stateVoteResult; // ── ordered guard chain — gates → escalation → entropy → majority ── if (!allGatesClear(gateCheckResult.value)) { return Ok({ outcome: Outcome.HOLD, details: { reason: "structural gates not clear" } }); } if (stateVoteResult.value.some((s) => s === VoteState.ESCALATE)) { return Ok({ outcome: Outcome.HUMAN_REVIEW, details: { reason: "escalate vote present" } }); } if (entropyBufferTriggered) { return Ok({ outcome: Outcome.DELAY_AND_AUDIT, details: { reason: "entropy buffer triggered ('?')" } }); } if (majorityYes(stateVoteResult.value)) { return Ok({ outcome: Outcome.SAFE_COMMIT, details: {} }); } return Ok({ outcome: Outcome.ABORT, details: { reason: "no majority yes" } }); }
References:
[1] Lamport, L. — "The Part-Time Parliament" (Paxos), ACM TOCS, 1998
[2] Ongaro, D. & Ousterhout, J. — "In Search of an Understandable Consensus Algorithm" (Raft), USENIX ATC, 2014
[3] Castro, M. & Liskov, B. — "Practical Byzantine Fault Tolerance", OSDI, 1999
[4] Lynch, N. — Distributed Algorithms, Morgan Kaufmann, 1996
[5] vortexVote.js — reference implementation source, Bun.js module, v1.0 (Miguel Ocampo, 2026)