Repository navigation
feat: ADR-049 profile + observer, D5 resolution, and one conserving rounder (#533) - #537
Merged
Merged
Conversation
ADR-049 was accepted 2026-06-28 and decided that geographically imprecise UCDP events should be distributed across their target polygon rather than dumped on a centroid cell, calling that the data factory's recommended behaviour. For four months nothing connected that decision to anything runnable, and its own golden criterion went unchecked while the cell it names grew from 273,076 to 274,358 with nothing going red. A correction first, because I got this wrong twice and it is recorded in two places that are now amended. I claimed ADR-049 "was never implemented". It was. proportional implements the mechanism in full, with 46 unit tests, the builder wired to it, the weight map built whenever the strategy is not passthrough, the GAUL crosswalks present, and ViewpointConfig defaulting to proportional, which honours section 6 as written. A full weight-map build over the 2.73M-row store takes 3.1 seconds. The real gap was narrower. production_parity was the only registered UCDP profile and it overrides the default back to passthrough to reproduce VIEWSER, which section 6 explicitly permits. But that left the recommendation with no name anyone could select, compare or test end to end, and no test asserted either choice, so the ADR and the invoked configuration could drift in silence. That is C-365's mechanism: a decision with no observer. This closes the join without touching any published number. kgi_distributed is registered. It differs from production_parity in exactly one concern, which takes two fields because section 6 frames drop, passthrough and distribute as three options on one mechanism: spatial_distribution_strategy passthrough to proportional, and exclude_where_prec (4, 6) to (). The temporal axis is left identical; one variable at a time. Two consequences of the empty exclusion are named in the profile comment: where_prec 7 and an undocumented where_prec 99 also flow in, both routed to the country polygon. tests/test_adr049_profile_contract.py is the observer. It pins every strategy field of production_parity, that build_viewpoint.py still defaults to it, that the config default still agrees with section 6, that kgi_distributed differs in exactly those two fields, and ADR-049's golden criterion on the real event: the centroid's share collapses, the allocation matches the weight map cell by cell to the rounding tolerance, and best, low and high are each conserved exactly. The mutation drill caught one of my own guards being decorative. Four mutations were applied; three were caught. The survivor concentrated the whole total onto a different single cell, which left cell 148759 at zero and so passed the "the centroid's share fell" assertion. A guard that cannot distinguish distribution from relocation is not a guard for this ADR, so test_allocation_is_proportional_to_the_weight_map was added; it predicts every cell's allocation from its weight. Replaying the mutation catches it, and a subtler one that doubles a single weight is caught too, by that test and by conservation. The measured effect, for the record: cell 148759 is in GAUL admin-1 1301, which has 18 weighted cells, and the cell's own weight is 0.0743. So distribution gives it about 8,423 of the Tigray event's 113,368 instead of all of it, a 13.5x reduction, conserving the total exactly. What this does not do: production still invokes production_parity, and a test now asserts that, so switching cannot happen by diff. Switching would move every published grid-level fatality figure for where_prec >= 4 events and invalidate the ADR-041 digests, and it should wait on evidence that proportional weighting beats uniform allocation by enough to justify its extra assumption, which has never been measured. ruff and mypy clean; 2,443 passed, 16 skipped, 17 xfailed, 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9
) Decision D5 in the allocation dossier. Croicu2026's Multiple Spatial Imputation raised the question of whether a stochastic location model belongs in this layer at all, and the answer turned out to be a governance conflict rather than a judgement call. ADR-014 Principle 1 names the category outright: "New survivorship rules, new temporal distribution methods, new uncertainty models -- these are research outputs, not bugs." Its Principle 4 versioning examples are "v2: Uncertainty-weighted temporal distribution, proportional fatality spread" and "v3: Nowcasting-adjusted with revision-informed confidence". ADR-049's Deferred section said the opposite, placing sampling downstream. Two Accepted ADRs disagreed, which ADR-000 does not permit. Resolved in ADR-014's favour, because the deferral conflated producing an uncertainty representation with consuming one. Producing m realisations is ADR-014 Principle 4's existing model: m versioned viewpoints, no new output shape at this layer. Consuming m is the downstream concern, needing m grids and an ADR-050 contract that can express a realisation axis, and that half of the caution stands. Restated: "sampling belongs downstream" was wrong; "consumers must be able to read m realisations before we emit them" is right. ADR-014 Principle 3 then supplies the admissibility test. A sampler introduces no information absent from the store, since the model is fitted to the store's own precisely-located events, which section 7 already required. But purity holds only if the seed is configuration: a sampler seeded from entropy breaks Principle 3 invisibly, because the same build would produce different output with no recorded cause. So ViewpointConfig.seed is declared, defaulting to None meaning "no stochastic strategy in use", and written to the provenance ledger on every build. It is deliberately unused today -- every registered strategy is deterministic -- because stating the contract before the first sampler arrives is cheaper than retrofitting it, and because a reproducibility record should exist before it is needed rather than after. A stochastic strategy must reject None rather than seed itself. Three tests hold the contract, and the third is a tripwire designed to fail: it asserts no strategy module has acquired randomness, so the first sampler to land turns it red, which is the prompt to thread the seed through. The mutation drill confirmed all three fire -- a sampler landing, a real default seed, and the field's removal. Scope admitted: fitting and sampling with a declared seed. Scope withheld: m > 1, because no consumer has asked. The one request on record asks for a per-cell-month precision flag, not m datasets, and Croicu measures m=1 beating centroid placement in 100% of simulations. Also withheld: building the sampler. This authorises nothing. Measurement, so the next decision is cheaper. A full kgi_distributed build takes 140s and 8.6GB peak RSS, turning 885,075 events into 2,414,552 rows against production_parity's 372,303 -- a 6.5x row increase downstream would absorb. Cell 148759 falls from 274,358 to 39,221 all-history and from 113,395 to 13,866 at month 504. But the largest state-based cell that month is still 41,645, about 36% of it: proportional distribution reduces the concentration 2.7x and does not remove it. A skepticism pre-registered in the dossier was that a remedy might move the spike rather than remove it, and it partly fired before the experiment was designed. That argues for continuing up Gotway2002's change-of-support ladder, not for stopping at ADR-049. If a sampler is ever built: 15,722 dyad-months contain a KGI event and would need a fitted model, and 68,084 KGI events need placing. Only 298 KGI events, 0.4% and 7,981 fatalities, belong to a dyad with no precisely-located events anywhere -- so ADR-049's deferral reason (b) is a fallback case, not a blocker, and the existing uniform fallback covers it. Reason (a), actor normalisation, is partly vindicated: dyad_new_id is always populated, but 122 ids carry more than one name, 75 names carry more than one id, and 15,020 events, 3.4%, change dyad_new_id between store versions. Found while measuring, recorded not fixed: ADR-049 section 8 is only partly implemented. proportional attaches all four required per-row provenance annotations and none of them reach the output parquet. The ledger counters are present and correct; the row annotations are computed and dropped at the write. That is the same defect #533 asks us to close, and the third recorded instance of the pattern after the legacy GedLoader's is_summary flag and event precision at cell-month aggregation, so it should be fixed once as that issue's deliverable rather than incidentally here. Also added, from views-models' measurement: a sixth evaluation metric. One cell is 38.3% of summed CRPS across ten baseline arms, yet excluding it leaves the ordering byte-identical -- because no arm can draw near 113,395, so it enters every arm as nearly the same additive constant. What it does instead is compress the relative spread across arms from 6.5% to 4.0%. The harm is dynamic-range compression, not rank distortion, which is both a weaker claim and a more tractable one. That becomes the primary endpoint for remedy comparison, with earth-mover distance demoted to diagnosing where the mass went. views-models also raised a sound objection to my own recommendation of scale-invariant scoring: their arms differ only in how magnitude is drawn, so scale-invariance may attack the very axis the discrimination lives on. Recorded as a caution with a pre-registered prediction rather than quietly dropped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9
Simon's redirection: working tools, not a research agenda. Everything in
VIEWS is blocked on being able to distribute summary events in time and
space, so this is the foundation for that and the specification of the
rest.
THE DESIGN MOVE. Every allocation strategy is a weight function composed
with one shared rounder:
a_e = LRM(v_e, w_e), w_e(t,c) = tau_e(t) * sigma_e(c)
Conservation (ADR-040 Invariant 1) is then proved once, in the rounder,
rather than re-argued per strategy; a new allocation idea becomes a new
tau or sigma registered through the existing OCP seam with no builder
change; and the two axes compose by multiplication rather than by
sequential re-rounding, which is strictly less rounding loss than
ADR-049 section 4's temporal-then-spatial prescription.
apportionment.py implements largest-remainder (Hamilton) with five
properties, each proved in the docstring and tested: conservation,
boundedness, determinism, zero-preservation, order preservation.
Zero-preservation is the substantive one -- a month or cell with no
supporting evidence never receives mass -- and it is provable: the
shortfall equals a sum of fractional remainders each below one, so it
can never exceed the count of strictly positive remainders. This
replaces ceil_split's inflation by construction.
TWO DEFECTS FOUND AND FIXED.
The existing _distribute_value was not largest-remainder. It ranked by
weight rather than by fractional remainder, which conserves but is not
bounded: for value=10 over weights (0.5, 0.25, 0.25) it returned
(6, 2, 2) although the first cell's ideal share is exactly 5.0. It also
sent the remainder to the already-largest cell every time, which is the
concentration bias ADR-049 lists among its own negative consequences.
The change carries zero parity risk: production_parity selects
passthrough, which never reaches that function.
refresh_pipeline.sh step 3 ran build_viewpoint.py with no --profile, so
production's allocation policy was decided by an argparse default in
another file and a one-line edit elsewhere could have switched what
production emits, silently. The profile is now named explicitly, with a
comment stating why and naming kgi_distributed as the deliberately
not-invoked alternative. Zero behaviour change; production emits exactly
what it did before.
THE SPEC, 11_allocator_spec.md, names what remains: temporal weights T0
endpoint (parity), T1 uniform, T2 activity with a dyad -> conflict ->
side_b -> country -> uniform cascade; spatial weights S0 centroid
(parity), S1 uniform, S2 fatality, S3 declared support. Four
combinations to test, with uniform as the floor any weighting must beat,
and a three-tier test plan with the surprises pre-named.
A THIRD DECLARED SIGNAL, previously unused anywhere in this repo or the
legacy loader. event_clarity == 2 is UCDP stating that the reported
count aggregates several incidents, and it covers 45.56% of all fatality
mass -- more than where_prec and date_prec combined. Crossed with
precision it separates the artefact class from real concentration:
imprecise-and-aggregated is 36.41% of mass and includes all four worked
examples, while precise-and-aggregated is 9.17% and includes Murambi,
40,000 deaths at one memorial site with the location known exactly. That
is real and must never be redistributed. Magnitude cannot tell the two
apart; this field can, which is why views-hydranet and views-models were
both right to refuse winsorising. It belongs to the declaration, not to
the allocator -- allocation is triggered by imprecision alone, and the
existing precision gates already handle that correctly.
ALSO FOUND, recorded not acted on. ADR-049 takes the target polygon from
the centroid's admin-1, but the events say otherwise: the Tigray event's
where_description is "Tigray, Afar, Amhara", three regions, and we
spread it over one. 20.7% of KGI mass has a comma-separated
where_description. If the true support is roughly three times wider than
the polygon we use, that is the size of the residual measured earlier --
cell 148759 fell 7x all-history while the largest cell at month 504
stayed at 41,645. So the support may be wrong, not just the weights, and
weight comparisons would be measuring the wrong thing. Added as S3 and
scoped as a hypothesis to test on top-mass events by hand rather than as
a general free-text parser.
CORRECTIONS TO MY OWN EARLIER CLAIMS, all from measurement.
Denton (1971) does not reject T1 uniform. His objection is to the
identity-matrix penalty when adjusting an existing sub-annual series; with
no indicator series his first-difference penalty reduces to the uniform
allocation. T1 is the correct no-indicator baseline and T2 is the
Chow-Lin case where that literature applies.
The detection critique is withdrawn. I argued the magnitude condition
best >= span was indefensible; measured, it excludes 2,382 events
carrying 2,560 fatalities, 0.06% of mass, median one death over three
months, and on the sources ceil_split actually runs on it is 375 events
and 209 fatalities. The condition is protecting against the inflation
ceil would otherwise cause.
A test name lied. test_remainder_to_highest_weight passed under both
rounding rules because weight and remainder coincide in its case;
renamed, with a discriminating case added.
MUTATION DRILLS. Seven mutations across the new guards. Six caught
immediately. One survived: removing the key tie-break broke nothing,
because sorted is stable and the determinism test had no tied leader.
Fixed with 11 over two equal weights, and now caught.
ruff and mypy clean (90 source files); 2,473 passed, 16 skipped, 17
xfailed, 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9
The working tool. A researcher blocked on summary events can now run:
uv run python scripts/build_viewpoint.py \
--profile kgi_kti_distributed \
--output data/viewpoint/kgi_kti_distributed.parquet
uniform_span is option T1 from the allocator spec: spread an event UCDP
declares temporally imprecise across the months it spans, conserving
exactly. It triggers on the DECLARATION -- date_prec >= 4 and a span
crossing a month boundary -- not on span and magnitude. Both conditions
are needed: date_prec >= 4 within one month needs no allocation, and a
span over several months with date_prec <= 3 is a precisely dated event
straddling a calendar boundary, which is a different problem.
Allocation goes through the shared largest-remainder rounder, so two
things follow by construction rather than by argument. It cannot inflate,
where ceil_split assigns ceil(v/span) to every month and overshoots by a
measured 0.91% across the store. And one death over three months stays
one death in one row, because rows are emitted only where the allocation
is nonzero, bounding output at min(v, span) instead of span.
kgi_kti_distributed differs from production_parity in four fields, and
the load-bearing one is dropping source_distribution_map. Both
production_parity and kgi_distributed route annual-source events to
date_end_only, reproducing the legacy annual loader's
fix_summary_events=False, and that override is the single reason the
Tigray war's whole-year aggregate lands entirely on December 2021.
MEASURED over the full store, 128s, 2,815,165 rows:
cell 148759 / month 504 / sb 113,395 -> 1,180
largest sb cell at month 504 113,395 -> 3,470
that cell's share of the month 98.1% -> 28.0%
cells with mass that month 248 -> 922
event 463131 1 row -> 216 rows (12 x 18)
best conserved 113,368 exactly
its own peak cell-month 113,368 -> 3,471
So the pre-registered surprise -- does a remedy move the spike rather
than shrink it -- is answered for both axes. The earlier
2.7x-not-removal figure was the SPATIAL axis alone; the temporal axis was
carrying most of the remaining concentration, which is what you would
expect of a whole-year aggregate.
Two cautions recorded rather than buried. Row count is 7.6x parity's and
downstream must absorb it. Total mass rises from 3,473,358 to 4,258,880,
which is not a conservation failure but the effect of
exclude_where_prec=() readmitting the where_prec 4 and 6 events parity
drops; those two changes are separable and should be reported separately.
AN UNPREDICTED SURPRISE, and it is UCDP's rather than ours. high < best
for 3,942 events carrying 510,908 fatalities, and it concentrates exactly
where we care: 0.89% of all events, 2.81% of KGI, 5.36% of
event_clarity==2, with 86% of the violating mass in KGI events. Event
463131 reads best 113,368, low 113,368, high 77,825. Attributed upstream
because the same three values appear in every version of the event in the
store and 90.7% of affected ids violate it in all their versions, so our
consolidation is not mixing fields across versions. Also: low == best for
93.4% of events and high == best for 84.9%, so the interval is degenerate
for the large majority.
Consequence: any consumer treating [low, high] as an uncertainty interval
gets a reversed interval for about 3% of KGI events, weighted toward the
largest. The allocator conserves each field independently and so
propagates the inconsistency rather than hiding it, which is correct, but
it has to be declared rather than discovered downstream. Not fixed here;
silently repairing source data would be worse than carrying it.
Mutation drill on uniform_span: ceil instead of the rounder, dropping the
span guard, emitting zero rows, conserving only best. All four caught.
production_parity is untouched and remains what refresh_pipeline.sh
invokes, explicitly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9
) Option T2. The measurement came first: on 20,000 events whose true month is known, with each event removed from its own indicator, activity weighting cut the mean distance from the true month from 4.883 months to 3.305 and raised the share of mass landing in the right month from 0.0825 to 0.1525, beating uniform on 64.9% of events against 22.9% worse. T2 needed a signature change across six registered strategies and that churn is only earned if the extra assumption pays, so scripts/evaluate_temporal_allocation.py ran before any of this was written. The same script is what would falsify the choice. THE SIGNATURE CHANGE, without breaking anything. The temporal registry now carries a typed TemporalStrategy Protocol whose weight_map parameter DEFAULTS to None. Every one of the six strategies gained the parameter and ignores it unless it wants it, which is the arrangement spatial_distribution already uses for passthrough. Result: 196 existing tests pass unchanged. That mattered more than it looks -- these functions are exported, the package is on PyPI, and 28 views-models requirement files depend on it, so a breaking signature change would have rippled outward for no benefit. temporal_weights.py is the temporal mirror of spatial_weights.py. The indicator pool is deliberately strict: where_prec <= 2 AND date_prec <= 2. where_prec == 3 is an admin-2 centroid assignment, precisely coded rather than precisely known, and using it would build the indicator partly out of the artefact the allocator exists to undo. activity_weighted shares uniform_span's trigger and rounder; only the weights differ. The cascade runs dyad, conflict, side_b, country and takes the first level with nonzero activity across the span -- dyad carries 98.6% of cases. With no map or no activity anywhere it falls back to uniform_span, never to date_end_only, because that is the behaviour being replaced. MEASURED, full-store builds to scratch: parity uniform activity cell 148759 / 504 / sb 113,395 1,180 240 month 504 largest cell 113,395 3,470 642 that cell's share 98.1% 28.0% 13.6% month 504 slice ESS 1.04 9.05 20.24 rows 372,303 2,815,165 2,654,111 build time - 128 s 426 s The effective sample size of that month's state-based slice goes from 1.04 to 20.24, a 19x improvement on views-hydranet's own instrument and the thing #533 is actually about. Under parity the score for that slice IS one number; now it is twenty cells' worth. A RESULT NOT TO OVERSELL. The Tigray event's own peak cell-month RISES, 3,471 under uniform to 11,732 under activity. That is the mechanism working. Uniform spreads flat across twelve months; activity concentrates into the months with observed fighting, so the event's internal peak goes up, while the cell-month total at 148759/504 goes down because December 2021 had low observed activity relative to the rest of the span. So activity weighting moves mass to where evidence says it belongs, sometimes raising a peak and sometimes lowering one. Net effect on concentration is strongly positive; monotonic peak reduction is not what it does and should not be claimed. THE LIMITS, pinned in tests rather than left in prose. The gain is in the mean, not the median -- median hit share is 0.00 for uniform and activity alike, because most events are small enough to land wholly in one month and usually the wrong one, so this helps the typical fatality rather than the typical event. And a zero-activity month receives zero, by the rounder's zero-preservation property, which says "no observed fighting, so invent none". That is the assumption most likely to be wrong: absence of well-located reporting is not absence of fighting, and 1,760 KTI events have at least one zero-activity month inside their span. A test pins it so changing it has to be a decision. Two incidental confirmations. Conservation held across both new allocators, total best identical at 4,258,880. And activity emits FEWER rows than uniform, 2,654,111 against 2,815,165, because zero-activity months are not emitted -- zero-preservation pays for itself in output size as well as in honesty. Cost: activity is 3.3x slower to build, from the indicator scan and the per-event cascade. Inside a monthly budget, but worth knowing before anyone runs it twenty times for a multiple-imputation ensemble. Mutation drill on activity_weighted: falling back to date_end_only, reversing the cascade, ignoring activity, dropping the span guard. All four caught. ruff and mypy clean (91 source files); 2,509 passed, 16 skipped, 17 xfailed, 0 failed. production_parity untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the three P0 governance items from the allocation dossier and lays the foundation for the
tools. Production emits exactly what it did before — every change is a new profile, a new
module, or a test.
Three commits:
3a88a2c(ADR-049 gets a profile and an observer),02f0d44(D5: sampling is aviewpoint concern),
b1af0f4(one conserving rounder + the allocator spec).Why this exists
Every effort in VIEWS that scores
lr_sbon an affected window is blocked by one cell holding awhole year of the Tigray war. The programme behind it was becoming a research agenda; this is the
turn back toward working tools.
The design move
Every allocation strategy is a weight function composed with one shared rounder:
Conservation (ADR-040 Invariant 1) is proved once, in the rounder, instead of re-argued per
strategy. A new allocation idea becomes a new
τorσregistered through the existing OCP seamwith no builder change. And the two axes compose by multiplication rather than sequential
re-rounding — strictly less rounding loss than ADR-049 §4's temporal-then-spatial prescription.
apportionment.pyimplements largest-remainder with five properties, each proved in the docstringand tested: conservation · boundedness · determinism · zero-preservation · order preservation.
Zero-preservation is the substantive one — a month or cell with no supporting evidence never
receives mass — and it's provable: the shortfall is a sum of fractional remainders each below 1, so
it can never exceed the count of strictly positive remainders. This replaces
ceil_split'sinflation by construction.
Two defects found and fixed
1. The existing rounder wasn't largest-remainder. It ranked by weight, not fractional
remainder — for
v=10over(0.5, 0.25, 0.25)it returned(6, 2, 2)when cell 1's ideal share isexactly 5.0. Conservation held; boundedness didn't. And ranking by weight sends the surplus to
the already-largest cell every time, which is the concentration bias ADR-049 lists among its own
negative consequences. Zero parity risk:
production_parityselectspassthrough, which neverreaches that function.
2.
refresh_pipeline.shdidn't say which profile production runs. Step 3 calledbuild_viewpoint.pywith no--profile, so production's allocation policy came from an argparsedefault in another file — a one-line edit elsewhere could have switched what production emits,
silently. Now named explicitly, with two guards. Zero behaviour change.
⭐ A third declared signal, unused anywhere — including in the legacy loader
event_clarity == 2is UCDP stating that the reported count aggregates several incidents. Itcovers 45.56% of all fatality mass — more than
where_precanddate_preccombined. Crossedwith precision it separates the artefact class from real concentration:
clarity=2clarity=1All four worked examples are in A. B must never be touched:
Forty thousand deaths at one memorial site, April 1994, location known exactly. Magnitude cannot
distinguish Murambi from Tigray; this field can — which is why views-hydranet and views-models
each independently refused to winsorise. It belongs to the declaration, not the allocator:
allocation is triggered by imprecision alone, and the existing precision gates already handle that.
ADR-049 takes the target polygon from the centroid's admin-1. The events say otherwise:
We spread a three-region event over one region; 20.7% of KGI mass has a comma-separated
where_description. If the true support is ~3× wider, that is the size of the residual alreadymeasured — cell 148759 fell 7× all-history while the largest cell at month 504 stayed at 41,645.
So weight comparisons may be measuring the wrong thing. Added as option S3, scoped as a
hypothesis to check on top-mass events by hand rather than a general free-text parser.
Corrections to my own earlier claims, all from measurement
T1 uniform. His objection is to the identity-matrix penaltywhen adjusting an existing sub-annual series; with no indicator his first-difference penalty
reduces to uniform. T1 is the correct no-indicator baseline; T2 is the Chow–Lin case.
best >= spanwas indefensible. Measured, itexcludes 2,382 events / 2,560 fatalities / 0.06% of mass (median: one death over three
months), and on the sources
ceil_splitactually runs on, 375 events / 209 fatalities. Thecondition protects against the inflation
ceilwould otherwise cause.test_remainder_to_highest_weightpassed under both rounding rules,because weight and remainder coincide in its case. Renamed, discriminating case added.
Mutation drills
Seven mutations across the new guards. Six caught immediately. One survived — removing the key
tie-break broke nothing, because
sortedis stable and the determinism test had no tied leader.Fixed with
v=11over two equal weights; now caught. (That's the second decorative guard of minethe drill has caught, which is the argument for running it every time.)
What's deliberately not here
production_paritystays byte-identical and remains what the pipeline invokes — now explicitly.kgi_distributedexists and is invoked by nobody. No sampler is built. No published figure moves.Verification
ruffclean ·mypyclean (90 source files) · 2,473 passed · 16 skipped · 17 xfailed · 0 failed🤖 Generated with Claude Code
https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9