From e0f886085062f5ca717596601bf2d75f17b2ebb2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 13:55:48 -0400 Subject: [PATCH 01/13] Track B G/R2: decide the open readings under the 10/20 delegation Money-changing readings (each pinned with its rejected alternatives, Hypothesis properties and killed mutants): - us:oasdi-dual-entitlement-zero-after-age-reduction: count GN 02603.045's before-age amount (GN .050A ties exceptions 2-3 to .045); children $115.30 each instead of a refusal. - us:oasdi-parent-entitlement-count-interval: the entitled set must satisfy 402(h)(1)(D) for every parent; one in-band parent yields to the other at 82.5 percent ($825); two in-band parents stay refused. - us:oasdi-dwb-1984-pre-1978-history: RS 00615.301B.1.e pays .715 of the unreduced WIB in every pre-60 month from January 1984, whatever the entitlement date; from 60 the money history still refuses. - us:oasdi-ret-grace-simultaneous-rib-dib: Act 202(k)(4) makes an elected RIB month entitlement to RIB alone, so the default now counts its NSMs. - us:oasdi-ret-deemed-entitlement-basis: an unmarried deemed declaration is an input error (Act 203(b)(1), 20 CFR 404.420); the residual adult-child case stays refused. Kept, with stated reasons and new markers: saving-clause procedure gaps, historical dual entitlement, NH charges with age reductions (sources silent); exact fractions, disability-maximum rounding, NESE floor, own-work age exemptions, other-deductee scope (no conflict once the Act is read whole). Notes re-framed; 402(d), (e), (h), (k), (s) excerpts added from the pinned LII body; new tests/test_track_b_readings_20261023.py checks markers against logged ids in both directions. Stacked on #563; retarget to master after it merges. Co-Authored-By: Claude Opus 5.5 --- docs/design/track_b_g_conformance_report.json | 2 +- docs/design/track_b_g_gross_benefits.md | 301 ++++++++++++------ docs/design/track_b_r2_ret_ledger.md | 270 ++++++++-------- scripts/track_b_g_conformance_report.py | 8 +- .../track_b/gross_benefits.py | 126 +++++--- src/populace_dynamics/track_b/ret_grace.py | 19 +- src/populace_dynamics/track_b/ret_ledger.py | 31 +- .../lii_usc_402_415_excerpts.txt | 24 ++ .../gross_benefit_sources/manifest.json | 8 +- tests/test_track_b_gross_benefits.py | 12 +- ...ck_b_gross_benefits_fix_round_8_history.py | 75 ++++- ...est_track_b_gross_benefits_parents_ieds.py | 152 ++++++++- tests/test_track_b_gross_benefits_parisi.py | 132 +++++--- tests/test_track_b_readings_20261023.py | 120 +++++++ tests/test_track_b_ret_grace.py | 100 ++++-- tests/test_track_b_ret_ledger.py | 24 +- tests/test_track_b_ret_ledger_readings.py | 52 +-- tests/test_track_b_ret_ledger_sources.py | 4 +- tests/track_b_ret_grace_reference.py | 2 +- 19 files changed, 1052 insertions(+), 410 deletions(-) create mode 100644 tests/test_track_b_readings_20261023.py diff --git a/docs/design/track_b_g_conformance_report.json b/docs/design/track_b_g_conformance_report.json index 6dc5b3927..5e463602f 100644 --- a/docs/design/track_b_g_conformance_report.json +++ b/docs/design/track_b_g_conformance_report.json @@ -740,7 +740,7 @@ "status": "refused" }, { - "description": "Two potential parents; one own RIB 780 lies between 75% and 82.5% of PIA 1000", + "description": "Two potential parents; own RIBs 780 and 790 both lie between 75% and 82.5% of PIA 1000", "id": "parent_benefit", "reason": "parent_benefit", "status": "refused" diff --git a/docs/design/track_b_g_gross_benefits.md b/docs/design/track_b_g_gross_benefits.md index 4b5270722..1f6c6df8e 100644 --- a/docs/design/track_b_g_gross_benefits.md +++ b/docs/design/track_b_g_gross_benefits.md @@ -418,8 +418,16 @@ Its sealed delayed-credit function agrees through `min(48, window)`; the new statutory float function agrees over the whole FRA-to-70 window, including months past 48 for births 1917–1942 (#494). A named pin preserves the sealed legacy result rather than treating that retained convention as -an open bug. These are invariant 10 below. The exact monetary -reimplementation needs Max's ratification, because §6 asked for reuse. +an open bug. These are invariant 10 below. + +Reading taken (October 23, 2026), logged as +`us:oasdi-reduction-exact-fractions`: this is not a conflict between +sources. Act 202(q)(1) states the reductions as fractions ("5/9 of 1 +percent", "25/36 of 1 percent", "19/40 of 1 percent"), and RS 00615.005B +says "only fractions can be used when computing reduced benefits". The +design's §6 suggestion to reuse the float primitives was a design +preference, not a source; the law requires the exact arithmetic, and the +float oracle stays a differential. ## Delayed credits @@ -470,9 +478,17 @@ dime on two sources, both in the Social Security Bulletin 75(3) capture: After that, 215(i)(2)(A)(ii) floors every COLA increase of the maximum to the dime. The captured POMS sections on the disability maximum (RS 00615.740 and .742) say nothing about rounding. Both quotes are pinned by -`test_worked_example_figures_are_quoted_from_the_captures`. The Bulletin is -an SSA research publication, not a program instruction, so resting the -rounding on it needs Max's acceptance. +`test_worked_example_figures_are_quoted_from_the_captures`. + +Reading taken (October 23, 2026), logged as +`us:oasdi-disability-family-maximum-rounding`: floor to the dime. The Act's +403(a)(6) and 20 CFR 404.403(d-1) are silent, so nothing in a statute or +regulation competes. The Act rounds the regular maximum "to the next lower +multiple of $0.10" (403(a)(2)) and dime-floors every later increase of this +maximum (215(i)(2)(A)(ii)), so an undimed initial maximum would be the only +one of its kind; SSA's own description, the Bulletin, floors it. That is +consistent usage within the Act, with the agency's reading breaking what +remains of the tie (P3). ## Households and the entry point @@ -621,23 +637,19 @@ rate: RS 00630.400's summary also requires "at least one beneficiary is reduced for age". G does not require it, which can only add refusals. -Needs Max's judgment: the literal 203(a)(5) guarantee that totals "for any -such subsequent month will not be less" reaches later composition changes. -RS 00630.400's operational condition instead states "Total family payment -is less after the PIA increase than it was before." Its additional condition -is "Benefits for that month are reduced for the MAX and at least one -beneficiary is reduced for age;". In the invented three-child case above, -two children leaving in the COLA month lowers the total from $1,750.00 to -$1,537.50. The literal reading refuses; the operational reading admits that -$1,537.50 total when no beneficiary is reduced for age. This module keeps -the literal refusal. - -Ruling d1418 (2026-10-12): keep the current defaults; reopen any one when a -population target needs it. - -Ruling d1338 (2026-10-12): adopt SSA's operational reading (RS 00630.400), -keeping the literal reading behind a switch; implemented in G part 2. -Until then this module applies the literal refusal. +Reading taken by Max, ruling d1338 (2026-10-12), logged as +`us:oasdi-family-maximum-saving-clause`: SSA's operational reading is the +default and the literal one stays selectable as `LITERAL_REFUSAL`. The +literal 203(a)(5) guarantee that totals "for any such subsequent month will +not be less" reaches later composition changes. RS 00630.400's operational +condition instead states "Total family payment is less after the PIA +increase than it was before." Its additional condition is "Benefits for +that month are reduced for the MAX and at least one beneficiary is reduced +for age;". In the invented three-child case above, two children leaving in +the COLA month lowers the total from $1,750.00 to $1,537.50. The literal +reading refuses; the operational reading admits that $1,537.50 total when +no beneficiary is reduced for age. "Operational saving clause, October 23, +2026" below implements the default. ## Supported and refused configurations @@ -1180,25 +1192,45 @@ limiting RIB-LIM remain interaction with the PTAP age-reduction and partial-payable terms. -### Zero reached after auxiliary age reduction - -This reading needs Max's judgment. GN 02603.045 says the counted amount is -"before any applicable reduction for age". GN 02603.050C requires the -"Age reduction amount for all workers or deductees". GN 02603.050A.2 -instead says "If dual entitlement reduces a beneficiary's rate to $0.00 -(technically entitled)" to disregard that beneficiary. A method-B spouse -with PIA $500, maximum $750, own RIB $64 and 36 auxiliary reduction months -has standard rate $83.30, age-adjusted $62.40, and zero auxiliary payable. -Disregarding the spouse pays children $125 each; retaining the $20.90 age -term pays $114.50 each, a $21 record-total difference. A reading of the -before-age text alone counts $83.30 minus $64, or $19.30, paying children -$115.30 each and a record total of $730.60. G refuses this -case as `parisi_redistribution_ambiguous`, rather than selecting the higher -payment. Zero before auxiliary age reduction is supported, including A.5 -and A.8. The alternatives and the refusal are executed by the Parisi tests. - -Ruling d1418 (2026-10-12): keep the current defaults; reopen any one when a -population target needs it. +### Reading taken: zero reached after auxiliary age reduction + +Source conflict `us:oasdi-dual-entitlement-zero-after-age-reduction`, +decided October 23, 2026 (definitions control; P3). Ruling d1418 had kept +it refused. GN 02603.045 says the counted amount is "before any applicable +reduction for age". GN 02603.050C requires the "Age reduction amount for +all workers or deductees". GN 02603.050A.2 says "If dual entitlement +reduces a beneficiary's rate to $0.00 (technically entitled)" to disregard +that beneficiary, and A.3 says to "subtract the partial payable rate" when +the rate is partial. A method-B spouse with PIA $500, maximum $750, own RIB +$64 and 36 auxiliary reduction months has standard rate $83.30, +age-adjusted $62.40, and zero auxiliary payable. + +GN 02603.050A states that "exceptions 2 and 3 apply only for cases +processed under the new rule as explained in GN 02603.045", so A.2 and A.3 +measure the dual rate as .045 defines it, before age reduction. Before age +reduction the spouse's rate is $83.30 minus $64, or $19.30: a partial rate, +not zero. G counts it whole (A.3), paying the children $115.30 each, a +record total of $730.60. This is also the amount G already counts whenever +the dual payable is positive: payable plus age reduction is $83.30 minus +the own benefit. The count therefore runs continuously from a positive +payable through the zero clamp to A.2's disregard, which applies exactly +when the own benefit reaches the standard rate (and covers A.5 and A.8, +where the zero arises before age reduction). It is consistent with 20 CFR +404.403(a)(5) and with every captured SSA example (A.5, A.8, A.9, B.2). + +The two rejected readings stay pinned as diagnostics. Disregarding the +spouse here (A.2 applied to the after-age rate) pays $125 each. Retaining +the full $20.90 age term under C pays $114.50 each; it would count less +than the before-age amount and, extended to an own benefit at or above the +standard rate, would contradict the supported A.2 disregard. +`test_method_b_zero_only_after_age_counts_the_before_age_amount` pins the +quotes and all three amounts; the Hypothesis property +`test_method_b_counted_amount_is_the_before_age_amount` checks the counted +amount equals max(0, standard minus own benefit) for every own benefit and +reduction period, matches payable plus age reduction whenever the payable +is positive, and never lowers the children as the own benefit rises. The +method-C property now also executes a positive excess that rounds to zero +after age reduction. ## Disabled widow(er) benefits @@ -1279,9 +1311,10 @@ remains technically entitled even if no parent benefit is payable (RS declared parents is refused as `parent_benefit`: the captured one/two-parent rules do not settle that configuration. -Needs Max's judgment: the parent entitlement count can be circular. RS -00209.001 requires "not be entitled to retirement insurance benefit (RIB) -equal to or exceeding +Reading taken (October 23, 2026), source conflict +`us:oasdi-parent-entitlement-count-interval` (P1, the Act's operative +words). Ruling d1418 had kept it refused. RS 00209.001 requires "not be +entitled to retirement insurance benefit (RIB) equal to or exceeding the amount of the parent's original benefit (One parent = 82 1/2%; Two parents = 75%);". RS 00209.015 distinguishes "2. One parent entitled" from "3. Two parents entitled". For PIA $1,000 and maximum $1,750, two potential @@ -1289,27 +1322,37 @@ parents, one with own RIB $780, qualify together at the one-parent OB of $825 but that first parent's RIB exceeds the two-parent OB of $750. Counting two first excludes that parent and pays the other $825; testing eligibility at $825 first would count two and pay the other $750 (a $75 difference), -while paying no auxiliary to the $780 own-RIB parent. Neither fixed order -is silently selected: `parent_benefit` refuses the interval between the two -dime-rounded OBs when both parents are potential entitlements. +while paying no auxiliary to the $780 own-RIB parent. + +The Act decides it. 402(h)(1)(D) tests the own RIB against 82½ percent "if +the amount of the parent's insurance benefit for such month is determinable +under paragraph (2)(A) (or 75 percent of such primary insurance amount in +any other case)", and (h)(2)(B) applies 75 percent "For any month for which +more than one parent is entitled". The entitled set must satisfy that test +for every parent at once. The parent without an own RIB is entitled +whatever the other does, so the $780 parent's benefit, if entitled, would +be the two-parent rate, which $780 reaches: that parent is not entitled, +and the other parent alone is paid $825 under (h)(2)(A). The other one-pass +order ends with one parent paid at the two-parent rate, which (h)(2)(A) +contradicts. With two parents whose own RIBs both lie in the band, either +parent alone satisfies the test for both, so `parent_benefit` still +refuses that configuration by name (filing order, which the inputs omit, +might settle it). `test_parent_entitlement_count_counterfactual_changes_payment_by_75` -executes both readings with a test-only exact helper. Counting two first -tests entitlement at $750, excludes the $780-own-RIB parent, and gives the -other parent's candidate payment $825. Rechecking entitlement at $825 -would count two. Testing entitlement at $825 first counts two, changes the -other parent's candidate payment to $750, and then excludes the first -parent when entitlement is rechecked at $750. Both candidates pay that -first parent no auxiliary, and the other parent's candidates differ by -$75. The public result remains the `parent_benefit` refusal. The Hypothesis -test `test_parent_count_counterfactual_orders_cycle_across_the_refused_band` -executes the same count reversal and the exact difference between the two -dime-rounded OBs throughout the refused interval. These reversals are an -intended diagnostic inconsistency of the two one-pass readings, not a -supported payment or a bug in the public refusal. - -Ruling d1418 (2026-10-12): keep the current defaults; reopen any one when a -population target needs it. +executes both one-pass orders with a test-only exact helper and asserts +that the public result is the consistent one: the $780 parent not +entitled, the other parent paid $825. The Hypothesis test +`test_parent_count_counterfactual_orders_cycle_across_the_refused_band` +keeps the two one-pass orders' count reversal as a diagnostic. +`test_one_parent_in_the_band_yields_to_the_other_parent` checks the reading +across the whole band; `test_two_parents_in_the_band_refuse_because_either_alone_qualifies` +the remaining refusal; and +`test_parent_entitlement_satisfies_402h1d_for_every_parent` is an exact +differential against a brute-force enumeration of every entitled set that +satisfies 402(h)(1)(D) for both parents, with no own benefit, an own DIB or +an own RIB anywhere: G computes exactly the unique consistent set and +refuses exactly when there is more than one. Invented hand references (the captures publish rules without monetary worked examples for these classes): PIA $1,000 pays a solo parent $825 or @@ -1886,25 +1929,33 @@ this contract. The envelope is deliberately conservative; it does not certify a tight set of all possible dated histories. Narrower historical support needs dated entitlement and reduction-money evidence. -### Needs Max's judgment: DWB after January 1984 - -The broad instruction in RS 00615.005C.2 is "all cases involving single -entitlement to actuarially reduced benefits prior to 1/78". In contrast, -RS 00615.301B.1.e says "all disabled widow(er)s under age 60 receive .715 of -the full unreduced WIB for 1/84 and later". RS 00615.005A.7 also states -that the 1983 amendments "Eliminated the additional reduction for the -period between age 50 and age 60." A specific-rule reading could treat -that change as settling the entire later amount, including earlier awards. -The general historical reading still requires the missing monetary ratio. - -This round conservatively refuses an otherwise unsettled pre-1978-capable -DWB cohort and its possible aged-widow conversion; it does not assign a -historical payment. For cohort 1927 in January 1984, with invented PIA and unreduced rate -$1,000, the fixed 71.5% reading would pay $715.00 ($715 whole dollars), while the implemented -reading refuses and retains the row and weight. Tests pin the captured -texts, the alternative exact $715 formula and the actual refusal. This is -an explicit support-boundary judgment item, separate from the earlier -$715 surviving-divorced child-care regression, which remains supported. +### Reading taken: DWB after January 1984 + +Source conflict `us:oasdi-dwb-1984-pre-1978-history`, decided October 23, +2026 (specific over general). The broad instruction in RS 00615.005C.2 is +"all cases involving single entitlement to actuarially reduced benefits +prior to 1/78". In contrast, RS 00615.301B.1.e says "all disabled +widow(er)s under age 60 receive .715 of the full unreduced WIB for 1/84 and +later". RS 00615.005A.7 also states that the 1983 amendments "Eliminated +the additional reduction for the period between age 50 and age 60", and RS +00615.310B.3 says DWB entitled before 1/84 "had their benefits recalculated +effective 1/84 as if the reduction for the period prior to age 60 did not +exist." The specific DWB rule governs the general pre-1978 method: from +January 1984, every month in which a disabled widow(er) is under 60 is paid +.715 of the unreduced WIB whatever the entitlement date. G applies it to +every payment month in a year before the SSA birth cohort can attain 60 +(cohort year plus 59 or earlier); in the year it can attain 60 the birth +month decides and is not supplied, and from 60 the money history governs +again, so those months keep `reduced_benefit_history_before_1978`. For +cohort 1927 in January 1984, with invented PIA and unreduced rate $1,000, +G now pays $715.00 ($715 whole dollars); in 1987 the row is refused and +keeps its weight. `test_historical_dwb_under_60_takes_the_specific_fixed_rate` +pins the texts and amounts, and the Hypothesis property +`test_historical_dwb_is_settled_exactly_before_the_cohort_can_attain_60` +checks, for cohorts 1912-1927 and payment years 1984-2000, that every +under-60 year equals a post-1977 cohort's computation exactly and every +later year refuses. The earlier $715 surviving-divorced child-care +regression remains supported. Dual-entitlement snapshots also lack dates and earlier money history. RS 00615.250 gives the current reduced-excess procedure and refers to @@ -1915,7 +1966,7 @@ claim that the conservative interval identifies a particular historical dual-entitlement amount. Expanding that historical surface is not in scope. -### Needs Max's judgment: historical dual entitlement +### Refused: historical dual entitlement (sources silent) RS 00615.005C.2 explicitly describes "single entitlement". RS 00615.020 states "Method C - First benefit calculated independently; reduce excess @@ -1935,8 +1986,16 @@ $99.30 ($99 whole dollars), with own $400. The conservative implementation refuses `reduced_benefit_history_before_1978`, retaining the row and weight; it does not assign a different historical dollar amount. The source-quote test executes that alternative and the cohort-1916 control side by side -with the historical refusal. Resolving this narrower historical dual -surface needs Max's judgment; no new monetary default is silently chosen. +with the historical refusal. + +Refusal kept (October 23, 2026), logged as +`us:oasdi-dual-entitlement-pre-1978-history`: the sources are silent, so +the refusal is safe. RS 00615.005C.2's money method covers single +entitlement only; RS 00615.250 sends subsequent adjustments to RS +00615.005F, which the captured section does not contain; RS 00615.240 +settles spouse-first awards but still needs the dated pre-1978 increases. +No statute or regulation in the captures fixes the own-first historical +amount, and the inputs carry no dated money history in any case. ## Fix round 9 of October 15, 2026 @@ -2224,10 +2283,12 @@ benefit, including parent, widow-basis and dime-rounding cases. `test_two_parents_do_not_pass_their_count_to_other_survivor_roles` execute the applicability, basis-bound and mixed-family properties. -### Needs Max's judgment and not in scope +### Judgment items and scope (as of this round) No new statutory choice arises. All earlier judgment items, conservative refusals, source-erratum controls and rulings d1418/d1338 remain unchanged. +(The October 23, 2026 readings pass, at the end of this note, has since +decided or re-framed every judgment item.) This round does not implement savings-clause raised payments, new family configurations, new sources or real-data processing. @@ -2471,7 +2532,7 @@ the guard, as well as the separate whole-dollar payment fields. | Skipped qualifying history | Existing `savings_clause_history_unknown` refusal | | Uninitialized/subclassed guard, guard copying or guard serialization | `InvalidFamilyInput`; no coverage refusal code | -### Needs Max's judgment and not in scope +### Judgment items and scope (as of this round) No new statutory interpretation is selected. Earlier judgment items and conservative savings-clause readings remain unchanged. This round does not @@ -2724,8 +2785,9 @@ identical; tests pin both quoted OBs and modern OBs rather than overriding the arithmetic or claiming historical rounding coverage. The captures do not date these examples or establish why they used upward rounding. -### Readings needing judgment +### Readings taken and refusals kept +Reading taken (logged as `us:oasdi-ret-temporary-partial-deduction`): GN 02603.050D.2 says "there are no partial deductions for temporary work deductions". RS 02501.160 Example 2 nevertheless instructs immediate same-household redistribution in June with C3 $39 and C2 $119, while E and @@ -2735,8 +2797,14 @@ amounts. The literal GN sentence would refuse that temporary partial case; it would withhold C2's $20 June increase. The source assertions pin both sentences. No switch invents a different benefit amount. -The interaction of NH reservations and age reductions is not established -by a captured worked example. The implementation refuses that new mixed +Refusal kept (October 23, 2026), logged as +`us:oasdi-nh-charge-age-reduction-interaction`: the sources are silent. +GN 02603.050C lists both "work deductions to be imposed against +auxiliaries or survivors due to the number holder's work" and the "Age +reduction amount for all workers or deductees" in PTAP, but no captured +rule or example says whether an NH charge falls on the age-reduced or the +pre-age rate when both apply; RS 02501.145-.160 and the 20 CFR 404.439-.441 +examples have no age reduction. The implementation refuses that mixed configuration by name; ordinary G/R2 age-reduced supported inputs remain unchanged. No population processing, combined maximum determination, multiple auxiliary-record selection or annual earnings determination is @@ -2953,6 +3021,20 @@ with GN 02603.050, not to input validity. No protected amount is inferred from a diagnostic pre-offset total, and every refusal retains its row and weight. Completing this combined payment procedure remains outside scope. +### Refused: gaps in the operational procedure (sources silent) + +Refusals kept (October 23, 2026), logged as +`us:oasdi-saving-clause-procedure-gaps`. Ruling d1338 chose RS 00630.400's +operational procedure over the Act's literal "any such subsequent month", +and that procedure states only the life-case allocation with the worker +alone, or one spouse and other auxiliaries, reduced for age. It says +nothing on how a protection survives a change of entitled membership +(`savings_clause_membership_change`), nor on survivor, own-benefit, +deduction, multiple-reduced, lone-reduced-spouse, mixed-basis or above-OB +rates (`savings_clause_rate_procedure`). Falling back to the literal Act +would reverse d1338 for those months, and no other captured SSA source +supplies the allocation, so each stays refused and counted. + ### Not in scope No combined family maximum, multiple auxiliary records, survivor allocation @@ -3000,3 +3082,42 @@ individuals = 225.30." Its displayed formula correctly gives $274.20 before the original-benefit cap of $225.30. The existing A.2 test pins the literal narrative, raw quotient and capped payment side by side. This is an omitted cap in the narrative, not an alternate arithmetic rule. + + +## Readings pass, October 23, 2026 + +Verification class: statutory conformance on invented cases and captured +sources; no population claim. Under Max's 2026-10-20 delegation, single +readings are the session's call under his principles (P1 statute first, +P2 regulations are law, P3 the agency breaks true ties, P4 taxpayer +choices), logged in the source-conflict rulings with the ids below; +patterns and collisions between principles go to him. Ruling d1418 had +kept the older readings as defaults pending a population need; this pass +decides each one. Every money-changing reading keeps the rejected +alternatives pinned as diagnostics, and every remaining refusal keeps its +row and weight in the denominators. + +| Item | Id | Outcome | Basis | +|---|---|---|---| +| 403(a)(5) literal or operational | `us:oasdi-family-maximum-saving-clause` | Operational default, literal behind a switch | Max, d1338 | +| Operational rounding direction | `us:oasdi-saving-clause-operational-rate-rounding` | Round equal rates up | P1 | +| Gaps in the operational procedure | `us:oasdi-saving-clause-procedure-gaps` | Refused | Sources silent | +| Zero reached only after auxiliary age reduction | `us:oasdi-dual-entitlement-zero-after-age-reduction` | Count the .045 before-age amount: children $115.30 each, was refused | Definitions control (.050A ties A.2-A.3 to .045); P3 | +| Circular parent count | `us:oasdi-parent-entitlement-count-interval` | The set satisfying 402(h)(1)(D) for both parents: other parent $825, was refused; two parents in the band stay refused | P1 | +| DWB from January 1984, pre-1978-capable cohorts | `us:oasdi-dwb-1984-pre-1978-history` | Under 60: .715 of the unreduced WIB ($715, was refused); from 60: refused | Specific over general | +| Historical dual entitlement | `us:oasdi-dual-entitlement-pre-1978-history` | Refused | Sources silent | +| Exact fractions or float reuse | `us:oasdi-reduction-exact-fractions` | Exact fractions | Not a conflict (Act and RS 00615.005B agree) | +| Disability-maximum rounding | `us:oasdi-disability-family-maximum-rounding` | Floor to the dime | Act's consistent usage; P3 | +| Temporary partial deduction | `us:oasdi-ret-temporary-partial-deduction` | Follow the RS 02501.160 example in G | Specific over general; P3 | +| NH reservations with age reductions | `us:oasdi-nh-charge-age-reduction-interaction` | Refused | Sources silent | + +The statutory texts newly relied on (42 U.S.C. 402(d)(1)(D), (d)(5), +(e)(1), (h)(1)(D), (h)(2), (k)(3)(A), (k)(4) and (s)) were added to +`lii_usc_402_415_excerpts.txt` from a re-fetch whose LII body matched the +pinned 402 SHA-256 byte for byte. Every `# Source conflict` marker in the +Track B modules names an id logged in the rulings, and every logged id +has a marker: `tests/test_track_b_readings_20261023.py` checks both +directions and that this note and the R2 and R3 notes carry no open +"Needs Max's judgment" heading. The invented conformance lattice is +unchanged; only the `parent_benefit` witness moved to two parents in the +band. diff --git a/docs/design/track_b_r2_ret_ledger.md b/docs/design/track_b_r2_ret_ledger.md index fdfeaa7f8..8723fc62f 100644 --- a/docs/design/track_b_r2_ret_ledger.md +++ b/docs/design/track_b_r2_ret_ledger.md @@ -469,9 +469,10 @@ deemed-entitled beneficiaries, and months with a binding maximum or another deduction event. A living NH must be present and receive RIB or DIB in entitled months. A deceased NH must be absent. Deemed entitlement asserts the married-person facts of Act 203(b)(1) and 20 CFR 404.420; -the broader unmarried-child reading of RS 02501.140 is refused. An -ordinary deemed child's age-18 months must be explicit. Their Act/CFR -coverage difference is refused even with those facts established. +an unmarried declaration under RS 02501.140's broader wording is an input +error (that recipient is not on this NH's ledger). An ordinary deemed +child's age-18 months must be explicit, and that residual adult-child case +is refused even with those facts established. Deemed entitlement requires a different NH with RIB entitlement in each applicable month. `own_work_exempt_months` accepts only CHILD or WIDOW payment months and asserts respectively attainment of age 18 or initial @@ -779,12 +780,13 @@ different section 202 type entered as DWB or CDB after a gap can still carry pending eligibility to a later year's first eligible NSM; the disability-only-year exclusion does not erase that entitlement history (20 CFR 404.435(b)(2)-(3); RS 02501.030B.2 and B.4). -The conservative default also -excludes simultaneous disability entitlement, following 20 CFR -404.435(b)(3). The named POMS alternative permits NSMs in months RIB is -elected while technical DIB entitlement continues, reproducing RS -02501.030B.4's published example. The money-changing difference is -recorded under Needs Max's judgment. +During simultaneous RIB and DIB entitlement the default counts NSMs in +months RIB is paid: under Act 202(k)(4) the person is then entitled to the +RIB alone, so 20 CFR 404.435(b)(3)'s disability exclusion does not reach +the month, and RS 02501.030B.4's published example is reproduced. The +former technical-entitlement exclusion remains an explicit alternative. +"Readings taken and refusals kept" below records the reading and its +money difference. ## Supported and refused configurations @@ -826,8 +828,8 @@ not a reconstruction of monetary RET under pre-1978 law. | Noncovered foreign work | `FOREIGN_WORK` / `noncovered_foreign_work` | Act 203(c); 20 CFR 404.417-.418 | | Refusal of vocational rehabilitation | `VOCATIONAL_REHABILITATION` / `vocational_rehabilitation_refusal` | RS 02501.130 | | Another deduction event on the record | `OTHER_DEDUCTION` / `other_deduction_event` | Act 203(c)/(d); RS 02501.130; GN 02603.040 | -| Unmarried purported deemed entitlement | `DEEMED_ENTITLEMENT_BASIS` / `deemed_entitlement_marriage_basis` | RS 02501.140's wording is broader than Act 203(b)(1)/20 CFR 404.420 | -| Ordinary deemed adult CHILD with asserted marriage and age facts | `DEEMED_ENTITLEMENT_BASIS` / `deemed_entitlement_marriage_basis` | Act 203(b)(1)'s adult-child wording is broader than CFR .420's disabled-child basis | +| Unmarried purported deemed entitlement | `InvalidFamilyInput`: not on this NH's ledger | Act 203(b)(1) and 20 CFR 404.420 require marriage; RS 02501.140's broader wording yields (P1) | +| Ordinary deemed adult CHILD with asserted marriage and age facts | `DEEMED_ENTITLEMENT_BASIS` / `deemed_entitlement_marriage_basis` | Entitled only through 402(d)(5) for a marriage made while disabled (402(s)(2)); CFR .420 omits that residual case | The monthly G context establishes maximum membership, original age reduction and dual entitlement. Its declared baseline must match the R2 @@ -895,11 +897,11 @@ In the table, unqualified property test names are in | Exact 1/2 and 1/3 excess rates use an integer floor: excess is nonnegative and differs from the unrounded amount by less than $1 | readings `test_property_excess_uses_exact_statutory_rate_and_dollar_floor`; unit `test_lower_threshold_and_whole_dollar_floor`; unit `test_fra_threshold_uses_one_third_and_floors`; R1 differential above | | Invalid input is never counted as a statutory coverage gap | `test_coverage_denominators_keep_every_supported_or_refused_row`; unit `test_invalid_wage_inputs_raise_before_coverage_refusal`; `test_invalid_rates_raise_before_coverage_refusal`; `test_duplicate_identity_is_invalid_even_in_an_unsupported_record`; `test_invalid_inputs_are_not_counted_as_coverage_gaps` | | Every raw month-set element is validated before deduplication, independent of order; valid integer duplicates in the ledger preserve the same set | `test_every_raw_month_set_element_is_validated_before_deduplication`; `test_valid_month_set_duplicates_and_order_preserve_the_same_months`; grace `test_property_external_grace_month_sets_validate_before_deduplication`; unit `test_invalid_self_employment_month_collision_is_an_input_error`; `test_invalid_entitlement_month_collision_precedes_foreign_work_refusal` | -| Under the explicit POMS election reading, only eligible elected RIB NSMs receive grace protection; the conservative CFR reading supplies none during concurrent disability entitlement | readings `test_property_explicit_poms_election_protects_only_eligible_rib_months`; `test_simultaneous_dib_rib_source_readings_change_4200_dollars` | +| Under the default 202(k)(4) reading, only eligible elected RIB NSMs receive grace protection; the explicit CFR technical-entitlement alternative supplies none during concurrent disability entitlement | readings `test_property_explicit_poms_election_protects_only_eligible_rib_months`; `test_simultaneous_dib_rib_source_readings_change_4200_dollars` | | Actual and estimated grace statuses are distinct; changed service facts can require recovery even when annual earnings coincide | readings `test_estimated_initial_grace_and_actual_grace_are_separate`; `test_property_changed_grace_estimate_can_require_recovery` | | Zero payable caps retain their original shares and count toward the three-beneficiary sub-dollar stop; exact net shares plus the residual conserve the pool | `test_zero_cap_participants_preserve_total_and_trigger_source_stop`; unit `test_entitled_zero_rate_auxiliary_keeps_its_original_partial_share` | | Explicit Act 203(f)(1)(C)-(D) protections exempt only the recipient's own work in asserted CHILD/WIDOW months | readings `test_property_other_statutory_own_exemptions_protect_only_that_workers_excess`; `test_own_work_exempt_flag_requires_a_statutory_benefit_type` | -| The test-only broader deemed-CDB reading agrees exactly with the supported married full-family charge; the Act/CFR reading preserves the unrelated record, while the public unmarried input always refuses | readings `test_property_deemed_counterfactual_matches_full_married_charge`; `test_unmarried_deemed_entitlement_counterfactual_changes_200_dollars` | +| The test-only broader deemed-CDB reading agrees exactly with the supported married full-family charge; the Act/CFR reading preserves the unrelated record, while the public unmarried input is always an input error | readings `test_property_deemed_counterfactual_matches_full_married_charge`; `test_unmarried_deemed_entitlement_counterfactual_changes_200_dollars` | The `ss/benefits.py` float oracle has no earnings-test ledger implementation, so there is no second RET-payment implementation against which to invent a @@ -1318,28 +1320,31 @@ concurrent-disability default, the termination exclusions, refusal codes, or any ruling d1418 choice. No new statutory judgment or source erratum is introduced by these two corrections. -## Needs Max's judgment +## Readings taken and refusals kept -**Ruling d1418 (Max, 2026-10-12): keep the current defaults for these -readings, and reopen any one when a population target needs it.** The -computed defaults below (NESE losses floored; no non-service months during -simultaneous RIB and DIB entitlement, per 20 CFR 404.435(b)(3); own-work -protections limited to the CFR's disability-based wording) stand, and the -broader POMS deemed-entitlement reading stays refused. +**Ruling d1418 (Max, 2026-10-12)** kept these readings at their defaults +until a population target needed them. Max's 2026-10-20 delegation makes +each single reading the session's call under his principles (P1 statute +first; P2 regulations are law; P3 the agency breaks true ties), logged in +the source-conflict rulings; this section records the October 23, 2026 +pass. Two defaults change money: an elected RIB month during simultaneous +RIB and DIB entitlement now counts as a non-service month, and an unmarried +"deemed" recipient is no longer on the NH's ledger. The other readings +keep their computed defaults with the basis stated. The review resolved the historical monthly fractions and sub-dollar reapportionment stop directly from the captured Act and CFR text. Those -rules are implemented above and are no longer judgment items. The -remaining money-changing readings are explicit below. -The production decision comments link the retained readings to the -source-conflict register IDs `us:oasdi-ret-negative-nese-proration`, +rules are implemented above and are no longer judgment items. +The production decision comments link each reading to its source-conflict +register ID: `us:oasdi-ret-negative-nese-proration`, `us:oasdi-ret-grace-simultaneous-rib-dib`, -`us:oasdi-ret-own-work-age-exemptions` and -`us:oasdi-ret-deemed-entitlement-basis`. +`us:oasdi-ret-own-work-age-exemptions`, +`us:oasdi-ret-deemed-entitlement-basis` and +`us:oasdi-ret-other-deductee-scope`. `test_source_conflict_ids_are_linked_at_the_decision_lines` checks every -link at its decision, including both deemed-entitlement refusals. +link at its decision, including both deemed-entitlement decisions. -### Signed NESE: floor versus dropping cents +### Reading taken: signed NESE rounds down to the floor RS 02501.080B.2.c says: “prorate the net earnings (or loss) equally over all months the beneficiary was engaged in self-employment”. Its @@ -1350,8 +1355,14 @@ the cents” supports truncation toward zero, while “round down” supports mathematical floor. The two readings agree on the published positive NESE examples. -`NESEProrationPolicy.FLOOR` follows the literal “round down” instruction; -`DROP_CENTS` is an explicit alternative. In an invented June FRA year, +Kept: `NESEProrationPolicy.FLOOR` (one meaning per section; P3). Act +203(f)(3) leaves the proration to the agency ("prorated in an equitable +manner under regulations of the Commissioner of Social Security"), and no +captured regulation fixes its rounding, so SSA's instruction governs. +Within RS 02501.080, "round down" means "round down to the next lower +dollar" (B.1.d, B.2.a); applied after the cents are dropped, only the floor +gives that step any work, while truncation would leave it surplus. +`DROP_CENTS` remains an explicit alternative. In an invented June FRA year, annual NESE loss −$13 over twelve months becomes monthly −$2 and five-month −$10 under floor, versus monthly −$1 and five-month −$5 under dropping cents. With pre-FRA wages equal to R1's higher exempt @@ -1362,7 +1373,7 @@ conservative. Tests `test_negative_nese_floor_and_drop_cents_readings_are_explic and `test_negative_nese_reading_changes_final_deduction_by_one_dollar` pin the one-dollar difference. -### Simultaneous RIB and DIB entitlement +### Reading taken: an elected RIB month counts as a non-service month 20 CFR 404.435(b)(3) says: “we will not count as a non-service month, a month that occurred while the beneficiary was entitled to disability @@ -1372,55 +1383,63 @@ says: “If the beneficiary becomes simultaneously entitled to RIB and DIB benefits, the earnings test (ET) applies to any month they elect to receive RIB.” It adds: “Count NSMs for any month the RIB applies.” -`DisabilityGraceReading.CFR_ENTITLEMENT_EXCLUSION` is the conservative -default: technical disability entitlement prevents those NSMs. -`POMS_RIB_ELECTION` follows the express operational exception and is -needed to reproduce the POMS 2010/2011/2012 election example. Both -preserve technical entitlement history; neither invents a break when -the elected payment changes. The grace properties and -`test_simultaneous_disability_cfr_default_and_poms_alternative_are_explicit` -pin both readings. In an invented 2026 ledger, $45,000 January wages -precede entitlement to both RIB and DIB in June. The beneficiary elects -RIB at $600 monthly June-December and has no wages or services in those -months. Under the conservative reading all $4,200 is deducted; under -the POMS reading all $4,200 is paid. Test +Changed default (P1): Act 202(k)(4) says a person entitled to both an +old-age and a disability insurance benefit "shall be entitled to only the +larger of such benefits for such month, except that, if such individual so +elects, he shall instead be entitled to only the smaller of such benefits +for such month" (20 CFR 404.407(c) says the same). A month in which RIB is +paid is therefore a month of entitlement to RIB alone, not one "entitled to +disability benefits under section 223", and the regulation and RS +02501.030B.4 agree once the Act is read. `grace_year` now defaults to +`DisabilityGraceReading.POMS_RIB_ELECTION`; DWB and CDB months, which +202(k)(4) does not touch, stay excluded, and +`CFR_ENTITLEMENT_EXCLUSION` keeps the former technical-entitlement +exclusion as an explicit alternative. Both preserve technical entitlement +history; neither invents a break when the elected payment changes. In an +invented 2026 ledger, $45,000 January wages precede entitlement to both +RIB and DIB in June. The beneficiary elects RIB at $600 monthly +June-December and has no wages or services in those months. The default +now pays all $4,200; the alternative deducts it. Test `test_simultaneous_dib_rib_source_readings_change_4200_dollars` executes -that money difference, and -`test_property_explicit_poms_election_protects_only_eligible_rib_months` -executes the related ledger property. +that money difference; +`test_simultaneous_disability_default_follows_202k4_and_cfr_alternative_is_explicit` +pins the default and quotes; and the grace properties execute both +values over arbitrary months. -### Other own-work age clauses +### Reading taken: the Act and the CFR coincide on own-work age exemptions Act 203(f)(1)(C) prohibits charging a month “in which such individual, if a child entitled to child's insurance benefits, has attained the age of 18”. Clause (D) prohibits charging a month “for which such individual is entitled to widow's or widower's insurance benefits if such individual became so entitled prior to attaining age 60”. The preceding sentence -expressly says “but subject to section 202(s)”. This lane has no full -capture of 202(s) and does not adjudicate that additional condition. - -20 CFR 404.435(a)(5)-(6) instead list “Are age 18 or over and entitled -to a child's insurance benefit based on disability” and “Are entitled -to a widow's or widower's insurance benefit based on disability”. The -POMS payment-type exemption in RS 02501.021B.5.a also lists DIB/DWB/CDB. -The Act's express age clauses appear broader than those operational -disabled-benefit descriptions. - -The automatic default exemptions therefore follow the explicit -disability types. The documented `own_work_exempt_months` parameter -defaults to empty and is the alternative Act-clause assertion: the -caller must establish lawful entitlement, the age facts and any 202(s) -condition before asserting it. The ledger validates CHILD/WIDOW types -but does not derive those exemptions silently from an inferred age. -Max must decide how monthly assembly will establish and select this -broader reading. In invented $200 monthly ordinary CHILD and WIDOW -cases, earnings equal R1's lower amount plus $1,200. Default own -deductions total $600; the explicitly asserted Act exemption pays the -additional $600. Both cases are executed by +expressly says “but subject to section 202(s)”. 20 CFR 404.435(a)(5)-(6) +instead list “Are age 18 or over and entitled to a child's insurance +benefit based on disability” and “Are entitled to a widow's or widower's +insurance benefit based on disability”. The POMS payment-type exemption in +RS 02501.021B.5.a also lists DIB/DWB/CDB. + +No conflict remains once the rest of the Act is read (P1). Section +202(s)(3), now captured, says "subsection (f)(1)(C) of section 403 ... +shall not apply in the case of any child with respect to any month +referred to therein unless in such month or the third month prior thereto +such child was under a disability", so clause (C) reaches only a disabled +child. A widow(er) entitled before 60 is a disabled widow(er), and 402(e)(1) +ends that entitlement with "the termination month" ("the third month +following the month in which her disability ceases") unless retirement age +comes first, so clause (D)'s months are months of a benefit based on +disability. The automatic exemptions therefore follow the explicit +disability types, which is the Act's own scope. The documented +`own_work_exempt_months` parameter defaults to empty; it is not a broader +lawful reading for an ordinary CHILD or WIDOW month, and remains only as a +caller's explicit assertion of facts the ledger does not adjudicate. In +invented $200 monthly ordinary CHILD and WIDOW cases, earnings equal R1's +lower amount plus $1,200. Default own deductions total $600; the asserted +exemption pays the additional $600. Both cases are executed by `test_optional_act_age_exemptions_are_an_explicit_600_dollar_reading`, with the general selected-exemption property in the invariant table. -### Broader POMS deemed entitlement +### Reading taken: deemed entitlement requires marriage to the NH Act 203(b)(1)'s third sentence says a recipient of child's benefits who has attained age 18, or mother's/father's benefits, “is married to an individual entitled to @@ -1432,65 +1451,60 @@ benefits on a third person's record is deemed entitled on the spouse's record. RS 02501.140 instead says: “deem a retirement beneficiary's spouse or child entitled on their record if they are receiving CDB, mother's or father's benefits, or child's benefits based on another -person's ER.” The POMS wording appears to reach an unmarried child -more broadly than the Act/CFR marriage requirement. The Act additionally -covers an entitled adult child without expressly requiring disability; -the CFR's stated child basis uses disability. This additional coverage -difference is refused for an ordinary CHILD even after the caller asserts -lawful age-18 and marriage facts, pending Max's reading. - -The default requires `deemed_married_to_nh=True` to assert the legal -marriage facts; broader unmarried cases receive -`DEEMED_ENTITLEMENT_BASIS`, as do ordinary deemed adult CHILD cases. -No monetary answer is fabricated for a -refused configuration. The lawful married benchmark has NH $400, -deemed-entitled CDB $200 and NH excess $600; the NH charge includes -the recipient's full $200 January benefit even though CDB's own -earnings test is exempt. Applying the broader wording to an otherwise -identical unmarried recipient would likewise remove $200 from that -recipient; treating the unrelated record independently leaves it -untouched. `test_unmarried_deemed_entitlement_is_refused_pending_source_judgment` -and `test_married_deemed_disabled_child_is_charged_number_holders_excess` -pin the refusal and the statutory benchmark. -`test_unmarried_deemed_entitlement_counterfactual_changes_200_dollars` -now executes both alternative readings with an explicit test-only -full-January-charge helper: the broader POMS reading pays $0 instead -of $200 on the unrelated recipient's record. Annual checks are $2,200 -versus $2,400 in the otherwise identical married benchmark and separate -unrelated record. It checks the public unmarried input still refuses. -`test_property_deemed_counterfactual_matches_full_married_charge` compares -the narrow helper with the supported married ledger and separate record -over invented whole-dollar rates, with zero tolerance. This helper does -not change marriage facts or enable the broader reading through the -public API; ruling d1418's defaults remain in effect. -The Act's ordinary-adult-child wording would include the same $200 in -that benchmark. The CFR disability-only boundary refuses that ordinary -child case. `test_deemed_ordinary_child_requires_explicit_adult_age_facts` -pins invalid missing-age facts and the default coverage refusal with -established age facts; the married CDB benchmark pins the supported -$200 NH deduction. - -### Scope wording for another deductee - -The part-(a) brief asks for refusal when an auxiliary's own excess is -charged with a binding maximum “or any other deductee on the record -that month”. It also requires reproduction of 20 CFR 404.441, whose -rule expressly covers the same-month interaction: “Where both the -insured individual and another person entitled (or deemed entitled) -on the same earnings record have excess earnings”. Act 203(b)(1) -likewise charges a beneficiary's remaining benefits after an earlier -deduction, and 20 CFR 404.434(b)(3) specifies NH charging first. - -This build interprets the other-deductee refusal as other deduction -events under Act 203(c)/(d)/222(b), represented by -`other_deduction_months`, which are always refused. Simultaneous NH -and auxiliary RET deductions without a binding maximum are supported -so the explicitly required CFR .441 example can be reproduced. That -example gives the wife's August $83 NH partial share, fully consumed -by her own excess, and her October $82 check. Interpreting the brief -as excluding any simultaneous RET deductee would instead refuse the -example, not produce a different computed amount. This scope reading -needs confirmation if the intended part-(a) boundary was broader. +person's ER.” + +Changed (P1; P2 agrees): the Act and the regulation both require the +marriage, so RS 02501.140's broader wording yields. An unmarried recipient +is not deemed and is not on the NH's ledger: declaring `deemed_entitled` +without `deemed_married_to_nh` is now `InvalidFamilyInput`, and the +recipient is computed on their own record. The lawful married benchmark +has NH $400, deemed-entitled CDB $200 and NH excess $600; the NH charge +includes the recipient's full $200 January benefit even though CDB's own +earnings test is exempt. The broader POMS wording would remove the same +$200 from an unmarried recipient; the reading taken leaves it paid on that +recipient's own record (annual $2,400 versus $2,200 in the married +benchmark). `test_unmarried_deemed_entitlement_is_an_input_error`, +`test_unmarried_deemed_entitlement_counterfactual_changes_200_dollars` and +`test_property_deemed_counterfactual_matches_full_married_charge` pin the +error, the $200 difference and the exact agreement of the test-only +broader harness with the supported married ledger. + +Refusal kept for the residual adult-child case: the Act's words also +reach "a child who has attained the age of 18 and is entitled to child's +insurance benefits", where 20 CFR 404.420 says "a child's insurance +benefit based on the child's disability". A non-disabled child's benefit +ends with the month the child marries (402(d)(1)(D)), and 402(d)(5)'s +exception for marrying a beneficiary applies only to a child "under a +disability" at the time of the marriage (402(s)(2)). So a married, +non-disabled adult child keeps child's benefits only in the residual case +of a marriage made while disabled followed by recovery, which the +regulation omits. That configuration stays refused as +`DEEMED_ENTITLEMENT_BASIS`, with its row and weight kept; +`test_deemed_ordinary_child_requires_explicit_adult_age_facts` pins +invalid missing-age facts and the refusal, and the married CDB benchmark +pins the supported $200 NH deduction. + +### Not a source conflict: another deductee's scope + +Logged as `us:oasdi-ret-other-deductee-scope`. The part-(a) brief asks for +refusal when an auxiliary's own excess is charged with a binding maximum +“or any other deductee on the record that month”. It also requires +reproduction of 20 CFR 404.441, whose rule expressly covers the same-month +interaction: “Where both the insured individual and another person +entitled (or deemed entitled) on the same earnings record have excess +earnings”. Act 203(b)(1) likewise charges a beneficiary's remaining +benefits after an earlier deduction, and 20 CFR 404.434(b)(3) specifies +NH charging first. + +The regulation settles the computation (P2), so the brief's +other-deductee refusal is read as other deduction events under Act +203(c)/(d)/222(b), represented by `other_deduction_months`, which are +always refused. Simultaneous NH and auxiliary RET deductions without a +binding maximum are supported so the explicitly required CFR .441 example +can be reproduced. That example gives the wife's August $83 NH partial +share, fully consumed by her own excess, and her October $82 check. +Interpreting the brief as excluding any simultaneous RET deductee would +instead refuse the example, not produce a different computed amount. ## Deduction before reduction and the G contract diff --git a/scripts/track_b_g_conformance_report.py b/scripts/track_b_g_conformance_report.py index 55cc6ffba..2a1e40d70 100644 --- a/scripts/track_b_g_conformance_report.py +++ b/scripts/track_b_g_conformance_report.py @@ -393,7 +393,7 @@ def multiple_records(child): ), ), reasons.PARENT_BENEFIT: ( - "Two potential parents; one own RIB 780 lies between 75% and 82.5% of PIA 1000", + "Two potential parents; own RIBs 780 and 790 both lie between 75% and 82.5% of PIA 1000", lambda: _family( _survivor(), [ @@ -402,7 +402,11 @@ def multiple_records(child): g.Role.PARENT, own_benefit=g.OwnBenefit(rib, F(780)), ), - g.Beneficiary("p2", g.Role.PARENT), + g.Beneficiary( + "p2", + g.Role.PARENT, + own_benefit=g.OwnBenefit(rib, F(790)), + ), ], ), ), diff --git a/src/populace_dynamics/track_b/gross_benefits.py b/src/populace_dynamics/track_b/gross_benefits.py index 16addf5c2..bc24ab140 100644 --- a/src/populace_dynamics/track_b/gross_benefits.py +++ b/src/populace_dynamics/track_b/gross_benefits.py @@ -969,6 +969,7 @@ def statutory_rates(params: SSAParameters) -> StatutoryRates: _input_type(getattr(params, name), int, name) if getattr(params, name) != 36: raise InvalidFamilyInput(f"{name} must be 36 (402(q))") + # Source conflict us:oasdi-reduction-exact-fractions return _construct_declared( StatutoryRates, worker_first=_STATUTE["worker_first"], @@ -1075,6 +1076,7 @@ def disability_family_maximum(pia: object, aime: object) -> Fraction: earnings = _exact(aime, "AIME") if earnings < 0 or earnings.denominator != 1: raise InvalidFamilyInput("AIME must be a nonnegative whole dollar") + # Source conflict us:oasdi-disability-family-maximum-rounding return _floor_dime_validated( min(max(_DI_AIME_SHARE * earnings, amount), _DI_PIA_CAP * amount) ) @@ -2838,12 +2840,27 @@ class _Row: counted: Fraction = Fraction(0) mother_father_rate: Fraction = Fraction(0) note: str = "" + dwb_under_60: bool = False @property def dual(self) -> bool: return self.own_mba is not None +def _dwb_under_60(beneficiary: Beneficiary, payment_month: YearMonth) -> bool: + """A DWB month in which the SSA birth cohort cannot yet have attained 60. + + Cohort Y (born January 2, Y through January 1, Y+1) attains 60 during + Y+60, so every month of Y+59 and earlier is under 60. Months of Y+60 + depend on the unsupplied birth month and are not treated as under 60. + """ + return ( + beneficiary.role is Role.DISABLED_WIDOW + and beneficiary.birth_year is not None + and payment_month.year <= beneficiary.birth_year + 59 + ) + + def _is_entitled( beneficiary: Beneficiary, pia: Fraction, @@ -2973,8 +2990,9 @@ def _parisi_counted( The own-RIB reduction is not an auxiliary age reduction (GN 02603.060B.2: $2.40 payable + $0.90 reduction, not the $16 own-RIB - reduction). GN 02603.050A.2 disregards a dual rate reduced to zero. - Own delayed credits and redistribution with an actually limiting + reduction). Together they are the .045 amount "before any applicable + reduction for age"; GN 02603.050A.2 disregards a dual rate whose + before-age amount is zero. Own delayed credits and redistribution with an actually limiting RIB-LIM remain refused: none of the captured worked cases settles those interacting amounts. A considered but nonlimiting RIB-LIM changes no rate (RS 00615.320A.3). @@ -3017,16 +3035,12 @@ def _parisi_counted( if payable == 0: before_age = row.standard - (own.pia if method_c else row.own_mba) # Source conflict us:oasdi-dual-entitlement-zero-after-age-reduction - if before_age > 0: - raise _computed( - FamilyConfigurationUnsupported, - UnsupportedReason.PARISI_AMBIGUOUS, - "dual payable reaches zero only after auxiliary age reduction: " - "GN 02603.045's before-age rule and GN 02603.050A.2's zero-rate " - "exception do not settle whether the age amount stays in PTAP", - beneficiary_id=beneficiary.beneficiary_id, - ) - return Fraction(0), Fraction(0) + # GN 02603.050A applies its exceptions 2 and 3 under .045, which + # counts the dual rate "before any applicable reduction for age". + # A before-age zero is disregarded (.050A.2); a positive before-age + # amount is a partial rate (.050A.3), counted whole as the age term. + # A positive payable below counts the same total. + return Fraction(0), max(Fraction(0), before_age) if method_c: excess = max(Fraction(0), row.standard - own.pia) age_reduction = excess - _spouse_age_reduced_validated( @@ -3070,6 +3084,12 @@ def _refuse_historical_auxiliary( if isinstance(beneficiary.child_in_care, str): return elif role is Role.DISABLED_WIDOW: + # Source conflict us:oasdi-dwb-1984-pre-1978-history + # RS 00615.301B.1.e: "all disabled widow(er)s under age 60 receive + # .715 of the full unreduced WIB for 1/84 and later", whatever the + # entitlement date; .005C.2's money history governs only from 60. + if row.dwb_under_60: + return first_age = 50 elif role in _AGED_WIDOW_ROLES: first_age = 50 @@ -3089,6 +3109,7 @@ def _refuse_historical_auxiliary( own = beneficiary.own_benefit low, high = Fraction(0), row.rate if role in (Role.SPOUSE, Role.DIVORCED_SPOUSE): + # Source conflict us:oasdi-dual-entitlement-pre-1978-history if own is not None and beneficiary.own_benefit_first: excess = max(Fraction(0), row.rate - own.pia) high = max(Fraction(0), row.rate - (own.pia - row.own_base)) @@ -3348,12 +3369,17 @@ def _entitled_parent_count( state: RecordState, beneficiaries: Sequence[Beneficiary], params: SSAParameters, -) -> int: - """Resolve 402(h)(2)'s count, refusing the circular RIB threshold. - - RS 00209.001 excludes a parent whose own RIB is not less than the - parent's benefit; RS 00209.015 counts an own-DIB parent as technically - entitled. More than two declared parents needs a separate rule. +) -> tuple[int, frozenset[str]]: + """Resolve 402(h)(2)'s count and the parents (h)(1)(D) excludes. + + 402(h)(1)(D) tests an own RIB against 82.5 percent of the PIA when the + parent's benefit "is determinable under paragraph (2)(A)", and against + 75 percent "in any other case". The entitled set must satisfy that test + for every parent at once: each entitled parent at its own rate, and + each excluded potential parent at the rate it would share. RS + 00209.015 counts an own-DIB parent as technically entitled. Returns + the count and the ids excluded at the two-parent rate. More than two + declared parents needs a separate rule. """ parents = [b for b in beneficiaries if b.role is Role.PARENT] if len(parents) > 2: @@ -3381,23 +3407,28 @@ def _entitled_parent_count( if own_mba >= one_ob: continue potential.append((parent, own_mba)) - if len(potential) == 2: - for parent, own_mba in potential: - # Source conflict us:oasdi-parent-entitlement-count-interval - if ( - parent.own_benefit is not None - and parent.own_benefit.kind is OwnBenefitKind.OLD_AGE - and own_mba >= two_ob - ): - raise _computed( - FamilyConfigurationUnsupported, - UnsupportedReason.PARENT_BENEFIT, - "two potential parents with own RIB between the " - "75% and 82.5% OBs have a circular entitlement count " - "under RS 00209.001 and .015", - beneficiary_id=parent.beneficiary_id, - ) - return max(1, len(potential)) + in_band = [ + parent + for parent, own_mba in potential + if parent.own_benefit is not None + and parent.own_benefit.kind is OwnBenefitKind.OLD_AGE + and own_mba >= two_ob + ] + if len(potential) < 2 or not in_band: + return max(1, len(potential)), frozenset() + # Source conflict us:oasdi-parent-entitlement-count-interval + # A RIB in [75%, 82.5%) fails beside an entitled parent and passes + # alone. With one such parent, only "the other parent alone" satisfies + # 402(h)(1)(D) for both; with two, either parent alone does. + if len(in_band) == 2: + raise _computed( + FamilyConfigurationUnsupported, + UnsupportedReason.PARENT_BENEFIT, + "two parents with own RIB between the 75% and 82.5% OBs: " + "402(h)(1)(D) is satisfied by either parent alone", + beneficiary_id=in_band[0].beneficiary_id, + ) + return 1, frozenset({in_band[0].beneficiary_id}) def _mother_father_care_rate( @@ -3530,7 +3561,16 @@ def _family_benefits( "IEDS before January 1985, or on a WEP-affected worker record " "before January 2024, is not implemented (RS 00202.100)", ) - parent_count = _entitled_parent_count(state, beneficiaries, params) + parent_count, excluded_parents = _entitled_parent_count( + state, beneficiaries, params + ) + two_parent_ob = ( + _original_benefit_validated( + Role.PARENT, FamilyKind.SURVIVOR, state.pia, parent_count=2 + ) + if excluded_parents + else None + ) life_case = state.kind is not FamilyKind.SURVIVOR and not ieds worker_benefit = None if state.kind is FamilyKind.RETIREMENT and not ieds: @@ -3557,7 +3597,14 @@ def _family_benefits( ), ) entitled, note = _is_entitled( - beneficiary, state.pia, own_mba, parent_ob=ob + beneficiary, + state.pia, + own_mba, + parent_ob=( + two_parent_ob + if beneficiary.beneficiary_id in excluded_parents + else ob + ), ) rows.append( _Row( @@ -3568,6 +3615,7 @@ def _family_benefits( own_base=own_base, own_mba=own_mba, note=note, + dwb_under_60=_dwb_under_60(beneficiary, payment_month), ) ) @@ -3948,6 +3996,7 @@ def _after_deductions( } if not events: return baseline + # Source conflict us:oasdi-nh-charge-age-reduction-interaction if any(worker_deductions.values()) and any( row.age_reduction_factor != 1 for row in baseline.beneficiaries ): @@ -4079,6 +4128,7 @@ def _after_deductions( standard=old.standard_rate, rate=rate, counted=rate, + dwb_under_60=_dwb_under_60(member, payment_month), ) if ( isinstance(member.child_in_care, str) @@ -4701,6 +4751,7 @@ def _savings_payment( row for row in result.beneficiaries if row.subject_to_family_maximum ] reduced = [row for row in subject if row.age_reduction_factor < 1] + # Source conflict us:oasdi-saving-clause-procedure-gaps if ( result.kind is FamilyKind.SURVIVOR or any(row.own_benefit is not None for row in subject) @@ -5020,6 +5071,7 @@ def _operational_admit( total: Fraction, ) -> FamilyBenefits: membership = _savings_membership(result) + # Source conflict us:oasdi-saving-clause-procedure-gaps if ( self._protected_membership is not None and membership != self._protected_membership diff --git a/src/populace_dynamics/track_b/ret_grace.py b/src/populace_dynamics/track_b/ret_grace.py index 268493519..047c5b618 100644 --- a/src/populace_dynamics/track_b/ret_grace.py +++ b/src/populace_dynamics/track_b/ret_grace.py @@ -84,12 +84,16 @@ class GraceKind(Enum): class DisabilityGraceReading(Enum): - """Explicit money-changing CFR/POMS reading for concurrent disability. - - 404.435(b)(3) excludes any month entitled to disability benefits; POMS - RS 02501.030B.4/.021B.5.a explicitly excepts simultaneous RIB/DIB when - RIB is elected. The default preserves the CFR's conservative exclusion. - The POMS alternative is necessary to reproduce its election example. + """Explicit money-changing reading for concurrent RIB and DIB. + + 404.435(b)(3) excludes a month "while the beneficiary was entitled to + disability benefits under section 223". Act 202(k)(4) makes a person + entitled to both "entitled to only the larger of such benefits for such + month", or, on election, "only the smaller": a month RIB is paid is a + month of entitlement to RIB alone, so the default counts its NSMs, as + RS 02501.030B.4 does. CFR_ENTITLEMENT_EXCLUSION keeps the former + technical-entitlement exclusion as an explicit alternative. DWB and CDB + months stay excluded under either value. """ CFR_ENTITLEMENT_EXCLUSION = "cfr_entitlement_exclusion" @@ -387,7 +391,7 @@ def grace_year( fra_month: int | None = None, termination_events: Sequence[TerminationEvent] = (), disability_reading: DisabilityGraceReading = ( - DisabilityGraceReading.CFR_ENTITLEMENT_EXCLUSION + DisabilityGraceReading.POMS_RIB_ELECTION ), ) -> GraceYearDecision: """Apply RS 02501.030B-D for one person's calendar entitlement year. @@ -449,6 +453,7 @@ def grace_year( if month.received_types - DISABILITY_TYPES and (fra_month is None or index < fra_month) and ( + # 202(k)(4): a month RIB is paid is entitlement to RIB alone. # Source conflict us:oasdi-ret-grace-simultaneous-rib-dib not month.entitled_types & DISABILITY_TYPES or ( diff --git a/src/populace_dynamics/track_b/ret_ledger.py b/src/populace_dynamics/track_b/ret_ledger.py index 2fe571868..47817da0d 100644 --- a/src/populace_dynamics/track_b/ret_ledger.py +++ b/src/populace_dynamics/track_b/ret_ledger.py @@ -239,10 +239,13 @@ class RETBeneficiary: least two years since divorce; prior NH RIB entitlement is the other Act 203(b)(2) exception. ``deemed_entitled`` and ``deemed_married_to_nh`` assert the married recipient in Act 203(b)(1) - / CFR .420. An ordinary child's Act 203(b)(1) age facts need explicit - ``deemed_child_age_18_months``; that Act/CFR coverage difference is - refused pending judgment. Broader unmarried-child wording in - RS 02501.140 is also refused. Own two-record excess is still refused. + / CFR .420; both require the marriage, so an unmarried deemed recipient + (RS 02501.140's broader wording) is an input error: that person is not + on this NH's ledger. An ordinary child's Act 203(b)(1) age facts need + explicit ``deemed_child_age_18_months``; such a married non-disabled + child is entitled only if 402(d)(5) kept the benefit through a marriage + made while disabled (402(s)(2)), a residual case CFR .420 omits, so it + stays refused. Own two-record excess is still refused. ``own_work_exempt_months`` supplies other Act 203(f)(1)(C)-(D) protections (entitled child who has attained 18 / widow(er) entitled before 60). It defaults to empty under the CFR's @@ -343,6 +346,14 @@ def __post_init__(self): "Child or widow benefits are required for other own-work exemptions" ) if self.deemed_entitled: + # Act 203(b)(1) and 20 CFR 404.420 deem only a recipient married + # to the NH; RS 02501.140's broader wording yields to them. + # Source conflict us:oasdi-ret-deemed-entitlement-basis + if not self.deemed_married_to_nh: + raise InvalidFamilyInput( + "Deemed entitlement requires marriage to the NH " + "(Act 203(b)(1); 20 CFR 404.420)" + ) if any( types[m - 1] not in ( @@ -1126,6 +1137,7 @@ def apply_ret_year( record.vocational_rehabilitation_refusal, UnsupportedReason.VOCATIONAL_REHABILITATION, ), + # Source conflict us:oasdi-ret-other-deductee-scope ( bool(record.other_deduction_months), UnsupportedReason.OTHER_DEDUCTION, @@ -1140,14 +1152,6 @@ def apply_ret_year( ) for b in record.beneficiaries: # Source conflict us:oasdi-ret-deemed-entitlement-basis - if b.deemed_entitled and not b.deemed_married_to_nh: - raise _computed( - RETConfigurationUnsupported, - UnsupportedReason.DEEMED_ENTITLEMENT_BASIS, - "Unmarried deemed entitlement: RS .140 is broader than Act 203(b)(1) / CFR .420", - beneficiary_id=b.beneficiary_id, - ) - # Source conflict us:oasdi-ret-deemed-entitlement-basis if b.deemed_entitled and any( b.benefit_types[m - 1] is BenefitType.CHILD for m in b.entitled_months @@ -1155,7 +1159,8 @@ def apply_ret_year( raise _computed( RETConfigurationUnsupported, UnsupportedReason.DEEMED_ENTITLEMENT_BASIS, - "Act adult-child basis versus CFR .420 disability basis needs judgment", + "A married non-disabled adult child keeps CIB only through " + "402(d)(5) and 402(s)(2); CFR .420 omits that residual case", beneficiary_id=b.beneficiary_id, ) people = { diff --git a/tests/data/track_b/gross_benefit_sources/lii_usc_402_415_excerpts.txt b/tests/data/track_b/gross_benefit_sources/lii_usc_402_415_excerpts.txt index 877f18bb3..926c1aad9 100644 --- a/tests/data/track_b/gross_benefit_sources/lii_usc_402_415_excerpts.txt +++ b/tests/data/track_b/gross_benefit_sources/lii_usc_402_415_excerpts.txt @@ -22,3 +22,27 @@ substring of that normalized text; the manifest records the full served bodies. [415(i)(2)(A)(ii)-(iii)] (ii) If the Commissioner of Social Security determines that the base quarter in any year is a cost-of-living computation quarter , the Commissioner shall, effective with the month of December of that year as provided in subparagraph (B), increase— (I) the benefit amount to which individuals are entitled for that month under section 427 or 428 of this title, (II) the primary insurance amount of each other individual on which benefit entitlement is based under this subchapter, and (III) the amount of total monthly benefits based on any primary insurance amount which is permitted under section 403 of this title (and such total shall be increased, unless otherwise so increased under another provision of this subchapter, at the same time as such primary insurance amount) or, in the case of a primary insurance amount computed under subsection (a) as in effect (without regard to the table contained therein) prior to January 1979, the amount to which the beneficiaries may be entitled under section 403 of this title as in effect in December 1978, except as provided by section 403(a)(7) and (8) of this title as in effect after December 1978. The increase shall be derived by multiplying each of the amounts described in subdivisions (I), (II), and (III) (including each of those amounts as previously increased under this subparagraph) by the applicable increase percentage ; and any amount so increased that is not a multiple of $0.10 shall be decreased to the next lower multiple of $0.10. Any increase under this subsection in a primary insurance amount determined under subparagraph (C)(i) of subsection (a)(1) shall be applied after the initial determination of such primary insurance amount under that subparagraph (with the amount of such increase, in the case of an individual who becomes eligible for old-age or disability insurance benefits or dies in a calendar year after 1979, being determined from the range of possible primary insurance amounts published by the Commissioner of Social Security under the last sentence of subparagraph (D)). (iii) In the case of an individual who becomes eligible for an old-age or disability insurance benefit, or who dies prior to becoming so eligible, in a year in which there occurs an increase provided under clause (ii), the individual's primary insurance amount (without regard to the time of entitlement to that benefit) shall be increased (unless otherwise so increased under another provision of this subchapter and, with respect to a primary insurance amount determined under subsection (a)(1)(C)(i)(I) in the case of an individual to whom that subsection (as in effect in December 1981) applied, subject to the provisions of subsection (a)(1)(C)(i) and clauses (iv) and (v) of this subparagraph (as then in effect)) by the amount of that increase and subsequent applicable increases, but only with respect to benefits payable for months after November of that year. + +[402(d)(1), closing clause through (D)] +and ending with the month preceding whichever of the following first occurs— (D) the month in which such child dies, or marries, + +[402(d)(5)] +(5) In the case of a child who has attained the age of eighteen and who marries— (A) an individual entitled to benefits under subsection (a), (b), (c), (e), (f), (g), or (h) of this section or under section 423(a) of this title , or (B) another individual who has attained the age of eighteen and is entitled to benefits under this subsection, such child 's entitlement to benefits under this subsection shall, notwithstanding the provisions of paragraph (1) but subject to subsection (s), not be terminated by reason of such marriage. + +[402(e)(1), termination month] +or, if she became entitled to such benefits before she attained age 60, subject to section 423(e) of this title , the termination month (unless she attains retirement age (as defined in section 416( l ) of this title) on or before the last day of such termination month). For purposes of the preceding sentence, the termination month for any individual shall be the third month following the month in which her disability ceases; + +[402(h)(1)(D)] +(D) is not entitled to old-age insurance benefits, or is entitled to old-age insurance benefits each of which is less than 82½ percent of the primary insurance amount of such deceased individual if the amount of the parent 's insurance benefit for such month is determinable under paragraph (2)(A) (or 75 percent of such primary insurance amount in any other case), and + +[402(h)(2)(A)-(B)] +(2) (A) Except as provided in subparagraphs (B) and (C), such parent 's insurance benefit for each month shall be equal to 82½ percent of the primary insurance amount of such deceased individual. (B) For any month for which more than one parent is entitled to parent 's insurance benefits on the basis of such deceased individual's wages and self-employment income, such benefit for each such parent for such month shall (except as provided in subparagraph (C)) be equal to 75 percent of the primary insurance amount of such deceased individual. + +[402(k)(3)(A)] +(3) (A) If an individual is entitled to an old-age or disability insurance benefit for any month and to any other monthly insurance benefit for such month, such other insurance benefit for such month, after any reduction under subsection (q), subsection (e)(2) or (f)(2), and any reduction under section 403(a) of this title , shall be reduced, but not below zero, by an amount equal to such old-age or disability insurance benefit (after reduction under such subsection (q)). + +[402(k)(4)] +(4) Any individual who, under this section and section 423 of this title , is entitled for any month to both an old-age insurance benefit and a disability insurance benefit under this subchapter shall be entitled to only the larger of such benefits for such month, except that, if such individual so elects, he shall instead be entitled to only the smaller of such benefits for such month. + +[402(s)] +(s) Child over specified age to be disregarded for certain benefit purposes unless disabled (1) For the purposes of subsections (b)(1), (c)(1), (g)(1), (q)(5), and (q)(7) of this section and paragraphs (2), (3), and (4) of section 403(c) of this title , a child who is entitled to child' s insurance benefits under subsection (d) for any month, and who has attained the age of 16 but is not in such month under a disability (as defined in section 423(d) of this title ), shall be deemed not entitled to such benefits for such month, unless he was under such a disability in the third month before such month. (2) So much of subsections (b)(3), (c)(4), 1 (d)(5), (g)(3), and (h)(4) of this section as precedes the semicolon, shall not apply in the case of any child unless such child, at the time of the marriage referred to therein, was under a disability (as defined in section 423(d) of this title ) or had been under such a disability in the third month before the month in which such marriage occurred. (3) The last sentence of subsection (c) of section 403 of this title , subsection (f)(1)(C) of section 403 of this title , and subsections (b)(3)(B), (c)(6)(B), 1 (f)(3)(B), and (g)(6)(B) 1 of section 416 of this title shall not apply in the case of any child with respect to any month referred to therein unless in such month or the third month prior thereto such child was under a disability (as defined in section 423(d) of this title ). diff --git a/tests/data/track_b/gross_benefit_sources/manifest.json b/tests/data/track_b/gross_benefit_sources/manifest.json index 30fc872b6..7ed002eff 100644 --- a/tests/data/track_b/gross_benefit_sources/manifest.json +++ b/tests/data/track_b/gross_benefit_sources/manifest.json @@ -445,7 +445,7 @@ }, { "id": "lii_usc_402_415", - "citation": "42 U.S.C. 402(b)(4)(A), (c)(4)(A), (q)(5)(A), (w)(2), (w)(6) and 415(i)(2)(A)(ii)-(iii), Legal Information Institute (excerpts)", + "citation": "42 U.S.C. 402(b)(4)(A), (c)(4)(A), (d)(1)(D), (d)(5), (e)(1), (h)(1)(D), (h)(2), (k)(3)(A), (k)(4), (q)(5)(A), (s), (w)(2), (w)(6) and 415(i)(2)(A)(ii)-(iii), Legal Information Institute (excerpts)", "url": "https://www.law.cornell.edu/uscode/text/42/402", "additional_urls": [ "https://www.law.cornell.edu/uscode/text/42/415" @@ -453,8 +453,8 @@ "retrieved_at_utc": "2026-09-29T20:58:57Z", "capture": "http_text_excerpt", "file": "lii_usc_402_415_excerpts.txt", - "bytes": 7358, - "sha256": "31daedaea817bf97d7d63685a897343ea5a47dcf98a91f9294406edfe89d0979", + "bytes": 11851, + "sha256": "9897f74871b73daf9f0d913a3dff5a38f43fc05d2c2b3427667f45f794e8ae51", "served_bodies": [ { "url": "https://www.law.cornell.edu/uscode/text/42/402", @@ -467,7 +467,7 @@ "sha256": "c3262e82e8e674caf43709c522ce60760eafb0b133e1f99221ca7aa97d54ea3a" } ], - "note": "The 402(w)(2) and (w)(6) excerpts were added from a re-fetch at 2026-09-30T00:24:38Z whose body matched the pinned 402 SHA-256 byte for byte." + "note": "The 402(w)(2) and (w)(6) excerpts were added from a re-fetch at 2026-09-30T00:24:38Z whose body matched the pinned 402 SHA-256 byte for byte. The 402(d), (e), (h), (k) and (s) excerpts were added from a re-fetch at 2026-10-23T15:21:00Z whose body matched the pinned 402 SHA-256 byte for byte." }, { "id": "ssa_act_203", diff --git a/tests/test_track_b_gross_benefits.py b/tests/test_track_b_gross_benefits.py index 1e126c56a..97c13432f 100644 --- a/tests/test_track_b_gross_benefits.py +++ b/tests/test_track_b_gross_benefits.py @@ -1782,7 +1782,11 @@ def _unsupported_cases(): g.Role.PARENT, own_benefit=g.OwnBenefit(OLD_AGE, F(780)), ), - g.Beneficiary("P2", g.Role.PARENT), + g.Beneficiary( + "P2", + g.Role.PARENT, + own_benefit=g.OwnBenefit(OLD_AGE, F(790)), + ), ], ), g.UnsupportedReason.DISABLED_WIDOW_UNDER_60: lambda: _family( @@ -2341,7 +2345,11 @@ def test_unsupported_rows_stay_in_the_denominator_and_block_totals(): g.Role.PARENT, own_benefit=g.OwnBenefit(OLD_AGE, F(780)), ), - g.Beneficiary("P2", g.Role.PARENT), + g.Beneficiary( + "P2", + g.Role.PARENT, + own_benefit=g.OwnBenefit(OLD_AGE, F(790)), + ), ], ), ) diff --git a/tests/test_track_b_gross_benefits_fix_round_8_history.py b/tests/test_track_b_gross_benefits_fix_round_8_history.py index 251aed08c..f5015401f 100644 --- a/tests/test_track_b_gross_benefits_fix_round_8_history.py +++ b/tests/test_track_b_gross_benefits_fix_round_8_history.py @@ -346,7 +346,15 @@ def test_aged_widow_can_carry_pre_1978_dwb_history_after_conversion(role): ) in source_text("poms_rs_00615_310") -def test_historical_dwb_specific_fixed_rate_alternative_is_explicit(): +def test_historical_dwb_under_60_takes_the_specific_fixed_rate(): + """Source conflict us:oasdi-dwb-1984-pre-1978-history. + + RS 00615.301B.1.e fixes every DWB month under age 60 from January 1984 + at .715 of the unreduced WIB, whatever the entitlement date, so a + cohort that could have been entitled before 1978 is paid like a later + one in those months. From the year the cohort can attain 60, .005C.2's + money history governs and the row stays refused. + """ text = source_text("poms_rs_00615_301") assert ( "all disabled widow(er)s under age 60 receive .715 of the full " @@ -354,25 +362,66 @@ def test_historical_dwb_specific_fixed_rate_alternative_is_explicit(): ) in text state, member = _case(g.Role.DISABLED_WIDOW, cohort=1927) - # January 1984 keeps both cohorts under 60, matching the specific - # captured instruction rather than relying on an aged DWB snapshot. - def at_reset(beneficiary): + def at(beneficiary, year): return g.family_benefits( state, [beneficiary], - payment_month=g.YearMonth(1984, 1), + payment_month=g.YearMonth(year, 1), params=PARAMS, standalone=True, ) - historical = g.evaluate_family("case", F(7, 3), lambda: at_reset(member)) - assert not historical.supported - assert historical.unsupported.reason.value == REASON - # Explicit alternative reading of the specific rule, not an imputed - # historical award in production. Needs Max's judgment against .005C.2. - assert F(1000) * F(715, 1000) == F(715) - modern = at_reset(replace(member, birth_year=1928)) - assert modern.by_id()["member"].auxiliary_payable == F(715) + for year in (1984, 1986): + historical = at(member, year).by_id()["member"] + modern = at(replace(member, birth_year=1928), year).by_id()["member"] + assert historical.auxiliary_payable == F(715) + assert historical.age_adjusted_rate == modern.age_adjusted_rate + assert historical.auxiliary_payable == modern.auxiliary_payable + reached_60 = g.evaluate_family("case", F(7, 3), lambda: at(member, 1987)) + assert not reached_60.supported + assert reached_60.unsupported.reason.value == REASON + counts = g.count_family_outcomes([reached_60]) + assert (counts.rows, counts.weight) == (1, F(7, 3)) + + +@given(st.integers(1912, 1927), st.integers(1984, 2000), st.integers(1, 5000)) +@settings(deadline=None) +def test_historical_dwb_is_settled_exactly_before_the_cohort_can_attain_60( + cohort, year, dollars +): + """Under 60 throughout the year: identical to a post-1977 cohort.""" + state, member = _case(g.Role.DISABLED_WIDOW, cohort=cohort, pia=F(dollars)) + month = g.YearMonth(year, 6) + outcome = g.evaluate_family( + "case", + F(1), + lambda: g.family_benefits( + state, + [member], + payment_month=month, + params=PARAMS, + standalone=True, + ), + ) + if year <= cohort + 59: + later = g.family_benefits( + state, + [replace(member, birth_year=1928)], + payment_month=month, + params=PARAMS, + standalone=True, + ) + assert outcome.supported + row = outcome.benefits.by_id()["member"] + assert ( + row.auxiliary_payable == later.by_id()["member"].auxiliary_payable + ) + assert row.auxiliary_payable == g.survivor_age_reduced( + row.family_maximum_rate, 60, 60, PARAMS + ) + else: + assert not outcome.supported + assert outcome.unsupported.reason.value == REASON def test_historical_rounding_can_defeat_a_nominal_75_percent_care_floor(): diff --git a/tests/test_track_b_gross_benefits_parents_ieds.py b/tests/test_track_b_gross_benefits_parents_ieds.py index 34fdef01c..d2e1e0f9b 100644 --- a/tests/test_track_b_gross_benefits_parents_ieds.py +++ b/tests/test_track_b_gross_benefits_parents_ieds.py @@ -242,22 +242,144 @@ def test_parent_original_benefit_uses_the_entitled_count_and_dime_floor( @given(st.integers(100, 1_000_000), st.integers(0, 100)) @settings(deadline=None) -def test_two_potential_parents_refuse_the_own_rib_entitlement_cycle( - dimes, position -): +def test_one_parent_in_the_band_yields_to_the_other_parent(dimes, position): + """Source conflict us:oasdi-parent-entitlement-count-interval. + + An own RIB in [75%, 82.5%) of the PIA fails 402(h)(1)(D) beside an + entitled parent and passes alone. The other parent (no own RIB) is + entitled either way, so the only entitled set satisfying (h)(1)(D) for + both parents is the other parent alone, paid at 82.5 percent. + """ low, high = dimes * 750 // 1000, dimes * 825 // 1000 own_dimes = low + (high - low - 1) * position // 100 own = g.OwnBenefit(g.OwnBenefitKind.OLD_AGE, F(own_dimes, 10)) - with pytest.raises(g.FamilyConfigurationUnsupported) as caught: + rows = _rows( _family( _survivor(F(dimes, 10), F(dimes, 5)), [_parent("p1", own_benefit=own), _parent("p2")], ) + ) + assert not rows["p1"].entitled + assert rows["p1"].auxiliary_payable == 0 + assert rows["p1"].own_benefit == F(own_dimes, 10) + assert rows["p2"].entitled + assert rows["p2"].original_benefit == F(high, 10) + assert rows["p2"].auxiliary_payable == F(high, 10) + + +@given(st.integers(100, 1_000_000), st.integers(0, 100), st.integers(0, 100)) +@settings(deadline=None) +def test_two_parents_in_the_band_refuse_because_either_alone_qualifies( + dimes, first, second +): + low, high = dimes * 750 // 1000, dimes * 825 // 1000 + members = [ + _parent( + bid, + own_benefit=g.OwnBenefit( + g.OwnBenefitKind.OLD_AGE, + F(low + (high - low - 1) * position // 100, 10), + ), + ) + for bid, position in (("p1", first), ("p2", second)) + ] + with pytest.raises(g.FamilyConfigurationUnsupported) as caught: + _family(_survivor(F(dimes, 10), F(dimes, 5)), members) assert caught.value.reason is g.UnsupportedReason.PARENT_BENEFIT +def _consistent_parent_sets(pia, own): + """Every entitled set satisfying 402(h)(1)(D) for every parent. + + ``own`` maps a parent id to None, ("DIB", amount) or ("RIB", amount). + A member's RIB must be below the rate of the set; an excluded parent's + RIB must reach the rate it would share by joining the set. + """ + one = g.floor_dime(pia * F(825, 1000)) + two = g.floor_dime(pia * F(3, 4)) + ids = sorted(own) + found = [] + for size in range(len(ids) + 1): + for chosen in ( + [frozenset()] + if size == 0 + else ( + [frozenset({i}) for i in ids] + if size == 1 + else [frozenset(ids)] + ) + ): + + def passes(parent, count): + kind, amount = own[parent] or (None, None) + return kind != "RIB" or amount < (one if count == 1 else two) + + if all(passes(p, len(chosen)) for p in chosen) and not any( + passes(p, len(chosen) + 1) for p in ids if p not in chosen + ): + found.append(chosen) + return found + + +own_benefits = st.one_of( + st.none(), + st.tuples( + st.sampled_from(["RIB", "DIB"]), + st.integers(1, 30_000).map(lambda dimes: F(dimes, 10)), + ), +) + + +@given(st.integers(100, 20_000), own_benefits, own_benefits) +@settings(deadline=None) +def test_parent_entitlement_satisfies_402h1d_for_every_parent( + dimes, first, second +): + """Exact differential against brute-force consistency of 402(h)(1)(D).""" + pia = F(dimes, 10) + own = {"p1": first, "p2": second} + members = [ + _parent( + bid, + own_benefit=( + None + if value is None + else g.OwnBenefit( + ( + g.OwnBenefitKind.OLD_AGE + if value[0] == "RIB" + else g.OwnBenefitKind.DISABILITY + ), + value[1], + ) + ), + ) + for bid, value in own.items() + ] + consistent = _consistent_parent_sets(pia, own) + assert consistent + outcome = g.evaluate_family( + "parents", + F(1), + lambda: _family(_survivor(pia, pia * 2), members), + ) + if len(consistent) > 1: + assert not outcome.supported + assert outcome.unsupported.reason is g.UnsupportedReason.PARENT_BENEFIT + return + assert outcome.supported + rows = _rows(outcome.benefits) + entitled = frozenset(bid for bid, row in rows.items() if row.entitled) + assert entitled == consistent[0] + rate = g.floor_dime( + pia * (F(825, 1000) if len(entitled) == 1 else F(3, 4)) + ) + for bid in entitled: + assert rows[bid].original_benefit == rate + + def test_parent_entitlement_count_counterfactual_changes_payment_by_75(): - """Invented $780 own RIB executes both orders behind the refusal.""" + """Invented $780 own RIB: both one-pass orders, and the reading taken.""" pia, maximum, own_rib = F(1000), F(1750), F(780) count_two_first = _parent_count_order_counterfactual( pia, maximum, own_rib, initial_parent_count=2 @@ -282,13 +404,24 @@ def test_parent_entitlement_count_counterfactual_changes_payment_by_75(): assert count_two_first["other_parent_payable"] - test_at_one_parent_rate[ "other_parent_payable" ] == F(75) + # Only the count-two-first state satisfies 402(h)(1)(D) for both + # parents: p1's $780 reaches the $750 it would share beside p2, and p2 + # alone takes $825. The other one-pass state pays p2 at the two-parent + # rate while p1 is excluded, which (h)(2)(A) contradicts. own = g.OwnBenefit(g.OwnBenefitKind.OLD_AGE, own_rib) - with pytest.raises(g.FamilyConfigurationUnsupported) as caught: + rows = _rows( _family( _survivor(pia, maximum), [_parent("p1", own_benefit=own), _parent("p2")], ) - assert caught.value.reason is g.UnsupportedReason.PARENT_BENEFIT + ) + assert not rows["p1"].entitled + assert ( + rows["p2"].auxiliary_payable == count_two_first["other_parent_payable"] + ) + assert _consistent_parent_sets( + pia, {"p1": ("RIB", own_rib), "p2": None} + ) == [frozenset({"p2"})] @given(st.integers(100, 1_000_000), st.integers(0, 100)) @@ -669,10 +802,11 @@ def test_parent_and_ieds_refusals_remain_in_the_denominator(): own = g.OwnBenefit(g.OwnBenefitKind.OLD_AGE, F(780)) outcomes = [ g.evaluate_family( - "parent-cycle", + "parent-two-in-band", 2, lambda: _family( - _survivor(), [_parent(own_benefit=own), _parent("p2")] + _survivor(), + [_parent(own_benefit=own), _parent("p2", own_benefit=own)], ), ), g.evaluate_family( diff --git a/tests/test_track_b_gross_benefits_parisi.py b/tests/test_track_b_gross_benefits_parisi.py index e50dc8af8..579008328 100644 --- a/tests/test_track_b_gross_benefits_parisi.py +++ b/tests/test_track_b_gross_benefits_parisi.py @@ -14,7 +14,7 @@ from fractions import Fraction as F import pytest -from hypothesis import given, settings +from hypothesis import example, given, settings from hypothesis import strategies as st from populace_dynamics.track_b import gross_benefits as g @@ -445,6 +445,7 @@ def method_c_cases(draw): @given(method_c_cases()) +@example((F(600), F("99.90"), 36, 0)) @settings(deadline=None) def test_method_c_own_rib_age_reduction_does_not_change_redistributed_rates( case, @@ -455,7 +456,7 @@ def test_method_c_own_rib_age_reduction_does_not_change_redistributed_rates( computations over their shared semantics. Own RIB payment changes; auxiliary payable and the non-deductee shares remain identical. A positive before-age excess rounded to zero after age reduction is - explicitly refused in both computations pending the source reading. + counted whole, as GN 02603.045's before-age amount. """ pia, own_pia, spouse_months, worker_months = case state = _retired(pia, _floor_dime(pia * F(3, 2))) @@ -479,12 +480,6 @@ def test_method_c_own_rib_age_reduction_does_not_change_redistributed_rates( 0, spouse_months - 36 ) * F(1, 240) auxiliary = _floor_dime(excess * (1 - reduction)) - if auxiliary == 0: - for member in (spouse, reduced_spouse): - with pytest.raises(g.FamilyConfigurationUnsupported) as caught: - _family(state, [member, *children]) - assert caught.value.reason is g.UnsupportedReason.PARISI_AMBIGUOUS - return unchanged = _family(state, [spouse, *children]) reduced = _family(state, [reduced_spouse, *children]) counted = excess @@ -509,16 +504,17 @@ def test_method_c_own_rib_age_reduction_does_not_change_redistributed_rates( ) -def test_method_b_zero_only_after_age_is_refused_and_kept_in_denominator(): - """Pinned reading requiring judgment: GN .050A.2 versus .045/.050C. +def test_method_b_zero_only_after_age_counts_the_before_age_amount(): + """Source conflict us:oasdi-dual-entitlement-zero-after-age-reduction. Invented PIA500/max750; B36months ownRIB64, method B. Share83.30 - reduces to62.40, then offsets to0. A.2 disregards B: children125 each. - Keeping the20.90 age term under C instead gives114.50 each; the record - totals are750 versus729. G refuses this reading pending judgment; both - reference amounts and their exact supporting quotes remain pinned. - A before-age-only reading of .045 counts83.30-64=19.30 instead, - giving children115.30 each and record total730.60. + reduces to62.40, then offsets to0. GN 02603.050A applies its + exceptions under .045, which counts the dual rate before age + reduction: 83.30-64=19.30 is a partial rate (.050A.3), so the + children receive115.30 each and the record total is730.60. The + rejected readings stay pinned as diagnostics: A.2's disregard + (125 each, total750) and the full20.90 age term under C (114.50 + each, total729). """ spouse = g.Beneficiary( "B", @@ -533,17 +529,14 @@ def test_method_b_zero_only_after_age_is_refused_and_kept_in_denominator(): g.Beneficiary("C1", g.Role.CHILD), g.Beneficiary("C2", g.Role.CHILD), ] - with pytest.raises(g.FamilyConfigurationUnsupported) as caught: - _family(state, members) - assert caught.value.reason is g.UnsupportedReason.PARISI_AMBIGUOUS - outcome = g.evaluate_family( - "zero-after-age", F(3, 2), lambda: _family(state, members) - ) - counts = g.count_family_outcomes([outcome]) - assert (counts.rows, counts.weight) == (1, F(3, 2)) - assert counts.unsupported_by_reason == { - g.UnsupportedReason.PARISI_AMBIGUOUS: (1, F(3, 2)) - } + rows = _family(state, members).by_id() + assert rows["B"].age_adjusted_rate == F("62.40") + assert rows["B"].auxiliary_payable == 0 + assert rows["B"].counted_against_maximum == F("19.30") + assert rows["C1"].auxiliary_payable == rows["C2"].auxiliary_payable + assert rows["C1"].auxiliary_payable == F("115.30") + total = sum((row.auxiliary_payable for row in rows.values()), F(0)) + assert F(500) + total == F("730.60") zero_rate_exception = g.maximum_redistribution_rates( {"B": F(250), "C1": F(250), "C2": F(250)}, F(250), @@ -551,37 +544,80 @@ def test_method_b_zero_only_after_age_is_refused_and_kept_in_denominator(): ) assert dict(zero_rate_exception) == {"C1": F(125), "C2": F(125)} assert F(500) + sum(zero_rate_exception.values(), F(0)) == 750 - alternative = g.maximum_redistribution_rates( + full_age_term = g.maximum_redistribution_rates( {"B": F(250), "C1": F(250), "C2": F(250)}, F(250), deductee_payables={"B": F(0)}, age_reductions={"B": F("20.90")}, ) - assert dict(alternative) == {"C1": F("114.50"), "C2": F("114.50")} - assert F(500) + sum(alternative.values(), F(0)) == 729 - before_age_only = g.maximum_redistribution_rates( - {"B": F(250), "C1": F(250), "C2": F(250)}, - F(250), - deductee_payables={"B": F("19.30")}, - ) - assert dict(before_age_only) == {"C1": F("115.30"), "C2": F("115.30")} - assert F(500) + sum(before_age_only.values(), F(0)) == F("730.60") - assert ( - sum(zero_rate_exception.values(), F(0)) - - sum(alternative.values(), F(0)) - == 21 - ) + assert dict(full_age_term) == {"C1": F("114.50"), "C2": F("114.50")} + assert F(500) + sum(full_age_term.values(), F(0)) == 729 assert ( "The benefit payable to the dually entitled person is the amount before any applicable reduction for age" in source_text("poms_gn_02603_045") ) - assert "Age reduction amount for all workers or deductees" in source_text( - "poms_gn_02603_050" - ) - assert ( - "If dual entitlement reduces a beneficiary's rate to $0.00 (technically entitled)" - in source_text("poms_gn_02603_050") + rule = " ".join(source_text("poms_gn_02603_050").split()) + for quote in ( + "exceptions 2 and 3 apply only for cases processed under the new " + "rule as explained in GN 02603.045", + "If dual entitlement reduces a beneficiary's rate to $0.00 " + "(technically entitled)", + "If dual entitlement reduces a beneficiary's rate to a partial " + "payable rate and no other deduction is involved for any other " + "beneficiary, subtract the partial payable rate", + "Age reduction amount for all workers or deductees", + ): + assert quote in rule + + +@settings(deadline=None) +@example(F(64), 36) +@example(F("83.20"), 36) +@example(F("83.30"), 36) +@example(F(50), 60) +@given( + st.integers(1, 1000).map(lambda dimes: F(dimes, 10)), + st.integers(0, 60), +) +def test_method_b_counted_amount_is_the_before_age_amount(own, months): + """Method B counts max(0, standard - own) whether or not age zeroes it. + + The counted amount is continuous and nonincreasing in the own benefit, + meets GN 02603.050A.2's disregard exactly when the own benefit reaches + the standard rate, and equals payable plus age reduction whenever the + payable is positive. Children never fall when the own benefit rises. + """ + spouse = g.Beneficiary( + "B", + g.Role.SPOUSE, + reduction_months=months, + own_benefit=g.OwnBenefit(OLD_AGE, own), + own_benefit_first=False, ) + state = _retired(500, 750) + children = [g.Beneficiary(key, g.Role.CHILD) for key in ("C1", "C2")] + rows = _family(state, [spouse, *children]).by_id() + standard = F("83.30") + before_age = max(F(0), standard - own) + assert rows["B"].counted_against_maximum == before_age + if rows["B"].auxiliary_payable: + assert rows["B"].auxiliary_payable == rows["B"].age_adjusted_rate - own + assert before_age == rows["B"].auxiliary_payable + ( + standard - rows["B"].age_adjusted_rate + ) + child = min(F(250), _floor_dime((F(250) - before_age) / 2)) + assert rows["C1"].auxiliary_payable == rows["C2"].auxiliary_payable + assert rows["C1"].auxiliary_payable == child + richer = _family( + state, + [ + dataclasses.replace( + spouse, own_benefit=g.OwnBenefit(OLD_AGE, own + F(1, 10)) + ), + *children, + ], + ).by_id() + assert richer["C1"].auxiliary_payable >= rows["C1"].auxiliary_payable @pytest.mark.parametrize( diff --git a/tests/test_track_b_readings_20261023.py b/tests/test_track_b_readings_20261023.py new file mode 100644 index 000000000..0df1f4b0f --- /dev/null +++ b/tests/test_track_b_readings_20261023.py @@ -0,0 +1,120 @@ +"""Track B readings pass of October 23, 2026: markers, notes and new quotes. + +Verification class: statutory conformance and documentation integrity on +captured sources only. No population, behavioral or forecast claim. +Every ``# Source conflict`` marker in the Track B modules names a logged +reading, every logged reading has a marker, and no design note keeps an +open "Needs Max's judgment" heading. +""" + +import re +from pathlib import Path + +import pytest + +from tests.track_b_gross_benefit_support import source_text + +ROOT = Path(__file__).parents[1] +MODULES = ROOT / "src" / "populace_dynamics" / "track_b" +NOTES = { + "G": ROOT / "docs/design/track_b_g_gross_benefits.md", + "R2": ROOT / "docs/design/track_b_r2_ret_ledger.md", + "R3": ROOT / "docs/design/track_b_r3_arf_recompute.md", +} + +#: Logged readings with a decision line in G, R2 or R3, by note. +READINGS = { + "G": { + "us:oasdi-family-maximum-saving-clause", + "us:oasdi-saving-clause-operational-rate-rounding", + "us:oasdi-saving-clause-procedure-gaps", + "us:oasdi-dual-entitlement-zero-after-age-reduction", + "us:oasdi-parent-entitlement-count-interval", + "us:oasdi-dwb-1984-pre-1978-history", + "us:oasdi-dual-entitlement-pre-1978-history", + "us:oasdi-reduction-exact-fractions", + "us:oasdi-disability-family-maximum-rounding", + "us:oasdi-ret-temporary-partial-deduction", + "us:oasdi-nh-charge-age-reduction-interaction", + }, + "R2": { + "us:oasdi-ret-negative-nese-proration", + "us:oasdi-ret-grace-simultaneous-rib-dib", + "us:oasdi-ret-own-work-age-exemptions", + "us:oasdi-ret-deemed-entitlement-basis", + "us:oasdi-ret-other-deductee-scope", + }, + "R3": { + "us:oasdi-survivor-fra-1911-births", + "us:oasdi-arf-widow-child-in-care-credits", + "us:oasdi-arf-spouse-reentitlement-credits", + }, +} +MARKER = re.compile(r"# Source conflict (us:oasdi-[a-z0-9-]+)") + + +def _module_markers(): + found = {} + for path in sorted(MODULES.glob("*.py")): + for match in MARKER.finditer(path.read_text()): + found.setdefault(match.group(1), set()).add(path.name) + return found + + +def test_markers_and_logged_readings_match_in_both_directions(): + markers = _module_markers() + logged = set().union(*READINGS.values()) + # Modules outside G, R2 and R3 (insured status) log their own readings. + outside = { + name + for name, files in markers.items() + if files <= {"insured_status.py", "_insured_status_qc.py"} + } + assert set(markers) - outside == logged + for name in logged: + assert markers[name] <= { + "gross_benefits.py", + "ret_ledger.py", + "ret_grace.py", + "arf.py", + }, name + + +@pytest.mark.parametrize("note", sorted(NOTES)) +def test_each_note_names_its_readings_and_has_no_open_judgment_heading(note): + text = NOTES[note].read_text() + assert not re.search(r"^#+ Needs Max's judgment", text, re.MULTILINE) + for name in READINGS[note]: + assert name in text, (note, name) + + +STATUTE_QUOTES = ( + # 402(h)(1)(D): the parent's own-RIB test depends on the entitled set. + "if the amount of the parent 's insurance benefit for such month is " + "determinable under paragraph (2)(A) (or 75 percent of such primary " + "insurance amount in any other case)", + "For any month for which more than one parent is entitled to parent 's " + "insurance benefits", + # 402(k)(4): an elected RIB month is entitlement to RIB alone. + "if such individual so elects, he shall instead be entitled to only the " + "smaller of such benefits for such month.", + # 402(s)(2)-(3): marriage and the 203(f)(1)(C) exemption need disability. + "shall not apply in the case of any child unless such child, at the " + "time of the marriage referred to therein, was under a disability", + "subsection (f)(1)(C) of section 403 of this title", + "unless in such month or the third month prior thereto such child was " + "under a disability", + # 402(d)(1)(D), (d)(5): marriage ends a child's benefit, with exceptions. + "(D) the month in which such child dies, or marries,", + "but subject to subsection (s), not be terminated by reason of such " + "marriage.", + # 402(e)(1): a widow(er) entitled before 60 ends at the termination month. + "if she became entitled to such benefits before she attained age 60", + "the termination month for any individual shall be the third month " + "following the month in which her disability ceases;", +) + + +@pytest.mark.parametrize("quote", STATUTE_QUOTES) +def test_new_statutory_excerpts_are_quoted_verbatim(quote): + assert quote in " ".join(source_text("lii_usc_402_415").split()) diff --git a/tests/test_track_b_ret_grace.py b/tests/test_track_b_ret_grace.py index 951923d4b..c9d905ad6 100644 --- a/tests/test_track_b_ret_grace.py +++ b/tests/test_track_b_ret_grace.py @@ -27,6 +27,9 @@ from populace_dynamics.track_b.ret_grace import ( TerminationReason as T, ) +from tests.track_b_gross_benefit_support import ( + source_text as gross_source_text, +) from tests.track_b_ret_ledger_support import source_text @@ -320,10 +323,24 @@ def test_simultaneous_rib_dib_elections_preserve_2010_lmety(): assert rib.next_state.last_met_year == 2010 -def test_simultaneous_disability_cfr_default_and_poms_alternative_are_explicit(): +def test_simultaneous_disability_default_follows_202k4_and_cfr_alternative_is_explicit(): + """Source conflict us:oasdi-ret-grace-simultaneous-rib-dib. + + Act 202(k)(4): a person entitled to RIB and DIB is "entitled to only" + the benefit paid for the month, so an elected RIB month is not a month + "entitled to disability benefits" under 404.435(b)(3). The default + counts its NSMs, as RS 02501.030B.4 does; the former technical- + entitlement exclusion stays selectable. + """ months = [EMPTY] * 5 + [month(B.RIB, entitled={B.RIB, B.DIB})] * 7 - conservative = grace_year(2026, GraceYearState(), months) - poms = grace_year( + poms = grace_year(2026, GraceYearState(), months) + conservative = grace_year( + 2026, + GraceYearState(), + months, + disability_reading=D.CFR_ENTITLEMENT_EXCLUSION, + ) + assert poms == grace_year( 2026, GraceYearState(), months, @@ -342,6 +359,12 @@ def test_simultaneous_disability_cfr_default_and_poms_alternative_are_explicit() "If the beneficiary becomes simultaneously entitled to RIB and DIB benefits, the earnings test (ET) applies to any month they elect to receive RIB." in source_text("poms_rs_02501_030") ) + assert ( + "shall be entitled to only the larger of such benefits for such " + "month, except that, if such individual so elects, he shall " + "instead be entitled to only the smaller of such benefits for " + "such month." + ) in " ".join(gross_source_text("lii_usc_402_415").split()) def test_cfr_404435_don_april_retirement_initial_may_through_december(): @@ -987,25 +1010,49 @@ def arbitrary_month(draw): def test_property_only_entitled_nondisability_prefra_nonservice_months_protected( months, fra ): - result = grace_year(2026, GraceYearState(), months, fra_month=fra) - expected = { - i - for i, value in enumerate(months, 1) - if i < fra - and value.nonservice - and value.received_types - DISABILITY_TYPES - and not value.entitled_types & DISABILITY_TYPES - } - assert result.protected_months == expected - assert result.met_eligible_months == { - i - for i, value in enumerate(months, 1) - if i < fra - and value.received_types - DISABILITY_TYPES - and not value.entitled_types & DISABILITY_TYPES - } - assert result.applies == bool(expected) - assert (result.next_state.last_met_year == 2026) == bool(expected) + """Default 202(k)(4) reading and the explicit CFR alternative. + + By default a paid RIB month with only DIB alongside counts (the person + is entitled to RIB alone that month); DWB and CDB months never count. + """ + + def counts(value, reading): + disability = value.entitled_types & DISABILITY_TYPES + return bool(value.received_types - DISABILITY_TYPES) and ( + not disability + or ( + reading is D.POMS_RIB_ELECTION + and B.RIB in value.received_types + and disability == {B.DIB} + ) + ) + + for reading, result in ( + ( + D.POMS_RIB_ELECTION, + grace_year(2026, GraceYearState(), months, fra_month=fra), + ), + ( + D.CFR_ENTITLEMENT_EXCLUSION, + grace_year( + 2026, + GraceYearState(), + months, + fra_month=fra, + disability_reading=D.CFR_ENTITLEMENT_EXCLUSION, + ), + ), + ): + eligible = { + i + for i, value in enumerate(months, 1) + if i < fra and counts(value, reading) + } + expected = {i for i in eligible if months[i - 1].nonservice} + assert result.met_eligible_months == eligible + assert result.protected_months == expected + assert result.applies == bool(expected) + assert (result.next_state.last_met_year == 2026) == bool(expected) @settings(max_examples=200, deadline=None) @@ -1135,7 +1182,14 @@ def test_property_poms_rib_election_separates_entitlement_from_receipt( assert poms.met_eligible_months == eligible assert poms.protected_months == expected assert poms.applies == bool(expected) - conservative = grace_year(2026, GraceYearState(), months, fra_month=fra) + assert poms == grace_year(2026, GraceYearState(), months, fra_month=fra) + conservative = grace_year( + 2026, + GraceYearState(), + months, + fra_month=fra, + disability_reading=D.CFR_ENTITLEMENT_EXCLUSION, + ) assert not conservative.applies assert not conservative.met_eligible_months diff --git a/tests/test_track_b_ret_ledger.py b/tests/test_track_b_ret_ledger.py index a8763ae26..18310fad9 100644 --- a/tests/test_track_b_ret_ledger.py +++ b/tests/test_track_b_ret_ledger.py @@ -1183,18 +1183,20 @@ def test_negative_nese_reading_changes_final_deduction_by_one_dollar(): assert by_month(alternative)[1].payment == 598 -def test_unmarried_deemed_entitlement_is_refused_pending_source_judgment(): - with pytest.raises(r.RETConfigurationUnsupported) as error: - ledger( - beneficiary(work=earnings(lower_earnings_for_excess(600))), - beneficiary( - "deemed", - rate=200, - benefit_type=BenefitType.CDB, - deemed_entitled=True, - ), +def test_unmarried_deemed_entitlement_is_an_input_error(): + """Source conflict us:oasdi-ret-deemed-entitlement-basis. + + Act 203(b)(1) and 20 CFR 404.420 deem only a recipient married to the + NH; RS 02501.140's broader wording yields to them (P1), so an + unmarried recipient is not on the NH's ledger at all. + """ + with pytest.raises(r.InvalidFamilyInput, match="marriage to the NH"): + beneficiary( + "deemed", + rate=200, + benefit_type=BenefitType.CDB, + deemed_entitled=True, ) - assert error.value.reason is r.UnsupportedReason.DEEMED_ENTITLEMENT_BASIS def test_married_deemed_disabled_child_is_charged_number_holders_excess(): diff --git a/tests/test_track_b_ret_ledger_readings.py b/tests/test_track_b_ret_ledger_readings.py index 51703a634..168180e88 100644 --- a/tests/test_track_b_ret_ledger_readings.py +++ b/tests/test_track_b_ret_ledger_readings.py @@ -435,7 +435,7 @@ def _deemed_cdb_january_counterfactual(nh, recipient, *, broader_poms): It never changes marriage facts or enables the broader reading through the public API. The CDB recipient has no excess of their own. """ - assert recipient.deemed_entitled and not recipient.deemed_married_to_nh + assert not recipient.deemed_entitled assert set(recipient.benefit_types) == {BenefitType.CDB} assert 1 in nh.entitled_months & recipient.entitled_months assert recipient.earnings == earnings() @@ -447,15 +447,21 @@ def _deemed_cdb_january_counterfactual(nh, recipient, *, broader_poms): * year.lower_withholding_rate.value ) assert excess >= nh.gross_rates[0] + recipient.gross_rates[0] - # RS 02501.140 puts the other-record CDB rate in the NH's full family - # deduction. Act 203(b)(1) / CFR .420 leave an unmarried recipient's - # separate record outside that charge. This is test-only arithmetic. + # RS 02501.140 would put the other-record CDB rate in the NH's full + # family deduction. Act 203(b)(1) / CFR .420 leave an unmarried + # recipient's separate record outside that charge, the reading taken; + # the broader figure is test-only arithmetic. nh_deduction = recipient.gross_rates[0] if broader_poms else F(0) return recipient.gross_rates[0] - nh_deduction def test_unmarried_deemed_entitlement_counterfactual_changes_200_dollars(): - """Invented $400/$200 rates and $600 excess, with no real data.""" + """Invented $400/$200 rates and $600 excess, with no real data. + + Reading taken (P1): an unmarried recipient is not deemed, so its own + record pays $200 in January; RS 02501.140's broader wording would + remove it. The public API rejects the unmarried declaration. + """ assert ( "is married to an individual entitled to old-age insurance benefits under section 202(a)" in source_text("ssa_act_203") @@ -476,21 +482,25 @@ def test_unmarried_deemed_entitlement_counterfactual_changes_200_dollars(): "deemed", rate=200, benefit_type=BenefitType.CDB, - deemed_entitled=True, ) - with pytest.raises(r.RETConfigurationUnsupported) as error: - r.apply_ret_year(record(nh, recipient), PARAMS) - assert error.value.reason is r.UnsupportedReason.DEEMED_ENTITLEMENT_BASIS + with pytest.raises(r.InvalidFamilyInput, match="marriage to the NH"): + replace(recipient, deemed_entitled=True) married = r.apply_ret_year( - record(nh, replace(recipient, deemed_married_to_nh=True)), PARAMS + record( + nh, + replace( + recipient, deemed_entitled=True, deemed_married_to_nh=True + ), + ), + PARAMS, ) unrelated = r.apply_ret_year( r.RecordYear( YEAR, "other", kind=r.RecordKind.DECEASED, - beneficiaries=(replace(recipient, deemed_entitled=False),), + beneficiaries=(recipient,), ), PARAMS, ) @@ -527,8 +537,8 @@ def test_property_deemed_counterfactual_matches_full_married_charge( Shared surface: whole-dollar RIB/CDB rates, no other excess/protection, and NH excess equal to the combined January rate. Tolerance is zero. - The Act/CFR counterfactual always preserves the unrelated CDB rate; - the public API always retains the unmarried source-conflict refusal. + The Act/CFR reading always preserves the unrelated CDB rate; the + public API always rejects the unmarried deemed declaration. """ family_rate = nh_rate + cdb_rate nh = beneficiary( @@ -541,20 +551,24 @@ def test_property_deemed_counterfactual_matches_full_married_charge( "deemed", rate=cdb_rate, benefit_type=BenefitType.CDB, - deemed_entitled=True, ) - with pytest.raises(r.RETConfigurationUnsupported) as error: - r.apply_ret_year(record(nh, recipient), PARAMS) - assert error.value.reason is r.UnsupportedReason.DEEMED_ENTITLEMENT_BASIS + with pytest.raises(r.InvalidFamilyInput, match="marriage to the NH"): + replace(recipient, deemed_entitled=True) married = r.apply_ret_year( - record(nh, replace(recipient, deemed_married_to_nh=True)), PARAMS + record( + nh, + replace( + recipient, deemed_entitled=True, deemed_married_to_nh=True + ), + ), + PARAMS, ) unrelated = r.apply_ret_year( r.RecordYear( YEAR, "other", kind=r.RecordKind.DECEASED, - beneficiaries=(replace(recipient, deemed_entitled=False),), + beneficiaries=(recipient,), ), PARAMS, ) diff --git a/tests/test_track_b_ret_ledger_sources.py b/tests/test_track_b_ret_ledger_sources.py index 35edf916d..225f2bcaa 100644 --- a/tests/test_track_b_ret_ledger_sources.py +++ b/tests/test_track_b_ret_ledger_sources.py @@ -47,9 +47,9 @@ def test_source_conflict_ids_are_linked_at_the_decision_lines(): "or b.benefit_types[month - 1] in DISABILITY_TYPES", ), ( - r.apply_ret_year, + r.RETBeneficiary.__post_init__, "us:oasdi-ret-deemed-entitlement-basis", - "if b.deemed_entitled and not b.deemed_married_to_nh:", + "if not self.deemed_married_to_nh:", ), ( r.apply_ret_year, diff --git a/tests/track_b_ret_grace_reference.py b/tests/track_b_ret_grace_reference.py index 307aba319..0b2eb1022 100644 --- a/tests/track_b_ret_grace_reference.py +++ b/tests/track_b_ret_grace_reference.py @@ -87,7 +87,7 @@ def _eligible( def reference_grace_history( history: tuple[HistoryYear, ...], reading: DisabilityGraceReading = ( - DisabilityGraceReading.CFR_ENTITLEMENT_EXCLUSION + DisabilityGraceReading.POMS_RIB_ELECTION ), ) -> tuple[ReferenceDecision, ...]: """Find every year's grace decisions directly from the entire timeline. From 479c321222822d580b5ccbba1bc400e85a8425ba Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 15:53:03 -0400 Subject: [PATCH 02/13] Readings pass round 2: apply the blind adjudications Four blind Opus adjudications (run on a clean master clone, without this pass's conclusions) agreed on the parent count, the 1984 DWB rule, the simultaneous RIB/DIB grace months, the deemed-entitlement marriage basis and every kept refusal. Changes from their disagreements and findings: - us:oasdi-dual-entitlement-zero-after-age-reduction: children $114.50, not $115.30. In 402(k)(3)(A)'s order the offset withholds $62.40 and age $20.90; 20 CFR 404.403(a)(5) excludes only the former (see 404.407(a)) and GN 02603.060 B.2 keeps the age slice in PTAP. A.2's disregard stays for zeros that dual entitlement alone makes. - us:oasdi-parent-entitlement-count-interval: test own RIBs against the unrounded 82.5 and 75 percent of the PIA that 402(h)(1)(D) names. - us:oasdi-ret-own-work-age-exemptions: a non-empty own_work_exempt_months is now an input error; the Act exempts only CDB and DWB months. - Historical dual entitlement: the method-B alternative ($96.50) is pinned beside method C's $99.30. - Disability-maximum rounding and NESE floor are kept; the contrary blind views and their dime/dollar stakes are recorded in the notes. Thirteen mutants (three per G2, four per parents, two per DWB, one each for the grace default, the override and the marriage check) fail the new tests in isolated copies. Co-Authored-By: Claude Opus 5.5 --- docs/design/track_b_g_gross_benefits.md | 92 ++++++++------ docs/design/track_b_r2_ret_ledger.md | 38 +++--- .../track_b/gross_benefits.py | 68 +++++------ src/populace_dynamics/track_b/ret_ledger.py | 21 ++-- ...ck_b_gross_benefits_fix_round_8_history.py | 3 + ...est_track_b_gross_benefits_parents_ieds.py | 42 +++++-- tests/test_track_b_gross_benefits_parisi.py | 71 +++++------ tests/test_track_b_ret_ledger_properties.py | 8 +- tests/test_track_b_ret_ledger_readings.py | 115 +++++------------- 9 files changed, 226 insertions(+), 232 deletions(-) diff --git a/docs/design/track_b_g_gross_benefits.md b/docs/design/track_b_g_gross_benefits.md index 1f6c6df8e..a6c5b9d68 100644 --- a/docs/design/track_b_g_gross_benefits.md +++ b/docs/design/track_b_g_gross_benefits.md @@ -486,9 +486,12 @@ Reading taken (October 23, 2026), logged as regulation competes. The Act rounds the regular maximum "to the next lower multiple of $0.10" (403(a)(2)) and dime-floors every later increase of this maximum (215(i)(2)(A)(ii)), so an undimed initial maximum would be the only -one of its kind; SSA's own description, the Bulletin, floors it. That is -consistent usage within the Act, with the agency's reading breaking what -remains of the tie (P3). +one of its kind; SSA's own description, the Bulletin, floors it. Where the +statute is silent, P1 lets the agency's published reading fill the gap. +A blind adjudication preferred leaving the maximum unrounded, reading the +contrast with 403(a)(1) and 20 CFR 404.403(c) as deliberate; the stakes are +at most $0.10 a month per auxiliary, and only after a COLA (the pre-COLA +shares floor to the same dimes), so the published agency reading is kept. ## Households and the entry point @@ -1195,42 +1198,48 @@ interaction with the PTAP age-reduction and partial-payable terms. ### Reading taken: zero reached after auxiliary age reduction Source conflict `us:oasdi-dual-entitlement-zero-after-age-reduction`, -decided October 23, 2026 (definitions control; P3). Ruling d1418 had kept -it refused. GN 02603.045 says the counted amount is "before any applicable +decided October 23, 2026 (the regulation's definition of the redistributed +amount, with the agency's worked accounting; P3). Ruling d1418 had kept it +refused. GN 02603.045 says the counted amount is "before any applicable reduction for age". GN 02603.050C requires the "Age reduction amount for all workers or deductees". GN 02603.050A.2 says "If dual entitlement reduces a beneficiary's rate to $0.00 (technically entitled)" to disregard -that beneficiary, and A.3 says to "subtract the partial payable rate" when -the rate is partial. A method-B spouse with PIA $500, maximum $750, own RIB +that beneficiary. A method-B spouse with PIA $500, maximum $750, own RIB $64 and 36 auxiliary reduction months has standard rate $83.30, age-adjusted $62.40, and zero auxiliary payable. -GN 02603.050A states that "exceptions 2 and 3 apply only for cases -processed under the new rule as explained in GN 02603.045", so A.2 and A.3 -measure the dual rate as .045 defines it, before age reduction. Before age -reduction the spouse's rate is $83.30 minus $64, or $19.30: a partial rate, -not zero. G counts it whole (A.3), paying the children $115.30 each, a -record total of $730.60. This is also the amount G already counts whenever -the dual payable is positive: payable plus age reduction is $83.30 minus -the own benefit. The count therefore runs continuously from a positive -payable through the zero clamp to A.2's disregard, which applies exactly -when the own benefit reaches the standard rate (and covers A.5 and A.8, -where the zero arises before age reduction). It is consistent with 20 CFR -404.403(a)(5) and with every captured SSA example (A.5, A.8, A.9, B.2). - -The two rejected readings stay pinned as diagnostics. Disregarding the -spouse here (A.2 applied to the after-age rate) pays $125 each. Retaining -the full $20.90 age term under C pays $114.50 each; it would count less -than the before-age amount and, extended to an own benefit at or above the -standard rate, would contradict the supported A.2 disregard. -`test_method_b_zero_only_after_age_counts_the_before_age_amount` pins the -quotes and all three amounts; the Hypothesis property -`test_method_b_counted_amount_is_the_before_age_amount` checks the counted -amount equals max(0, standard minus own benefit) for every own benefit and -reduction period, matches payable plus age reduction whenever the payable -is positive, and never lowers the children as the own benefit rises. The -method-C property now also executes a positive excess that rounds to zero -after age reduction. +Act 402(k)(3)(A) applies the own-benefit offset "after any reduction under +subsection (q)", so in a method-B case the $83.30 share divides into $20.90 +withheld for age and $62.40 withheld for dual entitlement. 20 CFR +404.403(a)(5) excludes from the total "any amount not paid because of that +person's entitlement on another earnings record (see § 404.407)", and +404.407(a) measures that offset after the 202(q) reduction. GN 02603.060 +B.2 states the same accounting ("In order to redistribute only the amount +being withheld because of dual entitlement we must account for the entire +$83.30 due B"), keeping the age slice in PTAP. So only the $62.40 is +redistributed: PTAP is $750 − $500 − $20.90 − $0 = $229.10, and each child +receives $114.50, a record total of $729. GN 02603.050A.2's disregard +applies, as in A.5, A.8 and A.9, when dual entitlement considered first +(.045's before-age measure; A.8: "we consider their dual entitlement +first") leaves nothing; here it leaves $83.30 − $64 = $19.30, so A.2 does +not apply. + +The rejected readings stay pinned as diagnostics: disregarding the spouse +pays $125 each (it would also pass the $20.90 age reduction to the +children), and counting only .045's before-age amount ($19.30) pays $115.30 +each, total $730.60 (it would treat $1.60 of the age reduction as withheld +for dual entitlement). Two blind adjudications on October 23 reached +$114.50; an intermediate draft of this pass had preferred $115.30 for its +continuity and was revised. +`test_method_b_zero_only_after_age_keeps_the_age_reduction_in_ptap` pins +the quotes and all three amounts; the Hypothesis property +`test_method_b_counts_payable_plus_age_reduction_until_a2` checks that a +method-B spouse counts its payable plus its own age reduction, that is +standard − min(own benefit, age-adjusted rate), until the own benefit +reaches the standard rate and A.2 disregards it, and that the children +never fall as the own benefit rises. The method-C property also executes a +positive excess that rounds to zero after age reduction, which counts the +whole excess under either reading. ## Disabled widow(er) benefits @@ -1337,7 +1346,10 @@ order ends with one parent paid at the two-parent rate, which (h)(2)(A) contradicts. With two parents whose own RIBs both lie in the band, either parent alone satisfies the test for both, so `parent_benefit` still refuses that configuration by name (filing order, which the inputs omit, -might settle it). +might settle it). The thresholds are the unrounded 82½ and 75 percent of +the PIA that (h)(1)(D) names, not the dime-floored OBs RS 00209.001 speaks +of (P1): at PIA $1,000.10 a lone parent with own RIB $825.00 is entitled +(below $825.0825), with $0 payable. `test_parent_entitlement_count_counterfactual_changes_payment_by_75` executes both one-pass orders with a test-only exact helper and asserts @@ -1982,7 +1994,11 @@ own-first sequence. For an invented cohort-1912 spouse with unreduced own RIB $400, spouse OB $500 and auxiliary RF1, the current method-C reading pays auxiliary -$99.30 ($99 whole dollars), with own $400. The conservative implementation +$99.30 ($99 whole dollars), with own $400. Method C applies only if the own +benefit was a DIB or an early RIB when the spouse benefit began +(402(q)(3)(A)); an unreduced RIB that began after the spouse benefit is +method B, $496.50 − $400 = $96.50 ($96), which the inputs cannot tell +apart either. The conservative implementation refuses `reduced_benefit_history_before_1978`, retaining the row and weight; it does not assign a different historical dollar amount. The source-quote test executes that alternative and the cohort-1916 control side by side @@ -3102,12 +3118,12 @@ row and weight in the denominators. | 403(a)(5) literal or operational | `us:oasdi-family-maximum-saving-clause` | Operational default, literal behind a switch | Max, d1338 | | Operational rounding direction | `us:oasdi-saving-clause-operational-rate-rounding` | Round equal rates up | P1 | | Gaps in the operational procedure | `us:oasdi-saving-clause-procedure-gaps` | Refused | Sources silent | -| Zero reached only after auxiliary age reduction | `us:oasdi-dual-entitlement-zero-after-age-reduction` | Count the .045 before-age amount: children $115.30 each, was refused | Definitions control (.050A ties A.2-A.3 to .045); P3 | -| Circular parent count | `us:oasdi-parent-entitlement-count-interval` | The set satisfying 402(h)(1)(D) for both parents: other parent $825, was refused; two parents in the band stay refused | P1 | +| Zero reached only after auxiliary age reduction | `us:oasdi-dual-entitlement-zero-after-age-reduction` | Keep the age reduction in PTAP, redistribute only the dual-entitlement amount: children $114.50 each, was refused | 20 CFR 404.403(a)(5), 404.407(a); GN .060 B.2; P3 | +| Circular parent count | `us:oasdi-parent-entitlement-count-interval` | The set satisfying 402(h)(1)(D) for both parents, at the unrounded percentages: other parent $825, was refused; two parents in the band stay refused | P1 | | DWB from January 1984, pre-1978-capable cohorts | `us:oasdi-dwb-1984-pre-1978-history` | Under 60: .715 of the unreduced WIB ($715, was refused); from 60: refused | Specific over general | | Historical dual entitlement | `us:oasdi-dual-entitlement-pre-1978-history` | Refused | Sources silent | | Exact fractions or float reuse | `us:oasdi-reduction-exact-fractions` | Exact fractions | Not a conflict (Act and RS 00615.005B agree) | -| Disability-maximum rounding | `us:oasdi-disability-family-maximum-rounding` | Floor to the dime | Act's consistent usage; P3 | +| Disability-maximum rounding | `us:oasdi-disability-family-maximum-rounding` | Floor to the dime (contrary blind view recorded) | P1's gap-filling clause; the agency's published reading | | Temporary partial deduction | `us:oasdi-ret-temporary-partial-deduction` | Follow the RS 02501.160 example in G | Specific over general; P3 | | NH reservations with age reductions | `us:oasdi-nh-charge-age-reduction-interaction` | Refused | Sources silent | diff --git a/docs/design/track_b_r2_ret_ledger.md b/docs/design/track_b_r2_ret_ledger.md index 8723fc62f..40d9a2d59 100644 --- a/docs/design/track_b_r2_ret_ledger.md +++ b/docs/design/track_b_r2_ret_ledger.md @@ -474,10 +474,9 @@ error (that recipient is not on this NH's ledger). An ordinary deemed child's age-18 months must be explicit, and that residual adult-child case is refused even with those facts established. Deemed entitlement requires a different NH with RIB entitlement in each -applicable month. `own_work_exempt_months` accepts only CHILD or WIDOW -payment months and asserts respectively attainment of age 18 or initial -widow(er) entitlement before 60 (Act 203(f)(1)(C)-(D)); it cannot -arbitrarily protect a RIB or spouse payment. +applicable month. `own_work_exempt_months` must stay empty: the Act +203(f)(1)(C)-(D) exemptions reach only CDB and DWB months (202(s)(3); +402(e)(1)), which the ledger exempts by type. Input errors raise `InvalidFamilyInput` before a coverage refusal. These include floats, invalid months, negative or non-dime rates, nonzero gross @@ -900,7 +899,7 @@ In the table, unqualified property test names are in | Under the default 202(k)(4) reading, only eligible elected RIB NSMs receive grace protection; the explicit CFR technical-entitlement alternative supplies none during concurrent disability entitlement | readings `test_property_explicit_poms_election_protects_only_eligible_rib_months`; `test_simultaneous_dib_rib_source_readings_change_4200_dollars` | | Actual and estimated grace statuses are distinct; changed service facts can require recovery even when annual earnings coincide | readings `test_estimated_initial_grace_and_actual_grace_are_separate`; `test_property_changed_grace_estimate_can_require_recovery` | | Zero payable caps retain their original shares and count toward the three-beneficiary sub-dollar stop; exact net shares plus the residual conserve the pool | `test_zero_cap_participants_preserve_total_and_trigger_source_stop`; unit `test_entitled_zero_rate_auxiliary_keeps_its_original_partial_share` | -| Explicit Act 203(f)(1)(C)-(D) protections exempt only the recipient's own work in asserted CHILD/WIDOW months | readings `test_property_other_statutory_own_exemptions_protect_only_that_workers_excess`; `test_own_work_exempt_flag_requires_a_statutory_benefit_type` | +| Act 203(f)(1)(C)-(D) protections are the CDB/DWB types themselves; any asserted own-work exemption month is an input error | readings `test_own_work_exempt_months_are_never_a_lawful_assertion`; `test_optional_act_age_exemptions_are_an_explicit_600_dollar_reading` | | The test-only broader deemed-CDB reading agrees exactly with the supported married full-family charge; the Act/CFR reading preserves the unrelated record, while the public unmarried input is always an input error | readings `test_property_deemed_counterfactual_matches_full_married_charge`; `test_unmarried_deemed_entitlement_counterfactual_changes_200_dollars` | The `ss/benefits.py` float oracle has no earnings-test ledger implementation, @@ -1355,14 +1354,20 @@ the cents” supports truncation toward zero, while “round down” supports mathematical floor. The two readings agree on the published positive NESE examples. -Kept: `NESEProrationPolicy.FLOOR` (one meaning per section; P3). Act +Kept: `NESEProrationPolicy.FLOOR` (one meaning per section; P3; a close +call). Act 203(f)(3) leaves the proration to the agency ("prorated in an equitable manner under regulations of the Commissioner of Social Security"), and no captured regulation fixes its rounding, so SSA's instruction governs. Within RS 02501.080, "round down" means "round down to the next lower dollar" (B.1.d, B.2.a); applied after the cents are dropped, only the floor -gives that step any work, while truncation would leave it surplus. -`DROP_CENTS` remains an explicit alternative. In an invented June FRA year, +gives that step any work, while truncation would leave it surplus; and +for positive amounts the instruction's direction lowers countable +earnings, which the floor keeps for a loss. A blind adjudication read +"drop the cents" as truncating to whole dollars (leaving "round down" +idle) and preferred `DROP_CENTS`, which here matches exact proration's $2 +excess; the stake is at most a dollar's worth of countable earnings per +month of loss. `DROP_CENTS` remains an explicit alternative. In an invented June FRA year, annual NESE loss −$13 over twelve months becomes monthly −$2 and five-month −$10 under floor, versus monthly −$1 and five-month −$5 under dropping cents. With pre-FRA wages equal to R1's higher exempt @@ -1429,15 +1434,14 @@ ends that entitlement with "the termination month" ("the third month following the month in which her disability ceases") unless retirement age comes first, so clause (D)'s months are months of a benefit based on disability. The automatic exemptions therefore follow the explicit -disability types, which is the Act's own scope. The documented -`own_work_exempt_months` parameter defaults to empty; it is not a broader -lawful reading for an ordinary CHILD or WIDOW month, and remains only as a -caller's explicit assertion of facts the ledger does not adjudicate. In -invented $200 monthly ordinary CHILD and WIDOW cases, earnings equal R1's -lower amount plus $1,200. Default own deductions total $600; the asserted -exemption pays the additional $600. Both cases are executed by -`test_optional_act_age_exemptions_are_an_explicit_600_dollar_reading`, -with the general selected-exemption property in the invariant table. +disability types, which is the Act's own scope. The former +`own_work_exempt_months` assertion had no lawful use (every month the Act +exempts is a CDB or DWB month), so any non-empty value is now +`InvalidFamilyInput`. In invented $200 monthly ordinary CHILD and WIDOW +cases, earnings equal R1's lower amount plus $1,200, and own deductions +total $600; the former assertion would have paid that $600. +`test_optional_act_age_exemptions_are_an_explicit_600_dollar_reading` and +`test_own_work_exempt_months_are_never_a_lawful_assertion` execute both. ### Reading taken: deemed entitlement requires marriage to the NH diff --git a/src/populace_dynamics/track_b/gross_benefits.py b/src/populace_dynamics/track_b/gross_benefits.py index bc24ab140..990835b7e 100644 --- a/src/populace_dynamics/track_b/gross_benefits.py +++ b/src/populace_dynamics/track_b/gross_benefits.py @@ -2866,7 +2866,7 @@ def _is_entitled( pia: Fraction, own_mba: Fraction | None, *, - parent_ob: Fraction | None = None, + parent_threshold: Fraction | None = None, ) -> tuple[bool, str]: own = beneficiary.own_benefit if own is None: @@ -2886,9 +2886,11 @@ def _is_entitled( if ( beneficiary.role is Role.PARENT and own.kind is OwnBenefitKind.OLD_AGE - and own_mba >= parent_ob + and own_mba >= parent_threshold ): - return False, "own RIB not less than parent's OB (RS 00209.001)" + return False, ( + "own RIB not less than the 402(h)(1)(D) share of the PIA" + ) if ( beneficiary.role is Role.MOTHER_FATHER and own.kind is OwnBenefitKind.OLD_AGE @@ -2990,9 +2992,9 @@ def _parisi_counted( The own-RIB reduction is not an auxiliary age reduction (GN 02603.060B.2: $2.40 payable + $0.90 reduction, not the $16 own-RIB - reduction). Together they are the .045 amount "before any applicable - reduction for age"; GN 02603.050A.2 disregards a dual rate whose - before-age amount is zero. Own delayed credits and redistribution with an actually limiting + reduction). GN 02603.050A.2 disregards a dual rate that is zero before + age reduction; a zero reached only after it counts the age reduction. + Own delayed credits and redistribution with an actually limiting RIB-LIM remain refused: none of the captured worked cases settles those interacting amounts. A considered but nonlimiting RIB-LIM changes no rate (RS 00615.320A.3). @@ -3034,13 +3036,14 @@ def _parisi_counted( ) if payable == 0: before_age = row.standard - (own.pia if method_c else row.own_mba) + # GN 02603.050A.2 disregards a rate that dual entitlement, considered + # first, reduces to zero (.045; .060 A.8). A zero reached only after + # the auxiliary's age reduction keeps that reduction in PTAP: only + # the amount withheld for dual entitlement in 402(k)(3)(A)'s order is + # redistributed (20 CFR 404.403(a)(5), 404.407(a); .060 B.2). # Source conflict us:oasdi-dual-entitlement-zero-after-age-reduction - # GN 02603.050A applies its exceptions 2 and 3 under .045, which - # counts the dual rate "before any applicable reduction for age". - # A before-age zero is disregarded (.050A.2); a positive before-age - # amount is a partial rate (.050A.3), counted whole as the age term. - # A positive payable below counts the same total. - return Fraction(0), max(Fraction(0), before_age) + if before_age <= 0: + return Fraction(0), Fraction(0) if method_c: excess = max(Fraction(0), row.standard - own.pia) age_reduction = excess - _spouse_age_reduced_validated( @@ -3365,6 +3368,11 @@ def _check_family_inputs( return beneficiaries +def _parent_entitlement_thresholds(pia: Fraction) -> tuple[Fraction, Fraction]: + """402(h)(1)(D): 82.5 and 75 percent of the PIA, unrounded.""" + return pia * Fraction(165, 200), pia * Fraction(3, 4) + + def _entitled_parent_count( state: RecordState, beneficiaries: Sequence[Beneficiary], @@ -3377,9 +3385,11 @@ def _entitled_parent_count( 75 percent "in any other case". The entitled set must satisfy that test for every parent at once: each entitled parent at its own rate, and each excluded potential parent at the rate it would share. RS - 00209.015 counts an own-DIB parent as technically entitled. Returns - the count and the ids excluded at the two-parent rate. More than two - declared parents needs a separate rule. + 00209.015 counts an own-DIB parent as technically entitled. The + thresholds are the unrounded percentages of the PIA that (h)(1)(D) + names, not the dime-floored OBs. Returns the count and the ids excluded + at the two-parent rate. More than two declared parents needs a + separate rule. """ parents = [b for b in beneficiaries if b.role is Role.PARENT] if len(parents) > 2: @@ -3389,12 +3399,7 @@ def _entitled_parent_count( "more than two declared parents is outside the one/two-parent " "rates settled by 402(h)(2) and RS 00209.015", ) - one_ob = _original_benefit_validated( - Role.PARENT, FamilyKind.SURVIVOR, state.pia - ) - two_ob = _original_benefit_validated( - Role.PARENT, FamilyKind.SURVIVOR, state.pia, parent_count=2 - ) + one_rate, two_rate = _parent_entitlement_thresholds(state.pia) potential = [] for parent in parents: own = parent.own_benefit @@ -3404,7 +3409,7 @@ def _entitled_parent_count( else _own_monthly_benefit_validated(own, params)[1] ) if own is not None and own.kind is OwnBenefitKind.OLD_AGE: - if own_mba >= one_ob: + if own_mba >= one_rate: continue potential.append((parent, own_mba)) in_band = [ @@ -3412,7 +3417,7 @@ def _entitled_parent_count( for parent, own_mba in potential if parent.own_benefit is not None and parent.own_benefit.kind is OwnBenefitKind.OLD_AGE - and own_mba >= two_ob + and own_mba >= two_rate ] if len(potential) < 2 or not in_band: return max(1, len(potential)), frozenset() @@ -3424,7 +3429,7 @@ def _entitled_parent_count( raise _computed( FamilyConfigurationUnsupported, UnsupportedReason.PARENT_BENEFIT, - "two parents with own RIB between the 75% and 82.5% OBs: " + "two parents with own RIB between 75% and 82.5% of the PIA: " "402(h)(1)(D) is satisfied by either parent alone", beneficiary_id=in_band[0].beneficiary_id, ) @@ -3564,13 +3569,8 @@ def _family_benefits( parent_count, excluded_parents = _entitled_parent_count( state, beneficiaries, params ) - two_parent_ob = ( - _original_benefit_validated( - Role.PARENT, FamilyKind.SURVIVOR, state.pia, parent_count=2 - ) - if excluded_parents - else None - ) + one_rate, two_rate = _parent_entitlement_thresholds(state.pia) + parent_threshold = two_rate if parent_count == 2 else one_rate life_case = state.kind is not FamilyKind.SURVIVOR and not ieds worker_benefit = None if state.kind is FamilyKind.RETIREMENT and not ieds: @@ -3600,10 +3600,10 @@ def _family_benefits( beneficiary, state.pia, own_mba, - parent_ob=( - two_parent_ob + parent_threshold=( + two_rate if beneficiary.beneficiary_id in excluded_parents - else ob + else parent_threshold ), ) rows.append( diff --git a/src/populace_dynamics/track_b/ret_ledger.py b/src/populace_dynamics/track_b/ret_ledger.py index 47817da0d..3f6f8b4a4 100644 --- a/src/populace_dynamics/track_b/ret_ledger.py +++ b/src/populace_dynamics/track_b/ret_ledger.py @@ -246,12 +246,12 @@ class RETBeneficiary: child is entitled only if 402(d)(5) kept the benefit through a marriage made while disabled (402(s)(2)), a residual case CFR .420 omits, so it stays refused. Own two-record excess is still refused. - ``own_work_exempt_months`` supplies other Act - 203(f)(1)(C)-(D) protections (entitled child who has attained 18 / - widow(er) entitled before 60). It defaults to empty under the CFR's - disability-type wording. Selecting this Act-clause alternative - asserts lawful entitlement, age facts and any 202(s) condition; the - ledger does not infer those facts or adjudicate that condition. + ``own_work_exempt_months`` must stay empty: Act 203(f)(1)(C) reaches + only a child under a disability that month or three months before + (202(s)(3)), and (D)'s widow(er) entitled before 60 is a disabled + widow(er) whose entitlement ends at the termination month (402(e)(1)), + so every month the Act exempts is a CDB or DWB month, which the ledger + exempts automatically. """ beneficiary_id: str @@ -338,12 +338,10 @@ def __post_init__(self): raise InvalidFamilyInput( "Divorce exception months need divorced-spouse benefits" ) - if any( - types[m - 1] not in (BenefitType.CHILD, BenefitType.WIDOW) - for m in self.own_work_exempt_months - ): + if self.own_work_exempt_months: raise InvalidFamilyInput( - "Child or widow benefits are required for other own-work exemptions" + "Act 203(f)(1)(C)-(D) exempt only disability-based months " + "(202(s)(3); 402(e)(1)); declare them as CDB or DWB" ) if self.deemed_entitled: # Act 203(b)(1) and 20 CFR 404.420 deem only a recipient married @@ -1010,7 +1008,6 @@ def _protected( or (b.fra_month is not None and month >= b.fra_month) # Source conflict us:oasdi-ret-own-work-age-exemptions or b.benefit_types[month - 1] in DISABILITY_TYPES - or month in b.own_work_exempt_months or ( grace is not None and grace.applies diff --git a/tests/test_track_b_gross_benefits_fix_round_8_history.py b/tests/test_track_b_gross_benefits_fix_round_8_history.py index f5015401f..4932fb0e6 100644 --- a/tests/test_track_b_gross_benefits_fix_round_8_history.py +++ b/tests/test_track_b_gross_benefits_fix_round_8_history.py @@ -326,6 +326,9 @@ def test_historical_scope_and_earliest_ages_are_quoted_from_captures(): assert not historical.supported assert historical.unsupported.reason.value == REASON assert g.spouse_age_reduced(F(500) - F(400), 1, PARAMS) == F("99.30") + # Method B, if the unreduced RIB began after the spouse benefit + # (402(q)(3)(A)(i) admits method C only for an early RIB or a DIB). + assert g.spouse_age_reduced(F(500), 1, PARAMS) - F(400) == F("96.50") modern = _outcome(state, [replace(member, birth_year=1916)], "family") assert modern.benefits.by_id()["member"].auxiliary_payable == F("99.30") diff --git a/tests/test_track_b_gross_benefits_parents_ieds.py b/tests/test_track_b_gross_benefits_parents_ieds.py index d2e1e0f9b..517f1d76d 100644 --- a/tests/test_track_b_gross_benefits_parents_ieds.py +++ b/tests/test_track_b_gross_benefits_parents_ieds.py @@ -206,13 +206,23 @@ def test_parent_entitlement_compares_the_reduced_rib_not_its_pia(): assert row.auxiliary_payable == F(25) -def test_parent_entitlement_threshold_is_the_dime_rounded_original_benefit(): +def test_parent_entitlement_threshold_is_the_unrounded_statutory_share(): + """402(h)(1)(D) compares with 82.5 percent of the PIA, not the OB. + + PIA $1,000.10: 82.5 percent is $825.0825 and the dime-floored OB is + $825.00. A lone parent with own RIB $825.00 is entitled (technically, + $0 payable). Beside another entitled parent the test is 75 percent + ($750.075), which $825 reaches, so the other parent alone is paid. + """ own = g.OwnBenefit(g.OwnBenefitKind.OLD_AGE, F(825)) - result = _family( - _survivor(F("1000.1"), F(1750)), - [_parent("high", own_benefit=own), _parent("other")], + state = _survivor(F("1000.1"), F(1750)) + alone = _rows(_family(state, [_parent("high", own_benefit=own)])) + assert alone["high"].entitled + assert alone["high"].original_benefit == F(825) + assert alone["high"].auxiliary_payable == 0 + rows = _rows( + _family(state, [_parent("high", own_benefit=own), _parent("other")]) ) - rows = _rows(result) assert not rows["high"].entitled assert rows["high"].auxiliary_payable == 0 assert rows["other"].original_benefit == F(825) @@ -240,6 +250,11 @@ def test_parent_original_benefit_uses_the_entitled_count_and_dime_floor( ) +def _statutory_band_dimes(dimes): + """Own-RIB dimes in [75%, 82.5%) of a PIA of ``dimes`` tenths.""" + return -(-dimes * 3 // 4), -(-dimes * 33 // 40) + + @given(st.integers(100, 1_000_000), st.integers(0, 100)) @settings(deadline=None) def test_one_parent_in_the_band_yields_to_the_other_parent(dimes, position): @@ -250,7 +265,9 @@ def test_one_parent_in_the_band_yields_to_the_other_parent(dimes, position): entitled either way, so the only entitled set satisfying (h)(1)(D) for both parents is the other parent alone, paid at 82.5 percent. """ - low, high = dimes * 750 // 1000, dimes * 825 // 1000 + low, high = _statutory_band_dimes(dimes) + if high <= low: + return own_dimes = low + (high - low - 1) * position // 100 own = g.OwnBenefit(g.OwnBenefitKind.OLD_AGE, F(own_dimes, 10)) rows = _rows( @@ -259,12 +276,13 @@ def test_one_parent_in_the_band_yields_to_the_other_parent(dimes, position): [_parent("p1", own_benefit=own), _parent("p2")], ) ) + one_ob = F(dimes * 825 // 1000, 10) assert not rows["p1"].entitled assert rows["p1"].auxiliary_payable == 0 assert rows["p1"].own_benefit == F(own_dimes, 10) assert rows["p2"].entitled - assert rows["p2"].original_benefit == F(high, 10) - assert rows["p2"].auxiliary_payable == F(high, 10) + assert rows["p2"].original_benefit == one_ob + assert rows["p2"].auxiliary_payable == one_ob @given(st.integers(100, 1_000_000), st.integers(0, 100), st.integers(0, 100)) @@ -272,7 +290,9 @@ def test_one_parent_in_the_band_yields_to_the_other_parent(dimes, position): def test_two_parents_in_the_band_refuse_because_either_alone_qualifies( dimes, first, second ): - low, high = dimes * 750 // 1000, dimes * 825 // 1000 + low, high = _statutory_band_dimes(dimes) + if high <= low: + return members = [ _parent( bid, @@ -295,8 +315,8 @@ def _consistent_parent_sets(pia, own): A member's RIB must be below the rate of the set; an excluded parent's RIB must reach the rate it would share by joining the set. """ - one = g.floor_dime(pia * F(825, 1000)) - two = g.floor_dime(pia * F(3, 4)) + one = pia * F(825, 1000) + two = pia * F(3, 4) ids = sorted(own) found = [] for size in range(len(ids) + 1): diff --git a/tests/test_track_b_gross_benefits_parisi.py b/tests/test_track_b_gross_benefits_parisi.py index 579008328..090881e0b 100644 --- a/tests/test_track_b_gross_benefits_parisi.py +++ b/tests/test_track_b_gross_benefits_parisi.py @@ -504,17 +504,17 @@ def test_method_c_own_rib_age_reduction_does_not_change_redistributed_rates( ) -def test_method_b_zero_only_after_age_counts_the_before_age_amount(): +def test_method_b_zero_only_after_age_keeps_the_age_reduction_in_ptap(): """Source conflict us:oasdi-dual-entitlement-zero-after-age-reduction. Invented PIA500/max750; B36months ownRIB64, method B. Share83.30 - reduces to62.40, then offsets to0. GN 02603.050A applies its - exceptions under .045, which counts the dual rate before age - reduction: 83.30-64=19.30 is a partial rate (.050A.3), so the - children receive115.30 each and the record total is730.60. The - rejected readings stay pinned as diagnostics: A.2's disregard - (125 each, total750) and the full20.90 age term under C (114.50 - each, total729). + reduces to62.40 for age, then offsets to0. Dual entitlement considered + first leaves83.30-64=19.30, so GN .050A.2's disregard does not apply. + In 402(k)(3)(A)'s order the offset withholds62.40 and age20.90; + 20 CFR 404.403(a)(5) excludes only the former and .060 B.2 keeps the + age slice in PTAP: children114.50 each, record total729. Rejected + diagnostics: A.2's disregard (125 each, total750) and .045's + before-age amount alone (115.30 each, total730.60). """ spouse = g.Beneficiary( "B", @@ -532,11 +532,11 @@ def test_method_b_zero_only_after_age_counts_the_before_age_amount(): rows = _family(state, members).by_id() assert rows["B"].age_adjusted_rate == F("62.40") assert rows["B"].auxiliary_payable == 0 - assert rows["B"].counted_against_maximum == F("19.30") + assert rows["B"].counted_against_maximum == F("20.90") assert rows["C1"].auxiliary_payable == rows["C2"].auxiliary_payable - assert rows["C1"].auxiliary_payable == F("115.30") + assert rows["C1"].auxiliary_payable == F("114.50") total = sum((row.auxiliary_payable for row in rows.values()), F(0)) - assert F(500) + total == F("730.60") + assert F(500) + total == 729 zero_rate_exception = g.maximum_redistribution_rates( {"B": F(250), "C1": F(250), "C2": F(250)}, F(250), @@ -544,34 +544,38 @@ def test_method_b_zero_only_after_age_counts_the_before_age_amount(): ) assert dict(zero_rate_exception) == {"C1": F(125), "C2": F(125)} assert F(500) + sum(zero_rate_exception.values(), F(0)) == 750 - full_age_term = g.maximum_redistribution_rates( + before_age_only = g.maximum_redistribution_rates( {"B": F(250), "C1": F(250), "C2": F(250)}, F(250), - deductee_payables={"B": F(0)}, - age_reductions={"B": F("20.90")}, + deductee_payables={"B": F("19.30")}, ) - assert dict(full_age_term) == {"C1": F("114.50"), "C2": F("114.50")} - assert F(500) + sum(full_age_term.values(), F(0)) == 729 + assert dict(before_age_only) == {"C1": F("115.30"), "C2": F("115.30")} + assert F(500) + sum(before_age_only.values(), F(0)) == F("730.60") assert ( "The benefit payable to the dually entitled person is the amount before any applicable reduction for age" in source_text("poms_gn_02603_045") ) rule = " ".join(source_text("poms_gn_02603_050").split()) for quote in ( - "exceptions 2 and 3 apply only for cases processed under the new " - "rule as explained in GN 02603.045", "If dual entitlement reduces a beneficiary's rate to $0.00 " "(technically entitled)", - "If dual entitlement reduces a beneficiary's rate to a partial " - "payable rate and no other deduction is involved for any other " - "beneficiary, subtract the partial payable rate", "Age reduction amount for all workers or deductees", + "Partial benefit payable to any worker or deductee", ): assert quote in rule + examples = " ".join(source_text("poms_gn_02603_060").split()) + for quote in ( + "In order to redistribute only the amount being withheld because " + "of dual entitlement we must account for the entire $83.30 due B.", + "This example shows that B's age reduction has no effect because " + "we consider their dual entitlement first.", + ): + assert quote in examples @settings(deadline=None) @example(F(64), 36) +@example(F("62.40"), 36) @example(F("83.20"), 36) @example(F("83.30"), 36) @example(F(50), 60) @@ -579,13 +583,13 @@ def test_method_b_zero_only_after_age_counts_the_before_age_amount(): st.integers(1, 1000).map(lambda dimes: F(dimes, 10)), st.integers(0, 60), ) -def test_method_b_counted_amount_is_the_before_age_amount(own, months): - """Method B counts max(0, standard - own) whether or not age zeroes it. +def test_method_b_counts_payable_plus_age_reduction_until_a2(own, months): + """Method B counts what dual entitlement does not withhold. - The counted amount is continuous and nonincreasing in the own benefit, - meets GN 02603.050A.2's disregard exactly when the own benefit reaches - the standard rate, and equals payable plus age reduction whenever the - payable is positive. Children never fall when the own benefit rises. + Counted = payable + the auxiliary's own age reduction, which is + standard - min(own, age-adjusted rate), until the own benefit reaches + the standard rate and GN 02603.050A.2 disregards the spouse. Children + never fall when the own benefit rises. """ spouse = g.Beneficiary( "B", @@ -598,14 +602,11 @@ def test_method_b_counted_amount_is_the_before_age_amount(own, months): children = [g.Beneficiary(key, g.Role.CHILD) for key in ("C1", "C2")] rows = _family(state, [spouse, *children]).by_id() standard = F("83.30") - before_age = max(F(0), standard - own) - assert rows["B"].counted_against_maximum == before_age - if rows["B"].auxiliary_payable: - assert rows["B"].auxiliary_payable == rows["B"].age_adjusted_rate - own - assert before_age == rows["B"].auxiliary_payable + ( - standard - rows["B"].age_adjusted_rate - ) - child = min(F(250), _floor_dime((F(250) - before_age) / 2)) + age_adjusted = rows["B"].age_adjusted_rate + counted = F(0) if own >= standard else standard - min(own, age_adjusted) + assert rows["B"].counted_against_maximum == counted + assert rows["B"].auxiliary_payable == max(F(0), age_adjusted - own) + child = min(F(250), _floor_dime((F(250) - counted) / 2)) assert rows["C1"].auxiliary_payable == rows["C2"].auxiliary_payable assert rows["C1"].auxiliary_payable == child richer = _family( diff --git a/tests/test_track_b_ret_ledger_properties.py b/tests/test_track_b_ret_ledger_properties.py index e3a6fc4f0..a3bb066e0 100644 --- a/tests/test_track_b_ret_ledger_properties.py +++ b/tests/test_track_b_ret_ledger_properties.py @@ -853,7 +853,13 @@ def test_every_raw_month_set_element_is_validated_before_deduplication( _month_set_case(field, values, month) -@pytest.mark.parametrize("field", MONTH_SET_FIELDS) +# Any non-empty own_work_exempt_months is an input error (202(s)(3)). +VALID_MONTH_SET_FIELDS = tuple( + field for field in MONTH_SET_FIELDS if field != "own_work_exempt_months" +) + + +@pytest.mark.parametrize("field", VALID_MONTH_SET_FIELDS) @settings(max_examples=30, deadline=None) @given(st.lists(st.integers(1, 12), min_size=1, max_size=30)) def test_valid_month_set_duplicates_and_order_preserve_the_same_months( diff --git a/tests/test_track_b_ret_ledger_readings.py b/tests/test_track_b_ret_ledger_readings.py index 168180e88..2c2d72cc6 100644 --- a/tests/test_track_b_ret_ledger_readings.py +++ b/tests/test_track_b_ret_ledger_readings.py @@ -28,6 +28,9 @@ earnings, record, ) +from tests.track_b_gross_benefit_support import ( + source_text as gross_source_text, +) from tests.track_b_ret_ledger_support import source_text @@ -311,82 +314,28 @@ def test_invalid_sequences_and_infinite_numbers_are_input_errors(make_input): make_input() -@pytest.mark.parametrize("benefit_type", [BenefitType.RIB, BenefitType.SPOUSE]) -def test_own_work_exempt_flag_requires_a_statutory_benefit_type(benefit_type): - with pytest.raises(r.InvalidFamilyInput, match="Child or widow"): - record( - beneficiary( - benefit_type=benefit_type, - own_work_exempt_months={1}, - ), - fiscal_year=True, - ) - - -@settings(max_examples=75, deadline=None) -@given( - st.sampled_from([BenefitType.CHILD, BenefitType.WIDOW]), - st.frozensets(st.integers(1, 12), max_size=12), - st.integers(0, 10_000), +@pytest.mark.parametrize( + "benefit_type", + [ + BenefitType.RIB, + BenefitType.SPOUSE, + BenefitType.CHILD, + BenefitType.WIDOW, + ], ) -def test_property_other_statutory_own_exemptions_protect_only_that_workers_excess( - benefit_type, protected, excess -): - nh = beneficiary() - auxiliary = beneficiary( - "aux", - benefit_type=benefit_type, - own_work_exempt_months=protected, - work=earnings( - PARAMS.for_year(YEAR).lower_exempt_amount.value + 2 * excess - ), - ) - case = ( - r.RecordYear( - YEAR, - "deceased", - kind=r.RecordKind.DECEASED, - beneficiaries=(auxiliary,), - ) - if benefit_type is BenefitType.WIDOW - else record(nh, auxiliary) +def test_own_work_exempt_months_are_never_a_lawful_assertion(benefit_type): + """Source conflict us:oasdi-ret-own-work-age-exemptions. + + 202(s)(3) limits Act 203(f)(1)(C) to a child under a disability that + month or three months before, and 402(e)(1) ends a pre-60 widow(er)'s + entitlement at the termination month, so the Act exempts only CDB and + DWB months, which the ledger exempts by type. + """ + with pytest.raises(r.InvalidFamilyInput, match="disability-based"): + beneficiary(benefit_type=benefit_type, own_work_exempt_months={1}) + assert "subsection (f)(1)(C) of section 403 of this title" in " ".join( + gross_source_text("lii_usc_402_415").split() ) - result = r.apply_ret_year(case, PARAMS) - assert all( - m.deduction == 0 - for m in result.for_beneficiary("aux") - if m.month in protected - ) - assert all(m.deduction == 0 for m in result.for_beneficiary("nh")) - assert sum(m.deduction for m in result.for_beneficiary("aux")) == min( - excess, 600 * (12 - len(protected)) - ) - if benefit_type is BenefitType.CHILD: - # A child's asserted own-work exemption must not protect that - # child's benefit from the NH's independently chargeable excess. - nh_worked = replace( - nh, - earnings=earnings( - PARAMS.for_year(YEAR).lower_exempt_amount.value + 2 * excess - ), - ) - protected_result = r.apply_ret_year( - record(nh_worked, auxiliary), PARAMS - ) - reference = r.apply_ret_year( - record( - nh_worked, - replace( - auxiliary, - own_work_exempt_months=frozenset(), - earnings=earnings(), - ), - ), - PARAMS, - ) - assert [m.nh_deduction for m in protected_result.months] == [ - m.nh_deduction for m in reference.months - ] @pytest.mark.parametrize("benefit_type", [BenefitType.SPOUSE, BenefitType.RIB]) @@ -610,6 +559,12 @@ def test_missing_supported_year_is_not_hidden_by_a_coverage_refusal(): def test_optional_act_age_exemptions_are_an_explicit_600_dollar_reading( benefit_type, ): + """Ordinary CHILD and WIDOW months are charged; no override exists. + + The former Act-clause assertion would have paid the $600; under + 202(s)(3) and 402(e)(1) it is not a lawful reading, so it is now an + input error and the charged $600 is the only answer. + """ person = beneficiary( "aux", rate=200, @@ -630,14 +585,6 @@ def case(person): ) conservative = r.apply_ret_year(case(person), PARAMS) - act = r.apply_ret_year( - case(replace(person, own_work_exempt_months=frozenset(range(1, 13)))), - PARAMS, - ) + with pytest.raises(r.InvalidFamilyInput, match="disability-based"): + replace(person, own_work_exempt_months=frozenset(range(1, 13))) assert sum(m.deduction for m in conservative.months) == 600 - assert sum(m.deduction for m in act.months) == 0 - assert ( - sum(m.payment for m in act.months) - - sum(m.payment for m in conservative.months) - == 600 - ) From 0cf5400a2b95f70323ad89dc3d85545cb2b86042 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 17:17:55 -0400 Subject: [PATCH 03/13] Review r1 fixes: tier counts, parent overview, 402(s)(2) wording, RIB/DIB elections GPT-6.1 Sol r1 requested changes on 479c3212 (no supported-dollar regression found): - tier counts: +19 unit tests (18 from the readings pass, one new invalid GraceMonth case below); unit 18,648, total 25,179, verified by collection; - the G parent overview now states 402(h)(1)(D)'s unrounded thresholds; - the R2 marriage passage and the RETBeneficiary docstring quote 402(d)(1)(D)'s "month preceding" and 402(s)(2)'s third-prior-month clause; - the disability-maximum note gives a real multi-COLA gap ($0.20), not a $0.10 bound; the NESE rationale states both meanings of "drop the cents"; the grace differential's labels name the new default; - GraceMonth rejects RIB and DIB both received in one month (402(k)(4)). Co-Authored-By: Claude Opus 5.5 --- docs/design/track_b_g_gross_benefits.md | 15 ++++++---- docs/design/track_b_r2_ret_ledger.md | 31 +++++++++++---------- src/populace_dynamics/track_b/ret_grace.py | 4 +++ src/populace_dynamics/track_b/ret_ledger.py | 5 ++-- tests/README-tiers.md | 4 +-- tests/test_track_b_ret_grace.py | 6 ++++ tests/tier_counts.json | 2 +- 7 files changed, 42 insertions(+), 25 deletions(-) diff --git a/docs/design/track_b_g_gross_benefits.md b/docs/design/track_b_g_gross_benefits.md index a6c5b9d68..1e86e2757 100644 --- a/docs/design/track_b_g_gross_benefits.md +++ b/docs/design/track_b_g_gross_benefits.md @@ -489,9 +489,10 @@ maximum (215(i)(2)(A)(ii)), so an undimed initial maximum would be the only one of its kind; SSA's own description, the Bulletin, floors it. Where the statute is silent, P1 lets the agency's published reading fill the gap. A blind adjudication preferred leaving the maximum unrounded, reading the -contrast with 403(a)(1) and 20 CFR 404.403(c) as deliberate; the stakes are -at most $0.10 a month per auxiliary, and only after a COLA (the pre-COLA -shares floor to the same dimes), so the published agency reading is kept. +contrast with 403(a)(1) and 20 CFR 404.403(c) as deliberate. The stakes +are a few dimes a month per auxiliary and arise only after COLAs: three +successive 5 percent COLAs on PIA $1,000.10 and AIME $1,701 pay children +$515.80 against $516.00. The published agency reading is kept. ## Households and the entry point @@ -1312,9 +1313,11 @@ correct PIA; this layer does not calculate WEP. Parents on survivor records use 82.5 percent for one entitled parent and 75 percent each for two, floored to a dime (202(h)(2); RS 00209.015). The maximum applies before 202(k)(3)(A)'s own-benefit offset. There is no age -reduction. A parent's own RIB must be less than the parent's OB to establish -entitlement (RS 00209.001). This compares the actual reduced or credited -own RIB and the dime-rounded OB, rather than the own PIA. An own-DIB parent +reduction. A parent's own RIB must be less than 82½ percent of the PIA, or +75 percent when another parent is entitled, to establish entitlement +(402(h)(1)(D); RS 00209.001 states the test with the OB). This compares the +actual reduced or credited own RIB with the unrounded statutory share, +rather than the dime-rounded OB or the own PIA. An own-DIB parent remains technically entitled even if no parent benefit is payable (RS 00209.015); that parent still counts for the two-parent rate. More than two declared parents is refused as `parent_benefit`: the captured one/two-parent diff --git a/docs/design/track_b_r2_ret_ledger.md b/docs/design/track_b_r2_ret_ledger.md index 40d9a2d59..6be3f4e46 100644 --- a/docs/design/track_b_r2_ret_ledger.md +++ b/docs/design/track_b_r2_ret_ledger.md @@ -828,7 +828,7 @@ not a reconstruction of monetary RET under pre-1978 law. | Refusal of vocational rehabilitation | `VOCATIONAL_REHABILITATION` / `vocational_rehabilitation_refusal` | RS 02501.130 | | Another deduction event on the record | `OTHER_DEDUCTION` / `other_deduction_event` | Act 203(c)/(d); RS 02501.130; GN 02603.040 | | Unmarried purported deemed entitlement | `InvalidFamilyInput`: not on this NH's ledger | Act 203(b)(1) and 20 CFR 404.420 require marriage; RS 02501.140's broader wording yields (P1) | -| Ordinary deemed adult CHILD with asserted marriage and age facts | `DEEMED_ENTITLEMENT_BASIS` / `deemed_entitlement_marriage_basis` | Entitled only through 402(d)(5) for a marriage made while disabled (402(s)(2)); CFR .420 omits that residual case | +| Ordinary deemed adult CHILD with asserted marriage and age facts | `DEEMED_ENTITLEMENT_BASIS` / `deemed_entitlement_marriage_basis` | Entitled only through 402(d)(5) for a marriage made while disabled or within three months of disability (402(s)(2)); CFR .420 omits that residual case | The monthly G context establishes maximum membership, original age reduction and dual entitlement. Its declared baseline must match the R2 @@ -1287,8 +1287,8 @@ production calls carried year by year against that model over invented months, concurrent entitlement, work conditions, and a single FRA date. The comparison covers grace kinds, applicability, eligible and protected months, LMETY, and pending eligibility at zero tolerance. It exercises -both the default CFR disability exclusion and the explicit POMS election -alternative. At December terminations, inputs explicitly label known +both the default 202(k)(4) reading (`POMS_RIB_ELECTION`) and the explicit +CFR technical-entitlement alternative. At December terminations, inputs explicitly label known January conversions as excluded, preserving the annual interface's existing requirement. Captured quotations are checked by `test_reference_rules_are_verbatim_in_manifest_captures`. This @@ -1360,10 +1360,12 @@ call). Act manner under regulations of the Commissioner of Social Security"), and no captured regulation fixes its rounding, so SSA's instruction governs. Within RS 02501.080, "round down" means "round down to the next lower -dollar" (B.1.d, B.2.a); applied after the cents are dropped, only the floor -gives that step any work, while truncation would leave it surplus; and -for positive amounts the instruction's direction lowers countable -earnings, which the floor keeps for a loss. A blind adjudication read +dollar" (B.1.d, B.2.a). If "drop the cents" removes only the fraction of +a cent the division leaves (−$1.0833 to −$1.08), rounding down to the next +lower dollar then gives the floor, −$2, and both steps do work; if it +truncates to whole dollars (−$1), "round down" is idle. For positive +amounts the instruction's direction lowers countable earnings, which the +floor keeps for a loss. A blind adjudication read "drop the cents" as truncating to whole dollars (leaving "round down" idle) and preferred `DROP_CENTS`, which here matches exact proration's $2 excess; the stake is at most a dollar's worth of countable earnings per @@ -1476,13 +1478,14 @@ broader harness with the supported married ledger. Refusal kept for the residual adult-child case: the Act's words also reach "a child who has attained the age of 18 and is entitled to child's insurance benefits", where 20 CFR 404.420 says "a child's insurance -benefit based on the child's disability". A non-disabled child's benefit -ends with the month the child marries (402(d)(1)(D)), and 402(d)(5)'s -exception for marrying a beneficiary applies only to a child "under a -disability" at the time of the marriage (402(s)(2)). So a married, -non-disabled adult child keeps child's benefits only in the residual case -of a marriage made while disabled followed by recovery, which the -regulation omits. That configuration stays refused as +benefit based on the child's disability". A child's entitlement ends +"with the month preceding" the month the child marries (402(d)(1)(D)), +and 402(d)(5)'s exception for marrying a beneficiary applies only if the +child "at the time of the marriage ... was under a disability ... or had +been under such a disability in the third month before the month in which +such marriage occurred" (402(s)(2)). So a married, non-disabled adult +child keeps child's benefits only in the residual case of such a marriage +followed by recovery, which the regulation omits. That configuration stays refused as `DEEMED_ENTITLEMENT_BASIS`, with its row and weight kept; `test_deemed_ordinary_child_requires_explicit_adult_age_facts` pins invalid missing-age facts and the refusal, and the married CDB benchmark diff --git a/src/populace_dynamics/track_b/ret_grace.py b/src/populace_dynamics/track_b/ret_grace.py index 047c5b618..0e7ba7a4b 100644 --- a/src/populace_dynamics/track_b/ret_grace.py +++ b/src/populace_dynamics/track_b/ret_grace.py @@ -155,6 +155,10 @@ def __post_init__(self): raise InvalidFamilyInput( "Received benefit types must be entitled benefit types" ) + if {BenefitType.RIB, BenefitType.DIB} <= received: + raise InvalidFamilyInput( + "A month pays RIB or DIB, not both (Act 202(k)(4))" + ) object.__setattr__(self, "entitled_types", entitled) object.__setattr__(self, "received_types", received) diff --git a/src/populace_dynamics/track_b/ret_ledger.py b/src/populace_dynamics/track_b/ret_ledger.py index 3f6f8b4a4..d4b49549f 100644 --- a/src/populace_dynamics/track_b/ret_ledger.py +++ b/src/populace_dynamics/track_b/ret_ledger.py @@ -244,8 +244,9 @@ class RETBeneficiary: on this NH's ledger. An ordinary child's Act 203(b)(1) age facts need explicit ``deemed_child_age_18_months``; such a married non-disabled child is entitled only if 402(d)(5) kept the benefit through a marriage - made while disabled (402(s)(2)), a residual case CFR .420 omits, so it - stays refused. Own two-record excess is still refused. + made while disabled, or disabled in the third month before it + (402(s)(2)), a residual case CFR .420 omits, so it stays refused. Own + two-record excess is still refused. ``own_work_exempt_months`` must stay empty: Act 203(f)(1)(C) reaches only a child under a disability that month or three months before (202(s)(3)), and (D)'s widow(er) entitled before 60 is a disabled diff --git a/tests/README-tiers.md b/tests/README-tiers.md index 18367b687..723dfb357 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` | 18,629 | +| `unit` | 18,648 | | `artifact` | 4,253 | | `integration_psid` | 1,450 | | `reproduction_legacy` | 520 | | `oracle_policyengine` | 308 | -| **Total** | **25,160** | +| **Total** | **25,179** | diff --git a/tests/test_track_b_ret_grace.py b/tests/test_track_b_ret_grace.py index c9d905ad6..f83cd6168 100644 --- a/tests/test_track_b_ret_grace.py +++ b/tests/test_track_b_ret_grace.py @@ -933,6 +933,9 @@ def test_property_disabled_different_type_preserves_pending_until_eligible_nsm( "construct", [ lambda: GraceMonth(received_types={B.RIB}), + lambda: GraceMonth( + entitled_types={B.RIB, B.DIB}, received_types={B.RIB, B.DIB} + ), lambda: GraceMonth(entitled_types={"rib"}), lambda: GraceMonth(nonservice=1), lambda: GraceYearState(gap_since_entitlement=True), @@ -996,6 +999,9 @@ def arbitrary_month(draw): if types else set() ) + if {B.RIB, B.DIB} <= received: + # 202(k)(4): one of the two is paid for the month. + received.discard(draw(st.sampled_from([B.RIB, B.DIB]))) return GraceMonth( entitled_types=types, received_types=received, diff --git a/tests/tier_counts.json b/tests/tier_counts.json index 1d08b5c15..f5f81c93b 100644 --- a/tests/tier_counts.json +++ b/tests/tier_counts.json @@ -1,7 +1,7 @@ { "schema_version": 1, "counts": { - "unit": 18629, + "unit": 18648, "artifact": 4253, "integration_psid": 1450, "reproduction_legacy": 520, From 4f1f70a24fa3f330afdb9b9432dc8c591ce87c48 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 18:25:40 -0400 Subject: [PATCH 04/13] Review r2 fix: state 402(s)(2)'s marriage condition exactly Sol r2 confirmed six of seven round-1 fixes; the remaining medium was the R2 note's summaries of 402(s)(2). The table and the follow-on sentence now say disability in the marriage month or in the third month before it, with recovery before or after the marriage, matching the quoted statute; the RETBeneficiary docstring says the same. Docs only; no code path changes. Co-Authored-By: Claude Opus 5.5 --- docs/design/track_b_r2_ret_ledger.md | 8 +++++--- src/populace_dynamics/track_b/ret_ledger.py | 5 +++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/design/track_b_r2_ret_ledger.md b/docs/design/track_b_r2_ret_ledger.md index 6be3f4e46..69a144346 100644 --- a/docs/design/track_b_r2_ret_ledger.md +++ b/docs/design/track_b_r2_ret_ledger.md @@ -828,7 +828,7 @@ not a reconstruction of monetary RET under pre-1978 law. | Refusal of vocational rehabilitation | `VOCATIONAL_REHABILITATION` / `vocational_rehabilitation_refusal` | RS 02501.130 | | Another deduction event on the record | `OTHER_DEDUCTION` / `other_deduction_event` | Act 203(c)/(d); RS 02501.130; GN 02603.040 | | Unmarried purported deemed entitlement | `InvalidFamilyInput`: not on this NH's ledger | Act 203(b)(1) and 20 CFR 404.420 require marriage; RS 02501.140's broader wording yields (P1) | -| Ordinary deemed adult CHILD with asserted marriage and age facts | `DEEMED_ENTITLEMENT_BASIS` / `deemed_entitlement_marriage_basis` | Entitled only through 402(d)(5) for a marriage made while disabled or within three months of disability (402(s)(2)); CFR .420 omits that residual case | +| Ordinary deemed adult CHILD with asserted marriage and age facts | `DEEMED_ENTITLEMENT_BASIS` / `deemed_entitlement_marriage_basis` | Entitled only through 402(d)(5), which needs disability in the month of marriage or in the third month before it (402(s)(2)); the child may have recovered before or after the marriage. CFR .420 omits that residual case | The monthly G context establishes maximum membership, original age reduction and dual entitlement. Its declared baseline must match the R2 @@ -1484,8 +1484,10 @@ and 402(d)(5)'s exception for marrying a beneficiary applies only if the child "at the time of the marriage ... was under a disability ... or had been under such a disability in the third month before the month in which such marriage occurred" (402(s)(2)). So a married, non-disabled adult -child keeps child's benefits only in the residual case of such a marriage -followed by recovery, which the regulation omits. That configuration stays refused as +child keeps child's benefits only in the residual case of a marriage that +qualified under (s)(2) (disability in the marriage month or in the third +month before it), with recovery before or after the marriage, which the +regulation omits. That configuration stays refused as `DEEMED_ENTITLEMENT_BASIS`, with its row and weight kept; `test_deemed_ordinary_child_requires_explicit_adult_age_facts` pins invalid missing-age facts and the refusal, and the married CDB benchmark diff --git a/src/populace_dynamics/track_b/ret_ledger.py b/src/populace_dynamics/track_b/ret_ledger.py index d4b49549f..93e5ef22b 100644 --- a/src/populace_dynamics/track_b/ret_ledger.py +++ b/src/populace_dynamics/track_b/ret_ledger.py @@ -244,8 +244,9 @@ class RETBeneficiary: on this NH's ledger. An ordinary child's Act 203(b)(1) age facts need explicit ``deemed_child_age_18_months``; such a married non-disabled child is entitled only if 402(d)(5) kept the benefit through a marriage - made while disabled, or disabled in the third month before it - (402(s)(2)), a residual case CFR .420 omits, so it stays refused. Own + in whose month, or in the third month before which, the child was + under a disability (402(s)(2)); recovery may come before or after the + marriage. CFR .420 omits that residual case, so it stays refused. Own two-record excess is still refused. ``own_work_exempt_months`` must stay empty: Act 203(f)(1)(C) reaches only a child under a disability that month or three months before From e75328f485e49d0e7c63bbca31ad8fdff0ab1261 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 13:08:16 -0500 Subject: [PATCH 05/13] Round-4 review fixes: whole-id marker checks and the G note's test claim The cloud round-4 review (APPROVE at 1fa56867) found two low items in this PR's bookkeeping: - The reverse marker test matched substrings, so a truncated or misspelled id passed when it was a prefix of a documented id. It now extracts whole ids from the Track B notes. - The G note still said the test checks markers against the logged ids in both directions. The test now does so again: the markers in the G, R2, R3 and grace modules must equal this pass's readings plus the ids other rounds register (#563's two), and the note says what it checks. The insured-status modules keep their own register, as in the round-3 test, so #562's markers don't enter these checks once it lands. Co-Authored-By: Claude Opus 5.5 --- docs/design/track_b_g_gross_benefits.md | 19 ++++++----- tests/test_track_b_readings_20261023.py | 42 ++++++++++++++++--------- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/docs/design/track_b_g_gross_benefits.md b/docs/design/track_b_g_gross_benefits.md index 7ead9017c..8e59c71d1 100644 --- a/docs/design/track_b_g_gross_benefits.md +++ b/docs/design/track_b_g_gross_benefits.md @@ -3271,14 +3271,17 @@ row and weight in the denominators. | Temporary partial deduction | `us:oasdi-ret-temporary-partial-deduction` | Follow the RS 02501.160 example in G | Specific over general; P3 | | NH reservations with age reductions | `us:oasdi-nh-charge-age-reduction-interaction` | Refused | Deferred coverage, not a conflict | -The statutory texts newly relied on (42 U.S.C. 402(d)(1)(D), (d)(5), -(e)(1), (h)(1)(D), (h)(2), (k)(3)(A), (k)(4) and (s)) were added to -`lii_usc_402_415_excerpts.txt` from a re-fetch whose LII body matched the -pinned 402 SHA-256 byte for byte. Every `# Source conflict` marker in the -Track B modules names an id logged in the rulings, and every logged id -has a marker: `tests/test_track_b_readings_20261023.py` checks both -directions and that this note and the R2 and R3 notes carry no open -"Needs Max's judgment" heading. The invented conformance lattice is +This pass added the statutory texts it newly relies on (42 U.S.C. +402(d)(1)(D), (d)(5), (e)(1), (h)(1)(D), (h)(2), (k)(3)(A), (k)(4) and +(s)) to `lii_usc_402_415_excerpts.txt`, from a re-fetch whose LII body +matched the pinned 402 SHA-256 byte for byte. +`tests/test_track_b_readings_20261023.py` pins the bookkeeping. The +`# Source conflict` markers in the G, R2, R3 and grace modules must be +exactly this pass's ids plus the ids other rounds register in that test, +and every marker must name a whole id that a Track B design note +documents. This note and the R2 and R3 notes must carry no open "Needs +Max's judgment" heading, and each statutory passage the test quotes must +appear verbatim in the capture. The invented conformance lattice is unchanged; only the `parent_benefit` witness moved to two parents in the band. diff --git a/tests/test_track_b_readings_20261023.py b/tests/test_track_b_readings_20261023.py index 4d8cac07d..de0f1e65f 100644 --- a/tests/test_track_b_readings_20261023.py +++ b/tests/test_track_b_readings_20261023.py @@ -2,9 +2,10 @@ Verification class: statutory conformance and documentation integrity on captured sources only. No population, behavioral or forecast claim. -Every ``# Source conflict`` marker in the Track B modules names a reading -documented in a design note, every reading of this pass has a marker, and -no design note keeps an open "Needs Max's judgment" heading. +The ``# Source conflict`` markers in the G, R2, R3 and grace modules are +exactly this pass's readings plus those other rounds register here, each +marker names a whole id that a Track B design note documents, and no +design note keeps an open "Needs Max's judgment" heading. """ import re @@ -50,7 +51,16 @@ "us:oasdi-arf-spouse-reentitlement-credits", }, } -MARKER = re.compile(r"# Source conflict (us:oasdi-[a-z0-9-]+)") +#: Readings that other rounds log in the G, R2, R3 and grace modules +#: (microcosm-dynamics#563). A new marker there needs its id here or above. +OTHER_ROUNDS = { + "us:oasdi-finality-protected-rates-before-coverage", + "us:oasdi-sole-full-deduction-zero-dual-composition", +} +#: Insured-status modules keep their own register of readings. +OWN_REGISTERS = {"insured_status.py", "_insured_status_qc.py"} +ID = r"us:oasdi-[a-z0-9]+(?:-[a-z0-9]+)*" +MARKER = re.compile(rf"# Source conflict ({ID})") def _module_markers(): @@ -58,13 +68,18 @@ def _module_markers(): for path in sorted(MODULES.glob("*.py")): for match in MARKER.finditer(path.read_text()): found.setdefault(match.group(1), set()).add(path.name) - return found + return { + name: files + for name, files in found.items() + if not files <= OWN_REGISTERS + } -def test_each_reading_of_this_pass_has_a_marker_in_its_module(): +def test_markers_and_logged_readings_match_in_both_directions(): markers = _module_markers() - for name in set().union(*READINGS.values()): - assert name in markers, name + logged = set().union(*READINGS.values()) | OTHER_ROUNDS + assert set(markers) == logged + for name in logged: assert markers[name] <= { "gross_benefits.py", "ret_ledger.py", @@ -74,13 +89,12 @@ def test_each_reading_of_this_pass_has_a_marker_in_its_module(): def test_every_marker_names_a_reading_documented_in_a_design_note(): - # Later rounds log their own readings; each must still be documented. - notes = " ".join( - path.read_text() - for path in (ROOT / "docs" / "design").glob("track_b_*.md") - ) + # Whole ids, so a truncated or misspelled marker cannot match a prefix. + documented = set() + for path in (ROOT / "docs" / "design").glob("track_b_*.md"): + documented.update(re.findall(ID, path.read_text())) for name in _module_markers(): - assert name in notes, name + assert name in documented, name @pytest.mark.parametrize("note", sorted(NOTES)) From 0d186c7163e186cbb99417f7b3cc182ece43f76c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 13:26:51 -0500 Subject: [PATCH 06/13] Round-5 review fixes: every source-conflict mention parses, exemption stated The cloud round-5 review requested changes on e75328f4: - A marker the regex couldn't parse (an id wrapped onto the next line, "conflict:" with a colon, lowercase) was invisible to both marker tests. Outside the exempt modules, every mention of a source conflict must now parse as a one-line marker. - An id extended past a non-id character ("...intervalX", "..._v2") read as the documented id. A marker's id must now end at a colon, whitespace or the end of the file, and a note's id must not run on into another id character or hyphen. - The note and docstring said "every marker" without the insured-status exemption. Both now state it and why: the source-conflict rulings log #562's insured-status readings, but no Track B note documents all of them yet. The note also says each note must name its own readings. Sixteen mutations, including all twelve of the review's, behave as expected; the file still collects 15 tests, so the tier counts don't change. Co-Authored-By: Claude Opus 5.5 --- docs/design/track_b_g_gross_benefits.md | 14 ++++++---- tests/test_track_b_readings_20261023.py | 34 ++++++++++++++++++------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/docs/design/track_b_g_gross_benefits.md b/docs/design/track_b_g_gross_benefits.md index 8e59c71d1..33c9a7467 100644 --- a/docs/design/track_b_g_gross_benefits.md +++ b/docs/design/track_b_g_gross_benefits.md @@ -3275,11 +3275,15 @@ This pass added the statutory texts it newly relies on (42 U.S.C. 402(d)(1)(D), (d)(5), (e)(1), (h)(1)(D), (h)(2), (k)(3)(A), (k)(4) and (s)) to `lii_usc_402_415_excerpts.txt`, from a re-fetch whose LII body matched the pinned 402 SHA-256 byte for byte. -`tests/test_track_b_readings_20261023.py` pins the bookkeeping. The -`# Source conflict` markers in the G, R2, R3 and grace modules must be -exactly this pass's ids plus the ids other rounds register in that test, -and every marker must name a whole id that a Track B design note -documents. This note and the R2 and R3 notes must carry no open "Needs +`tests/test_track_b_readings_20261023.py` pins the bookkeeping for every +Track B module except the insured-status ones (`insured_status.py` and +`_insured_status_qc.py`), whose readings the source-conflict rulings log +but no Track B note yet documents in full. In the other modules, every +mention of a source conflict must be a one-line `# Source conflict ` +marker. The marker ids must be exactly this pass's ids plus the ids other +rounds register in that test, each in the G, R2, R3 or grace module, and +each id must appear whole in a Track B design note. This note and the R2 +and R3 notes must name each of their readings and carry no open "Needs Max's judgment" heading, and each statutory passage the test quotes must appear verbatim in the capture. The invented conformance lattice is unchanged; only the `parent_benefit` witness moved to two parents in the diff --git a/tests/test_track_b_readings_20261023.py b/tests/test_track_b_readings_20261023.py index de0f1e65f..a6244faa6 100644 --- a/tests/test_track_b_readings_20261023.py +++ b/tests/test_track_b_readings_20261023.py @@ -2,10 +2,12 @@ Verification class: statutory conformance and documentation integrity on captured sources only. No population, behavioral or forecast claim. -The ``# Source conflict`` markers in the G, R2, R3 and grace modules are -exactly this pass's readings plus those other rounds register here, each -marker names a whole id that a Track B design note documents, and no -design note keeps an open "Needs Max's judgment" heading. +Outside the exempt insured-status modules, every mention of a source +conflict in a Track B module is a ``# Source conflict `` marker, the +marker ids are exactly this pass's readings plus those other rounds +register here, each in the G, R2, R3 or grace module, and each id appears +whole in a Track B design note. Each note names its readings and keeps no +open "Needs Max's judgment" heading. """ import re @@ -57,17 +59,31 @@ "us:oasdi-finality-protected-rates-before-coverage", "us:oasdi-sole-full-deduction-zero-dual-composition", } -#: Insured-status modules keep their own register of readings. +#: Exempt: the insured-status modules (#562). The source-conflict rulings +#: log their readings, but no Track B note documents all of them yet. OWN_REGISTERS = {"insured_status.py", "_insured_status_qc.py"} ID = r"us:oasdi-[a-z0-9]+(?:-[a-z0-9]+)*" -MARKER = re.compile(rf"# Source conflict ({ID})") +#: A marker's id ends at a colon, whitespace or the end of the file. +MARKER = re.compile(rf"# Source conflict ({ID})(?=:|\s|$)") +#: A whole id in a note: no id character or hyphen follows it. +DOCUMENTED = re.compile(rf"{ID}(?![A-Za-z0-9_-])") +MENTION = re.compile(r"source\s+conflict", re.IGNORECASE) def _module_markers(): + """Marker ids by module, leaving out ids only exempt modules carry. + + Outside the exempt modules every mention of a source conflict must + parse as a marker, so a wrapped, mistyped or reworded one fails. + """ found = {} for path in sorted(MODULES.glob("*.py")): - for match in MARKER.finditer(path.read_text()): - found.setdefault(match.group(1), set()).add(path.name) + text = path.read_text() + names = MARKER.findall(text) + if path.name not in OWN_REGISTERS: + assert len(MENTION.findall(text)) == len(names), path.name + for name in names: + found.setdefault(name, set()).add(path.name) return { name: files for name, files in found.items() @@ -92,7 +108,7 @@ def test_every_marker_names_a_reading_documented_in_a_design_note(): # Whole ids, so a truncated or misspelled marker cannot match a prefix. documented = set() for path in (ROOT / "docs" / "design").glob("track_b_*.md"): - documented.update(re.findall(ID, path.read_text())) + documented.update(DOCUMENTED.findall(path.read_text())) for name in _module_markers(): assert name in documented, name From db268f9767e81b7550cf4260fb801b72b85839c4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 13:41:52 -0500 Subject: [PATCH 07/13] Round-6 review fixes: any spelling, every package file, whole note ids The cloud round-6 review requested changes on 0d186c71: - The parse rule missed a hyphenated "Source-conflict" (the spelling the notes use) and never read the parameters subpackage, so an undocumented marker passed there. Mentions now match any spelling (source[\W_]*conflict, any case), and the scan covers every Python and YAML file in the Track B package, subpackages included. Files are named by their path inside the package, so only the top-level insured-status modules are exempt. - The check that each note names its readings matched substrings, so a note could name a reading only through a typo. It now uses whole ids. - The note now states where a marker's id ends. Thirty mutations behave as the note says, including all of rounds 5 and 6 plus YAML, subpackage, README and split-line cases. The file still collects 15 tests. Co-Authored-By: Claude Opus 5.5 --- docs/design/track_b_g_gross_benefits.md | 24 +++++++++-------- tests/test_track_b_readings_20261023.py | 36 +++++++++++++++---------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/docs/design/track_b_g_gross_benefits.md b/docs/design/track_b_g_gross_benefits.md index 33c9a7467..7daa1d0a2 100644 --- a/docs/design/track_b_g_gross_benefits.md +++ b/docs/design/track_b_g_gross_benefits.md @@ -3276,18 +3276,20 @@ This pass added the statutory texts it newly relies on (42 U.S.C. (s)) to `lii_usc_402_415_excerpts.txt`, from a re-fetch whose LII body matched the pinned 402 SHA-256 byte for byte. `tests/test_track_b_readings_20261023.py` pins the bookkeeping for every -Track B module except the insured-status ones (`insured_status.py` and +Python and YAML file in the Track B package, subpackages included, except +the insured-status modules (`insured_status.py` and `_insured_status_qc.py`), whose readings the source-conflict rulings log -but no Track B note yet documents in full. In the other modules, every -mention of a source conflict must be a one-line `# Source conflict ` -marker. The marker ids must be exactly this pass's ids plus the ids other -rounds register in that test, each in the G, R2, R3 or grace module, and -each id must appear whole in a Track B design note. This note and the R2 -and R3 notes must name each of their readings and carry no open "Needs -Max's judgment" heading, and each statutory passage the test quotes must -appear verbatim in the capture. The invented conformance lattice is -unchanged; only the `parent_benefit` witness moved to two parents in the -band. +but no Track B note yet documents in full. In the other files, every +mention of a source conflict, in any spelling, must be a one-line +`# Source conflict ` marker whose id ends at a colon, whitespace, a +line break or the end of the file. The marker ids must be exactly this +pass's ids plus the ids other rounds register in that test, each in the +G, R2, R3 or grace module, and each id must appear whole in a Track B +design note. This note and the R2 and R3 notes must name each of their +readings as whole ids and carry no open "Needs Max's judgment" heading, +and each statutory passage the test quotes must appear verbatim in the +capture. The invented conformance lattice is unchanged; only the +`parent_benefit` witness moved to two parents in the band. ## Round-3 input premises and excluded beneficiaries diff --git a/tests/test_track_b_readings_20261023.py b/tests/test_track_b_readings_20261023.py index a6244faa6..7c916dfe5 100644 --- a/tests/test_track_b_readings_20261023.py +++ b/tests/test_track_b_readings_20261023.py @@ -3,11 +3,12 @@ Verification class: statutory conformance and documentation integrity on captured sources only. No population, behavioral or forecast claim. Outside the exempt insured-status modules, every mention of a source -conflict in a Track B module is a ``# Source conflict `` marker, the -marker ids are exactly this pass's readings plus those other rounds -register here, each in the G, R2, R3 or grace module, and each id appears -whole in a Track B design note. Each note names its readings and keeps no -open "Needs Max's judgment" heading. +conflict, in any spelling, in a Python or YAML file of the Track B package +is a ``# Source conflict `` marker. The marker ids are exactly this +pass's readings plus those other rounds register here, each in the G, R2, +R3 or grace module, and each id appears whole in a Track B design note. +Each note names its readings as whole ids and keeps no open "Needs Max's +judgment" heading. """ import re @@ -67,23 +68,29 @@ MARKER = re.compile(rf"# Source conflict ({ID})(?=:|\s|$)") #: A whole id in a note: no id character or hyphen follows it. DOCUMENTED = re.compile(rf"{ID}(?![A-Za-z0-9_-])") -MENTION = re.compile(r"source\s+conflict", re.IGNORECASE) +#: Any spelling: "source conflict", "Source-conflict", "source_conflict". +MENTION = re.compile(r"source[\W_]*conflict", re.IGNORECASE) +#: Files that can carry a marker, subpackages included. +SOURCES = {".py", ".yaml", ".yml"} def _module_markers(): - """Marker ids by module, leaving out ids only exempt modules carry. + """Marker ids by file, leaving out ids only exempt modules carry. Outside the exempt modules every mention of a source conflict must parse as a marker, so a wrapped, mistyped or reworded one fails. """ found = {} - for path in sorted(MODULES.glob("*.py")): + for path in sorted(MODULES.rglob("*")): + if path.suffix not in SOURCES or not path.is_file(): + continue + name = path.relative_to(MODULES).as_posix() text = path.read_text() - names = MARKER.findall(text) - if path.name not in OWN_REGISTERS: - assert len(MENTION.findall(text)) == len(names), path.name - for name in names: - found.setdefault(name, set()).add(path.name) + ids = MARKER.findall(text) + if name not in OWN_REGISTERS: + assert len(MENTION.findall(text)) == len(ids), name + for found_id in ids: + found.setdefault(found_id, set()).add(name) return { name: files for name, files in found.items() @@ -117,8 +124,9 @@ def test_every_marker_names_a_reading_documented_in_a_design_note(): def test_each_note_names_its_readings_and_has_no_open_judgment_heading(note): text = NOTES[note].read_text() assert not re.search(r"^#+ Needs Max's judgment", text, re.MULTILINE) + named = set(DOCUMENTED.findall(text)) for name in READINGS[note]: - assert name in text, (note, name) + assert name in named, (note, name) STATUTE_QUOTES = ( From 079c33dae15c2a6693b3132e451567ecdcfd00fc Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 13:44:03 -0500 Subject: [PATCH 08/13] Count "Sources conflict" as a mention too A plural "# Sources conflict us:oasdi-..." still slipped past the parse rule. The mention pattern is now sources?[\W_]*conflict (any case). On this head, #562's 168bb3a8 and #563's dbdceac5, every mention in a non-exempt Python or YAML file still parses as a marker; the only mismatch is #562's two wrapped markers in the exempt insured_status.py. All 32 mutations behave as the note says. Co-Authored-By: Claude Opus 5.5 --- tests/test_track_b_readings_20261023.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_track_b_readings_20261023.py b/tests/test_track_b_readings_20261023.py index 7c916dfe5..27b463fd7 100644 --- a/tests/test_track_b_readings_20261023.py +++ b/tests/test_track_b_readings_20261023.py @@ -68,8 +68,8 @@ MARKER = re.compile(rf"# Source conflict ({ID})(?=:|\s|$)") #: A whole id in a note: no id character or hyphen follows it. DOCUMENTED = re.compile(rf"{ID}(?![A-Za-z0-9_-])") -#: Any spelling: "source conflict", "Source-conflict", "source_conflict". -MENTION = re.compile(r"source[\W_]*conflict", re.IGNORECASE) +#: Any spelling: "source conflict", "Source-conflict", "Sources conflict". +MENTION = re.compile(r"sources?[\W_]*conflict", re.IGNORECASE) #: Files that can carry a marker, subpackages included. SOURCES = {".py", ".yaml", ".yml"} From 5853c77ea92be122ef1db61a50ae5c945f33d806 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 15:42:17 -0500 Subject: [PATCH 09/13] Round-7 review fixes: check reading ids by their own text - STRAYS replaces the mention count. Once its markers are removed, a scanned file may hold no "source(s)" joined to "conflict" by whitespace, #, - or _, no "us" joined to "oasdi" in any case or separator, and no id without its "us:". - The scan reads .py, .pyi, .yaml and .yml files with the suffix in any case, as UTF-8. - The insured-status modules keep their own register and test (#562), so the scan skips them outright. - DOCUMENTED gets a left boundary. - Heading lines, code blocks included, after removing leading whitespace, > and list markers: - ATX lines; - lines that open with ** or __; - HTML h1 to h6, closed or not; - setext runs. The test compares their text after decoding entities and removing markup. - The G note states each rule. Co-Authored-By: Claude Opus 5.5 --- docs/design/track_b_g_gross_benefits.md | 71 ++++++++--- tests/test_track_b_readings_20261023.py | 154 ++++++++++++++++++------ 2 files changed, 177 insertions(+), 48 deletions(-) diff --git a/docs/design/track_b_g_gross_benefits.md b/docs/design/track_b_g_gross_benefits.md index 7daa1d0a2..5c0bdee7e 100644 --- a/docs/design/track_b_g_gross_benefits.md +++ b/docs/design/track_b_g_gross_benefits.md @@ -3275,20 +3275,63 @@ This pass added the statutory texts it newly relies on (42 U.S.C. 402(d)(1)(D), (d)(5), (e)(1), (h)(1)(D), (h)(2), (k)(3)(A), (k)(4) and (s)) to `lii_usc_402_415_excerpts.txt`, from a re-fetch whose LII body matched the pinned 402 SHA-256 byte for byte. -`tests/test_track_b_readings_20261023.py` pins the bookkeeping for every -Python and YAML file in the Track B package, subpackages included, except -the insured-status modules (`insured_status.py` and -`_insured_status_qc.py`), whose readings the source-conflict rulings log -but no Track B note yet documents in full. In the other files, every -mention of a source conflict, in any spelling, must be a one-line -`# Source conflict ` marker whose id ends at a colon, whitespace, a -line break or the end of the file. The marker ids must be exactly this -pass's ids plus the ids other rounds register in that test, each in the -G, R2, R3 or grace module, and each id must appear whole in a Track B -design note. This note and the R2 and R3 notes must name each of their -readings as whole ids and carry no open "Needs Max's judgment" heading, -and each statutory passage the test quotes must appear verbatim in the -capture. The invented conformance lattice is unchanged; only the +`tests/test_track_b_readings_20261023.py` pins the bookkeeping: + +- **Files it reads.** Every file in the Track B package, subpackages + included, whose suffix in any case is `.py`, `.pyi`, `.yaml` or `.yml`, + except the two insured-status modules that #562 adds at the top of the + package (`insured_status.py` and `_insured_status_qc.py`). Those keep + their own register of readings, with a test of its own, and this test + does not read them. +- **Markers.** Code logs a reading only as the text + `# Source conflict `, with exactly that spelling, case and spacing, + where the id ends at a colon, whitespace or the end of the file. With + those markers removed, a file the test reads may not contain, in any + case: + - "source" or "sources", with no letter just before it, then + "conflict", with nothing but whitespace, `#`, `-` or `_` between; + - "us", with no letter or digit just before it, then "oasdi", with at + most three colons, whitespace characters, hyphens or underscores + between; + - "oasdi", with no letter or digit just before it, then at least two + words of letters and digits, each after a hyphen. + + So a wrapped, reworded, recased or misspelled marker fails while any of + these survive in it. Wordings with none of them, such as "conflicting + sources" with no id, pass. +- **Ids.** The marker ids must be exactly this pass's ids plus the ids + other rounds register in that test, each in the G, R2, R3 or grace + module. Each must appear in a Track B design note as a whole token, + with no letter, digit, underscore or hyphen on either side, and this + note and the R2 and R3 notes must name each of their own readings that + way. +- **Headings.** None of those three notes may have a heading line whose + text starts with "Needs Max's judgment". In every line, code blocks + included, the test first removes leading whitespace, `>` characters + and list markers with a space or tab and more text after them. It then + reads these as heading lines: + - the text after the `#` run of a line that starts with one to six `#` + and then a space, a tab or the end of the line; + - a line that starts with `**` or `__`; + - the text after each `

` to `

` opening tag, up to its closing + tag or the end of the note; + - for each line of only `=` or only `-` characters, the text from each + line of the non-blank run directly above it down to that line. + + It decodes HTML entities; removes HTML tags, `*`, `_`, backticks, + backslashes, square brackets, soft hyphens and the zero-width + characters U+200B to U+200D, U+2060 and U+FEFF; and skips any + characters before the first letter A to Z. It then matches in any case, + with any whitespace between the words, a straight apostrophe or U+2018, + U+2019 or U+02BC, and "judgment" or "judgement". These rules can flag + lines that CommonMark would not read as headings, such as lines in code + blocks. +- **Quotes.** Each statutory passage the test quotes must appear in the + capture verbatim up to whitespace. The capture helper also reads + zero-width and non-breaking spaces as spaces and curly apostrophes as + straight ones. + +The invented conformance lattice stays the same; only the `parent_benefit` witness moved to two parents in the band. ## Round-3 input premises and excluded beneficiaries diff --git a/tests/test_track_b_readings_20261023.py b/tests/test_track_b_readings_20261023.py index 27b463fd7..4659a3a8b 100644 --- a/tests/test_track_b_readings_20261023.py +++ b/tests/test_track_b_readings_20261023.py @@ -2,15 +2,20 @@ Verification class: statutory conformance and documentation integrity on captured sources only. No population, behavioral or forecast claim. -Outside the exempt insured-status modules, every mention of a source -conflict, in any spelling, in a Python or YAML file of the Track B package -is a ``# Source conflict `` marker. The marker ids are exactly this -pass's readings plus those other rounds register here, each in the G, R2, -R3 or grace module, and each id appears whole in a Track B design note. -Each note names its readings as whole ids and keeps no open "Needs Max's -judgment" heading. +Markers: the scan reads every file in the Track B package, subpackages +included, whose suffix in any case is in ``SOURCES``, except the +``OWN_REGISTERS`` at the top of the package. With its markers removed, a +scanned file must hold nothing ``STRAYS`` matches. The ``MARKER`` ids are +exactly ``READINGS`` plus ``OTHER_ROUNDS``, each in the G, R2, R3 or grace +module, and each is ``DOCUMENTED`` in a Track B design note. Notes: G, R2 +and R3 name their ``READINGS`` the same way, and none has a heading line, +as ``_headings`` reads them, that ``JUDGMENT`` matches once ``_plain`` +cleans it. Quotes: each new statutory excerpt appears in its capture as +``source_text`` normalizes it, with each run of whitespace read as one +space. """ +import html import re from pathlib import Path @@ -60,42 +65,123 @@ "us:oasdi-finality-protected-rates-before-coverage", "us:oasdi-sole-full-deduction-zero-dual-composition", } -#: Exempt: the insured-status modules (#562). The source-conflict rulings -#: log their readings, but no Track B note documents all of them yet. +#: Exempt, by path inside the package: the insured-status modules that +#: #562 adds. They keep their own register of readings, with a test of its +#: own, and this scan does not read them. OWN_REGISTERS = {"insured_status.py", "_insured_status_qc.py"} +#: Scanned suffixes, compared in lower case. +SOURCES = {".py", ".pyi", ".yaml", ".yml"} ID = r"us:oasdi-[a-z0-9]+(?:-[a-z0-9]+)*" -#: A marker's id ends at a colon, whitespace or the end of the file. +#: The one marker form, with exactly this spelling, case and spacing. Its +#: id ends at a colon, whitespace or the end of the file. MARKER = re.compile(rf"# Source conflict ({ID})(?=:|\s|$)") -#: A whole id in a note: no id character or hyphen follows it. -DOCUMENTED = re.compile(rf"{ID}(?![A-Za-z0-9_-])") -#: Any spelling: "source conflict", "Source-conflict", "Sources conflict". -MENTION = re.compile(r"sources?[\W_]*conflict", re.IGNORECASE) -#: Files that can carry a marker, subpackages included. -SOURCES = {".py", ".yaml", ".yml"} +#: What a scanned file may hold only inside a marker, in any case. +STRAYS = ( + # "source" or "sources", with no letter just before it, then + # "conflict", with nothing but whitespace, "#", "-" or "_" between. + re.compile(r"(?" characters, and list markers with a space or +#: tab and more text after them. +CONTAINERS = re.compile( + r"(?:[ \t]*(?:>|(?:[-+*]|\d{1,9}[.)])(?=[ \t]+\S)))*[ \t]*" +) +#: One to six "#" and then a space, a tab or the end of the line. +ATX = re.compile(r"#{1,6}(?:[ \t](.*))?") +#: A line of only "=" or only "-" characters, with spaces or tabs after. +UNDERLINE = re.compile(r"(?:=+|-+)[ \t]*") +#: The text after each h1 to h6 opening tag, up to its closing tag or the +#: end of the note. The lookahead lets an unclosed tag's text overlap the +#: next heading's. +HTML_HEADING = re.compile( + r"]*>(?=(.*?)(?:|\Z))", + re.IGNORECASE | re.DOTALL, +) +#: What a heading loses before the comparison: HTML tags, "*", "_", +#: backticks, backslashes, square brackets, soft hyphens and the +#: zero-width characters U+200B to U+200D, U+2060 and U+FEFF. +MARKUP = re.compile(r"<[^>]+>|[*_`\\\[\]\u00ad\u200b-\u200d\u2060\ufeff]") +#: "Needs Max's judgment" after any characters other than the letters A +#: to Z, in any case, with any whitespace between the words, a straight +#: apostrophe or U+2018, U+2019 or U+02BC, and "judgment" or "judgement". +JUDGMENT = re.compile( + r"[^A-Za-z]*needs\s+max['\u2018\u2019\u02bc]s\s+judge?ment", + re.IGNORECASE, +) def _module_markers(): - """Marker ids by file, leaving out ids only exempt modules carry. + """Marker ids by file, across the files the scan reads. - Outside the exempt modules every mention of a source conflict must - parse as a marker, so a wrapped, mistyped or reworded one fails. + With its markers removed, a scanned file must hold nothing ``STRAYS`` + matches, so a wrapped, reworded, recased or misspelled marker fails + while its id, its id's words or its "source conflict" survive. """ found = {} for path in sorted(MODULES.rglob("*")): - if path.suffix not in SOURCES or not path.is_file(): - continue name = path.relative_to(MODULES).as_posix() - text = path.read_text() - ids = MARKER.findall(text) - if name not in OWN_REGISTERS: - assert len(MENTION.findall(text)) == len(ids), name - for found_id in ids: + if ( + name in OWN_REGISTERS + or path.suffix.lower() not in SOURCES + or not path.is_file() + ): + continue + text = path.read_text(encoding="utf-8") + rest = MARKER.sub("", text) + strays = [m.group() for p in STRAYS for m in p.finditer(rest)] + assert not strays, (name, strays) + for found_id in MARKER.findall(text): found.setdefault(found_id, set()).add(name) - return { - name: files - for name, files in found.items() - if not files <= OWN_REGISTERS - } + return found + + +def _headings(text): + """Heading lines in a note, code blocks included. + + With ``CONTAINERS`` removed from each line, these are heading lines: + the text after the "#" run of a line ``ATX`` matches; a line that + starts with "**" or "__"; each ``HTML_HEADING``; and, for each line + ``UNDERLINE`` matches, the text from each line of the non-blank run + directly above it down to that line. + """ + lines = [ + line[CONTAINERS.match(line).end() :] for line in text.splitlines() + ] + found = [m.group(2) for m in HTML_HEADING.finditer("\n".join(lines))] + run = [] + for line in lines: + if not line: + run = [] + continue + if UNDERLINE.fullmatch(line): + found += [" ".join(run[i:]) for i in range(len(run))] + elif atx := ATX.fullmatch(line): + found.append(atx.group(1) or "") + elif line.startswith(("**", "__")): + found.append(line) + run.append(line) + return found + + +def _plain(heading): + """A heading's text with HTML entities decoded and ``MARKUP`` gone.""" + return MARKUP.sub("", html.unescape(heading)) + + +def _open_judgment_headings(text): + return [h for h in _headings(text) if JUDGMENT.match(_plain(h))] def test_markers_and_logged_readings_match_in_both_directions(): @@ -115,15 +201,15 @@ def test_every_marker_names_a_reading_documented_in_a_design_note(): # Whole ids, so a truncated or misspelled marker cannot match a prefix. documented = set() for path in (ROOT / "docs" / "design").glob("track_b_*.md"): - documented.update(DOCUMENTED.findall(path.read_text())) + documented.update(DOCUMENTED.findall(path.read_text(encoding="utf-8"))) for name in _module_markers(): assert name in documented, name @pytest.mark.parametrize("note", sorted(NOTES)) def test_each_note_names_its_readings_and_has_no_open_judgment_heading(note): - text = NOTES[note].read_text() - assert not re.search(r"^#+ Needs Max's judgment", text, re.MULTILINE) + text = NOTES[note].read_text(encoding="utf-8") + assert not _open_judgment_headings(text), note named = set(DOCUMENTED.findall(text)) for name in READINGS[note]: assert name in named, (note, name) From dfde9e1a3d72ba4377b24be51d804c59583e692d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 16:29:33 -0500 Subject: [PATCH 10/13] Round-8 review fixes: check note openings by their letters The notes check now reads every opening, not only headings: a non-blank line continues the line above it only when both start with a letter A to Z after the same leading whitespace and neither holds ">" or "|"; every other line opens, through the end of its non-blank run. The text after each h1 to h6 tag counts too, with the tag read as ending at its first ">" and again at its first ">" outside quotes (round 8, finding 1). An opening fails when its letters A to Z, after entity decoding, NFKD and lower-casing, start with "needsmaxsjudgment" or "needsmaxsjudgement", in either of two readings: with HTML tags and Markdown link targets removed, and as written. Punctuation, apostrophes, digits, whitespace, markup and invisible characters no longer hide the phrase (finding 7). The note states what still can: a letter a reading keeps, or a look-alike letter from another script. The stray rules count only A to Z and 0 to 9 as letters and digits, even under IGNORECASE. The note gives the id grammar, says files and notes are read as UTF-8, and says whose whitespace the quote check collapses (findings 2 to 6). 170 mutations behave as the note says. Co-Authored-By: Claude Opus 5.5 --- docs/design/track_b_g_gross_benefits.md | 73 ++++++----- tests/test_track_b_readings_20261023.py | 167 ++++++++++++++---------- 2 files changed, 136 insertions(+), 104 deletions(-) diff --git a/docs/design/track_b_g_gross_benefits.md b/docs/design/track_b_g_gross_benefits.md index 5c0bdee7e..29bfd5952 100644 --- a/docs/design/track_b_g_gross_benefits.md +++ b/docs/design/track_b_g_gross_benefits.md @@ -3282,12 +3282,15 @@ matched the pinned 402 SHA-256 byte for byte. except the two insured-status modules that #562 adds at the top of the package (`insured_status.py` and `_insured_status_qc.py`). Those keep their own register of readings, with a test of its own, and this test - does not read them. + does not read them. It reads these files and the design notes as + UTF-8, so a file that doesn't decode fails. - **Markers.** Code logs a reading only as the text - `# Source conflict `, with exactly that spelling, case and spacing, - where the id ends at a colon, whitespace or the end of the file. With - those markers removed, a file the test reads may not contain, in any - case: + `# Source conflict `, with exactly that spelling, case and spacing. + An id is `us:oasdi-` and then one or more words of the letters a to z + and the digits 0 to 9, joined by single hyphens, and it ends at a + colon, whitespace or the end of the file. With those markers removed, a + file the test reads may not contain any of the following, in any case, + where a letter is one of A to Z and a digit one of 0 to 9: - "source" or "sources", with no letter just before it, then "conflict", with nothing but whitespace, `#`, `-` or `_` between; - "us", with no letter or digit just before it, then "oasdi", with at @@ -3302,34 +3305,40 @@ matched the pinned 402 SHA-256 byte for byte. - **Ids.** The marker ids must be exactly this pass's ids plus the ids other rounds register in that test, each in the G, R2, R3 or grace module. Each must appear in a Track B design note as a whole token, - with no letter, digit, underscore or hyphen on either side, and this - note and the R2 and R3 notes must name each of their own readings that - way. -- **Headings.** None of those three notes may have a heading line whose - text starts with "Needs Max's judgment". In every line, code blocks - included, the test first removes leading whitespace, `>` characters - and list markers with a space or tab and more text after them. It then - reads these as heading lines: - - the text after the `#` run of a line that starts with one to six `#` - and then a space, a tab or the end of the line; - - a line that starts with `**` or `__`; - - the text after each `

` to `

` opening tag, up to its closing - tag or the end of the note; - - for each line of only `=` or only `-` characters, the text from each - line of the non-blank run directly above it down to that line. - - It decodes HTML entities; removes HTML tags, `*`, `_`, backticks, - backslashes, square brackets, soft hyphens and the zero-width - characters U+200B to U+200D, U+2060 and U+FEFF; and skips any - characters before the first letter A to Z. It then matches in any case, - with any whitespace between the words, a straight apostrophe or U+2018, - U+2019 or U+02BC, and "judgment" or "judgement". These rules can flag - lines that CommonMark would not read as headings, such as lines in code - blocks. + with no letter A to Z, digit 0 to 9, underscore or hyphen on either + side, and this note and the R2 and R3 notes must name each of their own + readings that way. +- **Openings.** None of those three notes may open a heading, paragraph, + list item, table row or other block with "Needs Max's judgment". The + test reads every line, code blocks included. A non-blank line + continues the line above it only when both start with a letter A to Z + after the same leading whitespace and neither holds `>` or `|`. Every + other non-blank line opens, so a line after a blank line, or one that + starts with markup such as `#`, `*`, `-`, `>`, `|` or `<`, always opens. + What a line opens runs to the end of its run of non-blank lines. The + text after each `

` to `

` opening tag, up to its closing tag or + the end of the note, also counts as an opening. The test reads that + tag as ending at its first `>`, and again as ending at its first `>` + outside quotes. + + The test decodes HTML entities in each opening and reads it twice: + once with HTML tags and Markdown link targets (a `(...)` or `[...]` + right after a `]`) removed, and once as written. Each reading applies + Unicode compatibility decomposition (NFKD) and keeps only the letters + A to Z, in lower case. An opening fails when either reading starts + with "needsmaxsjudgment" or "needsmaxsjudgement". So punctuation, + apostrophes of any kind, digits, whitespace, markup and invisible + characters can't hide the phrase. Only letters can: a letter A to Z + that a reading keeps, including one that a character such as U+2122 + (the trademark sign) decomposes to, or a letter from another script + that looks like a Latin one, such as the Cyrillic U+0435, which the + readings drop. These rules can flag lines that CommonMark would not + read as opening a block, such as lines in code blocks. - **Quotes.** Each statutory passage the test quotes must appear in the - capture verbatim up to whitespace. The capture helper also reads - zero-width and non-breaking spaces as spaces and curly apostrophes as - straight ones. + capture verbatim, once each run of whitespace in the capture reads as + one space. Before that, the capture helper reads zero-width spaces + (U+200B) and non-breaking spaces as spaces, and U+2019 as a straight + apostrophe. The invented conformance lattice stays the same; only the `parent_benefit` witness moved to two parents in the band. diff --git a/tests/test_track_b_readings_20261023.py b/tests/test_track_b_readings_20261023.py index 4659a3a8b..bc46306a2 100644 --- a/tests/test_track_b_readings_20261023.py +++ b/tests/test_track_b_readings_20261023.py @@ -2,21 +2,22 @@ Verification class: statutory conformance and documentation integrity on captured sources only. No population, behavioral or forecast claim. -Markers: the scan reads every file in the Track B package, subpackages -included, whose suffix in any case is in ``SOURCES``, except the -``OWN_REGISTERS`` at the top of the package. With its markers removed, a -scanned file must hold nothing ``STRAYS`` matches. The ``MARKER`` ids are -exactly ``READINGS`` plus ``OTHER_ROUNDS``, each in the G, R2, R3 or grace -module, and each is ``DOCUMENTED`` in a Track B design note. Notes: G, R2 -and R3 name their ``READINGS`` the same way, and none has a heading line, -as ``_headings`` reads them, that ``JUDGMENT`` matches once ``_plain`` -cleans it. Quotes: each new statutory excerpt appears in its capture as -``source_text`` normalizes it, with each run of whitespace read as one -space. +Markers: the scan reads, as UTF-8, every file in the Track B package, +subpackages included, whose suffix in any case is in ``SOURCES``, except +the ``OWN_REGISTERS`` at the top of the package. With its markers +removed, a scanned file must hold nothing ``STRAYS`` matches. The +``MARKER`` ids are exactly ``READINGS`` plus ``OTHER_ROUNDS``, each in the +G, R2, R3 or grace module, and each is ``DOCUMENTED`` in a Track B design +note. Notes: G, R2 and R3 name their ``READINGS`` the same way, and none +has an opening, as ``_openings`` reads them, whose ``_letters`` start +with ``JUDGMENT``. Quotes: each new statutory excerpt appears in its +capture as ``source_text`` normalizes it, with each run of whitespace in +the capture read as one space. """ import html import re +import unicodedata from pathlib import Path import pytest @@ -71,63 +72,62 @@ OWN_REGISTERS = {"insured_status.py", "_insured_status_qc.py"} #: Scanned suffixes, compared in lower case. SOURCES = {".py", ".pyi", ".yaml", ".yml"} +#: "us:oasdi-" and then one or more words of the letters a to z and the +#: digits 0 to 9, joined by single hyphens. ID = r"us:oasdi-[a-z0-9]+(?:-[a-z0-9]+)*" #: The one marker form, with exactly this spelling, case and spacing. Its #: id ends at a colon, whitespace or the end of the file. MARKER = re.compile(rf"# Source conflict ({ID})(?=:|\s|$)") -#: What a scanned file may hold only inside a marker, in any case. +#: What a scanned file may hold only inside a marker, in any case. A +#: letter here is one of A to Z, and a digit one of 0 to 9. STRAYS = ( # "source" or "sources", with no letter just before it, then # "conflict", with nothing but whitespace, "#", "-" or "_" between. - re.compile(r"(?" characters, and list markers with a space or -#: tab and more text after them. -CONTAINERS = re.compile( - r"(?:[ \t]*(?:>|(?:[-+*]|\d{1,9}[.)])(?=[ \t]+\S)))*[ \t]*" -) -#: One to six "#" and then a space, a tab or the end of the line. -ATX = re.compile(r"#{1,6}(?:[ \t](.*))?") -#: A line of only "=" or only "-" characters, with spaces or tabs after. -UNDERLINE = re.compile(r"(?:=+|-+)[ \t]*") +#: Leading whitespace and then a letter A to Z. +LETTER = re.compile(r"(\s*)[A-Za-z]") #: The text after each h1 to h6 opening tag, up to its closing tag or the -#: end of the note. The lookahead lets an unclosed tag's text overlap the -#: next heading's. -HTML_HEADING = re.compile( - r"]*>(?=(.*?)(?:|\Z))", - re.IGNORECASE | re.DOTALL, -) -#: What a heading loses before the comparison: HTML tags, "*", "_", -#: backticks, backslashes, square brackets, soft hyphens and the -#: zero-width characters U+200B to U+200D, U+2060 and U+FEFF. -MARKUP = re.compile(r"<[^>]+>|[*_`\\\[\]\u00ad\u200b-\u200d\u2060\ufeff]") -#: "Needs Max's judgment" after any characters other than the letters A -#: to Z, in any case, with any whitespace between the words, a straight -#: apostrophe or U+2018, U+2019 or U+02BC, and "judgment" or "judgement". -JUDGMENT = re.compile( - r"[^A-Za-z]*needs\s+max['\u2018\u2019\u02bc]s\s+judge?ment", - re.IGNORECASE, +#: end of the note: once with the tag ending at its first ">", and once +#: with it ending at its first ">" outside quotes. The lookahead lets an +#: unclosed tag's text overlap the next heading's. +HTML_HEADINGS = tuple( + re.compile( + rf"(?=(.*?)(?:|\Z))", + re.IGNORECASE | re.DOTALL, + ) + for tag in (r"[^>]*", r"""(?:[^>"']|"[^"]*"|'[^']*')*""") ) +#: What the first of the two ``_letters`` readings removes: HTML tags, +#: each up to its first ">", and Markdown link targets, a "(...)" or +#: "[...]" right after a "]". +HIDDEN = re.compile(r"<[^>]+>|\](?:\([^)]*\)|\[[^\]]*\])") +#: How the letters of an open judgment item start: "Needs Max's judgment", +#: or "judgement", as ``_letters`` reads it. +JUDGMENT = ("needsmaxsjudgment", "needsmaxsjudgement") def _module_markers(): """Marker ids by file, across the files the scan reads. With its markers removed, a scanned file must hold nothing ``STRAYS`` - matches, so a wrapped, reworded, recased or misspelled marker fails - while its id, its id's words or its "source conflict" survive. + matches. So a wrapped, reworded, recased or misspelled marker trips + this check while some of its text still matches ``STRAYS``. """ found = {} for path in sorted(MODULES.rglob("*")): @@ -147,41 +147,64 @@ def _module_markers(): return found -def _headings(text): - """Heading lines in a note, code blocks included. +def _continues(above, line): + """Whether a line continues the one above it instead of opening. - With ``CONTAINERS`` removed from each line, these are heading lines: - the text after the "#" run of a line ``ATX`` matches; a line that - starts with "**" or "__"; each ``HTML_HEADING``; and, for each line - ``UNDERLINE`` matches, the text from each line of the non-blank run - directly above it down to that line. + Both must start with a letter A to Z after the same leading + whitespace (``LETTER``), and neither may hold ">" or "|". """ - lines = [ - line[CONTAINERS.match(line).end() :] for line in text.splitlines() - ] - found = [m.group(2) for m in HTML_HEADING.finditer("\n".join(lines))] + first, second = LETTER.match(above), LETTER.match(line) + return bool( + first + and second + and first.group(1) == second.group(1) + and not {">", "|"} & set(above + line) + ) + + +def _openings(text): + """What the lines of a note open, code blocks included. + + Every non-blank line opens unless it ``_continues`` the line above + it, and what it opens runs to the end of its run of non-blank lines. + The text after each ``HTML_HEADINGS`` opening tag counts too. + """ + found = [m.group(2) for p in HTML_HEADINGS for m in p.finditer(text)] run = [] - for line in lines: - if not line: - run = [] + for line in [*text.splitlines(), ""]: + if line.strip(): + run.append(line) continue - if UNDERLINE.fullmatch(line): - found += [" ".join(run[i:]) for i in range(len(run))] - elif atx := ATX.fullmatch(line): - found.append(atx.group(1) or "") - elif line.startswith(("**", "__")): - found.append(line) - run.append(line) + found += [ + " ".join(run[k:]) + for k in range(len(run)) + if not (k and _continues(run[k - 1], run[k])) + ] + run = [] return found -def _plain(heading): - """A heading's text with HTML entities decoded and ``MARKUP`` gone.""" - return MARKUP.sub("", html.unescape(heading)) +def _letters(text): + """The letters of a text, read two ways. + It decodes HTML entities, then reads the text once with ``HIDDEN`` + removed and once as written. Each reading applies Unicode + compatibility decomposition (NFKD) and keeps only the letters A to Z, + in lower case. + """ + text = html.unescape(text) + return [ + "".join(re.findall("[a-z]", unicodedata.normalize("NFKD", t).lower())) + for t in (HIDDEN.sub("", text), text) + ] -def _open_judgment_headings(text): - return [h for h in _headings(text) if JUDGMENT.match(_plain(h))] + +def _open_judgment_items(text): + return [ + opening + for opening in _openings(text) + if any(read.startswith(JUDGMENT) for read in _letters(opening)) + ] def test_markers_and_logged_readings_match_in_both_directions(): @@ -207,9 +230,9 @@ def test_every_marker_names_a_reading_documented_in_a_design_note(): @pytest.mark.parametrize("note", sorted(NOTES)) -def test_each_note_names_its_readings_and_has_no_open_judgment_heading(note): +def test_each_note_names_its_readings_and_opens_no_judgment_item(note): text = NOTES[note].read_text(encoding="utf-8") - assert not _open_judgment_headings(text), note + assert not _open_judgment_items(text), note named = set(DOCUMENTED.findall(text)) for name in READINGS[note]: assert name in named, (note, name) From ec4ae312806f840858e11af40b3799d98dbfcb90 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 6 Nov 2026 05:48:22 -0500 Subject: [PATCH 11/13] Guard the readings test's reads to Track B paths A blind role should be able to run this module without reaching a restricted file. A new test runs the other checks with Path.open and Path's listings patched: they may list only the Track B package and docs/design/track_b_*.md, and open only those files and the statute captures. It also checks that each patch is live and that each check reads what it should. The note states what the guard does not patch. Adds one unit test: unit 22,880, total 29,479. Co-Authored-By: Claude Opus 5.5 --- docs/design/track_b_g_gross_benefits.md | 8 +++ tests/README-tiers.md | 4 +- tests/test_track_b_readings_20261023.py | 85 ++++++++++++++++++++++++- tests/tier_counts.json | 2 +- 4 files changed, 95 insertions(+), 4 deletions(-) diff --git a/docs/design/track_b_g_gross_benefits.md b/docs/design/track_b_g_gross_benefits.md index 29bfd5952..bc66506e7 100644 --- a/docs/design/track_b_g_gross_benefits.md +++ b/docs/design/track_b_g_gross_benefits.md @@ -3339,6 +3339,14 @@ matched the pinned 402 SHA-256 byte for byte. one space. Before that, the capture helper reads zero-width spaces (U+200B) and non-breaking spaces as spaces, and U+2019 as a straight apostrophe. +- **Reads.** So that a blind role can run this module without reaching a + restricted file, `test_the_checks_list_and_open_only_track_b_paths` + runs the other checks with `Path.open` and `Path`'s listings patched. + The checks may list only the Track B package and + `docs/design/track_b_*.md`, and open only those files and the captures + under `tests/data/track_b/gross_benefit_sources/`. The guard patches + nothing else, so a direct `open()` or `os` listing would pass it. The + module and the capture helper use neither. The invented conformance lattice stays the same; only the `parent_benefit` witness moved to two parents in the band. diff --git a/tests/README-tiers.md b/tests/README-tiers.md index 9f1d36b7a..ce0d912f7 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` | 22,879 | +| `unit` | 22,880 | | `artifact` | 4,271 | | `integration_psid` | 1,471 | | `reproduction_legacy` | 520 | | `oracle_policyengine` | 337 | -| **Total** | **29,478** | +| **Total** | **29,479** | diff --git a/tests/test_track_b_readings_20261023.py b/tests/test_track_b_readings_20261023.py index bc46306a2..d7c1f5ce3 100644 --- a/tests/test_track_b_readings_20261023.py +++ b/tests/test_track_b_readings_20261023.py @@ -12,7 +12,9 @@ has an opening, as ``_openings`` reads them, whose ``_letters`` start with ``JUDGMENT``. Quotes: each new statutory excerpt appears in its capture as ``source_text`` normalizes it, with each run of whitespace in -the capture read as one space. +the capture read as one space. Reads: those checks list and open only the +Track B package, the notes named ``track_b_*.md`` and the statute captures, +and a guard test patches ``Path`` to fail on anything else. """ import html @@ -268,3 +270,84 @@ def test_each_note_names_its_readings_and_opens_no_judgment_item(note): @pytest.mark.parametrize("quote", STATUTE_QUOTES) def test_new_statutory_excerpts_are_quoted_verbatim(quote): assert quote in " ".join(source_text("lii_usc_402_415").split()) + + +def test_the_checks_list_and_open_only_track_b_paths(monkeypatch): + # So that a blind role can run this module without reaching a restricted + # file, these checks may list only the package and the notes named + # track_b_*.md, and open only those files and the statute captures. The + # guard checks each Path listing and open before it runs, and it spells + # out its paths so that a change to the constants above fails it. + package = (ROOT / "src/populace_dynamics/track_b").resolve() + design = (ROOT / "docs/design").resolve() + captures = (ROOT / "tests/data/track_b/gross_benefit_sources").resolve() + listings = {(package, "*"), (package, "**/*"), (design, "track_b_*.md")} + path_open, glob, rglob = Path.open, Path.glob, Path.rglob + opened = [] + + def checked_open(path, *args, **kwargs): + resolved = path.resolve() + assert ( + resolved.is_relative_to(package) + or resolved.is_relative_to(captures) + or (resolved.parent == design and resolved.match("track_b_*.md")) + ), resolved + opened.append(resolved) + return path_open(path, *args, **kwargs) + + def checked(listing): + def checked_listing(path, pattern, *args, **kwargs): + assert (path.resolve(), pattern) in listings, (path, pattern) + return listing(path, pattern, *args, **kwargs) + + return checked_listing + + def unchecked(path, *args, **kwargs): + raise AssertionError(f"listing of {path}") + + monkeypatch.setattr(Path, "open", checked_open) + monkeypatch.setattr(Path, "glob", checked(glob)) + monkeypatch.setattr(Path, "rglob", checked(rglob)) + monkeypatch.setattr(Path, "iterdir", unchecked) + monkeypatch.setattr(Path, "walk", unchecked, raising=False) + for reach in ( + lambda: (ROOT / "pyproject.toml").open(), + lambda: (design / "other.md").open(), + lambda: design.glob("*.md"), + lambda: design.rglob("track_b_*.md"), + lambda: package.parent.rglob("*"), + lambda: package.iterdir(), + lambda: package.walk(), + ): + with pytest.raises(AssertionError): + reach() + + def reads(check, *arguments): + opened.clear() + check(*arguments) + return set(opened) + + notes = {path.resolve() for path in NOTES.values()} + assert package / "gross_benefits.py" in reads( + test_markers_and_logged_readings_match_in_both_directions + ) + assert notes <= reads( + test_every_marker_names_a_reading_documented_in_a_design_note + ) + assert notes == set().union( + *( + reads( + test_each_note_names_its_readings_and_opens_no_judgment_item, + note, + ) + for note in NOTES + ) + ) + source_text.cache_clear() + assert {captures} == { + path.parent + for path in reads( + test_new_statutory_excerpts_are_quoted_verbatim, + STATUTE_QUOTES[0], + ) + } diff --git a/tests/tier_counts.json b/tests/tier_counts.json index bb7aec5fd..f0b27c104 100644 --- a/tests/tier_counts.json +++ b/tests/tier_counts.json @@ -1,7 +1,7 @@ { "schema_version": 1, "counts": { - "unit": 22879, + "unit": 22880, "artifact": 4271, "integration_psid": 1471, "reproduction_legacy": 520, From 5ed8adc84463f5303dea2b38fd03108c9aedcde7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 6 Nov 2026 06:05:52 -0500 Subject: [PATCH 12/13] Hold the read guard on Python 3.10 to 3.13 CI's pytest shards run only 3.14, so ec4ae312's guard passed there and failed on every older version the project allows. Python's rglob works three ways. 3.13 and 3.14 call glob with a "**/" pattern (a path object on 3.13), 3.12 calls walk on the folder it lists, and 3.10 and 3.11 do neither. One shared set of allowed listings therefore refused the package listing on 3.13, and let design.rglob("track_b_*.md") through on 3.12 and earlier. Each listing method now has its own set, patterns compare as strings, and walk may list only the package instead of nothing. Co-Authored-By: Claude Opus 5.5 --- tests/test_track_b_readings_20261023.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/test_track_b_readings_20261023.py b/tests/test_track_b_readings_20261023.py index d7c1f5ce3..7d951865d 100644 --- a/tests/test_track_b_readings_20261023.py +++ b/tests/test_track_b_readings_20261023.py @@ -281,8 +281,8 @@ def test_the_checks_list_and_open_only_track_b_paths(monkeypatch): package = (ROOT / "src/populace_dynamics/track_b").resolve() design = (ROOT / "docs/design").resolve() captures = (ROOT / "tests/data/track_b/gross_benefit_sources").resolve() - listings = {(package, "*"), (package, "**/*"), (design, "track_b_*.md")} path_open, glob, rglob = Path.open, Path.glob, Path.rglob + walk = getattr(Path, "walk", None) # New in Python 3.12. opened = [] def checked_open(path, *args, **kwargs): @@ -295,21 +295,31 @@ def checked_open(path, *args, **kwargs): opened.append(resolved) return path_open(path, *args, **kwargs) - def checked(listing): + def checked(listing, allowed): def checked_listing(path, pattern, *args, **kwargs): - assert (path.resolve(), pattern) in listings, (path, pattern) + assert (path.resolve(), str(pattern)) in allowed, (path, pattern) return listing(path, pattern, *args, **kwargs) return checked_listing + def checked_walk(path, *args, **kwargs): + # On Python 3.12, rglob walks the folder it lists. + assert path.resolve() == package, path + return walk(path, *args, **kwargs) + def unchecked(path, *args, **kwargs): raise AssertionError(f"listing of {path}") monkeypatch.setattr(Path, "open", checked_open) - monkeypatch.setattr(Path, "glob", checked(glob)) - monkeypatch.setattr(Path, "rglob", checked(rglob)) + # From Python 3.13, rglob calls glob with "**/" before its pattern. + monkeypatch.setattr( + Path, + "glob", + checked(glob, {(design, "track_b_*.md"), (package, "**/*")}), + ) + monkeypatch.setattr(Path, "rglob", checked(rglob, {(package, "*")})) monkeypatch.setattr(Path, "iterdir", unchecked) - monkeypatch.setattr(Path, "walk", unchecked, raising=False) + monkeypatch.setattr(Path, "walk", checked_walk, raising=False) for reach in ( lambda: (ROOT / "pyproject.toml").open(), lambda: (design / "other.md").open(), @@ -317,7 +327,8 @@ def unchecked(path, *args, **kwargs): lambda: design.rglob("track_b_*.md"), lambda: package.parent.rglob("*"), lambda: package.iterdir(), - lambda: package.walk(), + lambda: design.walk(), + lambda: package.parent.walk(), ): with pytest.raises(AssertionError): reach() From 410a93213bda929fe2ce148fb652a32e0d27ad53 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 6 Nov 2026 08:36:09 -0500 Subject: [PATCH 13/13] Recount test tiers on master 4ab97297 unit is master's 26,654 plus this PR's 21, total 36,144. No other tier moves. A collect of the changed modules gives 1,131 unit tests at master and 1,152 here. Co-Authored-By: Claude Opus 5.5 --- tests/README-tiers.md | 4 ++-- tests/tier_counts.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/README-tiers.md b/tests/README-tiers.md index ec4237523..edb45450a 100644 --- a/tests/README-tiers.md +++ b/tests/README-tiers.md @@ -39,9 +39,9 @@ pytest --collect-only -q -m oracle_policyengine | tail -1 | Tier | Tests at HEAD | |---|---:| -| `unit` | 26,654 | +| `unit` | 26,675 | | `artifact` | 7,141 | | `integration_psid` | 1,471 | | `reproduction_legacy` | 520 | | `oracle_policyengine` | 337 | -| **Total** | **36,123** | +| **Total** | **36,144** | diff --git a/tests/tier_counts.json b/tests/tier_counts.json index 1156225c5..4af4cfe1f 100644 --- a/tests/tier_counts.json +++ b/tests/tier_counts.json @@ -1,7 +1,7 @@ { "schema_version": 1, "counts": { - "unit": 26654, + "unit": 26675, "artifact": 7141, "integration_psid": 1471, "reproduction_legacy": 520,