Refactoring prompts with type-safety locks

The PR description prompt

4 min read

A good PR description has three parts: what changed, why it changed, and how to verify it. Engineers know this. They still write one-line PRs because the description is the last thing standing between them and pushing the change.

The diff is enough context for a model to write a good description. You shouldn't be doing this work.

Write a PR description for this change. Format:

Summary

(2-3 bullets)

Why

(1-2 sentences, name the root cause)

Test plan

(checklist of 3-4 items)

Diff:

- function isValidEmail(s) { return s.includes("@") && s.includes("."); }
+ function isValidEmail(s: string): boolean {
+   if (s.trim() !== s) return false;
+   const at = s.indexOf("@");
+   if (at < 1) return false;
+   const dot = s.indexOf(".", at);
+   if (dot < 0) return false;
+   const tld = s.slice(dot + 1);
+   return tld.length >= 2 && /^[a-zA-Z]+$/.test(tld);
+ }

Claude Sonnet 4.5 returned:

## Summary
- Improved email validation to check for leading/trailing whitespace, proper
  ordering of `@` and `.` characters, and minimum position requirements
- Added validation for top-level domain (TLD) to require at least 2
  alphabetic characters
- Added TypeScript type annotations for better type safety

## Why
The original implementation was too permissive and would accept invalid email
formats like `".@"`, `"@."`, `" user@example.com "`, or `"test@com.1"`, which
could lead to data quality issues and potential security vulnerabilities.

## Test plan
- [ ] Verify valid emails pass: `"user@example.com"`, `"test@domain.co"`
- [ ] Verify invalid emails fail: `"@example.com"`, `"user@"`, `"user@com"`,
      `"test@domain.c"`, `"user@domain.1om"`
- [ ] Verify whitespace handling: `" user@example.com"`, `"user@example.com "`
      should fail
- [ ] Verify TypeScript compilation succeeds with proper type checking

Captured from Claude Sonnet 4.5 (claude-sonnet-4-5) on 2026-04-27. Re-runs may differ slightly — see the model-lifecycle note in Module 1, lesson 1.

As a piece of writing this is publishable. The summary is bullet-pointed and scannable, the "why" names a real failure mode, and the test plan lists concrete inputs with checkboxes.

As a piece of review it is missing the most important thing in the diff. Run the new function over inputs the model did not list:

isValidEmail("user@mail.example.com")   // false
isValidEmail("user@example.co.uk")      // false

Both are valid addresses. Both are rejected. The cause is one character in the diff: s.indexOf(".", at) finds the first dot after the @, not the last, so for mail.example.com the "TLD" it tests is example.com — which fails /^[a-zA-Z]+$/ on the dot. Every subdomain and every multi-part country TLD now bounces.

That is a user-visible regression shipped under a heading that says the change makes validation better. And notice which way the description leans: the "## Why" says the original was too permissive and lists four inputs that used to be wrongly accepted. It is a one-directional argument. Nothing in it asks the question a reviewer must ask about any tightening — what does this now reject that it shouldn't? The test plan inherits the same blind spot: every "invalid" case listed is one the model already knew the code handles, because it derived them from the code it was reading.

This is structural, not a bad day for the model. A PR description is generated from the diff, so the diff is the only evidence in the room. Anything the diff gets wrong, the description will describe confidently and in the author's voice. The generator cannot audit its own only source. Fluent prose about a change is not evidence the change is correct — and a well-formatted description is more dangerous than a one-liner, because it reads like someone already thought about it.

So use this prompt, and pair it with one constraint that pushes against the grain:

In the Test plan, include at least two inputs that the OLD code accepted and the NEW code rejects. If you cannot find any, say so explicitly.

That single line turns a summary into a review. It forces the model to search the other direction across the behaviour boundary — the direction where regressions live — and an explicit "I could not find any" is itself information, where silence is not.

The correctness fix for this particular diff is lastIndexOf:

const dot = s.lastIndexOf(".");

which is what the module 5 capture of the same task got right — worth comparing the two side by side.

The format block is doing the work. Without ## Summary, ## Why, ## Test plan, you'd get a single paragraph. The Markdown headers force the structure your team's PR template expects.

The diff → description → reviewer pipeline:

diff → description → reviewer, and where to interrupt it

1. The diff

The only evidence the generator will ever see

2. Format block

## Summary / ## Why / ## Test plan — your team's template, filled

3. The audit constraint

Ask for inputs the OLD code accepted and the NEW code rejects. This is the step most people skip

4. Reviewer

Reads a description that argues both directions, not just the flattering one

Three principles to follow when adapting this prompt to your team:

  1. Match your team's template. If your repo's PR template has sections for ## Risk or ## Rollback plan, add them to the format block. The model will fill them.
  2. Keep test plans as checkboxes. A reviewer can run them and tick. A reviewer reading prose has to translate it into checkboxes mentally, then run them.
  3. Ask for the root cause in ## Why. "Refactored email validation" is a description, not a reason. "Original implementation accepted whitespace and short TLDs, leading to bounce rates of 8%" is a reason. The constraint "name the root cause" forces the second.

A small advanced trick: if your PRs go through a code review tool that supports labels, add to the prompt:

Suggest 1-3 labels from this set: bug, feature, refactor, chore, breaking. Output the labels on a final line as Labels: ....

The label suggestions are usually right. They save you the click. You still review them — the model can be wrong about whether a change is breaking — but the suggestion is ready when you commit.

Next module: turning the same skeleton into review prompts. :::

Quiz

Module 3: Refactoring Prompts

Take Quiz
Was this lesson helpful?

Sign in to rate