Skip to content

Normalize donor money with exact declared factors - #1395

Merged
MaxGhenis merged 35 commits into
mainfrom
nzhub/g8w2a-money
Oct 29, 2026
Merged

MaxGhenis merged 35 commits into
mainfrom
nzhub/g8w2a-money

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Targets main and lands after #1394. The UK Budget hold on shared compile-module hashes, which applied because implementation hashes that cover the edited modules move, ended at 2026-10-29 12:00Z. Review round 1 asked for changes; they are in b675b7a and 4f333f2, and the delta review approved them with wording nits (see Review). Head db02ae8: 044f179 merges #1394's head, which carries #1382's fix4 (Belgium OK'd it 10/26), and db02ae8 applies the two wording nits.

What this does (G8-w2a)

Adds exact, declared normalization of donor money: the USD-to-NZD conversion and the ageing of donor-period amounts, applied once per lineage. No NZ graph node runs it yet; G8-w3 wires it.

  • Shared module, money_normalization.py. It validates a complete declaration with no defaults:
    • a cited decimal-text rate, or a feed-replayed rate with its expected decimal, and an explicit quote direction;
    • half-even rounding at both stages, and the donor money period with its basis and evidence;
    • the one target_period every applied factor must age money to;
    • every transported money concept in exactly one roster (normalized, quantile-mapped or pending). A concept dropped in transport can't be normalized, though a quantile-mapped or pending row may name one;
    • per-family factors for each profile, or an explicit identity with a reason.
    • It applies the declaration with exact fractions for both rounding stages and the receipt sums. Non-finite amounts refuse, and every cents companion is checked against int64 bounds at both stages.
  • Transport kernel, money.normalize@1 (transport/money_kernels.py):
    • replays its selected factors against the authenticated feed;
    • refuses a donor money period that differs from CREATE's, or is null;
    • writes exactly its declared columns, each with an integer-cents companion and an exact receipt;
    • the profile knob selects each variant's factor explicitly. An absent knob selects the declared central profile; an unknown profile, or a family missing a profile's factor, refuses.
    • Its implementation hash binds the pure module and reference_factors. It is registered with the population kernels and admitted to SKELETON_KERNELS.
  • CREATE and the donor reader.
    • DonorBank.time_period reads the donor H5's _time_period: a one-row integer series, as the exporter writes it, or absent. A malformed value refuses.
    • CREATE records it as donor_time_period in its metadata and receipt, in every country (Belgium's ruling: general, not opt-in).
  • Producer checks (opt-in). Only a spec that declares a money_normalization resource is checked:
    • activation names every consumed money column without exactly one producer;
    • after each graph pass of the ancestry guard, compose refuses a second normalization along structural population inheritance, including a currency conversion followed by normalization;
    • cross-population evidence edges don't change money ownership, and new destination outputs count as derived money.
  • Factors. reference_factors gains the additive money_normalization_input purpose, and a public resolve_feed_operand, which resolves the feed-selected money rate. Its validity rules are untouched.
  • NZ.
    • money_normalization.json is the single source of the USD-to-NZD rate; currency_bridge.json points to it.
    • The generator adds three calendar-2024 donor factors (central, high, low), with the existing factors' validity. The existing factor records and the v1/v2 reference receipts are unchanged.
    • Ten columns are normalized and three are quantile-mapped. The declared target_period is 2027 (tax year 2026-27), the target of the donor factors and of every reference-profile factor. The scenarios' default aging_index is the declaration's central profile. The declaration is inactive: no NZ graph node reads it yet.
    • Ageing non-wage money by AWE growth is disclosed per family as provisional, pending a methodology decision. The factors use Stats NZ QES observations through June 2026, then Treasury PREFU 2026 forecast AWE growth (2.7%, 0.5 points lower and higher for low and high).
    • Ruled 2026-10-26 (d1981, option A): AWE ageing of the listed non-wage donor columns is adopted as a disclosed interim convention, with hold-fixed (factor 1) run beside it as a pre-declared sensitivity. This PR's code already applies A. A follow-up after the stack lands changes the "provisional pending Max's ruling on non-wage ageing (H3)" disclosure strings in nz/money_normalization.json (and the test that pins them) to cite d1981, and checks that the hold-fixed sensitivity is wired.
    • The golden changes only identity fields: the fingerprint, the resource list and four resource hashes.

Invariants (tested)

  1. Exact arithmetic. Normalization equals an independent Fraction oracle over random amounts, rates, factors and quote directions (property), plus hand cases for half-even rounding at each step. Decimal context never rounds products, quotients or receipt sums.
  2. Bounded two-step rounding. For a factor f, two-step rounding differs from one-step by at most floor(1 + f/2) cents, so by at most one cent for every factor below 2 (property). The three declared factors are all below 2, and their difference is at most one cent (property). The four-cent hand case gives 7 FX cents, then 8 indexed cents.
  3. Order. Permuting rows commutes with normalization (property).
  4. Exact receipts. Receipts record exact sums, residuals and identities. Identity families name their reason and carry no factor residual.
  5. Refusals. Non-finite amounts, int64 overflow at either step, negative amounts in unsigned columns, rates given as JSON numbers, unknown or unmapped families, and normalizing twice (property) all refuse.
  6. One producer per money column. With the resource declared:
    • a raw donor read without its normalizer is a gap;
    • a second normalization in one lineage, or currency then normalization, refuses;
    • variant lineages are independent;
    • S0 and V1 each normalize once, despite a cross-population baseline artifact.
  7. Opt-in is byte-stable. For a spec without the resource, the preflight reads nothing, and composition and activation are byte-identical with the hooks removed.
  8. Factors are authenticated. A self-consistent tampered factor refuses on feed replay. Without a feed pin, the factor source commit must be authenticated. A profile-input or tax-year factor is never applied to donor money.
  9. The donor period is consistent. A mismatched or null donor_time_period refuses at the normalizer. The reader returns the exporter's value and refuses a malformed one.
  10. One destination period. Every factor applied by money.normalize@1 has target_period equal to the declaration's, so no two families age money to different periods; a non-integer target_period refuses. The public arithmetic helper takes factors as given; its one production caller, the kernel, checks every selected factor first.
  11. Dropped concepts are never normalized. Listing one as normalized refuses at validation, not at run time.

Cross-country

  • Belgium OK'd every transport hunk (10/24), with two rulings: donor_time_period is general, and the _time_period refusal is fine for BE, which pins the same donor file as NZ (sha256 48b9d479…; the hub read _time_period as a one-row int64 series, value 2024).
  • CREATE runs nowhere in production yet. No packaged country spec declares a transport_graph resource, so CREATE and the normalizer run only in test fixtures today. The new receipt key and the CREATE node-key change reach a country when its graph lands.
  • Elsewhere, a one-time cache miss. Edited modules move any implementation hash that covers them (CREATE's covers population_kernels.py and frame/transport.py), under the same 10/29 hold.

Review

  • Round 1 (Claude Opus 5.5, at 7a2aa81): REQUEST_CHANGES. It ran as the review fallback; no Codex lane had capacity.
    • It found no defect in the arithmetic, factor authentication, donor period, producer checks, S5 placement, profile selection or NZ data. Its own Fraction oracle matched the module on 21,420 values.
    • B1–B3: three tests went stale in the rebases. B1: a planted rulespec revision targeted the moved IRS source record. B2: money_normalization.json cited repository line numbers. B3: the receipt digests predated Add exact income-band intervals and boundary diagnostics #1394's interval document. The hub's validation had found the same three. Fixed in b675b7a: the evidence names symbols, and a new AST test checks the cited constant and call expressions against the code.
    • m1 (a dropped concept validated as normalized) and m4 (nothing bound the destination period) are fixed in 4f333f2, along with the n1–n4 wording.
    • m2 is fixed in part: the scenario default/central profile check is added. Lineage completeness is owed by w3 (below), as is m3's run-entry extension.
  • Round 2 (GPT-6.1 Sol, at 4f333f2): APPROVE_WITH_NITS.
    • B1–B3, m1, m4 and the m2 check are fixed. The three receipt digests equal round 1's.
    • DROP refuses only in the normalized roster.
    • Every donor and profile factor targets 2027, meaning tax year 2026-27, and no other declaration needed the new field.
    • Its Fraction oracle matches on 21,420 values, all 11 producer outcomes are unchanged, and the rebase is a replay.
    • Its two nits were wording, fixed above. The AST test checks the cited constant and call expressions, not every fact. Invariant 10 is scoped to the kernel, because the arithmetic helper takes factors as given. The module docstring and the AST test's comment get the same wording in db02ae8.
  • BE re-clear on the landing head (see Landing).

Verification

  • Lane (GPT-6.1 Sol, before the rebases onto fix3 and r2b): 5 files passed; 4 mutants killed (double_normalize, float_round, ty2024_factor_on_donor, ancestor_count_check).
  • Hub, at 7a2aa81 with Add exact income-band intervals and boundary diagnostics #1394's dtype fix, one file per process at -n 0: 22 files.
    • 20 pass, including test_money_normalization.py 113, test_transport_money_kernels.py 40, test_transport_donor.py 94 (2 skipped), test_transport_composed_graph.py 166 and test_transport_entitlement_graph.py 67.
    • test_nz_spec_package.py (2 failed) and test_reference_profiles.py (3 failed) failed on B1–B3.
    • 13 of 13 mutants were killed with passing controls and verified restoration: the lane's four, rerun, and fx_on_quantile_column, skip_factor_replay, omit_module_from_hash, ignore_donor_period, rate_as_float, preinverted_rate, silent_identity_family, drop_rule_in_producer_check and preflight_without_optin.
    • ci_test_plan.py verify, graph_acceptance_burndown.py --verify and both generators' --check pass.
  • Hub, after b675b7a: the eight affected files pass: test_nz_spec_package.py 1,201, test_reference_profiles.py 123, test_country_spec.py 136, test_money_normalization.py 113, test_transport_money_kernels.py 40, test_nz_money_normalization.py 15, test_spec_only_country_packages.py 18 and test_spec_engine_country_bundles.py 21. Three source mutants of the facts the new AST test cites (a different PERIOD, a literal time_period, no int cast) each fail it.
  • Hub, after 4f333f2: a fleet disk hold blocked pytest, so the new and affected tests were called in-process: 29 calls pass. In-memory mutants removing the DROP refusal, the target-period check or the target-period validation each fail their test. ruff, format and git diff --check are clean, and the golden diff is identity fields only.
  • Round 2 reviewer, at 4f333f2:
    • Under pytest: test_money_normalization.py 123, test_nz_money_normalization.py 17 and test_reference_profiles.py 123 pass.
    • Then disk fell below 50 GB. Called directly, these pass: test_transport_money_kernels.py (40), test_country_spec.py (136), and the RulespecPin and SourceCitations classes of test_nz_spec_package.py (1,123).
  • Rerun test_transport_money_kernels.py, test_country_spec.py and test_nz_spec_package.py under pytest once disk allows. CI runs the whole suite when the PR targets main.
    • The hub ran them at 4f333f2 on 2026-10-25, each in full at -n 0: 40, 136 and 1,201 passed.
  • Hub at db02ae8, after the fix4 merge, one file per process at -n 0: test_country_spec.py 136, test_nz_spec_package.py 1,201, test_transport_money_kernels.py 40, test_money_normalization.py 123, test_nz_money_normalization.py 17, test_transport_composed_graph.py 172 and test_spec_engine_country_bundles.py 21 passed.
  • Merge Add exact income-band intervals and boundary diagnostics #1394's head (044f179). Belgium OK'd it; the merged transport/cli.py is c4ddcb4's blob (6f42b3b0) and the only transport file the merge changes. The NZ golden conflicted and was regenerated from the merged tree; per-key attribution shows no mismatches.
  • Batch delta review of the fix4 merges (GPT-6.1 Sol, at this head, db02ae8): APPROVE, no findings. It rebuilt each merge with git merge-tree (only the conflicted NZ golden differs), attributed the golden key by key, checked Belgium's two conditions and the wording commits, and ran test_country_spec.py 136, test_nz_money_normalization.py 17 and the 12 currency-check tests of test_transport_composed_graph.py.

Owed by G8-w3

When w3 wires money.normalize@1 into the NZ graph:

  • it extends cli._require_current_run_factors to the selected money factors, with an expiry test. The kernel is shared and admitted to SKELETON_KERNELS, so the run-entry extension should be country-neutral (review m3);
  • it adds a differential test that the node's profile_selection equals the scenario knobs;
  • it checks lineage completeness: each scenario's lineage has a normalizer carrying that scenario's knobs, so a normalizer only in the shared version can't age V1/V2 at the central factor (review m2);
  • it budgets for the exact path's cost, about 144 µs per value under load, once per lineage (review n5).
  • it adds a test that every NZ V1–V5 variant node id appears in entitlement._rows(spec)[0], so no variant can bypass S5's extension-row checks (promised to Belgium for Let transport specs declare pre-calibration stages #1392).

Until then, the donor factors carry the same validity window as the IRD factors the run-entry check already covers.

Landing (2026-10-29)

Merge order

#1375 → #1382 → #1394 → this → w1 → S5 (#1392) → w2b.

🤖 Generated with Claude Code

MaxGhenis and others added 6 commits October 24, 2026 02:38
G8-r2a. Adds the projected_period_average_ratio@1 factor operation
(reference_factors.py) and wires its selected factors into the shared
reference-profile resolver, composition preflight and target compilation.

- Operands are the authenticated Ledger JSON tokens (LexicalFloat keeps the
  token on a slotted float subclass; integers stay int). Factors are exact
  rationals with a certified root enclosure, rounded once (half-even) to six
  decimals and carried as p/10**6.
- reference_aging.json declares the six operational factors (IRD TY2024
  low/central/high 1.115056/1.117109/1.119158; P1 1.026690/1.028580/1.030467)
  and the 1.086069 diagnostic, each with a receipt bound to the seven-field
  Chronicle feed identity. Factors need re-declaration after the September
  QES observation (validity ends 2026-11-03).
- The resolver takes factor_context; selected factors enter a version-2
  selection identity, money companions take one common linear_combination
  weight, counts are unchanged, and non-empty interval rosters refuse until
  r2b.
- Composition checks selected factors against the declared feed pin;
  compilation replays every selected factor against the loaded artifact and
  refuses a self-consistent forged factor or an unauthenticated source commit.
  reference_factors joins _COMPILE_MODULES.
- S0-S5 and V3 use the central factor, V1 low, V2 high, V4 central plus the
  small-household residual. All 474 IRD rows stay parked on B4 measurement.

Tests: 24 engine-free files, 2,832 passed and 5 skipped (UK feed not
configured), one file per run. Eleven mutants killed, each by a named test.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes the #1382 review (Sol r1) and the UK hub's loader notes.

- Factor application: each transformed row's role (money or count) now
  comes from the selected profile's companion roster, not the rehashable
  action label. Money rows must carry exactly the verified singleton
  linear_combination; count rows keep their original operation, operands
  and kind. Both reviewer tampering cases refuse at the resolver and at
  compilation.
- Lexical floats: load_ledger_consumer_artifact(lexical_floats=False) by
  default returns builtin floats again. Only compile replay with selected
  factors and the NZ ageing generator opt in; a static test pins those
  call sites.
- _strict_scalar_equal treats LexicalFloat as float; bool, int and float
  stay distinct.
- Oracle: exact roots return a zero-width enclosure and ambiguous bounds
  are refined; the (32, 1, 0, 2, 4) tie is an explicit example.
- RuleSpec scan: only the seven Chronicle identity fields are exempt.
- Factor schedules: frequency 1-12 and digits 0-18, so exact receipts stay
  serializable.

Tests: 24 engine-free files, 3,024 passed and 5 skipped (UK feed not
configured), one file per run; six new mutants killed.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1d5fff4 embedded the selected declarations and the whole base reference
document in every v2 resolution receipt, which travels in each compile and
prepare node's params: the NZ default receipt grew to 4.07 MB canonical.
Those documents were trusted only through hashes in the selection identity,
so a compact record bound the same way is as strong.

- v2 receipts carry factor_companions: per roster name, its role, factor,
  profile and source value semantics (474 names, 392 KB).
  factor_companions_sha256 and transformed_names join the v2 selection
  identity.
- Roles still come from the bound record, never the action label; money
  operands and count semantics are checked as before.
- The NZ default receipt is now 1.67 MB canonical, capped by a test at
  2.6 MB. v1 receipt bytes are unchanged and pinned.

Tests: 15 files, 2,508 passed and 5 skipped; three mutants killed.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Belgium's Sol review of 4f77038 found two latent regressions in
validate_reference_resolution: a receipt without transformed_names raised
KeyError, and an unnamed unresolved row raised KeyError where base
accepted it. transformed_names is now checked as a list of strings, and
roster-name bookkeeping runs only with factor evidence. The docstring
states the trust boundary: checks assume a trusted selection identity;
selected factors are anchored by replay against the authenticated feed.

The resolver property test suppresses only too_slow: an integer draw took
31 s under host load in review.

Tests: test_legacy_receipt_without_transformed_names_refuses_as_a_value_error
and test_unnamed_unresolved_rows_still_validate_without_factor_evidence;
each goes red (KeyError) with its fix reverted. test_reference_profiles.py
74 passed, test_transport_target_kernels.py 52, test_nz_target_references.py
50.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Factor validation compared date.today() with each declaration's validity
window. Every NZ factor declares valid_until 2026-11-03, so main's CI would
have failed for every PR from 2026-11-04. Validation is now a pure function
of declaration and feed: declared_on <= as_of and valid_from <= as_of <=
valid_until. require_current_factors(selected, *, today) has no default for
today. The transport CLI calls it once, for --resume auto|forbid, with
today's UTC date, before preparation can run CREATE. --resume require
replays a stored graph and is exempt. No kernel reads the clock.

The CLI checks every factor the default and scenario profile views
select, through compose.reference_profile_views, the walk activation now
shares, so a spec without scenarios checks its default view alone
(Belgium's review). An end-to-end test replays a warm store under
--resume require after the factor expires: rc 0, no kernel runs, and the
stored export's bytes unchanged.

From the d28817f review (findings 1-4): resolution transformations must
map names to objects, resolved and factor rows need nonempty names, and a
v2 unresolved duplicate companion refuses. Unresolved unnamed v1 rows still
validate. The expired-factor mutation in test_reference_factors now expires
before its as_of, so the retained as_of check refuses it.

Tests (hub, one file per process, normal priority): 15 files, 2,077
passed and 5 skipped (the UK feed cases). Clock runs at 2026-11-05:
test_nz_reference_aging 18 and test_reference_profiles 97 passed. Whole
CLI test file with the CLI clock at 2100-01-01: 163 passed. Mutants
killed, 8 of 8: the clock back in validation, the currency check on
replay, no currency check, run entry walking other views than
activation, the local date, and review mutants M3, M6 and M7. Receipts,
effective intervals and factor contexts are byte-identical to d28817f
(8 payloads, re-checked on this tree).

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a shared reference_intervals module: per-table declared semantics
(band closure, nil band, negative-income policy) with no defaults, exact
rational cuts on signed integer-cent inputs, classification, scaling and
diagnostics. Interval tables scale once with their companions' selected
factor, keeping fractional-cent cuts exact.

Bind interval outputs as their own kind in calibration_columns, beside
predicate outputs, and leave roster preparation untouched. Preparation
receives the exact effective interval companion, recomputes indicators
from each variant's cuts and records input fingerprints; the problem
kernel re-derives them and adds fixed-weight boundary diagnostics to its
metadata, never to targets.

Declare NZ's 237 national and 183 age bands, checked against every feed
label, and scale the national table in all three ageing profiles. All 474
IRD and 235 P1 rows stay parked. fix3's expiry test now passes the
interval document NZ's profiles require.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Address the r2b review (GPT-6.1 Sol, 75c2768).

- Remove the validator guard that refused positive_band_closure "left"
  with a nil band and a zero first lower cut. Zero is nil under either
  closure (_position checks nil before any cut), so the first positive
  band is 0 < z < upper and nothing belongs to two bands. The refusal
  forced a one-cent stand-in cut, which moves membership after scaling:
  at factor two, one cent fell below the first cut.
- Replace the refusal test with acceptance tests: classification at the
  unscaled and twice-scaled cuts, and boundary diagnostics with zero in
  nil under the declared and alternative closures. Drop the two first=1
  workarounds, and let the Hypothesis table generator draw the
  combination.
- Give the composed-graph interval config's Owned output its required
  dtype; without it the whole test file failed collection.
- Correct the changelog's refusal sentence.

Verified: test_reference_intervals 107, test_nz_reference_intervals 14,
test_transport_target_kernels 67 and test_transport_composed_graph 166
passed. Restoring the guard, and checking nil after the cuts, each turn
the three new tests red with the control green. All 12 r2b mutants were
killed with the dtype fix in place; left_closed_cut and default_closure,
whose test file this changes, were rerun after it and still kill. ruff,
format and git diff --check are clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 3 commits October 24, 2026 23:19
Add a pure money_normalization module and a money.normalize@1 transport
kernel. A declaration names a cited decimal-text or feed-replayed FX
rate, its quote direction, half-even rounding, the donor money period,
and per-family factors or reasoned identities. Both rounding stages and
the receipt sums use exact fractions; non-finite inputs refuse and every
integer companion is checked against int64 bounds. The kernel replays
its factors against the authenticated feed and refuses a donor period
that differs from the one CREATE records.

CREATE records the donor H5's _time_period as donor_time_period in its
metadata and receipt in every country. DonorBank reads it and refuses a
value that is not a one-row integer series.

Producer checks are opt-in. With a money_normalization resource,
activation names every consumed money column without exactly one
producer, and compose refuses a second normalization along structural
population inheritance after each graph pass of the ancestry guard. A
test checks that, for a spec without the resource, composition and
activation are byte-identical with the hooks removed.
reference_factors gains the additive money_normalization_input purpose.

NZ declares generated calendar-2024 donor factors with the existing
factors' validity and explicit per-profile selection. No NZ graph node
reads the declaration yet (G8-w3). Ageing non-wage money by AWE growth
is disclosed as provisional.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hub's validation after rebasing onto #1394 found three failures the
lane's pre-rebase runs could not see.

- money_normalization.json cited local files by line number, which
  test_nz_spec_package refuses. The donor-period evidence now names the
  symbols (the PERIOD constant, _dataset_from_frame,
  PolicyEngineUSEngine._write_and_verify). A new test parses both files
  and checks each cited fact against the code. Mutants that change
  PERIOD, pass a literal time_period, or drop the int cast each fail it.
- The rulespec-pin plant edited currency_bridge.json's source record,
  which w2a moved into money_normalization.json. The plant now edits the
  IRS rate's citation there, so the planted revision is caught again.
- The v2 receipt digests were produced before #1394 added the interval
  document. The test now passes reference_intervals.json and pins the
  digests that #1394's 75c2768 produces; they are unchanged at f700082
  and at w2a.

The country-spec golden was regenerated for the new
money_normalization.json bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the #1395 round-1 review (findings m1, m4 and n4; m2 in part).

- A money declaration now names the target_period its factors age money to,
  as it already names the donor period. check_money_factor refuses a factor
  whose target_period differs, so no two families can age money to
  different periods. The declaration's sha256 already binds it into the
  kernel receipt. NZ declares 2027 (tax year 2026-27), the target of its
  donor factors and of every reference-profile factor.
- A concept dropped in transport can't be listed as normalized; it used to
  validate and only fail at run time. Quantile-mapped and pending rows may
  still name one.
- NZ's scenarios' default aging_index must be the money declaration's
  central profile, and the two must list the same profiles.
- The mixed-income disclosure now says the projection after June 2026 uses
  Treasury PREFU 2026 AWE growth (2.7%, 0.5 points lower and higher in the
  low and high profiles), not only Stats NZ QES.

The general two-step rounding property is renamed for what it asserts: at
most floor(1 + f/2) cents, so one cent for every factor below 2.

In-memory mutants that drop each new guard (the DROP refusal, the
target-period check, the target-period validation) each fail their test.
The NZ country-spec golden is regenerated for the new declaration bytes.

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

prepare_transport_build now runs the run-entry currency check itself, after it
validates the resume policy and before activation. Every library caller that
starts a new run (auto or forbid) therefore refuses an expired selected
factor, not only the CLI; require still reaches stored work. main drops its
own call, and execute_transport_graph stays clock-free: its docstring says a
caller that composes a graph itself owns the check. New tests cover library
callers under auto and forbid for the default and scenario views, the require
exemption, and resume validation before the currency check.

The warm-store replay test now compiles a factor-bound target surface. A
test-local targets.compile@1 node on nz.open reads the factor references and
the pinned factor feed, and is a CLI terminal endpoint, so the warm run
executes it. The test spies the factor replay and checks it ran once against
the facts of the pinned feed. After the factor expires, require runs no
kernel or replay, keeps the export bytes and serves the compiler from the
store under the same key; auto refuses on the factor before preparation work.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 5 commits October 25, 2026 12:10
#1375 merged main (#1393), which added atomic_geography_support.json and
changed country_package.json, so the NZ golden conflicted. The golden was
regenerated from the merged tree with the test's own summary function. Keys
only one side changed keep that side's values; the fingerprint, the
resource list and the country_package.json hash, which both sides changed,
are the merged tree's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1382's fix moved the factor-currency check into prepare_transport_build in
build/transport/cli.py, after the resume check and before activation
validation. This carries that change, and #1375's merge of main beneath it,
up the NZ stack. The merged cli.py is c4ddcb4's blob; no #1394 commit
touches it.

The NZ golden conflicted: both sides changed the fingerprint, the resource
list and the country_package.json hash. It was regenerated from the merged
tree with the test's own summary function. Keys only one side changed keep
that side's values; the three both-changed keys are the merged tree's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zero is nil only when the table declares a nil band (_position checks
nil_present first). Without one, a zero record with a zero first cut falls
below the first cut under right closure and into the first band under left
closure, so the unqualified sentence overstated it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1394 now carries #1382's head (c4ddcb4), whose fix moved the
factor-currency check into prepare_transport_build in build/transport/cli.py.
The merged cli.py is c4ddcb4's blob; no #1395 commit touches it.

The NZ golden conflicted: both sides changed the fingerprint, the resource
list and the country_package.json hash. It was regenerated from the merged
tree with the test's own summary function. Keys only one side changed keep
that side's values; the three both-changed keys are the merged tree's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The module docstring said every applied factor ages money to the declared
target period. That holds for money.normalize@1, which checks each selected
factor through money_factor_inputs; a direct caller of
normalize_money_tables supplies its factors unchecked. Say so.

The AST test's comment said it checks each cited fact. It checks the cited
constant and call expressions, so an in-memory reassignment of the writer's
period would still pass. Say what it checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 26, 2026
#1395 now carries #1382's head (c4ddcb4), whose fix moved the
factor-currency check into prepare_transport_build in build/transport/cli.py,
and #1395's two wording fixes. The merged cli.py is c4ddcb4's blob; no #1397
commit touches it. The merge is clean, and the NZ golden it takes from #1395
regenerates byte for byte from the merged tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis changed the base branch from nzhub/g8r2b-intervals to main October 29, 2026 12:12
MaxGhenis and others added 4 commits October 29, 2026 08:12
Two tests imported microcosm_build.reference_factors as a top-level
module. The hub's local runs put test_support on PYTHONPATH, so they
passed there, but CI imports it as test_support.microcosm_build and
stopped engine-free at collection once this stack first ran against
main. Every other test in the repo uses the test_support prefix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves the one conflict in test_reference_profiles.py: keep this
branch's two fixtures and #1382's test_support import path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 29, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 3 commits October 29, 2026 08:26
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 29, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`resolve_dotted_symbol` (shared, from #1407) imports the longest module
prefix of a dotted name and walks the rest with `getattr`. For
`pkg.mod.Class.method`, `import_module` fails one level up: it reports
`pkg.mod.Class` as the missing module (`'pkg.mod' is not a package`).
The resolver only skipped an import error that named the exact prefix it
tried, so it re-raised instead of falling back to `pkg.mod`.

This PR's `nz/money_normalization.json` cites
`microcosm.frame.adapters.policyengine_us.PolicyEngineUSEngine._write_and_verify`,
the first citation in any package that reaches through a class.
`test_every_dotted_microcosm_symbol_resolves[nz]` fails on it: in CI on
#1397, and locally on this branch with the old check restored.

The resolver now also skips an error that names a parent of the prefix,
as the NZ package test's own resolver does, and still re-raises a missing
dependency of a module that exists. A citation whose only importable
prefix is the top-level package still raises ModuleNotFoundError, as the
existing `microcosm.no_such_shard...` case requires. A new test resolves an
enum member and a method through their classes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 29, 2026
Brings f09ad72 into this branch: resolve_dotted_symbol now falls back past a parent that is a class, so this PR's PolicyEngineUSEngine._write_and_verify citation (inherited from #1395) resolves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 3 commits October 29, 2026 09:55
1d5fff4 made `ledger_targets` import `LexicalFloat` from `ledger_artifact`,
and `ledger_targets` is a UK target compile module. CI's
`test_every_direct_import_of_a_listed_module_is_listed_or_excluded` then
failed: every direct import of a listed module must be listed or excluded.

The import feeds one type check: `_strict_scalar_equal` reads an opt-in
lexical float as `float`. Only the transport factor compiler opts in, which
`test_lexical_float_opt_in_is_limited_to_factor_consumers` pins; the UK
kernels load plain floats, so no UK value is ever a `LexicalFloat`, and the
loaded facts are node inputs hashed by content. That matches the existing
exclusions for imports that never move a value, and it keeps the UK kernels'
hashed module set as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings b36c702 into this branch: ledger_artifact joins EXCLUDED_DIRECT_IMPORTS with its reason, so test_every_direct_import_of_a_listed_module_is_listed_or_excluded passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings b36c702 (via #1394) into this branch: ledger_artifact joins EXCLUDED_DIRECT_IMPORTS with its reason, so test_every_direct_import_of_a_listed_module_is_listed_or_excluded passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 29, 2026
Brings b36c702 (via #1395) into this branch: ledger_artifact joins EXCLUDED_DIRECT_IMPORTS with its reason, so test_every_direct_import_of_a_listed_module_is_listed_or_excluded passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 6 commits October 29, 2026 10:14
main now holds #1375 as 6af97bf/846b48a9, so this branch had two merge bases with main (fddbc70 and 840d933). Git merges them cleanly, but GitHub marked the PR conflicting and ran no CI. This merge gives one merge base; its tree equals `git merge-tree --write-tree origin/main b36c702`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main now holds #1375 as 6af97bf/846b48a9, so this branch had two merge bases with main (fddbc70 and 840d933). Git merges them cleanly, but GitHub marked the PR conflicting and ran no CI. This merge gives one merge base; its tree equals `git merge-tree --write-tree origin/main 6d1252a`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main now holds #1375 as 6af97bf/846b48a9, so this branch had two merge bases with main (fddbc70 and 840d933). Git merges them cleanly, but GitHub marked the PR conflicting and ran no CI. This merge gives one merge base; its tree equals `git merge-tree --write-tree origin/main e35612f`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both UK target kernels decode their ledger facts inside run, through
full_targets -> load_ledger_consumer_artifact -> _load_fact_rows. The
decoded floats feed listed compilers, but source keys hash the raw file
bytes and source_hash covers only the modules the kernels pass it, so an
edit to the decoder left both UK implementation hashes unchanged (the
delta review moved a national target by changing only the default
decimal parser). List the module instead of excluding it; its one
microcosm.build import, chronicle_epoch, stays excluded, and its reason
now names ledger_artifact's schema gate and epoch labels too.

This adds no cache invalidation of its own: this branch already edits
ledger_targets.py and ledger_artifact.py, so it re-keys every UK target
node either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings 7cd1d26 (via #1382) into this branch: ledger_artifact joins UK_TARGET_COMPILE_MODULE_NAMES instead of EXCLUDED_DIRECT_IMPORTS, so an edit to the fact decoder re-keys both UK target kernels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings 7cd1d26 (via #1394) into this branch: ledger_artifact joins UK_TARGET_COMPILE_MODULE_NAMES instead of EXCLUDED_DIRECT_IMPORTS, so an edit to the fact decoder re-keys both UK target kernels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 29, 2026
Brings 7cd1d26 (via #1395) into this branch: ledger_artifact joins UK_TARGET_COMPILE_MODULE_NAMES instead of EXCLUDED_DIRECT_IMPORTS, so an edit to the fact decoder re-keys both UK target kernels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 29, 2026 16:40
@MaxGhenis
MaxGhenis merged commit fdd1e85 into main Oct 29, 2026
10 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merged by the NZ hub (session local_e407a527) at 3f10e53 with --merge. Gates: gh pr checks exit 0; MERGEABLE; not draft; no CHANGES_REQUESTED review. Reviewed head: 3f10e53. Independent delta review of the ledger_artifact listing across all five stack heads: APPROVE_WITH_NITS (Opus 5.5 in session), jobs/review-microcosm-fix5list/out-opus.md. BE re-clear clean at this head (jobs/be-reclear-1029e).

MaxGhenis added a commit that referenced this pull request Oct 29, 2026
#1395 (money normalization) and this branch each add one resource to
nz/country_package.json, so the NZ golden spec summary conflicted. Git
merged country_package.json and the two shared test files cleanly: the
package lists both resources in sorted order, and each test file's diff
against main equals this branch's own change.

tests/golden/nz_country_spec.json is regenerated from the merged spec
with the golden test's own _loaded_spec_summary and canonical_json_bytes.
Against main it changes exactly the resource hashes this branch changes
(calibration_bindings, family_reconciliation, reference_inventory,
reference_profiles, target_references), the resource list, the
fingerprint, and country_package.json's hash, the one merged file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 29, 2026
Merging main (#1395 and #1397) into this branch moved two pinned
digests. Each test passes at main and at this branch's previous head
0a8568b, and fails only on the combination, because each pins output
over NZ data both lines touched.

test_prepare_matches_pinned_legacy_output_digest hashes
prepare_calibration_columns over nz/calibration_bindings.json, which
this branch extends; #1397 pinned it from the pre-refactor
implementation at 4f333f2. On the merged bindings the pre-refactor
implementation, main's and this head's all give 1b22f4e1, and on main's
bindings the pre-refactor implementation still gives the old pin
dca0e085, so the refactor stays byte-equivalent.

test_nz_factor_receipts_are_byte_identical_after_additive_factors pins
the v2 receipts before #1395's donor factors were added, over
target_references.json and reference_profiles.json, which this branch
changes. Its three receipts at 0a8568b (this branch's data, no donor
factors) equal the merged tree's (b0f8648d, 8989fa75, e246f615), so the
donor factors still leave them byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis deleted the nzhub/g8w2a-money branch October 29, 2026 19:06
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