Repository navigation
ACLED pagination walks cursors, not pages — offset dies 27 Oct (#487) - #490
Merged
Merged
Conversation
… it (#487) ACLED removes offset pagination on 27 October 2026. The migration is one loop, but it deletes that loop's termination condition and replaces it with another, and a termination bug in a harvester returns fewer events rather than failing. Nothing reads n_events back to notice (#488), so it would become the cached truth on the following run. ADR-027 exists because that already happened once. So the parity claim needs a measurement, and the measurement can only be taken before the 27th while both methods still work. This commit lands first so that "parity held" is a result and not a rationalisation: - six hypotheses, including two with no prior (H4 short pages mid-walk, H5 whether the cursor envelope populates count/total_count, which would close C-153 outright rather than amend it) - a decision table committing to both outcomes, including the branch nobody plans for: if the two walks disagree, stop and ship neither - the detection drill as a precondition, not a flag. The existing ACLED tests mock an ordered list of responses without inspecting the request, so they cannot tell a correct walk from one handed the right answer by accident. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9
…487) Confirmed against live ACLED first: next_cursor IS a top-level body key, and it is an INT (observed 1148657), not a string. So 0 is both the starting cursor and a falsy value, and termination must test `is None` — `if not next_cursor` would end a walk on a legitimate cursor. Two terminators deleted, and the deletion is the whole point: if not results: break if len(results) < config.page_size: break Under offset pagination a short page genuinely did mean the end of the series. Under cursor pagination it means nothing at all, and only next_cursor is authoritative. Keeping either break is how this migration would have silently truncated a harvest — which nothing downstream would notice, because n_events is written to the ledger and never read (#488). Two new raises, both fail-loud per ADR-011, because ACLED reports count/total_count as null (C-153) so next_cursor is the only completeness signal the response carries: - envelope with no next_cursor key — unrecoverable, names the field and lists the keys it did get, same shape as the missing 'access_token' guard - no events while the cursor is still live — refuses both to continue (unbounded walk against a rate-limited API) and to stop (the silent truncation). H6 of the pre-analysis plan predicts this never fires on a real walk; the live run tests that. AcledConfig is untouched — no field added or removed. page_size is already the `limit`, and _fetch_single_year rebuilds the per-year config by re-listing all eleven fields, so a new one would have been silently dropped there. max_pages keeps its name: ACLED's own notice still says "page" under cursor pagination, and renaming would churn one script and ten call sites for nothing. Tests: 17 failed on the bare implementation. The fix for 8 of them was one defaulted argument — _make_acled_response(events, next_cursor=None) — so a single-response test now describes a complete walk. The rest needed real work, and two needed care: test_mid_pagination_token_refresh and test_401_mid_pagination_refreshes_token are the ONLY coverage of the token-refresh and 401-recovery branches inside pagination, and both would have gone green as single-page walks while testing nothing. They carry live cursors instead. Six new tests, each aimed at a mutant: - full final page + null cursor terminates (the old code could not have passed this) - short page + live cursor continues - next_cursor == 0 is live, not terminal - cursor threads through three pages, asserting REQUEST COUNT, so `cursor = 0` in place of `cursor = next_cursor` cannot pass - empty page + live cursor raises; missing next_cursor key raises - no events + null cursor returns [] without raising Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9
…sting (#487) Measured: 7609 events, 8 requests each way, identical on all four oracles, content digest ae4a6e815168e95f both times. After 2026-10-27 this comparison cannot be made by anyone, so the recording is the deliverable, not the number. scripts/verify_acled_pagination_parity.py holds both walks, with the offset loop as a frozen local copy rather than an import — the same construction and the same reason as verify_area_majority_projection.py: importing production would mean a later change there silently moves the baseline the measurement was taken against. The detection drill is a precondition in main(), not a flag. Four drills: identical input reports parity; a dropped event trips all four oracles; a changed FIELD trips records_equal even where counts and id sets agree; and the two walk implementations actually diverge on input designed to separate them. Verified by sabotage — forcing "parity": True, and separately deleting the short-page break from the frozen offset walk — both caught, exit 1. Without it, "the digests match" is indistinguishable from hashing the same list twice, and the existing 22 ACLED mocks cannot substitute: they serve an ordered list without inspecting the request, so they cannot tell a correct walk from one handed the right answer by accident. Fixtures are a SEPARATE narrow slice (Somalia, 84 events, 113 KB) and --write-fixtures is off by default. The headline walk recorded 9.9 MB per side against a repo whose largest fixture is 9.4 KB. Trimming events would have broken the digest oracle and shrinking the headline slice would have weakened the evidence, so the two artifacts get two slices. Logged as a deviation in the pre-analysis plan rather than quietly re-scoped. test_acled_cursor_parity_fixture.py replays the real envelopes through the real fetch_paginated — 31 event fields, 9 envelope keys, the actual opaque cursors in the actual order. It carries its own truncation drill, because every other assertion in it would also pass against a loop that stopped early. Hypotheses resolved. H1 parity holds. H2 next_cursor is top-level and an INT (so 0 is falsy and `is None` is mandatory). H3 null exactly once, last. H5 count/total_count STILL null on every page — so C-153 is amended, not closed, and ADR-027's tolerance rule still cannot apply to ACLED. H6 no empty page with a live cursor. H4 came back NEGATIVE: no page arrived short while the cursor was live, so the deleted break would have agreed on this slice. Recorded as a negative result rather than omitted. The deletion is structurally right but not vindicated by this measurement — the honest basis for it is mutant M1 being caught, not H4. All 8 termination mutants caught (6 planned, plus M7 dropping the empty-page raise and M8 terminating on falsiness, which the int cursor made a live hazard the planned six did not probe). Register: C-153 amended — it named the short-page heuristic as ACLED's only detection signal, which this change deletes. C-359 added for the gap the planning surfaced: n_events is written to the ledger and never read, so a non-zero shortfall is recorded as success (#488). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9
…e parity (#487) Four findings from /code-review medium on #490. Three were right about real defects; the fourth was right about where an entry was filed. **The walk had no termination bound left (medium).** Both old terminators are gone and `page` is now only a counter, so the exits are `next_cursor is None`, the two raises, and `max_pages` — which production never passes. `_fetch_single_year` calls fetch_paginated with no cap; only the --proof probe sets one. If ACLED ever echoed a cursor back, or reset it after a restatement, the loop re-requested the same full page forever: unbounded duplicates, one request per page_delay, against a rate-limited API, in an unattended monthly cron. Under offset pagination `page += 1` guaranteed forward progress; under cursor pagination only the server can. The review also caught the inconsistency that makes this embarrassing rather than merely missed: the parity script gave its own walk MAX_REQUESTS = 40 with a comment explaining exactly this risk. Production had nothing. Guard is a seen-set rather than `next_cursor != cursor`, because it also catches a longer cycle (A→B→A) and 80 integers costs nothing. Seeded with {0} so a response echoing the starting cursor is caught too — and that case is why test_zero_cursor_is_not_treated_as_terminal is gone. It asserted that a walk continues after next_cursor=0, which this change reclassifies as a stalled walk. Replaced by test_echoed_zero_cursor_raises_not_terminates, which is a better test: one scenario now separates three behaviours — correct code raises, `if not next_cursor: break` returns silently, and no guard loops forever. M8 is still caught. **The empty-page raise can abort a harvest on a benign boundary (low).** A slice holding an exact multiple of `limit` yields a final full page followed by an empty one. If ACLED echoes a live cursor there, the year fails. Odds are ~1-in-page_size per year-slice, so non-trivial across a 28-year re-fetch, and never observed — H6 was confirmed on one 8-page and one 2-page walk. Kept the raise rather than taking the suggested retry-once: fail-loud is the governing principle and the consequence is a failed harvest, not a wrong one. But the message now names the exact-multiple boundary as the likely cause and says what to check, so whoever hits it does not start by suspecting ACLED. **The parity script could report PARITY on two truncated walks (low).** Both walks stopped silently at MAX_REQUESTS and _compare then compared two prefixes, which agree on all four oracles. On a slice larger than 40×limit the script logged "PARITY: both walks returned identical data" and exited 0 — the one verdict the pre-analysis plan reads as licence to ship. That is the error class this entire investigation is about, committed by its own instrument. Walks now return hit_cap, the report records it, a capped run forces a non-parity verdict and exits 1 as INCONCLUSIVE, and drill 5 asserts the flag fires — verified by sabotage, like the other four. **C-359 was filed under "Deferred by Design" (low).** Moved into the Tier 3 section where the header counts it and where every other Tier 3 entry lives. My error: I picked an insertion anchor without checking which section it was in. Mutants: 8 → 11, all caught. M9 drops the progress guard, M10 never records a cursor as seen, M11 seeds the set empty instead of with 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9
…amp both measurements (#487) Two of this document's claims had gone stale and one of its artifacts could not be traced to the code that made it. Section 3.2 said a restatement-reset cursor was an open question. It still is — but the worst version of it now fails loud instead of looping, so the section says what was closed and what was not. Section 3.1 gains the one trigger for the empty-page raise that is both benign and foreseeable: a slice holding an exact multiple of limit. Roughly half a percent odds across a 28-year re-fetch, presenting as a failed year rather than a wrong one. New section 5 records all five review findings, including the two worth keeping visible rather than quietly fixing: The parity script could have reported PARITY on two truncated walks — truncation mistaken for completion, in the instrument built to detect truncation mistaken for completion. It never affected the recorded results (both walks ended naturally in 8 and 2 requests) but it could have on a larger slice, and parity.json now states walks_ran_to_completion rather than leaving it inferable. And parity.json's git_sha named 13ddda1, which does not contain the script — it landed in eea3068. For a file whose whole purpose is evidence that cannot be re-collected, that stamp was close to useless. Both measurements re-run at 47089fd. The re-run reproduced BYTE-IDENTICALLY: 7609 events, 8 requests each way, digest ae4a6e815168e95f, and the fixture recording's md5 is unchanged — which also confirms ACLED has not restated this slice. That corroboration was not planned; it fell out of fixing the stamp. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9
… itself (#487) Iteration 2 of the ritual. Two warnings, three suggestions, no criticals. All taken. **A non-scalar next_cursor crashed with a bare TypeError.** The progress guard tracks cursors in a set, so a list or dict cursor hit `TypeError: unhashable type: 'list'` out of the membership test — verified empirically. Every other envelope anomaly in this loop names the field and says what to check; this one gave an unattended cron a stack trace with nothing pointing at ACLED. A type change is the same class of provider drift as the missing key already guarded two lines up, so it now gets the same named ValueError. New mutant M12. **max_pages now beats a stalled cursor.** The guard ran first, so a capped walk raised on a cursor anomaly it was about to stop for anyway — verified: max_pages=1 with an echoed cursor raised instead of returning its page. Only `harvest_acled.py --proof` passes a cap, so this was `--proof` reporting FAIL for a credential that plainly works. The cron passes no cap, so the ordering change cannot weaken it. **The parity verdict had two owners.** `_compare` returned `parity: all(checks.values())` and then main() overrode it to False and added a key to `checks` after that aggregate was taken. No live bug, but a check added inside `_compare` later would have sat outside main()'s override logic. Completion is now an oracle passed INTO `_compare` rather than a correction applied after it. **And that refactor opened a hole the drill did not see.** Drill 5 proves the WALKS set hit_cap; nothing proved `_compare` acts on it. Hard-coding `walks_ran_to_completion: True` inside `_compare` passed the whole drill cleanly — found by sabotage, which is the only reason it was found at all. Drill 6 closes it: identical data with walks_completed=False must not report parity. Re-sabotaged, now caught. Also: F1's stale "Three drills" docstring, and a note that `_fake_fetch` ignores params so no drill can catch a malformed request — stated so "drill passed" is not later read as broader than it is. **Unrelated guard tripped, and the fix is precedented.** test_falsification_ghsbuilts_coverage_parity F1 requires GHS-BUILT-S to hold >=70% of its siblings' test-function count. The 12 tests added here moved the denominator 245 -> 257 and the ratio 73% -> 69%. GHS-BUILT-S did not regress — it is 178 functions before and after. F2 was lowered 70% -> 65% on 2026-06-29 for exactly this reason; F1 now follows, with the trigger recorded in its docstring. Adding GHS-BUILT-S tests to restore a ratio would be gaming the metric. Both F1 and F2 also had failure messages claiming 70% while enforcing a different number. Mutants: 11 -> 12, all caught. Drills: 5 -> 6, each verified by sabotage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9
This was referenced Oct 12, 2026
Polichinel
added a commit
that referenced
this pull request
Oct 12, 2026
…dies 27 Oct (#487) (#491) The first release whose point is a deadline rather than a feature. ACLED deprecates offset pagination on 27 October 2026; the harvester walked their API by `&page=N`. In the wheel (#490): - fetch_paginated() in acled.py walks `&cursor=X` and terminates only on a server-declared `next_cursor is None` - the empty-page and short-page breaks are deleted — under cursor pagination a short page is not the end of the series, and treating it as one is how this migration would have truncated a harvest silently - the walk is bounded by a non-advancing-cursor guard, so a stalled cursor raises instead of looping against a rate-limited API in an unattended cron - AcledConfig is unchanged: no field added or removed Why now and not later: the failure shape. A missed migration does not crash on the 27th — it surfaces on the 21 November cron as a short harvest, quiet, a month late, and in the dataset rather than the logs. Deploying before the 21 October run means the new code runs in production while offset still works at ACLED, so `git checkout v1.13.0` remains a live revert target. Parity was measured while both methods still worked: 7,609 events, 8 requests each way, identical on count, event_id_cnty sets, full records and content_digest (ae4a6e815168e95f), reproduced byte-identically at a second commit. Real envelopes from both walks are committed as fixtures — after 27 October the offset side can never be recorded again. Not fixed here: a short harvest is still recorded as `outcome: "success"` because nothing reads `n_events` back (C-359, #488). C-153 is amended, not closed — ACLED still returns count/total_count as null. Claude-Session: https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #487 (by hand —
Closesdoes not fire on PRs intodevelopment).ACLED removed offset (
&page=N) pagination on 27 October 2026. This migratesfetch_paginated()to cursors and records the parity evidence while that evidence is stillpossible to collect.
Why the failure shape drove the approach
A missed migration does not crash on the 27th. It surfaces on 21 November as a short harvest —
quiet, a month late, and in the dataset rather than the logs. ADR-027 exists because that already
happened to UCDP:
page_size=50000returned 335,918 of 384,918 events and the harvest reportedsuccess.
So this is not treated as a twelve-line diff. It is treated as a change to a termination condition
in a monthly cron that nobody watches.
Six commits, in a deliberate order
18029fe— pre-registration, before any implementation code. Six hypotheses with falsificationcriteria, and a §4 decision table committing to both outcomes including the branch nobody plans
for: if the two walks disagree, stop and ship neither. Committing this first is what makes "parity
held" a result rather than a rationalisation.
13ddda1— the implementation. Two terminators deleted, and the deletion is the point:Under offset pagination a short page genuinely meant the end of the series. Under cursor pagination
it means nothing, and only
next_cursoris authoritative. Two conditions now raise rather thanguess (ADR-011): a response missing
next_cursor, and a response with no events while the cursoris still live.
eea3068— the parity evidence, the docs, the register.47089fd,acba3fb,8a0c6fd— two review rounds./code-review mediumfound four realdefects;
/review-difffound three more, one of them in the detection drill itself. See"What the reviews changed" below.
What the live probe changed about the design
next_cursoris anint(observed1156286), not a string. So0is both the starting cursorand falsy, and termination must test
is None—if not next_cursorwould end a walk on alegitimate cursor. That became mutant M8, which the planned six mutants would not have probed.
The measurement
7,609 events, 8 requests each way, identical on all four oracles, content digest
ae4a6e815168e95fboth times. Oracles: event count,event_id_cntyset, full-record equality overthe sorted lists, and the
content_digestthe ingestion ledger records.The digest is the load-bearing one because
validate_events()sorts before hashing — so adifferent page ordering could not produce a false mismatch — and because it is the exact value
_year_is_cached()compares against. Full-record equality runs alongside it because the digestcovers only
event_id_cntyandfatalities.The detection drill is a precondition, not a flag
main()aborts if it fails. Six drills: identical input reports parity; a dropped event tripsall four oracles; a changed field trips
records_equaleven where counts and id sets agree;and the two walk implementations actually diverge on input designed to separate them.
Verified by sabotage — forcing
"parity": True, and separately deleting the short-page break fromthe frozen offset walk. Both caught, exit 1.
This matters because the existing 22 ACLED mocks cannot substitute: they serve an ordered
side_effectlist without inspecting the request, so they cannot tell a correct walk from one thatasks for the wrong page and is handed the right answer anyway.
H4 came back negative, and it is recorded that way
No page arrived short while the cursor was live. Every non-terminal page was exactly full; the final
page was short and carried a null cursor — so the deleted break would have agreed on this
slice.
That is in
findings.mdas a negative result rather than omitted. The deletion is structurallyright but not vindicated by this measurement. The honest basis for believing it is mutant M1
being caught, not H4 being positive.
H5 held, which decided whether C-153 closes
countandtotal_countare still null on every page under cursor pagination. So ACLED stillhas no server-declared total, ADR-027's
len(fetched) >= TotalCount * (1 - tolerance)rule stillcannot be applied to it, and C-153 is amended rather than closed. Had either field carried a
value it would have closed outright — predicting them null and checking anyway is why that is now
known rather than assumed.
Tests
17 failed on the bare implementation. Eight were fixed by one defaulted argument —
_make_acled_response(events, next_cursor=None), so a single-response test now describes acomplete walk.
Two needed care rather than patching:
test_mid_pagination_token_refreshandtest_401_mid_pagination_refreshes_tokenare the only coverage of the token-refresh and401-recovery branches inside pagination, and both would have gone green as single-page walks while
testing nothing. They carry live cursors instead.
New: full-final-page termination (the old code could not have passed it), short-page-with-live-cursor
continuation,
next_cursor == 0is live, cursor threading asserted on request count (anevent-count assertion cannot catch
cursor = 0), both raise paths, and empty-with-null returning[].tests/test_acled_cursor_parity_fixture.pyreplays the recorded envelopes through the realfetch_paginated— 31 event fields, 9 envelope keys, the actual opaque cursors in the actual order— and carries its own truncation drill, because every other assertion in it would also pass against
a loop that stopped early.
All 12 termination mutants caught, baseline green,
git diffclean afterwards. Two of themexist only because a finding suggested them rather than the plan: M8 (terminate on falsiness
rather than
is None, which the int cursor made a live hazard) and M12 (drop the non-scalarcursor guard).
Fixtures: a separate, narrow slice
--write-fixturesis off by default. The headline walk recorded 9.9 MB per side against a repowhose largest existing fixture is 9.4 KB. Trimming events would have broken the digest oracle;
shrinking the headline slice would have weakened the evidence. So the two artifacts get two slices,
and the fixture run is Somalia / 84 events / 113 KB — still a genuine two-page walk with a full
non-terminal page and a short terminal one.
Logged as a deviation in the pre-analysis plan rather than quietly re-scoped.
These fixtures cannot be regenerated after 27 October by anyone.
Register
this change deletes. Reading the register before touching the code is what turned two silent
falsehoods into two amendments. ADR-027's Implementation Notes got the same treatment.
n_eventsis written to the ingestion ledger and never read back, so a non-zeroshortfall is recorded as
outcome: "success". Deliberately not fixed here — it touches sharedprovenance code and would confound two changes in one release. Tracked as A short ACLED harvest is recorded as success — n_events is written to the ledger and never read #488.
Scope, stated rather than implied
This change owns one sentence: the pagination loop cannot silently truncate. The
ledger-level count check is #488.
Verification
ruff check .mypy src/(strict)docs/validate_docs.shtest_docs_citations.pytest_falsification_shining_codex.pyasserts a filename views-models retired in their3f2d98ec. Skipped in CI (sibling repo absent). Tracked as #489What the reviews changed
Two rounds, seven findings, all fixed. Four are worth naming here because they change claims made
above or in
findings.md.The walk had no termination bound (
/code-review, medium). Both old terminators were gone,pagewas only a counter, and production passes nomax_pages— only the--proofprobe does. Acursor that stopped advancing, whether echoed by a bug or reset by a restatement, would have
re-requested the same full page forever: unbounded duplicates, one request per
page_delay, againsta rate-limited API, in an unattended monthly cron. Under offset pagination
page += 1guaranteedforward progress; under cursor pagination only the server can.
The review also caught the inconsistency that makes this embarrassing rather than merely missed:
the parity script had given its own walk a 40-request cap, with a comment explaining this exact
risk. The guard existed in the instrument and not in the thing being measured. Now a seen-cursor
set, which also catches a longer cycle (A→B→A).
The parity script could report PARITY on two truncated walks (
/code-review, low). Both stoppedsilently at the cap and the comparison then ran on two prefixes, which agree on all four data
oracles. On a slice larger than 40 ×
limitit would have logged "PARITY: both walks returnedidentical data" and exited 0 — the single verdict §4 of the pre-analysis plan treats as licence to
ship. That is the error class this entire investigation exists to prevent, committed by its own
instrument. It never touched the recorded measurements (both ran to natural completion in 8 and 2
requests), but it could have.
A non-scalar
next_cursorcrashed with a bareTypeError(/review-diff, warning). Theprogress guard tracks cursors in a set, so a list or dict cursor hit
TypeError: unhashable type: 'list'out of the membership test — verified empirically. Every otherenvelope anomaly in this loop names the field and says what to check; this one handed an unattended
cron a stack trace with nothing pointing at ACLED.
And the drill had a hole the refactor had just opened (found by sabotage, not by review).
Moving the completion check into
_compare— itself the fix for a two-owners-of-the-verdictfinding — left nothing verifying that
_comparehonours the flag. Hard-codingwalks_ran_to_completion: Truepassed the entire six-part drill cleanly. Drill 6 closes it.That is the second time in this branch that sabotaging a check caught something that would
otherwise have shipped.
One unrelated guard tripped, and the fix is precedented
test_falsification_ghsbuilts_coverage_parityF1 requires GHS-BUILT-S to hold ≥70% of its siblings'test-function count. The 12 tests added here moved the denominator 245 → 257 and the ratio 73% →
69%. GHS-BUILT-S did not regress — it is 178 functions before and after.
F2 was lowered 70% → 65% on 2026-06-29 for exactly this mechanism ("any new assertion in the
'other' files tripped the test"). F1 now follows, with the trigger recorded in its docstring.
Adding GHS-BUILT-S tests to restore a ratio would be gaming the metric rather than improving
anything. Both F1 and F2 also carried failure messages claiming 70% while enforcing a different
number.
Worth stating without acting on it: that guard penalises adding tests anywhere in its sibling set.
It is a relative measure doing a job that wants an absolute floor. Not this branch's business to
redesign.
Not in this PR
Release and deploy. The server runs a tag, so cursor code reaches production only via
1.13.0 → 1.14.0and a deploy. Recommended before the 21 October cron, so the October runexercises it while offset still works at ACLED and
v1.13.0is a live revert target. Separatelygated.
🤖 Generated with Claude Code
https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9