PROBLEM
Two back-to-back runs of canpy -i test/fixtures/whole_applications/requests -a 4 --no-ray with fresh caches produce different call graphs: 1027 vs 1057 edges. Every divergent edge carries prov:["pycg"]. Example instability: the same call resolving to requests.compat.MutableMapping.get in one run and collections.abc.MutableMapping.get in the other.
Separately, --ray vs --no-ray disagree on Jedi type inference for external symbols (e.g. pytest.main(...) inferred int in-process vs ExitCode in the Ray worker), shifting ~12 call-graph edges and ~35 param edges on the same fixture — the worker's interpreter environment resolves stubs differently than the parent process.
Observed 2026-07-14 on feat/v2-stage5-docs-release @ 3d39bbf during the stage-5 release gates (determinism gate: same-flags output must be byte-identical).
LIKELY MECHANISM
PyCG's fixpoint is capped (--pycg-max-iter, default 50) and its iteration order over the access-path domain is not stabilized, so a capped run stops at a different frontier each time. The cap itself is a documented sound-but-incomplete trade-off (CHANGELOG 0.3.0); the run-to-run variance is not.
IMPACT
- The v2 determinism gate cannot pass at L2+ as specified.
- Downstream monotonicity/superset comparisons across runs are noisy.
- Not a regression from the schema-v2 work — PyCG landed in 0.3.0 and the behavior predates the v2 chain. Shipping 0.4.0 with this as a known-limitation release note is defensible; fixing it means stabilizing PyCG's iteration order (sorted worklists) or emitting only the converged subset.
REPRO
canpy -i test/fixtures/whole_applications/requests -a 2 --no-ray -c /tmp/c1 -o /tmp/o1
canpy -i test/fixtures/whole_applications/requests -a 2 --no-ray -c /tmp/c2 -o /tmp/o2
diff <(jq -S .application.call_graph /tmp/o1/analysis.json) <(jq -S .application.call_graph /tmp/o2/analysis.json)
PROBLEM
Two back-to-back runs of
canpy -i test/fixtures/whole_applications/requests -a 4 --no-raywith fresh caches produce different call graphs: 1027 vs 1057 edges. Every divergent edge carriesprov:["pycg"]. Example instability: the same call resolving torequests.compat.MutableMapping.getin one run andcollections.abc.MutableMapping.getin the other.Separately,
--rayvs--no-raydisagree on Jedi type inference for external symbols (e.g.pytest.main(...)inferredintin-process vsExitCodein the Ray worker), shifting ~12 call-graph edges and ~35 param edges on the same fixture — the worker's interpreter environment resolves stubs differently than the parent process.Observed 2026-07-14 on feat/v2-stage5-docs-release @ 3d39bbf during the stage-5 release gates (determinism gate: same-flags output must be byte-identical).
LIKELY MECHANISM
PyCG's fixpoint is capped (
--pycg-max-iter, default 50) and its iteration order over the access-path domain is not stabilized, so a capped run stops at a different frontier each time. The cap itself is a documented sound-but-incomplete trade-off (CHANGELOG 0.3.0); the run-to-run variance is not.IMPACT
REPRO