Skip to content

Count households once: sum the weight column unweighted - #767

Merged
anth-volk merged 1 commit into
mainfrom
fix/households-weighted-sum
Oct 13, 2026
Merged

anth-volk merged 1 commit into
mainfrom
fix/households-weighted-sum

Conversation

@PavelMakarchuk

Copy link
Copy Markdown
Collaborator

Fixes #766

Summary

budget.households was the sum of squared household weights.

build_budgetary_impact summed household_weight through _sum_output_variable, which calls data[variable].sum(). Here data is the household MicroDataFrame, which is weighted by household_weight, so .sum() on the weight column is Σ w × w. On the sparse US data that is about 10⁴ times the household count, on every run. For example, state/ny 2026 reports 135,597,233,840 households.

The fix:

  • _count_output_entities(simulation, entity, weight) counts entities with a plain sum of the weight column.
  • build_budgetary_impact uses it for households.
  • There is no aggregate fallback, because a weighted frame always holds its weight column, and the aggregate path would weight it again.

The other budget fields are not changed. They are meant to be weighted sums (Σ w × value).

Tests

  • New tests/test_simulation_output_budget.py builds the budget from real MicroDataFrames, as policyengine does. The existing builder test uses plain pd.DataFrames, which is why it never caught this.
  • The new test fails on main with assert 5.0 == 3.0, and it passes with the fix.
  • The same test checks that baseline_net_income, tax_revenue_impact and state_tax_revenue_impact stay weighted sums.
  • uv run pytest tests/ --ignore=tests/integration: 733 passed, 23 skipped.
  • ruff format --check and ruff check are clean on the changed files.
  • I did not run Pyright or the integration tests.

The test file is separate from test_simulation_output_builder.py so that it does not conflict with #765, which reorders that file's imports.

Downstream

policyengine-app-v2 shows the average change per household as -budget.budgetary_impact / budget.households, so that headline is about 10⁴ times too small today. Cached results keep the wrong count until they are recomputed.

🤖 Generated with Claude Code

budget.households summed household_weight through the household
MicroDataFrame, whose columns are already weighted by household_weight,
so it returned the sum of squared weights: about 10^4 times the count on
every run (state/ny 135.6bn). Count entities with a plain sum of the
weight column, and test the budget on a real MicroDataFrame.

Fixes #766

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk

Copy link
Copy Markdown
Contributor

Note: this will fail deployment due to issues around SPM handling in .py, but merging anyway, those are under repair

@anth-volk
anth-volk marked this pull request as ready for review October 13, 2026 13:11
@anth-volk
anth-volk merged commit cdb120a into main Oct 13, 2026
11 checks passed
@anth-volk
anth-volk deleted the fix/households-weighted-sum branch October 13, 2026 13:11
anth-volk added a commit that referenced this pull request Oct 13, 2026
#767 landed on main after this stack branched, and its new test imports
are unsorted under the I rule this PR enables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 13, 2026
#767 landed on main after this stack branched. D401, which this PR
enables, wants each docstring's summary line in the imperative mood.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
The output-builder tests built household frames as plain pd.DataFrames,
which carry no weights, so they did not weight the way production's
MicroDataFrames do. That is how #767 escaped. fixtures/weighted_frames.py
now builds every builder test's entity frames as MicroDataFrames, and
rejects a weight column that is the same on every row.

The golden macro result keeps its budget: its households now weigh 0.5
and 1.5, with values chosen so that only a correctly weighted build
reproduces it. The materialized-columns budget test now expects weighted
totals, derived in the test from an explicit weighted formula; it
expected unweighted sums before, which production never returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
The executor's entity frames are MicroDataFrames weighted by
<entity>_weight, so any column's sum is already weighted. #767 fixed a
weight column aggregated through one, which returned the sum of squared
weights. Four rules now cover that class of mistake:

- sim.microdata.weights-not-values (doc-only): weights, values, labels
  and counts each allow different operations.
- sim.microdata.weights-not-aggregated-as-values: a weight is never the
  variable of a weighted aggregator or frame[<weight>] followed by a
  weighted reduction. Enforced, with no findings.
- sim.microdata.weight-stripping-confined: raw values are read from a
  weighted frame only in simulation_output_common.py. Ratcheted: the UK
  geography builders' plain DataFrame handoff (_output_household) is
  baselined for a decision.
- sim.microdata.builder-tests-use-weighted-frames: the output-builder
  tests build entity frames only through the shared weighted factory.
  Enforced, with no findings.

The AST guards live in the executor's tests, so the executor leg of the
Test job runs them. Each rule's proof uses the real pre-#767 budget line
or the mistake it mirrors. The rationale is in
docs/engineering/skills/microdata-weights.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
budget.households is a plain sum of the household weights. #767's bug
published the sum of squared weights instead, about 10^4 times too high
(135.6 billion for New York). build_budgetary_impact now raises
ImplausibleEntityCountError for a count that is negative, not finite,
or above its country's bound: 200 million US households (about 131
million, US Census Bureau Current Population Survey 2023) or 60 million
UK households (about 28 million, ONS Families and households 2023).
Other countries only need a finite, non-negative count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
The output-builder tests built household frames as plain pd.DataFrames,
which carry no weights, so they did not weight the way production's
MicroDataFrames do. That is how #767 escaped. fixtures/weighted_frames.py
now builds every builder test's entity frames as MicroDataFrames, and
rejects a weight column that is the same on every row.

The golden macro result keeps its budget: its households now weigh 0.5
and 1.5, with values chosen so that only a correctly weighted build
reproduces it. The materialized-columns budget test now expects weighted
totals, derived in the test from an explicit weighted formula; it
expected unweighted sums before, which production never returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
The executor's entity frames are MicroDataFrames weighted by
<entity>_weight, so any column's sum is already weighted. #767 fixed a
weight column aggregated through one, which returned the sum of squared
weights. Four rules now cover that class of mistake:

- sim.microdata.weights-not-values (doc-only): weights, values, labels
  and counts each allow different operations.
- sim.microdata.weights-not-aggregated-as-values: a weight is never the
  variable of a weighted aggregator or frame[<weight>] followed by a
  weighted reduction. Enforced, with no findings.
- sim.microdata.weight-stripping-confined: raw values are read from a
  weighted frame only in simulation_output_common.py. Ratcheted: the UK
  geography builders' plain DataFrame handoff (_output_household) is
  baselined for a decision.
- sim.microdata.builder-tests-use-weighted-frames: the output-builder
  tests build entity frames only through the shared weighted factory.
  Enforced, with no findings.

The AST guards live in the executor's tests, so the executor leg of the
Test job runs them. Each rule's proof uses the real pre-#767 budget line
or the mistake it mirrors. The rationale is in
docs/engineering/skills/microdata-weights.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
budget.households is a plain sum of the household weights. #767's bug
published the sum of squared weights instead, about 10^4 times too high
(135.6 billion for New York). build_budgetary_impact now raises
ImplausibleEntityCountError for a count that is negative, not finite,
or above its country's bound: 200 million US households (about 131
million, US Census Bureau Current Population Survey 2023) or 60 million
UK households (about 28 million, ONS Families and households 2023).
Other countries only need a finite, non-negative count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 14, 2026
#767 landed on main after this stack branched, and its new test imports
are unsorted under the I rule this PR enables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 15, 2026
The output-builder tests built household frames as plain pd.DataFrames,
which carry no weights, so they did not weight the way production's
MicroDataFrames do. That is how #767 escaped. fixtures/weighted_frames.py
now builds every builder test's entity frames as MicroDataFrames, and
rejects a weight column that is the same on every row.

The golden macro result keeps its budget: its households now weigh 0.5
and 1.5, with values chosen so that only a correctly weighted build
reproduces it. The materialized-columns budget test now expects weighted
totals, derived in the test from an explicit weighted formula; it
expected unweighted sums before, which production never returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 15, 2026
The executor's entity frames are MicroDataFrames weighted by
<entity>_weight, so any column's sum is already weighted. #767 fixed a
weight column aggregated through one, which returned the sum of squared
weights. Four rules now cover that class of mistake:

- sim.microdata.weights-not-values (doc-only): weights, values, labels
  and counts each allow different operations.
- sim.microdata.weights-not-aggregated-as-values: a weight is never the
  variable of a weighted aggregator or frame[<weight>] followed by a
  weighted reduction. Enforced, with no findings.
- sim.microdata.weight-stripping-confined: raw values are read from a
  weighted frame only in simulation_output_common.py. Ratcheted: the UK
  geography builders' plain DataFrame handoff (_output_household) is
  baselined for a decision.
- sim.microdata.builder-tests-use-weighted-frames: the output-builder
  tests build entity frames only through the shared weighted factory.
  Enforced, with no findings.

The AST guards live in the executor's tests, so the executor leg of the
Test job runs them. Each rule's proof uses the real pre-#767 budget line
or the mistake it mirrors. The rationale is in
docs/engineering/skills/microdata-weights.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 15, 2026
budget.households is a plain sum of the household weights. #767's bug
published the sum of squared weights instead, about 10^4 times too high
(135.6 billion for New York). build_budgetary_impact now raises
ImplausibleEntityCountError for a count that is negative, not finite,
or above its country's bound: 200 million US households (about 131
million, US Census Bureau Current Population Survey 2023) or 60 million
UK households (about 28 million, ONS Families and households 2023).
Other countries only need a finite, non-negative count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 15, 2026
* Enable ruff group 1 (safe auto-fixes) and fix everything it finds

Select FLY, FURB, I, PIE, PYI, RET, RSE, UP and W, and fix every finding:
ruff's safe fixes (import sorting, pyupgrade), then by hand:
- policyengine-fastapi re-exports JWTDecoder through __all__ and drops two
  unused imports (the 9 F401s noted in Phase 0).
- Two tests build their JSON receipts with json.dumps, not % formatting.
- smoke_app uses min() rather than sorted()[0] (ruff 0.16 FURB192).
scripts/** is linted as Python 3.11, the shared rules tool's minimum, so
import sorting matches policyengine-api byte for byte.

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

* Run ruff check in CI and enforce the lint rule

The lint job now runs ruff check . on the whole repository, and
sim.quality.ruff-lint moves from advisory to enforced with the codes
selected in ruff.toml.

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

* Add a changelog entry for ruff group 1

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

* Sort the imports in the budget output test from #767

#767 landed on main after this stack branched, and its new test imports
are unsorted under the I rule this PR enables.

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

* Sort the imports #743 and #744 added

#743 and #744 landed on main after this stack branched. A dataset-digest
test from #743 and the tomllib import #744 added to the deployment assets
test are unsorted under the I rule this PR enables.

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

* Sort the imports #787 added

#787 landed on main after this stack branched. It renamed the macro result
fixture module to fixtures.macro_output and added fixtures.fake_object_store,
and those imports in the output builder, Stage 12 artifact and Stage 12
runtime tests are unsorted under the I rule this PR enables.

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

* Sort the imports #791 added

#791 landed on main after this stack branched. I, which this PR enables,
flags the import block of its new Stage 12 failure-cause tests; ruff's
safe fix sorts it.

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

---------

Co-authored-by: Anthony Volk <14987227+anth-volk@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 15, 2026
The output-builder tests built household frames as plain pd.DataFrames,
which carry no weights, so they did not weight the way production's
MicroDataFrames do. That is how #767 escaped. fixtures/weighted_frames.py
now builds every builder test's entity frames as MicroDataFrames, and
rejects a weight column that is the same on every row.

The golden macro result keeps its budget: its households now weigh 0.5
and 1.5, with values chosen so that only a correctly weighted build
reproduces it. The materialized-columns budget test now expects weighted
totals, derived in the test from an explicit weighted formula; it
expected unweighted sums before, which production never returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 15, 2026
The executor's entity frames are MicroDataFrames weighted by
<entity>_weight, so any column's sum is already weighted. #767 fixed a
weight column aggregated through one, which returned the sum of squared
weights. Four rules now cover that class of mistake:

- sim.microdata.weights-not-values (doc-only): weights, values, labels
  and counts each allow different operations.
- sim.microdata.weights-not-aggregated-as-values: a weight is never the
  variable of a weighted aggregator or frame[<weight>] followed by a
  weighted reduction. Enforced, with no findings.
- sim.microdata.weight-stripping-confined: raw values are read from a
  weighted frame only in simulation_output_common.py. Ratcheted: the UK
  geography builders' plain DataFrame handoff (_output_household) is
  baselined for a decision.
- sim.microdata.builder-tests-use-weighted-frames: the output-builder
  tests build entity frames only through the shared weighted factory.
  Enforced, with no findings.

The AST guards live in the executor's tests, so the executor leg of the
Test job runs them. Each rule's proof uses the real pre-#767 budget line
or the mistake it mirrors. The rationale is in
docs/engineering/skills/microdata-weights.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 15, 2026
budget.households is a plain sum of the household weights. #767's bug
published the sum of squared weights instead, about 10^4 times too high
(135.6 billion for New York). build_budgetary_impact now raises
ImplausibleEntityCountError for a count that is negative, not finite,
or above its country's bound: 200 million US households (about 131
million, US Census Bureau Current Population Survey 2023) or 60 million
UK households (about 28 million, ONS Families and households 2023).
Other countries only need a finite, non-negative count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 15, 2026
The executor's entity frames are MicroDataFrames weighted by
<entity>_weight, so any column's sum is already weighted. #767 fixed a
weight column aggregated through one, which returned the sum of squared
weights. Four rules now cover that class of mistake:

- sim.microdata.weights-not-values (doc-only): weights, values, labels
  and counts each allow different operations.
- sim.microdata.weights-not-aggregated-as-values: a weight is never the
  variable of a weighted aggregator or frame[<weight>] followed by a
  weighted reduction. Enforced, with no findings.
- sim.microdata.weight-stripping-confined: raw values are read from a
  weighted frame only in simulation_output_common.py. Ratcheted: the UK
  geography builders' plain DataFrame handoff (_output_household) is
  baselined for a decision.
- sim.microdata.builder-tests-use-weighted-frames: the output-builder
  tests build entity frames only through the shared weighted factory.
  Enforced, with no findings.

The AST guards live in the executor's tests, so the executor leg of the
Test job runs them. Each rule's proof uses the real pre-#767 budget line
or the mistake it mirrors. The rationale is in
docs/engineering/skills/microdata-weights.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 15, 2026
budget.households is a plain sum of the household weights. #767's bug
published the sum of squared weights instead, about 10^4 times too high
(135.6 billion for New York). build_budgetary_impact now raises
ImplausibleEntityCountError for a count that is negative, not finite,
or above its country's bound: 200 million US households (about 131
million, US Census Bureau Current Population Survey 2023) or 60 million
UK households (about 28 million, ONS Families and households 2023).
Other countries only need a finite, non-negative count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The output-builder tests built household frames as plain pd.DataFrames,
which carry no weights, so they did not weight the way production's
MicroDataFrames do. That is how #767 escaped. fixtures/weighted_frames.py
now builds every builder test's entity frames as MicroDataFrames, and
rejects a weight column that is the same on every row.

The golden macro result keeps its budget: its households now weigh 0.5
and 1.5, with values chosen so that only a correctly weighted build
reproduces it. The materialized-columns budget test now expects weighted
totals, derived in the test from an explicit weighted formula; it
expected unweighted sums before, which production never returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The executor's entity frames are MicroDataFrames weighted by
<entity>_weight, so any column's sum is already weighted. #767 fixed a
weight column aggregated through one, which returned the sum of squared
weights. Four rules now cover that class of mistake:

- sim.microdata.weights-not-values (doc-only): weights, values, labels
  and counts each allow different operations.
- sim.microdata.weights-not-aggregated-as-values: a weight is never the
  variable of a weighted aggregator or frame[<weight>] followed by a
  weighted reduction. Enforced, with no findings.
- sim.microdata.weight-stripping-confined: raw values are read from a
  weighted frame only in simulation_output_common.py. Ratcheted: the UK
  geography builders' plain DataFrame handoff (_output_household) is
  baselined for a decision.
- sim.microdata.builder-tests-use-weighted-frames: the output-builder
  tests build entity frames only through the shared weighted factory.
  Enforced, with no findings.

The AST guards live in the executor's tests, so the executor leg of the
Test job runs them. Each rule's proof uses the real pre-#767 budget line
or the mistake it mirrors. The rationale is in
docs/engineering/skills/microdata-weights.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
budget.households is a plain sum of the household weights. #767's bug
published the sum of squared weights instead, about 10^4 times too high
(135.6 billion for New York). build_budgetary_impact now raises
ImplausibleEntityCountError for a count that is negative, not finite,
or above its country's bound: 200 million US households (about 131
million, US Census Bureau Current Population Survey 2023) or 60 million
UK households (about 28 million, ONS Families and households 2023).
Other countries only need a finite, non-negative count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The output-builder tests built household frames as plain pd.DataFrames,
which carry no weights, so they did not weight the way production's
MicroDataFrames do. That is how #767 escaped. fixtures/weighted_frames.py
now builds every builder test's entity frames as MicroDataFrames, and
rejects a weight column that is the same on every row.

The golden macro result keeps its budget: its households now weigh 0.5
and 1.5, with values chosen so that only a correctly weighted build
reproduces it. The materialized-columns budget test now expects weighted
totals, derived in the test from an explicit weighted formula; it
expected unweighted sums before, which production never returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The executor's entity frames are MicroDataFrames weighted by
<entity>_weight, so any column's sum is already weighted. #767 fixed a
weight column aggregated through one, which returned the sum of squared
weights. Four rules now cover that class of mistake:

- sim.microdata.weights-not-values (doc-only): weights, values, labels
  and counts each allow different operations.
- sim.microdata.weights-not-aggregated-as-values: a weight is never the
  variable of a weighted aggregator or frame[<weight>] followed by a
  weighted reduction. Enforced, with no findings.
- sim.microdata.weight-stripping-confined: raw values are read from a
  weighted frame only in simulation_output_common.py. Ratcheted: the UK
  geography builders' plain DataFrame handoff (_output_household) is
  baselined for a decision.
- sim.microdata.builder-tests-use-weighted-frames: the output-builder
  tests build entity frames only through the shared weighted factory.
  Enforced, with no findings.

The AST guards live in the executor's tests, so the executor leg of the
Test job runs them. Each rule's proof uses the real pre-#767 budget line
or the mistake it mirrors. The rationale is in
docs/engineering/skills/microdata-weights.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
budget.households is a plain sum of the household weights. #767's bug
published the sum of squared weights instead, about 10^4 times too high
(135.6 billion for New York). build_budgetary_impact now raises
ImplausibleEntityCountError for a count that is negative, not finite,
or above its country's bound: 200 million US households (about 131
million, US Census Bureau Current Population Survey 2023) or 60 million
UK households (about 28 million, ONS Families and households 2023).
Other countries only need a finite, non-negative count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
* Document the simulation contract library for ruff D

Validators, state helpers and URI functions get docstrings. Every Pydantic
model and enum whose class docstring would become a JSON-schema description
carries a D101 noqa instead, so the published contract schemas and the
OpenAPI documents built from them stay byte-identical.

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

* Document the FastAPI support library for ruff D

The ping and health models and the /ping/alive route are mounted in the
executor app, so they keep their existing (OpenAPI-visible) docstrings under
noqa. The no-op observability methods keep their explicit return None under
RET501 noqa, and two stray string statements in the GCP module become the
docstrings they were meant to be.

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

* Document the simulation observability library for ruff D

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

* Document the Stage 12 persistence library for ruff D

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

* Document the simulation entrypoint for ruff D

The entry routes are nested in create_app, so their docstrings (the OpenAPI
descriptions) are untouched and the generated spec is unchanged.

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

* Document the simulation gateway for ruff D

The /ping router endpoint keeps its OpenAPI description under a D205 noqa;
the golden OpenAPI test still passes. Two placeholder pass statements in the
test fixtures' mock exception classes go now that the classes have
docstrings (PIE790).

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

* Document the simulation executor for ruff D

Covers the executor package, the Modal apps, fixtures, scripts and test
docstrings. Docstrings are added to several Modal functions; images copy
src/modal into a layer, so this changes those layers like any source edit.
Protocol stubs keep their ... bodies.

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

* Document the integration tests, repo scripts and package markers for ruff D

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

* Document the Stage 12 schema-DDL proof fixtures for ruff D

The schema-authority proof reads the fixtures as text; the new docstrings
avoid DDL phrases, so the bad fixture is still rejected and the good one
accepted.

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

* Enable ruff group 4 (docstrings)

Select pydocstyle (D) and ignore D203 and D213, the incompatible halves of
the D203/D211 and D212/D213 pairs. Tests stay exempt from D1 only.

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

* Drop redundant return None from documented no-op methods

With a docstring as the body, the bare return None lines are redundant;
remove them instead of suppressing RET501.

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

* Document published contract models and routes

Replace the 61 D101 suppressions on contract, ping and health models with
class docstrings written for API consumers, and fix the /ping/alive and
gateway /ping route docstrings instead of suppressing D205. Pydantic and
FastAPI copy these into schema and operation descriptions, so the entry,
gateway and executor OpenAPI documents gain descriptions; paths, operations,
properties, types and required lists are unchanged. The gateway golden spec
is regenerated with generate_openapi.

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

* Use the Public API's docstrings in the shared rules tool

Both repos documented scripts/rules independently for ruff D, so the
copies drifted apart. Take the Public API's wording so the tool stays
byte-identical in both repos; only docstrings differ.

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

* Register D in the ruff-lint rule

sim.quality.ruff-lint now lists D, and the rendered rules page and
AGENTS.md Rules block are regenerated for the new tool hash.

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

* Put the docstrings #767 added in the imperative mood

#767 landed on main after this stack branched. D401, which this PR
enables, wants each docstring's summary line in the imperative mood.

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

* Put the docstrings #743 and #744 added in the D style

#743 and #744 landed on main after this stack branched. D, which this PR
enables, flags their new code: blank lines after docstrings (D202), a
two-line summary (D205, D209), summaries that are not in the imperative
mood (D401), an escaped backslash in the sentinel's module docstring
(D301), and three public functions without docstrings (D103).

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

* Put the docstrings #789 added in the D style

#789 landed on main after this stack branched. D, which this PR enables,
flags blank lines after its new docstrings (D202) and the undocumented
__call__ of its ConnectionOpener protocol (D102).

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

* Document the shared object store fake #787 added

#787 landed on main after this stack branched. D, which this PR enables,
flags the undocumented __init__ (D107), upload_bytes and read_bytes (D102)
of the in-memory object store it moved into the executor's fixtures.

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

* Put the docstrings #795 added in the D style

D, which this PR enables, flags the undocumented group_label property
(D102), the budget check's run-on summary (D205, D209) and two test
helpers' summaries that are not in the imperative mood (D401).

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

* Put the docstrings #791 added in the D style

#791 landed on main after this stack branched. D, which this PR enables,
flags blank lines after five of its new docstrings (D202) and the
undocumented __init__ (D107) and stage method (D102) of its StageProgress
tracker.

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

* Put the docstrings #758 added in the D style

#758 landed on main after this stack branched. D, which this PR enables,
flags its permission checks and deployed permission test:

- CallerAuthorizer's initializer and authorize now say what enforcement
  does with a caller that lacks a permission, and that the missing
  permission is always recorded;
- the permission_enforcement fixture says when it skips and when it
  raises, and the enforced-permission deployment test says what it
  checks;
- the blank lines after the docstrings of is_client_credentials,
  granted_permissions, the gty validator and the Cloud Run
  deployment-environment test helper go.

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

* Put the docstrings #781 added in the D style

#781 landed on main after this stack branched. D, which this PR enables,
flags its job progress code:

- the JobProgressReporter protocol and each of its methods say which
  stage boundary the runtime reports; the null reporter's methods say
  what they discard, with the docstring as the body;
- StoredJobProgressReporter's initializer, record, start, set_execution,
  enter, start_segments, segment_completed and attempts say how the
  record changes, including when a call changes nothing;
- the contract's job progress store functions say what they read and
  write, and StageReportingSimulation and its ensure say when the
  callback runs;
- the floor and maximum helpers, the Modal reporter factory, the
  published-record helper and the gateway's stage-field helper are in
  the imperative mood, and blank lines after docstrings go.

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

---------

Co-authored-by: Anthony Volk <14987227+anth-volk@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The output-builder tests built household frames as plain pd.DataFrames,
which carry no weights, so they did not weight the way production's
MicroDataFrames do. That is how #767 escaped. fixtures/weighted_frames.py
now builds every builder test's entity frames as MicroDataFrames, and
rejects a weight column that is the same on every row.

The golden macro result keeps its budget: its households now weigh 0.5
and 1.5, with values chosen so that only a correctly weighted build
reproduces it. The materialized-columns budget test now expects weighted
totals, derived in the test from an explicit weighted formula; it
expected unweighted sums before, which production never returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The executor's entity frames are MicroDataFrames weighted by
<entity>_weight, so any column's sum is already weighted. #767 fixed a
weight column aggregated through one, which returned the sum of squared
weights. Four rules now cover that class of mistake:

- sim.microdata.weights-not-values (doc-only): weights, values, labels
  and counts each allow different operations.
- sim.microdata.weights-not-aggregated-as-values: a weight is never the
  variable of a weighted aggregator or frame[<weight>] followed by a
  weighted reduction. Enforced, with no findings.
- sim.microdata.weight-stripping-confined: raw values are read from a
  weighted frame only in simulation_output_common.py. Ratcheted: the UK
  geography builders' plain DataFrame handoff (_output_household) is
  baselined for a decision.
- sim.microdata.builder-tests-use-weighted-frames: the output-builder
  tests build entity frames only through the shared weighted factory.
  Enforced, with no findings.

The AST guards live in the executor's tests, so the executor leg of the
Test job runs them. Each rule's proof uses the real pre-#767 budget line
or the mistake it mirrors. The rationale is in
docs/engineering/skills/microdata-weights.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
budget.households is a plain sum of the household weights. #767's bug
published the sum of squared weights instead, about 10^4 times too high
(135.6 billion for New York). build_budgetary_impact now raises
ImplausibleEntityCountError for a count that is negative, not finite,
or above its country's bound: 200 million US households (about 131
million, US Census Bureau Current Population Survey 2023) or 60 million
UK households (about 28 million, ONS Families and households 2023).
Other countries only need a finite, non-negative count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The output-builder tests built household frames as plain pd.DataFrames,
which carry no weights, so they did not weight the way production's
MicroDataFrames do. That is how #767 escaped. fixtures/weighted_frames.py
now builds every builder test's entity frames as MicroDataFrames, and
rejects a weight column that is the same on every row.

The golden macro result keeps its budget: its households now weigh 0.5
and 1.5, with values chosen so that only a correctly weighted build
reproduces it. The materialized-columns budget test now expects weighted
totals, derived in the test from an explicit weighted formula; it
expected unweighted sums before, which production never returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The executor's entity frames are MicroDataFrames weighted by
<entity>_weight, so any column's sum is already weighted. #767 fixed a
weight column aggregated through one, which returned the sum of squared
weights. Four rules now cover that class of mistake:

- sim.microdata.weights-not-values (doc-only): weights, values, labels
  and counts each allow different operations.
- sim.microdata.weights-not-aggregated-as-values: a weight is never the
  variable of a weighted aggregator or frame[<weight>] followed by a
  weighted reduction. Enforced, with no findings.
- sim.microdata.weight-stripping-confined: raw values are read from a
  weighted frame only in simulation_output_common.py. Ratcheted: the UK
  geography builders' plain DataFrame handoff (_output_household) is
  baselined for a decision.
- sim.microdata.builder-tests-use-weighted-frames: the output-builder
  tests build entity frames only through the shared weighted factory.
  Enforced, with no findings.

The AST guards live in the executor's tests, so the executor leg of the
Test job runs them. Each rule's proof uses the real pre-#767 budget line
or the mistake it mirrors. The rationale is in
docs/engineering/skills/microdata-weights.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
budget.households is a plain sum of the household weights. #767's bug
published the sum of squared weights instead, about 10^4 times too high
(135.6 billion for New York). build_budgetary_impact now raises
ImplausibleEntityCountError for a count that is negative, not finite,
or above its country's bound: 200 million US households (about 131
million, US Census Bureau Current Population Survey 2023) or 60 million
UK households (about 28 million, ONS Families and households 2023).
Other countries only need a finite, non-negative count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The output-builder tests built household frames as plain pd.DataFrames,
which carry no weights, so they did not weight the way production's
MicroDataFrames do. That is how #767 escaped. fixtures/weighted_frames.py
now builds every builder test's entity frames as MicroDataFrames, and
rejects a weight column that is the same on every row.

The golden macro result keeps its budget: its households now weigh 0.5
and 1.5, with values chosen so that only a correctly weighted build
reproduces it. The materialized-columns budget test now expects weighted
totals, derived in the test from an explicit weighted formula; it
expected unweighted sums before, which production never returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The executor's entity frames are MicroDataFrames weighted by
<entity>_weight, so any column's sum is already weighted. #767 fixed a
weight column aggregated through one, which returned the sum of squared
weights. Four rules now cover that class of mistake:

- sim.microdata.weights-not-values (doc-only): weights, values, labels
  and counts each allow different operations.
- sim.microdata.weights-not-aggregated-as-values: a weight is never the
  variable of a weighted aggregator or frame[<weight>] followed by a
  weighted reduction. Enforced, with no findings.
- sim.microdata.weight-stripping-confined: raw values are read from a
  weighted frame only in simulation_output_common.py. Ratcheted: the UK
  geography builders' plain DataFrame handoff (_output_household) is
  baselined for a decision.
- sim.microdata.builder-tests-use-weighted-frames: the output-builder
  tests build entity frames only through the shared weighted factory.
  Enforced, with no findings.

The AST guards live in the executor's tests, so the executor leg of the
Test job runs them. Each rule's proof uses the real pre-#767 budget line
or the mistake it mirrors. The rationale is in
docs/engineering/skills/microdata-weights.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
budget.households is a plain sum of the household weights. #767's bug
published the sum of squared weights instead, about 10^4 times too high
(135.6 billion for New York). build_budgetary_impact now raises
ImplausibleEntityCountError for a count that is negative, not finite,
or above its country's bound: 200 million US households (about 131
million, US Census Bureau Current Population Survey 2023) or 60 million
UK households (about 28 million, ONS Families and households 2023).
Other countries only need a finite, non-negative count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The output-builder tests built household frames as plain pd.DataFrames,
which carry no weights, so they did not weight the way production's
MicroDataFrames do. That is how #767 escaped. fixtures/weighted_frames.py
now builds every builder test's entity frames as MicroDataFrames, and
rejects a weight column that is the same on every row.

The golden macro result keeps its budget: its households now weigh 0.5
and 1.5, with values chosen so that only a correctly weighted build
reproduces it. The materialized-columns budget test now expects weighted
totals, derived in the test from an explicit weighted formula; it
expected unweighted sums before, which production never returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The executor's entity frames are MicroDataFrames weighted by
<entity>_weight, so any column's sum is already weighted. #767 fixed a
weight column aggregated through one, which returned the sum of squared
weights. Four rules now cover that class of mistake:

- sim.microdata.weights-not-values (doc-only): weights, values, labels
  and counts each allow different operations.
- sim.microdata.weights-not-aggregated-as-values: a weight is never the
  variable of a weighted aggregator or frame[<weight>] followed by a
  weighted reduction. Enforced, with no findings.
- sim.microdata.weight-stripping-confined: raw values are read from a
  weighted frame only in simulation_output_common.py. Ratcheted: the UK
  geography builders' plain DataFrame handoff (_output_household) is
  baselined for a decision.
- sim.microdata.builder-tests-use-weighted-frames: the output-builder
  tests build entity frames only through the shared weighted factory.
  Enforced, with no findings.

The AST guards live in the executor's tests, so the executor leg of the
Test job runs them. Each rule's proof uses the real pre-#767 budget line
or the mistake it mirrors. The rationale is in
docs/engineering/skills/microdata-weights.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
budget.households is a plain sum of the household weights. #767's bug
published the sum of squared weights instead, about 10^4 times too high
(135.6 billion for New York). build_budgetary_impact now raises
ImplausibleEntityCountError for a count that is negative, not finite,
or above its country's bound: 200 million US households (about 131
million, US Census Bureau Current Population Survey 2023) or 60 million
UK households (about 28 million, ONS Families and households 2023).
Other countries only need a finite, non-negative count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The output-builder tests built household frames as plain pd.DataFrames,
which carry no weights, so they did not weight the way production's
MicroDataFrames do. That is how #767 escaped. fixtures/weighted_frames.py
now builds every builder test's entity frames as MicroDataFrames, and
rejects a weight column that is the same on every row.

The golden macro result keeps its budget: its households now weigh 0.5
and 1.5, with values chosen so that only a correctly weighted build
reproduces it. The materialized-columns budget test now expects weighted
totals, derived in the test from an explicit weighted formula; it
expected unweighted sums before, which production never returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The executor's entity frames are MicroDataFrames weighted by
<entity>_weight, so any column's sum is already weighted. #767 fixed a
weight column aggregated through one, which returned the sum of squared
weights. Four rules now cover that class of mistake:

- sim.microdata.weights-not-values (doc-only): weights, values, labels
  and counts each allow different operations.
- sim.microdata.weights-not-aggregated-as-values: a weight is never the
  variable of a weighted aggregator or frame[<weight>] followed by a
  weighted reduction. Enforced, with no findings.
- sim.microdata.weight-stripping-confined: raw values are read from a
  weighted frame only in simulation_output_common.py. Ratcheted: the UK
  geography builders' plain DataFrame handoff (_output_household) is
  baselined for a decision.
- sim.microdata.builder-tests-use-weighted-frames: the output-builder
  tests build entity frames only through the shared weighted factory.
  Enforced, with no findings.

The AST guards live in the executor's tests, so the executor leg of the
Test job runs them. Each rule's proof uses the real pre-#767 budget line
or the mistake it mirrors. The rationale is in
docs/engineering/skills/microdata-weights.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
budget.households is a plain sum of the household weights. #767's bug
published the sum of squared weights instead, about 10^4 times too high
(135.6 billion for New York). build_budgetary_impact now raises
ImplausibleEntityCountError for a count that is negative, not finite,
or above its country's bound: 200 million US households (about 131
million, US Census Bureau Current Population Survey 2023) or 60 million
UK households (about 28 million, ONS Families and households 2023).
Other countries only need a finite, non-negative count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The output-builder tests built household frames as plain pd.DataFrames,
which carry no weights, so they did not weight the way production's
MicroDataFrames do. That is how #767 escaped. fixtures/weighted_frames.py
now builds every builder test's entity frames as MicroDataFrames, and
rejects a weight column that is the same on every row.

The golden macro result keeps its budget: its households now weigh 0.5
and 1.5, with values chosen so that only a correctly weighted build
reproduces it. The materialized-columns budget test now expects weighted
totals, derived in the test from an explicit weighted formula; it
expected unweighted sums before, which production never returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The executor's entity frames are MicroDataFrames weighted by
<entity>_weight, so any column's sum is already weighted. #767 fixed a
weight column aggregated through one, which returned the sum of squared
weights. Four rules now cover that class of mistake:

- sim.microdata.weights-not-values (doc-only): weights, values, labels
  and counts each allow different operations.
- sim.microdata.weights-not-aggregated-as-values: a weight is never the
  variable of a weighted aggregator or frame[<weight>] followed by a
  weighted reduction. Enforced, with no findings.
- sim.microdata.weight-stripping-confined: raw values are read from a
  weighted frame only in simulation_output_common.py. Ratcheted: the UK
  geography builders' plain DataFrame handoff (_output_household) is
  baselined for a decision.
- sim.microdata.builder-tests-use-weighted-frames: the output-builder
  tests build entity frames only through the shared weighted factory.
  Enforced, with no findings.

The AST guards live in the executor's tests, so the executor leg of the
Test job runs them. Each rule's proof uses the real pre-#767 budget line
or the mistake it mirrors. The rationale is in
docs/engineering/skills/microdata-weights.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
budget.households is a plain sum of the household weights. #767's bug
published the sum of squared weights instead, about 10^4 times too high
(135.6 billion for New York). build_budgetary_impact now raises
ImplausibleEntityCountError for a count that is negative, not finite,
or above its country's bound: 200 million US households (about 131
million, US Census Bureau Current Population Survey 2023) or 60 million
UK households (about 28 million, ONS Families and households 2023).
Other countries only need a finite, non-negative count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The output-builder tests built household frames as plain pd.DataFrames,
which carry no weights, so they did not weight the way production's
MicroDataFrames do. That is how #767 escaped. fixtures/weighted_frames.py
now builds every builder test's entity frames as MicroDataFrames, and
rejects a weight column that is the same on every row.

The golden macro result keeps its budget: its households now weigh 0.5
and 1.5, with values chosen so that only a correctly weighted build
reproduces it. The materialized-columns budget test now expects weighted
totals, derived in the test from an explicit weighted formula; it
expected unweighted sums before, which production never returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
The executor's entity frames are MicroDataFrames weighted by
<entity>_weight, so any column's sum is already weighted. #767 fixed a
weight column aggregated through one, which returned the sum of squared
weights. Four rules now cover that class of mistake:

- sim.microdata.weights-not-values (doc-only): weights, values, labels
  and counts each allow different operations.
- sim.microdata.weights-not-aggregated-as-values: a weight is never the
  variable of a weighted aggregator or frame[<weight>] followed by a
  weighted reduction. Enforced, with no findings.
- sim.microdata.weight-stripping-confined: raw values are read from a
  weighted frame only in simulation_output_common.py. Ratcheted: the UK
  geography builders' plain DataFrame handoff (_output_household) is
  baselined for a decision.
- sim.microdata.builder-tests-use-weighted-frames: the output-builder
  tests build entity frames only through the shared weighted factory.
  Enforced, with no findings.

The AST guards live in the executor's tests, so the executor leg of the
Test job runs them. Each rule's proof uses the real pre-#767 budget line
or the mistake it mirrors. The rationale is in
docs/engineering/skills/microdata-weights.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 16, 2026
budget.households is a plain sum of the household weights. #767's bug
published the sum of squared weights instead, about 10^4 times too high
(135.6 billion for New York). build_budgetary_impact now raises
ImplausibleEntityCountError for a count that is negative, not finite,
or above its country's bound: 200 million US households (about 131
million, US Census Bureau Current Population Survey 2023) or 60 million
UK households (about 28 million, ONS Families and households 2023).
Other countries only need a finite, non-negative count.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

budget.households is the sum of squared household weights

2 participants