Trace produces facts. Classify makes decisions.
These are distinct responsibilities with different stability guarantees.
Source IR (single_file.py)
→ TraceResult (cross_file.py: trace_symbol)
→ Classification (classification.py: ClassificationPipeline)
→ ApiCall / SymbolProvenance (cross_file.py)
- Given a symbol or call expression, produce an ordered chain of source references from the definition site to the ultimate origin.
- Report when the trace is incomplete (e.g., recursion limit, unresolvable structured source).
- Provide all candidate origins when the trace splits (via
SourceSet). - Never assign a
top_librarydirectly: that is the classifier's job.
- Given a
TraceResult, determine thetop_libraryusingClassificationPipeline.classify(). - Assign
reason,confidence, andalternatives. - Handle ambiguous cases (multiple candidates, local + import-backed mixed) explicitly.
- Unresolved cases produce
top_library = "unknown"withreason = "UNRESOLVED".
classify_source() in cross_file.py delegates to ClassificationPipeline.classify()
in classification.py, which applies priority-ordered rules:
| Location | Role |
|---|---|
extract_final_source() (cross_file.py) |
Walks chain reverse, returns ultimate source string |
_base_top_source() (cross_file.py) |
Resolves structured sources, delegates to _top_source() |
get_calls() (cross_file.py) |
Collects and classifies every API call through the pipeline |
ClassificationPipeline.classify() (classification.py) |
Priority-ordered reason, confidence, alternatives assignment |
| Field | Source | Semantics |
|---|---|---|
source |
SymbolRef.source or CallSite.base |
Original source object |
chain |
trace_symbol() output |
Ordered display chain, dedup'd |
tops |
extract_final_source() |
Current: single top string. Future: list with alternatives |
complete |
Trace outcome | Whether trace reached a terminal without errors |
diagnostics |
Trace errors | Recursion limit, cycle detection, etc. |
Rule priority order:
- Local function/method definition →
"local" - Python builtin / implicit builtin (no import required) →
"python" - Imported module/package (via import/from import) → top-level owner name
e.g.
json,pathlib,requests,numpy - Cross-file re-export → library name
- Parameter propagation → library name, confidence < 1.0
- Return value propagation → library name, confidence < 1.0
- Branch/fork multi-source → alternatives, confidence < 1.0
- Unresolved →
"unknown",REASON_UNRESOLVED
Unresolved symbols are normalised to "unknown" with REASON_UNRESOLVED
by ClassificationPipeline.classify().
| Constant | Meaning |
|---|---|
DIRECT_IMPORT |
Symbol is an import alias or from-import |
TRANSITIVE_IMPORT |
Symbol traces through a re-export chain |
LOCAL_DEFINITION |
Symbol resolves to a locally defined function/class |
BUILTIN |
Symbol is a Python builtin (no import required) |
PARAMETER_PROPAGATION |
Symbol traces through a function parameter |
RETURN_PROPAGATION |
Symbol traces through a function return value |
FLOW_MERGE |
Multiple branches merged (if/else, try/except) |
UNRESOLVED |
Trace could not reach a terminal |
| Case | Confidence |
|---|---|
| Single direct import | 1.0 |
| Single local definition | 1.0 |
| Single builtin | 1.0 |
| Parameter propagation, unique source | 0.9 |
| Return propagation, unique source | 0.9 |
| Multiple sources, same library | 0.85 |
| Multiple sources, multiple libraries | 1 / len(alternatives), min 0.2 |
| Unresolved | 0.0 |
- Parameter and return propagation uses exact call-site positions and a unique
project-local target. Ambiguous targets, conflicting owners, unsupported
argument binding, and recursive cycles remain
unknown. return_sourcesusesSourceSetfor multi-return paths; alternatives classification handles ambiguous cases.- Constructor argument to
self.attrpropagation and wrapper-class instance method resolution usesinstance_attrsand constructor call-site matching. Factory-returned instances are supported in 1.0.5 P1 via_resolve_receiver_object_top; complex factories with branches or unresolved returns remain conservative. nonlocalis first-edition no-crash only.
PCResolve performs context-sensitive propagation across a unique project-local call edge. The context is identified by the caller module and the exact call line and column. Positional and keyword arguments can replace the callee's parameters while resolving:
- a parameter used as a receiver inside a local function;
- a local function result returned from one or more simple wrappers;
- a constructor argument stored on
selfand returned by a local method; - the same local function called with different owners at different call sites.
Each context is evaluated independently. Reusing the same assignment name at
later call sites does not change earlier results. A return summary with one
owner produces RETURN_PROPAGATION; multiple concrete owners produce
FLOW_MERGE with alternatives and an unknown primary owner.
This is not whole-program call-graph inference. PCResolve does not guess through dynamic dispatch, unresolved callbacks, arbitrary mutation, external library implementations, or dead code without a statically supported value.
PCResolve handles common wrapper-class patterns using InstanceMethod,
CallResult.call_lineno/call_col_offset, return_sources, and constructor
call-site facts, without a full class hierarchy graph.
| Pattern | Example | Resolution |
|---|---|---|
| Instance method on locally-constructed object | x = ClassName(...); x.method(...) |
InstanceMethod(receiver=x, method=method) |
| Wrapper method return-source through constructor arg | api = Api(requests.Session()); api.get(...) |
requests |
| Multi-instance receiver-specific constructor matching | a = Api(requests.Session()); b = Api(httpx.Client()) |
a.get() → requests, b.get() → httpx |
| Simple alias following | c = b; c.get(...) |
follows to same constructor call-site |
self.attr.method() in method body |
self.session = param; ... return self.session.get(...) |
participates in return-source propagation |
| Pure-local method | no constructor-arg dependency | "local" |
- Factory-returned instances: simple local factories that return a
import-backed library-owned object (e.g.
def make(): return requests.Session()) are supported in 1.0.5 P1. Complex factories with branches, mutation, same-name method collisions, or unresolved returns remain conservative. - Method name collision / override:
return_sourcesis keyed by bare method name (e.g."get"). Complex same-name methods, inheritance, and overrides needClass.method/FunctionId(module, qualname)(future work). - Import-backed base-class methods,
@classmethod,@staticmethod, descriptors / properties: not in lite scope.
import requests, httpx
def make(client):
return client
class Api:
def __init__(self, session):
self.session = session
def get(self, url):
return self.session.get(url)
a = Api(requests.Session())
b = Api(httpx.Client())
c = make(httpx.Client()) # factory return: supported in 1.0.5 P1
a.get("x") # → requests
b.get("y") # → httpx
c.get("z") # → httpx (1.0.5 P1: factory return tracing)Class method resolution does not alter the decorator provenance contract:
- Decorator evidence continues to be exposed via
decorated_by. - A decorator never changes the primary identity of the decorated target.
ApiCall.decorated_byuses two-level matching (1.0.5 P2): exact(file_path, scope_name, func_name)with receiver-aware fallback for dotted calls. Instance methods on decorated classes still require full class-aware resolution (future work).
Decorators create two distinct kinds of evidence that must not be conflated:
| Evidence | Field | Semantics | Stability |
|---|---|---|---|
| Decorator expression call | ApiCall.top_library |
The decorator @lib.deco(args) itself is a call to lib |
Public, stable |
| Decorated target call | ApiCall.top_library |
A call to the decorated function/class is always "local" |
Public, stable |
| Decorator provenance evidence | SymbolProvenance(kind="decorated_by") |
Records which libraries decorated the target | Public, stable |
| Decorator evidence on call | ApiCall.decorated_by |
Mirrors decorated_by evidence via two-level match: exact (file_path, scope_name, func_name) then receiver-aware fallback for dotted calls |
Public, additive-only |
A decorator never changes the primary identity of the decorated target.
@app.route("/")makesindex()a Flask-decorated function, butindex()itself is still a locally-defined callable.
Itstop_libraryremains"local". Decorator provenance is surfaced viadecorated_by, not viatop_library.
Local decorator functions preserve their name as evidence, and chain through return_sources:
| Decorator | decorated_by evidence |
|---|---|
@app.route("/") |
flask |
@click.command() |
click |
@dataclass |
dataclasses |
@local_deco (returns click.command()(f)) |
click (via return_sources) |
@passthrough (returns f) |
"local" (filtered from ApiCall.decorated_by) |
To find all call sites potentially related to library lib:
- Direct API calls:
ApiCall.top_library == lib - Decorated local calls:
lib in ApiCall.decorated_byANDApiCall.top_library == "local" - Method calls: decorated callable receiver methods (e.g.
hello.main()) are supported in 1.0.5 P2 via receiver-aware lookup. Full class-aware receiver resolution (instance methods on decorated classes) is future work.
- Field type:
list[str], default[] - Stability: additive-only (new evidence may appear, but existing entries never removed without schema version bump)
- Null/empty semantics:
[]means "no decorator evidence found on this call". 1.0.5 P2 supports receiver-aware lookup for dotted method calls (e.g.hello.main()); instance methods on decorated classes require full class-aware resolution (future work). - Filtered values:
"local","python","unknown"are excluded; only import-backed library names appear - Matching: two-level lookup:
- exact:
(file_path, scope_name, func_name) - receiver-aware fallback (1.0.5 P2): if
func_nameis dotted, match the first dot-segment as the receiver in the decorator index Supported:hello.main()wherehellohasdecorated_byprovenance. Not yet supported: instance methods on decorated classes, MRO-based receiver resolution, aliased receivers (e.g.cmd = hello; cmd.main()).
- exact:
Calls such as hello.main() where hello is a local function decorated by
@click.command() are classified as local. This is intentional:
the decorated callable remains a same-project object, so decorator evidence
must not replace the primary top_library.
1.0.5 P2 adds receiver-aware decorated_by lookup: when func_name is
dotted (e.g. hello.main), lookup_decorated_by also checks the receiver
part (hello) in the decorator index. This means hello.main().decorated_by
now contains ["click"] while top_library stays local.
Receivers are identified by the first dot-segment of func_name; the
lookup does NOT guess libraries from method names like main, run,
or callback.
Contract points:
hello.main()continues to reporttop_library="local".- Decorated local callables are not reclassified as import-backed primary calls.
- 1.0.5 P2: receiver-aware lookup propagates
decorated_byfrom the decorated callable to its dotted method calls (e.g.hello.main()). - Full class-aware decorated instance methods are future work.
- Downstream consumers should inspect both
SymbolProvenance(kind="decorated_by")andApiCall.decorated_by.
- SourceSet alternatives: flow through
ClassificationPipelinefor multi-source resolution. - CallGraph edges:
call_graph.pyfeeds param/return propagation into trace. - Class method resolution:
instance_attrshandles constructor args; full class-aware receiver resolution (MRO,@classmethod,@staticmethod) is future work. - Classification:
ClassificationPipeline.classify()handles reason/confidence/alternatives. - Method decorator evidence:
ApiCall.decorated_byuses exact(file_path, scope_name, func_name)match plus receiver-aware fallback for dotted decorated callable calls (1.0.5 P2); full class-aware decorated instance methods are future work.