diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_transport_declarations.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_transport_declarations.py index 195711ae62..3cf8c8acfe 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_transport_declarations.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_transport_declarations.py @@ -864,16 +864,22 @@ def test_nz_packaged_graph_produces_region_and_bridge_columns_from_named_nodes() assert "region" not in pending and "Reg 17" not in pending -def test_nz_packaged_graph_compiles_with_only_pins_and_money_producers(): +@cache +def _nz_composed(): + """NZ's packaged graph, composed and compiled once per process.""" from microcosm.graph import compile_graph spec, config = _nz_composition_spec_and_config() + graph = compose.compose_transport_graph(spec, config) + return spec, config, graph, compile_graph(graph) + + +def test_nz_packaged_graph_compiles_with_only_pins_and_money_producers(): + _, config, _, compiled = _nz_composed() # No bridge or region root stays synthetic: CREATE declares none of them. assert _BRIDGE_AND_REGION.isdisjoint( (owned.entity, owned.column) for owned in config.create_outputs ) - graph = compose.compose_transport_graph(spec, config) - compiled = compile_graph(graph) assert "nz.package" in compiled.order assert "nz.scn.S0.income_decile" in compiled.order for variant in ("V1", "V2", "V3", "V4", "VHF"): @@ -881,6 +887,50 @@ def test_nz_packaged_graph_compiles_with_only_pins_and_money_producers(): assert f"nz.v.{variant}.gap" in compiled.order +def test_every_calibration_node_has_its_own_standin_as_the_only_writer(): + """R2C-15 on every calibration node, the variant branches' included. + + Composition checks the measurement proof on the skeleton alone: + ``_check_measurement_proofs`` runs before the scenario extensions append + their branches, so it never sees ``nz.v..calibrate``. For each + calibration node of the composed graph, exactly one compiled ancestor + declares the measurement, it is that branch's stand-in, and it is the + only ancestor whose outputs or EXPAND cells hold each proved column. + """ + spec, _, graph, compiled = _nz_composed() + proofs, refusals = compose.measurement_proofs(spec) + assert refusals == {} + columns = [tuple(name.split(".", 1)) for name in proofs[_MEASUREMENT]["columns"]] + _, cells = compose._precalibration_expansions(graph) + by_id = {node.id: node for node in graph.nodes} + roots = sorted( + node.id for node in graph.nodes if node.kernel.startswith("calibrate.") + ) + variants = ("V1", "V2", "V3", "V4", "VEX", "VHF", "VPT", "VSI") + assert roots == ["nz.calibrate", *(f"nz.v.{name}.calibrate" for name in variants)] + for root in roots: + branch = root.removesuffix(".calibrate") + standin = "nz.income.standin" if branch == "nz" else f"{branch}.income.standin" + ancestors = compose._ancestor_closure(compiled.predecessors, {root}) + declarers = sorted( + name + for name in ancestors + if (by_id[name].params or {}).get(compose.MEASUREMENT_PARAM) == _MEASUREMENT + ) + assert declarers == [standin], root + for entity, column in columns: + writers = sorted( + name + for name in ancestors + if (entity, column) in cells[name] + or any( + (output.entity, output.column) == (entity, column) + for output in by_id[name].outputs + ) + ) + assert writers == [standin], (root, f"{entity}.{column}") + + def test_nz_as_area_state_bindings_compare_the_declared_string_geography_codes(): resources = _spec()["resources"] support = resources["transport_atomic_geography_support"]["rows"] diff --git a/packages/microcosm-build/tests/engine_free/shared/test_nz_transport_toy_graph.py b/packages/microcosm-build/tests/engine_free/shared/test_nz_transport_toy_graph.py index 640bca1ff5..5d07658f56 100644 --- a/packages/microcosm-build/tests/engine_free/shared/test_nz_transport_toy_graph.py +++ b/packages/microcosm-build/tests/engine_free/shared/test_nz_transport_toy_graph.py @@ -19,6 +19,7 @@ from microcosm.build.transport.cli import through from microcosm.build.transport.compose import ( compose_transport_graph, + transport_resource, validate_transport_activation, ) from microcosm.build.transport.reporting_kernels import ( @@ -51,6 +52,7 @@ from test_support.microcosm_build.transport_nz_toy import ( DEMOGRAPHY, SCENARIOS, + TOY_STANDIN_MEMBERS, _cell_gates, _classifiers, make_nz_toy_fixture, @@ -237,9 +239,13 @@ def test_variants_normalize_once_with_their_knobs_and_hold_fixed_uses_identity(t def test_each_activated_calibration_row_compiles_once_and_exempt_rows_stay_out(toy): fixture, run = toy + # The effective view, after the ageing profile and the measurement proof + # activate the six parked stand-in rows. expected = { row["name"] - for row in fixture.spec["resources"]["target_references"]["target_references"] + for row in transport_resource(fixture.spec, "target_references")[ + "target_references" + ] if row["metadata"]["activation_status"] == "active" } surface = decode_target_surface(_artifact(run, "nz.targets.compile", "surface")) @@ -477,7 +483,10 @@ def test_four_regional_age_cells_prepare_and_calibrate_once(toy): frame = run[0].populations["nz.central"] names = [name + "_indicator" for name, _, _, _ in DEMOGRAPHY] assert frame.table("person")[names].sum(axis=1).eq(1).all() - assert run[0].nodes["nz.targets.problem"].receipt["n_targets"] == 4 + # The four cells plus the stand-in's count and amount row per coarse band. + assert run[0].nodes["nz.targets.problem"].receipt["n_targets"] == 4 + 2 * len( + TOY_STANDIN_MEMBERS + ) rows = fixture.spec["resources"]["target_references"]["target_references"] assert { row["measure"] @@ -739,3 +748,281 @@ def test_the_wage_map_v2_fits_the_toy_alpha_on_scaled_edges(toy): assert fit["alpha_decimal"] == "2" assert fit["top_band_lower"] == "500" == str(int(400 * TOY_EARNINGS_FACTOR)) assert [anchor["count"] for anchor in fit["anchors"]] == [3.0, 1.0] + + +# --------------------------------------------------------------------------- +# G8-r2c G on the toy: the income stand-in, its coarse rows and its gates +# --------------------------------------------------------------------------- + + +def _toy_problem(run): + from microcosm.calibrate.artifacts import decode_problem + + return decode_problem(_artifact(run, "nz.targets.problem", "problem")) + + +def _gate_row(run, gate_id): + report = json.loads(_artifact(run, "nz.gates.terminal", "gate_report")) + (row,) = [row for row in report["report"]["outcomes"] if row["id"] == gate_id] + return row + + +def _standin_bands(fixture): + from test_support.microcosm_build.transport_nz_toy import TOY_STANDIN_TABLE + + (table,) = [ + table + for table in transport_resource(fixture.spec, "toy_reference_intervals")[ + "tables" + ] + if table["id"] == TOY_STANDIN_TABLE + ] + return table, [ + (band, members) + for band, members in zip(table["bands"], TOY_STANDIN_MEMBERS, strict=True) + ] + + +def test_each_toy_coarse_row_enters_the_problem_matrix_once_with_support(toy): + """Design 80 section 6 G (1): the coarse income rows reach the toy matrix.""" + from test_support.microcosm_build.transport_nz_toy import ( + TOY_STANDIN_NODE, + TOY_STANDIN_OUTPUTS, + ) + + fixture, run = toy + _, bands = _standin_bands(fixture) + problem = _toy_problem(run) + # Matrix row names carry the period; target_metadata keeps the reference. + names = [row["reference"] for row in problem.target_metadata] + matrix = problem.problem.matrix.tocsr() + metadata = {row["reference"]: row for row in problem.target_metadata} + expected = { + band[role]: measure + for band, _ in bands + for role, measure in ( + ("count_reference", "toy_people"), + ("amount_reference", TOY_STANDIN_OUTPUTS["unit"]), + ) + } + assert len(expected) == 2 * len(TOY_STANDIN_MEMBERS) == 6 + for name, measure in expected.items(): + assert names.count(name) == 1, name + index = names.index(name) + assert matrix[[index], :].count_nonzero() > 0, name + assert metadata[name]["family"] == "income" + assert metadata[name]["measure"] == measure + # A's proof names the toy stand-in as nz.calibrate's only producer. It + # walks the transport skeleton (compose.py measurement_proofs), so the + # variant branches' calibrations sit outside it. + resolution = json.loads( + fixture.graph().node("nz.targets.compile").params["reference_resolution"] + ) + (proof,) = resolution["measurement_proofs"].values() + assert proof["producers"] == {"nz.calibrate": TOY_STANDIN_NODE} + + +def test_target_profile_coverage_passes_on_the_toy_with_income_required(toy): + """Design 80 section 6 G (2).""" + fixture, run = toy + (entry,) = [ + row + for row in fixture.spec["resources"]["gates"]["gates"] + if row["id"] == "toy_target_profile_coverage" + ] + assert entry["gate"] == "target_profile_coverage" + assert "income" in entry["parameters"]["required_families"] + row = _gate_row(run, "toy_target_profile_coverage") + assert row["status"] == "passed", row["failures"] + + +def test_measurement_prefit_fails_the_toy_only_on_its_unwired_evidence(toy): + """Design 80 section 6 G (3): R2C-18 leaves every alias unwired until G8-x. + + The gate runs on the toy's problem, and its only failures are one evidence + line per alias in the packaged gate's required_evidence; no roster, band, + support or diagnostics failure appears. + """ + from importlib.resources import files + + from test_support.microcosm_build.transport_nz_toy import ( + TOY_PREFIT_EVIDENCE, + TOY_STANDIN_TABLE, + ) + + fixture, run = toy + gates = json.loads((files("microcosm.build") / "nz" / "gates.json").read_text()) + (packaged,) = [ + gate for gate in gates["gates"] if gate["gate"] == "measurement_prefit" + ] + assert list(TOY_PREFIT_EVIDENCE) == packaged["parameters"]["required_evidence"] + row = _gate_row(run, "toy_measurement_prefit") + assert row["gate"] == "measurement_prefit" + assert row["status"] == "failed" + assert row["failures"] == [ + f"evidence: the run lacks required evidence {alias!r}." + for alias in TOY_PREFIT_EVIDENCE + ] + details = row["details"] + assert details["tables"] == [TOY_STANDIN_TABLE] + assert details["discrepant_bands"] == [] + assert [band["band"] for band in details["bands"]] == [ + f"{TOY_STANDIN_TABLE}/b{index}" for index in range(len(TOY_STANDIN_MEMBERS)) + ] + for band in details["bands"]: + for role in ("count", "amount"): + assert band[role]["inside_band_bounds"] is True, band + # A diagnostic entry: the refusal reaches the report, not the export. + report = json.loads(_artifact(run, "nz.gates.terminal", "gate_report")) + assert "toy_measurement_prefit" not in report["enforcement"]["blocking"] + + +def test_each_toy_coarse_reference_equals_the_sum_of_its_member_references(toy): + """Design 80 section 6 G (4), the differential. + + Each coarse count row's problem value is the sum of its declared members' + count facts in the pinned toy feed, and each amount row's is the selected + ageing factor times the sum of their amount facts. The members come from + the declared toy partition, not from the rows under test. + """ + from microcosm.build.ledger_artifact import load_ledger_consumer_artifact + from test_support.microcosm_build.transport_nz_toy import TOY_STANDIN_FACTS + + fixture, run = toy + resources = fixture.spec["resources"] + artifact = load_ledger_consumer_artifact( + fixture.sources["nz_calibration_facts"], + feed_pin=resources["toy_calibration_feed_pin"], + ) + native = {} + for fact in artifact.facts: + if fact.get("layout", {}).get("record_set_id") == "toy.standin.bands": + key = ( + fact["observed_measure"]["source_measure_id"], + fact["dimensions"]["band"], + ) + assert key not in native, key + native[key] = Fraction(str(fact["value"])) + assert native == { + (kind, label): Fraction(str(value)) + for label, pair in TOY_STANDIN_FACTS.items() + for kind, value in zip(("count", "money"), pair, strict=True) + } + (entry,) = resources["toy_reference_aging"]["factors"] + factor = Fraction( + entry["operational_factor"]["numerator"], + entry["operational_factor"]["denominator"], + ) + assert factor == Fraction(11, 10) + raw = { + row["name"]: row for row in resources["target_references"]["target_references"] + } + problem = _toy_problem(run) + targets = dict( + zip( + [row["reference"] for row in problem.target_metadata], + problem.problem.target_vector, + strict=True, + ) + ) + _, bands = _standin_bands(fixture) + for band, members in bands: + for role, kind, scale in ( + ("count_reference", "count", Fraction(1)), + ("amount_reference", "money", factor), + ): + name = band[role] + selector = raw[name]["ledger_selector"]["dimension_values"]["band"] + assert selector == list(members), name + expected = scale * sum(native[kind, label] for label in members) + assert targets[name] == pytest.approx(float(expected), rel=1e-12), name + + +@pytest.fixture(scope="module") +def toy_effective_standin_table(toy): + from microcosm.build.reference_intervals import interval_table + + table, _ = _standin_bands(toy[0]) + return interval_table(table) + + +def test_property_a_positive_standin_above_the_first_cut_lands_in_one_band( + toy, toy_effective_standin_table +): + """For any toy incomes: x > 0 above the aged first cut is in exactly one + coarse band (the independent cut count says which), 0 < x <= first cut is + in none, and x <= 0 is ``excluded`` and in none. The table, as NZ's, has + no nil band and a first cut above zero. + """ + from hypothesis import given, settings + from hypothesis import strategies as st + + from microcosm.build.reference_intervals import interval_indicators + from microcosm.build.transport.measure_kernels import combine_measure + from test_support.microcosm_build.transport_nz_toy import ( + TOY_STANDIN_COMPONENTS, + TOY_STANDIN_CUTS, + TOY_STANDIN_FIRST_CUT, + toy_standin_declaration, + ) + + declaration = toy_standin_declaration() + table = toy_effective_standin_table + factor = Fraction(11, 10) + cuts = [factor * cut for cut in (TOY_STANDIN_FIRST_CUT, *TOY_STANDIN_CUTS)] + signed = st.integers(-150_000, 150_000) + + @settings(max_examples=150, deadline=None) + @given( + st.lists( + st.tuples(*(signed for _ in TOY_STANDIN_COMPONENTS)), + min_size=1, + max_size=12, + ) + ) + def check(records): + columns = { + column: pd.Series([row[i] for row in records], dtype="int64") + for i, column in enumerate(TOY_STANDIN_COMPONENTS) + } + measures, statuses, _ = combine_measure( + declaration, columns, np.ones(len(records)) + ) + indicators = interval_indicators(table, measures, statuses) + hits = np.vstack(list(indicators.values())).astype(int) + names = list(indicators) + for index, (x, status) in enumerate(zip(measures, statuses, strict=True)): + if x <= 0: + assert status == "excluded" + assert hits[:, index].sum() == 0 + elif x <= cuts[0]: + assert status == "included" + assert hits[:, index].sum() == 0 + else: + assert status == "included" + assert hits[:, index].sum() == 1 + band = sum(x > cut for cut in cuts) - 1 + assert names[int(np.argmax(hits[:, index]))] == table.indicators[band] + + check() + + +def test_without_the_toy_standin_its_six_rows_refuse_activation_by_name(toy): + """The drop-the-node mutant, built in the test: A's proof fails closed.""" + from test_support.microcosm_build.transport_nz_toy import ( + TOY_MEASUREMENT, + TOY_STANDIN_NODE, + ) + + fixture, _ = toy + spec = copy.deepcopy(fixture.spec) + nodes = spec["resources"]["transport_graph"]["nodes"] + nodes[:] = [row for row in nodes if row["id"] != TOY_STANDIN_NODE] + with pytest.raises(ValueError) as refused: + validate_transport_activation(spec) + message = str(refused.value) + _, bands = _standin_bands(fixture) + for band, _ in bands: + for role in ("count_reference", "amount_reference"): + assert f"target_references.{band[role]}; awaits" in message + assert f"measurement:{TOY_MEASUREMENT}" in message diff --git a/test_support/microcosm_build/transport_nz_toy.py b/test_support/microcosm_build/transport_nz_toy.py index 9868a36347..90f44a87d8 100644 --- a/test_support/microcosm_build/transport_nz_toy.py +++ b/test_support/microcosm_build/transport_nz_toy.py @@ -577,6 +577,345 @@ def _earnings_map_v2(resources, rows, facts): ) +# --------------------------------------------------------------------------- +# The toy income stand-in (G8-r2c G at toy scale) +# --------------------------------------------------------------------------- + +#: The toy's counterpart of NZ's ird_taxable_income_ty2027_v1. +TOY_MEASUREMENT = "toy_income_standin_v1" +TOY_STANDIN_NODE = "nz.income.standin" +TOY_STANDIN_TABLE = "toy_income_standin" +TOY_STANDIN_OUTPUTS = { + "measure": "toy_income_proxy_cents", + "unit": "toy_income_proxy_nzd", + "universe_status": "toy_income_proxy_universe_status", +} +#: Signed components, as N/income_measurements.json lists NZ's: wages, signed +#: farm income, interest, rental (the one add-back) and the three annual +#: transfers the toy's annual-transfers node writes. +TOY_STANDIN_COMPONENTS = ( + "employment_income_cents", + "farm_self_employment_income_cents", + "interest_income_cents", + "rental_income_cents", + "toy_main_transfer_cents", + "toy_other_transfer_cents", + "toy_pension_transfer_cents", +) +#: The coarse table's source-period cuts in cents; the last band is open. +#: There is no nil band, and the first cut sits above zero, as NZ's does. +TOY_STANDIN_FIRST_CUT = 15_000 +TOY_STANDIN_CUTS = (40_000, 60_000) +#: Native member labels of each coarse band, in band order. A coarse row +#: sums its members' facts, as F's 24 rows sum IRD's native rows. +TOY_STANDIN_MEMBERS = ( + ("n01", "n02", "n03"), + ("n04", "n05"), + ("n06", "n07", "n08", "n09"), +) +#: Invented native facts: (count, amount in currency units) per member, +#: chosen near the toy's pre-fit weighted totals so R2C-3's ratio bounds hold. +TOY_STANDIN_FACTS = { + "n01": (17.0, 5_000.0), + "n02": (19.0, 5_000.0), + "n03": (18.0, 5_100.0), + "n04": (20.0, 9_300.0), + "n05": (19.0, 9_200.0), + "n06": (6.0, 9_000.0), + "n07": (6.0, 8_000.0), + "n08": (5.0, 7_000.0), + "n09": (5.0, 6_700.0), +} +#: The toy's own ageing dimension; its factors come from the shared fixture. +TOY_AGING_DIMENSION = "toy_aging_index" +#: Support thresholds suited to forty households; R2C-3's ratio bounds stand. +TOY_PREFIT_SUPPORT = {"minimum_source_households": 3, "minimum_kish_ess": 2} + + +def toy_standin_declaration(): + """The toy counterpart of income_measurements.json's ``measures.central``.""" + return { + "entity": "person", + "components": [ + {"column": column, "sign": 1} for column in TOY_STANDIN_COMPONENTS + ], + "add_backs": ["rental_income_cents"], + "floor": 0, + "scale": 100, + "outputs": dict(TOY_STANDIN_OUTPUTS), + } + + +def toy_standin_table(): + """The toy counterpart of ird_taxable_income_standin_ty2024.""" + from test_support.microcosm_build.reference_intervals import ( + interval_table_fixture, + ) + + table = interval_table_fixture( + table_id=TOY_STANDIN_TABLE, + cuts=TOY_STANDIN_CUTS, + nil=False, + first=TOY_STANDIN_FIRST_CUT, + companions=True, + indicator_prefix="toy_income_standin_band", + ) + table.update( + income_column=TOY_STANDIN_OUTPUTS["measure"], + universe_column=TOY_STANDIN_OUTPUTS["universe_status"], + ) + table["diagnostics"]["support_household_entity"] = "household" + for band in table["bands"]: + band["indicator"] += "_indicator" + band.update( + count_reference=f"toy_income_standin_count__{band['id']}", + amount_reference=f"toy_income_standin_amount__{band['id']}", + ) + return table + + +def toy_standin_member_facts(): + """One count and one amount fact per native member, NZ-geography.""" + from test_support.microcosm_build.reference_intervals import band_fact + + rows = [] + for label, (count, amount) in TOY_STANDIN_FACTS.items(): + for kind, value in (("count", count), ("money", amount)): + fact = band_fact(kind, label, value, record_set="toy.standin.bands") + fact["geography"].update(id="NZ", name="Synthetic destination") + rows.append(fact) + return rows + + +def toy_standin_references(table): + """F's multi-member rows at toy scale: ``sum`` counts, ``identity`` amounts.""" + from test_support.microcosm_build.reference_intervals import summed_row + + rows = [] + for band, members in zip(table["bands"], TOY_STANDIN_MEMBERS, strict=True): + for kind, name in ( + ("count", band["count_reference"]), + ("money", band["amount_reference"]), + ): + row = summed_row(kind, name, members, indicator=band["indicator"]) + row.update( + family="income", + measure="toy_people" + if kind == "count" + else TOY_STANDIN_OUTPUTS["unit"], + ) + row["metadata"].update( + required_reference_profile=TOY_AGING_DIMENSION, + required_measurement_binding=TOY_MEASUREMENT, + reference_prerequisites=json.dumps( + [ + f"reference_profile:{TOY_AGING_DIMENSION}", + f"measurement:{TOY_MEASUREMENT}", + ] + ), + interval_table_id=table["id"], + ) + rows.append(row) + return rows + + +def _income_standin(resources, rows, facts, problem): + """Wire the toy stand-in as G wires nz.income.standin, prepare and problem.""" + from test_support.microcosm_build.reference_intervals import prefit_gate_entry + + declaration = toy_standin_declaration() + resources["toy_income_measurements"] = { + "schema_version": 1, + "measures": { + "central": { + "declaration": declaration, + "declaration_sha256": sha256(canonical_json(declaration)).hexdigest(), + } + }, + } + # E's nz.earnings.cents: the mapped float wage becomes integer cents. + wages = { + "entity": "person", + "columns": [ + { + "source_column": "employment_income", + "output_column": "employment_income_cents", + "quantum": "0.01", + "rounding": "half_even", + } + ], + } + rows.append( + _node( + "nz.earnings.cents", + "money.minor_units@1", + population="nz.central", + inputs=[_slice("person", ["employment_income"])], + outputs=[_owned("person", "employment_income_cents", "int64")], + params={"declaration": canonical_json(wages).decode("utf-8")}, + ) + ) + rows.append( + _node( + TOY_STANDIN_NODE, + "measure.combine@1", + population="nz.central", + inputs=[_slice("person", list(TOY_STANDIN_COMPONENTS))], + outputs=[ + _owned("person", TOY_STANDIN_OUTPUTS["measure"], "Int64"), + _owned("person", TOY_STANDIN_OUTPUTS["unit"], "float64"), + _owned("person", TOY_STANDIN_OUTPUTS["universe_status"], "string"), + ], + params={ + "measure": _select( + "toy_income_measurements", + "json", + "measures", + "central", + "declaration", + ), + "measure_sha256": _select( + "toy_income_measurements", + "value", + "measures", + "central", + "declaration_sha256", + ), + "measurement_id": TOY_MEASUREMENT, + }, + ) + ) + table = toy_standin_table() + resources["toy_reference_intervals"] = {"schema_version": 1, "tables": [table]} + references = toy_standin_references(table) + document = resources["target_references"] + document["target_references"] += references + document["hierarchy"].setdefault("target_labels", {}).update( + { + row["metadata"]["contract_target_id"]: f"Synthetic {row['name']}" + for row in references + } + ) + facts += toy_standin_member_facts() + # The compiler replays each selected factor against the facts it loads, + # under the feed pin (_pin_calibration_facts writes both). + for row in rows: + if row["kernel"] == "targets.compile@1": + row["params"]["chronicle_feed_pin"] = _select("toy_calibration_feed_pin") + transformation = { + "operation": "scale_money_and_intervals", + "reference_names": [band["amount_reference"] for band in table["bands"]], + "count_reference_names": [band["count_reference"] for band in table["bands"]], + "interval_table_ids": [table["id"]], + "required_measurement_binding": TOY_MEASUREMENT, + } + resources["reference_profiles"] = { + "schema_version": 1, + "reference_resource": "target_references", + "factor_context_resource": "toy_reference_aging", + "interval_resource": "toy_reference_intervals", + "dimensions": { + TOY_AGING_DIMENSION: { + "default": "central", + # One profile: the toy feed carries one factor's anchor facts. + "profiles": { + "central": { + "transformations": [ + {**transformation, "factor_id": "toy_central"} + ] + } + }, + } + }, + } + bindings = resources["calibration_bindings"] + measured = [TOY_STANDIN_OUTPUTS["measure"], TOY_STANDIN_OUTPUTS["universe_status"]] + added = [{"entity": "person", "name": "toy_people", "constant": 1}] + [ + { + "entity": "person", + "name": band["indicator"], + "interval": {"table": table["id"], "band": band["id"]}, + } + for band in table["bands"] + ] + bindings["columns"] += added + prepare = next(row for row in rows if row["id"] == "nz.calibration.prepare") + prepare["inputs"].append(_slice("person", measured)) + prepare["outputs"] += [_owned("person", row["name"], "bool") for row in added] + prepare["outputs"][-len(added)]["dtype"] = "int64" + # R2C-25: the problem slices the unit column, or _compiled_problem + # (T/target_kernels.py:541-551) refuses every amount row. + problem["inputs"] += [ + _slice( + "person", + [row["name"] for row in added] + [*measured, TOY_STANDIN_OUTPUTS["unit"]], + ), + _slice("household", ["source_household_id"]), + ] + resources["gates"]["gates"] += [ + { + "id": "toy_target_profile_coverage", + "gate": "target_profile_coverage", + "phase": "terminal", + "criticality": "release_blocking", + "evidence_absent_blocks": True, + "parameters": {"required_families": ["income", "toy"]}, + }, + { + **prefit_gate_entry( + tables=[table["id"]], + evidence=list(TOY_PREFIT_EVIDENCE), + **TOY_PREFIT_SUPPORT, + ), + "id": "toy_measurement_prefit", + # R2C-18 leaves every alias unwired, so the gate fails every run; + # as a diagnostic it reports that without blocking the toy export. + "criticality": "diagnostic", + "evidence_absent_blocks": True, + }, + ] + + +def _pin_calibration_facts(resources, facts, root): + """Write the calibration facts and the ageing factors' facts as one feed. + + NZ's IRD ageing factors resolve against the pinned Chronicle feed the + compiler loads; the toy's ageing factors do the same against its own. + """ + from test_support.microcosm_build.reference_factors import factor_fixture + + _, factor_facts, _ = factor_fixture() + plain = json.loads(json.dumps(factor_facts, default=float)) + path, pin = write_pinned_artifact(root, [*facts, *plain]) + pin["source_commit"] = "c" * 40 + identity = chronicle_feed_identity(pin) + artifact = load_ledger_consumer_artifact(path, feed_pin=pin, lexical_floats=True) + declaration, _, _ = factor_fixture(feed_identity=identity) + declaration["id"] = "toy_central" + factor, receipt = resolve_reference_factor(declaration, artifact.facts, identity) + entries = [ + {"declaration": declaration, "operational_factor": factor, "receipt": receipt} + ] + resources["toy_calibration_feed_pin"] = pin + resources["toy_reference_aging"] = { + "schema_version": 1, + "feed_identity": identity, + "factors": entries, + } + return path + + +#: The packaged gate's required_evidence list (N/gates.json measurement_prefit). +TOY_PREFIT_EVIDENCE = ( + "native_band_report", + "omission_register", + "scenario_vsi", + "scenario_vex", + "scenario_vpt", + "identity_receipts", +) + + def make_nz_toy_fixture(root): """Prepare forty synthetic households and the complete transport declaration.""" base = make_entitlement_fixture(root, variants=False) @@ -782,6 +1121,10 @@ def make_nz_toy_fixture(root): problem = by_id["nz.targets.problem"] problem["inputs"].append(_slice("person", ["receives_other"])) _prepare(r, rows, problem) + _income_standin(r, rows, facts, problem) + sources["nz_calibration_facts"] = _pin_calibration_facts( + r, facts, root / "calibration-feed" + ) # A closed replay slice excludes every scratch cent, formula and reporting output. export_inputs = [ _slice(