Skip to content

Python: record module-level calls (app = FastAPI(), @app.post(...)) in a calls-only __global_context__ unit - #3704

Merged
squid-protocol merged 2 commits into
mainfrom
claude/python-module-level-calls
Sep 26, 2026
Merged

squid-protocol merged 2 commits into
mainfrom
claude/python-module-level-calls

Conversation

@squid-protocol

@squid-protocol squid-protocol commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Summary

Part of the Level 2 call-graph work (epic #3265).

Problem. A classification of every pyan3 edge the resolver doesn't link on language-crucible's Python repos found decorators to be the largest real gap: 641 edges, 18% of pyan's total.

  • 431 of those are decorator calls at module level that were recorded nowhere. Python (Mode C) sliced only defs, so the code a module runs at import time never entered the call graph. That includes app = FastAPI(), @app.post("/items"), setup_logging() and if __name__ == "__main__": main().
  • For a web app, that's the route registration and wiring: how a request reaches a handler.

Fix.

Type of change

  • Bug fix
  • Parsing or engine logic (detector.py, python profiles, signal_processor.py, archetype_classifier.py, statistical_auditor.py, record_keeper.py, audit_recorder.py, state_rehydrator.py)

Measured

Module-level call pairs now recorded, per language-crucible Python repo (confident = scoped/unique):

repo pairs confident steps
fastapi 417 416 typed 193, import 193, file 30
numpy 32 29 file 29, unseen 3
wtfpython 4 4
cython 3 2
twisted 2 2
airflow 0 0 the excerpted files have no top-level calls

call_graph_resolution.py python (pyan3) is unchanged by design: 99.4% precision, 51.3% recall, 96.9% resolution recall. pyan has no module node, and it attributes a decorator to the decorated function, so these links are invisible to that metric. call_graph_accuracy.py --ci and tree_sitter_accuracy_audit.py --ci --all (30 languages) both pass: the synthetic unit is never counted as a function.

Golden masters

Both fixtures re-blessed: 42 differences each. All were diffed with golden_diff against HEAD and reviewed, and all trace to the new cross-file call edges:

  • fastapi direct downstream / Popularity Rank:
    • applications.py 44 → 191: test modules build FastAPI() at module level, and FastAPI lives there;
    • routing.py 13 → 30;
    • param_functions.py 92 → 103.
  • What those move:
    • Composition Fit z-scores for 12 files, including a few non-Python files through the corpus-wide distribution;
    • within-repo file clusters (Repository Archetype and Ecosystem Fingerprint) in fastapi, numpy and wtfpython, via the PageRank / blast-radius features;
    • network_macro (modularity 0.849 → 0.824, avg path length 1.98 → 1.82);
    • three forensic top lists.
  • No Structural Magnitude, coordinate or function-count changes.

Tests

  • New tests/extraction/languages/test_python_module_level_unit.py:
    • module-level calls, qualifiers and receiver types;
    • unit bodies and class headers are not module calls;
    • a definitions-only file has no unit;
    • embedded_python gets it too;
    • the unit adds no impact.
  • tests/tools_recorders/test_fcall_data.py: a synthetic unit persists to synthetic_unit_data (not function_data), rehydrates with calls_only, and resolves identically after rehydration.

Verification

$ pytest tests/                                               -- 11456 passed; the 1 failure (dead-key audit on a SQL column read) is fixed in 59ad9f25
$ PATH=.venv/bin:$PATH python tests/tools/audit_check.py      -- all clear (after 59ad9f25)
$ python tests/tools/call_graph_accuracy.py --ci              -- OK
$ python tests/tools/call_graph_resolution.py python --ci     -- OK
$ python tests/tools/tree_sitter_accuracy_audit.py --ci --all -- 30 languages, all OK
$ python tests/tools/crucible_check.py --mode both --update --yes   -- 42 differences each, reviewed above

Next in this line: decorator → decorated-function edges (@provide_bucket_name wraps every call to the method), then reference edges for functions passed as values (Depends(get_db), callbacks).

🤖 Generated with Claude Code

https://claude.ai/code/session_01GCpfMCUMpco98QGbsuv2sr

…unit

Python sliced only defs, so the code a module runs at import -- app =
FastAPI(), @app.post("/items"), setup_logging(), the __main__ guard -- was
recorded nowhere, and a web app's route registration with it. The Mode C
slicer now emits Mode D's synthetic __global_context__ bucket for Python and
embedded_python (top-level module_level_unit flag): every sliced unit's span
and each `class Name` header's name are blanked, the rest is scanned for
calls (qualifiers and receiver types included, so app.post is typed
FastAPI.post).

It is calls-only: magnitude/impact 0 and flagged calls_only, which the file
mass, function-analysis report, function-impact forensics, file-archetype
function count, named-structure retention and folder function count skip, so
no file's size or classification moves because of it.

Synthetic units have no function_data row (#2691), so their calls now ride
on a synthetic_unit_data child table (FK cascade) that the state rehydrator
restores; a delta scan resolves them like a fresh scan. This also covers
Mode D's existing buckets, which were lost on delta scans.

Golden masters (both, 42 differences each): fastapi applications.py direct
downstream 44 -> 191, routing.py 13 -> 30, param_functions.py 92 -> 103
(module-level app/router construction and calls now link to the defining
file), and what those move: Composition Fit z-scores, within-repo file
clusters, network macros, three forensic top lists.

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

Copy link
Copy Markdown
Contributor

🐦‍⬛ Muninn Security Scan

✅ No security issues found.

🐦‍⬛ Powered by Muninn · Skald Lab

… audit)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCpfMCUMpco98QGbsuv2sr
@squid-protocol squid-protocol changed the title python: record module-level calls in a calls-only __global_context__ … Python: record module-level calls (app = FastAPI(), @app.post(...)) in a calls-only __global_context__ unit Sep 26, 2026
@squid-protocol
squid-protocol merged commit f7b7e32 into main Sep 26, 2026
36 checks passed
@squid-protocol
squid-protocol deleted the claude/python-module-level-calls branch September 26, 2026 03:04
squid-protocol added a commit that referenced this pull request Sep 26, 2026
…corator applied to it (#3708)

* python: record module-level calls in a calls-only __global_context__ unit

Python sliced only defs, so the code a module runs at import -- app =
FastAPI(), @app.post("/items"), setup_logging(), the __main__ guard -- was
recorded nowhere, and a web app's route registration with it. The Mode C
slicer now emits Mode D's synthetic __global_context__ bucket for Python and
embedded_python (top-level module_level_unit flag): every sliced unit's span
and each `class Name` header's name are blanked, the rest is scanned for
calls (qualifiers and receiver types included, so app.post is typed
FastAPI.post).

It is calls-only: magnitude/impact 0 and flagged calls_only, which the file
mass, function-analysis report, function-impact forensics, file-archetype
function count, named-structure retention and folder function count skip, so
no file's size or classification moves because of it.

Synthetic units have no function_data row (#2691), so their calls now ride
on a synthetic_unit_data child table (FK cascade) that the state rehydrator
restores; a delta scan resolves them like a fresh scan. This also covers
Mode D's existing buckets, which were lost on delta scans.

Golden masters (both, 42 differences each): fastapi applications.py direct
downstream 44 -> 191, routing.py 13 -> 30, param_functions.py 92 -> 103
(module-level app/router construction and calls now link to the defining
file), and what those move: Composition Fit z-scores, within-repo file
clusters, network macros, three forensic top lists.

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

* rehydrator: read the synthetic unit's name through an alias (dead-key audit)

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

* call graph: decorator edges (python) -- a decorated function -> each decorator

A decorator's wrapper runs around every call to the function it decorates,
and neither the call graph nor the module unit recorded that link. The
detector now captures, per def, the contiguous decorator block directly above
it (argument lines included, shielded text, bounded look-back) as
decorated_by / decorated_by_qualifiers (`@app.post(...)` -> post via app);
python and embedded_python opt in with a top-level decorator_edges flag.

The resolver links them through the usual ladder as fcall_data kind
'decorator' -- counted in decorators_by_step, never in the call-resolution
rates -- and they reach function fan-in/PageRank like COBOL transfers do.
A function now also falls back to its module's receiver types (`app =
FastAPI()` at top level) for a receiver it does not bind itself; a name the
function binds with no known class is recorded as "" and hides the module
type. Both columns persist in function_data and rehydrate.

call_graph_resolution.py python (pyan3): calls-only precision 99.4%
(1806 judged), recall 51.3 -> 51.4, resolution recall 96.9 -> 97.0; new,
not gated: decorator edges 100% precision (575 judged, 0 wrong), recall with
decorators 67.9%. Golden masters: no drift.

Stacked on #3704 (module-level unit).

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
squid-protocol added a commit that referenced this pull request Sep 26, 2026
…ed definitions win over the file's first (#3712)

* python: record module-level calls in a calls-only __global_context__ unit

Python sliced only defs, so the code a module runs at import -- app =
FastAPI(), @app.post("/items"), setup_logging(), the __main__ guard -- was
recorded nowhere, and a web app's route registration with it. The Mode C
slicer now emits Mode D's synthetic __global_context__ bucket for Python and
embedded_python (top-level module_level_unit flag): every sliced unit's span
and each `class Name` header's name are blanked, the rest is scanned for
calls (qualifiers and receiver types included, so app.post is typed
FastAPI.post).

It is calls-only: magnitude/impact 0 and flagged calls_only, which the file
mass, function-analysis report, function-impact forensics, file-archetype
function count, named-structure retention and folder function count skip, so
no file's size or classification moves because of it.

Synthetic units have no function_data row (#2691), so their calls now ride
on a synthetic_unit_data child table (FK cascade) that the state rehydrator
restores; a delta scan resolves them like a fresh scan. This also covers
Mode D's existing buckets, which were lost on delta scans.

Golden masters (both, 42 differences each): fastapi applications.py direct
downstream 44 -> 191, routing.py 13 -> 30, param_functions.py 92 -> 103
(module-level app/router construction and calls now link to the defining
file), and what those move: Composition Fit z-scores, within-repo file
clusters, network macros, three forensic top lists.

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

* rehydrator: read the synthetic unit's name through an alias (dead-key audit)

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

* call graph: decorator edges (python) -- a decorated function -> each decorator

A decorator's wrapper runs around every call to the function it decorates,
and neither the call graph nor the module unit recorded that link. The
detector now captures, per def, the contiguous decorator block directly above
it (argument lines included, shielded text, bounded look-back) as
decorated_by / decorated_by_qualifiers (`@app.post(...)` -> post via app);
python and embedded_python opt in with a top-level decorator_edges flag.

The resolver links them through the usual ladder as fcall_data kind
'decorator' -- counted in decorators_by_step, never in the call-resolution
rates -- and they reach function fan-in/PageRank like COBOL transfers do.
A function now also falls back to its module's receiver types (`app =
FastAPI()` at top level) for a receiver it does not bind itself; a name the
function binds with no known class is recorded as "" and hides the module
type. Both columns persist in function_data and rehydrate.

call_graph_resolution.py python (pyan3): calls-only precision 99.4%
(1806 judged), recall 51.3 -> 51.4, resolution recall 96.9 -> 97.0; new,
not gated: decorator edges 100% precision (575 judged, 0 wrong), recall with
decorators 67.9%. Golden masters: no drift.

Stacked on #3704 (module-level unit).

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

* call graph: reference edges (python), and nested definitions win over the file's first

A function name used as a value -- Depends(get_db), key=sort_key,
callback=self.on_done, return wrapper -- is a link the framework or caller
invokes later, and was recorded nowhere. The detector (python and
embedded_python, top-level reference_edges flag) records candidates in
argument, keyword, assignment, return/yield/lambda and collection positions
as references_to / references_qualifiers. Variables are excluded: the
function's parameters and locals (tuple and starred targets included), every
nested def's parameters, the bindings of every enclosing function (closure
variables), the module's own variables (`app = FastAPI()`), and in a
signature anything but a default or a call argument (so `Sequence[Depends]`
is not one). Names the function also invokes are already edges.

The resolver keeps a reference only when it reaches a function confidently
through a scoped step (class, qualified, typed, file, import) -- `unique`
is not enough for a bare name -- as fcall_data kind 'reference', counted in
references_by_step and never in the call rates. A bare call or reference to a
name the caller defines nested inside itself now resolves to that nested
definition, not the file's first of that name (every decorator's own
`wrapper`); the rehydrator restores loc for the caller's span. The module
unit carries no references yet (synthetic_unit_data does not persist them).

call_graph_resolution.py python (pyan3): call precision 99.4 -> 99.8 (1806
judged), recall 51.4 -> 51.6, resolution recall 97.0 -> 97.5; new, not
gated: reference precision 99.7% (323 judged), recall across calls,
decorators and references 77.3%. Golden masters: no drift.

Stacked on #3708 (decorator edges).

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

* reference edges: type the signature parser's buffer (mypy); C5 test scoped to calls

`return helper` now draws a reference edge o2 -> helper, which is the
point of this PR; the C5 test's claim is that the nested header is not a
call, so it checks call-kind rows and asserts the reference separately.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
squid-protocol added a commit that referenced this pull request Sep 26, 2026
… (with measurements) (#3713)

* python: record module-level calls in a calls-only __global_context__ unit

Python sliced only defs, so the code a module runs at import -- app =
FastAPI(), @app.post("/items"), setup_logging(), the __main__ guard -- was
recorded nowhere, and a web app's route registration with it. The Mode C
slicer now emits Mode D's synthetic __global_context__ bucket for Python and
embedded_python (top-level module_level_unit flag): every sliced unit's span
and each `class Name` header's name are blanked, the rest is scanned for
calls (qualifiers and receiver types included, so app.post is typed
FastAPI.post).

It is calls-only: magnitude/impact 0 and flagged calls_only, which the file
mass, function-analysis report, function-impact forensics, file-archetype
function count, named-structure retention and folder function count skip, so
no file's size or classification moves because of it.

Synthetic units have no function_data row (#2691), so their calls now ride
on a synthetic_unit_data child table (FK cascade) that the state rehydrator
restores; a delta scan resolves them like a fresh scan. This also covers
Mode D's existing buckets, which were lost on delta scans.

Golden masters (both, 42 differences each): fastapi applications.py direct
downstream 44 -> 191, routing.py 13 -> 30, param_functions.py 92 -> 103
(module-level app/router construction and calls now link to the defining
file), and what those move: Composition Fit z-scores, within-repo file
clusters, network macros, three forensic top lists.

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

* rehydrator: read the synthetic unit's name through an alias (dead-key audit)

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

* call graph: decorator edges (python) -- a decorated function -> each decorator

A decorator's wrapper runs around every call to the function it decorates,
and neither the call graph nor the module unit recorded that link. The
detector now captures, per def, the contiguous decorator block directly above
it (argument lines included, shielded text, bounded look-back) as
decorated_by / decorated_by_qualifiers (`@app.post(...)` -> post via app);
python and embedded_python opt in with a top-level decorator_edges flag.

The resolver links them through the usual ladder as fcall_data kind
'decorator' -- counted in decorators_by_step, never in the call-resolution
rates -- and they reach function fan-in/PageRank like COBOL transfers do.
A function now also falls back to its module's receiver types (`app =
FastAPI()` at top level) for a receiver it does not bind itself; a name the
function binds with no known class is recorded as "" and hides the module
type. Both columns persist in function_data and rehydrate.

call_graph_resolution.py python (pyan3): calls-only precision 99.4%
(1806 judged), recall 51.3 -> 51.4, resolution recall 96.9 -> 97.0; new,
not gated: decorator edges 100% precision (575 judged, 0 wrong), recall with
decorators 67.9%. Golden masters: no drift.

Stacked on #3704 (module-level unit).

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

* call graph: reference edges (python), and nested definitions win over the file's first

A function name used as a value -- Depends(get_db), key=sort_key,
callback=self.on_done, return wrapper -- is a link the framework or caller
invokes later, and was recorded nowhere. The detector (python and
embedded_python, top-level reference_edges flag) records candidates in
argument, keyword, assignment, return/yield/lambda and collection positions
as references_to / references_qualifiers. Variables are excluded: the
function's parameters and locals (tuple and starred targets included), every
nested def's parameters, the bindings of every enclosing function (closure
variables), the module's own variables (`app = FastAPI()`), and in a
signature anything but a default or a call argument (so `Sequence[Depends]`
is not one). Names the function also invokes are already edges.

The resolver keeps a reference only when it reaches a function confidently
through a scoped step (class, qualified, typed, file, import) -- `unique`
is not enough for a bare name -- as fcall_data kind 'reference', counted in
references_by_step and never in the call rates. A bare call or reference to a
name the caller defines nested inside itself now resolves to that nested
definition, not the file's first of that name (every decorator's own
`wrapper`); the rehydrator restores loc for the caller's span. The module
unit carries no references yet (synthetic_unit_data does not persist them).

call_graph_resolution.py python (pyan3): call precision 99.4 -> 99.8 (1806
judged), recall 51.4 -> 51.6, resolution recall 97.0 -> 97.5; new, not
gated: reference precision 99.7% (323 judged), recall across calls,
decorators and references 77.3%. Golden masters: no drift.

Stacked on #3708 (decorator edges).

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

* reference edges: type the signature parser's buffer (mypy); C5 test scoped to calls

`return helper` now draws a reference edge o2 -> helper, which is the
point of this PR; the C5 test's claim is that the nested header is not a
call, so it checks call-kind rows and asserts the reference separately.

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

* docs: edge kinds, and what the call graph adds to a parser, with its measurements

Adds two sections to docs/function_call_graph.md: the fcall_data edge kinds
(call, transfer, decorator, reference -- what each links, which languages
record it, what joins the file graph) and what the resolver knows on top of
a parser's callee names, each capability with its pyan3 measurement and the
limits those numbers carry (one reference tool, one language, tuned on the
scored corpus). Deliberately not a why_gitgalaxy_beats_ast_here.md claim:
resolution is something a parser does not attempt, not something it gets
wrong.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
@squid-protocol squid-protocol added this to the Compiler call-graph comparison milestone Sep 26, 2026 — with Claude
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants