Skip to content

Lock the US engine to policyengine-us 2.60.4 - #1365

Draft
MaxGhenis wants to merge 24 commits into
mainfrom
engine-lock-pe-us-2.60
Draft

MaxGhenis wants to merge 24 commits into
mainfrom
engine-lock-pe-us-2.60

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Moves main's US engine lock from policyengine-us 2.2.1 (core 3.32.5) to policyengine-us 2.60.4 on policyengine-core 3.33.3, so the next certified US Route A build calibrates and scores natively on the current engine. No certified build is started here.

Why

The certified d1386 release (8b0c195) claims policyengine-us==2.2.1. PolicyBench v2 waits for a policyengine.py bundle certified with 2.60.x (ruled 2026-10-23). The short-term bridge is a compatibility_recertification source-enrichment child that the PolicyBench session is building separately; this PR does not touch microcosm.data.contract, source_enrichment, or that tool. This is the long-term half.

Versions (verified on PyPI 2026-10-23)

  • policyengine-us 2.60.4, the newest (uploaded 2026-10-23 09:48Z). It requires core >=3.32.27 and spm-calculator >=1.0.0,<=1.0.0.post1.
  • policyengine-core 3.33.3, not the newest 3.33.12. US release manifests claim core with ==, and policyengine.py's certification requires the runtime core to be the build core or covered by that claim. policyengine.py 6.10.1 pins core 3.33.3, so a 2.60.x bundle can match without a second core bump, and the UK fork stays on its locked 3.33.3. Core 3.33.4 to 3.33.12 (rate-scale, set_input cache and dataset-membership fixes) is a follow-up to coordinate with policyengine.py.
  • spm-calculator stays 1.0.0 (policyengine.py's pin).
  • policyengine.py is not a workspace dependency, so no policyengine.py pin moves here.

The lock diff moves only policyengine-us and drops the separate 3.32.5 core entry. APPROVED_UV_LOCK_SHA256 follows.

What 2.60.4 changes for Microcosm

The full surface diff (6,167 to 6,797 variables) was cross-referenced against every tracked file. Evidence is in ~/PolicyEngine/_build_artifacts/engine-lock-pe-us-2.60/.

Removed inputs Microcosm produced: first_home_mortgage_interest, second_home_mortgage_interest (tax unit; policyengine-us#9605).

  • 2.2.1 read them only where no person-level home_mortgage_interest was reported.
  • On the d1386 release H5, first_home_mortgage_interest equals the person-level sum in every one of the 64,774 units that carry it, second_home_mortgage_interest is zero everywhere, and no unit relied on the fallback. The deduction base is therefore unchanged.
  • The PUF tax-detail QRF stops producing both legs. The ACS donor targets, stacked battery, degenerate-column exclusions and spec bundle follow.
  • The eCPS parity reference still carries the layers. REFERENCE_ECPS_RETIRED_LAYERS names their successor (home_mortgage_interest). The parity gate skips them and records the skip, and the input-coverage manifest drops the requirement (167 to 166 required columns).
  • Because the QRF conditions each target on the ones before it, the predictors for the later origination-year and HSA targets change.

New engine inputs Microcosm already writes: tax_unit_role_input, parent_1_id, parent_2_id.

  • They leave the stored non-variable register (51 to 48 entries).
  • d1386 already satisfies 2.60's contracts. Every tax unit has exactly one HEAD and at most one SPOUSE, with every member supplied (a malformed unit makes the engine raise for the whole simulation). All 523,150 parent pointers name a co-resident. One person has person_id 0 and cannot be named as a parent; that was already documented in eligibility_inputs.

New take-up flag: takes_up_medicare_part_d_if_eligible (person, default true).

  • Added to the take-up contract as rate_unsourced at the engine default. The ASEC has no Part D item; CMS publishes enrollment counts, so a count-calibrated stage is the follow-up.
  • 2.60 gates the Part D IRMAA surcharge on Part D enrollment (#10115). With the flag at its default, every Medicare enrollee above the thresholds is still charged, as on 2.2.1.

IRMAA lookback (#10106).

  • A single-year release now gets the earliest input year's income, deflated by per-capita CBO AGI, instead of zero.
  • The holdout's single-year test now expects the Trustees top tier: $543,850 of lookback MAGI from $600,000 of 2024 wages gives $419.30 (Part B) and $81.00 (Part D) a month.

Other 2.60.4 model changes the pins follow (each checked against the 2.60.4 source).

  • HUD gross rent now includes a utility allowance for the counties 2.60 encodes (LA County, the Texas TDHCA area, four Kansas PHAs). A one-bedroom renter in county 06037 with $12,000 rent gets $3,252 a year of allowance, so the housing payment rises from $11,975 to $15,227. The allowance also reads bedrooms, tenant_pays_utilities and is_sro, which the release does not store (engine defaults apply).
  • Alimony paid is deducted through a person-level alimony_expense_ald_person, leaving dependents out. The pre-2019 divorce-year test is unchanged, and divorce_year still defaults to 2010, so stored alimony stays deductible.
  • weeks_unemployed gains a live consumer, unemployment_compensation_months, for SNAP's work-registration exemption. The release already stores the column.

Reform-coverage probes pinned whole 2.2.1 parameter lists.

  • On 2.60.4 they would also have stripped about 45 state SSP items from SNAP unearned income, and undone the educator_expense_ald and traditional_ira_deduction renames, so each probe would have measured a different reform.
  • All nine lists are re-derived from the 2.60.4 baseline with only their own item changed. The educator probe now removes educator_expense_ald. Every reason text that states an engine fact was re-checked on 2.60.4.

Post-export watched-formula lists.

  • US_POPULATION_AGGREGATE_VARIABLES and POST_EXPORT_BASELINE_BRANCH_READERS are unchanged on the 2.60.4 scan.
  • The batched-materialization drift scan finds 30 new formulas. I read each one: 17 compare a record's own status or county with parameter lists (np.isin). The other 13 are household-local joins: parent links resolved among co-residents, family-keyed Wisconsin CTS units, SSI deemor pools, and an error-count .sum().
  • None joins the population-aggregate list. The reviewed-source digest moves.

Static consumer index (frame adapter; it gates every generator).

  • 2.60.4 had five formula sites it could not resolve. It now reads:
    • starred list items, from module constants and parameter lists;
    • one-argument dict.get with an is None skip;
    • a run-time key into a fully known name table (every value counts as a possible read);
    • helper return values, including tuple unpacking.
  • 2.60.4 also exposed four ways the index missed reads without raising. All four are closed, each fails closed on what it cannot resolve, and each has synthetic tests:
    • Package helpers outside variables/. 2.60 moved 77 tax-unit sums onto tax_unit_non_dep_add (tools/general.py), so net_capital_gain lost its form 4952 reader, for example. Every helper the formulas call now needs a review in _policyengine_us_helper_reviews.py: which arguments name variables it reads, what its body reads on its own, and reads made only when an argument is omitted. All 41 helpers 2.60.4 calls were read and reviewed. Each review pins a digest of the helper's code, every package function it uses and the module constants they read (docstrings excluded). An unreviewed or changed helper stops the index. The same reviews cover class-body factories (formula = sum_among_non_dependents("earned_income")).
    • Simulation reads. branch.calculate(name), get_holder(name) and simulation.get_array(name) now record simulation_read receipts (21 calculate sites on 2.60.4, 11 on 2.2.1).
    • Local functions and lambdas. A function defined inside a formula is interpreted at each call, in the caller's scope; one used as a value is interpreted where it is defined, with unknown arguments. Lambda bodies are scanned. This covers the IRMAA lookback's provided() and Oklahoma's applied().
    • Opaque comparisons. period.start.year != 2021 used to count as exactly true, which hid Alabama's 2021 Part II recompute. Equality and membership tests now decide a branch only when both sides are exact data.
  • Result on 2.60.4: 771 receipts added, none removed. No generated pin moves (audit, pool contracts, bundle, coverage manifest, concept golden, spec-engine coverage, parity fixture are all current), because none of the new edges reaches those surfaces.
  • test_us_pool_input_consumers.py moves from engine_free to engine_contract. It skipped itself when the engine was missing, so CI never ran it, and its 2.60.4 pins had drifted. tools/ci_test_plan.py now rejects that skip pattern.

SSI closure grows from 54 to 96 input leaves. 59 of them run at engine defaults (up from 18): state UI wage inputs (modelled UI applies only when no reported amount exists, and pays nothing at zero wages), SSI living-arrangement and deeming detail, and some pension splits.

Re-derived pins (all observed and written verbatim, none hand-derived)

  • Generated-variable audit: 119 to 120 variables (mt_agi).
  • Engine ABI lock: 18 take-up programs.
  • Pool contracts:
    • SSI closure 54/63/187 to 96/121/351 (input leaves / formula nodes / edges);
    • engine input projection 926 to 1,074 inputs;
    • remaining-stage manifest 1,250 rows.
  • Spec bundle (--check passes; spec_sha256 5ff97a0b).
  • Frozen generation-0 resources.
  • Release input-coverage manifest.
  • Concept-coverage golden (mapping reviewed against 2.60.4; it adds 148 uncovered inputs).
  • Spec-engine coverage: 43,674/43,674 fields, 41/41 inventory checks.
  • H3 parity fixture: it gains the Part D column, and expected.csv is byte-identical.

Invariants (executed)

  • Static index superset. On 2.60.4 the new index's receipts include every receipt main's index records (771 added, none removed).
  • Static index on 2.60.4. It resolves every site, every helper the formulas call has a current review, and no review is stale (test_policyengine_us_helper_reviews.py).
  • Reviewed helper reads (property-based). A reviewed helper's call reads exactly the listed variables and the helper's own reads, for any list.
  • Helper digests. A digest ignores docstrings and moves when the helper's code or a constant it reads changes; a changed helper fails until reviewed again.
  • Starred display (property-based). It reads exactly the union of its parts, for any lists.
  • Fail-closed cases. A helper returning an engine value, a run-time key into get, an unreviewed or changed package helper, a run-time name in calculate, a local function or lambda passed a run-time name, and a class-body call to a formula-module factory all raise.
  • Reference-layer retirement (property-based). It partitions every share map: each layer is kept or retired, never both. It retires only registered layers, onto their successor, and refuses an engine that still defines the layer or lacks the successor.
  • Reform probes. Each list-valued probe differs from the installed engine's baseline by exactly its declared item (the existing drift test, checked on 2.60.4).
  • Mortgage conservation. On d1386, the 2.60 total (home_mortgage_interest + second_residence_mortgage_interest) equals 2.2.1's home_mortgage_interest_tax_unit in every tax unit. This is a data property of the certified file, verified directly.
  • Lock. Every US install path resolves policyengine-us 2.60.4, core 3.33.3 and spm-calculator 1.0.0, and every US+UK combination is refused (test_engine_core_pins.py).

Consequences while main is ahead of the certified default

Main's loader enforces each release's == claim. So on this lock, microcosm.data.load("us") refuses the 2.2.1-certified default until the recertification child (or a native 2.60.x build) exists.

Follow-ups (not in this PR)

  • Coordinate core 3.33.12 with policyengine.py.
  • A count-calibrated Part D take-up stage.
  • Declare tax_unit_role_input on the pool input surface, so the SSI agreement simulation reads the constructor's roles. It currently falls back to age ordering, as 2.2.1 did.
  • Store bedrooms (and consider tenant_pays_utilities), which 2.60's HUD utility allowance and the payment standard read; the release leaves them at engine defaults.
  • spm-calculator defines variables at run time (spm_measurement_adults, spm_measurement_children) that the source index cannot see, so closures stop at them.

Status

Draft. CI on the latest pins and the Sol review are pending (Subfleet lanes reopen about 02:00Z).

axiom: n/a: engine lock and data-pipeline compatibility; no policy rule changes.

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 23, 2026 07:22
Move every extra that installs policyengine-us to >=2.60.4,<3 and its core
pin to 3.33.3, the core policyengine.py's bundle certifies against. The lock
changes only policyengine-us (2.2.1 -> 2.60.4) and drops the separate 3.32.5
core entry; spm-calculator stays at 1.0.0. APPROVED_UV_LOCK_SHA256 follows
the new lock. Compatibility fixes follow in later commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-us 2.60 writes ["adjusted_gross_income", *TAX_UNIT_ADDITIONS].
A starred item now contributes its items when it evaluates to an exact
sequence; anything else stays unknown and still fails closed. Four more
2.60.4 sites remain unresolved (see PR body).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 22 commits October 23, 2026 09:42
…-up flag

The static consumer index now resolves the five 2.60.4 sites it refused:
starred list items (module constants and parameter lists), one-argument
dict.get with an `is None` skip, a run-time key into a fully known name
table (every value is a possible read), and helper return values bound by
assignment, including tuple unpacking. Return values are evaluated with
sinks deferred; the enqueued helper context still checks them strictly. On
the policyengine-us 2.2.1 source the index's receipts are byte-identical to
main's (5,729 targets, 17,772 receipts).

policyengine-us 2.60 adds takes_up_medicare_part_d_if_eligible (person,
default true). It joins the take-up contract as rate_unsourced at the
engine default, which keeps the Part D income-related premium on every
enrollee above the thresholds, as 2.2.1 charged it. The generated-variable
audit is regenerated for 2.60.4 (119 -> 120 variables; mt_agi).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-us 2.60 removed the tax-unit first/second_home_mortgage_interest
inputs (policyengine-us#9605). The PUF tax-detail QRF no longer produces
them; the person-level home_mortgage_interest already carried the whole
amount. On the certified d1386 H5, first_home_mortgage_interest equals the
person-level sum in every tax unit, second_home_mortgage_interest is zero
everywhere, and no unit relied on 2.2.1's structured fallback, so the
deduction base is unchanged. The late transfer surface loses that target
(70 -> 69; terminal stacked surface 134 with the Part D take-up flag added).

2.60 also defines tax_unit_role_input, parent_1_id and parent_2_id as
inputs, so they leave the stored non-variable register (51 -> 48 entries);
the engine-contract premise test now pins their 2.60 shapes.

The batched-materialization drift scan finds 30 new formulas in 2.60.4.
Each is triaged as constant membership (17 np.isin over parameter or literal
lists) or a household-local join (parent links resolved among co-residents,
family-keyed Wisconsin CTS units, SSI deemor pools, an error-count .sum());
none joins the population-aggregate list. The reviewed-source digest moves.

The concept mapping is reviewed against 2.60.4: every binding still names
a pure input; the coverage golden adds 2.60's 148 new inputs as uncovered.

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

The list-valued reform-coverage probes pinned whole 2.2.1 parameter lists.
Applied on 2.60.4 they would also have stripped about 45 state SSP items
from SNAP unearned income and undone the educator_expense_ald and
traditional_ira_deduction renames, so each probe would have measured a
different reform. Every list is re-derived from the 2.60.4 baseline with
only its own item changed; the educator probe now removes the renamed
educator_expense_ald leaf. Reason texts that state engine facts were
re-checked on 2.60.4 before moving their version.

The eCPS parity reference still carries the retired first/second home
mortgage interest layers. REFERENCE_ECPS_RETIRED_LAYERS names their live
successor (home_mortgage_interest): they are no coverage requirement, the
parity gate skips them and records the skip, and the retirement refuses an
engine that still defines them or lacks the successor.

The Medicare IRMAA holdout now expects the top tier for a single-year
household: 2.60 includes policyengine-us#10106 and falls back to the
earliest input year's income, deflated ($543,850 from $600,000 of 2024
wages). Engine version guards, the known-gap evidence records (facts
re-verified), the core-pin test, the OBBBA premise pin, docs and the
changelog move to 2.60.4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
repin_us_pool_engine_contracts.py and generate_us_bundle_from_constants.py
rewrote every engine-derived pin; nothing was set by hand:

- SSI dependency closure 54/63/187 -> 96/121/351 (input leaves / formula
  nodes / edges); 59 of the 96 leaves are declared absent engine inputs, up
  from 18, so they run at their engine defaults.
- Engine input projection 926 -> 1,074 inputs (1,073 defaults); the
  remaining-stage manifest has 1,250 rows.
- The engine ABI lock covers 18 take-up programs (Part D added).
- The spec bundle drops the two retired mortgage outputs (PUF QRF 65 -> 63
  targets, late DAG 70 -> 69, producer graph 2,744/227/213 -> 2,741/224/211,
  catalog 176 -> 175 keys) and the frozen generation-0 resources
  (source_stages.json, take_up_contract.json) are re-attested. The PUF QRF
  target-order digest follows the shorter order. --check passes;
  spec_sha256 5ff97a0b.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
first_home_mortgage_interest leaves the required surface (167 -> 166);
the nine re-derived reform probe lists and the re-verified reason texts
land in the manifest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every value below was observed at this head and written verbatim:

- field usage: 43,674 configuration fields (32,352 authored, 11,322
  resolved; was 42,184 / 32,404 / 9,780), seven claim pointer pins, mode
  and generation-0 effect counts, and the pointer-inventory digest.
- inventory coverage: 41/41 again. The moved digests and counts follow the
  retired mortgage outputs (PUF QRF 63 targets/tuples, late 69, producer
  inputs 2,741, compiled outputs 224, tail control fields 930, catalog 175
  columns) and the Part D take-up program (18 programs, 29 steps); the seed
  protocol moves because it attests puf_support, puf_qrf_chain and
  acs_transfer.
- docs/evidence/spec-engine/us-f0-coverage.json regenerated by
  tools/spec_engine_coverage.py; the US spec_sha256 is 5ff97a0b, and the
  minimal-bundle loader golden, schedule (50aa8412) and payload (c04c59fa)
  digests follow.

The H3 US post-transfer parity fixture is regenerated with
tools/graph_us_post_transfer_fixture.py under 2.60.4: it gains the Part D
take-up column and the take-up digest moves; expected.csv is byte-identical,
so derive/seed/materialize outputs are unchanged by the engine move. The
tool now loads its helpers from test_support (the tests were reorganized
into category directories).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1,250 rows (34 derive, 34 seed, 1,182 simulate; the Part D take-up flag is
the new seed row) over a 1,074-input engine projection with 1,073 defaults,
as repin_us_pool_engine_contracts.py derived them.

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

The consumer index reads exactly the union of a starred display's parts for
any lists, and the eCPS reference retirement partitions any share map into
kept and retired layers, retiring only registered layers onto their
registered successor.

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

The manifest currency check now leaves the retired mortgage layers out of
the required reference surface, as the generator does. The shipped-probe
tests pin the re-derived 2.60.4 lists; the battery has one more boolean
target (Part D take-up) and one fewer monetary target (the retired
mortgage leg); the calibration legacy projection digest follows the
shorter calibrated-input list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The post-PUF completion receipt and its labels now name the 69-target late
surface; the stacked-spine tests follow the Part D take-up flag (one more
seeded boolean and battery target) and the retired mortgage leg (one fewer
monetary target, two fewer late occurrences), and the lineage tests follow
the 63/69-target imputation families (180 imputed variables, 129 amounts).

policyengine-us 2.60 treats California as a state that excludes child
support paid from SNAP gross income (the state parameter flipped), so the
$200 monthly payment moves from snap_child_support_deduction to
snap_child_support_gross_income_deduction. The engine test now asserts the
invariant: the countable payment is $200 and the exclusion plus the
deduction count it exactly once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- educator_expense is uprated by per-capita CBO AGI in 2.60, the ALD list
  names educator_expense_ald, and above_the_line_deductions sums that list
  in a formula; the contract and abolition-probe tests say so.
- The spec-engine coverage tool test pins 2,741 producer inputs and 930
  tail control fields.
- The late-transfer completion receipt fixtures carry 69 targets, and the
  legacy publication manifest golden moves with the engine identity surface
  (pool_h5 and agreement digests are unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The pool's remaining-stage manifest has 1,250 rows (34 derive, 34 seed,
  1,182 simulate) and digest 7681ca45 in the ABI lock tests.
- The post-transfer graph's create node owns 77 columns and its seed node
  18 (the Part D take-up flag).
- The frame metadata index reads 6,797 definitions, 1,074 inputs and 5,723
  formula-owned outputs; mt_agi joins the generated state MFS variables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same nine counts the spec-engine inventory moved: 63 primary targets
and tuples, 69 late targets, 224 compiled outputs, 2,741 producer inputs,
930 tail control fields, 18 take-up programs, 29 pipeline steps and 175
typed columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The declared surface, post-PUF transfer surface, metric registry and late
producer schedule digests follow the Part D take-up target and the retired
mortgage leg.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 transfer targets (69 late, 42 PUF-clone producers), 141 pool input
surface rows, 18 take-up programs, a 63-target primary QRF whose last
target sees 70 predictors, and the transfer target-name digest.

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

The examined files now also refuse the retired tax-unit mortgage legs on
each tax-unit table and register one fewer entry (tax_unit_role_input is an
engine variable). The take-up semantics test counts 18 programs; the PUF
support fixtures and pool consumer expectations drop the mortgage legs.

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

On policyengine-us 2.60.4 the static consumer index missed reads it could not
see and raised nothing:

- 77 tax-unit sums moved onto tax_unit_non_dep_add, a package helper outside
  variables/ (net_capital_gain lost its form 4952 reader). Every helper the
  formulas call now needs a review in POLICYENGINE_US_HELPER_REVIEWS naming
  the arguments it reads and its own reads, pinned to a digest of its code,
  the package functions it uses and their constants. An unreviewed or changed
  helper stops the index. Class-body factories (all_of_variables,
  sum_among_non_dependents) are read the same way.
- branch.calculate(name), get_holder(name) and simulation.get_array(name)
  record simulation_read receipts (21 calculate sites on 2.60.4).
- Functions defined inside a formula are interpreted at each call in the
  caller's scope (the IRMAA lookback's provided(), Oklahoma's applied()); one
  used as a value is interpreted where defined. Lambda bodies are scanned.
- An equality or membership test on an opaque value (period.start.year !=
  2021) no longer decides a branch, which had hidden Alabama's 2021 Part II.

On 2.60.4 the receipts are a strict superset of the previous index's (771
added, none removed). The pool input consumer guard moves to engine_contract
so engine-us runs it; it had skipped itself in engine-free CI, and the test
plan now rejects that pattern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Catalog and typed closure: 175 columns (two retired mortgage legs, one Part
  D flag), units 87 undeclared / 52 boolean, 224 compiled producer outputs.
- Canonical battery: 369 metric legs, 78 monetary and 52 incidence metrics.
- Alimony: 2.60 deducts each non-dependent's eligible alimony through
  alimony_expense_ald_person; the divorce-year test is unchanged.
- weeks_unemployed gains a live consumer, unemployment_compensation_months
  (SNAP work registration exemption), alongside the four blocked UI programs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-us 2.60.4 adds the published LA County utility allowance to HUD
gross rent: $3,252 a year for the probe's one-bedroom, so the payment is
$12,000 + $3,252 - $25 = $15,227 (2.2.1: $11,975).

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