Skip to main content

Code survey · map an unfamiliar project

A two-pass method for understanding large repositories: first feed the skeleton to get an architecture map, then fill in named files for a full call chain.

Disclaimer: The content shown on this page is generated by an AI model and is for reference only. It does not constitute any guarantee of results. Actual output and Credits consumption are subject to "Usage Details" in the console.
This walkthrough uses the asyncio package of CPython 3.13 (33 modules / about 15000 lines / 519 KB) to run through the two-pass survey method: the most time-consuming part of taking over an unfamiliar repo is first figuring out how it runs. Real projects run tens of thousands of lines — you can't and shouldn't stuff it all in at once. The right approach is to first give the structural skeleton so the model can point the way and name key files, then fill in the full text of those named files. The first pass takes only a tenth of the input. Call chain: qwen3.8-max (two calls). (1) Feed only the signature skeleton (52 KB, about 10% of the source) to get an architecture map and named files → (2) feed the full text of the named files to get a precise call chain.

Step 1: Give only the skeleton, get an architecture map and named files

Use ast to walk *.py, taking only class and function signatures plus the first docstring line, dropping the bodies. Generate the signature skeleton and send it along with the instruction below to qwen3.8-max:
I'm taking over maintenance of a Python project and need to understand its architecture
first. This package has 33 modules and about 15000 lines of code — too large to give you
all at once, so I'll first give you the **structural skeleton** (only classes, function
signatures, and the first docstring line, no implementation bodies).

Please do three things based on the skeleton:

I. Architectural layering
How many layers is this package designed into? What is each layer's responsibility? How
do the layers collaborate? Draw a concise layer diagram (text version).

II. Core abstractions and inheritance
Find the 5-8 most important abstractions (base classes / protocols / interfaces) in this
package and explain:
- What problem each abstraction solves
- Its main subclasses and along which dimension they diverge (by platform? by IO mechanism?)
- Draw the key inheritance relationships as an indented tree

III. What order should I read in, and which files must I look at now
- Give a reading order (which module to read first, and why)
- Then **explicitly name 3-5 files** that you need to see the full implementation of to
  explain the core runtime mechanism. List them on a separate line in this format:
  NEED_FILES: a.py, b.py, c.py

Requirements:
- Class names and method names must match the skeleton exactly; do not make things up
- You have only seen signatures; mark any conclusion reached by inference with "(inferred)"
- Do not restate the skeleton content; give your own synthesis

Skeleton:
[paste the signature skeleton generated with ast]

Step 2: Fill in the named files for a call chain

Send back the full text of the files the model named, and have it draw the core call chain based on the real code:
OK, here is the full implementation of the files you named. Please answer based on the
real code:

I. The complete call chain of a core flow
Pick the most common path — "run a coroutine to completion with asyncio.run()" — and draw
the complete call chain from the entry to where the coroutine is actually executed.
Requirements:
- Use real function/method names, annotating which class and file each belongs to
- Explain what key action each step performs
- Be specific: at which step the event loop actually starts turning, and how the coroutine
  is wrapped into a schedulable object

II. What happens in one iteration of the event loop
When the event loop runs one lap, what does it do, in order? List them in the order found
in the real code, and indicate which method each action is in.

III. Three implementation pitfalls
Based on the real code you read, point out three things users tend to misunderstand but
that the code handles explicitly. Each must indicate the corresponding code location.

Requirements: all function names, class names, and attribute names must exactly match
the code given to you; for anything not seen in the code but filled in from common
knowledge, clearly mark "(not confirmed in the files provided this round)".

[paste the full text of the files it named]
[II. Core abstractions and inheritance (Round 1 · synthesized from skeleton)]

  1) EventLoop — the hub of the entire package, responsible for scheduling and IO
       AbstractEventLoop (events.py, abstract interface)
         `- BaseEventLoop (base_events.py, generic implementation)
             `- BaseSelectorEventLoop (selector_events.py, selector-based IO)
                 `- _UnixSelectorEventLoop (unix_events.py, Unix platform)
             `- BaseProactorEventLoop (proactor_events.py, Windows IOCP)
       — diverges along two dimensions: "platform + IO mechanism"

  2) Future / Task — unify "results that arrive later" and "coroutine execution" into
     awaitable objects
       Future (futures.py)
         `- Task (tasks.py, the layer that drives coroutines)

  3) Transport / Protocol — a pair of partners: Transport handles "how bytes are sent,"
     Protocol handles "what to do with received bytes," and the two are decoupled via
     callbacks

[I. Call chain of asyncio.run() running a coroutine to completion (Round 2 · from real code)]

  asyncio.run(main)                              # runners.py (not provided this round, marked unconfirmed)
    `- BaseEventLoop.run_until_complete(future)   # base_events.py
        |- self._check_closed()                       confirm loop not closed
        |- self._check_running()                      confirm no other loop running
        |- new_task = not futures.isfuture(future)    determine if already a Future
        |- future = tasks.ensure_future(future,…)     <-- coroutine is wrapped into a Task here
        |- future._log_destroy_pending = False        suppress destroy pending warning
        |- future.add_done_callback(                  stop loop when target done
        |     _run_until_complete_cb)
        |- self.run_forever()                         <-- event loop truly starts turning here
        `- return future.result()                     normal return / re-raise exception / raise CancelledError

[III. Three implementation pitfalls (the model proactively pointed out)]
  · Future = _CFuture = _asyncio.Future (end of futures.py) — at runtime it may use the C
    implementation; the pure Python version is only a fallback when the C extension is absent
  · Callbacks registered with call_soon do not execute immediately; they are queued to the
    next lap of the event loop
  · The moment a coroutine is wrapped into a Task is ensure_future, not the moment run is called

Notes

  • Consumption: This playbook involves long context + deep reasoning (qwen3.8-max). Two-round actual usage: Round 1 input about 14000 / output about 20000 (of which thinking about 12000); Round 2 input about 70000 / output about 23000 (thinking about 15000). Thinking consumption is often close to or even exceeds the prose output — don't estimate cost by the report word count alone.
  • Where the two-pass method saves: Putting 519 KB of source directly into context is roughly hundreds of thousands of tokens, whereas feeding the 52 KB skeleton first costs only about 14000 input; filling in the full text after the first round names the files is far cheaper than stuffing everything in at once.
  • Sensitive information: Code entering the context means content is sent to the server side. Files containing keys, intranet addresses, or customer data must be removed or desensitized first.
Model Playground
Fine-tuning
Model Compression
Statistics and Monitoring
Support