Skip to content

NZ: pin the Axiom engine at 753ce7d9 and close #1563's follow-ups (G8-x2) - #1567

Draft
MaxGhenis wants to merge 14 commits into
mainfrom
nzhub/g8x2-pins
Draft

MaxGhenis wants to merge 14 commits into
mainfrom
nzhub/g8x2-pins

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

WIP. G8-x2 sets NZ's Axiom engine pins, refreshes NZ's input surface at that engine, and closes #1563's three follow-ups. NZ's first real-data run (G8-x, step 4) waits on this PR.

Plan

  • N3. transport.omission_register@1 reads the calibration solution beside the problem and decodes it against that problem's sha256 and ordered axis. The people's households must be exactly the axis: the register refuses a person whose household lies off it, and a household on it with no person. Each person's frame weight must equal its household's solution weight as a float64 value (np.array_equal: -0.0 matches 0.0, NaN matches nothing, and a one-ulp difference refuses). NZ graph: three follow-ups from #1555's review (register-fit tie, measurement id selection, constant-read check) #1563 named household weights; the register compares every person's. Its totals still use the problem's pre-fit weights. NZ's graph and the toy give the register an edge from nz.calibrate.

  • Nit 1, readers only. The three income evidence readers select their measurement from income_measurements' own measurement_id, so they move with that resource. The stand-in keeps its literal measurement_id. R2C-1's proof (compose._measurement_declaration) reads a producer's measurement_id as a literal string in the declared rows and refuses any other value, so selecting it there would break the proof. The declarations test pins the stand-in's literal to the resource's id.

  • O2. test_support/microcosm_build/static_reach.py walks a kernel's class and run() from source. It parses each first-party module under packages/*/src and imports nothing under packages/. Its docstring names eleven groups of what it follows, then what its lookups cost, then what it can't see:

    • bodies: every branch, default values, decorators, class bases, keywords and attributes, and module constants;
    • bindings: each alternative of a try or an if, at module or function level, star imports, re-exports, lazy __getattr__ exports (defined or assigned), every form of assignment (for and with targets, match captures and walrus targets included), and a name that a class body declares global, which binds at module level;
    • scopes, as Python scopes names: a class body reads a name it binds from its own namespace only once a top-level statement has bound it and no del can undo it, and from the module otherwise, and a class body's X += 1 reads the module's X until the body has bound it;
    • aliases: a module-level constant that names a module, class or function, also through a conditional, and or or, a walrus, a tuple, list or set (starred elements too), a subscript, and getattr, import_module or __import__ with a literal name;
    • inheritance, through aliased and subscripted bases;
    • chains through self or cls into a module the class body imports, and a name the class body imports where the body, a default value or a decorator reads it;
    • every dunder and sunder method of a referenced class;
    • import-time code, in each module that holds a reached item and in each reached class: expression statements, dels, stores to an item or an attribute, each compound statement's header and targets, in any block, and, in a reached class, each assignment to a name the body declares global;
    • changes in place to a reached name, in any module the walk has read, at module level or in any class body (a nested class's too): method calls (inside an assignment or a comprehension too), stores and dels (through a for or with target too), +=, and calls that take the name;
    • the attribute fallback, over referenced classes and modules used as values (dotted ones too);
    • literal names in getattr, hasattr, setattr, delattr, attrgetter, methodcaller, match class patterns (a class's own or inherited __match_args__), import_module (relative to a literal package, __package__, __spec__.parent or __name__) and __import__ (with a level too), also under a module-level name that an import or an assignment binds to them, and through a constant that each of its bindings sets to a string.

    Each lookup outside a cycle runs once per walk. Lookups that wait on each other in a cycle run again, each meeting what the others hold so far, until a pass over the cycle adds nothing. A cycle whose pass meets a lookup that began before the cycle and hasn't settled joins that lookup's cycle and runs again with it. So each settles on what it finds once the others are complete, whichever the walk met first. A round counts only what it adds, and the walk ends at the first round that adds nothing. A chain of about 200 re-exports, 250 star imports, or 330 constants or inherited __match_args__ passes Python's default recursion limit; the kernels' lookups nest at most four deep.

    test_each_kernel_hashes_every_module_its_run_can_reach checks that, for each of the three income kernels, the walk reaches exactly the listed modules outside microcosm.graph. The walk leaves out one guarded branch, which the test names (HIERARCHY_GUARD): Target.__post_init__ reads microcosm.calibrate.hierarchy only under self.hierarchy is not None, and _read_target builds every decoded target without a hierarchy.

  • O2's cases. static_reach_cases.py writes 191 synthetic roots, each reaching its targets one of those ways, and test_the_walk_reaches_each_way_code_names_a_module checks that each walk reaches exactly its targets. Each target defines only a module __getattr__ that records a read, so test_a_run_reads_what_the_walk_reaches runs every root for real in one python -I -S -B interpreter. Python must read exactly the walk's targets, less OVER (a referenced class's other dunders, and the branch, operand or tuple element a run doesn't take) and plus BLIND (22 roots, each a form on the docstring's list of what the walk can't see). test_the_walk_cases_account_for_every_target checks OVER, BLIND and the seven decoys. test_property_composing_roots_loses_no_target (Hypothesis) runs 2 to 6 roots from one run(): the walk still reaches every part's targets, and Python reads the union of the parts' reads. No two roots share a module whose reads depend on import order, since Python imports a module once. test_the_walk_runs_each_lookup_once_outside_a_cycle counts each lookup's runs and checks that, on every root, each lookup outside a cycle runs once, each lookup in a cycle once more for each pass over it, and each returns each item once. In each cycle, every pass but the last adds items or lookups, and the last adds nothing (_Walk.cycles logs each pass). The test also records which lookups a lookup met before they settled, so each pass must run distinct lookups, a logged cycle's lookups must have met each other, and no two logged cycles may run the same lookups. test_a_settled_lookup_holds_what_a_fresh_walk_finds checks that each lookup a root's walk settles holds what the same lookup finds in a fresh walk. test_each_lookup_in_a_random_tree_settles_on_what_a_fresh_walk_finds checks both on 300 seeded random trees (static_reach_cases.random_tree: modules that bind names to each other, star-import, define __getattr__ and define classes whose __match_args__ a class pattern reads), each walked from run() and from run2(), which make the same reads in opposite orders and must reach the same items. test_the_walk_makes_few_lookups_on_the_cost_roots counts every lookup call, the memo's answers too, and fails past 10,000 on the six diamond and clique roots.

  • Engine pins. axiom_rules_bindings.json: engine commit 753ce7d9ac4cfb161f6aa4f281f5a3980e67a57d, wheel sha256 054c9196d5371f13bb7c2b28f8161ac92294e5c8543aeb6e221fde243897ed42. The hub built that wheel at 753ce7d9 (maturin build --release --locked; CPython 3.14, macOS arm64). The RuleSpec pin stays 8dc2507d.

  • Input surface. The refresh of fixtures/axiom_input_surfaces/nz.json at 753ce7d9 moves only engine.commit. The dense-root differential agreed on every module (39 entity-programs, 0 input, status or catalog mismatches). The closure's surface engine and its four engine source pins move to 753ce7d9. engine_optional_evidence and undetermined_judgments now cite 753ce7d9's lines, each read again at that commit.

  • Activation tests. In test_transport_composed_graph.py:

    • the packaged spec activates;
    • each absent engine pin is the named gap, with an exact message;
    • prepare_transport_build with no sources refuses on the RuleSpec tree's path;
    • the CLI exits 1 with that message and leaves only an empty graph store;
    • the inventory-store test now runs on a copy of the spec without its pins.
  • Docs. The surface README and the refresh tool's docstring name each snapshot's own engine (NZ at 753ce7d9, Belgium at 04315d94). tools/transport_bridge_differential.py's docstring drops the three-evaluation recipe and why_not_solve_zero, which as_rate_bridge.json no longer has.

  • Regeneration (002b6c2). transport_graph.json: the readers' measurement selects income_measurements' measurement_id, and the register reads nz.calibrate's solution. export_replay_bindings.json takes the new closure sha256, and the golden takes the new fingerprint and resource hashes. Every --check passed.

  • Refresh-tool tests, after Belgium's review (cd85cff). A stand-in --dense-python drives build_snapshot in dense mode, and both Hypothesis properties carry deadline=None and suppress too_slow.

  • Local run of the three test files that exercise the walk and the income kernels (test_transport_income_evidence_kernels.py, test_nz_transport_toy_graph.py, test_nz_spec_package.py) with 65e3e79's text: 1,723 passed in 138 s (pytest -n 0, CPython 3.14.7). CI's engine-free jobs run these files and every other NZ and transport file.

  • Local run of the engine-gated tests at d403cce (the closure, the engine-ref goldens and the Axiom adapter tests, with the 753ce7d9 wheel; CI has no Axiom engine job): 356 passed, 6 skipped. The later commits change only the walk, its cases and their test file, which no engine-gated test imports.

  • Independent review at cd85cff (Opus 5.5, in-session; no Subfleet lane was open): APPROVE with no required findings. d403cce takes its three optional findings: the static walk's blind spots, "bit for bit" and "the frame's households".

  • Delta review of d403cce (Opus 5.5, in-session): REQUEST_CHANGES, with one required finding and six optional. eb236a6 and 005844b take all seven (below).

  • Delta review of d403cce..005844b (Opus 5.5, in-session): REQUEST_CHANGES, with one required finding and four optional. c622fc2 takes all five (below).

  • Delta review of 005844b..c622fc2 (Opus 5.5, in-session): REQUEST_CHANGES, with one required finding and six optional. e6e1f4c takes all seven (below).

  • Delta review of c622fc2..e6e1f4c (Opus 5.5, in-session): REQUEST_CHANGES, with two required findings, three optional and four nits. 6d0a1a0 takes all of them (below).

  • Delta review of e6e1f4c..6d0a1a0 (Opus 5.5, in-session): APPROVE, with four optional findings and two nits. 65e3e79 takes all of them (below).

  • Delta review of 6d0a1a0..65e3e79.

Reviews of 6d0a1a0

Two reviews of 6d0a1a0 came back. Belgium OK'd e6e1f4c..6d0a1a0 and asked for one change, which 2278fb3 makes with Belgium's own diff. The delta review approved 6d0a1a0 with no required change and offered four optional findings and two nits. 65e3e79 takes the four findings and adds four roots that pin Belgium's change, and this body takes the nits.

Belgium's getter change (2278fb3)

exported() skipped a module's __getattr__ whenever the name's resolve lookup had settled with any items, so any binding counted as bound for sure:

  • one in a branch, in a try, or under if TYPE_CHECKING:;
  • one that a del or the end of an except clause undoes;
  • one a star import brings in from a module whose __all__ leaves it out;
  • a walrus, or a comprehension's name, inside a subscript in an assignment's target.

A package's sub module that nothing has imported, read as pkg.sub, counted too. Python calls the getter in each of those cases, so the walk missed what the getter reads. The class-body rule in _bound_before shared two of the gaps: the end of an except clause, and a walrus or a comprehension's name inside a subscript target.

2278fb3 applies Belgium's suggested-getter-v5.diff verbatim: git diff 6d0a1a040 2278fb3d7 matches it byte for byte.

  • exported() skips the getter only when the name's lookup has settled with items and the module's top level binds the name for sure: directly, or through a star import of a public name from a module that binds it for sure and nowhere names __all__. _Walk._binds_for_sure decides, from _Module's new deleted, sure, sure_stars and names_all.
  • _binds counts only the names a target spells out (_stored_here). _undoes makes the end of an except clause undo a binding as del does, at module level and in a class body.
  • The docstring's Bindings, Scopes and "What it can't see" entries follow. "What it can't see" drops the sub module's name, which the walk now sees. It adds a function the module calls while it runs, a name another module deletes or the module removes without del, and an __all__ the walk can't see.
  • The cases gain ten roots for a name a module binds only maybe, and r_getattr_sub_attribute; each reaches its own target. r_getattr_submodule moves from BLIND to ROOTS. r_except_class and r_walrus_class pin the class-body rule, and r_getattr_call_cycle, r_getattr_outside_del and r_getattr_hidden_all pin the new blind spots.

65e3e79 adds four roots in the same pattern: a star import's private name (r_getattr_maybe_star_private), and a walrus inside a subscript of a with target, an annotated target and an augmented target (r_getattr_maybe_with, r_getattr_maybe_annotated and r_getattr_maybe_augmented). With them, each of the hub's 11 one-edit knockouts of the change fails a root; before them, two of the first ten failed none. 6d0a1a0's walk misses all 18 roots that the two commits add to ROOTS.

What it costs. The walk now looks up a module's __getattr__ for each name the module binds only maybe.

  • Of the budget test's six roots, r_import_diamond, whose modules bind the target inside a try, makes 263 lookups (6d0a1a0: 182). The most any makes is 1,261 (r_star_clique; 6d0a1a0: 1,257), of the 10,000 the test allows.
  • The 100-module staircase takes 18,125 lookups (18,023), and K nested cycles 9,916 at K = 10 and 43,026 at K = 20 (9,506 and 41,806). The budget test's comment gives the new figures.
  • NZ's 16 kernel walks make 8,088 lookups (8,086), and none has a lookup cycle.
  • Where the walk can't prove a binding sure but Python never calls the getter (if True:, a loop, a try that binds in its body), the walk also reaches what the getter reads. That widens a hash; it never misses a read. No kernel walk changes (Checks, below).

The delta review's findings (65e3e79)

  1. The once test checks what each lookup meets. The test took the walk's own pass log as its allowance for extra runs, so a walk that did extra work and logged it still passed. _counted now also records, from the calls themselves, each lookup that a call met before the lookup settled: one that had begun and that the memo didn't hold yet. _check_runs adds three checks:

    • each pass runs distinct lookups;
    • each logged cycle's first pass runs more than one lookup, or its one lookup met itself;
    • no two logged cycles run the same lookups, since a cycle runs its passes once and then settles or joins a larger one.

    The review's five edits that passed 6d0a1a0's test now fail it. _cycle called twice, and each member run twice in a pass, each fail on the 18 cycle roots. True in place of the cycle condition, and one more logged pass for a lookup that settled outside a cycle, each fail on all 191 roots. An open cycle that runs its passes again before it joins fails on 4 roots. Each also fails random trees. The five join the hub's knockouts (Cost, below). Belgium's four mutants that no test caught at 6d0a1a0 (M2, M3, M8 and M9 in Belgium's review) pass the new checks too, on every root and on seeds 0 to 2,999. Belgium's review reports that none changes an item or a settled lookup over 24,455 walks, only how many lookups a walk runs.

  2. Two more getter calls. The review named three more times Python calls a module's __getattr__ outside a cycle of lookups. Belgium's change makes the walk see the first: reading a package's attribute asks it for a sub module's name until something imports the sub module (r_getattr_sub_attribute, the review's a). "What it can't see" now lists the other two: a star import asks a module that doesn't define __all__ for it, and a star import asks for each name that __all__ lists and the module doesn't bind. BLIND roots pin each: r_getattr_star_noall and r_getattr_star_lazy (the review's b and c). Python reads each target, and the walk reaches neither. No kernel meets them: no first-party module star-imports.

  3. Nested cycles. The budget test's comment now names a second shape that passes its 10,000 lookups: nested cycles, each of which a pass of the next meets. Each then stays open and runs again in every pass of each cycle around it. On the hub's build of that shape, K of them make 9,916 lookup calls at K = 10 and 43,026 at K = 20 with 2278fb3's walk (9,506 and 41,806 with 6d0a1a0's; the review's build there gave 9,668 and 42,118). No kernel walk has a lookup cycle.

  4. Random trees. random_tree gains an eleventh form: a class Base whose base is another name's Base or the Base of another name's attribute, with a __match_args__ that names one of a, b and c, or with none, so that it takes its base's. On the test's 300 seeds:

    • 33 trees settle a match lookup, and 14 run one in a cycle. Before, none did.
    • The test now requires a walk that runs a match lookup in a cycle (28 of the 600 walks do).
    • Its floor counts walks that run a cycle through two passes or more (384 do, and it needs more than 300).
    • 9 trees (15 walks) take the branch that leaves a cycle open, against 8 (12) before. Five roots pin that branch.
    • e6e1f4c's walk fails 7 of the 300 trees, and 22 of the first 1,000.
  5. Nits. In the e6e1f4c section below:

    • the random 7-module trees sentence now names its seeds: e6e1f4c's walk fails 73 of seeds 0 to 2,999, and 78 of the review's own 3,000 (seeds 300 to 3,299);
    • the differential's Checks line now says how it draws its 200 composites.

CI at 6d0a1a0. engine-free (3.14) failed one US test, test_us_release_head_to_head_scorer.py::test_spawned_workers_score_identically_to_sequential_cli, outside this PR's files: the job got an ubuntu-26.04 runner, where uv took the image's Python 3.14.4, and the test's --worker-max-slices guard refuses 3.14.4. Main's run got ubuntu-24.04 and Python 3.14.10 and passed, and another branch fails the same way. The Microcosm hub owns that test and the workflow. Its #1576, which pins the runner image and uses uv's managed Python, merged at 02:32Z on 2026-11-11, before 65e3e79's push.

Checks

On 65e3e79, outside pytest, on CPython 3.14.7 and 3.13.15, which agree:

  • The walk's code is 2278fb3's, and 65e3e79 changes only its docstring (an AST comparison without docstrings finds no difference). git diff 6d0a1a040 2278fb3d7 is Belgium's file, byte for byte.
  • The walk passes every check on all 191 roots: the targets, the runtime reads, the once check, the fresh-walk check and the lookup budget. It passes the composing property on every pair (18,145), on 2,000 seeded draws of 3 to 6 roots and on the composite of all 191.
  • On seeds 0 to 2,999 of random_tree, each walk passes the once checks, each lookup it settles holds what a fresh walk's same lookup finds (287,246 lookups, 620 of them match lookups), and both orders reach the same items. 4,902 of the 6,000 walks run a cycle again (14,560 cycles, 29,803 passes); 84 trees run a match lookup in a cycle.
  • The test's own _counted and _check_runs, run outside pytest, pass on every root and on seeds 0 to 2,999, and fail each of the review's five edits (item 1).
  • The knockouts below: the hub's 164, the c622fc2 review's 51, Belgium's 51 and the e6e1f4c review's 61. Each fails a root or a tree, except those the block below lists with the reason each changes nothing a test can see.
  • The earlier reviews' probes, and Belgium's 52 first-round cases, give what they gave at 6d0a1a0. Only the budget roots' lookup counts move (above).
  • Against 6d0a1a0's walk, the walk loses no item on NZ's 16 kernel walks, the 191 roots or 200 seeded composites of them. 90 differ: the 18 roots that 6d0a1a0's walk misses, and 72 composites that hold one. Each gains items. No kernel walk changes.
  • Against e6e1f4c's walk, the walk loses no item on the same walks. 104 differ: those 18, the four roots in the e6e1f4c section's item 1 that miss their target on e6e1f4c's walk, and 82 composites that hold one of the 22. 101 gain items, and the other 3 trade a (value) site for the root's own lines.

Delta review of e6e1f4c

  1. A cycle that meets an earlier lookup (required; Belgium asked for the same change). e6e1f4c settled a cycle when its first lookup ended, even when a pass had met a lookup that began before the cycle and hadn't settled. The cycle's lookups then froze with that lookup's partial items, so what the walk reached still depended on which lookup it met first. Now such a cycle stays open, as a single lookup that meets an earlier one already did, and joins the earlier lookup's cycle, which runs it again with the rest (_settled, as both reviews proposed). Five roots pin it:

    • r_alias_cycle_outer, the review's shape: a pass meets a lookup under way;
    • r_alias_cycle_two_up, with that lookup two lookups up;
    • r_alias_cycle_sibling, with a lookup that waits rather than runs;
    • Belgium's r_outer_cycle_first, where only a later pass meets the earlier lookup, and r_outer_cycle, which meets the same lookups in the other order. Their modules take the prefix ocyc, since cyco already names r_alias_cycle_outer's package.

    On e6e1f4c's walk the first four miss their target and fail the fresh-walk check, and r_outer_cycle fails the fresh-walk check. On seeds 0 to 2,999 of the review's random 7-module trees, e6e1f4c's walk fails the fresh-walk check on 73, and the walk now fails none, on both versions. (The review's own 3,000, seeds 300 to 3,299, give 78 and none.) With the new lines undone, the walk reaches other items, or settles a lookup on other items, on 74 of seeds 0 to 2,999.

  2. The composing property (required; Belgium asked for a fix too). r_getattr_cycle and r_getattr_cycle_first shared one pair of modules, whose reads depend on which root imports them first, and Python imports a module once. So a composite that held both read only the first part's target, and the property failed in CI on both Pythons. r_getattr_cycle_first now imports its own copy of the pair (gcyf_a and gcyf_b), so no two roots share a module whose reads depend on import order. Belgium proposed skipping the Python check on such draws; the copy keeps that check on every draw. Outside pytest, on every pair of the 169 roots (14,196), on 2,000 seeded draws of 3 to 6 roots and on the composite of all 169, the walk reaches every part's targets and Python reads the union of the parts' reads, on both versions.

  3. Lines that no root pinned. Each of the review's four knockouts now fails a root:

    • the += branch's use of the name's value (r_aug_value: X += value hands X's module to Adder.__radd__, which reads an attribute that the module binds);
    • plain assignments among a class body's global stores (r_class_global_store);
    • exported()'s settled check (r_getattr_bound, whose module binds the name its __getattr__ would answer, and r_getattr_settled, where the name's lookup settles before the getter counts). Each holds a decoy target that the walk reaches without the check;
    • the pass that a join forces (r_join_no_growth).

    The review's three survivors from c622fc2 fail a root too: getattr under another name in a constant's value (r_getattr_named), a reached class's decorators (r_class_decorated) and keyword-only defaults (r_kw_default).

  4. Module __getattr__ calls outside a cycle of lookups. "What it can't see" now lists three other times Python calls a module's __getattr__ (the reviews of 6d0a1a0 found more, which the section above handles): for a name the module binds, when an import cycle reads the module part-built; for a sub module's name, which a from-import asks the package for before it imports the sub module; and for __path__, which a from-import asks a module that isn't a package for. BLIND roots pin each: r_getattr_import_cycle, r_getattr_import_cycle_from and r_getattr_init_cycle (the review's e1 to e3), r_getattr_submodule (e4) and r_getattr_path (e5). No kernel meets them. Of the 565 first-party modules, two define a module-level __getattr__, and the kernels' walks parse one, the package microcosm.graph. Its getter answers only its 48 lazy exports, and the package binds none of them and holds no sub module of the same name. A package has __path__, so Python never asks its getter for it.

  5. The pass log and two tests.

    • _Walk.cycles records each cycle's passes: the lookups each pass ran and how many items and lookups it added. reruns, which tools read, now counts from that log.
    • test_the_walk_runs_each_lookup_once_outside_a_cycle counts each lookup's runs itself instead of reading the walk's count. Each lookup must run once, and once more for each pass that runs it, and in each cycle every pass but the last must add something and the last nothing. The review's three edits that passed e6e1f4c's test (one more pass after the fixpoint, each member run twice in a pass, a join that forces no pass) now fail it or a root.
    • test_each_lookup_in_a_random_tree_settles_on_what_a_fresh_walk_finds walks 300 seeded random trees (static_reach_cases.random_tree), each from run() and from run2(), which make the same reads in opposite orders. Each walk passes the once test's checks, each lookup it settles holds what a fresh walk's same lookup finds, and both walks reach the same items. More than half of the 600 walks must run a cycle again. e6e1f4c's walk fails 7 of the 300 trees, and 31 of the first 1,000.
  6. Nits.

    • The site count: the Checks list of the c622fc2 section below now says that four (value) sites go, and names their roots.
    • The memo-off sentence in the knockouts block now gives what the knockout fails at 6d0a1a0, measured with the test's checks.
    • The __match_args__ entry in "What it can't see" stays. The walk could resolve a constant bound once to a tuple of literals, as it resolves a string constant, but no first-party module assigns __match_args__, so the rule would serve no kernel.
    • Long cycles: the budget test's comment now says that its 10,000 lookups hold for the six Cost roots, not for every cycle. A cycle that gathers one module a pass, over N modules, takes N + 3 passes, and 18,023 lookups at N = 100.
  7. Claims. e6e1f4c's Checks line said that the walk passed every check on all 151 roots. Each root passed alone, but a composite of two of them failed the composing property (item 2), which only CI ran; that line below now says so. The O2 cycle sentence and the composing and fresh-walk invariants hold at 6d0a1a0.

Belgium's asks. Belgium's review of e6e1f4c asked for the same two changes and two roots, and 6d0a1a0 takes them, with two differences: the roots' modules take the prefix ocyc, and the composing fix copies the modules instead of skipping the Python check. Belgium also found that native_band_report's hash on a merge with main matches neither e6e1f4c's nor main's, because main changed a file it hashes. No file in the tree pins it.

Checks on 6d0a1a0's walk, outside pytest, on CPython 3.14.7 and 3.13.15, which agree:

  • The walk passes every check on all 169 roots: the targets, the runtime reads, the once check, the fresh-walk check and the lookup budget. It passes the composing property on every pair, the 2,000 draws and the composite of all roots (item 2).
  • On 3,000 of the test's random trees (seeds 0 to 2,999; the test runs the first 300), each walk passes the once checks, each lookup it settles holds what a fresh walk's same lookup finds (301,826 lookups), and both orders reach the same items. 5,192 of the 6,000 walks run a cycle again (17,074 cycles, 36,375 passes).
  • The knockouts below: the hub's 148, the c622fc2 review's 51, Belgium's 51 and the e6e1f4c review's 61. Each fails a root or a tree, except those the block below lists with the reason each changes nothing a test can see.
  • The d403cce review's 46 probes, and the 005844b review's 46 probes and 6 zero-knockout probes, miss nothing Python reads. The c622fc2 review's 62 new-rule probes hold on 50 forms on 3.14.7 and 49 on 3.13.15, as on e6e1f4c. Belgium's 52 first-round cases give the same one miss, the registry decorator that BLIND holds.
  • Against e6e1f4c's walk, the walk loses no item on NZ's 16 kernel walks, the 169 roots or 200 seeded composites of them (one random.Random(20261110) draws all 200, each sample(sorted(ROOTS), randint(2, 6))). 24 differ (four roots and 20 composites), and each holds one of the four roots in item 1 that miss their target on e6e1f4c's walk. 20 gain items, and the other four trade a (value) site for the root's own lines; no other site goes. No kernel walk changes, and none of them has a lookup cycle (16 walks, 8,086 lookups).

Delta review of c622fc2

  1. Lookup cycles (required). c622fc2 gave every lookup in a cycle the items its first lookup ended with. A constant that maps another's items through an attribute (X = Y.sub, in a cycle with Y) then settled on less than a fresh lookup finds, and what it lost depended on which lookup the walk met first. 005844b reached what it lost. Now, when a cycle's first lookup ends, _Walk._cycle runs the cycle's lookups again, each meeting what the others hold so far, until a pass adds nothing. So each settles on what it finds once the others are complete, whichever the walk met first (except where a pass met an earlier lookup that hadn't settled, which 6d0a1a0 fixes in the section above). The review's four cycle probes reach their targets, and four roots pin the fix:

    • r_alias_cycle_dotted and r_alias_cycle_dotted_first, the review's shape met from each end;
    • r_alias_self_dotted, a lookup that waits on itself;
    • r_alias_cycle_join, a lookup that a pass begins and that joins the cycle.
  2. Recursion depth. The walk's depths match c622fc2's. The docstring now says that a chain of about 200 re-exports, 250 star imports, or 330 constants or inherited __match_args__ passes Python's default recursion limit (196, 246, 327 and 326 on 3.14.7). It also says that the kernels' lookups nest at most four deep.

  3. A nested class's global, and 4. a class-body global as a match class. "What it can't see" now lists both, with Belgium's form for a nested class's header, and a BLIND root pins each:

    • r_nested_global_value and r_nested_base_scope;
    • r_class_global_match;
    • r_function_global and r_function_global_import, for a name that a function binds through global, which 005844b missed too.

    A scan of the tree's 565 first-party modules at c622fc2 finds no class nested in a class body and no global statement in a class body, as Belgium's scans did. It finds 19 function-level global statements, all in us_runtime and uk_runtime modules, which none of NZ's 8 kernels reaches.

  4. Knockouts that failed no root. Seven of the review's eight now fail a root on both versions:

    • _CALLABLES narrowed to the importers (r_getattr_renamed, getattr under a module-level name);
    • the class table for a class that a class body declares global (r_class_global_inherit);
    • name == "__getattr__" in exported (r_getattr_self);
    • _members' filter of global names (r_global_read);
    • body= for a class-body global's value (r_class_global, whose module now binds _source to a decoy);
    • the cut that a lookup under way gives back (seven roots: r_alias_cycle_dotted, r_alias_cycle_join, r_alias_self_dotted, r_getattr_cycle, r_getattr_cycle_first, r_lazy_cycle and r_star_clique);
    • _met.add (r_alias_self_dotted).

    The eighth, the _own_names revert, stays equivalent, as c622fc2's message said. On the review's 51 knockouts at e6e1f4c, 50 fail a root on both versions, and both versions fail the same roots.

  5. Tests.

    • test_the_walk_runs_each_lookup_once becomes test_the_walk_runs_each_lookup_once_outside_a_cycle. A lookup in a cycle runs once more for each pass that runs the cycle again, by the walk's own count (reruns), and every lookup a walk begins settles.
    • test_the_walk_makes_few_lookups_on_the_cost_roots counts every call of the four lookup methods, the memo's answers too, and fails at the first call past 10,000. So a lookup that skips the memo, or keys it more finely than what it looks up, fails there instead of hanging. Five such knockouts each fail at least one of its six roots, in at most 0.1 s.
    • The fresh-walk test runs on every root, the new cycle shapes included.
  6. Claims and docs.

    • c622fc2's message, and its line for r_registry in the 005844b section below, said that registrar "now holds no reached item". The walk does reach registrar._register in the end, through the change pass itself: registrar holds no reached item until that pass finds its change. The line below now says so.
    • The old-lookup timings didn't reproduce, and the PR didn't say which build it timed. The Cost item below now counts lookups, which doesn't depend on the host.
    • The string-constant sentence now says that each of the constant's bindings must set it to a string. __match_args__ = _FIELDS joins "What it can't see" (r_match_args_constant).
    • The annotations sentence names class bodies and modules as well as def statements.
    • The review's unlisted misses join "What it can't see", each with a BLIND root: a change inside a lambda that a statement calls (k05, r_lambda_change), import_module with another package (i03, r_spec_name), and importers that partial wraps or a call returns (n02 and n03, r_partial_import, r_getattr_importer).
  7. Also. In a class the walk reaches, an assignment to a name the body declares global binds outside the class but runs as Python creates it, so the walk now follows it there (r_class_global_aug). An operator inside a statement that binds a name joins "What it can't see" (r_aug_module_effect).

Belgium's asks. Belgium's look at c622fc2 asked for two changes and four cases, and e6e1f4c takes them:

  • exported() now leaves a module's __getattr__ out only when the name's lookup has settled with items. Inside a cycle, Python can read a module part-built, before it binds the name, so the getter counts. Belgium's b3_getattr_order and b3_getattr_first became r_getattr_cycle and r_getattr_cycle_first. OVER holds the getter that Python's import order doesn't pick, since the walk doesn't track that order.
  • "What it can't see" lists Belgium's two nested-class forms (r_nested_global_value, r_nested_base_scope).
  • Belgium's two controls joined (r_class_iadd, r_class_comp_fill).

Belgium's OK at 005844b had left form 1 open and asked for its four probes and eight roots as cases. A class body's X += 1 reads X before it binds it, so it reads the module's X until the body has bound it (r_aug_class). The four probes are r_aug_class, r_nested_change, r_global_setter and r_global_read. The eight roots are r_del_class, r_self_ref, r_with_class, r_ann_class, r_aug_store, r_kw_fill, r_comp_first_iter and r_walrus_comp. OVER holds the target that the walk reaches and Python doesn't read in r_del_class, r_global_read and r_global_setter. r_base_set passes an alias through a set display, which no root did before (Belgium's named_no_set knockout).

Checks on e6e1f4c's walk, outside pytest, on CPython 3.14.7 and 3.13.15, which agree:

  • Each of the 151 roots passes every check alone: the targets, the runtime reads, the run-count check, the fresh-walk check and the lookup budget. A composite of two of them failed the composing property, which only CI ran (the section above, item 2).
  • The d403cce review's 46 probes, and the 005844b review's 46 probes and 6 zero-knockout probes, miss nothing Python reads. The c622fc2 review's 62 new-rule probes hold on 50 forms on 3.14.7 and 49 on 3.13.15. Each miss is a form that "What it can't see" lists, and the 3.13 one is a class-body annotation, which runs before 3.14.
  • Belgium's 52 first-round cases give the same one miss, the registry decorator that BLIND holds.
  • Against c622fc2's walk, the walk loses no item on NZ's 16 kernel walks, the 151 roots or 200 seeded composites of them. Twelve roots differ, each by a change above: ten gain items and two gain sites only. On three cycle roots, four sites that settle recorded for modules used as values go (one on r_alias_cycle_dotted, two on r_alias_cycle_dotted_first and one on r_alias_self_dotted), since the walk now reaches each item first through the root's own lines. No kernel walk changes, and none has a lookup cycle.

Delta review of 005844b

  1. Class-body globals (required). An assignment in a class body to a name that the body declares global rebinds the module's name. 005844b dropped such names from the class's members and recorded no module binding, a regression against d403cce. The module now binds the name to each such value, whether or not the walk reaches the class, and the value reads the class body's names (r_class_global, and r_class_nested_fill for a nested class).

  2. Misses that both walks shared. The walk now follows each one, and a root pins each:

    • a name the class body imports, where the body, a default value or a decorator reads it (r_class_import_scope);
    • a nested class body's changes in place (r_class_nested_fill);
    • a for or with target that stores into a reached name's item (r_for_store);
    • a change inside a comprehension (r_comprehension_fill);
    • an inherited __match_args__ (r_match_inherited);
    • __import__ with a level (r_import_level), import_module relative to __spec__.parent (r_import_spec), and these callables under another module-level name (r_import_renamed);
    • += on a name (r_augassign).
  3. Branches that no root pinned. 005844b's message said "Each branch now has one, and a root fails when it goes", and the O2 invariant said that each knockout fails a root. The review found seven branches whose knockout failed no root, and c622fc2's own sweep found an eighth. Each live one now has a root:

    • del in a class body (r_body_del);
    • __import__'s top package (r_dunder_import_top);
    • a class member's change (r_class_member_change);
    • the global filter for a nested function (r_global);
    • changes in every module the walk has read: r_registry's registrar holds no reached item until that pass finds its change, which then reaches registrar._register;
    • comprehension targets as locals (r_comprehension_decoy, whose module binds the same name to the shadowed decoy);
    • import_module relative to __name__ (r_import_name), from a plain module, whose __name__ differs from its __package__.

    _own_names no longer pops global names, since _visits already drops them. The c622fc2 review found eight more knockouts that failed no root; the section above covers them.

  4. Cost. Each lookup outside a cycle now runs once per walk (_Walk._settled, which follows Tarjan's strongly connected components): what a name binds in a module, what a constant stands for, a package's lazy exports, and a class's inherited __match_args__. At c622fc2 a cycle's lookups all took its first lookup's items; e6e1f4c runs the cycle to a fixpoint instead (the section above).

    • Sixteen Cost roots pin it: diamonds 22 levels deep of star imports, re-exports, constants, bases and import alternatives, a clique of 8 modules that star-import each other, a cycle of 3 lazy packages, two cycles of constants, and seven from the c622fc2 review and Belgium's: four alias cycles, two cycles through a module's __getattr__, and r_getattr_self.
    • With each lookup run afresh wherever the walk meets it (the memo off), each of the six diamond and clique roots passes 300,000 lookups. With the memo, the most any makes is 1,257 (r_star_clique), counting the memo's answers. Both Pythons give the same counts.
    • With 6d0a1a0, each of the 23 Cost roots (these sixteen, and seven that 6d0a1a0 adds: the five cycles that meet an earlier lookup, r_join_no_growth and r_getattr_settled) takes under 7 ms on 3.14.7 and 3.13.15 (the fastest of 5 runs). Run alternately with e6e1f4c's walk on r_alias_modules, r_import_diamond, r_star_diamond and r_star_clique, the two walks take the same time to within 0.4 ms.
  5. Sentences. The owner-push sentence and the parent-packages sentence (in the next section) and the knockout table's labels now match what the code does. r_self_method's comment now says that the class-attribute fallback reaches it.

On c622fc2's walk, on CPython 3.14.7 and 3.13.15, the d403cce review's 46 probes still pass, and Belgium's 52 cases still give the same one miss, the registry decorator that BLIND holds. The 005844b review's own probe script, run against c622fc2's walk on 3.14.7, finds the target of each of its 46 probes, with no runtime error.

Delta review of d403cce

  1. Class-body names (required). A class body that reads a name before binding it, binds it only in a branch, or only annotates it now also follows the module's binding, as Python does. A class inside a function follows the same rule: before its body binds such a name, it reads the module's binding, never the function's, on 3.13 and 3.14 alike. r_class_decorator still expects no target: its body binds target before the decorator reads it, and the runtime test confirms that Python reads nothing there. New roots: r_body_before, r_body_conditional, r_body_annotation, r_body_decorator and r_body_nested.
  2. The namespace sub-package loop. settle counts an item only when push adds it, and the walk raises after 10,000 rounds without settling (r_namespace).
  3. Misses the docstring didn't list. The walk now follows each one, and a root pins each: statement headers (r_headers, r_class_header), filler calls (r_fill_global, with Belgium's r_setup_call and r_fill_call), dotted module values (r_dotted), function-level alternatives (r_local_alternative, r_local_alternative_first), subscripted bases (r_subscript), match class patterns (r_match_keyword, r_match_positional), a getattr alias (r_getattr_alias, r_base_getattr), a module-level setattr (r_setattr_patch), an assigned __getattr__ (r_lazy_assigned), __import__ with a fromlist (r_import_from), setdefault inside an assignment (r_setdefault), a module-level match capture (r_bind_match), a star import of a lazy package (r_star_lazy), a class-level import used through self (r_class_import), a relative import_module (r_import_relative), and getattr with a string constant (r_getattr_constant).
  4. Cases and mechanisms. Each root now tests the mechanism its comment names, and the table below lists each mechanism with the roots that fail without it. r_alternative's live import now sits in the except branch, and r_alternative_first keeps the other order. New roots cover hasattr (r_hasattr), delattr (r_delattr), methodcaller (r_methodcaller), __import__ with a fromlist (r_import_from), a lambda that shadows a name (r_lambda_shadow), and a nested class body's names kept out of its methods (r_nested_scope). Each alias form now has a root whose alias names a class base, which only the alias path follows. The review suggested an alias through a dotted chain, but that no longer isolates the path, because the walk now records a dotted module value too (finding 3). The self/cls owner push goes: restoring it fails no root. Without the class-member attribute fallback, the push still reaches one root's target on its own, r_self_method's, so the fallback is now that root's only path (on c622fc2's 115 roots, CPython 3.14.7, the fallback's knockout fails 17 roots, and 16 with the push restored). On the 8 kernels (3 income, 5 evidence), with and without the cut, the module sets match d403cce's, and the 30 items only the push reached name attributes that their class body doesn't define, such as AxiomEngine._schema.
  5. "The three changed test files." The local-run line now says "the three test files that exercise the walk and the income kernels".
  6. Nits. r_sunder's _missing_ now returns a member, so no root raises, and a root that raises fails the runtime test. The docstring says "imports nothing under packages/", and that __import__ reaches its top package as well as the module it names.

Belgium's seven import-time forms became roots (r_setattr_patch, r_fill_call, r_del_item, r_mutation_alias, r_delattr_patch, r_class_body_update and r_setup_call), and BLIND holds their registry decorator (r_registry_decorator). Of Belgium's 52 cases, 51 match Python exactly, and the 52nd is that decorator. The review's 46 probes all pass. The walk follows import-time code only in modules that hold a reached item and in reached classes, as Belgium proposed. Following each reached module's parent packages as well would add microcosm.build, microcosm.build.transport and microcosm.frame to all 8 kernels' reach, microcosm.calibrate to 3 (the omission register, the native band report and the weights audit), and microcosm.frame.adapters to the input closure, with or without the cut (a variant of c622fc2's walk that pushes each first-party parent package, CPython 3.14.7). microcosm.frame comes through the frame-version check that microcosm/build/__init__.py runs at import (line 50). The docstring names that gap.

The knockouts

Each of the hub's 164 one-edit knockouts of static_reach.py at 65e3e79 fails at least one root or one of the test's 300 random trees, except 2 that change nothing a test can see (below), on CPython 3.14.7 and 3.13.15, and both versions fail the same roots and the same first tree. The walk as committed passes every check on every root and tree. A root fails by its targets unless a line says once (each lookup runs once, and once more for each pass over its cycle; every pass but a cycle's last adds something, and the last adds nothing; each pass runs distinct lookups; a logged cycle's lookups met each other, and no two logged cycles run the same lookups; each lookup returns each item once) or history (each lookup the walk settles holds what a fresh walk's same lookup finds). No knockout fails only a random tree. With the memo off (settled lookups reused), 177 of the 191 roots fail, by targets and once, on both versions. Each line is a group from the walk's docstring, where Cost is the paragraph after the groups, then each knockout and the roots it fails. The Bindings group's o9 lines knock out Belgium's getter change (2278fb3) one edit at a time.

_Module.__init__ reads imports through _statements, the same function that import-time code reads, so a knockout of one of its blocks can fail a root through bindings. Split between the two uses (both versions agree), the bodies knockout fails r_alternative_first and r_lazy_cycle through bindings and r_blocks through an effect, and the except-handlers knockout fails r_alternative through bindings and r_blocks through an effect.

  • Bodies (7): default values (r_class_import_scope, r_default, r_kw_default; by targets, once); decorators (r_body_decorator, r_class_import_scope, r_decorator; by targets, once); class keywords (r_metaclass; by targets, once); class bases (r_class_global_inherit, r_match_diamond, r_root_base; by targets, once); class attributes (r_factory, r_subclass); constant values (r_alias_cycle_join, r_aug_class, r_bind_walrus and 8 more); a class-body global's value knows what the body bound (r_class_global).
  • Bindings (36): module alternatives, first only (r_alternative); module alternatives, last only (r_alternative_first, r_lazy_cycle); function alternatives, last visited (r_local_alternative); function alternatives, first visited (r_local_alternative_first); star imports (r_getattr_hidden_all, r_getattr_settled, r_star and 3 more); star import of a lazy package (r_star_lazy); lazy __getattr__ (r_getattr_cycle, r_getattr_cycle_first, r_getattr_maybe_annotated and 19 more); assigned __getattr__ (r_lazy_assigned); assignments (r_alias, r_alias_base, r_alias_cycle and 77 more; by targets, once); annotated and augmented assignments (r_ann_class, r_aug_class, r_class_global_aug and 1 more); for targets (r_bind_for); with targets (r_bind_with, r_with_class); match captures (r_bind_match, r_bind_rest, r_bind_star); match star captures (r_bind_star); match mapping rests (r_bind_rest); walrus targets (r_bind_walrus); re-exports (r_add, r_alias, r_alias_base and 161 more); class-body globals, assignments (r_class_global, r_global_setter); class-body globals, imports (r_class_global); class-body globals, defs and classes (r_class_global, r_class_global_inherit); class-body globals in nested classes (r_class_nested_fill, r_nested_change); a class-body global's value reads the class's names (r_class_global); a lookup in a cycle counts the module's __getattr__ (r_getattr_cycle, r_getattr_cycle_first; by targets, history); __getattr__ is no lazy export of itself (r_getattr_self); a class-body global class is a class (r_class_global_inherit); the getter skips a settled name that binds only maybe (6d0a1a0's rule) (r_getattr_maybe_annotated, r_getattr_maybe_augmented, r_getattr_maybe_branch and 13 more); a module's del or except clause undoes nothing (r_getattr_maybe_deleted, r_getattr_maybe_except, r_getattr_maybe_global_del); an except clause undoes nothing (r_except_class, r_getattr_maybe_except); a del through global undoes nothing (r_getattr_maybe_global_del); a star import never binds for sure (r_getattr_hidden_all, r_getattr_settled); a star import binds for sure whatever __all__ says (r_getattr_maybe_star_all); a star import's private name binds for sure (r_getattr_maybe_star_private); the importer's del doesn't stop a star import's binding (r_getattr_maybe_star_del); an assignment binds the names inside a subscript target (r_getattr_maybe_target, r_getattr_maybe_walrus, r_walrus_class); an annotated or augmented assignment binds the names inside a subscript target (r_getattr_maybe_annotated, r_getattr_maybe_augmented); a with target binds the names inside a subscript (r_getattr_maybe_with).
  • Scopes (14): comprehensions (r_comp_first_iter, r_comprehension; by targets, once); lambdas (r_lambda_change, r_lambda_shadow); nested class names out of methods (r_nested_scope); class names out of methods (r_class_scope, r_lambda_shadow; by targets, once); class body, never the module (r_aug_class, r_body_annotation, r_body_before and 7 more; by targets, once); class body, always the module (r_ann_class, r_class_decorator, r_class_global and 1 more); nested class body falls back to the module (r_body_nested); nested class body falls back to the function (r_body_nested); nonlocal (r_nonlocal); global (r_aug_value, r_global; by targets, once); del undoes a class-body binding (r_body_del, r_del_class, r_except_class; by targets, once); comprehension targets (r_comprehension_decoy); += reads its target (r_aug_class, r_aug_value, r_class_global_aug; by targets, once); a class body's global names aren't its members (r_global_read).
  • Aliases (11): constants (r_alias_base, r_alias_cycle_dotted, r_alias_cycle_dotted_first and 20 more); conditional body (r_base_conditional); conditional orelse (r_base_conditional); or and and (r_base_or); walrus (r_base_walrus); starred (r_base_set, r_base_starred); __import__'s top package (r_base_dunder_import); tuple elements (r_base_set, r_base_starred, r_base_tuple); getattr (r_base_getattr, r_getattr_named); subscript (r_base_set, r_base_starred, r_base_subscript); import_module (r_base_dunder_import, r_import_alias_base).
  • Inheritance (2): bases (r_alias_base, r_base_conditional, r_base_dunder_import and 13 more); subscripted bases (r_subscript).
  • Self and cls (2): chain through class imports (r_class_import); names the class body imports (r_class_import_scope).
  • Implicit calls (3): dunders (r_add, r_aug_class, r_aug_value and 7 more); sunders (r_sunder); the root's class (r_ann_class, r_aug_class, r_body_annotation and 21 more; by targets, once).
  • Import-time code (23): module statements (r_aug_store, r_blocks, r_del_hidden and 4 more; by targets, once); expression statements (r_blocks, r_fill_global, r_setup_call); del (r_del_hidden); stores into items (r_aug_store, r_store_hidden); if headers (r_class_header, r_headers; by targets, once); while headers (r_headers); for headers (r_for_store, r_headers); with headers (r_for_store, r_headers); match subjects (r_headers); match patterns (r_headers); match guards (r_headers); except* headers (r_headers); effects in bodies (r_alternative_first, r_blocks, r_lazy_cycle); effects in else branches (r_blocks); effects in finally (r_blocks); effects in except handlers (r_alternative, r_blocks); effects in match cases (r_blocks); except headers (r_headers); class bodies (r_class_header; by targets, once); for targets (r_headers); with targets (r_for_store, r_headers); a reached class's global stores (r_class_global_aug, r_class_global_store; by targets, once); global stores include augmented ones (r_class_global_aug).
  • Changes by name (12): all (r_aug_module_effect, r_augassign, r_class_comp_fill and 12 more; by targets, once); method calls (r_class_comp_fill, r_class_fill, r_class_member_change and 5 more; by targets, once); stores and dels (r_class_fill, r_for_store, r_setitem); call arguments (r_class_fill, r_kw_fill, r_registry); through aliases (r_class_fill); class bodies (r_class_comp_fill, r_class_fill, r_class_iadd and 2 more; by targets, once); += on a name (r_aug_module_effect, r_augassign, r_class_iadd; by targets, once); inside comprehensions (r_class_comp_fill, r_comprehension_fill); in every module read (r_comprehension_fill, r_for_store, r_registry and 2 more); a class member's change (r_class_member_change); a header runs whole (r_for_store); nested classes run whole (r_class_nested_fill, r_nested_change).
  • Attribute fallback (4): class members (r_alias_cycle_dotted, r_alias_cycle_dotted_first, r_alias_cycle_join and 26 more; by targets, once); value modules (r_aug_value, r_dotted, r_dunder_import_top and 9 more); dotted values (r_dotted); bare-name values (r_value).
  • Literal names (22): getattr (r_getattr, r_getattr_constant, r_getattr_named and 1 more); hasattr (r_hasattr); setattr (r_setattr); delattr (r_delattr); attrgetter (r_attrgetter); methodcaller (r_methodcaller); match keywords (r_match_keyword); match positional (r_alias_cycle_dotted, r_alias_cycle_dotted_first, r_alias_cycle_join and 12 more); import_module (r_import_alias_base, r_import_module, r_import_name and 4 more); __import__ (r_base_dunder_import, r_dunder_import_top, r_import_from and 1 more); relative import_module (r_import_name, r_import_relative, r_import_spec); module constants (r_getattr_constant); inherited __match_args__ (r_class_global_inherit, r_match_diamond, r_match_inherited); __import__ level (r_import_level); __package__ (r_import_relative); __name__ as package (r_import_name); __name__ names the module, not its package (r_import_name); __spec__.parent (r_import_spec); callables under imported names (r_import_renamed); callables under assigned names (r_getattr_named, r_getattr_renamed, r_import_renamed); __import__'s top package (r_dunder_import_top); getattr and the namers under module-level names (r_getattr_named, r_getattr_renamed).
  • Cost (26): settled lookups reused (r_add, r_alias, r_alias_base and 174 more; by targets, once); one copy of each item (r_alias_diamond, r_alias_modules, r_import_diamond and 2 more; by once); a lookup under way gives its cut (r_alias_cycle, r_alias_cycle_dotted, r_alias_cycle_dotted_first and 15 more; by targets, once, history); meeting a lookup under way joins its cycle (r_alias_cycle, r_alias_cycle_dotted, r_alias_cycle_dotted_first and 13 more; by targets, history); a cycle settles together (r_alias_cycle_dotted, r_alias_cycle_dotted_first, r_alias_cycle_join and 7 more; by once, history); a cycle settles on each one's cut (r_alias_cycle, r_alias_cycle_dotted, r_alias_cycle_dotted_first and 14 more; by targets, history); a constant under way stands for itself (r_alias_cycle_dotted, r_alias_cycle_dotted_first, r_alias_cycle_join and 7 more; by history); the walk ends when a round adds nothing (r_namespace; by targets, once, history); a cycle runs again until a pass adds nothing (r_alias_cycle_dotted, r_alias_cycle_dotted_first, r_alias_cycle_join and 12 more); a cycle runs one pass only (r_alias_cycle, r_alias_cycle_dotted, r_alias_cycle_dotted_first and 15 more; by targets, once, history); a lookup a pass begins joins the cycle (r_alias_cycle_join, r_alias_cycle_sibling, r_alias_cycle_two_up and 3 more; by once, history); a pass keeps what each member held (r_alias_cycle, r_alias_cycle_dotted, r_alias_cycle_outer and 6 more; by targets, history); a lookup that meets itself runs again (r_alias_self_dotted, r_outer_cycle, r_outer_cycle_first); a pass that meets an earlier lookup joins its cycle (r_alias_cycle_outer, r_alias_cycle_sibling, r_alias_cycle_two_up and 2 more; by targets, history); a joined cycle stays unsettled (r_alias_cycle_outer, r_alias_cycle_sibling, r_alias_cycle_two_up and 2 more; by targets, history); a pass logs that it added nothing (r_alias_cycle, r_alias_cycle_dotted, r_alias_cycle_dotted_first and 15 more; by once); the log names the members after the pass (r_alias_cycle_join, r_join_no_growth, r_outer_cycle and 1 more; by once); one more pass after the fixpoint (r_alias_cycle, r_alias_cycle_dotted, r_alias_cycle_dotted_first and 15 more; by once); each member runs twice in a pass (r_alias_cycle, r_alias_cycle_dotted, r_alias_cycle_dotted_first and 15 more; by once); the walk keeps no pass log (r_alias_cycle, r_alias_cycle_dotted, r_alias_cycle_dotted_first and 15 more; by once); reruns counts each pass once (r_alias_cycle, r_alias_cycle_dotted, r_alias_cycle_dotted_first and 15 more; by once); a cycle runs its passes twice (r_alias_cycle, r_alias_cycle_dotted, r_alias_cycle_dotted_first and 15 more; by once); every lookup runs as a cycle (r_add, r_alias, r_alias_base and 188 more; by once); each member runs twice in a pass, logged twice (r_alias_cycle, r_alias_cycle_dotted, r_alias_cycle_dotted_first and 15 more; by once); an open cycle runs its passes again before it joins (r_alias_cycle_outer, r_alias_cycle_sibling, r_alias_cycle_two_up and 1 more; by once); a lookup outside a cycle runs once more as a logged pass (r_add, r_alias, r_alias_base and 188 more; by once).

Knockouts that change nothing a test sees. a waiting lookup passes its cycle up (a lookup that waits on an earlier one stays open, so the lookup that roots the next cycle runs it again, meets the earlier lookup and keeps the cycle open: it costs a pass); a joined cycle passes its start up (the cycle stays open either way, so the lookup that roots the next cycle runs it again and meets the earlier lookup itself: it costs a pass).

The c622fc2 review's knockouts. The review wrote 49 one-edit knockouts and two extras. Applied to 65e3e79's walk, with the one anchor that moved since c622fc2 rewritten to make the same change, and checked on all 191 roots and the 300 trees: 50 of the review's 51 fail at least one root or tree on both versions, and both versions fail the same roots. The other one changes nothing a test can see: global names stay in _own_names (_visits already drops a function's global names).

Belgium's knockouts. Belgium wrote 51 one-edit knockouts of 005844b's walk. With the anchors that moved since then re-pointed to 65e3e79's text, each fails at least one of NZ's 191 roots by its targets, on both versions (Belgium's own runner, over a copy of the tree). The e6e1f4c review's port of the same 51, run with the test's checks: 51 of Belgium's 51 fail at least one root or tree on both versions, and both versions fail the same roots. Its port of the c622fc2 review's 51: 50 of the review's 51 fail at least one root or tree on both versions, and both versions fail the same roots. The other one changes nothing a test can see: global names stay in _own_names (_visits already drops a function's global names).

The e6e1f4c review's knockouts. The review wrote 61 one-edit knockouts in 12 groups, over every line of _cycle, _computes, the _settled tail, exported()'s settled check, the += branch and _global_stores. With the eight anchors in _cycle that 6d0a1a0 rewrote, and the four in exported(), _bound_before and _binds that 2278fb3 rewrote, re-pointed to their text: 57 of the review's 61 fail at least one root or tree on both versions, and both versions fail the same roots. The other 4 change nothing a test can see: first-begun first (reversed) (with the ancestor fix in, a pass's order changes only how many passes a cycle takes); met keys never discarded (_met matters only for a lookup that has just begun, and a settled lookup never begins again); waiting lookup drops its low (the hub's a waiting lookup passes its cycle up, from the other side: it costs a pass); += read node is the statement (a bare name's node and its statement start at the same line and column).

What the refresh found

  • Artifact format 3. Engine 753ce7d9 writes compiled-artifact format 3 (590d1dc9, axiom-rules-engine#250), and the refresh tool read only format 2. Format 3 adds program.interface, engine_build and source_closure, none of which the walk reads. The engine's docs/artifact-format.md says that, for source without elections, program without interface, plus metadata, are what format 2 emits. The tool now reads formats 2 and 3, and each artifact must carry the format the engine's capabilities names.
  • Cross-entity advice. Engines from 4d7ec14f (axiom-rules-engine#292) on follow a cross-entity dense error with ". " and advice (cross_entity_advice, src/dense.rs:560-590). The tool now accepts a cross-entity blocker's exact text followed by ". " and any advice, and the snapshot keeps the walk's text. NZ's only blocker is the versioned-formula one, whose text 753ce7d9 keeps word for word. be.json's seven dense_unsupported modules, each a cross-entity error at 04315d94, would need the change at any engine from 4d7ec14f on.
  • New spec kinds. 753ce7d9 adds ten spec kinds that the walk still refuses (fail closed). NZ's modules use none of them.
  • Evidence kernels. The same static walk finds three differences in the shared evidence kernels' lists in evidence_contract.py. A follow-up handles them, because a list change re-keys Belgium's evidence nodes too.

Invariants

The register:

  • Its included totals depend only on the frame's columns and the problem's pre-fit weights. At whatever solution the frame carries, they stay the same (property test).
  • It refuses whenever one record's frame weight differs from its household's solution weight; one ulp is enough (property test).
  • The solution must decode against the problem's sha256 and its ordered axis; a solution of another problem or axis refuses.
  • Every household on the problem's axis holds at least one person in the frame.

Activation:

  • The packaged NZ spec activates. With either engine pin null, activation refuses and names exactly the null pins.
  • The closure's surface engine commit equals the bindings' engine pin.

The refresh tool:

  • It reads exactly the integer formats 2 and 3 (3.0, true and "3" refuse). An artifact whose format differs from its engine's refuses, in either direction.
  • Format-2 and format-3 artifacts of one program give equal snapshots.
  • A cross-entity blocker explains exactly the messages that name its own (dependency, derived, entity), with or without advice. Any other blocker explains only its own text (Hypothesis properties).
  • In dense mode, a cross-entity error, bare or with any of the engine's four advice strings, records the walk's text, so the engine's advice can't change a snapshot.
  • In dense mode the tool also refuses:
    • an error no blocker explains;
    • a missing program;
    • another root;
    • duplicate root inputs.
  • The differential counts each place where dense and the walk disagree: inputs, status and catalog.

O2:

  • Each income kernel's list equals what its run() can reach outside microcosm.graph, less the one named cut.
  • The cut removes microcosm.calibrate.hierarchy and nothing else.
  • Its premise holds: _read_target passes no hierarchy, so a decoded target has none.
  • Mutant: with artifact_types dropped from the native report's list, the trace test still passes and the walk names the module.
  • Each of the 191 synthetic roots reaches exactly its targets.
  • Python, running each root, reads exactly its targets less OVER plus BLIND (a differential test against CPython).
  • OVER only removes targets a root's walk reaches, BLIND only adds targets it doesn't, and every target is a root's, BLIND's or a decoy.
  • Composing roots loses no target, and Python reads the union of the parts' reads (Hypothesis property). No two roots share a module whose reads depend on import order.
  • Each lookup outside a cycle runs once per walk, each lookup in a cycle once more for each pass over it, and each returns each item once. In each cycle every pass but the last adds items or lookups, and the last adds nothing. Each pass runs distinct lookups, a logged cycle's lookups met each other, and no two logged cycles run the same lookups (a test, on every root and on 300 random trees).
  • Each lookup the walk settles holds what a fresh walk's same lookup finds, so a cycle's result doesn't depend on which of its lookups the walk met first (a test, on every root and on 300 random trees, each walked in both orders of the same reads, which reach the same items).
  • On the six diamond and clique roots, a walk makes at most 10,000 lookup calls, the memo's answers included (a test). With each lookup run afresh, each makes more than 300,000.
  • The walk ends: a pass over a cycle stops at the first pass that adds nothing, since items and members only grow and both are finite. A round counts only what it adds, and the walk stops at the first round that adds nothing, or raises after 10,000 rounds.
  • Each of the hub's 164 one-edit knockouts of the walk fails at least one root or random tree by one of those checks, except 2 that change nothing a test can see. So do 50 of the c622fc2 review's 51, each of Belgium's 51 and 57 of the e6e1f4c review's 61, on CPython 3.14.7 and 3.13.15 (knockout runs outside the repository, at 65e3e79). The knockouts block names the rest and why each changes nothing.
  • Against 6d0a1a0's walk and e6e1f4c's, a differential finds no lost item on NZ's 16 kernel walks, the 191 roots or 200 composites of them (one random.Random(20261110) draws all 200, each sample(sorted(ROOTS), randint(2, 6))), on both versions. Against 6d0a1a0's, the 90 that differ each hold one of the 18 roots its walk misses, and each gains items. Against e6e1f4c's, the 104 that differ each hold one of those 18 or of the four roots that miss their target on e6e1f4c's walk: 101 gain items, and the other 3 trade a (value) site for the root's own lines.
  • A module's __getattr__ counts for a name unless the module's top level binds the name for sure (a test, on the 18 roots that 6d0a1a0's walk misses).

Keys

N3, nit 1, and the docstring and comment edits change income_evidence_kernels.py, so the three income kernels' implementation hashes change, and with them their nodes' keys. Only NZ's graph uses these kernels. The module stays outside the gate battery's binding closure (test_the_module_stays_outside_the_batterys_binding_closure), so no battery node re-keys. The commits after d403cce change only test_support/ and a test file, which no kernel's list names, so they re-key nothing.

Shared with Belgium

Belgium also uses these parts:

  • the activation tests in test_transport_composed_graph.py;
  • the surface README;
  • tools/refresh_axiom_input_surface.py and its new test;
  • tools/transport_bridge_differential.py;
  • test_support/microcosm_build/static_reach.py and static_reach_cases.py.

Belgium OKs those hunks before this lands. Their OK covers d403cce for every hunk, and 005844b for static_reach.py and static_reach_cases.py. c622fc2, e6e1f4c, 6d0a1a0, 2278fb3 and 65e3e79 change those two files again. Belgium's review of c622fc2..e6e1f4c asked for two changes and two roots, which 6d0a1a0 takes. Belgium OK'd e6e1f4c..6d0a1a0 and asked for one more change, which 2278fb3 makes with Belgium's own diff, so the two files wait on Belgium's look at 2278fb3..65e3e79.

Closes #1563.

🤖 Generated with Claude Code

MaxGhenis and others added 12 commits November 9, 2026 20:36
…the measurement

Two of #1563's follow-ups:

- N3: transport.omission_register@1 now reads the calibration solution
  beside the problem. It decodes the solution against that problem and
  its axis, requires every household on the axis to hold a person, and
  refuses a frame whose person weights differ, bit for bit, from the
  solution's household weights. The register's totals still use the
  problem's pre-fit weights. NZ's graph and the toy give the register an
  edge from nz.calibrate.
- Nit 1, readers only: the three income evidence readers now select their
  `measurement` from income_measurements' own measurement_id, so they
  move with that resource. The stand-in keeps its literal measurement_id,
  because R2C-1's proof reads a producer's measurement_id as a literal
  string in the declared rows and refuses any other value. The toy's
  income measurements carry the id the same way.

Not yet in this commit: the engine pins (753ce7d9) and the input surface
refreshed at that engine, the closure re-review, the activation test
flips, the regenerated declarations and golden, O2's static test, and the
differential docstring fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Engine 753ce7d9, G8-x2's pin, emits artifact format 3, and the refresh
tool refused it before compiling anything. Format 3 adds
program.interface, engine_build and source_closure, which the walk never
reads. The tool now reads formats 2 and 3, and each artifact must carry
the format the engine's capabilities names.

From 4d7ec14f (#292) on, the engine follows a cross-entity dense error
with ". " and advice. A cross-entity blocker now also explains its own
text followed by ". " and any advice, and the snapshot keeps the walk's
text. be.json's seven cross-entity modules need this at 753ce7d9; NZ's
one blocker, a versioned formula, keeps its exact text.

The docs name each snapshot's own engine (nz 753ce7d9, be 04315d94).
New engine-free tests drive a stand-in CLI, and Hypothesis checks the
error matcher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- axiom_rules_bindings.json: engine.commit 753ce7d9 and wheel_sha256
  054c9196, the dense-extension wheel the NZ hub built at that commit
  (maturin build --release --locked; CPython 3.14, macOS arm64). They were
  the spec's last two activation gaps. The description's relations sentence
  now says what assert_no_relations checks.
- nz.json: the refresh at 753ce7d9, with the walk that reads format 3, moves
  only engine.commit. The dense-root differential agreed on every module.
- axiom_input_closure.json: the surface engine and the four engine source
  pins move to 753ce7d9. engine_optional_evidence and the undetermined
  judgment cite 753ce7d9's lines, each one read again at that commit.
- test_nz_axiom_input_closure.py: the closure's surface engine commit equals
  the bindings' engine pin.
- test_transport_composed_graph.py: the packaged spec activates, and each
  absent engine pin is the named gap. Preparation and the CLI now refuse at
  the missing RuleSpec tree, after the CLI opens its empty graph store. The
  inventory-store test drops the pins from its copy of the spec.
- The input-surface README names each snapshot's own engine, as the refresh
  tool's docstring does.
- transport_bridge_differential.py: the docstring describes the bridge as
  it stands (a counterfactual base rate and a solve_zero cutout). It drops
  the three-evaluation recipe and why_not_solve_zero, which
  as_rate_bridge.json no longer has.

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

The trace test checks the modules a kernel's run() executes, and can't see a
module the run only reads constants from. test_support's static_reach walks
run()'s source instead: every branch, with default argument values,
decorators, class bases and attributes, and the module-level constants it
reaches, each name followed through its imports to the module that defines
it. It imports nothing from the repository.

- test_each_kernel_hashes_every_module_its_run_can_reach: for each of the
  three kernels, the walk reaches exactly the listed modules outside
  microcosm.graph.
- One cut, which the test names: Target.__post_init__ reads
  microcosm.calibrate.hierarchy only under `self.hierarchy is not None`.
  test_no_decoded_target_carries_a_hierarchy checks the premise:
  _read_target, which builds every decoded target, passes no hierarchy.
  test_the_walk_cuts_only_the_hierarchy_guard checks that the cut removes
  that one module and nothing else.
- test_the_walk_sees_a_constant_the_trace_cant: with artifact_types dropped
  from the native report's list, the trace test still passes and the walk
  names the module.
- The kernel module's docstring describes both tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/build_nz_transport_graph.py and the golden writer, run on this branch:
- transport_graph.json: the three income evidence readers select their
  measurement from income_measurements' measurement_id (nit 1), and the
  omission register gains its solution edge from nz.calibrate (N3).
- export_replay_bindings.json: the input closure's new sha256.
- tests/golden/nz_country_spec.json: the fingerprint and the hashes of the
  four resources that changed.

Every generator's --check passes on the result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Belgium's review of d40c004 asked for two changes before landing, and
this commit makes both, plus three of their optional ones.

- A stand-in --dense-python drives build_snapshot in dense mode. It
  ignores the probe after -c, records the jobs it reads from stdin, and
  answers each entity from the outcomes its test gives it. A Person rule
  that reads a Household rule gives the walk one cross-entity blocker.
  - With the engine's error as the blocker's text, or that text followed
    by each of the engine's four advice forms, the module records
    dense_unsupported with the walk's text, and the differential counts
    two entity programs and one dense compile.
  - The tool refuses an error the walk can't explain (another
    cross-entity read, a new limitation, an advised error on an entity
    the walk doesn't block), a missing program, another root and
    duplicate root inputs.
  - The differential records an input, a status and a catalog mismatch
    where dense and the walk disagree.
- Both Hypothesis properties carry deadline=None and suppress the
  too_slow health check, as test_be_target_references.py does. The
  cross-entity property failed that check in 3 of 13 runs on Belgium's
  host.
- The versioned property now checks the blocker's own text, another
  rule's versioned text, and its text with advice after it. The
  cross-entity property checks its own text with and without advice.
- The off-format test adds an engine that names format 2 and writes 3.
- The advice list adds the engine's fourth form (a relation read where
  the program declares no relation).
- The tool's docstring says what the advice suggests (compile for the
  other entity, read through a relation, or compile only the outputs
  that don't reach the reading rule) and drops two passives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cd85cff review found four ways a run can reach a module that
static_reach.py missed: a module alias held in a module-level constant,
getattr with a literal name, __float__ and __add__. None occurs in the
modules the three income kernels reach today, and their lists stand.

The walk now
- resolves a name to every definition that can bind it, through
  try/except alternatives, star imports and a package's lazy
  __getattr__ exports;
- follows module-level constants that alias a module, class or function;
- runs a module-level statement that changes a reached name in place
  (X.update(...), X[k] = v) along with that name;
- reaches every dunder and sunder method of a referenced class;
- reads the literal names in getattr, hasattr, setattr, delattr,
  attrgetter, methodcaller, import_module and __import__;
- sends an attribute of an object it can't type to every same-named
  member of each referenced class and of each module used as a value;
- starts from the root's class as well as its run();
- binds names by scope as Python does: comprehensions, lambdas, class
  bodies, global, nonlocal and local imports.

Its docstring lists each of these and what it still can't see, which
now includes a module that only an annotation names.

static_reach_cases.py writes 32 synthetic roots, each reaching its
targets one of those ways, and a parametrized test checks that each walk
reaches exactly its targets. The cd85cff walk misses a target on 25 of
the 32 and reaches an extra one on a 26th. Turning off any one of the 15
mechanisms fails at least one case.

Under the new walk each kernel still reaches exactly its listed modules
outside microcosm.graph, and only the hierarchy guard's cut applies.

income_evidence_kernels.py: the docstring describes the new walk, and
the solution check's comment now names what np.array_equal compares,
float64 values (finding 2), instead of "bit for bit".

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

The delta review of d403cce found that a class-body name lost its
module-level binding. Python reads a name the class body binds from the
class namespace only once a top-level statement of the body has bound it.
Before that, or when only a branch, a loop or a bare annotation binds it,
Python reads the module's binding. The walk now follows that rule, and a
class inside a function reads its own binding or the module's, never the
function's, on 3.13 and 3.14 alike.

The commit also closes each miss the review listed rather than
documenting it: statement headers at module and class level; filler calls
and other import-time effects in each module that holds a reached item;
changes in place to a reached name (method calls, stores, dels and call
arguments, also through an alias); dotted module values; function-level
alternative imports; subscripted bases; match class patterns; getattr,
hasattr, setattr, delattr, attrgetter and methodcaller with a literal or
constant name; __getattr__ assigned rather than defined; __import__ with
a fromlist; a star import of a lazy package; class-level imports used
through self; and a relative import_module. settle now counts only real
pushes, and the walk raises after 10,000 rounds without settling, so a
namespace sub-package can no longer hang it. The self/cls owner push
goes: the class-attribute fallback reaches the same members, and no
module set moves on any of NZ's 8 kernels.

static_reach_cases.py grows from 32 to 80 roots and gains a runtime
oracle. Each target module records the reads Python makes, and
python_reads runs every root in one isolated interpreter. A new test
asserts that Python reads exactly ROOTS less OVER plus BLIND for each
root, so ROOTS can't record a walk's mistake as the answer. Another
checks OVER, BLIND and the two decoys. A Hypothesis property composes 2
to 6 roots and checks that the walk loses no target and that Python reads
the union of the parts' reads. Belgium's seven import-time forms became
roots, and BLIND holds their registry decorator.

Each of 71 one-edit knockouts of the walk fails at least one root. The
income kernels' lists still equal the walk's reach outside
microcosm.graph, with the one cut as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es (#1567)

eb236a6's 71 knockouts left some branches of the walk's helpers with no
knockout. Each branch now has one, and a root fails when it goes:

- Bindings: a for or with target, a match capture (plain, star and
  mapping rest) and a walrus target, each imported from the module that
  binds it. That import alone leads the walk to the module.
- Aliases: or, a walrus, a starred list, and __import__'s top package, each
  as a class base.
- Import-time code: match patterns and guards, except* types, and effects
  inside if, for-else, finally, except and match-case blocks.

The walk's docstring now names these forms; its code doesn't change.
static_reach_cases.py grows to 91 roots, and each of 91 one-edit
knockouts fails at least one of them, on Python 3.13 and 3.14.

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

The delta review of 005844b asked for one change and offered four
more. This commit takes all five.

- Class-body globals (required). An assignment in a class body to a name
  that the body declares global rebinds the module's name. The walk now
  follows its value whether or not it reaches the class (r_class_global,
  r_class_nested_fill).
- Misses that both walks shared. The walk now follows:
  - a class-level import that the class body, a default value or a
    decorator reads (r_class_import_scope);
  - a nested class body's changes in place (r_class_nested_fill);
  - a for or with target that stores into a reached name's item
    (r_for_store);
  - a change inside a comprehension (r_comprehension_fill);
  - an inherited __match_args__ (r_match_inherited);
  - __import__ with a level (r_import_level);
  - import_module relative to __spec__.parent (r_import_spec);
  - these callables under another module-level name (r_import_renamed);
  - += on a name (r_augassign).
- Branches that no root pinned. 005844b's message said "Each branch now
  has one, and a root fails when it goes". That was wrong. The review
  found seven branches whose knockout failed no root, and this commit's
  own sweep found an eighth. Each live one now has a root:
  - del in a class body (r_body_del);
  - __import__'s top package (r_dunder_import_top);
  - a class member's change (r_class_member_change);
  - the global filter for a nested function (r_global);
  - changes in every module the walk has read: r_registry's registrar
    now holds no reached item, so only that pass reaches its change;
  - comprehension targets as locals (r_comprehension_decoy);
  - import_module relative to __name__ (r_import_name), from a plain
    module, whose __name__ differs from its __package__.
  The redundant pop of global names in _own_names goes, since _visits
  already drops them.
- Cost. Each lookup now runs once per walk (_Walk._settled, which follows
  Tarjan's strongly connected components). That covers what a name binds
  in a module, what a constant stands for, a package's lazy exports, and
  a class's inherited __match_args__. Lookups in a cycle settle on the
  same items, whichever the walk met first.
  - Nine Cost roots pin it:
    - diamonds 22 levels deep of star imports, re-exports, constants,
      bases and import alternatives;
    - a clique of 8 modules that star-import each other;
    - a cycle of 3 lazy packages;
    - two cycles of constants.
  - On the same shapes (CPython 3.14.7), the old lookups ran past a 20 s
    limit on the 8-module clique, and took 11.8 s, 11.7 s and 19.2 s on
    the 22-level diamonds of constants, re-exports and bases. With this
    commit each Cost root takes at most 4.1 ms on 3.14.7 and 4.5 ms on
    3.13.15 (the fastest of 5 runs).
  - Two new tests check that each lookup runs once and returns each item
    once, and that each lookup the walk settles holds what a fresh walk's
    same lookup finds.

Checks outside pytest, on CPython 3.14.7 and 3.13.15:
- The walk as committed passes every check on all 115 roots: the targets,
  the run-once check and the fresh-walk check.
- Each of 125 one-edit knockouts of the walk fails at least one root by
  one of those checks. Both versions agree on every knockout.
- A differential against the same walk with the old lookups found no
  difference in items, sites or cuts (3.14.7). It covered NZ's 8 kernels,
  with and without the cut (16 walks), the 106 roots before the Cost
  roots, and 200 seeded composites of them.
- On NZ's 8 kernels, with and without the cut, the module sets equal
  d403cce's (3.14.7).

Local run of the three test files that exercise the walk and the income
kernels (test_transport_income_evidence_kernels.py,
test_nz_transport_toy_graph.py, test_nz_spec_package.py) on this tree,
CPython 3.14.7: 1,564 passed in 130.4 s.

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

The delta review of c622fc2 asked for one change and offered six
more. Belgium's look at c622fc2 asked for two changes and four cases,
and its OK at 005844b asked for one open form and twelve roots. This
commit takes all of them.

- Lookup cycles (required). c622fc2 gave every lookup in a cycle the
  items its first lookup ended with. A constant that maps another's
  items through an attribute (X = Y.sub, in a cycle with Y) then
  settled on less than a fresh lookup finds, and what it lost depended
  on which lookup the walk met first. 005844b reached what it lost.
  Now, when a cycle's first lookup ends, the cycle's lookups run again,
  each meeting what the others hold so far, until a pass adds nothing
  (_Walk._cycle). So each settles on what it finds once the others are
  complete, whichever the walk met first. New roots:
  - r_alias_cycle_dotted and r_alias_cycle_dotted_first, the review's
    shape met from each end;
  - r_alias_self_dotted, a lookup that waits on itself;
  - r_alias_cycle_join, a lookup that a pass begins and that joins the
    cycle.
- A module's __getattr__ in a cycle (Belgium). exported() added a
  module's __getattr__ only when the name's lookup had found nothing.
  Inside a cycle that lookup hasn't settled, and Python can read the
  module part-built, before it binds the name. So what the walk reached
  depended on which lookup it met first. exported() now leaves the
  getter out only when the name's lookup has settled with items
  (r_getattr_cycle, r_getattr_cycle_first). OVER holds the getter that
  Python's import order doesn't pick, since the walk doesn't track that
  order. r_getattr_self pins the older rule that Python finds a
  module's __getattr__ in its namespace only.
- Belgium's roots.
  - Form 1, the last of the three forms Belgium found at 005844b. A
    class body's X += 1 reads X before it binds it, so it reads the
    module's X until the body has bound it (r_aug_class).
  - Belgium's four probes become roots: r_aug_class, r_nested_change,
    r_global_setter and r_global_read. So do its eight roots for rules
    that no NZ root pinned: r_del_class, r_self_ref, r_with_class,
    r_ann_class, r_aug_store, r_kw_fill, r_comp_first_iter and
    r_walrus_comp. OVER takes the target that the walk reaches and
    Python doesn't read in r_del_class, r_global_read and
    r_global_setter.
  - Belgium's two controls at c622fc2 join too (r_class_iadd,
    r_class_comp_fill), and r_base_set passes an alias through a set
    display, which no root did.
- Class-body globals at import. In a class the walk reaches, an
  assignment to a name the body declares global binds outside the
  class, but it runs as Python creates the class. The walk now follows
  it there (r_class_global_aug: X += 1 calls GlobalBox's +).
- Knockouts that failed no root (review finding 5). Each now fails one:
  - _CALLABLES narrowed to the importers (r_getattr_renamed: getattr
    under a module-level name);
  - the class table for a class that a class body declares global
    (r_class_global_inherit);
  - __getattr__ as a lazy export of itself (r_getattr_self);
  - _members' filter of global names (r_global_read);
  - body= for a class-body global's value (r_class_global, whose module
    now binds _source to a decoy);
  - _met.add (r_alias_self_dotted), and the cut that a lookup under
    way gives back (seven roots, r_lazy_cycle and r_star_clique
    among them).
  The _own_names revert stays equivalent, as c622fc2's message said.
- Recursion depth (finding 2). The docstring now says how deep a chain
  of re-exports, star imports, constants or inherited __match_args__
  can go before the walk raises RecursionError (about 200, 250 and
  330), and that the kernels' lookups nest at most four deep.
- Misses the docstring didn't list (findings 3, 4 and 7). "What it
  can't see" now lists each, and a BLIND root pins each:
  - a nested class's global value, and the bases, keywords and
    decorators of a nested class that the walk runs whole
    (r_nested_global_value, r_nested_base_scope);
  - a match class pattern through a class-body global bound to the
    body's own import (r_class_global_match), and a name that a
    function binds through global (r_function_global,
    r_function_global_import);
  - a change inside a lambda that a statement calls (r_lambda_change);
  - import_module with another package (r_spec_name), and importers
    that partial wraps or a call returns (r_partial_import,
    r_getattr_importer);
  - an explicit __match_args__ that names a constant
    (r_match_args_constant);
  - an operator's side effect inside a statement that binds a name
    (r_aug_module_effect).
  The literal-constant sentence now says that each binding of the
  constant must set it to a string, and the annotations sentence names
  class bodies and modules as well as def statements.
- Tests (finding 6).
  - test_the_walk_runs_each_lookup_once becomes
    test_the_walk_runs_each_lookup_once_outside_a_cycle. A lookup in a
    cycle runs once more for each pass that runs the cycle again (the
    walk's own count), and every lookup a walk begins settles.
  - test_the_walk_makes_few_lookups_on_the_cost_roots counts every call
    of the four lookup methods, the memo's answers too, and fails at the
    first call past 10,000. So a lookup that skips the memo, or keys it
    more finely than what it looks up, fails there instead of hanging.
    With the memo, the most any of the six roots makes is 1,257; with
    each lookup run afresh, each passes 300,000.
  - The fresh-walk test now runs on the new cycle shapes too.

c622fc2's message said "r_registry's registrar now holds no reached
item". The walk does reach registrar._register in the end, through the
change pass itself: registrar holds no reached item until that pass
finds its change, so only that pass reaches it.

Checks outside pytest, on CPython 3.14.7 and 3.13.15, which agree:
- The walk passes every check on all 151 roots: the targets, the
  runtime reads, the run-count check, the fresh-walk check and the
  lookup budget.
- Each of the hub's 139 one-edit knockouts of the walk fails at least
  one root, and so does each of Belgium's 51. Of the c622fc2 review's
  51, 50 fail a root, and the 51st, the _own_names revert, is
  equivalent. Both Pythons fail the same roots.
- Against c622fc2's walk, the walk loses no item on NZ's 16 kernel
  walks, the 151 roots or 200 seeded composites of them. Twelve roots
  differ, each by a change above: ten gain items and two gain sites
  only. On three cycle roots, a site that settle() recorded for a module
  used as a value goes, since the walk now reaches the item first
  through the root's own lines. No kernel walk changes.
- On NZ's 8 kernels, with and without the hierarchy cut, the module
  sets equal d403cce's, and no kernel walk has a lookup cycle.

CI's engine-free jobs run the three test files; this commit had no
local pytest run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The delta review of e6e1f4c asked for two changes, offered three more
and listed four nits. Belgium's review of e6e1f4c asked for the same
two changes and two roots. This commit takes all of them.

- A cycle that meets an earlier lookup (required; Belgium's first
  change). e6e1f4c settled a cycle when its first lookup ended, even
  when a pass had met a lookup that began before the cycle and hadn't
  settled. The cycle's lookups then froze with that lookup's partial
  items, so what the walk reached still depended on which lookup it met
  first. Now such a cycle stays open, as a single lookup that meets an
  earlier one already did, and joins the earlier lookup's cycle, which
  runs it again with the rest (_settled, as both reviews proposed). New
  roots:
  - r_alias_cycle_outer, the review's shape: a pass meets a lookup
    under way;
  - r_alias_cycle_two_up, with that lookup two lookups up;
  - r_alias_cycle_sibling, with a lookup that waits rather than runs;
  - Belgium's r_outer_cycle_first, where only a later pass meets the
    earlier lookup, and r_outer_cycle, which meets the same lookups in
    the other order. Their modules take the prefix ocyc, since cyco
    already names r_alias_cycle_outer's package.
  On e6e1f4c's walk the first four miss their target and fail the
  fresh-walk check, and r_outer_cycle fails the fresh-walk check.
- The composing property (required; Belgium's second change).
  r_getattr_cycle and r_getattr_cycle_first shared one pair of modules,
  whose reads depend on which root imports them first, and Python
  imports a module once. So a composite that held both read only the
  first part's target, and the property failed in CI on both Pythons.
  r_getattr_cycle_first now imports its own copy of the pair (gcyf_a
  and gcyf_b), so no two roots share a module whose reads depend on
  import order. Belgium proposed skipping the Python check on such
  draws; the copy keeps that check on every draw.
- Lines that no root pinned (finding 3). Each of the review's four
  knockouts now fails a root:
  - the += branch's use of the name's value (r_aug_value: X += value
    hands X's module to Adder.__radd__, which reads an attribute that
    the module binds);
  - plain assignments among a class body's global stores
    (r_class_global_store);
  - exported()'s settled check (r_getattr_bound, whose module binds the
    name that its __getattr__ would answer, and r_getattr_settled, where
    the name's lookup settles before the getter counts). Each holds a
    decoy target that the walk reaches without the check;
  - the pass that a join forces (r_join_no_growth).
  The review's three survivors from c622fc2 fail a root too: getattr
  under another name in a constant's value (r_getattr_named), a reached
  class's decorators (r_class_decorated) and keyword-only defaults
  (r_kw_default).
- Module __getattr__ calls outside a cycle of lookups (finding 4).
  "What it can't see" now lists the three other times Python calls a
  module's __getattr__: for a name the module binds, when an import
  cycle reads the module part-built; for a sub module's name, which a
  from-import asks the package for before it imports the sub module;
  and for __path__, which a from-import asks a module that isn't a
  package for. BLIND roots pin each: r_getattr_import_cycle,
  r_getattr_import_cycle_from and r_getattr_init_cycle (the review's
  e1 to e3), r_getattr_submodule (e4) and r_getattr_path (e5). No
  kernel meets them. Of the 565 first-party modules, two define a
  module-level __getattr__, and the kernels' walks parse one, the
  package microcosm.graph. Its getter answers only its 48 lazy exports,
  and the package binds none of them and holds no sub module of the
  same name. A package has __path__, so Python never asks its getter
  for it.
- The pass log and two tests (finding 5).
  - _Walk.cycles records each cycle's passes: the lookups each pass ran
    and how many items and lookups it added. reruns, which tools read,
    now counts from that log.
  - test_the_walk_runs_each_lookup_once_outside_a_cycle counts each
    lookup's runs itself rather than reading the walk's count. Each
    lookup must run once, and once more for each pass that runs it, and
    in each cycle every pass but the last must add something and the
    last nothing. Six knockouts of the pass log each fail it: one more
    pass after the fixpoint, each member running twice in a pass, a pass
    that logs that it added nothing, no log, and reruns counting the
    first pass only (18 cycle roots each), and a log that names the
    members after the pass rather than the lookups it ran (4 roots).
  - test_each_lookup_in_a_random_tree_settles_on_what_a_fresh_walk_finds
    walks 300 seeded random trees (static_reach_cases.random_tree): four
    modules, a package pk with two sub modules and a getter, and a root
    whose run() and run2() make the same reads in opposite orders. Each
    walk passes the once test's checks, each lookup it settles holds
    what a fresh walk's same lookup finds, and the two walks reach the
    same items. More than half of the 600 walks must run a cycle again.
    e6e1f4c's walk fails 7 of the 300 trees, and 31 of the first
    1,000.
- Long cycles (the review's note). The budget test's comment now says
  that its 10,000 lookups hold for the six Cost roots, not for every
  cycle: a cycle that gathers one module a pass, over N modules, takes
  N + 3 passes, and 18,023 lookups at N = 100.
- Corrections and nits.
  - e6e1f4c's message said that the walk passed every check on all
    151 roots. Each root passed alone, but a composite of
    r_getattr_cycle and r_getattr_cycle_first failed the composing
    property, which only CI ran.
  - It also said that a site goes on each of three cycle roots. Four
    (value) sites go: one on r_alias_cycle_dotted, two on
    r_alias_cycle_dotted_first and one on r_alias_self_dotted.
  - The __match_args__ entry in "What it can't see" stays. The walk
    could resolve a constant bound once to a tuple of literals, as it
    resolves a string constant, but no first-party module assigns
    __match_args__, so the rule would serve no kernel.

Checks outside pytest, on CPython 3.14.7 and 3.13.15, which agree:
- Each of the 169 roots passes every check: the targets, the runtime
  reads, the once check, the fresh-walk check and the lookup budget.
- The composing property's two checks pass on every pair of the 169
  roots (14,196), on 2,000 seeded draws of 3 to 6 roots and on the
  composite of all 169.
- On seeds 0 to 2,999 of random_tree, each walk passes the once
  checks, each of the 301,826 lookups it settles holds what a fresh
  walk's same lookup finds, and both orders reach the same items. On
  the review's 3,000 random 7-module trees, e6e1f4c's walk fails the
  fresh-walk check on 73, and this walk on none.
- One-edit knockouts of the walk, run outside the repository: each of
  the hub's 148 fails a root but two. So do 50 of the c622fc2
  review's 51, 57 of the e6e1f4c review's 61, each of that review's
  ports of Belgium's 51, and each of Belgium's 51 under Belgium's own
  runner. The seven that fail none change nothing a test can see, and
  the PR body says why for each.
- Against e6e1f4c's walk, no item goes on NZ's 16 kernel walks, the
  169 roots or 200 seeded composites of them. 24 differ (four roots and
  20 composites), each holding one of the four roots that miss their
  target on e6e1f4c's walk: 20 gain items, and the other four trade a
  (value) site for the root's own lines. No kernel walk changes, and
  none has a lookup cycle.

Local run: pytest -n 0 passes the three test files with this commit's
text on CPython 3.14.7 (1,679 passed in 611 s on a loaded host).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 2 commits November 10, 2026 20:49
The Belgium session's review of 6d0a1a0 found a gap in exported(). It
skipped a module's __getattr__ whenever the name's resolve lookup had
settled with any items, and its comment called that "bound for sure", so
any binding counted:
- one in a branch, in a try, or under if TYPE_CHECKING;
- one that a del or the end of an except clause undoes;
- one a star import brings in from a module whose __all__ leaves it out;
- a walrus, or a comprehension's name, inside a subscript in an
  assignment's target.
A package's sub module that nothing has imported, read as pkg.sub,
counted too. Python calls the getter in each of those cases, so the walk
missed what the getter reads. The class-body rule in _bound_before shared
two of the gaps: the end of an except clause, and a walrus or a
comprehension's name inside a subscript target.

This commit applies that session's suggested-getter-v5.diff verbatim:
git diff 6d0a1a0 against this commit matches the file byte for byte
(sha256 fb084e9e0488f01b...).
- _undoes() returns the name a del target or an except clause unbinds,
  and _stored_here() returns only the names an assignment target spells
  out. _binds() uses _stored_here(), and _bound_before() uses _undoes().
- _Module gains deleted, sure, sure_stars and names_all, and the new
  _Walk._binds_for_sure() reads them. exported() skips the getter only
  when the name's lookup has settled with items and the module's top
  level binds the name for sure: directly, or through a star import of a
  public name from a module that binds it for sure and nowhere names
  __all__.
- The docstring's Bindings, Scopes and "What it can't see" entries and
  the OVER comment follow. "What it can't see" drops the sub module's
  name, which the walk now sees. It adds a function the module calls
  while it runs, a name another module deletes or the module removes
  without del, and an __all__ the walk can't see.
- The cases gain ten getter roots for a name a module binds only maybe,
  and r_getattr_sub_attribute; each reaches its own target.
  r_getattr_submodule moves from BLIND to ROOTS. r_except_class and
  r_walrus_class pin the class-body rule, and r_getattr_call_cycle,
  r_getattr_outside_del and r_getattr_hidden_all pin the new blind
  spots. The cases hold 185 roots, 222 targets, 14 in OVER, 20 in BLIND
  and seven decoys.

The next commit adds o8's changes on top, and its message gives the
checks of both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The delta review of 6d0a1a0 approved it with no required change and
offered four optional findings and two nits. This commit takes the four
findings and adds four roots that pin the getter change in 2278fb3.
The nits correct the PR body.

- The once test checks what each lookup meets (finding 1). The test took
  the walk's own pass log as its allowance for extra runs, so a walk
  that did extra work and logged it still passed. _counted now also
  records, from the calls themselves, each lookup that a call met before
  the lookup settled: one that had begun and that the memo didn't hold
  yet. _check_runs adds three checks: each pass runs distinct lookups;
  each logged cycle's first pass runs more than one lookup, or its one
  lookup met itself; and no two logged cycles run the same lookups,
  since a cycle runs its passes once and then settles or joins a larger
  one. The review's five edits that passed 6d0a1a0's test now fail it:
  - _cycle called twice, and each member run twice in a pass (each on
    the 18 cycle roots);
  - True in place of the cycle condition, and one more logged pass for a
    lookup that settled outside a cycle (each on all 191 roots);
  - an open cycle that runs its passes again before it joins (4 roots).
  They join the hub's knockouts.
- Two more getter calls (finding 2). The review named three more times
  Python calls a module's __getattr__ outside a cycle of lookups.
  2278fb3 makes the walk see the first: reading a package's attribute
  asks it for a sub module's name until something imports the sub module
  (r_getattr_sub_attribute). "What it can't see" now lists the other
  two: a star import asks a module that doesn't define __all__ for it,
  and a star import asks for each name that __all__ lists and the module
  doesn't bind. BLIND roots pin each: r_getattr_star_noall and
  r_getattr_star_lazy. Python reads each target, and the walk reaches
  neither. No kernel meets them: no first-party module star-imports.
- Nested cycles (finding 3). The budget test's comment names a second
  shape past its 10,000 lookups: nested cycles, each of which a pass of
  the next meets. Each then stays open and runs again in every pass of
  each cycle around it, and K of them make 9,916 lookups at K = 10 and
  43,026 at K = 20. The comment's staircase figure moves to 18,125
  lookups at N = 100, since 2278fb3's walk also looks up the getter of
  a module that binds a name only maybe. No kernel walk has a lookup
  cycle.
- Random trees (finding 4). random_tree gains an eleventh form, a class
  Base whose base is another name's Base or the Base of another name's
  attribute, with a __match_args__ that names one of a, b and c or with
  none, so that it takes its base's. Before, no tree settled a match
  lookup. Now, on the test's 300 seeds, 33 trees settle one and 14 run
  one in a cycle. The random test now requires a walk that runs a match
  lookup in a cycle (28 of the 600 do), and its floor counts walks that
  run a cycle through two passes or more (384 do; it needs more than
  300). e6e1f4c's walk fails 7 of the 300 trees, and 22 of the first
  1,000.
- Four getter roots for 2278fb3: a star import's private name
  (r_getattr_maybe_star_private), and a walrus inside a subscript of a
  with target, an annotated target and an augmented target
  (r_getattr_maybe_with, r_getattr_maybe_annotated and
  r_getattr_maybe_augmented). Python reads each target through the
  module's getter, and the walk reaches it; 6d0a1a0's walk doesn't.
  Without them, two of the hub's one-edit knockouts of 2278fb3 failed
  no root. The cases hold 191 roots, 228 targets, 14 in OVER, 22 in
  BLIND and seven decoys.

Checks outside pytest, on CPython 3.14.7 and 3.13.15, which agree:
- The walk's code is 2278fb3's: only its docstring changes.
- Each of the 191 roots passes every check: the targets, the runtime
  reads, the once check, the fresh-walk check and the lookup budget.
- The composing property's two checks pass on every pair of the 191
  roots (18,145), on 2,000 seeded draws of 3 to 6 roots and on the
  composite of all 191.
- On seeds 0 to 2,999 of random_tree, each walk passes the once checks,
  each of the 287,246 lookups it settles holds what a fresh walk's same
  lookup finds, and both orders reach the same items.
- The test's own _counted and _check_runs, run outside pytest, pass on
  every root and on seeds 0 to 2,999, and fail each of the five edits.
  Belgium's four mutants that 6d0a1a0's tests didn't catch (M2, M3, M8
  and M9) pass them too; Belgium's review reports that none changes an
  item or a settled lookup.
- One-edit knockouts of the walk, run outside the repository: each of
  the hub's 164 fails a root or a tree but two that only cost a pass. So
  do 50 of the c622fc2 review's 51, 57 of the e6e1f4c review's 61,
  each of that review's ports of Belgium's 51, and each of Belgium's 51
  under Belgium's own runner, with two of its anchors re-pointed to
  2278fb3's text. The PR body says why each of the rest changes
  nothing a test can see.
- Against 6d0a1a0's walk, no item goes on NZ's 16 kernel walks, the
  191 roots or 200 seeded composites of them; the 90 that differ each
  hold one of the 18 roots it misses and gain items. Against e6e1f4c's
  walk, no item goes either. 104 walks differ there: the 18, the four
  roots that miss their target on e6e1f4c's walk, and 82 composites
  that hold one of the 22. 101 gain items, and the other 3 trade a
  (value) site for the root's own lines.

Local run: pytest -n 0 passes the three test files with this commit's
text on CPython 3.14.7 (1,723 passed in 138 s).

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

This branch has not been deployed

No deployments
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.

NZ graph: three follow-ups from #1555's review (register-fit tie, measurement id selection, constant-read check)

1 participant