Skip to content

Commit 9302883

Browse files
authored
Merge pull request #16 from joshuabaer/feature/USV-2-review-usvotes-code
refactor: consolidate duplicated patterns and improve code quality
2 parents 6cfc30c + 30c1584 commit 9302883

File tree

9 files changed

+115
-236
lines changed

9 files changed

+115
-236
lines changed

worker/src/county-seeder.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import { extractSourcesFromResponse, mergeSources, validateRaceUpdate } from "./updater.js";
1414
import { logTokenUsage } from "./usage-logger.js";
1515
import { buildCondensedBallotDescription } from "./pwa-guide.js";
16+
import { ELECTION_SUFFIX } from "./state-config.js";
1617

1718
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1819

@@ -342,7 +343,7 @@ IMPORTANT:
342343
// Ensure countyName is in the ballot data for the candidates index
343344
if (!result.countyName) result.countyName = countyName;
344345

345-
const key = `ballot:county:${countyFips}:${party}_primary_2026`;
346+
const key = `ballot:county:${countyFips}:${party}${ELECTION_SUFFIX}`;
346347
await env.ELECTION_DATA.put(key, JSON.stringify(result));
347348
// Invalidate candidates_index cache so it rebuilds with new county data
348349
try { await env.ELECTION_DATA.delete("candidates_index"); } catch { /* non-fatal */ }

worker/src/index.js

Lines changed: 47 additions & 87 deletions
Large diffs are not rendered by default.

worker/src/llm-experiment.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
VALID_LLMS,
1212
SYSTEM_PROMPT,
1313
} from "./pwa-guide.js";
14+
import { ELECTION_SUFFIX } from "./state-config.js";
1415

1516
// MARK: - Cost Rates (per 1M tokens)
1617

@@ -207,11 +208,11 @@ async function runSingleExperiment(env, profileId, llmKey, runNumber) {
207208

208209
try {
209210
// 1. Read ballot data from KV
210-
var ballotKey = "ballot:statewide:" + expProfile.party + "_primary_2026";
211+
var ballotKey = "ballot:statewide:" + expProfile.party + ELECTION_SUFFIX;
211212
var raw = await env.ELECTION_DATA.get(ballotKey);
212213
if (!raw) {
213214
// Try legacy key
214-
raw = await env.ELECTION_DATA.get("ballot:" + expProfile.party + "_primary_2026");
215+
raw = await env.ELECTION_DATA.get("ballot:" + expProfile.party + ELECTION_SUFFIX);
215216
}
216217
if (!raw) {
217218
result.error = "No ballot data found for " + expProfile.party;

worker/src/pwa-guide.js

Lines changed: 45 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Ported from ClaudeService.swift
33

44
import { logTokenUsage } from "./usage-logger.js";
5-
import { getElectionPhase, ELECTION_PHASES } from "./state-config.js";
5+
import { getElectionPhase, ELECTION_PHASES, ELECTION_SUFFIX } from "./state-config.js";
66

77
const SYSTEM_PROMPT =
88
"You are a non-partisan voting guide assistant for Texas elections. " +
@@ -21,6 +21,22 @@ const SYSTEM_PROMPT =
2121

2222
const MODELS = ["claude-sonnet-4-6", "claude-sonnet-4-20250514", "claude-haiku-4-5-20251001"];
2323

24+
function bufToHex(buffer) {
25+
var arr = new Uint8Array(buffer);
26+
var hex = "";
27+
for (var i = 0; i < arr.length; i++) {
28+
hex += arr[i].toString(16).padStart(2, "0");
29+
}
30+
return hex;
31+
}
32+
33+
async function resolvePhase(url, env) {
34+
var stateCode = url.pathname.startsWith("/dc/") ? "dc" : "tx";
35+
var testPhase = url.searchParams.get("test_phase");
36+
var kvPhase = (testPhase && ELECTION_PHASES.includes(testPhase)) ? testPhase : await env.ELECTION_DATA.get("site_phase:" + stateCode);
37+
return getElectionPhase(stateCode, { kvPhase });
38+
}
39+
2440
function json(data, status = 200) {
2541
return new Response(JSON.stringify(data), {
2642
status,
@@ -71,22 +87,15 @@ async function hashGuideKey(profile, ballot, party, lang, readingLevel, llm) {
7187

7288
var data = new TextEncoder().encode(JSON.stringify(keyObj));
7389
var hashBuffer = await crypto.subtle.digest("SHA-256", data);
74-
var hashArray = new Uint8Array(hashBuffer);
75-
var hex = "";
76-
for (var i = 0; i < hashArray.length; i++) {
77-
hex += hashArray[i].toString(16).padStart(2, "0");
78-
}
90+
var hex = bufToHex(hashBuffer);
7991
return hex;
8092
}
8193

8294
export async function handlePWA_Guide(request, env) {
8395
try {
8496
// Check election phase — block guide generation after polls close
8597
var requestUrl = new URL(request.url);
86-
var stateCode = requestUrl.pathname.startsWith("/dc/") ? "dc" : "tx";
87-
var testPhase = requestUrl.searchParams.get("test_phase");
88-
var kvPhase = (testPhase && ELECTION_PHASES.includes(testPhase)) ? testPhase : await env.ELECTION_DATA.get("site_phase:" + stateCode);
89-
var phase = getElectionPhase(stateCode, { kvPhase });
98+
var phase = await resolvePhase(requestUrl, env);
9099
if (phase === "post-election" || phase === "election-night") {
91100
return json({ error: "Guide generation is closed. The primary election has ended.", phase }, 410);
92101
}
@@ -105,10 +114,10 @@ export async function handlePWA_Guide(request, env) {
105114

106115
// Parallel KV reads — statewide, legacy fallback, county, and manifest are independent
107116
var [statewideRaw, legacyRaw, countyRaw, manifestRaw] = await Promise.all([
108-
env.ELECTION_DATA.get("ballot:statewide:" + party + "_primary_2026"),
109-
env.ELECTION_DATA.get("ballot:" + party + "_primary_2026"),
117+
env.ELECTION_DATA.get("ballot:statewide:" + party + ELECTION_SUFFIX),
118+
env.ELECTION_DATA.get("ballot:" + party + ELECTION_SUFFIX),
110119
countyFips
111-
? env.ELECTION_DATA.get("ballot:county:" + countyFips + ":" + party + "_primary_2026")
120+
? env.ELECTION_DATA.get("ballot:county:" + countyFips + ":" + party + ELECTION_SUFFIX)
112121
: Promise.resolve(null),
113122
env.ELECTION_DATA.get("manifest"),
114123
]);
@@ -180,11 +189,7 @@ export async function handlePWA_Guide(request, env) {
180189
electionName: ballot.electionName,
181190
}));
182191
var ballotDescHashBuf = await crypto.subtle.digest("SHA-256", ballotDescData);
183-
var ballotDescHashArr = new Uint8Array(ballotDescHashBuf);
184-
var ballotDescHex = "";
185-
for (var h = 0; h < ballotDescHashArr.length; h++) {
186-
ballotDescHex += ballotDescHashArr[h].toString(16).padStart(2, "0");
187-
}
192+
var ballotDescHex = bufToHex(ballotDescHashBuf);
188193
ballotDescCacheKey = "ballot_desc:" + ballotDescHex;
189194
var cachedDesc = await env.ELECTION_DATA.get(ballotDescCacheKey);
190195
if (cachedDesc) {
@@ -269,10 +274,7 @@ export async function handlePWA_Summary(request, env) {
269274
try {
270275
// Check election phase — block summary generation after polls close
271276
var summaryUrl = new URL(request.url);
272-
var summaryState = summaryUrl.pathname.startsWith("/dc/") ? "dc" : "tx";
273-
var summaryTestPhase = summaryUrl.searchParams.get("test_phase");
274-
var summaryKvPhase = (summaryTestPhase && ELECTION_PHASES.includes(summaryTestPhase)) ? summaryTestPhase : await env.ELECTION_DATA.get("site_phase:" + summaryState);
275-
var summaryPhase = getElectionPhase(summaryState, { kvPhase: summaryKvPhase });
277+
var summaryPhase = await resolvePhase(summaryUrl, env);
276278
if (summaryPhase === "post-election" || summaryPhase === "election-night") {
277279
return json({ error: "Guide generation is closed. The primary election has ended.", phase: summaryPhase }, 410);
278280
}
@@ -557,7 +559,7 @@ async function loadCachedTranslations(env, party, countyFips) {
557559
var translations = [];
558560

559561
// Load statewide translations
560-
var statewideKey = "translations:es:" + party + "_primary_2026";
562+
var statewideKey = "translations:es:" + party + ELECTION_SUFFIX;
561563
var statewideRaw = await env.ELECTION_DATA.get(statewideKey);
562564
if (statewideRaw) {
563565
try {
@@ -570,7 +572,7 @@ async function loadCachedTranslations(env, party, countyFips) {
570572

571573
// Load county-specific translations if countyFips provided
572574
if (countyFips) {
573-
var countyKey = "translations:es:county:" + countyFips + ":" + party + "_primary_2026";
575+
var countyKey = "translations:es:county:" + countyFips + ":" + party + ELECTION_SUFFIX;
574576
var countyRaw = await env.ELECTION_DATA.get(countyKey);
575577
if (countyRaw) {
576578
try {
@@ -602,29 +604,28 @@ async function loadCachedTranslations(env, party, countyFips) {
602604
*/
603605
export async function handleSeedTranslations(env, party, countyFips) {
604606
// Load ballot data
605-
var ballotKey, translationKey;
607+
var ballotKey, translationKey, ballotRaw;
606608
if (countyFips) {
607-
ballotKey = "ballot:county:" + countyFips + ":" + party + "_primary_2026";
608-
translationKey = "translations:es:county:" + countyFips + ":" + party + "_primary_2026";
609+
ballotKey = "ballot:county:" + countyFips + ":" + party + ELECTION_SUFFIX;
610+
translationKey = "translations:es:county:" + countyFips + ":" + party + ELECTION_SUFFIX;
611+
ballotRaw = await env.ELECTION_DATA.get(ballotKey);
612+
if (!ballotRaw) {
613+
return { error: "No ballot data found at " + ballotKey };
614+
}
609615
} else {
610-
ballotKey = "ballot:statewide:" + party + "_primary_2026";
611-
translationKey = "translations:es:" + party + "_primary_2026";
616+
ballotKey = "ballot:statewide:" + party + ELECTION_SUFFIX;
617+
translationKey = "translations:es:" + party + ELECTION_SUFFIX;
612618
// Fallback to legacy key
613-
var raw = await env.ELECTION_DATA.get(ballotKey);
614-
if (!raw) {
615-
ballotKey = "ballot:" + party + "_primary_2026";
616-
raw = await env.ELECTION_DATA.get(ballotKey);
619+
ballotRaw = await env.ELECTION_DATA.get(ballotKey);
620+
if (!ballotRaw) {
621+
ballotKey = "ballot:" + party + ELECTION_SUFFIX;
622+
ballotRaw = await env.ELECTION_DATA.get(ballotKey);
617623
}
618-
if (!raw) {
624+
if (!ballotRaw) {
619625
return { error: "No ballot data found for " + party };
620626
}
621627
}
622628

623-
var ballotRaw = await env.ELECTION_DATA.get(ballotKey);
624-
if (!ballotRaw) {
625-
return { error: "No ballot data found at " + ballotKey };
626-
}
627-
628629
var ballot = JSON.parse(ballotRaw);
629630
if (!ballot.races || !ballot.races.length) {
630631
return { error: "No races in ballot" };
@@ -1772,10 +1773,7 @@ export async function handlePWA_GuideStream(request, env) {
17721773
var requestUrl = new URL(request.url);
17731774

17741775
// Check election phase — block guide generation after polls close
1775-
var stateCode = requestUrl.pathname.startsWith("/dc/") ? "dc" : "tx";
1776-
var testPhase = requestUrl.searchParams.get("test_phase");
1777-
var kvPhase = (testPhase && ELECTION_PHASES.includes(testPhase)) ? testPhase : await env.ELECTION_DATA.get("site_phase:" + stateCode);
1778-
var phase = getElectionPhase(stateCode, { kvPhase });
1776+
var phase = await resolvePhase(requestUrl, env);
17791777
if (phase === "post-election" || phase === "election-night") {
17801778
return new Response("event: error\ndata: " + JSON.stringify({ error: "Guide generation is closed. The primary election has ended.", phase }) + "\n\n", {
17811779
status: 410,
@@ -1836,10 +1834,10 @@ export async function handlePWA_GuideStream(request, env) {
18361834
try {
18371835
// Parallel KV reads
18381836
var [statewideRaw, legacyRaw, countyRaw, manifestRaw] = await Promise.all([
1839-
env.ELECTION_DATA.get("ballot:statewide:" + party + "_primary_2026"),
1840-
env.ELECTION_DATA.get("ballot:" + party + "_primary_2026"),
1837+
env.ELECTION_DATA.get("ballot:statewide:" + party + ELECTION_SUFFIX),
1838+
env.ELECTION_DATA.get("ballot:" + party + ELECTION_SUFFIX),
18411839
countyFips
1842-
? env.ELECTION_DATA.get("ballot:county:" + countyFips + ":" + party + "_primary_2026")
1840+
? env.ELECTION_DATA.get("ballot:county:" + countyFips + ":" + party + ELECTION_SUFFIX)
18431841
: Promise.resolve(null),
18441842
env.ELECTION_DATA.get("manifest"),
18451843
]);
@@ -1968,11 +1966,7 @@ export async function handlePWA_GuideStream(request, env) {
19681966
electionName: ballot.electionName,
19691967
}));
19701968
var ballotDescHashBuf = await crypto.subtle.digest("SHA-256", ballotDescData);
1971-
var ballotDescHashArr = new Uint8Array(ballotDescHashBuf);
1972-
var ballotDescHex = "";
1973-
for (var h = 0; h < ballotDescHashArr.length; h++) {
1974-
ballotDescHex += ballotDescHashArr[h].toString(16).padStart(2, "0");
1975-
}
1969+
var ballotDescHex = bufToHex(ballotDescHashBuf);
19761970
ballotDescCacheKey = "ballot_desc:" + ballotDescHex;
19771971
var cachedDesc = await env.ELECTION_DATA.get(ballotDescCacheKey);
19781972
if (cachedDesc) {

worker/src/state-config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ export function getElectionPhase(stateCode, options = {}) {
6464
return 'post-election';
6565
}
6666

67+
// Election suffix for KV key construction (changes each election cycle)
68+
export const ELECTION_SUFFIX = '_primary_2026';
69+
6770
// Valid state codes
6871
export const VALID_STATES = Object.keys(STATE_CONFIG);
6972

worker/src/stats-email.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import { getUsageLog, estimateCost } from "./usage-logger.js";
1010
import { checkBallotBalance } from "./balance-check.js";
11+
import { ELECTION_SUFFIX } from "./state-config.js";
1112

1213
// Election date for frequency switching
1314
const ELECTION_DATE = "2026-03-03";
@@ -103,8 +104,8 @@ export async function collectEmailStats(env, options = {}) {
103104
cronStatusRaw,
104105
cronStatusYesterdayRaw,
105106
] = await Promise.all([
106-
env.ELECTION_DATA.get("ballot:statewide:republican_primary_2026"),
107-
env.ELECTION_DATA.get("ballot:statewide:democrat_primary_2026"),
107+
env.ELECTION_DATA.get("ballot:statewide:republican" + ELECTION_SUFFIX),
108+
env.ELECTION_DATA.get("ballot:statewide:democrat" + ELECTION_SUFFIX),
108109
env.ELECTION_DATA.get("audit:summary"),
109110
env.ELECTION_DATA.get(`usage_log:${today}`),
110111
env.ELECTION_DATA.get(`usage_log:${yesterday}`),

worker/src/updater.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { TOP_COUNTIES, seedCountyBallot, seedCountyInfo, seedPrecinctMap } from
44
import { logTokenUsage } from "./usage-logger.js";
55
import { checkSingleCandidateBalance } from "./balance-check.js";
66
import { buildCondensedBallotDescription } from "./pwa-guide.js";
7+
import { ELECTION_SUFFIX } from "./state-config.js";
78

89
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
910

@@ -632,8 +633,8 @@ export function isUpdateMeaningful(updates) {
632633

633634
const PARTIES = ["republican", "democrat"];
634635
const BALLOT_KEYS = {
635-
republican: "ballot:statewide:republican_primary_2026",
636-
democrat: "ballot:statewide:democrat_primary_2026",
636+
republican: `ballot:statewide:republican${ELECTION_SUFFIX}`,
637+
democrat: `ballot:statewide:democrat${ELECTION_SUFFIX}`,
637638
};
638639

639640
// ---------------------------------------------------------------------------
@@ -712,7 +713,7 @@ export async function seedBaseline(party, env) {
712713
baseline.races.push(baselineRace);
713714
}
714715

715-
const key = `${BASELINE_KEY_PREFIX}${party}_primary_2026`;
716+
const key = `${BASELINE_KEY_PREFIX}${party}${ELECTION_SUFFIX}`;
716717
await env.ELECTION_DATA.put(key, JSON.stringify(baseline));
717718

718719
return { success: true, candidateCount, key };
@@ -932,7 +933,7 @@ export async function runDailyUpdate(env, options = {}) {
932933
// --- Load verified baseline for this party (if it exists) ---
933934
let baseline = null;
934935
try {
935-
const baselineKey = `${BASELINE_KEY_PREFIX}${party}_primary_2026`;
936+
const baselineKey = `${BASELINE_KEY_PREFIX}${party}${ELECTION_SUFFIX}`;
936937
const baselineRaw = await env.ELECTION_DATA.get(baselineKey);
937938
if (baselineRaw) baseline = JSON.parse(baselineRaw);
938939
} catch { /* no baseline or corrupt — proceed without it */ }
@@ -1922,7 +1923,7 @@ export async function runCountyRefresh(env, options = {}) {
19221923
for (const party of PARTIES) {
19231924
try {
19241925
// Check if county ballot exists in KV -- only refresh existing data
1925-
const ballotKey = `ballot:county:${county.fips}:${party}_primary_2026`;
1926+
const ballotKey = `ballot:county:${county.fips}:${party}${ELECTION_SUFFIX}`;
19261927
const existing = await env.ELECTION_DATA.get(ballotKey);
19271928

19281929
if (!existing) {

worker/tests/index-helpers.test.js

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,11 @@ import { describe, it, expect } from "vitest";
22
import { readFileSync } from "fs";
33
import { join, dirname } from "path";
44
import { fileURLToPath } from "url";
5+
import { normalizeEndorsement, nameToSlug, isSparseCandidate, escapeHtml, resolveTone, resolveToneArray } from "../src/index.js";
56

67
const __dirname = dirname(fileURLToPath(import.meta.url));
78
const indexSrc = readFileSync(join(__dirname, "../src/index.js"), "utf-8");
89

9-
// ---------------------------------------------------------------------------
10-
// Extract helper functions from index.js source (same approach as routes.test.js)
11-
// ---------------------------------------------------------------------------
12-
13-
function normalizeEndorsement(e) {
14-
if (typeof e === "string") return { name: e, type: null };
15-
return { name: e.name || String(e), type: e.type || null };
16-
}
17-
18-
function resolveTone(value) {
19-
if (value === null || value === undefined) return null;
20-
if (typeof value === "string") return value;
21-
if (typeof value === "object" && !Array.isArray(value)) {
22-
return value["3"] || value[Object.keys(value).sort()[0]] || null;
23-
}
24-
return null;
25-
}
26-
27-
function resolveToneArray(arr) {
28-
if (!Array.isArray(arr)) return [];
29-
return arr.map(item => resolveTone(item)).filter(Boolean);
30-
}
31-
32-
function nameToSlug(name) {
33-
if (!name) return "";
34-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
35-
}
36-
37-
function isSparseCandidate(c) {
38-
let filled = 0;
39-
if (c.pros && (Array.isArray(c.pros) ? c.pros.length : true)) filled++;
40-
if (c.cons && (Array.isArray(c.cons) ? c.cons.length : true)) filled++;
41-
if (c.endorsements && (Array.isArray(c.endorsements) ? c.endorsements.length : true)) filled++;
42-
if (c.keyPositions && c.keyPositions.length) filled++;
43-
return filled < 2;
44-
}
45-
46-
function escapeHtml(str) {
47-
if (!str) return "";
48-
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
49-
}
50-
5110
function classifyConfidence(candidate) {
5211
const hasSources = candidate.sources && candidate.sources.length > 0;
5312
const hasOfficialSource = hasSources && candidate.sources.some(s =>

0 commit comments

Comments
 (0)