Skip to content

feat(backend): Ohio finance PR 7 — sync, batchSync, scheduler, npm scripts - #545

Merged
shu1513 merged 4 commits into
mainfrom
claude/ohio-finance-sync-pr7
Aug 6, 2026
Merged

shu1513 merged 4 commits into
mainfrom
claude/ohio-finance-sync-pr7

Conversation

@shu1513

@shu1513 shu1513 commented Aug 5, 2026 •

Copy link
Copy Markdown
Owner

Ohio campaign-finance PR 7 of 9 (ohio_plan.md): sync + batchSync + scheduler + npm scripts, maryland block as template.

What's here

  • ohioCandidateFinanceSync.ts — per-candidate write step. Takes aggregation results, not raw rows: the batch layer owns the single stream over the ~90 MB CAC_CON files (decision 10). Committee identity is the due row's active link (the due list only returns linked candidates, so no resolver call). outsideFinance: null writes NULL outside totals — the writer's preserveWhenNull policy keeps stored values — and leaves outside-group rows untouched; an empty groups list clears stale rows.
  • ohioCandidateFinanceBatchSync.ts — due list via the shared factory over the oh_ tables; auto-link phase reuses the PR 5 walker with the cached active-candidate list; per election year one direct accumulator per distinct linked committee fed by one CAC_CON_{Y-1,Y} pass, cover files loaded once per run, and one outside-spending aggregation over (candidate name, office)-deduped targets — without the dedupe, a candidate due for both primary and general would appear twice and quarantine every row aimed at them as ambiguous.
  • Artifact failure policy: missing direct artifacts fail the year's rows (receipts would be fabricated); missing outside artifacts only disable the outside leg, surfaced in a per-year outsideAggregationByYear summary for the PR 9 live-run report.
  • readOhioSos31uDetailBundle — reads the 31-U detail bundle fail-closed, accepting both the version-1 payload PR 4 writes and the 2026-08-04 spike checkpoint format, so the existing 305 MB cache works without another attended Chrome session.
  • Scheduler + scripts: BullMQ scheduler (queue ohio_candidate_finance_sync_maintenance, daily cron default 55 9 * * * UTC), sync-due / scheduler:upsert / scheduler:worker / scheduler:trigger scripts, and the ohio-candidates:finance:* npm block.

Verification

  • npm run typecheck + npm test green in backend/ (6,289 tests; 17 new: 6 sync, 7 batchSync, 4 bundle reader).
  • End-to-end dry run over the real cached 2026 cycle reproduces the PR 6 smoke exactly: Acton receipts $15,898,732.10 / cash $8,136,281.98 / oppose $8,211,114.50; Ramaswamy receipts $55,081,342.57 (cover beats the $30.3M itemized sum) / support $600,000; Stephens receipts $343,371.62 / oppose $217,704.16. 13 reports reconciled, 0 quarantined; the attributed-cents delta vs the PR 6 smoke is exactly the Perez+Haines $102,348.10 this due list does not cover.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added automated synchronization of Ohio candidate campaign-finance data, including direct contributions and outside spending.
    • Added scheduled and manually triggered sync operations with configurable limits, date ranges, and dry-run support.
    • Added compatibility for current and legacy finance-data bundles.
    • Added structured results, reconciliation summaries, and improved handling when source data is unavailable or incomplete.
  • Documentation

    • Updated the Ohio finance implementation plan to reflect completed synchronization and scheduling capabilities.

…ripts

Per-candidate sync (ohioCandidateFinanceSync.ts) is a write step that takes
aggregation results rather than raw rows: the batch layer owns the single
stream over the ~90 MB CAC_CON files (plan decision 10). The committee comes
from the due row's active link; outsideFinance null writes NULL outside
totals (writer preserveWhenNull) and leaves stored outside groups untouched,
while an empty list clears stale rows.

Batch sync (ohioCandidateFinanceBatchSync.ts): shared due-list factory over
the oh_ tables; auto-link phase reuses the PR 5 walker with the cached
active-candidate list; per election year one direct accumulator per distinct
linked committee fed by one CAC_CON_{Y-1,Y} pass, cover files loaded once
per run, and one outside-spending aggregation over (name, office)-deduped
targets so a candidate due for both primary and general cannot quarantine
their own rows as ambiguous. Missing direct artifacts fail the year's rows;
missing outside artifacts only disable the outside leg, surfaced in a
per-year availability summary.

readOhioSos31uDetailBundle accepts both the version-1 payload and the
2026-08-04 spike checkpoint format, keeping the existing 305 MB artifact
cache usable without another attended Chrome session.

BullMQ scheduler (queue ohio_candidate_finance_sync_maintenance, daily cron
default 55 9 * * * UTC) plus sync-due / scheduler:upsert / scheduler:worker
/ scheduler:trigger scripts and the ohio-candidates:finance npm block,
maryland as template.

Verified: typecheck + full backend suite green (6,289 tests; 17 new), and an
end-to-end dry run over the real cached cycle reproduces the PR 6 smoke
exactly - Acton oppose $8,211,114.50, Ramaswamy support $600,000 with cover
receipts $55,081,342.57 beating the $30.3M itemized sum, Stephens oppose
$217,704.16; 13 reports reconciled, 0 quarantined.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@shu1513, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a4659024-d82f-4179-8756-dc7e3949d5a5

📥 Commits

Reviewing files that changed from the base of the PR and between fe07f67 and cd7132f.

📒 Files selected for processing (4)
  • backend/src/scripts/syncDueOhioCandidateFinance.ts
  • backend/src/scripts/triggerOhioCandidateFinanceSync.ts
  • backend/src/scripts/upsertOhioCandidateFinanceSyncScheduler.ts
  • backend/tests/scripts/ohioCandidateFinanceCliArgs.test.ts
📝 Walkthrough

Walkthrough

Added Ohio candidate finance synchronization with cached artifact parsing, direct and outside-spending aggregation, snapshot persistence, due-row batch processing, BullMQ scheduling, worker execution, CLI commands, and comprehensive tests.

Changes

Ohio finance synchronization

Layer / File(s) Summary
Finance persistence and artifact compatibility
backend/src/pipeline/ohioFinance/ohioCandidateFinanceSync.ts, backend/src/pipeline/ohioFinance/ohioSosArtifactAcquisition.ts, backend/src/pipeline/ohioFinance/ohioSosBulkFiles.ts, backend/src/pipeline/ohioFinance/ohioDirectContributionAggregator.ts, backend/src/pipeline/ohioFinance/ohioOutsideSpendingAggregator.ts, backend/tests/pipeline/ohioFinance/*Sync.test.ts, backend/tests/pipeline/ohioFinance/ohioSosArtifactAcquisition.test.ts, backend/tests/pipeline/ohioFinance/ohioOutsideSpendingAggregator.test.ts
Validates inputs, writes finance snapshots, preserves unavailable outside data, supports dry runs, handles cover rows, and reads version-1 and legacy 31-U bundles.
Due-row batch aggregation
backend/src/pipeline/ohioFinance/ohioCandidateFinanceBatchSync.ts, backend/tests/pipeline/ohioFinance/ohioCandidateFinanceBatchSync.test.ts
Loads cached artifacts, aggregates direct and outside finance by election year, auto-links missing candidates, handles artifact failures, and returns per-item results.
Scheduler and worker runtime
backend/src/scheduler/ohioCandidateFinanceSyncScheduler.ts, backend/src/scripts/runOhioCandidateFinanceSyncSchedulerWorker.ts
Adds BullMQ queue management, recurring and manual jobs, feature-flag handling, single-concurrency processing, lifecycle logging, and graceful shutdown.
CLI and project wiring
backend/src/scripts/syncDueOhioCandidateFinance.ts, backend/src/scripts/upsertOhioCandidateFinanceSyncScheduler.ts, backend/src/scripts/triggerOhioCandidateFinanceSync.ts, backend/package.json, backend/src/pipeline/ohioFinance/index.ts, ohio_plan.md
Adds executable commands, argument validation, environment handling, structured output, pipeline exports, and plan updates.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant SchedulerCLI
  participant BullMQ
  participant OhioFinanceWorker
  participant OhioCandidateFinanceBatchSync
  participant PostgreSQL
  Operator->>SchedulerCLI: configure or trigger finance sync
  SchedulerCLI->>BullMQ: upsert or enqueue job
  BullMQ->>OhioFinanceWorker: deliver finance job
  OhioFinanceWorker->>OhioCandidateFinanceBatchSync: run due-row synchronization
  OhioCandidateFinanceBatchSync->>PostgreSQL: read due rows and write snapshots
  PostgreSQL-->>OhioCandidateFinanceBatchSync: return synchronization results
  OhioCandidateFinanceBatchSync-->>OhioFinanceWorker: return job result
Loading

Possibly related PRs

  • shu1513/voteapp#534: Adds the Ohio finance writer and related schema and feature-flag foundation.
  • shu1513/voteapp#538: Adds the Ohio SOS artifact acquisition and 31-U bundle outputs used by this synchronization.
  • shu1513/voteapp#539: Adds the Ohio candidate committee auto-linking flow used before due synchronization.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.87% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the Ohio finance synchronization, batch processing, scheduler, and npm script changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/ohio-finance-sync-pr7

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@shu1513

shu1513 commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
backend/src/pipeline/ohioFinance/ohioSosArtifactAcquisition.ts (1)

502-507: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

The version-1 branch casts row contents without validating them.

The header comment states the reader is fail-closed and that any shape surprise throws. The version-1 branch checks only that reportKey is a string and rows is an array. It then casts the elements with report.rows as OhioSos31uDetailRow[]. A damaged or hand-edited cache entry, for example amountCents stored as "1000" instead of a number, passes this check.

aggregateOhioOutsideSpending then evaluates row.amountCents <= 0 and adds the value to the report total. A string value would produce wrong reconciliation totals instead of an error, which contradicts the fail-closed policy. The legacy branch does not have this gap, because parseOhioSos31uDetailTable validates every cell.

Consider validating the numeric and direction fields per row before returning them.

♻️ Suggested per-row validation
     return payload.reports.map((report) => {
       if (typeof report.reportKey !== "string" || !Array.isArray(report.rows)) {
         throw new Error(`Ohio SoS 31-U detail bundle at ${detailPath} has a malformed report entry`);
       }
-      return { reportKey: report.reportKey, rows: report.rows as OhioSos31uDetailRow[] };
+      const rows = report.rows.map((row, index) => {
+        const candidate = row as Partial<OhioSos31uDetailRow>;
+        if (
+          typeof candidate.reportKey !== "string" ||
+          (candidate.amountCents !== null && typeof candidate.amountCents !== "number") ||
+          (candidate.year !== null && typeof candidate.year !== "number")
+        ) {
+          throw new Error(
+            `Ohio SoS 31-U detail bundle at ${detailPath} report ${report.reportKey} row ${index + 1} is malformed`
+          );
+        }
+        return candidate as OhioSos31uDetailRow;
+      });
+      return { reportKey: report.reportKey, rows };
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/pipeline/ohioFinance/ohioSosArtifactAcquisition.ts` around lines
502 - 507, Update the version-1 report handling around the report mapping to
validate every row’s numeric and direction fields before returning it, rather
than relying on the OhioSos31uDetailRow cast. Reject malformed values, including
numeric fields stored as strings, by throwing an error that preserves the
fail-closed behavior; keep valid rows unchanged and continue returning the
validated reportKey and rows.
backend/tests/pipeline/ohioFinance/ohioSosArtifactAcquisition.test.ts (1)

382-396: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the malformed-entry throws.

The tests cover the happy paths, the cycle mismatch, and the unrecognized shape. They do not cover the two malformed-entry throws in readOhioSos31uDetailBundle (source lines 504 and 513). They also do not pin the behavior for a payload that declares version: 1 but omits reports; that payload reaches the "unrecognized format" throw rather than a malformed-entry error.

✅ Suggested added cases
it("rejects a version-1 report entry without a string reportKey", async () => {
  const cacheDir = await makeCacheDir();
  await writeBundle(cacheDir, { version: 1, cycleYear: 2026, reports: [{ reportKey: 100, rows: [] }] });
  await expect(readOhioSos31uDetailBundle({ cacheDir, cycleYear: 2026 })).rejects.toThrow(
    /malformed report entry/
  );
});

it("rejects a legacy entry without a string key", async () => {
  const cacheDir = await makeCacheDir();
  await writeBundle(cacheDir, { rows: [{ key: 512315395, rows: [] }] });
  await expect(readOhioSos31uDetailBundle({ cacheDir, cycleYear: 2026 })).rejects.toThrow(
    /malformed legacy report entry/
  );
});

it("rejects a version-1 payload without a reports array", async () => {
  const cacheDir = await makeCacheDir();
  await writeBundle(cacheDir, { version: 1, cycleYear: 2026 });
  await expect(readOhioSos31uDetailBundle({ cacheDir, cycleYear: 2026 })).rejects.toThrow(
    /unrecognized format/
  );
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/pipeline/ohioFinance/ohioSosArtifactAcquisition.test.ts` around
lines 382 - 396, Add tests in the existing ohioSos31uDetailBundle suite for both
malformed-entry branches: a version-1 report with a non-string reportKey must
reject with “malformed report entry,” and a legacy entry with a non-string key
must reject with “malformed legacy report entry.” Also cover a version-1 payload
missing its reports array and assert it rejects with “unrecognized format,”
preserving the documented classification.
backend/src/pipeline/ohioFinance/ohioCandidateFinanceBatchSync.ts (2)

333-333: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The broad catch reports data-corruption errors as unavailable artifacts.

The try block spans the artifact reads and the aggregation call. aggregateOhioOutsideSpending throws on conditions that are not missing files, for example Duplicate Ohio 31-U detail report key: ... and a target without a candidateKey. The catch reports every such failure as Ohio SoS outside-spending artifacts unavailable, sets available: false, and lets the run preserve stale outside totals.

The error field still carries the real message, so the failure is observable. The log line misattributes the cause, which slows diagnosis of upstream corruption.

Consider limiting the try to the artifact loads at lines 334-355 and letting aggregation errors surface with their own handling, or reword the warning so it does not assert a cause it has not established.

Also applies to: 407-417

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/pipeline/ohioFinance/ohioCandidateFinanceBatchSync.ts` at line
333, Limit the try/catch around the artifact-loading operations in the Ohio
candidate finance sync, such as the loads near the artifact read block, so only
missing or unavailable artifacts produce the unavailable warning and
available:false result. Let aggregation errors from
aggregateOhioOutsideSpending, including duplicate keys or missing candidateKey
failures, propagate through their existing handling without being relabeled as
artifact unavailability; apply the same correction to the corresponding catch
block near the later sync path.

267-294: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Key each direct contribution row by its master key before aggregating.

aggregateDirectForYear calls accumulator.add(row) for every committee accumulator on each contribution row before add filters by row.masterKey.trim() !== committeeId. Build Map per transactionYear with input.committeeIds once, collect it, then stream both CAC_CON files and route rows with a keyed accumulator lookup. Add an empty committeeIds/accumulators guard to skip both files when no committee ids are linked for the year.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/pipeline/ohioFinance/ohioCandidateFinanceBatchSync.ts` around
lines 267 - 294, The contribution sync in aggregateDirectForYear currently scans
every row through every accumulator; build a per-transactionYear Map keyed by
trimmed master key from input.committeeIds, return early when no committee
IDs/accumulators exist, and route each streamed CAC_CON row only to the
accumulator matching its trimmed row.masterKey instead of iterating
accumulators.values().
backend/src/scheduler/ohioCandidateFinanceSyncScheduler.ts (1)

135-149: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider not persisting force into the recurring scheduler payload.

force is stored in the repeatable job data. Every later daily run then calls isOhioCampaignFinanceSyncEnabled(true) and bypasses OHIO_CAMPAIGN_FINANCE_SYNC_ENABLED (see backend/src/config/featureFlags.ts lines 242-247). An operator who runs ohio-candidates:finance:scheduler:upsert -- --force once therefore disables the sync kill switch for the daily schedule until the next upsert. The state is not visible from the flag env vars.

If force is intended only as a one-shot operator override, restrict it to the manual enqueue path and drop it from the recurring payload.

♻️ Proposed change to keep the kill switch effective for daily runs
         data: {
           dryRun: Boolean(jobData.dryRun),
-          force: Boolean(jobData.force),
           maxCandidates: jobData.maxCandidates,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/scheduler/ohioCandidateFinanceSyncScheduler.ts` around lines 135
- 149, Remove force from the recurring job data created by the scheduler upsert
flow around OHIO_CANDIDATE_FINANCE_SYNC_JOB_NAME, so repeatable daily runs do
not persist a one-shot override. Keep force available only in the manual enqueue
path, while preserving the other scheduler payload fields unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/pipeline/ohioFinance/ohioSosArtifactAcquisition.ts`:
- Around line 489-494: Update aggregateOutsideForYear and the cached
detail-bundle parsing flow to avoid readFile/JSON.parse loading the entire 31-U
artifact into memory; use streaming or chunked parsing while preserving the
existing report and row data behavior. Ensure row parsing caps legacy raw-cell
replication, and avoid retaining parsed report collections alongside stacked
rows and cover rows.

In `@backend/src/scripts/triggerOhioCandidateFinanceSync.ts`:
- Around line 9-30: Extract parseFlagValue and parsePositiveIntegerFlag into a
shared module imported by syncDueOhioCandidateFinance.ts,
triggerOhioCandidateFinanceSync.ts, and
upsertOhioCandidateFinanceSyncScheduler.ts. Replace the local parsers in
triggerOhioCandidateFinanceSync.ts (lines 9-30) and
upsertOhioCandidateFinanceSyncScheduler.ts (lines 10-31), passing each script’s
known boolean and value flag names; retain validateKnownFlags and duplicate-flag
rejection in the shared implementation.

---

Nitpick comments:
In `@backend/src/pipeline/ohioFinance/ohioCandidateFinanceBatchSync.ts`:
- Line 333: Limit the try/catch around the artifact-loading operations in the
Ohio candidate finance sync, such as the loads near the artifact read block, so
only missing or unavailable artifacts produce the unavailable warning and
available:false result. Let aggregation errors from
aggregateOhioOutsideSpending, including duplicate keys or missing candidateKey
failures, propagate through their existing handling without being relabeled as
artifact unavailability; apply the same correction to the corresponding catch
block near the later sync path.
- Around line 267-294: The contribution sync in aggregateDirectForYear currently
scans every row through every accumulator; build a per-transactionYear Map keyed
by trimmed master key from input.committeeIds, return early when no committee
IDs/accumulators exist, and route each streamed CAC_CON row only to the
accumulator matching its trimmed row.masterKey instead of iterating
accumulators.values().

In `@backend/src/pipeline/ohioFinance/ohioSosArtifactAcquisition.ts`:
- Around line 502-507: Update the version-1 report handling around the report
mapping to validate every row’s numeric and direction fields before returning
it, rather than relying on the OhioSos31uDetailRow cast. Reject malformed
values, including numeric fields stored as strings, by throwing an error that
preserves the fail-closed behavior; keep valid rows unchanged and continue
returning the validated reportKey and rows.

In `@backend/src/scheduler/ohioCandidateFinanceSyncScheduler.ts`:
- Around line 135-149: Remove force from the recurring job data created by the
scheduler upsert flow around OHIO_CANDIDATE_FINANCE_SYNC_JOB_NAME, so repeatable
daily runs do not persist a one-shot override. Keep force available only in the
manual enqueue path, while preserving the other scheduler payload fields
unchanged.

In `@backend/tests/pipeline/ohioFinance/ohioSosArtifactAcquisition.test.ts`:
- Around line 382-396: Add tests in the existing ohioSos31uDetailBundle suite
for both malformed-entry branches: a version-1 report with a non-string
reportKey must reject with “malformed report entry,” and a legacy entry with a
non-string key must reject with “malformed legacy report entry.” Also cover a
version-1 payload missing its reports array and assert it rejects with
“unrecognized format,” preserving the documented classification.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b2582fc9-d268-4814-8ae8-37578bf79c6d

📥 Commits

Reviewing files that changed from the base of the PR and between 938c0bb and 474808c.

📒 Files selected for processing (14)
  • backend/package.json
  • backend/src/pipeline/ohioFinance/index.ts
  • backend/src/pipeline/ohioFinance/ohioCandidateFinanceBatchSync.ts
  • backend/src/pipeline/ohioFinance/ohioCandidateFinanceSync.ts
  • backend/src/pipeline/ohioFinance/ohioSosArtifactAcquisition.ts
  • backend/src/scheduler/ohioCandidateFinanceSyncScheduler.ts
  • backend/src/scripts/runOhioCandidateFinanceSyncSchedulerWorker.ts
  • backend/src/scripts/syncDueOhioCandidateFinance.ts
  • backend/src/scripts/triggerOhioCandidateFinanceSync.ts
  • backend/src/scripts/upsertOhioCandidateFinanceSyncScheduler.ts
  • backend/tests/pipeline/ohioFinance/ohioCandidateFinanceBatchSync.test.ts
  • backend/tests/pipeline/ohioFinance/ohioCandidateFinanceSync.test.ts
  • backend/tests/pipeline/ohioFinance/ohioSosArtifactAcquisition.test.ts
  • ohio_plan.md

Comment thread backend/src/pipeline/ohioFinance/ohioSosArtifactAcquisition.ts
Comment thread backend/src/scripts/triggerOhioCandidateFinanceSync.ts Outdated
shu1513 and others added 2 commits August 5, 2026 17:22
…rgets, fail-closed reader, cover-IE zero

Four fixes from the Codex/CodeRabbit review of PR #545:

- A 31-U detail bundle missing report keys the annual files discovered now
  fails the year closed to outside-unavailable instead of publishing
  partial totals and zeros. The real cache's spike bundle misses 15 keys
  carrying ~$5.9M of annual 31-U money, so this was production-relevant.
- The outside target dedupe keys on candidateId + office, not display
  name: one person due for primary and general still shares a target,
  while two different people with the same name stay separate and the
  aggregator quarantines that name as ambiguous instead of paying both.
- The version-1 bundle reader validates every field the aggregator
  consumes (direction pinned to support/oppose/null, integer cents,
  matching report keys) instead of casting - a bogus direction would have
  landed in the aggregator's oppose branch.
- Cover-IE gate (leftover from the #542 review): a blank
  VALUE_IND_EXPENDITURES on an otherwise-filled cover row now counts as
  zero and feeds the third reconciliation leg; fully-blank rows stay
  ignored like missing ones (shared isBlankOhioSosCoverPageRow). A probe
  of all 28 cycle report keys found 13 exact IE matches, 15 no-cover-row,
  and 0 blank-on-filled, so this is added protection with no behavior
  change on the real files.

Also: direct contribution rows route to their accumulator by MASTER_KEY
instead of the rows x committees scan, and the outside catch warning no
longer claims "artifacts unavailable" for what may be an aggregation
failure.

Declined from review: streaming JSON for the bundle (9.8 KB file - the
305 MB figure is the whole cache), shared CLI flag parser, and dropping
force from the recurring scheduler payload (both verbatim the
maryland/all-states template; repo-wide questions).

Verified: typecheck green, 6,299 tests pass (10 new), and the real-data
dry run now reports outside unavailable on the stale spike bundle with
direct totals unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…, provenance, strict flags

All four Codex findings fixed:

- Cache integrity: every cached stream passes a manifest gate first. A
  file whose size no longer matches its manifest (torn copy, outside
  edit, truncation on a row boundary the parser cannot see) fails the
  year instead of publishing from it. A file with no manifest streams
  with a warning - there is nothing to verify against, and the extant
  spike cache carries no manifests; a strict ready-only gate would brick
  it while detecting nothing more.

- Same-name ambiguity guard: the outside target universe is now the
  year's FULL set of active oh_candidate_finance_links (new per-year
  query), not the stale-filtered, 25-row due page. A same-name double is
  visible to the aggregator even when not due in the same run, so
  attribution no longer depends on sync timing. Unlinked same-name
  candidates remain the residual blind spot; auto-link runs first.

- Link provenance: link_source rides in the due row (the established
  alaska/tennessee/virginia linkColumns pattern) and the sync writes it
  back unchanged - a manual link stays manual instead of being rewritten
  as sos_bulk_export, which would have lost provenance and exposed the
  manual pin to auto-link supersession.

- CLI safety: trigger and scheduler-upsert scripts now validate known
  flags and reject duplicates exactly like sync-due, so a typo such as
  --dryrun fails loudly instead of silently enqueueing a real write.

Verified: typecheck green, 6,303 tests pass (4 new), real-data dry run
unchanged (direct totals identical; manifest-less cache warns per file;
outside still honestly unavailable on the stale spike bundle).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/scripts/triggerOhioCandidateFinanceSync.ts`:
- Around line 21-30: Update validateKnownFlags to explicitly reject or correctly
parse inline assignments for the boolean flags in KNOWN_BOOLEAN_FLAGS,
especially --dry-run and --force; ensure downstream checks such as
args.includes("--dry-run") cannot treat an assigned boolean form as a real sync.
Preserve existing validation for value flags and standalone boolean flags.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 56093dc4-0d9f-427a-9f6e-8f837d94b2df

📥 Commits

Reviewing files that changed from the base of the PR and between 474808c and fe07f67.

📒 Files selected for processing (13)
  • backend/src/pipeline/ohioFinance/ohioCandidateFinanceBatchSync.ts
  • backend/src/pipeline/ohioFinance/ohioCandidateFinanceSync.ts
  • backend/src/pipeline/ohioFinance/ohioDirectContributionAggregator.ts
  • backend/src/pipeline/ohioFinance/ohioOutsideSpendingAggregator.ts
  • backend/src/pipeline/ohioFinance/ohioSosArtifactAcquisition.ts
  • backend/src/pipeline/ohioFinance/ohioSosBulkFiles.ts
  • backend/src/scripts/triggerOhioCandidateFinanceSync.ts
  • backend/src/scripts/upsertOhioCandidateFinanceSyncScheduler.ts
  • backend/tests/pipeline/ohioFinance/ohioCandidateFinanceBatchSync.test.ts
  • backend/tests/pipeline/ohioFinance/ohioCandidateFinanceSync.test.ts
  • backend/tests/pipeline/ohioFinance/ohioOutsideSpendingAggregator.test.ts
  • backend/tests/pipeline/ohioFinance/ohioSosArtifactAcquisition.test.ts
  • ohio_plan.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • backend/tests/pipeline/ohioFinance/ohioCandidateFinanceSync.test.ts
  • backend/src/pipeline/ohioFinance/ohioCandidateFinanceSync.ts
  • backend/src/pipeline/ohioFinance/ohioSosArtifactAcquisition.ts
  • ohio_plan.md
  • backend/src/scripts/upsertOhioCandidateFinanceSyncScheduler.ts
  • backend/src/pipeline/ohioFinance/ohioCandidateFinanceBatchSync.ts

Comment thread backend/src/scripts/triggerOhioCandidateFinanceSync.ts
CodeRabbit round 3, one valid finding (the other two comments were stale
duplicates of already-addressed rounds): --dry-run=true passed
validateKnownFlags (the name before "=" matches the boolean set) but the
later args.includes("--dry-run") check returned false, so the command
would run or enqueue a REAL sync instead of a dry run. Boolean flags now
reject any inline "=value" form in all three scripts (sync-due, trigger,
scheduler-upsert), and a shared parameterized test file pins the strict
contract: unknown flag, boolean-with-value, repeated flag, and
non-positive integer all throw.

Verified: typecheck green, 6,318 tests pass (15 new).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant