Your First Prompt with a PViz Bundle
How to construct a bundle-grounded LLM prompt for codebase questions — with a worked example using Scrapy.
If you've read Part 1, you know why pasting files into a chat window breaks down past a certain complexity threshold.
This post is the hands-on counterpart. We're going to generate a PViz bundle, construct a prompt around it, and ask a realistic question — the kind you'd actually ask when working on a codebase you don't fully know yet.
The codebase
We'll use Scrapy, the Python web scraping framework, as our subject. It's a good choice for this because it's large enough to be realistic, well-structured enough to be readable, and public enough that you can follow along.
PViz has a published analysis of Scrapy in its showcase, so you can download the bundle directly from there if you want to skip the generation step.
If you're following along with your own codebase, the process is identical. Generate your bundle from the PViz interface, download the JSON, and substitute it where we use the Scrapy bundle below.
What's in the bundle
Before you paste anything into a prompt, it's worth knowing what you're working with.
Open the JSON file. You'll see three top-level keys:
{
"nodes": [],
"edges": [],
"folder_index": {}
}
Each part gives the LLM a different kind of structural context.
nodes
The nodes array is the most important one for this exercise. Each entry represents a module or file in the codebase.
A simplified node might look like this:
{
"id": "scrapy/crawler.py",
"role": "hotspot",
"imports": ["scrapy/core/engine.py", "scrapy/extension.py"],
"exports": ["CrawlerProcess", "CrawlerRunner"],
"tc": 41
}
Every field is doing something specific.
id is the module path relative to the project root. The LLM can use this to cite specific files rather than describing the codebase in general terms. When you ask about Scrapy's signal system, a bundle-grounded answer should name something like scrapy/signals.py instead of vaguely referring to "the module that handles signals."
role is a classification PViz assigns based on structural position. The main values you'll see are:
hotspot— structurally important modules with broad graph reach or central positioninghub— modules imported by many other modulesleaf— modules with minimal downstream structure, often utilities, data definitions, or thin wrappersisolated— modules not connected to the rest of the graph in a meaningful way, often fixtures, scripts, or standalone files
Role labels help orient the LLM quickly. If you're asking "where does this behavior get initialized?" you probably want to start around hotspots and hubs, not isolated leaves.
imports and exports describe direct dependency relationships: what this module pulls in and what it makes available to other modules. These are the raw material behind the graph.
tc is transitive closure size — the count of modules this node can reach by following dependency edges. A high tc means the module sits near a broad dependency surface and may pull in a large portion of the system.
That matters, but it needs to be interpreted carefully. A high tc does not automatically mean many modules depend on this file. It means this file can reach many modules through its own dependency chain. For change-risk questions, tc is most useful when combined with role, in-degree, and edge context. High tc tells you what the module reaches; high in-degree tells you how many modules reach it.
For an LLM, that distinction is useful. It can separate "this file depends on a lot of the system" from "a lot of the system depends on this file."
edges
The edges array represents typed relationships between modules.
Where imports and exports give you direct relationships inside each node, edges let the model traverse the graph: follow dependency chains, identify callers, find connections between subsystems, or reason about what sits between two parts of the codebase.
For most chat-based prompts, you don't need to explain the edge schema in detail. The important thing is that the edge list is present. It gives the LLM a way to justify structural claims by pointing to actual relationships instead of guessing from filenames.
folder_index
The folder_index is a compact hierarchical summary of the project.
Think of it as the table of contents for the bundle. It helps the LLM understand the shape of the repository before it digs into individual modules.
For a project like Scrapy, the folder index helps establish that:
scrapy/core/contains core execution machineryscrapy/extensions/contains optional extension behaviorscrapy/http/handles request and response modelingscrapy/pipelines/and related modules participate in item processing
That context matters because it shapes how the LLM interprets the node list. A file in scrapy/core/ usually carries a different kind of architectural weight than a file in a narrow utility directory.
The question
We're going to ask:
I want to add a hook that fires after every item is scraped. What parts of the codebase would I need to understand, and what files would I likely touch?
This is a realistic task. It requires understanding Scrapy's signal system, item flow, pipeline behavior, and how those pieces connect to the engine.
A generic LLM answer may sound plausible. It may even be mostly correct if the model has seen Scrapy before. But that is not the same as being grounded in this exact codebase.
That distinction matters. Well-known open-source frameworks are represented in LLM training data, but "roughly right" is where mistakes creep in. If you're working with a fork, an older version, or a project that added its own abstractions, general training data can mislead you.
The bundle gives the model a structural map of what is actually present.
Prompt construction
Here's the prompt structure we'll use:
You are a software engineer helping me understand and modify an unfamiliar codebase.
I'm going to give you a PViz dependency analysis bundle for the Scrapy web scraping
framework. The bundle contains:
- nodes: one entry per module, with imports, exports, role, and transitive closure size
- edges: typed dependency relationships between modules
- folder_index: a hierarchical summary of the project structure
Use the bundle to answer my question. Ground your answer in the actual structure of this
codebase. Do not rely on general knowledge of how Scrapy works unless the bundle supports it.
<pviz_bundle>
[paste bundle JSON here]
</pviz_bundle>
My question:
I want to add a hook that fires after every item is scraped. What parts of the codebase
should I understand first, and which files would I likely need to touch?
There are three important pieces in that prompt.
The grounding instruction
This line is doing real work:
Ground your answer in the actual structure of this codebase. Do not rely on general
knowledge of how Scrapy works unless the bundle supports it.
Without that instruction, an LLM may default to its training data when it recognizes a popular project. For Scrapy, that training data may be good enough to sound authoritative while still being wrong about this specific version, fork, or repository layout.
The grounding instruction tells the model to treat the bundle as the primary source. That should push the answer toward specific node IDs, folder locations, and dependency relationships.
You can test this yourself. Ask the same question with and without the grounding instruction. The answers may look similar at first. The difference usually appears when you ask the model to justify a specific claim. With the instruction in place, a good response should be able to point back to files and graph relationships from the bundle.
The XML tags
The <pviz_bundle> tags are not strictly required, but they help.
Large JSON objects can blur the boundary between instruction and data. Named tags make that boundary explicit. The model can more easily distinguish your task instructions, the structured bundle, and the actual question.
This is not specific to PViz. Whenever you inject a large structured object into a prompt, wrapping it in named tags is a good practice. Use a descriptive name like <pviz_bundle>, <dependency_graph>, or <codebase_context> — not a generic label like <data>.
The way the question is phrased
The question is task-oriented, not abstract.
Instead of asking "How does Scrapy work?", we ask "What parts of the codebase should I understand first, and which files would I likely need to touch?" That phrasing pulls for an actionable answer. It encourages the model to produce a short map of relevant files, explain why each one matters, and distinguish between files you should understand and files you should actually modify.
Bundle-grounded prompts are best suited to questions with answers that live in the graph:
- Where is this behavior defined?
- What depends on this module?
- What would be affected if I changed this file?
- Which files sit between subsystem A and subsystem B?
- What should I read before modifying this feature?
- Which modules are likely extension points versus core internals?
They are less useful for broad judgment questions like "Is Scrapy's architecture good?" or "Should I rewrite this in Rust?" Those require external judgment, product context, or tradeoff analysis.
What a good answer should look like
With a well-formed bundle and a prompt like this, a good answer should identify the files that sit near the item-scraping path rather than giving generic advice.
For the Scrapy example, you should expect an answer shaped roughly like this:
Start by reading:
1. scrapy/signals.py
Defines the signal surface. For a post-item hook, this is where existing item-related
signals such as item_scraped or item_dropped would be confirmed.
2. scrapy/core/scraper.py
Coordinates item scraping and pipeline dispatch. This is likely where item completion
behavior connects to signal emission.
3. scrapy/extension.py
Shows how extension-style behavior is registered and connected to signals.
Likely avoid modifying:
- scrapy/core/engine.py unless the new hook needs to affect crawl scheduling or engine
lifecycle. It may be structurally central, but signals/extensions are safer extension
points for this kind of change.
The exact answer will depend on the bundle, but the important part is the shape of the reasoning. A useful response should name specific files, explain why each file matters, separate "read this first" from "modify this," identify safer extension points, and justify claims using the node and edge structure.
You can also follow up with:
- Show the relevant path through the bundle that supports this recommendation.
- Which of these files are extension points, and which are core execution files?
That is where the graph becomes more useful than a normal file dump. The model is not just reading isolated files — it has a structural map of how the files relate.
Why this is different from asking about Scrapy directly
You could ask an LLM: "How do I add a hook after every item is scraped in Scrapy?" For a popular framework, the answer may be decent.
But that answer is based on the model's general knowledge of Scrapy. It may not match the version you're using, a forked codebase, internal modifications, renamed modules, removed extension points, or project-specific wrappers around the framework.
The PViz bundle changes the task. Instead of asking the model to remember how Scrapy works, you're asking it to reason over a structural representation of the codebase in front of it.
The bundle does not make the LLM omniscient. It does not replace reading source code. But it gives the model a grounded map before it starts answering.
A note on bundle size and context limits
Depending on your codebase, the full PViz bundle may be large enough that loading it directly into a chat interface approaches context limits. A roughly 200-node Python codebase typically lands in the 600–800 KB range with PViz's compact schema — manageable for current frontier models. Larger codebases, especially 500+ node repositories, can push past what a single context window handles well.
When that happens, the answer is not arbitrary truncation. Truncating a bundle unpredictably breaks the graph — edges may reference nodes that no longer exist, directory summaries become inaccurate, and the model is reasoning from a structurally incoherent artifact. The right answer is a focused view that preserves the part of the graph relevant to the question while keeping nodes, edges, and metadata aligned.
Slicing: the native solution
PViz generates slice artifacts directly from the canonical bundle for exactly this purpose. A slice is a derived view — not a truncated copy — that extracts a coherent view for a specific architectural question. The edge list, node list, semantic facts, and structural summaries stay consistent because the slicer handles the extraction step automatically.
The available slice types map directly to the kinds of questions you'd otherwise reach for jq to answer:
| Question | Slice |
|---|---|
| What are the dependency hubs? | hub-summary |
| Which files are in cycles? | scc-list |
| What does touching this file affect? | blast-radius |
| What does the directory structure look like? | directory-metrics |
| What are the likely entry points? | entrypoint-candidates |
| What files declare classes, methods, exports, or framework-specific symbols? | symbol-list |
| How much language-specific semantic information is available in this bundle? | language-facts-summary |
The older structural slices focus on how files relate: nodes, edges, cycles, directories, hubs, entrypoints, tests, and blast radius. The newer semantic slices focus on what files contain: declarations, methods, exports, framework roles, and symbol inventories where the parser supports them.
That distinction matters when you're asking an LLM a codebase question. If you want to know what depends on a file, you want graph slices such as edge-list, hub-summary, or blast-radius. If you want to know which files declare a class, expose a method, or contain framework-specific symbols, you want semantic slices such as symbol-list or language-facts-summary.
Each slice is designed to fit more comfortably within a model's context window than the full bundle. The hub summary for a large TypeScript codebase might be 50 KB. The SCC list for a Python project with significant cycle complexity might be 130 KB. A symbol inventory may be larger than a summary slice, but still much smaller and more targeted than the full bundle that produced it. The difference is what you hand to the model.
If you already have a PViz bundle, you can generate slices from the dashboard by dropping the bundle into the slicer and selecting the slice mode that matches your question. The slicer reads the job ID from the bundle, retrieves the canonical stored artifact, and generates the requested views without re-running the analysis.
Manual filtering with jq
If you're working with a bundle outside PViz — for quick local exploration, scripting, or understanding the format — jq filters are a practical alternative. A few useful patterns:
Filter by directory:
jq '.nodes |= map(select(.id | startswith("scrapy/core")))' bundle.json
Filter by role:
jq '.nodes |= map(select(.role == "hotspot" or .role == "hub"))' bundle.json
Filter by transitive closure threshold:
jq '.nodes |= map(select(.tc > 10))' bundle.json
One important caveat: filtering nodes does not automatically produce a coherent subgraph. Edges whose source or target no longer exists in the retained node set will still appear in the output, leaving the model with references to files that aren't in the bundle. For quick interactive exploration that may be acceptable, but for agent workflows or repeatable analysis you want a proper subgraph extraction step — which is what the slicer provides.
jq is available on most Unix-like systems, installable through package managers on Linux, and available on Windows through Chocolatey or WSL.
What comes next
This post used the simplest workflow: put the bundle in the prompt and ask a codebase question. That is enough for many chat-based investigations.
Part 3 moves from chat prompts to agent pipelines. We'll look at how to use the PViz node and edge list to scope what an agent needs to read before it acts:
- why giving an agent the full bundle upfront is usually the wrong move
- how to construct a task-specific subgraph
- how to keep the graph coherent after filtering
- how to hand off from a planning step that reads structure to an execution step that touches files
The basic idea is the same as this post: give the model structure before asking it to act. But for agents, the bundle is not just context — it becomes the planning layer.
Try PViz on your own codebase
Get dependency graphs, coupling signals, and a compressed bundle ready for your LLM — for any GitHub repository, in minutes.