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.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub miguelspizza/skills --plugin skills