diff --git a/changelog.d/sehi-farm-partnership-attribution.changed.md b/changelog.d/sehi-farm-partnership-attribution.changed.md new file mode 100644 index 0000000000..4bf527f00a --- /dev/null +++ b/changelog.d/sehi-farm-partnership-attribution.changed.md @@ -0,0 +1 @@ +The US self-employed health premium attribution (the `other_health_insurance_premiums` stage) now covers people whose only profitable business is a Schedule F farm (`farm_operations_income`) or a partnership's Schedule K-1 box 14 code A earnings (`partnership_self_employment_net_earnings`), not only Schedule C. Each business qualifies on its own, so a loss in one never cancels another's profit; the Medicare-proxy and measured-ESI exclusions are unchanged. This matches the per-business Form 7206 limit in PolicyEngine/policyengine-us#10120. diff --git a/docs/evidence/spec-engine/us-f0-coverage.json b/docs/evidence/spec-engine/us-f0-coverage.json index 0fabd95a86..49888d0e9d 100644 --- a/docs/evidence/spec-engine/us-f0-coverage.json +++ b/docs/evidence/spec-engine/us-f0-coverage.json @@ -4,9 +4,9 @@ "version": 1 }, "country": "us", - "documentation_sha256": "0931cbd3aae2abcd8c072b78eba40c1c006b1bf7fa23c6a6681f9a895e47070e", + "documentation_sha256": "38828bde202269e0d60fe159e7ddee6d73cdc272eb208a19ad07576952abd39b", "field_usage": { - "authored_normative_field_count": 32404, + "authored_normative_field_count": 32406, "claim_count": 49, "claims": [ { @@ -514,8 +514,8 @@ ], "mode": "legacy_behavior", "pointer_class": "all", - "pointer_count": 1716, - "pointer_sha256": "3e808ed9f5a791c0dfb85be0c59adf803366bf6e8c7a9f5a58f92974e044b607", + "pointer_count": 1718, + "pointer_sha256": "ce9297355f6d931d55ceee220d9a8bd84f73c571c7254af95f83865a05183867", "rationale": null, "relative_sink_prefix": "/source_manifest/stages", "source_prefix": "/authored/spec~1sources.yaml/stages", @@ -772,20 +772,20 @@ "verifier": "vintages" } ], - "configuration_field_count": 42184, - "consumed_field_count": 42184, + "configuration_field_count": 42186, + "consumed_field_count": 42186, "generation0_effect_counts": { - "legacy_behavior": 38498, + "legacy_behavior": 38500, "no_generation0_effect": 3686 }, "mode_counts": { "compiler_semantic": 27717, "front_end_validation": 354, "identity_only": 103, - "legacy_behavior": 14010 + "legacy_behavior": 14012 }, "multiple_primary_use_field_count": 0, - "pointer_inventory_sha256": "8f655670b9794adae905ede64bdceba6395eeb8d642b6c5896d5c516f1f6a6a7", + "pointer_inventory_sha256": "11e11693ab6b89b1008c3fb69d933b3dbebcb61d49eeba0d62f9edb776ba01cb", "resolved_binding_field_count": 9780, "unused_field_count": 0 }, @@ -1361,12 +1361,12 @@ ], "expected": { "producer_count": 38, - "sha256": "cc34ba11288d01145fc2dc1a669df3f7c4ef2019d73afb02dd8eeee852b16d20" + "sha256": "d5be3aafad998f0a3445cc08a6f8555d373ed983353d628634e4b2ab85edfc61" }, "failures": [], "observed": { "producer_count": 38, - "sha256": "cc34ba11288d01145fc2dc1a669df3f7c4ef2019d73afb02dd8eeee852b16d20" + "sha256": "d5be3aafad998f0a3445cc08a6f8555d373ed983353d628634e4b2ab85edfc61" }, "status": "covered" }, @@ -1656,13 +1656,13 @@ "compiler_ir.node_slices" ], "expected": { - "map_sha256": "b4d6ec9c11ce323d83319a3d0289e0cf7216a7936d6c6f08cef0a463ad369641", - "protocol_sha256": "114217a2918329b0601d94deb8ed83f73919584b85ca8aabe45a40665afb6560" + "map_sha256": "fd964b17be1461f4a5bb14954a8933d1ecfc121330ea222e2401e6bffc98b6b0", + "protocol_sha256": "bd26d3022e82f86714e735f69effc7a92dcc35c011fb71ab50c7f4d43b9b20fd" }, "failures": [], "observed": { - "map_sha256": "b4d6ec9c11ce323d83319a3d0289e0cf7216a7936d6c6f08cef0a463ad369641", - "protocol_sha256": "114217a2918329b0601d94deb8ed83f73919584b85ca8aabe45a40665afb6560" + "map_sha256": "fd964b17be1461f4a5bb14954a8933d1ecfc121330ea222e2401e6bffc98b6b0", + "protocol_sha256": "bd26d3022e82f86714e735f69effc7a92dcc35c011fb71ab50c7f4d43b9b20fd" }, "status": "covered" }, @@ -1677,7 +1677,7 @@ "compiler_ir.seed_stream_map" ], "expected": { - "implementation_sha256": "114217a2918329b0601d94deb8ed83f73919584b85ca8aabe45a40665afb6560", + "implementation_sha256": "bd26d3022e82f86714e735f69effc7a92dcc35c011fb71ab50c7f4d43b9b20fd", "protocol": "legacy-v1", "streams": [ "build_model", @@ -1698,7 +1698,7 @@ }, "failures": [], "observed": { - "implementation_sha256": "114217a2918329b0601d94deb8ed83f73919584b85ca8aabe45a40665afb6560", + "implementation_sha256": "bd26d3022e82f86714e735f69effc7a92dcc35c011fb71ab50c7f4d43b9b20fd", "protocol": "legacy-v1", "streams": [ "build_model", @@ -1775,12 +1775,12 @@ "legacy_adapter.imputation.late_producer_resource_semantics" ], "expected": { - "sha256": "f4f8bfeb79268a4043a843b47cec1d1c0e58e9f5dd6bc23417ada95446dd7b8e", + "sha256": "1db3c553867432ceeed99c0bc805bb05d56c28a14fbbb537407403660c4e22e0", "stages": 37 }, "failures": [], "observed": { - "sha256": "f4f8bfeb79268a4043a843b47cec1d1c0e58e9f5dd6bc23417ada95446dd7b8e", + "sha256": "1db3c553867432ceeed99c0bc805bb05d56c28a14fbbb537407403660c4e22e0", "stages": 38 }, "status": "covered" @@ -1887,7 +1887,7 @@ "alpha", "zeta" ], - "sha256": "3db095ce2d1ef89028d3862f0edc827d704f099c5b3323f9b115642a3c27e007" + "sha256": "1980cde7a84ed8d1ff9277e2d95932e3b3b3fca3ae2812aebc00d8ba8c6e517e" }, "failures": [], "observed": { @@ -1911,7 +1911,7 @@ "alpha", "zeta" ], - "sha256": "3db095ce2d1ef89028d3862f0edc827d704f099c5b3323f9b115642a3c27e007" + "sha256": "1980cde7a84ed8d1ff9277e2d95932e3b3b3fca3ae2812aebc00d8ba8c6e517e" }, "status": "covered" }, @@ -1975,7 +1975,7 @@ "take_up_contract", "us_qbi_reconciliation_contract" ], - "sha256": "61a5484eab9fae1d6651b3ae15f1793d4934ea2f6ed2c08d05716b745c221014" + "sha256": "5460d64d5204c4be0fefa7e049fb5adc6f99d72c308b50098deb5efe9708cbeb" }, "status": "covered" }, @@ -2599,7 +2599,7 @@ "country": "us", "schema_id": "country_spec", "schema_version": 1, - "spec_sha256": "f76f921d4f74392b106b8c0729ce250494e0a55455eaba7b910b19cdf7825178" + "spec_sha256": "9550605da8976e20d07a8c453b091d825a36020fe34b34a65f8724043d0b5f1c" } }, "report_schema_version": 3, @@ -2609,7 +2609,7 @@ "country": "us", "schema_id": "country_spec", "schema_version": 1, - "spec_sha256": "f76f921d4f74392b106b8c0729ce250494e0a55455eaba7b910b19cdf7825178" + "spec_sha256": "9550605da8976e20d07a8c453b091d825a36020fe34b34a65f8724043d0b5f1c" }, "status": "pass" } diff --git a/packages/microcosm-build/src/microcosm/build/spec_engine/field_usage.py b/packages/microcosm-build/src/microcosm/build/spec_engine/field_usage.py index 56f39195cf..81d7634473 100644 --- a/packages/microcosm-build/src/microcosm/build/spec_engine/field_usage.py +++ b/packages/microcosm-build/src/microcosm/build/spec_engine/field_usage.py @@ -26,9 +26,9 @@ ) from .schemas import load_schema_registry -EXPECTED_AUTHORED_FIELD_COUNT = 32_404 +EXPECTED_AUTHORED_FIELD_COUNT = 32_406 EXPECTED_RESOLVED_BINDING_FIELD_COUNT = 9_780 -EXPECTED_CONFIGURATION_FIELD_COUNT = 42_184 +EXPECTED_CONFIGURATION_FIELD_COUNT = 42_186 class FieldUsageError(AssertionError): @@ -485,8 +485,8 @@ def _path_inventory(rows: Sequence[tuple[str, object]]) -> tuple[int, str]: "d6782c5de5bbed1bdc6bf653c4a6d4aadcad4ccc72d35e1092e130fcb04680a3", ), "source_stages": ( - 1_716, - "3e808ed9f5a791c0dfb85be0c59adf803366bf6e8c7a9f5a58f92974e044b607", + 1_718, + "ce9297355f6d931d55ceee220d9a8bd84f73c571c7254af95f83865a05183867", ), "spine_assembly_household_mass_shares": ( 2, diff --git a/packages/microcosm-build/src/microcosm/build/spec_engine/inventory_coverage.py b/packages/microcosm-build/src/microcosm/build/spec_engine/inventory_coverage.py index 673ab08d35..5cb72e0b71 100644 --- a/packages/microcosm-build/src/microcosm/build/spec_engine/inventory_coverage.py +++ b/packages/microcosm-build/src/microcosm/build/spec_engine/inventory_coverage.py @@ -350,18 +350,18 @@ "acs_person_predictors": "878c788a6f037d7aca12b3586ea034eff04f3034ffa11935a736493042551f25", "authority": "9d4a9672a0f03039b1fe874b9fe21ed575be0d29f14afc396d03cdf5c809bdd2", "early_families": "4aa9f736fd76e83955477ad1667e58f48f264783f05bdc7f0102cd32d61323bd", - "full_checkpoint": "3db095ce2d1ef89028d3862f0edc827d704f099c5b3323f9b115642a3c27e007", + "full_checkpoint": "1980cde7a84ed8d1ff9277e2d95932e3b3b3fca3ae2812aebc00d8ba8c6e517e", "gap_fill_schedule": "1c31f9868f7884347cc19cf1ff65da43f950b9114941a715bab168246db414a7", "graph_nodes": "40cd51ffdfe2e9d9d08d48c08e8ded9de1e4b134783bab05c4abc6ad5c72ca1e", "geography_assignment": "44e7f5f6cd7ceeef85d532c3d46306d03de4f4ee811376e6d29920742746b65c", "late_families": "d91f9ff0eb52f43e7b6eed3d5c58c37abe1620c3a11021da15dae9c10e16d382", - "late_resource_semantics": "cc34ba11288d01145fc2dc1a669df3f7c4ef2019d73afb02dd8eeee852b16d20", + "late_resource_semantics": "d5be3aafad998f0a3445cc08a6f8555d373ed983353d628634e4b2ab85edfc61", "late_schedule": "e59c019d3d454eac99ac0ac209b6c5b6faaf9bdfcaeee18c36a25be19bf7da2f", "ownership": "5f64f0aac49e2313177564f71876bffc8c81b3ded4df701e70930e60e9c98356", "primary_tuples": "fdf23da429f8c501e198a2f5b719763d523a41565c2153fdf17b0344f21bcd7c", - "seed_map": "b4d6ec9c11ce323d83319a3d0289e0cf7216a7936d6c6f08cef0a463ad369641", - "seed_protocol": "114217a2918329b0601d94deb8ed83f73919584b85ca8aabe45a40665afb6560", - "source_manifest": "f4f8bfeb79268a4043a843b47cec1d1c0e58e9f5dd6bc23417ada95446dd7b8e", + "seed_map": "fd964b17be1461f4a5bb14954a8933d1ecfc121330ea222e2401e6bffc98b6b0", + "seed_protocol": "bd26d3022e82f86714e735f69effc7a92dcc35c011fb71ab50c7f4d43b9b20fd", + "source_manifest": "1db3c553867432ceeed99c0bc805bb05d56c28a14fbbb537407403660c4e22e0", "take_up": "9522ce40f7dea569312dd7a7beb474e5a5afadd2f1e10cb5876534c3ef623d35", "tail": "ac92829c88a1a4fb6460d61190918d5d99c6c377fc8dd8f62f02b332d09bf59c", } diff --git a/packages/microcosm-build/src/microcosm/build/spec_engine/schema/sources.schema.json b/packages/microcosm-build/src/microcosm/build/spec_engine/schema/sources.schema.json index 839249b199..b535618ca0 100644 --- a/packages/microcosm-build/src/microcosm/build/spec_engine/schema/sources.schema.json +++ b/packages/microcosm-build/src/microcosm/build/spec_engine/schema/sources.schema.json @@ -765,14 +765,21 @@ "output_self_employed_flag": { "type": "string" }, + "plan_business_income_sources": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + } + }, "reported_source": { "type": "string" - }, - "self_employment_income_sources": { - "type": "array", - "items": { - "type": "string" - } } }, "required": [ @@ -783,8 +790,8 @@ "medicare_ssdi_source", "output_premiums", "output_self_employed_flag", - "reported_source", - "self_employment_income_sources" + "plan_business_income_sources", + "reported_source" ] }, { diff --git a/packages/microcosm-build/src/microcosm/build/us/release_input_coverage_manifest.json b/packages/microcosm-build/src/microcosm/build/us/release_input_coverage_manifest.json index 0e8c171624..b3aa45b5c3 100644 --- a/packages/microcosm-build/src/microcosm/build/us/release_input_coverage_manifest.json +++ b/packages/microcosm-build/src/microcosm/build/us/release_input_coverage_manifest.json @@ -154,7 +154,7 @@ "status": "required" }, "health_insurance_premiums": { - "note": "Self-employed premium attribution leaf (PolicyEngine/microcosm#451 item 2): the deterministic attribution operation of the other_health_insurance_premiums release stage copies the reported non-Part-B premium onto this person input for strictly-positive Schedule C people outside the Medicare proxy, which is the only channel populating the section 162(l) self-employed health ALD (SOI Pub 1304 Table 1.4 TY2023: 3,595,764 returns / $31.23B, ledger#105). Absent on pre-attribution artifacts by construction; any new release produces it.", + "note": "Self-employed premium attribution leaf (PolicyEngine/microcosm#451 item 2): the deterministic attribution operation of the other_health_insurance_premiums release stage copies the reported non-Part-B premium onto this person input for people with a strictly positive Schedule C, Schedule F or partnership (Schedule K-1 box 14 code A) business who are outside the Medicare proxy and measured employer coverage, which is the only channel populating the section 162(l) self-employed health ALD (SOI Pub 1304 Table 1.4 TY2023: 3,595,764 returns / $31.23B, ledger#105). Absent on pre-attribution artifacts by construction; any new release produces it.", "status": "required" }, "health_insurance_premiums_without_medicare_part_b": { @@ -237,7 +237,7 @@ "status": "required" }, "is_self_employed": { - "note": "Gate flag for the self-employed premium attribution (PolicyEngine/microcosm#451 item 2): opens the defined_for gate on the engine's self_employed_health_insurance_premiums adds-aggregation for every strictly-positive Schedule C person. Written by the same attribution operation as health_insurance_premiums.", + "note": "Gate flag for the self-employed premium attribution (PolicyEngine/microcosm#451 item 2): opens the defined_for gate on the engine's self_employed_health_insurance_premiums adds-aggregation for every person whose Schedule C, Schedule F or partnership net earnings are strictly positive, each business on its own. Written by the same attribution operation as health_insurance_premiums.", "status": "required" }, "is_separated": { diff --git a/packages/microcosm-build/src/microcosm/build/us/source_stages.json b/packages/microcosm-build/src/microcosm/build/us/source_stages.json index 72b56c12ab..0e9073e959 100644 --- a/packages/microcosm-build/src/microcosm/build/us/source_stages.json +++ b/packages/microcosm-build/src/microcosm/build/us/source_stages.json @@ -2914,10 +2914,18 @@ { "kind": "attribute_self_employed_health_premiums", "reported_source": "health_insurance_premiums_without_medicare_part_b", - "self_employment_income_sources": [ - "self_employment_income_before_lsr", - "sstb_self_employment_income_before_lsr" - ], + "plan_business_income_sources": { + "schedule_c": [ + "self_employment_income_before_lsr", + "sstb_self_employment_income_before_lsr" + ], + "schedule_f": [ + "farm_operations_income" + ], + "partnership": [ + "partnership_self_employment_net_earnings" + ] + }, "medicare_age_source": "age", "medicare_age_threshold": 65, "medicare_ssdi_source": "social_security_disability", @@ -2937,7 +2945,7 @@ "other_health_insurance_premiums", "health_insurance_premiums" ], - "notes": "After ACA and Medicaid take-up inputs are materialized, the retired annual residual is max(PHIP_VAL - chip_premium - marketplace_net_premium - medicaid_premium, 0), with tax-unit modeled premiums assigned to the first person (archived commit 42ed5d45c56df80d754fbe24cce21cfeb8d05cbe, datasets/cps/cps.py lines 828-944). The archived CPS-only output list and exact eight-predictor, at-most-5,000-person joint QRF are extended_cps.py lines 135-194, 234-248, and 639-745, with only the PUF support half replaced at lines 1014-1076. Because the archive predicted the two PUF leaves jointly without clipping one to the other, PUF-only residual-order exceedances remain diagnostics; measured ASEC rows must preserve the exact residual identity. The archive did not explicitly pin tree count or weights; Microcosm pins 100 trees and typed person weights as reproducibility hardening. The final attribution operation (PolicyEngine/microcosm#451 item 2) is deterministic: it copies the finished reported non-Part-B premium onto the person health_insurance_premiums input exactly where combined Schedule C income (self_employment_income_before_lsr + sstb_self_employment_income_before_lsr) is strictly positive, the person is outside the Medicare proxy (age 65+ or any Social Security disability income), and the person is outside measured employer-sponsored coverage (has_esi - the conservative, incomplete proxy for the 26 USC 162(l)(2)(B) subsidized-employer-plan month exclusion; eligibility through a spouse's or dependent's employer is not measured and remains documented residual overbreadth), and marks is_self_employed for every strictly-positive Schedule C person. PolicyEngine-US 2.2.1 then computes self_employed_health_insurance_premiums (a formula-owned adds-aggregation gated by is_self_employed, so it cannot ship as a column) and the section 162(l) ALD as min(total_self_employment_income, premiums). The Medicare proxy keeps the statutory medical-expense premium concept numerically invariant because the direct input equals the decomposed reported premium whenever the modeled Part B add-on is zero. Beyond the federal ALD, PolicyEngine-US 2.2.1 reads the bare health_insurance_premiums input in five state/local formulas (MO qualified-premium subtraction, MI household resources, OH uninsured unreimbursed medical expenses, CA Orange County general relief countable income, MO child-care-subsidy adjusted income): attribution moves those programs from premiums-invisible toward the measured premium structure for the self-employed slice, outside the federal income_tax probe's scope. External anchor class: IRS SOI Pub 1304 Table 1.4 TY2023 self-employed health insurance deduction, 3,595,764 returns / $31.23B (ledger#105, buildn v9.2 feed)." + "notes": "After ACA and Medicaid take-up inputs are materialized, the retired annual residual is max(PHIP_VAL - chip_premium - marketplace_net_premium - medicaid_premium, 0), with tax-unit modeled premiums assigned to the first person (archived commit 42ed5d45c56df80d754fbe24cce21cfeb8d05cbe, datasets/cps/cps.py lines 828-944). The archived CPS-only output list and exact eight-predictor, at-most-5,000-person joint QRF are extended_cps.py lines 135-194, 234-248, and 639-745, with only the PUF support half replaced at lines 1014-1076. Because the archive predicted the two PUF leaves jointly without clipping one to the other, PUF-only residual-order exceedances remain diagnostics; measured ASEC rows must preserve the exact residual identity. The archive did not explicitly pin tree count or weights; Microcosm pins 100 trees and typed person weights as reproducibility hardening. The final attribution operation (PolicyEngine/microcosm#451 item 2) is deterministic: it copies the finished reported non-Part-B premium onto the person health_insurance_premiums input exactly where some business a plan can be established under has strictly positive net earnings on its own (26 USC 162(l)(2)(A); Form 7206 instructions, 2023, p. 1): Schedule C (self_employment_income_before_lsr + sstb_self_employment_income_before_lsr), a Schedule F farm (farm_operations_income) or a partnership's Schedule K-1 box 14 code A earnings (partnership_self_employment_net_earnings), so a loss in one business never cancels another's profit; the person is outside the Medicare proxy (age 65+ or any Social Security disability income), and the person is outside measured employer-sponsored coverage (has_esi - the conservative, incomplete proxy for the 26 USC 162(l)(2)(B) subsidized-employer-plan month exclusion; eligibility through a spouse's or dependent's employer is not measured and remains documented residual overbreadth), and marks is_self_employed for every person with such a business. The other two Form 7206 paths, the Schedule SE optional methods and more-than-2% S corporation shareholders, have no measured input. PolicyEngine-US then computes self_employed_health_insurance_premiums (a formula-owned adds-aggregation gated by is_self_employed, so it cannot ship as a column) and the section 162(l) ALD as premiums capped at earned income. Version 2.2.1 caps at Schedule C income alone (min(max(total_self_employment_income, 0), premiums)), so the people a farm or partnership alone qualifies receive no federal deduction until the engine carries PolicyEngine/policyengine-us#10120, which computes the Form 7206 limit for each business and takes the largest. On PUF-support people the attributed premium is the joint QRF's prediction, whose eight predictors include Schedule C income but neither farm nor partnership earnings, so it is not conditioned on the business that qualifies them. The Medicare proxy keeps the statutory medical-expense premium concept numerically invariant because the direct input equals the decomposed reported premium whenever the modeled Part B add-on is zero. The Form 7206 instructions (2023, p. 1) let voluntarily paid Medicare premiums figure the deduction, so the proxy withholds a deductible premium from Medicare-enrolled self-employed people; that residual predates the farm and partnership paths and applies to them too. Beyond the federal ALD, PolicyEngine-US 2.2.1 reads the bare health_insurance_premiums input in five state/local formulas (MO qualified-premium subtraction, MI household resources, OH uninsured unreimbursed medical expenses, CA Orange County general relief countable income, MO child-care-subsidy adjusted income): attribution moves those programs from premiums-invisible toward the measured premium structure for the self-employed slice, outside the federal income_tax probe's scope; that state and local effect applies to farm and partnership people under 2.2.1 already. External anchor class: IRS SOI Pub 1304 Table 1.4 TY2023 self-employed health insurance deduction, 3,595,764 returns / $31.23B (ledger#105, buildn v9.2 feed)." } ] } diff --git a/packages/microcosm-build/src/microcosm/build/us/spec/sources.yaml b/packages/microcosm-build/src/microcosm/build/us/spec/sources.yaml index e33ffec848..04fef17e83 100644 --- a/packages/microcosm-build/src/microcosm/build/us/spec/sources.yaml +++ b/packages/microcosm-build/src/microcosm/build/us/spec/sources.yaml @@ -99,7 +99,7 @@ sources: stage_asset: id: source_stages path: microcosm.build.us/source_stages.json - sha256: e75217c0db3075bf2664492167d31ce7ee82c2c5f6a70e3b67e8bb6f4ddd7ecf + sha256: 22e365df58aafaa02a65e29b2ed2c98531de795c4233bd3b65473efd6cd3e1b2 stage_manifest: version: 1 country: us @@ -2880,9 +2880,14 @@ stages: weight: person_weight - kind: attribute_self_employed_health_premiums reported_source: health_insurance_premiums_without_medicare_part_b - self_employment_income_sources: - - self_employment_income_before_lsr - - sstb_self_employment_income_before_lsr + plan_business_income_sources: + schedule_c: + - self_employment_income_before_lsr + - sstb_self_employment_income_before_lsr + schedule_f: + - farm_operations_income + partnership: + - partnership_self_employment_net_earnings medicare_age_source: age medicare_age_threshold: 65 medicare_ssdi_source: social_security_disability @@ -2908,20 +2913,34 @@ stages: preserve the exact residual identity. The archive did not explicitly pin tree count or weights; Microcosm pins 100 trees and typed person weights as reproducibility hardening. The final attribution operation (PolicyEngine/microcosm#451 item 2) is deterministic: it copies the finished reported non-Part-B premium - onto the person health_insurance_premiums input exactly where combined Schedule C income (self_employment_income_before_lsr - + sstb_self_employment_income_before_lsr) is strictly positive, the person is outside the Medicare - proxy (age 65+ or any Social Security disability income), and the person is outside measured employer-sponsored - coverage (has_esi - the conservative, incomplete proxy for the 26 USC 162(l)(2)(B) subsidized-employer-plan - month exclusion; eligibility through a spouse''s or dependent''s employer is not measured and remains - documented residual overbreadth), and marks is_self_employed for every strictly-positive Schedule - C person. PolicyEngine-US 2.2.1 then computes self_employed_health_insurance_premiums (a formula-owned - adds-aggregation gated by is_self_employed, so it cannot ship as a column) and the section 162(l) - ALD as min(total_self_employment_income, premiums). The Medicare proxy keeps the statutory medical-expense - premium concept numerically invariant because the direct input equals the decomposed reported premium - whenever the modeled Part B add-on is zero. Beyond the federal ALD, PolicyEngine-US 2.2.1 reads the - bare health_insurance_premiums input in five state/local formulas (MO qualified-premium subtraction, - MI household resources, OH uninsured unreimbursed medical expenses, CA Orange County general relief - countable income, MO child-care-subsidy adjusted income): attribution moves those programs from premiums-invisible - toward the measured premium structure for the self-employed slice, outside the federal income_tax - probe''s scope. External anchor class: IRS SOI Pub 1304 Table 1.4 TY2023 self-employed health insurance - deduction, 3,595,764 returns / $31.23B (ledger#105, buildn v9.2 feed).' + onto the person health_insurance_premiums input exactly where some business a plan can be established + under has strictly positive net earnings on its own (26 USC 162(l)(2)(A); Form 7206 instructions, + 2023, p. 1): Schedule C (self_employment_income_before_lsr + sstb_self_employment_income_before_lsr), + a Schedule F farm (farm_operations_income) or a partnership''s Schedule K-1 box 14 code A earnings + (partnership_self_employment_net_earnings), so a loss in one business never cancels another''s profit; + the person is outside the Medicare proxy (age 65+ or any Social Security disability income), and the + person is outside measured employer-sponsored coverage (has_esi - the conservative, incomplete proxy + for the 26 USC 162(l)(2)(B) subsidized-employer-plan month exclusion; eligibility through a spouse''s + or dependent''s employer is not measured and remains documented residual overbreadth), and marks is_self_employed + for every person with such a business. The other two Form 7206 paths, the Schedule SE optional methods + and more-than-2% S corporation shareholders, have no measured input. PolicyEngine-US then computes + self_employed_health_insurance_premiums (a formula-owned adds-aggregation gated by is_self_employed, + so it cannot ship as a column) and the section 162(l) ALD as premiums capped at earned income. Version + 2.2.1 caps at Schedule C income alone (min(max(total_self_employment_income, 0), premiums)), so the + people a farm or partnership alone qualifies receive no federal deduction until the engine carries + PolicyEngine/policyengine-us#10120, which computes the Form 7206 limit for each business and takes + the largest. On PUF-support people the attributed premium is the joint QRF''s prediction, whose eight + predictors include Schedule C income but neither farm nor partnership earnings, so it is not conditioned + on the business that qualifies them. The Medicare proxy keeps the statutory medical-expense premium + concept numerically invariant because the direct input equals the decomposed reported premium whenever + the modeled Part B add-on is zero. The Form 7206 instructions (2023, p. 1) let voluntarily paid Medicare + premiums figure the deduction, so the proxy withholds a deductible premium from Medicare-enrolled + self-employed people; that residual predates the farm and partnership paths and applies to them too. + Beyond the federal ALD, PolicyEngine-US 2.2.1 reads the bare health_insurance_premiums input in five + state/local formulas (MO qualified-premium subtraction, MI household resources, OH uninsured unreimbursed + medical expenses, CA Orange County general relief countable income, MO child-care-subsidy adjusted + income): attribution moves those programs from premiums-invisible toward the measured premium structure + for the self-employed slice, outside the federal income_tax probe''s scope; that state and local effect + applies to farm and partnership people under 2.2.1 already. External anchor class: IRS SOI Pub 1304 + Table 1.4 TY2023 self-employed health insurance deduction, 3,595,764 returns / $31.23B (ledger#105, + buildn v9.2 feed).' diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/__init__.py b/packages/microcosm-build/src/microcosm/build/us_runtime/__init__.py index 74e4757cba..496b14a6b0 100644 --- a/packages/microcosm-build/src/microcosm/build/us_runtime/__init__.py +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/__init__.py @@ -475,7 +475,8 @@ US_OTHER_HEALTH_INSURANCE_STAGE_OUTPUT_COLUMNS, US_SE_HEALTH_ATTRIBUTION_OUTPUT_COLUMNS, US_SE_HEALTH_MEDICARE_AGE_THRESHOLD, - US_SE_HEALTH_SELF_EMPLOYMENT_INCOME_SOURCES, + US_SE_HEALTH_PLAN_BUSINESS_INCOME_SOURCES, + US_SE_HEALTH_PLAN_BUSINESS_SOURCE_COLUMNS, attribute_us_se_health_premiums, attribute_us_se_health_premiums_from_manifest, derive_us_other_health_insurance_from_asec, @@ -1762,7 +1763,8 @@ "US_OTHER_HEALTH_INSURANCE_STAGE_OUTPUT_COLUMNS", "US_SE_HEALTH_ATTRIBUTION_OUTPUT_COLUMNS", "US_SE_HEALTH_MEDICARE_AGE_THRESHOLD", - "US_SE_HEALTH_SELF_EMPLOYMENT_INCOME_SOURCES", + "US_SE_HEALTH_PLAN_BUSINESS_INCOME_SOURCES", + "US_SE_HEALTH_PLAN_BUSINESS_SOURCE_COLUMNS", "attribute_us_se_health_premiums", "attribute_us_se_health_premiums_from_manifest", "derive_us_other_health_insurance_from_asec", diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/other_health_insurance.py b/packages/microcosm-build/src/microcosm/build/us_runtime/other_health_insurance.py index c2ad1993e1..5da8fe06ee 100644 --- a/packages/microcosm-build/src/microcosm/build/us_runtime/other_health_insurance.py +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/other_health_insurance.py @@ -19,7 +19,9 @@ from __future__ import annotations import gc +from collections.abc import Iterable, Mapping, Sequence from importlib.resources import files +from types import MappingProxyType from typing import Any import numpy as np @@ -59,7 +61,8 @@ "US_OTHER_HEALTH_INSURANCE_STAGE_OUTPUT_COLUMNS", "US_SE_HEALTH_ATTRIBUTION_OUTPUT_COLUMNS", "US_SE_HEALTH_MEDICARE_AGE_THRESHOLD", - "US_SE_HEALTH_SELF_EMPLOYMENT_INCOME_SOURCES", + "US_SE_HEALTH_PLAN_BUSINESS_INCOME_SOURCES", + "US_SE_HEALTH_PLAN_BUSINESS_SOURCE_COLUMNS", "attribute_us_se_health_premiums", "attribute_us_se_health_premiums_from_manifest", "derive_us_other_health_insurance_from_asec", @@ -113,20 +116,46 @@ "medicaid_premium", ) # Self-employed premium attribution (PolicyEngine/microcosm#451 item 2): the -# section 162(l) ALD chain in PolicyEngine-US 2.2.1 reads person inputs +# section 162(l) ALD chain in PolicyEngine-US reads person inputs # health_insurance_premiums (via the self_employed_health_insurance_premiums -# adds-aggregation, gated by is_self_employed) and caps the deduction at -# total_self_employment_income = self_employment_income + -# sstb_self_employment_income. self_employed_health_insurance_premiums itself -# is formula-owned (adds) and cannot ship as a column, so the stage ships the -# two pure inputs the engine intends. +# adds-aggregation, gated by is_self_employed) and caps the deduction at the +# earned income of the business the plan is established under. +# self_employed_health_insurance_premiums itself is formula-owned (adds) and +# cannot ship as a column, so the stage ships the two pure inputs the engine +# intends. US_SE_HEALTH_ATTRIBUTION_OUTPUT_COLUMNS: tuple[str, ...] = ( "health_insurance_premiums", "is_self_employed", ) -US_SE_HEALTH_SELF_EMPLOYMENT_INCOME_SOURCES: tuple[str, ...] = ( - "self_employment_income_before_lsr", - "sstb_self_employment_income_before_lsr", +# 26 USC 162(l)(2)(A) limits the deduction to the earned income "derived by +# the taxpayer from the trade or business with respect to which the plan +# providing the medical care coverage is established". The Form 7206 +# instructions (2023, p. 1) name who qualifies: a net profit on Schedule C or +# Schedule F, a partner's net earnings on Schedule K-1 box 14 code A, the +# Schedule SE optional methods, and wages of a more-than-2% S corporation +# shareholder. No measured input carries the last two. +# PolicyEngine/policyengine-us#10120 computes the limit for each business +# from these person inputs and, with no named plan business, takes the +# largest. Treating both Schedule C inputs as one business (the engine's +# total_self_employment_income) is that PR's modelling choice, followed here +# so the flag opens exactly where its limit can be positive. Each business's +# net earnings stand alone, so a loss in one never cancels another's profit. +US_SE_HEALTH_PLAN_BUSINESS_INCOME_SOURCES: Mapping[str, tuple[str, ...]] = ( + MappingProxyType( + { + "schedule_c": ( + "self_employment_income_before_lsr", + "sstb_self_employment_income_before_lsr", + ), + "schedule_f": ("farm_operations_income",), + "partnership": ("partnership_self_employment_net_earnings",), + } + ) +) +US_SE_HEALTH_PLAN_BUSINESS_SOURCE_COLUMNS: tuple[str, ...] = tuple( + column + for columns in US_SE_HEALTH_PLAN_BUSINESS_INCOME_SOURCES.values() + for column in columns ) # Attribution is withheld from people on the Medicare proxy (age 65+ or any # Social Security disability income) so the statutory medical-expense premium @@ -463,12 +492,24 @@ def _finite_values(person: pd.DataFrame, column: str) -> np.ndarray: return values +def _has_positive_plan_business( + net_earnings_by_business: Iterable[np.ndarray], + size: int, +) -> np.ndarray: + """Whether any one plan business has strictly positive net earnings.""" + + positive = np.zeros(size, dtype=bool) + for net_earnings in net_earnings_by_business: + positive |= net_earnings > 0.0 + return positive + + def attribute_us_se_health_premiums( person: pd.DataFrame, *, reported_source_column: str = _REPORTED_OUTPUT, - self_employment_income_source_columns: tuple[str, ...] = ( - US_SE_HEALTH_SELF_EMPLOYMENT_INCOME_SOURCES + plan_business_income_source_columns: Mapping[str, Sequence[str]] = ( + US_SE_HEALTH_PLAN_BUSINESS_INCOME_SOURCES ), medicare_age_source_column: str = _SE_MEDICARE_AGE_SOURCE, medicare_age_threshold: int = US_SE_HEALTH_MEDICARE_AGE_THRESHOLD, @@ -479,30 +520,36 @@ def attribute_us_se_health_premiums( ) -> pd.DataFrame: """Attribute reported premiums to self-employed people deterministically. - The premium output copies the reported non-Part-B premium exactly for - people with strictly positive combined Schedule C income who are outside - the Medicare proxy AND outside measured employer-sponsored coverage, and - is zero elsewhere: attribution never invents premium mass. Section - 162(l)(2)(B) disallows the deduction for months the taxpayer is eligible - to participate in a subsidized employer plan; the measured ``has_esi`` + A person is self-employed here when any business a plan can be + established under has strictly positive net earnings: Schedule C (both + Schedule C inputs summed), a Schedule F farm, or a partnership's Schedule + K-1 box 14 code A earnings. The premium output copies the reported + non-Part-B premium exactly for self-employed people outside the Medicare + proxy AND outside measured employer-sponsored coverage, and is zero + elsewhere: attribution never invents premium mass. Section 162(l)(2)(B) + disallows the deduction for months the taxpayer is eligible to + participate in a subsidized employer plan; the measured ``has_esi`` indicator (actual employer-sponsored coverage) is the conservative, incomplete proxy for that eligibility — eligibility through a spouse's or dependent's employer is not measured and remains documented residual - overbreadth. The flag marks every strictly-positive Schedule C person so - the engine's ``defined_for`` gate opens exactly where the section 162(l) - earned-income cap can bind. Schedule C losses are legitimate measured - signal and simply leave the flag off. + overbreadth. The flag marks every self-employed person so the engine's + ``defined_for`` gate opens exactly where a section 162(l) earned-income + limit can be positive. A loss is legitimate measured signal: it leaves + the flag off unless another business is profitable. """ reported = _strict_nonnegative_values(person, reported_source_column) - self_employment = np.zeros(len(person), dtype=np.float64) - for column in self_employment_income_source_columns: - self_employment += _finite_values(person, column) + net_earnings_by_business = [] + for columns in plan_business_income_source_columns.values(): + net_earnings = np.zeros(len(person), dtype=np.float64) + for column in columns: + net_earnings += _finite_values(person, column) + net_earnings_by_business.append(net_earnings) age = _finite_values(person, medicare_age_source_column) ssdi = _strict_nonnegative_values(person, medicare_ssdi_source_column) employer_covered = _boolean_values(person, employer_coverage_source_column) - self_employed = self_employment > 0.0 + self_employed = _has_positive_plan_business(net_earnings_by_business, len(person)) medicare_proxy = (age >= float(medicare_age_threshold)) | (ssdi > 0.0) result = person.copy(deep=True) @@ -564,9 +611,10 @@ def attribute_us_se_health_premiums_from_manifest( ) expected = { "reported_source": _REPORTED_OUTPUT, - "self_employment_income_sources": list( - US_SE_HEALTH_SELF_EMPLOYMENT_INCOME_SOURCES - ), + "plan_business_income_sources": { + business: list(columns) + for business, columns in US_SE_HEALTH_PLAN_BUSINESS_INCOME_SOURCES.items() + }, "medicare_age_source": _SE_MEDICARE_AGE_SOURCE, "medicare_age_threshold": US_SE_HEALTH_MEDICARE_AGE_THRESHOLD, "medicare_ssdi_source": _SE_MEDICARE_SSDI_SOURCE, @@ -583,9 +631,10 @@ def attribute_us_se_health_premiums_from_manifest( return attribute_us_se_health_premiums( frame, reported_source_column=parameters["reported_source"], - self_employment_income_source_columns=tuple( - parameters["self_employment_income_sources"] - ), + plan_business_income_source_columns={ + business: tuple(columns) + for business, columns in parameters["plan_business_income_sources"].items() + }, medicare_age_source_column=parameters["medicare_age_source"], medicare_age_threshold=int(parameters["medicare_age_threshold"]), medicare_ssdi_source_column=parameters["medicare_ssdi_source"], @@ -1005,7 +1054,7 @@ def _se_attribution_summary( column for column in ( *US_SE_HEALTH_ATTRIBUTION_OUTPUT_COLUMNS, - *US_SE_HEALTH_SELF_EMPLOYMENT_INCOME_SOURCES, + *US_SE_HEALTH_PLAN_BUSINESS_SOURCE_COLUMNS, _SE_MEDICARE_AGE_SOURCE, _SE_MEDICARE_SSDI_SOURCE, _SE_EMPLOYER_COVERAGE_SOURCE, @@ -1037,15 +1086,18 @@ def _se_attribution_summary( # any of them makes the recomputed identity meaningless, so it is a # failure in its own right, not a silently not-self-employed person. nonfinite_sources: dict[str, int] = {} - self_employment = np.zeros(len(person), dtype=np.float64) - for column in US_SE_HEALTH_SELF_EMPLOYMENT_INCOME_SOURCES: - values = pd.to_numeric(person[column], errors="coerce").to_numpy( - dtype=np.float64 - ) - bad = int(np.count_nonzero(~np.isfinite(values))) - if bad: - nonfinite_sources[column] = bad - self_employment += values + net_earnings_by_business: dict[str, np.ndarray] = {} + for business, columns in US_SE_HEALTH_PLAN_BUSINESS_INCOME_SOURCES.items(): + net_earnings = np.zeros(len(person), dtype=np.float64) + for column in columns: + values = pd.to_numeric(person[column], errors="coerce").to_numpy( + dtype=np.float64 + ) + bad = int(np.count_nonzero(~np.isfinite(values))) + if bad: + nonfinite_sources[column] = bad + net_earnings += values + net_earnings_by_business[business] = net_earnings age = pd.to_numeric(person[_SE_MEDICARE_AGE_SOURCE], errors="coerce").to_numpy( dtype=np.float64 ) @@ -1063,7 +1115,9 @@ def _se_attribution_summary( bad = int(np.count_nonzero(~np.isfinite(values))) if bad: nonfinite_sources[column] = bad - self_employed = self_employment > 0.0 + self_employed = _has_positive_plan_business( + net_earnings_by_business.values(), len(person) + ) medicare_proxy = (age >= float(US_SE_HEALTH_MEDICARE_AGE_THRESHOLD)) | (ssdi > 0.0) employer_covered = employer_numeric == 1.0 expected = np.where( @@ -1091,6 +1145,12 @@ def _se_attribution_summary( "flag_share": ( float(weights[flag].sum()) / total_weight if total_weight > 0.0 else 0.0 ), + # Diagnostics only: a person can have more than one profitable + # business, so these overlap and need not sum to flag_rows. + "positive_plan_business_rows": { + business: int(np.count_nonzero(net_earnings > 0.0)) + for business, net_earnings in net_earnings_by_business.items() + }, "invalid_flag_values": invalid_flag_values, "nonfinite_sources": nonfinite_sources, "identity_violations": identity_violations, diff --git a/packages/microcosm-build/tests/engine_contract/us/test_spec_engine_coverage_tool.py b/packages/microcosm-build/tests/engine_contract/us/test_spec_engine_coverage_tool.py index 7be8062b02..c1c66a3b50 100644 --- a/packages/microcosm-build/tests/engine_contract/us/test_spec_engine_coverage_tool.py +++ b/packages/microcosm-build/tests/engine_contract/us/test_spec_engine_coverage_tool.py @@ -82,21 +82,21 @@ def test_us_coverage_is_exact_complete_and_honest( assert_coverage_complete(coverage_report) assert coverage_report["status"] == "pass" fields = coverage_report["field_usage"] - assert fields["configuration_field_count"] == 42_184 - assert fields["authored_normative_field_count"] == 32_404 + assert fields["configuration_field_count"] == 42_186 + assert fields["authored_normative_field_count"] == 32_406 assert fields["resolved_binding_field_count"] == 9_780 - assert fields["consumed_field_count"] == 42_184 + assert fields["consumed_field_count"] == 42_186 assert fields["unused_field_count"] == 0 assert fields["multiple_primary_use_field_count"] == 0 assert fields["claim_count"] == 49 assert fields["mode_counts"] == { - "legacy_behavior": 14_010, + "legacy_behavior": 14_012, "compiler_semantic": 27_717, "front_end_validation": 354, "identity_only": 103, } assert fields["generation0_effect_counts"] == { - "legacy_behavior": 38_498, + "legacy_behavior": 38_500, "no_generation0_effect": 3_686, } diff --git a/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_field_usage.py b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_field_usage.py index c5ec18bb51..d584218a70 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_field_usage.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_field_usage.py @@ -95,22 +95,22 @@ def _mutated_bundle( def test_exact_complete_ledger_has_one_primary_mode_per_pointer(field_ledger) -> None: - assert len(field_ledger.fields) == EXPECTED_CONFIGURATION_FIELD_COUNT == 42_184 + assert len(field_ledger.fields) == EXPECTED_CONFIGURATION_FIELD_COUNT == 42_186 assert field_ledger.source_counts == { - "authored": 32_404, + "authored": 32_406, "resolved_bindings": 9_780, } assert field_ledger.mode_counts == { - "legacy_behavior": 14_010, + "legacy_behavior": 14_012, "compiler_semantic": 27_717, "front_end_validation": 354, "identity_only": 103, } assert field_ledger.generation0_effect_counts == { - "legacy_behavior": 38_498, + "legacy_behavior": 38_500, "no_generation0_effect": 3_686, } - assert len({field.pointer for field in field_ledger.fields}) == 42_184 + assert len({field.pointer for field in field_ledger.fields}) == 42_186 def test_eligibility_concepts_are_validation_not_generation0_behavior( diff --git a/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_loader.py b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_loader.py index 17cd0b715a..2edec68947 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_loader.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_spec_engine_loader.py @@ -12,7 +12,7 @@ def test_semantic_hash_has_golden_vector_and_surface_separation(tmp_path) -> Non # Pin the domain separator, normalization rules, schema-set receipt, and # exact normative projection as one reviewable golden vector. assert first.spec_sha256 == ( - "69de1ffc232c3a4da09925658c9d105af55bdaae90852824ac6283306932aab9" + "17ea0a6c106ed0c2bbada0ca7cee6be838a9b635806610cc678e45504459f00e" ) second_root = _rich_minimal(tmp_path / "xy", note="second", store="local:b") diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_bundle_core_contracts.py b/packages/microcosm-build/tests/engine_free/us/test_us_bundle_core_contracts.py index a780c4814b..26ad27d6a1 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_bundle_core_contracts.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_bundle_core_contracts.py @@ -25,7 +25,7 @@ def test_source_surface_classification_is_complete() -> None: } assert normative["stage_asset"] == { "id": "source_stages", - "sha256": "e75217c0db3075bf2664492167d31ce7ee82c2c5f6a70e3b67e8bb6f4ddd7ecf", + "sha256": "22e365df58aafaa02a65e29b2ed2c98531de795c4233bd3b65473efd6cd3e1b2", } assert operational["stage_asset"] == { "path": "microcosm.build.us/source_stages.json" diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_other_health_insurance.py b/packages/microcosm-build/tests/engine_free/us/test_us_other_health_insurance.py index bd3d3c2284..04d4ae1f67 100644 --- a/packages/microcosm-build/tests/engine_free/us/test_us_other_health_insurance.py +++ b/packages/microcosm-build/tests/engine_free/us/test_us_other_health_insurance.py @@ -74,9 +74,14 @@ def test_stage_manifest_pins_residual_and_joint_puf_qrf() -> None: } assert spec.operations[3].parameters == { "reported_source": _REPORTED, - "self_employment_income_sources": list( - US_SE_HEALTH_SELF_EMPLOYMENT_INCOME_SOURCES - ), + "plan_business_income_sources": { + "schedule_c": [ + "self_employment_income_before_lsr", + "sstb_self_employment_income_before_lsr", + ], + "schedule_f": ["farm_operations_income"], + "partnership": ["partnership_self_employment_net_earnings"], + }, "medicare_age_source": "age", "medicare_age_threshold": US_SE_HEALTH_MEDICARE_AGE_THRESHOLD, "medicare_ssdi_source": "social_security_disability", @@ -395,6 +400,26 @@ def test_se_attribution_is_deterministic_reported_identity_with_guards() -> None lambda source: source.drop(columns=["has_esi"]), "has_esi", ), + ( + lambda source: source.drop(columns=["farm_operations_income"]), + "farm_operations_income", + ), + ( + lambda source: source.drop( + columns=["partnership_self_employment_net_earnings"] + ), + "partnership_self_employment_net_earnings", + ), + ( + lambda source: source.assign(farm_operations_income=[np.inf]), + "nonnumeric or nonfinite", + ), + ( + lambda source: source.assign( + partnership_self_employment_net_earnings=[np.nan] + ), + "nonnumeric or nonfinite", + ), ( lambda source: source.assign(has_esi=["True"]), "object dtype", @@ -426,6 +451,86 @@ def test_se_attribution_allows_negative_self_employment_losses() -> None: assert result[_FLAG_OUTPUT].tolist() == [False, False] +def test_se_attribution_covers_farm_and_partnership_plan_businesses() -> None: + # 26 USC 162(l)(2)(A) and the Form 7206 instructions let the plan be + # established under a Schedule F farm or a partnership (Schedule K-1 box + # 14 code A), and PolicyEngine-US #10120 limits the deduction business by + # business, so each profitable business qualifies on its own. + source = _attribution_source( + reported=[6_000.0] * 9, + self_employment=[0.0, 0.0, -10_000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + farm=[20_000.0, 0.0, 5_000.0, -3_000.0, 20_000.0, 20_000.0, 0.0, 0.0, -1.0], + partnership=[0.0, 15_000.0, 0.0, 8_000.0, 0.0, 0.0, 15_000.0, 0.0, -1.0], + age=[40.0, 40.0, 40.0, 40.0, 65.0, 40.0, 40.0, 40.0, 40.0], + ssdi=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 12_000.0, 0.0, 0.0], + esi=[False, False, False, False, False, True, False, False, False], + ) + + result = attribute_us_se_health_premiums(source) + + assert result[_PREMIUMS_OUTPUT].tolist() == [ + 6_000.0, # farm profit alone + 6_000.0, # partnership earnings alone + 6_000.0, # a Schedule C loss does not cancel the farm's profit + 6_000.0, # nor does a farm loss cancel the partnership's earnings + 0.0, # farm profit, Medicare age proxy + 0.0, # farm profit, measured employer coverage + 0.0, # partnership earnings, SSDI Medicare proxy + 0.0, # no business at all + 0.0, # losses everywhere + ] + assert result[_FLAG_OUTPUT].tolist() == [ + True, + True, + True, + True, + True, + True, + True, + False, + False, + ] + + +def test_se_attribution_manifest_refuses_the_schedule_c_only_parameter() -> None: + from microcosm.build.source_manifest import SourceOperationSpec + + spec = us_other_health_insurance_stage_spec() + operation = spec.operations[3] + retired = { + key: value + for key, value in operation.parameters.items() + if key != "plan_business_income_sources" + } + retired["self_employment_income_sources"] = [ + "self_employment_income_before_lsr", + "sstb_self_employment_income_before_lsr", + ] + source = _attribution_source(reported=[5_000.0], self_employment=[0.0]) + + with pytest.raises(SourceRuntimeError, match="drifted from the pinned"): + attribute_us_se_health_premiums_from_manifest( + source, + SourceOperationSpec.from_mapping({"kind": operation.kind, **retired}), + None, + ) + narrowed = dict(operation.parameters) + narrowed["plan_business_income_sources"] = { + "schedule_c": narrowed["plan_business_income_sources"]["schedule_c"] + } + with pytest.raises(SourceRuntimeError, match="drifted from the pinned"): + attribute_us_se_health_premiums_from_manifest( + source, + SourceOperationSpec.from_mapping({"kind": operation.kind, **narrowed}), + None, + ) + assert attribute_us_se_health_premiums_from_manifest( + _attribution_source(reported=[5_000.0], self_employment=[0.0], farm=[1.0]), + operation, + None, + )[_FLAG_OUTPUT].tolist() == [True] + + def test_with_inputs_materializes_se_attribution_and_carries_signal( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -434,18 +539,66 @@ def test_with_inputs_materializes_se_attribution_and_carries_signal( result = with_us_other_health_insurance_inputs(_frame(), seed=0, time_period=2024) person = result.table("person") + self_employed = ( + (_SELF_EMPLOYMENT_VALUES > 0.0) + | (_FARM_VALUES > 0.0) + | (_PARTNERSHIP_VALUES > 0.0) + ) expected_premiums = np.where( - (_SELF_EMPLOYMENT_VALUES > 0.0) & ~person["has_esi"].to_numpy(), + self_employed & ~person["has_esi"].to_numpy(), _REPORTED_VALUES, 0.0, ) np.testing.assert_allclose(person[_PREMIUMS_OUTPUT], expected_premiums) - assert person[_FLAG_OUTPUT].tolist() == (_SELF_EMPLOYMENT_VALUES > 0.0).tolist() + assert person[_FLAG_OUTPUT].tolist() == self_employed.tolist() + # The farm and partnership paths flag their people; neither carries a + # premium here (no reported premium; measured employer coverage). + assert person[_FLAG_OUTPUT].tolist()[2:4] == [True, True] + assert person[_FLAG_OUTPUT].tolist()[8] is False + summary = us_other_health_insurance_summary(result)["se_attribution"] + assert summary["positive_plan_business_rows"] == { + "schedule_c": 1, + "schedule_f": 1, + "partnership": 1, + } assert person[_FLAG_OUTPUT].dtype == bool gate = us_other_health_insurance_signal_gate(result) assert gate.passed, gate.failures +def test_with_inputs_carries_farm_and_partnership_premiums_through_the_gate( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(module, "PolicyEngineUSEngine", _ZeroPremiumEngine) + frame = _frame() + person = frame.table("person") + # Person 7 (reported 4,000) farms and person 1 (reported 1,000) is a + # partner; neither has Schedule C income or measured employer coverage. + person.loc[[1, 7], "has_esi"] = False + person.loc[7, "farm_operations_income"] = 12_000.0 + person.loc[1, "partnership_self_employment_net_earnings"] = 9_000.0 + + result = with_us_other_health_insurance_inputs(frame, seed=0, time_period=2024) + + attributed = result.table("person") + assert attributed.loc[7, _PREMIUMS_OUTPUT] == 4_000.0 + assert attributed.loc[1, _PREMIUMS_OUTPUT] == 1_000.0 + assert attributed.loc[[1, 7], _FLAG_OUTPUT].tolist() == [True, True] + assert attributed.loc[[1, 7], "self_employment_income_before_lsr"].tolist() == [ + 0.0, + 0.0, + ] + gate = us_other_health_insurance_signal_gate(result) + assert gate.passed, gate.failures + assert gate.details["se_attribution"]["positive_rows"] == 3 + + # Dropping the farm person's premium breaks the identity the gate checks. + attributed.loc[7, _PREMIUMS_OUTPUT] = 0.0 + gate = us_other_health_insurance_signal_gate(result) + assert not gate.passed + assert any("attribution identity" in failure for failure in gate.failures) + + def test_signal_gate_rejects_broken_or_absent_se_attribution( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -477,6 +630,29 @@ def test_signal_gate_rejects_broken_or_absent_se_attribution( gate = us_other_health_insurance_signal_gate(unflagged) assert not gate.passed + # The farm person's flag is part of the identity the gate recomputes. + farm_unflagged = with_us_other_health_insurance_inputs( + _frame(), seed=0, time_period=2024 + ) + farm_unflagged.table("person").loc[2, _FLAG_OUTPUT] = False + gate = us_other_health_insurance_signal_gate(farm_unflagged) + assert not gate.passed + assert any("attribution identity" in failure for failure in gate.failures) + + # Without a plan-business source the identity cannot be recomputed. + unsourced = with_us_other_health_insurance_inputs( + _frame(), seed=0, time_period=2024 + ) + unsourced.table("person").drop( + columns=["partnership_self_employment_net_earnings"], inplace=True + ) + gate = us_other_health_insurance_signal_gate(unsourced) + assert not gate.passed + assert any( + "partnership_self_employment_net_earnings" in failure + for failure in gate.failures + ) + def test_signal_gate_rejects_malformed_flag_and_nonfinite_identity_sources( monkeypatch: pytest.MonkeyPatch, diff --git a/packages/microcosm-build/tests/engine_free/us/test_us_se_health_attribution_properties.py b/packages/microcosm-build/tests/engine_free/us/test_us_se_health_attribution_properties.py new file mode 100644 index 0000000000..de81ac6712 --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/us/test_us_se_health_attribution_properties.py @@ -0,0 +1,262 @@ +"""Properties of the self-employed health premium attribution. + +The attribution flags a person when any one business a plan can be +established under (Schedule C, a Schedule F farm, a partnership) has strictly +positive net earnings, and copies the reported premium for flagged people +outside the Medicare proxy and measured employer coverage. These tests state +the invariants for every input and check them against an independent +row-wise reference, the pre-farm Schedule C rule, and the signal gate's own +recomputation of the identity. +""" + +from __future__ import annotations + +import numpy as np +import pandas as pd +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +import microcosm.build.us_runtime.other_health_insurance as module +from microcosm.build.us_runtime.other_health_insurance import ( + US_SE_HEALTH_ATTRIBUTION_OUTPUT_COLUMNS, + US_SE_HEALTH_MEDICARE_AGE_THRESHOLD, + US_SE_HEALTH_PLAN_BUSINESS_INCOME_SOURCES, + US_SE_HEALTH_PLAN_BUSINESS_SOURCE_COLUMNS, + attribute_us_se_health_premiums, +) +from test_support.microcosm_build.us_other_health_insurance import ( + _REPORTED, + _attribution_source, +) + +_PREMIUMS, _FLAG = US_SE_HEALTH_ATTRIBUTION_OUTPUT_COLUMNS +_SCHEDULE_C = ( + "self_employment_income_before_lsr", + "sstb_self_employment_income_before_lsr", +) +_FARM = "farm_operations_income" +_PARTNERSHIP = "partnership_self_employment_net_earnings" +_SETTINGS = settings( + max_examples=150, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) + +# Net earnings draw exact zeros, losses, profits and values next to zero, so +# strict positivity and cancelling Schedule C legs are both exercised. +_money = st.one_of( + st.just(0.0), + st.sampled_from([-1.0, -0.01, 0.01, 1.0, 5e-324, -5e-324]), + st.floats(min_value=-1e6, max_value=1e6, allow_nan=False, allow_infinity=False), +) +_premium = st.one_of( + st.just(0.0), + st.floats(min_value=0.0, max_value=1e5, allow_nan=False, allow_infinity=False), +) +_row = st.fixed_dictionaries( + { + "reported": _premium, + "self_employment": _money, + "sstb": _money, + "farm": _money, + "partnership": _money, + "age": st.integers(min_value=0, max_value=90).map(float), + "ssdi": st.one_of(st.just(0.0), st.floats(min_value=0.01, max_value=4e4)), + "esi": st.booleans(), + } +) +_rows = st.lists(_row, min_size=1, max_size=25) + + +def _source(rows: list[dict[str, float | bool]]) -> pd.DataFrame: + return _attribution_source( + reported=[row["reported"] for row in rows], + self_employment=[row["self_employment"] for row in rows], + sstb=[row["sstb"] for row in rows], + farm=[row["farm"] for row in rows], + partnership=[row["partnership"] for row in rows], + age=[row["age"] for row in rows], + ssdi=[row["ssdi"] for row in rows], + esi=[row["esi"] for row in rows], + ) + + +def _reference(row: dict[str, float | bool]) -> tuple[float, bool]: + """Form 7206's businesses, one at a time, in plain Python.""" + + businesses = ( + row["self_employment"] + row["sstb"], + row["farm"], + row["partnership"], + ) + self_employed = any(net > 0.0 for net in businesses) + medicare = row["age"] >= US_SE_HEALTH_MEDICARE_AGE_THRESHOLD or row["ssdi"] > 0.0 + attributed = self_employed and not medicare and not row["esi"] + return (row["reported"] if attributed else 0.0), self_employed + + +def _schedule_c_only_rule(source: pd.DataFrame) -> tuple[np.ndarray, np.ndarray]: + """The attribution before farm and partnership businesses counted.""" + + schedule_c = (source[_SCHEDULE_C[0]] + source[_SCHEDULE_C[1]]).to_numpy() + flag = schedule_c > 0.0 + medicare = (source["age"].to_numpy() >= US_SE_HEALTH_MEDICARE_AGE_THRESHOLD) | ( + source["social_security_disability"].to_numpy() > 0.0 + ) + premiums = np.where( + flag & ~medicare & ~source["has_esi"].to_numpy(), + source[_REPORTED].to_numpy(), + 0.0, + ) + return premiums, flag + + +def test_plan_business_sources_are_the_form_7206_businesses() -> None: + assert dict(US_SE_HEALTH_PLAN_BUSINESS_INCOME_SOURCES) == { + "schedule_c": _SCHEDULE_C, + "schedule_f": (_FARM,), + "partnership": (_PARTNERSHIP,), + } + assert US_SE_HEALTH_PLAN_BUSINESS_SOURCE_COLUMNS == ( + *_SCHEDULE_C, + _FARM, + _PARTNERSHIP, + ) + + +@_SETTINGS +@given(rows=_rows) +def test_attribution_matches_the_row_wise_reference(rows) -> None: + source = _source(rows) + original = source.copy(deep=True) + + result = attribute_us_se_health_premiums(source) + + expected = [_reference(row) for row in rows] + assert result[_PREMIUMS].tolist() == [premium for premium, _ in expected] + assert result[_FLAG].tolist() == [flag for _, flag in expected] + assert result[_FLAG].dtype == bool + pd.testing.assert_frame_equal(source, original) + + # The businesses are alternatives, so their order cannot matter. + reversed_order = attribute_us_se_health_premiums( + source, + plan_business_income_source_columns=dict( + reversed(list(US_SE_HEALTH_PLAN_BUSINESS_INCOME_SOURCES.items())) + ), + ) + pd.testing.assert_frame_equal(reversed_order, result) + + +@_SETTINGS +@given(rows=_rows) +def test_attribution_never_invents_premium_mass(rows) -> None: + source = _source(rows) + + result = attribute_us_se_health_premiums(source) + + premiums = result[_PREMIUMS].to_numpy() + reported = source[_REPORTED].to_numpy() + flag = result[_FLAG].to_numpy() + assert np.all((premiums == 0.0) | (premiums == reported)) + assert np.all((premiums >= 0.0) & (premiums <= reported)) + assert premiums.sum() <= reported.sum() + assert not np.any((premiums > 0.0) & ~flag) + + +@_SETTINGS +@given(rows=_rows) +def test_widening_only_adds_people_to_the_schedule_c_rule(rows) -> None: + source = _source(rows) + old_premiums, old_flag = _schedule_c_only_rule(source) + + widened = attribute_us_se_health_premiums(source) + schedule_c_only = attribute_us_se_health_premiums( + source, + plan_business_income_source_columns={ + "schedule_c": US_SE_HEALTH_PLAN_BUSINESS_INCOME_SOURCES["schedule_c"] + }, + ) + + # Differential: the Schedule C-only configuration is the old rule exactly. + assert schedule_c_only[_PREMIUMS].tolist() == old_premiums.tolist() + assert schedule_c_only[_FLAG].tolist() == old_flag.tolist() + # Monotone: everyone the old rule attributed keeps the same premium and + # flag; the only new people have a profitable farm or partnership. + new_flag = widened[_FLAG].to_numpy() + new_premiums = widened[_PREMIUMS].to_numpy() + assert np.all(new_flag[old_flag]) + np.testing.assert_array_equal(new_premiums[old_flag], old_premiums[old_flag]) + added = new_flag & ~old_flag + other_profit = (source[_FARM].to_numpy() > 0.0) | ( + source[_PARTNERSHIP].to_numpy() > 0.0 + ) + assert np.all(other_profit[added]) + unchanged = ~other_profit + np.testing.assert_array_equal(new_flag[unchanged], old_flag[unchanged]) + np.testing.assert_array_equal(new_premiums[unchanged], old_premiums[unchanged]) + + +@_SETTINGS +@given( + rows=_rows, + replacement=st.floats(max_value=0.0, min_value=-1e6), + schedule_c_profit_leg=st.floats(min_value=0.0, max_value=1e6), + schedule_c_net_loss=st.floats(min_value=0.0, max_value=1e6), +) +def test_a_loss_never_cancels_another_business( + rows, replacement, schedule_c_profit_leg, schedule_c_net_loss +) -> None: + source = _source(rows) + result = attribute_us_se_health_premiums(source) + + # Only whether each business is profitable matters: every non-positive + # business can be swapped for any other non-positive amount, however + # large the loss, without changing the attribution. For Schedule C the + # business is the sum of its two inputs, so an unprofitable Schedule C is + # replaced by another split that still nets to a loss or zero. + moved = source.copy(deep=True) + for column in (_FARM, _PARTNERSHIP): + moved[column] = np.where(moved[column] > 0.0, moved[column], replacement) + schedule_c_profitable = ( + source[_SCHEDULE_C[0]] + source[_SCHEDULE_C[1]] + ).to_numpy() > 0.0 + moved[_SCHEDULE_C[0]] = np.where( + schedule_c_profitable, source[_SCHEDULE_C[0]], schedule_c_profit_leg + ) + moved[_SCHEDULE_C[1]] = np.where( + schedule_c_profitable, + source[_SCHEDULE_C[1]], + -(schedule_c_profit_leg + schedule_c_net_loss), + ) + pd.testing.assert_frame_equal( + attribute_us_se_health_premiums(moved)[[_PREMIUMS, _FLAG]], + result[[_PREMIUMS, _FLAG]], + ) + + +@_SETTINGS +@given(rows=_rows) +def test_signal_gate_recomputation_agrees_with_the_attribution(rows) -> None: + source = _source(rows) + result = attribute_us_se_health_premiums(source) + + # Differential: the gate recomputes the identity from shipped columns + # with its own code path; it must find no divergence on true output. + summary = module._se_attribution_summary( + result, + np.ones(len(result), dtype=np.float64), + result[_REPORTED].to_numpy(dtype=np.float64), + ) + assert summary["identity_violations"] == 0 + assert summary["flag_violations"] == 0 + assert summary["invalid_flag_values"] == 0 + assert summary["nonfinite_sources"] == {} + assert summary["flag_rows"] == int(result[_FLAG].sum()) + assert summary["positive_plan_business_rows"] == { + "schedule_c": int( + ((source[_SCHEDULE_C[0]] + source[_SCHEDULE_C[1]]) > 0.0).sum() + ), + "schedule_f": int((source[_FARM] > 0.0).sum()), + "partnership": int((source[_PARTNERSHIP] > 0.0).sum()), + } diff --git a/packages/microcosm-build/tests/engine_workflow/us/test_us_multispine_pool_tool.py b/packages/microcosm-build/tests/engine_workflow/us/test_us_multispine_pool_tool.py index 9606d75b4c..9b5b97b158 100644 --- a/packages/microcosm-build/tests/engine_workflow/us/test_us_multispine_pool_tool.py +++ b/packages/microcosm-build/tests/engine_workflow/us/test_us_multispine_pool_tool.py @@ -338,7 +338,7 @@ def capture_equality(expected: object, actual: object) -> None: "country": "us", "schema_id": "country_spec", "schema_version": 1, - "spec_sha256": "f76f921d4f74392b106b8c0729ce250494e0a55455eaba7b910b19cdf7825178", + "spec_sha256": "9550605da8976e20d07a8c453b091d825a36020fe34b34a65f8724043d0b5f1c", }, } diff --git a/test_support/microcosm_build/us_other_health_insurance.py b/test_support/microcosm_build/us_other_health_insurance.py index 3359f519f3..b3f8091671 100644 --- a/test_support/microcosm_build/us_other_health_insurance.py +++ b/test_support/microcosm_build/us_other_health_insurance.py @@ -28,7 +28,8 @@ US_OTHER_HEALTH_INSURANCE_STAGE_OUTPUT_COLUMNS, US_SE_HEALTH_ATTRIBUTION_OUTPUT_COLUMNS, US_SE_HEALTH_MEDICARE_AGE_THRESHOLD, - US_SE_HEALTH_SELF_EMPLOYMENT_INCOME_SOURCES, + US_SE_HEALTH_PLAN_BUSINESS_INCOME_SOURCES, + US_SE_HEALTH_PLAN_BUSINESS_SOURCE_COLUMNS, attribute_us_se_health_premiums, attribute_us_se_health_premiums_from_manifest, derive_us_other_health_insurance_from_asec, @@ -36,6 +37,7 @@ impute_us_other_health_insurance_to_puf_support_from_manifest, us_other_health_insurance_signal_gate, us_other_health_insurance_stage_spec, + us_other_health_insurance_summary, with_us_other_health_insurance_inputs, ) from microcosm.build.us_runtime.puf_support import clone_us_frame_for_puf_support @@ -66,6 +68,12 @@ _SELF_EMPLOYMENT_VALUES = np.asarray( [0.0, 0.0, 0.0, 0.0, 0.0, 20_000.0, 0.0, 0.0, 0.0, 0.0] ) +# Person 2 (no reported premium) farms and person 3 (reported 2,000, measured +# employer coverage) has partnership earnings, so the Schedule F and +# partnership paths flag both without either carrying a premium; person 8's +# farm loss leaves the flag off. +_FARM_VALUES = np.asarray([0.0, 0.0, 15_000.0, 0.0, 0.0, 0.0, 0.0, 0.0, -2_000.0, 0.0]) +_PARTNERSHIP_VALUES = np.asarray([0.0, 0.0, 0.0, 9_000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) def _frame() -> Frame: @@ -91,6 +99,8 @@ def _frame() -> Frame: "employment_income_before_lsr": np.arange(count) * 5_000.0, "self_employment_income_before_lsr": _SELF_EMPLOYMENT_VALUES, "sstb_self_employment_income_before_lsr": np.zeros(count), + "farm_operations_income": _FARM_VALUES, + "partnership_self_employment_net_earnings": _PARTNERSHIP_VALUES, "social_security_retirement": np.zeros(count), "social_security_disability": np.zeros(count), "social_security_dependents": np.zeros(count), @@ -155,6 +165,8 @@ def _attribution_source( reported: list[float], self_employment: list[float], sstb: list[float] | None = None, + farm: list[float] | None = None, + partnership: list[float] | None = None, age: list[float] | None = None, ssdi: list[float] | None = None, esi: list[bool] | None = None, @@ -167,6 +179,10 @@ def _attribution_source( "sstb_self_employment_income_before_lsr": ( sstb if sstb is not None else [0.0] * count ), + "farm_operations_income": farm if farm is not None else [0.0] * count, + "partnership_self_employment_net_earnings": ( + partnership if partnership is not None else [0.0] * count + ), "age": age if age is not None else [40.0] * count, "social_security_disability": (ssdi if ssdi is not None else [0.0] * count), "has_esi": esi if esi is not None else [False] * count, diff --git a/test_support/microcosm_build/us_spec_bundle.py b/test_support/microcosm_build/us_spec_bundle.py index ef4bc8868b..d296133abd 100644 --- a/test_support/microcosm_build/us_spec_bundle.py +++ b/test_support/microcosm_build/us_spec_bundle.py @@ -195,7 +195,7 @@ def _load_generator_module(): LEGACY_COMPATIBILITY_SHA256 = { "source_stages.json": ( - "e75217c0db3075bf2664492167d31ce7ee82c2c5f6a70e3b67e8bb6f4ddd7ecf" + "22e365df58aafaa02a65e29b2ed2c98531de795c4233bd3b65473efd6cd3e1b2" ), "support_spine.json": ( "68f37dc6ae6e0cde7ebccb53f88dd4a800e63456f838fa214ff98d1db8d815be" diff --git a/tools/build_us_release_input_coverage_manifest.py b/tools/build_us_release_input_coverage_manifest.py index be6f9f56f2..b298ec685f 100644 --- a/tools/build_us_release_input_coverage_manifest.py +++ b/tools/build_us_release_input_coverage_manifest.py @@ -127,8 +127,10 @@ "Self-employed premium attribution leaf (PolicyEngine/microcosm#451 " "item 2): the deterministic attribution operation of the " "other_health_insurance_premiums release stage copies the reported " - "non-Part-B premium onto this person input for strictly-positive " - "Schedule C people outside the Medicare proxy, which is the only " + "non-Part-B premium onto this person input for people with a " + "strictly positive Schedule C, Schedule F or partnership (Schedule " + "K-1 box 14 code A) business who are outside the Medicare proxy and " + "measured employer coverage, which is the only " "channel populating the section 162(l) self-employed health ALD " "(SOI Pub 1304 Table 1.4 TY2023: 3,595,764 returns / $31.23B, " "ledger#105). Absent on pre-attribution artifacts by construction; " @@ -138,8 +140,9 @@ "Gate flag for the self-employed premium attribution " "(PolicyEngine/microcosm#451 item 2): opens the defined_for gate on " "the engine's self_employed_health_insurance_premiums " - "adds-aggregation for every strictly-positive Schedule C person. " - "Written by the same attribution operation as " + "adds-aggregation for every person whose Schedule C, Schedule F or " + "partnership net earnings are strictly positive, each business on " + "its own. Written by the same attribution operation as " "health_insurance_premiums." ), "pre_subsidy_care_expenses": ( diff --git a/tools/generate_us_bundle_from_constants.py b/tools/generate_us_bundle_from_constants.py index 5dc45835db..728617b5f3 100644 --- a/tools/generate_us_bundle_from_constants.py +++ b/tools/generate_us_bundle_from_constants.py @@ -122,7 +122,7 @@ def _domain_builders() -> dict[str, Callable[[], object]]: # frozen files, is the forward YAML -> legacy-payload path. FROZEN_LEGACY_RESOURCE_SHA256 = { "source_stages.json": ( - "e75217c0db3075bf2664492167d31ce7ee82c2c5f6a70e3b67e8bb6f4ddd7ecf" + "22e365df58aafaa02a65e29b2ed2c98531de795c4233bd3b65473efd6cd3e1b2" ), "support_spine.json": ( "68f37dc6ae6e0cde7ebccb53f88dd4a800e63456f838fa214ff98d1db8d815be" diff --git a/tools/spec_engine_coverage.py b/tools/spec_engine_coverage.py index c066d573ce..9ec21e9932 100644 --- a/tools/spec_engine_coverage.py +++ b/tools/spec_engine_coverage.py @@ -41,7 +41,7 @@ REPORT_SCHEMA_VERSION = 3 EXPECTED_POINTER_INVENTORY_SHA256 = ( - "8f655670b9794adae905ede64bdceba6395eeb8d642b6c5896d5c516f1f6a6a7" + "11e11693ab6b89b1008c3fb69d933b3dbebcb61d49eeba0d62f9edb776ba01cb" ) DEFAULT_REPORT_PATH = ( Path(__file__).resolve().parents[1]