Skip to content

Replace the two cell-polity crosswalks with the polycell, and move the area denominators onto it - #619

Draft
eduaguilera wants to merge 46 commits into
mainfrom
edu/polycell-spatial-support
Draft

Replace the two cell-polity crosswalks with the polycell, and move the area denominators onto it#619
eduaguilera wants to merge 46 commits into
mainfrom
edu/polycell-spatial-support

Conversation

@eduaguilera

@eduaguilera eduaguilera commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Replaces the two incompatible cell↔polity crosswalks with one unit, and moves every area denominator on the extent side of #458 onto it.

Draft — the identity half is not here, and two known gaps are called out at the bottom. Please read those before the diff.

The unit

The polycell is the intersection of a 0.5° cell with a polity over that polity's validity interval, polycell_id = f(cell_id, polity_code). It carries territory decomposed into three separately addressable categories:

polity_area_ha = land_area_ha + inland_water_ha + ice_area_ha

Per-hectare biogeochemical rates apply to land_area_ha; polity_area_ha is what reconciles with official national areas. Inland water and ice are territory that receives real fluxes, not rounding error — deposition to freshwater is a live eutrophication term.

What was wrong

  • A per-hectare rate multiplied by whole-cell area over-counts by 11.0% — global terrestrial area is 12.9931 Gha against 14.3195 Gha of whole cells. polity_frac is a valid partition of the land; it was simply multiplied by the wrong base.
  • Two crosswalks were live in one package. The carbon path read a centroid grid where cell_area_frac defaults to 1, so a whole border cell went to one polity; the nitrogen path split fractionally. No test detected it, because each path was internally self-consistent.
  • Border splits were quantised to 1/36 of a cell (subcells = 6L), not the 1/144 often assumed.
  • Two live end_year fenceposts read the boundary inclusively, so on a succession year both epochs activated and the first-sorting row took every cell — and the dissolved predecessor sorts first in 315 of 315 cases, so the successor got no row and cells outside its smaller extent were dropped. That lost mass, not just labels.

Measured movement, per consumer

Each consumer moved on its own commit, against a characterisation baseline pinned first, so the movement is attributable rather than aggregate.

consumer movement
deposition, split key mass conserved exactly — new − old = 0 t; rate identical() over 63,065 cells
deposition, categories scope selectable, defaults to whole territory → ledger bit-identical
synthetic per-polity totals moved by exactly 0; weighted share movement 1.99%
carbon +1.32% on masses (stock, C input, SON)
spatialize zero — enforcement only
whole ledger −0.504% on N input, of which 95.6% is the identity gap, not geometry

Carbon is the one to look at. Per-hectare densities are invariant under a pure re-split, so a density check reports ~0 whether or not the migration is correct — and here the global mean stock density falls while the mass rises, so a density check would have reported the wrong sign. All carbon movement is therefore measured on sum(density × area_ha).

Two decisions worth disagreeing with, if you do

The ledger keeps the whole territory. Restricting deposition to the land share would remove 0.89 Tg N (−1.44%), concentrated on lake and glacier states. We measured it, then didn't take it: N deposited on a lake or glacier still drives indirect N₂O and still reaches the eutrophication pathway, so the terrestrial-only scope is wrong for those terms. Scope is an explicit argument defaulting to "territory", recorded in an output column.

The table keys on polity_code, not the bucket. polity_area_code folds 544 codes into 201, with 206 holding Sudan and South Sudan — the fold #591 caught corrupting published series. Consumers convert at their own boundary, so the fold happens visibly at a join rather than irrecoverably in the grid. This is why #579's grid_area_code as an added column was the right shape.

Evidence

The producer went through five adversarial verification rounds; four found a real defect, two of them blocking, and every one was invisible to a green test suite. Silent territory deletion (Greece losing 10% while reporting healthy status); an abort that made the production call impossible at any year; five live mutants; and dead gap code whose only guard could detect duplication but never omission.

So the acceptance check here is mutation testing, not the suite. Every commit carries a baseline-gated sweep in a sandbox that cannot write the repo, with sources checksummed either side. Surviving mutants are either killed or proved equivalent by measurement — one over 228,175 comparisons, another over 102,988.

Every commit also proves "no new failures" against a git archive of its own parent, in the same environment, diffed at block level rather than by totals — two offsetting changes would cancel in a total. One earlier control was invalidated because the two sides ran in different environments; the tell was a skip-count mismatch, and it is now caught by printing environment fingerprints on both sides.

rcmdcheck run with tests 0 errors / 0 warnings / 1 note; devtools::test() FAIL 0 | PASS 8459 in the offline job's own configuration; lint_package() 0; pkgdown complete (302 topics, 302 _pkgdown.yml entries, 0 missing and 0 extra).

One disclosure about that evidence, because it is the reason this PR first went red on seven checks. Every local check on this branch ran rcmdcheck(args = c("--no-tests", …)), which skips the suite entirely — so a set of census assertions pinned against an older polity vintage stayed green locally and failed everywhere in CI. whep-polities #551 flipped GRC-1830-1913 to wiki_status = "superseded"; the live filter then drops it, build_polycell_support() returns 0 rows with no error and no warning, and 13 assertions ran against an absent frame. They are re-pinned on the live successor GRC-1881-1913 against an engine-independent reference (planar clip measured in Lambert azimuthal equal-area, 7.4e-05 relative), so the pin constrains neither the clipping nor the area engine. Two of them were passing vacuously on empty frames and now cannot: under a mutant that kills the fixture the same way #551 did, they still pass while the new guards fail.

Known, and deliberately not fixed here

  • 8 of those 9 polities still lose their deposition, and Fix polity_area_crosswalk for regions.csv's 9 present-day-invalid area codes #459 did not fix it. Re-measured after the fourth reconciliation with main, against the deployed cell_polity_fraction.parquet (191 area codes). The codes that still receive no polycell are 61 Equatorial Guinea, 153 New Caledonia, 154 North Macedonia, 209 Eswatini, 212 Syria, 276 Sudan, 277 South Sudan, 299 Palestine. Only Fiji is fixed — the Refresh the polities snapshot: geometry now loads under s2 #662 snapshot refresh leaves FJI-1800-2025 s2-readable (0 s2-invalid on the 751-row table) and the crosswalk maps area 66 onto it. Fix polity_area_crosswalk for regions.csv's 9 present-day-invalid area codes #459 is closed and Model the Rest-of-World reporting members in their own right (#459) #628 is merged, but they promoted these territories on the reporting_polity_code axis only: polity_area_crosswalk still carries polity_code = ROW-1850-2025 and polity_area_code = 999 for all six, and carries no row at all for SYR-1967-2025, PSE-1948-2025, SWZ-1894-2025 or GNQ-1968-2025. The support keys on polity_code → polity_area_code, and the producer drops polity_type == "aggregate", so the promotion never reaches it. Sudan and South Sudan stay folded on 206 — Bucket 206 sums Sudan + South Sudan but is attributed to SDN-2011-2025: the value and its polity describe different territories #414 is now closed, and the open decision is costed in Un-folding FAOSTAT 276/277 out of bucket 206 loses 4.2% of the region's tonnage and breaks the ISO3 lookup #680 and lbm364dl/whep-polities#139.
  • What carbon drops is larger than this PR claimed, and two different defects were being summed as one. At the carbon support year 2015, 32 clipped polities carry no area_code and are deleted by .carbon_drop_unkeyed(). The six named above are 28.2 Mha of territory (Syria 18.70, Equatorial Guinea 2.70, North Macedonia 2.54, New Caledonia 1.88, Eswatini 1.71, Palestine 0.62) — the old 26.6 Mha over seven, minus Fiji, on a different basis. The other 26 are 252.5 Mha, almost all of it Greenland 213.2, Western Sahara 26.8 and French Guiana 8.4. None of those 26 resolves to a reporting bucket at all, so they are territory outside the reporting vocabulary rather than a crosswalk gap, and no deposition row ever existed for them to lose. Basis warning: the GLWD water layer and the Natural Earth ice layer are unreadable in this environment (WHEP_LPJML_INPUT_DIR and WHEP_NATURALEARTH_DIR unset), so every figure in this bullet is territory, an upper bound on land. That is a tight bound for the six (Syria 18.70 territory against 17.91 land measured previously) and a poor one for Greenland, most of which is ice. Measured on a polity-subset build, proved subset-invariant by a control that rebuilds the same targets with five neighbouring polities present (max |land diff| = 0 ha). The "single largest lever left in the ledger" claim is withdrawn rather than restated: it was never measured against the ledger's other terms, and these two populations should not be added into one lever.
  • Water and urban are not migrated. Both are gated on data layers on your side — water on what the LPJmL depths are referenced to (see WHEP_LPJML_RUN_DIR and lpjml_hydrology.R both still name the 1901-2009 run, while the complete 1901-2023 run sits beside it #598: the wiring and the docstring both still name the 2009 run while the complete 2023 run sits beside it), urban on HYDE. The decisions are recorded: regenerate landfrac and rerun for water, key at HYDE's native 5 arcmin for urban.
  • feed_lpjml.R is frozen. Its cell_area_ha multiplication inverts a normalisation applied upstream, so substituting land area there would under-count global grass — the mirror image of the defect this PR fixes.
  • Six no visible binding lines are deliberate — three in water_balance.R, one in feed_intake_redistribute.R, one in the frozen feed_lpjml.R, and, as of the fourth reconciliation with main, one in main's own new .read_fraction_country_grid(). Dropping the polity_frac global was the mechanism that proved no consumer was missed: with it present a planted missed consumer is invisible, with it removed the planted site is named — which is exactly how main's new fractional-grid reader announced itself here. The note count is unchanged at 1.
  • The great-circle border displacement (cshapes straight borders render as great circles: 123,276 km2 of Canada is booked to the USA, invisible to every conservation check #529, ~123,000 km² of Canadian prairie booked to the USA) is detected and reported, not corrected — the fix belongs in the polygons.

Also in here

Fixes for two live epoch fenceposts (1,912 polity-years move, 1,103 off a dead label); a status-aware tie-break so a retired interval can no longer outrank a live one (Montenegro 1913–14 was resolving to a 37.6% smaller polygon); the open-interval rule so 2025 resolves at all; and inst/scripts/reconcile_polity_areas.R, which reconciles every polity against FAOSTAT land + inland waters and attributes all 45 material discrepancies to polygon quality, identity, or convention.

Small polities are the headline there: Kiribati drew 34.3× its official area under the old crosswalk and now draws 1.18×.

@lbm364dl — review please. The data$cell_polity contract is measured to the byte if you want to sequence the build_cell_polity() re-key against it.

eduaguilera and others added 30 commits August 4, 2026 09:28
The polycell spatial-support work rests on numbers measured interactively
(LUH2's terrestrial mask, the whole-cell over-count, the cell-polity
crosswalk's shape and the border-cell cost). None of them were reproducible.
This script regenerates all of them from source, prints each against the
recorded value, and flags any that moved.

It also settles two things the migration depends on. HaNi's per-cell mass is
referenced to the WHOLE grid cell inside a land-masked domain, not to the
land fraction: coastal cells divided by whole-cell area sit at a median 0.9937
of their interior neighbours, where a land-referenced mass would sit near 0.5.
And the zero-terrestrial crosswalk cells are not marine at all -- every one of
them carries Natural Earth country land; they are LUH2 coastline/small-island
resolution misses and cells LUH2 classes wholly as ice or water.

Every input is resolved from an environment variable and the script aborts
naming it, so no machine-specific path is committed. Lives in inst/scripts
alongside the other diagnose_* scripts, which is .Rbuildignore'd.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The polycell producer does not exist yet. These 18 tests are its contract,
written first so the implementation is measured against a stated spec rather
than against itself. Every fixture is a synthetic sf geometry, which is what
the injectable geometries/water/ice arguments exist for -- no pins, no
network, no rasters.

They pin the eight cases the plan names plus four the preflight added: a
coastal polycell at 5% land (the whole-cell base is never the answer); the
four zero-land island states with EA6's polycell counts, recovering polygon
area even when LUH2 is injected as zero land, since a producer that masked by
LUH2 would silently delete four countries; two-, three- and four-polity
cells; a geodesic border split that whole-subcell counting gets wrong in both
of its ways; a border change across an exclusive end_year; the pig fixture,
where the two polities' national totals are deliberately incompatible and
nothing may cross the border; the three area categories as separately
addressable non-land territory; unclaimed land emitted rather than
renormalised away; polity_code never parsed for dates, with a code whose
embedded years disagree with its columns and a hyphenated-prefix code;
NA-explicit wiki_status and polity_type filtering; polygon_status passed
through including an unknown level and NA; and dead and aggregate rows
receiving neither data nor land.

Tolerances are measured, not guessed, and stated in the file header with the
number each was calibrated against. Fixture polygons are inset in latitude
inside their cells on purpose: under s2 a cell's east-west edge is a great
circle rather than a parallel, so a polygon whose edge sits ON a cell boundary
loses 5.2e-5 to 2.7e-4 of its area to the clip. Inset polygons reproduce their
own area to 1e-14, so the tolerances measure the producer instead of the way
a grid cell is represented.

The spec was checked to be satisfiable before being committed: a throwaway
reference implementation, kept out of the package, makes all 113 expectations
pass. Against the real package all 18 blocks fail with "'build_polycell_support'
is not an exported object from 'namespace:whep'", which is the intended state
until T-A4 lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The schema assertion is a subset check, so DA-13's transitional polity_frac
and DA-12's second footprint can ride alongside without breaking it. Both are
claims about the interim crosswalk geometry, which these polygon fixtures do
not exercise, so they belong to T-A5's before-and-after measurement rather
than here. Recorded in the header so T-A4 does not read the omission as an
oversight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add the geodesic polycell producer: intersect the 0.5-degree grid with
polity polygons over their validity interval using sf::st_area() on
unprojected WGS84 with s2 enabled (matching WHEP's spherical cell-area
convention, rejecting the WGS84 ellipsoid and equal-area projections).

- Injectable geometries (default get_polity_geometries()); year resolution
  reads start_year/end_year columns with an exclusive end_year, never the
  polity_code.
- NA-explicit wiki_status/polity_type filtering (keeps NA-status real
  territories); polygon_status passed through untouched.
- Three separately addressable area categories
  (land + inland_water + ice = polity_area_ha): DA-6 ice subtracted by exact
  intersection with s2-invalid-feature repair (EA9), GLWD water apportioned
  pro rata by polity_area_ha.
- DA-5 LUH2 reconciliation emitted via attr(result, unassigned); no
  LUH2 masking, no renormalisation.

All 18 T-A3 tests in test_polycell_support.R pass.
Complete T-A4a and T-A4b on top of the core producer.

Inputs (R/polycell_layers.R, a new capability: WHEP had no CLM reader).
- read_glwd_water() reads the LPJmL CLM binary via its .json sidecar.
  Measured 67,420 cells, 32,358 with water, 2.4759 Mkm2 -- EA10 exactly.
- read_glaciated_areas() reads ne_10m_glaciated_areas and classifies every
  feature as ok / s2-repaired / still-invalid instead of crashing on it.
- read_luh2_terrestrial(vintage) reads (1 - icwtr) * carea from
  staticData_quarterdeg.nc: 12.9931 Gha, EA1/EA2 exactly. The vintage is
  recorded in the luh2_vintage output column (DA-9).

Producer.
- DA-16 interval grain: one row per polycell per interval over which every
  area is constant, plus expand_polycell_years() for the per-year view.
  Intervals split at each cell's own breakpoints, because the inland-water
  apportionment depends on which polities share the cell. Measured 564,304
  interval rows against 13,742,974 per-year rows for 1850-2022.
- DA-19 water clamp: apportioned GLWD water is capped at the polycell's
  territory and the excess emitted, so land_area_ha never goes negative.
  94 polycells clamp in 2015, almost all of them on the Caspian.
- DA-13 shim: polycell_shim_view() is identical() to build_cell_polity(),
  68,527 rows, bit-for-bit on polity_frac and cell_area_ha.
- DA-12: both crosswalk footprints emitted and reconciled at crosswalk_year.
- DA-15 and EA9: 23 polities have no geometry and 7 carry an s2-invalid
  polygon (4 repairable, 3 not, one of them live Fiji). All carry an explicit
  coverage_status and are named in the "coverage" attribute, never a silent
  zero area.
- New "overlap" diagnostic: 441 cells in 2015 hold more territory than the
  cell, because two live polities were handed the same polygon. Emitted, not
  renormalised: deciding who owns the ground is not the producer's call.
- DA-17: polycell_support registered in whep_inputs.csv with an env-var
  override. Nothing uploaded.

Measured on the shipped polities for 2015: land 12.7806 Gha rather than the
whole-cell 14.70 Gha, inland water 2.3983 Mkm2, ice 2.2789 Mkm2, water plus
ice 3.53% of territory against AM-6's predicted ~3.7%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The S-A11 diagnostic keyed its rows on the intervals in which a cell was
claimed, so a cell held 1900-1950 and unclaimed afterwards had no row covering
2015 and its unclaimed land vanished from that year's slice. Measured on the
shipped polities the 2015 figure was halved: 158.05 Mha reported against
315.50 Mha actually unclaimed, the latter confirmed by recomputing the
quantity independently from two separate builds.

Each cell now contributes rows that partition the whole domain: the intervals
it was claimed in, the stretches between them, and the stretch after the last
one, with `claimed_land_ha = 0` on the gaps. Cells no polity ever claimed get
one row spanning the domain instead of a row with NA years. A year therefore
resolves to exactly one row per cell, asserted in the new test.

Measured after the fix, 2015: 34,903 cells, 315.50 Mha, 2.428% of LUH2's
12.9931 Gha terrestrial area.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`.pcs_valid_pieces()` discarded every intersection piece that stayed s2-invalid
after a planar repair, with no warning, no diagnostic and no change to
`coverage_status`. The pieces are not slivers: 21 of them hold 1.4191 Mha
across 5 polities, including seven whole Peloponnese and Aegean cells worth
466,032 ha, 10.08% of GRC-1830-1913, which still reported `has_geometry`. The
loss broke S-A2 re-aggregation at every pre-1950 year and re-emerged as fake
unclaimed land in the S-A11 diagnostic, mixing a producer bug with the genuine
unclaimed territory DA-7 exists to keep attributable.

The replacement, `.pcs_measure_pieces()`, keeps every piece and measures the
unreadable ones with `terra::expanse()`, as `read_glaciated_areas()` already
did. A new `area_engine` column records `"s2"` or `"terra"` per row, the
`"terra_measured"` attribute lists the affected polycells, and the substitution
raises a warning with its magnitude. terra is measured one feature at a time:
handed a whole sfc it warns "not all geometries were transferred" on a
GEOMETRYCOLLECTION and returns a shorter vector that would recycle against the
rows it is assigned to. The ice clip goes through the same helper, so ice can
no longer be under-subtracted silently either.

Also fixed, all found while re-measuring:
- `inland_water_ha` went negative on 56 fully ice-covered Greenland rows,
  violating T-A3's `expect_col_vals_gte(inland_water_ha, 0)`. Ice and water are
  independent intersections, so the water headroom is now floored where it is
  formed rather than only at `land_area_ha`, and the comment no longer claims
  the clamp bounds both terms.
- `split_method` labelled a single-polity cell carrying GLWD water as
  `polygon_intersection`, hiding that its water was apportioned rather than
  placed. DA-6 asks the column to record which rule applied, so it now does
  wherever the pro-rata step ran.
- `build_polycell_support()` took 6 arguments against CLAUDE.md's limit. The
  `example` argument is gone, replaced by `polycell_example_geometries()`, a
  tiny real fixture the documented examples build from. That also removes the
  hard-coded toy table, which carried a `year` column although the default
  grain is interval-keyed.
- `inst/scripts/verify_polycell_support.R` re-derives every reported number, so
  none of them lives only in a scratchpad. Its S-A2 section measures at a year
  and never sums a polity across intervals, which is how the drop went
  unnoticed in the first place.

Regression tests: GRC-1830-1913 from shipped package data pins the seven
recovered cells and the engine they were measured with; a fully ice-covered
polycell pins the water floor; a single-polity watered cell pins split_method.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
C0 of T-A5: the characterisation baseline, taken on unmodified consumer
code so that every value the polycell migration moves is attributable
rather than silent. Nothing here asserts that current behaviour is
right; several of the pins record behaviour that is arguably wrong.

The one that matters most is deposition. R/n_deposition.R:196 divides
the HaNi cell mass by cell_area_ha and :197-199 multiplies it straight
back, so the area cancels and today deposition_n_t is value_g_total x
polity_frac / 1e6 -- mass is already conserved and the 11% whole-cell
defect never reaches it. AM-5's fifth risk is swapping :198 to a land
area while leaving :196: that moves the total ~10% DOWN, the direction
a reviewer expects a fix to move it, while destroying the conservation
property. Its first risk is dividing by each polycell's own land area,
which emits a shared cell's mass once per polity and is invisible to
every single-polity fixture in the repo. Both are guarded, with
multi-polity fixtures throughout.

Urban and grass are pinned as pure partitions with NO area term at all,
confirming AM-1 against the A2 table: handing their crosswalk both
cell_area_ha and land_area_ha must leave the output identical.

All five guards were mutation-tested against those exact failure modes
in memory (assignInNamespace, no file touched); every mutant died.

inst/scripts/characterize_consumer_baseline.R records the numbers that
need real inputs: the deployed crosswalk fingerprint DA-12 names as the
S-A10 baseline (68,527 / 64,438 / 191, min polity_frac identical to
1/36 per AM-1, not 1/144), and conservation at full crosswalk scale.

Two measured corrections to the plan's evidence. Per-cell
sum(polity_frac) is NOT exactly 1: two of 64,438 cells fall one ulp
short (max |sum - 1| = 1.11e-16), so EA3's "min = max = 1.000000" holds
only to six decimals. And the cancellation is exact algebraically but
round-trips through IEEE-754, so today's deposition_n_t already differs
bitwise from its own identity on 14,775 of 68,527 rows at 3.22e-16 --
no downstream "bit-for-bit" claim can be made about consumer masses,
only about the crosswalk columns DA-13 covers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BLOCKING REGRESSION. Keeping the s2-invalid clip pieces made their area
recoverable, but it also left them in the polycell geometry column, and
`.pcs_ice_areas()` then ran `sf::st_intersects()` across that column. s2 aborts
on the first such piece, so with the shipped polities and the real ice layer
the production call died at ANY year -- `years` is applied only after every
polity has been clipped, and GRC-1830-1913 is always in the table. The two
populations are now separated before either engine is asked to do anything: s2
clips the pieces it can read, terra clips the rest, so ice is subtracted on
both rather than skipped on one. Greece with the real ice layer completes in
14.4 s.

The suite missed it because the only test exercising the terra path passed no
`ice`, and the two tests that passed `ice` used synthetic geometry with no
invalid pieces. The new test combines them.

Ice in terra also has to crop before it converts: handing the worldwide ice
union to `terra::vect()` took the resident set past 22 GB on a job whose s2
path peaks near 1.2 GB. Cropping to the polycell's bounding box first holds it
at 406 MB.

DA-21 supersedes the earlier densification escalation, which was a
misdiagnosis. The shortfall was never a parallel-vs-great-circle clipping bias:
`.pcs_candidate_cells()` enumerated cells from the polygon's COORDINATE box, so
the cells a bulging edge reaches into were never offered to the intersection.
The window is now that box unioned with `s2::s2_bounds_rect()`, which is exact
by construction rather than padded. Measured: SWA-1884-1912 -1.95e-04 ->
-1.43e-14 (78 pieces -> 82), KEN-1888-1891 -6.29e-05 -> -4.55e-15 (179 -> 187),
ESP-1800-2025 unchanged at -2.92e-14. S-A2 at 1900 now maxes at 6.70e-05 with
3 polities over 1e-6, all three of them carrying terra-measured pieces; at 2015
it maxes at 2.99e-11 with none.

Three unreported loss paths closed:
- A degenerate or NA-bounded validity interval deleted a polity in full with no
  warning while still reporting `has_geometry`. It now classifies as
  `invalid_interval`, warns, and appears in the coverage diagnostic. Latent on
  the 603-row table, live the moment the source refreshes to overlapping
  periods.
- Cells the water layer and the polycells do not share were silently booked as
  having no inland water, turning that water into land, or had their water
  dropped. Both directions now ride in the `"water_unmatched"` attribute, which
  is what EA10 asked for.
- Only the under-claim side of the LUH2 disagreement was emitted, so the
  over-claim was reconciled away by construction. `over_claimed_land_ha` is now
  emitted alongside it.

Also: `overlap`, `terra_measured` and `water_excess` carry `end_year` as well
as `start_year`, so they can be sliced to a year as the roxygen instructs;
`@return` documents the `"crosswalk_only"` rows and the NA areas that make an
unfiltered `sum(land_area_ha)` return NA; the terra warning pluralises; and the
piece counts in the comments and the pinned test are corrected to the measured
1,419,140.84 ha over 21 pieces and 5 polities, with the seven Greek pieces
described as the partial cells they are (shares 0.858 down to 1.7e-05) rather
than whole ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment quoted 1,697 cells and 1,488 GLWD cells from an external
measurement; the emitted diagnostic reports 1,906 polycell cells holding
0.403 Gha with no water row and 110 WET GLWD cells reaching no polycell. The
second side is deliberately restricted to wet cells -- a further 1,378
unmatched GLWD cells carry no water and so lose nothing -- and the comment now
says so rather than citing a figure the code does not produce.

Measured on the whole-domain production build: shipped geometry table, real
ne_10m_glaciated_areas ice, real GLWD water, real LUH2, both crosswalks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kills the five mutants the verification sweep found surviving: the
over-claim block that passed with the quantity forced to zero, the ice
block that admitted a whole-piece, halved or under-cropped result, and
the window block that ignored the longitude half of the bounds union.

Corrects three figures the code stated but did not produce: the
unmatched-water count, the s2-invalid glaciated-feature count, and the
clamped-polycell count.

INCOMPLETE. An expanded sweep then found four further surviving mutants
which are not yet investigated or fixed. Committed green (260 passing)
so the work is not lost; it is not finished and has not been verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sweep found four mutants the suite could not detect. Two were missing
assertions and two were genuine coverage holes:

- M07 "window: drop the s2 longitude UPPER bound" survived because the shipped
  table exercises only the lower one: F228-1800-1856 extends 10.46407 degrees
  below its coordinate box while the largest extension above any box is
  2.842e-14. The new test asserts the CONTAINMENT PROPERTY exactly -- the
  window must cover the spherical rectangle on all four sides -- so a margin of
  2.8e-14 is enough to fail it.
- M10 "water floor removed" survived because no fixture can reach it: built
  from one geometry, `polity_area - ice` comes out at exactly 0, while on real
  data ice and territory are two INDEPENDENT intersections and the difference
  lands at -1e-9. It is now unit-tested on `.pcs_add_water()` directly with
  that numeric situation, which is what 56 Greenland rows hit.
- M18 "terra area: first feature only" survived because every fixture had a
  single part. Now covered with a GEOMETRYCOLLECTION of two disjoint polygons.
- M19 "ice repair skipped on the union" survived because no test ever handed
  the producer an ice layer whose union is invalid. Measured: all 1,885 kept
  ne_10m_glaciated_areas features pass `st_is_valid()` and their union does
  not, so the repair is load-bearing. The smallest reproducing subset is two
  Greenland features carrying 22,544 vertices and simplification collapses
  them, so there is no offline fixture: the test uses the real layer and SKIPS
  without `WHEP_NATURALEARTH_DIR`. That CI gap is stated in the test.

Two mutants now survive and both are equivalent, with the measurement recorded
next to the code rather than left for the next sweep to re-derive:

- M24, skipping `.pcs_polygonal_part()` before terra, changes no area because
  terra extracts the polygonal parts itself: measured 69,978.2271 ha either way
  for a two-polygon collection and 0 either way for a line. The helper is kept
  for its explicit empty return.
- M25, dropping the span prefilter, changes no output because the 0.01-degree
  bulge floor already implies it: the largest bulge any sub-degree span can
  produce is 0.0011 degrees, and reaching the floor at latitude 45 needs 3.03
  degrees.

Both equivalences are pinned by tests, so they stop being equivalent the moment
the constants move.

Also: the reproduction script now reports the S-A2 exception list over EVERY
interval at a year inside its own validity, not at one calendar year, and fails
loudly if the list grows beyond the five known members -- GRC-1830-1913,
DEU-1800-1866, DEU-1866-1871, GBR-1800-1921, FRA-1800-1919 -- or if a member
appears without terra-measured pieces. Two of the five are live in neither 1900
nor 2015, which is why a single-year check reports three and misses them. The
regenerated Rd picks up the corrected 1,343 clamped polycells.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`.pcs_gaps_before()` was dead code. `transmute()` evaluates sequentially, so
`end_year = .data$start_year` read the `start_year` rebound one line earlier
rather than the claim's own start; every gap came out zero-length and the
`start_year < end_year` filter then deleted all of them. Re-running the broken
version over the shipped build returns 0 gap rows where the fixed one returns
7,340, and 6,487 of the 32,248 wet GLWD cells that some polycell reaches had no
row at the domain start. The domain-covering diagnostics therefore under-read:
the wet unmatched side at 2015 was 110 (its never-reached cells and nothing
else) instead of 477. The gap bounds are now built under distinct names and
renamed, which makes that class of mistake impossible here. No polycell area
moves; only the diagnostics change.

`.vps_exception_list()` summed `polity_area_ha` across every interval of a
polity before comparing it with the polygon, inflating GRC-1830-1913 1,843x, so
the gate could not have detected a real S-A2 regression. It now joins each
polity to its own probe year and measures the one interval live there. The
verdict is set equality against the expected five, and it prints a
machine-checkable counts line plus explicit alerts for growth, shrinkage and any
listed interval that carries no terra piece.

`inst/scripts/` is under no test, so an unexecuted change there is unverified by
construction. The script now takes `polity_codes` (default: the whole table, the
production call) and reads WHEP_VPS_POLITY_CODES, so it can be executed in
minutes after editing. Six mutants of the gate -- interval summing, relaxed
tolerance, and suppressed growth/shrinkage/terra reporting, the last three
paired with a positive control that makes the condition genuinely true -- all
die against a passing baseline.

Also in this commit:

- `.pcs_s2_window()` kept the spherical longitude bounds but discarded the
  latitude bounds when the longitude interval wraps the antimeridian. The
  latitude bounds stay usable there. Found by widening the domain-partition
  property test to every polity: it omitted the extreme-latitude cells of
  F228's nine intervals and of KIR-1800-2025.
- Block 46 now reads `min_bulge_deg` from `formals(.pcs_long_edges)` instead of
  repeating the literal, and block 42 asserts over every polity rather than
  three. Block 39 covers the whole diagnostic table (42 edges, 30 polities) and
  pins Egypt's 22nd parallel at 0.113289925 degrees of bulge.
- M19 (the ice union invalid while all its features are valid) had no CI
  protection because it needed the 1.4 GB layer. `tests/testthat/fixtures/
  ice_invalid_union.rds` stores the two Greenland features verbatim (22,544
  vertices, 106 KB xz) and reproduces the condition offline.

Verification: 48 + 6 blocks, 347 + 11 assertions, 0 failures, 0 skips; 29
producer mutants 27 died / 2 survived (both proved equivalent by measurement
and pinned by blocks 47 and 48); 6 script mutants all died; `air format .`,
`devtools::document()` and `lint_package()` clean (0 lints);
`inst/scripts/verify_polycell_support.R` executed end to end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verification measured the pre-fix window as differing by 2.8e-14 degrees,
which floors to the same cell index, so no candidate cell changed for any
of the 580 polities. The comment claimed it had omitted cells from F228
and Kiribati; it had not.

The bound is still kept, for the reason that is true: a refreshed
geometry source need not be as forgiving, and block 43 makes that
enforceable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings the branch up to date with `main`, which is 138 commits ahead of
this branch's base `3f479f7a` (AM-17 recorded 50; it has moved since).
Merge, not rebase: this branch has published commits and a long review
trail.

Four conflicts, all resolved by keeping both sides:

- `NAMESPACE`: both sides inserted an export in the same alphabetical
  slot (`read_polycell_support` ours, `read_population` theirs). Kept
  both, in roxygen's order. `devtools::document()` reproduces the file
  byte-for-byte, so the manual resolution is what the generator emits.
- `R/utils.R`: both sides appended to the same `utils::globalVariables()`
  block. Kept both, ours first.
- `inst/extdata/whep_inputs.csv`: both sides appended rows. Kept all
  three, main's two first. Main's separate deletion of the retired
  `luh2_v2h_states` row auto-merged and is preserved -- main ships a test
  asserting that alias is gone.
- `data/whep_inputs.rda`: binary, regenerated from the merged CSV with
  `Rscript data-raw/whep_inputs.R` rather than resolved by picking a
  side. 72 aliases = 70 base - 1 retired + 2 main + 1 ours.

`DESCRIPTION`, `_pkgdown.yml` and `tests/testthat/test_n_urban.R`
auto-merged.

No line of `R/polycell_support.R` or `R/polycell_layers.R` was touched.

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

Three of the four C0 urban blocks stopped being CONSTRUCTIBLE after the
merge, and none of them stopped being true. `build_urban_n()` now routes
`area_code` through `.manure_territory_to_area_code()` (#463/#512), which
aborts on a value it cannot resolve:

  Could not resolve territory to an area_code.
  Unrecognised values: "A", "B", and "C".

The letters were deliberate: C0 picked codes nothing could look up so no
assertion could pass by resolving one. That is now an abort rather than an
opaque label, so the fixture takes the numeric codes the rest of this file
already carries after main's own ISO3 -> numeric port (203 Spain, 68
France, 231 USA).

Only the labels move. Every property, expected value and tolerance is
untouched, and each was measured to reproduce EXACTLY under the new codes
before the substitution was made, on the merged tree:

  sum(urban_n_t)         1129.3082821669493 = pop x rate / 1000, rel gap 0
  shared-cell split      470.5451176 / 282.3270705 / 188.218047
                         = 1e6 x rate/1000 x (0.5, 0.3, 0.2), exactly
  no area term           adding cell_area_ha AND land_area_ha to the
                         crosswalk: identical() TRUE; tripling
                         cell_area_ha: identical() TRUE
  silent drop            crosswalk-missing cell still vanishes with no
                         warning; total 941.09023513912 as before

Counts before and after the merge are the same file for file:
test_n_deposition.R 13 blocks / 34, test_n_urban.R 12 / 26,
test_feed_lpjml.R 20 / 47, all green, 0 skips. The full-scale
`characterize_consumer_baseline.R` run is byte-identical to the pre-merge
run, including the 68,527 / 64,438 / 191 crosswalk fingerprint,
min(polity_frac) identical to 1/36, and deposition mass conservation at
relative gap 0.

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

`polities` uses a half-open validity interval: `start_year` inclusive,
`end_year` exclusive. Year 2014 belongs to `RUS-2014-2025`, never to
`RUS-1991-2014`. Two implementations read it as inclusive, and the
documentation they were written from said "Inclusive".

`R/constant_territory.R` filtered `end_year >= yr` with no dedupe. On a
boundary year both epochs came back active and `.assign_polity()`'s "keep
first match on overlap" handed every cell to whichever sorted first. Measured
over the shipped table that is the DISSOLVED predecessor in 315 of 315 cases,
because `X-a-b` always sorts before `X-b-c`; the successor received no row at
all, and cells lying outside the predecessor's smaller extent were dropped
entirely. It is exported, vignetted and runs during `R CMD check`, so this
moved published numbers.

`R/spatialize_compartments.R` filtered `yr <= end_vals`. Verified dead today:
the deployed crosswalk (68,527 rows) and today's producer output (62,780) both
carry no validity column, so `.country_grid_is_dynamic()` is FALSE and the
resolver returns the grid untouched. It goes live the instant the polycell
producer emits `start_year`/`end_year`, which is why it is fixed before rather
than after.

`R/table_mappings.R` documented `end_year` as "Inclusive", contradicting the
convention `polity_end_year` is already documented under twenty lines below and
matching what both filters did. Corrected here, with the `country_grid` docs in
`spatialize.R` and `spatialize_livestock.R`, so the bug cannot be read back out
of the documentation.

Year resolution moves to `.active_polities()`, which also keeps one interval
per polity using the tie-break `.whep_polity_lookup()` already uses
(`R/polities.R:143-153`): exact start first, then latest start. The exclusive
bound alone does not settle the 86 genuinely overlapping live polity-years
(`PER-1825-1909` beside `PER-1825-1884`, `MNE-1913-1915` beside
`MNE-1913-1918`). The dedupe runs AFTER the reported-code filter on the source
side, so a source the caller named is never discarded for an interval nobody
reported.

Measured movement, decomposed so each half stays attributable:

  exclusive bound alone   665 polity-years / 349 polities
                          298 reattributed to the successor, 147 polities
                          correctly dissolved on their own end_year, 220 at the
                          2025 domain end
  same-polity dedupe    1,263 polity-years / 22 polities
  both (shipped)        1,912 polity-years / 349 polities, of which 1,103 move
                          from a retired/superseded label onto a live one

Named: `RUS-1991-2014` -> `RUS-2014-2025` and `UKR-1991-2014` ->
`UKR-2014-2025` at 2014; USA at 1803/1848/1867/1959; 1870-1885 leaves the
retired `CAN-1866-1886` for `CAN-1870-1886`. Largest movers by count are BLZ
140, ARG 127, BRA 123, GRC 113, IRQ 94, EGY 84, ROU 84. On a real three-epoch
Greek fixture at 1881 the reported total falls from 114.85 to 100 -- the
dissolved epoch was attracting donor imputation on top of the double count.

The roxygen example and all four pre-existing fixtures are bit-identical
(`identical()` TRUE): they were written on non-adjacent epochs and so passed
under either convention.

Consequence worth knowing: under the exclusive bound 2025 has no active
polity, since all 229 open intervals end there. 2024 is the last covered year,
and the abort message now says so.

Tests assert the property over every polity and every year of the domain
instead of hand-picked codes, and add the adjacent-epoch fixture the existing
blocks deliberately avoid. 15 mutants, 14 dead; the survivor -- dropping
`exact_start` from the tie-break -- is proved equivalent, since
`start_year <= yr` makes an exact start the maximum start, checked over all
342 years and 20,000 randomised tables.

Related: whep-polities #550 reports the same defect class in
`add_polity_code()` (`R/polities.R:139`), which is upstream's and deliberately
untouched here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
C2 made `end_year` exclusive and that was right, but a uniformly exclusive
read leaves the current year unrepresentable: all 229 open intervals of the
740-row table end in 2025, so 2024 became the last covered year and
`ref_year = 2025` aborted. DA-24 keeps the bound EXCLUSIVE AT A SUCCESSION --
that is what stops the boundary-year double count -- and makes it INCLUSIVE AT
THE OPEN END, where nothing succeeds the interval and no double count is
possible.

"Still open" is read from the data, never from a hardcoded 2025, because PR
#551 is already moving the table to 749 rows. An interval is open when it ends
on `max(end_year)` of the supplied table AND no later-starting interval of the
same polity also reaches that year. The second half is load-bearing rather than
belt-and-braces: seven polities carry TWO intervals ending in 2025 --
`AGO-1816-2025` beside `AGO-1975-2025`, and likewise ARG, BLZ, BRA, GRC, IRQ,
ROU -- so a bare "end_year is the maximum" test opens 229 intervals where only
222 are open, and the compartment resolver, which has no same-polity dedupe,
would return the terminal year twice. Succession is judged only among the
intervals that reach the domain end: a sibling that starts later but ends
earlier succeeds nothing there, and treating it as a successor would punch a
one-year hole in a continuous polity.

The rule lives in one predicate, `.covers_year()`, used by both live resolvers.
`.active_polities()` passes the polity family; `.filter_country_grid_year()`
passes cell + `area_code` and deliberately NOT `polycell_id`, which DA-2 makes
a function of the polity code and which therefore changes at every succession,
filing an interval and its own successor under different keys. The flag is
computed once on the WHOLE table in `build_constant_territory_series()` and
indexed for the per-year source subset, because a subset can hide the successor
and would reopen a genuinely dissolved epoch.

Measured movement against `f7675039`, over every polity and every year of the
domain: exactly ONE year changes attribution, 2025, which goes from 0 rows to
222. Nothing else moves -- 1684-2024 is row-for-row identical. The 222 are
precisely the polities live in 2024, since no family has an interval starting
in 2025. Largest by polygon area: Russia 1681.5 Mha (`RUS-2014-2025`), Canada
977.5, USA 955.2, Brazil 850.4, Australia 770.2, China 755.5, India 315.9,
Argentina 278.1, Kazakhstan 271.5, DR Congo 233.7. The compartment resolver
moves nothing today: no shipped country grid carries a validity column, so
`.country_grid_is_dynamic()` is FALSE, exactly as C2 found.

The open-end branch is skipped entirely except on a terminal year, and takes a
unique-key fast path that is provably the general case when no compartment
repeats. On a 70k-row grid an ordinary year costs 0.007 s and the open end
0.43 s, down from 1.34 s before the fast path.

C2's property test is extended, not duplicated: it still asserts over every
polity and every year 1684-2025 that no year resolves to two intervals of one
polity, and now also that the open end is non-empty (so that assertion is not
vacuous exactly where the current polities live), that it resolves to the same
set as 2024, that the seven succeeded siblings are enumerated rather than
tolerated, that a table ending in 2000 opens 2000 and not 2025, and that
`ref_year` on the open end works end to end through the exported function.

`tests/testthat/test_datasets_balances.R` re-derived ESP's polity with
`polity_end_year >= yr`. Inert -- ESP is one interval and 0 of 10 benchmark
years disagree -- but it encoded the wrong convention, so it now states DA-24.
`data-raw/balance_coefficients.R` has the same defect and is deliberately left
alone: it is issue #565, and fixing it regenerates packaged `.rda` data whose
movement needs its own measurement.

Every statement of the convention now carries the special case:
`R/table_mappings.R` for both `polities` and `polity_area_crosswalk`,
`R/constant_territory.R`, `R/spatialize.R`, `R/spatialize_livestock.R`,
`R/spatialize_compartments.R`, and `R/polities.R`, where
`.area_year_polity_conflicts()` keeps the strict read on purpose and says so
with the measurement behind it (190 open intervals, 0 areas resolving to more
than one polity on the open end).

Sweep baseline-gated before and after: 14 mutants, 12 dead. The two survivors
are equivalence claims, both discharged rather than excused. Dropping
`.polity_family()` from the resolver's grouping is proved equivalent -- the
same-family dedupe downstream already keeps one interval per family on the same
latest-start ordering -- at 0 differing years over all 342 years of the shipped
table and 0 counterexamples in 20,000 randomised tables. Forcing the slow
branch is a deliberate positive control for the fast path and must survive.

Full suite 6002 passed / 18 failed / 1 error / 2 skipped / 57 warnings, and a
pristine `git archive` of `f7675039` reproduces the identical census -- same
four blocks, same 9/4/5 split, same HWSD error. The 18 are AM-25's census
failures deferred to PR #551. `lintr::lint_package()` with the CI linter set:
0 lints.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…DA-24)

DA-24 settled the convention in `.covers_year()` / `.open_ended_intervals()`
(`R/constant_territory.R`, `6fa90e43`): `start_year` inclusive, `end_year`
EXCLUSIVE at a succession and INCLUSIVE at the open end. `R/polycell_support.R`
kept the uniformly exclusive read at three sites, so the producer stopped one
year short of its own data:

  `expand_polycell_years()`     2025 returned NOTHING -- every polycell of the
                               current world unresolvable
  `.pcs_add_shim()`            a crosswalk describing the domain end attached
                               to no interval, so all 4,420 crosswalk rows fell
                               through to `.pcs_append_crosswalk_only()` and the
                               shim degenerated into a copy of its own input
  `.pcs_polycell_footprint()`  the polycell footprint came back EMPTY while
                               `.pcs_footprints()` still reported a row for it,
                               voiding the reconciliation that measures the
                               migration's movement: 269 disagreeing cells -> 0

The rule is not restated here. All three sites call the existing predicate, via
one new pair of private helpers whose only job is to supply the two things
`.covers_year()` needs from THIS table.

THE DOMAIN. A row with no `polity_code` is `.pcs_append_crosswalk_only()`'s
padding, pinned to a synthetic `[crosswalk_year, crosswalk_year + 1)` window. It
is not a validity interval: it can never be open, and it must not sit at
`max(end_year)` and move the domain end off the year the real intervals reach.

THE GROUP is the physical cell plus the polity FAMILY. Wrong is silent in both
directions, since the rule opens a group's latest-starting member and closes the
rest. TOO FINE and every terminal row is its own group maximum, so all of them
open: `polycell_id` and `polity_code` are both a function of the epoch (DA-2), so
an interval and its own successor land in different groups, and this grain has no
dedupe downstream -- that is the boundary-year double count C2 removed. TOO
COARSE and one lineage closes another: `cell_id` alone merges every polity in the
cell, and `area_code` is a label rather than an identity -- measured on the
shipped table, it is NA for 31 of the 220 live intervals reaching the domain end
(and a pasted key stringifies NA, so those collapse into one bucket rather than
being skipped), while `SDN-2011-2025` and `SSD-2011-2025` are two lineages under
one code, 206. `cell_id` is in the key because a family's successor need not
occupy the same cells: on the family alone, a later start in some OTHER cell
closes a still-open interval here. `.polity_family()` is exactly what
`.active_polities()` groups on, so the package keeps one notion of succession;
`.filter_country_grid_year()` reaches the same key from the other end because a
country grid need not carry `polity_code` at all.

The three sites differ in what a mistake costs, and the grouping was chosen per
site rather than assumed. `expand_polycell_years()` is the per-year consumer
surface with no dedupe anywhere after it, so a wrongly opened interval lands
straight in `polity_area_ha`; its flag is hoisted out of the year loop because
being succeeded is a property of the table, not of the year. `.pcs_add_shim()`
resolves after the join, since the join can duplicate a row and the flag must be
aligned to the rows the mutate sees. `.pcs_polycell_footprint()` ends in
`distinct()`, so it cannot double-count -- its risk is the opposite one, a
wrongly closed interval dropping a cell and manufacturing the disagreement it
exists to measure; dropping the crosswalk-only rows first is what keeps the shim
padding out of the footprint, which is a separate guard from keeping it out of
the domain.

MOVEMENT: exactly one year, measured against a pristine `git archive` of
`6fa90e43`. On a bounded build of 151 shipped live polities (19,884 intervals,
6,024 cells, 1684-2025) every year 1684-2024 is row-for-row identical -- the
resolved key set matches, the area sums differ by 0, and the WHOLE tibble is
`identical()` at 1684, 1800, 1900, 1950, 2000, 2011, 2015, 2023 and 2024. 2025
goes from 0 rows to 4,725, carrying 917.73 Mha, and its polycell set is
`identical()` to 2024's. No `polycell_id` resolves twice in any year on either
side. With a 4,420-row crosswalk, `crosswalk_year = 2015` (the default, and the
only value the shipped pipeline uses) is `identical()` on the table, the shim,
both footprints and the 269-row footprint diff; only `crosswalk_year = 2025`
moves, from the degenerate 24,304-row / 0-attached / empty-footprint shape to
exactly the 2015 one.

SWEEP, baseline-gated: 14 mutants, 13 dead. The one survivor, M13, is the
deliberate positive control -- dropping the per-table hoist must NOT change an
answer -- and the hoist is a performance claim, not a correctness one (342 years
of the bounded table expand in 1.77 s). The four candidate grouping keys are
separated by a unit test on the helper itself, because `.pcs_split_intervals()`
happens to give every interval reaching the domain end in one cell the same
start year and would hide three of the four. The baseline reproduced the working
tree's 379-passed / 18-failed census before AND after the sweep; two earlier
runs were voided by their own gate, once by orphaned concurrent sweeps rewriting
the source mid-census, so the source is now checksummed either side of every run.

Full suite 1,842 blocks / 5,975 passed / 18 failed / 1 error / 57 warnings / 2
skipped, against 1,832 / 5,925 / 18 / 1 / 57 / 2 for a pristine archive of
`6fa90e43` -- the identical failure set, the same three blocks in the same 9/4/5
split and the same HWSD error, AM-25's census failures deferred to PR #551.
`lint_package()` with the CI linter set: 0 lints. `rcmdcheck()`: 0 errors, 0
warnings, 0 notes.

Not fixed, deliberately: the interval-grain DIAGNOSTIC attributes have the same
one-year shortfall one layer out. Filtering them with a plain half-open test at
the domain end returns 0 rows where 2024 returns 9 (`overlap`) and 50
(`coverage`), and `inst/scripts/verify_polycell_support.R` holds three such
filters (`:190`, `:343`, `:374`). All are inert today because the script pins
`year = 2015L`, and the readers of those attributes are consumers, which this
commit does not migrate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
build_n_deposition() splits each cell's HaNi mass across the polities holding
that cell. The split key was `polity_frac`: a raw subcell count over Natural
Earth 10m polygons at 1/12 degree, quantised to 1/36 of a cell. It is now the
polity's share of the cell's territory, `polity_area_ha` as
build_polycell_support() measures it geodesically. The key change only. No
category split (that is C3b), no new denominator, and `deposition_kgn_ha` is
untouched.

Either key is a share of the cell, so the source mass is redistributed and
never created or destroyed. Measured on the deployed crosswalk over the 63,065
cells both supports carry, with a uniform synthetic field:

  source mass                 : 63065000000000 g
  sum(deposition_n_t)*1e6 OLD : 63065000000000 g | rel gap 0.000e+00
  sum(deposition_n_t)*1e6 NEW : 63065000000000 g | rel gap 0.000e+00
  NEW total - OLD total       : 0 t (rel 0.000e+00)
  per-cell mass under NEW: min 1000.000000000000 max 1000.000000000000 t
  cells whose polities disagree on deposition_kgn_ha: 0

An unmigrated consumer is unaffected, proved rather than argued: over the whole
68,527-row crosswalk this commit is identical() to HEAD 0dbc9c4 on all 11
shared output columns. The one difference is the new `method_polity_split`,
which records which partition produced a table so the two cannot be confused
after the fact; `split =` selects it explicitly and naming a key the support
lacks aborts instead of falling back to the coarser one.

`deposition_kgn_ha` stays a whole-cell mean shared by every polity of a cell.
That is the guard against dividing the allocated mass by each polycell's own
territory, which would make rate x area recover the whole cell mass once per
polity behind entirely plausible rates. The cell_area_ha round trip is kept
rather than simplified away, so substituting a land area on one side alone
stays a visible break instead of a silent ~10% fall.

build_polycell_support() keys on polity_code, and polity_area_crosswalk folds
distinct polities into one area_code (Sudan and South Sudan share 206) or
leaves it NA. Deposition rows are keyed on area_code, so a support carrying a
duplicated or NA area_code is refused with an instruction to convert at the
caller's boundary (DA-23) rather than folded here, where the fold would look
like a partition.

Movement per S-A10, measured on values and decomposed so the partition change
stays attributable from the area_code churn that travels with it. Over the
3,532 shared cells whose polity set is unchanged, per-cell L1 between the old
and new share vectors is median 0.0432, p90 0.1190, p99 0.3212, max 1.3396,
and 38.6% exceed the 1/36 = 0.0278 quantum AM-8 predicts. The excess is
expected: the swap changes the polygon SOURCE (Natural Earth to the periodized
whep polities) as well as the estimator, and AM-8 bounded only the estimator.

No published balance moves on this commit. Nothing in the package reads
`deposition_n_t`; .n_inputs_deposition() reads `deposition_kgn_ha`, which is
bit-identical between the two keys.

Mutation sweep, baseline-gated at 70 assertions green and byte-restored after
each mutant: 19 mutants, the 18 defect mutants all dead and the comment-only
positive control alive.

Full suite against a pristine git archive of 0dbc9c4: identical 18-failure /
1-error set, the AM-25 census failures deferred to PR #551.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ger (C3b)

build_n_deposition() decomposes each polycell's share of the cell's HaNi mass
over the three DA-3 territory categories -- land, inland water and ice -- in
DA-19's apportionment shape, the one the producer already uses. The categories
are emitted side by side and separately addressable, so a consumer picks a
scope per purpose instead of being forced onto one. A support with no category
columns cannot be decomposed and carries the single "territory" category,
which says the row is undecomposed rather than claiming it is land.

DA-14 DECIDED, 2026-08-06, on the measurement this commit checks in: the
nitrogen ledger takes the WHOLE territory. That is a scientific correction,
not a conservative default. Nitrogen deposited on a lake or a glacier still
drives indirect N2O and still reaches the eutrophication pathway, so the impact
terms have to account for it; the terrestrial-only scope would have discarded
0.89 Tg N of real flux. Scope is exposed as data$deposition_scope, default
"territory", with "land" selectable per the repo's multi-method rule, recorded
in a new method_deposition_scope output column, and refused rather than
silently downgraded when the support cannot serve it.

NO PUBLISHED NUMBER MOVES. Under "territory" the scope factor is a sum divided
by itself, exactly 1, so every ledger value is bit-identical -- asserted with
expect_identical() over the whole deposition slice of build_n_inputs(), not
argued here. The C0 characterisation re-run at full crosswalk scale reproduces
AM-14 exactly: 68,527 rows, source 64438000000000 g recovered to a relative gap
of 0, the row-wise polity_frac identity bitwise on 53,752 of 68,527 rows with a
worst relative gap of 3.22e-16, a 3x cell_area_ha moving 26,122 rows by at most
3.9e-16 with the total bit-identical, and C3a's named guard still at 0 cells
whose polities disagree on deposition_kgn_ha.

THE DA-14 MEASUREMENT, checked in at inst/scripts/measure_deposition_categories.R
and run at 2014 on real HaNi, GLWD and Natural Earth ice input:

  BEFORE (all territory) 61.6285 Tg N | AFTER (land only) 60.7385 Tg N
  change -0.8901 Tg N (-1.444%)
  NEW QUANTITIES: freshwater 0.7248 Tg N (1.176%) | ice 0.1652 Tg N (0.268%)
  conservation: source 61628543587563.125 g, three categories recover
    61628543587563.125 g, relative gap 0 (DA-18 bound 1e-9)
  per polycell the three categories re-sum to the undecomposed share to
    2.78e-13 over 62,725 polycells

AM-6's ~3.7% MUST NOT BE QUOTED AS THE BOUND for this question. It is an
AREA-weighted bound; measured area-weighted is 3.532% and the DEPOSITION-
weighted figure is 1.444%, because deposition is not distributed like territory
-- ice is 1.72% of territory and takes 0.27% of deposition. AM-6's named
polities do reproduce in rank (Canada #1 -6.93%, USA #3, Russia #4, Sweden #11,
Finland #13 -7.50%, Norway #23) but AM-6 misses two classes that dominate the
relative ranking: mountain-glacier states (Tajikistan -13.41%, Iceland -10.48%,
Switzerland -6.39%, Nepal -6.86%) and African rift-lake states (Malawi -7.58%,
Rwanda -5.81%). The Great Lakes box carries 74.88 kt N to freshwater, 10.3% of
the world's, best cell ranked 15 of 63,185 -- a real hotspot but not the top
one, which is Tibetan and Chinese lakes.

THE ICE TERM IS A FLOOR, not an estimate. The deployed crosswalk has no
area_code for Greenland at all (0 of its 191 codes, 0 rows at central
Greenland), so 4,554 cells holding 177.51 Mha of ice -- 77.9% of the world's --
are unreachable by the ledger. Pre-existing, not introduced here, and the same
class as EA9's missing terrestrial cells.

PARTITION MISMATCH (AM-29), resolved as far as this commit can. The ledger is
self-consistent as shipped because deposition_kgn_ha is a cell constant
carrying no partition, so polity_frac via build_ag_land_support() is the only
partition inside it. Under a future "land" scope only the ice term is
partition-dependent -- DA-19 apportions water pro rata, so every polity of a
cell carries the same water fraction whichever partition splits it -- and that
is bounded by measurement at 0.074841 Tg of 0.165237 Tg, 0.12% of the total.
Consuming the geodesically split land mass directly would NOT be
self-consistent and needs build_ag_land_support() to migrate first (C-series).

R/n_balance.R FIXES A CLASS, NOT AN INSTANCE, and this is the part to review.
Two hardcoded c("method_recycling_n", "method_synthetic") vectors would have
swallowed ANY future method stamp, so the new scope column reached
build_n_inputs() and was silently dropped before the published balance -- a
result that looks like it names its own scope while the reader can never see
it. Both sites now route through .nb_input_method_cols().

A DEFECT FOUND BY ITS OWN TEST, recorded because the class keeps recurring.
The new one-rate-per-polycell guard first computed rate_spread AFTER
deposition_kgn_ha inside the same dplyr::summarise(), so it measured the spread
of a scalar and was 0 by construction: an assertion structurally incapable of
failing, the third on this branch after .pcs_gaps_before() (AM-20). Fixed by
ordering, and pinned as sweep mutant M22.

VERIFICATION. Mutation sweep run inside a git ls-files sandbox copy that cannot
write the repo, triple-gated (baseline green before each mutant, patch text
present exactly once, source md5-restored after each): 25 mutants, 24 defect
mutants ALL DEAD, 0 survived, 0 patch-miss, comment-only control ALIVE, all
gates TRUE, post-sweep baseline pass 224 fail 0 err 0. The four named risks are
M14 (unserviceable scope accepted), M16 (default silently "land"), M17 (scope
column mis-recorded), M19 (ledger takes every category) -- all dead.

Tests: test_n_deposition.R 113/0/0 (was 70), test_n_balance_inputs.R 111/0/0
(was 85), test_n_balance.R 76/0/0 (was 71), test_n_surplus.R 15/0/0,
test_exported_polity_columns.R 29/0/0. air format . clean over the whole repo.
Pristine control at d94444e: 1853 blocks, 5979 pass, 18 fail, 0 error, 11 skip,
the 18 confined to 3 test_polycell_support.R blocks.

NOT YET RUN AT COMMIT TIME, stated rather than implied: the head-side control
run (only the pristine side has completed, so "no new failures" is not yet
proved), rcmdcheck() and lintr::lint_package() with the CI linter set. Those
follow in their own commits.

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

PR #577 landed the exclusive `end_year` and DA-24's open-end rule upstream, so
this merge drops our `f7675039` where the two overlap and keeps theirs. What
#577 does not carry is the same-polity dedupe, and that half is re-landed here
because it fixes something the exclusive bound alone does not: on a boundary or
overlap year two intervals of one polity both come back active,
`.assign_polity()` keeps whichever sorts first, and the dissolved predecessor
sorts first in 2,134 of 2,134 co-active pairs on the shipped table because
`X-a-b` precedes `X-b-c` lexically. The successor then gets no row at all and
every cell outside the predecessor's smaller extent is dropped, so the defect
loses mass rather than merely relabelling it.

Conflicts and how they were resolved:

- `R/constant_territory.R`. Kept our `.active_polities()` / `.covers_year()` /
  `.open_ended_intervals()` / `.polity_family()`, which are a strict superset of
  main's inline `.active()`: main's is exclusive everywhere, so it leaves 2025
  unresolvable, and it has no dedupe. `R/polycell_support.R` and
  `R/spatialize_compartments.R` call those helpers directly, so replacing them
  would put a second copy of the convention back in the producer. Main's
  measured comment (238 polities carrying a polygon in 1993 under an inclusive
  read, 453 extra active polity-years over 1850-2024) and main's `as.character()`
  fix in `.assign_polity()` (#209) are both taken.
- `R/table_mappings.R`. Merged rather than chosen. Main's `polities` doc is
  stale against main's own code: it says an open period "stops one year short"
  of its `end_year`, while `.polity_join_end_year()` on the same branch widens
  open periods by one. The merged text keeps main's F51-1947-1993 succession
  example and their successor-based framing, adds the open-end half, and states
  the figures re-measured on the 749-row table.
- `tests/testthat/test_constant_territory.R`. Both sides' blocks kept.
- `NAMESPACE`, `man/*.Rd`. Union, then regenerated with `devtools::document()`.
- `inst/extdata/whep_inputs.csv`. Main's newer pin versions for the four LPJmL
  aliases plus our `polycell_support` row; `data/whep_inputs.rda` regenerated
  from the merged CSV with `data-raw/whep_inputs.R`.

Re-measured on the 749-row table (#551), because every figure moved:

- Shared-start-year exceptions: 9, not 8. #551 added `CAN-1886-1949` beside the
  now-retired `CAN-1886-1948`. Re-pinned as an enumerated list.
- Intervals ending on the domain end: 237, of which 229 are open and 8 are
  succeeded there. `CAN-1948-2025` is the eighth, so a bare "end_year is the
  maximum" test now opens 237 where 229 are open. Upstream's own `successor`
  column agrees with the data-derived predicate on every open interval, and a
  new assertion pins that so the two mechanisms cannot drift.
- Self-overlapping polity-years: 2,134 across 23 families on the 703
  polygon-carrying rows the resolver actually sees, up from 1,991. On live,
  non-aggregate rows it is 0, down from 86 across MNE and PER, because #551
  retired `MNE-1913-1915` and superseded `PER-1825-1909` rather than deleting
  them. `get_polity_geometries()` does not filter by `wiki_status`, so the
  dedupe matters more after #551, not less.

FINDING, pinned rather than fixed: the tie-break is status-blind, so on a shared
start year a live interval can lose to a dead one. Two cases.
`CAN-1886-1949` is harmless, its polygon being `st_equals()` to the retired
`CAN-1886-1948`'s. `MNE-1913-1918` is not: the retired `MNE-1913-1915` that wins
carries 0.9923 Mha against its 1.5893 Mha, so 1913 and 1914 resolve to a 37.6%
smaller Montenegro. Deciding by status would change the function's contract
(`wiki_status` is not among the three columns a caller supplies), so it is
enumerated in the test instead.

Verification. Full suite against a pristine `git archive` of `2287ad22` in the
same environment (identical WHEP_* fingerprint on both sides), diffed at block
level: pristine 1872 blocks / 6085 pass / 18 fail / 1 error / 2 skip, merge
result 1980 / 6464 / 18 / 1 / 2. 110 blocks added, all from main; 2 removed,
both deleted by main; 0 shared blocks changed fail/error/skip; the only 2 shared
blocks whose pass count moved are the two assertions added here (+3). The
failing set is identical -- the same three deferred `test_polycell_support.R`
blocks with the same 9/4/5 split, and the same pre-existing HWSD error (#596).
The dedupe is mutation-tested in a sandbox that cannot write the repo, baseline
re-verified green before every mutant and the source md5-restored after: 9
mutants, 8 dead, 1 (dropping the redundant `exact_start` key) proved equivalent
over 342 years on both populations and 228,175 randomised comparisons, with an
unmutated control alive. `air format .`, `devtools::document()`,
`lint_package()` 0 lints, `rcmdcheck()` 0 errors / 0 warnings / 1 note --
and that note reproduces byte-for-byte on a pristine `origin/main`, so it is
inherited (`sovereign_iso3c`, `key_row`, `canonical_area` are undeclared in
`globalVariables()` on main).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`.active_polities()` ranked a same-polity overlap on the start year alone,
so where two intervals of one polity share a start year nothing decided the
winner and a `retired`/`superseded` row could take the year from the live
row that replaced it. Measured on the shipped 749-row table: 1,344
polity-years across 10 polities resolved to a dead interval.

`wiki_status` is now the LAST key of the ranking, below the start year. A
later start is a more specific epoch, so ranking status above it would
resurrect a superseded wider period over the narrower one that succeeded it
-- the C2 defect in reverse, and measurably 132 further polity-years
(`BLX-1921-1999`, `IDN-1889-1945`).

The column stays OPTIONAL. Where it is absent every row reads as live, so
the key is constant and the ordering is bit-for-bit the one that stood
before: verified identical over all 47,741 polity-years against a pristine
`git archive` of b778cc1. The difference between the two call styles is
documented in `@param polities` rather than left implicit.

Movement, measured on values rather than row counts. Exactly one case moves
territory: `MNE-1913-1915` (retired, 0.9923 Mha) to `MNE-1913-1918` (draft,
1.5893 Mha), so 1913 and 1914 stop resolving to a Montenegro 37.6% too
small; the symmetric difference is 0.5970 Mha. `CAN-1886-1948` to
`CAN-1886-1949` (62 yr) and `IDN-1800-1889` to `IDN-1800-1945` (89 yr) are
`st_equals()` at 937.96 and 188.51 Mha, so those are relabelling only. The
remaining 1,191 are seven reporting-bucket aggregates whose dead side
carries no polygon at all, and `build_constant_territory_series()` drops
empty geometries before resolving, so its own exported surface moves 153
polity-years, not 1,344.

The exception list re-measured with the column supplied still has nine
entries, but every one is now a `retired`/`superseded` row the tie-break is
supposed to pass over: ZERO live losers, against two before.

`.polity_is_live()` states which rows are dead once for the package;
`resolve_polity_label()` and `.pcs_prepare_polities()` now read it instead
of each carrying its own literal, so the producer's filter and the
resolver's tie-break cannot drift.

Sweep: 11 mutants, 9 dead, control alive, baseline green before and after
every mutant; the one survivor is proved equivalent over 102,988
comparisons. Full suite diffed at block level against a pristine
`git archive` of b778cc1 in the same environment: 7 blocks added, 0
removed, 0 shared blocks changed on pass/fail/error/skip, failing set
identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two changes that had to land together. The crosswalk: every default reader on
the carbon path read `spatialize-country-grid`, whose pin carries only
(lon, lat, area_code), so `.normalize_country_grid()` defaulted
`cell_area_frac` to 1 and gave a border cell's whole land to one polity (EA4).
They now resolve one polycell support through `.carbon_cell_support()`. The
area (DA-26): the LUH2 class fractions are rescaled onto the polycell's own
`land_area_ha`, so the carbon path and the nitrogen path share one land
definition instead of two.

`area_basis = c("polycell_land", "luh2_fraction")` keeps the pre-DA-26
arithmetic selectable and records the choice in `method_land_area`, so the
crosswalk change and the area change can be measured apart and two tables built
on different bases are not indistinguishable afterwards.

`cell_area_frac` is the polycell's share of the cell's LAND, never
`land_area_ha / cell_area_ha`: everything it splits is already land-only, so
that ratio would remove the water twice while the shares still summed to 1.
The share denominator is taken over the cell's whole measured land BEFORE any
row is dropped, so an unkeyable polity's hectares are lost visibly instead of
being renormalised onto its neighbour.

DA-23 is handled at one boundary and refused everywhere else: polycells with no
`area_code` are dropped with their land reported, polycells sharing an
`area_code` in one cell have their land summed with the fold reported, and a
support handed to a consumer that is not already one row per cell and
`area_code` aborts.

`build_ag_land_support()` is pinned to the transitional basis: it is the
nitrogen path's land support, whose cropland half is still split by
`polity_frac`, and it must not end up with its two halves on two land
definitions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
spatialize_country_n_to_crops() distributes a national synthetic-fertiliser
total across grid cells. Each cell's weight was its crop-pattern (or cropland)
hectares times `polity_frac`, the crosswalk's subcell-count share of the cell,
quantised to 1/36. It is now the polycell's share of the cell's LAND,
`land_area_ha / sum(land_area_ha)` over the cell, as build_polycell_support()
measures it geodesically.

AM-4 makes C5 the CONTROL CASE: the cell weights are renormalised inside each
polity-crop-year, so a change of partition redistributes the national total and
cannot change it. The prediction held, exactly. Measured at 2015 on real FAOSTAT
synthetic-N totals and real harvested-area crop shares, over the 173 polities
both supports can place (106.5352 of 106.6550 Mt N):

  173 polities; old 106.535153 Mt N, new 106.535153 Mt N
  difference                          0.000000000 t
  worst per-polity |difference|       0.000000000 t
  polities moving more than 1e-6 t    0

And no ~11% signal on the weight. Restricted to the 1,011 polity-crop-years
landing on exactly the same cells -- the weight alone, with the footprint held
fixed -- the L1 distance between the old and new normalised cell-share vectors
is median 0.000251, p90 0.0592, p99 0.2310, max 0.3643, with 40.95% exactly
zero; per-cell relative share change is median 0.0000%, p99 26.60% and
old-share-weighted mean 1.99%, and 57.29% of cells are bit-identical. Over the
13 polities whose whole cell set is unchanged the L1 is median 0.0368, max
0.1897, which is the same order as AM-29's L1 median 0.043 for the same key swap
in the deposition path.

Over the full migration the L1 tail reaches 2.000000, and that is the FOOTPRINT,
not the weight: the two supports differ by 641 crosswalk-only and 4,574
polycell-only cells, and a polity gaining or losing a cell can switch a crop
between the crop-pattern route and the uniform-cropland fallback. It vanishes
under the same-cell-set cut (max 2.000000 -> 0.3643), and mass is conserved
exactly in every case. Cells with a non-zero allocation: 43,272 -> 43,293,
43,177 shared, 95 old-only, 116 new-only.

The share is of the cell's LAND, never `land_area_ha / cell_area_ha` -- what it
splits is already land-only, so the whole cell as denominator would remove the
inland water and ice a second time. And it is a share, never an absolute area:
`crop_pattern_ha * land_area_ha` weights every cell a second time by its own
size and is no partition at all. Both are AM-5 risk 3 in its C5 form, and both
are pinned as mutants that die.

`split = c("auto", "land_area_ha", "polity_frac")` keeps the transitional key
selectable and records the resolved key in `method_polity_split`, so two tables
built on different partitions are not indistinguishable afterwards. `auto` takes
the measured land when the support carries it; naming a key the support lacks
aborts rather than falling back to the coarser one.

DA-23 is refused rather than folded: a support whose `area_code` is duplicated
or NA is rejected with an instruction to convert at the caller's boundary. The
refusal is a property of the polycell key only, so the transitional path stays
exactly what it is today (DA-13), and a `polity_frac` support is used as
supplied and never renormalised.

The share denominator is the cell's whole supplied land, taken BEFORE the
landless cells are dropped. Dropping first renormalises the survivors onto the
whole cell and hands them hectares that are not theirs while the shares still
sum to 1 -- C7's own defect, pinned here as a mutant. It also bites at the
CALLER's boundary, where the DA-23 conversion happens, so `@param cell_polity`
names the correct treatment: keep unkeyable land in the denominator under a
bucket no country total can join. Measured, that matters: 3,568 polycells carry
no `area_code` (101.938 Mha), of which 42.102 Mha sits in 289 cells a keyed
polity also holds. Dropping them instead moves 16% of polity-crop-years
(L1 p99 0.6232, max 1.3581) with the totals still conserved.

Mutation sweep in a `git archive` copy that cannot write the repository,
baseline-gated at 69 assertions green, patch text present exactly once, source
restored byte-identically after each mutant: 22 mutants, the 21 defect mutants
all dead and the comment-only control alive. Two genuine holes were found and
closed rather than excused -- `auto`'s preference order was untested (every
fixture carried only one key) and the transitional key's no-renormalisation rule
was untested (every fixture already summed to 1 per cell).

Full suite against a pristine `git archive` of 0477a4d in the same environment,
fingerprints diffed and identical, compared at BLOCK level: 17 blocks added (all
C5's), 0 removed, 0 of the 2,004 shared blocks changed pass/fail/error/skip, and
the failing set is identical (the 18 deferred test_polycell_support.R failures
and 2 pre-existing errors). `lint_package()` 0 lints; `rcmdcheck()` 0 errors,
0 warnings, 1 note -- the note inherited from main (canonical_area, key_row,
sovereign_iso3c).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
S-A5. `.normalize_country_grid()` defaulted a missing share to
`cell_area_frac = 1`, which is exactly how the deployed
`spatialize-country-grid` pin (58,795 rows, `lon`/`lat`/`area_code`, 0
multi-polity cells) gave a border cell wholly to one polity while every
conservation check still passed. That default is now an abort, and so
are an NA share and a share outside [0, 1] beyond float tolerance -- the
last of which catches an absolute area substituted for a normalised
weight. `landfrac` is removed from the alias list and named as a
refusable quantity: it is a LAND fraction, one value per cell and
identical for every polity in it, so reading it as a polity share
delivers the cell once per polity (AM-5 risk 7).

S-A6. `.compartment_join_cols()` aborts rather than silently degrading
to a `(lon, lat)` join, which across a polycell grid is many-to-many.
`.build_base_grid_cp()` drops pattern cells no compartment claims
instead of giving them a whole-cell share under an NA `area_code`.
`.build_proxy_grid()` carries only the compartment key and the weight.
The acceptance note at the head of `R/spatialize_compartments.R` states
what remains irreducible: every driver layer is per physical cell, so
two polycells in one cell share one cropland density, one grass
productivity and one GLW3 density. That is resolution loss, not
cross-border contamination -- the share denominators stay per polity.

AM-5 risk 19: the capacity ceiling is soft, and it is soft at scale.
`.warn_capacity_breach()` measures and reports it without changing
which invariant yields; on the deployed crosswalk at 2000 and 2015,
22,800 of 78,083 polycell-years (29.2%) hold more harvested area than
their own ceiling, 308.7 Mha in total.

AM-5 risk 26: `.normalize_to_cropland()` and `.get_area_code_from_grid()`
deleted. Both were unreachable and both keyed on the physical cell.

AM-5 risks 16 and 17, in `inst/scripts/run_spatialize.R`: the `(lon,
lat)` re-attachment of `country_grid` is gone and the hand-copied
compartment keys now call `.compartment_id_cols()`. Its call to
`build_gridded_landuse()` also passed three arguments the engine has
not accepted since `config` was introduced, so the script could not run
past its first call; fixed. All three are pinned by static checks with
positive controls, because the script is neither linted nor executed.

Value movement: none. Against pristine b6b36e9 on the deployed
crosswalk (68,527 polycells, 3,925 multi-polity cells) at 2000 and
2015, land use is `identical()` over 3,702,607 rows and livestock over
832,386; per (country, crop/species, year) share-vector L1 is 0 for all
15,322 and 2,686 groups, and no cell moves by more than 1e-12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DA-13 had `build_polycell_support()` carry `polity_frac` and pad itself with
the deployed crosswalk's unreproducible rows, so every unmigrated consumer was
provably unchanged while consumers migrated one at a time. That is the only
reason C3a, C3b, C5, C7 and C8 could each be attributed a movement. The
transition is over for the consumers on our side of DA-28, so it goes.

Removed: the `polity_frac` output column and `.pcs_add_shim()`; the
`coverage_status == "crosswalk_only"` padding and
`.pcs_append_crosswalk_only()`; the exported `polycell_shim_view()` and the
`.pcs_strip_diagnostics()` helper that existed only to serve it; and the four
now-dead `crosswalk_only` filters in the water-unmatched, overlap, footprint
and unassigned diagnostics.

Kept, deliberately: `cell_area_ha`. DA-13 named it a shim column, but it is
not one. `.nd_polity_share()` requires it and `deposition_kgn_ha` is the cell
mass divided by it (`n_deposition.R:241,491`), and
`.carbon_support_to_area_code()` requires it too (`carbon_balance.R:1282`), so
dropping it aborts two MIGRATED consumers. `data$crosswalk` and
`data$crosswalk_year` also stay: they now feed the DA-12 footprint
reconciliation alone, which reports the same disagreement the padding used to
absorb.

The two DA-13 validators are flipped rather than deleted.
`test_polycell_support.R`'s "the shim reproduces the crosswalk bit-for-bit"
becomes "the DA-13 shim is gone and a crosswalk cannot revive it", asserted on
the same fixture and with `data$crosswalk` supplied, so the removal is proved
on the path that used to produce the shim rather than on one where it could
not have appeared. `verify_polycell_support.R`'s DA-13 section now ABORTS on a
`polity_frac` column, a padding row or an unmeasured row instead of printing
`identical: TRUE`.

`"polity_frac"` is dropped from `utils::globalVariables()` LAST, per AM-4.
What that exposes, measured with `codetools`: exactly 5 call sites, all in the
three files this PR does not migrate — `water_balance.R` x3 (`.wb_finalise`,
`.wb_drop_polity_cols`, `.wb_aggregate_polity`, DA-28), `feed_lpjml.R` x1
(`aggregate_grass_to_polity`, frozen by AM-2) and
`feed_intake_redistribute.R` x1 (`.grass_to_cells`). No migrated consumer is
among them.

Value movement is EXACTLY ZERO, proved rather than asserted. Built on 61 real
polity intervals with the real GLWD water, Natural Earth ice, LUH2 and the
deployed 68,527-row crosswalk, pre-C9 against post-C9: 74,131 rows -> 7,456,
the difference being 66,675 padding rows and nothing else; all 20 shared
columns `identical()` in the same row order; all five area masses delta
exactly 0; and all nine diagnostic attributes `identical()`, which is what
proves the four removed filters were dead. `sum(land_area_ha)` over the
unfiltered table goes from NA to 953,941,550.84999514 ha.

The whole-ledger measurement is in the report, not here, but its headline
belongs on the record: at polity grain the migration's ledger movement is
entirely in the deposition INPUT term, -678,612.5 t N (-2.410% of the term,
-0.504% of `n_input_full_t`), of which -648,491.3 t (95.6%) is nine polities
losing their whole deposition to the DA-23/DA-28 identity gap and only
-30,121.2 t (0.107% of the term) is geometry. The deposition SPLIT KEY -- the
thing C3a changed -- contributes exactly 0 on 27 of 28 ledger quantities and
1.26e-16 relative on the 28th. Synthetic (C5) moves by exactly 0 t.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
S-A8. `inst/scripts/reconcile_polity_areas.R` measures each polity's polycell
total against its official national area and books every material discrepancy
to one owner. It changes no producer code: a wrong area is a finding.

The reference is FAOSTAT Land Use (RL) from the package's own pin, and the
choice of quantity is load-bearing. FAO's Country Notes define "Country area"
as land + inland waters + COASTAL waters, which puts Canada at 15.64 Mkm2;
"Land area + Inland waters" is used instead, and it is immune to France, whose
country area never absorbed its coastal waters (residual -5,732,300 ha).

EA6 and AM-29 are BOTH right and neither is a correction of the other: the
crosswalk census gives 23 of 191 on the LUH2-land weighting and 21 on the
whole-cell weighting (at 45%: 25 and 24). EA6's "Bahrain 46%" is 45.9% on
land; AM-29's 29.9% is the same country on cells, and Croatia is 63.9% / 46.5%.
Both weightings are now printed side by side so the ambiguity cannot recur.

The four island states no longer draw a whole cell: 34.3x / 17.5x / 15.3x /
10.3x of their official area under the crosswalk, 1.18 / 1.00 / 1.16 / 0.58 now.

Greenland is the case that proves the reference must be read rather than
assumed: FAO states "Greenland 'Country area' refers to area free from ice",
so comparing 213 Mha of territory against it reported +419% until the ice was
taken off WHEP's side, and -12.9% after. Separating the polygons two live
polities both hold moved 16 further polities out of "polygon quality".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
T-A12 closes the three defects the T-A1 verifier logged against
`inst/scripts/diagnose_polycell_support.R`.

(a) `.pc_polities()` read `whep::polities` and nothing else, so every finding
in section P -- the status vocabularies, the dead-row count, the DA-2
code-versus-column disagreement -- could only ever describe whatever snapshot
happened to be embedded. `WHEP_POLITIES_PATH` now points the section at any
other polity table (an `sf` saved with `saveRDS()`, or any layer
`sf::st_read()` opens), the source is printed, and the DA-2 count carries a
note saying it belongs to the vintage rather than to the package. Proved end
to end: with a table whose two offending `end_year` values are corrected, the
count goes 2 -> 0 and the source line names the injected file.

Note for the record: the symptom the verifier saw is already gone for an
unrelated reason. `whep::polities` is now the re-synced 749-row table, not the
603-row one, so the checked-in artifact prints "2 disagreeing codes"
(`NNG-1949-1963`, `TAN-1922-1964`) and agrees with DA-2 rather than
contradicting it. That was an upstream re-sync, not a fix -- the missing
injection point is what actually gets fixed here.

(b) The LUH2 time origin was assumed (`year - 850 + 1`). It is now read from
`nc$dim$time$units` and the year vector built from the axis values, with an
abort naming the covered span. DA-9 makes vintage switching a supported
operation, so a tree with another origin would have been sliced at the wrong
year confidently. The file in hand declares a 850 origin over 1173 steps,
850-2022, which the script now prints instead of deriving.

(c) Section H hardcoded 2014 and its own 1850 origin while every other section
ran at 2015. It now takes the script's evidence year, prints that year next to
the mass, and reads its origin from the file too (1850-2020). The plan's EA9
and DA-10 HaNi figures are 2014 measurements, so `WHEP_DIAGNOSE_YEAR=2014`
reproduces them exactly: 34.77 Tg NHx, coastal median 0.9937 (n = 53,624)
against an interior control of 1.0000 (n = 2,192,108). At 2015 the mass is
35.25 Tg and the coastal median 0.9935. EA1, EA2, EA3, EA6 and Z are
byte-identical to the pre-fix run.

T-A8 puts the load-bearing consequences in user-facing documentation rather
than only in commit messages.

`build_polycell_support()` gains two sections and a `@return` paragraph. The
four-way land ladder is stated with its numbers -- whole cells 14.3195 Gha,
HaNi's mask 13.5977, the polity polygons 13.2795, LUH2 terrestrial 12.9931 --
with the polygon row flagged as vintage-dependent rather than a constant, and
the GLWD/CRU mask named as a fifth that is reconciled instead of joined away.
`ice_area_ha` is documented as a present-day snapshot that does not vary
historically, acceptable only while ice is a reporting category and not a
driver. And identity is stated to be `polity_code` alone: measured on the
shipped crosswalk, 505 polity codes fold into 201 reporting buckets, 113 of
which hold more than one polity and one of which (206) holds Sudan and South
Sudan at once, so the conversion belongs at the consumer's boundary where the
fold is visible.

`build_n_deposition()` gains "Which land definition governs what": WHEP's
territory governs PLACEMENT, HaNi's land mask governs the TOTAL. Both rejected
alternatives are recorded with their cost (a whole-cell rate times
`land_area_ha` sheds ~9% of the source mass; re-referencing to HaNi's own mask
moves the total ~4.5%), and the identity that is NOT available is named so
nobody looks for it. `read_n_deposition()` gains the HaNi `@source`, verified
against Crossref rather than remembered.

NEWS.md gains the migration notes: what a consumer changes, the measured
movement (all of it in the deposition input term, -0.504% of `n_input_full_t`,
95.6% of it nine polities losing their deposition to the DA-23/DA-28 identity
gap and only 0.107% geometry), the island-state fix, the Greenland reference
mismatch, DA-14's scope default and why the terrestrial-only scope would have
been wrong, and the five surviving `polity_frac` call sites --
`water_balance.R` x3, `feed_lpjml.R`, `feed_intake_redistribute.R` -- stated
as deliberate, out of scope, and inside a NOTE the package already had.

Verified rather than inherited: 13.5977 Gha and 12.9931 Gha re-measured here,
14.3195 Gha reproduced by the diagnostic, and the crosswalk fold measured
directly.

Gate, all against a `git archive` of 7d1858d in the same environment:
`air format --check .` 0; `devtools::document()` touches only the three
expected `.Rd`; `lintr::lint_package()` 0 lints in 20.7 min; `rcmdcheck()`
0 errors / 0 warnings / 1 NOTE with the note text BYTE-IDENTICAL to pristine;
pkgdown reference index builds clean with 0 man topics missing from
`_pkgdown.yml`. Test suite at block level: 2,039 blocks both sides, 0 added,
0 removed, 0 shared blocks changed on failed/skipped/error/warning/passed/nb,
failing and erroring sets identical -- 18 expectations across the same 3
`test_polycell_support.R` blocks and the same 1 HWSD error (#596).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second reconciliation with `main`, which moved 53 commits since the first
one's merge base. Two conflicts, both insertion-versus-insertion, both
resolved as a lossless union rather than a choice:

- `NAMESPACE`: each side added an export at the same alphabetical slot.
  Both kept. `devtools::document()` then regenerates the file with no
  change, which is what proves the ordering rather than my asserting it.
- `R/n_balance_spatialize.R`: #579 added the `.cell_polity_*` area-key
  helpers and C3a/C5 added the `.n_*` split helpers, both immediately
  before `.n_check_totals_shares()`. Both blocks kept, with main's left
  exactly where main put it so the next merge does not re-conflict here.
  The merged file differs from `1c7edf86` by precisely main's 149/6 and
  from `origin/main` by precisely our 187/20, so no line from either side
  was dropped.

The other fourteen shared files auto-merged; `R/polities.R`, `R/utils.R`,
`tests/testthat/test_n_balance_spatialize.R`, `NEWS.md` and `_pkgdown.yml`
were each checked to be the same lossless union.

#473 was already in the merge base, so T-A11 stays superseded:
`read_luh2_landuse()` carries main's `states_source` vintage selector
untouched, and this branch's `area_basis` land-definition selector beside
it. There is no second vintage mechanism to reconcile.

#618 has not merged, so there is nothing to absorb into
`R/soil_carbon_inputs.R` yet. The merge leaves that file, `R/feed_lpjml.R`,
`R/water_balance.R`, `R/n_urban.R` and `data-raw/balance_coefficients.R`
untouched, and main changed none of them either.

#579's "the `grid` default reproduces the parquet bit-for-bit" claim is
verified, not trusted: `build_cell_polity()` read from the deployed
parquet is `identical()` across the merge, 68,527 x 5, every column at
max |diff| 0. `grid_area_code` is added only under
`area_key = "polity_area"`, which this branch does not opt into. The
default does now emit a diagnostic warning naming 8 off-bucket grid codes
over 1,069 cells; no test block's warning count moved because of it.

`whep::polities` did not move: `data/polities.rda` is the same blob at the
merge base, at `1c7edf86` and on `origin/main`, and measures 749 rows /
20 columns / 749 distinct codes on both sides of the merge. AM-44's
vintage-dependent area figures therefore stand unchanged.

Block-level control against a pristine `git archive 1c7edf8` run in the
same environment with identical printed env fingerprints: 47 blocks added
and 1 removed, every one of them in the twelve test files main changed
(the removed block is a rename main made in `test_energy_co2_extension.R`);
0 shared blocks changed fail, error or skip; the failing set is identical,
still the 18 deferred `test_polycell_support.R` expectations plus the one
pre-existing HWSD error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@eduaguilera
eduaguilera requested a review from lbm364dl August 7, 2026 04:07
The 18 assertions AM-25 deferred were failing on every CI platform. They
are census and identity pins against the old polity table, not behaviour:
`R CMD check` treats a failing test as a hard ERROR, and the local gate
had been running `rcmdcheck(args = "--no-tests")`, which skips tests
entirely. The producer is untouched.

Blocks 1 and 2 used `GRC-1830-1913`, whose `wiki_status` the #551 re-sync
turned to `superseded`. DA-7's live filter drops it, the build returns 0
rows with no error and no warning, and every assertion then ran against
an absent frame. Re-pinned onto its live successor `GRC-1881-1913`, the
same `cshapes-europe` feature 350 at a later vintage: it reproduces the
defect on all seven originally pinned cells plus 401255 in the Ionian
islands the 1886 polygon adds. 8 terra pieces / 530,378.73 ha / 8.34% of
the polity, corroborated at 530,417.74 ha by a planar clip measured in an
equal-area projection, which shares neither engine with the producer.
Both blocks now assert the liveness precondition, so a future
supersession fails by name instead of cascading.

The two assertions AM-25 flagged as passing vacuously on empty results
are guarded rather than rewritten: the census (63 rows) and a positive
total are pinned first, so the setequal and the sum have something to
compare. Same treatment for the two zero-sums over `outside`.

Block 3's long-edge census moves 42 -> 43 edges, 30 polities either way.
The physical defect is unchanged: the 49th parallel is the same segment
at the same span and bulge, now asserted as one identical triple shared
by all three live Canadian intervals, which a relabelling cannot move.
`CAN-1866-1886`/`CAN-1886-1948`/`CAN-1948-2025` are retired in favour of
`CAN-1870-1886`/`CAN-1886-1949`/`CAN-1949-2025`; that they are retired
and absent from the census is now asserted from the table itself.

Verification: `devtools::test(stop_on_failure = TRUE)` in the offline
job's own configuration FAIL 0; `rcmdcheck()` WITH tests 0/0/1 with
`WHEP_*` unset as on CI, and 1 ERROR / 0 / 1 with `WHEP_HWSD_DIR` set,
that error being the pre-existing #596; `lint_package()` 0 lints; `air`
and `document()` clean. Block-level control against a pristine
`git archive` of 79d1ec3 in the same environment: 2085 blocks both
sides, 0 added, 0 removed, fail 18 -> 0, and the only three blocks that
move at all are the three re-pinned ones. Mutation sweep: control alive,
6 mutants dead, including a 1e-9 relative perturbation of one Canadian
bulge that only the new identical-triple assertion catches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`area_engine` is decided by the platform's s2 build, not by WHEP: s2
validity turns on ULP-level degeneracy, so the clipped pieces the Windows
and Linux runners refuse are read back on macOS ARM64. Eight expectations
pinned the Windows classification and failed there (run 31154665994) --
five in "an unreadable clip piece is measured, never dropped", three in
"an ice layer over an unreadable piece does not abort".

Both blocks now assert the property and never the engine: the piece census
(63 polycells, all 8 recovered cells present, every area finite and
positive), the recovered area against T-A13's engine-independent reference
(planar clip, Lambert azimuthal equal-area, 530,417.74 ha), the partition
re-aggregating to the polity, and the ice confined to its rectangle.
Tolerances are derived, not rounded: from the s2/terra spread measured on
this polity's own 55 readable pieces (+2.23e-04 to +9.88e-04 at latitudes
36.25-39.75 -- the global 0.45-0.86% band does not apply, the
ellipsoid-over-sphere ratio crosses 1 near latitude 35.3) and from the
great-circle bulge of the ice rectangle's edges (271.31 / 92.76 /
476.27 ha of Greek land). Each is stated at its assertion.

The `expect_warning("could not measure")` calls become a link -- the
producer warns exactly when it substituted -- so a platform needing no
substitution is legal while a silent substitution still fails. A new
`pcs_s2_refuses()` helper reads the hazard from `.s2_repair()` rather than
from `area_engine`, so neither block can pass vacuously once no piece is
unreadable.

Tests only; no producer change and no published value changes.

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

Copy link
Copy Markdown
Owner Author

Final post-main acceptance (head 202e5d6, 2026-08-08):

  • Merged origin/main at f8e55e5 and re-pinned PR Refresh the polities snapshot: geometry now loads under s2 #662's polity snapshot: 751 snapshot rows, 692 live polities, 414,479 measured/retained pieces, 21 terra pieces across 9 polities, and zero DA-22 long edges. The exact full clip took 3,843.01 s.
  • The census verifier now gates the checked blob, requested/output code-set identity, coverage/status completeness, engine census, S-A2 exceptions, and the all-runtime terra/s2 residual envelope. Its advertised GRC subset reaches Done with 63 pieces, 4 terra pieces, and 227,311.6 ha.
  • The macOS sf/GEOS failure is closed: missing or unusable intersection idx metadata now falls back to exact per-source intersections. The focused polycell file is green locally with 424 expectations.
  • Final CI is green: format 31221505737, lint 31221506959, offline tests 31221507000, coverage 31221506978, pkgdown 31221506957 (attempt 2), and all five R-CMD-check platforms including macOS 31221506958.

eduaguilera and others added 6 commits August 10, 2026 12:40
…ation)

Reconciles 51 upstream commits (merge base f8e55e5). Four conflicts, each
resolved as a lossless union and verified by diffstat against both parents:

* NAMESPACE  - insertion vs insertion; both exports kept.
  devtools::document() regenerated it with no change, which is what proves
  the ordering.
* NEWS.md    - insertion vs insertion under one heading; both blocks kept,
  main's left where main put it so the next merge does not re-conflict on
  the same seam. 0 deletions either side.
* R/polities.R - upstream extracted .area_year_span_conflicts(), so `spans`
  became that helper's ARGUMENT and is read after the Map(). Our stale
  `spans <- Map(` would have shadowed it and left `long` undefined, so
  upstream's code is taken verbatim. Our DA-24 note is re-homed onto
  .area_year_polity_conflicts(), the declared-span caller where it is true,
  and re-measured: on polities 751 / 0e52f1f the two readings now DISAGREE
  (declared 0 ambiguous pairs, open-end-widened 1 - area 7 at 1975,
  AGO-1975-2025 vs ANG-1905-1975). That is upstream #683. The old note
  claimed they agreed; it was measured on a superseded snapshot.
* tests/testthat/test_run_spatialize.R - append vs append at one seam; both
  blocks kept.

data/polities.rda and data/polity_area_crosswalk.rda are byte-identical
across this merge (0e52f1f), so the polycell census pins already keyed to
"polities 751 / 0e52f1f" are not invalidated and were not re-pinned.

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

The merge was textually clean on these files and semantically red: 4 of the
70 test blocks main added fail against this branch's producer. None is a
regression -- the block control shows 0 shared blocks changing fail/error/
skip -- and none is fixed by weakening this branch's guards.

* test_spatialize.R (x2) and test_spatialize_livestock.R (x1): main's new
  `.warn_grid_missing_reporters()` fixtures build a `country_grid` with no
  share column, which is exactly what C8/S-A5 forbids -- on main a share-less
  grid defaults to `cell_area_frac = 1` and hands a border cell wholly to one
  polity, and `.abort_missing_polity_share()` refuses rather than defaults.
  Each fixture cell is owned outright by the single polity keyed on it, so an
  explicit `cell_area_frac = 1` is the honest value; the missing-reporter
  behaviour under test is unchanged and the guard is untouched.

* test_join_audit.R: main's new whep#669 gate requires every year-free
  territorial join to be classified, and this branch adds one --
  `.carbon_warn_fold()` semi-joins on (lon, lat, area_code). Classified as
  `diagnostic` rather than dodged: both sides of that join are the SAME
  carbon support, already filtered to `.carbon_support_year()`, so there is
  no second year for the key to disagree about, and the join selects which
  polity codes to name in the DA-23 fold warning rather than reaching a
  value. The enumerated cap moves 58 -> 59 with the reason in the row, which
  is the one direction that comment permits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four textual conflicts, all additive: NAMESPACE and NEWS.md concatenate,
R/polities.R takes main's `.area_year_span_conflicts()` extraction with this
branch's DA-24 note moved into the function header, and
test_run_spatialize.R keeps both appended test sections.

Two integration failures neither branch could see on its own:

* main's grid-vintage and re-keyed-grid fixtures carry no share column, which
  C8's `.normalize_country_grid()` now refuses. Every cell in them is held
  whole by one reporting area, so the explicit share is 1.

* main's whep#669 join gate caught `.carbon_warn_fold()`'s `semi_join` on
  (lon, lat, area_code). It is classified `diagnostic`: it names the folded
  polity codes for the warning text alone and cannot reach a number, and both
  sides are the one snapshot `.carbon_support_year()` has already resolved.

`devtools::test()` clean, `lint_package()` 0, `roxygenise()` no drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merges origin/main (753-row polity snapshot) and closes the three things that
kept the default paths from running at all.

THE PIN. `polycell_support` is published at 20260810T165201Z-7caf6 and frozen
in `inst/extdata/whep_inputs.csv`, so `read_polycell_support()` resolves and
the five carbon readers stop aborting. Built from all four layers on the
753-row snapshot; `polity_area_ha == land + inland_water + ice` holds to
1.8e-9 ha over 480,866 rows with no negative area, land at 2015 is 12.9253 Gha
against LUH2's independent 12.9931, and ice is 231.4 Mha, the non-Antarctic
glaciated area.

THE GRID DEFAULT. `.load_country_grid()` takes polycell/centroid/fraction and
defaults to the first, read through `.carbon_cell_support()` so the
spatialization and carbon paths cannot land on different reference years or
folds. `"centroid"` stays selectable to reproduce a published run: it carries
no share, which C8 refuses.

THE CENSUS. Re-measured on the new snapshot rather than adjusted: live rows
692 -> 694, clipped 664 -> 666, polycells 414,479 -> 414,485, all from Aruba
and the Holy See. The terra figures are IDENTICAL to the hectare -- 21
polycells over 9 polities, 1,429,276.70 ha -- which is worth recording because
they were first measured on Windows and re-measured here on Linux.

Publishing the pin then ran four code paths that had never executed:

* `read_luh2_landuse(area_basis = "polycell_land")` -- its real-data test had
  always taken the centroid fallback. `fraction` is a share of the COMPARTMENT
  and sums to 1 per (lon, lat, area_code); the test summed over (lon, lat)
  alone, adding one whole partition per polity of a border cell and reading up
  to 4.0 across 4,603 cells. Equivalent while every cell had one row.

* `.load_country_grid(NULL)` -- pinned by asserting an error while the pin was
  unpublished, so it stopped testing anything once it was. Now mocks the
  support reader and asserts the default reads it.

* main's "unknown country_grid source" used "polycell" as its bogus value,
  which is now the default and would have passed vacuously.

* `data/whep_inputs.rda` regenerated for the frozen version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ared defect

Edu pushed the same four semantic-conflict fixes independently while this
branch was being merged onto the newer main. Both passes reached the same
conclusions, so where they differ this keeps the upstream wording:

* the three spatialize fixtures take his comment, which names
  `.abort_missing_polity_share()` and says the guard is not weakened to
  accommodate the fixture.

* `.carbon_warn_fold`'s baseline row keeps his entry, alphabetically placed.
  BOTH sides added one and they auto-merged as separate adjacent lines, so the
  ratchet read 61 and the gate caught the duplicate.

* the cap stays 60, not his 59: this branch merges a newer main where
  whep#698/#709 had already moved it to 59 on their own, so `.carbon_warn_fold`
  is the sixtieth. His reasoning for the row is kept verbatim.

NEWS.md keeps one copy of the polycell entry; his branch re-added it lower in
the file after this branch had already placed it at the top.

devtools::test() clean, lint_package() 0, roxygenise() no drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread R/polycell_layers.R
if (length(v2) > 0L) {
return(list(path = v2[[1L]], version = "v2", dir = v2_dir))
}
v1 <- c(file.path(glwd_dir, "glwd_3", "hdr.adf"), file.path(glwd_dir, "glwd_3.tif"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[air] reported by reviewdog 🐶

Suggested change
v1 <- c(file.path(glwd_dir, "glwd_3", "hdr.adf"), file.path(glwd_dir, "glwd_3.tif"))
v1 <- c(
file.path(glwd_dir, "glwd_3", "hdr.adf"),
file.path(glwd_dir, "glwd_3.tif")
)

Comment on lines +126 to +127
1L, 1L, 0L, 0L,
1L, 1L, 0L, 0L,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[air] reported by reviewdog 🐶

Suggested change
1L, 1L, 0L, 0L,
1L, 1L, 0L, 0L,
1L,
1L,
0L,
0L,
1L,
1L,
0L,
0L,

Comment on lines +131 to +132
100, 100, 100, 100,
NA, NA, 100, 100,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[air] reported by reviewdog 🐶

Suggested change
100, 100, 100, 100,
NA, NA, 100, 100,
100,
100,
100,
100,
NA,
NA,
100,
100,

…ch's one row

Two conflicts, both in files this branch has now merged four times.

`test_join_audit.R` is the substantive one, and it moves in BOTH directions:
main's ratchet fell to 58 because `.nbx_image_region` left -- the deposited
Schulte-Uebbing crosswalk carries IMAGE membership on the canonical cell key,
so the gridded boundary no longer reaches IMAGE through a year-free country
join. This branch's `.carbon_warn_fold` then puts it at 59. Main's reason for
the fall is kept and this branch's reason for the rise appended, and the cap is
read back off `.territorial_join_baseline()` rather than inferred: it sums to
exactly 59. Arithmetic would have agreed with a duplicated row too, which is
how the previous merge briefly reached 61.

NEWS.md concatenates, as before.

devtools::test() clean, lint_package() 0, roxygenise() regenerates only the
man pages main's own commits touched.

Co-Authored-By: Claude Opus 5 (1M context) <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.

2 participants