Skip to content

Enforce source-household weight-quality gates and per-cell support for the NZ graph (G8a) - #1202

Merged
MaxGhenis merged 3 commits into
mainfrom
nzhub/g8a-cont
Oct 12, 2026
Merged

MaxGhenis merged 3 commits into
mainfrom
nzhub/g8a-cont

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Part of #343, alongside #1183 (G8). This is package G8a of the NZ graph build: the weight-quality gates set by the NZ method card's MC15 item, enforced end to end. The NZ graph stays inactive; G8 activates it.

The thresholds this enforces

The method card sets a ±40% sampling-precision budget per reported cell as binding, and tracks ±20% as a target. The resulting thresholds live in build/nz/gates.json as spec data:

Gate Threshold On failure
weight_ess, on source-household-aggregated calibrated household weights ESS ≥ 4,000 (absolute) release-blocking
weight_ratio, same aggregation max / positive-median ≤ 5 release-blocking
per-cell support, every reporting cell × scenario source-household ESS ≥ 100, contribution CV ≤ 2, single-donor dollar share ≤ 10% the cell is suppressed (and its bands omitted); missing or scenario-incomplete diagnostics block
tracked target, diagnostic only global ESS ≥ 16,000; cells with ESS ≥ 400 and donor share ≤ 5% flagged as meeting ±20% never blocks

What changes

  1. Source-household identity. The pinned donor has 57,240 household rows but 47,277 original source households, because some are cloned. A new country-neutral concept carries a vintage-scoped source-household key from explicit donor lineage through CREATE and benefit-unit construction (microcosm-frame transport.py, unit_construction.py, concepts.py).

    • Partial or conflicting lineage fails with a named error.
    • Following the precedent of Add the liquid financial assets concept, schema version 2 (NZ v0, G3a) #1120, the concept schema goes from 2 to 3. All four engine adapters (UK, US, BE, NZ) record the new concept as unmapped, and their coverage goldens and docs refresh only the digest and that reason. Hypothesis properties show no encoded engine input changes.
  2. Aggregated weight gates. transport/weight_quality.py sums clone weights by source key. weight_ess and weight_ratio gain an optional declared aggregation, an absolute ESS floor, and a refusal when the weights aren't calibrated household weights or lineage is missing.

  3. Per-cell diagnostics. takeup.gap@1 now computes six metrics per cell × scenario over the declared reporting grid:

    • source-household ESS;
    • distinct contributors;
    • contribution CV;
    • maximum donor dollar share;
    • dollar ESS;
    • a structural-zero flag.

    The diagnostics are a typed required artifact. The entitlement graph wires them into the battery, so their absence blocks export, and failing cells are suppressed. Undeclared reporting classifications are refused.

  4. NZ spec. gates.json activates only the two ruled weight gates and adds the cell-support and tracked-target entries; every other not_applicable entry is unchanged. scenarios.json declares the 40 decile × unit-type cells and the three receipt-group cells. country_spec.py allowlists the two new gate names.

  5. One set of cell targets. The tracked targets (cell ESS 400, donor share 5%) and the required cells, scenarios and aggregation column are written on both the cell-support contract and the tracked-target gate. The build refuses a spec where the two copies disagree, naming the gate, both keys and both values.

    • Each of the five values must be declared on both sides.
    • Lists compare as multisets: order doesn't matter, duplicates do. Where the two copies differ, a list that mixes types or holds mappings, or a string standing in for a list, is refused by this check's named error rather than a TypeError. Two older paths in _cell_contract still fail closed with bare errors on malformed specs, outside this check: matching lists of mappings on both sides reach a later set() and raise TypeError, and a contract missing a target_* threshold raises KeyError before this check runs. A follow-up validates presence and types first.
    • A gate counts as active when not_applicable is absent or null, as the gate parser reads it. That rule now applies to both the target gate and the support contract.

Invariants, scoped to the new code

  • Aggregation. With no clones, the aggregated ESS and ratio equal the row-level ones exactly. With clones, the aggregated ESS never exceeds the row-level ESS. Hypothesis properties check both.
  • Scale invariance. Scaling every weight by c > 0 leaves ESS, the ratio and every cell diagnostic unchanged, as long as the scaled weights and their clone sums stay representable as finite floats. Hypothesis property. Past that range, for example weights near 1e308, the clone sum overflows and the gate refuses rather than passing.
  • Monotonicity. Raising one source donor's weights never lowers its dollar share. Hypothesis property, checked against independently aggregated contributions.
  • Absence blocks. Missing lineage, diagnostics or a scenario each block, with a named reason, in unit tests and in end-to-end entitlement graphs.
  • UK and US unchanged. The row-level gate results match the base commit byte for byte: 1,435 direct and 16 binding-result comparisons. The UK and US gate test files are untouched and pass.

Not in this PR

  • Producers for the cell classifications. No code yet produces income_decile or receipt_group. The graph refuses to run cells without them, which is intended until G8 or a later package supplies them.
  • Activation. There is no transport_graph.json or entitlement_graph.json for NZ yet; that is Activate the NZ graph against pinned Chronicle facts (NZ graph G8) #1183.

Tests

  • The build lanes: 1,310 passed, with 2 existing availability skips, across 28 targeted test files, one file per run. 61 new test cases each fail under in-memory mutants. Four of the mutants are required by the brief: row-level weights used instead of aggregated, the absolute floor ignored, diagnostics absence not enforced, and suppression not applied. tools/ci_test_plan.py verify, ruff and git diff --check pass.

  • The hub, on the final tree:

    File Result
    test_transport_graph.py (no country name or float literal in transport) 15 passed
    test_nz_spec_package.py, after the drift guard 72 passed
    test_concepts.py 151 passed
    test_transport_weight_quality.py 30 passed
    test_transport_source_household_lineage.py 8 passed
    test_transport_gate_bindings.py 114 passed
    the US test_concept_mapping.py 11 passed
    test_transport_takeup_kernels.py 37 passed
    test_transport_entitlement_graph.py, after the drift guard 46 passed
    test_transport_gate_bindings.py, after the drift guard 114 passed

    No frozen file is touched.

Built in two GPT-6.1 Sol lane attempts from the NZ hub's brief. The first was lost before reporting; the second audited its work file by file and finished it.

Review

An independent GPT-6.1 Sol review at a392f97f returned APPROVE_WITH_NITS, "Needs Max: no", with no blocking finding.

  • Its own runs: 980 passed, 0 failed, across 15 targeted files. 67 changed or new cases fail under its in-memory mutants, including the four the brief required. ci_test_plan.py verify, ruff, graph_acceptance_burndown.py --verify and git diff --check pass.
  • UK and US unchanged: its own comparison against main found 1,524 direct and 14 binding-result comparisons, all identical. That is a new vector set, not a rerun of the lane's 1,435 and 16.
  • It checked by hand: a four-row fixture for all six cell metrics, suppression removing estimates and bands across six scenarios, and that deleting the diagnostics artifact stops the export.

Its three nits, and what happened to each:

  1. The cell targets were declared twice, with no drift guard. A support-only edit passed the tests. Fixed in e73481d7 (item 5 above). The reviewer's mutant now fails, as do drift cases for each of the five shared keys: six of the seven new cases fail with the guard disabled in memory. The NZ spec test also pins the contract's copy.
  2. Scale invariance needed a representability limit. Qualified above; the code already fails closed.
  3. The ruling's provenance isn't in the repo. gates.json cites d1281. The ruling and the adopted method card are in the NZ hub's decision record, not in this checkout. The lane's historical test and mutant counts above are the lane's reports; the review's own counts are the independent check.

Round 2

A delta review of e73481d7 lost its first two attempts to Codex transport failures. Probes the second attempt saved found three gaps in the new guard:

  • a target gate marked "not_applicable": null was skipped, though the gate parser treats it as active;
  • an omitted shared value passed;
  • a list mixing types, or holding mappings, raised a TypeError.

Fixed in 16f8308e, as item 5 describes, with 11 new cases. Four in-memory mutants each fail at least one of them, and every new case fails under one: the e73481d7 guard, a presence-only not_applicable check, order-sensitive lists, and sorting a non-list. ### Round 3

A GPT-6.1 Sol delta review of 16f8308e returned APPROVE_WITH_NITS, "Needs Max: no".

  • Its runs: 247 passed, 0 failed, across four files: entitlement 46, NZ spec 72, transport graph 15, gate bindings 114.
  • Mutants: all 11 new cases fail under the four mutants, and its own mutant fails both guarded cases. Ruff, the CI plan check, the graph ratchet and git diff --check pass.
  • Nits:
    • item 5's named-error claim was broader than the code; now narrowed above, with the two older bare-error paths listed;
    • the same two paths, for the follow-up;
    • the d1281 ruling isn't packaged in this repo, as this PR already discloses.

Previous review rounds were lost to quota limits; their saved evidence is in the hub's records.

🤖 Generated with Claude Code

Carry a vintage-scoped source-household concept from explicit donor lineage
through CREATE and benefit-unit construction. Follow the schema-addition
precedent by bumping the schema to 3 and recording provenance as unmapped in
all four engine adapters.

Add declared source aggregation and absolute ESS floors while preserving
the legacy row-gate result bytes. Adopt the ruled calibrated household
ESS 4000 and ratio 5 screens in the NZ spec.

Collect required, scenario-complete diagnostics for the declared reporting
grid. Aggregate clone weights and dollar contributions before computing
support metrics. Suppress failing cells and omit their scenario bands;
missing diagnostics, scenarios or lineage prevent export. Complete explicit
gap output declarations and refuse undeclared reporting classifications.
Track ESS 16000 and cell ESS 400/share 5% as diagnostic targets.

Invariants checked:
- No-clone ESS/ratio exact equality and cloned ESS non-increase: Hypothesis
  properties in test_transport_weight_quality.py.
- Weight scale invariance of ESS, ratio and every cell diagnostic: Hypothesis
  properties in the same file.
- Increasing one source donor's weights never decreases its dollar share:
  Hypothesis property compared with independently aggregated contributions.
- Missing lineage, diagnostics and scenarios block: named unit-gate failures
  and end-to-end entitlement graph regressions.
- UK/US row-gate compatibility: 1435 direct and 16 complete binding-result
  byte comparisons against dispatched HEAD, plus unchanged country tests.

Verification: 1310 passed and two availability skips across 28 targeted files.
All 44 new test functions / 61 parametrized cases fail under relevant runtime
mutants, including the four required defects. CI-plan verification, ruff check,
format check and git diff --check pass.

Exact commands and counts, failing mutant ids and E lines, and the initial five
fixture failures with their passing rerun are in the lane report (kept by the
NZ hub). Built in two lane attempts on GPT-6.1 Sol; the first was lost before
reporting and the second audited and finished its work. No population build
or pinned donor H5 read was made.

Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
MaxGhenis added a commit that referenced this pull request Oct 11, 2026
The tracked ±20% targets (cell ESS 400, donor share 5%) and the
required cells, scenarios and aggregation column are declared twice in
a spec: on the cell_support gate's contract and on the
cell_support_target gate. Nothing kept the two copies equal, so a
support-only edit could leave the tracked target reporting against
different numbers (review finding on #1202).

_cell_contract now refuses a spec where any shared key disagrees, and
names the gate, both keys and both values. Lists compare as sets.

Tests: a matching target builds; drift in each of the five keys is
refused; the reviewer's support-only mutant is refused. With the guard
disabled in memory, six of the seven new cases fail. The NZ spec test
also pins the contract's target values and the shared cell and
scenario lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tracked ±20% targets (cell ESS 400, donor share 5%) and the
required cells, scenarios and aggregation column are declared twice in
a spec: on the cell_support gate's contract and on the
cell_support_target gate. Nothing kept the two copies equal, so a
support-only edit could leave the tracked target reporting against
different numbers (review finding on #1202).

_cell_contract now refuses a spec where any shared key disagrees, and
names the gate, both keys and both values. Lists compare after
sorting, so order does not matter but duplicates do.

Tests: a matching target builds; drift in each of the five keys is
refused; the reviewer's support-only mutant is refused. With the guard
disabled in memory, six of the seven new cases fail. The NZ spec test
also pins the contract's target values and the shared cell and
scenario lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-2 reviewer's probes on e73481d found that the guard:
- skipped a target gate declared "not_applicable": null, which the gate
  parser treats as active, so drift on that gate escaped;
- let an omitted shared value through, though the target binding needs
  all five;
- raised a bare TypeError on a list mixing types or holding mappings.

A gate entry now counts as active when not_applicable is absent or
null, as GateSelectionSpec reads it, for both the target gate and the
support contract. Each shared value must be declared on both sides.
Lists compare as multisets ordered by canonical JSON, and only when
both sides are lists, so a malformed list or a string standing in for
a list is refused by name.

Tests: 11 new cases (lists in another order build; a null-marked
target is checked; each of five omitted values is refused; mixed,
mapping and string lists are refused by name; a null-marked contract
is the contract). Four in-memory mutants (the e73481d guard, the
presence-only check, order-sensitive lists, sorting a non-list) each
fail at least one case, and every new case fails under one of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis merged commit d1fda72 into main Oct 12, 2026
10 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merged by the NZ hub (session local_e407a527) at 16f8308 with --squash. Gates: gh pr checks exit 0; MERGEABLE; not draft; no CHANGES_REQUESTED review. Reviewed head: 16f8308. Independent GPT-6.1 Sol reviews: r1 at a392f97 APPROVE_WITH_NITS (Needs Max: no; 980 passes, 67 cases red under mutants; nit 1 = duplicated cell targets without a drift guard -> fixed e73481d, then 16f8308 after a second review found three gaps in that guard); r3 at 16f8308, the merged head, APPROVE_WITH_NITS, Needs Max: no, 247 passed across four files, all 11 new cases red under mutants. Remaining nits are two pre-existing fail-closed bare-error paths for malformed specs (follow-up owed by the NZ hub) and d1281 provenance outside the repo (disclosed). Thresholds are Max's d1281 ruling. Reports: ~/reviews/nz-hub/jobs/review-microcosm-1202/.

@MaxGhenis
MaxGhenis deleted the nzhub/g8a-cont branch October 12, 2026 08:47
MaxGhenis added a commit that referenced this pull request Oct 13, 2026
…follow-up) (#1219)

* Refuse a malformed cell-support contract with a named error

The round-3 review of #1202 found two older paths in _cell_contract that
failed closed with bare errors on a malformed spec: a contract missing a
target_* threshold raised KeyError while the thresholds were read, and
matching lists of mappings on both gates reached a set() comparison and
raised TypeError.

_require_cell_contract_shape now runs first. It requires every key the
contract is read for, and requires required_cells and required_scenarios
to be non-empty lists of non-empty strings, naming the gate and the
offending key or value.

Tests: each of the eight omitted keys; mapping, mixed, empty, string and
blank values for both lists; and the reviewer's matching-mapping case.
All 19 fail with the check removed. One #1202 case, a string contract
list, is now refused by this earlier check, and its expected message
says so.

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

* Require the cell contract's weight kind and entity by name

_cell_contract reads required_weight_kind and required_weight_entity, but
the shape check did not list them, so a contract missing either passed the
check and then raised a bare KeyError. Both are now required, with an
omission test each (red without the fix: 2 failed).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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