diff --git a/docs/design/track_b_r3_arf_recompute.md b/docs/design/track_b_r3_arf_recompute.md new file mode 100644 index 00000000..f3e990bb --- /dev/null +++ b/docs/design/track_b_r3_arf_recompute.md @@ -0,0 +1,395 @@ +# Track B R3: adjustment of the reduction factor and recomputation + +**Verification class: statutory conformance. Build only, 2026-10-11; +review fixes 2026-10-12.** +Admitted scope is ARF credits and earnings recomputation mechanics for +supported cases. Inputs are invented histories and captured SSA examples. +There is no population, behavioral, forecast or payment-processing claim. + +The governing Track B design is revision 3, approved under ruling d515, +`microcosm-launch-evidence/dynasim-parity-20260909/trackb-design-20260928.md`. +Sections 3.1, 3.2, 5.4, 6 and 7 require separate ARF and recomputation +references, record-specific credits, preserved original computation factors, +no future earnings in earlier recomputations, and explicit unavailable +histories. Axiom comparisons remain diagnostics after a rule is pinned. + +Code is `src/populace_dynamics/track_b/arf.py` and `recompute.py`. Example +and Hypothesis tests are `tests/test_track_b_arf.py`, +`tests/test_track_b_arf_properties.py`, `tests/test_track_b_recompute.py` +and `tests/test_track_b_recompute_properties.py`. Source integrity and +verbatim quotations are in `tests/test_track_b_r3_sources.py`. + +## Statutory order + +R2 supplies final full or partial work-deduction months for each benefit +record. R3 takes iterables of `YearMonth` or `(year, month)` values and +imports no R2 implementation. Provisional or prorated suspensions are not +final deduction months. Credits are a union, so overlapping categories +credit a month once. Only original reduction-window months count +(42 U.S.C. 402(q)(6)-(7); RS 00615.482). + +Every iterable field in an ARF claim is snapshotted once at construction +into a tuple or frozenset. Month contents and condition types are still +validated before any coverage refusal. An ARF result freezes its crediting +months, and a recomputation basis freezes its conditions at construction; +its bend points require a tuple. Neither evaluating the original claim nor +inspecting a result can consume credits or erase an exceptional-history +refusal. Iterator and immutable-set inputs have exactly the same supported +results and refusal reasons. This implements deterministic record-specific +credits and retention of unsupported rows under design sections 6 and 7. + +ARF input validation first rejects non-finite numeric statutory-rate, +benefit-share and bracket values, including NaN and either infinity, with +`ValueError`. It then calls G's unmodified `statutory_rates` to check the +complete shared rate bundle, including the RIB and spouse rates and their +36-month brackets (402(q)(1)(A); 20 CFR 404.410; RS 00615.101, .201). +A mismatched finite bundle retains G's original `ValueError`. Both errors +precede any explicit or automatic coverage refusal and benefit arithmetic, +and propagate through `apply_arf`, `evaluate_arf` and lazy inputs to +`count_arf_outcomes`; no unsupported outcome or denominator is constructed +for the invalid row. Valid coverage refusals still precede benefit +arithmetic. Recomputation uses a separate parameter surface: required NAWI +and contribution bases are already checked for finite positive values +before coverage; ARF age-reduction rates and benefit shares are not inputs +to its original 90/32/15-percent formula (415(a)(1), (f)(2)(C)). + +ARF applies at FRA for RIB and spouses. Ordinary widow adjustments apply at +age 62 and again at widow FRA; credits recognized at 62 persist until FRA, +without adding intervening credits early (402(q)(1)(B)(ii); 20 CFR +404.412(b); RS 00615.490, .580, .582). SSA attains age the day before a +birthday, so a first-day birth attains FRA in the previous month. January 1 +also belongs to the preceding birth-year cohort (RS 00615.003, .004, .015). +Initial RIB at 62 has the separate throughout-the-month requirement. + +Reduction is recalculated on the current COLA-increased unreduced amount, +already adjusted for any family maximum. G's `worker_reduction_fraction`, +`spouse_reduction_fraction`, `survivor_reduction_fraction`, `ceil_dime`, +`floor_dime` and `whole_dollars` are reused. RIB uses 5/9 of one percent for +36 months and 5/12 thereafter; spouse uses 25/36 and 5/12; widow reduction +spreads 28.5 percent across the cohort's full reduction period. The money +reduction rounds upward to a dime, then the benefit rounds downward to a +whole dollar separately (402(q)(8)-(9); RS 00615.101, .201, .301). + +When own entitlement starts after completed spouse ARF, retain the single +spouse adjustment and subtract the supplied independently adjusted own +payment, bounded below by zero (402(k)(3)(A); RS 00615.500A.1). Own PIA +and payable may differ, including because of delayed retirement credits. +Varying own PIA with that payment fixed leaves this spouse component +unchanged. Reapplication retains the supplied payment. Concurrent or +preceding own entitlement uses the excess over own PIA under .500A.2. + +Recomputation extends computation base years through the earnings year, +while preserving the original indexing year, computation-year count and +eligibility-year bend points (415(f)(2)(B)-(C); CFR 404.281, .284). Every +base year from 1951 through that cutoff must have an explicit nonnegative +amount, including explicit zero. A missing key or `UNAVAILABLE` raises +`InvalidFamilyInput` before any coverage refusal. Later amount values are +not read. The adapter calls `ss.statutory_aime.aime_with_computation_years` +and guards its integer floor with exact Fraction indexing and selection. +The original ordinary RIB computation-year count must be consistent with +its eligibility cohort; R3 preserves it rather than recomputing it using +the later earnings year (415(b)(2), (f)(2)(C)). Original insured status and +entitlement are supplied as established facts; earnings arithmetic does +not verify insured status or quarters of coverage. Low-level arbitrary +computation counts and synthetic parameters are formula diagnostics, not +complete claimant cases. + +Required NAWI and contribution-base availability, positivity and finite +values are validated separately from AIME capping, indexing and selection. +Complete earnings and COLA histories and all required parameters are checked +before coverage. Coverage refusals precede any ordinary AIME or PIA +calculation (415(f)(2)(B)-(C); CFR 404.283-.284). An annual sequence validates +every required year through its last cutoff before its first refusal, +without calculating AIME. Unsupported formulas retain their named reason +and produce no ordinary candidate; invalid required inputs still raise +`InvalidFamilyInput` rather than being counted as coverage gaps. + +PIA is the exact 90/32/15-percent formula, floored to a dime. Original bend +points are $180 and $1,085 scaled by NAWI, with half-dollar ties upward +(415(a)(1)(B); G's RS 00605.900 capture). December COLAs from the +eligibility year through the effective month are applied one by one, +flooring each increase to a dime (415(i); existing G capture). A complete +COLA history must report zero increases explicitly. The $1 rule compares +actual dime PIAs after those COLAs; whole-dollar check amounts and +eligibility-level differences cannot substitute (415(f)(4); RS 00605.501). + +An ordinary recomputation takes effect in January after the earnings year. +Death-year earnings take effect in the death month when death is at or +after FRA. The alternative death-before-FRA computation in 415(f)(5) is +refused (415(f)(2)(D); CFR 404.282; RS 00605.401). December processing in +Publication 05-10069 is distinct from January statutory effectiveness. +A sequence carries only accepted eligibility-level PIAs forward. A sub-$1 +candidate remains pending; later positive earnings can produce an increase +against the PIA then in force. A year with zero earnings cannot trigger +reconsideration solely because additional COLAs have accrued (RS 00605.501). + +## Supported and refused configurations + +All refusals carry `FAMILY_CONFIG_UNSUPPORTED` and a named reason. Invalid +inputs raise first. Both modules retain refused rows and weights through +G's denominator counter. A recomputed PIA aggregate is blocked if any +required row is refused. No refused family is replaced by a worker-only +case or removed from a denominator. + +| Configuration | Treatment and authority | +| --- | --- | +| Modern single RIB, spouse or ordinary widow ARF | Supported for original entitlement from January 1978 and payment months from January 1983; 402(q)(10), RS 00615.490 | +| Full or partial final work deduction | One full credit per qualifying month, separately per record; RS 00615.482 | +| Spouse child in care and terminating-event months | Explicit certified months supported at FRA; 402(q)(7)(B)-(C) | +| RIB simultaneously entitled to DIB | Explicit months supported as credits; 402(q)(7)(F), CFR 404.412(a)(6) | +| Widow terminating-event and actual mother/father-rate months | Explicit certified months supported; 402(q)(7)(D)-(E), RS 00615.482 | +| Spouse own RIB/DIB concurrent or preceding | Reduction applied to excess over own PIA; independently adjusted own amount supplied; RS 00615.500 | +| Own benefit first entitled after completed spouse ARF | Single spouse ARF retained, independently computed own payment offset; RS 00615.500A.1 | +| Deceased RIB number holder | Hypothetical adjustment at would-have-been FRA, final RIB deductions before death month; RS 00615.598. The result is a survivor-limit component, never payment to the deceased | +| Ordinary AIME RIB recomputation | Supported eligibility from 1983, complete original basis and histories, continuous declared entitlement; 415(f)(2), CFR 404.284 | +| Death-year recomputation at or after FRA | Supported, effective death month, COLAs cut off at that month; 415(f)(2)(D)(ii) | +| Earlier reentitled spouse adjustment | `arf_spouse_reentitlement_before_fra`; RS 00615.201A.1 conflicts with RS 00615.491A on whether nonentitlement months are removed at FRA or on earlier reentitlement. Both readings are quoted below. A known prior terminating month automatically triggers this refusal before FRA; neither reading is silently admitted | +| Own entitlement after spouse but before ARF | `arf_spouse_own_after_spouse_before_arf`; sequence not settled by the captured .500 procedures | +| Disabled widow and widow dual DIB or RIB | `arf_disabled_widow`, `arf_widow_dual_dib`, `arf_widow_dual_rib`; .590/.595 conversion and earlier reduction histories require additional state | +| RIB-LIM widow | `arf_widow_rib_limit`; .585 requires its separate cap and sometimes no increase despite ARF | +| Nonbinding widow child-in-care floor | `arf_widow_child_care_without_rate_increase`; conflicting readings below | +| Historical widow FRA | `arf_widow_fra_before_1911` or `arf_historical_widow_fra_conflict`; old FRA 62 lies outside G's survivor surface; disputed 1911 cohort below | +| Historical ARF rounding or other suspensions | `arf_initial_entitlement_before_1978`, `arf_payment_before_1983`, `arf_other_suspension`; historical reduction-ratio/rounding rules and nonwork suspensions require separate procedures | +| Historical recomputation eligibility | `recompute_eligibility_before_1983`; CFR .284's historical upward rounding differs from the shared modern helpers | +| AMW, old-start, special/frozen minimum or guarantee PIA | `recompute_non_aime_formula_pia`; CFR .283/.284(f), RS 00605.501D | +| DIB, disability freeze or prior DIB | `recompute_disability_entitlement`, `recompute_disability_freeze`, `recompute_prior_disability`; additional indexing/count, conversion and guarantee rules not implemented | +| WEP for months before 2024 | `recompute_wep_before_2024`; RS 00605.501B.2, CFR .288 | +| Administrative finality or corrections | `recompute_administrative_finality`; RS 00605.405/.580 distinguish a correct new computation from reopening/recalculation | +| Waived or disadvantageous recomputation | `recompute_waived_or_disadvantageous`; CFR .287, RS 00605.407. A higher PIA does not prove higher total family payment | +| Special earnings credits | `recompute_special_earnings_credits`; 415(f)(6) requires additional evidence | +| Death before FRA | `recompute_death_before_fra_415f5`; separate formula under 415(f)(5). Missing FRA is an input error | + +## Executed invariants + +Each row has a Hypothesis property test beside the example tests. + +| Invariant | Property test | +| --- | --- | +| ARF is idempotent, deterministic and retains the original claim | `test_arf_is_idempotent_deterministic_and_preserves_original_claim` | +| Repeated evaluation of any supported or refused iterator ARF claim preserves results, reasons and denominator weights; result credits remain immutable on repeated inspection; immutable-set and iterator inputs agree exactly | `test_iterator_claim_evaluation_preserves_results_and_refusals` | +| Adjusted months + distinct credits = original months; credits are bounded and only original-window months count; input order does not matter | `test_credit_bounds_window_and_order_independence` | +| More credits never lower the nominal benefit factor, dime benefit or whole-dollar benefit; the factor is nonincreasing in adjusted months | `test_more_credits_never_lower_benefit_or_nominal_factor` | +| A partial deduction counts as a full month; duplicate full/partial inputs never double credit | `test_partial_deduction_equals_full_credit_and_duplicate_categories_do_not_add` | +| RIB and spouse credits do not apply before FRA | `test_no_rib_or_spouse_adjustment_before_fra` | +| Widow age-62 credits persist; intervening credits wait for FRA | `test_widow_age62_freezes_credits_until_fra` | +| Zero credits preserve G's benefit; benefits are dime multiples in [0, unreduced amount], factors in [0, 1] | `test_zero_credits_matches_g_exactly_and_remaining_months_differential` | +| FRA follows the original cohort schedule and SSA day-before-birthday calendar | `test_fra_calendar_differential_with_original_cohort_schedule` | +| Invalid age inputs remain errors even with an exceptional-history refusal flag | `test_invalid_age_precedes_any_disabled_widow_coverage_refusal` | +| G's statutory parameter errors propagate unchanged before explicit and automatic ARF refusals through application, evaluation and counting; invalid inputs never become unsupported rows | `test_invalid_statutory_parameters_never_become_arf_refusals` | +| Non-finite numeric rates, shares and brackets raise the same input error through application, evaluation and lazy counting, before ordinary arithmetic or explicit and automatic coverage refusals; invalid rows produce neither an ARF result nor an unsupported outcome | `test_nonfinite_statutory_fields_never_create_arf_results_or_refusals` | +| After completed spouse ARF, the benefit is the nonnegative excess of G's exact adjusted spouse amount over supplied own payable; own PIA changes alone do not affect it, whole dollars floor that excess, and reapplication preserves it | `test_post_arf_own_payable_offset_is_independent_of_pia_and_idempotent` | +| Every ARF row and its weight remains in the denominator | `test_refusal_denominator_keeps_every_weight` | +| Original indexing, computation count and per-year caps match an independent Fraction reference; later NAWI cannot change earlier indexing | `test_original_index_count_and_caps_against_independent_fraction_reference` | +| Original ordinary computation-year count agrees with the statutory count and stays frozen | `test_frozen_ordinary_year_count_matches_original_statutory_count` | +| Exact PIA agrees with the float oracle within $0.000000001 on its shared formula/bend-point surface | `test_exact_pia_matches_float_oracle_to_one_nanodollar` | +| Complete integer histories agree with reused statutory AIME on the shared integer surface | `test_complete_integer_aime_matches_reused_statutory_oracle` | +| Ordinary PIA never decreases even when the candidate is lower | `test_ordinary_recomputed_pia_never_decreases` | +| A chronological annual fold equals individual steps and is nondecreasing | `test_annual_fold_is_non_decreasing_and_matches_individual_steps` | +| Later earnings, including later unavailable values, never alter earlier results | `test_later_earnings_never_change_earlier_results` | +| Earnings are capped per year before indexing and selection | `test_aime_caps_each_year_before_selection` | +| Every year from 1951 through the cutoff requires explicit availability | `test_every_statutory_base_year_requires_explicit_availability` | +| An absent or unavailable base year never becomes zero | `test_unavailable_year_never_becomes_zero` | +| PIA is nonnegative, monotone in AIME and a dime multiple | `test_pia_is_dime_multiple_monotone_and_nonnegative` | +| Application is equivalent to positive earnings and at least $1 after COLAs; rejected candidates preserve the PIA | `test_dollar_rule_exact_equivalence` | +| Earnings map order does not change results; repeated evaluation is deterministic | `test_earnings_mapping_order_independent_and_deterministic` | +| Iterator conditions in a recomputation basis preserve repeated supported results and refused outcomes, with every weight retained | `test_iterator_conditions_preserve_repeated_results_and_outcomes` | +| Every recomputation coverage refusal precedes ordinary AIME/PIA arithmetic, in annual calls and sequences | `test_all_coverage_refusals_precede_ordinary_arithmetic` | +| Invalid required NAWI and contribution bases raise before any coverage refusal or ordinary arithmetic | `test_invalid_required_parameters_precede_refusal_without_arithmetic` | +| An invalid original ordinary computation count raises before a later death-before-FRA refusal in a sequence | `test_invalid_original_count_precedes_sequence_death_refusal` | +| Refusals retain every row and weight and block a PIA target | `test_refusals_retained_in_weighted_denominator` | +| Invalid histories are never counted as coverage gaps | `test_input_error_is_never_classified_as_coverage` | +| Death-month effectiveness excludes later COLAs | `test_death_effective_month_excludes_later_cola` | + +Exact ARF comparisons against G have zero tolerance. Published-source +parameters are reused rather than fit. Deliberate rule mutations and their +failing tests are recorded in the completion report. + +## Worked examples + +Publication 05-10069 starts with PIA $1,300, FRA 67, claim age 62 and 60 +reduction months: 30 percent reduction, $390, gives $910. Twelve crediting +months leave 48 months: 25 percent reduction, $325, gives $975. All 60 +months credited give $1,300. Dime values are $910.00/$975.00/$1,300.00; +whole-dollar benefits are 910/975/1300. RS 00615.480-.598 contains no numeric +ARF worked examples, so the other ARF combinations are invented cases. + +Two invented post-ARF own-entitlement cases distinguish own PIA from own +payable under RS 00615.500A.1. Both use birth January 2, 1960, first spouse +entitlement January 2022, no credits and payment January 2028: 60 RF gives +a 35-percent reduction. Unreduced spouse amount $26.20 has reduction +$9.17 rounded up to $9.20, leaving $17.00. Own PIA $15.00 with independently +adjusted own payable $15.10 leaves $1.90 and whole-dollar $1; using the +PIA as offset would instead give $2.00/$2. Unreduced $650.00 has reduction +$227.50 and adjusted spouse amount $422.50. Own PIA $200.00 and payable +$208.00 leave $214.50/$214; a PIA offset would give $222.50/$222. Both +cases and repeated application are pinned by +`test_own_entitlement_after_spouse_arf_offsets_own_payable_not_pia`. Their +figures are invented diagnostics, not published worked-example figures. + +RS 00615.015 contains twelve birthday cases. Intermediate dime benefits +below are derived exactly; the source prints whole-dollar payments. + +| Birth date | First month | PIA | Reduction months | Exact benefit | Whole dollars | +| --- | --- | ---: | ---: | ---: | ---: | +| 1935-05-31 | 1997-06 | 1207 | 35 | 972.30 | 972 | +| 1935-06-01 | 1997-06 | 1207 | 35 | 972.30 | 972 | +| 1935-06-02 | 1997-06 | 1207 | 36 | 965.60 | 965 | +| 1935-06-03 | 1997-07 | 1207 | 35 | 972.30 | 972 | +| 1935-12-31 | 1998-01 | 1232 | 35 | 992.40 | 992 | +| 1936-01-01 | 1998-01 | 1232 | 35 | 992.40 | 992 | +| 1936-01-02 | 1998-01 | 1266 | 36 | 1012.80 | 1012 | +| 1936-01-03 | 1998-02 | 1266 | 35 | 1019.80 | 1019 | +| 1937-12-31 | 2000-01 | 1372 | 35 | 1105.20 | 1105 | +| 1938-01-01 | 2000-01 | 1372 | 35 | 1105.20 | 1105 | +| 1938-01-02 | 2000-01 | 1410 | 38 | 1116.20 | 1116 | +| 1938-01-03 | 2000-02 | 1410 | 37 | 1122.10 | 1122 | + +RS 00605.501's PIA-only examples are reproduced at the threshold interface, +because SSA supplies no earnings histories. $849.40 to $850.00 is only +$0.60 and is refused despite a whole-dollar increase; $850.40 meets $1. +The $445.50 December 1997 PIA with a $0.90 January 1998 increase remains +$445.50. December 1998's PIA is $451.20, and a $452.20 January 1999 +candidate is accepted if there were 1998 earnings; without earnings it +remains $451.20. The inferred first candidate is $446.40. + +An invented complete 35-year case has original AIME 100 and PIA $90.00. +Adding $420 earnings produces AIME 101 and $90.90, which is refused; +another $420 produces AIME 102 and $91.80, accepted against the PIA still +in force. With a 12-percent accrued COLA, $90.00/$90.90 become +$100.80/$101.80, so the $0.90 eligibility increase meets the actual $1 rule. +Other invented cases write every intermediate figure in their test +docstrings, including preserved 2020 bend points $960/$5,785 versus later +2021 points $996/$6,002 (AIME 1000: $876.80 versus $897.60). + +CFR 404.284's 1979 example uses historical upward dime rounding. AIME 911 +has raw PIA $395.92, rounded to $396.00; successive historical COLAs +9.9/14.3/11.2 percent give $435.30/$497.60/$553.40. These historical +arithmetic figures are pinned beside the modern API's explicit refusal. +The derived whole-dollar original is 432; the successive revised amounts +are 435/497/553, each explicitly asserted. +The legacy average-monthly-wage (AMW) example replaces $2,700 with $12,000, +giving $142,000 total versus the derived prior $132,700. Divisor 22*12=264 +gives AMW $537 versus earlier $502, with supplied table PIA $407.70. +Historical COLAs give $448.10, $512.20 and +$569.60. Its initial $429.20 and table row are source-supplied inputs; +the captured material does not include the AMW table needed to +derive them. The modern API explicitly refuses that formula. The supplied initial +$429.20 is $21.50 above the $407.70 candidate. Applying the same historical +COLAs to the initial value produces $471.70/$539.20/$599.60, versus +$448.10/$512.20/$569.60 for the candidate. That is a $30.00 decrease by +June 1981, rather than an effective $1 increase. Tests preserve this +unexplained captured-source inconsistency side by side; no figure is +changed and no complete accepted recomputation is claimed. The captures +cannot settle whether the initial figure, table row or stated assumptions +need correction. +Tests also assert the derived whole-dollar amounts: 429 initially, +448/512/569 for the candidate and 471/539/599 for the original after COLAs. + +## Findings and readings requiring judgment + +**Ruling d1418 (Max, 2026-10-12): keep the current defaults, and reopen any +one when a population target needs it.** The three readings below that +change money (survivor FRA for births 1911-01-02 to 1912-01-01, nonbinding +widow(er) child-in-care credits, and the timing of a reentitled spouse's +termination credits) stay refused by name and counted in denominators. + +RS 00615.015 says "In 2/1998 Demi has 37 RF." Its table and the same +example's payment text say "Demi - $1122 starting 2/2000". Tests pin both; +February 2000 produces the stated 37 months and $1,122.10/$1,122. This is a +source date erratum, not an altered monetary rule. + +The reused float AIME floors zero for cent earnings $0.02, $2.07 and +$33.91 over three computation years: binary summation is +35.99999999999999 instead of exact $36. The exact guard yields AIME 1. +The executed counterexample was minimized over cent amounts; no two-year +cent-valued example summing $24 was found. This intended oracle divergence +is named `test_minimized_cent_earnings_float_aime_boundary_is_intended_correction`. +Python's ties-to-even bend-point rounding also differs from the statute at +an exact half dollar; the named tie test pins the statutory upward result. +These are arithmetic diagnostics, not competing statutory interpretations. + +Three source readings change money and require Max's judgment: + +1. **Historical widow cohort.** Source conflict + `us:oasdi-survivor-fra-1911-births`. RS 00615.003 says "People Born Before + 1/2/1911" and "FRA for widow(er)'s benefits was age 62", then "People + Born from 1/2/1911 Through 1/1/40" at age 65. CFR 404.409 says "Before + 1/2/1912 62 years." and "1/2/1912—1/1/1940 65 years." Births + January 2, 1911 through January 1, 1912 are refused, including the + public FRA helper. An invented January 2, 1911 birth claiming January + 1973 with PIA $1,000 gives $829 under FRA 65 (36/60 of 28.5 percent) + versus $1,000 under FRA 62: $171 per month. Neither reading is silently + admitted. +2. **Nonbinding widow child-in-care floor.** Source conflict + `us:oasdi-arf-widow-child-in-care-credits`. CFR 404.412(a)(4) credits + "any month in which she or he had in her or his care a child of the + deceased insured individual and for which the child was entitled to + child's benefits". RS 00615.482 instead specifies "widow(er)'s + benefits were increased to the mother or father's rate." The Act + 402(q)(7)(D) refers to "any month in which the reduction in the amount + of such benefit was determined under paragraph (5)(D)". The supported + input certifies actual mother/father-rate months. Nonbinding caregiving + histories require the named refusal flag. At age 64, FRA 67, PIA $1,000, + 36/84 of 28.5 percent gives $877.80. Crediting six nonbinding caregiving + months under the broader CFR reading gives $898.20: $20.40 per month. + Both counterfactual amounts and the explicit refusal are tested. +3. **Spouse reentitlement before FRA.** Source conflict + `us:oasdi-arf-spouse-reentitlement-credits`. RS 00615.201A.1 says "After + 12/72 a spouse reentitled prior to FRA as a spouse on the same earnings + is reduced the same number of months as the earlier entitlement. An + ARF at FRA will remove all months of nonentitlement." RS 00615.491A + says "Beginning in 1/78, if a reduced spouse is terminated and then + reentitled, (months in which the spouse's benefits were terminated will + be eliminated from the reduction factor when the spouse is reentitled." + The unmatched parenthesis is present in the captured .491 text. The + first reading keeps the original reduction months until FRA; the second + removes terminated months on reentitlement. For an invented birth on + January 2, 1960, first reduced entitlement in January 2022, termination + in February 2022 and reentitlement in March 2022, unreduced benefit + $650 and FRA in January 2027 give 60 original reduction months. + Keeping all 60 under .201 gives $422.50 and whole-dollar $422. + Removing February under .491 leaves 59 months and gives $425.20 and + whole-dollar $425, a $2.70 monthly benefit difference and $3 difference + after whole-dollar rounding. The named refusal + `arf_spouse_reentitlement_before_fra` remains in force for the March + 2022 claim. Both counterfactual amounts and the refusal are pinned by + `test_spouse_reentitlement_counterfactual_readings_keep_named_refusal`; + the source and documentation quote test keeps both readings visible. + +The $1-after-COLAs rule is settled expressly by RS 00605.501. Historical +upward rounding is different law, not a source erratum. No choice between +these rules is left implicit. + +## Monthly assembly inputs + +I must supply original benefit-record identity, benefit kind, exact date of +birth, original reduced-entitlement/election month, certified final work +deductions separated from provisional suspensions, and explicit category +credits. It must declare exceptional histories, maintain complete dated +entitlement and termination state, apply any current child-in-care payment +floor separately, and adjust the spouse's own benefit independently. For a +deceased worker it supplies date of death; hypothetical ARF does not imply +payment in or after the death month. + +Recomputation needs the established eligibility year, original indexing +year and computation count, original bend points, eligibility-level PIA in +force, complete annual earnings from 1951 with unavailable years marked, +annual December COLA percentages including explicit zeros, entitlement +window, and death/FRA months. I supplies the earnings-year cutoff and must +retain each result's effective date. Death-year annual earnings may include +amounts paid after death (CFR 404.282); this is retrospective statutory +recomputation, not permission to use later-year earnings early. + +Both modules are added to the historical birth-evidence exclusions and the +matching disjointness test. They remain opt-in references and are not +reachable from the frozen historical projection. + +## Not in scope + +No real microdata, population pipeline, claiming behavior, forecast, +monthly calendar assembly, final deduction calculation, automatic SSA +notices, processing-payment dates, family-payment monotonicity, disability +freeze or conversion machinery, AMW, old-start, guarantee and minimum formulas, +reopening, judicial review, death-before-FRA alternative computation, or +unsupported widow/spouse histories are admitted. Refusals block dependent +results until a separately sourced implementation exists. diff --git a/scripts/first_estimates_birth_evidence.py b/scripts/first_estimates_birth_evidence.py index dec9cf8a..1fd0ac6d 100644 --- a/scripts/first_estimates_birth_evidence.py +++ b/scripts/first_estimates_birth_evidence.py @@ -313,6 +313,9 @@ # Track B G (the gross-benefit layer) is an opt-in statutory reference; # nothing in the historical projection imports it. Path("src/populace_dynamics/track_b/gross_benefits.py"), + # Track B R3 is a statutory reference, with no historical projection use. + Path("src/populace_dynamics/track_b/arf.py"), + Path("src/populace_dynamics/track_b/recompute.py"), # Track B B2 (the 2010-2014 transport gate's shared rules) is used only # by B2's floor builder, comparator and scorer; nothing in the # historical projection imports it. diff --git a/src/populace_dynamics/track_b/arf.py b/src/populace_dynamics/track_b/arf.py new file mode 100644 index 00000000..3b91d853 --- /dev/null +++ b/src/populace_dynamics/track_b/arf.py @@ -0,0 +1,623 @@ +"""R3: record-specific adjustment of the reduction factor (ARF). + +Verification class: statutory conformance. This module admits ARF credits +and benefit arithmetic for the supported invented or published cases only; +it admits no population, behavioral, or forecast claim. + +42 U.S.C. 402(q)(6)-(9), 20 CFR 404.409-.412 and POMS RS 00615.480-.598 +govern the reduction window, excluded months, timing and rounding. R2 +supplies final full/partial work-deduction months separately for each +benefit record. Prorated provisional suspensions are not that input. +The current unreduced benefit is already COLA-increased and, for an +auxiliary, already reduced for the family maximum. G's exact Fraction +reduction functions, ceil_dime, floor_dime and whole_dollars are reused. + +An ARF always starts from the original reduction window. Applying a +result again therefore cannot credit the same month twice. Widow(er) +credits are applied at age 62 and at widow(er) FRA; the age-62 adjustment +persists, without new credits, until FRA (RS 00615.582; CFR 404.412(b)). +The month of age attainment precedes the birth month for a first-day +birthday; a January 1 birth also uses the preceding FRA cohort. + +Collection inputs are snapshotted into immutable tuples or frozensets at +construction, including one-shot iterators. Repeated original-claim +evaluation preserves both dollar amounts and coverage refusals. Contents +are validated before coverage is decided, without consuming caller state. +Pre-FRA spouse reentitlement is refused because RS 00615.201A.1 and +.491A conflict on when termination months leave the reduction factor. + +Unsupported configurations raise FamilyConfigurationUnsupported with +named ARF reasons. Invalid inputs, including G's statutory rate bundle, +are checked before coverage and benefit arithmetic. Non-finite numeric +rates, benefit shares and brackets are rejected locally before G's +statutory comparison. Parameter ValueErrors propagate through evaluate_arf +and lazy count_arf_outcomes; only valid coverage refusals enter the weighted +denominator. +This module does not assemble payments, deductions, or event histories. +""" + +from __future__ import annotations + +import math +from collections.abc import Callable, Iterable +from dataclasses import dataclass, field +from datetime import date +from enum import Enum +from fractions import Fraction + +from populace_dynamics.ss.params import SSAParameters +from populace_dynamics.track_b.gross_benefits import ( + FamilyConfigurationUnsupported, + FamilyDenominator, + FamilyOutcome, + InvalidFamilyInput, + YearMonth, + ceil_dime, + count_family_outcomes, + floor_dime, + spouse_reduction_fraction, + statutory_rates, + survivor_reduction_fraction, + whole_dollars, + widow_reduction_period_months, + worker_reduction_fraction, +) + +ARF_VERSION = "track-b-r3-arf-v1" +MonthInput = YearMonth | tuple[int, int] +_CREDIT_FIELDS = ( + "full_work_deduction_months", + "partial_work_deduction_months", + "child_in_care_months", + "nonentitlement_months", + "simultaneous_dib_months", + "mother_father_rate_months", +) + + +class ARFBenefitKind(Enum): + RIB = "rib" + SPOUSE = "spouse" + WIDOW = "widow" + + +class ARFUnsupportedReason(Enum): + INITIAL_ENTITLEMENT_BEFORE_1978 = "arf_initial_entitlement_before_1978" + PAYMENT_BEFORE_1983 = "arf_payment_before_1983" + SPOUSE_REENTITLEMENT_BEFORE_FRA = "arf_spouse_reentitlement_before_fra" + DISABLED_WIDOW = "arf_disabled_widow" + WIDOW_DUAL_DIB = "arf_widow_dual_dib" + WIDOW_DUAL_RIB = "arf_widow_dual_rib" + WIDOW_RIB_LIMIT = "arf_widow_rib_limit" + WIDOW_FRA_BEFORE_1911 = "arf_widow_fra_before_1911" + HISTORICAL_WIDOW_FRA_CONFLICT = "arf_historical_widow_fra_conflict" + WIDOW_CHILD_CARE_WITHOUT_RATE_INCREASE = ( + "arf_widow_child_care_without_rate_increase" + ) + SPOUSE_OWN_AFTER_SPOUSE_BEFORE_ARF = ( + "arf_spouse_own_after_spouse_before_arf" + ) + OTHER_SUSPENSION = "arf_other_suspension" + + +class SpouseOwnSequence(Enum): + """The supported RS 00615.500 sequencing, and one named refusal.""" + + CONCURRENT_OR_PRECEDING = "concurrent_or_preceding" + AFTER_SPOUSE_ARF = "after_spouse_arf" + AFTER_SPOUSE_BEFORE_ARF = "after_spouse_before_arf" + + +def _month(value: MonthInput, label: str) -> YearMonth: + if isinstance(value, YearMonth): + result = value + elif isinstance(value, tuple) and len(value) == 2: + try: + result = YearMonth(*value) + except (TypeError, ValueError) as error: + raise InvalidFamilyInput(f"{label}: {error}") from error + else: + raise InvalidFamilyInput( + f"{label} must be a YearMonth or (year, month)" + ) + if result.year < 1: + raise InvalidFamilyInput(f"{label} year must be positive") + return result + + +def _month_set( + values: Iterable[MonthInput], label: str +) -> frozenset[YearMonth]: + try: + return frozenset(_month(value, label) for value in values) + except TypeError as error: + raise InvalidFamilyInput( + f"{label} must be an iterable of months" + ) from error + + +def _amount(value: object, label: str) -> Fraction: + """Use exact inputs, with explicit zeros; do not accept binary floats.""" + if isinstance(value, (bool, float)): + raise InvalidFamilyInput(f"{label} must be an exact amount") + try: + result = Fraction(value) + except (ValueError, TypeError, ZeroDivisionError) as error: + raise InvalidFamilyInput(f"{label} must be an exact amount") from error + if result < 0 or (result * 10).denominator != 1: + raise InvalidFamilyInput(f"{label} must be nonnegative whole dimes") + return result + + +def _index(month: YearMonth) -> int: + return month.year * 12 + month.month - 1 + + +def _from_index(index: int) -> YearMonth: + year, month = divmod(index, 12) + return YearMonth(year, month + 1) + + +def age_attainment_month(birth_date: date, age_months: int) -> YearMonth: + """SSA day-before-birthday month (RS 00615.004/.015). + + The throughout-the-month requirement for initial RIB at 62 is a + different rule and is not imposed on FRA attainment. + """ + if type(birth_date) is not date: + raise InvalidFamilyInput("birth_date must be a datetime.date") + if type(age_months) is not int or age_months < 0: + raise InvalidFamilyInput("age_months must be a nonnegative integer") + index = birth_date.year * 12 + birth_date.month - 1 + age_months + return _from_index(index - (birth_date.day == 1)) + + +def _fra_month_for_input_validation( + birth_date: date, kind: ARFBenefitKind, params: SSAParameters +) -> YearMonth: + """Validate dates against POMS FRA; the disputed cohort is then refused. + + The POMS age-65 reading gives the wider legal input window for the + 1911 cohort. This helper does not authorize payment under that reading. + """ + if type(birth_date) is not date or not isinstance(kind, ARFBenefitKind): + raise InvalidFamilyInput("birth_date and benefit kind must be valid") + if kind is ARFBenefitKind.WIDOW: + # RS 00615.003B.1's historical FRA 62 is outside G's 60-84-month + # survivor interpolation surface, and is refused by apply_arf. + months = ( + 744 + if birth_date < date(1911, 1, 2) + else 720 + widow_reduction_period_months(birth_date) + ) + else: + cohort = birth_date.year - ( + birth_date.month == 1 and birth_date.day == 1 + ) + months = params.fra_months(cohort) + return age_attainment_month(birth_date, months) + + +def full_retirement_age_month( + birth_date: date, kind: ARFBenefitKind, params: SSAParameters +) -> YearMonth: + """Record-specific FRA; refuse the disputed historic survivor cohort.""" + result = _fra_month_for_input_validation(birth_date, kind, params) + if kind is ARFBenefitKind.WIDOW and date(1911, 1, 2) <= birth_date <= date( + 1912, 1, 1 + ): + # Source conflict us:oasdi-survivor-fra-1911-births + _refuse( + ARFUnsupportedReason.HISTORICAL_WIDOW_FRA_CONFLICT, + "RS 00615.003 gives FRA 65 but 20 CFR 404.409 gives FRA 62", + ) + return result + + +@dataclass(frozen=True) +class SpouseOwnBenefit: + """Own RIB/DIB amounts supplied after their independent adjustment. + + CONCURRENT_OR_PRECEDING reduces the spouse excess over own PIA. + AFTER_SPOUSE_ARF preserves the single-entitlement spouse reduction + and offsets the independently computed own payment (.500A.1). + This component does not recompute the own benefit or its ARF credits. + """ + + pia: Fraction + payable: Fraction + sequence: SpouseOwnSequence + + +@dataclass(frozen=True) +class ARFClaim: + """An original reduction window and explicit record-specific credits. + + first_reduction_month is the effective reduced-spouse election month + for a spouse. For ordinary RIB or WIB it is the first entitlement + month. Nonentitlement months mean termination under 402(q)(7)(C)/(E), + not missing records. Mother/father-rate months mean (q)(5)(D) actually + determined the widow(er)'s benefit, rather than every caregiving month. + All month and condition iterables are copied at construction. Tuple + snapshots retain malformed contents for the complete validation that + precedes coverage checks, while never retaining a mutable collection + or a one-shot iterator as claim state. + """ + + kind: ARFBenefitKind + birth_date: date + first_reduction_month: MonthInput + unreduced_benefit: Fraction + full_work_deduction_months: Iterable[MonthInput] = field( + default_factory=frozenset + ) + partial_work_deduction_months: Iterable[MonthInput] = field( + default_factory=frozenset + ) + child_in_care_months: Iterable[MonthInput] = field( + default_factory=frozenset + ) + nonentitlement_months: Iterable[MonthInput] = field( + default_factory=frozenset + ) + simultaneous_dib_months: Iterable[MonthInput] = field( + default_factory=frozenset + ) + mother_father_rate_months: Iterable[MonthInput] = field( + default_factory=frozenset + ) + death_date: date | None = None + spouse_own: SpouseOwnBenefit | None = None + conditions: Iterable[ARFUnsupportedReason] = field( + default_factory=frozenset + ) + + def __post_init__(self): + for name in (*_CREDIT_FIELDS, "conditions"): + try: + values = getattr(self, name) + snapshot = ( + values if isinstance(values, frozenset) else tuple(values) + ) + except TypeError as error: + raise InvalidFamilyInput( + f"{name} must be an iterable" + ) from error + object.__setattr__(self, name, snapshot) + + +@dataclass(frozen=True) +class ARFResult: + """Benefit component after ARF and, when supplied, the own offset. + + factor is the nominal age factor before dime rounding or own offset. + An own RIB/DIB payment is separate from benefit, and is not rounded + together with it. A deceased worker result is a hypothetical RIB for + the survivor limit, never an instruction to pay the deceased. + """ + + claim: ARFClaim + payment_month: YearMonth + fra_month: YearMonth + effective_adjustment_month: YearMonth | None + original_reduction_months: int + adjusted_reduction_months: int + crediting_months: frozenset[YearMonth] + factor: Fraction + reduction: Fraction + benefit: Fraction + whole_dollar_benefit: int + + def __post_init__(self): + object.__setattr__( + self, + "crediting_months", + _month_set(self.crediting_months, "crediting_months"), + ) + + +def _check_finite_statutory_parameters(params: SSAParameters) -> None: + """Reject non-finite bundle values before G's rate comparisons. + + Check the full shared statutory bundle for every ARF record kind. + Finite mismatches still use G's existing statutory_rates errors. + """ + fields = [ + (f"{name}[{index}]", value) + for name in ("early_monthly_rates", "spousal_early_monthly_rates") + for index, value in enumerate(getattr(params, name)) + ] + fields.extend( + (name, getattr(params, name)) + for name in ( + "early_first_bracket_months", + "spousal_early_first_bracket_months", + "survivor_reduction_floor", + "rib_lim_pia_share", + "spousal_pia_share", + "survivor_pia_share", + ) + ) + for label, value in fields: + if not math.isfinite(float(value)): + raise ValueError( + f"Parameter bundle {label}={value!r} must be finite; " + "refusing to compute ARF." + ) + + +def _check_claim(claim: ARFClaim, params: SSAParameters) -> ARFClaim: + """Validate claim and statutory bundle before coverage or money rules.""" + if not isinstance(claim, ARFClaim): + raise InvalidFamilyInput("claim must be an ARFClaim") + if not isinstance(claim.kind, ARFBenefitKind): + raise InvalidFamilyInput("kind must be an ARFBenefitKind") + if type(claim.birth_date) is not date: + raise InvalidFamilyInput("birth_date must be a datetime.date") + first = _month(claim.first_reduction_month, "first_reduction_month") + amount = _amount(claim.unreduced_benefit, "unreduced_benefit") + try: + conditions = frozenset(claim.conditions) + except TypeError as error: + raise InvalidFamilyInput("conditions must be an iterable") from error + if any(not isinstance(c, ARFUnsupportedReason) for c in conditions): + raise InvalidFamilyInput( + "conditions must contain ARFUnsupportedReason" + ) + values = {} + for name in _CREDIT_FIELDS: + values[name] = _month_set(getattr(claim, name), name) + if ( + values["child_in_care_months"] + and claim.kind is not ARFBenefitKind.SPOUSE + ): + raise InvalidFamilyInput( + "child_in_care_months are spouse credits only" + ) + if values["nonentitlement_months"] and claim.kind is ARFBenefitKind.RIB: + raise InvalidFamilyInput( + "RIB nonentitlement months are not ARF credits" + ) + if ( + values["simultaneous_dib_months"] + and claim.kind is not ARFBenefitKind.RIB + ): + raise InvalidFamilyInput( + "simultaneous_dib_months are RIB credits only" + ) + if ( + values["mother_father_rate_months"] + and claim.kind is not ARFBenefitKind.WIDOW + ): + raise InvalidFamilyInput( + "mother_father_rate_months are widow credits only" + ) + if claim.death_date is not None: + if ( + type(claim.death_date) is not date + or claim.death_date < claim.birth_date + ): + raise InvalidFamilyInput("death_date must be a date after birth") + if claim.kind is not ARFBenefitKind.RIB: + raise InvalidFamilyInput( + "death_date applies to the deceased RIB only" + ) + if YearMonth(claim.death_date.year, claim.death_date.month) < first: + raise InvalidFamilyInput( + "death cannot precede first reduction month" + ) + own = claim.spouse_own + if own is not None: + if claim.kind is not ARFBenefitKind.SPOUSE or not isinstance( + own, SpouseOwnBenefit + ): + raise InvalidFamilyInput( + "spouse_own requires a spouse own benefit" + ) + if not isinstance(own.sequence, SpouseOwnSequence): + raise InvalidFamilyInput("invalid spouse own entitlement sequence") + own = SpouseOwnBenefit( + _amount(own.pia, "own PIA"), + _amount(own.payable, "own payable"), + own.sequence, + ) + _check_finite_statutory_parameters(params) + statutory_rates(params) + fra = _fra_month_for_input_validation(claim.birth_date, claim.kind, params) + if claim.kind is ARFBenefitKind.WIDOW: + minimum_age = ( + 600 if ARFUnsupportedReason.DISABLED_WIDOW in conditions else 720 + ) + else: + minimum_age = 744 + earliest = age_attainment_month(claim.birth_date, minimum_age) + if claim.kind is ARFBenefitKind.RIB and claim.birth_date.day != 2: + earliest = _from_index(_index(earliest) + 1) + if first < earliest: + raise InvalidFamilyInput( + "first reduction month precedes the minimum age" + ) + if first > fra: + raise InvalidFamilyInput("first reduction month must not follow FRA") + return ARFClaim( + claim.kind, + claim.birth_date, + first, + amount, + **values, + death_date=claim.death_date, + spouse_own=own, + conditions=conditions, + ) + + +def _refuse(reason: ARFUnsupportedReason, detail: str) -> None: + raise FamilyConfigurationUnsupported(reason, detail) + + +def apply_arf( + claim: ARFClaim | ARFResult, + *, + payment_month: MonthInput, + params: SSAParameters, +) -> ARFResult: + """Apply record-specific credits at FRA (widow(er) also at age 62). + + Reapplying an ARFResult uses its original claim, not its adjusted + months. Crediting inputs outside the original window do not count. + Zero credits leave the benefit exactly as G would compute it. + """ + if isinstance(claim, ARFResult): + claim = claim.claim + claim = _check_claim(claim, params) + payment = _month(payment_month, "payment_month") + fra = _fra_month_for_input_validation(claim.birth_date, claim.kind, params) + first = claim.first_reduction_month + if payment < first: + raise InvalidFamilyInput( + "payment_month precedes the first reduction month" + ) + own = claim.spouse_own + if ( + own is not None + and own.sequence is SpouseOwnSequence.AFTER_SPOUSE_ARF + and payment < fra + ): + raise InvalidFamilyInput( + "own entitlement after spouse ARF cannot precede FRA" + ) + if claim.kind is ARFBenefitKind.WIDOW and date( + 1911, 1, 2 + ) <= claim.birth_date <= date(1912, 1, 1): + # Source conflict us:oasdi-survivor-fra-1911-births + _refuse( + ARFUnsupportedReason.HISTORICAL_WIDOW_FRA_CONFLICT, + "RS 00615.003 gives FRA 65 but 20 CFR 404.409 gives FRA 62", + ) + if first < YearMonth(1978, 1): + _refuse( + ARFUnsupportedReason.INITIAL_ENTITLEMENT_BEFORE_1978, + "RS 00615.490B.2 requires the historical money-reduction ratio", + ) + if payment < YearMonth(1983, 1): + _refuse( + ARFUnsupportedReason.PAYMENT_BEFORE_1983, + "G's shared dime-rounding surface begins January 1983", + ) + if claim.kind is ARFBenefitKind.WIDOW and claim.birth_date < date( + 1911, 1, 2 + ): + _refuse( + ARFUnsupportedReason.WIDOW_FRA_BEFORE_1911, + "RS 00615.003B.1's historical FRA 62 is outside G's widow period", + ) + # Source conflict us:oasdi-arf-widow-child-in-care-credits + # Declared nonbinding care is refused; actual rate months count below. + if claim.conditions: + reason = sorted(claim.conditions, key=lambda c: c.value)[0] + _refuse( + reason, "this exceptional ARF history requires a separate rule" + ) + if ( + claim.kind is ARFBenefitKind.SPOUSE + and payment < fra + and any( + first <= month < payment for month in claim.nonentitlement_months + ) + ): + # Source conflict us:oasdi-arf-spouse-reentitlement-credits + _refuse( + ARFUnsupportedReason.SPOUSE_REENTITLEMENT_BEFORE_FRA, + "RS 00615.201A.1 and .491A conflict on pre-FRA reentitlement timing; " + "this module supports the final FRA adjustment only", + ) + if ( + own is not None + and own.sequence is SpouseOwnSequence.AFTER_SPOUSE_BEFORE_ARF + ): + _refuse( + ARFUnsupportedReason.SPOUSE_OWN_AFTER_SPOUSE_BEFORE_ARF, + "RS 00615.500 does not supply this entitlement sequence's procedure", + ) + age62 = age_attainment_month(claim.birth_date, 744) + effective = fra if payment >= fra else None + if claim.kind is ARFBenefitKind.WIDOW and first <= age62 <= payment < fra: + effective = age62 + limit = effective + if claim.death_date is not None: + death_month = YearMonth(claim.death_date.year, claim.death_date.month) + # Final RIB work deductions precede the death month: the worker + # has no RIB benefit in the month of death. RS 00615.598 permits + # the hypothetical adjustment only at would-have-been FRA. + if limit is not None: + limit = min(limit, death_month) + original = _index(fra) - _index(first) + eligible = ( + claim.full_work_deduction_months + | claim.partial_work_deduction_months + | claim.child_in_care_months + | claim.nonentitlement_months + | claim.simultaneous_dib_months + # Source conflict us:oasdi-arf-widow-child-in-care-credits + | claim.mother_father_rate_months + ) + credits = frozenset( + month + for month in eligible + if limit is not None and first <= month < limit + ) + adjusted = original - len(credits) + if claim.kind is ARFBenefitKind.RIB: + reduction_fraction = worker_reduction_fraction(adjusted, params) + elif claim.kind is ARFBenefitKind.SPOUSE: + reduction_fraction = spouse_reduction_fraction(adjusted, params) + else: + reduction_fraction = survivor_reduction_fraction( + adjusted, widow_reduction_period_months(claim.birth_date), params + ) + base = claim.unreduced_benefit + if ( + own is not None + and own.sequence is SpouseOwnSequence.CONCURRENT_OR_PRECEDING + ): + base = max(Fraction(0), base - own.pia) + reduction = ceil_dime(base * reduction_fraction) + benefit = floor_dime(base - reduction) + if own is not None and own.sequence is SpouseOwnSequence.AFTER_SPOUSE_ARF: + benefit = max(Fraction(0), benefit - own.payable) + return ARFResult( + claim, + payment, + fra, + effective, + original, + adjusted, + credits, + 1 - reduction_fraction, + reduction, + benefit, + whole_dollars(benefit), + ) + + +def evaluate_arf( + key: str, weight: object, compute: Callable[[], ARFResult] +) -> FamilyOutcome: + """Keep ARF refusals in G's existing weighted outcome denominator. + + FamilyOutcome's benefits field carries an ARFResult here; G's + denominator uses only supported/weight/unsupported, not family money. + G's family-money aggregation functions are not ARF aggregators. + """ + try: + result = compute() + except FamilyConfigurationUnsupported as error: + return FamilyOutcome(key, weight, None, error) + if not isinstance(result, ARFResult): + raise TypeError("compute must return an ARFResult") + return FamilyOutcome(key, weight, result, None) + + +def count_arf_outcomes(outcomes: Iterable[FamilyOutcome]) -> FamilyDenominator: + """Reuse G's denominator: unsupported rows and weights are retained.""" + return count_family_outcomes(outcomes) diff --git a/src/populace_dynamics/track_b/recompute.py b/src/populace_dynamics/track_b/recompute.py new file mode 100644 index 00000000..98f4406b --- /dev/null +++ b/src/populace_dynamics/track_b/recompute.py @@ -0,0 +1,703 @@ +"""R3 automatic earnings recomputation: a Python statutory reference. + +Verification class: statutory conformance. Admitted scope: ordinary AIME +retirement recomputations for eligibility from 1983, including death-year +earnings after FRA, over invented cases and captured SSA examples. There +is no population, behavioral, forecast, administrative-payment-date or +family-payment claim. Monthly assembly must independently apply G and +R2 to the resulting PIA and retain refusals in every denominator. + +42 U.S.C. 415(f)(2)(B)-(C) freezes the original indexing year, computation +year count and benefit formula. Base years extend through the earnings +year, and later earnings cannot enter an earlier recomputation. The AIME +adapter calls ss.statutory_aime.aime_with_computation_years, after requiring +every base year's earnings explicitly, including explicit zeros. Because +that oracle casts earnings to binary floats, the adapter also checks its +floor against exact Fraction indexing and corrects a binary boundary +disagreement; no formula or year-selection convention is changed. + +PIA arithmetic is exact 90/32/15 percent, dime-floored (415(a)(1), (i)). +Each COLA is dime-floored using G's helper. RS 00605.501A settles the $1 +rule of 415(f)(4): compare the actual PIAs AFTER every COLA through the +effective month, never whole-dollar benefits or eligibility-year PIAs. +Ordinary effectiveness is January following the earnings year; death-year +earnings take effect in the month of death (415(f)(2)(D), CFR 404.282). +The December processing described in Publication 05-10069 is not an +effective date and is outside this module. + +The caller supplies the established original basis, the eligibility-level +PIA then in force, complete earnings and COLA histories, and entitlement +and death/FRA months. Original insured status and entitlement are supplied +and assumed; this arithmetic does not establish insured status or quarters +of coverage. A chronological sequence is a fold: a sub-$1 +candidate never replaces the PIA of record; later years compare against +the last accepted PIA. A year without earnings cannot trigger a delayed +sub-$1 recomputation (RS 00605.501A). + +Unsupported configurations raise G's FamilyConfigurationUnsupported with +named reasons. Invalid inputs are validated before coverage refusals. +History and required-parameter validation precede every coverage check; +ordinary AIME indexing, selection and PIA arithmetic run only after it. +Pre-1983 eligibility, other PIA formulas, DIB/freeze/prior DIB, WEP before +2024, administrative finality, waived/disadvantageous recomputations, +special credits and death before FRA under 415(f)(5) are explicitly +refused. No unavailable year is silently treated as zero. +""" + +from __future__ import annotations + +import math +from collections.abc import Callable, Iterable, Mapping, Sequence +from dataclasses import dataclass +from decimal import Decimal +from enum import Enum +from fractions import Fraction + +from populace_dynamics.ss.params import SSAParameters +from populace_dynamics.ss.statutory_aime import aime_with_computation_years +from populace_dynamics.track_b.gross_benefits import ( + FamilyConfigurationUnsupported, + FamilyTargetBlocked, + InvalidFamilyInput, + YearMonth, + count_family_outcomes, + floor_dime, + increase_by_colas, +) + +__all__ = [ + "UNAVAILABLE", + "PIAFormula", + "RecomputationBasis", + "RecomputationCondition", + "RecomputationDecision", + "RecomputationResult", + "RecomputationOutcome", + "RecomputationUnsupportedReason", + "annual_recomputation", + "earnings_aime", + "evaluate_recomputation", + "eligibility_bend_points", + "exact_pia", + "recomputation_decision", + "recompute_sequence", + "weighted_recomputed_pia", +] + + +class EarningsAvailability(Enum): + """An unavailable amount is distinct from an explicitly observed zero.""" + + UNAVAILABLE = "unavailable" + + +UNAVAILABLE = EarningsAvailability.UNAVAILABLE + + +class PIAFormula(str, Enum): + AIME = "aime" + AMW = "average_monthly_wage" + OLD_START = "old_start" + SPECIAL_MINIMUM = "special_minimum" + FROZEN_MINIMUM = "frozen_minimum" + GUARANTEE = "guarantee" + + +class RecomputationCondition(str, Enum): + DISABILITY_ENTITLEMENT = "disability_entitlement" + DISABILITY_FREEZE = "disability_freeze" + PRIOR_DISABILITY = "prior_disability" + WEP_BEFORE_2024 = "wep_before_2024" + ADMINISTRATIVE_FINALITY = "administrative_finality" + WAIVED_OR_DISADVANTAGEOUS = "waived_or_disadvantageous" + SPECIAL_EARNINGS_CREDITS = "special_earnings_credits" + + +class RecomputationUnsupportedReason(str, Enum): + ELIGIBILITY_BEFORE_1983 = "recompute_eligibility_before_1983" + NON_AIME_FORMULA_PIA = "recompute_non_aime_formula_pia" + DISABILITY_ENTITLEMENT = "recompute_disability_entitlement" + DISABILITY_FREEZE = "recompute_disability_freeze" + PRIOR_DISABILITY = "recompute_prior_disability" + WEP_BEFORE_2024 = "recompute_wep_before_2024" + ADMINISTRATIVE_FINALITY = "recompute_administrative_finality" + WAIVED_OR_DISADVANTAGEOUS = "recompute_waived_or_disadvantageous" + SPECIAL_EARNINGS_CREDITS = "recompute_special_earnings_credits" + DEATH_BEFORE_FRA = "recompute_death_before_fra_415f5" + + +def _year(value: object, label: str) -> int: + if type(value) is not int or not 1 <= value <= 9999: + raise InvalidFamilyInput(f"{label} must be an integer year 1-9999") + return value + + +def _exact(value: object, label: str) -> Fraction: + if isinstance(value, bool) or not isinstance( + value, (int, str, Decimal, Fraction) + ): + raise InvalidFamilyInput(f"{label} requires an exact number") + try: + amount = Fraction(value) + except (ValueError, TypeError, ZeroDivisionError) as exc: + raise InvalidFamilyInput(f"{label} requires a finite number") from exc + if amount < 0: + raise InvalidFamilyInput(f"{label} must be nonnegative") + return amount + + +def _dimes(value: object, label: str) -> Fraction: + amount = _exact(value, label) + if (amount * 10).denominator != 1: + raise InvalidFamilyInput(f"{label} must be a multiple of $0.10") + return amount + + +def _month(value: object, label: str) -> YearMonth: + if not isinstance(value, YearMonth): + raise InvalidFamilyInput(f"{label} must be a YearMonth") + _year(value.year, label) + return value + + +def _bend_points(values: object) -> tuple[int, int]: + if not isinstance(values, tuple) or len(values) != 2: + raise InvalidFamilyInput("bend_points must be a pair") + first, second = values + if ( + type(first) is not int + or type(second) is not int + or not 0 < first < second + ): + raise InvalidFamilyInput( + "bend points must be increasing whole dollars" + ) + return first, second + + +@dataclass(frozen=True) +class RecomputationBasis: + """Original, established 415 computation factors; never derived anew. + + ``first_base_year`` is 1951 for the ordinary AIME cases supported here. + Every year through the earnings cutoff must be explicitly reported. + The entitlement window is continuous; interruption cases require + their own adapter. Conditions are frozen once at construction, + including when supplied as a one-shot iterator. + """ + + eligibility_year: int + indexing_year: int + computation_years: int + bend_points: tuple[int, int] + entitlement_month: YearMonth + first_base_year: int = 1951 + entitlement_end_month: YearMonth | None = None + formula: PIAFormula = PIAFormula.AIME + conditions: frozenset[RecomputationCondition] = frozenset() + + def __post_init__(self): + _year(self.eligibility_year, "eligibility_year") + _year(self.indexing_year, "indexing_year") + _year(self.first_base_year, "first_base_year") + if self.first_base_year != 1951: + raise InvalidFamilyInput("ordinary AIME base years begin in 1951") + if self.indexing_year != self.eligibility_year - 2: + raise InvalidFamilyInput("original indexing year must be ELY - 2") + if ( + type(self.computation_years) is not int + or not 2 <= self.computation_years <= 35 + ): + raise InvalidFamilyInput("computation_years must be 2-35") + _bend_points(self.bend_points) + _month(self.entitlement_month, "entitlement_month") + if self.entitlement_month.year < self.eligibility_year: + raise InvalidFamilyInput("entitlement cannot precede eligibility") + if self.first_base_year >= self.entitlement_month.year: + raise InvalidFamilyInput( + "original base period must precede entitlement" + ) + if self.computation_years > self.entitlement_month.year - 1951: + raise InvalidFamilyInput( + "computation count exceeds original base period" + ) + if self.entitlement_end_month is not None: + _month(self.entitlement_end_month, "entitlement_end_month") + if self.entitlement_end_month < self.entitlement_month: + raise InvalidFamilyInput( + "entitlement end precedes entitlement" + ) + if not isinstance(self.formula, PIAFormula): + raise InvalidFamilyInput("formula must be a PIAFormula") + try: + conditions = frozenset(self.conditions) + except TypeError as exc: + raise InvalidFamilyInput( + "conditions must be a collection" + ) from exc + if any(not isinstance(c, RecomputationCondition) for c in conditions): + raise InvalidFamilyInput("unknown recomputation condition") + object.__setattr__(self, "conditions", conditions) + + +@dataclass(frozen=True) +class RecomputationDecision: + """Threshold comparison at the effective-month PIA level, not checks.""" + + pia_of_record: Fraction + candidate_pia: Fraction + increase: Fraction + applied: bool + pia_in_force: Fraction + + +@dataclass(frozen=True) +class RecomputationResult: + earnings_year: int + effective_month: YearMonth + aime: int + candidate_eligibility_pia: Fraction + eligibility_pia_in_force: Fraction + decision: RecomputationDecision + + +@dataclass(frozen=True) +class RecomputationOutcome: + """An earnings-year row retained for G's common denominator counter.""" + + key: str + weight: Fraction + result: RecomputationResult | None + unsupported: FamilyConfigurationUnsupported | None + + def __post_init__(self): + if (self.result is None) == (self.unsupported is None): + raise InvalidFamilyInput("exactly one result/refusal is required") + object.__setattr__(self, "weight", _exact(self.weight, "weight")) + + @property + def supported(self) -> bool: + return self.result is not None + + +def evaluate_recomputation( + key: str, + weight: object, + compute: Callable[[], RecomputationResult], +) -> RecomputationOutcome: + """Catch coverage only; input errors never become denominator gaps.""" + checked_weight = _exact(weight, "weight") + try: + result = compute() + except FamilyConfigurationUnsupported as error: + return RecomputationOutcome(key, checked_weight, None, error) + if not isinstance(result, RecomputationResult): + raise InvalidFamilyInput("compute must return a recomputation result") + return RecomputationOutcome(key, checked_weight, result, None) + + +def weighted_recomputed_pia( + outcomes: Iterable[RecomputationOutcome], +) -> Fraction: + """PIA aggregate over all rows, blocked when any row was refused.""" + rows = tuple(outcomes) + counts = count_family_outcomes(rows) + if counts.unsupported_rows: + raise FamilyTargetBlocked( + f"{counts.unsupported_rows} of {counts.rows} recomputations are " + "FAMILY_CONFIG_UNSUPPORTED; the PIA target requires them" + ) + return sum( + (row.weight * row.result.decision.pia_in_force for row in rows), + Fraction(0), + ) + + +def eligibility_bend_points( + eligibility_year: int, params: SSAParameters +) -> tuple[int, int]: + """415(a)(1)(B): NAWI-scaled $180/$1,085, $0.50 ties upward. + + Call once when constructing the original basis. Oracle bend_points + uses Python's ties-to-even ``round``; the statutory half-dollar case + is an intended divergence, pinned explicitly by a test. + """ + _year(eligibility_year, "eligibility_year") + try: + index = Fraction(str(params.nawi[eligibility_year - 2])) + base = Fraction(str(params.nawi[1977])) + except (KeyError, ValueError, ZeroDivisionError) as exc: + raise InvalidFamilyInput("required NAWI is unavailable") from exc + if index <= 0 or base <= 0: + raise InvalidFamilyInput("NAWI must be positive") + return tuple( + math.floor(v * index / base + Fraction(1, 2)) for v in (180, 1085) + ) + + +def exact_pia(aime: int, bend_points: tuple[int, int]) -> Fraction: + """415(a)(1): original 90/32/15 formula, dime-floored exactly.""" + if type(aime) is not int or aime < 0: + raise InvalidFamilyInput("AIME must be nonnegative whole dollars") + first, second = _bend_points(bend_points) + amount = ( + Fraction(9, 10) * min(aime, first) + + Fraction(8, 25) * max(0, min(aime, second) - first) + + Fraction(3, 20) * max(0, aime - second) + ) + return floor_dime(amount) + + +def _history_prefix( + history: Mapping[int, object], basis: RecomputationBasis, year: int +) -> dict[int, Fraction]: + if not isinstance(history, Mapping): + raise InvalidFamilyInput("earnings history must be a mapping") + for key in history: + _year(key, "earnings year") + values = {} + for base_year in range(basis.first_base_year, year + 1): + if base_year not in history or history[base_year] is UNAVAILABLE: + raise InvalidFamilyInput(f"earnings unavailable for {base_year}") + values[base_year] = _exact(history[base_year], f"earnings {base_year}") + return values + + +def _aime_parameters( + prefix: Mapping[int, Fraction], + basis: RecomputationBasis, + params: SSAParameters, +) -> tuple[dict[int, Fraction], dict[int, Fraction]]: + """Check availability and positivity without computing any earnings.""" + try: + nawi = { + basis.indexing_year: Fraction( + str(params.nawi[basis.indexing_year]) + ) + } + if nawi[basis.indexing_year] <= 0: + raise InvalidFamilyInput("NAWI must be positive") + wage_base = {} + for year in prefix: + wage_base[year] = Fraction(str(params.wage_base_for(year))) + if wage_base[year] <= 0: + raise InvalidFamilyInput("wage base must be positive") + if year < basis.indexing_year: + nawi[year] = Fraction(str(params.nawi[year])) + if nawi[year] <= 0: + raise InvalidFamilyInput("NAWI must be positive") + except ( + KeyError, + TypeError, + ValueError, + ZeroDivisionError, + OverflowError, + ) as exc: + raise InvalidFamilyInput( + "required AIME parameter is unavailable or invalid" + ) from exc + return nawi, wage_base + + +def _aime_from_prefix( + prefix: Mapping[int, Fraction], + basis: RecomputationBasis, + params: SSAParameters, + checked_parameters: tuple[dict[int, Fraction], dict[int, Fraction]], +) -> int: + """Reuse statutory AIME, guarding the float oracle's integer boundary.""" + nawi, wage_base = checked_parameters + try: + indexed = [] + capped = {} + for year, earnings in prefix.items(): + amount = min(earnings, wage_base[year]) + capped[year] = amount + if year < basis.indexing_year: + amount *= nawi[basis.indexing_year] / nawi[year] + indexed.append(amount) + top = sorted(indexed, reverse=True)[: basis.computation_years] + exact = math.floor(sum(top) / (12 * basis.computation_years)) + # Cap exactly before the float adapter: even a finite Fraction + # above float range must receive the statutory contribution cap. + reference = aime_with_computation_years( + capped, + basis.computation_years, + params, + indexing_year=basis.indexing_year, + ) + except ( + KeyError, + TypeError, + ValueError, + ZeroDivisionError, + OverflowError, + ) as exc: + raise InvalidFamilyInput( + "required AIME parameter is unavailable or invalid" + ) from exc + return reference if reference == exact else exact + + +def earnings_aime( + history: Mapping[int, object], + basis: RecomputationBasis, + earnings_year: int, + params: SSAParameters, +) -> int: + """Complete explicit prefix to AIME, retaining original index/count.""" + _year(earnings_year, "earnings_year") + if not isinstance(basis, RecomputationBasis): + raise InvalidFamilyInput("basis must be a RecomputationBasis") + if earnings_year < basis.first_base_year: + raise InvalidFamilyInput("earnings cutoff precedes the base period") + prefix = _history_prefix(history, basis, earnings_year) + return _aime_from_prefix( + prefix, basis, params, _aime_parameters(prefix, basis, params) + ) + + +def recomputation_decision( + pia_of_record: object, candidate_pia: object, *, has_earnings: bool = True +) -> RecomputationDecision: + """RS 00605.501A: at least $1 AFTER COLAs and only with earnings.""" + old = _dimes(pia_of_record, "PIA of record") + candidate = _dimes(candidate_pia, "candidate PIA") + if type(has_earnings) is not bool: + raise InvalidFamilyInput("has_earnings must be a bool") + increase = candidate - old + applied = has_earnings and increase >= 1 + return RecomputationDecision( + old, candidate, increase, applied, candidate if applied else old + ) + + +def _colas( + history: Mapping[int, object], eligibility_year: int, month: YearMonth +) -> tuple[Fraction, ...]: + if not isinstance(history, Mapping): + raise InvalidFamilyInput("COLA history must be a mapping") + for key in history: + _year(key, "COLA year") + last = month.year - (month.month != 12) + values = [] + for year in range(eligibility_year, last + 1): + if year not in history or history[year] is UNAVAILABLE: + raise InvalidFamilyInput(f"COLA unavailable for December {year}") + values.append(_exact(history[year], f"COLA {year}")) + return tuple(values) + + +def _require_coverage( + basis: RecomputationBasis, + death_month: YearMonth | None, + fra_month: YearMonth | None, +) -> None: + reason = None + if basis.eligibility_year < 1983: + reason = RecomputationUnsupportedReason.ELIGIBILITY_BEFORE_1983 + elif basis.formula is not PIAFormula.AIME: + reason = RecomputationUnsupportedReason.NON_AIME_FORMULA_PIA + elif basis.conditions: + condition = sorted(basis.conditions, key=lambda c: c.value)[0] + reason = RecomputationUnsupportedReason[condition.name] + elif death_month is not None and death_month < fra_month: + reason = RecomputationUnsupportedReason.DEATH_BEFORE_FRA + if reason is not None: + raise FamilyConfigurationUnsupported( + reason, + "Original AIME RIB recomputation only; see R3 coverage table", + ) + + +def _validate_original_computation_count(basis: RecomputationBasis) -> None: + """415(b)(2): ordinary RIB's original count follows eligibility.""" + # 415(b)(2)(A), (B)(iii): elapsed years start after1950 (or age21), + # end before age62; deduct5, with40 elapsed/35 computed at most. + # ELY1956 is1951+5. Prior-DIB exceptions require their own basis. + if ( + basis.eligibility_year >= 1983 + and basis.formula is PIAFormula.AIME + and not basis.conditions + and basis.computation_years + != max(2, min(35, basis.eligibility_year - 1956)) + ): + raise InvalidFamilyInput( + "original ordinary RIB computation count disagrees with eligibility year" + ) + + +def annual_recomputation( + basis: RecomputationBasis, + history: Mapping[int, object], + earnings_year: int, + eligibility_pia_of_record: object, + params: SSAParameters, + december_cola_percent: Mapping[int, object], + *, + death_month: YearMonth | None = None, + fra_month: YearMonth | None = None, +) -> RecomputationResult: + """415(f)(2): one earnings year; January next year or death month. + + ``eligibility_pia_of_record`` is the eligibility-level amount last + accepted, before COLAs. Complete history and parameter validation + precede coverage refusal; no ordinary AIME or PIA is calculated for + refused cases. + Death-year input requires FRA so 415(f)(5) cannot be bypassed. + """ + if not isinstance(basis, RecomputationBasis): + raise InvalidFamilyInput("basis must be a RecomputationBasis") + year = _year(earnings_year, "earnings_year") + old_base = _dimes(eligibility_pia_of_record, "eligibility PIA of record") + if year < basis.entitlement_month.year: + raise InvalidFamilyInput( + "recomputation year must be within entitlement" + ) + if ( + basis.entitlement_end_month is not None + and year > basis.entitlement_end_month.year + ): + raise InvalidFamilyInput("no entitlement in recomputation year") + if year == 9999: + raise InvalidFamilyInput( + "next-year effective month is unrepresentable" + ) + if fra_month is not None: + _month(fra_month, "fra_month") + if fra_month.year < basis.eligibility_year: + raise InvalidFamilyInput("FRA precedes eligibility") + if death_month is not None: + _month(death_month, "death_month") + if death_month < basis.entitlement_month or year > death_month.year: + raise InvalidFamilyInput( + "death or earnings year contradicts entitlement" + ) + if fra_month is None: + raise InvalidFamilyInput( + "death-year processing requires FRA month" + ) + effective = ( + death_month + if death_month is not None and death_month.year == year + else YearMonth(year + 1, 1) + ) + prefix = _history_prefix(history, basis, year) + colas = _colas(december_cola_percent, basis.eligibility_year, effective) + checked_parameters = _aime_parameters(prefix, basis, params) + _validate_original_computation_count(basis) + _require_coverage( + basis, + ( + death_month + if death_month is not None and death_month.year == year + else None + ), + fra_month, + ) + aime = _aime_from_prefix(prefix, basis, params, checked_parameters) + candidate_base = exact_pia(aime, basis.bend_points) + decision = recomputation_decision( + increase_by_colas(old_base, colas), + increase_by_colas(candidate_base, colas), + has_earnings=prefix[year] > 0, + ) + return RecomputationResult( + year, + effective, + aime, + candidate_base, + candidate_base if decision.applied else old_base, + decision, + ) + + +def recompute_sequence( + basis: RecomputationBasis, + history: Mapping[int, object], + earnings_years: Sequence[int], + eligibility_pia_of_record: object, + params: SSAParameters, + december_cola_percent: Mapping[int, object], + *, + death_month: YearMonth | None = None, + fra_month: YearMonth | None = None, +) -> tuple[RecomputationResult, ...]: + """Chronological annual fold, carrying only an accepted base PIA. + + Validate all required earnings, COLAs and parameters before entering + a fold that can refuse coverage, so a later input error is never + counted as a gap. Validation performs no ordinary AIME or PIA + calculation. A refusal in any required earnings year blocks the + entire sequence before its first arithmetic step. + """ + if not isinstance(basis, RecomputationBasis): + raise InvalidFamilyInput("basis must be a RecomputationBasis") + try: + years = tuple(earnings_years) + except TypeError as exc: + raise InvalidFamilyInput("earnings_years must be a sequence") from exc + for year in years: + _year(year, "earnings_year") + if years != tuple(sorted(set(years))): + raise InvalidFamilyInput("recomputation years must increase strictly") + current = _dimes(eligibility_pia_of_record, "eligibility PIA of record") + for year in years: + if year < basis.entitlement_month.year or year == 9999: + raise InvalidFamilyInput("invalid recomputation year in fold") + if ( + basis.entitlement_end_month is not None + and year > basis.entitlement_end_month.year + ): + raise InvalidFamilyInput("no entitlement in recomputation year") + if fra_month is not None: + _month(fra_month, "fra_month") + if fra_month.year < basis.eligibility_year: + raise InvalidFamilyInput("FRA precedes eligibility") + if death_month is not None: + _month(death_month, "death_month") + if death_month < basis.entitlement_month or ( + years and years[-1] > death_month.year + ): + raise InvalidFamilyInput( + "death or earnings year contradicts entitlement" + ) + if fra_month is None: + raise InvalidFamilyInput( + "death-year processing requires FRA month" + ) + if years: + prefix = _history_prefix(history, basis, years[-1]) + _aime_parameters(prefix, basis, params) + final_month = ( + death_month + if death_month is not None and death_month.year == years[-1] + else YearMonth(years[-1] + 1, 1) + ) + _month(final_month, "final effective month") + _colas(december_cola_percent, basis.eligibility_year, final_month) + _validate_original_computation_count(basis) + _require_coverage( + basis, + ( + death_month + if death_month is not None and death_month.year == years[-1] + else None + ), + fra_month, + ) + results = [] + for year in years: + result = annual_recomputation( + basis, + history, + year, + current, + params, + december_cola_percent, + death_month=death_month, + fra_month=fra_month, + ) + results.append(result) + current = result.eligibility_pia_in_force + return tuple(results) diff --git a/tests/README-tiers.md b/tests/README-tiers.md index a92727b2..c590fff4 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` | 7,547 | +| `unit` | 8,786 | | `artifact` | 4,080 | | `integration_psid` | 1,384 | | `reproduction_legacy` | 520 | | `oracle_policyengine` | 308 | -| **Total** | **13,839** | +| **Total** | **15,078** | diff --git a/tests/data/track_b/arf_recompute_sources/README.md b/tests/data/track_b/arf_recompute_sources/README.md new file mode 100644 index 00000000..9cb9b08b --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/README.md @@ -0,0 +1,41 @@ +# ARF and recomputation (Track B milestone R3) captures + +Verification class: statutory conformance. These sources admit ARF credits +and recomputation mechanics for supported cases only. They contain published +rules and invented or SSA worked cases, with no population, behavioral or +forecast claim. + +`manifest.json` preserves the orchestrator's URL, UTC retrieval time, byte +count and SHA-256 for each capture. POMS entries also preserve the transmittal +and page date. The two `_new_*_20261011.json` staging logs were folded into +this manifest without modifying any captured source bytes. The excerpt +entries receive their own byte counts and SHA-256 in addition to the supplied +full response provenance in `served_bodies`. + +`http_body` captures are complete POMS HTML and eCFR section XML; the eCFR +snapshot is dated 2026-10-07. `http_text_excerpt` is the supplied verbatim LII +text for 42 U.S.C. 402(q) and 415(f), with complete response hashes. +`browser_text` is the supplied SSA Publication 05-10069 (2026) excerpt from +`pdftotext -layout`, with the complete PDF's hash. R3 made no network requests. + +| Source | Rule or supported/refused case | +| --- | --- | +| RS 00615.003, .004, .010, .015 | FRA, age attainment, cohort boundaries and kinds of reduction | +| RS 00615.480, .482, .490 | Crediting months, full and partial final work deductions, single-entitlement ARF | +| RS 00615.491, .500 | Reentitled spouses and spouse dual entitlement | +| RS 00615.580, .582, .585 | Widow adjustments at age 62 and FRA, RIB-LIM variants | +| RS 00615.590, .595, .598 | Disability-widow variants and deceased worker adjustment | +| RS 00605.005, .401, .405, .407 | Eligibility basis, recomputation/recalculation distinction, administrative finality and family-effect limit | +| RS 00605.501, .560, .570, .580 | $1 test after COLAs, AERO, processing and recalculation refusals | +| 20 CFR 404.280-.288 | Preserved formula, earnings cutoff, next January/death effective dates, exceptional methods | +| 20 CFR 404.409-.412 | FRA, exact reduction arithmetic and adjusted reduction periods | +| 42 U.S.C. 402(q), 415(f) | Excluded months, rounded reductions, original formula and computation years, minimum increase | +| SSA Publication 05-10069 | $910/$975/$1,300 ARF example; annual recomputation timing | + +`tests/test_track_b_r3_sources.py` recomputes every capture's SHA-256 and +checks substantive rule quotations and every source-derived worked figure +verbatim. Refused configurations also have explicit source quotations. +Sources reused from G and R1 remain in their existing folders and manifests: +G's exact reduction and rounding rules (RS 00615.101, .201, .301), PIA bend +points (RS 00605.900), 415(i) COLAs, and R1's published NAWI and COLA history. +No capture is duplicated. diff --git a/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_280.xml b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_280.xml new file mode 100644 index 00000000..094dd20d --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_280.xml @@ -0,0 +1,14 @@ + + +§ 404.280 Recomputations. +

At times after you or your survivors become entitled to benefits, we will recompute your primary insurance amount. Usually we will recompute only if doing so will increase your primary insurance amount. However, we will also recompute your primary insurance amount if you first became eligible for old-age or disability insurance benefits after 1985, and later become entitled to a pension based on your noncovered employment, as explained in § 404.213. There is no limit on the number of times your primary insurance amount may be recomputed, and we do most recomputations automatically. In the following sections, we explain:

+

(a) Why a recomputation is made (§ 404.281),

+

(b) When a recomputation takes effect (§ 404.282),

+

(c) Methods of recomputing (§§ 404.283 and 404.284),

+

(d) Automatic recomputations (§ 404.285),

+

(e) Requesting a recomputation (§ 404.286),

+

(f) Waiving a recomputation (§ 404.287), and

+

(g) Recomputing when you are entitled to a pension based on noncovered employment (§ 404.288).

+[52 FR 47918, Dec. 17, 1987] + +
diff --git a/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_281.xml b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_281.xml new file mode 100644 index 00000000..ad551d79 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_281.xml @@ -0,0 +1,12 @@ + + +§ 404.281 Why your primary insurance amount may be recomputed. +

(a) Earnings not included in earlier computation or recomputation. The most common reason for recomputing your primary insurance amount is to include earnings of yours that were not used in the first computation or in an earlier recomputation, as described in paragraphs (c) through (e) of this section. These earnings will result in a revised average monthly wage or revised average indexed monthly earnings.

+

(b) New computation method enacted. If a new method of computing or recomputing primary insurance amounts is enacted into law and you are eligible to have your primary insurance amount recomputed under the new method, we will recompute it under the new method if doing so would increase your primary insurance amount.

+

(c) Earnings in the year you reach age 62 or become disabled. In the initial computation of your primary insurance amount, we do not use your earnings in the year you become entitled to old-age insurance benefits or become disabled. However, we can use those earnings (called lag earnings) in a recomputation of your primary insurance amount. We recompute and begin paying you the higher benefits in the year after the year you become entitled to old-age benefits or become disabled.

+

(d) Earnings not reported to us in time to use them in the computation of your primary insurance amount. Because of the way reports of earnings are required to be submitted to us for years after 1977, the earnings you have in the year before you become entitled to old-age insurance benefits, or become disabled or in the year you die might not be reported to us in time to use them in computing your primary insurance amount. We recompute your primary insurance amount based on the new earnings information and begin paying you (or your survivors) the higher benefits based on the additional earnings, beginning with the month you became entitled or died.

+

(e) Earnings after entitlement that are used in a recomputation. Earnings that you have after you become entitled to benefits will be used in a recomputation of your primary insurance amount.

+

(f) Entitlement to a monthly pension. We will recompute your primary insurance amount if in a month after you became entitled to old-age or disability insurance benefits, you become entitled to a pension based on noncovered employment, as explained in § 404.213. Further, we will recompute your primary insurance amount after your death to disregard a monthly pension based on noncovered employment which affected your primary insurance amount.

+[47 FR 30734, July 15, 1982, as amended at 52 FR 47918, Dec. 17, 1987] + +
diff --git a/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_282.xml b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_282.xml new file mode 100644 index 00000000..84886126 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_282.xml @@ -0,0 +1,7 @@ + + +§ 404.282 Effective date of recomputations. +

Most recomputations are effective beginning with January of the calendar year after the year in which the additional earnings used in the recomputation were paid. However, a recomputation to include earnings in the year of death (whether or not paid before death) is effective for the month of death. Additionally if you first became eligible for old-age or disability insurance benefits after 1985 and you later also become entitled to a monthly pension based on noncovered employment, we will recompute your primary insurance amount under the rules in § 404.213; this recomputed Social Security benefit amount is effective for the first month you are entitled to the pension. Finally, if your primary insurance amount was affected by your entitlement to a pension, we will recompute the amount to disregard the pension, effective with the month of your death.

+[47 FR 30734, July 15, 1982, as amended at 52 FR 47918, Dec. 17, 1987] + +
diff --git a/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_283.xml b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_283.xml new file mode 100644 index 00000000..376e016d --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_283.xml @@ -0,0 +1,5 @@ + + +§ 404.283 Recomputation under method other than that used to find your primary insurance amount. +

In some cases, we may recompute your primary insurance amount under a computation method different from the method used in the computation (or earlier recomputation) of your primary insurance amount, if you are eligible for a computation or recomputation under the different method.

+
diff --git a/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_284.xml b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_284.xml new file mode 100644 index 00000000..fd781879 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_284.xml @@ -0,0 +1,19 @@ + + +§ 404.284 Recomputations for people who reach age 62, or become disabled, or die before age 62 after 1978. +

(a) General. Years of your earnings after 1978 not used in the computation of your primary insurance amount (or in earlier recomputations) under the average-indexed-monthly-earnings method may be substituted for earlier years of your indexed earnings in a recomputation, but only under the average-indexed-monthly-earnings method. See § 404.288 for the rules on recomputing when you are entitled to a monthly pension based on noncovered employment.

+

(b) Substituting actual dollar amounts in earnings for earlier years of indexed earnings. When we recompute your primary insurance amount under the average-indexed-monthly earnings method, we use actual dollar amounts, i.e., no indexing, for earnings not included in the initial computation or earlier recomputation. These later earnings are substituted for earlier years of indexed or actual earnings that are lower.

+

(c) Benefit formula used in recomputation. The formula that was used in the first computation of your primary insurance amount is also used in recomputations of your primary insurance amount.

+

(d) Your recomputed primary insurance amount. We recompute your primary insurance amount by applying the benefit formula to your average indexed monthly earnings as revised to include additional earnings. See § 404.281. We then increase the recomputed PIA by the amounts of any automatic cost-of-living or ad hoc increases in primary insurance amounts that have become effective since you reached age 62, or became disabled or died before age 62.

+

(e) Minimum increase in primary insurance amounts. Your primary insurance amount may not be recomputed unless doing so would increase it by at least $1.

+ +Example 1.Ms. A, whose primary insurance amount we computed to be $432.40 in June 1979 in §§ 404.210 through 404.212 (based on average indexed monthly earnings of $903), had earnings of $11,000 in 1979 which were not used in the initial computation of her primary insurance amount. We may recompute her primary insurance amount effective for January 1980. In this recomputation, her 1979 earnings may be substituted in their actual dollar amount for the lowest year of her indexed earnings that was used in the initial computation. In Ms. A's case, we substitute the $11,000 for her 1966 indexed earnings of $8,911.36. Her total indexed earnings are now $251,470.05 and her new average indexed monthly earnings are $911. We apply to Ms. A's new average indexed monthly earnings the same benefit formula we used in the initial computation. Doing so produces an amount of $396.00. An automatic cost-of-living increase of 9.9 percent was effective in June 1979. We increase the $396.00 amount by 9.9 percent to find Ms. A's recomputed primary insurance amount of $435.30. Later we increased the primary insurance amount to $497.60 to reflect the 14.3 percent cost-of-living increase beginning June 1980 and to $553.40 to reflect the 11.2 percent cost-of-living increase beginning June 1981. + +Example 2.Mr. B, whose primary insurance amount we computed to be $429.20 (based on average monthly wages of $502) in June 1978 in §§ 404.220 through 404.222, had earnings of $12,000 in 1978 which were not used in the initial computation of his primary insurance amount. We may recompute his primary insurance amount effective for January 1979. In this recomputation, his 1978 earnings are substituted for the lowest year of earnings used in the initial computation ($2,700 in 1952). Mr. B's total earnings are now $142,000 and his new average monthly wage is $537. +

We next find Mr. B's new average monthly wage in column III of the December 1978 benefit table in appendix III. Reading across, we find his recomputed primary insurance amount on the same line in column IV, which is $407.70. We then apply the 9.9 percent, the 14.3 percent and the 11.2 percent automatic cost-of-living increases for June 1979, June 1980, and June 1981, respectively, to compute Mr. B's primary insurance amount of $569.60.

+

(f) Guaranteed alternatives. We may recompute your primary insurance amount by any of the following methods for which you qualify, if doing so would result in a higher amount than the one computed under the average-indexed-monthly-earnings method. Earnings in or after the year you reach age 62 cannot be used.

+

(1) If you reached age 62 after 1978 and before 1984, we may recompute to include earnings for years before the year you reached age 62 by using the guaranteed alternative (§ 404.231). We will increase the result by any cost-of-living or ad hoc increases in the primary insurance amounts that have become effective in and after the year you reached age 62.

+

(2) We will also recompute under the old-start guarantee (§ 404.242) and the prior-disability guarantee (§ 404.252) if you meet the requirements of either or both these methods.

+[47 FR 30734, July 15, 1982, as amended at 52 FR 47918, Dec. 17, 1987] + +
diff --git a/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_285.xml b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_285.xml new file mode 100644 index 00000000..07a22f89 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_285.xml @@ -0,0 +1,5 @@ + + +§ 404.285 Recomputations performed automatically. +

Each year, we examine the earnings record of every retired, disabled, and deceased worker to see if the worker's primary insurance amount may be recomputed under any of the methods we have described. When a recomputation is called for, we perform it automatically and begin paying the higher benefits based on your recomputed primary insurance amount for the earliest possible month that the recomputation can be effective. You do not have to request this service, although you may request a recomputation at an earlier date than one would otherwise be performed (see § 404.286). Doing so, however, does not allow your increased primary insurance amount to be effective any sooner than it would be under an automatic recomputation. You may also waive a recomputation if one would disadvantage you or your family (see § 404.287).

+
diff --git a/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_286.xml b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_286.xml new file mode 100644 index 00000000..0ac787bf --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_286.xml @@ -0,0 +1,5 @@ + + +§ 404.286 How to request an immediate recomputation. +

You may request that your primary insurance amount be recomputed sooner than it would be recomputed automatically. To do so, you must make the request in writing to us and provide acceptable evidence of your earnings not included in the first computation or earlier recomputation of your primary insurance amount. If doing so will increase your primary insurance amount, we will recompute it. However, we cannot begin paying higher benefits on the recomputed primary insurance amount any sooner than we could under an automatic recomputation, i.e., for January of the year following the year in which the earnings were paid or derived.

+
diff --git a/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_287.xml b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_287.xml new file mode 100644 index 00000000..4267b5d7 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_287.xml @@ -0,0 +1,5 @@ + + +§ 404.287 Waiver of recomputation. +

If you or your family would be disadvantaged in some way by a recomputation of your primary insurance amount, or you and every member of your family do not want your primary insurance amount to be recomputed for any other reason, you may waive (that is, give up your right to) a recomputation, but you must do so in writing. That you waive one recomputation, however, does not mean that you also waive future recomputations for which you might be eligible.

+
diff --git a/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_288.xml b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_288.xml new file mode 100644 index 00000000..eacfbb75 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_288.xml @@ -0,0 +1,9 @@ + + +§ 404.288 Recomputing when you are entitled to a monthly pension based on noncovered employment. +

(a) After entitlement to old-age or disability insurance benefits. If you first become eligible for old-age or disability insurance benefits after 1985 and you later become entitled to a monthly pension based on noncovered employment, we may recompute your primary insurance amount under the rules in § 404.213. When recomputing, we will use the amount of the pension to which you are entitled or deemed entitled in the first month that you are concurrently eligible for both the pension and old-age or disability insurance benefits. We will disregard the rule in § 404.284(e) that the recomputation must increase your primary insurance amount by at least $1.

+

(b) Already entitled to benefits and to a pension based on noncovered employment. If we have already computed or recomputed your primary insurance amount to take into account your monthly pension, we may later recompute for one of the reasons explained in § 404.281. We will recompute your primary insurance amount under the rules in §§ 404.213 and 404.284. Any increase resulting from the recomputation under the rules of § 404.284 will be added to the most recent primary insurance amount which we had computed to take into account your monthly pension.

+

(c) After your death. If one or more survivors are entitled to benefits after your death, we will recompute the primary insurance amount as though it had never been affected by your entitlement to a monthly pension based in whole or in part on noncovered employment.

+[52 FR 47918, Dec. 17, 1987] + +
diff --git a/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_409.xml b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_409.xml new file mode 100644 index 00000000..ecc5a7bd --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_409.xml @@ -0,0 +1,142 @@ + + +§ 404.409 What is full retirement age? +

Full retirement age is the age at which you may receive unreduced old-age, wife's, husband's, widow's, or widower's benefits. Full retirement age has been 65 but is being gradually raised to age 67 beginning with people born after January 1, 1938. See § 404.102 regarding determination of age.

+

(a) What is my full retirement age for old-age benefits or wife's or husband's benefits? You may receive unreduced old-age, wife's, or husband's benefits beginning with the month you attain the age shown.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
If your birth date is:Full retirement age is:
Before 1/2/193865 years.
1/2/1938—1/1/193965 years and 2 months.
1/2/1939—1/1/194065 years and 4 months.
1/2/1940—1/1/194165 years and 6 months.
1/2/1941—1/1/194265 years and 8 months.
1/2/1942—1/1/194365 years and 10 months.
1/2/1943—1/1/195566 years.
1/2/1955—1/1/195666 years and 2 months.
1/2/1956—1/1/195766 years and 4 months.
1/2/1957—1/1/195866 years and 6 months.
1/2/1958—1/1/195966 years and 8 months.
1/2/1959—1/1/196066 years and 10 months.
1/2/1960 and later67 years.
+
+

(b) What is my full retirement age for widow's or widower's benefits? You may receive unreduced widow's or widower's benefits beginning with the month you attain the age shown.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
If your birth date is:Full retirement age is:
Before 1/2/191262 years.
1/2/1912—1/1/194065 years.
1/2/1940—1/1/194165 years and 2 months.
1/2/1941—1/1/194265 years and 4 months.
1/2/1942—1/1/194365 years and 6 months.
1/2/1943—1/1/194465 years and 8 months.
1/2/1944—1/1/194565 years and 10 months.
1/2/1945—1/1/195766 years.
1/2/1957—1/1/195866 years and 2 months.
1/2/1958—1/1/195966 years and 4 months.
1/2/1959—1/1/196066 years and 6 months.
1/2/1960—1/1/196166 years and 8 months.
1/2/1961—1/1/196266 years and 10 months.
1/2/1962 and later67 years.
+
+

(c) Can I still retire before full retirement age? You may still elect early retirement. You may receive old-age, wife's or husband's benefits at age 62. You may receive widow's or widower's benefits at age 60. Those benefits will be reduced as explained in § 404.410.

+[68 FR 4707, Jan. 30, 2003] + +
diff --git a/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_410.xml b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_410.xml new file mode 100644 index 00000000..8924d9fa --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_410.xml @@ -0,0 +1,40 @@ + + +§ 404.410 How does SSA reduce my benefits when my entitlement begins before full retirement age? +

Generally your old-age, wife's, husband's, widow's, or widower's benefits are reduced if entitlement begins before the month you attain full retirement age (as defined in § 404.409). However, your benefits as a wife or husband are not reduced for any month in which you have in your care a child of the worker on whose earnings record you are entitled. The child must be entitled to child's benefits. Your benefits as a widow or widower are not reduced below the benefit amount you would receive as a mother or father for any month in which you have in your care a child of the worker on whose record you are entitled. The child must be entitled to child's benefits. Subject to §§ 404.411 through 404.413, reductions in benefits are made in the amounts described.

+

(a) How does SSA reduce my old-age benefits? The reduction in your primary insurance amount is based on the number of months of entitlement prior to the month you attain full retirement age. The reduction is +5/9 of 1 percent for each of the first 36 months and +5/12 of 1 percent for each month in excess of 36.

+ +Example:Alex's full retirement age for unreduced benefits is 65 years and 8 months. She elects to begin receiving benefits at age 62. Her primary insurance amount of $980.50 must be reduced because of her entitlement to benefits 44 months prior to full retirement age. The reduction is 36 months at +5/9 of 1 percent and 8 months at +5/12 of 1 percent. + +980.50 × 36 × +5/9 × .01 = $196.10 + +980.50 × 8 × +5/12 × .01 = $ 32.68 + +The two added together equal a total reduction of $228.78. This amount is rounded to $228.80 (the next higher multiple of 10 cents) and deducted from the primary insurance amount. The resulting $751.70 is the monthly benefit payable. +

(b) How does SSA reduce my wife's or husband's benefits? Your wife's or husband's benefits before any reduction (see §§ 404.304 and 404.333) are reduced first (if necessary) for the family maximum under § 404.403. They are then reduced based on the number of months of entitlement prior to the month you attain full retirement age. This does not include any month in which you have a child of the worker on whose earnings record you are entitled in your care. The child must be entitled to child benefits. The reduction is +25/36 of 1 percent for each of the first 36 months and +5/12 of 1 percent for each month in excess of 36.

+ +Example:Sam is entitled to old-age benefits. His spouse Ashley elects to begin receiving wife's benefits at age 63. Her full retirement age for unreduced benefits is 65 and 4 months. Her benefit will be reduced for 28 months of entitlement prior to full retirement age. If her unreduced benefit is $412.40 the reduction will be $412.40 × 28 × +25/36 × .01. The resulting $80.18 is rounded to $80.20 (the next higher multiple of 10 cents) and subtracted from $412.40 to determine the monthly benefit amount of $332.20. +

(c) How does SSA reduce my widow's or widower's benefits? Your entitlement to widow's or widower's benefits may begin at age 60 based on age or at age 50 based on disability. Refer to § 404.335 for more information on the requirements for entitlement. Both types are reduced if entitlement begins prior to attainment of full retirement age (as defined in § 404.409).

+

(1) Widow's or widower's benefits based on age. Your widow's or widower's unreduced benefit amount (the worker's primary insurance amount after any reduction for the family maximum under § 404.403), is reduced or further reduced based on the number of months of entitlement prior to the month you attain full retirement age. This does not include any month in which you have in your care a child of the worker on whose earnings record you are entitled. The child must be entitled to child's benefits. The number of months of entitlement prior to full retirement age is multiplied by .285 and then divided by the number of months in the period beginning with the month of attainment of age 60 and ending with the month immediately before the month of attainment of full retirement age.

+ +Example:Ms. Bogle is entitled to an unreduced widow benefit of $785.70 beginning at age 64. Her full retirement age for unreduced old-age benefits is 65 years and 4 months. She will receive benefits for 16 months prior to attainment of full retirement age. The number of months in the period from age 60 through full retirement age of 65 and 4 months is 64. The reduction in her benefit is $785.70 × 16 × .285 divided by 64 or $55.98. $55.98 is rounded to the next higher multiple of 10 cents ($56.00) and subtracted from $785.70. The result is a monthly benefit of $729.70. +

(2) Widow's or widower's benefits based on disability. (i) For months after December 1983, your widow's or widower's benefits are not reduced for months of entitlement prior to age 60. You are deemed to be age 60 in your month of entitlement to disabled widow's or widower's benefits and your benefits are reduced only under paragraph (c)(1) of this section.

+

(ii) For months from January 1973 through December 1983, benefits as a disabled widow or widower were reduced under paragraph (c)(1) of this section. The benefits were then subject to an additional reduction of +43/240 of one percent for each month of entitlement prior to age 60 based on disability.

+

(3) Widow's or widower's benefits prior to 1973. For months prior to January 1973 benefits as a widow or widower were reduced only for months of entitlement prior to age 62. The reduction was +5/9 of one percent for each month of entitlement from the month of attainment of age 60 through the month prior to the month of attainment of age 62. There was an additional reduction of +43/198 of one percent for each month of entitlement prior to age 60 based on disability.

+

(d) If my benefits are reduced under this section does SSA ever change the reduction? The reduction computed under paragraphs (a), (b) or (c) of this section may later be adjusted to eliminate reduction for certain months of entitlement prior to full retirement age as provided in § 404.412. For special provisions on reducing benefits for months prior to full retirement age involving entitlement to two or more benefits, see § 404.411.

+

(e) Are my widow's or widower's benefits affected if the deceased worker was entitled to old-age benefits? If the deceased individual was entitled to old-age benefits, see § 404.338 for special rules that may affect your reduced widow's or widower's benefits.

+[68 FR 4708, Jan. 30, 2003] + +
diff --git a/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_411.xml b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_411.xml new file mode 100644 index 00000000..41b146c7 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_411.xml @@ -0,0 +1,25 @@ + + +§ 404.411 How are benefits reduced for age when a person is entitled to two or more benefits? +

(a) What is the general rule? Except as specifically provided in this section, benefits of an individual entitled to more than one benefit will be reduced for months of entitlement before full retirement age (as defined in § 404.409) according to the provisions of § 404.410. Such age reductions are made before any reduction under the provisions of § 404.407.

+

(b) How is my disability benefit reduced after entitlement to an old-age benefit or widow's or widower's benefit? A person's disability benefit is reduced following entitlement to an old-age or widow's or widower's benefit (or following the month in which all conditions for entitlement to the widow's or widower's benefit are met except that the individual is entitled to an old-age benefit which equals or exceeds the primary insurance amount on which the widow's or widower's benefit is based) in accordance with the following provisions:

+

(1) Individuals born January 2, 1928, or later whose disability began January 1, 1990, or later. When an individual is entitled to a disability benefit for a month after the month in which she or he becomes entitled to an old-age benefit which is reduced for age under § 404.410, the disability benefit is reduced by the amount by which the old-age benefit would be reduced under § 404.410 if she or he attained full retirement age in the first month of the most recent period of entitlement to the disability benefit.

+

(2) Individuals born January 2, 1928, or later whose disability began before January 1, 1990, and, all individuals born before January 2, 1928, regardless of when their disability began—(i) First entitled to disability in or after the month of attainment of age 62. When an individual is first entitled to a disability benefit in or after the month in which she or he attains age 62 and for which she or he is first entitled to a widow's or widower's benefit (or would be so entitled except for entitlement to an equal or higher old-age benefit) before full retirement age, the disability benefit is reduced by the larger of:

+

(A) The amount the disability benefit would have been reduced under paragraph (b)(1) of this section; or

+

(B) The amount equal to the sum of the amount the widow's or widower's benefit would have been reduced under the provisions of § 404.410 if full retirement age for unreduced benefits were age 62 plus the amount by which the disability benefit would have been reduced under paragraph (b)(1) of this section if the benefit were equal to the excess of such benefit over the amount of the widow's or widower's benefit (without consideration of this paragraph).

+

(ii) First entitled to disability before age 62. When a person is first entitled to a disability benefit for a month before the month in which she or he attains age 62 and she or he is also entitled to a widow's or widower's benefit (or would be so entitled except for entitlement to an equal or higher old-age benefit), the disability benefit is reduced as if the widow or widower attained full retirement age in the first month of her or his most recent period of entitlement to the disability benefits.

+

(c) How is my old-age benefit reduced after entitlement to a widow's or widower's benefit?—(1) Individual born after January 1, 1928. The old-age benefit is reduced in accordance with § 404.410(a). There is no further reduction.

+

(2) Individual born before January 2, 1928. The old-age benefit is reduced if, in the first month of entitlement, she or he is also entitled to a widow's or widower's benefit to which she or he was first entitled for a month before attainment of full retirement age or if, before attainment of full retirement age, she or he met all conditions for entitlement to widow's or widower's benefits in or before the first month for which she or he was entitled to old-age benefits except that the old-age benefit equals or exceeds the primary insurance amount on which the widow's or widower's benefit would be based. Under these circumstances, the old-age benefit is reduced by the larger of the following:

+

(i) The amount by which the old-age benefit would be reduced under the regular age reduction provisions of § 404.410; or

+

(ii) An amount equal to the sum of:

+

(A) The amount by which the widow's or widower's benefit would be reduced under § 404.410 for months prior to age 62; and

+

(B) The amount by which the old-age benefit would be reduced under § 404.410 if it were equal to the excess of the individual's primary insurance amount over the widow's or widower's benefit before any reduction for age (but after any reduction for the family maximum under § 404.403).

+

(d) How is my wife's or husband's benefit reduced when I am entitled to a reduced old-age benefit in the same month? When a person is first entitled to a wife's or husband's benefit in or after the month of attainment of age 62, that benefit is reduced if, in the first month of entitlement, she or he is also entitled to an old-age benefit (but is not entitled to a disability benefit) to which she or he was first entitled before attainment of full retirement age. Under these circumstances, the wife's or husband's benefit is reduced by the sum of:

+

(1) The amount by which the old-age benefit would be reduced under the provisions of § 404.410; and

+

(2) The amount by which the spouse benefit would be reduced under the provisions of § 404.410 if it were equal to the excess of such benefit (before any reduction for age but after reduction for the family maximum under § 404.403) over the individual's own primary insurance amount.

+

(e) How is my wife's or husband's or widow's or widower's benefit reduced when I am entitled to a reduced disability benefit in the same month? When a person is first entitled to a spouse or widow's or widower's benefit in or after the month of attainment of age 62 (or in the case of widow's or widower's benefits, age 50) that benefit is reduced if, in the first month of entitlement to that benefit, he or she is also entitled to a reduced disability benefit. Under these circumstances, the wife's or husband's or widow's or widower's benefit is reduced by the sum of:

+

(1) The amount (if any) by which the disability benefit is reduced under paragraph (b)(1) of this section, and

+

(2) The amount by which the wife's or husband's or widow's or widower's benefit would be reduced under § 404.410 if it were equal to the excess of such benefit (before any reduction for age but after reduction for the family maximum under § 404.403) over the disability benefit (before any reduction under paragraph (b) of this section).

+[68 FR 4709, Jan. 30, 2003] + +
diff --git a/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_412.xml b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_412.xml new file mode 100644 index 00000000..6b2a341a --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/ecfr_20cfr_404_412.xml @@ -0,0 +1,14 @@ + + +§ 404.412 After my benefits are reduced for age when and how will adjustments to that reduction be made? +

(a) When may adjustment be necessary? The following months are not counted for purposes of reducing benefits in accordance with § 404.410;

+

(1) Months subject to deduction under § 404.415 or § 404.417;

+

(2) In the case of a wife's or husband's benefit, any month in which she or he had a child of the insured individual in her or his care and for which the child was entitled to child's benefits;

+

(3) In the case of a wife's or husband's benefit, any month for which entitlement to such benefits is precluded because the insured person's disability ceased (and, as a result, the insured individual's entitlement to disability benefits ended);

+

(4) In the case of a widow's or widower's benefit, any month in which she or he had in her or his care a child of the deceased insured individual and for which the child was entitled to child's benefits;

+

(5) In the case of a widow's or widower's benefit, any month before attainment of full retirement age for which she or he was not entitled to such benefits;

+

(6) In the case of an old-age benefit, any month for which the individual was entitled to disability benefits.

+

(b) When is the adjustment made? We make automatic adjustments in benefits to exclude the months of entitlement described in paragraphs (a)(1) through (6) of this section from consideration when determining the amount by which such benefits are reduced. Each year we examine beneficiary records to identify when an individual has attained full retirement age and one or more months described in paragraphs (a)(1) through (6) of this section occurred prior to such age during the period of entitlement to benefits reduced for age. Increases in benefit amounts based upon this adjustment are effective with the month of attainment of full retirement age. In the case of widow's or widower's benefits, this adjustment is made in the month of attainment of age 62 as well as the month of attainment of full retirement age.

+[68 FR 4710, Jan. 30, 2003, as amended at 68 FR 40122, July 7, 2003] + +
diff --git a/tests/data/track_b/arf_recompute_sources/lii_usc_402q_415f_excerpts.txt b/tests/data/track_b/arf_recompute_sources/lii_usc_402q_415f_excerpts.txt new file mode 100644 index 00000000..f944dcb6 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/lii_usc_402q_415f_excerpts.txt @@ -0,0 +1,11 @@ +# Verbatim excerpts of the Legal Information Institute's 42 U.S.C. 402 and 415 pages +# after stripping tags, decoding entities, collapsing whitespace and straightening +# curly apostrophes. Full served bodies are pinned in the manifest (served_bodies). + +## 42 U.S.C. 402(q) (https://www.law.cornell.edu/uscode/text/42/402) + +(q) Reduction of benefit amounts for certain beneficiaries (1) Subject to paragraph (9), if the first month for which an individual is entitled to an old-age, wife 's, husband 's, widow' s, or widower' s insurance benefit is a month before the month in which such individual attains retirement age , the amount of such benefit for such month and for any subsequent month shall, subject to the succeeding paragraphs of this subsection, be reduced by— (A) 5 ⁄ 9 of 1 percent of such amount if such benefit is an old-age insurance benefit, 25 ⁄ 36 of 1 percent of such amount if such benefit is a wife' s or husband' s insurance benefit, or 19 ⁄ 40 of 1 percent of such amount if such benefit is a widow' s or widower' s insurance benefit, multiplied by (B) (i) the number of months in the reduction period for such benefit (determined under paragraph (6)), if such benefit is for a month before the month in which such individual attains retirement age , or (ii) if less, the number of such months in the adjusted reduction period for such benefit (determined under paragraph (7)), if such benefit is (I) for the month in which such individual attains age 62, or (II) for the month in which such individual attains retirement age . (2) If an individual is entitled to a disability insurance benefit for a month after a month for which such individual was entitled to an old-age insurance benefit, such disability insurance benefit for each month shall be reduced by the amount such old-age insurance benefit would be reduced under paragraphs (1) and (4) for such month had such individual attained retirement age (as defined in section 416(l) of this title ) in the first month for which he most recently became entitled to a disability insurance benefit. (3) (A) If the first month for which an individual both is entitled to a wife 's, husband 's, widow' s, or widower' s insurance benefit and has attained age 62 (in the case of a wife' s or husband 's insurance benefit) or age 50 (in the case of a widow' s or widower' s insurance benefit) is a month for which such individual is also entitled to— (i) an old-age insurance benefit (to which such individual was first entitled for a month before he attains retirement age (as defined in section 416(l) of this title )), or (ii) a disability insurance benefit, then in lieu of any reduction under paragraph (1) (but subject to the succeeding paragraphs of this subsection) such wife 's, husband 's, widow' s, or widower' s insurance benefit for each month shall be reduced as provided in subparagraph (B), (C), or (D). (B) For any month for which such individual is entitled to an old-age insurance benefit and is not entitled to a disability insurance benefit, such individual's wife' s or husband' s insurance benefit shall be reduced by the sum of— (i) the amount by which such old-age insurance benefit is reduced under paragraph (1) for such month, and (ii) the amount by which such wife 's or husband 's insurance benefit would be reduced under paragraph (1) for such month if it were equal to the excess of such wife' s or husband 's insurance benefit (before reduction under this subsection) over such old-age insurance benefit (before reduction under this subsection). (C) For any month for which such individual is entitled to a disability insurance benefit, such individual's wife' s, husband' s, widow' s, or widower' s insurance benefit shall be reduced by the sum of— (i) the amount by which such disability insurance benefit is reduced under paragraph (2) for such month (if such paragraph applied to such benefit), and (ii) the amount by which such wife 's, husband 's, widow' s, or widower' s insurance benefit would be reduced under paragraph (1) for such month if it were equal to the excess of such wife' s, husband 's, widow' s, or widower' s insurance benefit (before reduction under this subsection) over such disability insurance benefit (before reduction under this subsection). (D) For any month for which such individual is entitled neither to an old-age insurance benefit nor to a disability insurance benefit, such individual's wife' s, husband' s, widow' s, or widower' s insurance benefit shall be reduced by the amount by which it would be reduced under paragraph (1). (E) Notwithstanding subparagraph (A) of this paragraph, if the first month for which an individual is entitled to a widow 's or widower 's insurance benefit is a month for which such individual is also entitled to an old-age insurance benefit to which such individual was first entitled for that month or for a month before she or he became entitled to a widow' s or widower 's benefit, the reduction in such widow' s or widower 's insurance benefit shall be determined under paragraph (1). (4) If— (A) an individual is or was entitled to a benefit subject to reduction under paragraph (1) or (3) of this subsection, and (B) such benefit is increased by reason of an increase in the primary insurance amount of the individual on whose wages and self-employment income such benefit is based, then the amount of the reduction of such benefit (after the application of any adjustment under paragraph (7)) for each month beginning with the month of such increase in the primary insurance amount shall be computed under paragraph (1) or (3), whichever applies, as though the increased primary insurance amount had been in effect for and after the month for which the individual first became entitled to such monthly benefit reduced under such paragraph (1) or (3). (5) (A) No wife 's or husband 's insurance benefit shall be reduced under this subsection— (i) for any month before the first month for which there is in effect a certificate filed by him or her with the Commissioner of Social Security, in accordance with regulations prescribed by the Commissioner of Social Security, in which he or she elects to receive wife 's or husband 's insurance benefits reduced as provided in this subsection, or (ii) for any month in which he or she has in his or her care (individually or jointly with the person on whose wages and self-employment income the wife' s or husband' s insurance benefit is based) a child of such person entitled to child' s insurance benefits. (B) Any certificate described in subparagraph (A)(i) shall be effective for purposes of this subsection (and for purposes of preventing deductions under section 403(c)(2) of this title )— (i) for the month in which it is filed and for any month thereafter, and (ii) for months, in the period designated by the individual filing such certificate, of one or more consecutive months (not exceeding 12) immediately preceding the month in which such certificate is filed; except that such certificate shall not be effective for any month before the month in which he or she attains age 62, nor shall it be effective for any month to which subparagraph (A)(ii) applies. (C) If an individual does not have in his or her care a child described in subparagraph (A)(ii) in the first month for which he or she is entitled to a wife' s or husband 's insurance benefit, and if such first month is a month before the month in which he or she attains retirement age (as defined in section 416(l) of this title ), he or she shall be deemed to have filed in such first month the certificate described in subparagraph (A)(i). (D) No widow 's or widower 's insurance benefit for a month in which he or she has in his or her care a child of his or her deceased spouse (or deceased former spouse) entitled to child' s insurance benefits shall be reduced under this subsection below the amount to which he or she would have been entitled had he or she been entitled for such month to mother's or father's insurance benefits on the basis of his or her deceased spouse' s (or deceased former spouse' s) wages and self-employment income . (6) For purposes of this subsection, the “reduction period” for an individual's old-age, wife 's, husband 's, widow' s, or widower' s insurance benefit is the period— (A) beginning— (i) in the case of an old-age insurance benefit, with the first day of the first month for which such individual is entitled to such benefit, (ii) in the case of a wife 's or husband 's insurance benefit, with the first day of the first month for which a certificate described in paragraph (5)(A)(i) is effective, or (iii) in the case of a widow 's or widower 's insurance benefit, with the first day of the first month for which such individual is entitled to such benefit or the first day of the month in which such individual attains age 60, whichever is the later, and (B) ending with the last day of the month before the month in which such individual attains retirement age . (7) For purposes of this subsection, the “adjusted reduction period” for an individual's old-age, wife 's, husband 's, widow' s, or widower' s insurance benefit is the reduction period prescribed in paragraph (6) for such benefit, excluding— (A) any month in which such benefit was subject to deductions under section 403(b) , 403(c)(1) , 403(d)(1) , or 422(b) of this title, (B) in the case of wife 's or husband 's insurance benefits, any month in which such individual had in his or her care (individually or jointly with the person on whose wages and self-employment income such benefit is based) a child of such person entitled to child' s insurance benefits, (C) in the case of wife 's or husband 's insurance benefits, any month for which such individual was not entitled to such benefits because of the occurrence of an event that terminated her or his entitlement to such benefits, (D) in the case of widow 's or widower 's insurance benefits, any month in which the reduction in the amount of such benefit was determined under paragraph (5)(D), (E) in the case of widow 's or widower 's insurance benefits, any month before the month in which she or he attained age 62, and also for any later month before the month in which she or he attained retirement age , for which she or he was not entitled to such benefit because of the occurrence of an event that terminated her or his entitlement to such benefits, and (F) in the case of old-age insurance benefits, any month for which such individual was entitled to a disability insurance benefit. (8) This subsection shall be applied after reduction under section 403(a) of this title and before application of section 415(g) of this title . If the amount of any reduction computed under paragraph (1), (2), or (3) is not a multiple of $0.10, it shall be increased to the next higher multiple of $0.10. (9) The amount of the reduction for early retirement specified in paragraph (1)— (A) for old-age insurance benefits, wife 's insurance benefits, and husband 's insurance benefits, shall be the amount specified in such paragraph for the first 36 months of the reduction period (as defined in paragraph (6)) or adjusted reduction period (as defined in paragraph (7)), and five-twelfths of 1 percent for any additional months included in such periods; and (B) for widow 's insurance benefits and widower 's insurance benefits, shall be periodically revised by the Commissioner of Social Security such that— (i) the amount of the reduction at early retirement age as defined in section 416(l) of this title shall be 28.5 percent of the full benefit; and (ii) the amount of the reduction for each month in the reduction period (specified in paragraph (6)) or the adjusted reduction period (specified in paragraph (7)) shall be established by linear interpolation between 28.5 percent at the month of attainment of early retirement age and 0 percent at the month of attainment of retirement age. (10) For purposes of applying paragraph (4), with respect to monthly benefits payable for any month after December 1977 to an individual who was entitled to a monthly benefit as reduced under paragraph (1) or (3) prior to January 1978, the amount of reduction in such benefit for the first month for which such benefit is increased by reason of an increase in the primary insurance amount of the individual on whose wages and self-employment income such benefit is based and for all subsequent months (and similarly for all subsequent increases) shall be increased by a percentage equal to the percentage increase in such primary insurance amount (such increase being made in accordance with the provisions of paragraph (8)). In the case of an individual whose reduced benefit under this section is increased as a result of the use of an adjusted reduction period (in accordance with paragraphs (1) and (3) of this subsection), then for the first month for which such increase is effective, and for all subsequent months, the amount of such reduction (after the application of the previous sentence, if applicable) shall be determined— (A) in the case of old-age, wife 's, and husband 's insurance benefits, by multiplying such amount by the ratio of (i) the number of months in the adjusted reduction period to (ii) the number of months in the reduction period, (B) in the case of widow 's and widower 's insurance benefits for the month in which such individual attains age 62, by multiplying such amount by the ratio of (i) the number of months in the reduction period beginning with age 62 multiplied by 19 ⁄ 40 of 1 percent, plus the number of months in the adjusted reduction period prior to age 62 multiplied by 19 ⁄ 40 of 1 percent to (ii) the number of months in the reduction period multiplied by 19 ⁄ 40 of 1 percent, and (C) in the case of widow 's and widower 's insurance benefits for the month in which such individual attains retirement age (as defined in section 416(l) of this title ), by multiplying such amount by the ratio of (i) the number of months in the adjusted reduction period multiplied by 19 ⁄ 40 of 1 percent to (ii) the number of months in the reduction period beginning with age 62 multiplied by 19 ⁄ 40 of 1 percent, plus the number of months in the adjusted reduction period prior to age 62 multiplied by 19 ⁄ 40 of 1 percent, such determination being made in accordance with the provisions of paragraph (8). (11) When an individual is entitled to more than one monthly benefit under this subchapter and one or more of such benefits are reduced under this subsection, paragraph (10) shall apply separately to each such benefit reduced under this subsection before the application of subsection (k) (pertaining to the method by which monthly benefits are offset when an individual is entitled to more than one kind of benefit) and the application of this paragraph shall operate in conjunction with paragraph (3). + +## 42 U.S.C. 415(f) (https://www.law.cornell.edu/uscode/text/42/415) + +(f) Recomputation of benefits (1) After an individual's primary insurance amount has been determined under this section, there shall be no recomputation of such individual's primary insurance amount except as provided in this subsection or, in the case of a World War II veteran who died prior to July 27, 1954 , as provided in section 417(b) of this title . (2) (A) If an individual has wages or self-employment income for a year after 1978 for any part of which he is entitled to old-age or disability insurance benefits, the Commissioner of Social Security shall, at such time or times and within such period as the Commissioner may by regulation prescribe, recompute the individual's primary insurance amount for that year. (B) For the purpose of applying subparagraph (A) of subsection (a)(1) to the average indexed monthly earnings of an individual to whom that subsection applies and who receives a recomputation under this paragraph, there shall be used, in lieu of the amounts established by subsection (a)(1)(B) for purposes of clauses (i) and (ii) of subsection (a)(1)(A), the amounts so established that were (or, in the case of an individual described in subsection (a)(4)(B), would have been) used in the computation of such individual's primary insurance amount prior to the application of this subsection. (C) A recomputation of any individual's primary insurance amount under this paragraph shall be made as provided in subsection (a)(1) as though the year with respect to which it is made is the last year of the period specified in subsection (b)(2)(B)(ii); and subsection (b)(3)(A) shall apply with respect to any such recomputation as it applied in the computation of such individual's primary insurance amount prior to the application of this subsection. (D) A recomputation under this paragraph with respect to any year shall be effective— (i) in the case of an individual who did not die in that year, for monthly benefits beginning with benefits for January of the following year; or (ii) in the case of an individual who died in that year, for monthly benefits beginning with benefits for the month in which he died. (3) Repealed. Pub. L. 95–216, title II, § 201(f)(2) , Dec. 20, 1977 , 91 Stat. 1521 . (4) A recomputation shall be effective under this subsection only if it increases the primary insurance amount by at least $1. (5) In the case of a man who became entitled to old-age insurance benefits and died before the month in which he attained retirement age (as defined in section 416(l) of this title ), the Commissioner of Social Security shall recompute his primary insurance amount as provided in subsection (a) as though he became entitled to old-age insurance benefits in the month in which he died; except that (i) his computation base years referred to in subsection (b)(2) shall include the year in which he died, and (ii) his elapsed years referred to in subsection (b)(3) shall not include the year in which he died or any year thereafter. Such recomputation of such primary insurance amount shall be effective for and after the month in which he died. (6) Upon the death after 1967 of an individual entitled to benefits under section 402(a) or section 423 of this title , if any person is entitled to monthly benefits or a lump-sum death payment, on the wages and self-employment income of such individual, the Commissioner of Social Security shall recompute the decedent's primary insurance amount, but only if the decedent during his lifetime was paid compensation which was treated under section 405(o) of this title as remuneration for employment. (7) This subsection as in effect in December 1978 shall continue to apply to the recomputation of a primary insurance amount computed under subsection (a) or (d) as in effect (without regard to the table in subsection (a)) in that month, and, where appropriate, under subsection (d) as in effect in December 1977, including a primary insurance amount computed under any such subsection whose operation is modified as a result of the amendments made by section 5117 of the Omnibus Budget Reconciliation Act of 1990 . For purposes of recomputing a primary insurance amount determined under subsection (a) or (d) (as so in effect) in the case of an individual to whom those subsections apply by reason of subsection (a)(4)(B) as in effect after December 1978, no remuneration shall be taken into account for the year in which the individual initially became eligible for an old-age or disability insurance benefit or died, or for any year thereafter, and (effective January 1982) the recomputation shall be modified by the application of subsection (a)(6) where applicable. (8) The Commissioner of Social Security shall recompute the primary insurance amounts applicable to beneficiaries whose benefits are based on a primary insurance amount which was computed under subsection (a)(3) effective prior to January 1979, or would have been so computed if the dollar amount specified therein were $11.50. Such recomputation shall be effective January 1979, and shall include the effect of the increase in the dollar amount provided by subsection (a)(1)(C)(i). Such primary insurance amount shall be deemed to be provided under such section for purposes of subsection (i). diff --git a/tests/data/track_b/arf_recompute_sources/manifest.json b/tests/data/track_b/arf_recompute_sources/manifest.json new file mode 100644 index 00000000..33c49304 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/manifest.json @@ -0,0 +1,486 @@ +{ + "schema_version": 1, + "purpose": "Track B milestone R3 statutory ARF and recomputation rules and worked examples; no population data", + "capture_formats": { + "http_body": "Decoded HTTP response body, unmodified; POMS HTML or eCFR section XML as of 2026-10-07.", + "http_text_excerpt": "Verbatim LII excerpts after stripping tags and collapsing whitespace; served_bodies pins the full responses.", + "browser_text": "SSA PDF excerpts obtained by the orchestrator through its browser, extracted by pdftotext -layout; served_bodies pins the PDF." + }, + "sources": [ + { + "id": "poms_rs_00615_003", + "citation": "POMS RS 00615.003", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300615003", + "retrieved_at_utc": "2026-10-11T11:35:58Z", + "capture": "http_body", + "file": "poms_rs_00615_003.html", + "bytes": 33488, + "sha256": "935b391658a9f94117fde850fa4eb88293eb9ac5f19e05a5b9f946c4e15e6c2d", + "poms_transmittal": "TN 24 (09-02)", + "page_date": "05/21/2004", + "title": "RS 00615.003 - Full Retirement Age - 05/" + }, + { + "id": "poms_rs_00615_004", + "citation": "POMS RS 00615.004", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300615004", + "retrieved_at_utc": "2026-10-11T11:35:59Z", + "capture": "http_body", + "file": "poms_rs_00615_004.html", + "bytes": 227765, + "sha256": "207a83b86365a276e97a882220b3413b1ae60677d5661415a88db4faf913b398", + "poms_transmittal": "TN 24 (09-02)", + "page_date": "09/16/2002", + "title": "RS 00615.004 - Attainment of Full Retirement Age - 09/" + }, + { + "id": "poms_rs_00615_010", + "citation": "POMS RS 00615.010", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300615010", + "retrieved_at_utc": "2026-10-11T11:36:00Z", + "capture": "http_body", + "file": "poms_rs_00615_010.html", + "bytes": 36552, + "sha256": "cf6bf2ca186a3186735986d945508afbdb5bf55f32252782111e52be7212abd6", + "poms_transmittal": "TN 50 (12-23)", + "page_date": "12/07/2023", + "title": "RS 00615.010 - Chart on Reduced Benefits - 12/" + }, + { + "id": "poms_rs_00615_015", + "citation": "POMS RS 00615.015", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300615015", + "retrieved_at_utc": "2026-10-11T11:36:00Z", + "capture": "http_body", + "file": "poms_rs_00615_015.html", + "bytes": 52789, + "sha256": "04fccc30851e1ecea2aaf3417b23305fc5af7754a22134cedc2f8cdca8dc29e5", + "poms_transmittal": "TN 40 (12-23)", + "page_date": "01/09/2004", + "title": "RS 00615.015 - How the Day of Birth Affects Benefits - 01/" + }, + { + "id": "poms_rs_00615_480", + "citation": "POMS RS 00615.480", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300615480", + "retrieved_at_utc": "2026-10-11T11:36:01Z", + "capture": "http_body", + "file": "poms_rs_00615_480.html", + "bytes": 19441, + "sha256": "c75dba7710902e1833371d8ef47c3272766260de05684bef5155a4399ac44c78", + "poms_transmittal": "TN 77 (09-24)", + "page_date": "09/16/2002", + "title": "RS 00615.480 - Reduction Factor Adjustment (ARF) - 09/" + }, + { + "id": "poms_rs_00615_482", + "citation": "POMS RS 00615.482", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300615482", + "retrieved_at_utc": "2026-10-11T11:36:02Z", + "capture": "http_body", + "file": "poms_rs_00615_482.html", + "bytes": 33687, + "sha256": "8b579019f4e8d0111175b9756ddaa688317026c7db68faefbdf7b322571cdad0", + "poms_transmittal": "TN 76 (08-24)", + "page_date": "08/06/2024", + "title": "RS 00615.482 - Requirements for Adjustment of Reduction Factor (ARF) — Crediting Months - 08/" + }, + { + "id": "poms_rs_00615_490", + "citation": "POMS RS 00615.490", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300615490", + "retrieved_at_utc": "2026-10-11T11:36:02Z", + "capture": "http_body", + "file": "poms_rs_00615_490.html", + "bytes": 22950, + "sha256": "62acdbcc3790601074414fd23fdee15081f56c8d75543216752f96fc64b40f1e", + "poms_transmittal": "TN 24 (09-02)", + "page_date": "09/16/2002", + "title": "RS 00615.490 - Computing the RIB or Spouse Benefit (Single Entitlement) — Reduction Factor Adjustment - 09/" + }, + { + "id": "poms_rs_00615_491", + "citation": "POMS RS 00615.491", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300615491", + "retrieved_at_utc": "2026-10-11T11:36:03Z", + "capture": "http_body", + "file": "poms_rs_00615_491.html", + "bytes": 18787, + "sha256": "6bccc5680188c727cf68914ecc907b6c41d58b97aab62e2f26d1798ea735de5b", + "poms_transmittal": "TN 49 (12-23)", + "page_date": "12/06/2023", + "title": "RS 00615.491 - ARF Processing After 1977 for a Reentitled Actuarially Reduced Spouse - 12/" + }, + { + "id": "poms_rs_00615_500", + "citation": "POMS RS 00615.500", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300615500", + "retrieved_at_utc": "2026-10-11T11:36:04Z", + "capture": "http_body", + "file": "poms_rs_00615_500.html", + "bytes": 19385, + "sha256": "14b0717fad5f6d392e8945f1f90525fbd08b67f042e39d16ecd3131aed0a272f", + "poms_transmittal": "TN 24 (09-02)", + "page_date": "09/16/2002", + "title": "RS 00615.500 - Computing the Spouse's Benefit Reduction Factor Adjustment — Spouse also Entitled to RIB or DIB - 09/" + }, + { + "id": "poms_rs_00615_580", + "citation": "POMS RS 00615.580", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300615580", + "retrieved_at_utc": "2026-10-11T11:36:04Z", + "capture": "http_body", + "file": "poms_rs_00615_580.html", + "bytes": 22043, + "sha256": "403981fe78797059314fb255173efadd0f49ac06f658e4f30c549ba037147319", + "poms_transmittal": "TN 24 (09-02)", + "page_date": "09/16/2002", + "title": "RS 00615.580 - Computing the Widow(er)'s Benefits — Reduction Factor Adjustment - 09/" + }, + { + "id": "poms_rs_00615_582", + "citation": "POMS RS 00615.582", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300615582", + "retrieved_at_utc": "2026-10-11T11:36:05Z", + "capture": "http_body", + "file": "poms_rs_00615_582.html", + "bytes": 19920, + "sha256": "61f0dc95c7318af53cbf9f5a26d3f21f97936802fc1f477a87be45be7c4b3fc1", + "poms_transmittal": "TN 24 (09-02)", + "page_date": "09/16/2002", + "title": "RS 00615.582 - Computing the Reduction Factor Adjustment — No Entitlement Prior to Age 60 - 09/" + }, + { + "id": "poms_rs_00615_585", + "citation": "POMS RS 00615.585", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300615585", + "retrieved_at_utc": "2026-10-11T11:36:06Z", + "capture": "http_body", + "file": "poms_rs_00615_585.html", + "bytes": 19591, + "sha256": "2e574c13ee21827cade7e35cbe3f39242a8d52174ab2b96c87c054106954da87", + "poms_transmittal": "TN 24 (09-02)", + "page_date": "09/16/2002", + "title": "RS 00615.585 - Unusual Characteristics of the Widow(er)'s ARF - 09/" + }, + { + "id": "poms_rs_00615_590", + "citation": "POMS RS 00615.590", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300615590", + "retrieved_at_utc": "2026-10-11T11:36:06Z", + "capture": "http_body", + "file": "poms_rs_00615_590.html", + "bytes": 23730, + "sha256": "2d0e05e38849fc9da5b58de7f2c5af594d84aefbc99cd7bbe088ee9441fb7004", + "poms_transmittal": "TN 24 (09-02)", + "page_date": "09/16/2002", + "title": "RS 00615.590 - Computing the Widow(er)'s Benefit Reduction Factor Adjustment — Initial Entitlement to Disabled Widow(er)'s Benefit - 09/" + }, + { + "id": "poms_rs_00615_595", + "citation": "POMS RS 00615.595", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300615595", + "retrieved_at_utc": "2026-10-11T11:36:07Z", + "capture": "http_body", + "file": "poms_rs_00615_595.html", + "bytes": 18703, + "sha256": "7c8725bb840e2b2707069947a000c5195a83b9dcec6f241675484bb2f54a89cf", + "poms_transmittal": "TN 24 (09-02)", + "page_date": "07/01/2011", + "title": "RS 00615.595 - Computing the Widow(er)'s Benefit — Reduction Factor Adjustment — Dual Entitlement to DIB Before or Concurrent With WIB - 07/" + }, + { + "id": "poms_rs_00615_598", + "citation": "POMS RS 00615.598", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300615598", + "retrieved_at_utc": "2026-10-11T11:36:08Z", + "capture": "http_body", + "file": "poms_rs_00615_598.html", + "bytes": 17499, + "sha256": "159726c85182842c5c376b1a51335ae4f8dbe44c3cc9a8b23f04b793719f3595", + "poms_transmittal": "TN 44 (12-23)", + "page_date": "12/06/2023", + "title": "RS 00615.598 - Adjustment of the Reduction Factor for a Deceased Numberholder - 12/" + }, + { + "id": "poms_rs_00605_005", + "citation": "POMS RS 00605.005", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300605005", + "retrieved_at_utc": "2026-10-11T11:36:09Z", + "capture": "http_body", + "file": "poms_rs_00605_005.html", + "bytes": 32296, + "sha256": "1f018b0996fc946c46603293d756332ec0847103a7cdfb795861e02a63a83815", + "poms_transmittal": "TN 95 (10-23)", + "page_date": "03/21/2006", + "title": "RS 00605.005 - Benchmark Year (Eligibility Year) - 03/" + }, + { + "id": "poms_rs_00605_401", + "citation": "POMS RS 00605.401", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300605401", + "retrieved_at_utc": "2026-10-11T11:36:09Z", + "capture": "http_body", + "file": "poms_rs_00605_401.html", + "bytes": 25076, + "sha256": "2234e0909fb874b32c501892aec8db1b6d8bf94a98011b38cc1ac9c5b3720e01", + "poms_transmittal": "TN 137 (09-25)", + "page_date": "09/15/2025", + "title": "RS 00605.401 - Recomputations and Recalculations - 09/" + }, + { + "id": "poms_rs_00605_405", + "citation": "POMS RS 00605.405", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300605405", + "retrieved_at_utc": "2026-10-11T11:36:10Z", + "capture": "http_body", + "file": "poms_rs_00605_405.html", + "bytes": 17822, + "sha256": "75c6d37811591f3d44f723e67512abcaae73526b9cb4b6d26e8c1a9dd99e6cc8", + "poms_transmittal": "TN 137 (09-25)", + "page_date": "09/15/2025", + "title": "RS 00605.405 - Recomputations and Administrative Finality - 09/" + }, + { + "id": "poms_rs_00605_407", + "citation": "POMS RS 00605.407", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300605407", + "retrieved_at_utc": "2026-10-11T11:36:11Z", + "capture": "http_body", + "file": "poms_rs_00605_407.html", + "bytes": 29539, + "sha256": "0e9813c138269ad303f3dc26c796cc45ed5913d8cb652e8ab92104043e3f6878", + "poms_transmittal": "TN 28 (08-99)", + "page_date": "03/15/2006", + "title": "RS 00605.407 - Disadvantageous Recomputations - 03/" + }, + { + "id": "poms_rs_00605_501", + "citation": "POMS RS 00605.501", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300605501", + "retrieved_at_utc": "2026-10-11T11:36:11Z", + "capture": "http_body", + "file": "poms_rs_00605_501.html", + "bytes": 25157, + "sha256": "82d050e85f960956fb69e62ce5e10690a6088fa549711285ac194f0b37742096", + "poms_transmittal": "TN 137 (09-25)", + "page_date": "09/15/2025", + "title": "RS 00605.501 - Current Recomputations - 1977 Amendments - 09/" + }, + { + "id": "poms_rs_00605_560", + "citation": "POMS RS 00605.560", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300605560", + "retrieved_at_utc": "2026-10-11T11:36:12Z", + "capture": "http_body", + "file": "poms_rs_00605_560.html", + "bytes": 17435, + "sha256": "a8b9162210f7a783ea5e2ce09cc2b012c7899b75b603e703f2f3aca4f2b3e365", + "poms_transmittal": "TN 28 (08-99)", + "page_date": "08/02/1999", + "title": "RS 00605.560 - Automatic Earnings Reappraisal Operation (AERO) - 08/" + }, + { + "id": "poms_rs_00605_570", + "citation": "POMS RS 00605.570", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300605570", + "retrieved_at_utc": "2026-10-11T11:36:13Z", + "capture": "http_body", + "file": "poms_rs_00605_570.html", + "bytes": 19484, + "sha256": "8cf357c54a8f316ea3afba2d2a171996768894d7c4fa9eee5b417e5546d25c79", + "poms_transmittal": "TN 28 (08-99)", + "page_date": "08/26/2015", + "title": "RS 00605.570 - Processing Recomputation Requests - 08/" + }, + { + "id": "poms_rs_00605_580", + "citation": "POMS RS 00605.580", + "url": "https://secure.ssa.gov/poms.nsf/lnx/0300605580", + "retrieved_at_utc": "2026-10-11T11:36:13Z", + "capture": "http_body", + "file": "poms_rs_00605_580.html", + "bytes": 21129, + "sha256": "cd64e8e63d773dd94c50b0fa4a6cdf750816b38fecf921ec34236de923a0f5ec", + "poms_transmittal": "TN 28 (08-99)", + "page_date": "08/02/1999", + "title": "RS 00605.580 - Recalculation of Benefits - 08/" + }, + { + "id": "ecfr_20cfr_404_280", + "citation": "20 CFR 404.280 (eCFR, as of 2026-10-07)", + "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-10-07/title-20.xml?part=404§ion=404.280", + "retrieved_at_utc": "2026-10-11T11:36:14Z", + "capture": "http_body", + "file": "ecfr_20cfr_404_280.xml", + "bytes": 1389, + "sha256": "7785688410ccc531336cf8bb6edcab062a3e7d4b891495de6bb3dd596edd2320" + }, + { + "id": "ecfr_20cfr_404_281", + "citation": "20 CFR 404.281 (eCFR, as of 2026-10-07)", + "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-10-07/title-20.xml?part=404§ion=404.281", + "retrieved_at_utc": "2026-10-11T11:36:14Z", + "capture": "http_body", + "file": "ecfr_20cfr_404_281.xml", + "bytes": 2998, + "sha256": "6220d7718179030f8ffae5a3c33523711bbcbf16d7abf12fb893320b616dee91" + }, + { + "id": "ecfr_20cfr_404_282", + "citation": "20 CFR 404.282 (eCFR, as of 2026-10-07)", + "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-10-07/title-20.xml?part=404§ion=404.282", + "retrieved_at_utc": "2026-10-11T11:36:19Z", + "capture": "http_body", + "file": "ecfr_20cfr_404_282.xml", + "bytes": 1251, + "sha256": "601775c323a13319f5b1e348e1a020ebc7a1decee335d06cedae21324c56ff5e" + }, + { + "id": "ecfr_20cfr_404_283", + "citation": "20 CFR 404.283 (eCFR, as of 2026-10-07)", + "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-10-07/title-20.xml?part=404§ion=404.283", + "retrieved_at_utc": "2026-10-11T11:36:24Z", + "capture": "http_body", + "file": "ecfr_20cfr_404_283.xml", + "bytes": 630, + "sha256": "501e458e646c20f92a496f9f09f576e63bd7febc06c5b6647ccba43884e28532" + }, + { + "id": "ecfr_20cfr_404_284", + "citation": "20 CFR 404.284 (eCFR, as of 2026-10-07)", + "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-10-07/title-20.xml?part=404§ion=404.284", + "retrieved_at_utc": "2026-10-11T11:36:29Z", + "capture": "http_body", + "file": "ecfr_20cfr_404_284.xml", + "bytes": 5474, + "sha256": "5cbca424f931555ae8576b4e89d652a8be857041da39bf6836bb7f6aae0df1ea" + }, + { + "id": "ecfr_20cfr_404_285", + "citation": "20 CFR 404.285 (eCFR, as of 2026-10-07)", + "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-10-07/title-20.xml?part=404§ion=404.285", + "retrieved_at_utc": "2026-10-11T11:36:29Z", + "capture": "http_body", + "file": "ecfr_20cfr_404_285.xml", + "bytes": 1131, + "sha256": "9d68132c1ecb47f95599894af72bd562a97c5eaa63da6d06c984e53fa689695d" + }, + { + "id": "ecfr_20cfr_404_286", + "citation": "20 CFR 404.286 (eCFR, as of 2026-10-07)", + "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-10-07/title-20.xml?part=404§ion=404.286", + "retrieved_at_utc": "2026-10-11T11:36:30Z", + "capture": "http_body", + "file": "ecfr_20cfr_404_286.xml", + "bytes": 946, + "sha256": "b1c980f4204bf6fe073872ed66b25cc0639e0c62fb172e52233bc929229bd434" + }, + { + "id": "ecfr_20cfr_404_287", + "citation": "20 CFR 404.287 (eCFR, as of 2026-10-07)", + "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-10-07/title-20.xml?part=404§ion=404.287", + "retrieved_at_utc": "2026-10-11T11:36:34Z", + "capture": "http_body", + "file": "ecfr_20cfr_404_287.xml", + "bytes": 742, + "sha256": "8cdbb57984bd5e64574c8302bdd4553191d40091e62a76e274635aa1b14fb6ca" + }, + { + "id": "ecfr_20cfr_404_288", + "citation": "20 CFR 404.288 (eCFR, as of 2026-10-07)", + "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-10-07/title-20.xml?part=404§ion=404.288", + "retrieved_at_utc": "2026-10-11T11:36:34Z", + "capture": "http_body", + "file": "ecfr_20cfr_404_288.xml", + "bytes": 1923, + "sha256": "1d07ea68fb8ce0b5c7b977f6c8a5e8911092d4a4d1b73dc1bcef87dd777d887c" + }, + { + "id": "ecfr_20cfr_404_410", + "citation": "20 CFR 404.410 (eCFR, as of 2026-10-07)", + "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-10-07/title-20.xml?part=404§ion=404.410", + "retrieved_at_utc": "2026-10-11T11:36:39Z", + "capture": "http_body", + "file": "ecfr_20cfr_404_410.xml", + "bytes": 7247, + "sha256": "2b4c11db537329505d0f9534707cac9c969baebdeb1919c52ac103dd3a1797ba" + }, + { + "id": "ecfr_20cfr_404_411", + "citation": "20 CFR 404.411 (eCFR, as of 2026-10-07)", + "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-10-07/title-20.xml?part=404§ion=404.411", + "retrieved_at_utc": "2026-10-11T11:36:44Z", + "capture": "http_body", + "file": "ecfr_20cfr_404_411.xml", + "bytes": 7186, + "sha256": "afeee4eaf40387f1759ddf04b6c62d1fe9d6fa5ca1fb7170fe8872f9e651c5e8" + }, + { + "id": "ecfr_20cfr_404_412", + "citation": "20 CFR 404.412 (eCFR, as of 2026-10-07)", + "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-10-07/title-20.xml?part=404§ion=404.412", + "retrieved_at_utc": "2026-10-11T11:36:45Z", + "capture": "http_body", + "file": "ecfr_20cfr_404_412.xml", + "bytes": 2445, + "sha256": "e560ab4ca39a5281573578ad89fcc64ef59edb6c3876bc2c75dd828bdac1aa89" + }, + { + "id": "ecfr_20cfr_404_409", + "citation": "20 CFR 404.409 (eCFR, as of 2026-10-07)", + "url": "https://www.ecfr.gov/api/versioner/v1/full/2026-10-07/title-20.xml?part=404§ion=404.409", + "retrieved_at_utc": "2026-10-11T11:36:46Z", + "capture": "http_body", + "file": "ecfr_20cfr_404_409.xml", + "bytes": 5298, + "sha256": "f5545ff658026f9926715cbc75d032ab1d502833d828c552e303bef494e9dce9" + }, + { + "id": "lii_usc_402q_415f_excerpts", + "citation": "42 U.S.C. 402(q) and 415(f) (LII)", + "capture": "http_text_excerpt", + "file": "lii_usc_402q_415f_excerpts.txt", + "served_bodies": [ + { + "url": "https://www.law.cornell.edu/uscode/text/42/402", + "retrieved_at_utc": "2026-10-11T11:37:16Z", + "bytes": 1089709, + "sha256": "e5bb977c3ab381c2c3bd4f6bb320b86f25e2f0ad2b629e01333be56727d3823b" + }, + { + "url": "https://www.law.cornell.edu/uscode/text/42/415", + "retrieved_at_utc": "2026-10-11T11:37:16Z", + "bytes": 481427, + "sha256": "c3262e82e8e674caf43709c522ce60760eafb0b133e1f99221ca7aa97d54ea3a" + } + ], + "url": "https://www.law.cornell.edu/uscode/text/42/402", + "additional_urls": [ + "https://www.law.cornell.edu/uscode/text/42/415" + ], + "retrieved_at_utc": "2026-10-11T11:37:16Z", + "bytes": 20306, + "sha256": "f63cf9e32dc15729810857525f2f0f1070f9bba6b02653e1efa57c6d0195c28c" + }, + { + "id": "ssa_pub_05_10069_arf_recompute_excerpts", + "citation": "SSA Publication No. 05-10069, How Work Affects Your Benefits (2026), ARF and recomputation passages", + "capture": "browser_text", + "file": "ssa_pub_05_10069_arf_recompute_excerpts.txt", + "served_bodies": [ + { + "url": "https://www.ssa.gov/pubs/EN-05-10069.pdf", + "retrieved_at_utc": "2026-10-11T11:38:33Z", + "bytes": 261038, + "sha256": "2ecbfd9234ba445e73259f284d861277ca186d2a8582744a541f2ac1077bbf4f" + } + ], + "note": "www.ssa.gov answers curl with HTTP 403; PDF fetched in the in-app browser and hashed in-page; text by pdftotext -layout (Poppler). The full PDF capture is in R2 PR #528.", + "url": "https://www.ssa.gov/pubs/EN-05-10069.pdf", + "retrieved_at_utc": "2026-10-11T11:38:33Z", + "bytes": 1466, + "sha256": "f2877909a960f6bcd5497641374d2e53b0feb46f59e2f7085eb6eab02de0e06b" + } + ] +} diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00605_005.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00605_005.html new file mode 100644 index 00000000..c3fe5171 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00605_005.html @@ -0,0 +1,562 @@ + + +SSA - POMS: RS 00605.005 - Benchmark Year (Eligibility Year) - 03/21/2006 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 95 (10-23)

+

RS 00605.005 Benchmark Year (Eligibility Year) +

+ + +
+
+

A. Policy - General +

+
+
+
+
+

1. Definition +

+
+
+

The benchmark year, or eligibility year, for computation purposes is the year in which + the number holder attains a specific age or a specific event occurs. +

+
+
+
+
+

2. Retirement Insurance Benefit +

+
+
+

The benchmark year for RIB is the year of age 62 attainment (see RS 00605.040A.2.e. for men born before January 2, 1913).

+
+
+
+
+

3. Disability Insurance Benefit +

+
+
+

The benchmark year for DIB is the earlier of:

+
    +
  • +
    +
    •
    +
    +

    the year of actual age 62 attainment, or

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    the year of disability onset or date of first insured status if later (freeze computation); + or assumed age 62 (non-freeze computation). See RS 00605.220 - RS 00605.225 for discussions of freeze and non-freeze. See RS 00605.201 for a definition of “assumed age 62.” +

    + +
    +
    +
  • +
+
+
+
+
+

4. Survivor +

+
+
+

The benchmark year for survivor's benefits is the earlier of:

+
    +
  • +
    +
    •
    +
    +

    the year of age 62 attainment; or

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    the year of death.

    + +
    +
    +
  • +
+
+
+

NOTE:: 

+
+
+ +

See RS 00615.302 for a benchmark determination for the widow(er)'s indexing provision. This provision + does not change the benchmark determination for a deceased number holder's (NH's) + record. +

+ +
+
+
+
+
+
+ +
+
+

B. Policy - Prior DIB Involved +

+
+
+

In determining the benchmark year, the following rules apply where the NH was entitled + to DIB before the current entitlement or death. +

+

NOTE: Whether the benefit was paid is immaterial. A month of suspension for EPE still + counts as entitlement. +

+
+
+

1. Within 12 Months +

+
+
+

If the NH was entitled to DIB in any of the 12 months before the date (month and year) + determined in RS 00605.005A., the benchmark year is the year of onset from the prior DIB. This is unless the + prior period of DIB is not used. See RS 00605.005B.3.If there is more than one prior period of DIB, the benchmark year reverts backwards + through the periods of disability until the first period of disability that is preceded + by a gap in DIB entitlement of more than 12 months (or there is no DIB at all). +

+
+
+

NOTE:: 

+
+
+ +

The prior DIB entitlement may continue after the date in RS 00605.005A

+ +
+
+
+
+
+
+

2. More Than 12 Months +

+
+
+

If the NH was entitled to DIB, and there is a gap of more than 12 months before the + date (month and year) determined in RS 00605.005A., the current eligibility date (month and year) from RS 00605.005A. will be the benchmark year. +

+
+
+
+
+

3. Prior Period Not Used +

+
+
+

You may disregard the prior period of disability if the NH is insured without considering + the period. +

+

If you disregard the period for one purpose, you must disregard it for all computation + purposes. This includes elapsed years, base years, and benchmark year determinations. +

+
+
+
+
+

4. DIB Guarantee PIA used as Survivor PIA +

+
+
+

If using a DIB Guarantee PIA to pay survivor benefits, the eligibility year is the + year of age 62 or death if before age 62. The DIB Guarantee is a 78 New Law PIA, so + Windexing may apply. +

+
+
+
+
+ +
+
+

C. Procedure +

+
+
+

To determine the benchmark year:

+
+
+

1. Insured Status +

+
+
+

Establish the benchmark year without considering insured status if you are using age + 62 attainment for the benchmark year. Make an insured status determination if you + are basing the benchmark year on disability onset or death. +

+
+
+
+
+

2. PIA Computations +

+
+
+

Compute a PIA using all applicable computation methods for each benchmark year determined + under RS 00605.005A. and RS 00605.005B. +

+
+
+
+
+

3. COLA's +

+
+
+

Include all COLAs that apply to the computation method up to the date of entitlement.

+
+
+

NOTE:: 

+
+
+ +

Since we apply COLAs for some PIA methods by using the benchmark year, the number + of applicable COLAs may vary for the same NH. +

+ +
+
+

Select the PIA upon which you will base payment after you apply COLAs.

+
+
+
+
+

4. Selection +

+
+
+

Compare all PIAs and select the highest. This is the PIA of record. Establish the + benchmark year by selecting the one that produces the highest PIA. +

+
+
+
+
+ +
+
+

D. Examples +

+
+
+

Example 1

+

Susan, DOB 2/20/29, filed for DIB in 1/88. A period of disability was established + beginning 6/12/87. Their benchmark year was 1987, the year in which the period of + disability began. +

+

Their condition improved so their DIB was terminated 10/89 (the DIB entitlement continued + through 9/89). In 3/91, at age 62, they filed for RIB. Since they were not entitled + to DIB for any of the 12 months preceding the month they attained age 62, their benchmark + year for the RIB PIA computation is 1991. Nevertheless, the prior period may be considered + in establishing insured status and in determining elapsed years, computation years, + and the base years. A non-freeze computation must also be considered. +

+

Example 2

+

Same facts as above except Susan again became disabled in 7/90. Since they were eligible + for DIB within 12 months of the last month of their prior DIB entitlement for a freeze + computation, their benchmark year remains 1987, the year their prior period of disability + began. +

+

If the prior period of disability is not considered, their benchmark year is 1990, + the year of assumed age 62. Disability insured status must be met in 7/90 if this + non-freeze computation is used. +

+

Example 3

+

Sam, DOB 04/05/45, filed for DIB in 12/90. They had no prior periods of disability. + We established a period of disability with an onset date of 05/18/90. Their benchmark + year based on this period of disability was 1990. +

+

Sam’s initial period of disability terminated in 05/92. They reapplied for DIB in + 02/93 with an onset date of 01/15/93. We approved Sam for DIB, and their benchmark + year remained 1990. +

+

Sam’s second period of DIB terminated in 07/97 They reapplied for disability benefits + in 12/97 with an onset date of 11/20/97. Again, we approved Sam for disability benefits. + Using a DIB freeze computation, Sam’s benchmark year remained 1990. +

+

Even though Sam had multiple periods of disability, there was never a gap in entitlement + greater than 12 months. Therefore, the benchmark year remained the same as their very + first period of disability. This assumes that we used the prior period of disability. +

+
+
+ +
+
+

E. Reference +

+
+
+

RS 00605.035 Subsequent DIB guarantee PIA +

+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300605005
RS 00605.005 - Benchmark Year (Eligibility Year) - 03/21/2006
+Batch run: 03/09/2026
+Rev:03/21/2006
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00605_401.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00605_401.html new file mode 100644 index 00000000..7eaed365 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00605_401.html @@ -0,0 +1,438 @@ + + +SSA - POMS: RS 00605.401 - Recomputations and Recalculations - 09/15/2025 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 137 (09-25)

+

RS 00605.401 Recomputations and Recalculations +

+ + +
+
+

A. Policy +

+
+
+

The PIA can be changed only by a recomputation or recalculation.

+
+
+ +
+
+

B. Definitions +

+
+
+
+
+

1. Recomputations +

+
+
+

Recomputations:

+
    +
  • +
    +
    •
    +
    +

    Involve the use of a new computation period;

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Can be effective only to increase the PIA (or to impose WEP or remove WEP for any + reason for months of entitlement prior to January 2024); +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Are specifically provided for in the law;

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Can be waived, in writing, if the numberholder (NH) or their family would be disadvantaged + in some way or if they and every member of their family do not want the primary insurance + amount to be recomputed for any other reason; and +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Are effective January of the year following the year the earnings were earned. For + example, earnings for 1999 will be included in a recomputation effective January 2000. + There are three exceptions: +

    + +
      +
    1. +
      +
      a. 
      +
      +

      A recomputation to impose WEP is effective the first month that WEP applies for months + of entitlement prior to January 2024. +

      + +
      +
      +
    2. +
    3. +
      +
      b. 
      +
      +

      A recomputation to allow credit for earnings in the year of death is effective the + month of death. +

      + +
      +
      +
    4. +
    5. +
      +
      c. 
      +
      +

      A recomputation meeting the requirements in RS 00605.407B.9. will be effective in the first month it is no longer disadvantageous. +

      + +
      +
      +
    6. +
    + +
    +
    +
  • +
+
+
+
+
+

2. Recalculations +

+
+
+

Recalculations:

+
    +
  • +
    +
    •
    +
    +

    Involve the reopening of a previous computation to change computation factors or to + include or exclude additional earnings in the base year period; +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Can increase or decrease the PIA;

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Are not specifically provided for in the law; and

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Are effective the same month as the original computation.

    + +
    +
    +

    NOTE: 

    +
    +
    + +

    Verify the accuracy of the PIA and benefit entitlement after removing inaccurate earnings.

    + +
    +
    + +
    +
    +
  • +
+

 

+
+
+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300605401
RS 00605.401 - Recomputations and Recalculations - 09/15/2025
+Batch run: 09/15/2025
+Rev:09/15/2025
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00605_405.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00605_405.html new file mode 100644 index 00000000..36f6fa3f --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00605_405.html @@ -0,0 +1,281 @@ + + +SSA - POMS: RS 00605.405 - Recomputations and Administrative Finality - 09/15/2025 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 137 (09-25)

+

RS 00605.405 Recomputations and Administrative Finality +

+ + +
+
+

Policy +

+
+
+

A recomputation is a new initial determination. A recomputation uses only correct + information. A PIA that is incorrect but barred to correction by administrative finality + will remain the PIA of record (COLAs apply as they occur) until a PIA computed using + correct information exceeds it. Consider a recomputation for each additional base + year of earnings. If the resulting PIA is lower than the PIA of record, the recomputation + will not be processed. See GN 04030.050 for more detailed information about this and other related issues. +

+

NOTE: The correct information to be used in the recomputation refers to computation factor + information (ELY, bend points etc.) A decision to impose WEP for months prior to January + 2024, GPO for months prior to January 2024, or PDB offset is not a computation factor. + That decision is subject to the same rules for reopening as any other determination + such as a person’s age or disability status. +

+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300605405
RS 00605.405 - Recomputations and Administrative Finality - 09/15/2025
+Batch run: 09/15/2025
+Rev:09/15/2025
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00605_407.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00605_407.html new file mode 100644 index 00000000..fe94d693 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00605_407.html @@ -0,0 +1,506 @@ + + +SSA - POMS: RS 00605.407 - Disadvantageous Recomputations - 03/15/2006 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 28 (08-99)

+

RS 00605.407 Disadvantageous Recomputations +

+ + +
+
+

A. Policy +

+
+
+

A recomputation of the PIA can result in a decrease in total family benefits. This + can occur in various ways: +

+
    +
  • +
    +
    •
    +
    +

    When the PIA is recomputed and the maximum of record does not change, the new distribution + among the family members can cause the total family payment to be lowered due to rounding. +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    A shift of a simultaneously entitled child from one ER to another can also cause loss + of payment in some circumstances. +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    In primary/secondary dual entitlement cases, a recomputation on the primary record + may cause a decrease in the total payable after rounding. +

    + +
    +
    +
  • +
+

Use the following procedure to determine when not to process a recomputation because + it is truly disadvantageous. Any recomputation found to be disadvantageous will be + processed when it becomes advantageous. +

+
+
+

NOTE: If a recomputation is not covered by the rules in RS 00605.407B. and is still disadvantageous in any way, the numberholder may ask that the recomputation + not be processed. Obtain a signed statement waiving the recomputation and follow the + steps in RS 00605.407B.9.a. On a survivor claim all beneficiaries must waive the recomputation. +

+ +
+
+
+
+ +
+
+

B. Procedure - Determining Whether a Recomputation is Disadvantageous +

+
+
+

To determine whether a recomputation is truly disadvantageous it is necessary to consider + both the MBA and the MBC. +

+

There are many possible situations that may occur. Determine which description below + applies and follow the instructions for that heading. +

+
+
+

1. The MBA and the MBC increase +

+
+
+

Process the recomputation.

+
+
+
+
+

2. The MBA increases and the MBC decreases +

+
+
+

Process the recomputation. The decrease in the MBC is caused by rounding. Use existing + waiver instructions for any overpayment which results. +

+
+
+
+
+

3. The MBA increases and the MBC stays the same +

+
+
+

Process the recomputation.

+
+
+
+
+

4. The MBA stays the same and the MBC increases +

+
+
+

Process the recomputation.

+
+
+
+
+

5. The MBA and the MBC stay the same +

+
+
+

Process the recomputation.

+
+
+
+
+

6. The MBA stays the same and the MBC decreases +

+
+
+

Process the recomputation. The decrease in the MBC is caused by rounding. Use existing + waiver instructions for any overpayment which results. +

+
+
+
+
+

7. The MBA decreases and the MBC increases +

+
+
+

Process the recomputation.

+
+
+
+
+

8. The MBA decreases and the MBC stays the same +

+
+
+

Process the recomputation.

+
+
+
+
+

9. The MBA and the MBC decrease +

+
+
+
+
+

a. Underpayment is not due +

+
+
+

If no underpayment is due, do not process the recomputation. List the case using code + 434. Add a special message to the MBR explaining that the recomputation is disadvantageous + and that the recomputation will be processed only when it is no longer disadvantageous. +

+
+
+

NOTE: The effective month for the recomputation will be the first month for which the recomputation + is advantageous. +

+ +
+
+
+
+
+
+

b. Underpayment is due +

+
+
+

If an underpayment is due, contact the numberholder (NH) and explain that the recomputation + results in an underpayment but continuing rates will be decreased. Document the NH's + decision. If the NH desires the recomputation, process it. If the NH does not desire + the recomputation follow the instructions in RS 00605.407B.9.a.

+
+
+
+
+
+
+

10. Some benefits increase but total to family decreases +

+
+
+

If one or more beneficiaries are due an increase, but the total benefit payable decreases + and: +

+
+
+

a. Decrease is caused by rounding +

+
+
+

Process and use existing blanket waiver procedure.

+
+
+
+
+

b. Decrease not caused by rounding +

+
+
+

Contact the numberholder (NH) and explain. Document the NH's decision. If the NH desires + the recomputation, process it. If the NH does not desire the recomputation follow + the instructions in RS 00605.407B.9.a.

+
+
+
+
+
+
+

11. NH rate decreases but total to family increases +

+
+
+

If the NH's benefit decreases but the total payable to the family increases contact + the numberholder (NH) and explain. Document the NH's decision. If the NH desires the + recomputation, process it. If the NH does not desire the recomputation follow the + instructions in RS 00605.407B.9.a.

+
+
+
+
+

12. Dual Entitlement cases when the spouse benefit is in suspense +

+
+
+

If the spouse benefit decreases and it is suspended, an increase may be due on the + RIB which will increase the total payable. However when the spouse benefit is again + payable the combined rate will be reduced. Contact the numberholder (NH) and explain. + Document the NH's decision. If the NH desires the recomputation, process it. If the + NH does not desire the recomputation follow the instructions in RS 00605.407B.9.a.

+
+
+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300605407
RS 00605.407 - Disadvantageous Recomputations - 03/15/2006
+Batch run: 02/21/2013
+Rev:03/15/2006
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00605_501.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00605_501.html new file mode 100644 index 00000000..6cfd09eb --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00605_501.html @@ -0,0 +1,417 @@ + + +SSA - POMS: RS 00605.501 - Current Recomputations - 1977 Amendments - 09/15/2025 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 137 (09-25)

+

RS 00605.501 Current Recomputations - 1977 Amendments +

+ + +
+
+

A. Policy - One Dollar Rule +

+
+
+

This policy applies to all 1977 Amendment and later PIAs. Consider a recomputation + for each additional base year of earnings credited to the NH's record. The increase + in the PIA after the recomputation must equal at least $1.00. NOTE: This means the + actual amount of the increase must be at least $1.00. An increase from $849.40 to + $850.00 is not an increase of $1.00 or more even though the amount payable could increase + by $1.00. The PIA would have to increase to $850.40 for a recomputation to apply. +

+

When earnings for a year do not result in a recomputation (because of $1.00 limit) + the NH may be eligible for a recomputation based on those earnings when a subsequent + base year is considered. This is true even if the subsequent base year is not actually + used in the recomputation, i.e., the earnings do not exceed the lowest year used in + the computation. If the increase over the PIA of record after considering all applicable + COLAs up to the effective date of the recomputation is less than $1.00, a recomputation + is not due. +

+
+

Example:

+ +

The NH has been entitled for several years. Their 78 New Start PIA is $445.50 in 12/1997. + The NH had earnings in 1997. When we consider a recomputation in 1 /1998 the increase + is only $.90 so no recomputation is allowed. The 12/ 1998 COLA increases the PIA to + $451.20. The NH had earnings in 1998. This allows us to consider a recomputation for + 1/1999. We then find that considering all earnings through 1998 causes the PIA to + increase to $452.20 as of 1/1999. We can therefore allow the recomputation because + the increase is at least $1.00 over the 12/1998 PIA. This applies even if 1998 is + not actually used as a computation year due to the low amount of earnings. If the + NH had no earnings for 1998 we could not consider the recomputation for 1/ 1999. +

+ +
+
+
+ +
+
+

B. Policy - Special Situations Involving the One Dollar Rule +

+
+
+
+
+

1. Independently Entitled Divorced Spouse (IEDS) +

+
+
+

When an IEDS is entitled to benefits (see RS 00202.005), the initial PIA for the record is established as of the first month of the IEDS + entitlement. The NH's earnings in the year of the IEDS initial entitlement and any + subsequent years are considered recomputation years. The $1.00 increase rule applies. +

+
+
+
+
+

2. Windfall Elimination Provision (WEP) for months of entitlement prior to January 2024 +

+
+
+

When WEP is involved the WEP PIA is the PIA of record. The actual WEP PIA must increase + by $1.00 in order to cause a recomputation. +

+
+
+
+
+

3. DIB after RIB +

+
+
+

DIB after RIB requires an initial determination of the PIA. Computation of the DIB + PIA is independent of the RIB PIA and is not a recomputation. The $1.00 rule does + not apply. +

+
+
+
+
+

4. Special Minimum PIA +

+
+
+

When a Special Minimum PIA is being paid and the support PIA increases, the new support + PIA must be at least $1.00 more than the PIA being paid in order to switch to the + support PIA. +

+
+
+
+
+

5. Conversion from DIB to RIB at FRA +

+
+
+

When a DIB converts to RIB at FRA the $1.00 rule does not apply.

+
+
+
+
+

6. Conversion from DIB or RIB to Survivor +

+
+
+

When a RIB or DIB is recomputed to a survivor claim the $1.00 rule does apply.

+
+
+
+
+ +
+
+

C. Policy - Automatic Recomputations +

+
+
+

The term “Automatic Recomputations” means that the NH does not have to take any action to request a recomputation.

+
+
+

1. Disability or Retirement Cases +

+
+
+

An automatic recomputation applies if the earnings are for 1965 or later. It can be + effective 1/1966 or later. +

+
+
+
+
+

2. Survivor Cases +

+
+
+

An automatic recomputation applies if the date of death is 2/1965 or later and earnings + are for the year of death. The effective date cannot be earlier than 1/1966. +

+
+
+
+
+ +
+
+

D. Policy - Recomputations Prohibited +

+
+
+

Certain PIA methods do not permit recomputations. They are the 1977 Simplified Old + Start with a benchmark year of 1979 or later, the New Start Transitional Guarantee + PIA and the Subsequent Disability Guarantee PIA. +

+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300605501
RS 00605.501 - Current Recomputations - 1977 Amendments - 09/15/2025
+Batch run: 09/15/2025
+Rev:09/15/2025
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00605_560.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00605_560.html new file mode 100644 index 00000000..aa11b296 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00605_560.html @@ -0,0 +1,283 @@ + + +SSA - POMS: RS 00605.560 - Automatic Earnings Reappraisal Operation (AERO) - 08/02/1999 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 28 (08-99)

+

RS 00605.560 Automatic Earnings Reappraisal Operation (AERO) +

+ + +
+
+

A. Definition +

+
+
+

AERO is the process that automatically screens ERs that have changes in earnings information + and computes the necessary changes. +

+
+
+ +
+
+

B. Policy +

+
+
+

AERO follows all applicable policies and procedures when determining if a benefit + change is required. Refer to SM 04605.000 ff. for a complete explanation of AERO actions. +

+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300605560
RS 00605.560 - Automatic Earnings Reappraisal Operation (AERO) - 08/02/1999
+Batch run: 02/17/2026
+Rev:08/02/1999
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00605_570.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00605_570.html new file mode 100644 index 00000000..0ced07fa --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00605_570.html @@ -0,0 +1,334 @@ + + +SSA - POMS: RS 00605.570 - Processing Recomputation Requests - 08/26/2015 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 28 (08-99)

+

RS 00605.570 Processing Recomputation Requests +

+ + +
+
+

A. Procedure - Increase Due +

+
+
+

When a recomputation request is received:

+
    +
  • +
    +
    •
    +
    +

    Verify the posted earnings. A certified ER is not needed if the computation using + SEQY, DEQY, CIP, EC or ICF earnings matches the PIA shown on the MBR. +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Obtain evidence of lag earnings,

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Develop prelag earnings if there is a discrepancy, and

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Process the recomputation or recalculation without the folder using the interactive + computation facility (ICF) or CIP. +

    + +
    +
    +
  • +
+

DRAMS may be accessed for proven periods of military service if consistent with the + military service code on the MBR. If railroad earnings are used in the MBR PIA (RR=RCU), + those earnings can be obtained from the DEQY or EC. +

+
+
+

NOTE: Do not consider the filing of an annual report to be an implied request for a recomputation. +

+ +
+
+
+
+ +
+
+

B. Procedure - No Increase Due +

+
+
+

When an increase is not due, a notice is required. See NL 00703.913 for the notice to be used. the “No Increase” notice can be automatically generated + via CIP also. +

+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300605570
RS 00605.570 - Processing Recomputation Requests - 08/26/2015
+Batch run: 08/26/2015
+Rev:08/26/2015
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00605_580.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00605_580.html new file mode 100644 index 00000000..3308db16 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00605_580.html @@ -0,0 +1,364 @@ + + +SSA - POMS: RS 00605.580 - Recalculation of Benefits - 08/02/1999 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 28 (08-99)

+

RS 00605.580 Recalculation of Benefits +

+ + +
+
+

Policy +

+
+
+

When a prior computation is incorrect, any necessary recalculation is made under the + rules applicable to the computation being corrected. Recalculations are made when + allowed under the rules of administrative finality. +

+
+
+

1. When Recalculations Are Required +

+
+
+

Recalculations are required whenever earnings within the original base years are detected + and those earnings were not used in the initial computation. Those earnings could + be: +

+
    +
  • +
    +
    •
    +
    +

    Lag earnings;

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    State and local retroactive coverage;

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Partnership income in the year of death;

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    WWII Japanese internment deemed wage credits;

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    MS wage credits; and

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Earnings added to the record.

    + +
    +
    +
  • +
+
+
+
+
+

2. Fiscal Year Recalculation +

+
+
+

AERO will detect cases where, at the time of adjudication, evidence of self-employment + income (SEI) was not available for a taxable year that includes part of the last base + year and the next. When a larger PIA results, prepare an amended award to include + that part of the SEI credited in or before the last base year or before the closing + date. +

+
+
+
+
+

3. Japanese Internee's Deemed Wage Recalculations +

+
+
+

For instructions on Japanese internee's deemed wage recalculations, see RS 01404.000 ff. +

+
+
+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300605580
RS 00605.580 - Recalculation of Benefits - 08/02/1999
+Batch run: 03/20/2026
+Rev:08/02/1999
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00615_003.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_003.html new file mode 100644 index 00000000..58fc40d3 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_003.html @@ -0,0 +1,662 @@ + + +SSA - POMS: RS 00615.003 - Full Retirement Age - 05/21/2004 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 24 (09-02)

+

RS 00615.003 Full Retirement Age +

+ + +

Full retirement age (FRA) is the age at which unreduced retirement (old-age), spouse's, + or widow(er)'s benefits may be paid. +

+ +
+
+

A. POLICY - RETIREMENT AND SPOUSE'S BENEFITS +

+
+
+
+
+

1. People Born 1/1/38 or Earlier +

+
+
+

FRA for retirement or spouse's benefits is age 65.

+
+
+
+
+

2. People Born 1/2/38 or Later +

+
+
+

Use the following chart to determine FRA for retirement or spouse's benefits:

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
If the Birth Date is...The FRA is...
1/2/38 thru 1/1/39 +

65 years and 2 months

+ +
+

1/2/39 thru 1/1/40

+ +
65 years and 4 months
1/2/40 thru 1/1/41 +

65 years and 6 months

+ +
1/2/41 thru 1/1/4265 years and 8 months
+

1/2/42 thru 1/1/43

+ +
+

65 years and 10 months

+ +
+

1/2/43 thru 1/1/55

+ +
+

66 years

+ +
+

1/2/55 thru 1/1/56

+ +
+

66 years and 2 months

+ +
+

1/2/56 thru 1/1/57

+ +
+

66 years and 4 months

+ +
+

1/2/57 thru 1/1/58

+ +
+

66 years and 6 months

+ +
+

1/2/58 thru 1/1/59

+ +
+

66 years and 8 months

+ +
+

1/2/59 thru 1/1/60

+ +
66 years and 10 months
+

1/2/60 and later

+ +
+

67 years

+ +
+
+
+
+
+
+ +
+
+

B. POLICY - WIDOW(ER)'S BENEFITS +

+
+
+
+
+

1. People Born Before 1/2/1911 +

+
+
+

FRA for widow(er)'s benefits was age 62

+
+
+
+
+

2. People Born from 1/2/1911 Through 1/1/40 +

+
+
+

FRA for widow(er)'s benefits was age 65.

+
+
+
+
+

3. People Born 1/2/40 or Later +

+
+
+

FRA for widow(er)'s benefits gradually increases from age 65 and 2 months to age 67. + Use the following chart to determine FRA for widow(er)'s benefits: +

+

 

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
If the Birth Date is...Then FRA is...
+

1/2/40 thru 1/1/41

+ +
+

65 years and 2 months

+ +
+

1/2/41 thru 1/1/42

+ +
+

65 years and 4 months

+ +
+

1/2/42 thru 1/1/43

+ +
+

65 years and 6 months

+ +
+

1/2/43 thru 1/1/44

+ +
+

65 years and 8 months

+ +
+

1/2/44 thru 1/1/45

+ +
+

65 years and 10 months

+ +
+

1/2/45 thru 1/1/57

+ +
66 years
+

1/2/57 thru 1/1/58

+ +
+

66 years and 2 months

+ +
+

1/2/58 thru 1/1/59

+ +
+

66 years and 4 months

+ +
+

1/2/59 thru 1/1/60

+ +
+

66 years and 6 months

+ +
+

1/2/60 thru 1/1/61

+ +
+

66 years and 8 months

+ +
+

1/2/61 thru 1/1/62

+ +
+

66 years and 10 months

+ +
+

1/2/62 and later

+ +
+

67 years

+ +
+
+
+
+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300615003
RS 00615.003 - Full Retirement Age - 05/21/2004
+Batch run: 02/21/2013
+Rev:05/21/2004
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00615_004.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_004.html new file mode 100644 index 00000000..a9b7739d --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_004.html @@ -0,0 +1,5106 @@ + + +SSA - POMS: RS 00615.004 - Attainment of Full Retirement Age - 09/16/2002 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 24 (09-02)

+

RS 00615.004 Attainment of Full Retirement Age +

+ + +
+
+

POLICY +

+
+
+
+
+

1. Attainment of FRA for Those Born Before 1/2/1938 +

+
+
+

For those born before 1/2/1938, FRA for retirement, spouse's or widow(er)'s benefits + is age 65. Attainment of FRA always occurs in the same month for all possible types + of benefits. +

+

EXCEPTION: FRA for widow(er)'s benefits for those born before 1/2/1911 was age 62. +

+
+
+
+
+

2. Attainment of FRA for Those Born After 1/1/1962 +

+
+
+

For those born after 1/1/62, FRA for retirement, spouse's or widow(er)'s benefits + will be age 67. Attainment of FRA will always occur in the same month for all possible + types of benefits. +

+
+
+
+
+

3. Attainment of FRA for Those Born from 1/2/1938 through 1/1/1962 +

+
+
+

For those born after 1/1/1938 and before 1/2/1962, FRA for retirement, spouse's and + widow(er)'s benefits increases incrementally from age 65 to age 67. Attainment of + FRA for retirement and spouse's benefits will occur at a later date than attainment + of FRA for widow(er)'s benefits. +

+

Use this chart to determine when a person will attain FRA for each type of benefit. + To use the chart, find the row for the claimant's date of birth. Follow that row across + to find the FRA for widow(er)'s benefits and for retirement or spouse's benefits. + Continue to follow the same row to find the month of attainment of FRA for each type + of benefit. +

+

 

+

 

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Date of Birth +

Full Retirement Age

+ +
Attainment of FRA
+

+ +
+

for WIB is

+ +
+

for RIB or Spouse is

+ +
+

for WIB is

+ +
+

for RIB or Spouse is

+ +
+

1/2/38-2/1/38

+ +
+

65

+ +
+

65 & 2

+ +
+

1/2003

+ +
+

3/2003

+ +
+

2/2/38-3/1/38

+ +
+

65

+ +
+

65 & 2

+ +
+

2/2003

+ +
+

4/2003

+ +
+

3/2/38-4/1/38

+ +
+

65

+ +
+

65 & 2

+ +
+

3/2003

+ +
+

5/2003

+ +
+

4/2/38-5/1/38

+ +
+

65

+ +
+

65 & 2

+ +
+

4/2003

+ +
+

6/2003

+ +
+

5/2/38-6/1/38

+ +
+

65

+ +
+

65 & 2

+ +
+

5/2003

+ +
+

7/2003

+ +
+

6/2/38-7/1/38

+ +
+

65

+ +
+

65 & 2

+ +
+

6/2003

+ +
+

8/2003

+ +
+

7/2/38-8/1/38

+ +
+

65

+ +
+

65 & 2

+ +
+

7/2003

+ +
+

9/2003

+ +
+

8/2/38-9/1/38

+ +
+

65

+ +
+

65 & 2

+ +
+

8/2003

+ +
+

10/2003

+ +
+

9/2/38-10/1/38

+ +
+

65

+ +
+

65 & 2

+ +
+

9/2003

+ +
+

11/2003

+ +
+

10/2/38-11/1/38

+ +
+

65

+ +
+

65 & 2

+ +
+

10/2003

+ +
+

12/2003

+ +
+

11/2/38-12/1/38

+ +
+

65

+ +
+

65 & 2

+ +
+

11/2003

+ +
+

1/2004

+ +
+

12/2/38-1/1/39

+ +
+

65

+ +
+

65 & 2

+ +
+

12/2003

+ +
+

2/2004

+ +
+

1/2/39-2/1/39

+ +
+

65

+ +
+

65 & 4

+ +
+

1/2004

+ +
+

5/2004

+ +
+

2/2/39-3/1/39

+ +
+

65

+ +
+

65 & 4

+ +
+

2/2004

+ +
+

6/2004

+ +
+

3/2/39-4/1/39

+ +
+

65

+ +
+

65 & 4

+ +
+

3/2004

+ +
+

7/2004

+ +
+

4/2/39-5/1/39

+ +
+

65

+ +
+

65 & 4

+ +
+

4/2004

+ +
+

8/2004

+ +
+

5/2/39-6/1/39

+ +
+

65

+ +
+

65 & 4

+ +
+

5/2004

+ +
+

9/2004

+ +
+

6/2/39-7/1/39

+ +
+

65

+ +
+

65 & 4

+ +
+

6/2004

+ +
+

10/2004

+ +
+

7/2/39-8/1/39

+ +
+

65

+ +
+

65 & 4

+ +
+

7/2004

+ +
+

11/2004

+ +
+

8/2/39-9/1/39

+ +
+

65

+ +
+

65 & 4

+ +
+

8/2004

+ +
+

12/2004

+ +
+

9/2/39-10/1/39

+ +
+

65

+ +
+

65 & 4

+ +
+

9/2004

+ +
+

1/2005

+ +
+

10/2/39-11/1/39

+ +
+

65

+ +
+

65 & 4

+ +
+

10/2004

+ +
+

2/2005

+ +
+

11/2/39-12/1/39

+ +
+

65

+ +
+

65 & 4

+ +
+

11/2004

+ +
+

3/2005

+ +
+

12/2/39-1/1/40

+ +
+

65

+ +
+

65 & 4

+ +
+

12/2004

+ +
+

4/2005

+ +
+

1/2/40-2/1/40

+ +
+

65 & 2

+ +
+

65 & 6

+ +
+

3/2005

+ +
+

7/2005

+ +
+

2/2/40-3/1/40

+ +
+

65 & 2

+ +
+

65 & 6

+ +
+

4/2005

+ +
+

8/2005

+ +
+

3/2/40-4/1/40

+ +
+

65 & 2

+ +
+

65 & 6

+ +
+

5/2005

+ +
+

9/2005

+ +
+

4/2/40-5/1/40

+ +
+

65 & 2

+ +
+

65 & 6

+ +
+

6/2005

+ +
+

10/2005

+ +
+

5/2/40-6/1/40

+ +
+

65 & 2

+ +
+

65 & 6

+ +
+

7/2005

+ +
+

11/2005

+ +
+

6/2/40-7/1/40

+ +
+

65 & 2

+ +
+

65 & 6

+ +
+

8/2005

+ +
+

12/2005

+ +
+

7/2/40-8/1/40

+ +
+

65 & 2

+ +
+

65 & 6

+ +
+

9/2005

+ +
+

1/2006

+ +
+

8/2/40-9/1/40

+ +
+

65 & 2

+ +
+

65 & 6

+ +
+

10/2005

+ +
+

2/2006

+ +
+

9/2/40-10/1/40

+ +
+

65 & 2

+ +
+

65 & 6

+ +
+

11/2005

+ +
+

3/2006

+ +
+

10/2/40-11/1/40

+ +
+

65 & 2

+ +
+

65 & 6

+ +
+

12/2005

+ +
+

4/2006

+ +
+

11/2/40-12/1/40

+ +
+

65 & 2

+ +
+

65 & 6

+ +
+

1/2006

+ +
+

5/2006

+ +
+

12/2/40-1/1/41

+ +
+

65 & 2

+ +
+

65 & 6

+ +
+

2/2006

+ +
+

6/2006

+ +
+

1/2/41-2/1/41

+ +
+

65 & 4

+ +
+

65 & 8

+ +
+

5/2006

+ +
+

9/2006

+ +
+

2/2/41-3/1/41

+ +
+

65 & 4

+ +
+

65 & 8

+ +
+

6/2006

+ +
+

10/2006

+ +
+

3/2/41-4/1/41

+ +
+

65 & 4

+ +
+

65 & 8

+ +
+

7/2006

+ +
+

11/2006

+ +
+

4/2/41-5/1/41

+ +
+

65 & 4

+ +
+

65 & 8

+ +
+

8/2006

+ +
+

12/2006

+ +
+

5/2/41-6/1/41

+ +
+

65 & 4

+ +
+

65 & 8

+ +
+

9/2006

+ +
+

1/2007

+ +
+

6/2/41-7/1/41

+ +
+

65 & 4

+ +
+

65 & 8

+ +
+

10/2006

+ +
+

2/2007

+ +
+

7/2/41-8/1/41

+ +
+

65 & 4

+ +
+

65 & 8

+ +
+

11/2006

+ +
+

3/2007

+ +
+

8/2/41-9/1/41

+ +
+

65 & 4

+ +
+

65 & 8

+ +
+

12/2006

+ +
+

4/2007

+ +
+

9/2/41-10/1/41

+ +
+

65 & 4

+ +
+

65 & 8

+ +
+

1/2007

+ +
+

5/2007

+ +
+

10/2/41-11/1/41

+ +
+

65 & 4

+ +
+

65 & 8

+ +
+

2/2007

+ +
+

6/2007

+ +
+

11/2/41-12/1/41

+ +
+

65 & 4

+ +
+

65 & 8

+ +
+

3/2007

+ +
+

7/2007

+ +
+

12/2/41-1/1/42

+ +
+

65 & 4

+ +
+

65 & 8

+ +
+

4/2007

+ +
+

8/2007

+ +
+

1/2/42-2/1/42

+ +
+

65 & 6

+ +
+

65 & 10

+ +
+

7/2007

+ +
+

11/2007

+ +
+

2/2/42-3/1/42

+ +
+

65 & 6

+ +
+

65 & 10

+ +
+

8/2007

+ +
+

12/2007

+ +
+

3/2/42-4/1/42

+ +
+

65 & 6

+ +
+

65 & 10

+ +
+

9/2007

+ +
+

1/2008

+ +
+

4/2/42-5/1/42

+ +
+

65 & 6

+ +
+

65 & 10

+ +
+

10/2007

+ +
+

2/2008

+ +
+

5/2/42-6/1/42

+ +
+

65 & 6

+ +
+

65 & 10

+ +
+

11/2007

+ +
+

3/2008

+ +
+

6/2/42-7/1/42

+ +
+

65 & 6

+ +
+

65 & 10

+ +
+

12/2007

+ +
+

4/2008

+ +
+

7/2/42-8/1/42

+ +
+

65 & 6

+ +
+

65 & 10

+ +
+

1/2008

+ +
+

5/2008

+ +
+

8/2/42-9/1/42

+ +
+

65 & 6

+ +
+

65 & 10

+ +
+

2/2008

+ +
+

6/2008

+ +
+

9/2/42-10/1/42

+ +
+

65 & 6

+ +
+

65 & 10

+ +
+

3/2008

+ +
+

7/2008

+ +
+

10/2/42-11/1/42

+ +
+

65 & 6

+ +
+

65 & 10

+ +
+

4/2008

+ +
+

8/2008

+ +
+

11/2/42-12/1/42

+ +
+

65 & 6

+ +
+

65 & 10

+ +
+

5/2008

+ +
+

9/2008

+ +
+

12/2/42-1/1/43

+ +
+

65 & 6

+ +
+

65 & 10

+ +
+

6/2008

+ +
+

10/2008

+ +
+

1/2/43-2/1/43

+ +
+

65 & 8

+ +
+

66

+ +
+

9/2008

+ +
+

1/2009

+ +
+

2/2/43-3/1/43

+ +
+

65 & 8

+ +
+

66

+ +
+

10/2008

+ +
+

2/2009

+ +
+

3/2/43-4/1/43

+ +
+

65 & 8

+ +
+

66

+ +
+

11/2008

+ +
+

3/2009

+ +
+

4/2/43-5/1/43

+ +
+

65 & 8

+ +
+

66

+ +
+

12/2008

+ +
+

4/2009

+ +
+

5/2/43-6/1/43

+ +
+

65 & 8

+ +
+

66

+ +
+

1/2009

+ +
+

5/2009

+ +
+

6/2/43-7/1/43

+ +
+

65 & 8

+ +
+

66

+ +
+

2/2009

+ +
+

6/2009

+ +
+

7/2/43-8/1/43

+ +
+

65 & 8

+ +
+

66

+ +
+

3/2009

+ +
+

7/2009

+ +
+

8/2/43-9/1/43

+ +
+

65 & 8

+ +
+

66

+ +
+

4/2009

+ +
+

8/2009

+ +
+

9/2/43-10/1/43

+ +
+

65 & 8

+ +
+

66

+ +
+

5/2009

+ +
+

9/2009

+ +
+

10/2/43-11/1/43

+ +
+

65 & 8

+ +
+

66

+ +
+

6/2009

+ +
+

10/2009

+ +
+

11/2/43-12/1/43

+ +
+

65 & 8

+ +
+

66

+ +
+

7/2009

+ +
+

11/2009

+ +
+

12/2/43-1/1/44

+ +
+

65 & 8

+ +
+

66

+ +
+

8/2009

+ +
+

12/2009

+ +
+

1/2/44-2/1/44

+ +
+

65 & 10

+ +
+

66

+ +
+

11/2009

+ +
+

1/2010

+ +
+

2/2/44-3/1/44

+ +
+

65 & 10

+ +
+

66

+ +
+

12/2009

+ +
+

2/2010

+ +
+

3/2/44-4/1/44

+ +
+

65 & 10

+ +
+

66

+ +
+

1/2010

+ +
+

3/2010

+ +
+

4/2/44-5/1/44

+ +
+

65 & 10

+ +
+

66

+ +
+

2/2010

+ +
+

4/2010

+ +
+

5/2/44-6/1/44

+ +
+

65 & 10

+ +
+

66

+ +
+

3/2010

+ +
+

5/2010

+ +
+

6/2/44-7/1/44

+ +
+

65 & 10

+ +
+

66

+ +
+

4/2010

+ +
+

6/2010

+ +
+

7/2/44-8/1/44

+ +
+

65 & 10

+ +
+

66

+ +
+

5/2010

+ +
+

7/2010

+ +
+

8/2/44-9/1/44

+ +
+

65 & 10

+ +
+

66

+ +
+

6/2010

+ +
+

8/2010

+ +
+

9/2/44-10/1/44

+ +
+

65 & 10

+ +
+

66

+ +
+

7/2010

+ +
+

9/2010

+ +
+

10/2/44-11/1/44

+ +
+

65 & 10

+ +
+

66

+ +
+

8/2010

+ +
+

10/2010

+ +
+

11/2/44-12/1/44

+ +
+

65 & 10

+ +
+

66

+ +
+

9/2010

+ +
+

11/2010

+ +
+

12/2/44-1/1/45

+ +
+

65 & 10

+ +
+

66

+ +
+

10/2010

+ +
+

12/2010

+ +
+

1/2/45-1/1/55

+ +
+

Claimants born during this period will attain FRA for all benefits in the month of + attainment of age 66. +

+ +
+

1/2/55-2/1/55

+ +
+

66

+ +
+

66 & 2

+ +
+

1/2021

+ +
+

3/2021

+ +
+

2/2/55-3/1/55

+ +
+

66

+ +
+

66 & 2

+ +
+

2/2021

+ +
+

4/2021

+ +
+

3/2/55-4/1/55

+ +
+

66

+ +
+

66 & 2

+ +
+

3/2021

+ +
+

5/2021

+ +
+

4/2/55-5/1/55

+ +
+

66

+ +
+

66 & 2

+ +
+

4/2021

+ +
+

6/2021

+ +
+

5/2/55-6/1/55

+ +
+

66

+ +
+

66 & 2

+ +
+

5/2021

+ +
+

7/2021

+ +
+

6/2/55-7/1/55

+ +
+

66

+ +
+

66 & 2

+ +
+

6/2021

+ +
+

8/2021

+ +
+

7/2/55-8/1/55

+ +
+

66

+ +
+

66 & 2

+ +
+

7/2021

+ +
+

9/2021

+ +
+

8/2/55-9/1/55

+ +
+

66

+ +
+

66 & 2

+ +
+

8/2021

+ +
+

10/2021

+ +
+

9/2/55-10/1/55

+ +
+

66

+ +
+

66 & 2

+ +
+

9/2021

+ +
+

11/2021

+ +
+

10/2/55-11/1/55

+ +
+

66

+ +
+

66 & 2

+ +
+

10/2021

+ +
+

12/2021

+ +
+

11/2/55-12/1/55

+ +
+

66

+ +
+

66 & 2

+ +
+

11/2021

+ +
+

1/2022

+ +
+

12/2/55-1/1/56

+ +
+

66

+ +
+

66 & 2

+ +
+

12/2021

+ +
+

2/2022

+ +
+

1/2/56-2/1/56

+ +
+

66

+ +
+

66 & 4

+ +
+

1/2022

+ +
+

5/2022

+ +
+

2/2/56-3/1/56

+ +
+

66

+ +
+

66 & 4

+ +
+

2/2022

+ +
+

6/2022

+ +
+

3/2/56-4/1/56

+ +
+

66

+ +
+

66 & 4

+ +
+

3/2022

+ +
+

7/2022

+ +
+

4/2/56-5/1/56

+ +
+

66

+ +
+

66 & 4

+ +
+

4/2022

+ +
+

8/2022

+ +
+

5/2/56-6/1/56

+ +
+

66

+ +
+

66 & 4

+ +
+

5/2022

+ +
+

9/2022

+ +
+

6/2/56-7/1/56

+ +
+

66

+ +
+

66 & 4

+ +
+

6/2022

+ +
+

10/2022

+ +
+

7/2/56-8/1/56

+ +
+

66

+ +
+

66 & 4

+ +
+

7/2022

+ +
+

11/2022

+ +
+

8/2/56-9/1/56

+ +
+

66

+ +
+

66 & 4

+ +
+

8/2022

+ +
+

12/2022

+ +
+

9/2/56-10/1/56

+ +
+

66

+ +
+

66 & 4

+ +
+

9/2022

+ +
+

1/2023

+ +
+

10/2/56-11/1/56

+ +
+

66

+ +
+

66 & 4

+ +
+

10/2022

+ +
+

2/2023

+ +
+

11/2/56-12/1/56

+ +
+

66

+ +
+

66 & 4

+ +
+

11/2022

+ +
+

3/2023

+ +
+

12/2/56-1/1/57

+ +
+

66

+ +
+

66 & 4

+ +
+

12/2022

+ +
+

4/2023

+ +
+

1/2/57-2/1/57

+ +
+

66 & 2

+ +
+

66 & 6

+ +
+

3/2023

+ +
+

7/2023

+ +
+

2/2/57-3/1/57

+ +
+

66 & 2

+ +
+

66 & 6

+ +
+

4/2023

+ +
+

8/2023

+ +
+

3/2/57-4/1/57

+ +
+

66 & 2

+ +
+

66 & 6

+ +
+

5/2023

+ +
+

9/2023

+ +
+

4/2/57-5/1/57

+ +
+

66 & 2

+ +
+

66 & 6

+ +
+

6/2023

+ +
+

10/2023

+ +
+

5/2/57-6/1/57

+ +
+

66 & 2

+ +
+

66 & 6

+ +
+

7/2023

+ +
+

11/2023

+ +
+

6/2/57-7/1/57

+ +
+

66 & 2

+ +
+

66 & 6

+ +
+

8/2023

+ +
+

12/2023

+ +
+

7/2/57-8/1/57

+ +
+

66 & 2

+ +
+

66 & 6

+ +
+

9/2023

+ +
+

1/2024

+ +
+

8/2/57-9/1/57

+ +
+

66 & 2

+ +
+

66 & 6

+ +
+

10/2023

+ +
+

2/2024

+ +
+

9/2/57-10/1/57

+ +
+

66 & 2

+ +
+

66 & 6

+ +
+

11/2023

+ +
+

3/2024

+ +
+

10/2/57-11/1/57

+ +
+

66 & 2

+ +
+

66 & 6

+ +
+

12/2023

+ +
+

4/2024

+ +
+

11/2/57-12/1/57

+ +
+

66 & 2

+ +
+

66 & 6

+ +
+

1/2024

+ +
+

5/2024

+ +
+

12/2/57-1/1/58

+ +
+

66 & 2

+ +
+

66 & 6

+ +
+

2/2024

+ +
+

6/2024

+ +
+

1/2/58-2/1/58

+ +
+

66 & 4

+ +
+

66 & 8

+ +
+

5/2024

+ +
+

9/2024

+ +
+

2/2/58-3/1/58

+ +
+

66 & 4

+ +
+

66 & 8

+ +
+

6/2024

+ +
+

10/2024

+ +
+

3/2/58-4/1/58

+ +
+

66 & 4

+ +
+

66 & 8

+ +
+

7/2024

+ +
+

11/2024

+ +
+

4/2/58-5/1/58

+ +
+

66 & 4

+ +
+

66 & 8

+ +
+

8/2024

+ +
+

12/2024

+ +
+

5/2/58-6/1/58

+ +
+

66 & 4

+ +
+

66 & 8

+ +
+

9/2024

+ +
+

1/2025

+ +
+

6/2/58-7/1/58

+ +
+

66 & 4

+ +
+

66 & 8

+ +
+

10/2024

+ +
+

2/2025

+ +
+

7/2/58-8/1/58

+ +
+

66 & 4

+ +
+

66 & 8

+ +
+

11/2024

+ +
+

3/2025

+ +
+

8/2/58-9/1/58

+ +
+

66 & 4

+ +
+

66 & 8

+ +
+

12/2024

+ +
+

4/2025

+ +
+

9/2/58-10/1/58

+ +
+

66 & 4

+ +
+

66 & 8

+ +
+

1/2025

+ +
+

5/2025

+ +
+

10/2/58-11/1/58

+ +
+

66 & 4

+ +
+

66 & 8

+ +
+

2/2025

+ +
+

6/2025

+ +
+

11/2/58-12/1/58

+ +
+

66 & 4

+ +
+

66 & 8

+ +
+

3/2025

+ +
+

7/2025

+ +
+

12/2/58-1/1/59

+ +
+

66 & 4

+ +
+

66 & 8

+ +
+

4/2025

+ +
+

8/2025

+ +
+

1/2/59-2/1/59

+ +
+

66 & 6

+ +
+

66 & 10

+ +
+

7/2025

+ +
+

11/2025

+ +
+

2/2/59-3/1/59

+ +
+

66 & 6

+ +
+

66 & 10

+ +
+

8/2025

+ +
+

12/2025

+ +
+

3/2/59-4/1/59

+ +
+

66 & 6

+ +
+

66 & 10

+ +
+

9/2025

+ +
+

1/2026

+ +
+

4/2/59-5/1/59

+ +
+

66 & 6

+ +
+

66 & 10

+ +
+

10/2025

+ +
+

2/2026

+ +
+

5/2/59-6/1/59

+ +
+

66 & 6

+ +
+

66 & 10

+ +
+

11/2025

+ +
+

3/2026

+ +
+

6/2/59-7/1/59

+ +
+

66 & 6

+ +
+

66 & 10

+ +
+

12/2025

+ +
+

4/2026

+ +
+

7/2/59-8/1/59

+ +
+

66 & 6

+ +
+

66 & 10

+ +
+

1/2026

+ +
+

5/2026

+ +
+

8/2/59-9/1/59

+ +
+

66 & 6

+ +
+

66 & 10

+ +
+

2/2026

+ +
+

6/2026

+ +
+

9/2/59-10/1/59

+ +
+

66 & 6

+ +
+

66 & 10

+ +
+

3/2026

+ +
+

7/2026

+ +
+

10/2/59-11/1/59

+ +
+

66 & 6

+ +
+

66 & 10

+ +
+

4/2026

+ +
+

8/2026

+ +
+

11/2/59-12/1/59

+ +
+

66 & 6

+ +
+

66 & 10

+ +
+

5/2026

+ +
+

9/2026

+ +
+

12/2/59-1/1/60

+ +
+

66 & 6

+ +
+

66 & 10

+ +
+

6/2026

+ +
+

10/2026

+ +
+

1/2/60-2/1/60

+ +
+

66 & 8

+ +
+

67

+ +
+

9/2026

+ +
+

1/2027

+ +
+

2/2/60-3/1/60

+ +
+

66 & 8

+ +
+

67

+ +
+

10/2026

+ +
+

2/2027

+ +
+

3/2/60-4/1/60

+ +
+

66 & 8

+ +
+

67

+ +
+

11/2026

+ +
+

3/2027

+ +
+

4/2/60-5/1/60

+ +
+

66 & 8

+ +
+

67

+ +
+

12/2026

+ +
+

4/2027

+ +
+

5/2/60-6/1/60

+ +
+

66 & 8

+ +
+

67

+ +
+

1/2027

+ +
+

5/2027

+ +
+

6/2/60-7/1/60

+ +
+

66 & 8

+ +
+

67

+ +
+

2/2027

+ +
+

6/2027

+ +
+

7/2/60-8/1/60

+ +
+

66 & 8

+ +
+

67

+ +
+

3/2027

+ +
+

7/2027

+ +
+

8/2/60-9/1/60

+ +
+

66 & 8

+ +
+

67

+ +
+

4/2027

+ +
+

8/2027

+ +
+

9/2/60-10/1/60

+ +
+

66 & 8

+ +
+

67

+ +
+

5/2027

+ +
+

9/2027

+ +
+

10/2/60-11/1/60

+ +
+

66 & 8

+ +
+

67

+ +
+

6/2027

+ +
+

10/2027

+ +
+

11/2/60-12/1/60

+ +
+

66 & 8

+ +
+

67

+ +
+

7/2027

+ +
+

11/2027

+ +
+

12/2/60-1/1/61

+ +
+

66 & 8

+ +
+

67

+ +
+

8/2027

+ +
+

12/2027

+ +
+

1/2/61-2/1/61

+ +
+

66 & 10

+ +
+

67

+ +
+

11/2027

+ +
+

1/2028

+ +
+

2/2/61-3/1/61

+ +
+

66 & 10

+ +
+

67

+ +
+

12/2027

+ +
+

2/2028

+ +
+

3/2/61-4/1/61

+ +
+

66 & 10

+ +
+

67

+ +
+

1/2028

+ +
+

3/2028

+ +
+

4/2/61-5/1/61

+ +
+

66 & 10

+ +
+

67

+ +
+

2/2028

+ +
+

4/2028

+ +
+

5/2/61-6/1/61

+ +
+

66 & 10

+ +
+

67

+ +
+

3/2028

+ +
+

5/2028

+ +
+

6/2/61-7/1/61

+ +
+

66 & 10

+ +
+

67

+ +
+

4/2028

+ +
+

6/2028

+ +
+

7/2/61-8/1/61

+ +
+

66 & 10

+ +
+

67

+ +
+

5/2028

+ +
+

7/2028

+ +
+

8/2/61-9/1/61

+ +
+

66 & 10

+ +
+

67

+ +
+

6/2028

+ +
+

8/2028

+ +
+

9/2/61-10/1/61

+ +
+

66 & 10

+ +
+

67

+ +
+

7/2028

+ +
+

9/2028

+ +
+

10/2/61-11/1/61

+ +
+

66 & 10

+ +
+

67

+ +
+

8/2028

+ +
+

10/2028

+ +
+

11/2/61-12/1/61

+ +
+

66 & 10

+ +
+

67

+ +
+

9/2028

+ +
+

11/2028

+ +
+

12/2/61-1/1/62

+ +
+

66 & 10

+ +
+

67

+ +
+

10/2028

+ +
+

12/2028

+ +
+

1/2/62 or later

+ +
+

Beginning 1/2029, claimants born after 1/1/62 will attain FRA for all benefits in + the month of attainment of age 67. +

+ +
+
+
+
+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300615004
RS 00615.004 - Attainment of Full Retirement Age - 09/16/2002
+Batch run: 02/21/2013
+Rev:09/16/2002
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00615_010.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_010.html new file mode 100644 index 00000000..76121d43 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_010.html @@ -0,0 +1,704 @@ + + +SSA - POMS: RS 00615.010 - Chart on Reduced Benefits - 12/07/2023 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 50 (12-23)

+

RS 00615.010 Chart on Reduced Benefits +

+ + +

The following chart summarizes the method of reduction for different kinds of entitlement. + More specific information on each situation is located in the appropriate section + in this subchapter. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EntitlementMethod of Reduction
+

Retirement Insurance Benefit (RIB)

+ +
+

The RIB is equal to the PIA reduced by the number of months of entitlement to RIB + before full retirement age (FRA). See RS 00615 .101. +

+ +
+

RIB after reduced DIB

+ +
+

Reduce RIB by number of months remaining until FRA plus the number of months reduced + DIB was paid +

+ +
+

RIB after unreduced DIB (HA then A)

+ +
+

The DIB amount converts to a RIB at FRA with no reduction. If the DIB ends before + FRA, the RIB is reduced by the number of months remaining before FRA. +

+ +
+

Simultaneous Entitlement to DIB and RIB (HA & A)

+ +
+

Reduce the RIB based on number of months before FRA. The DIB is unreduced. Pay the + larger of the two benefits unless payment of the smaller benefit is elected. +

+ +
+

Simultaneous Entitlement to WIB and RIB (D & A)

+ +
+

Determine each benefit independent of the other. (RS 00615.150) +

+ +
+

RIB After Reduced WIB (D then A) with date of birth of 1/2/1928 or later

+ +
+

Reduce the RIB in the usual manner without regard to the WIB (see RS 00615.430). +

+ +
+

RIB After Reduced WIB (D then A) with date of birth before 1/2/1928

+ +
+

If there was prior entitlement to a reduced WIB in any month before the month of attainment + of age 62 reduce the RIB per RS 00615.160 and RS 00615.170. +

+ +
+

Reduced RIB After Reduced WIB (Intervening or simultaneous with DIB) (D then HA then + A, or D/HA then A) with date of birth of 1/2/1928 or later +

+ +
+

Reduce the RIB, See RS 00615.101, RS 00615.003, in the usual manner without regard to the WIB or DIB. +

+ +
+

Reduced RIB After Reduced WIB (Intervening or simultaneous with DIB) (D then HA then + A, or D/HA then A) with date of birth 1/2/1928 +

+ +
+

If there was prior entitlement to a reduced WIB in any month before the month of attainment + of age 62 reduce the RIB following the instructions in RS 00615.160, RS 00615.170 and RS 00615.020. +

+ +
+

Reduced RIB after reduced spouse (B then A)

+ +
+

RIB and Spouse are computed separately. RIB plus excess paid. See RS 00615.020 and RS 00615.003. +

+ +
+

RIB After Entitlement to any benefit other than reduced RIB, reduced spouse, reduced + WIB or reduced DIB. +

+ +
+

Reduce the RIB PIA in the usual manner without regard to the auxiliary benefit.

+ +
+

Disability Insurance Benefit (DIB)

+ +
+

DIB is not reduced unless there was prior entitlement to reduced RIB (or WIB prior + to 1/90). +

+ +
+

DIB After reduced RIB (A then HA)

+ +
+

Reduce the DIB PIA by the amount the RIB would be reduced if the NH attained FRA in + the first MOET to DIB. +

+ +
+

DIB After Entitlement to Reduced WIB (D then HA) (DIB MOET age 62 or later)

+ +
+

After 12/89 no carryover reduction applies to the DIB. Prior to 1/90 reduce the DIB + by the amount that the WIB would be reduced if retirement age were age 62. (RS 00615.420.) +

+ +

 

+ +
+

DIB After Entitlement to Reduced WIB (D then HA) (DIB MOET prior to age 62)

+ +
+

After 12/89 no carryover reduction applies to the DIB. Prior to 1/90 reduce the DIB + by the money amount the WIB would be reduced if the reduction applied only for months + of WIB entitlement before the first MOET to DIB. +

+ +
+

DIB After RIB Entitlement, Both After Reduced WIB (D then A then HA)

+ +
+

Reduce the DIB by the amount the RIB would be reduced if the NH attained FRA in the + first MOET to DIB. See RS 00615.110. +

+ +
+

Aged Spouse (B, B1, B6, BR)

+ +
+

Reduce the spouse's benefit (adjusted for the maximum if necessary) by the number + of months of entitlement before FRA. A spouse's benefit is not reduced for any month + in which the spouse or divorced spouse has an entitled child in care. If the entitled + child was in care in the MOET, a divorced spouse's benefit remains unreduced after + the child's benefit terminates. (RS 00615.201A.2.) +

+ +
+

RIB and Unreduced Spouse's Benefits (A, B2 or BY)

+ +
+

The spouse's benefit is unreduced for any month they have a child in care; however, + if they are also entitled to a reduced RIB, the amount payable is the difference between + the unreduced spouse's benefit and the reduced RIB. When entitlement to B2 or BY benefits + is established in the first month of entitlement to reduced RIB, the claimant may + later elect when they wish B (B1) benefits to begin. (The B (B1) benefits need not + immediately follow B2 (BY) entitlement.) The reduction factor if any, which will apply + to the B (B1) benefit will be governed by the age in the first MOET as an aged spouse. +

+ +
+

Reduced Spouse's Benefit Simultaneously With or after RIB (A/B or A then B)—See above + if spouse has a child in care. +

+ +
+

Reduce the excess spouse's benefit (the difference between the spouse's own PIA and + the unreduced spouse's benefit) by the number of months of entitlement to the spouse's + benefit, if any, before FRA. See RS 00615.020, RS 00615.003, and RS 00615.201. +

+ +
+

Reduced Spouse's and reduced DIB (HA/B) See above if spouse has a child in care

+ +
+

Reduce the spouse's benefit by (1) the reduction amount applicable to the DIB plus + (2) the amount the excess spouse's benefit would be reduced if it were the only benefit + to which the spouse is entitled. +

+ +
+

Widow(er)'s Insurance Benefit (WIB)

+ +
+

Beginning January 1973, reduce the WIB (adjusted for the maximum, if necessary) by + the number of months of entitlement, if any, prior to the attainment of age 65 by + 19/40 of 1 percent for months from age 60-65 and 43 /240 of 1 percent for months from + age 50-60. For benefits payable after 12/83, there is no additional reduction for + months prior to age 60. If the DNH was previously entitled to a reduced RIB or reduced + DIB, the WIB may be further reduced to the RIB or DIB the NH would have been entitled + to or, if higher, 82 1/2 percent of the death PIA. (See RS 00615.320.) +

+ +

For those who attain age 60 in or after 2000, the age at which unreduced benefits + may be received and the reduction amounts change. (See RS 00615.301). +

+ +
+

Sole Surviving Widow(er)

+ +
+

$84.50 reduced for months of entitlement, if any, prior to age 62 or, if higher, the + WIB computed without regard to this sole survivor's provision (see RS 00615.650). The sole survivor's rate was eliminated when NH died after 1981 and was not eligible + for RIB or DIB (see RS 00605.100). Prior to 1982, see RS 00605.022. +

+ +
+

Reduced WIB and DIB Simultaneously or DIB then WIB (D/HA or HA then D)

+ +
+

If in the first month of WIB entitlement the claimant is also entitled to a DIB, reduce + the excess WIB (the difference between the DIB PIA and the unreduced WIB benefit) + for the months of entitlement, if any, before FRA (or age 60 for calculations of benefits + payable prior to 1/84). See RS 00615.020. +

+ +
+

Widow(er) with Child in Care

+ +
+

Determine the WIB under the appropriate rules summarized above subject to the condition + that it shall not be less than the mother's or father's benefit would be for any month + the widow(er) has an entitled child in care (see RS 00615.340). +

+ +
+

Remarried Widow or Widower

+ +
+

Benefits for D4 and D5 are calculated same as for D. Before 1/79, the WIB is one-half + the PIA; (adjusted for the family maximum if necessary) or, if higher, the minimum + sole survivor rate if it applied. See RS 00615.714. +

+ +
+

Surviving Divorced Spouse

+ +
+

The WIB is equal to the PIA reduced for age but not adjusted for the family maximum. + The RIB limitation also may apply. (See RS 00615.320) +

+ +
+
+ +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300615010
RS 00615.010 - Chart on Reduced Benefits - 12/07/2023
+Batch run: 11/21/2025
+Rev:12/07/2023
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00615_015.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_015.html new file mode 100644 index 00000000..cadd6f5c --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_015.html @@ -0,0 +1,959 @@ + + +SSA - POMS: RS 00615.015 - How the Day of Birth Affects Benefits - 01/09/2004 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 40 (12-23)

+

RS 00615.015 How the Day of Birth Affects Benefits

+ + +
+
+

A. POLICY

+
+
+

The day of the month on which a person is born affects both when they can be entitled + as well as the amount of the benefit. Retirement benefits can begin the first month + a person is age 62 throughout the entire month. (See RS 00201.001C.) Social Security follows English common law that finds that a person attains an age + on the day before the birthday. (See GN 00302.400.) For example, a person born January 15, 2003 will attain age one on January 14, + 2004. Therefore, a person is age 62 throughout an entire month if the 62nd birthday + is on the first or second day of that month. Retirement benefits are reduced for each + month of entitlement (MOE) prior to the month full retirement age (FRA) is attained. + FRA ranges from age 65 for some people up to age 67, depending on the date of birth. + Retirement benefits are computed using two factors:

+
    +
  • +
    +
    •
    +
    +

    Earnings during the entire working career (RS 00605.018), and

    +
    +
    +
  • +
  • +
    +
    •
    +
    +

    The year of attainment of age 62 (RS 00605.005).

    +
    +
    +
  • +
+

NOTE: The throughout the month rule does not apply to the attainment of FRA. Unreduced + benefits are payable beginning with the month FRA is attained regardless of the day + of the month. For example, if the birth date is June 1, 1935, FRA is attained on May + 31, 2000 and unreduced benefits can begin with May 2000.

+
+
+ +
+
+

B. EXAMPLES

+
+
+

To show how these variables interact let us take four people born over a four-day + period through several examples.

+
+
+

1. Different Days of the Month

+
+
+

Anne was born 5/31/1935, Bill was born 6/1/1935, Cory was born 6/2/1935 and Demi was + born 6/3/1935. They all worked at the same job and had maximum earnings from 1961 + through 1991. They attain age 62 as follows: Anne on 5/30/1997, Bill on 5/31/1997, + Cory on 6/1/1997 and Demi on 6/2/1997. Anne, Bill and Cory can begin receiving benefits + 6/1997 (the first month they are age 62 throughout the entire month). Demi must wait + until 7/1997 because they are not age 62 throughout the entire month of 6/1997. FRA + for all four is age 65 so the number of reduction months (RF) varies. In 6/1997 Anne + has 35 RF, Bill has 35 RF and Cory has 36 RF. In 7/1997 Demi has 35 RF. Because all + four claimants have the same earnings and attain age 62 in the same year the full + (unreduced for age) benefit amount would be the same – $1207. After reduction for + entitlement before FRA, payments are made as follows:

+

Anne - $972 starting 6/1997

+

Bill - $972 starting 6/1997

+

Cory - $965 starting 6/1997

+

Demi - $972 starting 7/1997

+
+
+
+
+

2. Different Days of the Month and Different Years

+
+
+

Now lets change the four-day period. Anne was born 12/31/1935, Bill was born 1/1/1936, + Cory was born 1/2/1936 and Demi was born 1/3/1936. They all worked at the same job + and had maximum earnings from 1961 through 1991. They attain age 62 as follows: Anne + on 12/30/1997, Bill on 12/31/1997, Cory on 1/1/1998 and Demi on 1/2/1998. Anne, Bill + and Cory can begin receiving benefits 1/1998 (the first month they are age 62 throughout + the entire month). Demi must wait until 2/1998 because they are not age 62 throughout + the entire month of 1/1998. FRA for all four is age 65 so the number of reduction + months varies. In 1/1998 Anne has 35 RF, Bill has 35 RF and Cory has 36 RF. In 2/1998 + Demi has 35 RF. Although all four claimants have the same earnings they attain age + 62 in different years. The year of attainment of age 62 determines the indexing factors + and bend points used in computing the benefit amount. Anne and Bill who attain age + 62 in 1997 have a full unreduced benefit amount of $1232. Cory and Demi attain age + 62 in 1998 and have a full unreduced benefit amount of $1266. After reduction for + entitlement before FRA, payments are made as follows:

+

Anne - $992 starting 1/1998

+

Bill - $992 starting 1/1998

+

Cory - $1012 starting 1/1998

+

Demi - $1019 starting 2/1998

+
+
+
+
+

3. Different Days of the Month, Different Years and Different Full Retirement Ages

+
+
+

One last time with another four-day period. Anne was born 12/31/1937, Bill was born + 1/1/1938, Cory was born 1/2/1938 and Demi was born 1/3/1938. They all worked at the + same job and had maximum earnings from 1961 through 1991. They attain age 62 as follows: + Anne on 12/30/1999, Bill on 12/31/1999, Cory on 1/1/2000 and Demi on 1/2/2000. Anne, + Bill and Cory can begin receiving benefits 1/2000 (the first month they are age 62 + throughout the entire month). Demi must wait until 2/2000 because they are not age + 62 throughout the entire month of 1/2000. FRA for Anne and Bill is age 65. FRA for + Cory and Demi is age 65 and 2 months. The number of reduction months varies. In 1/2000 + Anne has 35 RF, Bill has 35 RF and Cory has 38 RF. In 2/1998 Demi has 37 RF. Although + all four claimants have the same earnings they attain age 62 in different years. The + year of attainment of age 62 determines the indexing factors and bend points used + in computing the benefit amount. Anne and Bill attain age 62 in 1999 and have a full + unreduced benefit amount of $1372. Cory and Demi attain age 62 in 2000 and have a + full unreduced benefit amount of $1410. After reduction for entitlement before FRA, + payments are made as follows:

+

Anne - $1105 starting 1/2000

+

Bill - $1105 starting 1/2000

+

Cory - $1116 starting 1/2000

+

Demi - $1122 starting 2/2000

+
+
+
+
+

4. Summary of the Three Scenarios to Show How the Different Birth Dates Affect the Benefit + Amounts and Payment Months:

+
+
+

Example One -

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Name

+
+

Birth Date

+
+

FRA

+
+

Full PIA

+
+

Age 62

+
+

1st MOE

+
+

RF

+
+

Benefit

+
+

Anne

+
+

5/31/35

+
+

65

+
+

1207

+
+

5/97

+
+

6/97

+
+

35

+
+

972

+
+

Bill

+
+

6/1/35

+
+

65

+
+

1207

+
+

5/97

+
+

6/97

+
+

35

+
+

972

+
+

Cory

+
+

6/2/35

+
+

65

+
+

1207

+
+

6/97

+
+

6/97

+
+

36

+
+

965

+
+

Demi

+
+

6/3/35

+
+

65

+
+

1207

+
+

6/97

+
+

7/97

+
+

35

+
+

972

+
+
+

Example Two -

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Name

+
+

Birth Date

+
+

FRA

+
+

Full PIA

+
+

Age 62

+
+

1st MOE

+
+

RF

+
+

Benefit

+
+

Anne

+
+

12/31/35

+
+

65

+
+

1232

+
+

12/97

+
+

1/98

+
+

35

+
+

992

+
+

Bill

+
+

1/1/36

+
+

65

+
+

1232

+
+

12/97

+
+

1/98

+
+

35

+
+

992

+
+

Cory

+
+

1/2/36

+
+

65

+
+

1266

+
+

1/98

+
+

1/98

+
+

36

+
+

1012

+
+

Demi

+
+

1/3/36

+
+

65

+
+

1266

+
+

1/98

+
+

2/98

+
+

35

+
+

1019

+
+
+

Example Three -

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Name

+
+

Birth Date

+
+

FRA

+
+

Full PIA

+
+

Age 62

+
+

1st MOE

+
+

RF

+
+

Benefit

+
+

Anne

+
+

12/31/37

+
+

65

+
+

1372

+
+

12/99

+
+

1/00

+
+

35

+
+

1105

+
+

Bill

+
+

1/1/38

+
+

65

+
+

1372

+
+

12/99

+
+

1/00

+
+

35

+
+

1105

+
+

Cory

+
+

1/2/38

+
+

65+2

+
+

1410

+
+

1/00

+
+

1/00

+
+

38

+
+

1116

+
+

Demi

+
+

1/3/38

+
+

65+2

+
+

1410

+
+

1/00

+
+

2/00

+
+

37

+
+

1122

+
+
+
+
+
+
+
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300615015
RS 00615.015 - How the Day of Birth Affects Benefits - 01/09/2004
+Batch run: 04/21/2026
+Rev:01/09/2004
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00615_480.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_480.html new file mode 100644 index 00000000..c07e6bed --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_480.html @@ -0,0 +1,325 @@ + + +SSA - POMS: RS 00615.480 - Reduction Factor Adjustment (ARF) - 09/16/2002 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 77 (09-24)

+

RS 00615.480 Reduction Factor Adjustment (ARF)

+ + +
+
+

POLICY

+
+
+
+
+

1. General

+
+
+

An adjustment of the reduction factor eliminates certain deduction and non-entitlement + months from the original reduction factor. This adjustment is automatic; i.e., no + application or request is needed.

+
+
+

Note: 

+
+
+ +

If a request is received, manually grant ARFs for years that have been closed out + per RS 02510.015.

+
+
+
+
+
+
+

2. Effective Dates

+
+
+
    +
  • +
    +
    •
    +
    +

    RIB FRA

    +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Spouse's Benefit FRA

    +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Widow(er)'s benefit age 62 and FRA

    +
    +
    +
  • +
  • +
    +
    •
    +
    +

    DIB benefits - The first month of entitlement or reentitlement to DIB.

    +
    +
    +
  • +
+
+
+
+
+
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300615480
RS 00615.480 - Reduction Factor Adjustment (ARF) - 09/16/2002
+Batch run: 05/21/2026
+Rev:09/16/2002
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00615_482.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_482.html new file mode 100644 index 00000000..5b4adb8b --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_482.html @@ -0,0 +1,593 @@ + + +SSA - POMS: RS 00615.482 - Requirements for Adjustment of Reduction Factor (ARF) &mdash; Crediting Months - 08/06/2024 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 76 (08-24)

+

RS 00615.482 Requirements for Adjustment of Reduction Factor (ARF) — Crediting Months +

+ + +
+
+

A. Eligibility requirement +

+
+
+

A beneficiary needs only one crediting month to qualify for an automatic ARF.

+

For an explanation of ARF processing, see TC 11001.120 and OS 01201.470.

+
+
+ +
+
+

B. Crediting months +

+
+
+
+
+

1. Retirement Insurance Benefits (RIB) cases +

+
+
+

Grant crediting months in RIB cases for months of:

+
    +
  • +
    +
    •
    +
    +

    full or partial work deduction; or

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    simultaneous RIB-Disability Insurance Benefit (DIB) entitlement.

    + +
    +
    +
  • +
+

NOTE: Proration of work deductions has no effect on the adjustment of the reduction factor, + as stated under RS 02501.120B.3.

+

For information regarding proration of work deductions, see RS 02501.120 and RS 02501.125. +

+

For information regarding charging excess earnings, see RS 02501.095. +

+
+
+
+
+

2. Spouse's cases +

+
+
+

Grant crediting months to a spouse for all months that the spouse:

+
    +
  • +
    +
    •
    +
    +

    is subject to a full or partial work deduction based on their work;

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    is subject to a full or partial work deduction based on the NH’s work (including months + for which the NH worked in which the full spouse’s benefit is paid after the benefits + for a partial month are prorated); +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    is not entitled because the NH's disability terminated;

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    is not entitled due to any terminating event (upon re-entitlement);

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    has a child in care;

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    is in non-payment due to their prior entitlement as a spouse (included in a divorced + spouse's reduction factor); +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    is in non-payment because the NH refused vocational rehabilitation (no longer applicable + as of January 1, 2001. For more information, see DI 55001.600); or +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    is in non-payment prior to their full retirement age (FRA) due to the NH’s voluntary + suspension request made on or after April 30, 2016. For more information, see GN 02409.110. +

    + +
    +
    +
  • +
+
+
+
+
+

3. Widow(er)’s cases +

+
+
+

Grant crediting months for a widow(er) in all months the:

+
    +
  • +
    +
    •
    +
    +

    widow(er) has full or partial work deduction;

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    widow(er) is not entitled because of a terminating event;

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    disabled widow(er) refused VR (no longer applicable as of January 1, 2001. For more + information, see DI 55001.600); or +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    widow(er)’s benefits were increased to the mother or father's rate.

    + +
    +
    +
  • +
+

NOTE: When a beneficiary is dually entitled to RIB and/or auxiliary, survivor benefits, + grant ARFs separately on each record. +

+
+
+
+
+

4. DIB cases +

+
+
+

Grant crediting months in DIB cases for any month the beneficiary was entitled to + a reduced RIB (see RS 00615.410) or WIB (see RS 00615.420) and for which an ARF of the other benefit would otherwise be appropriate. +

+
+
+
+
+ +
+
+

C. Manual preparation of full retirement age (FRA) diaries +

+
+
+

For instructions on how to establish diaries, see GN 01040.005. +

+
+
+

1. Cases requiring a manual diary at FRA +

+
+
+

The following require a manual diary to adjust the ARF at FRA:

+
    +
  • +
    +
    •
    +
    +

    Conversion of a reduced spouse's benefit to a young spouse's benefit with child in + care prior to FRA; +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Conversion of a widow(er)’s or disabled widow(er)’s benefit to a surviving mother’s + or father’s benefit prior to attainment of age 62 (and/or FRA); +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Disabled widow(er) in suspense prior to the attainment of age 62 (and/or FRA) for + refusing Vocational Rehabilitation (no longer applicable as of January 1, 2001. For + more information, see DI 55001.600); +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Re-entitlement of a widow(er)’s benefits prior to FRA who was previously entitled + as a widow(er) on the same record; +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Technically entitled widow(er) terminated for reason T5 (entitled to benefits equal + or larger) prior to re-entitlement and the work deduction occurred only on the survivor + record as of the month of termination. +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    DIB beneficiary becomes entitled to a reduced WIB. For more information, see RS 00615.350C.3.; or +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Spousal claims that were in non-payment due to the NH’s voluntary suspension request + made on or after April 30, 2016. For more information, see GN 02409.110. +

    + +
    +
    +
  • +
+
+
+
+
+

2. Manual diary due the year following FRA attainment +

+
+
+

If the NH requests proration of the work deduction, diary the NH’s claim for an adjustment + to the ARF in the year following FRA. For more information, see TC 11001.120. +

+
+
+
+
+

3. Diary due date +

+
+
+

The diary due date for the above cases is the month of FRA attainment unless there + are work deductions for the year of FRA attainment. For cases involving work deductions + for the year of FRA attainment, the diary due date is May of the year after attainment + of FRA. +

+
+
+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300615482
RS 00615.482 - Requirements for Adjustment of Reduction Factor (ARF) &mdash; Crediting Months - 08/06/2024
+Batch run: 12/14/2024
+Rev:08/06/2024
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00615_490.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_490.html new file mode 100644 index 00000000..fd850d9c --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_490.html @@ -0,0 +1,404 @@ + + +SSA - POMS: RS 00615.490 - Computing the RIB or Spouse Benefit (Single Entitlement) &mdash; Reduction Factor Adjustment - 09/16/2002 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 24 (09-02)

+

RS 00615.490 Computing the RIB or Spouse Benefit (Single Entitlement) — Reduction Factor Adjustment +

+ + +
+
+

A. POLICY +

+
+
+

These adjustments are effective at FRA.

+
+
+ +
+
+

B. PROCEDURE - COMPUTING THE ARF +

+
+
+
+
+

1. Initial Entitlement 1/78 or Later +

+
+
+
    +
  • +
    +
    •
    +
    +

    Determine crediting months.

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Subtract from original reduction factor.

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Apply result to PIA (or unreduced spouse's benefit).

    + +
    +
    +
  • +
+
+
+
+
+

2. Initial Entitlement Prior to 1/78-Reduction Factor Adjustment 1/78 or Later +

+
+
+
    +
  • +
    +
    •
    +
    +

    Subtract the MBA from the PIA or unreduced spouse's benefit = CMAR.

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Determine ORF and ARF.

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    NMAR = CMAR X ARF

    + +
    +
    +
  • +
+

          ORF

+
    +
  • +
    +
    •
    +
    +

    Subtract NMAR from the PIA or unreduced spouses benefit.

    + +
    +
    +
  • +
+
+
+
+
+

3. Reduction Factor Adjustment Prior to 1/78 +

+
+
+
    +
  • +
    +
    •
    +
    +

    Refigure the rates from the original entitlement date.

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Reduce each increase separately (see RS 00630.100). +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Subtract crediting months occurring from the date of increase to age 65 from the original + reduction factor. +

    + +
    +
    +
  • +
+
+
+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300615490
RS 00615.490 - Computing the RIB or Spouse Benefit (Single Entitlement) &mdash; Reduction Factor Adjustment - 09/16/2002
+Batch run: 02/21/2013
+Rev:09/16/2002
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00615_491.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_491.html new file mode 100644 index 00000000..98f6ebd8 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_491.html @@ -0,0 +1,314 @@ + + +SSA - POMS: RS 00615.491 - ARF Processing After 1977 for a Reentitled Actuarially Reduced Spouse - 12/06/2023 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 49 (12-23)

+

RS 00615.491 ARF Processing After 1977 for a Reentitled Actuarially Reduced Spouse +

+ + +
+
+

A. Policy - general +

+
+
+

Beginning in 1/78, if a reduced spouse is terminated and then reentitled, (months + in which the spouse's benefits were terminated will be eliminated from the reduction + factor when the spouse is reentitled. +

+
+
+ +
+
+

B. Policy - eligibility requirements +

+
+
+

When a spouse is reentitled after reduced spouse's benefits were terminated for the + following reasons all months of termination will be eliminated from the reduction + months: +

+
    +
  • +
    +
    •
    +
    +

    divorce occurred prior to 10th year of marriage; or

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    NH's DIB ceased; or

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    spouse entitled to a higher PIA on their own record.

    + +
    +
    +
  • +
+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300615491
RS 00615.491 - ARF Processing After 1977 for a Reentitled Actuarially Reduced Spouse - 12/06/2023
+Batch run: 12/06/2023
+Rev:12/06/2023
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00615_500.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_500.html new file mode 100644 index 00000000..8b51f93a --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_500.html @@ -0,0 +1,317 @@ + + +SSA - POMS: RS 00615.500 - Computing the Spouse's Benefit Reduction Factor Adjustment &mdash; Spouse also Entitled to RIB or DIB - 09/16/2002 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 24 (09-02)

+

RS 00615.500 Computing the Spouse's Benefit Reduction Factor Adjustment — Spouse also Entitled + to RIB or DIB +

+ + +
+
+

A. POLICY +

+
+
+
+
+

1. Entitled to Spouses Benefits ARF Before RIB or DIB Entitlement +

+
+
+

Entitlement to RIB or DIB does not affect the ARF for the spouse benefit. The adjustment + of the reduction factor for a spouse also entitled to RIB or DIB is the same as for + an individual entitled only to spouse's benefits. See RS 00615.490. +

+
+
+
+
+

2. RIB/DIB Concurrent or Preceding Spouse's Benefits +

+
+
+

The adjusted spouse reduction factor is applied to the excess spouse's benefit.

+
+
+
+
+ +
+
+

B. PROCEDURE - CALCULATION +

+
+
+
+
+

1. Unreduced B Excess +

+
+
+

Unreduced B - A(HA) PIA = unreduced B excess.

+
+
+
+
+

2. ARF MBA +

+
+
+

B excess reduced by ARF = ARF MBA.

+
+
+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300615500
RS 00615.500 - Computing the Spouse's Benefit Reduction Factor Adjustment &mdash; Spouse also Entitled to RIB or DIB - 09/16/2002
+Batch run: 02/21/2013
+Rev:09/16/2002
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00615_580.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_580.html new file mode 100644 index 00000000..2808d8a7 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_580.html @@ -0,0 +1,386 @@ + + +SSA - POMS: RS 00615.580 - Computing the Widow(er)'s Benefits &mdash; Reduction Factor Adjustment - 09/16/2002 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 24 (09-02)

+

RS 00615.580 Computing the Widow(er)'s Benefits — Reduction Factor Adjustment +

+ + +
+
+

A. POLICY +

+
+
+
+
+

1. Effective Date(s) +

+
+
+
    +
  1. +
    +
    a. 
    +
    +

    At age 62; and

    + +
    +
    +
  2. +
  3. +
    +
    b. 
    +
    +

    At FRA.

    + +
    +
    +
  4. +
+
+
+
+
+

2. Method +

+
+
+

Different methods are used for these adjustments.

+
+
+
+
+ +
+
+

B. DEFINITION - ACRONYMS +

+
+
+
+
+

1. ORF +

+
+
+

Original reduction factor for period beginning at age 60 or MOET, if later.

+
+
+
+
+

2. Age 62 ARF +

+
+
+

Adjusted reduction factor at age 62. ORF minus the Crediting Months from age 60 (or + MOET, if later) to age 62. +

+
+
+
+
+

3. FRA ARF ` +

+
+
+

Adjusted reduction factor at FRA. ORF minus the Crediting Months from age 60 (or MOET, + if later) to FRA. +

+
+
+
+
+

4. ORF(50) +

+
+
+

Original reduction factor for period of DWB entitlement (MOET to age 60). Not applicable + for benefits 1/84 or later. +

+
+
+
+
+

5. ORF(36) +

+
+
+

Reduction factor beginning at age 62 (or MOET, if later).

+
+
+
+
+

6. ARF(60-62) +

+
+
+

Number of months in the adjusted reduction period from age 60 (or MOET, if later) + to age 62. +

+
+
+
+
+

7. AARF +

+
+
+

Additional adjustment reduction factor for period of DWB entitlement.

+

ORF 50 - Crediting Months from MOET to age 60.

+
+
+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300615580
RS 00615.580 - Computing the Widow(er)'s Benefits &mdash; Reduction Factor Adjustment - 09/16/2002
+Batch run: 02/21/2013
+Rev:09/16/2002
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00615_582.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_582.html new file mode 100644 index 00000000..bb61b492 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_582.html @@ -0,0 +1,338 @@ + + +SSA - POMS: RS 00615.582 - Computing the Reduction Factor Adjustment &mdash; No Entitlement Prior to Age 60 - 09/16/2002 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 24 (09-02)

+

RS 00615.582 Computing the Reduction Factor Adjustment — No Entitlement Prior to Age 60 +

+ + +
+
+

A. POLICY +

+
+
+

Compute the rates at 62 and FRA by removing crediting months.

+
+
+ +
+
+

B. PROCEDURE +

+
+
+
+
+

1. Age 62 Adjustment +

+
+
+
    +
  • +
    +
    •
    +
    +

    Original reduction factor minus crediting months from MOET up to 62 = Age 62 adjusted + reduction factor (ARF). +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    The unreduced widow(er)'s benefit adjusted by the Age 62 ARF = MBA at age 62.

    + +
    +
    +
  • +
+
+
+
+
+

2. FRA Adjustment +

+
+
+
    +
  • +
    +
    •
    +
    +

    Original reduction factor minus crediting months from MOET up to FRA = FRA adjusted + reduction factor (ARF). +

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    The unreduced widow(er)'s benefit adjusted by the FRA ARF = MBA at FRA.

    + +
    +
    +
  • +
+
+
+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300615582
RS 00615.582 - Computing the Reduction Factor Adjustment &mdash; No Entitlement Prior to Age 60 - 09/16/2002
+Batch run: 02/21/2013
+Rev:09/16/2002
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00615_585.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_585.html new file mode 100644 index 00000000..751c7944 --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_585.html @@ -0,0 +1,333 @@ + + +SSA - POMS: RS 00615.585 - Unusual Characteristics of the Widow(er)'s ARF - 09/16/2002 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 24 (09-02)

+

RS 00615.585 Unusual Characteristics of the Widow(er)'s ARF +

+ + +
+
+

A. INTRODUCTION +

+
+
+

The RIB limitation creates some unusual ARF situations.

+
+
+ +
+
+

B. PROCEDURE +

+
+
+
+
+

1. RIB Limitation Applies - Widow(er) Due FRA ARF +

+
+
+

No additional benefits are payable.

+
+
+
+
+

2. RIB Limitation Not Previously Applicable - Widow(er) Due FRA ARF +

+
+
+

Pay no more than RIB limitation.

+
+
+
+
+

3. RIB Limitation Not Previously Applicable - DNH Due ARF +

+
+
+

No increase can be paid.

+
+
+
+
+

4. Both Widow(er) and DNH Due ARF +

+
+
+
    +
  • +
    +
    •
    +
    +

    Use RIB limitation if widow(er)'s ARF raises MBA above the new DNH rate.

    + +
    +
    +
  • +
  • +
    +
    •
    +
    +

    Remove RIB limitation if the DNH's new MBA is higher than new widow(er)'s rate.

    + +
    +
    +
  • +
+
+
+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300615585
RS 00615.585 - Unusual Characteristics of the Widow(er)'s ARF - 09/16/2002
+Batch run: 02/21/2013
+Rev:09/16/2002
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00615_590.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_590.html new file mode 100644 index 00000000..096dbd7a --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_590.html @@ -0,0 +1,411 @@ + + +SSA - POMS: RS 00615.590 - Computing the Widow(er)'s Benefit Reduction Factor Adjustment &mdash; Initial Entitlement to Disabled Widow(er)'s Benefit - 09/16/2002 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 24 (09-02)

+

RS 00615.590 Computing the Widow(er)'s Benefit Reduction Factor Adjustment — Initial Entitlement + to Disabled Widow(er)'s Benefit +

+ + +
+
+

A. PROCEDURE - INITIAL ENTITLEMENT 1/78 THROUGH 12/83 +

+
+
+
+
+

1. Age 62 Adjustment +

+
+
+
    +
  1. +
    +
    a. 
    +
    +

    Determine the original reduction factor from MOET to age 60 minus crediting months + from MOET to age 60. This is the adjusted reduction factor (AARF). +

    + +
    +
    +
  2. +
  3. +
    +
    b. 
    +
    +

    Adjust benefits at age 62 by the AARF.

    + +
    +
    +
  4. +
  5. +
    +
    c. 
    +
    +

    Determine the original reduction factor from age 60 to age 65 minus crediting months + from age 60 to age 62. This is the age 62 ARF. +

    + +
    +
    +
  6. +
  7. +
    +
    d. 
    +
    +

    Reduce the benefit applicable for age 62 by the age 62 ARF.

    + +
    +
    +
  8. +
  9. +
    +
    e. 
    +
    +

    Step d. minus step b. = MBA at age 62.

    + +
    +
    +
  10. +
+
+
+
+
+

2. Age 65 Adjustment +

+
+
+
    +
  1. +
    +
    a. 
    +
    +

    Determine the original reduction factor from MOET to age 60 minus crediting months + from MOET to age 60. This is the age 60 adjusted reduction factor (AARF). +

    + +
    +
    +
  2. +
  3. +
    +
    b. 
    +
    +

    Reduce benefit for age 65 by AARF.

    + +
    +
    +
  4. +
  5. +
    +
    c. 
    +
    +

    Determine the original reduction factor from age 60 to age 65 minus crediting months + from age 60 to age 65. This is the age 65 ARF. +

    + +
    +
    +
  6. +
  7. +
    +
    d. 
    +
    +

    Reduce the benefit applicable for age 65 by the age 65 ARF.

    + +
    +
    +
  8. +
  9. +
    +
    e. 
    +
    +

    Step d. minus step b. = MBA at age 65.

    + +
    +
    +
  10. +
+
+
+
+
+ +
+
+

B. PROCEDURE - ARF DUE AFTER 12/83 +

+
+
+

The 1983 amendments repealed the additional reduction percentage for disabled widow(er)'s + benefits. Determine the reduction factor as if the widow(er) attains age 60 in the + MOET. +

+
+
+ +
+
+

C. REFERENCES +

+
+
+

Crediting months see RS 00615.482. +

+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300615590
RS 00615.590 - Computing the Widow(er)'s Benefit Reduction Factor Adjustment &mdash; Initial Entitlement to Disabled Widow(er)'s Benefit - 09/16/2002
+Batch run: 02/21/2013
+Rev:09/16/2002
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00615_595.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_595.html new file mode 100644 index 00000000..dcb01efd --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_595.html @@ -0,0 +1,300 @@ + + +SSA - POMS: RS 00615.595 - Computing the Widow(er)'s Benefit &mdash; Reduction Factor Adjustment &mdash; Dual Entitlement to DIB Before or Concurrent With WIB - 07/01/2011 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 24 (09-02)

+

RS 00615.595 Computing the Widow(er)'s Benefit — Reduction Factor Adjustment — Dual Entitlement + to DIB Before or Concurrent With WIB +

+ + +
+
+

A. POLICY +

+
+
+

When WIB follows or is concurrent with DIB, the age 62 ARF involves the same method + as the AB dual entitlement reduction factor adjustment. +

+

NOTE: An ARF is not applicable when WIB follows or is concurrent with Medicare for Qualified + Government Employment (MQGE) entitlement. +

+
+
+ +
+
+

B. PROCEDURE +

+
+
+

The adjusted reduction factor is applied to the excess of the WIB PIA over the DIB + PIA. Because the WIB FRA and RIB FRA can occur in different months, an ARF may be + due at WIB FRA, followed by removal of all WIB reduction when DIB converts to RIB + at RIB FRA.. +

+
+
+ +
+
+

C. REFERENCE +

+
+
+

Dual entitlement adjustment - see RS 00615.500. +

+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300615595
RS 00615.595 - Computing the Widow(er)'s Benefit &mdash; Reduction Factor Adjustment &mdash; Dual Entitlement to DIB Before or Concurrent With WIB - 07/01/2011
+Batch run: 02/21/2013
+Rev:07/01/2011
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/poms_rs_00615_598.html b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_598.html new file mode 100644 index 00000000..4f1d58ef --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/poms_rs_00615_598.html @@ -0,0 +1,282 @@ + + +SSA - POMS: RS 00615.598 - Adjustment of the Reduction Factor for a Deceased Numberholder - 12/06/2023 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + +

Program Operations Manual System (POMS)

+ + +
+ + + +
+ +
+ +
+
+ + +
+

TN 44 (12-23)

+

RS 00615.598 Adjustment of the Reduction Factor for a Deceased Numberholder +

+ + +
+
+

A. POLICY +

+
+
+

DNH's are eligible for an ARF if deductions were made for at least one month prior + to FRA or death, if earlier. An ARF for NH's who died before FRA can be given no earlier + than the month they would have attained FRA. +

+
+
+ +
+
+

B. PROCEDURE +

+
+
+

Consider as crediting months only months NH was alive.

+
+
+ + +
+
+
+ + +
To Link to this section - Use this URL:
http://policy.ssa.gov/poms.nsf/lnx/0300615598
RS 00615.598 - Adjustment of the Reduction Factor for a Deceased Numberholder - 12/06/2023
+Batch run: 12/06/2023
+Rev:12/06/2023
+
+ + +
+ + +
+ + +
+ + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/tests/data/track_b/arf_recompute_sources/ssa_pub_05_10069_arf_recompute_excerpts.txt b/tests/data/track_b/arf_recompute_sources/ssa_pub_05_10069_arf_recompute_excerpts.txt new file mode 100644 index 00000000..870f9c2c --- /dev/null +++ b/tests/data/track_b/arf_recompute_sources/ssa_pub_05_10069_arf_recompute_excerpts.txt @@ -0,0 +1,43 @@ +# SSA Publication No. 05-10069 (2026), pdftotext -layout of the served PDF; the two +# passages on ARF and automatic recomputation, verbatim. + +Will you receive higher monthly +benefits later if benefits are +withheld because of work? +Yes. If some of your retirement benefits +are withheld because of your earnings, +your monthly benefit will increase +starting at your full retirement age. This +takes into account those months in +which benefits were withheld. +As an example, let’s say you claim +retirement benefits upon turning 62 in +2026, and your payment is $910 per +month. Then, you return to work and +have 12 months of benefits withheld. +We would recalculate your benefit at +your full retirement age of 67 and pay +you $975 per month (in today’s dollars). +Or, maybe you earn so much between +the ages of 62 and 67 that all benefits in +those years are withheld. In that case, +we would pay you $1,300 a month +starting at age 67. + +Are there other ways that work +can increase your benefits? +Yes. Each year we review the records for +all Social Security beneficiaries who work. +If your latest year of earnings turns out to +be one of your highest years, we refigure +your benefit and pay you any increase +due. This is an automatic process, +and benefits are paid in December +of the following year. For example, in + + + 7 + December 2026, you should get an +increase for your 2025 earnings if those +earnings raised your benefit. The increase +would be retroactive to January 2026. diff --git a/tests/estimates/test_birth_evidence_artifact.py b/tests/estimates/test_birth_evidence_artifact.py index 84e93028..1599bf0e 100644 --- a/tests/estimates/test_birth_evidence_artifact.py +++ b/tests/estimates/test_birth_evidence_artifact.py @@ -182,6 +182,8 @@ def test_post_review_sources_are_outside_historical_reducer_identity(): Path("src/populace_dynamics/track_a_v2/structural_inputs.py"), Path("src/populace_dynamics/track_b/reconstructed.py"), Path("src/populace_dynamics/track_b/gross_benefits.py"), + Path("src/populace_dynamics/track_b/arf.py"), + Path("src/populace_dynamics/track_b/recompute.py"), Path("src/populace_dynamics/track_b/b2.py"), Path("src/populace_dynamics/data/u2_source_registry.py"), Path("scripts/capture_track_u2_income_wealth.py"), @@ -381,6 +383,8 @@ def test_post_review_exclusions_are_unreachable_from_birth_evidence(): "populace_dynamics.track_b.runner", "populace_dynamics.track_b.reconstructed", "populace_dynamics.track_b.gross_benefits", + "populace_dynamics.track_b.arf", + "populace_dynamics.track_b.recompute", "populace_dynamics.track_b.b2", } assert track_b_modules.issubset(module_paths) diff --git a/tests/test_track_b_arf.py b/tests/test_track_b_arf.py new file mode 100644 index 00000000..22e310c5 --- /dev/null +++ b/tests/test_track_b_arf.py @@ -0,0 +1,984 @@ +"""R3 statutory ARF examples and boundaries; invented or published inputs. + +Verification class: statutory conformance. These cases admit ARF credit +and rounding mechanics only, without population or behavioral claims. +Published figures are checked against locally captured SSA text. +""" + +from dataclasses import replace +from datetime import date +from decimal import Decimal +from fractions import Fraction as F + +import pytest + +from populace_dynamics.track_b import arf as a +from populace_dynamics.track_b import gross_benefits as g +from tests.track_b_gross_benefit_support import ssa_params + +PARAMS = ssa_params() +FRA = g.YearMonth(2027, 1) + +INVALID_RIB_PARAMETERS = [ + pytest.param( + {"early_first_bracket_months": 35}, + "early_first_bracket_months must be 36 (402(q))", + id="bracket", + ), + pytest.param( + {"early_monthly_rates": (0, 0.00416667)}, + "Parameter bundle early_monthly_rates[0]=0 disagrees with the " + "statutory 1/180; refusing to compute gross benefits.", + id="first-rate", + ), + pytest.param( + {"early_monthly_rates": (0.00555556, 0)}, + "Parameter bundle early_monthly_rates[1]=0 disagrees with the " + "statutory 1/240; refusing to compute gross benefits.", + id="later-rate", + ), +] + +# Every scalar in the complete shared bundle read by G's statutory_rates. +# ARF validates this bundle for each record kind before any coverage rule. +STATUTORY_RATE_FIELDS = ( + ("early_monthly_rates", 0), + ("early_monthly_rates", 1), + ("early_first_bracket_months", None), + ("spousal_early_monthly_rates", 0), + ("spousal_early_monthly_rates", 1), + ("spousal_early_first_bracket_months", None), + ("survivor_reduction_floor", None), + ("rib_lim_pia_share", None), + ("spousal_pia_share", None), + ("survivor_pia_share", None), +) + + +def nonfinite_statutory_params(field, index, value): + """Replace one scalar while retaining every other statutory value.""" + if index is None: + replacement = value + else: + replacement = list(getattr(PARAMS, field)) + replacement[index] = value + replacement = tuple(replacement) + return ssa_params(**{field: replacement}) + + +def statutory_validation_claim(kind, coverage, amount=F(1300)): + """Valid invented claims, including a condition-free coverage refusal.""" + claim = a.ARFClaim(kind, date(1960, 1, 2), (2022, 1), amount) + payment = FRA + if coverage == "explicit": + claim = replace( + claim, conditions=(a.ARFUnsupportedReason.OTHER_SUSPENSION,) + ) + elif coverage == "automatic": + # All three kinds validly reach INITIAL_ENTITLEMENT_BEFORE_1978. + claim = replace( + claim, + birth_date=date(1915, 1, 2), + first_reduction_month=(1977, 1), + ) + payment = g.YearMonth(1983, 1) + return claim, payment + + +def months(first, count): + """Generate invented final deduction months, independently of ARF.""" + index = first.year * 12 + first.month - 1 + return frozenset( + g.YearMonth((index + i) // 12, (index + i) % 12 + 1) + for i in range(count) + ) + + +def rib_claim(**kwargs): + values = dict( + kind=a.ARFBenefitKind.RIB, + birth_date=date(1960, 1, 2), + first_reduction_month=g.YearMonth(2022, 1), + unreduced_benefit=F(1300), + ) + values.update(kwargs) + return a.ARFClaim(**values) + + +def test_original_claim_iterator_credit_survives_repeated_application(): + """Reviewer counterexample: one credit always pays $915.40/$915.""" + claim = rib_claim(full_work_deduction_months=iter([(2022, 1)])) + first = a.apply_arf(claim, payment_month=FRA, params=PARAMS) + second = a.apply_arf(claim, payment_month=FRA, params=PARAMS) + assert first.adjusted_reduction_months == 59 + assert first.benefit == F("915.4") + assert first.whole_dollar_benefit == 915 + assert second == first + + +def test_original_claim_iterator_refusal_survives_repeated_evaluation(): + """Reviewer counterexample: evaluating a refusal never erases it.""" + claim = rib_claim( + conditions=iter([a.ARFUnsupportedReason.OTHER_SUSPENSION]) + ) + outcomes = [ + a.evaluate_arf( + key, + F(1), + lambda: a.apply_arf(claim, payment_month=FRA, params=PARAMS), + ) + for key in ("left", "right") + ] + for outcome in outcomes: + assert not outcome.supported + assert ( + outcome.unsupported.reason + is a.ARFUnsupportedReason.OTHER_SUSPENSION + ) + denominator = a.count_arf_outcomes(outcomes) + assert denominator.rows == denominator.unsupported_rows == 2 + assert denominator.weight == denominator.unsupported_weight == 2 + + +@pytest.mark.parametrize( + "kind,field", + [ + (a.ARFBenefitKind.RIB, "full_work_deduction_months"), + (a.ARFBenefitKind.RIB, "partial_work_deduction_months"), + (a.ARFBenefitKind.SPOUSE, "child_in_care_months"), + (a.ARFBenefitKind.SPOUSE, "nonentitlement_months"), + (a.ARFBenefitKind.RIB, "simultaneous_dib_months"), + (a.ARFBenefitKind.WIDOW, "mother_father_rate_months"), + ], +) +def test_every_iterator_credit_field_is_stable_for_supported_and_refused_claims( + kind, field +): + for refused in (False, True): + claim = rib_claim( + kind=kind, + **{field: iter([(2022, 1)])}, + conditions=iter( + [a.ARFUnsupportedReason.OTHER_SUSPENSION] if refused else [] + ), + ) + + def compute(claim=claim): + return a.apply_arf(claim, payment_month=FRA, params=PARAMS) + + first = a.evaluate_arf("same", F(1), compute) + second = a.evaluate_arf("same", F(1), compute) + assert second.supported == first.supported == (not refused) + if refused: + assert second.unsupported.reason is first.unsupported.reason + else: + assert second.benefits == first.benefits + assert first.benefits.crediting_months == frozenset( + {g.YearMonth(2022, 1)} + ) + assert first.benefits.adjusted_reduction_months == ( + first.benefits.original_reduction_months - 1 + ) + assert isinstance(getattr(claim, field), (tuple, frozenset)) + assert isinstance(claim.conditions, (tuple, frozenset)) + + +def test_iterator_inputs_are_snapshotted_before_the_caller_can_consume_them(): + credits = iter([(2022, 1)]) + conditions = iter([a.ARFUnsupportedReason.OTHER_SUSPENSION]) + claim = rib_claim( + full_work_deduction_months=credits, conditions=conditions + ) + assert list(credits) == list(conditions) == [] + with pytest.raises(g.FamilyConfigurationUnsupported) as caught: + a.apply_arf(claim, payment_month=FRA, params=PARAMS) + assert caught.value.reason is a.ARFUnsupportedReason.OTHER_SUSPENSION + supported = a.apply_arf( + replace(claim, conditions=()), payment_month=FRA, params=PARAMS + ) + assert supported.benefit == F("915.4") + + +def test_result_iterator_credits_are_immutable_and_survive_inspection(): + result = a.apply_arf( + rib_claim(full_work_deduction_months={(2022, 1)}), + payment_month=FRA, + params=PARAMS, + ) + direct = replace(result, crediting_months=iter(result.crediting_months)) + assert direct == result + assert frozenset(direct.crediting_months) == result.crediting_months + assert frozenset(direct.crediting_months) == result.crediting_months + + +@pytest.mark.parametrize("credits,expected", [(0, 910), (12, 975), (60, 1300)]) +def test_publication_05_10069_worked_example(credits, expected): + """SSA: PIA1300,60 RF;0/12/60credits give910/975/1300 exactly. + + 60 RF reduction =36*5/900+24*5/1200=3/10;1300*(7/10)=910. + 48 RF reduction =36*5/900+12*5/1200=1/4;1300*(3/4)=975. + Zero remaining RF pays1300. Figures are today's dollars, without COLA. + """ + claim = rib_claim( + full_work_deduction_months=months(g.YearMonth(2022, 1), credits) + ) + result = a.apply_arf(claim, payment_month=FRA, params=PARAMS) + assert result.original_reduction_months == 60 + assert result.adjusted_reduction_months == 60 - credits + assert result.benefit == F(expected) + assert result.whole_dollar_benefit == expected + + +def test_partial_work_deduction_is_a_full_crediting_month(): + one = frozenset({g.YearMonth(2022, 1)}) + full = a.apply_arf( + rib_claim(full_work_deduction_months=one), + payment_month=FRA, + params=PARAMS, + ) + partial = a.apply_arf( + rib_claim(partial_work_deduction_months=one), + payment_month=FRA, + params=PARAMS, + ) + assert partial.adjusted_reduction_months == 59 + assert partial.benefit == full.benefit == F("915.4") + assert partial.crediting_months == one + + +def test_rib_simultaneous_dib_entitlement_is_an_independent_crediting_month(): + """Invented q7F:60RF minus1 simultaneousDIB month gives59RF. + + 36*5/900+23*5/1200=71/240;1300*71/240=384.58333..., + reduction rounds up384.6;hypothetical RIB is915.4. + There is no work deduction in this case. + """ + one = frozenset({g.YearMonth(2022, 1)}) + result = a.apply_arf( + rib_claim(simultaneous_dib_months=one), + payment_month=FRA, + params=PARAMS, + ) + assert result.crediting_months == one + assert result.adjusted_reduction_months == 59 + assert result.reduction == F("384.6") + assert result.benefit == F("915.4") + + +def test_overlapping_categories_credit_a_month_once_and_ignore_outside_window(): + first = g.YearMonth(2022, 1) + inside = first + outside = {g.YearMonth(2021, 12), FRA, g.YearMonth(2027, 2)} + result = a.apply_arf( + rib_claim( + full_work_deduction_months={inside, *outside}, + partial_work_deduction_months={inside}, + simultaneous_dib_months={inside}, + ), + payment_month=FRA, + params=PARAMS, + ) + assert result.crediting_months == frozenset({inside}) + assert result.adjusted_reduction_months == 59 + + +@pytest.mark.parametrize( + "birth,kind,expected", + [ + (date(1963, 3, 1), a.ARFBenefitKind.RIB, (2030, 2)), + (date(1963, 3, 2), a.ARFBenefitKind.RIB, (2030, 3)), + (date(1963, 3, 31), a.ARFBenefitKind.RIB, (2030, 3)), + (date(1960, 1, 1), a.ARFBenefitKind.RIB, (2026, 10)), + (date(1960, 1, 2), a.ARFBenefitKind.RIB, (2027, 1)), + (date(1962, 1, 1), a.ARFBenefitKind.WIDOW, (2028, 10)), + (date(1962, 1, 2), a.ARFBenefitKind.WIDOW, (2029, 1)), + ], +) +def test_birthday_fra_boundaries_use_day_before_and_january_cohort( + birth, kind, expected +): + assert ( + a.full_retirement_age_month(birth, kind, PARAMS).as_tuple() == expected + ) + + +def test_first_day_birthday_adjustment_is_not_one_month_early(): + claim = rib_claim( + birth_date=date(1963, 3, 1), + first_reduction_month=(2025, 3), + partial_work_deduction_months={(2025, 3)}, + ) + before = a.apply_arf(claim, payment_month=(2030, 1), params=PARAMS) + at = a.apply_arf(claim, payment_month=(2030, 2), params=PARAMS) + assert before.fra_month == at.fra_month == g.YearMonth(2030, 2) + assert before.adjusted_reduction_months == 59 + assert before.crediting_months == frozenset() + assert at.adjusted_reduction_months == 58 + assert at.benefit > before.benefit + + +@pytest.mark.parametrize( + "kind,birth,first,expected_reduction,expected_benefit", + [ + (a.ARFBenefitKind.RIB, date(1960, 1, 2), (2026, 12), "5.6", "994.4"), + ( + a.ARFBenefitKind.SPOUSE, + date(1960, 1, 2), + (2026, 12), + "7.0", + "993.0", + ), + (a.ARFBenefitKind.WIDOW, date(1962, 1, 2), (2028, 12), "3.4", "996.6"), + ], +) +def test_reduction_rounds_up_to_dime_after_exact_fraction( + kind, birth, first, expected_reduction, expected_benefit +): + claim = a.ARFClaim(kind, birth, first, F(1000)) + at = a.full_retirement_age_month(birth, kind, PARAMS) + result = a.apply_arf(claim, payment_month=at, params=PARAMS) + assert result.reduction == F(expected_reduction) + assert result.benefit == F(expected_benefit) + + +def test_widow_age62_credits_persist_and_later_credits_wait_for_fra(): + """Invented:84 RF,PIA1000;credits1 before62 and1 after62. + + At62,83 RF: reduction1000*.285*83/84=281.607142...,ceil281.7, + benefit718.3. AtFRA82 RF:278.214285...,ceil278.3,benefit721.7. + """ + claim = a.ARFClaim( + a.ARFBenefitKind.WIDOW, + date(1962, 1, 2), + (2022, 1), + F(1000), + partial_work_deduction_months={(2022, 1), (2024, 2)}, + ) + before = a.apply_arf(claim, payment_month=(2023, 12), params=PARAMS) + age62 = a.apply_arf(claim, payment_month=(2024, 1), params=PARAMS) + later = a.apply_arf(claim, payment_month=(2028, 12), params=PARAMS) + fra = a.apply_arf(claim, payment_month=(2029, 1), params=PARAMS) + assert before.adjusted_reduction_months == 84 + assert before.benefit == F(715) + assert ( + age62.adjusted_reduction_months + == later.adjusted_reduction_months + == 83 + ) + assert age62.benefit == later.benefit == F("718.3") + assert fra.adjusted_reduction_months == 82 + assert fra.benefit == F("721.7") + + +def test_spouse_child_in_care_and_termination_months_are_credits_at_fra(): + claim = a.ARFClaim( + a.ARFBenefitKind.SPOUSE, + date(1960, 1, 2), + (2022, 1), + F(650), + child_in_care_months={(2022, 1)}, + nonentitlement_months={(2022, 2)}, + ) + result = a.apply_arf(claim, payment_month=FRA, params=PARAMS) + assert result.adjusted_reduction_months == 58 + assert result.benefit == F("427.9") + + +def test_widow_mother_father_rate_and_termination_months_are_credits(): + claim = a.ARFClaim( + a.ARFBenefitKind.WIDOW, + date(1962, 1, 2), + (2022, 1), + F(1000), + mother_father_rate_months={(2022, 1)}, + nonentitlement_months={(2022, 2)}, + ) + result = a.apply_arf(claim, payment_month=(2029, 1), params=PARAMS) + assert result.adjusted_reduction_months == 82 + assert result.benefit == F("721.7") + + +def test_spouse_dual_concurrent_or_preceding_reduces_only_excess(): + """Invented:.500 excess650-200=450;48 RF,rate.3 gives315. + + Own independently adjusted RIB160 is separate; together475 before + whole-dollar rounding each component. Crediting sets are per record. + """ + claim = a.ARFClaim( + a.ARFBenefitKind.SPOUSE, + date(1960, 1, 2), + (2022, 1), + F(650), + full_work_deduction_months=months(g.YearMonth(2022, 1), 12), + spouse_own=a.SpouseOwnBenefit( + F(200), F(160), a.SpouseOwnSequence.CONCURRENT_OR_PRECEDING + ), + ) + result = a.apply_arf(claim, payment_month=FRA, params=PARAMS) + assert result.adjusted_reduction_months == 48 + assert result.benefit == F(315) + assert result.benefit + claim.spouse_own.payable == F(475) + + +def test_own_entitlement_after_spouse_arf_preserves_spouse_adjustment(): + """Invented:.500A.1,650 at48RF=>455;new ownRIB200=>excess255.""" + claim = a.ARFClaim( + a.ARFBenefitKind.SPOUSE, + date(1960, 1, 2), + (2022, 1), + F(650), + full_work_deduction_months=months(g.YearMonth(2022, 1), 12), + spouse_own=a.SpouseOwnBenefit( + F(200), F(200), a.SpouseOwnSequence.AFTER_SPOUSE_ARF + ), + ) + result = a.apply_arf(claim, payment_month=(2027, 2), params=PARAMS) + assert result.benefit == F(255) + + +@pytest.mark.parametrize( + "unreduced,own_pia,own_payable,expected,whole_dollars", + [ + (F("26.2"), F(15), F("15.1"), F("1.9"), 1), + (F(650), F(200), F(208), F("214.5"), 214), + ], +) +def test_own_entitlement_after_spouse_arf_offsets_own_payable_not_pia( + unreduced, own_pia, own_payable, expected, whole_dollars +): + """Invented reviewer cases: separately adjusted own RIB changes money. + + At 60 RF the single spouse rates are $17.00 and $422.50. The supplied + own payments, $15.10 and $208.00, leave $1.90/$1 and $214.50/$214. + RS 00615.500A.1 preserves the spouse ARF before this own offset. + """ + claim = a.ARFClaim( + a.ARFBenefitKind.SPOUSE, + date(1960, 1, 2), + (2022, 1), + unreduced, + spouse_own=a.SpouseOwnBenefit( + own_pia, own_payable, a.SpouseOwnSequence.AFTER_SPOUSE_ARF + ), + ) + result = a.apply_arf(claim, payment_month=(2028, 1), params=PARAMS) + assert result.adjusted_reduction_months == 60 + assert result.benefit == expected + assert result.whole_dollar_benefit == whole_dollars + assert ( + a.apply_arf(result, payment_month=(2028, 1), params=PARAMS) == result + ) + + +@pytest.mark.parametrize( + "kind,first", + [ + (a.ARFBenefitKind.RIB, (2021, 12)), + (a.ARFBenefitKind.SPOUSE, (2021, 12)), + (a.ARFBenefitKind.WIDOW, (1990, 1)), + ], +) +def test_disabled_widow_flag_does_not_hide_invalid_minimum_age(kind, first): + """Executed minimized counterexamples:age61y11m RIB,age30 widow.""" + claim = a.ARFClaim( + kind, + date(1960, 1, 2), + first, + F(1000), + conditions={a.ARFUnsupportedReason.DISABLED_WIDOW}, + ) + with pytest.raises(g.InvalidFamilyInput): + a.apply_arf(claim, payment_month=(2027, 1), params=PARAMS) + + +def test_valid_disabled_widow_at_age50_is_refused_as_coverage(): + claim = a.ARFClaim( + a.ARFBenefitKind.WIDOW, + date(1960, 1, 2), + (2010, 1), + F(1000), + conditions={a.ARFUnsupportedReason.DISABLED_WIDOW}, + ) + with pytest.raises(g.FamilyConfigurationUnsupported) as caught: + a.apply_arf(claim, payment_month=(2027, 1), params=PARAMS) + assert caught.value.reason is a.ARFUnsupportedReason.DISABLED_WIDOW + + +def test_deceased_numberholder_waits_until_hypothetical_fra_and_credits_alive_months(): + claim = rib_claim( + death_date=date(2023, 4, 15), + full_work_deduction_months={(2023, 3), (2023, 4), (2023, 5)}, + ) + before = a.apply_arf(claim, payment_month=(2026, 12), params=PARAMS) + result = a.apply_arf(claim, payment_month=FRA, params=PARAMS) + assert before.crediting_months == frozenset() + assert before.benefit == F(910) + assert result.crediting_months == frozenset({g.YearMonth(2023, 3)}) + assert result.benefit == F("915.4") + + +@pytest.mark.parametrize("reason", list(a.ARFUnsupportedReason)) +def test_explicit_exceptional_history_is_refused_by_name(reason): + with pytest.raises(g.FamilyConfigurationUnsupported) as caught: + a.apply_arf( + rib_claim(conditions={reason}), payment_month=FRA, params=PARAMS + ) + assert caught.value.code == "FAMILY_CONFIG_UNSUPPORTED" + assert caught.value.reason is reason + + +def test_spouse_reentitlement_before_fra_is_explicitly_refused(): + claim = a.ARFClaim( + a.ARFBenefitKind.SPOUSE, + date(1960, 1, 2), + (2022, 1), + F(650), + nonentitlement_months={(2022, 2)}, + conditions={a.ARFUnsupportedReason.SPOUSE_REENTITLEMENT_BEFORE_FRA}, + ) + with pytest.raises(g.FamilyConfigurationUnsupported) as caught: + a.apply_arf(claim, payment_month=(2024, 1), params=PARAMS) + assert ( + caught.value.reason + is a.ARFUnsupportedReason.SPOUSE_REENTITLEMENT_BEFORE_FRA + ) + + +def test_spouse_reentitlement_before_fra_is_detected_from_termination_history(): + claim = a.ARFClaim( + a.ARFBenefitKind.SPOUSE, + date(1960, 1, 2), + (2022, 1), + F(650), + nonentitlement_months={(2022, 2)}, + ) + with pytest.raises(g.FamilyConfigurationUnsupported) as caught: + a.apply_arf(claim, payment_month=(2024, 1), params=PARAMS) + assert ( + caught.value.reason + is a.ARFUnsupportedReason.SPOUSE_REENTITLEMENT_BEFORE_FRA + ) + # The same known history has a settled final adjustment at FRA. + result = a.apply_arf(claim, payment_month=FRA, params=PARAMS) + assert result.adjusted_reduction_months == 59 + + +def test_dual_spouse_after_spouse_before_arf_is_refused(): + claim = a.ARFClaim( + a.ARFBenefitKind.SPOUSE, + date(1960, 1, 2), + (2022, 1), + F(650), + spouse_own=a.SpouseOwnBenefit( + F(200), F(160), a.SpouseOwnSequence.AFTER_SPOUSE_BEFORE_ARF + ), + ) + with pytest.raises(g.FamilyConfigurationUnsupported) as caught: + a.apply_arf(claim, payment_month=FRA, params=PARAMS) + assert ( + caught.value.reason + is a.ARFUnsupportedReason.SPOUSE_OWN_AFTER_SPOUSE_BEFORE_ARF + ) + + +def test_historic_survivor_fra_conflict_is_refused(): + claim = a.ARFClaim( + a.ARFBenefitKind.WIDOW, date(1911, 1, 2), (1972, 1), F(1000) + ) + with pytest.raises(g.FamilyConfigurationUnsupported) as caught: + a.apply_arf(claim, payment_month=(1983, 1), params=PARAMS) + assert ( + caught.value.reason + is a.ARFUnsupportedReason.HISTORICAL_WIDOW_FRA_CONFLICT + ) + with pytest.raises(g.FamilyConfigurationUnsupported) as calendar_error: + a.full_retirement_age_month( + date(1911, 1, 2), a.ARFBenefitKind.WIDOW, PARAMS + ) + assert ( + calendar_error.value.reason + is a.ARFUnsupportedReason.HISTORICAL_WIDOW_FRA_CONFLICT + ) + # Invented counterfactual: first1/1973,36/60 RF gives829 under + # POMS FRA65; CFR FRA62 gives1000. Neither reading is paid by R3. + assert g.survivor_age_reduced(F(1000), 36, 60, PARAMS) == F(829) + assert F(1000) - F(829) == F(171) + + +def test_widow_nonbinding_child_care_readings_are_explicit(): + """Invented q7D/CFR.412(a)(4) conflict:1000PIA,36 RF,6 care months. + + Without the mother/father floor binding:1000-.285*36/84=877.8 + after ceil-dime reduction. Categorical care credits give30RF:898.2. + This20.4 difference is not silently selected by the adapter. + """ + claim = a.ARFClaim( + a.ARFBenefitKind.WIDOW, date(1962, 3, 2), (2026, 3), F(1000) + ) + no_credit = a.apply_arf(claim, payment_month=(2029, 3), params=PARAMS) + assert no_credit.benefit == F("877.8") + with pytest.raises(g.FamilyConfigurationUnsupported) as caught: + a.apply_arf( + replace( + claim, + conditions={ + a.ARFUnsupportedReason.WIDOW_CHILD_CARE_WITHOUT_RATE_INCREASE + }, + ), + payment_month=(2029, 3), + params=PARAMS, + ) + assert ( + caught.value.reason + is a.ARFUnsupportedReason.WIDOW_CHILD_CARE_WITHOUT_RATE_INCREASE + ) + alternative = g.survivor_age_reduced(F(1000), 30, 84, PARAMS) + assert alternative == F("898.2") + assert alternative - no_credit.benefit == F("20.4") + + +@pytest.mark.parametrize( + "change", + [ + dict(unreduced_benefit=F(-1)), + dict(unreduced_benefit=F("1.01")), + dict(unreduced_benefit=1000.0), + dict(full_work_deduction_months={(2022, 13)}), + dict(child_in_care_months={(2022, 1)}), + dict(first_reduction_month=(2021, 12)), + dict(first_reduction_month=(2027, 2)), + dict(death_date=date(1950, 1, 1)), + dict(conditions={"not a reason"}), + ], +) +def test_invalid_input_precedes_coverage_refusal(change): + claim = rib_claim( + conditions={a.ARFUnsupportedReason.OTHER_SUSPENSION}, + **{k: v for k, v in change.items() if k != "conditions"}, + ) + if "conditions" in change: + claim = replace(claim, **change) + with pytest.raises(g.InvalidFamilyInput): + a.apply_arf(claim, payment_month=FRA, params=PARAMS) + + +def test_refusals_remain_in_weighted_denominator_and_invalid_rows_raise(): + supported = a.evaluate_arf( + "supported", + F(2), + lambda: a.apply_arf(rib_claim(), payment_month=FRA, params=PARAMS), + ) + refused = a.evaluate_arf( + "refused", + F(3), + lambda: a.apply_arf( + rib_claim(conditions={a.ARFUnsupportedReason.OTHER_SUSPENSION}), + payment_month=FRA, + params=PARAMS, + ), + ) + denominator = a.count_arf_outcomes([supported, refused]) + assert denominator.rows == 2 + assert denominator.weight == 5 + assert denominator.supported_weight_share == F(2, 5) + assert denominator.unsupported_by_reason[ + a.ARFUnsupportedReason.OTHER_SUSPENSION + ] == (1, F(3)) + with pytest.raises(g.InvalidFamilyInput): + a.evaluate_arf( + "invalid", + F(1), + lambda: a.apply_arf( + rib_claim(unreduced_benefit=-1), + payment_month=FRA, + params=PARAMS, + ), + ) + + +# .015 source figures are whole-dollar payments; dime intermediate values +# below are independently reproduced with 402(q)'s exact Fraction rates. +BIRTHDAY_EXAMPLES = [ + (date(1935, 5, 31), (1997, 6), 1207, 35, "972.3", 972), + (date(1935, 6, 1), (1997, 6), 1207, 35, "972.3", 972), + (date(1935, 6, 2), (1997, 6), 1207, 36, "965.6", 965), + (date(1935, 6, 3), (1997, 7), 1207, 35, "972.3", 972), + (date(1935, 12, 31), (1998, 1), 1232, 35, "992.4", 992), + (date(1936, 1, 1), (1998, 1), 1232, 35, "992.4", 992), + (date(1936, 1, 2), (1998, 1), 1266, 36, "1012.8", 1012), + (date(1936, 1, 3), (1998, 2), 1266, 35, "1019.8", 1019), + (date(1937, 12, 31), (2000, 1), 1372, 35, "1105.2", 1105), + (date(1938, 1, 1), (2000, 1), 1372, 35, "1105.2", 1105), + (date(1938, 1, 2), (2000, 1), 1410, 38, "1116.2", 1116), + (date(1938, 1, 3), (2000, 2), 1410, 37, "1122.1", 1122), +] + + +@pytest.mark.parametrize("birth,first,pia,rf,dimes,payment", BIRTHDAY_EXAMPLES) +def test_poms_00615_015_birthday_worked_examples( + birth, first, pia, rf, dimes, payment +): + claim = a.ARFClaim(a.ARFBenefitKind.RIB, birth, first, F(pia)) + result = a.apply_arf(claim, payment_month=first, params=PARAMS) + assert result.original_reduction_months == rf + assert result.benefit == F(dimes) + assert result.whole_dollar_benefit == payment + + +def test_worked_example_figures_are_quoted_from_the_captures(): + from tests.track_b_arf_recompute_support import source_text + + publication = source_text("ssa_pub_05_10069_arf_recompute_excerpts") + for figure in ("$910", "12 months", "67", "$975", "$1,300", "62", "2026"): + assert figure in publication + birthdays = source_text("poms_rs_00615_015") + for figure in ( + "$1207", + "$972", + "$965", + "$1232", + "$1266", + "$992", + "$1012", + "$1019", + "$1372", + "$1410", + "$1105", + "$1116", + "$1122", + "35", + "36", + "37", + "38", + ): + assert figure in birthdays + + +def test_poms_00615_015_date_erratum_is_pinned_against_correct_table(): + from tests.track_b_arf_recompute_support import source_text + + text = source_text("poms_rs_00615_015") + assert "In 2/1998 Demi has 37 RF." in text + assert "Demi - $1122 starting 2/2000" in text + # Correct chronology is the captured example's benefit date and table. + claim = a.ARFClaim( + a.ARFBenefitKind.RIB, date(1938, 1, 3), (2000, 2), F(1410) + ) + assert ( + a.apply_arf( + claim, payment_month=(2000, 2), params=PARAMS + ).original_reduction_months + == 37 + ) + + +@pytest.mark.parametrize("overrides,message", INVALID_RIB_PARAMETERS) +@pytest.mark.parametrize("entrypoint", ["apply", "evaluate", "count"]) +@pytest.mark.parametrize( + "claim,payment", + [ + pytest.param( + rib_claim(first_reduction_month=(2027, 1), unreduced_benefit=F(0)), + FRA, + id="ordinary-zero-rf", + ), + pytest.param( + rib_claim( + first_reduction_month=(2027, 1), + unreduced_benefit=F(0), + conditions=iter([a.ARFUnsupportedReason.OTHER_SUSPENSION]), + ), + FRA, + id="explicit-suspension-zero-rf", + ), + pytest.param( + rib_claim( + birth_date=date(1915, 1, 2), + first_reduction_month=(1977, 1), + unreduced_benefit=F(0), + ), + (1983, 1), + id="automatic-historical-entitlement", + ), + pytest.param( + rib_claim( + birth_date=date(1920, 1, 2), + first_reduction_month=(1982, 1), + unreduced_benefit=F(0), + ), + (1982, 1), + id="automatic-historical-payment", + ), + pytest.param( + a.ARFClaim( + a.ARFBenefitKind.WIDOW, + date(1911, 1, 2), + (1972, 1), + F(0), + ), + (1983, 1), + id="automatic-historical-widow-conflict", + ), + pytest.param( + a.ARFClaim( + a.ARFBenefitKind.SPOUSE, + date(1960, 1, 2), + (2022, 1), + F(0), + nonentitlement_months={(2022, 2)}, + ), + (2024, 1), + id="automatic-spouse-reentitlement", + ), + pytest.param( + a.ARFClaim( + a.ARFBenefitKind.SPOUSE, + date(1960, 1, 2), + (2022, 1), + F(0), + spouse_own=a.SpouseOwnBenefit( + F(200), F(160), a.SpouseOwnSequence.AFTER_SPOUSE_BEFORE_ARF + ), + ), + FRA, + id="automatic-own-entitlement-sequence", + ), + ], +) +def test_invalid_rib_parameters_precede_all_arf_refusals_and_counts( + overrides, message, entrypoint, claim, payment +): + """Invalid bundles propagate; no unsupported outcome is constructed.""" + params = ssa_params(**overrides) + + def compute(): + return a.apply_arf(claim, payment_month=payment, params=params) + + with pytest.raises(ValueError) as caught: + if entrypoint == "apply": + compute() + elif entrypoint == "evaluate": + a.evaluate_arf("invalid", F(1), compute) + else: + a.count_arf_outcomes( + a.evaluate_arf(key, F(1), compute) for key in ("left", "right") + ) + assert str(caught.value) == message + + +@pytest.mark.parametrize("overrides,message", INVALID_RIB_PARAMETERS) +def test_invalid_rib_parameters_precede_arf_money_arithmetic( + monkeypatch, overrides, message +): + def forbidden(*args, **kwargs): + pytest.fail("invalid statutory bundle reached money arithmetic") + + monkeypatch.setattr(a, "worker_reduction_fraction", forbidden) + with pytest.raises(ValueError) as caught: + a.apply_arf( + rib_claim(), payment_month=FRA, params=ssa_params(**overrides) + ) + assert str(caught.value) == message + + +@pytest.mark.parametrize( + "conditions", [(), (a.ARFUnsupportedReason.OTHER_SUSPENSION,)] +) +def test_reviewer_nan_rib_rate_is_an_input_error_before_support_or_refusal( + conditions, +): + """Reviewer NaN counterexample: neither a $0 result nor a refused row.""" + params = ssa_params(early_monthly_rates=(float("nan"), 0.00416667)) + claim = rib_claim( + first_reduction_month=(2027, 1), + unreduced_benefit=F(0), + conditions=conditions, + ) + with pytest.raises(ValueError, match="finite") as caught: + a.apply_arf(claim, payment_month=FRA, params=params) + assert "early_monthly_rates[0]" in str(caught.value) + + +@pytest.mark.parametrize( + "field,index", + STATUTORY_RATE_FIELDS, + ids=[ + field if index is None else f"{field}[{index}]" + for field, index in STATUTORY_RATE_FIELDS + ], +) +@pytest.mark.parametrize( + "value", + [float("nan"), float("inf"), float("-inf")], + ids=["nan", "positive-infinity", "negative-infinity"], +) +@pytest.mark.parametrize("kind", list(a.ARFBenefitKind), ids=lambda k: k.value) +@pytest.mark.parametrize("coverage", ["ordinary", "explicit", "automatic"]) +@pytest.mark.parametrize("entrypoint", ["apply", "evaluate", "count"]) +def test_nonfinite_statutory_fields_precede_arf_refusals_and_arithmetic( + monkeypatch, field, index, value, kind, coverage, entrypoint +): + """Invalid rates/brackets cannot create benefits or unsupported rows.""" + params = nonfinite_statutory_params(field, index, value) + claim, payment = statutory_validation_claim(kind, coverage) + + def forbidden(*args, **kwargs): + pytest.fail("non-finite statutory input reached coverage or money") + + for name in ( + "statutory_rates", + "_refuse", + "worker_reduction_fraction", + "spouse_reduction_fraction", + "survivor_reduction_fraction", + "ceil_dime", + "floor_dime", + "whole_dollars", + ): + monkeypatch.setattr(a, name, forbidden) + + calls = [] + + def compute(): + calls.append("compute") + return a.apply_arf(claim, payment_month=payment, params=params) + + def evaluate(): + return a.evaluate_arf("invalid", F(3), compute) + + if entrypoint == "apply": + invoke = compute + elif entrypoint == "evaluate": + invoke = evaluate + else: + outcomes = (evaluate() for _ in range(2)) + assert calls == [] # The invalid row is not evaluated at creation. + + def invoke(): + return a.count_arf_outcomes(outcomes) + + with pytest.raises(ValueError, match="finite") as caught: + invoke() + label = field if index is None else f"{field}[{index}]" + assert label in str(caught.value) + assert calls == ["compute"] # No invalid row enters the denominator. + + +@pytest.mark.parametrize( + "field,index", + [ + ("early_monthly_rates", 0), + ("spousal_early_first_bracket_months", None), + ("survivor_reduction_floor", None), + ], +) +@pytest.mark.parametrize("value", ["NaN", "Infinity", "-Infinity"]) +def test_nonfinite_decimal_statutory_fields_are_input_errors( + field, index, value +): + """Numeric finiteness also applies to Decimal rate and bracket inputs.""" + params = nonfinite_statutory_params(field, index, Decimal(value)) + with pytest.raises(ValueError, match="finite") as caught: + a.apply_arf(rib_claim(), payment_month=FRA, params=params) + label = field if index is None else f"{field}[{index}]" + assert label in str(caught.value) diff --git a/tests/test_track_b_arf_properties.py b/tests/test_track_b_arf_properties.py new file mode 100644 index 00000000..7a77c11c --- /dev/null +++ b/tests/test_track_b_arf_properties.py @@ -0,0 +1,552 @@ +"""R3 ARF Hypothesis invariants and exact shared-surface differentials. + +Verification class: statutory conformance. All histories are invented. +G and ARF share exact age-reduction semantics, so tolerance is zero. +Widow(er)'s age-62 adjustment is the intended exception to FRA-only timing. +""" + +from dataclasses import replace +from datetime import date +from fractions import Fraction as F + +import pytest +from hypothesis import example, given, settings +from hypothesis import strategies as st + +from populace_dynamics.track_b import arf as a +from populace_dynamics.track_b import gross_benefits as g +from tests.test_track_b_arf import ( + STATUTORY_RATE_FIELDS, + months, + nonfinite_statutory_params, + rib_claim, + statutory_validation_claim, +) +from tests.track_b_gross_benefit_support import ssa_params + +PARAMS = ssa_params() +kinds = st.sampled_from(list(a.ARFBenefitKind)) +amounts = st.integers(0, 45000).map(lambda cents: F(cents, 10)) + + +def claim_for(kind, amount=F(1300)): + return a.ARFClaim( + kind, + date(1962, 1, 2), + g.YearMonth(2022 if kind is a.ARFBenefitKind.WIDOW else 2024, 1), + amount, + ) + + +def at_fra(claim): + return a.apply_arf( + claim, + payment_month=a.full_retirement_age_month( + claim.birth_date, claim.kind, PARAMS + ), + params=PARAMS, + ) + + +@settings(deadline=None) +@given(kinds, amounts, st.integers(0, 84)) +def test_arf_is_idempotent_deterministic_and_preserves_original_claim( + kind, amount, count +): + claim = claim_for(kind, amount) + original = at_fra(claim).original_reduction_months + claim = replace( + claim, + full_work_deduction_months=months( + claim.first_reduction_month, min(count, original) + ), + ) + result = at_fra(claim) + twice = a.apply_arf( + result, payment_month=result.payment_month, params=PARAMS + ) + assert twice == result == at_fra(claim) + assert result.claim.first_reduction_month == claim.first_reduction_month + assert result.original_reduction_months == original + + +@settings(deadline=None) +@example(a.ARFBenefitKind.RIB, F(1300), [0], []) +@example( + a.ARFBenefitKind.RIB, + F(1300), + [], + [a.ARFUnsupportedReason.OTHER_SUSPENSION], +) +@given( + kinds, + amounts, + st.lists(st.integers(-20, 110), max_size=100), + st.lists(st.sampled_from(list(a.ARFUnsupportedReason)), max_size=5), +) +def test_iterator_claim_evaluation_preserves_results_and_refusals( + kind, amount, offsets, conditions +): + """Every supported or refused iterator claim has stable meaning.""" + base_claim = claim_for(kind, amount) + base = base_claim.first_reduction_month.year * 12 + supplied = [ + g.YearMonth((base + i) // 12, (base + i) % 12 + 1) for i in offsets + ] + category = { + a.ARFBenefitKind.RIB: "simultaneous_dib_months", + a.ARFBenefitKind.SPOUSE: "child_in_care_months", + a.ARFBenefitKind.WIDOW: "mother_father_rate_months", + }[kind] + claim = replace( + base_claim, + full_work_deduction_months=iter(supplied), + partial_work_deduction_months=iter(supplied), + **{category: iter(supplied)}, + nonentitlement_months=iter( + supplied if kind is not a.ARFBenefitKind.RIB else [] + ), + conditions=iter(conditions), + ) + canonical = replace( + base_claim, + full_work_deduction_months=frozenset(supplied), + partial_work_deduction_months=frozenset(supplied), + **{category: frozenset(supplied)}, + nonentitlement_months=frozenset( + supplied if kind is not a.ARFBenefitKind.RIB else [] + ), + conditions=frozenset(conditions), + ) + outcomes = [ + a.evaluate_arf(str(index), F(1), lambda value=value: at_fra(value)) + for index, value in enumerate((claim, claim, canonical)) + ] + first, second, expected = outcomes + assert first.supported == second.supported == expected.supported + if first.supported: + assert first.benefits == second.benefits == expected.benefits + snapshotted_result = replace( + first.benefits, + crediting_months=iter(first.benefits.crediting_months), + ) + assert snapshotted_result == first.benefits + assert ( + frozenset(snapshotted_result.crediting_months) + == frozenset(snapshotted_result.crediting_months) + == first.benefits.crediting_months + ) + else: + assert ( + first.unsupported.reason + is second.unsupported.reason + is expected.unsupported.reason + ) + assert first.unsupported.detail == second.unsupported.detail + counts = a.count_arf_outcomes(outcomes[:2]) + assert counts.rows == counts.weight == 2 + assert ( + counts.unsupported_rows + == counts.unsupported_weight + == (0 if first.supported else 2) + ) + + +@settings(deadline=None) +@given(kinds, st.lists(st.integers(-20, 110), max_size=100)) +def test_credit_bounds_window_and_order_independence(kind, offsets): + claim = claim_for(kind) + original = at_fra(claim).original_reduction_months + first = claim.first_reduction_month + base = first.year * 12 + first.month - 1 + supplied = [ + g.YearMonth((base + i) // 12, (base + i) % 12 + 1) for i in offsets + ] + result = at_fra(replace(claim, full_work_deduction_months=supplied)) + reversed_result = at_fra( + replace(claim, full_work_deduction_months=list(reversed(supplied))) + ) + assert result == reversed_result + assert len(result.crediting_months) == len( + {i for i in offsets if 0 <= i < original} + ) + assert 0 <= len(result.crediting_months) <= original + assert ( + result.adjusted_reduction_months + len(result.crediting_months) + == original + ) + assert 0 <= result.adjusted_reduction_months <= original + + +@settings(deadline=None) +@given(kinds, amounts, st.integers(0, 84), st.integers(0, 84)) +def test_more_credits_never_lower_benefit_or_nominal_factor( + kind, amount, left, right +): + claim = claim_for(kind, amount) + original = at_fra(claim).original_reduction_months + low, high = sorted((min(left, original), min(right, original))) + fewer = at_fra( + replace( + claim, + full_work_deduction_months=months( + claim.first_reduction_month, low + ), + ) + ) + more = at_fra( + replace( + claim, + full_work_deduction_months=months( + claim.first_reduction_month, high + ), + ) + ) + assert more.benefit >= fewer.benefit + assert more.whole_dollar_benefit >= fewer.whole_dollar_benefit + assert more.factor >= fewer.factor + assert more.adjusted_reduction_months <= fewer.adjusted_reduction_months + + +@settings(deadline=None) +@given(kinds, amounts, st.integers(0, 84)) +def test_partial_deduction_equals_full_credit_and_duplicate_categories_do_not_add( + kind, amount, count +): + claim = claim_for(kind, amount) + original = at_fra(claim).original_reduction_months + credited = months(claim.first_reduction_month, min(count, original)) + full = at_fra(replace(claim, full_work_deduction_months=credited)) + partial = at_fra(replace(claim, partial_work_deduction_months=credited)) + both = at_fra( + replace( + claim, + full_work_deduction_months=credited, + partial_work_deduction_months=credited, + ) + ) + assert full.benefit == partial.benefit == both.benefit + assert ( + full.adjusted_reduction_months + == partial.adjusted_reduction_months + == both.adjusted_reduction_months + ) + assert ( + full.crediting_months + == partial.crediting_months + == both.crediting_months + ) + if kind is a.ARFBenefitKind.RIB: + simultaneous_dib = at_fra( + replace(claim, simultaneous_dib_months=credited) + ) + assert simultaneous_dib.benefit == full.benefit + assert simultaneous_dib.crediting_months == full.crediting_months + assert ( + simultaneous_dib.adjusted_reduction_months + == full.adjusted_reduction_months + ) + + +@settings(deadline=None) +@given( + st.sampled_from([a.ARFBenefitKind.RIB, a.ARFBenefitKind.SPOUSE]), + amounts, + st.integers(0, 59), +) +def test_no_rib_or_spouse_adjustment_before_fra(kind, amount, offset): + claim = claim_for(kind, amount) + claim = replace( + claim, + full_work_deduction_months=months(claim.first_reduction_month, 60), + ) + base = ( + claim.first_reduction_month.year * 12 + + claim.first_reduction_month.month + - 1 + ) + payment = g.YearMonth((base + offset) // 12, (base + offset) % 12 + 1) + result = a.apply_arf(claim, payment_month=payment, params=PARAMS) + assert result.crediting_months == frozenset() + assert result.effective_adjustment_month is None + assert ( + result.adjusted_reduction_months + == result.original_reduction_months + == 60 + ) + + +@settings(deadline=None) +@given(amounts, st.integers(0, 23), st.integers(24, 83)) +def test_widow_age62_freezes_credits_until_fra(amount, early, later): + claim = claim_for(a.ARFBenefitKind.WIDOW, amount) + base = 2022 * 12 + credited = { + g.YearMonth((base + i) // 12, (base + i) % 12 + 1) + for i in (early, later) + } + claim = replace(claim, partial_work_deduction_months=credited) + at62 = a.apply_arf(claim, payment_month=(2024, 1), params=PARAMS) + before_fra = a.apply_arf(claim, payment_month=(2028, 12), params=PARAMS) + fra = at_fra(claim) + assert at62.crediting_months == before_fra.crediting_months + assert len(at62.crediting_months) == 1 + assert at62.benefit == before_fra.benefit <= fra.benefit + assert len(fra.crediting_months) == 2 + + +@settings(deadline=None) +@given(kinds, amounts, st.integers(0, 84)) +def test_zero_credits_matches_g_exactly_and_remaining_months_differential( + kind, amount, count +): + claim = claim_for(kind, amount) + original = at_fra(claim).original_reduction_months + claim = replace( + claim, + full_work_deduction_months=months( + claim.first_reduction_month, min(count, original) + ), + ) + result = at_fra(claim) + rf = result.adjusted_reduction_months + if kind is a.ARFBenefitKind.RIB: + expected = g.worker_age_adjusted(amount, rf, 0, None, PARAMS)[0] + elif kind is a.ARFBenefitKind.SPOUSE: + expected = g.spouse_age_reduced(amount, rf, PARAMS) + else: + expected = g.survivor_age_reduced( + amount, + rf, + g.widow_reduction_period_months(claim.birth_date), + PARAMS, + ) + assert result.benefit == expected # tolerance zero, exact Fractions + assert result.whole_dollar_benefit == g.whole_dollars(expected) + assert 0 <= result.benefit <= amount + assert 0 <= result.factor <= 1 + assert (result.benefit * 10).denominator == 1 + no_credits = at_fra(replace(claim, full_work_deduction_months=frozenset())) + assert no_credits.adjusted_reduction_months == original + assert no_credits.crediting_months == frozenset() + + +@settings(deadline=None) +@example(F("26.2"), F(15), F("15.1"), 0) +@example(F(650), F(200), F(208), 0) +@given(amounts, amounts, amounts, st.integers(0, 60)) +def test_post_arf_own_payable_offset_is_independent_of_pia_and_idempotent( + unreduced, own_pia, own_payable, credited +): + """RS 00615.500A.1: subtract supplied own payment after spouse ARF. + + PIA and payable vary independently, including separately adjusted own + benefits. G supplies the same spouse reduction; tolerance is zero. + """ + own = a.SpouseOwnBenefit( + own_pia, own_payable, a.SpouseOwnSequence.AFTER_SPOUSE_ARF + ) + claim = a.ARFClaim( + a.ARFBenefitKind.SPOUSE, + date(1960, 1, 2), + (2022, 1), + unreduced, + full_work_deduction_months=months(g.YearMonth(2022, 1), credited), + spouse_own=own, + ) + result = a.apply_arf(claim, payment_month=(2028, 1), params=PARAMS) + expected = max( + F(0), + g.spouse_age_reduced(unreduced, 60 - credited, PARAMS) - own_payable, + ) + assert result.benefit == expected + assert result.whole_dollar_benefit == g.whole_dollars(expected) + changed_pia = replace( + claim, spouse_own=replace(own, pia=own_pia + F(1, 10)) + ) + assert ( + a.apply_arf( + changed_pia, payment_month=(2028, 1), params=PARAMS + ).benefit + == expected + ) + assert ( + a.apply_arf(result, payment_month=(2028, 1), params=PARAMS) == result + ) + + +@settings(deadline=None) +@given(st.dates(min_value=date(1938, 1, 1), max_value=date(2000, 12, 31))) +def test_fra_calendar_differential_with_original_cohort_schedule(birth): + """Independent month assembly; same R1/G parameter schedule, zero tolerance.""" + cohort = birth.year - int(birth.month == 1 and birth.day == 1) + age = PARAMS.fra_months(cohort) + total = birth.year * 12 + birth.month - 1 + age + expected = g.YearMonth(total // 12, total % 12 + 1) + if birth.day == 1: + total -= 1 + expected = g.YearMonth(total // 12, total % 12 + 1) + assert ( + a.full_retirement_age_month(birth, a.ARFBenefitKind.RIB, PARAMS) + == expected + ) + assert ( + a.full_retirement_age_month(birth, a.ARFBenefitKind.SPOUSE, PARAMS) + == expected + ) + + +@settings(deadline=None) +@given(st.integers(0, 100), st.integers(0, 100)) +def test_refusal_denominator_keeps_every_weight( + supported_weight, unsupported_weight +): + supported = a.evaluate_arf( + "supported", F(supported_weight), lambda: at_fra(rib_claim()) + ) + refused = a.evaluate_arf( + "refused", + F(unsupported_weight), + lambda: at_fra( + rib_claim(conditions={a.ARFUnsupportedReason.OTHER_SUSPENSION}) + ), + ) + denominator = a.count_arf_outcomes([supported, refused]) + assert ( + denominator.rows + == denominator.supported_rows + denominator.unsupported_rows + == 2 + ) + assert denominator.weight == F(supported_weight + unsupported_weight) + assert ( + denominator.weight + == denominator.supported_weight + denominator.unsupported_weight + ) + + +@settings(deadline=None) +@given(kinds, st.integers(1, 120)) +def test_invalid_age_precedes_any_disabled_widow_coverage_refusal( + kind, months_too_early +): + birth = date(1960, 1, 2) + minimum = 600 if kind is a.ARFBenefitKind.WIDOW else 744 + first_index = 1960 * 12 + minimum - months_too_early + first = g.YearMonth(first_index // 12, first_index % 12 + 1) + claim = a.ARFClaim( + kind, + birth, + first, + F(1000), + conditions={a.ARFUnsupportedReason.DISABLED_WIDOW}, + ) + with pytest.raises(g.InvalidFamilyInput): + a.apply_arf(claim, payment_month=(2027, 1), params=PARAMS) + + +@settings(deadline=None) +@example("bracket", 35, "explicit", F(0), 1) +@given( + st.sampled_from(["bracket", "first-rate", "later-rate"]), + st.integers(0, 120).filter(lambda value: value != 36), + st.sampled_from(["ordinary", "explicit", "historical", "reentitlement"]), + amounts, + st.integers(0, 100), +) +def test_invalid_statutory_parameters_never_become_arf_refusals( + field, bad_bracket, coverage, amount, weight +): + """G's bundle errors propagate identically through all ARF entrypoints.""" + overrides = { + "bracket": {"early_first_bracket_months": bad_bracket}, + "first-rate": {"early_monthly_rates": (0, 0.00416667)}, + "later-rate": {"early_monthly_rates": (0.00555556, 0)}, + }[field] + params = ssa_params(**overrides) + with pytest.raises(ValueError) as expected: + g.statutory_rates(params) + claim = rib_claim(unreduced_benefit=amount) + payment = g.YearMonth(2027, 1) + if coverage == "explicit": + claim = replace( + claim, + conditions=iter([a.ARFUnsupportedReason.OTHER_SUSPENSION]), + ) + elif coverage == "historical": + claim = replace( + claim, + birth_date=date(1915, 1, 2), + first_reduction_month=(1977, 1), + ) + elif coverage == "reentitlement": + claim = replace( + claim, + kind=a.ARFBenefitKind.SPOUSE, + nonentitlement_months={(2022, 2)}, + ) + payment = g.YearMonth(2024, 1) + + def compute(): + return a.apply_arf(claim, payment_month=payment, params=params) + + def evaluate(): + return a.evaluate_arf("invalid", F(weight), compute) + + for invoke in ( + compute, + evaluate, + lambda: a.count_arf_outcomes(evaluate() for _ in range(2)), + ): + with pytest.raises(ValueError) as caught: + invoke() + assert str(caught.value) == str(expected.value) + + +@settings(deadline=None) +@example( + ("early_monthly_rates", 0), + float("nan"), + a.ARFBenefitKind.RIB, + "explicit", + F(0), + 1, +) +@given( + st.sampled_from(STATUTORY_RATE_FIELDS), + st.sampled_from([float("nan"), float("inf"), float("-inf")]), + kinds, + st.sampled_from(["ordinary", "explicit", "automatic"]), + amounts, + st.integers(0, 100), +) +def test_nonfinite_statutory_fields_never_create_arf_results_or_refusals( + field_spec, value, kind, coverage, amount, weight +): + """Input error and lazy failure are independent of money and row weight.""" + field, index = field_spec + params = nonfinite_statutory_params(field, index, value) + claim, payment = statutory_validation_claim(kind, coverage, amount) + calls = [] + + def compute(): + calls.append("compute") + return a.apply_arf(claim, payment_month=payment, params=params) + + def evaluate(): + return a.evaluate_arf("invalid", F(weight), compute) + + outcomes = (evaluate() for _ in range(2)) + assert calls == [] + errors = [] + for invoke in ( + compute, + evaluate, + lambda: a.count_arf_outcomes(outcomes), + ): + with pytest.raises(ValueError, match="finite") as caught: + invoke() + errors.append(str(caught.value)) + label = field if index is None else f"{field}[{index}]" + assert label in errors[0] + assert errors[0] == errors[1] == errors[2] + assert calls == ["compute"] * 3 diff --git a/tests/test_track_b_r3_sources.py b/tests/test_track_b_r3_sources.py new file mode 100644 index 00000000..84a8b7db --- /dev/null +++ b/tests/test_track_b_r3_sources.py @@ -0,0 +1,506 @@ +"""R3 statutory captures: byte integrity and verbatim rule/example text. + +Verification class: statutory conformance, for supported ARF and +recomputation mechanics only. No population or forecast evidence. +""" + +import hashlib +import inspect +import re +from datetime import date +from fractions import Fraction +from pathlib import Path + +import pytest + +from populace_dynamics.track_b import arf, gross_benefits +from tests.track_b_arf_recompute_support import SOURCES, manifest, source_text +from tests.track_b_gross_benefit_support import ( + SOURCES as GROSS_SOURCES, +) +from tests.track_b_gross_benefit_support import ( + manifest as gross_manifest, +) +from tests.track_b_gross_benefit_support import ( + source_text as gross_source_text, +) +from tests.track_b_gross_benefit_support import ( + ssa_params, +) + + +@pytest.mark.parametrize( + ("function_name", "decision", "conflict_id"), + [ + pytest.param( + "full_retirement_age_month", + "ARFUnsupportedReason.HISTORICAL_WIDOW_FRA_CONFLICT,", + "us:oasdi-survivor-fra-1911-births", + id="historical-fra-helper", + ), + pytest.param( + "apply_arf", + "ARFUnsupportedReason.HISTORICAL_WIDOW_FRA_CONFLICT,", + "us:oasdi-survivor-fra-1911-births", + id="historical-fra-application", + ), + pytest.param( + "apply_arf", + "if claim.conditions:", + "us:oasdi-arf-widow-child-in-care-credits", + id="nonbinding-child-care-refusal", + ), + pytest.param( + "apply_arf", + "| claim.mother_father_rate_months", + "us:oasdi-arf-widow-child-in-care-credits", + id="certified-mother-father-rate-credit", + ), + pytest.param( + "apply_arf", + "ARFUnsupportedReason.SPOUSE_REENTITLEMENT_BEFORE_FRA,", + "us:oasdi-arf-spouse-reentitlement-credits", + id="spouse-reentitlement-refusal", + ), + ], +) +def test_source_conflict_markers_are_adjacent_to_runtime_decisions( + function_name, decision, conflict_id +): + """Connect each statutory reading to its source-conflict register.""" + lines = [ + line.strip() + for line in inspect.getsource(getattr(arf, function_name)).splitlines() + ] + decision_line = lines.index(decision) + assert ( + f"# Source conflict {conflict_id}" + in lines[max(0, decision_line - 4) : decision_line] + ) + + +def test_manifest_pins_every_capture(): + document = manifest() + assert document["schema_version"] == 1 + assert set(document["capture_formats"]) == { + "http_body", + "http_text_excerpt", + "browser_text", + } + entries = document["sources"] + assert len({entry["id"] for entry in entries}) == len(entries) + for entry in entries: + data = (SOURCES / entry["file"]).read_bytes() + assert len(data) == entry["bytes"], entry["id"] + assert hashlib.sha256(data).hexdigest() == entry["sha256"] + assert entry["url"].startswith("https://") + assert re.fullmatch( + r"2026-10-11T\d\d:\d\d:\d\dZ", entry["retrieved_at_utc"] + ) + assert entry["capture"] in document["capture_formats"] + if entry["id"].startswith("poms_"): + assert re.fullmatch( + r"TN \d+ \(\d\d-\d\d\)", entry["poms_transmittal"] + ) + assert re.fullmatch(r"\d\d/\d\d/\d{4}", entry["page_date"]) + if entry["id"].startswith("ecfr_"): + assert "2026-10-07" in entry["url"] + if entry["capture"] == "http_body": + assert "served_bodies" not in entry + else: + urls = [entry["url"], *entry.get("additional_urls", [])] + assert [b["url"] for b in entry["served_bodies"]] == urls + for body in entry["served_bodies"]: + assert re.fullmatch(r"[0-9a-f]{64}", body["sha256"]) + assert body["bytes"] > len(data) + assert body["retrieved_at_utc"] == entry["retrieved_at_utc"] + listed = {entry["file"] for entry in entries} + assert {p.name for p in SOURCES.iterdir()} - listed == { + "README.md", + "manifest.json", + } + + +def test_ssa_full_pdf_capture_provenance_names_separate_r2_pr(): + entry = next( + source + for source in manifest()["sources"] + if source["id"] == "ssa_pub_05_10069_arf_recompute_excerpts" + ) + assert "R2 PR #528" in entry["note"] + assert "../ret_ledger_sources/" not in entry["note"] + + +RULE_QUOTES = { + "poms_rs_00615_003": [ + "FRA for retirement or spouse's benefits is age 65." + ], + "poms_rs_00615_004": [ + "Attainment of FRA will always occur in the same month for all possible types of benefits." + ], + "poms_rs_00615_010": [ + "DIB is not reduced unless there was prior entitlement to reduced RIB (or WIB prior to 1/90)." + ], + "poms_rs_00615_015": [ + "Social Security follows English common law that finds that a person attains an age on the day before the birthday." + ], + "poms_rs_00615_480": [ + "This adjustment is automatic; i.e., no application or request is needed." + ], + "poms_rs_00615_482": [ + "full or partial work deduction; or", + "grant ARFs separately on each record.", + ], + "poms_rs_00615_490": ["These adjustments are effective at FRA."], + "poms_rs_00615_491": [ + "all months of termination will be eliminated from the reduction months:" + ], + "poms_rs_00615_500": [ + "Entitlement to RIB or DIB does not affect the ARF for the spouse benefit.", + "The adjusted spouse reduction factor is applied to the excess spouse's benefit.", + ], + "poms_rs_00615_580": ["Different methods are used for these adjustments."], + "poms_rs_00615_582": [ + "Compute the rates at 62 and FRA by removing crediting months." + ], + "poms_rs_00615_585": ["Pay no more than RIB limitation."], + "poms_rs_00615_590": [ + "Determine the reduction factor as if the widow(er) attains age 60 in the MOET." + ], + "poms_rs_00615_595": [ + "The adjusted reduction factor is applied to the excess of the WIB PIA over the DIB PIA." + ], + "poms_rs_00615_598": [ + "Consider as crediting months only months NH was alive." + ], + "poms_rs_00605_005": [ + "The benchmark year for RIB is the year of age 62 attainment" + ], + "poms_rs_00605_401": [ + "A recomputation to allow credit for earnings in the year of death is effective the month of death." + ], + "poms_rs_00605_405": [ + "A recomputation is a new initial determination.", + "If the resulting PIA is lower than the PIA of record, the recomputation will not be processed.", + ], + "poms_rs_00605_407": [ + "A recomputation of the PIA can result in a decrease in total family benefits." + ], + "poms_rs_00605_501": [ + "If the increase over the PIA of record after considering all applicable COLAs up to the effective date of the recomputation is less than $1.00, a recomputation is not due.", + "If the NH had no earnings for 1998 we could not consider the recomputation for 1/ 1999.", + ], + "poms_rs_00605_560": [ + "AERO is the process that automatically screens ERs that have changes in earnings information and computes the necessary changes." + ], + "poms_rs_00605_570": [ + "When an increase is not due, a notice is required." + ], + "poms_rs_00605_580": [ + "When a prior computation is incorrect, any necessary recalculation is made under the rules applicable to the computation being corrected." + ], + "ecfr_20cfr_404_280": [ + "There is no limit on the number of times your primary insurance amount may be recomputed, and we do most recomputations automatically." + ], + "ecfr_20cfr_404_281": [ + "Earnings that you have after you become entitled to benefits will be used in a recomputation of your primary insurance amount." + ], + "ecfr_20cfr_404_282": [ + "However, a recomputation to include earnings in the year of death (whether or not paid before death) is effective for the month of death." + ], + "ecfr_20cfr_404_283": [ + "In some cases, we may recompute your primary insurance amount under a computation method different from the method used in the computation (or earlier recomputation) of your primary insurance amount" + ], + "ecfr_20cfr_404_284": [ + "The formula that was used in the first computation of your primary insurance amount is also used in recomputations of your primary insurance amount." + ], + "ecfr_20cfr_404_285": ["You do not have to request this service"], + "ecfr_20cfr_404_286": [ + "we cannot begin paying higher benefits on the recomputed primary insurance amount any sooner than we could under an automatic recomputation" + ], + "ecfr_20cfr_404_287": [ + "That you waive one recomputation, however, does not mean that you also waive future recomputations for which you might be eligible." + ], + "ecfr_20cfr_404_288": [ + "We will disregard the rule in § 404.284(e) that the recomputation must increase your primary insurance amount by at least $1." + ], + "ecfr_20cfr_404_409": [ + "You may receive unreduced old-age, wife's, or husband's benefits beginning with the month you attain the age shown." + ], + "ecfr_20cfr_404_410": [ + "The reduction is 5/9 of 1 percent for each of the first 36 months and 5/12 of 1 percent for each month in excess of 36." + ], + "ecfr_20cfr_404_411": [ + "Such age reductions are made before any reduction under the provisions of § 404.407." + ], + "ecfr_20cfr_404_412": [ + "Increases in benefit amounts based upon this adjustment are effective with the month of attainment of full retirement age." + ], +} + +RULE_QUOTES.update( + { + "lii_usc_402q_415f_excerpts": [ + "If the amount of any reduction computed under paragraph (1), (2), or (3) is not a multiple of $0.10, it shall be increased to the next higher multiple of $0.10.", + "A recomputation shall be effective under this subsection only if it increases the primary insurance amount by at least $1.", + ], + "ssa_pub_05_10069_arf_recompute_excerpts": [ + "your monthly benefit will increase starting at your full retirement age.", + "This is an automatic process, and benefits are paid in December of the following year.", + ], + } +) + + +@pytest.mark.parametrize( + ("source_id", "quote"), + [ + (source, quote) + for source, quotes in RULE_QUOTES.items() + for quote in quotes + ], +) +def test_rule_text_is_quoted_from_the_captures(source_id, quote): + assert quote in source_text(source_id) + + +def test_every_capture_is_used_by_a_substantive_rule_test(): + assert set(RULE_QUOTES) == { + source["id"] for source in manifest()["sources"] + } + + +WORKED_EXAMPLE_QUOTES = { + "ssa_pub_05_10069_arf_recompute_excerpts": [ + "$910", + "$975", + "$1,300", + "12 months", + "62", + "67", + "2026", + ], + "poms_rs_00615_015": [ + "5/31/1935", + "6/1/1935", + "6/2/1935", + "6/3/1935", + "12/31/1935", + "1/1/1936", + "1/2/1936", + "1/3/1936", + "12/31/1937", + "1/1/1938", + "1/2/1938", + "1/3/1938", + "$1207", + "$972", + "$965", + "$1232", + "$1266", + "$992", + "$1012", + "$1019", + "$1372", + "$1410", + "$1105", + "$1116", + "$1122", + "35 RF", + "36 RF", + "37 RF", + "38 RF", + "age 65 and 2 months", + "In 2/1998 Demi has 37 RF.", + "Demi - $1122 starting 2/2000", + "6/1997", + "7/1997", + "1/1998", + "2/1998", + "1/2000", + "2/2000", + ], + "poms_rs_00605_501": [ + "$849.40", + "$850.00", + "$850.40", + "$1.00", + "$445.50", + "$.90", + "$451.20", + "$452.20", + "12/1997", + "1 /1998", + "12/ 1998", + "1/1999", + ], + # Example 1 is deliberately refused by modern eligibility scope, not + # reproduced using modern floor rounding. Keep all historical figures + # beside that refusal and the historical arithmetic demonstration. + "ecfr_20cfr_404_284": [ + "$432.40", + "$903", + "$11,000", + "$8,911.36", + "$251,470.05", + "$911", + "$396.00", + "9.9 percent", + "$435.30", + "14.3 percent", + "$497.60", + "11.2 percent", + "$553.40", + "$12,000", + "$2,700", + "$537", + "$407.70", + "$569.60", + "$429.20", + "$502", + "$142,000", + ], +} + + +@pytest.mark.parametrize( + ("source_id", "quote"), + [ + (source, quote) + for source, quotes in WORKED_EXAMPLE_QUOTES.items() + for quote in quotes + ], +) +def test_worked_example_figures_are_quoted_from_the_captures(source_id, quote): + assert quote in source_text(source_id) + + +READING_QUOTES = { + "poms_rs_00615_003": [ + "People Born Before 1/2/1911", + "FRA for widow(er)'s benefits was age 62", + "People Born from 1/2/1911 Through 1/1/40", + ], + "ecfr_20cfr_404_409": [ + "Before 1/2/1912 62 years.", + "1/2/1912—1/1/1940 65 years.", + ], + "ecfr_20cfr_404_412": [ + "any month in which she or he had in her or his care a child of the deceased insured individual and for which the child was entitled to child's benefits;" + ], + "poms_rs_00615_482": [ + "widow(er)'s benefits were increased to the mother or father's rate." + ], + "lii_usc_402q_415f_excerpts": [ + "any month in which the reduction in the amount of such benefit was determined under paragraph (5)(D)" + ], +} + + +@pytest.mark.parametrize( + ("source_id", "quote"), + [ + (source, quote) + for source, quotes in READING_QUOTES.items() + for quote in quotes + ], +) +def test_alternative_readings_are_quoted_from_captures(source_id, quote): + assert quote in source_text(source_id) + + +SPOUSE_REENTITLEMENT_QUOTES = { + "RS 00615.201A.1": ( + "After 12/72 a spouse reentitled prior to FRA as a spouse on the " + "same earnings is reduced the same number of months as the " + "earlier entitlement. An ARF at FRA will remove all months of " + "nonentitlement." + ), + "RS 00615.491A": ( + "Beginning in 1/78, if a reduced spouse is terminated and then " + "reentitled, (months in which the spouse's benefits were " + "terminated will be eliminated from the reduction factor when " + "the spouse is reentitled." + ), +} + + +def test_spouse_reentitlement_conflict_quotes_and_refusal_are_documented(): + """Both already captured POMS readings must remain visible to review.""" + # The .201 source is listed in G's manifest rather than R3's. Check + # those referenced bytes here as well as the verbatim quoted text. + entry = next( + s + for s in gross_manifest()["sources"] + if s["id"] == "poms_rs_00615_201" + ) + data = (GROSS_SOURCES / entry["file"]).read_bytes() + assert len(data) == entry["bytes"] + assert hashlib.sha256(data).hexdigest() == entry["sha256"] + texts = { + "RS 00615.201A.1": gross_source_text("poms_rs_00615_201"), + "RS 00615.491A": source_text("poms_rs_00615_491"), + } + note = ( + Path(__file__).parents[1] / "docs/design/track_b_r3_arf_recompute.md" + ).read_text() + judgment = " ".join( + note.split("## Findings and readings requiring judgment", 1)[1] + .split("## Monthly assembly inputs", 1)[0] + .split() + ) + refusal_row = next( + line + for line in note.splitlines() + if line.startswith("| Earlier reentitled spouse adjustment |") + ) + for authority, quote in SPOUSE_REENTITLEMENT_QUOTES.items(): + assert quote in texts[authority] + assert quote in judgment + assert authority in refusal_row + assert "conflict" in refusal_row + assert "arf_spouse_reentitlement_before_fra" in refusal_row + for amount in ("$422.50", "$425.20", "$2.70"): + assert amount in judgment + + +def test_spouse_reentitlement_counterfactual_readings_keep_named_refusal(): + """Invented 1960-01-02 spouse: $650, 60 RF versus 59 RF. + + First entitlement 2022-01, termination 2022-02, reentitlement + 2022-03. The .201 reading preserves 60 RF: 35% reduction gives + $422.50/$422. The .491 reading removes the terminated month: 59 RF + has reduction 83/240, rounded upward from $224.7916... to $224.80, + giving $425.20/$425. These are counterfactual arithmetic diagnostics; + neither reading is admitted by the refused pre-FRA claim. + """ + params = ssa_params() + unreduced = Fraction(650) + amounts = tuple( + gross_benefits.floor_dime( + unreduced + - gross_benefits.ceil_dime( + unreduced + * gross_benefits.spouse_reduction_fraction(months, params) + ) + ) + for months in (60, 59) + ) + assert amounts == (Fraction("422.50"), Fraction("425.20")) + assert tuple(map(gross_benefits.whole_dollars, amounts)) == (422, 425) + assert amounts[1] - amounts[0] == Fraction("2.70") + claim = arf.ARFClaim( + kind=arf.ARFBenefitKind.SPOUSE, + birth_date=date(1960, 1, 2), + first_reduction_month=(2022, 1), + unreduced_benefit=unreduced, + nonentitlement_months=frozenset({(2022, 2)}), + ) + with pytest.raises( + gross_benefits.FamilyConfigurationUnsupported + ) as caught: + arf.apply_arf(claim, payment_month=(2022, 3), params=params) + assert ( + caught.value.reason + is arf.ARFUnsupportedReason.SPOUSE_REENTITLEMENT_BEFORE_FRA + ) diff --git a/tests/test_track_b_recompute.py b/tests/test_track_b_recompute.py new file mode 100644 index 00000000..bdba4a68 --- /dev/null +++ b/tests/test_track_b_recompute.py @@ -0,0 +1,807 @@ +"""R3 statutory conformance: captured rules and invented earnings, no data. + +All reference earnings are invented. POMS RS 00605.501's PIA-only examples +are reproduced at the decision boundary because SSA supplies no earnings +history. Each invented worked case spells out its intermediate figures. +Original insured status and entitlement are supplied and assumed; these +arithmetic cases do not verify quarters of coverage or insured status. +""" + +import html +import json +import math +import re +from dataclasses import replace +from fractions import Fraction as F +from hashlib import sha256 +from unittest.mock import patch + +import pytest + +from populace_dynamics.ss.statutory_aime import aime_with_computation_years +from populace_dynamics.track_b import recompute as r +from populace_dynamics.track_b.gross_benefits import ( + FamilyConfigurationUnsupported, + FamilyTargetBlocked, + InvalidFamilyInput, + YearMonth, + ceil_dime, + count_family_outcomes, + increase_by_colas, + whole_dollars, +) +from tests.track_b_gross_benefit_support import TRACK_B_DATA, ssa_params + +PARAMS = ssa_params(wage_base={1951: 1_000_000}) + + +def basis(**overrides): + values = dict( + eligibility_year=2020, + indexing_year=2018, + computation_years=35, + bend_points=r.eligibility_bend_points(2020, PARAMS), + first_base_year=1951, + entitlement_month=YearMonth(2020, 6), + ) + values.update(overrides) + return r.RecomputationBasis(**values) + + +def explicit_early_zeros(values): + """Invented history explicitly sets every 1951-2017 year to zero.""" + return {year: 0 for year in range(1951, 2018)} | values + + +def annual(**overrides): + values = dict( + basis=basis(), + history={2018: 21000, 2019: 42000, 2020: 63000}, + earnings_year=2020, + eligibility_pia_of_record=135, + params=PARAMS, + december_cola_percent={2020: 0}, + ) + values.update(overrides) + values["history"] = explicit_early_zeros(values["history"]) + return r.annual_recomputation(**values) + + +def death_annual(month=11, cola=0): + """Invented DOB1958-06-02: ELY2020, FRA2025-02, death2026.""" + history = {year: 0 for year in range(1951, 2027)} + history.update({2018: 21000, 2019: 42000, 2026: 63000}) + return annual( + history=history, + earnings_year=2026, + death_month=YearMonth(2026, month), + fra_month=YearMonth(2025, 2), + december_cola_percent={year: 0 for year in range(2020, 2026)} + | {2026: cola}, + ) + + +def test_invented_ordinary_recomputation_preserves_original_basis(): + """35 years: 21000+42000 /420=150; PIA135.0; 33 explicit zeros. + + New63000 replaceszero; revised126000/420=300; original2020 bend + points960/5785; .9*300=270. + 10% December2020 COLA: old148.5, candidate297.0, increase148.5. + Effective January2021; whole-dollar reference PIAs148 and297. + """ + result = annual(december_cola_percent={2020: 10}) + assert basis().bend_points == (960, 5785) + assert result.aime == 300 + assert result.candidate_eligibility_pia == F(270) + assert result.decision.pia_of_record == F("148.5") + assert result.decision.candidate_pia == F(297) + assert result.decision.increase == F("148.5") + assert result.decision.applied + assert result.effective_month == YearMonth(2021, 1) + assert whole_dollars(result.decision.pia_of_record) == 148 + assert whole_dollars(result.decision.pia_in_force) == 297 + # RS00605.401's published date example: earnings1999 -> January2000. + # Invented amount42000/420=AIME100 ->PIA90 with an explicit zeroCOLA. + history_1999 = {year: 0 for year in range(1951, 2000)} + history_1999[1999] = 42000 + original_1999 = basis( + eligibility_year=1999, + indexing_year=1997, + bend_points=r.eligibility_bend_points(1999, PARAMS), + entitlement_month=YearMonth(1999, 6), + ) + published_date = r.annual_recomputation( + original_1999, history_1999, 1999, 0, PARAMS, {1999: 0} + ) + assert published_date.effective_month == YearMonth(2000, 1) + assert published_date.decision.pia_in_force == 90 + + +@pytest.mark.parametrize("increase", [F(d, 10) for d in range(1, 10)]) +def test_sub_dollar_increase_is_not_effective(increase): + decision = r.recomputation_decision(100, 100 + increase) + assert not decision.applied + assert decision.pia_in_force == 100 + + +def test_poms_rs00605501_actual_pia_not_whole_dollar_threshold(): + """SSA: $849.40->$850.00 is $.60; $850.40 is the $1 boundary.""" + low = r.recomputation_decision("849.40", "850.00") + sufficient = r.recomputation_decision("849.40", "850.40") + assert low.increase == F("0.60") and not low.applied + assert ( + whole_dollars(low.candidate_pia) - whole_dollars(low.pia_of_record) + == 1 + ) + assert sufficient.increase == 1 and sufficient.applied + + +def test_poms_rs00605501_sub_dollar_then_later_earnings(): + """SSA PIA-only example: 445.50 +.90 refused January1998. + + December1998 old451.20; January1999 candidate452.20; $1 accepted, + including when that year's positive earnings do not enter the top set. + Without1998 earnings the same candidate cannot trigger recomputation. + """ + first = r.recomputation_decision("445.50", F("445.50") + F(".90")) + second = r.recomputation_decision("451.20", "452.20") + no_earnings = r.recomputation_decision( + "451.20", "452.20", has_earnings=False + ) + assert first.candidate_pia == F("446.40") + assert not first.applied and first.pia_in_force == F("445.50") + historical_colas = json.loads( + (TRACK_B_DATA / "ret_history.json").read_text() + ) + cola_1998 = F(historical_colas["december_cola_percent"]["1998"]) + assert cola_1998 == F("1.3") + ret_sources = TRACK_B_DATA / "ret_sources" + ret_manifest = json.loads((ret_sources / "manifest.json").read_text()) + pin = next( + p for p in ret_manifest["sources"] if p["id"] == "ssa_cola_history" + ) + capture = (ret_sources / pin["file"]).read_bytes() + assert len(capture) == pin["bytes"] + assert sha256(capture).hexdigest() == pin["sha256"] + captured_text = " ".join( + html.unescape(re.sub("<[^>]*>", " ", capture.decode())).split() + ) + assert "1998 1.3" in captured_text + assert increase_by_colas(first.pia_in_force, [cola_1998]) == F("451.20") + assert increase_by_colas(first.candidate_pia, [cola_1998]) == F("452.20") + assert second.applied and second.pia_in_force == F("452.20") + assert not no_earnings.applied and no_earnings.pia_in_force == F("451.20") + + +def test_invented_sub_dollar_fold_compares_against_pia_in_force(): + """35 years, original42000/420=100, PIA90.0; +420 =>101/90.9. + + The .90 increase is refused. Another420 =>102/91.8; against the + original90.0 this raises1.8, accepted January2022. Zero COLAs. + """ + original = basis(computation_years=35) + history = {year: 0 for year in range(1951, 2022)} + history.update({2019: 42000, 2020: 420, 2021: 420}) + results = r.recompute_sequence( + original, history, [2020, 2021], 90, PARAMS, {2020: 0, 2021: 0} + ) + assert [v.aime for v in results] == [101, 102] + assert [v.candidate_eligibility_pia for v in results] == [ + F("90.9"), + F("91.8"), + ] + assert [v.eligibility_pia_in_force for v in results] == [F(90), F("91.8")] + assert results[1].decision.increase == F("1.8") + assert results[1].effective_month == YearMonth(2022, 1) + + +def test_invented_dollar_threshold_is_compared_after_colas(): + """AIME100/PIA90.0 -> AIME101/PIA90.9; 12% COLA =>100.8/101.8. + + The eligibility-level .90 becomes an actual $1 PIA increase. + """ + history = {year: 0 for year in range(1951, 2021)} + history.update({2019: 42000, 2020: 420}) + result = annual( + basis=basis(computation_years=35), + history=history, + eligibility_pia_of_record=90, + december_cola_percent={2020: 12}, + ) + assert result.candidate_eligibility_pia == F("90.9") + assert result.decision.pia_of_record == F("100.8") + assert result.decision.candidate_pia == F("101.8") + assert result.decision.applied + + +def test_invented_recomputation_uses_original_bend_points(): + """AIME1000 with 2020 bends960/5785 =>864+12.8=876.8. + + 2021 bends996/6002 yield896.4+1.28=897.6, an intended $20.8 divergence. + """ + result = annual( + history={2018: 0, 2019: 0, 2020: 0, 2021: 420000}, + earnings_year=2021, + eligibility_pia_of_record=0, + december_cola_percent={2020: 0, 2021: 0}, + ) + assert result.aime == 1000 + assert result.candidate_eligibility_pia == F("876.8") + assert r.exact_pia(1000, r.eligibility_bend_points(2021, PARAMS)) == F( + "897.6" + ) + + +def test_unavailable_and_absent_base_years_never_become_zero(): + for history in ( + {2018: r.UNAVAILABLE, 2019: 42000, 2020: 63000}, + {2019: 42000, 2020: 63000}, + ): + with pytest.raises(InvalidFamilyInput, match="2018"): + annual(history=history) + assert annual(history={2018: 0, 2019: 42000, 2020: 63000}).aime == 250 + + +def test_first_statutory_base_year_cannot_be_hidden(): + history = {year: 0 for year in range(1952, 2021)} + with pytest.raises(InvalidFamilyInput, match="1951"): + r.annual_recomputation(basis(), history, 2020, 0, PARAMS, {2020: 0}) + with pytest.raises(InvalidFamilyInput, match="1951"): + basis(first_base_year=2018) + + +def test_invented_aime_wage_caps_and_original_indexing_year(): + """1951 earnings2799.16 index to52145.80 at original2018 NAWI. + + With2019 earnings42000 and2020 earnings63000, total157145.80/420 floor374; + original2020 bends960/5785 =>.9*374=336.6. Later NAWI is + immaterial. Annual caps1000/2000/3000 instead give total6000/420=14. + """ + history = {2018: 0, 2019: 42000, 2020: 63000, 1951: F("2799.16")} + result = annual(history=history) + assert result.aime == 374 + assert result.candidate_eligibility_pia == F("336.6") + changed = ssa_params( + nawi=PARAMS.nawi | {2019: 1_000_000}, + wage_base={1951: 1_000_000}, + ) + assert result == annual(history=history, params=changed) + capped = ssa_params(wage_base={1951: 1000, 2019: 2000, 2020: 3000}) + assert annual(params=capped).aime == 14 + assert annual(params=capped).candidate_eligibility_pia == F("12.6") + huge = {year: F(10) ** 400 for year in (2018, 2019, 2020)} + assert annual(params=capped, history=huge).aime == 14 + + +def test_later_earnings_never_enter_earlier_recomputation(): + base = annual() + changed = annual( + history={2018: 21000, 2019: 42000, 2020: 63000, 2021: 1_000_000} + ) + assert base == changed + unavailable_later = annual( + history={2018: 21000, 2019: 42000, 2020: 63000, 2021: r.UNAVAILABLE} + ) + assert base == unavailable_later + + +def test_death_year_after_fra_effective_death_month_and_cola_cutoff(): + """PIA135->270 effective November2026; December2026 COLA excluded. + + Invented DOB1958-06-02, RIB June2020, FRA February2025, death2026. + Original63000/420=150/135; revised126000/420=300/270; zero priorCOLAs. + December death includes10% December2026 COLA: candidate297. + """ + result = death_annual(11, 99) + assert result.effective_month == YearMonth(2026, 11) + assert result.decision.pia_of_record == 135 + assert result.decision.candidate_pia == 270 + december = death_annual(12, 10) + assert december.decision.candidate_pia == 297 + + +def test_death_before_fra_is_named_refusal(): + with pytest.raises(FamilyConfigurationUnsupported) as error: + annual(death_month=YearMonth(2020, 11), fra_month=YearMonth(2025, 2)) + assert ( + error.value.reason is r.RecomputationUnsupportedReason.DEATH_BEFORE_FRA + ) + + +def test_death_before_fra_not_assumed_when_fra_unavailable(): + with pytest.raises(InvalidFamilyInput, match="FRA"): + annual(death_month=YearMonth(2020, 11)) + + +def test_later_death_before_fra_does_not_change_earlier_recomputation(): + assert annual() == annual( + death_month=YearMonth(2021, 11), fra_month=YearMonth(2025, 2) + ) + + +def test_later_death_refuses_entire_sequence_before_any_arithmetic(): + history = {year: 0 for year in range(1951, 2022)} + with ( + patch.object( + r, "_aime_from_prefix", wraps=r._aime_from_prefix + ) as adapter, + patch.object( + r, + "aime_with_computation_years", + wraps=r.aime_with_computation_years, + ) as aime, + patch.object(r, "exact_pia", wraps=r.exact_pia) as pia, + ): + with pytest.raises(FamilyConfigurationUnsupported) as error: + r.recompute_sequence( + basis(), + history, + [2020, 2021], + 0, + PARAMS, + {2020: 0, 2021: 0}, + death_month=YearMonth(2021, 11), + fra_month=YearMonth(2025, 2), + ) + assert ( + error.value.reason + is r.RecomputationUnsupportedReason.DEATH_BEFORE_FRA + ) + assert (adapter.call_count, aime.call_count, pia.call_count) == ( + 0, + 0, + 0, + ) + + +def test_invalid_original_count_precedes_later_death_sequence_refusal(): + history = {year: 0 for year in range(1951, 2022)} + with pytest.raises(InvalidFamilyInput, match="computation count"): + r.recompute_sequence( + basis(computation_years=2), + history, + [2020, 2021], + 0, + PARAMS, + {2020: 0, 2021: 0}, + death_month=YearMonth(2021, 11), + fra_month=YearMonth(2025, 2), + ) + + +def test_ordinary_rib_original_count_must_match_known_eligibility_basis(): + with pytest.raises(InvalidFamilyInput, match="computation count"): + annual(basis=basis(computation_years=2)) + + +@pytest.mark.parametrize("condition", list(r.RecomputationCondition)) +def test_exceptional_conditions_have_named_refusals(condition): + with pytest.raises(FamilyConfigurationUnsupported) as error: + annual(basis=replace(basis(), conditions=frozenset({condition}))) + assert error.value.code == "FAMILY_CONFIG_UNSUPPORTED" + assert ( + error.value.reason is r.RecomputationUnsupportedReason[condition.name] + ) + + +@pytest.mark.parametrize( + "formula", [f for f in r.PIAFormula if f is not r.PIAFormula.AIME] +) +def test_non_aime_formulas_are_refused(formula): + with pytest.raises(FamilyConfigurationUnsupported) as error: + annual(basis=replace(basis(), formula=formula)) + assert ( + error.value.reason + is r.RecomputationUnsupportedReason.NON_AIME_FORMULA_PIA + ) + + +@pytest.mark.parametrize( + "formula", [f for f in r.PIAFormula if f is not r.PIAFormula.AIME] +) +@pytest.mark.parametrize("sequence", [False, True]) +def test_refused_formula_never_calculates_ordinary_aime_or_pia( + formula, sequence +): + """Review's minimized AMW case: complete zero history, ELY2020.""" + original = basis(formula=formula) + history = {year: 0 for year in range(1951, 2021)} + with ( + patch.object( + r, "_aime_from_prefix", wraps=r._aime_from_prefix + ) as adapter, + patch.object( + r, + "aime_with_computation_years", + wraps=r.aime_with_computation_years, + ) as aime, + patch.object(r, "exact_pia", wraps=r.exact_pia) as pia, + ): + with pytest.raises(FamilyConfigurationUnsupported) as error: + if sequence: + r.recompute_sequence( + original, history, [2020], 0, PARAMS, {2020: 0} + ) + else: + r.annual_recomputation( + original, history, 2020, 0, PARAMS, {2020: 0} + ) + assert ( + error.value.reason + is r.RecomputationUnsupportedReason.NON_AIME_FORMULA_PIA + ) + assert (adapter.call_count, aime.call_count, pia.call_count) == ( + 0, + 0, + 0, + ) + + +@pytest.mark.parametrize("sequence", [False, True]) +@pytest.mark.parametrize( + "params", + [ + replace(PARAMS, nawi=PARAMS.nawi | {2018: 0}), + replace(PARAMS, nawi=PARAMS.nawi | {1951: -1}), + replace(PARAMS, nawi=PARAMS.nawi | {2018: float("nan")}), + replace( + PARAMS, nawi={k: v for k, v in PARAMS.nawi.items() if k != 2018} + ), + replace( + PARAMS, nawi={k: v for k, v in PARAMS.nawi.items() if k != 1951} + ), + replace(PARAMS, wage_base={}), + replace(PARAMS, wage_base={1951: float("inf")}), + replace(PARAMS, wage_base={1951: 1_000_000, 2021: 0}), + ], + ids=[ + "zero-original-nawi", + "negative-early-nawi", + "nonfinite-original-nawi", + "missing-original-nawi", + "missing-early-nawi", + "missing-wage-base", + "nonfinite-wage-base", + "invalid-later-wage-base", + ], +) +def test_refused_formula_validates_all_parameters_without_calculation( + params, sequence +): + original = basis(formula=r.PIAFormula.AMW) + history = {year: 0 for year in range(1951, 2022)} + with ( + patch.object( + r, "_aime_from_prefix", wraps=r._aime_from_prefix + ) as adapter, + patch.object(r, "exact_pia", wraps=r.exact_pia) as pia, + ): + with pytest.raises(InvalidFamilyInput): + if sequence: + r.recompute_sequence( + original, + history, + [2020, 2021], + 0, + params, + {2020: 0, 2021: 0}, + ) + else: + r.annual_recomputation( + original, history, 2021, 0, params, {2020: 0, 2021: 0} + ) + assert (adapter.call_count, pia.call_count) == (0, 0) + + +@pytest.mark.parametrize("sequence", [False, True]) +@pytest.mark.parametrize("field", ["earnings", "cola"]) +def test_refused_formula_validates_complete_histories_before_calculation( + field, sequence +): + original = basis(formula=r.PIAFormula.AMW) + history = {year: 0 for year in range(1951, 2022)} + colas = {2020: 0, 2021: 0} + (history if field == "earnings" else colas)[2021] = r.UNAVAILABLE + with patch.object( + r, "_aime_from_prefix", wraps=r._aime_from_prefix + ) as adapter: + with pytest.raises(InvalidFamilyInput, match="2021"): + if sequence: + r.recompute_sequence( + original, history, [2020, 2021], 0, PARAMS, colas + ) + else: + r.annual_recomputation( + original, history, 2021, 0, PARAMS, colas + ) + assert adapter.call_count == 0 + + +@pytest.mark.parametrize( + "conditions", [[], [r.RecomputationCondition.ADMINISTRATIVE_FINALITY]] +) +def test_iterator_conditions_are_frozen_for_repeated_evaluation(conditions): + original = basis(conditions=iter(conditions)) + assert original.conditions == frozenset(conditions) + assert isinstance(original.conditions, frozenset) + first = r.evaluate_recomputation( + "first", 1, lambda: annual(basis=original) + ) + second = r.evaluate_recomputation( + "second", 1, lambda: annual(basis=original) + ) + assert first.result == second.result + assert first.supported == second.supported == (not conditions) + if conditions: + assert first.unsupported.reason == second.unsupported.reason + assert count_family_outcomes([first, second]).unsupported_rows == 2 + + +def test_cfr1979_roundup_example_explicitly_outside_modern_scope(): + """CFR404.284 Ex1: AIME911 gives raw395.92, historical roundup396.00. + + Historical COLAs9.9/14.3/11.2 give435.30/497.60/553.40. G's modern + floor yields395.90/435.00/497.20/552.80; historical formula refused. + """ + old = basis( + eligibility_year=1979, + indexing_year=1977, + computation_years=23, + first_base_year=1951, + entitlement_month=YearMonth(1979, 6), + bend_points=(180, 1085), + ) + assert r.exact_pia(911, old.bend_points) == F("395.9") + with pytest.raises(FamilyConfigurationUnsupported) as error: + annual( + basis=old, + history={1977: 0, 1978: 0, 1979: 11000}, + earnings_year=1979, + eligibility_pia_of_record="432.4", + december_cola_percent={1979: 0}, + ) + assert ( + error.value.reason + is r.RecomputationUnsupportedReason.ELIGIBILITY_BEFORE_1983 + ) + + +def test_cfr404284_example_one_historical_arithmetic_reproduced_separately(): + """Historical statutory demonstration; supported API refuses1979. + + Source supplies total251470.05, replaced8911.36, new11000. Derived + oldtotal249381.41, divisor23*12=276: oldAIME903, newAIME911. + Historical PIA ceiling393.36->393.4 and9.9% ceiling->432.4; + revised395.92->396.0, then9.9/14.3/11.2%->435.3/497.6/553.4. + Derived whole-dollar values are 432 and 435/497/553, respectively. + """ + total = F("251470.05") + old_total = total - 11000 + F("8911.36") + assert old_total == F("249381.41") + assert math.floor(old_total / 276) == 903 + assert math.floor(total / 276) == 911 + old_raw = F(180) * F(9, 10) + F(903 - 180) * F(8, 25) + new_raw = F(180) * F(9, 10) + F(911 - 180) * F(8, 25) + assert old_raw == F("393.36") + assert new_raw == F("395.92") + old_pia = ceil_dime(ceil_dime(old_raw) * F("1.099")) + assert old_pia == F("432.4") + assert whole_dollars(old_pia) == 432 + value = ceil_dime(new_raw) + assert value == 396 + for multiplier, expected, whole in [ + (F("1.099"), F("435.3"), 435), + (F("1.143"), F("497.6"), 497), + (F("1.112"), F("553.4"), 553), + ]: + value = ceil_dime(value * multiplier) + assert value == expected + assert whole_dollars(value) == whole + + +def test_cfr404284_example_two_amw_table_and_historical_colas(): + """SourceAMW502->537, tablePIA407.7, COLAs->448.1/512.2/569.6. + + Source initialJune1978 PIA429.20 uses an unavailable benefit-table + row; it is explicitly a source-supplied amount, not an AIME result. + The original amount implies132700 total:142000-12000+2700. + Derived whole-dollar values are 429 initially, 448/512/569 for the + candidate and 471/539/599 when the initial PIA receives those COLAs. + """ + assert math.floor((142000 - 12000 + 2700) / 264) == 502 + assert math.floor(142000 / 264) == 537 + supplied_initial_pia = F("429.20") + assert whole_dollars(supplied_initial_pia) == 429 + value = F("407.7") + for multiplier, expected, whole in [ + (F("1.099"), F("448.1"), 448), + (F("1.143"), F("512.2"), 512), + (F("1.112"), F("569.6"), 569), + ]: + value = ceil_dime(value * multiplier) + assert value == expected + assert whole_dollars(value) == whole + with pytest.raises(FamilyConfigurationUnsupported) as error: + annual(basis=replace(basis(), formula=r.PIAFormula.AMW)) + assert ( + error.value.reason + is r.RecomputationUnsupportedReason.NON_AIME_FORMULA_PIA + ) + # The capture's supplied replacement tablePIA is lower than its + # supplied oldPIA. Applying identical historical COLAs to both cannot + # resolve that contradiction; do not claim an effective recomputation. + assert F("407.70") - supplied_initial_pia == F("-21.50") + old_value = supplied_initial_pia + for multiplier, expected, whole in [ + (F("1.099"), F("471.70"), 471), + (F("1.143"), F("539.20"), 539), + (F("1.112"), F("599.60"), 599), + ]: + old_value = ceil_dime(old_value * multiplier) + assert old_value == expected + assert whole_dollars(old_value) == whole + assert value == F("569.60") + assert value - old_value == -30 + assert not r.recomputation_decision(old_value, value).applied + + +def test_pre1979_amw_earnings_is_coverage_refusal_not_invalid_input(): + """CFR404.284 Ex2's 1978 earnings/January1979 date is legal legacy. + + Its AMW formula is outside the supported ordinary AIME method. A + complete invented history demonstrates refusal rather than a date + input error; the table arithmetic is checked separately above. + """ + original = basis( + eligibility_year=1978, + indexing_year=1976, + computation_years=22, + entitlement_month=YearMonth(1978, 6), + bend_points=(180, 1085), + formula=r.PIAFormula.AMW, + ) + history = {year: 0 for year in range(1951, 1979)} + history[1978] = 12000 + with pytest.raises(FamilyConfigurationUnsupported) as error: + r.annual_recomputation( + original, history, 1978, "429.2", PARAMS, {1978: 0} + ) + assert ( + error.value.reason + is r.RecomputationUnsupportedReason.ELIGIBILITY_BEFORE_1983 + ) + with pytest.raises(FamilyConfigurationUnsupported): + r.recompute_sequence( + original, history, [1978], "429.2", PARAMS, {1978: 0} + ) + + +def test_minimized_cent_earnings_float_aime_boundary_is_intended_correction(): + """Lexicographic cent search: .02+2.07+33.91=$36 exact /36=1. + + Sorted binary sum is35.99999999999999: old oracle floors to0. Two + cent-valued summands totaling24 have no such AIME1 counterexample. + R3's exact guard returns1, explicitly preserving the statutory floor. + """ + original = basis(computation_years=3) + history = explicit_early_zeros( + {2018: F(".02"), 2019: F("2.07"), 2020: F("33.91")} + ) + assert ( + aime_with_computation_years(history, 3, PARAMS, indexing_year=2018) + == 0 + ) + assert r.earnings_aime(history, original, 2020, PARAMS) == 1 + + +def test_statutory_bend_half_dollar_tie_up_is_intended_oracle_divergence(): + params = ssa_params(nawi={1977: 360, 2018: 361}) + assert params.bend_points(2020)[0] == 180 + assert r.eligibility_bend_points(2020, params)[0] == 181 + + +@pytest.mark.parametrize( + "history", + [{2018: -1, 2019: 0, 2020: 0}, {2018: r.UNAVAILABLE, 2019: 0, 2020: 0}], +) +def test_input_errors_precede_coverage_refusal(history): + unsupported = replace(basis(), formula=r.PIAFormula.OLD_START) + with pytest.raises(InvalidFamilyInput): + annual(basis=unsupported, history=history) + + +def test_recomputation_refusals_stay_in_denominator_and_block_target(): + supported = r.evaluate_recomputation("yes", 2, annual) + refused = r.evaluate_recomputation( + "no", + 3, + lambda: annual(basis=replace(basis(), formula=r.PIAFormula.GUARANTEE)), + ) + counts = count_family_outcomes([supported, refused]) + assert (counts.rows, counts.weight) == (2, 5) + assert (counts.unsupported_rows, counts.unsupported_weight) == (1, 3) + assert counts.supported_weight_share == F(2, 5) + with pytest.raises(FamilyTargetBlocked): + r.weighted_recomputed_pia([supported, refused]) + assert r.weighted_recomputed_pia([supported]) == 540 + with pytest.raises(InvalidFamilyInput): + r.evaluate_recomputation("bad", 1, lambda: annual(history={})) + + +@pytest.mark.parametrize("years", [[2021, 2020], [2020, 2020]]) +def test_annual_fold_requires_strict_chronology(years): + with pytest.raises(InvalidFamilyInput, match="increase strictly"): + r.recompute_sequence(basis(), {}, years, 135, PARAMS, {}) + + +def test_later_fold_input_error_precedes_earlier_coverage_refusal(): + unsupported = replace(basis(), formula=r.PIAFormula.GUARANTEE) + with pytest.raises(InvalidFamilyInput, match="2021"): + r.recompute_sequence( + unsupported, + explicit_early_zeros( + {2018: 0, 2019: 0, 2020: 1, 2021: r.UNAVAILABLE} + ), + [2020, 2021], + 0, + PARAMS, + {2020: 0, 2021: 0}, + ) + + +@pytest.mark.parametrize( + "values", + [ + {"computation_years": 0}, + {"indexing_year": 2019}, + {"first_base_year": 1950}, + {"bend_points": (960, 900)}, + {"entitlement_end_month": YearMonth(2019, 1)}, + ], +) +def test_invalid_original_basis_is_input_error(values): + with pytest.raises(InvalidFamilyInput): + basis(**values) + + +@pytest.mark.parametrize("value", [None, 1, False]) +def test_invalid_collection_inputs_raise_input_error(value): + with pytest.raises(InvalidFamilyInput): + basis(conditions=value) + with pytest.raises(InvalidFamilyInput): + r.recompute_sequence(basis(), {}, value, 0, PARAMS, {}) + + +@pytest.mark.parametrize( + "values", + [ + {"december_cola_percent": {}}, + {"december_cola_percent": {2020: -1}}, + {"eligibility_pia_of_record": F("135.01")}, + {"earnings_year": 2019}, + {"history": {2018: 1.1, 2019: 0, 2020: 0}}, + ], +) +def test_invalid_annual_input_is_input_error(values): + with pytest.raises(InvalidFamilyInput): + annual(**values) + + +def test_current_pia_never_decreases_even_if_candidate_is_lower(): + result = annual(eligibility_pia_of_record=1000) + assert not result.decision.applied + assert result.decision.pia_in_force == 1000 + assert result.eligibility_pia_in_force == 1000 + + +def test_no_earnings_cannot_trigger_an_earlier_candidate(): + result = annual( + history={2018: 21000, 2019: 63000, 2020: 0}, + eligibility_pia_of_record=0, + ) + assert result.candidate_eligibility_pia == 180 + assert not result.decision.applied + + +def test_cola_rounding_occurs_after_each_increase(): + assert increase_by_colas(F("90.9"), [12, 10]) == F("111.9") diff --git a/tests/test_track_b_recompute_properties.py b/tests/test_track_b_recompute_properties.py new file mode 100644 index 00000000..a0d45bac --- /dev/null +++ b/tests/test_track_b_recompute_properties.py @@ -0,0 +1,460 @@ +"""R3 invariants and differentials over invented Hypothesis histories.""" + +import math +from dataclasses import replace +from fractions import Fraction as F +from unittest.mock import patch + +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from populace_dynamics.ss import benefits as oracle +from populace_dynamics.ss.statutory_aime import ( + aime_with_computation_years, + benefit_computation_years, +) +from populace_dynamics.track_b import recompute as r +from populace_dynamics.track_b.gross_benefits import ( + FamilyConfigurationUnsupported, + FamilyTargetBlocked, + InvalidFamilyInput, + YearMonth, + count_family_outcomes, +) +from tests.test_track_b_recompute import ( + PARAMS, + annual, + basis, + death_annual, + explicit_early_zeros, +) + + +@settings(deadline=None) +@given(st.integers(0, 100_000), st.integers(1983, 2026)) +def test_exact_pia_matches_float_oracle_to_one_nanodollar(aime, year): + """Same bends/factors, tolerance1e-9 for float representation only.""" + bends = r.eligibility_bend_points(year, PARAMS) + assert bends == tuple(int(v) for v in PARAMS.bend_points(year)) + assert ( + abs(float(r.exact_pia(aime, bends)) - oracle.pia(aime, year, PARAMS)) + <= 1e-9 + ) + + +@settings(deadline=None) +@given( + st.lists(st.integers(0, 100_000), min_size=3, max_size=3), + st.integers(2, 3), +) +def test_complete_integer_aime_matches_reused_statutory_oracle(values, count): + original = basis(computation_years=count) + history = explicit_early_zeros( + dict(zip(range(2018, 2021), values, strict=True)) + ) + assert r.earnings_aime( + history, original, 2020, PARAMS + ) == aime_with_computation_years( + history, count, PARAMS, indexing_year=2018 + ) + + +@settings(deadline=None) +@given(st.integers(1983, 2026)) +def test_frozen_ordinary_year_count_matches_original_statutory_count(year): + assert max(2, min(35, year - 1956)) == benefit_computation_years(year - 62) + + +@settings(max_examples=50, deadline=None) +@given( + st.integers(1983, 2020), + st.integers(0, 100_000), + st.integers(0, 100_000), + st.integers(0, 100_000), + st.integers(1, 100_000), +) +def test_original_index_count_and_caps_against_independent_fraction_reference( + year, early, later1, later2, cap +): + from tests.track_b_gross_benefit_support import ssa_params + + params = ssa_params(wage_base={1951: cap}) + count = max(2, min(35, year - 1956)) + original = basis( + eligibility_year=year, + indexing_year=year - 2, + computation_years=count, + bend_points=r.eligibility_bend_points(year, params), + entitlement_month=YearMonth(year, 6), + ) + history = {base_year: 0 for base_year in range(1951, 2021)} + history.update({1951: early, 2019: later1, 2020: later2}) + early_indexed = ( + min(early, cap) + * F(str(params.nawi[year - 2])) + / F(str(params.nawi[1951])) + ) + expected = math.floor( + (early_indexed + min(later1, cap) + min(later2, cap)) / (12 * count) + ) + assert r.earnings_aime(history, original, 2020, params) == expected + changed_future_nawi = replace(params, nawi=params.nawi | {2019: 1_000_000}) + assert ( + r.earnings_aime(history, original, 2020, changed_future_nawi) + == expected + ) + + +@settings(deadline=None) +@given(st.integers(0, 50_000), st.integers(0, 50_000), st.integers(0, 50_000)) +def test_ordinary_recomputed_pia_never_decreases(old_dimes, first, second): + result = annual( + history={2018: first, 2019: second, 2020: first + second}, + eligibility_pia_of_record=F(old_dimes, 10), + ) + assert result.decision.pia_in_force >= result.decision.pia_of_record + assert result.eligibility_pia_in_force >= F(old_dimes, 10) + + +@settings(deadline=None) +@given( + st.lists(st.integers(0, 100_000), min_size=6, max_size=6), + st.integers(0, 20), +) +def test_annual_fold_is_non_decreasing_and_matches_individual_steps( + values, cola +): + history = explicit_early_zeros( + dict(zip(range(2018, 2024), values, strict=True)) + ) + results = r.recompute_sequence( + basis(), + history, + [2020, 2021, 2022, 2023], + 0, + PARAMS, + {year: cola for year in range(2020, 2024)}, + ) + pia = 0 + last_current = 0 + for year, result in zip(range(2020, 2024), results, strict=True): + independent = r.annual_recomputation( + basis(), + history, + year, + pia, + PARAMS, + {y: cola for y in range(2020, 2024)}, + ) + assert result == independent + assert result.eligibility_pia_in_force >= pia + assert result.decision.pia_in_force >= last_current + pia = result.eligibility_pia_in_force + last_current = result.decision.pia_in_force + + +@settings(deadline=None) +@given( + st.lists(st.integers(0, 100_000), min_size=3, max_size=3), + st.integers(0, 10_000_000), + st.integers(0, 10_000_000), +) +def test_later_earnings_never_change_earlier_results(values, later1, later2): + prefix = dict(zip(range(2018, 2021), values, strict=True)) + first = annual(history=prefix | {2021: later1, 2022: later2}) + second = annual(history=prefix | {2021: later2, 2022: r.UNAVAILABLE}) + assert first == second + + +@settings(deadline=None) +@given( + st.integers(2018, 2020), + st.lists(st.integers(0, 100_000), min_size=3, max_size=3), + st.booleans(), +) +def test_unavailable_year_never_becomes_zero(year, values, omit): + history = explicit_early_zeros( + dict(zip(range(2018, 2021), values, strict=True)) + ) + if omit: + del history[year] + else: + history[year] = r.UNAVAILABLE + with pytest.raises(InvalidFamilyInput, match=str(year)): + annual(history=history) + + +@settings(deadline=None) +@given(st.integers(1951, 2020), st.booleans()) +def test_every_statutory_base_year_requires_explicit_availability(year, omit): + history = {base_year: 0 for base_year in range(1951, 2021)} + if omit: + del history[year] + else: + history[year] = r.UNAVAILABLE + with pytest.raises(InvalidFamilyInput, match=str(year)): + r.annual_recomputation(basis(), history, 2020, 0, PARAMS, {2020: 0}) + + +@settings(deadline=None) +@given( + st.lists(st.integers(0, 100_000), min_size=3, max_size=3), + st.integers(1, 100_000), +) +def test_aime_caps_each_year_before_selection(values, cap): + from tests.track_b_gross_benefit_support import ssa_params + + params = ssa_params(wage_base={1951: cap}) + history = explicit_early_zeros( + dict(zip(range(2018, 2021), values, strict=True)) + ) + capped = {year: min(value, cap) for year, value in history.items()} + assert r.earnings_aime(history, basis(), 2020, params) == r.earnings_aime( + capped, basis(), 2020, params + ) + + +@settings(deadline=None) +@given(st.integers(0, 100_000), st.integers(0, 100_000)) +def test_pia_is_dime_multiple_monotone_and_nonnegative(aime, increment): + first = r.exact_pia(aime, basis().bend_points) + second = r.exact_pia(aime + increment, basis().bend_points) + assert (first * 10).denominator == 1 + assert (second * 10).denominator == 1 + assert 0 <= first <= second + + +@settings(deadline=None) +@given(st.integers(0, 20_000), st.integers(0, 20_000), st.booleans()) +def test_dollar_rule_exact_equivalence(old, candidate, earnings): + decision = r.recomputation_decision( + F(old, 10), F(candidate, 10), has_earnings=earnings + ) + assert decision.applied == (earnings and candidate - old >= 10) + assert decision.pia_in_force == F( + candidate if decision.applied else old, 10 + ) + + +@settings(deadline=None) +@given(st.lists(st.integers(0, 100_000), min_size=3, max_size=3)) +def test_earnings_mapping_order_independent_and_deterministic(values): + items = list(zip(range(2018, 2021), values, strict=True)) + forward = annual(history=dict(items)) + backward = annual(history=dict(reversed(items))) + assert forward == backward == annual(history=dict(items)) + + +@settings(deadline=None) +@given( + st.lists(st.sampled_from(list(r.RecomputationCondition))), + st.lists(st.integers(0, 100_000), min_size=3, max_size=3), +) +def test_iterator_conditions_preserve_repeated_results_and_outcomes( + conditions, values +): + original = basis(conditions=iter(conditions)) + history = dict(zip(range(2018, 2021), values, strict=True)) + outcomes = [ + r.evaluate_recomputation( + key, 1, lambda: annual(basis=original, history=history) + ) + for key in ("first", "second") + ] + first, second = outcomes + assert original.conditions == frozenset(conditions) + assert isinstance(original.conditions, frozenset) + assert first.supported == second.supported == (not conditions) + assert first.result == second.result + counts = count_family_outcomes(outcomes) + assert (counts.rows, counts.weight) == (2, 2) + assert ( + counts.unsupported_rows + == counts.unsupported_weight + == (2 if conditions else 0) + ) + if conditions: + assert first.unsupported.reason == second.unsupported.reason + + +@settings(deadline=None) +@given( + st.sampled_from( + [f for f in r.PIAFormula if f is not r.PIAFormula.AIME] + + list(r.RecomputationCondition) + + ["pre1983", "death_before_fra"] + ), + st.lists(st.integers(0, 100_000), min_size=5, max_size=5), + st.booleans(), + st.integers(1, 3), +) +def test_all_coverage_refusals_precede_ordinary_arithmetic( + refusal, values, sequence, year_count +): + original = basis() + options = {} + last_year = 2019 + year_count + if isinstance(refusal, r.PIAFormula): + original = basis(formula=refusal) + elif isinstance(refusal, r.RecomputationCondition): + original = basis(conditions=iter([refusal])) + elif refusal == "pre1983": + original = basis( + eligibility_year=1979, + indexing_year=1977, + computation_years=23, + bend_points=(180, 1085), + entitlement_month=YearMonth(1979, 6), + ) + else: + options = { + "death_month": YearMonth(last_year, 11), + "fra_month": YearMonth(2025, 2), + } + history = explicit_early_zeros( + dict(zip(range(2018, 2023), values, strict=True)) + ) + colas = { + year: 0 for year in range(original.eligibility_year, last_year + 1) + } + with ( + patch.object( + r, "_aime_from_prefix", wraps=r._aime_from_prefix + ) as adapter, + patch.object( + r, + "aime_with_computation_years", + wraps=r.aime_with_computation_years, + ) as aime, + patch.object(r, "exact_pia", wraps=r.exact_pia) as pia, + ): + with pytest.raises(FamilyConfigurationUnsupported): + if sequence: + r.recompute_sequence( + original, + history, + list(range(2020, last_year + 1)), + 0, + PARAMS, + colas, + **options, + ) + else: + r.annual_recomputation( + original, history, last_year, 0, PARAMS, colas, **options + ) + assert (adapter.call_count, aime.call_count, pia.call_count) == ( + 0, + 0, + 0, + ) + + +@settings(deadline=None) +@given( + st.integers(1951, 2018), + st.booleans(), + st.sampled_from(["nawi", "wage_base"]), + st.booleans(), +) +def test_invalid_required_parameters_precede_refusal_without_arithmetic( + year, missing, parameter, sequence +): + original = basis(formula=r.PIAFormula.AMW) + if parameter == "nawi": + values = PARAMS.nawi.copy() + if missing: + del values[year] + else: + values[year] = 0 + params = replace(PARAMS, nawi=values) + else: + params = replace( + PARAMS, + wage_base={} if missing else {1951: 1_000_000, 2021: 0}, + ) + history = {base_year: 0 for base_year in range(1951, 2022)} + with ( + patch.object( + r, "_aime_from_prefix", wraps=r._aime_from_prefix + ) as adapter, + patch.object(r, "exact_pia", wraps=r.exact_pia) as pia, + ): + with pytest.raises(InvalidFamilyInput): + if sequence: + r.recompute_sequence( + original, + history, + [2020, 2021], + 0, + params, + {2020: 0, 2021: 0}, + ) + else: + r.annual_recomputation( + original, history, 2021, 0, params, {2020: 0, 2021: 0} + ) + assert (adapter.call_count, pia.call_count) == (0, 0) + + +@settings(deadline=None) +@given(st.integers(2, 34), st.integers(2020, 2024)) +def test_invalid_original_count_precedes_sequence_death_refusal(count, year): + with pytest.raises(InvalidFamilyInput, match="computation count"): + r.recompute_sequence( + basis(computation_years=count), + {base_year: 0 for base_year in range(1951, year + 1)}, + list(range(2020, year + 1)), + 0, + PARAMS, + {cola_year: 0 for cola_year in range(2020, year + 1)}, + death_month=YearMonth(year, 11), + fra_month=YearMonth(2025, 2), + ) + + +@settings(deadline=None) +@given(st.integers(1, 100), st.integers(1, 100)) +def test_refusals_retained_in_weighted_denominator( + supported_weight, refused_weight +): + supported = r.evaluate_recomputation("yes", supported_weight, annual) + refused = r.evaluate_recomputation( + "no", + refused_weight, + lambda: annual(basis=replace(basis(), formula=r.PIAFormula.OLD_START)), + ) + counts = count_family_outcomes([supported, refused]) + assert counts.rows == 2 + assert counts.weight == supported_weight + refused_weight + assert counts.supported_weight_share == F( + supported_weight, supported_weight + refused_weight + ) + with pytest.raises(FamilyTargetBlocked): + r.weighted_recomputed_pia([supported, refused]) + + +@settings(deadline=None) +@given( + st.sampled_from(list(r.PIAFormula)[1:]), + st.sampled_from([r.UNAVAILABLE, -1, "bad"]), +) +def test_input_error_is_never_classified_as_coverage(formula, bad): + with pytest.raises(InvalidFamilyInput): + r.evaluate_recomputation( + "invalid", + 1, + lambda: annual( + basis=replace(basis(), formula=formula), + history={2018: bad, 2019: 0, 2020: 0}, + ), + ) + + +@settings(max_examples=40, deadline=None) +@given(st.integers(7, 11), st.integers(0, 99)) +def test_death_effective_month_excludes_later_cola(month, later_cola): + result = death_annual(month, later_cola) + assert result.effective_month == YearMonth(2026, month) + assert result.decision.candidate_pia == 270 diff --git a/tests/tier_counts.json b/tests/tier_counts.json index f5ae5bb2..15808335 100644 --- a/tests/tier_counts.json +++ b/tests/tier_counts.json @@ -1,7 +1,7 @@ { "schema_version": 1, "counts": { - "unit": 7547, + "unit": 8786, "artifact": 4080, "integration_psid": 1384, "reproduction_legacy": 520, diff --git a/tests/track_b_arf_recompute_support.py b/tests/track_b_arf_recompute_support.py new file mode 100644 index 00000000..4606f3cb --- /dev/null +++ b/tests/track_b_arf_recompute_support.py @@ -0,0 +1,34 @@ +"""Captured statutory text for R3; no real data or network access.""" + +from __future__ import annotations + +import html +import json +import re +from functools import cache +from pathlib import Path + +SOURCES = ( + Path(__file__).with_name("data") / "track_b" / "arf_recompute_sources" +) + + +@cache +def manifest() -> dict: + """R3 source provenance in the same schema used by milestone G.""" + return json.loads((SOURCES / "manifest.json").read_text()) + + +@cache +def source_text(source_id: str) -> str: + """Normalize captured HTML, XML or text without changing figures.""" + entry = next(s for s in manifest()["sources"] if s["id"] == source_id) + raw = (SOURCES / entry["file"]).read_text() + if entry["file"].endswith((".html", ".xml")): + raw = re.sub(r"(?is)<(script|style)[^>]*>.*?", " ", raw) + raw = re.sub(r"<[^>]+>", " ", raw) + raw = html.unescape(raw) + for char in ("\u200b", "\xa0"): + raw = raw.replace(char, " ") + raw = raw.replace("’", "'") + return re.sub(r"\s+", " ", raw).strip()