Skip to content

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

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

MaxGhenis wants to merge 8 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 paid the Alaska Permanent Fund Dividend and energy relief to everyone who lives in Alaska: 756,512 people for 2027, about 19% more than Alaska pays. AS 43.23.005 pays only people who apply and pass its residence, presence, immigration status and Selective Service tests, and (d) bars 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 the model's inputs can resolve;
  • 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. PolicyEngine/microcosm#1551 asks the data pipeline to set the flag and to take reported dividends out of miscellaneous_income (PolicyEngine/microcosm#1443). A prototype flag, calibrated to Alaska's 2024 count on the file's released weights, matches that count by construction and shows what such a flag does to totals and poverty.

Who Alaska pays

The PFD Division's summary of dividend applications and payments gives:

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%

The Division's 2024 annual report 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, so this PR cites each source for its own figures.

Where each piece belongs

The status test belongs here. The statute names the statuses, and the model already has immigration_status. State v. Andrade, 23 P.3d 58 (Alaska 2001), reads "lawfully admitted for permanent residence" as legal presence plus the ability to intend to remain, rather than the federal definition, and 15 AAC 23.154(a) applies that to a status under 8 U.S.C. 1101 to 1189 that allows domicile. docs/validation/alaska-pfd.md gives the reason for each status. Populace's Alaska records hold 714,298 citizens, 15,852 permanent residents, 9,894 undocumented people and almost no DACA recipients, so the codes the law leaves open don't move these totals. Populace measures citizenship but imputes the non-citizen labels, so the status test's count inherits that imputation's error.

Age can't carry the birth-date test. A child born during the dividend year can't qualify: the child fails (a)(3), and (c) reaches only children born in the two calendar years before it. 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%

The figures assume births spread evenly over the year before the survey. The ranges run from mid-February to mid-April interviews, and the counts use mid-March. The first dating leans on the release weights, which give the 2025 survey 0.3% of the age-0 weight but 25% of all Alaska weight; with the age-0 weight spread over surveys like all Alaska weight, 68% (65% to 70%) can qualify. Either way, a gate at age 1 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. Other programs handle this the same way: default-true inputs such as takes_up_snap_if_eligible and takes_up_medicaid_if_eligible, which 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. ASEC rows carry 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. So the first Microcosm engine bump past a release containing this PR has to add, in the same PR, a contract row for the new flag, an 18th program in the spec, a count of 18 and a regenerated us/engine_abi.lock.json. PolicyEngine/microcosm#1551 covers it.

Impact

Every recipient count, dollar total and poverty rate comes from running Microsimulation, 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. Only Alaska's formulas change, so these records give what the full file gives. 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, with no flag in the data. This column shows what merging changes today.
  • With prototype flag: this branch reading a flag that a prototype set, averaged over 20 seeds, with the range across seeds in parentheses. The prototype flags appear in no Populace release; they exist 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. 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 for that reason.
  • Reported dividends removed: the same flags, with matching reported dividends also taken 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

The 2026 Alaska figure gives an estimate rather than a count of people paid: it divides the Legislature's $127,340,800 estimate by the $200 energy relief. The prototype matches 2024 by construction. Later years show the same flags at uprated 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. The prototype's 84.3% of Alaska's people follows from calibrating to 624,489.
  • 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. On Populace's Alaska records, each of the 60 records of children aged 0 or 1 who pass the status test (18,402 people) shares its SPM unit, tax unit and household with an adult who passes it too, so a rule requiring such an adult would move nothing here. (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 held during the qualifying year and on the application date, and PolicyEngine has no status history.
  • National population uprating grows Alaska's weights (740,043 people for 2024, 756,512 for 2027), while the Division's population fell 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 a status the statute names, and take it up. A status or flag left unset counts as citizen and 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 on purpose, so a parameter change the statute doesn't support fails. 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. Andrade, 15 AAC 23.154 and the Division's immigration page read the status test as legal presence plus the ability to intend to remain, and OAH No. 25-3180-PFD applies that reading to a fixed-term parolee. They may differ in one place: Andrade's test alone might reach a TPS holder, but the court didn't address TPS, and the regulation's range leaves it out. The parameter follows the regulation, and Populace's Alaska records hold no TPS holders, so nothing moves either way. The parameter maps the model's codes onto that reading. PAROLED_ONE_YEAR doesn't say a parole runs indefinitely. TPS (8 U.S.C. 1254a) and DEPORTATION_WITHHELD (8 U.S.C. 1231(b)(3)) fall outside 8 U.S.C. 1101 to 1189, and DACA isn't a status under those sections. UNDOCUMENTED doesn't meet Andrade's legal presence requirement. 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 peaked at a 6.45 GiB resident set by GNU time; 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):
    # float64: the model stores float32, which rounds a weighted sum by about $50.
    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. Its output is in no Populace release.

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 keyed by stable source identity, as
  microcosm's take_up.py keys them, 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). ASEC-channel rows carry 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 that equals one to eight dividends 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 with reported dividends
removed from 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)
"""How specific is the amount rule? Apply it to every state in the full file.

A record matches when OI_VAL falls within $100 of one to eight dividends paid in
its source year or the prior year (the seed_flag.py 'tolerant' rule), unless its
OI_OFF code is 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 is 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))
"""How concentrated are 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 is the ASEC income year (AsecSource.year, passed as
income_year in microcosm's asec_pool.py). 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 (c) reaches only children born in the two calendar years
before the dividend year. A child born earlier can qualify under (c) or (a).

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())
    # The same age-0 total spread 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 surveyed in the year before it, born 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?

AS 43.23.005(c) makes a child born during the qualifying year eligible only
through a parent who is 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 a stand-in for
stage-time weights scaled to the released total, then counts eligible flagged
people on the released weights. The release has no design-weight column, so these
are stand-ins, and this does not 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 (2d0402d).
⚠️ Report is 93 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.

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