From skills
Detects and inlines single-caller forwarding functions in TypeScript codebases, reducing over-abstraction and dependency-injection ceremony.
How this skill is triggered — by the user, by Claude, or both
Slash command
/skills:flatten-fake-layersThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Operationalizes the `delete-fake-layers` doctrine with a script instead of prose. The thesis:
Operationalizes the delete-fake-layers doctrine with a script instead of prose. The thesis:
a model is bad at finding its own over-abstraction by reading code, but good at judging a
concrete candidate the moment a tool points at it. So this skill does not ask you to scan for
slop — it runs a detector that flags exact violations, and you apply one judgment per item.
A function/const that exists only to forward to another call, reshape arguments, or inject a
dependency — adding no invariant, validation, lifecycle guarantee, or real ownership. The test
(from delete-fake-layers): what becomes true after this layer runs? If the answer is "same
data, different name" or "same call, different function", it is a fake layer.
Whole-program tools (knip, fallow) find code with zero callers. They cannot find the layer with one caller that should not exist. That is this script's gap to fill.
A one-caller function earns its keep when it does work the caller would lose by inlining it — not when it merely reads nicer with a name. A nice name is a weak reason and usually an inline target anyway. Real work means it changes types or control flow, or owns a boundary:
requireSession(req): Session narrows Session | undefined to
Session (and throws if absent). Inlining loses the narrowing; the caller would re-handle
undefined everywhere.try/finally cleanup).This is the delete-fake-layers test: what becomes true after this layer runs? If a real
guarantee becomes true (a narrowed type, a thrown error, a validated value), keep it — even at one
caller, even when it is registered as middleware. If the honest answer is "same data, different
name", inline it. The judgment per flagged item is therefore: does this do real work the caller
would lose, or does it only forward/reshape/rename?
cd scripts && npm install # one-time: installs ts-morph + tsx
node_modules/.bin/tsx find-fake-layers.mts <path/to/tsconfig.json> [pathFilter] [--json]
pathFilter is an optional substring (e.g. src/server) to scope a large repo. --json emits
the candidate list as structured evidence for a loop/goal model to consume.
The flagged list is ranked. It reports three shapes:
di-factory — a factory whose parameter is captured inside the closure it returns
(hand-rolled dependency injection). Almost always real slop: import the dependency directly
and make the product a module-level value.alias — forwards its arguments unchanged to one project function ("same call, different
name"). Inline it; replace the reference with the target.reshape — builds an object literal and forwards it, annotated with +N const fields
(how many constant values it injects). Few const fields = clean inline-upstream target
(push the constant to the caller); many = it is assembling a real config block, judge with care.Suppressed (never flagged): predicates (is/has/should), React hooks (use*), try/finally
and validation-boundary bodies, cross-package exports (public API), and framework-registered
route handlers. See the TUNING block at the top of the script to adjust these per stack
(router method names, monorepo layout, React/Zod signals).
For each flagged item, apply the judgment, then if it is slop use only two operations: delete and inline. Never introduce new indirection; the touched file's line count must not increase.
di-factory: import the injected dependency directly (it is a static import everywhere else),
drop the parameter, and turn the returned product into a module-level const/export.alias: delete it; point its single caller at the real function.reshape: move the injected constant fields to the call site (or let the upstream function
default them) and delete the wrapper.After each change run the project's type-check and lint (e.g. tsc --noEmit, the repo linter).
Both must pass. Then re-run the detector — a flattened candidate disappears from the list. Repeat
until the flagged list contains only items you have judged "keep". A closed list means done.
To run this autonomously across a whole codebase, feed prompts/goal.md to a goal/loop runner
(Codex --goal, Claude Code, etc.). It instructs the model to run the detector, triage each
candidate against the judgment, inline the real slop, verify, and re-run until the list is clean —
with the script as the ground-truth feedback signal on every iteration.
npx claudepluginhub miguelspizza/skills --plugin skillsCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.