Skip to content

Add binding-local views to the Axiom adapter, registry, compose and closure (S2b PR 1) - #1482

Merged
MaxGhenis merged 22 commits into
mainfrom
be/s2b-views
Nov 5, 2026
Merged

MaxGhenis merged 22 commits into
mainfrom
be/s2b-views

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

S2b PR 1 of 3 from Belgium's port (d048; design thread #259). It adds binding-local views to the Axiom adapter, the registry, compose and the input closure, so the adapter can run a Child module over the person rows a selector picks. #1480 writes Child inputs and defaults as person columns, and review brief 6040 ran the two PRs together. This PR branched from main 4a36edcf. 02265bb5 merges main 875506e8 into it, and 36800b14 merges main 3e7a7045, after #1480 and #1488 landed; it edits none of #1480's files. Reviews 6039, 6040 and 6041 ran at ef398616, and commits 11 to 18 answer their findings. A first review of that fix round asked for one change, which commit 20 makes. The fix round's two reviews at f1d48f79 approve, and commit 21 makes review B's two docstring fixes. The NZ hub cleared the delta since f1d48f79 at 22:05Z, and CI passed on the head.

What it changes

A rules binding may declare views:

"views": [{"logical_entity": "Child", "selector": {"column": "is_child"}, "outside": {"child_benefit": 0.0}}]

F/ = packages/microcosm-frame/src/microcosm/frame/ and T/ = packages/microcosm-build/src/microcosm/build/transport/. Line numbers refer to this PR's head, 36800b14.

Adapter (F/adapters/axiom.py).

  • AxiomView (:342) holds the logical entity, the selector column and the outside values. Two views compare equal and hash alike when their canonical JSON matches (:381-387), so outside values 0, 0.0 and False stay distinct. AxiomEngine(views=...) refuses an empty list, views beside relations, output dtypes other than graph, a logical entity that a frame entity maps to, two views on one logical entity, and a selector that names a structural column or a view output (_validate_views, :632).
  • materialize groups requested names by execution root (:1072). _materialize_view (:1172) gathers the selected person rows with one boolean mask, in table order, checks the engine's array against the selected count, and scatters it back through the same mask. Every other row holds the outside value, which _outside_values (:2307) casts through _graph_output. An empty selection makes no engine call. _view_selection (:1147) refuses an absent, null or non-bool selector.
  • _view_program (:1433) compiles each view under its own cache key, re-checks a pinned tree before it compiles, refuses dense relations, and merges its derived metadata. _derived_metadata (:1521) loads view programs, so a Child-only module has metadata. variable_metadata resolves a view output to person (:733). validate_input_override refuses, by name, an override of a view's selector that no program reads, before its early return for adapters without relations (:833-841). A selector that a program reads as a root input stays an engine input: an override changes that input and the rows each view selects.
  • relation_dependencies("person") (:803) carries each selector. view_root_inputs() (:686) reports each view's roots. axiom_engine_ref writes views only for an adapter that has them (:2026), and refuses to pin an adapter whose view program compiled first (:1974).

Registry (T/registry.py). _binding_views (:285) parses each view and refuses malformed or conflicting declarations. _check_engine compares every Axiom adapter's views with the binding's, so an injected adapter must match (:448), and refuses views on an injected non-Axiom adapter (:467). _check_views (:471) runs after the reference pins the adapter. It checks relations on each view's program, outside keys against the binding's variables on the logical entity, and any view input that shares its name with a root input of the binding entity's program. It casts each outside value through the adapter's _outside_values (:502), so the registry refuses at build a value the adapter would refuse at run time, such as true on a decimal output or 0.5 on a judgment. binding_view_dependencies (:355) returns each selector as a person column.

Compose (T/compose.py). _require_relation_inputs (:4257) unions view and relation dependencies. It refuses a view node whose person slice omits an input that the closure encodes (other than through a graph node) or defaults on the logical entity (:4320). _check_closure_relation_slices requires each selector (:2566). _closure_defaults (:2278) maps a view binding's defaulted Child roots to person, so the support roster classes the columns #1480 writes as closure_default.

Closure (T/evidence_contract.py, T/evidence_kernels.py). closure_partition drops the reserved refusal and BINDING_LOCAL_VIEW_MESSAGE. Per binding, it treats each view's logical entity as live and maps it to the person table (evidence_contract.py:1067, :1046). When bindings share a module path, it checks that module against the union of their checked roots per engine entity, in any binding order, and refuses bindings that give one path different module SHA-256 values (evidence_contract.py:1164-1177). engine_root_inputs adds view roots under the logical entity (evidence_kernels.py:345) and refuses views on an injected non-Axiom adapter (:348).

Goldens. tests/golden/axiom_engine_refs.json holds 10 references that the cloud builder (brief 6037) generated at the base, for NZ's six binding rows over a synthetic tree and for the relation fixtures. Its test reads those six rows from a frozen copy, test_support/microcosm_build/axiom_ref_bindings.py, so an edit to NZ's live bindings can't move it. tests/golden/nz_real_axiom_engine_refs.json holds NZ's six real references as the base computes them from a git archive of rulespec-nz 8dc2507d, the commit NZ's bindings pin. A reference that gains a views key without a declared view fails both.

Docs. docs/transport-declared-units.md describes the declaration, the gather and scatter, the outside values, the refusals, the selector slice and how the closure partitions view roots. It also states the row-locality assumption that test_differential_b_real_child_program_against_the_view is written to test, that a reference keeps the views list in declaration order and each outside mapping in sorted key order, and when an input override may change a selector.

Commits

  1. 2d8b6271 adds AxiomView and the view gather, scatter, cache, metadata, dependencies and reference to the adapter, with test_axiom_views.py.
  2. a2108456 partitions view roots on the logical entity in the closure.
  3. f5a06700 parses views in the registry and adds the compose guards, with test_transport_views.py.
  4. c04320dd pins NZ-shaped and relation engine references to goldens from the base.
  5. 446e0316 adds the changelog fragment.
  6. 5933b6ff tests that every rules kernel refuses a view node without the whole selector slice.
  7. 4d4a99b4 pins view code to axiom.py and the no-view outputs to the base path.
  8. 881d78aa classes a view's defaulted roots as person columns in the support roster.
  9. 6872f50c freezes NZ's real-tree engine references at the base.
  10. ef398616 documents binding-local views in the declared-units guide.

The fix round, one commit per item of its brief:

  1. b31d4b60 checks each shared path against the union of its bindings' roots, so the closure check doesn't depend on binding order, and refuses differing module SHA-256 values (6039 P2-1).
  2. 679f21fc tests two views on one binding (6039 P2-2).
  3. 94709352 checks outside declarations fully at registry build (6039 P3-1, 6040 P3-1).
  4. 959ea22a keeps outside value types in view equality and hashing (6040 P3-2).
  5. ad854d25 makes the view properties reach their edges and all four dtypes (6039 P3-2).
  6. 0d136ed5 types differential (b)'s inputs by an explicit table, and checks that an output varies across the batch and that the selector isn't a root input (6041; 6039 P3-3).
  7. 1cf06b54 freezes the CI golden's NZ rows (6040 P3-4).
  8. 0d6201ad corrects the guide's wording and names a view selector in the override refusal (6040 P3-5; 6039 P3-4).
  9. 02265bb5 merges main 875506e8. git merges T/compose.py and test_transport_evidence_kernels.py without conflicts.
  10. f1d48f79 accepts an override of a selector that a program reads, and names only a selector that no program reads (the fix round's first review, P2).
  11. 760b0b4a corrects the two docstrings that review B flagged: _real_inputs said the pinned engine names a wrong-typed input, and axiom_ref_goldens.py rendered its "\n" as a line break.
  12. 36800b14 merges main 3e7a7045, which adds Give a stage launched on Modal a telemetry-only collector token #1473, Re-pin the US Chronicle feed to add Federal Student Aid's loan portfolio tables #1506, H2 (Bind NZS co-residence from household composition (H2) #1485), Tighten five G tests that checked less than their names claim #1501, Encode person_role bindings and their closure defaults on the person table (S2b PR 2) #1480 and Prove extension calibration nodes' measurement producers in compose #1488. git merges T/compose.py and test_transport_evidence_kernels.py without conflicts.

No commit takes 6040 P3-3, the compose view guard without a closure resource; PR 3 does.

Reach

Kernel hashes [executed, the NZ hub's method: make_composed_fixture(root) in a temporary directory, then implementation_hash(ref) for each of its registry's 45 kernels. The fixture binds ToyRulesEngine, so the three simulate.* kernels hash again with _adapter_classes = (AxiomEngine,)]:

Commit Toy registry (8) Under AxiomEngine (3)
2d8b6271 adapter export.prepare@1, export.readback@1, transport.input_closure@1 simulate.rules_by_ref@1, simulate.counterfactual@1, simulate.solve_zero@1
a2108456 closure transport.input_closure@1, transport.donor_receipt@1, transport.donor_ranges@1, transport.support_bounds@1, transport.weights_audit@1, gates.battery@1 none
f5a06700 to ef398616 none none
base 4a36edcf to ef398616 the 8 above the 3 above
b31d4b60 closure union transport.input_closure@1, transport.donor_receipt@1, transport.donor_ranges@1, transport.support_bounds@1, transport.weights_audit@1, gates.battery@1 none
679f21fc, 94709352 none none
959ea22a view equality export.prepare@1, export.readback@1, transport.input_closure@1 simulate.rules_by_ref@1, simulate.counterfactual@1, simulate.solve_zero@1
ad854d25 to 1cf06b54 none none
0d6201ad selector refusal export.prepare@1, export.readback@1, transport.input_closure@1 the 3 simulate.*
ef398616 to 0d6201ad the same 8 the same 3
f1d48f79 selector overrides export.prepare@1, export.readback@1, transport.input_closure@1 the 3 simulate.*
760b0b4a docstrings none none

Base to ef398616 moves these 11 and no others. Both transport.create hashes stay at cd21a414f12ab1b9. After a one-line edit to each file at the base, these kernels move [executed]:

  • F/adapters/axiom.py: export.prepare@1, export.readback@1, transport.input_closure@1 and the three simulate.* under AxiomEngine. The export kernels hash it as a module (T/terminal_kernels.py:57, :558, :644 at the head), transport.input_closure@1 lists it (T/evidence_contract.py:169), and the simulate.* kernels hash each bound adapter class (F/rules_kernels.py:111-116, :126-135) [read].
  • T/evidence_kernels.py: transport.donor_ranges@1, transport.donor_receipt@1, transport.input_closure@1, transport.support_bounds@1 and transport.weights_audit@1.
  • T/evidence_contract.py: those five and gates.battery@1, which hashes it (T/gate_kernels.py:759) [read].
  • T/registry.py and T/compose.py: none.

No US or UK runtime kernel hashes an edited file: no module under uk_runtime/, us_runtime/ or the US and UK adapters imports or names one [executed, git grep at ef398616, and again at 02265bb5 over its 340 US and UK runtime modules and the five PolicyEngine adapter modules]. microcosm/build/frame_serializer_registry.py:95 names axiom.py as a write site, not a hash input [read].

US primary-QRF worker identity: the binding at ef398616 equals the one at 4a36edcf, and the one at 0d6201ad equals it, byte for byte [executed, fresh processes].

Main to head [executed, the same method]: from main 875506e8 to f1d48f79, the same 11 kernels move. The other 37 of the registry's 45 keep their hashes, among them transport.create@1 and @2 (f003dbed400ad679 at both). The US primary-QRF worker identity at f1d48f79 equals main 875506e8's, byte for byte [executed, fresh processes]; main's own PRs moved it between 4a36edcf and 875506e8. After the merge, from main 3e7a7045 to the head 36800b14, the same 11 kernels move and the other 37 keep their hashes (transport.create@1 and @2 at 289aae38fc7b1db0 on both sides), and the US identity at the head equals 3e7a7045's, byte for byte [executed, fresh processes].

NZ's node keys. The 8 toy-registry kernels re-key the NZ nodes that use them, and the three simulate.* re-key wherever a binding runs AxiomEngine. S2D-AGREED item 6 asks for a stated re-run of such a change once NZ's M4, its first calibrated population, has passed, and M4 hasn't passed [NZ, ask 7, and again at 22:05Z on 11/04]. Until then, PR 1 costs NZ cache misses on those nodes and no stated re-run. NZ's rules bindings hold no engine pin yet, and build_transport_registry raises without one (T/registry.py:129-132, :559-566) [read], so NZ can't build its registry from them until G8-x, its first real-data run, sets both pins [NZ].

Tests

The cloud builder's hand-off comment (#1482 (comment)) gives its counts, skips and mutants at 881d78aa. test_nz_real_refs_match_their_frozen_copy froze its copy on a first run at the base, passed on a second, and passed at 881d78aa against that copy, which 6872f50c commits byte for byte [executed]. At the base, test_engine_refs_match_the_base_golden_byte_for_byte passed too, so axiom_engine_refs.json matches the base's references.

test_differential_b_real_child_program_against_the_view runs the real Child program from rulespec-be's regional_routing.yaml on a full batch, then on shuffled, single-row and random-subset batches and through a view over a person table, and checks that each row gets the value the full batch gives it. It skips without the Axiom dense extension and a rulespec-be export (POPULACE_RULESPEC_BE). Brief 6042 ran it on Linux, on the pinned engine build, where it passed (#1482 (comment)).

The fix round [builder, executed at 0d6201ad, one file per process]: ten files, 593 passed, 42 skipped and none failed. test_axiom_views.py 57 passed and 1 skipped, test_axiom_relations.py 63 and 1, test_axiom_adapter.py 20 and 25, test_axiom_graph_adapter.py 165 and 5, the US test_rules_engine_contract.py 0 and 8, test_transport_views.py 51, test_transport_registry.py 59, test_transport_evidence_kernels.py 156, test_nz_axiom_input_closure.py 19 and 2, and test_axiom_engine_ref_goldens.py 3. The skipped tests need the Axiom dense extension, POPULACE_RULESPEC_BE, an NZ checkout, or a filesystem that keeps NFC and NFD names apart. Both golden JSON files and NZ's spec and bindings keep their bytes.

Each new test failed on the code before its fix, or under the mutant the review named [builder, executed]:

  • Closure order: the order test failed with "age (Child, child.yaml) is closed but not a root input", and both SHA-256 cases didn't raise.
  • Two views: under 6039's first-view-only mutant, Vehicle.vehicle_value dropped out and the test failed.
  • Outside declarations: 6039's superset mutant accepted the extra Person key, and true on a decimal output, 2 and 0.5 on a judgment all passed registry build.
  • Equality: 6040's pair compared equal, and an injected engine whose outside values differ from the binding's only as 0 against 0.0 passed.
  • Property edges: the coverage audit failed, 6039's outside-zero mutant fails P1 to P3, and its scatter-without-mask mutant fails P5, P6 and differential (a). Over the run that measured them, 0 rows occurred 26 times, an empty selection 41, a full selection 33, and the four dtypes 143 (decimal), 74 (integer), 132 (judgment) and 55 (bool), explicit examples included.
  • Differential (b): test_real_inputs_preserve_types_regions_and_seed_across_root_orders failed on the inputs' dtype kinds. The typing table agrees with the test cases rulespec-be 530849e holds for regional_routing.yaml and the modules it imports: 101 inputs with examples, 0 mismatches. Six roots have no case but generated zeros, so the check leaves them out, and the checker's run over the whole export stops on an unrelated #relation key. Differential (b) itself skips here.
  • CI golden: with NZ's live bindings path replaced, test_ci_golden_source_ignores_live_nz_bindings raised FileNotFoundError.
  • Selector override: test_input_overrides_name_a_view_selector_in_the_refusal didn't raise.

Commit 20 [executed at f1d48f79, one file per process]: test_axiom_views.py 58 passed and 1 skipped, test_axiom_relations.py 63 and 1, test_counterfactual_kernels.py 37, test_quantized_solve_zero.py 134, test_axiom_adapter.py 20 and 25, test_axiom_graph_adapter.py 165 and 5, test_transport_views.py 51, test_transport_registry.py 59, and test_axiom_engine_ref_goldens.py 3, with a rulespec-nz export at 8dc2507d. The skips have the reasons above. On 02265bb5's code with this head's test file, test_a_selector_that_a_program_reads_stays_overridable fails with "Input overrides cannot change view selector 'is_child'."

On main 3e7a7045 [executed on the head's tree 98800a01, one file per process, with a rulespec-nz export at 8dc2507d]: test_transport_evidence_kernels.py 156 passed, test_transport_views.py 51, test_transport_registry.py 59, test_axiom_engine_ref_goldens.py 3, test_nz_axiom_input_closure.py 19 and 2 skipped, test_axiom_views.py 58 and 1, test_nz_transport_toy_graph.py 40, test_transport_measurement_proof.py 49, test_transport_column_kernels.py 36, test_concept_mapping.py 266, test_group_encode.py 82, test_axiom_relations.py 63 and 1, test_counterfactual_kernels.py 37, test_quantized_solve_zero.py 134 and test_transport_composed_graph.py 469. test_nz_spec_package.py lists the checkout's files with git ls-files, so it ran in a scratch git repository whose index reproduces that tree: 1,203 passed. The skips have the reasons above. From git diff 875506e8 f1d48f79 to git diff 3e7a7045 36800b14, 14 of this PR's 16 files keep their patch-ids; the other two carry commit 21's docstrings.

After CI. Main moved to fc5bb901 after CI run 41915381015 started: #1513 (US), #1491 (UK) and #1507 (NZ's ageing factors from the September 2026 QES), 51 files in all, none of them this PR's. git merge-tree merges the head with fc5bb901 cleanly, into tree abff51a7, which adds to the head's tree exactly main's changes since 3e7a7045: the same 51 files, with the same patch-id. On abff51a7 [executed, one file per process, with a rulespec-nz export at 8dc2507d], each engine-free test file that main changed since 3e7a7045 passes: test_nz_transport_declarations.py 60 and 2 skipped, test_nz_target_references.py 20 and 41 skipped, test_nz_reference_aging.py 6 and 16 skipped, test_nz_reference_intervals.py 15 and 1 skipped, test_nz_money_normalization.py 56, test_us_esi_premiums.py 175, test_uk_company_cars.py 34, test_uk_company_cars_stage_health.py 6, test_uk_measure_simulation.py 65, test_uk_measurement_period.py 21, test_uk_population_targets.py 32, test_uk_target_references.py 34 and 5 skipped, test_uk_terminal_gates.py 62 and test_transport_composed_graph.py 469. Their skips need NZ's or the UK's Chronicle feed, which the local run doesn't stage. The 15 files of the run on 98800a01 above pass here with the same counts, and test_nz_spec_package.py passes 1,203 in a scratch git repository. #1491 also changed the UK spine parity fixtures, and none of the 52 files outside tools/ that name uk_spine loads a module this PR changes [executed: each imported in its own process on abff51a7].

Still to do

  • Independent reviews at ef398616. 6039, the mechanisms (Add binding-local views to the Axiom adapter, registry, compose and closure (S2b PR 1) #1482 (comment)), wanted changes: two P2s and four P3s. 6040, the integration (Add binding-local views to the Axiom adapter, registry, compose and closure (S2b PR 1) #1482 (comment)), approved with five P3s. 6041, the engine-gated run (Add binding-local views to the Axiom adapter, registry, compose and closure (S2b PR 1) #1482 (comment)), found differential (b) failing on the test's input typing. Commits 11 to 18 answer each finding but 6040 P3-3, which PR 3 takes.
  • The fix round's first review, of the mechanisms and their tests at 02265bb5: one P2, which commit 20 answers. Its sandbox allowed no temporary files, so it ran its mutants in memory and couldn't run the tests that need a temp directory.
  • Independent reviews at f1d48f79, both GPT-6.1 Sol through Subfleet. A2, of commit 20 and the mutants on scratch copies, approves with no P1 or P2: the five named mutants fail their tests, and on 02265bb5's code the new selector test fails. Its one P3: in a whole-file run on a loaded host, TestClosurePartition::test_property_every_live_root_lands_in_exactly_one_class failed Hypothesis's slow-input-generation health check once, and it passed alone at the head and at main 875506e8. That test predates this PR (92a3d6bf), so this PR leaves it to a follow-up. B, of differential (b), the goldens, the main merge, reach and this body, approves with no P1 or P2 and two P3 docstring fixes, which commit 21 makes.
  • The NZ hub's clearance (ask 6) of f1d48f79, at 18:55Z: patch-ids, the diff, the merge with main e2918f4f, the kernel table, and NZ's real-tree references.
  • The NZ hub's clearance of the delta since f1d48f79, at 22:05Z: commit 21 and the merge of main 3e7a7045. Its own runs agree: the same 11 kernels move, and test_nz_transport_declarations.py passes 62, none skipped, on main plus NZ: re-declare the ageing factors from the September 2026 QES #1507 plus this PR.
  • CI at f1d48f79: run 41882901707.
  • CI at 36800b14: run 41915381015 passed all 11 checks. Both engine-free jobs ran test_transport_evidence_kernels.py, the one file both H2 and this PR edit: each job's pytest command lists it, and each job's --durations=25 report lists two of its tests. Each job passed 25,982 tests, skipped 291 and xfailed 3, and none failed.
  • test_differential_b_real_child_program_against_the_view on Linux, on the pinned engine build: brief 6042 passed it, with every other engine-gated file, at f1d48f79 and merged with Encode person_role bindings and their closure defaults on the person table (S2b PR 2) #1480's head then (17e26433).
  • At landing: the review verdicts and the NZ hub's clearances appear above. Item 6: M4 still hasn't passed [NZ, 22:05Z], and it can't pass before G8-x sets NZ's engine pins, which nz/axiom_rules_bindings.json still leaves null on main. So the stated re-run hasn't come due, and this PR costs NZ cache misses on the 11 kernels' nodes. The NZ hub reports the day M4 passes.

🤖 Generated with Claude Code

AxiomEngine takes an optional views sequence of AxiomView declarations:
a logical engine entity no frame entity maps to (rulespec-be's Child), one
bool person selector column, and an outside value per output. The adapter
gathers the selected person rows in table order, runs the module's program
on the logical entity, checks the engine's array against the selected count,
and scatters it back through the same mask; every other row holds the
outside value, cast through the output's graph dtype. An empty selection
makes no engine call, and a null or non-bool selector refuses.

View outputs resolve to the person entity, so callers that key on
(binding entity, variable) read them unedited. Each view's program compiles
under its own cache key, re-checks the pinned RuleSpec tree first, refuses
dense relations, and contributes its derived metadata, so a Child-only
module has metadata. The selector joins the person entity's
relation_dependencies; view_root_inputs() reports each view's roots.
axiom_engine_ref writes views only when present, so every existing
reference is unchanged. The view code stays in axiom.py, because
source_hash hashes defining files and does not follow imports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6Jc8SMdMbiMGS2yvWB
closure_partition no longer refuses a binding that declares views, and
BINDING_LOCAL_VIEW_MESSAGE is gone. Per binding, each view's logical entity
(Child) joins the live entities and maps to the person table, so a view's
roots partition against the closure's Child entries and the existing frame
check reads them on the person slice. Roots on that entity stay unmapped on
every other binding, and unreached_roots still reads only the binding's
engine entity, so it cannot excuse a view root.

engine_root_inputs adds each view's roots under its logical entity through
AxiomEngine.view_root_inputs(), and refuses a view binding on an injected
adapter. binding_view_entities reads a binding's logical entities for the
closure and compose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6Jc8SMdMbiMGS2yvWB
The registry parses a binding's views block into AxiomView declarations
(logical_entity, selector {column}, outside {name: value}) and builds the
adapter with them. It refuses an empty list, views beside relations, a
binding off the person entity, a logical entity that names a mapped engine
entity, a repeated logical entity, a selector with any field but column, a
structural selector, a selector that names a binding output, and an outside
key that names no binding variable. Once the adapter is pinned, it checks
each view's program for relations in place of the binding entity's program,
requires each view's outside keys to equal the binding's variables on its
logical entity, and refuses a view input named like a root input of the
binding entity's program. _check_engine compares an injected adapter's views
with the binding's and refuses views on an injected Python adapter.

binding_view_dependencies reports each selector as a whole-table person
slice, the same columns AxiomEngine.relation_dependencies reports to the
rules kernels. Compose unions them with the relation dependencies for every
rules node, refuses a view node whose person slice omits an input the
closure encodes (other than through a graph node) or defaults on the view's
logical entity, and requires each selector on the closure producer's person
slice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6Jc8SMdMbiMGS2yvWB
No NZ CI test builds an axiom_engine_ref for NZ's rows: NZ's engine-free
graph injects Python toy engines. The new golden holds references generated
at main 4a36edc for NZ's six binding rows (unreached_roots included) over a
synthetic tree, and for the registry and adapter relation fixtures, all
built engine-free through build_transport_registry and axiom_engine_ref.
The test compares them byte for byte, so a reference that gains
"views": [] or "views": null without a declared view fails.

A second test, gated on POPULACE_RULESPEC_NZ, builds NZ's six real
references from the tree alone and compares them with a frozen copy the NZ
hub writes at the base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6Jc8SMdMbiMGS2yvWB
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6Jc8SMdMbiMGS2yvWB
SimulateRulesKernel._require_relation_inputs reads the adapter's
relation_dependencies, which now carry each view's selector, so a node
whose person slice omits or masks the selector refuses before the engine
runs. The test pins that path for a view adapter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6Jc8SMdMbiMGS2yvWB
The placement lint now walks every import node in axiom.py, relative and
function-body imports included, and a second test requires AxiomView and
every view helper to carry axiom.py's module name. A helper moved into a
module axiom.py already imports (materialize, concept_mapping) would leave
the simulate.* hashes unmoved by an edit to it; the module-name test
refuses that move, which the import pin alone passes.

P4 gains an output clause: with views=None, materialize returns the same
names, order, dtypes and values, through the same engine calls, as a
reference copy of main 4a36edc's no-view path kept in the test. NZ's CI
graph injects toy engines and never runs AxiomEngine.materialize, so this
is the executable guard on view-free outputs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6Jc8SMdMbiMGS2yvWB
_support_roster mapped each defaulted closure entry to a frame entity
through entity_names alone, so a defaulted Child root, which PR 2 writes on
the person table, mapped to no entity and missed the closure_default class.
_closure_defaults now maps a binding-local view's logical entity to the
person entity for entries of that binding's module. Without views the
mapping is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6Jc8SMdMbiMGS2yvWB
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 881d78aa32dac07f105222d8a6974c95853fab30 on be/s2b-views, base main 4a36edcf.

Commits

  1. 2d8b6271 adds AxiomView and the view gather, scatter, cache, metadata, dependencies and reference to F/adapters/axiom.py, with test_axiom_views.py (P1 to P6, differential (a), differential (b) skip-gated, the placement lint, the adapter examples).
  2. a2108456 partitions view roots on the logical entity in closure_partition, removes BINDING_LOCAL_VIEW_MESSAGE, adds view roots in engine_root_inputs, and flips test_a_binding_local_view_is_refused_with_the_reserved_message into test_a_binding_local_view_partitions_its_logical_entity_roots.
  3. f5a06700 parses views in the registry, adds binding_view_dependencies and the compose guards, adds test_transport_views.py, and flips the views case of test_relation_declaration_refuses_ambiguous_or_future_shapes (test_transport_registry.py:590) to the empty-list refusal.
  4. c04320dd adds the base-generated reference golden (tests/golden/axiom_engine_refs.json, 10 references) and the POPULACE_RULESPEC_NZ-gated real-tree test.
  5. 446e0316 adds changelog.d/259-binding-local-views.added.md.
  6. 5933b6ff tests that every rules kernel refuses a view node without the whole selector slice.
  7. 4d4a99b4 applies owner note 2: the import walk now covers every import node (relative and function-body ones included), a test pins __module__ of AxiomView and every view helper, and P4 gains its output clause against a reference copy of main's no-view materialize kept in the test.
  8. 881d78aa maps a view binding's defaulted Child roots to person in the support roster (_closure_defaults, T/compose.py:2208). Without it, the Child defaults Encode person_role bindings and their closure defaults on the person table (S2b PR 2) #1480 writes as person columns map to entity None (main T/compose.py:2255-2267) and miss the closure_default class [read].

Kernel hashes

Method: make_composed_fixture(root), implementation_hash(ref) for all 45 refs, and the three simulate.* kernels again with _adapter_classes = (AxiomEngine,), computed on a clean worktree at each commit [executed].

Toy registry Under AxiomEngine
2d8b6271 export.prepare@1, export.readback@1, transport.input_closure@1 simulate.rules_by_ref@1, simulate.counterfactual@1, simulate.solve_zero@1
a2108456 transport.input_closure@1, transport.donor_receipt@1, transport.donor_ranges@1, transport.support_bounds@1, transport.weights_audit@1, gates.battery@1 none
every later commit none none
base to head export.prepare@1 af31fa6e→ef0e26f0, export.readback@1 96be393c→e0af913f, gates.battery@1 c11c40dd→c1eccf00, transport.donor_ranges@1 62def362→10fb4577, transport.donor_receipt@1 62def362→10fb4577, transport.input_closure@1 6d0e24f0→ca2537a6, transport.support_bounds@1 2303c60a→7be0d9a7, transport.weights_audit@1 1f3d4750→eefbe7cd simulate.counterfactual@1 fee0468c→50788c5c, simulate.rules_by_ref@1 63adf504→c460f951, simulate.solve_zero@1 9512f2b4→1638d775

8 + 3 = the spec's 11, with no difference. transport.create@1 and @2 stay at cd21a414f12ab1b9. No US or UK runtime kernel can move: no uk_runtime, us_runtime or PolicyEngine adapter module imports or names an edited file [read]. No commit edits test_support/microcosm_build/transport_composed.py.

Tests

  • Head 881d78aa, -n 4, 61 files: 4677 passed, 130 skipped [executed]. The set is the brief's list (S2D-AGREED item 6's files, test_axiom_relations.py, test_transport_evidence_kernels.py, test_transport_holdout_separation.py, test_be_target_references.py and the rest) plus every engine-free test file that names an edited module or AxiomEngine, axiom_engine_ref, closure_partition, engine_root_inputs, transport_rules_node, build_transport_registry or compose_transport_graph (git grep). The transport.create@1-versus-versioned differential runs inside test_transport_composed_graph.py and test_transport_population_kernels.py, both in the set.
  • python3 tools/ci_test_plan.py verify: ok. uvx ruff@0.15.16 format --check and check on the 11 touched Python files: clean.
  • Skips (130), all environment-gated: 47 NZ_CHRONICLE_ARTIFACT unset; 32 BE_CHRONICLE_ARTIFACT unset; 28 no axiom_rules_engine dense extension (differential (b) among them); 11 NZ AS-area pinned sources unset; 3 POPULACE_RULESPEC_BE unset; 3 the NFC/NFD git case this file system cannot express; 3 no rulespec-nz at POPULACE_RULESPEC_NZ (the new real-tree golden among them); 2 pinned feed not staged; 1 NZ TA ERP facts unset.
  • Differential (b) (test_differential_b_real_child_program_against_the_view) is written and unrun, as the brief says. It reads POPULACE_RULESPEC_BE, checks be/statutes/family_benefits/regional_routing.yaml against SHA-256 408e7358… from be.json module 61, and runs the real Child program on full, shuffled, single-row and random-subset batches, then through the view with P3. RuleSpec inputs carry no dtype, so _real_inputs types inputs by name (_years/_count integers, _income/_amount decimals, _region an integer code, all others bool). The Linux run may need to correct that guess; the engine names any input it refuses.

Mutants

Each ran on a scratch worktree at 4d4a99b4, with the file restored afterwards [executed]. All 15 turn red.

Mutant File Failing tests
scatter without the mask (out[:selected] =) axiom.py P1, P2, P5, P6, differential (a), test_a_view_runs_the_logical_entity_on_selected_rows_only, test_a_child_only_module_materializes, test_view_and_frame_programs_cache_under_separate_keys
engine returns one value (shape check removed) axiom.py test_the_batch_length_must_equal_the_selected_count
outside values replaced by 0 axiom.py P1, P2, P3, test_an_empty_selection_makes_no_engine_call, test_a_missing_or_uncastable_outside_value_refuses, 4 more
outside values replaced by NaN axiom.py P1, P2, P3, P5, test_an_empty_selection_makes_no_engine_call, 4 more
gather sorted by an input value axiom.py P1, P5, P6, differential (a)
views: [] serialized when absent axiom.py test_engine_refs_match_the_base_golden_byte_for_byte, test_absent_relation_declaration_stays_absent (test_transport_registry.py:451), P4, test_the_reference_carries_views_in_declaration_order
"views": null written when absent axiom.py the same four
a null selector read as false axiom.py test_a_null_absent_or_non_bool_selector_refuses
selector dropped from relation_dependencies axiom.py test_every_rules_kernel_requires_the_whole_selector, test_the_selector_is_a_whole_table_person_dependency, test_view_dependencies_agree_with_the_adapter
selector dropped from binding_view_dependencies registry.py test_compose_refuses_an_incomplete_view_node, test_view_dependencies_agree_with_the_adapter
old closure refusal kept evidence_contract.py 7 closure tests, test_a_binding_local_view_partitions_its_logical_entity_roots among them
Child roots put on Person evidence_kernels.py test_the_closure_reads_view_roots_on_the_logical_entity
view program cached under the frame-entity key axiom.py test_view_and_frame_programs_cache_under_separate_keys, test_a_view_runs_the_logical_entity_on_selected_rows_only
view helper moved into materialize.py axiom.py test_view_code_is_defined_in_the_adapter_module
view code moved to a new module axiom.py test_axiom_imports_are_pinned_everywhere_in_the_module

Merge-tree

Judgment calls

  1. Views require output_dtypes="graph" in the adapter: each outside value casts through its output's graph dtype, and the registry builds only graph adapters.
  2. The adapter refuses views=() as well as the registry refusing [].
  3. An outside value casts by the output's engine dtype. A bool never stands for a number, an integer stands for a decimal only when float64 holds it exactly, and a judgment code must be -1, 0 or 1.
  4. engine_root_inputs reads view roots only when the binding declares views. An existing test builds an AxiomEngine through object.__new__ (test_transport_evidence_kernels.py:1377), and this keeps the no-view path unchanged for it.
  5. variables() includes each view's root inputs. assert_no_relations accepts a view's logical entity and checks that view's program.
  6. For a module with both a Person program and a view, the registry checks the Person program for relations too, then each view.
  7. The registry refuses a selector that names a binding variable, which covers "never an output of the same node".
  8. The view-slice guard lives in _require_relation_inputs, which both skeleton rows and transport_rules_node call. The closure-slice message keeps its "relation columns" text, which test_nz_transport_toy_graph.py:665 matches.
  9. A Frame refuses an empty table (its weights cannot be empty), so P1 to P6 run their 0-row draws through _materialize_view directly.
  10. Commit 8 edits the support roster in T/compose.py, which the spec does not name. It is in a listed file, clear of Give a person with no household no household's row, and read malformed holdout rows alike in both guards #1481's regions, and moves no kernel.

Not done

  • The real-tree NZ golden has no frozen copy: I have no rulespec-nz tree here. On its first run with POPULACE_RULESPEC_NZ, the test writes tests/golden/nz_real_axiom_engine_refs.json and fails; run it at the base, commit the file, and rerun at head. It uses placeholder engine pins, because NZ's bindings carry none.
  • Differential (b) is unrun (see Tests).
  • docs/transport-declared-units.md:84 still says binding-local views "remain separate work"; docs were outside the brief's file list.

Generated by Claude Code

MaxGhenis and others added 2 commits November 3, 2026 04:19
test_nz_real_refs_match_their_frozen_copy compares NZ's six real
references with a frozen copy, and wrote none without a rulespec-nz
tree. This adds the copy, generated at PR 1's base (main 4a36edc) from
a git archive of rulespec-nz 8dc2507d, the commit NZ's bindings pin.

The base run froze the file, and a second base run passed against it.
At this branch's head the test passes too, so the six references keep
their bytes with views in place. The CI golden test passed at the base
as well, which checks that axiom_engine_refs.json came from the base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guide said binding-local views remained separate work. It now
describes the declaration, the gather and scatter, the outside values,
the registry's refusals, the selector slice, the closure's treatment of
view roots, and the row-locality assumption that the skip-gated
real-engine differential tests.

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

MaxGhenis commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

S2b PR 1 engine-gated run (brief 6041): failures (1 test, differential (b); the cause sits in the test's input typing, not in the adapter)

Eight of the nine files pass at PR 1's head and at the PR 1 + PR 2 merge, with no setup skips. test_differential_b_real_child_program_against_the_view fails in both runs, because its input generator gives 15 of the 107 Child roots a type the engine refuses. With those 15 retyped by an out-of-tree plugin, the engine agrees with itself on every batch shape, and the view agrees with the engine on every row of all 54 outputs. The test's last check, an exact float == on two sums, then fails by about 1e-12. And because program.root_inputs comes back in a different order in each process, the seed doesn't reproduce the batch.

Build

  • Microcosm PR 1 head ef39861676582382ff62de60aeed3f6f025de2b8, base 4a36edcf3600f28fa4232db50f2741298bbe1b28; PR 2 head 8a4e7a4719dc7a20ae4e6afa3d02a7a85312e0f6 [ran]. All three worktrees showed a clean git status --porcelain after the runs [ran].
  • Engine TheAxiomFoundation/axiom-rules-engine at 753ce7d9ac4cfb161f6aa4f281f5a3980e67a57d, cloned with git clone [ran]. git merge-base --is-ancestor 33ce416 HEAD exits 0, and git status --porcelain prints nothing before and after the build [ran]. git diff --stat 5529f1cf5651fa83026eb1fdcd4cfccef6f4fc46 HEAD lists only .github/workflows/ci.yml (3 lines) and python/tests/test_dense_surface.py [ran].
  • Wheel: axiom_rules_engine_dense-0.1.0-cp314-cp314-manylinux_2_34_x86_64.whl, sha256 1d888c276be8078fe160bd3ffd76758d4735a9de26e744b71a8ad6dce9ecea0e, from maturin 1.14.1 (maturin build --release --locked --manifest-path python-ext/Cargo.toml), rustc 1.97.0 [ran]. CLI from cargo build --release --locked [ran].
  • axiom-rules-engine capabilities prints engine_build: {commit: 753ce7d9ac4cfb161f6aa4f281f5a3980e67a57d, dirty: false} [ran].
  • from axiom_rules_engine.dense import NativeCompiledDenseProgram, DenseRelationBatch imports, and axiom_rules_engine and axiom_rules_engine_dense both load from the venv's site-packages. DenseRelationBatch takes (offsets, inputs, related_row_count=None) [ran].
  • rulespec-nz 8dc2507d5f10a0467c285fed54d018ee37246798 and rulespec-be 530849e4d53f83fc5109acbfc9f72f933be5c299, each exported with git archive into its own directory [ran]. be/statutes/family_benefits/regional_routing.yaml hashes to 408e73587e60d91136f80a71612802cb8c111f7ea5baf5abaeecb772c43c2413 [ran].
  • Python 3.14.6, from uv sync --all-packages --locked --python 3.14 into a venv outside the checkout [ran]. The workspace packages install as editable copies of whichever worktree ran uv sync, so run B got its own venv from the merge worktree. I checked that each venv imports microcosm.frame.adapters.axiom from its own worktree [ran].

Each file ran on its own as python -m pytest <file> -n 4 -rs, with POPULACE_RULESPEC_NZ and POPULACE_RULESPEC_BE set and the CLI first on PATH [ran].

Run A: PR 1 head ef39861 [ran]

file passed failed skipped
frame shared/test_axiom_relations.py 64 0 0
frame shared/test_axiom_views.py 54 1 0
frame shared/test_axiom_adapter.py 44 0 1
frame shared/test_axiom_graph_adapter.py 167 0 3
build shared/test_nz_axiom_input_closure.py 21 0 0
frame us/test_rules_engine_contract.py 8 0 0
build shared/test_axiom_engine_ref_goldens.py 2 0 0
build shared/test_transport_views.py 42 0 0
build shared/test_transport_registry.py 59 0 0

Skips:

  • test_axiom_adapter.py:118, "axiom engine is installed here": test_engine_methods_describe_installation_when_missing skips by design once the engine is installed [ran, read test_axiom_adapter.py:118-122].
  • test_axiom_graph_adapter.py:2019 (3 tests), "git here records the decomposed name as the file system lists it…": _decomposed_checkout skips on a Linux file system, where git records an NFD file name as listed. The guard reads git and the file system, not the engine [read test_axiom_graph_adapter.py:2000-2022].

No test skipped for a missing engine, dense extension or RuleSpec variable. test_real_dense_relation_fixture and test_nz_real_refs_match_their_frozen_copy both ran and passed (-k selection, 2 passed) [ran]. The three TestBelgianPilotSlice tests ran and passed [ran]. The golden test passed, so I didn't diff the references.

Run B: local merge of PR 2 into PR 1 [ran]

git merge --no-ff 8a4e7a4719dc7a20ae4e6afa3d02a7a85312e0f6 on top of ef39861676582382ff62de60aeed3f6f025de2b8 merged with no conflict. Merge commit f485801c5fa75651dbed9c84909c1f58ff6fa9a7 (local, unpushed), tree 85044a15c272696fc86bea1c03eaf566d03f1659, merge base 3995d92d8f26b5b4d6334d429e80b9f8d63f448a. Against PR 1's head, the merge changes five files (transport/column_kernels.py, frame/concept_mapping.py and three test files). It leaves uv.lock, test_axiom_views.py and F/adapters/axiom.py unchanged.

file run A passed / failed / skipped run B passed / failed / skipped
test_axiom_relations.py 64 / 0 / 0 64 / 0 / 0
test_axiom_views.py 54 / 1 / 0 54 / 1 / 0
test_axiom_adapter.py 44 / 0 / 1 44 / 0 / 1
test_axiom_graph_adapter.py 167 / 0 / 3 167 / 0 / 3
test_nz_axiom_input_closure.py 21 / 0 / 0 21 / 0 / 0
test_rules_engine_contract.py 8 / 0 / 0 8 / 0 / 0
test_axiom_engine_ref_goldens.py 2 / 0 / 0 2 / 0 / 0
test_transport_views.py 42 / 0 / 0 42 / 0 / 0
test_transport_registry.py 59 / 0 / 0 59 / 0 / 0

The skips match run A's, with the same reasons. The one failure matches run A's: same test, same line and same engine error [ran].

Failure: test_differential_b_real_child_program_against_the_view

Where it fails. The full-batch execute at test_axiom_views.py:980 raises RuntimeError: type mismatch: right side of comparison is not numeric, from axiom_rules_engine/dense.py:414 [ran]. None of the shape or view checks runs.

At the base. test_axiom_views.py doesn't exist at 4a36edcf3600 [ran ls], so PR 1 adds this failure.

Cause: the test's input typing. _real_inputs (test_axiom_views.py:923-941) picks each input's type from its name suffix: _years/_count/_months/_rank become int64, _income/_amount/_eur become float64, _region becomes a region code in 1–3, and every other name becomes bool [read]. The engine refuses a comparison with a bool on one side and a number on the other (engine/src/dense.rs:7811-7826, with is_numeric at dense.rs:7369-7374) [read]. Bisecting the 54 outputs one at a time showed 31 of them failing with this error [ran]. Then I traced which inputs cause it [ran, read the rule files]:

  • 6 bool conditions end in _region, so they get a region code: flanders_family_benefits_child_domiciled_in_dutch_language_region, brussels_family_benefits_child_domiciled_in_brussels_capital_region, german_speaking_community_family_benefits_child_domiciled_in_german_language_region, wallonia_family_benefits_child_legal_domicile_in_french_language_region, wallonia_family_benefits_child_without_legal_domicile_effectively_resides_in_french_language_region, wallonia_family_benefits_parents_eu_or_reg883_and_economic_activity_in_french_region. For example, if flanders_family_benefits_child_domiciled_in_dutch_language_region: (rulespec-be/be-vlg/statutes/family_benefits/eligibility.yaml:60) fails on an integer column.
  • 1 bool condition ends in _months: german_speaking_community_family_benefits_foreign_stay_or_settlement_authorized_more_than_three_months (be-dg/statutes/family_benefits/eligibility.yaml:72).
  • 7 numeric inputs fall through to bool: flanders_family_benefits_disability_tier, flanders_family_benefits_orphan_status_code, wallonia_family_benefits_disability_tier, german_speaking_community_family_benefits_disability_category (each compared == 1 in the regions' amounts.yaml), german_speaking_community_family_benefits_benefit_year (>= against a freeze start year), german_speaking_community_family_benefits_article_10_extension_months_used and german_speaking_community_family_benefits_months_since_disappearance (each compared in be-dg/statutes/family_benefits/eligibility.yaml).
  • 1 decimal falls through to bool: german_speaking_community_family_benefits_current_index_factor, which the engine refuses with "expected decimal-compatible dense column".

With those 15 inputs retyped, the engine runs the full batch [ran].

With the typing corrected. I ran the test unchanged except for _real_inputs, which an out-of-tree pytest plugin replaced with a version that retypes those 15 names. I didn't edit the checkout [ran]:

  • Child roots the program reads: 107; relations: none; outputs: 54, all decimal under the graph dtypes [ran].
  • Batch sizes: full 40; shuffled 40; single-row 1; random subset 15 in one run [ran]. The view frame holds 80 person rows, 40 of them selected by is_child [ran].
  • Shuffled, single-row and random-subset batches gave every row the full batch's value for all 54 outputs [ran].
  • Through the view (AxiomView("Child", "is_child", outside)), selected rows equal the full batch's values (:1032), and unselected rows equal outside (:1033), for all 54 outputs [ran].
  • The P3 check (test_axiom_views.py:1034-1038) compares two float64 sums with ==, one over 80 rows with zeros interleaved and one over 40 rows. It failed in both runs I made. The first failed with assert np.float64(58559298.47) == (np.float64(58559298.470000006) + (0.0 * 40)). The second, which recorded the check instead of asserting it, found 51 of 54 sums equal and 3 that differ by 9.1e-13 to 1.8e-12 (flanders_family_benefits_disability_supplement_monthly_amount, flanders_family_benefits_selected_monthly_amount, wallonia_family_benefits_disability_supplement_monthly_amount). All 54 agree to rtol=1e-12 [ran]. Row-level equality holds, so these differences come from summation order, not from the view.

Reproducibility. CompiledDenseProgram.root_inputs returned the 107 names in a different order in each of three processes (order hashes b2bd9620e1a2, 3d52ddf5b419, 755552b2c107) [ran]. _real_inputs draws from default_rng(530849) in that order, so the seed doesn't fix which input gets which values, and the P3 pairs differ between runs. The engine fills root_inputs in first-reference order during compilation (engine/src/dense.rs:3837-3845) and keeps its compiler indexes in HashMaps (dense.rs:2298-2305) [read]. I didn't trace which iteration sets the visit order. Sorting program.root_inputs before drawing would fix the batch per seed.

Fixing the test would mean three changes: type those 15 inputs by name, compare the P3 sums with a tolerance, and sort the root names before drawing. Those are suggestions; this run changed nothing.

What I couldn't do

  • Rerun differential (b) at the base: the test doesn't exist there.
  • Report differential (b)'s shape results from the test as written: it fails before its first shape check. The shape and view results above come from the plugin run.

Generated by Claude Code

@MaxGhenis

MaxGhenis commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

S2b PR 1 mechanism review (brief 6039): CHANGES WANTED

Head ef39861676582382ff62de60aeed3f6f025de2b8, base main 4a36edcf. Mechanism lens: adapter, programs and cache, closure, properties, mutants. I changed nothing in the repo. Scratch tests and the mutation runner sit outside the tree, every patched file was restored, and git status --porcelain is empty.

I found no P1. The gather, scatter, outside-value, cache and closure mechanics held against everything I tried. I found two P2s, both small: an order-dependent closure check, and a mutant that no test catches. The P3s are test-quality notes.

F/ = packages/microcosm-frame/src/microcosm/frame/, T/ = packages/microcosm-build/src/microcosm/build/transport/.

Findings

P2-1. Two bindings on one module path, one with a view and one without: the closure check depends on binding order

closure_partition builds the checker's surface as {row["rulespec_path"]: AxiomModuleSurface(inputs=checked_roots[row["id"]]) for row in bindings} (T/evidence_contract.py, the surface = AxiomInputSurface(...) block). Two rows on one path collide, and the last row wins. Before this PR, two rows on one path had the same live entities. With views, the live entities are per binding (live_entities = mapped | view_entities[binding]), so the two rows' root sets differ. The registry allows such a pair: it refuses only a repeated binding id or engine reference (T/registry.py:141, :666), and views change the reference.

Ran: the test_transport_views.py fixture (_fixture, _partition). I added a copy of the view binding with id no_view and no views, on the same child.yaml, and partitioned in both orders:

  • View binding first: check = ['age (Child, child.yaml) is closed but not a root input.', 'child_income (Child, child.yaml) is closed but not a root input.'].
  • View binding last: check = [].
  • The per-module rows are the same in both orders. The view row has its Child roots live, and the no_view row lists them under unmapped_entity and unexpected.

So the same bindings pass or fail the closure check depending on file order. No binding does this today, and NZ binds no views. But Belgium binding one module twice (by period, say) is plausible, and the failure would point away from its cause.

Fix, either:

  • the registry refuses two bindings on one rulespec_path whose views differ; or
  • the surface takes, for each path, the union of its rows' checked roots.

Add a test for both orders.

P2-2. Mutant survives: view_root_inputs() dropping a view

Mutant: in F/adapters/axiom.py view_root_inputs, for view in self._views or () becomes for view in (self._views or ())[:1].

Result: 394 passed, 5 skipped across test_axiom_views.py, test_axiom_relations.py, test_transport_views.py, test_transport_evidence_kernels.py, test_nz_axiom_input_closure.py, test_transport_registry.py and test_axiom_engine_ref_goldens.py. Nothing turns red.

Impact: with two views, the second view's roots would drop out of engine_root_inputs, so the closure would never class them, not even as gap. They would also drop out of variables(), so a counterfactual override of one would be refused, and out of _check_views' name-clash check.

The only two-view test, test_the_reference_carries_views_in_declaration_order, checks the reference only. test_several_view_bindings_keep_their_roots_in_order uses two bindings with one view each.

Fix: add a two-view adapter test, for example Child plus Vehicle with distinct roots. It should assert view_root_inputs(), variables(), and engine_root_inputs partitioning both entities.

P3-1. The outside-key equality rule is tested in one direction only

Mutant: in T/registry.py _check_views, if set(view.outside) != on_view: becomes if not set(view.outside) >= on_view:, so extra keys are accepted.

Result: 394 passed on the full set above. The spec says the outside keys equal the binding's variables on the logical entity.

An outside key naming a binding variable on Person is accepted and then ignored, since materialize routes that name to the frame program. It's harmless at runtime, but the declaration contradicts itself. One registry test with an extra key that names a Person variable closes it.

P3-2. P1 to P6 never draw 0 rows, an empty selection or a full selection, and cover only decimal outputs

Ran: a copy of _population() with hypothesis.event() and --hypothesis-show-statistics.

  • P1-shaped run: 60 valid draws and 7 invalid. Every valid draw had more than 0 rows and a partial selection. None had 0 rows, an empty selection or a full selection.
  • Differential (a)'s draws: the same, 40 of 40 partial.

Cause: Hypothesis's minimal draw sets every value to its minimum, so n=0 comes with outside=0, which .filter(lambda value: value != 0) rejects (the "unable to satisfy" events). In practice the edge cases come only from the example tests:

  • test_an_empty_selection_makes_no_engine_call covers an empty selection and the 0-row path through _materialize_view.
  • test_a_view_runs_the_logical_entity_on_selected_rows_only covers selections, but none full.

The properties request only child_amount, so 100% of draws exercise the decimal dtype. Judgment, integer and bool outputs get example tests only.

Oracles: right. P3 compares with exact == and no tolerance, and the values are integer-valued, so the comparison is exact.

Fix: draw outside with st.integers(1, 1000) times a sign, so the minimal draw survives. Also add @examples for 0 rows, an empty selection and a full selection, and a sampled_from over the four output dtypes.

P3-3. Differential (b) can pass vacuously, and its input typing likely misreads some roots

I read it and did not run it: no engine here.

  • Row-locality shape: right. It compares the full batch with a shuffled batch, a single row and a random subset, row by row, then compares the view path with the full batch, with P3 on decimals.
  • Vacuous pass: nothing asserts that any output varies across rows. If _real_inputs' guessed types make every row route the same way (for example, all outputs 0), every comparison passes trivially. Add assert any(len(set(full[name].tolist())) > 1 for name in outputs).
  • Typing by suffix: a root named age, child_age or a bare region falls through to bool, because only _years/_count/_months/_rank, _income/_amount/_eur and _region are typed. Whether the engine refuses a bool where it compares an integer, and names the input, is unknown. It may coerce silently, which feeds the vacuous case above.
  • Selector clobbering: if any root input is named is_child, the for name, values in batch.items(): people[name] = column loop overwrites the selector with zeros outside the batch rows, and the view comparison breaks for a reason unrelated to the engine. Use a selector name that can't collide (for example __view_selected), or assert that it isn't a root.

P3-4. Overriding the selector is refused with a misleading message

Through SimulateCounterfactualKernel._evaluate with the fake view adapter, input_overrides=[{"entity": "person", "column": "is_child", "value": True}] raises Overrides must name distinct engine input columns. The refusal is right: the selector isn't an engine input. The message is wrong, though, and a reader would look for a duplicate. Optional.

Mechanism checks that passed (what I tried)

  1. Gather and scatter on non-range indexes. A Frame with person index [4,2,0,3,1], [7,7,7,7,7] (all duplicates), ['e','d','c','b','a'] and [10,20,30,40,50], with selection [T,F,T,T,F]. Every output equals f of its own row, or the outside value. table.loc[mask] with a bool ndarray is positional, and _batch_from_table uses to_numpy, so order holds.

  2. Engine dtypes against declared dtypes. For each declared dtype, the fake program returned other numpy types:

    Declared Cast to graph dtype Refused
    decimal float32 → float64 int32, bool
    integer int32, uint64 → int64 float32, bool
    judgment int8, uint8 → int64 float64
    bool bool int32, float32

    The outside array and the scattered values always share one dtype, so out[mask] = … never casts silently.

  3. _outside_values, at 0 rows and at 3:

    • Decimal: 2**53+1 is refused and 2**53 is accepted. np.float64(0.25) passes, because it subclasses float. inf is refused at AxiomView. -0.0 passes.
    • Integer: True, 3.0 and 2**63 are refused.
    • Judgment: 2 is refused and -1 passes.
    • Bool: 1 is refused.
    • np.int64(3) and np.True_ are refused at AxiomView, which accepts only Python numbers. That's stricter than _outside_values, but consistent with JSON input.
  4. Shapes. A fake engine returned length 1 with 2 rows selected, (selected, 1), and length 4 with 2 selected (4 = n). Each is refused with "selects 2 row(s)".

  5. Counterfactual and solve-zero, which the spec doesn't cover. With the fake view adapter:

    • _evaluate with an override person.age = 10 reaches the gather: the engine saw ages [10, 10], and the outputs were [100.0, -7.5, 106.0].
    • Solve-zero's trial on person.income reaches the view: [100.0, -7.5, 320.0].
    • validate_input_override returns early without relations, and variables() includes the view roots, so view inputs are admitted.
    • Both kernels call SimulateRulesKernel._require_relation_inputs per component in _prepare (F/rules_kernels.py:333), and relation_dependencies("person") now carries the selector. A masked person slice or one without the selector refuses there; test_every_rules_kernel_requires_the_whole_selector pins it.
  6. Closure. Built on the test_transport_views.py fixture with a person frame slice of {is_child}:

    • An encoded (concept_binding) Child.age the slice lacks lands in frame_missing.
    • An encoded (graph_node) one does not.
    • An unclosed Child.child_income is a gap, and a defaulted one the slice lacks is frame_missing.
    • For a module bound without views, its Child closure entries land in unexpected and its roots in unmapped_entity.
    • unreached_roots can't excuse a view root: it is checked against live_roots[binding][engine_entity], and the registry refuses a Child input named like a Person root.
    • The cross-binding question (the union passed to without_unreached): for a binding without views, Child isn't in its live roots, so the union can't reach them. The one cross-binding effect I found is P2-1.
  7. Engine-free paths can't compile a view. In packages/*/src, the transport callers of variables() and view_root_inputs() are engine_root_inputs (inside the closure kernel at run time), _check_views (registry, after axiom_engine_ref) and the counterfactual bridge (at run time). Compose reaches the selector only through binding_view_dependencies, which builds no adapter. rules_engine_input_refs in F/concept_mapping.py isn't used for Axiom.

Mutants (re-run of the hand-off's, plus mine)

Method. Each mutant was applied and restored by script. Rows 1 to 17 ran against test_axiom_views.py, test_transport_views.py, test_transport_evidence_kernels.py::TestClosurePartition and test_axiom_engine_ref_goldens.py (111 tests at baseline, 1 to 1.3 minutes each). The three survivors were re-run on the full 7-file set (394 tests).

Mutant Result
scatter without the mask (out[:selected] =) red, 9 (P1, P2, P5, P6, diff (a), …)
outside values → 0 red, 8 (P1, P2, P3, empty selection, …)
outside values → NaN red, 13
gather sorted by age red, 4 (P1, P5, P6, diff (a))
a null selector read as false red, 1 (null-selector test)
selector dropped from relation_dependencies red, 4 (kernel refusal ×2, dependency, agreement)
view program also cached under the frame key red, 3
Child roots put on Person in engine_root_inputs red, 1
own: view entities made live on every binding red, 1 (test_a_binding_local_view_partitions_its_logical_entity_roots)
own: selector added for every entity red, 1
own: compose guard drops defaulted red, 1
own: scatter through np.where on a resized, misaligned array red, 8
own: selector dtype check accepts ints red, 1
own: view_root_inputs keeps only the first view survives, full set (P2-2)
own: outside keys only a superset survives, full set (P3-1)
own: binding_to_frame gets every binding's view entities survives, full set; equivalent: the map is consulted only for a binding's live roots, which already exclude other bindings' view entities
own: _view_program metadata merge overwrites, not setdefault survives; equivalent: one name has one derived item per module

I did not re-run the hand-off's views: []/null ref, closure-refusal-kept, and placement-lint rows. The golden and lint tests pass at head, and I read both.

PR claims confirmed

  • Tests: at ef398616, -n 4, the 7 files above: 394 passed, 5 skipped, each skip gated on its environment:
    • differential (b): no dense extension or POPULACE_RULESPEC_BE;
    • test_axiom_relations.py:798: no dense extension;
    • test_nz_axiom_input_closure.py:535 and :610: no rulespec-nz;
    • the real-tree golden: no POPULACE_RULESPEC_NZ.
  • Differential (a) compares the view path with an AxiomEngine whose person entity is child (entity_names={"child": "Child", …}), through _batch_from_table and _graph_output, comparing values and dtypes, as ruling 8 asks.
  • Separate cache keys (_view_programs apart from _programs), the pinned-tree re-check before each view compile, and axiom_engine_ref's refusal after a view compile: read in code, each pinned by its test, and the cache mutant turns red.
  • Absent views add no ref key: if engine._views is not None. The golden test passes at head.
  • The docs commit (ef398616) matches the code: the registry's refusal list, the selector slice, unreached_roots excusing only engine-entity roots, and the support roster.

Not run


Generated by Claude Code

@MaxGhenis

MaxGhenis commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

S2b PR 1 integration review (brief 6040): APPROVE

Review head ef39861676582382ff62de60aeed3f6f025de2b8, base 4a36edcf, Python 3.13, uv sync --all-packages --locked. I found no P1 or P2. The five P3s below are optional. I made no commits, pushes or PR edits. All experiments ran from scratch files or scratch worktrees outside the tracked tree, and git status --porcelain is empty at the end.

F/ = packages/microcosm-frame/src/microcosm/frame/, T/ = packages/microcosm-build/src/microcosm/build/transport/.

Findings

P3-1. The registry pins outside values that the adapter refuses only when the view runs. I ran build_transport_registry on the test_transport_views.py fixture with three edits. Each one built a registry and an engine reference that carried the value:

  • child_amount (decimal) set to true
  • child_test (judgment) set to 2
  • child_test set to 0.5

_outside_values (F/adapters/axiom.py:2285) refuses all three, but only inside materialize. The run still fails closed, but it fails at the rules node rather than at preparation, and the reference names a declaration that can never run. A cheap fix: in _check_views, call _outside_values(name, derived[name].dtype, value, 0) for each outside key, after the outside-keys equality check.

P3-2. AxiomView equality ignores the outside value's type, and the class can't be hashed.

  • AxiomView("Child","s",{"a":0.0,"b":0}) == AxiomView("Child","s",{"a":0,"b":False}) returns True, but to_dict() serializes 0.0/0 in one and 0/false in the other.
  • So _check_engine accepts an injected AxiomEngine whose outside values differ in type from the binding's. axiom_engine_ref then writes the engine's values, not the binding's. An injected engine with False for a judgment output would then refuse at run time (see P3-1).
  • Values do survive the round trip: _binding_semantics stores to_dict(), and the re-parse compares equal (v == w and (v,) == (w,) are both True).
  • hash(view) raises TypeError: unhashable type: 'dict': the frozen dataclass generates __hash__, and the MappingProxyType field can't be hashed. Nothing hashes a view today. I grepped the adapter, registry, compose and evidence modules for sets, dict keys and cache; the only @cache is axiom.py:438, which is unrelated. Comparing (type(v), v) per outside entry in _check_engine, or defining __eq__/__hash__ over to_dict(), would close both.

P3-3. The compose guard on view inputs needs a closure resource. _require_relation_inputs returns before the closed-input check when the spec has no axiom_input_closure. On the merged #1482 + #1480 tree, a view node whose person slice omits a defaulted Child root composes when I drop the closure resource. With a fake engine, the rules kernel then raises KeyError: 'child_rate'. With the real engine, the outcome depends on whether the engine treats the input as required or optional. This is the same exposure that mapped-entity rules nodes have today, and without a closure nothing writes Child defaults anyway. The docs state the condition correctly ("When the spec carries an input closure"). Recording it so S2b PR 3, or BE's spec, keeps the closure mandatory for any view binding.

P3-4. The CI golden couples to NZ's live binding file. nz_document() reads B/nz/axiom_rules_bindings.json at test time and overrides only the pins and sha256. Any later NZ edit to a binding row turns test_engine_refs_match_the_base_golden_byte_for_byte red, even with no view involved: a new binding, a changed variables, periods, or unreached_roots. After such an edit, "generated at the base" no longer describes how to regenerate the file. The real-tree golden also reads the bindings' rulespec commit, so an NZ re-pin breaks it too; that one looks intended. The NZ hub should know this before G8-x touches the bindings.

P3-5. Docs wording. Every sentence in the new "Binding-local views" section matches the code; I checked each refusal, the selector-slice rule, the closure and roster behaviour, and absent-means-absent. One overstatement: "a real-engine differential … tests the assumption". That test is written but has never run. "is written to test" is accurate until the Linux run lands. Also, outside serializes in sorted key order, not declaration order; the doc's "in declaration order" holds for the views list itself.

What I ran and what it showed

  1. Registry refusals. I built each extra shape through build_transport_registry:
    • Refused, each with a clear message:
      • selector given as a string (TypeError … selector must be a mapping)
      • an extra key in the view (… hold exactly logical_entity, selector and outside)
      • outside given as a list (… outside must be a mapping)
      • a string, null or NaN outside value (View outside value … must be a finite bool, integer or decimal number)
      • logical_entity: "Person" and logical_entity: "Household" (names a mapped engine entity)
      • a missing outside entry (outside keys ['child_amount'] differ from … ['child_amount','child_test'])
      • outside: {}
      • views: [], views: null and views: {...}
      • two views on one logical entity
      • an empty selector column
    • Accepted: the three outside values in P3-1, and a selector naming a non-person column (household_id). The adapter refuses the selector at run time as absent.
  2. Compose.
    • Slice.columns can't be None or empty: Slice.__post_init__ raises GraphError on a falsy columns, so the closed-input check can't crash or read "every column".
    • _closure_defaults: I put two bindings on one module path, one with a view, in both orders, with Child and Person defaults sharing the name child_income. Both cases gave the class closure_default on ('person', …). Only the provenance label keeps one source. Encode person_role bindings and their closure defaults on the person table (S2b PR 2) #1480's _defaults refuses conflicting values on the person table, and apply_defaults refuses a defaulted name that is also encoded. I found no wrong class.
    • The five (binding["entity"], variable) sites (T/compose.py:1761, 3035, 3083/3114, 3423, 3802) key on the binding row's entity, which stays person for a view binding. I confirmed this by reading the code only; I did not run it (see "Not run").
  3. Goldens.
    • axiom_engine_refs.json: regenerated at base 4a36edcf with base source plus the PR's axiom_ref_goldens.py. Byte-equal, 10 refs.
    • rulespec-nz: git clone works; I exported 8dc2507d5f10a0467c285fed54d018ee37246798 with git archive.
      • test_nz_real_refs_match_their_frozen_copy passes at base and at head.
      • A fresh freeze at base (golden deleted, test rerun) is byte-equal to the committed nz_real_axiom_engine_refs.json.
      • It holds the six keys accommodation_supplement, family_tax_credits, income_tax_schedule_1, main_benefits_entitlement, main_benefits_rates and nz_super. None carries views.
    • Mutant views: [] written when absent, then mutant "views": null. Each turns test_absent_relation_declaration_stays_absent, the CI golden test and the real-tree golden test red (3 failed, 58 passed each). axiom.py was restored.
  4. Kernel hashes. make_composed_fixture gives 45 refs, plus the three simulate.* with _adapter_classes = (AxiomEngine,).
    • Base to head, exactly 11 move, the hand-off's list with the same prefixes: export.prepare@1, export.readback@1, gates.battery@1, transport.{donor_ranges,donor_receipt,input_closure,support_bounds,weights_audit}@1 and the three simulate.* under Axiom.
    • transport.create@1 and @2 both stay cd21a414f12ab1b9.
    • git diff --stat 881d78aa ef398616 touches only the doc and the NZ golden JSON, so the owner's two commits move no hash.
    • US primary-QRF worker: fresh processes at base and head. primary_qrf_worker_bindings_semantically_equal(..., boundary=...) returns True; the only differences are the launcher aliases (sys_executable, sys_prefix, argv_template_0). No "Duplicate source modules".
  5. NZ.
    • load_transport_spec("nz") loads at both commits, and no NZ binding declares views (grep -c '"views"' = 0).
    • I ran NZ's toy graph (make_nz_toy_fixture + run_entitlement_graph, 136 nodes) at base and head.
      • Node ids are identical.
      • Only the 8 toy-registry kernels change kernel_impl_hash. NZ's toy graph binds toy engines, so the simulate.* don't move there.
      • Every population table on every node is equal (pandas content hash).
    • Node keys and some opaque artifacts differ even between two head runs at the same path, so node keys can't be compared across commits this way. To separate noise from change, I removed every artifact that differs head-vs-head. That leaves three base-vs-head artifacts: nz.evidence.input_closure, nz.evidence.donor_ranges and nz.evidence.support. Their decoded JSON differs only in implementation_hash, plus the donor_ranges digest that embeds it.
  6. PR 1 with PR 2.
  7. Tests at head (-n 4, the brief's 13 files): 2422 passed, 37 skipped, 0 failed.
    • Skips: 32 BE_CHRONICLE_ARTIFACT unset; 2 need the dense extension plus rulespec-nz; 1 POPULACE_RULESPEC_NZ (the real-tree golden, which I ran separately above with the export); 1 differential (b) (dense extension / rulespec-be); 1 dense extension missing.
    • python3 tools/ci_test_plan.py verify: verification=ok.
    • PR 1 merged onto current main 31c89390, running test_axiom_views, test_transport_views, the goldens (with rulespec-nz), test_transport_registry and test_transport_evidence_kernels: 312 passed, 1 skipped, 1 failed. The failure is TestImplementationHashes::test_the_batterys_binding_identity_adds_only_the_leaf_and_its_imports (222 vs 220 modules), and it fails the same way on main 31c89390 without this PR. It's pre-existing on main, not caused by PR 1.

PR claims confirmed

Not run

  • Differential (b) and any real axiom_rules_engine run: there is no dense extension here.
  • A full composed toy graph with a view binding through precalibration, holdout and export. I confirmed the downstream claim by reading the five key sites and by the PR's variable_metadata/graph_dtype test only.
  • The full repository suite beyond the brief's 13 files.
  • I did not read the other reviewer's (6039) comment.

Generated by Claude Code

MaxGhenis and others added 10 commits November 3, 2026 23:17
main moved from this PR's base, 4a36edc, through G (#1478, f8f543a) to 875506e. The merge has no conflict: git merge-tree writes tree 70ca7aa.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first review of the fix round (P2) found that 0d6201a's selector
refusal also refused an override of a selector that a Person program
reads as a root input. The registry accepts that shape, variables()
lists the selector, and before 0d6201a the counterfactual bridge
applied such an override. validate_input_override now names a selector
only when no program reads it, which is exactly when the bridge refuses
the override as outside the engine's inputs, so the check changes no
override the bridge applies.

test_a_selector_that_a_program_reads_stays_overridable drives the
counterfactual bridge with a Person program that reads is_child: the
override reaches that program's input and the rows the view selects. It
fails on 0d6201a's check. The refusal test now also runs through the
bridge, which calls no program. The guide states the rule.

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

MaxGhenis commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor Author

S2b PR 1 engine-gated run, round 2 (brief 6042): pass

Every file in run A (PR 1's head) and run B (PR 1 merged with PR 2) passed on the real engine, with no failures and no setup skips [ran]. Differential (b) now passes, and three fresh processes built byte-identical input batches from seed 530849 [ran].

Build

  • Microcosm, PR 1's head: f1d48f79642634bedeee9ef290380cacbb0a4e66 (git rev-parse HEAD in a detached worktree) [ran]. git status --porcelain printed nothing after both runs [ran].
  • Microcosm, PR 2's head: 17e2643316d31d7fd49eb9809e0de94945b3679f [ran].
  • Engine: TheAxiomFoundation/axiom-rules-engine at 753ce7d9ac4cfb161f6aa4f281f5a3980e67a57d, cloned over HTTPS. git merge-base --is-ancestor 33ce416 HEAD exited 0, and git status --porcelain printed nothing before the build [ran]. The maturin build then left its dist/ output as the only untracked path.
  • rulespec-nz: 8dc2507d5f10a0467c285fed54d018ee37246798; rulespec-be: 530849e4d53f83fc5109acbfc9f72f933be5c299, each exported with git archive | tar -x [ran]. be/statutes/family_benefits/regional_routing.yaml hashes to 408e73587e60d91136f80a71612802cb8c111f7ea5baf5abaeecb772c43c2413, which the test expects [ran].
  • Wheel: axiom_rules_engine_dense-0.1.0-cp314-cp314-manylinux_2_34_x86_64.whl, sha256 d87e02997c9d43b0d011ed28daa8f897e426d4ad715af00d66c0a31562843f8a, built with maturin 1.14.1 under rustc 1.97.0 (2d8144b78 2026-07-07) [ran]. It differs from 6041's 1d888c27…ea0e under the same rustc version. I did not look into why.
  • CLI: cargo build --release --locked. axiom-rules-engine capabilities prints engine_build: {commit: 753ce7d9ac4cfb161f6aa4f281f5a3980e67a57d, dirty: false} [ran].
  • Python 3.14.6 (uv python install 3.14). Each of three venvs outside the checkouts (run A, run B and an ef398616 baseline) imports axiom_rules_engine and axiom_rules_engine_dense from its own site-packages, and microcosm.frame.adapters.axiom from its own worktree. from axiom_rules_engine.dense import NativeCompiledDenseProgram, DenseRelationBatch runs in each [ran].

I ran each file with python -m pytest <file> -n 4 -rs -p no:cacheprovider, with POPULACE_RULESPEC_NZ and POPULACE_RULESPEC_BE set and the CLI first on PATH [ran]. -p no:cacheprovider stops pytest from writing .pytest_cache into the worktree.

Run A: PR 1's head, and run B: PR 1 + PR 2

For run B, I made a local merge commit of 17e26433 into f1d48f79 and did not push it. The merge reported no conflicts and produced tree dc5ace4ffe9d8e06dce8690703b72257f4d9a70c, with merge base 875506e8aecbc7bd975f4ca234c3edbbe000e47a. Both match the owner's merge-tree result [ran]. Run B used its own venv, synced from its own worktree.

File A passed A failed A skipped B passed B failed B skipped 6041 at ef398616
frame test_axiom_relations.py 64 0 0 64 0 0 64 / 0 / 0
frame test_axiom_views.py 59 0 0 59 0 0 54 / 1 / 0
frame test_axiom_adapter.py 44 0 1 44 0 1 44 / 0 / 1
frame test_axiom_graph_adapter.py 167 0 3 167 0 3 167 / 0 / 3
build test_nz_axiom_input_closure.py 21 0 0 21 0 0 21 / 0 / 0
frame us test_rules_engine_contract.py 8 0 0 8 0 0 8 / 0 / 0
build test_axiom_engine_ref_goldens.py 3 0 0 3 0 0 2 / 0 / 0
build test_transport_views.py 51 0 0 51 0 0 42 / 0 / 0
build test_transport_registry.py 59 0 0 59 0 0 59 / 0 / 0

Skips, identical in both runs and matching 6041's:

  • test_axiom_adapter.py:118, "axiom engine is installed here" [ran]. The skipif(_ENGINE_INSTALLED) guard on test_engine_methods_describe_installation_when_missing skips it by design when the engine is installed [read].
  • test_axiom_graph_adapter.py:2019 (3 cases), "git here records the decomposed name as the file system lists it, so a commit's NFC name and an NFD file never name one file" [ran]. The test skips when ls-tree lacks the NFC name or _checkout_files lacks the NFD name (test_axiom_graph_adapter.py:2016-2022) [read]. This is the Linux file-system case.

The engine-gated tests ran instead of skipping: test_real_dense_relation_fixture, test_differential_a_…, test_differential_b_…, all three TestBelgianPilotSlice tests and test_nz_real_refs_match_their_frozen_copy each reported PASSED under -rA [ran].

The added tests explain the higher counts [read, git diff ef398616 f1d48f79]:

  • test_axiom_views.py adds test_input_overrides_name_a_view_selector_in_the_refusal, test_a_selector_that_a_program_reads_stays_overridable, test_real_inputs_preserve_types_regions_and_seed_across_root_orders and test_view_equality_preserves_outside_value_types_and_hashes, and gives P1 to P6 and differential (a) a dtype parameter.
  • test_axiom_engine_ref_goldens.py adds test_ci_golden_source_ignores_live_nz_bindings.
  • test_transport_views.py adds five shared-path, two-view and outside-value tests.

No test that passed at ef398616 fails or skips here. Nothing failed, so I had no failure to rerun at ef398616 and no golden references to diff.

The only warnings came from 9 PerformanceWarnings for a fragmented DataFrame, at test_axiom_views.py:1183 (people[name] = column) [ran].

Differential (b)

I reproduced the test's steps in a script that loads _real_inputs and the test module's names from test_axiom_views.py at f1d48f79 through importlib.util.spec_from_file_location. The script draws from the generator in the same order as the test (test_axiom_views.py:1148-1172) [ran].

  • The program reads 107 Child roots, has no relations, and does not read is_child [ran].
  • _real_inputs gives 21 int64, 81 bool and 5 float64 columns. The engine accepted all of them [ran].
  • It has 54 Child outputs in the graph kinds, all decimal in derived_metadata, including names such as *_condition_met and *_applies [ran]. The view returns each as float64 [ran].
  • Batch sizes: full 40, shuffled 40, single-row 1, random subset 15. Through the view, 80 persons with 40 selected [ran].
  • Every batch shape gave every row the full batch's value for all 54 outputs [ran].
  • 52 of the 54 outputs vary across the full batch, which satisfies the check at test_axiom_views.py:1153 [ran]. brussels_family_benefits_article_25_age_or_status_condition_met and wallonia_family_benefits_article_5_age_or_status_condition_met equal 1 on all 40 rows (fsum 40.0) [ran].
  • Through the view, every selected row matched the full batch, and every unselected row held its outside value (0.0) [ran].
  • The math.fsum check (test_axiom_views.py:1211) held for all 54 decimal outputs. For example, wallonia_family_benefits_selected_monthly_amount gives 30231.329999999998 on both sides [ran].
  • No assertion failed. test_differential_b_real_child_program_against_the_view passed in both runs [ran].

Seed reproducibility

I ran three fresh processes. Each one compiled the program as the test does, built _real_inputs(program.root_inputs, 40, np.random.default_rng(530849)), and hashed the sorted names with each column's dtype and bytes [ran]:

Process root_inputs order sha256 (first 16) Batch sha256
1 17cd540887cfef08 7371d47a483e7ae27af9ae6c0fb29443dc27cd684bfb0fb625a9c52399aa2044
2 2d0c8961cb452518 7371d47a483e7ae27af9ae6c0fb29443dc27cd684bfb0fb625a9c52399aa2044
3 2e0ba3d06f7c0654 7371d47a483e7ae27af9ae6c0fb29443dc27cd684bfb0fb625a9c52399aa2044

root_inputs still comes back in a different order in each process, but all three batch hashes match. _real_inputs iterates sorted(names) (test_axiom_views.py:1087) [read], so the seed now fixes the batch.

What I could not do

  • I set up the ef398616 worktree and venv, but did not use them, because no test failed.
  • The script that loads the test module needs PYTHONPATH set to the worktree root, because test_axiom_views.py:34 imports test_support from there [ran]. Pytest supplies that path itself.
  • I did not explain the wheel hash difference from 6041.

Generated by Claude Code

MaxGhenis and others added 2 commits November 4, 2026 15:45
`_real_inputs` said the pinned engine refuses a wrong-typed input by name.
At axiom-rules-engine 753ce7d, `src/dense.rs` reports a bool, text or date
column as a type mismatch when a program needs a number from it ("expected
decimal-compatible dense column" for an arithmetic operand, "{side} side of
comparison is not numeric" for a comparison), and neither message names the
input. The docstring now says that.

`axiom_ref_goldens.py`'s module docstring wrote `"\n"` in a non-raw string,
so it rendered a line break where the regeneration recipe needs the two
characters. It now escapes the backslash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since the merge base 875506e, main gained #1473, #1506, H2 (#1485),
#1501, #1480 and #1488. The merge is clean. Two files changed on both
sides, in separate hunks: compose.py (#1488 on main's side) and
test_transport_evidence_kernels.py (H2 on main's side).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review November 5, 2026 00:20
@MaxGhenis
MaxGhenis merged commit 5c57c51 into main Nov 5, 2026
11 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Landed as squash 5c57c51d on main, from head 36800b14 (gh pr merge --squash --match-head-commit).

At merge time:

  • CI: run 41915381015 passed all 11 checks on 36800b14, which merges main 3e7a7045 into the branch. CI checked out the merge ref 893d2ae, the head merged into 3e7a7045, which has the head's tree (98800a01). Both engine-free jobs ran test_transport_evidence_kernels.py, the one file both H2 and this PR edit, as the NZ hub asked: each job's pytest command lists it, and each job's --durations=25 report lists two of its tests. Each job passed 25,982 tests and failed none.
  • Reviews: at ef398616, 6039 wanted changes (two P2s and four P3s), 6040 approved with five P3s, and 6041 found differential (b) failing on its input typing. Commits 11 to 18 answer them, except 6040 P3-3, which PR 3 takes. The fix round's first review, at 02265bb5, asked for one change, which commit 20 makes. At f1d48f79, A2 and B, both GPT-6.1 Sol through Subfleet, approved with no P1 or P2. A2's one P3 concerns a property test that predates this PR. Commit 21 makes B's two P3 docstring fixes. 6042 passed every engine-gated file on Linux at f1d48f79, differential (b) included. No review after the fix round asked for changes, and GitHub reported the PR mergeable.
  • The NZ hub cleared f1d48f79 (ask 6) at 18:55Z. At 22:05Z it cleared the delta since then: commit 21 and the merge of main 3e7a7045.
  • Since f1d48f79: 760b0b4a changes two docstrings and nothing else, and 36800b14 merges main 3e7a7045 without conflicts. The head differs from the clean merge of f1d48f79 with that main by exactly 760b0b4a's diff, and 14 of the PR's 16 files keep their patch-ids across the merge. The squash's parent is main fc5bb901, and its tree is abff51a7, the tree of the after-CI runs.
  • After CI: main moved to fc5bb901 after CI started: Test low counts and digests, and anchors listed second, in later reports #1513, Sign off the local cuts' target-fit rows (dbacb023, 83802786), and the company car base #1491 and NZ: re-declare the ageing factors from the September 2026 QES #1507, 51 files, none of them this PR's. git merge-tree merges the head with fc5bb901 cleanly, into tree abff51a7, which adds to the head's tree exactly main's changes since 3e7a7045 (the same 51 files, with the same patch-id). On abff51a7, one file per process, every engine-free test file that main changed since 3e7a7045 passed, with skips only for NZ's and the UK's Chronicle feeds; the 15 files of the local run below passed with the same counts as on 98800a01; and test_nz_spec_package.py passed 1,203 in a scratch git repository. Sign off the local cuts' target-fit rows (dbacb023, 83802786), and the company car base #1491 also changed the UK spine parity fixtures, and none of the 52 files outside tools/ that name uk_spine loads a module this PR changes, each imported in its own process. The PR body lists the counts.
  • Locally, on 98800a01, one file per process with a rulespec-nz export at 8dc2507d: test_transport_evidence_kernels.py (156), test_transport_views.py (51), test_transport_registry.py (59), test_axiom_engine_ref_goldens.py (3), test_nz_axiom_input_closure.py (19; 2 skipped), test_axiom_views.py (58; 1 skipped), test_nz_transport_toy_graph.py (40), test_transport_measurement_proof.py (49), test_transport_column_kernels.py (36), test_concept_mapping.py (266), test_group_encode.py (82), test_axiom_relations.py (63; 1 skipped), test_counterfactual_kernels.py (37), test_quantized_solve_zero.py (134), test_transport_composed_graph.py (469) and test_nz_spec_package.py (1,203; in a scratch git repository whose index reproduces the tree). From main 3e7a7045 to the head, the same 11 kernels move as from 875506e8 to f1d48f79, and the US primary-QRF worker identity stays byte-identical.
  • Item 6: M4 still hasn't passed [NZ, 22:05Z], and it can't pass before G8-x sets NZ's engine pins, which nz/axiom_rules_bindings.json still leaves null on main fc5bb901. So the stated re-run hasn't come due, this PR costs NZ cache misses on the 11 kernels' nodes, and the NZ hub reports the day M4 passes.

@MaxGhenis
MaxGhenis deleted the be/s2b-views branch November 5, 2026 00:24
MaxGhenis added a commit that referenced this pull request Nov 5, 2026
Brings in #1482 and the rest of main since 6448792, so CI tests these
commits on top of #1482. Main has changed neither of this branch's files
since 6448792, and git merges without conflicts.

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.

2 participants