diff --git a/docs/design/track_b_g_conformance_report.json b/docs/design/track_b_g_conformance_report.json index f4a524c6..b355ad52 100644 --- a/docs/design/track_b_g_conformance_report.json +++ b/docs/design/track_b_g_conformance_report.json @@ -750,7 +750,7 @@ "status": "refused" }, { - "description": "Two potential parents; one own RIB 780 lies between 75% and 82.5% of PIA 1000", + "description": "Two potential parents; own RIBs 780 and 790 both lie between 75% and 82.5% of PIA 1000", "id": "parent_benefit", "reason": "parent_benefit", "status": "refused" diff --git a/docs/design/track_b_g_gross_benefits.md b/docs/design/track_b_g_gross_benefits.md index 201d829c..bc66506e 100644 --- a/docs/design/track_b_g_gross_benefits.md +++ b/docs/design/track_b_g_gross_benefits.md @@ -423,8 +423,16 @@ Its sealed delayed-credit function agrees through `min(48, window)`; the new statutory float function agrees over the whole FRA-to-70 window, including months past 48 for births 1917–1942 (#494). A named pin preserves the sealed legacy result rather than treating that retained convention as -an open bug. These are invariant 10 below. The exact monetary -reimplementation needs Max's ratification, because §6 asked for reuse. +an open bug. These are invariant 10 below. + +Reading taken (October 23, 2026), logged as +`us:oasdi-reduction-exact-fractions`: this is not a conflict between +sources. Act 202(q)(1) states the reductions as fractions ("5/9 of 1 +percent", "25/36 of 1 percent", "19/40 of 1 percent"), and RS 00615.005B +says "only fractions can be used when computing reduced benefits". The +design's §6 suggestion to reuse the float primitives was a design +preference, not a source; the law requires the exact arithmetic, and the +float oracle stays a differential. ## Delayed credits @@ -475,9 +483,21 @@ dime on two sources, both in the Social Security Bulletin 75(3) capture: After that, 215(i)(2)(A)(ii) floors every COLA increase of the maximum to the dime. The captured POMS sections on the disability maximum (RS 00615.740 and .742) say nothing about rounding. Both quotes are pinned by -`test_worked_example_figures_are_quoted_from_the_captures`. The Bulletin is -an SSA research publication, not a program instruction, so resting the -rounding on it needs Max's acceptance. +`test_worked_example_figures_are_quoted_from_the_captures`. + +Reading taken (October 23, 2026), logged as +`us:oasdi-disability-family-maximum-rounding`: floor to the dime. The Act's +403(a)(6) and 20 CFR 404.403(d-1) are silent, so nothing in a statute or +regulation competes. The Act rounds the regular maximum "to the next lower +multiple of $0.10" (403(a)(2)) and dime-floors every later increase of this +maximum (215(i)(2)(A)(ii)), so an undimed initial maximum would be the only +one of its kind; SSA's own description, the Bulletin, floors it. Where the +statute is silent, P1 lets the agency's published reading fill the gap. +A blind adjudication preferred leaving the maximum unrounded, reading the +contrast with 403(a)(1) and 20 CFR 404.403(c) as deliberate. The stakes +are a few dimes a month per auxiliary and arise only after COLAs: three +successive 5 percent COLAs on PIA $1,000.10 and AIME $1,701 pay children +$515.80 against $516.00. The published agency reading is kept. ## Households and the entry point @@ -626,23 +646,19 @@ rate: RS 00630.400's summary also requires "at least one beneficiary is reduced for age". G does not require it, which can only add refusals. -Needs Max's judgment: the literal 203(a)(5) guarantee that totals "for any -such subsequent month will not be less" reaches later composition changes. -RS 00630.400's operational condition instead states "Total family payment -is less after the PIA increase than it was before." Its additional condition -is "Benefits for that month are reduced for the MAX and at least one -beneficiary is reduced for age;". In the invented three-child case above, -two children leaving in the COLA month lowers the total from $1,750.00 to -$1,537.50. The literal reading refuses; the operational reading admits that -$1,537.50 total when no beneficiary is reduced for age. This module keeps -the literal refusal. - -Ruling d1418 (2026-10-12): keep the current defaults; reopen any one when a -population target needs it. - -Ruling d1338 (2026-10-12): adopt SSA's operational reading (RS 00630.400), -keeping the literal reading behind a switch; implemented in G part 2. -Until then this module applies the literal refusal. +Reading taken by Max, ruling d1338 (2026-10-12), logged as +`us:oasdi-family-maximum-saving-clause`: SSA's operational reading is the +default and the literal one stays selectable as `LITERAL_REFUSAL`. The +literal 203(a)(5) guarantee that totals "for any such subsequent month will +not be less" reaches later composition changes. RS 00630.400's operational +condition instead states "Total family payment is less after the PIA +increase than it was before." Its additional condition is "Benefits for +that month are reduced for the MAX and at least one beneficiary is reduced +for age;". In the invented three-child case above, two children leaving in +the COLA month lowers the total from $1,750.00 to $1,537.50. The literal +reading refuses; the operational reading admits that $1,537.50 total when +no beneficiary is reduced for age. "Operational saving clause, October 23, +2026" below implements the default. ## Supported and refused configurations @@ -1188,25 +1204,51 @@ limiting RIB-LIM remain interaction with the PTAP age-reduction and partial-payable terms. -### Zero reached after auxiliary age reduction - -This reading needs Max's judgment. GN 02603.045 says the counted amount is -"before any applicable reduction for age". GN 02603.050C requires the -"Age reduction amount for all workers or deductees". GN 02603.050A.2 -instead says "If dual entitlement reduces a beneficiary's rate to $0.00 -(technically entitled)" to disregard that beneficiary. A method-B spouse -with PIA $500, maximum $750, own RIB $64 and 36 auxiliary reduction months -has standard rate $83.30, age-adjusted $62.40, and zero auxiliary payable. -Disregarding the spouse pays children $125 each; retaining the $20.90 age -term pays $114.50 each, a $21 record-total difference. A reading of the -before-age text alone counts $83.30 minus $64, or $19.30, paying children -$115.30 each and a record total of $730.60. G refuses this -case as `parisi_redistribution_ambiguous`, rather than selecting the higher -payment. Zero before auxiliary age reduction is supported, including A.5 -and A.8. The alternatives and the refusal are executed by the Parisi tests. - -Ruling d1418 (2026-10-12): keep the current defaults; reopen any one when a -population target needs it. +### Reading taken: zero reached after auxiliary age reduction + +Source conflict `us:oasdi-dual-entitlement-zero-after-age-reduction`, +decided October 23, 2026 (the regulation's definition of the redistributed +amount, with the agency's worked accounting; P3). Ruling d1418 had kept it +refused. GN 02603.045 says the counted amount is "before any applicable +reduction for age". GN 02603.050C requires the "Age reduction amount for +all workers or deductees". GN 02603.050A.2 says "If dual entitlement +reduces a beneficiary's rate to $0.00 (technically entitled)" to disregard +that beneficiary. A method-B spouse with PIA $500, maximum $750, own RIB +$64 and 36 auxiliary reduction months has standard rate $83.30, +age-adjusted $62.40, and zero auxiliary payable. + +Act 402(k)(3)(A) applies the own-benefit offset "after any reduction under +subsection (q)", so in a method-B case the $83.30 share divides into $20.90 +withheld for age and $62.40 withheld for dual entitlement. 20 CFR +404.403(a)(5) excludes from the total "any amount not paid because of that +person's entitlement on another earnings record (see § 404.407)", and +404.407(a) measures that offset after the 202(q) reduction. GN 02603.060 +B.2 states the same accounting ("In order to redistribute only the amount +being withheld because of dual entitlement we must account for the entire +$83.30 due B"), keeping the age slice in PTAP. So only the $62.40 is +redistributed: PTAP is $750 − $500 − $20.90 − $0 = $229.10, and each child +receives $114.50, a record total of $729. GN 02603.050A.2's disregard +applies, as in A.5, A.8 and A.9, when dual entitlement considered first +(.045's before-age measure; A.8: "we consider their dual entitlement +first") leaves nothing; here it leaves $83.30 − $64 = $19.30, so A.2 does +not apply. + +The rejected readings stay pinned as diagnostics: disregarding the spouse +pays $125 each (it would also pass the $20.90 age reduction to the +children), and counting only .045's before-age amount ($19.30) pays $115.30 +each, total $730.60 (it would treat $1.60 of the age reduction as withheld +for dual entitlement). Two blind adjudications on October 23 reached +$114.50; an intermediate draft of this pass had preferred $115.30 for its +continuity and was revised. +`test_method_b_zero_only_after_age_keeps_the_age_reduction_in_ptap` pins +the quotes and all three amounts; the Hypothesis property +`test_method_b_counts_payable_plus_age_reduction_until_a2` checks that a +method-B spouse counts its payable plus its own age reduction, that is +standard − min(own benefit, age-adjusted rate), until the own benefit +reaches the standard rate and A.2 disregards it, and that the children +never fall as the own benefit rises. The method-C property also executes a +positive excess that rounds to zero after age reduction, which counts the +whole excess under either reading. ## Disabled widow(er) benefits @@ -1279,17 +1321,20 @@ correct PIA; this layer does not calculate WEP. Parents on survivor records use 82.5 percent for one entitled parent and 75 percent each for two, floored to a dime (202(h)(2); RS 00209.015). The maximum applies before 202(k)(3)(A)'s own-benefit offset. There is no age -reduction. A parent's own RIB must be less than the parent's OB to establish -entitlement (RS 00209.001). This compares the actual reduced or credited -own RIB and the dime-rounded OB, rather than the own PIA. An own-DIB parent +reduction. A parent's own RIB must be less than 82½ percent of the PIA, or +75 percent when another parent is entitled, to establish entitlement +(402(h)(1)(D); RS 00209.001 states the test with the OB). This compares the +actual reduced or credited own RIB with the unrounded statutory share, +rather than the dime-rounded OB or the own PIA. An own-DIB parent remains technically entitled even if no parent benefit is payable (RS 00209.015); that parent still counts for the two-parent rate. More than two declared parents is refused as `parent_benefit`: the captured one/two-parent rules do not settle that configuration. -Needs Max's judgment: the parent entitlement count can be circular. RS -00209.001 requires "not be entitled to retirement insurance benefit (RIB) -equal to or exceeding +Reading taken (October 23, 2026), source conflict +`us:oasdi-parent-entitlement-count-interval` (P1, the Act's operative +words). Ruling d1418 had kept it refused. RS 00209.001 requires "not be +entitled to retirement insurance benefit (RIB) equal to or exceeding the amount of the parent's original benefit (One parent = 82 1/2%; Two parents = 75%);". RS 00209.015 distinguishes "2. One parent entitled" from "3. Two parents entitled". For PIA $1,000 and maximum $1,750, two potential @@ -1297,27 +1342,40 @@ parents, one with own RIB $780, qualify together at the one-parent OB of $825 but that first parent's RIB exceeds the two-parent OB of $750. Counting two first excludes that parent and pays the other $825; testing eligibility at $825 first would count two and pay the other $750 (a $75 difference), -while paying no auxiliary to the $780 own-RIB parent. Neither fixed order -is silently selected: `parent_benefit` refuses the interval between the two -dime-rounded OBs when both parents are potential entitlements. +while paying no auxiliary to the $780 own-RIB parent. + +The Act decides it. 402(h)(1)(D) tests the own RIB against 82½ percent "if +the amount of the parent's insurance benefit for such month is determinable +under paragraph (2)(A) (or 75 percent of such primary insurance amount in +any other case)", and (h)(2)(B) applies 75 percent "For any month for which +more than one parent is entitled". The entitled set must satisfy that test +for every parent at once. The parent without an own RIB is entitled +whatever the other does, so the $780 parent's benefit, if entitled, would +be the two-parent rate, which $780 reaches: that parent is not entitled, +and the other parent alone is paid $825 under (h)(2)(A). The other one-pass +order ends with one parent paid at the two-parent rate, which (h)(2)(A) +contradicts. With two parents whose own RIBs both lie in the band, either +parent alone satisfies the test for both, so `parent_benefit` still +refuses that configuration by name (filing order, which the inputs omit, +might settle it). The thresholds are the unrounded 82½ and 75 percent of +the PIA that (h)(1)(D) names, not the dime-floored OBs RS 00209.001 speaks +of (P1): at PIA $1,000.10 a lone parent with own RIB $825.00 is entitled +(below $825.0825), with $0 payable. `test_parent_entitlement_count_counterfactual_changes_payment_by_75` -executes both readings with a test-only exact helper. Counting two first -tests entitlement at $750, excludes the $780-own-RIB parent, and gives the -other parent's candidate payment $825. Rechecking entitlement at $825 -would count two. Testing entitlement at $825 first counts two, changes the -other parent's candidate payment to $750, and then excludes the first -parent when entitlement is rechecked at $750. Both candidates pay that -first parent no auxiliary, and the other parent's candidates differ by -$75. The public result remains the `parent_benefit` refusal. The Hypothesis -test `test_parent_count_counterfactual_orders_cycle_across_the_refused_band` -executes the same count reversal and the exact difference between the two -dime-rounded OBs throughout the refused interval. These reversals are an -intended diagnostic inconsistency of the two one-pass readings, not a -supported payment or a bug in the public refusal. - -Ruling d1418 (2026-10-12): keep the current defaults; reopen any one when a -population target needs it. +executes both one-pass orders with a test-only exact helper and asserts +that the public result is the consistent one: the $780 parent not +entitled, the other parent paid $825. The Hypothesis test +`test_parent_count_counterfactual_orders_cycle_across_the_refused_band` +keeps the two one-pass orders' count reversal as a diagnostic. +`test_one_parent_in_the_band_yields_to_the_other_parent` checks the reading +across the whole band; `test_two_parents_in_the_band_refuse_because_either_alone_qualifies` +the remaining refusal; and +`test_parent_entitlement_satisfies_402h1d_for_every_parent` is an exact +differential against a brute-force enumeration of every entitled set that +satisfies 402(h)(1)(D) for both parents, with no own benefit, an own DIB or +an own RIB anywhere: G computes exactly the unique consistent set and +refuses exactly when there is more than one. Invented hand references (the captures publish rules without monetary worked examples for these classes): PIA $1,000 pays a solo parent $825 or @@ -1895,25 +1953,33 @@ this contract. The envelope is deliberately conservative; it does not certify a tight set of all possible dated histories. Narrower historical support needs dated entitlement and reduction-money evidence. -### Needs Max's judgment: DWB after January 1984 - -The broad instruction in RS 00615.005C.2 is "all cases involving single -entitlement to actuarially reduced benefits prior to 1/78". In contrast, -RS 00615.301B.1.e says "all disabled widow(er)s under age 60 receive .715 of -the full unreduced WIB for 1/84 and later". RS 00615.005A.7 also states -that the 1983 amendments "Eliminated the additional reduction for the -period between age 50 and age 60." A specific-rule reading could treat -that change as settling the entire later amount, including earlier awards. -The general historical reading still requires the missing monetary ratio. - -This round conservatively refuses an otherwise unsettled pre-1978-capable -DWB cohort and its possible aged-widow conversion; it does not assign a -historical payment. For cohort 1927 in January 1984, with invented PIA and unreduced rate -$1,000, the fixed 71.5% reading would pay $715.00 ($715 whole dollars), while the implemented -reading refuses and retains the row and weight. Tests pin the captured -texts, the alternative exact $715 formula and the actual refusal. This is -an explicit support-boundary judgment item, separate from the earlier -$715 surviving-divorced child-care regression, which remains supported. +### Reading taken: DWB after January 1984 + +Source conflict `us:oasdi-dwb-1984-pre-1978-history`, decided October 23, +2026 (specific over general). The broad instruction in RS 00615.005C.2 is +"all cases involving single entitlement to actuarially reduced benefits +prior to 1/78". In contrast, RS 00615.301B.1.e says "all disabled +widow(er)s under age 60 receive .715 of the full unreduced WIB for 1/84 and +later". RS 00615.005A.7 also states that the 1983 amendments "Eliminated +the additional reduction for the period between age 50 and age 60", and RS +00615.310B.3 says DWB entitled before 1/84 "had their benefits recalculated +effective 1/84 as if the reduction for the period prior to age 60 did not +exist." The specific DWB rule governs the general pre-1978 method: from +January 1984, every month in which a disabled widow(er) is under 60 is paid +.715 of the unreduced WIB whatever the entitlement date. G applies it to +every payment month in a year before the SSA birth cohort can attain 60 +(cohort year plus 59 or earlier); in the year it can attain 60 the birth +month decides and is not supplied, and from 60 the money history governs +again, so those months keep `reduced_benefit_history_before_1978`. For +cohort 1927 in January 1984, with invented PIA and unreduced rate $1,000, +G now pays $715.00 ($715 whole dollars); in 1987 the row is refused and +keeps its weight. `test_historical_dwb_under_60_takes_the_specific_fixed_rate` +pins the texts and amounts, and the Hypothesis property +`test_historical_dwb_is_settled_exactly_before_the_cohort_can_attain_60` +checks, for cohorts 1912-1927 and payment years 1984-2000, that every +under-60 year equals a post-1977 cohort's computation exactly and every +later year refuses. The earlier $715 surviving-divorced child-care +regression remains supported. Dual-entitlement snapshots also lack dates and earlier money history. RS 00615.250 gives the current reduced-excess procedure and refers to @@ -1924,7 +1990,7 @@ claim that the conservative interval identifies a particular historical dual-entitlement amount. Expanding that historical surface is not in scope. -### Needs Max's judgment: historical dual entitlement +### Refused: historical dual entitlement (sources silent) RS 00615.005C.2 explicitly describes "single entitlement". RS 00615.020 states "Method C - First benefit calculated independently; reduce excess @@ -1940,12 +2006,24 @@ own-first sequence. For an invented cohort-1912 spouse with unreduced own RIB $400, spouse OB $500 and auxiliary RF1, the current method-C reading pays auxiliary -$99.30 ($99 whole dollars), with own $400. The conservative implementation +$99.30 ($99 whole dollars), with own $400. Method C applies only if the own +benefit was a DIB or an early RIB when the spouse benefit began +(402(q)(3)(A)); an unreduced RIB that began after the spouse benefit is +method B, $496.50 − $400 = $96.50 ($96), which the inputs cannot tell +apart either. The conservative implementation refuses `reduced_benefit_history_before_1978`, retaining the row and weight; it does not assign a different historical dollar amount. The source-quote test executes that alternative and the cohort-1916 control side by side -with the historical refusal. Resolving this narrower historical dual -surface needs Max's judgment; no new monetary default is silently chosen. +with the historical refusal. + +Refusal kept (October 23, 2026), logged as +`us:oasdi-dual-entitlement-pre-1978-history`: the sources are silent, so +the refusal is safe. RS 00615.005C.2's money method covers single +entitlement only; RS 00615.250 sends subsequent adjustments to RS +00615.005F, which the captured section does not contain; RS 00615.240 +settles spouse-first awards but still needs the dated pre-1978 increases. +No statute or regulation in the captures fixes the own-first historical +amount, and the inputs carry no dated money history in any case. ## Fix round 9 of October 15, 2026 @@ -2233,10 +2311,12 @@ benefit, including parent, widow-basis and dime-rounding cases. `test_two_parents_do_not_pass_their_count_to_other_survivor_roles` execute the applicability, basis-bound and mixed-family properties. -### Needs Max's judgment and not in scope +### Judgment items and scope (as of this round) No new statutory choice arises. All earlier judgment items, conservative refusals, source-erratum controls and rulings d1418/d1338 remain unchanged. +(The October 23, 2026 readings pass, at the end of this note, has since +decided or re-framed every judgment item.) This round does not implement savings-clause raised payments, new family configurations, new sources or real-data processing. @@ -2480,7 +2560,7 @@ the guard, as well as the separate whole-dollar payment fields. | Skipped qualifying history | Existing `savings_clause_history_unknown` refusal | | Uninitialized/subclassed guard, guard copying or guard serialization | `InvalidFamilyInput`; no coverage refusal code | -### Needs Max's judgment and not in scope +### Judgment items and scope (as of this round) No new statutory interpretation is selected. Earlier judgment items and conservative savings-clause readings remain unchanged. This round does not @@ -2789,8 +2869,9 @@ identical; tests pin both quoted OBs and modern OBs rather than overriding the arithmetic or claiming historical rounding coverage. The captures do not date these examples or establish why they used upward rounding. -### Settled reading and deferred coverage +### Readings taken and refusals kept +Reading taken (logged as `us:oasdi-ret-temporary-partial-deduction`): GN 02603.050D.2 says "there are no partial deductions for temporary work deductions". RS 02501.160 Example 2 nevertheless instructs immediate same-household redistribution in June with C3 $39 and C2 $119, while E and @@ -2800,12 +2881,17 @@ amounts. The literal GN sentence would refuse that temporary partial case; it would withhold C2's $20 June increase. The source assertions pin both sentences. No switch invents a different benefit amount. -The interaction of NH reservations and age reductions is deferred coverage: -no allocation procedure has been derived for the combined configuration. -This is not a conflict between authoritative algorithms, and missing worked -examples alone do not establish an ambiguous reading. The implementation -refuses the mixed configuration as `deduction_age_work_interaction`. -No population processing, combined maximum determination, +Refusal kept (October 23, 2026), logged as +`us:oasdi-nh-charge-age-reduction-interaction`: the interaction of NH +reservations and age reductions is deferred coverage, not a conflict +between authoritative algorithms. GN 02603.050C lists both "work +deductions to be imposed against auxiliaries or survivors due to the +number holder's work" and the "Age reduction amount for all workers or +deductees" in PTAP, but no allocation procedure has been derived for the +combined configuration, and missing worked examples alone do not establish +an ambiguous reading. The implementation refuses the mixed configuration +as `deduction_age_work_interaction`; ordinary G/R2 age-reduced supported +inputs remain unchanged. No population processing, combined maximum determination, multiple auxiliary-record selection or annual earnings determination is in scope here. @@ -3036,6 +3122,20 @@ with GN 02603.050, not to input validity. No protected amount is inferred from a diagnostic pre-offset total, and every refusal retains its row and weight. Completing this combined payment procedure remains outside scope. +### Refused: gaps in the operational procedure (sources silent) + +Refusals kept (October 23, 2026), logged as +`us:oasdi-saving-clause-procedure-gaps`. Ruling d1338 chose RS 00630.400's +operational procedure over the Act's literal "any such subsequent month", +and that procedure states only the life-case allocation with the worker +alone, or one spouse and other auxiliaries, reduced for age. It says +nothing on how a protection survives a change of entitled membership +(`savings_clause_membership_change`), nor on survivor, own-benefit, +deduction, multiple-reduced, lone-reduced-spouse, mixed-basis or above-OB +rates (`savings_clause_rate_procedure`). Falling back to the literal Act +would reverse d1338 for those months, and no other captured SSA source +supplies the allocation, so each stays refused and counted. + ### Round-2 input and history regressions The following are invented calculations, not published SSA examples. @@ -3143,6 +3243,114 @@ before the original-benefit cap of $225.30. The existing A.2 test pins the literal narrative, raw quotient and capped payment side by side. This is an omitted cap in the narrative, not an alternate arithmetic rule. + +## Readings pass, October 23, 2026 + +Verification class: statutory conformance on invented cases and captured +sources; no population claim. Under Max's 2026-10-20 delegation, single +readings are the session's call under his principles (P1 statute first, +P2 regulations are law, P3 the agency breaks true ties, P4 taxpayer +choices), logged in the source-conflict rulings with the ids below; +patterns and collisions between principles go to him. Ruling d1418 had +kept the older readings as defaults pending a population need; this pass +decides each one. Every money-changing reading keeps the rejected +alternatives pinned as diagnostics, and every remaining refusal keeps its +row and weight in the denominators. + +| Item | Id | Outcome | Basis | +|---|---|---|---| +| 403(a)(5) literal or operational | `us:oasdi-family-maximum-saving-clause` | Operational default, literal behind a switch | Max, d1338 | +| Operational rounding direction | `us:oasdi-saving-clause-operational-rate-rounding` | Round equal rates up | P1 | +| Gaps in the operational procedure | `us:oasdi-saving-clause-procedure-gaps` | Refused | Sources silent | +| Zero reached only after auxiliary age reduction | `us:oasdi-dual-entitlement-zero-after-age-reduction` | Keep the age reduction in PTAP, redistribute only the dual-entitlement amount: children $114.50 each, was refused | 20 CFR 404.403(a)(5), 404.407(a); GN .060 B.2; P3 | +| Circular parent count | `us:oasdi-parent-entitlement-count-interval` | The set satisfying 402(h)(1)(D) for both parents, at the unrounded percentages: other parent $825, was refused; two parents in the band stay refused | P1 | +| DWB from January 1984, pre-1978-capable cohorts | `us:oasdi-dwb-1984-pre-1978-history` | Under 60: .715 of the unreduced WIB ($715, was refused); from 60: refused | Specific over general | +| Historical dual entitlement | `us:oasdi-dual-entitlement-pre-1978-history` | Refused | Sources silent | +| Exact fractions or float reuse | `us:oasdi-reduction-exact-fractions` | Exact fractions | Not a conflict (Act and RS 00615.005B agree) | +| Disability-maximum rounding | `us:oasdi-disability-family-maximum-rounding` | Floor to the dime (contrary blind view recorded) | P1's gap-filling clause; the agency's published reading | +| Temporary partial deduction | `us:oasdi-ret-temporary-partial-deduction` | Follow the RS 02501.160 example in G | Specific over general; P3 | +| NH reservations with age reductions | `us:oasdi-nh-charge-age-reduction-interaction` | Refused | Deferred coverage, not a conflict | + +This pass added the statutory texts it newly relies on (42 U.S.C. +402(d)(1)(D), (d)(5), (e)(1), (h)(1)(D), (h)(2), (k)(3)(A), (k)(4) and +(s)) to `lii_usc_402_415_excerpts.txt`, from a re-fetch whose LII body +matched the pinned 402 SHA-256 byte for byte. +`tests/test_track_b_readings_20261023.py` pins the bookkeeping: + +- **Files it reads.** Every file in the Track B package, subpackages + included, whose suffix in any case is `.py`, `.pyi`, `.yaml` or `.yml`, + except the two insured-status modules that #562 adds at the top of the + package (`insured_status.py` and `_insured_status_qc.py`). Those keep + their own register of readings, with a test of its own, and this test + does not read them. It reads these files and the design notes as + UTF-8, so a file that doesn't decode fails. +- **Markers.** Code logs a reading only as the text + `# Source conflict `, with exactly that spelling, case and spacing. + An id is `us:oasdi-` and then one or more words of the letters a to z + and the digits 0 to 9, joined by single hyphens, and it ends at a + colon, whitespace or the end of the file. With those markers removed, a + file the test reads may not contain any of the following, in any case, + where a letter is one of A to Z and a digit one of 0 to 9: + - "source" or "sources", with no letter just before it, then + "conflict", with nothing but whitespace, `#`, `-` or `_` between; + - "us", with no letter or digit just before it, then "oasdi", with at + most three colons, whitespace characters, hyphens or underscores + between; + - "oasdi", with no letter or digit just before it, then at least two + words of letters and digits, each after a hyphen. + + So a wrapped, reworded, recased or misspelled marker fails while any of + these survive in it. Wordings with none of them, such as "conflicting + sources" with no id, pass. +- **Ids.** The marker ids must be exactly this pass's ids plus the ids + other rounds register in that test, each in the G, R2, R3 or grace + module. Each must appear in a Track B design note as a whole token, + with no letter A to Z, digit 0 to 9, underscore or hyphen on either + side, and this note and the R2 and R3 notes must name each of their own + readings that way. +- **Openings.** None of those three notes may open a heading, paragraph, + list item, table row or other block with "Needs Max's judgment". The + test reads every line, code blocks included. A non-blank line + continues the line above it only when both start with a letter A to Z + after the same leading whitespace and neither holds `>` or `|`. Every + other non-blank line opens, so a line after a blank line, or one that + starts with markup such as `#`, `*`, `-`, `>`, `|` or `<`, always opens. + What a line opens runs to the end of its run of non-blank lines. The + text after each `

` to `

` opening tag, up to its closing tag or + the end of the note, also counts as an opening. The test reads that + tag as ending at its first `>`, and again as ending at its first `>` + outside quotes. + + The test decodes HTML entities in each opening and reads it twice: + once with HTML tags and Markdown link targets (a `(...)` or `[...]` + right after a `]`) removed, and once as written. Each reading applies + Unicode compatibility decomposition (NFKD) and keeps only the letters + A to Z, in lower case. An opening fails when either reading starts + with "needsmaxsjudgment" or "needsmaxsjudgement". So punctuation, + apostrophes of any kind, digits, whitespace, markup and invisible + characters can't hide the phrase. Only letters can: a letter A to Z + that a reading keeps, including one that a character such as U+2122 + (the trademark sign) decomposes to, or a letter from another script + that looks like a Latin one, such as the Cyrillic U+0435, which the + readings drop. These rules can flag lines that CommonMark would not + read as opening a block, such as lines in code blocks. +- **Quotes.** Each statutory passage the test quotes must appear in the + capture verbatim, once each run of whitespace in the capture reads as + one space. Before that, the capture helper reads zero-width spaces + (U+200B) and non-breaking spaces as spaces, and U+2019 as a straight + apostrophe. +- **Reads.** So that a blind role can run this module without reaching a + restricted file, `test_the_checks_list_and_open_only_track_b_paths` + runs the other checks with `Path.open` and `Path`'s listings patched. + The checks may list only the Track B package and + `docs/design/track_b_*.md`, and open only those files and the captures + under `tests/data/track_b/gross_benefit_sources/`. The guard patches + nothing else, so a direct `open()` or `os` listing would pass it. The + module and the capture helper use neither. + +The invented conformance lattice stays the same; only the +`parent_benefit` witness moved to two parents in the band. + ## Round-3 input premises and excluded beneficiaries Verification class: statutory conformance on invented cases and captured diff --git a/docs/design/track_b_r2_ret_ledger.md b/docs/design/track_b_r2_ret_ledger.md index 66c49e01..cd9bd109 100644 --- a/docs/design/track_b_r2_ret_ledger.md +++ b/docs/design/track_b_r2_ret_ledger.md @@ -477,14 +477,14 @@ deemed-entitled beneficiaries, and months with a binding maximum or another deduction event. A living NH must be present and receive RIB or DIB in entitled months. A deceased NH must be absent. Deemed entitlement asserts the married-person facts of Act 203(b)(1) and 20 CFR 404.420; -the broader unmarried-child reading of RS 02501.140 is refused. An -ordinary deemed child's age-18 months must be explicit. Their Act/CFR -coverage difference is refused even with those facts established. +an unmarried declaration under RS 02501.140's broader wording is an input +error (that recipient is not on this NH's ledger). An ordinary deemed +child's age-18 months must be explicit, and that residual adult-child case +is refused even with those facts established. Deemed entitlement requires a different NH with RIB entitlement in each -applicable month. `own_work_exempt_months` accepts only CHILD or WIDOW -payment months and asserts respectively attainment of age 18 or initial -widow(er) entitlement before 60 (Act 203(f)(1)(C)-(D)); it cannot -arbitrarily protect a RIB or spouse payment. +applicable month. `own_work_exempt_months` must stay empty: the Act +203(f)(1)(C)-(D) exemptions reach only CDB and DWB months (202(s)(3); +402(e)(1)), which the ledger exempts by type. Input errors raise `InvalidFamilyInput` before a coverage refusal. These include floats, invalid months, negative or non-dime rates, nonzero gross @@ -791,12 +791,13 @@ different section 202 type entered as DWB or CDB after a gap can still carry pending eligibility to a later year's first eligible NSM; the disability-only-year exclusion does not erase that entitlement history (20 CFR 404.435(b)(2)-(3); RS 02501.030B.2 and B.4). -The conservative default also -excludes simultaneous disability entitlement, following 20 CFR -404.435(b)(3). The named POMS alternative permits NSMs in months RIB is -elected while technical DIB entitlement continues, reproducing RS -02501.030B.4's published example. The money-changing difference is -recorded under Needs Max's judgment. +During simultaneous RIB and DIB entitlement the default counts NSMs in +months RIB is paid: under Act 202(k)(4) the person is then entitled to the +RIB alone, so 20 CFR 404.435(b)(3)'s disability exclusion does not reach +the month, and RS 02501.030B.4's published example is reproduced. The +former technical-entitlement exclusion remains an explicit alternative. +"Readings taken and refusals kept" below records the reading and its +money difference. ## Supported and refused configurations @@ -838,8 +839,8 @@ not a reconstruction of monetary RET under pre-1978 law. | Noncovered foreign work | `FOREIGN_WORK` / `noncovered_foreign_work` | Act 203(c); 20 CFR 404.417-.418 | | Refusal of vocational rehabilitation | `VOCATIONAL_REHABILITATION` / `vocational_rehabilitation_refusal` | RS 02501.130 | | Another deduction event on the record | `OTHER_DEDUCTION` / `other_deduction_event` | Act 203(c)/(d); RS 02501.130; GN 02603.040 | -| Unmarried purported deemed entitlement | `DEEMED_ENTITLEMENT_BASIS` / `deemed_entitlement_marriage_basis` | RS 02501.140's wording is broader than Act 203(b)(1)/20 CFR 404.420 | -| Ordinary deemed adult CHILD with asserted marriage and age facts | `DEEMED_ENTITLEMENT_BASIS` / `deemed_entitlement_marriage_basis` | Act 203(b)(1)'s adult-child wording is broader than CFR .420's disabled-child basis | +| Unmarried purported deemed entitlement | `InvalidFamilyInput`: not on this NH's ledger | Act 203(b)(1) and 20 CFR 404.420 require marriage; RS 02501.140's broader wording yields (P1) | +| Ordinary deemed adult CHILD with asserted marriage and age facts | `DEEMED_ENTITLEMENT_BASIS` / `deemed_entitlement_marriage_basis` | Entitled only through 402(d)(5), which needs disability in the month of marriage or in the third month before it (402(s)(2)); the child may have recovered before or after the marriage. CFR .420 omits that residual case | The monthly G context establishes maximum membership, original age reduction and dual entitlement. Its declared baseline must match the R2 @@ -911,11 +912,11 @@ In the table, unqualified property test names are in | Exact 1/2 and 1/3 excess rates use an integer floor: excess is nonnegative and differs from the unrounded amount by less than $1 | readings `test_property_excess_uses_exact_statutory_rate_and_dollar_floor`; unit `test_lower_threshold_and_whole_dollar_floor`; unit `test_fra_threshold_uses_one_third_and_floors`; R1 differential above | | Invalid input is never counted as a statutory coverage gap | `test_coverage_denominators_keep_every_supported_or_refused_row`; unit `test_invalid_wage_inputs_raise_before_coverage_refusal`; `test_invalid_rates_raise_before_coverage_refusal`; `test_duplicate_identity_is_invalid_even_in_an_unsupported_record`; `test_invalid_inputs_are_not_counted_as_coverage_gaps` | | Every raw month-set element is validated before deduplication, independent of order; valid integer duplicates in the ledger preserve the same set | `test_every_raw_month_set_element_is_validated_before_deduplication`; `test_valid_month_set_duplicates_and_order_preserve_the_same_months`; grace `test_property_external_grace_month_sets_validate_before_deduplication`; unit `test_invalid_self_employment_month_collision_is_an_input_error`; `test_invalid_entitlement_month_collision_precedes_foreign_work_refusal` | -| Under the explicit POMS election reading, only eligible elected RIB NSMs receive grace protection; the conservative CFR reading supplies none during concurrent disability entitlement | readings `test_property_explicit_poms_election_protects_only_eligible_rib_months`; `test_simultaneous_dib_rib_source_readings_change_4200_dollars` | +| Under the default 202(k)(4) reading, only eligible elected RIB NSMs receive grace protection; the explicit CFR technical-entitlement alternative supplies none during concurrent disability entitlement | readings `test_property_explicit_poms_election_protects_only_eligible_rib_months`; `test_simultaneous_dib_rib_source_readings_change_4200_dollars` | | Actual and estimated grace statuses are distinct; changed service facts can require recovery even when annual earnings coincide | readings `test_estimated_initial_grace_and_actual_grace_are_separate`; `test_property_changed_grace_estimate_can_require_recovery` | | Zero payable caps retain their original shares and count toward the three-beneficiary sub-dollar stop; exact net shares plus the residual conserve the pool | `test_zero_cap_participants_preserve_total_and_trigger_source_stop`; unit `test_entitled_zero_rate_auxiliary_keeps_its_original_partial_share` | -| Explicit Act 203(f)(1)(C)-(D) protections exempt only the recipient's own work in asserted CHILD/WIDOW months | readings `test_property_other_statutory_own_exemptions_protect_only_that_workers_excess`; `test_own_work_exempt_flag_requires_a_statutory_benefit_type` | -| The test-only broader deemed-CDB reading agrees exactly with the supported married full-family charge; the Act/CFR reading preserves the unrelated record, while the public unmarried input always refuses | readings `test_property_deemed_counterfactual_matches_full_married_charge`; `test_unmarried_deemed_entitlement_counterfactual_changes_200_dollars` | +| Act 203(f)(1)(C)-(D) protections are the CDB/DWB types themselves; any asserted own-work exemption month is an input error | readings `test_own_work_exempt_months_are_never_a_lawful_assertion`; `test_optional_act_age_exemptions_are_an_explicit_600_dollar_reading` | +| The test-only broader deemed-CDB reading agrees exactly with the supported married full-family charge; the Act/CFR reading preserves the unrelated record, while the public unmarried input is always an input error | readings `test_property_deemed_counterfactual_matches_full_married_charge`; `test_unmarried_deemed_entitlement_counterfactual_changes_200_dollars` | The `ss/benefits.py` float oracle has no earnings-test ledger implementation, so there is no second RET-payment implementation against which to invent a @@ -1302,8 +1303,8 @@ production calls carried year by year against that model over invented months, concurrent entitlement, work conditions, and a single FRA date. The comparison covers grace kinds, applicability, eligible and protected months, LMETY, and pending eligibility at zero tolerance. It exercises -both the default CFR disability exclusion and the explicit POMS election -alternative. At December terminations, inputs explicitly label known +both the default 202(k)(4) reading (`POMS_RIB_ELECTION`) and the explicit +CFR technical-entitlement alternative. At December terminations, inputs explicitly label known January conversions as excluded, preserving the annual interface's existing requirement. Captured quotations are checked by `test_reference_rules_are_verbatim_in_manifest_captures`. This @@ -1334,28 +1335,31 @@ concurrent-disability default, the termination exclusions, refusal codes, or any ruling d1418 choice. No new statutory judgment or source erratum is introduced by these two corrections. -## Needs Max's judgment +## Readings taken and refusals kept -**Ruling d1418 (Max, 2026-10-12): keep the current defaults for these -readings, and reopen any one when a population target needs it.** The -computed defaults below (NESE losses floored; no non-service months during -simultaneous RIB and DIB entitlement, per 20 CFR 404.435(b)(3); own-work -protections limited to the CFR's disability-based wording) stand, and the -broader POMS deemed-entitlement reading stays refused. +**Ruling d1418 (Max, 2026-10-12)** kept these readings at their defaults +until a population target needed them. Max's 2026-10-20 delegation makes +each single reading the session's call under his principles (P1 statute +first; P2 regulations are law; P3 the agency breaks true ties), logged in +the source-conflict rulings; this section records the October 23, 2026 +pass. Two defaults change money: an elected RIB month during simultaneous +RIB and DIB entitlement now counts as a non-service month, and an unmarried +"deemed" recipient is no longer on the NH's ledger. The other readings +keep their computed defaults with the basis stated. The review resolved the historical monthly fractions and sub-dollar reapportionment stop directly from the captured Act and CFR text. Those -rules are implemented above and are no longer judgment items. The -remaining money-changing readings are explicit below. -The production decision comments link the retained readings to the -source-conflict register IDs `us:oasdi-ret-negative-nese-proration`, +rules are implemented above and are no longer judgment items. +The production decision comments link each reading to its source-conflict +register ID: `us:oasdi-ret-negative-nese-proration`, `us:oasdi-ret-grace-simultaneous-rib-dib`, -`us:oasdi-ret-own-work-age-exemptions` and -`us:oasdi-ret-deemed-entitlement-basis`. +`us:oasdi-ret-own-work-age-exemptions`, +`us:oasdi-ret-deemed-entitlement-basis` and +`us:oasdi-ret-other-deductee-scope`. `test_source_conflict_ids_are_linked_at_the_decision_lines` checks every -link at its decision, including both deemed-entitlement refusals. +link at its decision, including both deemed-entitlement decisions. -### Signed NESE: floor versus dropping cents +### Reading taken: signed NESE rounds down to the floor RS 02501.080B.2.c says: “prorate the net earnings (or loss) equally over all months the beneficiary was engaged in self-employment”. Its @@ -1366,8 +1370,22 @@ the cents” supports truncation toward zero, while “round down” supports mathematical floor. The two readings agree on the published positive NESE examples. -`NESEProrationPolicy.FLOOR` follows the literal “round down” instruction; -`DROP_CENTS` is an explicit alternative. In an invented June FRA year, +Kept: `NESEProrationPolicy.FLOOR` (one meaning per section; P3; a close +call). Act +203(f)(3) leaves the proration to the agency ("prorated in an equitable +manner under regulations of the Commissioner of Social Security"), and no +captured regulation fixes its rounding, so SSA's instruction governs. +Within RS 02501.080, "round down" means "round down to the next lower +dollar" (B.1.d, B.2.a). If "drop the cents" removes only the fraction of +a cent the division leaves (−$1.0833 to −$1.08), rounding down to the next +lower dollar then gives the floor, −$2, and both steps do work; if it +truncates to whole dollars (−$1), "round down" is idle. For positive +amounts the instruction's direction lowers countable earnings, which the +floor keeps for a loss. A blind adjudication read +"drop the cents" as truncating to whole dollars (leaving "round down" +idle) and preferred `DROP_CENTS`, which here matches exact proration's $2 +excess; the stake is at most a dollar's worth of countable earnings per +month of loss. `DROP_CENTS` remains an explicit alternative. In an invented June FRA year, annual NESE loss −$13 over twelve months becomes monthly −$2 and five-month −$10 under floor, versus monthly −$1 and five-month −$5 under dropping cents. With pre-FRA wages equal to R1's higher exempt @@ -1378,7 +1396,7 @@ conservative. Tests `test_negative_nese_floor_and_drop_cents_readings_are_explic and `test_negative_nese_reading_changes_final_deduction_by_one_dollar` pin the one-dollar difference. -### Simultaneous RIB and DIB entitlement +### Reading taken: an elected RIB month counts as a non-service month 20 CFR 404.435(b)(3) says: “we will not count as a non-service month, a month that occurred while the beneficiary was entitled to disability @@ -1388,55 +1406,62 @@ says: “If the beneficiary becomes simultaneously entitled to RIB and DIB benefits, the earnings test (ET) applies to any month they elect to receive RIB.” It adds: “Count NSMs for any month the RIB applies.” -`DisabilityGraceReading.CFR_ENTITLEMENT_EXCLUSION` is the conservative -default: technical disability entitlement prevents those NSMs. -`POMS_RIB_ELECTION` follows the express operational exception and is -needed to reproduce the POMS 2010/2011/2012 election example. Both -preserve technical entitlement history; neither invents a break when -the elected payment changes. The grace properties and -`test_simultaneous_disability_cfr_default_and_poms_alternative_are_explicit` -pin both readings. In an invented 2026 ledger, $45,000 January wages -precede entitlement to both RIB and DIB in June. The beneficiary elects -RIB at $600 monthly June-December and has no wages or services in those -months. Under the conservative reading all $4,200 is deducted; under -the POMS reading all $4,200 is paid. Test +Changed default (P1): Act 202(k)(4) says a person entitled to both an +old-age and a disability insurance benefit "shall be entitled to only the +larger of such benefits for such month, except that, if such individual so +elects, he shall instead be entitled to only the smaller of such benefits +for such month" (20 CFR 404.407(c) says the same). A month in which RIB is +paid is therefore a month of entitlement to RIB alone, not one "entitled to +disability benefits under section 223", and the regulation and RS +02501.030B.4 agree once the Act is read. `grace_year` now defaults to +`DisabilityGraceReading.POMS_RIB_ELECTION`; DWB and CDB months, which +202(k)(4) does not touch, stay excluded, and +`CFR_ENTITLEMENT_EXCLUSION` keeps the former technical-entitlement +exclusion as an explicit alternative. Both preserve technical entitlement +history; neither invents a break when the elected payment changes. In an +invented 2026 ledger, $45,000 January wages precede entitlement to both +RIB and DIB in June. The beneficiary elects RIB at $600 monthly +June-December and has no wages or services in those months. The default +now pays all $4,200; the alternative deducts it. Test `test_simultaneous_dib_rib_source_readings_change_4200_dollars` executes -that money difference, and -`test_property_explicit_poms_election_protects_only_eligible_rib_months` -executes the related ledger property. +that money difference; +`test_simultaneous_disability_default_follows_202k4_and_cfr_alternative_is_explicit` +pins the default and quotes; and the grace properties execute both +values over arbitrary months. -### Other own-work age clauses +### Reading taken: the Act and the CFR coincide on own-work age exemptions Act 203(f)(1)(C) prohibits charging a month “in which such individual, if a child entitled to child's insurance benefits, has attained the age of 18”. Clause (D) prohibits charging a month “for which such individual is entitled to widow's or widower's insurance benefits if such individual became so entitled prior to attaining age 60”. The preceding sentence -expressly says “but subject to section 202(s)”. This lane has no full -capture of 202(s) and does not adjudicate that additional condition. - -20 CFR 404.435(a)(5)-(6) instead list “Are age 18 or over and entitled -to a child's insurance benefit based on disability” and “Are entitled -to a widow's or widower's insurance benefit based on disability”. The -POMS payment-type exemption in RS 02501.021B.5.a also lists DIB/DWB/CDB. -The Act's express age clauses appear broader than those operational -disabled-benefit descriptions. - -The automatic default exemptions therefore follow the explicit -disability types. The documented `own_work_exempt_months` parameter -defaults to empty and is the alternative Act-clause assertion: the -caller must establish lawful entitlement, the age facts and any 202(s) -condition before asserting it. The ledger validates CHILD/WIDOW types -but does not derive those exemptions silently from an inferred age. -Max must decide how monthly assembly will establish and select this -broader reading. In invented $200 monthly ordinary CHILD and WIDOW -cases, earnings equal R1's lower amount plus $1,200. Default own -deductions total $600; the explicitly asserted Act exemption pays the -additional $600. Both cases are executed by -`test_optional_act_age_exemptions_are_an_explicit_600_dollar_reading`, -with the general selected-exemption property in the invariant table. - -### Broader POMS deemed entitlement +expressly says “but subject to section 202(s)”. 20 CFR 404.435(a)(5)-(6) +instead list “Are age 18 or over and entitled to a child's insurance +benefit based on disability” and “Are entitled to a widow's or widower's +insurance benefit based on disability”. The POMS payment-type exemption in +RS 02501.021B.5.a also lists DIB/DWB/CDB. + +No conflict remains once the rest of the Act is read (P1). Section +202(s)(3), now captured, says "subsection (f)(1)(C) of section 403 ... +shall not apply in the case of any child with respect to any month +referred to therein unless in such month or the third month prior thereto +such child was under a disability", so clause (C) reaches only a disabled +child. A widow(er) entitled before 60 is a disabled widow(er), and 402(e)(1) +ends that entitlement with "the termination month" ("the third month +following the month in which her disability ceases") unless retirement age +comes first, so clause (D)'s months are months of a benefit based on +disability. The automatic exemptions therefore follow the explicit +disability types, which is the Act's own scope. The former +`own_work_exempt_months` assertion had no lawful use (every month the Act +exempts is a CDB or DWB month), so any non-empty value is now +`InvalidFamilyInput`. In invented $200 monthly ordinary CHILD and WIDOW +cases, earnings equal R1's lower amount plus $1,200, and own deductions +total $600; the former assertion would have paid that $600. +`test_optional_act_age_exemptions_are_an_explicit_600_dollar_reading` and +`test_own_work_exempt_months_are_never_a_lawful_assertion` execute both. + +### Reading taken: deemed entitlement requires marriage to the NH Act 203(b)(1)'s third sentence says a recipient of child's benefits who has attained age 18, or mother's/father's benefits, “is married to an individual entitled to @@ -1448,65 +1473,63 @@ benefits on a third person's record is deemed entitled on the spouse's record. RS 02501.140 instead says: “deem a retirement beneficiary's spouse or child entitled on their record if they are receiving CDB, mother's or father's benefits, or child's benefits based on another -person's ER.” The POMS wording appears to reach an unmarried child -more broadly than the Act/CFR marriage requirement. The Act additionally -covers an entitled adult child without expressly requiring disability; -the CFR's stated child basis uses disability. This additional coverage -difference is refused for an ordinary CHILD even after the caller asserts -lawful age-18 and marriage facts, pending Max's reading. - -The default requires `deemed_married_to_nh=True` to assert the legal -marriage facts; broader unmarried cases receive -`DEEMED_ENTITLEMENT_BASIS`, as do ordinary deemed adult CHILD cases. -No monetary answer is fabricated for a -refused configuration. The lawful married benchmark has NH $400, -deemed-entitled CDB $200 and NH excess $600; the NH charge includes -the recipient's full $200 January benefit even though CDB's own -earnings test is exempt. Applying the broader wording to an otherwise -identical unmarried recipient would likewise remove $200 from that -recipient; treating the unrelated record independently leaves it -untouched. `test_unmarried_deemed_entitlement_is_refused_pending_source_judgment` -and `test_married_deemed_disabled_child_is_charged_number_holders_excess` -pin the refusal and the statutory benchmark. -`test_unmarried_deemed_entitlement_counterfactual_changes_200_dollars` -now executes both alternative readings with an explicit test-only -full-January-charge helper: the broader POMS reading pays $0 instead -of $200 on the unrelated recipient's record. Annual checks are $2,200 -versus $2,400 in the otherwise identical married benchmark and separate -unrelated record. It checks the public unmarried input still refuses. -`test_property_deemed_counterfactual_matches_full_married_charge` compares -the narrow helper with the supported married ledger and separate record -over invented whole-dollar rates, with zero tolerance. This helper does -not change marriage facts or enable the broader reading through the -public API; ruling d1418's defaults remain in effect. -The Act's ordinary-adult-child wording would include the same $200 in -that benchmark. The CFR disability-only boundary refuses that ordinary -child case. `test_deemed_ordinary_child_requires_explicit_adult_age_facts` -pins invalid missing-age facts and the default coverage refusal with -established age facts; the married CDB benchmark pins the supported -$200 NH deduction. - -### Scope wording for another deductee - -The part-(a) brief asks for refusal when an auxiliary's own excess is -charged with a binding maximum “or any other deductee on the record -that month”. It also requires reproduction of 20 CFR 404.441, whose -rule expressly covers the same-month interaction: “Where both the -insured individual and another person entitled (or deemed entitled) -on the same earnings record have excess earnings”. Act 203(b)(1) -likewise charges a beneficiary's remaining benefits after an earlier -deduction, and 20 CFR 404.434(b)(3) specifies NH charging first. - -This build interprets the other-deductee refusal as other deduction -events under Act 203(c)/(d)/222(b), represented by -`other_deduction_months`, which are always refused. Simultaneous NH -and auxiliary RET deductions without a binding maximum are supported -so the explicitly required CFR .441 example can be reproduced. That -example gives the wife's August $83 NH partial share, fully consumed -by her own excess, and her October $82 check. Interpreting the brief -as excluding any simultaneous RET deductee would instead refuse the -example, not produce a different computed amount. This scope reading -needs confirmation if the intended part-(a) boundary was broader. +person's ER.” + +Changed (P1; P2 agrees): the Act and the regulation both require the +marriage, so RS 02501.140's broader wording yields. An unmarried recipient +is not deemed and is not on the NH's ledger: declaring `deemed_entitled` +without `deemed_married_to_nh` is now `InvalidFamilyInput`, and the +recipient is computed on their own record. The lawful married benchmark +has NH $400, deemed-entitled CDB $200 and NH excess $600; the NH charge +includes the recipient's full $200 January benefit even though CDB's own +earnings test is exempt. The broader POMS wording would remove the same +$200 from an unmarried recipient; the reading taken leaves it paid on that +recipient's own record (annual $2,400 versus $2,200 in the married +benchmark). `test_unmarried_deemed_entitlement_is_an_input_error`, +`test_unmarried_deemed_entitlement_counterfactual_changes_200_dollars` and +`test_property_deemed_counterfactual_matches_full_married_charge` pin the +error, the $200 difference and the exact agreement of the test-only +broader harness with the supported married ledger. + +Refusal kept for the residual adult-child case: the Act's words also +reach "a child who has attained the age of 18 and is entitled to child's +insurance benefits", where 20 CFR 404.420 says "a child's insurance +benefit based on the child's disability". A child's entitlement ends +"with the month preceding" the month the child marries (402(d)(1)(D)), +and 402(d)(5)'s exception for marrying a beneficiary applies only if the +child "at the time of the marriage ... was under a disability ... or had +been under such a disability in the third month before the month in which +such marriage occurred" (402(s)(2)). So a married, non-disabled adult +child keeps child's benefits only in the residual case of a marriage that +qualified under (s)(2) (disability in the marriage month or in the third +month before it), with recovery before or after the marriage, which the +regulation omits. That configuration stays refused as +`DEEMED_ENTITLEMENT_BASIS`, with its row and weight kept; +`test_deemed_ordinary_child_requires_explicit_adult_age_facts` pins +invalid missing-age facts and the refusal, and the married CDB benchmark +pins the supported $200 NH deduction. + +### Not a source conflict: another deductee's scope + +Logged as `us:oasdi-ret-other-deductee-scope`. The part-(a) brief asks for +refusal when an auxiliary's own excess is charged with a binding maximum +“or any other deductee on the record that month”. It also requires +reproduction of 20 CFR 404.441, whose rule expressly covers the same-month +interaction: “Where both the insured individual and another person +entitled (or deemed entitled) on the same earnings record have excess +earnings”. Act 203(b)(1) likewise charges a beneficiary's remaining +benefits after an earlier deduction, and 20 CFR 404.434(b)(3) specifies +NH charging first. + +The regulation settles the computation (P2), so the brief's +other-deductee refusal is read as other deduction events under Act +203(c)/(d)/222(b), represented by `other_deduction_months`, which are +always refused. Simultaneous NH and auxiliary RET deductions without a +binding maximum are supported so the explicitly required CFR .441 example +can be reproduced. That example gives the wife's August $83 NH partial +share, fully consumed by her own excess, and her October $82 check. +Interpreting the brief as excluding any simultaneous RET deductee would +instead refuse the example, not produce a different computed amount. ## Deduction before reduction and the G contract diff --git a/scripts/track_b_g_conformance_report.py b/scripts/track_b_g_conformance_report.py index 0d0deb9f..ffee9463 100644 --- a/scripts/track_b_g_conformance_report.py +++ b/scripts/track_b_g_conformance_report.py @@ -445,7 +445,7 @@ def multiple_records(child): ), ), reasons.PARENT_BENEFIT: ( - "Two potential parents; one own RIB 780 lies between 75% and 82.5% of PIA 1000", + "Two potential parents; own RIBs 780 and 790 both lie between 75% and 82.5% of PIA 1000", lambda: _family( _survivor(), [ @@ -454,7 +454,11 @@ def multiple_records(child): g.Role.PARENT, own_benefit=g.OwnBenefit(rib, F(780)), ), - g.Beneficiary("p2", g.Role.PARENT), + g.Beneficiary( + "p2", + g.Role.PARENT, + own_benefit=g.OwnBenefit(rib, F(790)), + ), ], ), ), diff --git a/src/populace_dynamics/track_b/gross_benefits.py b/src/populace_dynamics/track_b/gross_benefits.py index 5bbe3d26..f55073ff 100644 --- a/src/populace_dynamics/track_b/gross_benefits.py +++ b/src/populace_dynamics/track_b/gross_benefits.py @@ -1012,6 +1012,7 @@ def statutory_rates(params: SSAParameters) -> StatutoryRates: _input_type(getattr(params, name), int, name) if getattr(params, name) != 36: raise InvalidFamilyInput(f"{name} must be 36 (402(q))") + # Source conflict us:oasdi-reduction-exact-fractions return _construct_declared( StatutoryRates, worker_first=_STATUTE["worker_first"], @@ -1118,6 +1119,7 @@ def disability_family_maximum(pia: object, aime: object) -> Fraction: earnings = _exact(aime, "AIME") if earnings < 0 or earnings.denominator != 1: raise InvalidFamilyInput("AIME must be a nonnegative whole dollar") + # Source conflict us:oasdi-disability-family-maximum-rounding return _floor_dime_validated( min(max(_DI_AIME_SHARE * earnings, amount), _DI_PIA_CAP * amount) ) @@ -3331,18 +3333,33 @@ class _Row: counted: Fraction = Fraction(0) mother_father_rate: Fraction = Fraction(0) note: str = "" + dwb_under_60: bool = False @property def dual(self) -> bool: return self.own_mba is not None +def _dwb_under_60(beneficiary: Beneficiary, payment_month: YearMonth) -> bool: + """A DWB month in which the SSA birth cohort cannot yet have attained 60. + + Cohort Y (born January 2, Y through January 1, Y+1) attains 60 during + Y+60, so every month of Y+59 and earlier is under 60. Months of Y+60 + depend on the unsupplied birth month and are not treated as under 60. + """ + return ( + beneficiary.role is Role.DISABLED_WIDOW + and beneficiary.birth_year is not None + and payment_month.year <= beneficiary.birth_year + 59 + ) + + def _is_entitled( beneficiary: Beneficiary, pia: Fraction, own_mba: Fraction | None, *, - parent_ob: Fraction | None = None, + parent_threshold: Fraction | None = None, ) -> tuple[bool, str]: own = beneficiary.own_benefit if own is None: @@ -3362,9 +3379,11 @@ def _is_entitled( if ( beneficiary.role is Role.PARENT and own.kind is OwnBenefitKind.OLD_AGE - and own_mba >= parent_ob + and own_mba >= parent_threshold ): - return False, "own RIB not less than parent's OB (RS 00209.001)" + return False, ( + "own RIB not less than the 402(h)(1)(D) share of the PIA" + ) if ( beneficiary.role is Role.MOTHER_FATHER and own.kind is OwnBenefitKind.OLD_AGE @@ -3466,7 +3485,8 @@ def _parisi_counted( The own-RIB reduction is not an auxiliary age reduction (GN 02603.060B.2: $2.40 payable + $0.90 reduction, not the $16 own-RIB - reduction). GN 02603.050A.2 disregards a dual rate reduced to zero. + reduction). GN 02603.050A.2 disregards a dual rate that is zero before + age reduction; a zero reached only after it counts the age reduction. Own delayed credits and redistribution with an actually limiting RIB-LIM remain refused: none of the captured worked cases settles those interacting amounts. A considered but nonlimiting RIB-LIM @@ -3509,17 +3529,14 @@ def _parisi_counted( ) if payable == 0: before_age = row.standard - (own.pia if method_c else row.own_mba) + # GN 02603.050A.2 disregards a rate that dual entitlement, considered + # first, reduces to zero (.045; .060 A.8). A zero reached only after + # the auxiliary's age reduction keeps that reduction in PTAP: only + # the amount withheld for dual entitlement in 402(k)(3)(A)'s order is + # redistributed (20 CFR 404.403(a)(5), 404.407(a); .060 B.2). # Source conflict us:oasdi-dual-entitlement-zero-after-age-reduction - if before_age > 0: - raise _computed( - FamilyConfigurationUnsupported, - UnsupportedReason.PARISI_AMBIGUOUS, - "dual payable reaches zero only after auxiliary age reduction: " - "GN 02603.045's before-age rule and GN 02603.050A.2's zero-rate " - "exception do not settle whether the age amount stays in PTAP", - beneficiary_id=beneficiary.beneficiary_id, - ) - return Fraction(0), Fraction(0) + if before_age <= 0: + return Fraction(0), Fraction(0) if method_c: excess = max(Fraction(0), row.standard - own.pia) age_reduction = excess - _spouse_age_reduced_validated( @@ -3563,6 +3580,12 @@ def _refuse_historical_auxiliary( if isinstance(beneficiary.child_in_care, str): return elif role is Role.DISABLED_WIDOW: + # Source conflict us:oasdi-dwb-1984-pre-1978-history + # RS 00615.301B.1.e: "all disabled widow(er)s under age 60 receive + # .715 of the full unreduced WIB for 1/84 and later", whatever the + # entitlement date; .005C.2's money history governs only from 60. + if row.dwb_under_60: + return first_age = 50 elif role in _AGED_WIDOW_ROLES: first_age = 50 @@ -3582,6 +3605,7 @@ def _refuse_historical_auxiliary( own = beneficiary.own_benefit low, high = Fraction(0), row.rate if role in (Role.SPOUSE, Role.DIVORCED_SPOUSE): + # Source conflict us:oasdi-dual-entitlement-pre-1978-history if own is not None and beneficiary.own_benefit_first: excess = max(Fraction(0), row.rate - own.pia) high = max(Fraction(0), row.rate - (own.pia - row.own_base)) @@ -3837,16 +3861,44 @@ def _check_family_inputs( return beneficiaries +def _parent_entitlement_thresholds(pia: Fraction) -> tuple[Fraction, Fraction]: + """402(h)(1)(D): 82.5 and 75 percent of the PIA, unrounded.""" + return pia * Fraction(165, 200), pia * Fraction(3, 4) + + +def _parent_threshold( + pia: Fraction, parents: tuple[int, frozenset[str]], beneficiary_id: str +) -> Fraction: + """One parent's 402(h)(1)(D) own-RIB threshold in the resolved set. + + ``parents`` is ``_entitled_parent_count``'s (count, excluded ids). An + entitled parent is tested at its own rate; an excluded parent at the + two-parent rate it would share. + """ + parent_count, excluded = parents + one_rate, two_rate = _parent_entitlement_thresholds(pia) + if parent_count == 2 or beneficiary_id in excluded: + return two_rate + return one_rate + + def _entitled_parent_count( state: RecordState, beneficiaries: Sequence[Beneficiary], params: SSAParameters, -) -> int: - """Resolve 402(h)(2)'s count, refusing the circular RIB threshold. - - RS 00209.001 excludes a parent whose own RIB is not less than the - parent's benefit; RS 00209.015 counts an own-DIB parent as technically - entitled. More than two declared parents needs a separate rule. +) -> tuple[int, frozenset[str]]: + """Resolve 402(h)(2)'s count and the parents (h)(1)(D) excludes. + + 402(h)(1)(D) tests an own RIB against 82.5 percent of the PIA when the + parent's benefit "is determinable under paragraph (2)(A)", and against + 75 percent "in any other case". The entitled set must satisfy that test + for every parent at once: each entitled parent at its own rate, and + each excluded potential parent at the rate it would share. RS + 00209.015 counts an own-DIB parent as technically entitled. The + thresholds are the unrounded percentages of the PIA that (h)(1)(D) + names, not the dime-floored OBs. Returns the count and the ids excluded + at the two-parent rate. More than two declared parents needs a + separate rule. """ parents = [b for b in beneficiaries if b.role is Role.PARENT] if len(parents) > 2: @@ -3856,12 +3908,7 @@ def _entitled_parent_count( "more than two declared parents is outside the one/two-parent " "rates settled by 402(h)(2) and RS 00209.015", ) - one_ob = _original_benefit_validated( - Role.PARENT, FamilyKind.SURVIVOR, state.pia - ) - two_ob = _original_benefit_validated( - Role.PARENT, FamilyKind.SURVIVOR, state.pia, parent_count=2 - ) + one_rate, two_rate = _parent_entitlement_thresholds(state.pia) potential = [] for parent in parents: own = parent.own_benefit @@ -3871,26 +3918,31 @@ def _entitled_parent_count( else _own_monthly_benefit_validated(own, params)[1] ) if own is not None and own.kind is OwnBenefitKind.OLD_AGE: - if own_mba >= one_ob: + if own_mba >= one_rate: continue potential.append((parent, own_mba)) - if len(potential) == 2: - for parent, own_mba in potential: - # Source conflict us:oasdi-parent-entitlement-count-interval - if ( - parent.own_benefit is not None - and parent.own_benefit.kind is OwnBenefitKind.OLD_AGE - and own_mba >= two_ob - ): - raise _computed( - FamilyConfigurationUnsupported, - UnsupportedReason.PARENT_BENEFIT, - "two potential parents with own RIB between the " - "75% and 82.5% OBs have a circular entitlement count " - "under RS 00209.001 and .015", - beneficiary_id=parent.beneficiary_id, - ) - return max(1, len(potential)) + in_band = [ + parent + for parent, own_mba in potential + if parent.own_benefit is not None + and parent.own_benefit.kind is OwnBenefitKind.OLD_AGE + and own_mba >= two_rate + ] + if len(potential) < 2 or not in_band: + return max(1, len(potential)), frozenset() + # Source conflict us:oasdi-parent-entitlement-count-interval + # A RIB in [75%, 82.5%) fails beside an entitled parent and passes + # alone. With one such parent, only "the other parent alone" satisfies + # 402(h)(1)(D) for both; with two, either parent alone does. + if len(in_band) == 2: + raise _computed( + FamilyConfigurationUnsupported, + UnsupportedReason.PARENT_BENEFIT, + "two parents with own RIB between 75% and 82.5% of the PIA: " + "402(h)(1)(D) is satisfied by either parent alone", + beneficiary_id=in_band[0].beneficiary_id, + ) + return 1, frozenset({in_band[0].beneficiary_id}) def _mother_father_care_rate( @@ -4051,16 +4103,17 @@ def possible_drc_basis(member): possible_count += 1 possible_widow |= possible_drc_basis(member) continue - parent_count = ( + parents = ( _entitled_parent_count(state, beneficiaries, params) if member.role is Role.PARENT - else 1 + else (1, frozenset()) ) row = _original_entitlement_rows( state, (_with_effective_cohort(member),), params, - parent_count, + payment_month, + parents, )[0] subject = row.subject except FamilyConfigurationUnsupported: @@ -4072,11 +4125,15 @@ def possible_drc_basis(member): def _original_entitlement_rows( - state, beneficiaries, params, parent_count=None + state, beneficiaries, params, payment_month, parents=None ): - """G's OB and own-benefit entitlement, before any family allocation.""" - if parent_count is None: - parent_count = _entitled_parent_count(state, beneficiaries, params) + """G's OB and own-benefit entitlement, before any family allocation. + + ``parents`` is ``_entitled_parent_count``'s (count, excluded ids) for + the whole roster; an excluded parent is tested at the two-parent rate. + """ + if parents is None: + parents = _entitled_parent_count(state, beneficiaries, params) rows = [] for beneficiary in beneficiaries: own_base = own_mba = None @@ -4090,11 +4147,16 @@ def _original_entitlement_rows( state.pia, beneficiary.original_benefit_basis, parent_count=( - parent_count if beneficiary.role is Role.PARENT else None + parents[0] if beneficiary.role is Role.PARENT else None ), ) entitled, note = _is_entitled( - beneficiary, state.pia, own_mba, parent_ob=ob + beneficiary, + state.pia, + own_mba, + parent_threshold=_parent_threshold( + state.pia, parents, beneficiary.beneficiary_id + ), ) rows.append( _Row( @@ -4105,6 +4167,7 @@ def _original_entitlement_rows( own_base=own_base, own_mba=own_mba, note=note, + dwb_under_60=_dwb_under_60(beneficiary, payment_month), ) ) return rows @@ -4187,7 +4250,7 @@ def _family_benefits( "IEDS before January 1985, or on a WEP-affected worker record " "before January 2024, is not implemented (RS 00202.100)", ) - parent_count = _entitled_parent_count(state, beneficiaries, params) + parents = _entitled_parent_count(state, beneficiaries, params) life_case = state.kind is not FamilyKind.SURVIVOR and not ieds worker_benefit = None if _declared_worker_entitled(state): @@ -4199,7 +4262,11 @@ def _family_benefits( worker_benefit = state.pia rows = _original_entitlement_rows( - state, beneficiaries, params, parent_count + state, + beneficiaries, + params, + payment_month, + parents, ) _refuse_widow_drc_savings_clause(state, rows) @@ -4861,6 +4928,7 @@ def _readjusted_for_events( ), Fraction(0), ) + # Source conflict us:oasdi-nh-charge-age-reduction-interaction if reserve and any(row.age_reduction_factor != 1 for row in subject): raise _computed( FamilyConfigurationUnsupported, @@ -5061,6 +5129,7 @@ def _readjust_deduction_rows( standard=old.standard_rate, rate=rate, counted=rate, + dwb_under_60=_dwb_under_60(member, payment_month), ) minimum = care_minimums.get(bid) if minimum is not None: @@ -5744,6 +5813,7 @@ def _savings_payment( row for row in result.beneficiaries if row.subject_to_family_maximum ] reduced = [row for row in subject if row.age_reduction_factor < 1] + # Source conflict us:oasdi-saving-clause-procedure-gaps if ( result.kind is FamilyKind.SURVIVOR or any(row.own_benefit is not None for row in subject) @@ -6069,6 +6139,7 @@ def _operational_admit( total: Fraction, ) -> FamilyBenefits: membership = _savings_membership(result) + # Source conflict us:oasdi-saving-clause-procedure-gaps if ( self._protected_membership is not None and membership != self._protected_membership diff --git a/src/populace_dynamics/track_b/ret_grace.py b/src/populace_dynamics/track_b/ret_grace.py index 26849351..0e7ba7a4 100644 --- a/src/populace_dynamics/track_b/ret_grace.py +++ b/src/populace_dynamics/track_b/ret_grace.py @@ -84,12 +84,16 @@ class GraceKind(Enum): class DisabilityGraceReading(Enum): - """Explicit money-changing CFR/POMS reading for concurrent disability. - - 404.435(b)(3) excludes any month entitled to disability benefits; POMS - RS 02501.030B.4/.021B.5.a explicitly excepts simultaneous RIB/DIB when - RIB is elected. The default preserves the CFR's conservative exclusion. - The POMS alternative is necessary to reproduce its election example. + """Explicit money-changing reading for concurrent RIB and DIB. + + 404.435(b)(3) excludes a month "while the beneficiary was entitled to + disability benefits under section 223". Act 202(k)(4) makes a person + entitled to both "entitled to only the larger of such benefits for such + month", or, on election, "only the smaller": a month RIB is paid is a + month of entitlement to RIB alone, so the default counts its NSMs, as + RS 02501.030B.4 does. CFR_ENTITLEMENT_EXCLUSION keeps the former + technical-entitlement exclusion as an explicit alternative. DWB and CDB + months stay excluded under either value. """ CFR_ENTITLEMENT_EXCLUSION = "cfr_entitlement_exclusion" @@ -151,6 +155,10 @@ def __post_init__(self): raise InvalidFamilyInput( "Received benefit types must be entitled benefit types" ) + if {BenefitType.RIB, BenefitType.DIB} <= received: + raise InvalidFamilyInput( + "A month pays RIB or DIB, not both (Act 202(k)(4))" + ) object.__setattr__(self, "entitled_types", entitled) object.__setattr__(self, "received_types", received) @@ -387,7 +395,7 @@ def grace_year( fra_month: int | None = None, termination_events: Sequence[TerminationEvent] = (), disability_reading: DisabilityGraceReading = ( - DisabilityGraceReading.CFR_ENTITLEMENT_EXCLUSION + DisabilityGraceReading.POMS_RIB_ELECTION ), ) -> GraceYearDecision: """Apply RS 02501.030B-D for one person's calendar entitlement year. @@ -449,6 +457,7 @@ def grace_year( if month.received_types - DISABILITY_TYPES and (fra_month is None or index < fra_month) and ( + # 202(k)(4): a month RIB is paid is entitlement to RIB alone. # Source conflict us:oasdi-ret-grace-simultaneous-rib-dib not month.entitled_types & DISABILITY_TYPES or ( diff --git a/src/populace_dynamics/track_b/ret_ledger.py b/src/populace_dynamics/track_b/ret_ledger.py index 288115a9..8306230f 100644 --- a/src/populace_dynamics/track_b/ret_ledger.py +++ b/src/populace_dynamics/track_b/ret_ledger.py @@ -255,19 +255,24 @@ class RETBeneficiary: least two years since divorce; prior NH RIB entitlement is the other Act 203(b)(2) exception. ``deemed_entitled`` and ``deemed_married_to_nh`` assert the married recipient in Act 203(b)(1) - / CFR .420. An ordinary child's Act 203(b)(1) age facts need explicit - ``deemed_child_age_18_months``; that Act/CFR coverage difference is - refused pending judgment. Broader unmarried-child wording in - RS 02501.140 is also refused. Own two-record excess is still refused. + / CFR .420; both require the marriage, so an unmarried deemed recipient + (RS 02501.140's broader wording) is an input error: that person is not + on this NH's ledger. An ordinary child's Act 203(b)(1) age facts need + explicit ``deemed_child_age_18_months``; such a married non-disabled + child is entitled only if 402(d)(5) kept the benefit through a marriage + in whose month, or in the third month before which, the child was + under a disability (402(s)(2)); recovery may come before or after the + marriage. CFR .420 omits that residual case, so it stays refused. Own + two-record excess is still refused. ``two_record_entitlement=None`` derives entitlement from supplied G monthly own-benefit facts, or asserts no known two-record entitlement without context. An explicit boolean must agree with those G facts. - ``own_work_exempt_months`` supplies other Act - 203(f)(1)(C)-(D) protections (entitled child who has attained 18 / - widow(er) entitled before 60). It defaults to empty under the CFR's - disability-type wording. Selecting this Act-clause alternative - asserts lawful entitlement, age facts and any 202(s) condition; the - ledger does not infer those facts or adjudicate that condition. + ``own_work_exempt_months`` must stay empty: Act 203(f)(1)(C) reaches + only a child under a disability that month or three months before + (202(s)(3)), and (D)'s widow(er) entitled before 60 is a disabled + widow(er) whose entitlement ends at the termination month (402(e)(1)), + so every month the Act exempts is a CDB or DWB month, which the ledger + exempts automatically. """ beneficiary_id: str @@ -360,14 +365,20 @@ def __post_init__(self): raise InvalidFamilyInput( "Divorce exception months need divorced-spouse benefits" ) - if any( - types[m - 1] not in (BenefitType.CHILD, BenefitType.WIDOW) - for m in self.own_work_exempt_months - ): + if self.own_work_exempt_months: raise InvalidFamilyInput( - "Child or widow benefits are required for other own-work exemptions" + "Act 203(f)(1)(C)-(D) exempt only disability-based months " + "(202(s)(3); 402(e)(1)); declare them as CDB or DWB" ) if self.deemed_entitled: + # Act 203(b)(1) and 20 CFR 404.420 deem only a recipient married + # to the NH; RS 02501.140's broader wording yields to them. + # Source conflict us:oasdi-ret-deemed-entitlement-basis + if not self.deemed_married_to_nh: + raise InvalidFamilyInput( + "Deemed entitlement requires marriage to the NH " + "(Act 203(b)(1); 20 CFR 404.420)" + ) if any( types[m - 1] not in ( @@ -1128,7 +1139,6 @@ def _protected( or (b.fra_month is not None and month >= b.fra_month) # Source conflict us:oasdi-ret-own-work-age-exemptions or b.benefit_types[month - 1] in DISABILITY_TYPES - or month in b.own_work_exempt_months or ( grace is not None and grace.applies @@ -1263,27 +1273,24 @@ def settled( raise InvalidFamilyInput( "G context does not match RET rates" ) - parent_count = None - parent_count_known = False + parents = None for auxiliary in context.beneficiaries: auxiliary = g._with_effective_cohort(auxiliary) person = people[auxiliary.beneficiary_id] original = None if settled("F4", auxiliary) or settled("F6", auxiliary): try: - if auxiliary.role is g.Role.PARENT: - if not parent_count_known: - parent_count = g._entitled_parent_count( - state, context.beneficiaries, context.params - ) - parent_count_known = True + if auxiliary.role is g.Role.PARENT and parents is None: + parents = g._entitled_parent_count( + state, context.beneficiaries, context.params + ) original = g._original_benefit_validated( auxiliary.role, state.kind, state.pia, auxiliary.original_benefit_basis, parent_count=( - parent_count + parents[0] if auxiliary.role is g.Role.PARENT else None ), @@ -1305,7 +1312,16 @@ def settled( # fails holds on every filling of the missing amount. own_mba = Fraction(0) entitled, _ = g._is_entitled( - auxiliary, state.pia, own_mba, parent_ob=original + auxiliary, + state.pia, + own_mba, + parent_threshold=( + g._parent_threshold( + state.pia, parents, auxiliary.beneficiary_id + ) + if auxiliary.role is g.Role.PARENT + else None + ), ) if not entitled: raise InvalidFamilyInput( @@ -1422,6 +1438,7 @@ def apply_ret_year( record.vocational_rehabilitation_refusal, UnsupportedReason.VOCATIONAL_REHABILITATION, ), + # Source conflict us:oasdi-ret-other-deductee-scope ( bool(record.other_deduction_months), UnsupportedReason.OTHER_DEDUCTION, @@ -1436,14 +1453,6 @@ def apply_ret_year( ) for b in record.beneficiaries: # Source conflict us:oasdi-ret-deemed-entitlement-basis - if b.deemed_entitled and not b.deemed_married_to_nh: - raise _computed( - RETConfigurationUnsupported, - UnsupportedReason.DEEMED_ENTITLEMENT_BASIS, - "Unmarried deemed entitlement: RS .140 is broader than Act 203(b)(1) / CFR .420", - beneficiary_id=b.beneficiary_id, - ) - # Source conflict us:oasdi-ret-deemed-entitlement-basis if b.deemed_entitled and any( b.benefit_types[m - 1] is BenefitType.CHILD for m in b.entitled_months @@ -1451,7 +1460,8 @@ def apply_ret_year( raise _computed( RETConfigurationUnsupported, UnsupportedReason.DEEMED_ENTITLEMENT_BASIS, - "Act adult-child basis versus CFR .420 disability basis needs judgment", + "A married non-disabled adult child keeps CIB only through " + "402(d)(5) and 402(s)(2); CFR .420 omits that residual case", beneficiary_id=b.beneficiary_id, ) people = { diff --git a/tests/README-tiers.md b/tests/README-tiers.md index ec423752..edb45450 100644 --- a/tests/README-tiers.md +++ b/tests/README-tiers.md @@ -39,9 +39,9 @@ pytest --collect-only -q -m oracle_policyengine | tail -1 | Tier | Tests at HEAD | |---|---:| -| `unit` | 26,654 | +| `unit` | 26,675 | | `artifact` | 7,141 | | `integration_psid` | 1,471 | | `reproduction_legacy` | 520 | | `oracle_policyengine` | 337 | -| **Total** | **36,123** | +| **Total** | **36,144** | diff --git a/tests/data/track_b/gross_benefit_sources/lii_usc_402_415_excerpts.txt b/tests/data/track_b/gross_benefit_sources/lii_usc_402_415_excerpts.txt index 877f18bb..926c1aad 100644 --- a/tests/data/track_b/gross_benefit_sources/lii_usc_402_415_excerpts.txt +++ b/tests/data/track_b/gross_benefit_sources/lii_usc_402_415_excerpts.txt @@ -22,3 +22,27 @@ substring of that normalized text; the manifest records the full served bodies. [415(i)(2)(A)(ii)-(iii)] (ii) If the Commissioner of Social Security determines that the base quarter in any year is a cost-of-living computation quarter , the Commissioner shall, effective with the month of December of that year as provided in subparagraph (B), increase— (I) the benefit amount to which individuals are entitled for that month under section 427 or 428 of this title, (II) the primary insurance amount of each other individual on which benefit entitlement is based under this subchapter, and (III) the amount of total monthly benefits based on any primary insurance amount which is permitted under section 403 of this title (and such total shall be increased, unless otherwise so increased under another provision of this subchapter, at the same time as such primary insurance amount) or, in the case of a primary insurance amount computed under subsection (a) as in effect (without regard to the table contained therein) prior to January 1979, the amount to which the beneficiaries may be entitled under section 403 of this title as in effect in December 1978, except as provided by section 403(a)(7) and (8) of this title as in effect after December 1978. The increase shall be derived by multiplying each of the amounts described in subdivisions (I), (II), and (III) (including each of those amounts as previously increased under this subparagraph) by the applicable increase percentage ; and any amount so increased that is not a multiple of $0.10 shall be decreased to the next lower multiple of $0.10. Any increase under this subsection in a primary insurance amount determined under subparagraph (C)(i) of subsection (a)(1) shall be applied after the initial determination of such primary insurance amount under that subparagraph (with the amount of such increase, in the case of an individual who becomes eligible for old-age or disability insurance benefits or dies in a calendar year after 1979, being determined from the range of possible primary insurance amounts published by the Commissioner of Social Security under the last sentence of subparagraph (D)). (iii) In the case of an individual who becomes eligible for an old-age or disability insurance benefit, or who dies prior to becoming so eligible, in a year in which there occurs an increase provided under clause (ii), the individual's primary insurance amount (without regard to the time of entitlement to that benefit) shall be increased (unless otherwise so increased under another provision of this subchapter and, with respect to a primary insurance amount determined under subsection (a)(1)(C)(i)(I) in the case of an individual to whom that subsection (as in effect in December 1981) applied, subject to the provisions of subsection (a)(1)(C)(i) and clauses (iv) and (v) of this subparagraph (as then in effect)) by the amount of that increase and subsequent applicable increases, but only with respect to benefits payable for months after November of that year. + +[402(d)(1), closing clause through (D)] +and ending with the month preceding whichever of the following first occurs— (D) the month in which such child dies, or marries, + +[402(d)(5)] +(5) In the case of a child who has attained the age of eighteen and who marries— (A) an individual entitled to benefits under subsection (a), (b), (c), (e), (f), (g), or (h) of this section or under section 423(a) of this title , or (B) another individual who has attained the age of eighteen and is entitled to benefits under this subsection, such child 's entitlement to benefits under this subsection shall, notwithstanding the provisions of paragraph (1) but subject to subsection (s), not be terminated by reason of such marriage. + +[402(e)(1), termination month] +or, if she became entitled to such benefits before she attained age 60, subject to section 423(e) of this title , the termination month (unless she attains retirement age (as defined in section 416( l ) of this title) on or before the last day of such termination month). For purposes of the preceding sentence, the termination month for any individual shall be the third month following the month in which her disability ceases; + +[402(h)(1)(D)] +(D) is not entitled to old-age insurance benefits, or is entitled to old-age insurance benefits each of which is less than 82½ percent of the primary insurance amount of such deceased individual if the amount of the parent 's insurance benefit for such month is determinable under paragraph (2)(A) (or 75 percent of such primary insurance amount in any other case), and + +[402(h)(2)(A)-(B)] +(2) (A) Except as provided in subparagraphs (B) and (C), such parent 's insurance benefit for each month shall be equal to 82½ percent of the primary insurance amount of such deceased individual. (B) For any month for which more than one parent is entitled to parent 's insurance benefits on the basis of such deceased individual's wages and self-employment income, such benefit for each such parent for such month shall (except as provided in subparagraph (C)) be equal to 75 percent of the primary insurance amount of such deceased individual. + +[402(k)(3)(A)] +(3) (A) If an individual is entitled to an old-age or disability insurance benefit for any month and to any other monthly insurance benefit for such month, such other insurance benefit for such month, after any reduction under subsection (q), subsection (e)(2) or (f)(2), and any reduction under section 403(a) of this title , shall be reduced, but not below zero, by an amount equal to such old-age or disability insurance benefit (after reduction under such subsection (q)). + +[402(k)(4)] +(4) Any individual who, under this section and section 423 of this title , is entitled for any month to both an old-age insurance benefit and a disability insurance benefit under this subchapter shall be entitled to only the larger of such benefits for such month, except that, if such individual so elects, he shall instead be entitled to only the smaller of such benefits for such month. + +[402(s)] +(s) Child over specified age to be disregarded for certain benefit purposes unless disabled (1) For the purposes of subsections (b)(1), (c)(1), (g)(1), (q)(5), and (q)(7) of this section and paragraphs (2), (3), and (4) of section 403(c) of this title , a child who is entitled to child' s insurance benefits under subsection (d) for any month, and who has attained the age of 16 but is not in such month under a disability (as defined in section 423(d) of this title ), shall be deemed not entitled to such benefits for such month, unless he was under such a disability in the third month before such month. (2) So much of subsections (b)(3), (c)(4), 1 (d)(5), (g)(3), and (h)(4) of this section as precedes the semicolon, shall not apply in the case of any child unless such child, at the time of the marriage referred to therein, was under a disability (as defined in section 423(d) of this title ) or had been under such a disability in the third month before the month in which such marriage occurred. (3) The last sentence of subsection (c) of section 403 of this title , subsection (f)(1)(C) of section 403 of this title , and subsections (b)(3)(B), (c)(6)(B), 1 (f)(3)(B), and (g)(6)(B) 1 of section 416 of this title shall not apply in the case of any child with respect to any month referred to therein unless in such month or the third month prior thereto such child was under a disability (as defined in section 423(d) of this title ). diff --git a/tests/data/track_b/gross_benefit_sources/manifest.json b/tests/data/track_b/gross_benefit_sources/manifest.json index 30fc872b..7ed002ef 100644 --- a/tests/data/track_b/gross_benefit_sources/manifest.json +++ b/tests/data/track_b/gross_benefit_sources/manifest.json @@ -445,7 +445,7 @@ }, { "id": "lii_usc_402_415", - "citation": "42 U.S.C. 402(b)(4)(A), (c)(4)(A), (q)(5)(A), (w)(2), (w)(6) and 415(i)(2)(A)(ii)-(iii), Legal Information Institute (excerpts)", + "citation": "42 U.S.C. 402(b)(4)(A), (c)(4)(A), (d)(1)(D), (d)(5), (e)(1), (h)(1)(D), (h)(2), (k)(3)(A), (k)(4), (q)(5)(A), (s), (w)(2), (w)(6) and 415(i)(2)(A)(ii)-(iii), Legal Information Institute (excerpts)", "url": "https://www.law.cornell.edu/uscode/text/42/402", "additional_urls": [ "https://www.law.cornell.edu/uscode/text/42/415" @@ -453,8 +453,8 @@ "retrieved_at_utc": "2026-09-29T20:58:57Z", "capture": "http_text_excerpt", "file": "lii_usc_402_415_excerpts.txt", - "bytes": 7358, - "sha256": "31daedaea817bf97d7d63685a897343ea5a47dcf98a91f9294406edfe89d0979", + "bytes": 11851, + "sha256": "9897f74871b73daf9f0d913a3dff5a38f43fc05d2c2b3427667f45f794e8ae51", "served_bodies": [ { "url": "https://www.law.cornell.edu/uscode/text/42/402", @@ -467,7 +467,7 @@ "sha256": "c3262e82e8e674caf43709c522ce60760eafb0b133e1f99221ca7aa97d54ea3a" } ], - "note": "The 402(w)(2) and (w)(6) excerpts were added from a re-fetch at 2026-09-30T00:24:38Z whose body matched the pinned 402 SHA-256 byte for byte." + "note": "The 402(w)(2) and (w)(6) excerpts were added from a re-fetch at 2026-09-30T00:24:38Z whose body matched the pinned 402 SHA-256 byte for byte. The 402(d), (e), (h), (k) and (s) excerpts were added from a re-fetch at 2026-10-23T15:21:00Z whose body matched the pinned 402 SHA-256 byte for byte." }, { "id": "ssa_act_203", diff --git a/tests/test_track_b_gross_benefits.py b/tests/test_track_b_gross_benefits.py index f094ab43..07ac623e 100644 --- a/tests/test_track_b_gross_benefits.py +++ b/tests/test_track_b_gross_benefits.py @@ -1782,7 +1782,11 @@ def _unsupported_cases(): g.Role.PARENT, own_benefit=g.OwnBenefit(OLD_AGE, F(780)), ), - g.Beneficiary("P2", g.Role.PARENT), + g.Beneficiary( + "P2", + g.Role.PARENT, + own_benefit=g.OwnBenefit(OLD_AGE, F(790)), + ), ], ), g.UnsupportedReason.DISABLED_WIDOW_UNDER_60: lambda: _family( @@ -2393,7 +2397,11 @@ def test_unsupported_rows_stay_in_the_denominator_and_block_totals(): g.Role.PARENT, own_benefit=g.OwnBenefit(OLD_AGE, F(780)), ), - g.Beneficiary("P2", g.Role.PARENT), + g.Beneficiary( + "P2", + g.Role.PARENT, + own_benefit=g.OwnBenefit(OLD_AGE, F(790)), + ), ], ), ) diff --git a/tests/test_track_b_gross_benefits_fix_round_8_history.py b/tests/test_track_b_gross_benefits_fix_round_8_history.py index 251aed08..4932fb0e 100644 --- a/tests/test_track_b_gross_benefits_fix_round_8_history.py +++ b/tests/test_track_b_gross_benefits_fix_round_8_history.py @@ -326,6 +326,9 @@ def test_historical_scope_and_earliest_ages_are_quoted_from_captures(): assert not historical.supported assert historical.unsupported.reason.value == REASON assert g.spouse_age_reduced(F(500) - F(400), 1, PARAMS) == F("99.30") + # Method B, if the unreduced RIB began after the spouse benefit + # (402(q)(3)(A)(i) admits method C only for an early RIB or a DIB). + assert g.spouse_age_reduced(F(500), 1, PARAMS) - F(400) == F("96.50") modern = _outcome(state, [replace(member, birth_year=1916)], "family") assert modern.benefits.by_id()["member"].auxiliary_payable == F("99.30") @@ -346,7 +349,15 @@ def test_aged_widow_can_carry_pre_1978_dwb_history_after_conversion(role): ) in source_text("poms_rs_00615_310") -def test_historical_dwb_specific_fixed_rate_alternative_is_explicit(): +def test_historical_dwb_under_60_takes_the_specific_fixed_rate(): + """Source conflict us:oasdi-dwb-1984-pre-1978-history. + + RS 00615.301B.1.e fixes every DWB month under age 60 from January 1984 + at .715 of the unreduced WIB, whatever the entitlement date, so a + cohort that could have been entitled before 1978 is paid like a later + one in those months. From the year the cohort can attain 60, .005C.2's + money history governs and the row stays refused. + """ text = source_text("poms_rs_00615_301") assert ( "all disabled widow(er)s under age 60 receive .715 of the full " @@ -354,25 +365,66 @@ def test_historical_dwb_specific_fixed_rate_alternative_is_explicit(): ) in text state, member = _case(g.Role.DISABLED_WIDOW, cohort=1927) - # January 1984 keeps both cohorts under 60, matching the specific - # captured instruction rather than relying on an aged DWB snapshot. - def at_reset(beneficiary): + def at(beneficiary, year): return g.family_benefits( state, [beneficiary], - payment_month=g.YearMonth(1984, 1), + payment_month=g.YearMonth(year, 1), params=PARAMS, standalone=True, ) - historical = g.evaluate_family("case", F(7, 3), lambda: at_reset(member)) - assert not historical.supported - assert historical.unsupported.reason.value == REASON - # Explicit alternative reading of the specific rule, not an imputed - # historical award in production. Needs Max's judgment against .005C.2. - assert F(1000) * F(715, 1000) == F(715) - modern = at_reset(replace(member, birth_year=1928)) - assert modern.by_id()["member"].auxiliary_payable == F(715) + for year in (1984, 1986): + historical = at(member, year).by_id()["member"] + modern = at(replace(member, birth_year=1928), year).by_id()["member"] + assert historical.auxiliary_payable == F(715) + assert historical.age_adjusted_rate == modern.age_adjusted_rate + assert historical.auxiliary_payable == modern.auxiliary_payable + reached_60 = g.evaluate_family("case", F(7, 3), lambda: at(member, 1987)) + assert not reached_60.supported + assert reached_60.unsupported.reason.value == REASON + counts = g.count_family_outcomes([reached_60]) + assert (counts.rows, counts.weight) == (1, F(7, 3)) + + +@given(st.integers(1912, 1927), st.integers(1984, 2000), st.integers(1, 5000)) +@settings(deadline=None) +def test_historical_dwb_is_settled_exactly_before_the_cohort_can_attain_60( + cohort, year, dollars +): + """Under 60 throughout the year: identical to a post-1977 cohort.""" + state, member = _case(g.Role.DISABLED_WIDOW, cohort=cohort, pia=F(dollars)) + month = g.YearMonth(year, 6) + outcome = g.evaluate_family( + "case", + F(1), + lambda: g.family_benefits( + state, + [member], + payment_month=month, + params=PARAMS, + standalone=True, + ), + ) + if year <= cohort + 59: + later = g.family_benefits( + state, + [replace(member, birth_year=1928)], + payment_month=month, + params=PARAMS, + standalone=True, + ) + assert outcome.supported + row = outcome.benefits.by_id()["member"] + assert ( + row.auxiliary_payable == later.by_id()["member"].auxiliary_payable + ) + assert row.auxiliary_payable == g.survivor_age_reduced( + row.family_maximum_rate, 60, 60, PARAMS + ) + else: + assert not outcome.supported + assert outcome.unsupported.reason.value == REASON def test_historical_rounding_can_defeat_a_nominal_75_percent_care_floor(): diff --git a/tests/test_track_b_gross_benefits_parents_ieds.py b/tests/test_track_b_gross_benefits_parents_ieds.py index 34fdef01..517f1d76 100644 --- a/tests/test_track_b_gross_benefits_parents_ieds.py +++ b/tests/test_track_b_gross_benefits_parents_ieds.py @@ -206,13 +206,23 @@ def test_parent_entitlement_compares_the_reduced_rib_not_its_pia(): assert row.auxiliary_payable == F(25) -def test_parent_entitlement_threshold_is_the_dime_rounded_original_benefit(): +def test_parent_entitlement_threshold_is_the_unrounded_statutory_share(): + """402(h)(1)(D) compares with 82.5 percent of the PIA, not the OB. + + PIA $1,000.10: 82.5 percent is $825.0825 and the dime-floored OB is + $825.00. A lone parent with own RIB $825.00 is entitled (technically, + $0 payable). Beside another entitled parent the test is 75 percent + ($750.075), which $825 reaches, so the other parent alone is paid. + """ own = g.OwnBenefit(g.OwnBenefitKind.OLD_AGE, F(825)) - result = _family( - _survivor(F("1000.1"), F(1750)), - [_parent("high", own_benefit=own), _parent("other")], + state = _survivor(F("1000.1"), F(1750)) + alone = _rows(_family(state, [_parent("high", own_benefit=own)])) + assert alone["high"].entitled + assert alone["high"].original_benefit == F(825) + assert alone["high"].auxiliary_payable == 0 + rows = _rows( + _family(state, [_parent("high", own_benefit=own), _parent("other")]) ) - rows = _rows(result) assert not rows["high"].entitled assert rows["high"].auxiliary_payable == 0 assert rows["other"].original_benefit == F(825) @@ -240,24 +250,156 @@ def test_parent_original_benefit_uses_the_entitled_count_and_dime_floor( ) +def _statutory_band_dimes(dimes): + """Own-RIB dimes in [75%, 82.5%) of a PIA of ``dimes`` tenths.""" + return -(-dimes * 3 // 4), -(-dimes * 33 // 40) + + @given(st.integers(100, 1_000_000), st.integers(0, 100)) @settings(deadline=None) -def test_two_potential_parents_refuse_the_own_rib_entitlement_cycle( - dimes, position -): - low, high = dimes * 750 // 1000, dimes * 825 // 1000 +def test_one_parent_in_the_band_yields_to_the_other_parent(dimes, position): + """Source conflict us:oasdi-parent-entitlement-count-interval. + + An own RIB in [75%, 82.5%) of the PIA fails 402(h)(1)(D) beside an + entitled parent and passes alone. The other parent (no own RIB) is + entitled either way, so the only entitled set satisfying (h)(1)(D) for + both parents is the other parent alone, paid at 82.5 percent. + """ + low, high = _statutory_band_dimes(dimes) + if high <= low: + return own_dimes = low + (high - low - 1) * position // 100 own = g.OwnBenefit(g.OwnBenefitKind.OLD_AGE, F(own_dimes, 10)) - with pytest.raises(g.FamilyConfigurationUnsupported) as caught: + rows = _rows( _family( _survivor(F(dimes, 10), F(dimes, 5)), [_parent("p1", own_benefit=own), _parent("p2")], ) + ) + one_ob = F(dimes * 825 // 1000, 10) + assert not rows["p1"].entitled + assert rows["p1"].auxiliary_payable == 0 + assert rows["p1"].own_benefit == F(own_dimes, 10) + assert rows["p2"].entitled + assert rows["p2"].original_benefit == one_ob + assert rows["p2"].auxiliary_payable == one_ob + + +@given(st.integers(100, 1_000_000), st.integers(0, 100), st.integers(0, 100)) +@settings(deadline=None) +def test_two_parents_in_the_band_refuse_because_either_alone_qualifies( + dimes, first, second +): + low, high = _statutory_band_dimes(dimes) + if high <= low: + return + members = [ + _parent( + bid, + own_benefit=g.OwnBenefit( + g.OwnBenefitKind.OLD_AGE, + F(low + (high - low - 1) * position // 100, 10), + ), + ) + for bid, position in (("p1", first), ("p2", second)) + ] + with pytest.raises(g.FamilyConfigurationUnsupported) as caught: + _family(_survivor(F(dimes, 10), F(dimes, 5)), members) assert caught.value.reason is g.UnsupportedReason.PARENT_BENEFIT +def _consistent_parent_sets(pia, own): + """Every entitled set satisfying 402(h)(1)(D) for every parent. + + ``own`` maps a parent id to None, ("DIB", amount) or ("RIB", amount). + A member's RIB must be below the rate of the set; an excluded parent's + RIB must reach the rate it would share by joining the set. + """ + one = pia * F(825, 1000) + two = pia * F(3, 4) + ids = sorted(own) + found = [] + for size in range(len(ids) + 1): + for chosen in ( + [frozenset()] + if size == 0 + else ( + [frozenset({i}) for i in ids] + if size == 1 + else [frozenset(ids)] + ) + ): + + def passes(parent, count): + kind, amount = own[parent] or (None, None) + return kind != "RIB" or amount < (one if count == 1 else two) + + if all(passes(p, len(chosen)) for p in chosen) and not any( + passes(p, len(chosen) + 1) for p in ids if p not in chosen + ): + found.append(chosen) + return found + + +own_benefits = st.one_of( + st.none(), + st.tuples( + st.sampled_from(["RIB", "DIB"]), + st.integers(1, 30_000).map(lambda dimes: F(dimes, 10)), + ), +) + + +@given(st.integers(100, 20_000), own_benefits, own_benefits) +@settings(deadline=None) +def test_parent_entitlement_satisfies_402h1d_for_every_parent( + dimes, first, second +): + """Exact differential against brute-force consistency of 402(h)(1)(D).""" + pia = F(dimes, 10) + own = {"p1": first, "p2": second} + members = [ + _parent( + bid, + own_benefit=( + None + if value is None + else g.OwnBenefit( + ( + g.OwnBenefitKind.OLD_AGE + if value[0] == "RIB" + else g.OwnBenefitKind.DISABILITY + ), + value[1], + ) + ), + ) + for bid, value in own.items() + ] + consistent = _consistent_parent_sets(pia, own) + assert consistent + outcome = g.evaluate_family( + "parents", + F(1), + lambda: _family(_survivor(pia, pia * 2), members), + ) + if len(consistent) > 1: + assert not outcome.supported + assert outcome.unsupported.reason is g.UnsupportedReason.PARENT_BENEFIT + return + assert outcome.supported + rows = _rows(outcome.benefits) + entitled = frozenset(bid for bid, row in rows.items() if row.entitled) + assert entitled == consistent[0] + rate = g.floor_dime( + pia * (F(825, 1000) if len(entitled) == 1 else F(3, 4)) + ) + for bid in entitled: + assert rows[bid].original_benefit == rate + + def test_parent_entitlement_count_counterfactual_changes_payment_by_75(): - """Invented $780 own RIB executes both orders behind the refusal.""" + """Invented $780 own RIB: both one-pass orders, and the reading taken.""" pia, maximum, own_rib = F(1000), F(1750), F(780) count_two_first = _parent_count_order_counterfactual( pia, maximum, own_rib, initial_parent_count=2 @@ -282,13 +424,24 @@ def test_parent_entitlement_count_counterfactual_changes_payment_by_75(): assert count_two_first["other_parent_payable"] - test_at_one_parent_rate[ "other_parent_payable" ] == F(75) + # Only the count-two-first state satisfies 402(h)(1)(D) for both + # parents: p1's $780 reaches the $750 it would share beside p2, and p2 + # alone takes $825. The other one-pass state pays p2 at the two-parent + # rate while p1 is excluded, which (h)(2)(A) contradicts. own = g.OwnBenefit(g.OwnBenefitKind.OLD_AGE, own_rib) - with pytest.raises(g.FamilyConfigurationUnsupported) as caught: + rows = _rows( _family( _survivor(pia, maximum), [_parent("p1", own_benefit=own), _parent("p2")], ) - assert caught.value.reason is g.UnsupportedReason.PARENT_BENEFIT + ) + assert not rows["p1"].entitled + assert ( + rows["p2"].auxiliary_payable == count_two_first["other_parent_payable"] + ) + assert _consistent_parent_sets( + pia, {"p1": ("RIB", own_rib), "p2": None} + ) == [frozenset({"p2"})] @given(st.integers(100, 1_000_000), st.integers(0, 100)) @@ -669,10 +822,11 @@ def test_parent_and_ieds_refusals_remain_in_the_denominator(): own = g.OwnBenefit(g.OwnBenefitKind.OLD_AGE, F(780)) outcomes = [ g.evaluate_family( - "parent-cycle", + "parent-two-in-band", 2, lambda: _family( - _survivor(), [_parent(own_benefit=own), _parent("p2")] + _survivor(), + [_parent(own_benefit=own), _parent("p2", own_benefit=own)], ), ), g.evaluate_family( diff --git a/tests/test_track_b_gross_benefits_parisi.py b/tests/test_track_b_gross_benefits_parisi.py index e50dc8af..090881e0 100644 --- a/tests/test_track_b_gross_benefits_parisi.py +++ b/tests/test_track_b_gross_benefits_parisi.py @@ -14,7 +14,7 @@ from fractions import Fraction as F import pytest -from hypothesis import given, settings +from hypothesis import example, given, settings from hypothesis import strategies as st from populace_dynamics.track_b import gross_benefits as g @@ -445,6 +445,7 @@ def method_c_cases(draw): @given(method_c_cases()) +@example((F(600), F("99.90"), 36, 0)) @settings(deadline=None) def test_method_c_own_rib_age_reduction_does_not_change_redistributed_rates( case, @@ -455,7 +456,7 @@ def test_method_c_own_rib_age_reduction_does_not_change_redistributed_rates( computations over their shared semantics. Own RIB payment changes; auxiliary payable and the non-deductee shares remain identical. A positive before-age excess rounded to zero after age reduction is - explicitly refused in both computations pending the source reading. + counted whole, as GN 02603.045's before-age amount. """ pia, own_pia, spouse_months, worker_months = case state = _retired(pia, _floor_dime(pia * F(3, 2))) @@ -479,12 +480,6 @@ def test_method_c_own_rib_age_reduction_does_not_change_redistributed_rates( 0, spouse_months - 36 ) * F(1, 240) auxiliary = _floor_dime(excess * (1 - reduction)) - if auxiliary == 0: - for member in (spouse, reduced_spouse): - with pytest.raises(g.FamilyConfigurationUnsupported) as caught: - _family(state, [member, *children]) - assert caught.value.reason is g.UnsupportedReason.PARISI_AMBIGUOUS - return unchanged = _family(state, [spouse, *children]) reduced = _family(state, [reduced_spouse, *children]) counted = excess @@ -509,16 +504,17 @@ def test_method_c_own_rib_age_reduction_does_not_change_redistributed_rates( ) -def test_method_b_zero_only_after_age_is_refused_and_kept_in_denominator(): - """Pinned reading requiring judgment: GN .050A.2 versus .045/.050C. +def test_method_b_zero_only_after_age_keeps_the_age_reduction_in_ptap(): + """Source conflict us:oasdi-dual-entitlement-zero-after-age-reduction. Invented PIA500/max750; B36months ownRIB64, method B. Share83.30 - reduces to62.40, then offsets to0. A.2 disregards B: children125 each. - Keeping the20.90 age term under C instead gives114.50 each; the record - totals are750 versus729. G refuses this reading pending judgment; both - reference amounts and their exact supporting quotes remain pinned. - A before-age-only reading of .045 counts83.30-64=19.30 instead, - giving children115.30 each and record total730.60. + reduces to62.40 for age, then offsets to0. Dual entitlement considered + first leaves83.30-64=19.30, so GN .050A.2's disregard does not apply. + In 402(k)(3)(A)'s order the offset withholds62.40 and age20.90; + 20 CFR 404.403(a)(5) excludes only the former and .060 B.2 keeps the + age slice in PTAP: children114.50 each, record total729. Rejected + diagnostics: A.2's disregard (125 each, total750) and .045's + before-age amount alone (115.30 each, total730.60). """ spouse = g.Beneficiary( "B", @@ -533,17 +529,14 @@ def test_method_b_zero_only_after_age_is_refused_and_kept_in_denominator(): g.Beneficiary("C1", g.Role.CHILD), g.Beneficiary("C2", g.Role.CHILD), ] - with pytest.raises(g.FamilyConfigurationUnsupported) as caught: - _family(state, members) - assert caught.value.reason is g.UnsupportedReason.PARISI_AMBIGUOUS - outcome = g.evaluate_family( - "zero-after-age", F(3, 2), lambda: _family(state, members) - ) - counts = g.count_family_outcomes([outcome]) - assert (counts.rows, counts.weight) == (1, F(3, 2)) - assert counts.unsupported_by_reason == { - g.UnsupportedReason.PARISI_AMBIGUOUS: (1, F(3, 2)) - } + rows = _family(state, members).by_id() + assert rows["B"].age_adjusted_rate == F("62.40") + assert rows["B"].auxiliary_payable == 0 + assert rows["B"].counted_against_maximum == F("20.90") + assert rows["C1"].auxiliary_payable == rows["C2"].auxiliary_payable + assert rows["C1"].auxiliary_payable == F("114.50") + total = sum((row.auxiliary_payable for row in rows.values()), F(0)) + assert F(500) + total == 729 zero_rate_exception = g.maximum_redistribution_rates( {"B": F(250), "C1": F(250), "C2": F(250)}, F(250), @@ -551,14 +544,6 @@ def test_method_b_zero_only_after_age_is_refused_and_kept_in_denominator(): ) assert dict(zero_rate_exception) == {"C1": F(125), "C2": F(125)} assert F(500) + sum(zero_rate_exception.values(), F(0)) == 750 - alternative = g.maximum_redistribution_rates( - {"B": F(250), "C1": F(250), "C2": F(250)}, - F(250), - deductee_payables={"B": F(0)}, - age_reductions={"B": F("20.90")}, - ) - assert dict(alternative) == {"C1": F("114.50"), "C2": F("114.50")} - assert F(500) + sum(alternative.values(), F(0)) == 729 before_age_only = g.maximum_redistribution_rates( {"B": F(250), "C1": F(250), "C2": F(250)}, F(250), @@ -566,22 +551,74 @@ def test_method_b_zero_only_after_age_is_refused_and_kept_in_denominator(): ) assert dict(before_age_only) == {"C1": F("115.30"), "C2": F("115.30")} assert F(500) + sum(before_age_only.values(), F(0)) == F("730.60") - assert ( - sum(zero_rate_exception.values(), F(0)) - - sum(alternative.values(), F(0)) - == 21 - ) assert ( "The benefit payable to the dually entitled person is the amount before any applicable reduction for age" in source_text("poms_gn_02603_045") ) - assert "Age reduction amount for all workers or deductees" in source_text( - "poms_gn_02603_050" - ) - assert ( - "If dual entitlement reduces a beneficiary's rate to $0.00 (technically entitled)" - in source_text("poms_gn_02603_050") + rule = " ".join(source_text("poms_gn_02603_050").split()) + for quote in ( + "If dual entitlement reduces a beneficiary's rate to $0.00 " + "(technically entitled)", + "Age reduction amount for all workers or deductees", + "Partial benefit payable to any worker or deductee", + ): + assert quote in rule + examples = " ".join(source_text("poms_gn_02603_060").split()) + for quote in ( + "In order to redistribute only the amount being withheld because " + "of dual entitlement we must account for the entire $83.30 due B.", + "This example shows that B's age reduction has no effect because " + "we consider their dual entitlement first.", + ): + assert quote in examples + + +@settings(deadline=None) +@example(F(64), 36) +@example(F("62.40"), 36) +@example(F("83.20"), 36) +@example(F("83.30"), 36) +@example(F(50), 60) +@given( + st.integers(1, 1000).map(lambda dimes: F(dimes, 10)), + st.integers(0, 60), +) +def test_method_b_counts_payable_plus_age_reduction_until_a2(own, months): + """Method B counts what dual entitlement does not withhold. + + Counted = payable + the auxiliary's own age reduction, which is + standard - min(own, age-adjusted rate), until the own benefit reaches + the standard rate and GN 02603.050A.2 disregards the spouse. Children + never fall when the own benefit rises. + """ + spouse = g.Beneficiary( + "B", + g.Role.SPOUSE, + reduction_months=months, + own_benefit=g.OwnBenefit(OLD_AGE, own), + own_benefit_first=False, ) + state = _retired(500, 750) + children = [g.Beneficiary(key, g.Role.CHILD) for key in ("C1", "C2")] + rows = _family(state, [spouse, *children]).by_id() + standard = F("83.30") + age_adjusted = rows["B"].age_adjusted_rate + counted = F(0) if own >= standard else standard - min(own, age_adjusted) + assert rows["B"].counted_against_maximum == counted + assert rows["B"].auxiliary_payable == max(F(0), age_adjusted - own) + child = min(F(250), _floor_dime((F(250) - counted) / 2)) + assert rows["C1"].auxiliary_payable == rows["C2"].auxiliary_payable + assert rows["C1"].auxiliary_payable == child + richer = _family( + state, + [ + dataclasses.replace( + spouse, own_benefit=g.OwnBenefit(OLD_AGE, own + F(1, 10)) + ), + *children, + ], + ).by_id() + assert richer["C1"].auxiliary_payable >= rows["C1"].auxiliary_payable @pytest.mark.parametrize( diff --git a/tests/test_track_b_readings_20261023.py b/tests/test_track_b_readings_20261023.py new file mode 100644 index 00000000..7d951865 --- /dev/null +++ b/tests/test_track_b_readings_20261023.py @@ -0,0 +1,364 @@ +"""Track B readings pass of October 23, 2026: markers, notes and new quotes. + +Verification class: statutory conformance and documentation integrity on +captured sources only. No population, behavioral or forecast claim. +Markers: the scan reads, as UTF-8, every file in the Track B package, +subpackages included, whose suffix in any case is in ``SOURCES``, except +the ``OWN_REGISTERS`` at the top of the package. With its markers +removed, a scanned file must hold nothing ``STRAYS`` matches. The +``MARKER`` ids are exactly ``READINGS`` plus ``OTHER_ROUNDS``, each in the +G, R2, R3 or grace module, and each is ``DOCUMENTED`` in a Track B design +note. Notes: G, R2 and R3 name their ``READINGS`` the same way, and none +has an opening, as ``_openings`` reads them, whose ``_letters`` start +with ``JUDGMENT``. Quotes: each new statutory excerpt appears in its +capture as ``source_text`` normalizes it, with each run of whitespace in +the capture read as one space. Reads: those checks list and open only the +Track B package, the notes named ``track_b_*.md`` and the statute captures, +and a guard test patches ``Path`` to fail on anything else. +""" + +import html +import re +import unicodedata +from pathlib import Path + +import pytest + +from tests.track_b_gross_benefit_support import source_text + +ROOT = Path(__file__).parents[1] +MODULES = ROOT / "src" / "populace_dynamics" / "track_b" +NOTES = { + "G": ROOT / "docs/design/track_b_g_gross_benefits.md", + "R2": ROOT / "docs/design/track_b_r2_ret_ledger.md", + "R3": ROOT / "docs/design/track_b_r3_arf_recompute.md", +} + +#: Logged readings with a decision line in G, R2 or R3, by note. +READINGS = { + "G": { + "us:oasdi-family-maximum-saving-clause", + "us:oasdi-saving-clause-operational-rate-rounding", + "us:oasdi-saving-clause-procedure-gaps", + "us:oasdi-dual-entitlement-zero-after-age-reduction", + "us:oasdi-parent-entitlement-count-interval", + "us:oasdi-dwb-1984-pre-1978-history", + "us:oasdi-dual-entitlement-pre-1978-history", + "us:oasdi-reduction-exact-fractions", + "us:oasdi-disability-family-maximum-rounding", + "us:oasdi-ret-temporary-partial-deduction", + "us:oasdi-nh-charge-age-reduction-interaction", + }, + "R2": { + "us:oasdi-ret-negative-nese-proration", + "us:oasdi-ret-grace-simultaneous-rib-dib", + "us:oasdi-ret-own-work-age-exemptions", + "us:oasdi-ret-deemed-entitlement-basis", + "us:oasdi-ret-other-deductee-scope", + }, + "R3": { + "us:oasdi-survivor-fra-1911-births", + "us:oasdi-arf-widow-child-in-care-credits", + "us:oasdi-arf-spouse-reentitlement-credits", + }, +} +#: Readings that other rounds log in the G, R2, R3 and grace modules +#: (microcosm-dynamics#563). A new marker there needs its id here or above. +OTHER_ROUNDS = { + "us:oasdi-finality-protected-rates-before-coverage", + "us:oasdi-sole-full-deduction-zero-dual-composition", +} +#: Exempt, by path inside the package: the insured-status modules that +#: #562 adds. They keep their own register of readings, with a test of its +#: own, and this scan does not read them. +OWN_REGISTERS = {"insured_status.py", "_insured_status_qc.py"} +#: Scanned suffixes, compared in lower case. +SOURCES = {".py", ".pyi", ".yaml", ".yml"} +#: "us:oasdi-" and then one or more words of the letters a to z and the +#: digits 0 to 9, joined by single hyphens. +ID = r"us:oasdi-[a-z0-9]+(?:-[a-z0-9]+)*" +#: The one marker form, with exactly this spelling, case and spacing. Its +#: id ends at a colon, whitespace or the end of the file. +MARKER = re.compile(rf"# Source conflict ({ID})(?=:|\s|$)") +#: What a scanned file may hold only inside a marker, in any case. A +#: letter here is one of A to Z, and a digit one of 0 to 9. +STRAYS = ( + # "source" or "sources", with no letter just before it, then + # "conflict", with nothing but whitespace, "#", "-" or "_" between. + re.compile(r"(?", and once +#: with it ending at its first ">" outside quotes. The lookahead lets an +#: unclosed tag's text overlap the next heading's. +HTML_HEADINGS = tuple( + re.compile( + rf"(?=(.*?)(?:|\Z))", + re.IGNORECASE | re.DOTALL, + ) + for tag in (r"[^>]*", r"""(?:[^>"']|"[^"]*"|'[^']*')*""") +) +#: What the first of the two ``_letters`` readings removes: HTML tags, +#: each up to its first ">", and Markdown link targets, a "(...)" or +#: "[...]" right after a "]". +HIDDEN = re.compile(r"<[^>]+>|\](?:\([^)]*\)|\[[^\]]*\])") +#: How the letters of an open judgment item start: "Needs Max's judgment", +#: or "judgement", as ``_letters`` reads it. +JUDGMENT = ("needsmaxsjudgment", "needsmaxsjudgement") + + +def _module_markers(): + """Marker ids by file, across the files the scan reads. + + With its markers removed, a scanned file must hold nothing ``STRAYS`` + matches. So a wrapped, reworded, recased or misspelled marker trips + this check while some of its text still matches ``STRAYS``. + """ + found = {} + for path in sorted(MODULES.rglob("*")): + name = path.relative_to(MODULES).as_posix() + if ( + name in OWN_REGISTERS + or path.suffix.lower() not in SOURCES + or not path.is_file() + ): + continue + text = path.read_text(encoding="utf-8") + rest = MARKER.sub("", text) + strays = [m.group() for p in STRAYS for m in p.finditer(rest)] + assert not strays, (name, strays) + for found_id in MARKER.findall(text): + found.setdefault(found_id, set()).add(name) + return found + + +def _continues(above, line): + """Whether a line continues the one above it instead of opening. + + Both must start with a letter A to Z after the same leading + whitespace (``LETTER``), and neither may hold ">" or "|". + """ + first, second = LETTER.match(above), LETTER.match(line) + return bool( + first + and second + and first.group(1) == second.group(1) + and not {">", "|"} & set(above + line) + ) + + +def _openings(text): + """What the lines of a note open, code blocks included. + + Every non-blank line opens unless it ``_continues`` the line above + it, and what it opens runs to the end of its run of non-blank lines. + The text after each ``HTML_HEADINGS`` opening tag counts too. + """ + found = [m.group(2) for p in HTML_HEADINGS for m in p.finditer(text)] + run = [] + for line in [*text.splitlines(), ""]: + if line.strip(): + run.append(line) + continue + found += [ + " ".join(run[k:]) + for k in range(len(run)) + if not (k and _continues(run[k - 1], run[k])) + ] + run = [] + return found + + +def _letters(text): + """The letters of a text, read two ways. + + It decodes HTML entities, then reads the text once with ``HIDDEN`` + removed and once as written. Each reading applies Unicode + compatibility decomposition (NFKD) and keeps only the letters A to Z, + in lower case. + """ + text = html.unescape(text) + return [ + "".join(re.findall("[a-z]", unicodedata.normalize("NFKD", t).lower())) + for t in (HIDDEN.sub("", text), text) + ] + + +def _open_judgment_items(text): + return [ + opening + for opening in _openings(text) + if any(read.startswith(JUDGMENT) for read in _letters(opening)) + ] + + +def test_markers_and_logged_readings_match_in_both_directions(): + markers = _module_markers() + logged = set().union(*READINGS.values()) | OTHER_ROUNDS + assert set(markers) == logged + for name in logged: + assert markers[name] <= { + "gross_benefits.py", + "ret_ledger.py", + "ret_grace.py", + "arf.py", + }, name + + +def test_every_marker_names_a_reading_documented_in_a_design_note(): + # Whole ids, so a truncated or misspelled marker cannot match a prefix. + documented = set() + for path in (ROOT / "docs" / "design").glob("track_b_*.md"): + documented.update(DOCUMENTED.findall(path.read_text(encoding="utf-8"))) + for name in _module_markers(): + assert name in documented, name + + +@pytest.mark.parametrize("note", sorted(NOTES)) +def test_each_note_names_its_readings_and_opens_no_judgment_item(note): + text = NOTES[note].read_text(encoding="utf-8") + assert not _open_judgment_items(text), note + named = set(DOCUMENTED.findall(text)) + for name in READINGS[note]: + assert name in named, (note, name) + + +STATUTE_QUOTES = ( + # 402(h)(1)(D): the parent's own-RIB test depends on the entitled set. + "if the amount of the parent 's insurance benefit for such month is " + "determinable under paragraph (2)(A) (or 75 percent of such primary " + "insurance amount in any other case)", + "For any month for which more than one parent is entitled to parent 's " + "insurance benefits", + # 402(k)(4): an elected RIB month is entitlement to RIB alone. + "if such individual so elects, he shall instead be entitled to only the " + "smaller of such benefits for such month.", + # 402(s)(2)-(3): marriage and the 203(f)(1)(C) exemption need disability. + "shall not apply in the case of any child unless such child, at the " + "time of the marriage referred to therein, was under a disability", + "subsection (f)(1)(C) of section 403 of this title", + "unless in such month or the third month prior thereto such child was " + "under a disability", + # 402(d)(1)(D), (d)(5): marriage ends a child's benefit, with exceptions. + "(D) the month in which such child dies, or marries,", + "but subject to subsection (s), not be terminated by reason of such " + "marriage.", + # 402(e)(1): a widow(er) entitled before 60 ends at the termination month. + "if she became entitled to such benefits before she attained age 60", + "the termination month for any individual shall be the third month " + "following the month in which her disability ceases;", +) + + +@pytest.mark.parametrize("quote", STATUTE_QUOTES) +def test_new_statutory_excerpts_are_quoted_verbatim(quote): + assert quote in " ".join(source_text("lii_usc_402_415").split()) + + +def test_the_checks_list_and_open_only_track_b_paths(monkeypatch): + # So that a blind role can run this module without reaching a restricted + # file, these checks may list only the package and the notes named + # track_b_*.md, and open only those files and the statute captures. The + # guard checks each Path listing and open before it runs, and it spells + # out its paths so that a change to the constants above fails it. + package = (ROOT / "src/populace_dynamics/track_b").resolve() + design = (ROOT / "docs/design").resolve() + captures = (ROOT / "tests/data/track_b/gross_benefit_sources").resolve() + path_open, glob, rglob = Path.open, Path.glob, Path.rglob + walk = getattr(Path, "walk", None) # New in Python 3.12. + opened = [] + + def checked_open(path, *args, **kwargs): + resolved = path.resolve() + assert ( + resolved.is_relative_to(package) + or resolved.is_relative_to(captures) + or (resolved.parent == design and resolved.match("track_b_*.md")) + ), resolved + opened.append(resolved) + return path_open(path, *args, **kwargs) + + def checked(listing, allowed): + def checked_listing(path, pattern, *args, **kwargs): + assert (path.resolve(), str(pattern)) in allowed, (path, pattern) + return listing(path, pattern, *args, **kwargs) + + return checked_listing + + def checked_walk(path, *args, **kwargs): + # On Python 3.12, rglob walks the folder it lists. + assert path.resolve() == package, path + return walk(path, *args, **kwargs) + + def unchecked(path, *args, **kwargs): + raise AssertionError(f"listing of {path}") + + monkeypatch.setattr(Path, "open", checked_open) + # From Python 3.13, rglob calls glob with "**/" before its pattern. + monkeypatch.setattr( + Path, + "glob", + checked(glob, {(design, "track_b_*.md"), (package, "**/*")}), + ) + monkeypatch.setattr(Path, "rglob", checked(rglob, {(package, "*")})) + monkeypatch.setattr(Path, "iterdir", unchecked) + monkeypatch.setattr(Path, "walk", checked_walk, raising=False) + for reach in ( + lambda: (ROOT / "pyproject.toml").open(), + lambda: (design / "other.md").open(), + lambda: design.glob("*.md"), + lambda: design.rglob("track_b_*.md"), + lambda: package.parent.rglob("*"), + lambda: package.iterdir(), + lambda: design.walk(), + lambda: package.parent.walk(), + ): + with pytest.raises(AssertionError): + reach() + + def reads(check, *arguments): + opened.clear() + check(*arguments) + return set(opened) + + notes = {path.resolve() for path in NOTES.values()} + assert package / "gross_benefits.py" in reads( + test_markers_and_logged_readings_match_in_both_directions + ) + assert notes <= reads( + test_every_marker_names_a_reading_documented_in_a_design_note + ) + assert notes == set().union( + *( + reads( + test_each_note_names_its_readings_and_opens_no_judgment_item, + note, + ) + for note in NOTES + ) + ) + source_text.cache_clear() + assert {captures} == { + path.parent + for path in reads( + test_new_statutory_excerpts_are_quoted_verbatim, + STATUTE_QUOTES[0], + ) + } diff --git a/tests/test_track_b_ret_grace.py b/tests/test_track_b_ret_grace.py index 951923d4..f83cd616 100644 --- a/tests/test_track_b_ret_grace.py +++ b/tests/test_track_b_ret_grace.py @@ -27,6 +27,9 @@ from populace_dynamics.track_b.ret_grace import ( TerminationReason as T, ) +from tests.track_b_gross_benefit_support import ( + source_text as gross_source_text, +) from tests.track_b_ret_ledger_support import source_text @@ -320,10 +323,24 @@ def test_simultaneous_rib_dib_elections_preserve_2010_lmety(): assert rib.next_state.last_met_year == 2010 -def test_simultaneous_disability_cfr_default_and_poms_alternative_are_explicit(): +def test_simultaneous_disability_default_follows_202k4_and_cfr_alternative_is_explicit(): + """Source conflict us:oasdi-ret-grace-simultaneous-rib-dib. + + Act 202(k)(4): a person entitled to RIB and DIB is "entitled to only" + the benefit paid for the month, so an elected RIB month is not a month + "entitled to disability benefits" under 404.435(b)(3). The default + counts its NSMs, as RS 02501.030B.4 does; the former technical- + entitlement exclusion stays selectable. + """ months = [EMPTY] * 5 + [month(B.RIB, entitled={B.RIB, B.DIB})] * 7 - conservative = grace_year(2026, GraceYearState(), months) - poms = grace_year( + poms = grace_year(2026, GraceYearState(), months) + conservative = grace_year( + 2026, + GraceYearState(), + months, + disability_reading=D.CFR_ENTITLEMENT_EXCLUSION, + ) + assert poms == grace_year( 2026, GraceYearState(), months, @@ -342,6 +359,12 @@ def test_simultaneous_disability_cfr_default_and_poms_alternative_are_explicit() "If the beneficiary becomes simultaneously entitled to RIB and DIB benefits, the earnings test (ET) applies to any month they elect to receive RIB." in source_text("poms_rs_02501_030") ) + assert ( + "shall be entitled to only the larger of such benefits for such " + "month, except that, if such individual so elects, he shall " + "instead be entitled to only the smaller of such benefits for " + "such month." + ) in " ".join(gross_source_text("lii_usc_402_415").split()) def test_cfr_404435_don_april_retirement_initial_may_through_december(): @@ -910,6 +933,9 @@ def test_property_disabled_different_type_preserves_pending_until_eligible_nsm( "construct", [ lambda: GraceMonth(received_types={B.RIB}), + lambda: GraceMonth( + entitled_types={B.RIB, B.DIB}, received_types={B.RIB, B.DIB} + ), lambda: GraceMonth(entitled_types={"rib"}), lambda: GraceMonth(nonservice=1), lambda: GraceYearState(gap_since_entitlement=True), @@ -973,6 +999,9 @@ def arbitrary_month(draw): if types else set() ) + if {B.RIB, B.DIB} <= received: + # 202(k)(4): one of the two is paid for the month. + received.discard(draw(st.sampled_from([B.RIB, B.DIB]))) return GraceMonth( entitled_types=types, received_types=received, @@ -987,25 +1016,49 @@ def arbitrary_month(draw): def test_property_only_entitled_nondisability_prefra_nonservice_months_protected( months, fra ): - result = grace_year(2026, GraceYearState(), months, fra_month=fra) - expected = { - i - for i, value in enumerate(months, 1) - if i < fra - and value.nonservice - and value.received_types - DISABILITY_TYPES - and not value.entitled_types & DISABILITY_TYPES - } - assert result.protected_months == expected - assert result.met_eligible_months == { - i - for i, value in enumerate(months, 1) - if i < fra - and value.received_types - DISABILITY_TYPES - and not value.entitled_types & DISABILITY_TYPES - } - assert result.applies == bool(expected) - assert (result.next_state.last_met_year == 2026) == bool(expected) + """Default 202(k)(4) reading and the explicit CFR alternative. + + By default a paid RIB month with only DIB alongside counts (the person + is entitled to RIB alone that month); DWB and CDB months never count. + """ + + def counts(value, reading): + disability = value.entitled_types & DISABILITY_TYPES + return bool(value.received_types - DISABILITY_TYPES) and ( + not disability + or ( + reading is D.POMS_RIB_ELECTION + and B.RIB in value.received_types + and disability == {B.DIB} + ) + ) + + for reading, result in ( + ( + D.POMS_RIB_ELECTION, + grace_year(2026, GraceYearState(), months, fra_month=fra), + ), + ( + D.CFR_ENTITLEMENT_EXCLUSION, + grace_year( + 2026, + GraceYearState(), + months, + fra_month=fra, + disability_reading=D.CFR_ENTITLEMENT_EXCLUSION, + ), + ), + ): + eligible = { + i + for i, value in enumerate(months, 1) + if i < fra and counts(value, reading) + } + expected = {i for i in eligible if months[i - 1].nonservice} + assert result.met_eligible_months == eligible + assert result.protected_months == expected + assert result.applies == bool(expected) + assert (result.next_state.last_met_year == 2026) == bool(expected) @settings(max_examples=200, deadline=None) @@ -1135,7 +1188,14 @@ def test_property_poms_rib_election_separates_entitlement_from_receipt( assert poms.met_eligible_months == eligible assert poms.protected_months == expected assert poms.applies == bool(expected) - conservative = grace_year(2026, GraceYearState(), months, fra_month=fra) + assert poms == grace_year(2026, GraceYearState(), months, fra_month=fra) + conservative = grace_year( + 2026, + GraceYearState(), + months, + fra_month=fra, + disability_reading=D.CFR_ENTITLEMENT_EXCLUSION, + ) assert not conservative.applies assert not conservative.met_eligible_months diff --git a/tests/test_track_b_ret_ledger.py b/tests/test_track_b_ret_ledger.py index a8763ae2..18310fad 100644 --- a/tests/test_track_b_ret_ledger.py +++ b/tests/test_track_b_ret_ledger.py @@ -1183,18 +1183,20 @@ def test_negative_nese_reading_changes_final_deduction_by_one_dollar(): assert by_month(alternative)[1].payment == 598 -def test_unmarried_deemed_entitlement_is_refused_pending_source_judgment(): - with pytest.raises(r.RETConfigurationUnsupported) as error: - ledger( - beneficiary(work=earnings(lower_earnings_for_excess(600))), - beneficiary( - "deemed", - rate=200, - benefit_type=BenefitType.CDB, - deemed_entitled=True, - ), +def test_unmarried_deemed_entitlement_is_an_input_error(): + """Source conflict us:oasdi-ret-deemed-entitlement-basis. + + Act 203(b)(1) and 20 CFR 404.420 deem only a recipient married to the + NH; RS 02501.140's broader wording yields to them (P1), so an + unmarried recipient is not on the NH's ledger at all. + """ + with pytest.raises(r.InvalidFamilyInput, match="marriage to the NH"): + beneficiary( + "deemed", + rate=200, + benefit_type=BenefitType.CDB, + deemed_entitled=True, ) - assert error.value.reason is r.UnsupportedReason.DEEMED_ENTITLEMENT_BASIS def test_married_deemed_disabled_child_is_charged_number_holders_excess(): diff --git a/tests/test_track_b_ret_ledger_properties.py b/tests/test_track_b_ret_ledger_properties.py index e3a6fc4f..a3bb066e 100644 --- a/tests/test_track_b_ret_ledger_properties.py +++ b/tests/test_track_b_ret_ledger_properties.py @@ -853,7 +853,13 @@ def test_every_raw_month_set_element_is_validated_before_deduplication( _month_set_case(field, values, month) -@pytest.mark.parametrize("field", MONTH_SET_FIELDS) +# Any non-empty own_work_exempt_months is an input error (202(s)(3)). +VALID_MONTH_SET_FIELDS = tuple( + field for field in MONTH_SET_FIELDS if field != "own_work_exempt_months" +) + + +@pytest.mark.parametrize("field", VALID_MONTH_SET_FIELDS) @settings(max_examples=30, deadline=None) @given(st.lists(st.integers(1, 12), min_size=1, max_size=30)) def test_valid_month_set_duplicates_and_order_preserve_the_same_months( diff --git a/tests/test_track_b_ret_ledger_readings.py b/tests/test_track_b_ret_ledger_readings.py index 51703a63..2c2d72cc 100644 --- a/tests/test_track_b_ret_ledger_readings.py +++ b/tests/test_track_b_ret_ledger_readings.py @@ -28,6 +28,9 @@ earnings, record, ) +from tests.track_b_gross_benefit_support import ( + source_text as gross_source_text, +) from tests.track_b_ret_ledger_support import source_text @@ -311,82 +314,28 @@ def test_invalid_sequences_and_infinite_numbers_are_input_errors(make_input): make_input() -@pytest.mark.parametrize("benefit_type", [BenefitType.RIB, BenefitType.SPOUSE]) -def test_own_work_exempt_flag_requires_a_statutory_benefit_type(benefit_type): - with pytest.raises(r.InvalidFamilyInput, match="Child or widow"): - record( - beneficiary( - benefit_type=benefit_type, - own_work_exempt_months={1}, - ), - fiscal_year=True, - ) - - -@settings(max_examples=75, deadline=None) -@given( - st.sampled_from([BenefitType.CHILD, BenefitType.WIDOW]), - st.frozensets(st.integers(1, 12), max_size=12), - st.integers(0, 10_000), +@pytest.mark.parametrize( + "benefit_type", + [ + BenefitType.RIB, + BenefitType.SPOUSE, + BenefitType.CHILD, + BenefitType.WIDOW, + ], ) -def test_property_other_statutory_own_exemptions_protect_only_that_workers_excess( - benefit_type, protected, excess -): - nh = beneficiary() - auxiliary = beneficiary( - "aux", - benefit_type=benefit_type, - own_work_exempt_months=protected, - work=earnings( - PARAMS.for_year(YEAR).lower_exempt_amount.value + 2 * excess - ), - ) - case = ( - r.RecordYear( - YEAR, - "deceased", - kind=r.RecordKind.DECEASED, - beneficiaries=(auxiliary,), - ) - if benefit_type is BenefitType.WIDOW - else record(nh, auxiliary) +def test_own_work_exempt_months_are_never_a_lawful_assertion(benefit_type): + """Source conflict us:oasdi-ret-own-work-age-exemptions. + + 202(s)(3) limits Act 203(f)(1)(C) to a child under a disability that + month or three months before, and 402(e)(1) ends a pre-60 widow(er)'s + entitlement at the termination month, so the Act exempts only CDB and + DWB months, which the ledger exempts by type. + """ + with pytest.raises(r.InvalidFamilyInput, match="disability-based"): + beneficiary(benefit_type=benefit_type, own_work_exempt_months={1}) + assert "subsection (f)(1)(C) of section 403 of this title" in " ".join( + gross_source_text("lii_usc_402_415").split() ) - result = r.apply_ret_year(case, PARAMS) - assert all( - m.deduction == 0 - for m in result.for_beneficiary("aux") - if m.month in protected - ) - assert all(m.deduction == 0 for m in result.for_beneficiary("nh")) - assert sum(m.deduction for m in result.for_beneficiary("aux")) == min( - excess, 600 * (12 - len(protected)) - ) - if benefit_type is BenefitType.CHILD: - # A child's asserted own-work exemption must not protect that - # child's benefit from the NH's independently chargeable excess. - nh_worked = replace( - nh, - earnings=earnings( - PARAMS.for_year(YEAR).lower_exempt_amount.value + 2 * excess - ), - ) - protected_result = r.apply_ret_year( - record(nh_worked, auxiliary), PARAMS - ) - reference = r.apply_ret_year( - record( - nh_worked, - replace( - auxiliary, - own_work_exempt_months=frozenset(), - earnings=earnings(), - ), - ), - PARAMS, - ) - assert [m.nh_deduction for m in protected_result.months] == [ - m.nh_deduction for m in reference.months - ] @pytest.mark.parametrize("benefit_type", [BenefitType.SPOUSE, BenefitType.RIB]) @@ -435,7 +384,7 @@ def _deemed_cdb_january_counterfactual(nh, recipient, *, broader_poms): It never changes marriage facts or enables the broader reading through the public API. The CDB recipient has no excess of their own. """ - assert recipient.deemed_entitled and not recipient.deemed_married_to_nh + assert not recipient.deemed_entitled assert set(recipient.benefit_types) == {BenefitType.CDB} assert 1 in nh.entitled_months & recipient.entitled_months assert recipient.earnings == earnings() @@ -447,15 +396,21 @@ def _deemed_cdb_january_counterfactual(nh, recipient, *, broader_poms): * year.lower_withholding_rate.value ) assert excess >= nh.gross_rates[0] + recipient.gross_rates[0] - # RS 02501.140 puts the other-record CDB rate in the NH's full family - # deduction. Act 203(b)(1) / CFR .420 leave an unmarried recipient's - # separate record outside that charge. This is test-only arithmetic. + # RS 02501.140 would put the other-record CDB rate in the NH's full + # family deduction. Act 203(b)(1) / CFR .420 leave an unmarried + # recipient's separate record outside that charge, the reading taken; + # the broader figure is test-only arithmetic. nh_deduction = recipient.gross_rates[0] if broader_poms else F(0) return recipient.gross_rates[0] - nh_deduction def test_unmarried_deemed_entitlement_counterfactual_changes_200_dollars(): - """Invented $400/$200 rates and $600 excess, with no real data.""" + """Invented $400/$200 rates and $600 excess, with no real data. + + Reading taken (P1): an unmarried recipient is not deemed, so its own + record pays $200 in January; RS 02501.140's broader wording would + remove it. The public API rejects the unmarried declaration. + """ assert ( "is married to an individual entitled to old-age insurance benefits under section 202(a)" in source_text("ssa_act_203") @@ -476,21 +431,25 @@ def test_unmarried_deemed_entitlement_counterfactual_changes_200_dollars(): "deemed", rate=200, benefit_type=BenefitType.CDB, - deemed_entitled=True, ) - with pytest.raises(r.RETConfigurationUnsupported) as error: - r.apply_ret_year(record(nh, recipient), PARAMS) - assert error.value.reason is r.UnsupportedReason.DEEMED_ENTITLEMENT_BASIS + with pytest.raises(r.InvalidFamilyInput, match="marriage to the NH"): + replace(recipient, deemed_entitled=True) married = r.apply_ret_year( - record(nh, replace(recipient, deemed_married_to_nh=True)), PARAMS + record( + nh, + replace( + recipient, deemed_entitled=True, deemed_married_to_nh=True + ), + ), + PARAMS, ) unrelated = r.apply_ret_year( r.RecordYear( YEAR, "other", kind=r.RecordKind.DECEASED, - beneficiaries=(replace(recipient, deemed_entitled=False),), + beneficiaries=(recipient,), ), PARAMS, ) @@ -527,8 +486,8 @@ def test_property_deemed_counterfactual_matches_full_married_charge( Shared surface: whole-dollar RIB/CDB rates, no other excess/protection, and NH excess equal to the combined January rate. Tolerance is zero. - The Act/CFR counterfactual always preserves the unrelated CDB rate; - the public API always retains the unmarried source-conflict refusal. + The Act/CFR reading always preserves the unrelated CDB rate; the + public API always rejects the unmarried deemed declaration. """ family_rate = nh_rate + cdb_rate nh = beneficiary( @@ -541,20 +500,24 @@ def test_property_deemed_counterfactual_matches_full_married_charge( "deemed", rate=cdb_rate, benefit_type=BenefitType.CDB, - deemed_entitled=True, ) - with pytest.raises(r.RETConfigurationUnsupported) as error: - r.apply_ret_year(record(nh, recipient), PARAMS) - assert error.value.reason is r.UnsupportedReason.DEEMED_ENTITLEMENT_BASIS + with pytest.raises(r.InvalidFamilyInput, match="marriage to the NH"): + replace(recipient, deemed_entitled=True) married = r.apply_ret_year( - record(nh, replace(recipient, deemed_married_to_nh=True)), PARAMS + record( + nh, + replace( + recipient, deemed_entitled=True, deemed_married_to_nh=True + ), + ), + PARAMS, ) unrelated = r.apply_ret_year( r.RecordYear( YEAR, "other", kind=r.RecordKind.DECEASED, - beneficiaries=(replace(recipient, deemed_entitled=False),), + beneficiaries=(recipient,), ), PARAMS, ) @@ -596,6 +559,12 @@ def test_missing_supported_year_is_not_hidden_by_a_coverage_refusal(): def test_optional_act_age_exemptions_are_an_explicit_600_dollar_reading( benefit_type, ): + """Ordinary CHILD and WIDOW months are charged; no override exists. + + The former Act-clause assertion would have paid the $600; under + 202(s)(3) and 402(e)(1) it is not a lawful reading, so it is now an + input error and the charged $600 is the only answer. + """ person = beneficiary( "aux", rate=200, @@ -616,14 +585,6 @@ def case(person): ) conservative = r.apply_ret_year(case(person), PARAMS) - act = r.apply_ret_year( - case(replace(person, own_work_exempt_months=frozenset(range(1, 13)))), - PARAMS, - ) + with pytest.raises(r.InvalidFamilyInput, match="disability-based"): + replace(person, own_work_exempt_months=frozenset(range(1, 13))) assert sum(m.deduction for m in conservative.months) == 600 - assert sum(m.deduction for m in act.months) == 0 - assert ( - sum(m.payment for m in act.months) - - sum(m.payment for m in conservative.months) - == 600 - ) diff --git a/tests/test_track_b_ret_ledger_sources.py b/tests/test_track_b_ret_ledger_sources.py index 35edf916..225f2bca 100644 --- a/tests/test_track_b_ret_ledger_sources.py +++ b/tests/test_track_b_ret_ledger_sources.py @@ -47,9 +47,9 @@ def test_source_conflict_ids_are_linked_at_the_decision_lines(): "or b.benefit_types[month - 1] in DISABILITY_TYPES", ), ( - r.apply_ret_year, + r.RETBeneficiary.__post_init__, "us:oasdi-ret-deemed-entitlement-basis", - "if b.deemed_entitled and not b.deemed_married_to_nh:", + "if not self.deemed_married_to_nh:", ), ( r.apply_ret_year, diff --git a/tests/tier_counts.json b/tests/tier_counts.json index 1156225c..4af4cfe1 100644 --- a/tests/tier_counts.json +++ b/tests/tier_counts.json @@ -1,7 +1,7 @@ { "schema_version": 1, "counts": { - "unit": 26654, + "unit": 26675, "artifact": 7141, "integration_psid": 1471, "reproduction_legacy": 520, diff --git a/tests/track_b_ret_grace_reference.py b/tests/track_b_ret_grace_reference.py index 307aba31..0b2eb102 100644 --- a/tests/track_b_ret_grace_reference.py +++ b/tests/track_b_ret_grace_reference.py @@ -87,7 +87,7 @@ def _eligible( def reference_grace_history( history: tuple[HistoryYear, ...], reading: DisabilityGraceReading = ( - DisabilityGraceReading.CFR_ENTITLEMENT_EXCLUSION + DisabilityGraceReading.POMS_RIB_ELECTION ), ) -> tuple[ReferenceDecision, ...]: """Find every year's grace decisions directly from the entire timeline.