Before writing any logic over a type the project owns — merging, defaulting, unwrapping, normalizing, converting — grep that type's own static/factory methods and search the codebase for the verb you need. A mature codebase has already solved it, with the edge cases handled and a name the reviewer reads fluently. A plausible hand-rolled helper is worse than a bug: it compiles, it passes, and it reads to a maintainer as unfamiliarity with the house. When you do find the helper, check its RETURN TYPE against your call site before swapping, and verify by running rather than reading. Use before writing any utility loop over a house type. Trigger terms: helper method, merge params, apply defaults, unwrap, convert, write a small utility, is there an existing, reinvent, "I'll just loop over".
Given "apply these defaults without clobbering what the caller set", the obvious move
is a loop: iterate the names, check whether the target already has each one, set it if
not. Eight lines, correct, passes.
And in a codebase that has existed for fifteen years, that operation has a name, a
one-line call, and edge-case handling you didn't write. The hand-rolled version costs
three ways: it is longer, it duplicates semantics that can now drift apart, and — the
expensive one — it signals to the reviewer that you didn't look. Reviewers read
unfamiliarity with their own API as a proxy for care, and increasingly as a tooling
signature.
The check is one grep. It is cheaper than the code it replaces.
When to use
Before writing any loop or helper that manipulates a type the project owns.
Especially for these verbs: merge, default, override, unwrap, flatten, normalize,
convert, copy-with-change, wrap.
When a reviewer asks "why not use X?" — treat it as a class of miss, not one miss,
and sweep the rest of the diff for siblings.
When NOT to use
Don't stall on it. Two greps and a look at the type's public surface is the whole
budget; if nothing turns up, write the loop and say in the PR that you looked.
The practice (checklist)
Read the type's own factory/utility surface:
grep -n "public static" path/to/TheType.java
Search the codebase for the verb, in the house's vocabulary:
grep -rn "wrapDefaults\|applyDefaults\|withDefaults\|merge" --include='*.java' <module>/src/java | head
Ask "what would this be called here?" Names cluster by house —
wrapDefaults, wrapAppended, of, getRootCause, copyOf.
Prefer the house call even when yours works: fewer lines, inherits the edge
cases, and reads fluently to the reviewer.
⚠️ Check the return type before swapping. A lazy view is not a mutable
builder; if the call site mutates downstream, you may need to materialize.
Done when you have identified what the call site does with the value.
Verify by running, not reading — a signature match is not a semantics match.
When one is found, sweep for siblings: the same miss usually occurs more than
once in a diff.
Rationalizations
Shortcut
Why it fails
"My loop is correct and tested."
Correctness was never the question. It is longer, duplicates semantics that can drift, and reads as not having looked.
"I don't know what it would be called."
That is what the two greps are for. public static on the type is a 30-line list.
"The helper returns the wrong type."
Sometimes true — then wrap or materialize, and say why in the PR. Check before concluding it.
"I'll refactor it later."
Later is after review, and review is the scarce resource you just spent.
"The reviewer only flagged one place."
They flagged one instance of a class. Sweep the diff.
"I can see you (really the LLMs you use :-) ) are unfamiliar with
SolrParams.wrapDefaults use that. Even has the null check."
The hand-rolled version was an 8-line loop merging parser params into
ModifiableSolrParams, skipping any the request had already set. wrapDefaults had done
exactly that for years, and does carry the null check
(if (defaults == null) return params;). The method went from 15 lines to 7.
The sibling miss, same diff:new MapSolrParams(Map.of(k, v)) had a one-line house
form, SolrParams.of(k, v). One grep "public static" on SolrParams would have found
both.
The return-type trap, same call site:wrapDefaults returns a lazy view, but
downstream code calls .remove() on the result, so it must stay a ModifiableSolrParams.
The swap was right — wrap, then materialize — but the naive substitution would not compile.
Counter-receipt, so the check has a stop condition: in the same file
new NamedList<>(Map) looked like the helper for a Map→NamedList conversion. Reading
it showed it copies values verbatim (nvPairs.add(ent.getValue())) with no recursion, so
it could not serve a recursive normalizer. Hand-rolling was correct there — and
"I checked, it doesn't recurse" is a far stronger PR sentence than silence.
Lifecycle
Signals it worked: no reviewer asks "why not use X?"; diffs shrink because the
house call replaces a loop.
What to log on a misfire: the type, the helper you missed, and the verb you were
searching for — the verb vocabulary per house is the reusable part. Record it in
LEDGER.md.
Death criterion: none foreseeable; every mature codebase has this surface.
Relates to: sibling to match-the-house-shape (that one sizes the PR to the house,
this one writes the code in the house's vocabulary) and to
obey-the-houses-own-tooling — all three are "the house already solved this".
1---2name: grep-the-types-statics-first3description: Before writing any logic over a type the project owns — merging, defaulting, unwrapping, normalizing, converting — grep that type's own static/factory methods and search the codebase for the verb you need. A mature codebase has already solved it, with the edge cases handled and a name the reviewer reads fluently. A plausible hand-rolled helper is worse than a bug: it compiles, it passes, and it reads to a maintainer as unfamiliarity with the house. When you do find the helper, check its RETURN TYPE against your call site before swapping, and verify by running rather than reading. Use before writing any utility loop over a house type. Trigger terms: helper method, merge params, apply defaults, unwrap, convert, write a small utility, is there an existing, reinvent, "I'll just loop over".4---56# Grep the type's statics first78## Purpose910Given "apply these defaults without clobbering what the caller set", the obvious move11is a loop: iterate the names, check whether the target already has each one, set it if12not. Eight lines, correct, passes.1314And in a codebase that has existed for fifteen years, that operation has a name, a15one-line call, and edge-case handling you didn't write. The hand-rolled version costs16three ways: it is longer, it duplicates semantics that can now drift apart, and — the17expensive one — it signals to the reviewer that you didn't look. Reviewers read18unfamiliarity with their own API as a proxy for care, and increasingly as a tooling19signature.2021The check is one grep. It is cheaper than the code it replaces.2223## When to use2425- Before writing any loop or helper that manipulates a type the project owns.26- Especially for these verbs: merge, default, override, unwrap, flatten, normalize,27 convert, copy-with-change, wrap.28- When a reviewer asks "why not use X?" — treat it as a class of miss, not one miss,29 and sweep the rest of the diff for siblings.3031## When NOT to use3233Don't stall on it. Two greps and a look at the type's public surface is the whole34budget; if nothing turns up, write the loop and say in the PR that you looked.3536## The practice (checklist)3738- [ ] Read the type's own factory/utility surface:39 `grep -n "public static" path/to/TheType.java`40- [ ] Search the codebase for the verb, in the house's vocabulary:41 `grep -rn "wrapDefaults\|applyDefaults\|withDefaults\|merge" --include='*.java' <module>/src/java | head`42- [ ] Ask **"what would this be called here?"** Names cluster by house —43 `wrapDefaults`, `wrapAppended`, `of`, `getRootCause`, `copyOf`.44- [ ] Prefer the house call **even when yours works**: fewer lines, inherits the edge45 cases, and reads fluently to the reviewer.46- [ ] ⚠️ Check the **return type** before swapping. A lazy view is not a mutable47 builder; if the call site mutates downstream, you may need to materialize.48 *Done when* you have identified what the call site does with the value.49- [ ] Verify by **running**, not reading — a signature match is not a semantics match.50- [ ] When one is found, sweep for siblings: the same miss usually occurs more than51 once in a diff.5253## Rationalizations5455| Shortcut | Why it fails |56|---|---|57| "My loop is correct and tested." | Correctness was never the question. It is longer, duplicates semantics that can drift, and reads as not having looked. |58| "I don't know what it would be called." | That is what the two greps are for. `public static` on the type is a 30-line list. |59| "The helper returns the wrong type." | Sometimes true — then wrap or materialize, and say why in the PR. Check before concluding it. |60| "I'll refactor it later." | Later is after review, and review is the scarce resource you just spent. |61| "The reviewer only flagged one place." | They flagged one instance of a class. Sweep the diff. |6263## RECEIPT6465**A Solr committer, apache/solr PR #4640, 2026-08** — review comment, verbatim:6667> "I can see you (really the LLMs you use :-) ) are unfamiliar with68> `SolrParams.wrapDefaults` use that. Even has the null check."6970The hand-rolled version was an 8-line loop merging parser params into71`ModifiableSolrParams`, skipping any the request had already set. `wrapDefaults` had done72exactly that for years, and does carry the null check73(`if (defaults == null) return params;`). The method went from 15 lines to 7.7475**The sibling miss, same diff:** `new MapSolrParams(Map.of(k, v))` had a one-line house76form, `SolrParams.of(k, v)`. One `grep "public static"` on `SolrParams` would have found77both.7879**The return-type trap, same call site:** `wrapDefaults` returns a lazy view, but80downstream code calls `.remove()` on the result, so it must stay a `ModifiableSolrParams`.81The swap was right — wrap, then materialize — but the naive substitution would not compile.8283**Counter-receipt, so the check has a stop condition:** in the same file84`new NamedList<>(Map)` looked like the helper for a `Map`→`NamedList` conversion. Reading85it showed it copies values verbatim (`nvPairs.add(ent.getValue())`) with no recursion, so86it could not serve a recursive normalizer. Hand-rolling was correct there — and87*"I checked, it doesn't recurse"* is a far stronger PR sentence than silence.8889## Lifecycle9091- **Signals it worked:** no reviewer asks "why not use X?"; diffs shrink because the92 house call replaces a loop.93- **What to log on a misfire:** the type, the helper you missed, and the verb you were94 searching for — the verb vocabulary per house is the reusable part. Record it in95 [`LEDGER.md`](../../LEDGER.md).96- **Death criterion:** none foreseeable; every mature codebase has this surface.97- **Relates to:** sibling to match-the-house-shape (that one sizes the PR to the house,98 this one writes the code in the house's vocabulary) and to99 obey-the-houses-own-tooling — all three are "the house already solved this".
Run npx skillmds@latest add serhiy-bzhezytskyy/grep-the-types-statics-first in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Before writing any logic over a type the project owns — merging, defaulting, unwrapping, normalizing, converting — grep that type's own static/factory methods and search the codebase for the verb you need. A mature codebase has already solved it, with the edge cases handled and a name the reviewer reads fluently. A plausible hand-rolled helper is worse than a bug: it compiles, it passes, and it reads to a maintainer as unfamiliarity with the house. When you do find the helper, check its RETURN TYPE against your call site before swapping, and verify by running rather than reading. Use before writing any utility loop over a house type. Trigger terms: helper method, merge params, apply defaults, unwrap, convert, write a small utility, is there an existing, reinvent, "I'll just loop over". It is listed under Coding & Dev Tools on SkillMD.
This skill has not completed SkillMD's automated safety review yet. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
serhiy-bzhezytskyy (@serhiy-bzhezytskyy) published this skill. Their other Agent Skills are listed on their SkillMD profile.