Skip to content

Limit Alaska's dividend to the statute's immigration statuses, and add a take-up input - #10763

Open
MaxGhenis wants to merge 11 commits into
mainfrom
ak-pfd-receipt
Open

MaxGhenis wants to merge 11 commits into
mainfrom
ak-pfd-receipt

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Follows #10692.

What and why

PolicyEngine pays the Alaska Permanent Fund Dividend and energy relief to everyone who lives in Alaska: by our estimate, 756,512 people for 2027, about 19% more than Alaska pays. Under AS 43.23.005, Alaska pays only people who apply and pass the statute's residence, presence, immigration status and Selective Service tests, and under (d) it denies the dividend to some people with felony or repeat convictions.

This PR:

  • adds ak_permanent_fund_dividend_eligible, which applies the immigration status test in AS 43.23.005(a)(5), the one test PolicyEngine can resolve from its inputs;
  • adds takes_up_ak_permanent_fund_dividend_if_eligible, a default-true input for the statute's other tests and for anyone who doesn't apply;
  • gates ak_permanent_fund_dividend and ak_energy_relief on both, because Alaska pays energy relief as part of the dividend (sec. 13, ch. 34, SLA 2026).

A household calculation still pays a resident unless an input says otherwise: an unset status counts as citizen, and an unset flag counts as true.

The status test closes 9% of the gap by itself. In PolicyEngine/microcosm#1551, we specify a Microcosm stage that sets the flag and takes reported dividends out of miscellaneous_income (PolicyEngine/microcosm#1443). To show what such a flag does to totals and poverty, we set a prototype flag and picked its draw cutoff so the eligible people it flags match Alaska's 2024 count on the file's released weights.

Who Alaska pays

The PFD Division reports these counts in its summary of dividend applications and payments:

Dividend year State population Applications received Applications paid Paid share of population
2025 738,737 665,327 618,863 83.8%
2024 741,147 666,213 624,489 84.3%
2023 733,406 673,366 624,354 85.1%
2022 733,583 665,764 625,912 85.3%
2021 732,670 674,454 636,895 86.9%
2020 728,903 673,371 630,937 86.6%
2019 732,734 678,738 633,243 86.4%
2018 736,239 670,759 639,247 86.8%
2017 737,847 670,706 633,005 85.8%
2016 739,828 674,939 638,178 86.3%
2015 737,625 678,308 641,561 87.0%

In its 2024 annual report, the Division counts 659,422 applications for 2024, from 494,717 adults and 164,705 children. As of December 31, it lists 627,030 as eligible, 23,629 as denied with no appeal, 8,317 in review and 446 in appeal. Those counts differ from the summary table's, and we compare the model with the table's paid counts.

Where each piece belongs

The status test belongs here. The Legislature names the statuses in AS 43.23.005(a)(5), and the model already has immigration_status. In State v. Andrade, 23 P.3d 58 (Alaska 2001), the Alaska Supreme Court read "lawfully admitted for permanent residence" as legal presence plus the ability to intend to remain, rather than the federal definition. In 15 AAC 23.154(a), the Department of Revenue applies that reading to a status under 8 U.S.C. 1101 to 1189 that allows domicile. We give the reason for each status in docs/validation/alaska-pfd.md. Populace's Alaska records hold 714,298 citizens, 15,852 permanent residents, 9,894 undocumented people and almost no DACA recipients, so these totals don't move however we read the other codes. The Census Bureau asks about citizenship in the ASEC, and Microcosm imputes the non-citizen labels, so the status test's count inherits that imputation's error.

The model can't apply the birth-date test from age. A child born during the dividend year can't qualify: the child fails (a)(3), and under (c) only children born in the two calendar years before it qualify. The Division takes applications for a child "born on or before December 31st of the qualifying year." But for a child under one, age doesn't say which calendar year the birth fell in. Populace's age equals ASEC A_AGE on all 2,444 Alaska records. The Census Bureau conducts the ASEC in February, March and April, records age as of the survey date, and asks about income in the year before, which Populace records as source_year. Of Alaska's 6,337 people aged 0 for 2024, all of whom pass the status test, 74% come from the 2023 survey, 26% from 2024's and 0.3% from 2025's. So the share who can qualify for 2024 turns on how a record's survey date maps to the dividend year:

Dating People aged 0 who can qualify for 2024 Share
Each record keeps its own survey date 5,981 94% (92% to 96%)
Every survey falls in the dividend year 5,052 80% (71% to 87%)
birth_year's formula (the year minus age) puts every birth in the dividend year 0 0%

We assume births spread evenly over the year before the survey. The ranges run from mid-February to mid-April interviews, and for the counts we assume mid-March. The first dating depends on the release weights, which give the 2025 survey 0.3% of the age-0 weight but 25% of all Alaska weight. If the age-0 weight spread over surveys like all Alaska weight, 68% (65% to 70%) could qualify. Either way, if the model gated at age 1, it would remove 6,337 people, mostly children who can qualify, so this PR leaves age out. Set the take-up input to false for a child born during the dividend year.

Who applies and passes the other tests belongs in the data.

  • policyengine_core.commons.formulas.random raises "Formula-time randomness is not allowed", so a formula can't draw take-up. PolicyEngine models take-up of other programs the same way, with default-true inputs such as takes_up_snap_if_eligible and takes_up_medicaid_if_eligible that Microcosm sets.
  • The model and Populace record none of the statute's other tests. is_incarcerated records no conviction and doesn't tie to the qualifying year, and Populace doesn't set it.
  • The only record of who received a dividend sits in raw ASEC OI_VAL, which the model doesn't read. Microcosm puts ASEC OI_VAL in miscellaneous_income, apart from the alimony and strike benefit codes, so reported dividends overlap the simulated dividend today.

This PR couples to Microcosm's next engine bump. assert_take_up_contract_current fails when the engine carries a takes_up_* variable that us/take_up_contract.json omits, and test_checked_in_table_matches_installed_engine fails when the table lists a variable the engine lacks. The bundle's take-up spec has to cover the engine too: _take_up_program_bindings requires exactly US_TAKE_UP_PROGRAM_COUNT (17) programs in us/spec/take_up.yaml, and assert_engine_abi_lock_current raises "take_up/programs: mapping is not total over the fresh engine ABI" when the engine's take-up variables differ from the bundle's. Microcosm locks policyengine-us 2.2.1, and draft PolicyEngine/microcosm#1365, which moves the lock to 2.60.4, adds takes_up_medicare_part_d_if_eligible as the 18th program. So whoever first bumps Microcosm's engine past a release containing this PR has to add, in the same PR, a contract row and a 19th program for the new flag, a count of 19 and a regenerated us/engine_abi.lock.json. We spell out each change in PolicyEngine/microcosm#1551.

Impact

We ran Microsimulation for every recipient count, dollar total and poverty rate, on the Alaska households of the default dataset: populace_us_2024.h5 at populace-us-2024-spm-20260909 (sha256 6496cc43...), 826 households and 2,444 person records at their own weights. The match-rule and weight diagnostics read the file's inputs directly. This PR changes only Alaska's formulas, so all its effects fall on these records. Main's 2027 dividend total, $756,512,330.77, equals the figure from #10692's full-file run.

  • Main: origin/main at 30d03c3.
  • This PR: this branch on today's data, which carries no flag.
  • With prototype flag: this branch reading the prototype flag, averaged over 20 seeds, with the range across seeds in parentheses. No Populace release carries the prototype flags; we set them only to measure what such a flag would do. The prototype picks its draw cutoff on the file's released weights, so these ranges don't describe a stage that runs before weight calibration, where Microcosm's take-up stages run. For that reason, the stage in Seed Alaska's Permanent Fund Dividend take-up and take reported dividends out of miscellaneous_income microcosm#1551 adds a weight target and a delivered-weight gate.
  • Reported dividends removed: the same flags, and the prototype also takes matching reported dividends out of miscellaneous_income.

Recipients

Dividend year Alaska paid Main This PR With prototype flag
2024 624,489 740,043 (+18.5%) 730,150 (+16.9%) 624,141 (-0.1%), draws 618,190 to 628,769
2025 618,863 746,914 (+20.7%) 736,929 (+19.1%) 629,936 (+1.8%), draws 623,930 to 634,607
2026 636,704 (estimate) 752,248 (+18.1%) 742,191 (+16.6%) 634,434 (-0.4%), draws 628,385 to 639,139
2027 n/a 756,512 746,398 638,030, draws 631,947 to 642,762

For 2026, we estimated Alaska's count by dividing the Legislature's $127,340,800 estimate by the $200 energy relief. The prototype matches 2024 because we set its cutoff there. The model keeps the same flags in later years and uprates the weights: each seed's 2025 and 2026 counts equal its 2024 count times main's uprating ratios (1.00928 and 1.01649). Those rows compare national population uprating with Alaska's counts, so they don't test the draw rule.

2027

Main This PR With prototype flag Reported dividends removed
Dividend $756.5M $746.4M $638.0M ($631.9M to $642.8M) $638.0M ($631.9M to $642.8M)
Energy relief $0.0M $0.0M $0.0M $0.0M
Household net income $38,783.9M $38,774.7M $38,677.4M ($38,672.5M to $38,681.6M) $38,496.1M ($38,491.2M to $38,500.2M)
Change from main n/a -$9.2M -$106.5M (-$111.4M to -$102.4M) -$287.8M (-$292.7M to -$283.7M)
SPM poverty rate 8.85% 8.85% 8.86% (8.85% to 8.93%) 8.86% (8.85% to 8.93%)
Child SPM poverty rate 10.82% 10.82% 10.82% 10.82%

2026

Main This PR With prototype flag Reported dividends removed
Dividend $752.2M $742.2M $634.4M ($628.4M to $639.1M) $634.4M ($628.4M to $639.1M)
Energy relief $150.4M $148.4M $126.9M ($125.7M to $127.8M) $126.9M ($125.7M to $127.8M)
Household net income $37,544.0M $37,532.8M $37,414.4M ($37,408.8M to $37,419.7M) $37,239.8M ($37,234.2M to $37,245.1M)
Change from main n/a -$11.2M -$129.5M (-$135.2M to -$124.3M) -$304.1M (-$309.8M to -$298.9M)
SPM poverty rate 8.45% 8.45% 8.47% (8.45% to 8.53%) 8.87% (8.85% to 8.93%)
Child SPM poverty rate 10.82% 10.82% 10.82% 10.82%

Limits

  • This PR alone leaves the count 16.9% above Alaska's for 2024. It removes 9,894 people for that year. The rest waits on Seed Alaska's Permanent Fund Dividend take-up and take reported dividends out of miscellaneous_income microcosm#1551.
  • The microsimulation has no inputs before its 2024 base year, so it can't report recipients for 2015 to 2023. Alaska paid 83.8% to 87.0% of its population over 2015 to 2025. Because we set the prototype's cutoff to match 624,489, it pays 84.3% of Alaska's people.
  • The model doesn't check a parent's eligibility. Under AS 43.23.005(c), a child born during the qualifying year qualifies only through a parent eligible for that dividend, and the model tests the child's own status. Among Populace's Alaska records, 60 hold children aged 0 or 1 who pass the status test (18,402 people). On each, the child shares an SPM unit, tax unit and household with an adult who passes it too, so if the model required such an adult, nothing here would move. (One child aged 1, on 2 records weighing 2,204 people, forms a family unit alone.) In a household calculation, set the take-up input to false for a child born during the qualifying year who has no eligible parent.
  • The status test reads the dividend year's status. Alaska tests the status a person holds during the qualifying year and on the application date, and PolicyEngine has no status history.
  • PolicyEngine grows Alaska's weights with national population uprating (740,043 people for 2024, 756,512 for 2027), while the Division reports Alaska's population falling from 741,147 in 2024 to 738,737 in 2025.
  • SPM poverty moves in steps here. Of Alaska's 2,444 person records, 1,821 weigh under 1 and hold 0.009% of the population. The 877 SPM units amount to about 147 effective units. One two-person unit at a weight of about 1,505 moves the rate by 0.4 points, which accounts for the 2026 gap between the last two columns. The range across seeds comes from which units each seed pays. The child rate doesn't move: the units with children that flip weigh close to zero.

Invariants

These hold for every modeled year, every mix of people in a household and every age, unless an input overrides the eligibility variable or a payment:

  1. A person's dividend and energy relief each equal zero or the full amount for the year.
  2. A person gets the dividend exactly when they live in Alaska, hold one of the statute's statuses, and take it up. An unset status counts as citizen, and an unset flag as true. Age plays no part.
  3. Energy relief passes through the same gate as the dividend.
  4. Nobody outside Alaska gets either payment.
  5. Turning one person's take-up off lowers that person's payment to zero and changes nobody else's.

Test plan

  • YAML tests with expected values from AS 43.23.005(a)(5) and (c), 15 AAC 23.154 and the amounts Alaska paid: ak_permanent_fund_dividend.yaml cases 15 to 25, ak_energy_relief.yaml cases 13 to 18, and ak_permanent_fund_dividend_eligible.yaml cases 1 to 7. All 50 cases in the folder pass.
  • Hypothesis properties for the invariants above in policyengine_us/tests/test_ak_permanent_fund_dividend_receipt.py: every combination of status, take-up and six representative ages in Alaska and Washington for 2015, 2022, 2026 and 2027, plus drawn years, states, ages and household mixes. The file restates the statute's four statuses, so it fails if someone changes the parameter's statuses in a year it tests without changing the file. YAML can't draw household mixes or exhaust that product. Each of eleven mutations fails the new file or the YAML folder: dropping ASYLEE, adding TPS, ignoring take-up in the dividend or in energy relief, restoring the residence-only gate on either payment, removing the state gate from eligibility, waiving the status test or the take-up input for children, tying energy relief to a paid dividend, and failing a person aged 0.
  • Source conflict: none to register. The Alaska Supreme Court (Andrade), the Department of Revenue (15 AAC 23.154) and the Division (on its immigration page) read the status test as legal presence plus the ability to intend to remain, and in OAH No. 25-3180-PFD an administrative law judge applied that reading to a fixed-term parolee. They may differ in one place. A TPS holder might meet Andrade's test alone, but the court didn't address TPS. In 15 AAC 23.154(a) and (e), the Department of Revenue counts only statuses under 8 U.S.C. 1101 to 1189, and TPS falls under 8 U.S.C. 1254a. PolicyEngine follows the regulation, and Populace's Alaska records hold no TPS holders, so nothing moves either way. We map the model's codes onto that reading. PAROLED_ONE_YEAR doesn't say a parole runs indefinitely. TPS and DEPORTATION_WITHHELD (8 U.S.C. 1231(b)(3)) fall outside 8 U.S.C. 1101 to 1189, and a person coded DACA holds no status under those sections. A person coded UNDOCUMENTED lacks the legal presence the court required in Andrade. CUBAN_HAITIAN_ENTRANT and CONDITIONAL_ENTRANT don't say whether the person may adopt the United States as their domicile.
  • ruff format and ruff check on the whole repository, as make format runs them
  • Changelog fragment in changelog.d/

Test cost

At 059bf7c, CI run 42622268857 ran the new file on Python shard 5, in the shard planner's remaining-1 process. Its seven tests took 14.9 s there. That process ran 76 files (2,323 tests) in 20:40 of wall time, and GNU time measured its peak resident set at 6.45 GiB; the Makefile gives the runner 16 GB. Locally on macOS the file takes 10 s, or 35 s of wall time including its import of the model, and peaks at a 1.5 GB footprint. It builds no extra model and loads no dataset; each case reuses the shared system through Simulation(situation=...).

Scripts for the figures

build_state_subset.py writes the Alaska records. run_scenarios.py runs main and this branch.

"""Write every household of the named states to its own file, for a state-only impact pair (hub, 2026-11-05).

usage: build_state_subset.py SRC_H5 OUT_H5 XX[,YY...]

SRC_H5 is the default dataset's entity-level HDFStore. Unlike the 10% screen sample (build_screen_sample.py), this file
holds all of each named state's households at their own weights, so a PR that changes only those states' formulas gets
the same record-level results here as on the full file. Other entities stay when a member stays. A JSON manifest beside
OUT_H5 records the source's sha256, the states and the counts. No model code runs.
"""
import hashlib
import json
import sys

import pandas as pd

FIPS = {
    "AL": 1, "AK": 2, "AZ": 4, "AR": 5, "CA": 6, "CO": 8, "CT": 9, "DE": 10, "DC": 11, "FL": 12, "GA": 13, "HI": 15,
    "ID": 16, "IL": 17, "IN": 18, "IA": 19, "KS": 20, "KY": 21, "LA": 22, "ME": 23, "MD": 24, "MA": 25, "MI": 26,
    "MN": 27, "MS": 28, "MO": 29, "MT": 30, "NE": 31, "NV": 32, "NH": 33, "NJ": 34, "NM": 35, "NY": 36, "NC": 37,
    "ND": 38, "OH": 39, "OK": 40, "OR": 41, "PA": 42, "RI": 44, "SC": 45, "SD": 46, "TN": 47, "TX": 48, "UT": 49,
    "VT": 50, "VA": 51, "WA": 53, "WV": 54, "WI": 55, "WY": 56,
}

src, out, states = sys.argv[1], sys.argv[2], sorted(s.strip().upper() for s in sys.argv[3].split(","))
fips = [FIPS[s] for s in states]

h = hashlib.sha256()
with open(src, "rb") as f:
    for chunk in iter(lambda: f.read(1 << 20), b""):
        h.update(chunk)

hh = pd.read_hdf(src, "household")
hh_s = hh[hh["state_fips"].isin(fips)].copy()
assert len(hh_s), f"no households for {states}"
hid = set(hh_s["household_id"])
person = pd.read_hdf(src, "person")
p_s = person[person["person_household_id"].isin(hid)].copy()
tables = {"household": hh_s, "person": p_s}
for ent in ["tax_unit", "spm_unit", "family", "marital_unit"]:
    t = pd.read_hdf(src, ent)
    ids = set(p_s[f"person_{ent}_id"])
    tables[ent] = t[t[f"{ent}_id"].isin(ids)].copy()

with pd.HDFStore(out, "w") as store:
    store.put("_time_period", pd.read_hdf(src, "_time_period"), format="table")
    for k, v in tables.items():
        store.put(k, v, format="table")

manifest = {
    "source": src,
    "source_sha256": h.hexdigest(),
    "states": states,
    "design": "every household with these state_fips; weights unchanged",
    "counts": {k: int(len(v)) for k, v in tables.items()},
    "source_counts": {"household": int(len(hh)), "person": int(len(person))},
    "weighted_households": float(hh_s["household_weight"].sum()),
}
json.dump(manifest, open(out + ".json", "w"), indent=1)
print(json.dumps({k: manifest[k] for k in ("states", "counts", "weighted_households")}))
"""Real microsimulations of the Alaska dividend on the Alaska-only Populace subset.

usage: run_scenarios.py OUT.json label=path [label=path ...]

Reports, for each scenario and year from the file's 2024 base year to 2027:
weighted recipients, the dividend and energy relief totals, and, for 2026 and
2027, Alaska's SPM poverty rates and household net income.
"""

import json
import sys
import time

import numpy as np
import policyengine_us
from policyengine_us import Microsimulation

YEARS = (2024, 2025, 2026, 2027)
FULL_YEARS = (2026, 2027)
out_path, specs = sys.argv[1], [s.split("=", 1) for s in sys.argv[2:]]
out = {"module": policyengine_us.__file__, "scenarios": {}}


def values(sim, name, year, **kw):
    # The model stores float32, which rounds a weighted sum by about $50, so sum in float64.
    return np.asarray(sim.calculate(name, year, **kw).values, dtype=np.float64)


for label, path in specs:
    t0 = time.time()
    sim = Microsimulation(dataset=path, dataset_end_year=max(YEARS))
    result = {"dataset": path}
    for year in YEARS:
        w = values(sim, "person_weight", year)
        dividend = values(sim, "ak_permanent_fund_dividend", year)
        relief = values(sim, "ak_energy_relief", year)
        row = {
            "alaska_people": float(w.sum()),
            "recipients": float(w[dividend > 0].sum()),
            "dividend_total": float((w * dividend).sum()),
            "energy_relief_total": float((w * relief).sum()),
        }
        if year in FULL_YEARS:
            poor = np.asarray(sim.calculate("spm_unit_is_in_spm_poverty", year, map_to="person").values).astype(bool)
            age = values(sim, "age", year)
            hw = values(sim, "household_weight", year)
            net = values(sim, "household_net_income", year)
            row["spm_poverty_rate"] = float(w[poor].sum() / w.sum())
            row["spm_child_poverty_rate"] = float(w[poor & (age < 18)].sum() / w[age < 18].sum())
            row["spm_poor_people"] = float(w[poor].sum())
            row["household_net_income_total"] = float((hw * net).sum())
        result[str(year)] = row
    result["seconds"] = round(time.time() - t0, 1)
    out["scenarios"][label] = result
    print(label, json.dumps(result), flush=True)
    del sim
    json.dump(out, open(out_path, "w"), indent=1)

seed_flag.py sets the prototype flag, sweep.py runs it across seeds, and match_rule_check.py, weights_check.py, age_check.py, age_dating_check.py, parent_check.py and stage_time_emulation.py produce the diagnostics.

"""PROTOTYPE of the proposed Microcosm stage. No Populace release contains its output.

Writes copies of the Alaska-only Populace subset with
takes_up_ak_permanent_fund_dividend_if_eligible set, so the PR's impact comes from
real microsimulations of what the data stage would do:

- Anyone in a unit where someone reported a dividend gets the flag (anchor).
- Other units get a seeded blake2b draw on their stable source identity, the
  key microcosm's take_up.py uses, so support-channel clones share a flag.
- It picks the draw cutoff so that eligible flagged people equal Alaska's 2024
  paid applications, 624,489, on the file's 2024 weights.

With --clean, it also takes reported dividends out of miscellaneous_income
(microcosm#1443). Microcosm puts ASEC-channel OI_VAL in miscellaneous_income,
apart from the alimony and strike benefit codes.

usage: seed_flag.py SRC OUT LEVEL SEED MATCH CLEAN
  LEVEL spm_unit|person; MATCH exact|tolerant; CLEAN clean|keep
"""

import hashlib
import json
import sys

import numpy as np
import pandas as pd

VAR = "takes_up_ak_permanent_fund_dividend_if_eligible"
TARGET_2024 = 624_489
ELIGIBLE = {"CITIZEN", "LEGAL_PERMANENT_RESIDENT", "REFUGEE", "ASYLEE"}
# Whole payments Alaska made, and the 2022 and 2024 components people may report alone.
PAID = {
    2021: [1114],
    2022: [3284, 2622, 662],
    2023: [1312],
    2024: [1702, 1403.83, 298.17],
}
TABLES = ["_time_period", "household", "person", "tax_unit", "spm_unit", "family", "marital_unit"]

src, out, level, seed, match, clean = sys.argv[1:7]
assert level in ("spm_unit", "person") and match in ("exact", "tolerant") and clean in ("clean", "keep")
tables = {t: pd.read_hdf(src, t) for t in TABLES}
p = tables["person"].copy()
hh = tables["household"].set_index("household_id")
w = p["person_household_id"].map(hh["household_weight"]).astype(float).to_numpy()
eligible = p["immigration_status_str"].isin(ELIGIBLE).to_numpy()


def is_dividend(value, year):
    """A reported amount equal to one to eight dividends Alaska paid in this or the prior year."""
    if not value > 0:
        return False
    tol = 0.5 if match == "exact" else 100
    for y in (year, year - 1):
        for d in PAID.get(y, []):
            for k in range(1, 9):
                if abs(value - k * d) <= tol:
                    return True
    return False


reporter = np.array([is_dividend(v, int(y)) for v, y in zip(p["OI_VAL"], p["source_year"])])
sid = list(zip(p["source_year"], p["source_household_id"], p["source_person_id"]))
if level == "person":
    key = pd.Series([f"{a}:{b}:{c}" for a, b, c in sid], index=p.index)
else:
    frame = pd.DataFrame(
        {
            "unit": p["person_spm_unit_id"],
            "year": p["source_year"],
            "hh": p["source_household_id"],
            "pid": p["source_person_id"],
        }
    )
    first = frame.groupby("unit").agg(year=("year", "first"), hh=("hh", "first"), pid=("pid", "min"))
    unit_key = first["year"].astype(str) + ":" + first["hh"].astype(str) + ":" + first["pid"].astype(str)
    key = p["person_spm_unit_id"].map(unit_key)

units = pd.DataFrame({"key": key.to_numpy(), "w_e": w * eligible, "rep": reporter}).groupby("key").agg(
    w_e=("w_e", "sum"), anchored=("rep", "any")
)
units["u"] = [
    int.from_bytes(hashlib.blake2b(f"{seed}:{VAR}:{k}".encode(), digest_size=8).digest(), "big") / 2**64
    for k in units.index
]
anchored_weight = float(units.loc[units["anchored"], "w_e"].sum())
free = units.loc[~units["anchored"]].sort_values("u")
cum = anchored_weight + free["w_e"].cumsum().to_numpy()
# Keep the cutoff whose total falls closest to the target.
gaps = np.abs(np.concatenate(([anchored_weight], cum)) - TARGET_2024)
n_free = int(gaps.argmin())
granted = set(free.index[:n_free])
units["flag"] = units["anchored"] | units.index.isin(granted)
flag = key.map(units["flag"]).to_numpy(dtype=bool)
p[VAR] = flag

removed = 0.0
if clean == "clean":
    asec = (p["person_support_channel"] == "asec").to_numpy()
    mask = reporter & asec
    removed = float((w[mask] * p.loc[mask, "miscellaneous_income"].to_numpy()).sum())
    p.loc[mask, "miscellaneous_income"] = 0.0

tables["person"] = p
with pd.HDFStore(out, "w") as store:
    store.put("_time_period", tables["_time_period"], format="table")
    for t in TABLES[1:]:
        store.put(t, tables[t], format="table")

meta = {
    "PROTOTYPE_NOT_RELEASED": True,
    "source": src,
    "level": level,
    "seed": seed,
    "match": match,
    "clean": clean,
    "target_2024": TARGET_2024,
    "alaska_people_2024": float(w.sum()),
    "eligible_people_2024": float((w * eligible).sum()),
    "reporter_people": float(w[reporter].sum()),
    "reporter_records": int(reporter.sum()),
    "anchored_eligible_people": anchored_weight,
    "units": int(len(units)),
    "anchored_units": int(units["anchored"].sum()),
    "granted_units": n_free,
    "eligible_flagged_people_2024": float((w * eligible * flag).sum()),
    "eligible_share_flagged": float((w * eligible * flag).sum() / (w * eligible).sum()),
    "free_share_granted": float(free["w_e"].iloc[:n_free].sum() / free["w_e"].sum()),
    "misc_income_removed": removed,
    "misc_share_after": float(w[p["miscellaneous_income"].to_numpy() > 0].sum() / w.sum()),
}
json.dump(meta, open(out + ".json", "w"), indent=1)
print(json.dumps(meta))
"""Real microsimulations of the Alaska dividend across flag draws (prototype flag, in no release).

usage: sweep.py OUT.json FIRST_SEED LAST_SEED

For each seed, this writes temporary copies of the Alaska subset with the
flag set three ways (SPM-unit draw; SPM-unit draw that also takes reported
dividends out of miscellaneous income; person draw), runs the model on each, and
deletes them.
"""

import json
import os
import subprocess
import sys
import tempfile

import numpy as np
from policyengine_us import Microsimulation

HERE = os.path.dirname(os.path.abspath(__file__))
AK = os.path.join(HERE, "..", "data", "AK.h5")
YEARS = (2024, 2025, 2026, 2027)
FULL = (2026, 2027)
SETS = {"C": ("spm_unit", "keep"), "D": ("spm_unit", "clean"), "Cp": ("person", "keep")}
out_path, first, last = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])


def values(sim, name, year, **kw):
    return np.asarray(sim.calculate(name, year, **kw).values, dtype=np.float64)


def evaluate(path):
    sim = Microsimulation(dataset=path, dataset_end_year=max(YEARS))
    res = {}
    for year in YEARS:
        w = values(sim, "person_weight", year)
        d = values(sim, "ak_permanent_fund_dividend", year)
        r = values(sim, "ak_energy_relief", year)
        row = {"recipients": float(w[d > 0].sum()), "dividend_total": float((w * d).sum()), "energy_relief_total": float((w * r).sum())}
        if year in FULL:
            poor = np.asarray(sim.calculate("spm_unit_is_in_spm_poverty", year, map_to="person").values).astype(bool)
            age = values(sim, "age", year)
            hw = values(sim, "household_weight", year)
            net = values(sim, "household_net_income", year)
            row.update(spm_poverty_rate=float(w[poor].sum() / w.sum()), spm_child_poverty_rate=float(w[poor & (age < 18)].sum() / w[age < 18].sum()), household_net_income_total=float((hw * net).sum()))
        res[str(year)] = row
    return res


results = {k: {} for k in SETS}
for seed in range(first, last + 1):
    for name, (level, clean) in SETS.items():
        with tempfile.TemporaryDirectory(dir=os.path.join(HERE, "..", "data")) as tmp:
            path = os.path.join(tmp, "seeded.h5")
            subprocess.run([sys.executable, os.path.join(HERE, "seed_flag.py"), AK, path, level, str(seed), "tolerant", clean], check=True, capture_output=True)
            results[name][str(seed)] = evaluate(path)
    print("seed", seed, "done", flush=True)
    json.dump(results, open(out_path, "w"), indent=1)
"""Apply the amount rule to every state in the full file, to see how often it matches outside Alaska.

A record matches when OI_VAL falls within $100 of one to eight dividends Alaska
paid in its source year or the prior year (the seed_flag.py 'tolerant' rule), unless it
carries OI_OFF code 12 or 20, which microcosm's split already sends to strike_benefits
and alimony_income. No Alaska record with OI_VAL above 0 carries either code, so
the skip changes nothing in seed_flag.py's Alaska runs. A match outside
Alaska counts as a false positive unless the person received a dividend and moved before the
survey, so this bounds the rule's error from above.
"""
import sys
import numpy as np
import pandas as pd

src = sys.argv[1]
PAID = {2021: [1114], 2022: [3284, 2622, 662], 2023: [1312], 2024: [1702, 1403.83, 298.17]}
cols = ["person_household_id", "OI_VAL", "OI_OFF", "source_year", "person_support_channel", "miscellaneous_income"]
p = pd.read_hdf(src, "person", columns=cols)
h = pd.read_hdf(src, "household", columns=["household_id", "state_fips", "household_weight"]).set_index("household_id")
p["fips"] = p["person_household_id"].map(h["state_fips"])
p["w"] = p["person_household_id"].map(h["household_weight"]).astype(float)
amounts = {y: np.array([k * d for yy in (y, y - 1) for d in PAID.get(yy, []) for k in range(1, 9)]) for y in p["source_year"].unique()}
SEPARATE_CODES = (12, 20)
def match(v, code, y, tol):
    if not v > 0 or code in SEPARATE_CODES:
        return False
    return bool((np.abs(amounts[y] - v) <= tol).any())
pos = p[p["OI_VAL"] > 0].copy()
pos["tol100"] = [match(v, c, y, 100) for v, c, y in zip(pos["OI_VAL"], pos["OI_OFF"], pos["source_year"])]
pos["tol0"] = [match(v, c, y, 0.5) for v, c, y in zip(pos["OI_VAL"], pos["OI_OFF"], pos["source_year"])]
skipped = pos["OI_OFF"].isin(SEPARATE_CODES) & np.array(
    [match(v, 0, y, 100) for v, y in zip(pos["OI_VAL"], pos["source_year"])]
)
print("records the code skip removes from the $100 rule:", int(skipped.sum()), "in", pos.loc[skipped, "fips"].nunique(), "states")
tot = p.groupby("fips")["w"].sum()
rows = pos.groupby("fips").apply(lambda d: pd.Series({
    "oi_pos_share": d["w"].sum(), "tol100_share": d.loc[d.tol100, "w"].sum(), "tol0_share": d.loc[d.tol0, "w"].sum()}), include_groups=False)
rows = rows.div(tot, axis=0)
us_ex_ak = rows.drop(index=2)
print("AK shares of people", rows.loc[2].round(4).to_dict())
print("median other state", us_ex_ak.median().round(4).to_dict())
print("max other state", us_ex_ak.max().round(4).to_dict())
print("WA", rows.loc[53].round(4).to_dict(), "OR", rows.loc[41].round(4).to_dict())
ak = p[p["fips"] == 2]
print("AK unweighted share of records with miscellaneous_income > 0:", round(float((ak["miscellaneous_income"] > 0).mean()), 4))
print("WA", round(float((p[p.fips == 53]["miscellaneous_income"] > 0).mean()), 4), "OR", round(float((p[p.fips == 41]["miscellaneous_income"] > 0).mean()), 4))
"""Measure the concentration of Alaska's weights in the default file. Reads inputs only."""
import sys
import numpy as np
import pandas as pd

path = sys.argv[1]
p = pd.read_hdf(path, "person", columns=["person_household_id", "person_spm_unit_id"])
h = pd.read_hdf(path, "household", columns=["household_id", "household_weight"]).set_index("household_id")
p["w"] = p["person_household_id"].map(h["household_weight"]).astype(float)
w = p["w"].to_numpy()
print("person records", len(w), "weighted people", round(w.sum()))
print("records weighing under 1:", int((w < 1).sum()), "share of people", round(100 * w[w < 1].sum() / w.sum(), 3), "%")
print("records weighing 100 or more:", int((w >= 100).sum()), "share of people", round(100 * w[w >= 100].sum() / w.sum(), 2), "%")
print("Kish effective person records", round(w.sum() ** 2 / (w**2).sum(), 1))
u = p.groupby("person_spm_unit_id")["w"].first().to_numpy()
print("SPM units", len(u), "Kish effective SPM units", round(u.sum() ** 2 / (u**2).sum(), 1))
"""What the Alaska records' ages can and can't say about birth dates.

usage: age_check.py AK_H5 OUT_JSON
"""

import json
import sys

import numpy as np
import pandas as pd

path, out = sys.argv[1:3]
p = pd.read_hdf(path, "person")
h = pd.read_hdf(path, "household").set_index("household_id")
w = p.person_household_id.map(h.household_weight).astype(float)
eligible = p.immigration_status_str.isin({"CITIZEN", "LEGAL_PERMANENT_RESIDENT", "REFUGEE", "ASYLEE"})
zero = p.age == 0
result = {
    "records": int(len(p)),
    "records_with_age_equal_to_A_AGE": int((p.age == p.A_AGE).sum()),
    "age_0_records": int(zero.sum()),
    "age_0_people": float(w[zero].sum()),
    "age_0_people_with_a_qualifying_status": float(w[zero & eligible].sum()),
    "people_by_status": {k: float(v) for k, v in w.groupby(p.immigration_status_str).sum().items()},
    "records_by_status": {k: int(v) for k, v in p.immigration_status_str.value_counts().items()},
}
json.dump(result, open(out, "w"), indent=1)
print(json.dumps(result, indent=1))
"""Which Alaska people aged 0 can qualify for the 2024 dividend, by dating.

Populace's source_year holds the ASEC income year (AsecSource.year, which
microcosm's asec_pool.py passes as income_year). Census surveys that year's income in
February to April of the next year and records age as of the survey date. A
child born during the dividend year can't qualify: the child fails AS
43.23.005(a)(3), and under (c) only children born in the two calendar years
before the dividend year qualify. A child born earlier can qualify under (c) or (a).

Assumes births spread evenly over the 365 days before the survey date. Three
datings of a record's survey:

- own_survey: each record keeps its own survey, in source_year + 1;
- survey_in_dividend_year: every survey falls in the dividend year;
- birth_year: the dividend year minus age, as policyengine-us's birth_year
  formula reads it, which puts every child aged 0 in the dividend year.

usage: age_dating_check.py AK_H5 OUT_JSON
"""

import json
import sys

import pandas as pd

DIVIDEND_YEAR = 2024
# Day of the year of each survey date, in a 365-day year.
SURVEY_DAYS = {"mid_february": 46, "mid_march": 74, "mid_april": 105}


def share_born_before(survey_year, survey_day, year):
    """Share of children under one at the survey date born before `year`."""
    # Days from January 1 of the survey year: births fall in
    # (survey_day - 365, survey_day]; January 1 of `year` sits at
    # (year - survey_year) * 365 + 1.
    cutoff = (year - survey_year) * 365
    share = (cutoff - (survey_day - 365)) / 365
    return min(max(share, 0.0), 1.0)


def qualifying_share(people_by_source_year, dating, survey_day):
    total = sum(people_by_source_year.values())
    can = 0.0
    for source_year, people in people_by_source_year.items():
        if dating == "own_survey":
            survey_year = source_year + 1
        elif dating == "survey_in_dividend_year":
            survey_year = DIVIDEND_YEAR
        else:
            return 0.0
        can += people * share_born_before(survey_year, survey_day, DIVIDEND_YEAR)
    return can / total


def main():
    path, out = sys.argv[1:3]
    person = pd.read_hdf(path, "person")
    household = pd.read_hdf(path, "household").set_index("household_id")
    weight = person.person_household_id.map(household.household_weight).astype(float)
    zero = person.age == 0

    age0 = {
        int(year): float(people)
        for year, people in weight[zero].groupby(person.source_year[zero]).sum().items()
    }
    age0_records = {
        int(year): int(count)
        for year, count in person.source_year[zero].value_counts().sort_index().items()
    }
    everyone = {
        int(year): float(people)
        for year, people in weight.groupby(person.source_year).sum().items()
    }
    total0 = sum(age0.values())
    # Spread the same age-0 total over source years like all Alaska weight.
    total_all = sum(everyone.values())
    spread = {year: total0 * people / total_all for year, people in everyone.items()}

    result = {
        "dividend_year": DIVIDEND_YEAR,
        "age_0_records": int(zero.sum()),
        "age_0_people": total0,
        "age_0_records_by_source_year": age0_records,
        "age_0_people_by_source_year": age0,
        "age_0_share_by_source_year": {y: v / total0 for y, v in age0.items()},
        "all_people_share_by_source_year": {
            y: v / total_all for y, v in everyone.items()
        },
        "datings": {},
    }
    for dating in ("own_survey", "survey_in_dividend_year", "birth_year"):
        rows = {}
        for name, day in SURVEY_DAYS.items():
            share = qualifying_share(age0, dating, day)
            rows[name] = {
                "share_can_qualify": share,
                "people_who_can_qualify": share * total0,
                "people_who_cannot": (1 - share) * total0,
                "share_if_spread_like_all_weight": qualifying_share(
                    spread, dating, day
                ),
            }
        result["datings"][dating] = rows
    # Under own_survey, the share born two calendar years before the dividend
    # year: records from the survey in the year before it, with births before
    # that year.
    result["own_survey_share_born_two_years_before"] = {
        name: sum(
            people * share_born_before(year + 1, day, DIVIDEND_YEAR - 1)
            for year, people in age0.items()
            if year + 1 == DIVIDEND_YEAR - 1
        )
        / total0
        for name, day in SURVEY_DAYS.items()
    }
    json.dump(result, open(out, "w"), indent=1)
    print(json.dumps(result, indent=1))


if __name__ == "__main__":
    main()
"""Does each Alaska child under two who passes the status test live with an adult who does?

Under AS 43.23.005(c), a child born during the qualifying year qualifies only
through a parent eligible for that year's dividend. policyengine-us
checks each child's own status, not the parents', so this counts children aged
0 or 1 who pass the status test and share no unit with an adult (18 or older)
who passes it too.

usage: parent_check.py AK_H5 OUT_JSON
"""

import json
import sys

import pandas as pd

ELIGIBLE = {"CITIZEN", "LEGAL_PERMANENT_RESIDENT", "REFUGEE", "ASYLEE"}
UNITS = (
    "person_spm_unit_id",
    "person_tax_unit_id",
    "person_family_id",
    "person_household_id",
)


def main():
    path, out = sys.argv[1:3]
    person = pd.read_hdf(path, "person")
    household = pd.read_hdf(path, "household").set_index("household_id")
    weight = person.person_household_id.map(household.household_weight).astype(float)
    passes = person.immigration_status_str.isin(ELIGIBLE)
    young = (person.age <= 1) & passes
    adult = (person.age >= 18) & passes
    result = {
        "children_0_or_1_passing_records": int(young.sum()),
        "children_0_or_1_passing_people": float(weight[young].sum()),
        "without_a_passing_adult_in_unit": {},
    }
    for unit in UNITS:
        units_with_adult = set(person.loc[adult, unit])
        alone = young & ~person[unit].isin(units_with_adult)
        result["without_a_passing_adult_in_unit"][unit] = {
            "records": int(alone.sum()),
            "people": float(weight[alone].sum()),
        }
    json.dump(result, open(out, "w"), indent=1)
    print(json.dumps(result, indent=1))


if __name__ == "__main__":
    main()
"""Emulate choosing the prototype's fill cutoff on stage-time weights.

Microcosm assigns take-up before calibrate_l0_refit, so a stage that picks its
cutoff at that point sees different weights from the released ones. This script
sets the flag as seed_flag.py does, but chooses the cutoff on stand-in
stage-time weights that it scales to the released total, then counts eligible flagged
people on the released weights. The release has no design-weight column, so the script
uses stand-ins and doesn't run the real refit.

PROTOTYPE: these flags appear in no Populace release.

usage: stage_time_emulation.py AK_H5 [SEEDS]
"""

import hashlib
import sys

import numpy as np
import pandas as pd

path = sys.argv[1]
seeds = int(sys.argv[2]) if len(sys.argv) > 2 else 500
TARGET = 624_489
ELIGIBLE = {"CITIZEN", "LEGAL_PERMANENT_RESIDENT", "REFUGEE", "ASYLEE"}
PAID = {2021: [1114], 2022: [3284, 2622, 662], 2023: [1312], 2024: [1702, 1403.83, 298.17]}
VAR = "takes_up_ak_permanent_fund_dividend_if_eligible"

p = pd.read_hdf(path, "person")
h = pd.read_hdf(path, "household").set_index("household_id")
w = p.person_household_id.map(h.household_weight).astype(float).to_numpy()
elig = p.immigration_status_str.isin(ELIGIBLE).to_numpy()
rep = np.array(
    [
        v > 0 and any(abs(v - k * d) <= 100 for yy in (int(y), int(y) - 1) for d in PAID.get(yy, []) for k in range(1, 9))
        for v, y in zip(p.OI_VAL, p.source_year)
    ]
)
frame = pd.DataFrame(dict(u=p.person_spm_unit_id, y=p.source_year, h=p.source_household_id, i=p.source_person_id))
first = frame.groupby("u").agg(y=("y", "first"), h=("h", "first"), i=("i", "min"))
key = p.person_spm_unit_id.map(first.y.astype(str) + ":" + first.h.astype(str) + ":" + first.i.astype(str)).to_numpy()
asec = (p.person_support_channel == "asec").to_numpy()

raw = p.A_FNLWGT.astype(float).to_numpy()
standins = {
    "A_FNLWGT, all rows": raw,
    "A_FNLWGT, ASEC rows only": raw * asec,
    "Equal weight per ASEC person": asec.astype(float),
    "Released weights (the prototype's own basis)": w,
}

units = pd.DataFrame(dict(k=key, we=w * elig, a=rep)).groupby("k").agg(we=("we", "sum"), a=("a", "any"))
draws = {}
for s in range(1, seeds + 1):
    draws[s] = np.array(
        [int.from_bytes(hashlib.blake2b(f"{s}:{VAR}:{k}".encode(), digest_size=8).digest(), "big") / 2**64 for k in units.index]
    )

kish = units.we.sum() ** 2 / (units.we**2).sum()
print(f"Units by source identity: {len(units)}; effective units, Kish, eligible people on released weights: {kish:.1f}")
free = ~units.a.to_numpy()
print(f"Non-anchored eligible people: {units.we[free].sum():,.0f}; anchored: {units.we[~free].sum():,.0f}")
for name, d in standins.items():
    d = d / d.sum() * w.sum()
    de = pd.DataFrame(dict(k=key, de=d * elig)).groupby("k").de.sum().reindex(units.index).to_numpy()
    a = units.a.to_numpy()
    we = units.we.to_numpy()
    out = []
    for s in range(1, seeds + 1):
        idx = np.where(~a)[0]
        order = idx[np.argsort(draws[s][idx])]
        cum = np.r_[de[a].sum(), de[a].sum() + de[order].cumsum()]
        n = int(np.abs(cum - TARGET).argmin())
        flag = a.copy()
        flag[order[:n]] = True
        out.append(we[flag].sum())
    out = np.array(out)
    lo, hi = np.quantile(out, [0.05, 0.95])
    within = np.mean(np.abs(out / TARGET - 1) <= 0.02)
    print(
        f"{name}: mean {out.mean():,.0f}, SD {out.std():,.0f}, 5th to 95th {lo:,.0f} to {hi:,.0f} "
        f"({lo / TARGET - 1:+.1%} to {hi / TARGET - 1:+.1%}), seeds within 2%: {within:.0%}"
    )

axiom: TheAxiomFoundation/rulespec-us#2140 queued

TheAxiomFoundation/rulespec-us#2066 covers the amounts.

🤖 Generated with Claude Code

…-up input

The microsimulation pays the dividend to every Alaska resident: 756,512 people
for 2027, against 618,863 applications Alaska paid for 2025. AS 43.23.005(a)(5)
limits the dividend to citizens, lawful permanent residents, refugees and
asylees, so an eligibility variable now applies that test.

The statute's other tests (an application, residence during all of the prior
year, absences, felony bars) have no PolicyEngine inputs, and Core forbids
formula-time draws. A boolean input that defaults to true, like the other
programs' take-up flags, lets microdata or a user say Alaska did not pay a
person. Household calculations keep paying each eligible resident.

Energy relief shares the gate because Alaska pays it as part of the dividend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 4 commits November 9, 2026 06:01
Draw any state in the Hypothesis properties, so invariant 4 (nobody
outside Alaska gets paid) holds for more than Washington. Point the
validation doc at microcosm#1551 and say why is_incarcerated does not
feed the dividend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The eligibility variable's documentation now matches the validation
doc: the model has no input for conviction-based incarceration during
the qualifying year, even though is_incarcerated exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
State v. Andrade and 15 AAC 23.154 read "lawfully admitted for permanent
residence" as legal presence plus the ability to intend to remain, so the
parameter comment, docs and variable documentation now say that and give
the reason each excluded status stays out. OAH 25-3180-PFD covers fixed-term
parole.

A child born during the dividend year cannot qualify, but age 0 does not
show that: Populace's age is ASEC A_AGE, measured as of a February to April
survey date, so most records aged 0 were born the year before. The tests now
draw ages and pin that a resident aged 0 stays paid unless an input says
otherwise.

Add YAML cases for an undocumented child and for a supplied zero dividend
that leaves energy relief in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
15 AAC 23.154(e) tests status on the application date, alongside the
qualifying year that OAH 25-3180-PFD applies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review November 9, 2026 13:04
@codecov

codecov Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (aae7f98) to head (b16079b).
⚠️ Report is 141 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main    #10763   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            1         4    +3     
  Lines           37        49   +12     
=========================================
+ Hits            37        49   +12     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

MaxGhenis and others added 2 commits November 9, 2026 12:13
The dividend docs and the eligibility variable's documentation said why a
person "is incarcerated" and that a child "was born" in a year. They now
say that `is_incarcerated` records no conviction, and they give children's
birth dates instead. No formula, parameter or test changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The children-under-one section now says how a record's survey date maps
to the dividend year. If each record keeps its own survey date, 92% to
96% of the people aged 0 in Populace's 2024 Alaska records have birth
dates in 2022 or 2023 and can qualify under AS 43.23.005(c); if every
survey fell in the dividend year, 71% to 87% could. `birth_year` would
put all of them in the dividend year.

The section also says that a child born during the qualifying year
qualifies only through an eligible parent, which the model does not
check, and the TPS bullet says that Andrade did not address TPS. The
eligibility variable's documentation adds the parent test to the list of
tests the take-up input covers.

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

Copy link
Copy Markdown
Contributor Author

Handing this PR to the US + core hub. Only CI remains.

Text only. The Alaska Supreme Court held Andrade, the Department of
Revenue applies its reading in 15 AAC 23.154(a), an alien's qualifying
year starts under 15 AAC 23.154(c), a person coded DACA holds no status
under 8 U.S.C. 1101 to 1189, and PolicyEngine calculates both payments.

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

Copy link
Copy Markdown
Contributor Author

I'm taking this PR back from the US + core hub for a text-only commit, 2d0402dda8. It names who acts in the validation doc, the parameter comment and two documentation strings: the Alaska Supreme Court in Andrade, the Department of Revenue in 15 AAC 23.154 and PolicyEngine in the docstrings. Once the reviewer agrees at the new head, I'll hand the PR off again.

The administrative law judge upheld the Division's denial in OAH No.
25-3180-PFD; the decision itself denied nothing, so the docs and the
parameter comment now say who acted. Test names and docstrings take
PolicyEngine or Alaska as subject where a document stood in before.
Text only: no formula, parameter value or test expectation changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis MaxGhenis changed the title Limit Alaska's dividend to the statuses the statute names, and add a take-up input Limit Alaska's dividend to the statute's immigration statuses, and add a take-up input Nov 11, 2026
MaxGhenis and others added 2 commits November 11, 2026 00:27
The parameter comment and the validation doc now name the Permanent Fund
Dividend Division at first mention. The property tests' docstring says
they catch a changed status list only in a year they test, and Case 5's
name covers the codes PolicyEngine leaves unmapped for other reasons.
Text only: no formula, parameter value or test expectation changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docstring said the tests catch a changed status list "in a year they
test", where "they" could read as the person editing. It now says "in a
year these tests cover". Docstring only.

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant