election-vibe 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cli.js +61 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -302,6 +302,8 @@ To remove it all, in this order (the plugin lives inside the package's folder, s
|
|
|
302
302
|
|
|
303
303
|
**Does collection stop on November 4?** No: the collector runs until you stop it. November 4 is how long the tool is supported; see [Operating window](#operating-window).
|
|
304
304
|
|
|
305
|
+
**Why does `collect --once` take so long?** It is not stuck. `--once` reads everything that is due, once, then exits. On a fresh install that is every contest the collector reads from civicAPI (471, plus one for each followed California contest that civicAPI also carries), one request at a time at about one per second, so the single pass takes about 10 minutes and prints its one log line at the end. Plain `elections collect` paces itself (at most 9 civicAPI requests per pass, then a 10-second wait before the next pass) and is the normal way to run it. For a quick connectivity check, `elections collect --once --civic-limit 2` bounds the pass to two civicAPI requests and exits in seconds.
|
|
306
|
+
|
|
305
307
|
**Does this run on Windows?** Not tested. This release was built and tested on macOS. The CLI uses only Node, but the plugin assumes a macOS or Linux shell path, and nothing here has been tried on Windows or Linux.
|
|
306
308
|
|
|
307
309
|
**Does `race` treat the comma as part of the query?** Yes. `race` takes its words as one phrase, punctuation included, and looks for it in titles and ids: `"palo alto, transactions"` matches Palo Alto's Measure J because its title contains that phrase. `find` is the one that matches word by word.
|
package/dist/cli.js
CHANGED
|
@@ -216,7 +216,7 @@ var REHEARSAL_SENTENCE = {
|
|
|
216
216
|
|
|
217
217
|
// src/http.ts
|
|
218
218
|
import { gunzipSync } from "zlib";
|
|
219
|
-
var USER_AGENT = `ElectionsCompanion/${"0.1.
|
|
219
|
+
var USER_AGENT = `ElectionsCompanion/${"0.1.2"} (unofficial results reader; local, no telemetry)`;
|
|
220
220
|
function retryAfterMs(header, nowMs) {
|
|
221
221
|
if (!header) return void 0;
|
|
222
222
|
const t = header.trim();
|
|
@@ -1943,6 +1943,8 @@ var CIVIC_BLANKET_FLOOR_SEC = 900;
|
|
|
1943
1943
|
var THROTTLE_FLOOR_MS = 5 * 6e4;
|
|
1944
1944
|
var BACKOFF_MAX_MS = 30 * 6e4;
|
|
1945
1945
|
var MAX_EVENTS = 2e3;
|
|
1946
|
+
var CLOSE_SPREAD_MS = 6e4;
|
|
1947
|
+
var READ_JITTER_SHARE = 0.1;
|
|
1946
1948
|
var CLARITY_RECHECK_MS = 30 * 6e4;
|
|
1947
1949
|
var LockLost = class extends Error {
|
|
1948
1950
|
constructor(report) {
|
|
@@ -2001,10 +2003,40 @@ function newerRecord(a, b) {
|
|
|
2001
2003
|
function blocked(s, nowMs) {
|
|
2002
2004
|
return !!s.deniedAt || !!s.cooldownUntil && Date.parse(s.cooldownUntil) > nowMs;
|
|
2003
2005
|
}
|
|
2004
|
-
|
|
2006
|
+
var closeOffsets = /* @__PURE__ */ new WeakMap();
|
|
2007
|
+
function closeOffsetMs(deps, id) {
|
|
2008
|
+
let run = closeOffsets.get(deps);
|
|
2009
|
+
if (!run) {
|
|
2010
|
+
run = /* @__PURE__ */ new Map();
|
|
2011
|
+
closeOffsets.set(deps, run);
|
|
2012
|
+
}
|
|
2013
|
+
let o = run.get(id);
|
|
2014
|
+
if (o === void 0) {
|
|
2015
|
+
const r = deps.random();
|
|
2016
|
+
o = r >= 0 && r < 1 ? Math.floor(r * CLOSE_SPREAD_MS) : 0;
|
|
2017
|
+
run.set(id, o);
|
|
2018
|
+
}
|
|
2019
|
+
return o;
|
|
2020
|
+
}
|
|
2021
|
+
var readJitters = /* @__PURE__ */ new WeakMap();
|
|
2022
|
+
function readJitterMs(deps, id, after, cadenceMs) {
|
|
2023
|
+
let run = readJitters.get(deps);
|
|
2024
|
+
if (!run) {
|
|
2025
|
+
run = /* @__PURE__ */ new Map();
|
|
2026
|
+
readJitters.set(deps, run);
|
|
2027
|
+
}
|
|
2028
|
+
let held = run.get(id);
|
|
2029
|
+
if (!held || held.after !== after) {
|
|
2030
|
+
const r = deps.random();
|
|
2031
|
+
held = { after, share: r >= 0 && r < 1 ? r : 0 };
|
|
2032
|
+
run.set(id, held);
|
|
2033
|
+
}
|
|
2034
|
+
return Math.floor(held.share * READ_JITTER_SHARE * cadenceMs);
|
|
2035
|
+
}
|
|
2036
|
+
function sourceCadenceMs(src, contests, nowMs, rehearsal, offsetMs, jitterMs) {
|
|
2005
2037
|
if (rehearsal) return src.cadenceSec * 1e3;
|
|
2006
|
-
const anyClosed = contests.some((c) => c.sources.some((m2) => m2.source === src.id) && Date.parse(c.pollsClose) <= nowMs);
|
|
2007
|
-
return
|
|
2038
|
+
const anyClosed = contests.some((c) => c.sources.some((m2) => m2.source === src.id) && Date.parse(c.pollsClose) + offsetMs <= nowMs);
|
|
2039
|
+
return anyClosed ? src.cadenceSec * 1e3 + jitterMs() : src.preCloseCadenceSec * 1e3;
|
|
2008
2040
|
}
|
|
2009
2041
|
function due(s, cadenceMs, nowMs) {
|
|
2010
2042
|
if (blocked(s, nowMs)) return false;
|
|
@@ -2139,7 +2171,7 @@ function sealSources(raw) {
|
|
|
2139
2171
|
}
|
|
2140
2172
|
return out;
|
|
2141
2173
|
}
|
|
2142
|
-
function civicQueue(registry, src, config, last, nowMs) {
|
|
2174
|
+
function civicQueue(registry, src, config, last, nowMs, closeOffsetMs2 = 0, jitterMs = () => 0) {
|
|
2143
2175
|
const blanketSec = Math.max(CIVIC_BLANKET_FLOOR_SEC, config.blanketSec ?? Number(src.config.blanketSec ?? 1800));
|
|
2144
2176
|
const out = [];
|
|
2145
2177
|
for (const def of registry) {
|
|
@@ -2147,10 +2179,10 @@ function civicQueue(registry, src, config, last, nowMs) {
|
|
|
2147
2179
|
if (!m2) continue;
|
|
2148
2180
|
const watch = config.follow.includes(def.id) || (def.presets ?? []).includes("hot");
|
|
2149
2181
|
if (m2.role !== "primary" && !watch) continue;
|
|
2150
|
-
const preClose = nowMs < Date.parse(def.pollsClose);
|
|
2182
|
+
const preClose = nowMs < Date.parse(def.pollsClose) + closeOffsetMs2;
|
|
2151
2183
|
const cadence = (preClose ? src.preCloseCadenceSec : watch ? src.cadenceSec : blanketSec) * 1e3;
|
|
2152
2184
|
const prev = last[m2.key];
|
|
2153
|
-
if (prev !== void 0 && nowMs - prev < cadence) continue;
|
|
2185
|
+
if (prev !== void 0 && nowMs - prev < cadence + (preClose ? 0 : jitterMs(m2.key, prev, cadence))) continue;
|
|
2154
2186
|
out.push({ def, key: m2.key, tier: watch ? "watch" : "blanket", age: prev === void 0 ? Number.POSITIVE_INFINITY : nowMs - prev });
|
|
2155
2187
|
}
|
|
2156
2188
|
out.sort((a, b) => a.tier === b.tier ? b.age - a.age : a.tier === "watch" ? -1 : 1);
|
|
@@ -2239,7 +2271,15 @@ async function cycle(deps, opts) {
|
|
|
2239
2271
|
if (src.kind === "civicapi") continue;
|
|
2240
2272
|
const approval = approvals[src.id];
|
|
2241
2273
|
const approvedSinceAttempt = !!approval && !!s.lastAttemptAt && Date.parse(approval.approvedAt) > Date.parse(s.lastAttemptAt);
|
|
2242
|
-
|
|
2274
|
+
const cadenceMs = sourceCadenceMs(
|
|
2275
|
+
src,
|
|
2276
|
+
deps.registry,
|
|
2277
|
+
t0,
|
|
2278
|
+
opts.admissionDisabled === true,
|
|
2279
|
+
closeOffsetMs(deps, src.id),
|
|
2280
|
+
() => readJitterMs(deps, src.id, s.lastAttemptAt, src.cadenceSec * 1e3)
|
|
2281
|
+
);
|
|
2282
|
+
if (!(approvedSinceAttempt && !blocked(s, t0)) && !due(s, cadenceMs, t0)) {
|
|
2243
2283
|
report.outcomes[src.id] = "skipped";
|
|
2244
2284
|
continue;
|
|
2245
2285
|
}
|
|
@@ -2497,7 +2537,15 @@ async function cycle(deps, opts) {
|
|
|
2497
2537
|
const civic = deps.sources.find((s) => s.kind === "civicapi");
|
|
2498
2538
|
if (civic && !config.disabledSources.includes(civic.id)) {
|
|
2499
2539
|
const minInterval = Number(civic.config.minIntervalMs ?? 1100);
|
|
2500
|
-
const queue = civicQueue(
|
|
2540
|
+
const queue = civicQueue(
|
|
2541
|
+
deps.registry,
|
|
2542
|
+
civic,
|
|
2543
|
+
config,
|
|
2544
|
+
cstate.civicLast,
|
|
2545
|
+
deps.now(),
|
|
2546
|
+
opts.admissionDisabled === true ? 0 : closeOffsetMs(deps, civic.id),
|
|
2547
|
+
opts.admissionDisabled === true ? void 0 : (key, prev, cadenceMs) => readJitterMs(deps, `civic:${key}`, prev, cadenceMs)
|
|
2548
|
+
);
|
|
2501
2549
|
const limit = opts.civicLimit ?? queue.length;
|
|
2502
2550
|
let n = 0;
|
|
2503
2551
|
for (const item of queue) {
|
|
@@ -2508,7 +2556,9 @@ async function cycle(deps, opts) {
|
|
|
2508
2556
|
log("civicapi: hourly ceiling reached; the rest waits");
|
|
2509
2557
|
break;
|
|
2510
2558
|
}
|
|
2511
|
-
|
|
2559
|
+
const draw = deps.random();
|
|
2560
|
+
const spread = draw >= 0 && draw < 1 ? Math.round(draw * 250) : 0;
|
|
2561
|
+
await deps.sleep(n > 0 ? minInterval + spread : spread);
|
|
2512
2562
|
n++;
|
|
2513
2563
|
cstate.civicLast[item.key] = deps.now();
|
|
2514
2564
|
const r = await get(civic, `${civic.config.base}/race/${item.key}`);
|
|
@@ -46285,7 +46335,7 @@ function storeOf(p) {
|
|
|
46285
46335
|
}
|
|
46286
46336
|
async function main(argv, io) {
|
|
46287
46337
|
if (argv[0] === "--version" || argv[0] === "version") {
|
|
46288
|
-
io.out("0.1.
|
|
46338
|
+
io.out("0.1.2");
|
|
46289
46339
|
return 0;
|
|
46290
46340
|
}
|
|
46291
46341
|
const p = parseArgs(argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "election-vibe",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "US election night, Nov 3, 2026, in your terminal and in Claude Code: unofficial results from official free sources and civicAPI, kept on your machine. Never calls a race. Unofficial results. Not an election authority. Not affiliated with any government, campaign, or party.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"election",
|