Skip to content

fix(impact): Python transitive reach — two over-reach sources, Optional class-object params - #1891

Merged
swapnilpaliwal-sd merged 3 commits into
apps/integration-0.1.9from
fix/context-sensitive-reach
Oct 10, 2026
Merged

swapnilpaliwal-sd merged 3 commits into
apps/integration-0.1.9from
fix/context-sensitive-reach

Conversation

@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor

Follows #1886 (merged into apps/integration-0.1.9).

Question

Does context-insensitivity explain Python transitive impact's false reach, and what lifts precision without losing recall?

Measurement (16-repo Python oracle corpus, runtime traces + mutation truth)

Every false row was classified by the first static hop the runtime never made (walking from the false node toward the change, through hops the runtime also made). An offline replay of the impact walk from dumped solves reproduces the engine's reach and test sets exactly, so pruning policies could be measured before writing rules.

Transitive reach (52.9k false rows; precision = runtime caller at any depth):

first wrong hop share
by-name seed (mostly __init__ through an unresolved super() into a library base) 30%
by key (decoration strings) 13%
decorator by name 13%
one of a set (multi_inferred) 11%
protocol (constructor proxy) 9%
defines (enclosing function of a returned closure) 9%
contract seeds 5.5%
resolved call never executed 4%
spawns (subprocess; invisible to the trace) 3%

Context-insensitivity produces no false reach under this metric: a chain of edges the runtime made somewhere is a runtime caller. It matters only for test selection, where it is a minority: of the test files that ran but never ran the change, the hop is a dispatch fan that ran elsewhere 11%, a resolved call that ran elsewhere (branch-dependent, not fixable by context) 12%, a library callback into a CLI runner 24%, by-name 11%.

Changes

  1. Decoration strings that name a parameter of the decorated function (or sit inside a nested call) are no registration keys.
  2. "decorator by name" lands on what the decorator returns (a wrapper -> the decorated name stands for it; the function itself -> no hop; unknown -> old hop).
  3. Engine: type[X] | None / Optional[Type[X]] parameters construct X (concrete and bounded TypeVar).

Effect (all 16 repos; base -> this branch)

verb base fix
reach precision 0.675 0.724
reach recall src / test-side 0.740 / 0.848 0.739 / 0.846
tests recall / precision 0.851 / 0.417 0.850 / 0.422
callers (src direct) / precision 0.828 / 0.921 0.829 / 0.921
path found / hop valid 0.727 / 0.986 0.729 / 0.986

Tune vs held-out reach precision: 0.657 -> 0.713 / 0.764 -> 0.771. Query latency flat (p50 ~0.45-0.5 s on the two largest repos), index time flat. Case suite 1601/1601, Python engine suite + torture unchanged, literal gate ok.

Measured, not shipped

  • super() sites no project class can answer as library receivers: precision -> 0.782 but test-side reach recall -1.5pt (coincidental hits: wrong chains that covered true callers the graph misses).
  • Receiver-type gating of self-dispatch fans (1-object sensitivity): +0.0.
  • Dropping defines-of-returned-closures, contract seeds, protocol proxy, weak-hop budgets: each trades recall.

F1 0.8 at today's recall needs reach precision ~0.87; test-selection precision is capped near 0.66 by false rows no static rule can see (files that never ran, ran the change without failing, branch-dependent calls).

swapnilpaliwal-sd and others added 2 commits October 9, 2026 23:42
Measured against runtime traces on the Python oracle corpus, every false
transitive-reach row has a static edge the runtime never made (the scorer
accepts any runtime caller at any depth, so context-insensitivity cannot
produce one). Two of the edge kinds are wrong by construction and are
fixed here; reach precision 0.675 -> 0.724, reach recall -0.1pt (src) /
-0.2pt (test-side), test-file recall -0.1pt, callers and path unchanged.

1. A decoration string naming a PARAMETER of the decorated declaration is
   no registration key (ax_registration.decoration_key_strings, shape 4),
   nor is a string inside another call in the decoration (shape 5).
   `@option("--params", "-p", "params") def main(params)`: the flags are
   what a caller writes to reach the command; the parameter name is written
   by every function that builds a dict with that key, and the by-key join
   made each of them a caller of the command. The flags stay keys.

2. The "decorator by name" hop lands on what the decorator RETURNS, not on
   the decorator's body. It used to run decorated -> decorator, making the
   decorated function (and everything reaching it: a route's tests) a
   caller of whatever the decorator calls. Calling the decorated name never
   runs the decorator; it runs what the decorator returned:
     - bare `@d`: d's return; written as a call `@d(...)`: the return of
       what d returned (decorated_call);
     - a wrapper (fn_returns, from the engine's method_returns_method): the
       decorated declaration now stands for that wrapper;
     - the function handed back unchanged (fn_returns_param, a registering
       decorator): no hop at all; the decoration itself runs at import,
       which the import walk already follows;
     - unknown return: the old hop stays (a route the walk cannot judge).

IMPACT_VERSION 70 (new facts fn_returns, fn_returns_param, decorated_call).
Cases: decorator-by-name-runs-its-wrapper, option-parameter-name-is-no-key
(each fails on the base, with a control that must not change);
decorator-the-project-declares now expects the route it really takes
(`at import`) instead of the by-name hop.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
… its class

`cls: type[X] | None = None` and `Optional[Type[X]]` are the default-None
spelling of a class-object parameter: the body swaps None for a default class
and calls `cls(...)`. Only a top-level `type[...]` annotation was read, so the
call stayed `callee_is_parameter` and every constructor behind it lost that
caller (a decorator factory building its command class in a closure is the
common shape). param_class_object_ref now names the `type[...]` subscript an
annotation is or one of its union operands is, and both the concrete
(expr_type_class_object) and the bounded-TypeVar (param_class_object_bound)
rules read it. The existing top-level rules are unchanged.

Corpus: one-hop callers src 0.828 -> 0.829, path found 0.727 -> 0.729,
test->target path 0.559 -> 0.562; reach and tests unchanged; engine suite and
torture unchanged (agree 588 / missing 46 / extra 37); index time flat.
Case: optional-class-object-parameter (fails on the base; the bare
`type[Command]` control passes on both).

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
@swapnilpaliwal-sd
swapnilpaliwal-sd marked this pull request as ready for review October 10, 2026 06:48
@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor Author

Tracked in #1897 (0.1.9 batch); merge in the order listed there.

@swapnilpaliwal-sd
swapnilpaliwal-sd merged commit a523fd3 into apps/integration-0.1.9 Oct 10, 2026
12 checks passed
@swapnilpaliwal-sd
swapnilpaliwal-sd deleted the fix/context-sensitive-reach branch October 10, 2026 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant