DEV Community

Cover image for Help Your Coding Assistant Find the Right Code in Large Repos
Saurabh "Rob" Dahal
Saurabh "Rob" Dahal

Posted on

Help Your Coding Assistant Find the Right Code in Large Repos

Get the starter: sdahal1/repo-search-workshop on GitHub. The reproduction, searches, tests, and typecheck were rehearsed locally on October 16, 2026, with Node.js 24.10.0, npm 11.6.0, ripgrep 15.2.0, and ast-grep 0.51.0. The published repository was cloned and its starter revision verified. The supplied local folder and Git bundle also work for this guide.

Author: Saurabh Rob Dahal, Developer Advocate, AWS

Description: Investigate a TypeScript retry bug using repository mapping, ripgrep, ast-grep, symbol navigation, and an evidence note you can use with different coding assistants.

Level: Beginner · Time: 60 minutes with a facilitator, after setup

A coding assistant can suggest a plausible fix before it understands a repository. A useful investigation makes its reasoning checkable: where does the behavior live, what evidence explains it, and which checks would distinguish a correct fix from a partial one?

In this workshop, you will investigate one prepared bug. A client configured with maxRetries: 0 still makes three requests when its transport fails. You will follow that setting across files, ask an assistant for a supported explanation, and make a small change.

The bug is deliberately small. The practice is learning how to approach unfamiliar code without reading every file or accepting the first plausible explanation. The same approach is useful when an application setting behaves unexpectedly, a wrapper changes an argument, or an error originates in another package.

The eight investigation steps progress from reproduce and map through text search, structural search, symbol navigation, evidence, explanation, and verification.

Each step answers a different question. If the evidence is incomplete, return to a focused lookup before editing.

View the animated investigation sequence

You can follow along with Kiro, Codex, Claude Code, or another assistant that can read your project, run shell commands, and edit files. Choose a coding model supported by your tool. The exercise uses ordinary commands and a Markdown note, so it does not depend on a particular model, context window, or project-memory format.

Your editor supplies definition and reference navigation. No AWS account, credentials, or cloud deployment is required. If you are working independently, pause at the evaluation prompts and take the time you need.

Before the workshop

You need Git, Node.js 24 with npm, ripgrep, and an editor with TypeScript definition and reference navigation. You should be comfortable opening a terminal and editing a text file. Complete your chosen assistant's sign-in before the session.

The commands use a Bash-compatible shell: Terminal on macOS, a Linux terminal, or WSL on Windows. Run commands from the repository root unless a step says otherwise. Copy the contents of a command block without adding a leading $ prompt.

Install ripgrep using the official installation instructions. On a Mac with Homebrew:

brew install ripgrep
Enter fullscreen mode Exit fullscreen mode

Use a fresh checkout. Choose one of these routes, then run the shared setup below. Clone the workshop starter repository:

git clone https://github.com/sdahal1/repo-search-workshop.git repo-search-workshop
cd repo-search-workshop
Enter fullscreen mode Exit fullscreen mode

If you already have the supplied local folder, skip cloning and enter it:

cd repo-search-workshop
Enter fullscreen mode Exit fullscreen mode

If you have repo-search-workshop.bundle in your current directory, create a checkout from that local Git bundle instead:

git clone --branch starter ./repo-search-workshop.bundle repo-search-workshop
cd repo-search-workshop
Enter fullscreen mode Exit fullscreen mode

From the repository root, pin the workshop's starter revision and install its dependencies:

git checkout --detach 5280be1e6659942e2335b10bde88b1fccdf9b76d
git switch -c workshop/my-investigation
node --version
npm --version
rg --version
npm ci
npx --no-install ast-grep --version
git status --short
Enter fullscreen mode Exit fullscreen mode

The checkout puts everyone on the same source revision. The new branch gives you a place to keep your work. The starter pins @ast-grep/cli to 0.51.0 as a development dependency. npm ci installs dependencies from the committed lockfile without updating it. npm ci documentation

Check your setup: Node should report version 24, ast-grep should report 0.51.0, and git status --short should produce no output in this fresh checkout. Open the entire repository folder in your editor so it can find the TypeScript project configuration.

The npx commands use the installed project executable. If npm offers to download a package, stop and check that installation succeeded and that you are in the repository root.

The starter provides a fake transport, a failing regression test, an investigation template, and a saved AST rule. A transport is the component that performs the underlying request. Here, a small local function stands in for it, records calls, and fails predictably. No real service is involved.

You may type each command yourself or ask the assistant to run the exact command. For the first pass, run the searches yourself so you can connect their inputs to their results. Later, delegate those same steps with an explicit scope.

1 Show the failure

Time: 0–4 minutes

Concept: A reproduction turns a report into an observable difference between expected and actual behavior. It also gives you a check to run after editing.

The report says: “I set maxRetries to zero, but the client still retries.” Before investigating, define what counts as a retry. In this project, it is an additional attempt after the initial request. Therefore:

total attempts when every request fails = 1 + allowed retries
zero retries = one initial attempt
Enter fullscreen mode Exit fullscreen mode

A client configured with zero retries should make one initial request, but the prepared bug makes that request plus two additional attempts.

Count transport calls, including the initial request. The symptom does not yet tell us which function is responsible.

Task: Run the demo, then the focused test:

npm run demo
npm run test:repro
Enter fullscreen mode Exit fullscreen mode

Expected result before the fix: The demo records three transport calls for maxRetries: 0. The focused test fails because it expects one call and observes three. A failing assertion is intentional here; a missing-module or command-not-found error is a setup problem.

The local rehearsal confirmed this failure. The full starter suite ran 11 tests: 10 passed, and only the zero-retries reproduction failed. Typechecking passed before the fix, showing that this behavioral bug is not a type error.

Evaluate: Open packages/client/test/client.test.ts. Find where the test supplies zero to the public client API, where the fake transport increments its counter, and where the test checks the final count. Confirm that the test observes behavior through the public API rather than directly testing a helper expression.

Write the reproduction command, expected count, and actual count in workshop/INVESTIGATION.md. Do not change an assertion to match the current behavior.

Teaching moment: “The transport ran three times” is evidence. “The retry loop is off by one” is a hypothesis. Other explanations are possible: the option might be dropped, defaulted incorrectly, or interpreted differently. Keep those possibilities open until you trace the value.

2 Map the repository

Time: 4–12 minutes

Concept: A repository map identifies responsibilities and boundaries. It helps you choose where to look before spending time on implementation details.

Task: Run:

git ls-files
git ls-files 'README*' '*package.json' '*tsconfig*.json'
git ls-files 'packages/client/*'
Enter fullscreen mode Exit fullscreen mode

The first command shows tracked paths. The next commands narrow that inventory to project descriptions, manifests, TypeScript configuration, and the client package. Quoting the patterns passes them to Git instead of asking the shell to expand them. git ls-files documentation

Open README.md, the root package.json, packages/client/package.json, and docs/client-options.md. Find the test scripts and the meaning of maxRetries. A manifest describes a package's dependencies, scripts, and entry points; it helps connect a folder to what the project runs.

The starter repository map separates root configuration, option documentation, client source and tests, and the workshop investigation files.

Use the map to choose a starting area. The relationships must still be confirmed in source and tests.

Ask your assistant:

Map this repository using its tracked file paths, README, package
manifests, and docs/client-options.md. Do not edit files.

Identify the package most likely to own the reported retry behavior.
Explain your choice using paths and documented responsibilities.
Do not read every implementation file yet.
Enter fullscreen mode Exit fullscreen mode

Expected result: packages/client is the likely owner. Its src directory contains implementation and its test directory contains the reproduction. The option documentation establishes that zero is valid.

Evaluate: Write one sentence explaining why the client package is relevant. “It has client in its name” is a starting clue. A documented responsibility and a reproduction that imports the package are stronger support.

Also identify the package's test command before editing. That prevents the assistant from guessing how the project is verified.

Teaching moment: A map is a working hypothesis about ownership. Expand your scope if imports lead elsewhere. git ls-files is an inventory of tracked files; it does not include every untracked file on disk, and it does not describe runtime behavior.

3 Search by text

Time: 12–18 minutes

Concept: Text search is useful when you know a name, error, or literal value. It finds mentions in code, tests, comments, and documentation.

Task: Search the owning package and option documentation:

rg -n -F 'maxRetries' packages/client docs/client-options.md
rg -n -C 3 -F 'maxRetries' packages/client/src
Enter fullscreen mode Exit fullscreen mode

rg runs ripgrep. -n displays line numbers, -F treats the query as literal text, and -C 3 includes three surrounding lines. The final arguments select where to search. ripgrep guide

Expected result: Matches lead to the option type, request path, retry implementation, and regression test. Read a result as a location to inspect, not a complete explanation. Open the enclosing function when three surrounding lines are insufficient.

Three search approaches answer different questions: text finds mentions, AST search finds syntax shapes, and symbol navigation resolves definitions and references.

Choose the search that matches your question. These tools complement one another throughout an investigation.

Evaluate: Classify useful matches by their role:

Kind of match What to learn from it
Option definition Which values the API accepts
Caller or wrapper Whether the value is forwarded or transformed
Implementation How the value affects behavior
Test Which behavior is asserted
Documentation What behavior is promised

Add useful paths and symbols to your investigation note. Record actual line numbers from your checkout; the article does not prescribe fixed line numbers.

If the search returns nothing, first check spelling, case, and your directory. A zero-result search means no matching text was found in the searched scope. It does not prove the behavior is absent from the program.

Teaching moment: A matching name does not establish a call relationship. Two modules may declare independent variables with the same spelling. Text search finds candidates; symbol navigation will help you distinguish them.

4 Search by structure

Time: 18–23 minutes

Concept: An abstract syntax tree, or AST, represents code through syntax nodes and their relationships. An expression such as left || right has an operator and two operands. Structural search asks for that arrangement while allowing selected parts to vary. ast-grep core concepts

You already know the option's name. Now ask a second question: where does the code use an OR expression that might supply a fallback? This is useful when the same structure appears under different variable names or across multiple lines.

Task: Start with the operator as literal text:

rg -n -F '||' packages/client/src
Enter fullscreen mode Exit fullscreen mode

Keep -F: the pipe character has special meaning in regular expressions. Then run the prepared structural query:

npx --no-install ast-grep run --lang ts \
  --pattern '$VALUE || $FALLBACK' packages/client/src
Enter fullscreen mode Exit fullscreen mode

Here, run performs a one-time search, --lang ts selects TypeScript, and --pattern supplies the code shape. There is no rewrite argument, so this command does not modify code. ast-grep run reference

Understand the placeholders

$VALUE and $FALLBACK are metavariables that match syntax nodes. Leave those names in the command. Keep the single quotes so the shell passes the dollar signs unchanged instead of expanding shell variables. ast-grep quick start

For this candidate:

options.maxRetries || DEFAULT_MAX_RETRIES
Enter fullscreen mode Exit fullscreen mode

the left capture is options.maxRetries and the right capture is DEFAULT_MAX_RETRIES. The names VALUE and FALLBACK are labels we chose; they do not tell ast-grep what the author intended.

An OR expression has a left operand captured by VALUE and a right operand captured by FALLBACK; the example captures options.maxRetries and DEFAULT_MAX_RETRIES.

The simplified tree shows what stays fixed—the OR operator—and what the metavariables can capture.

Compare matches and counterexamples

Consider these illustrative snippets. They explain the pattern; they are not claimed to be exact starter files.

const a = options.maxRetries || DEFAULT_MAX_RETRIES;

const b = options.timeoutMs
  ||
  5000;

const allowed = isAdmin || isOwner;

// options.maxRetries || DEFAULT_MAX_RETRIES
const explanation = "options.maxRetries || DEFAULT_MAX_RETRIES";

const enabled = isReady && hasPermission;
Enter fullscreen mode Exit fullscreen mode

The broad pattern matches the OR expressions assigned to a, b, and allowed. The multiline expression still has the requested shape. The comment and string are not executable OR expressions, and the final expression uses a different operator. ast-grep pattern syntax

isAdmin || isOwner is a valuable counterexample: it matches, but the boolean logic may be entirely correct. A syntax match is not a bug diagnosis. The tool does not know whether zero is meaningful to this particular API.

Narrow and save the query

Require the property name while leaving the object and right-hand expression variable:

npx --no-install ast-grep run --lang ts \
  --pattern '$OPTIONS.maxRetries || $FALLBACK' packages/client/src
Enter fullscreen mode Exit fullscreen mode

Expected result: The candidate in packages/client/src/retry.ts remains. The object name can vary, so config.maxRetries || 2 also matches. However, this narrower pattern would miss options['maxRetries'] || 2 or a property read first assigned to a local variable. That is a reason to inspect the surrounding code, not a reason to search every possible expression at once.

The starter's workshop/defaulting.yml saves the broad query:

id: find-or-expressions
language: TypeScript
rule:
  pattern: $VALUE || $FALLBACK
Enter fullscreen mode Exit fullscreen mode

Run it with:

npx --no-install ast-grep scan \
  --rule workshop/defaulting.yml packages/client/src
Enter fullscreen mode Exit fullscreen mode

The rule gives the query a name and language so someone else can repeat it. ast-grep rule essentials

In the pinned starter, the broad query returns four candidates: the multiline request-label expression, the trace expression, the retry-input validation condition, and the retry-setting expression. The first three implement intentional behavior. The narrowed query returns only the candidate in retry.ts. Compare these results with the literal text search, which also finds mentions inside a comment and a string.

Evaluate: Explain why each relevant match deserves inspection. Compare what text search found with what the structural query found. Save the candidate and its enclosing symbol in your note.

Teaching moment: Structural search reduces ambiguity about code shape. It does not establish runtime values, symbol identity, or business intent. Those need different evidence.

5 Follow the symbols

Time: 23–28 minutes

Concept: Symbol navigation resolves an identifier to its declaration and finds uses of that declaration. It helps answer which function is actually involved when names repeat or imports are renamed.

For example, a module could import withRetry as runWithRetries. Searching only for the original name might find the import but miss calls written with the alias. Definition navigation can connect those uses through the project language service.

Task: In your editor:

  1. Open the reproduction test and use Go to Definition on createClient.
  2. In packages/client/src/client.ts, locate the request path and the call to withRetry.
  3. Use Go to Definition on withRetry to reach packages/client/src/retry.ts.
  4. Use Find References on its definition to inspect the callers.
  5. Inspect ClientOptions in packages/client/src/types.ts and compare it with the documented contract.

Use the editor's context menu or command palette to find these actions. The names and shortcuts can differ; for one concrete implementation, see TypeScript navigation in Visual Studio Code.

The starter's symbol path connects the reproduction test to createClient and its request method, then withRetry, with ClientOptions providing the option contract.

Confirm the arrows by navigating definitions and inspecting callers. The picture illustrates the starter's relationships; it is not a screenshot or a generated runtime trace.

Expected result: You can follow the option from the public client API to the retry implementation. Check whether the wrapper forwards zero unchanged before interpreting the defaulting expression.

Evaluate: Record the caller and callee paths and symbols. A reference result may be a declaration, import, or call. Read it before recording a call relationship. Navigation also does not prove that a branch executed during the reproduction; combine it with the test and implementation.

Teaching moment: Language Server Protocol, or LSP, standardizes communication for code intelligence. Editors can also provide TypeScript navigation through their own language-service integrations. The shared requirement here is working definition/reference navigation. There is no universal lsp terminal command, and your assistant does not need direct LSP access. LSP specification

6 Gather evidence before asking for a fix

Time: 28–36 minutes

Concept: A short investigation note separates observations from interpretations and makes your context portable. Another assistant or teammate can inspect the same evidence without reconstructing a long conversation.

An investigation note brings together the observed failure, repository owner, relevant symbols and paths, constraints, open hypothesis, proposed edits, and verification commands.

Keep the evidence compact enough to review, but specific enough to reproduce and challenge.

Task: Complete workshop/INVESTIGATION.md:

# Investigation

## Observed behavior
Reproduction command:
Expected behavior:
Actual behavior:

## Repository area
Owning package and evidence:

## Relevant code
Paths, symbols, actual line numbers, and caller relationships:

## Constraints
Preserve the public API and omitted-option behavior.
Keep the fake transport and meaningful assertions.

## Root cause hypothesis
Leave blank until the next step.

## Proposed modification points
Leave blank until the next step.

## Verification
npm run test:repro
npm test
npm run typecheck
git status --short
git diff
git diff --check
Enter fullscreen mode Exit fullscreen mode

An evidence entry might say, “The reproduction passes zero at the public API and observes three transport calls.” Attach the actual test location. A hypothesis would say, “A defaulting expression may replace zero before the loop runs.” Keeping these in different sections makes it easier to notice what remains unproven.

Include a short reason beside each source location. “This wrapper forwards the option” is more useful than a list of six paths with no explanation. Leave out unrelated search results and large copied files; the assistant can open the referenced code.

Evaluate: Could someone who missed the walkthrough reproduce the failure and locate the relevant expressions from your note? Could they tell which claims you observed and which still require checking?

Teaching moment: Good context is selected evidence with an explicit purpose. Paths and symbols help someone relocate it; line numbers identify positions in this checkout and may move after edits.

7 Explain the root cause before editing

Time: 36–43 minutes

Concept: A supported hypothesis connects the input, the transformation, and the observed output. Evaluate that connection before changing code.

Task: In a fresh or existing assistant conversation in the same checkout, paste:

Read workshop/INVESTIGATION.md. Do not edit files.

Inspect the referenced code and explain the failing behavior.
If evidence is missing, identify the next focused lookup needed.

Return:
- Your root-cause hypothesis.
- Supporting paths, symbols, and actual line numbers.
- Any uncertainty or competing explanation.
- The smallest proposed change and expected modified files.
- Tests that distinguish a correct fix from a partial fix.

Stop before editing.
Enter fullscreen mode Exit fullscreen mode

You may use a plan or read-only mode if your tool provides one. The explicit instruction is still useful because it tells the assistant what deliverable you want at this stage.

Expected explanation: 0 || DEFAULT_MAX_RETRIES selects the default because zero is falsy. With a default of two retries, the always-failing transport receives one initial call plus two retries. The ?? operator uses its fallback only for null or undefined, preserving zero. TypeScript nullish coalescing documentation

A value table compares OR and nullish coalescing with fallback 2: zero becomes 2 under OR but remains zero under nullish coalescing; omitted values use the fallback.

The operator table explains language behavior. Its null, false, and empty-string examples do not expand the client's numeric input contract.

For a text version of the diagram:

| Left value (x) | x \|\| 2 | x ?? 2 | API meaning here |
| --- | --- | --- | --- |
| 0 | 2 | 0 | Valid: disable retries |
| undefined | 2 | 2 | Omitted option: use the default |
| 1 | 1 | 1 | Valid: allow one retry |
| null | 2 | 2 | Language example, outside the option type |
| false | 2 | false | Language example, outside the option type |
| '' | 2 | '' | Language example, outside the option type |

Neither operator validates input. The proposed change corrects defaulting for values already allowed by the API.

Evaluate: Check three links in the explanation:

  1. The public contract allows zero, and the wrapper delivers it unchanged.
  2. The identified expression replaces zero with the documented default of two.
  3. The retry loop interprets two as additional attempts, producing three total calls.

If one link is missing, perform the next focused lookup. An answer that simply recommends ?? has not yet explained the whole symptom. A proposed loop change also needs evidence that the loop itself is wrong.

Optional self-check in your terminal:

node -e 'console.log({ or: 0 || 2, nullish: 0 ?? 2 })'
Enter fullscreen mode Exit fullscreen mode

The expected values are or: 2 and nullish: 0. This checks language behavior in isolation; it does not prove the repository's value follows that path. The earlier evidence establishes that connection.

Update the hypothesis and proposed modification sections of your note. Do not broaden the API to accept null, strings, or booleans just because the operator table includes them.

Teaching moment: Different models may phrase the explanation differently. Judge the evidence and predicted behavior. A useful hypothesis also tells you what you should observe after the fix.

8 Make the narrow change and verify it

Time: 43–56 minutes

Concept: A fix must correct the reported behavior and preserve the surrounding contract. Test results and diff review provide different parts of that evidence.

Task: After checking the explanation, tell the assistant:

Apply the smallest change supported by our investigation.
Preserve the public API and default retry behavior.
Keep the reproduction's original assertion intact.
Add focused regression coverage only where it is missing.
Do not refactor unrelated code or update dependencies.
Do not stage or commit the changes.
Run the verification commands in workshop/INVESTIGATION.md.
Report the modified files and actual results, including failures.
Enter fullscreen mode Exit fullscreen mode

The expected production change is:

- const retries = options.maxRetries || DEFAULT_MAX_RETRIES;
+ const retries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
Enter fullscreen mode Exit fullscreen mode

If you are editing manually, make the same justified change in the editor. Avoid a global replacement of ||: other matches may intentionally use boolean logic or falsy-value defaults.

Run the checks yourself if the assistant has not run them:

npm run test:repro
npm test
npm run typecheck
npm run demo
git status --short
git diff
git diff --check
Enter fullscreen mode Exit fullscreen mode

Evaluate the behavior

A verification matrix checks zero, omitted, and positive retry settings against expected request counts and also checks immediate success. Separate checks cover types and diff scope.

A useful test matrix checks the repaired boundary and nearby behavior that should remain valid.

With the fake transport failing on every attempt, verify:

Input Expected total transport calls after the fix Why check it
maxRetries: 0 1 Reproduces the reported boundary
Option omitted, default of 2 3 Preserves the default
maxRetries: 1 2 Preserves explicit positive values

Also preserve immediate success: when the first request succeeds, there should be one call, regardless of a larger retry allowance. A retry budget allows extra attempts after failure; it does not require them after success.

The rehearsed solution passed the reproduction, all 11 tests, and typechecking; its demo showed one call for zero retries. Those are the expected results for your fix. Read the test output rather than accepting a summary that says “looks good.” Check which command ran and whether it completed successfully.

Evaluate the change

Inspect every changed file. The investigation note is expected to change. Production edits should be limited to the justified implementation; test edits should strengthen coverage while preserving the reproduction's original assertion.

Ask why any dependency lockfile, public type, unrelated helper, or large formatting block changed. Such edits need their own justification. Also inspect untracked files reported by git status, because a normal diff does not display their contents.

git diff shows unstaged tracked changes. If you staged anything, also run git diff --cached. git diff --check checks whitespace errors; silence does not establish behavioral correctness. git diff documentation

Teaching moment: The reproduction checks the bug. Nearby tests check preserved behavior. Typechecking checks type compatibility. Diff review checks the scope and meaning of edits. A passing check cannot answer all four questions.

Finish with a reusable investigation

Time: 56–60 minutes

Explain the bug to a partner in three sentences: what failed, where the value changed meaning, and how you verified the fix. Point to evidence for each sentence.

Then name the question each tool answered. Text search found mentions. AST search found a code shape. Symbol navigation connected definitions and uses. Tests observed behavior. Your investigation note connected those findings into an explanation someone else could review.

On your next unfamiliar repository, start with a reproducible symptom and a small map. Choose searches according to the question you need answered. Expand the scope when the evidence requires it, and give your assistant a clear boundary between investigating and editing.

If a step behaves differently

Symptom Next check
rg is not found Install ripgrep, reopen the terminal, and run rg --version.
ast-grep is missing or the version differs Return to the repository root; confirm npm ci succeeded with development dependencies installed.
AST search has no matches Check the directory, TypeScript language flag, and single quotes. Confirm you still have the unfixed starter.
Definition navigation fails Open the repository root, install dependencies, and confirm the editor recognizes the TypeScript project. Follow imports manually while fixing navigation.
The reproduction already passes Inspect the checkout and existing diff. You may have the solution or a previous edit. Preserve your work and use a fresh starter checkout.
A test reports a setup error Resolve that error before interpreting it as evidence of the retry bug.
The assistant edits during investigation Inspect the diff before continuing. Preserve your note and use a fresh starter checkout if needed.
The fix passes one test but breaks another Compare the failing case with the documented contract; revisit the hypothesis instead of weakening an assertion.

No cloud resources are created by this exercise, so there is no AWS cleanup step. Keep your checkout to reuse the investigation note and commands.

Top comments (2)

Collapse
 
reidmarlow profile image
Reid Marlow •

Separating the search step from the edit prompt is the single biggest reliability boost when pointing an agent at a repo it has not indexed yet.If you let a tool like Claude Code or Codex do free-form string searches across the whole workspace, it almost always gloms onto variable names inside fixture mocks or test assertions before it ever finds the actual transport layer. Forcing it to run structural queries with ast-grep or grep with path boundaries first anchors the context on the call-site signature. Once the model has the exact AST node and the surrounding ten lines pinned in its evidence buffer, it stops trying to solve the problem by hallucinating fallback options on the wrapper.

Collapse
 
hacksore profile image
Sean Boult •

rg (mentioned here) and fzf are two pieces of fantastic software!