Skip to content

About

this module implements three Result-typed, no-silent-failure threshold-gating decision rules (3-4-1/3, 3-6-9, 7-4-?) for gating risky actions think of a rules engine for "should this proceed?"

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

8 Commits

Folders and files

Repository files navigation

vortexVote.js

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.

The three paths, by risk tier

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.

Formal model

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.

Decision flow

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;
Loading

Fast Path — 3-4-1/3 (unanimity gate → plurality gate)

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;
Loading

Middle Path — 3-6-9 (quorum gate → repeated majority sampling)

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;
Loading

Slow Path — 7-4-? (priority-ordered guard chain)

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;
Loading

Use cases, matched to path

  • 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.

Install

No dependencies. Copy vortexVote.js into your project, or clone the repo:

bun add ./vortexVote.js   # local module

Requires Bun (or any modern JS runtime that supports ES modules).

Usage

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

API

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

Testing

bun test

34 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.

Design notes

  • 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 like ABORT. 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 pollFn so 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.

License

MIT

About

this module implements three Result-typed, no-silent-failure threshold-gating decision rules (3-4-1/3, 3-6-9, 7-4-?) for gating risky actions think of a rules engine for "should this proceed?"

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages