Purpose
-------
A static call graph for a Python project, resolved from the AST through
the project's real import bindings. Chart C of the overview-charts plan
(docs/DOCS_OVERVIEW_CHARTS_PLAN_2026-09-27.md in the workspace) uses it to
draw the full graph of an entry point such as ``oct git commit``.
It exists because Doxygen cannot do this for Python. Measured on oct on
2026-09-27, Doxygen's call analysis resolves a call only when caller and
callee share a file: ``oct git commit`` calls 48 distinct names, Doxygen
resolved 14, and every call into the quality gate - imported inside the
function - was invisible to it. This module follows the imports.
Responsibilities
----------------
- :class:`ProjectIndex` - every module's functions, classes, methods and
import bindings (module level and function local, aliases, relative
imports), with re-export chains followed on lookup.
- :func:`build_call_graph` - resolve each function's call sites into
internal edges, external calls and unresolved calls, each counted.
- :func:`to_callgraph` - adapt the result to
:class:`oct.docs.callgraph.CallGraph` so the Doxygen chart's closure and
DOT renderer are reused unchanged.
Diagnostics
-----------
Domain: OCT-DEPS
Levels:
L2 - build entry / exit with totals
L3 - per-module indexing decisions
L4 - individual unresolved call sites
Contracts
---------
- Purely static: no project module is imported or executed.
- Nothing is guessed. A call becomes an internal edge only when its target
resolves, through a binding or a definition, to a function or method
that exists in the project. A call on a receiver whose type cannot be
known statically (``result.stdout.split()``) is counted as unresolved
with that reason, never matched by name to some function that happens
to share it - which is the mistake a textual resolver makes.
- Calls inside nested functions and lambdas are attributed to the
enclosing top-level function or method, because that is where they run.
- A project function passed as an argument to a call is an edge too
(a callback: a thread target, a sort key, a handler), since it runs
because of that call. Counted separately as ``callbacks``.
- A project function placed in a tuple, list, set or dict literal is an
edge too (a dispatch-table entry, later called through a loop
variable no static resolver can follow). Counted separately as
``references``. Found by the runtime trace of ``oct git commit``
(2026-09-27): ``format_file``'s fixer table was the whole of the
difference between the trace and this graph.
- Decorators and default-argument expressions are not callees: they run
when the function is defined, not when it is called.
- Cross-checked against Doxygen on oct (2026-09-27): of the 681 Doxygen
call edges that map to a function here by file and line, every one is
in this graph except six that Doxygen got wrong - four ``self.m()``
calls matched by name to a same-named method of an unrelated class,
and two module-level calls credited to the function above them.
- Tests and the top-level ``docs`` directory are skipped, as in the
import graph.
Dependencies
------------
- oct.core.diagnostics (_dbg structured logger)
- oct.deps.import_graph (file walk, relative-import resolution, test skip)
- oct.deps.oct_deps (module_name_for - the shared module key scheme)
- oct.docs.callgraph (CallGraph / Function - the shared rendering model)