Skip to content

Refuse a view binding in a spec with no input closure - #1552

Merged
MaxGhenis merged 3 commits into
mainfrom
be/view-binding-needs-closure
Nov 9, 2026
Merged

MaxGhenis merged 3 commits into
mainfrom
be/view-binding-needs-closure

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Takes up P3-3 of review 6040 on #1482 (#1482 (comment)).

What it changes

Composition refuses a spec that declares a view binding but carries no axiom_input_closure resource, whether or not a node selects that binding.

  • compose_transport_graph checks the spec before it builds any node (_check_view_bindings_have_a_closure in transport/compose.py). The check covers every route to a view binding: a rules node, a solve or counterfactual that selects the binding through an engine_ref param, an extension's node, and a binding no node names.
  • _require_relation_inputs refuses the same case for a rules node, so transport_rules_node refuses it when a caller runs that function without compose_transport_graph. Before this PR, that guard returned before its closed-input check when the spec had no closure, so a view node that omitted a defaulted input composed anyway.

A binding without views keeps composing with no closure. docs/transport-declared-units.md states the rule.

Invariant. If compose_transport_graph accepts a spec in which some binding declares views, that spec carries axiom_input_closure. A binding without views composes with or without one.

What it refuses on main

Nothing tracked [executed]. No JSON or YAML under packages/microcosm-build/src declares views. NZ's packaged spec carries a closure, and its bindings declare no views. Belgium's packaged spec carries neither bindings nor a closure today.

Reach

From main 3db65154 to 5da21904, none of the 45 kernels in the composed toy registry move, nor the three simulate.* hashes bound to the Axiom adapter, and the US primary-QRF worker identity stays byte-identical [executed].

Evidence [executed]

Each mutant ran against the whole of test_transport_views.py (59 tests):

Mutant Tests that fail
The rules-node guard removed test_compose_refuses_a_view_binding_in_a_spec_without_a_closure, test_a_rules_binding_row_refuses_a_view_binding_without_a_closure
The rules-node guard checks the closure before the views the relations-only and real-relation cases of test_a_binding_without_views_needs_no_closure_at_compose
A binding with relations needs a closure its real-relation case
_node's call to the rules-node guard removed test_a_rules_binding_row_refuses_a_view_binding_without_a_closure
compose_transport_graph's call to the spec check removed both cases of test_composition_refuses_a_view_binding_in_a_spec_without_a_closure
The spec check ignores views both of those cases, and test_composition_needs_no_closure_when_no_binding_declares_views
The spec check looks for the bindings resource instead of the closure both cases of the composition refusal test
The test helper renames every mention of the closure, the document's own format included the three tests that use the helper, on "An input closure must declare format"

One file per process, on 52fe3e0c's code and tests. 5da21904 changes only test_transport_views.py, which passes 59 of 59 at the head:

File Passed Skipped
test_transport_views.py 59 0
test_transport_registry.py 59 0
test_axiom_engine_ref_goldens.py 2 1
test_transport_declared_units.py 18 0
test_transport_evidence_kernels.py 156 0
test_nz_transport_declarations.py 60 2
test_nz_transport_toy_graph.py 40 0
test_transport_composed_graph.py 469 0

The golden's skip needs a rulespec-nz export, and the two NZ skips need NZ_CHRONICLE_ARTIFACT.

This head merged with main f023db0b passes test_transport_views.py (59), test_transport_declared_units.py (18) and test_nz_transport_toy_graph.py (40). Main's move from 1d174fc7 changed none of this PR's files, and the diff's patch-id reads the same on either base [executed]. Main then moved to 70b9e691 (#1217: a changelog fragment, a US test and a US release tool), again none of this PR's files; the patch-id holds there too, and test_transport_views.py passes 59 on that merge [executed].

Outside this PR. With a closure present, a solve or counterfactual on a view binding still skips the closed-input check that rules nodes get. That gap predates this PR, and a separate follow-up records it.

Checklist

  • A first independent review (Opus 5.5) asked for changes at 44050d0b: one P2 and four P3s. 52fe3e0c takes up the P2 and the two P3s on the tests. As the review advised, it leaves the presence-only check as it stands, and the gap above goes to the follow-up.
  • Reach at 5da21904.
  • An independent review of 52fe3e0c (Opus 5.5) approved it, with no P1 or P2 and two optional P3s. 5da21904 takes up the first: the test helper's rename also rewrote the closure document's format, so the renamed document no longer parsed. This body takes up the second.
  • The same reviewer approved 5da21904 with no findings.
  • The NZ hub's OK on the revised compose.py hunk, given at 52fe3e0c. 5da21904 leaves compose.py unchanged, and NZ confirmed that its OK covers 5da21904.
  • CI: run 42696696064 passed all 11 jobs on this head merged with main f023db0b. An earlier run at this head concluded failure with four jobs never scheduled, although gh pr checks exited 0; closing and reopening this PR started the complete run.

🤖 Generated with Claude Code

MaxGhenis and others added 3 commits November 9, 2026 06:58
_require_relation_inputs returned before its closed-input check when
the spec had no axiom_input_closure, so a view node that omitted a
defaulted input composed anyway. Composition now refuses the binding
and names the missing resource. A binding without views keeps composing
without a closure.

Review 6040 of #1482 raised this as its P3-3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first review of this PR found that only rules nodes reached the new
guard. A solve or counterfactual that selects a view binding through an
engine_ref param, an extension's node, and a binding no node names all
composed with no closure, while the docs promised a refusal for any
view binding.

compose_transport_graph now refuses a spec in which any binding declares
views and no axiom_input_closure resource exists, before it builds a
node. The node-level guard stays for transport_rules_node, which can run
without compose_transport_graph.

New tests compose the toy spec, with its closure renamed, for a view
binding that no node names and for one a rules node names; they also
pass a rules_binding row through _node, and cover a binding with a real
relation and no views, which still composes without a closure. The docs
now state the rule as the code applies it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The delta review found that _toy_spec_without_a_closure renamed every
occurrence of axiom_input_closure, the closure document's own format
included, so InputClosure.from_dict refused the renamed document. The
composition tests never parse it, but a later reuse would fail with a
refusal that reads like a closure bug.

The helper now replaces the quoted name only, which renames the
resource key and the four selections that name it, and parses the
renamed document, so a return to the bare replace fails all three tests
that use the helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis MaxGhenis closed this Nov 9, 2026
@MaxGhenis MaxGhenis reopened this Nov 9, 2026
@MaxGhenis
MaxGhenis marked this pull request as ready for review November 9, 2026 22:59
@MaxGhenis
MaxGhenis merged commit 4dac02e into main Nov 9, 2026
18 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Landed by squash as 4dac02e9, at head 5da21904, on parent 70b9e691, with tree 6fe98aa8.

  • CI: run 42696696064 passed all 11 jobs on merge e90b9267 (main f023db0b and this head). An earlier run at this head, 42672617316, concluded failure with four jobs never scheduled (both engine-us legs, engine-uk (3.14) and integration-uk) while gh pr checks exited 0; closing and reopening the PR started the complete run.
  • Review: an independent Opus 5.5 agent asked for changes at 44050d0b: the guard reached rules nodes only, so other routes to a view binding composed without a closure. It approved 52fe3e0c, which adds the spec-level check, with two optional findings. 5da21904 takes up the first, the test helper's rename, and the body the second. The same agent then approved 5da21904 with no findings. The reviews fell to Opus because the operator had held every Codex lane.
  • NZ hub: OK'd the compose.py hunk at 52fe3e0c and confirmed that the OK covers 5da21904, which changes only a test helper.
  • Reach: from 3db65154 to 5da21904, none of the 45 toy-registry kernels or the three Axiom-adapter rows move, and the US primary-QRF worker identity stays byte-identical.
  • Merge: squash with a written message at 23:00:10Z, --match-head-commit 5da21904, after gh pr checks exited 0 again. The commit's tree equals the merge tree where the views file passed.
  • Main: main moved to 70b9e691 after that run. Record the SIPP/SCF asset-blend audit in release diagnostics #1217 touches a changelog fragment, a US test and a US release tool, none of this PR's files. The diff's patch-id reads 164276cc6c50cc12 on 3db65154, f023db0b and 70b9e691, and test_transport_views.py passes 59 on the merge with 70b9e691.

@MaxGhenis
MaxGhenis deleted the be/view-binding-needs-closure branch November 9, 2026 23:00
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