Code Slop Detection Taste-level review of code for AI-generated patterns. Contains 24 rules across 6 categories covering comments, naming, over-engineering, defensive overdose, test slop, and style fingerprints. Where measures quantitative code debt (complexity, duplication, CVEs), this skill measures the qualitative failure mode: code that passes every metric but reads like a tutorial blog post, not like a human wrote it. Metadata - Version: 1.0.0 - Scope: PHP / Laravel + TypeScript / React (Node) - Rule Count: 24 rules across 6 categories - License: MIT Why this skill exists Industry data o…

\"$f\")\n if [ \"$PUBLIC\" = \"1\" ] && [ \"$CTOR\" = \"0\" ] && [ \"$PROPS\" = \"0\" ]; then\n echo \"SUSPECT (single-method, no state): $f\"\n fi\ndone\n\n# TS — classes with one method and no constructor or fields\ngrep -rln 'class\\s\\+[A-Z]' --include='*.ts' src/ | while read f; do\n METHODS=$(grep -cE '^\\s+(public\\s+|private\\s+|protected\\s+)?[a-zA-Z]+\\s*\\(' \"$f\")\n CTOR=$(grep -c 'constructor' \"$f\")\n if [ \"$METHODS\" = \"1\" ] && [ \"$CTOR\" = \"0\" ]; then\n echo \"SUSPECT: $f\"\n fi\ndone\n```\n\nReference: Internal: [`naming-suffix-abuse`](naming-suffix-abuse.md), [`over-eng-useless-wrapper`](over-eng-useless-wrapper.md)\n\n---\n\n\n## Useless Wrapper Functions\n\n**Impact: HIGH (Adds indirection layer for no behavioural gain; callers chase one extra hop to find real code)**\n\nA function that exists only to delegate to another function, with no added validation, transformation, or context. AI generates these because it pattern-matches on \"wrap external dependencies for testability\" but applies it indiscriminately.\n\nA wrapper earns its place when it adds **something** the underlying call doesn't: validation, default arguments, error normalisation, logging, retry logic, type narrowing. A wrapper that just forwards arguments is pure indirection.\n\n## Incorrect\n\n```php\n// ❌ Wrappers that just delegate\n\nclass UserService\n{\n public function __construct(private UserRepository $repo) {}\n\n public function findUser(int $id): ?User\n {\n return $this->repo->find($id);\n }\n\n public function deleteUser(int $id): void\n {\n $this->repo->delete($id);\n }\n\n public function createUser(array $data): User\n {\n return $this->repo->create($data);\n }\n}\n```\n\n```typescript\n// ❌ Same TS pattern\nclass StripeWrapper {\n private stripe: Stripe;\n constructor(stripe: Stripe) { this.stripe = stripe; }\n\n charge(amount: number, token: string) {\n return this.stripe.charges.create({ amount, source: token });\n }\n\n refund(chargeId: string) {\n return this.stripe.refunds.create({ charge: chargeId });\n }\n}\n```\n\n**Why it's slop:**\n- `UserService::findUser($id)` does exactly what `$repo->find($id)` does\n- Every caller now goes through TWO layers (`Service → Repo`) to find the actual logic\n- Adding a parameter requires touching both files\n- Tests of `UserService` mostly verify \"did we call the repo correctly?\" — they don't verify business behaviour\n\n## Correct\n\n```php\n// ✅ Drop the wrapper; use the underlying type directly\n\n// Controller depends on the Repository or Model directly\npublic function show(int $id, UserRepository $users): UserResource\n{\n return new UserResource($users->findOrFail($id));\n}\n```\n\n## When a wrapper IS worth keeping\n\nA wrapper earns its place when it adds something real:\n\n```php\n// ✅ Wrapper that normalises errors + adds retry — real value\nfinal class ResilientStripeGateway\n{\n public function __construct(private Stripe $stripe) {}\n\n public function charge(Money $amount, string $token): Charge\n {\n return retry(times: 3, sleepMs: 200, callback: function () use ($amount, $token) {\n try {\n return $this->stripe->charges->create([\n 'amount' => $amount->cents(),\n 'currency' => $amount->currency()->code(),\n 'source' => $token,\n ]);\n } catch (CardException $e) {\n throw new PaymentDeclined($e->getMessage(), $e); // domain exception\n }\n });\n }\n}\n```\n\nThis wrapper adds:\n- Retry on transient failures\n- Domain-specific exception (`PaymentDeclined`, not `CardException`)\n- Type-safe `Money` input rather than raw cents + currency strings\n\nThat's worth the file.\n\n**Why it reads human:**\n- Wrappers exist when they add value; non-wrappers don't pad the layer count\n- Caller can see \"this charge call is resilient\" because the type is `ResilientStripeGateway`\n- No mystery hops to chase\n\n## Detection\n\n```bash\n# Heuristic: methods that are one-liners and just call $this->dependency->method($args)\n# PHP — find public methods whose body is exactly one delegate call\ngrep -rEn -A1 '^\\s+public function [a-zA-Z]+\\([^)]*\\)' --include='*.php' app/ | \\\n awk '/public function/{name=$0; next} /^\\s+return \\$this->[a-zA-Z]+->[a-zA-Z]+\\(/{print name; print $0; print \"---\"}'\n\n# TS — same heuristic\ngrep -rEn -A1 '^\\s+(public |async )?[a-zA-Z]+\\([^)]*\\):' --include='*.ts' src/ | \\\n awk '/[a-zA-Z]+\\(/{name=$0; next} /return this\\.[a-zA-Z]+\\./{print name; print $0; print \"---\"}'\n```\n\nThe cleanest signal: **call-sites**. If a wrapper is called from exactly one place, it's almost certainly slop. Inline it.\n\n```bash\n# For each public method in Service.php, count call sites\n# (rough — use phpstorm 'Find Usages' for accurate results)\n```\n\nReference: [Sandi Metz — The Wrong Abstraction](https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction) · [Martin Fowler — Inline Function](https://refactoring.com/catalog/inlineFunction.html) · Internal: [`over-eng-single-method-class`](over-eng-single-method-class.md), [`over-eng-premature-interface`](over-eng-premature-interface.md)\n\n---\n\n\n## Dependency Creep — New Library When Existing One Suffices\n\n**Impact: HIGH (Inflates bundle/install size, adds CVE surface, signals model picked training-data favourites)**\n\nAI tends to introduce a new dependency whenever a problem matches a library it has seen in training, even when the project already includes a dependency that solves the same problem. Example signals:\n\n- Adding `date-fns` to a project that already uses `dayjs`\n- Adding `axios` to a project that already imports `fetch` everywhere\n- Adding `lodash` to a project that already has `lodash-es` (or uses native ES methods)\n- Adding `uuid` when the project already uses `nanoid`\n- Adding `winston` when the app uses `pino`\n- Adding `joi` / `yup` to a TS project that already uses `zod`\n- Adding `bcrypt` (the npm package) in a Laravel project where `Hash::make` handles it\n- Adding `moment` in a project that just removed `moment` last quarter\n\nEach new dependency:\n- Adds to install / bundle size\n- Adds a new CVE surface (every `npm audit` lights up)\n- Forces the team to maintain TWO libraries for the same concern, indefinitely\n- Drifts as the LLM picks whichever was popular in the training-data slice it sampled from\n\n## Incorrect\n\n```json\n// ❌ package.json — two libs doing the same job\n{\n \"dependencies\": {\n \"dayjs\": \"^1.11.10\", // already in use\n \"date-fns\": \"^3.0.0\", // ADDED — same purpose\n \"axios\": \"^1.6.0\", // ADDED — but app uses fetch everywhere\n \"lodash\": \"^4.17.21\", // ADDED — lodash-es already present\n \"lodash-es\": \"^4.17.21\"\n }\n}\n```\n\n```php\n// ❌ composer.json — two HTTP clients\n{\n \"require\": {\n \"guzzlehttp/guzzle\": \"^7.8\",\n \"symfony/http-client\": \"^7.0\" // ADDED — same purpose\n }\n}\n```\n\n**Why it's slop:**\n- Both libraries get pulled into every install\n- New `dayjs` code still gets written alongside the new `date-fns` code — drift forever\n- Two CVE feeds to track\n- Reviewer didn't notice because each PR seems reasonable in isolation\n\n## Correct\n\n```bash\n# ✅ Use the existing dep; reject the PR adding the new one with a note\n\n# Before merging a PR that adds a new dependency, ask:\n# 1. Does an existing dependency in package.json/composer.json already do this?\n# 2. Does a native browser/Node/PHP API already do this?\n# 3. Is the new dep > 50KB gzipped (or > 1MB unpacked) for a single function call?\n# 4. Does it have CVEs / abandoned upstream?\n#\n# If any answer is \"yes\" or \"maybe\", reject and use the existing tool.\n```\n\nCommon collisions and the canonical choice:\n\n| Concern | Pick one |\n|---|---|\n| Date / time | `dayjs` OR `date-fns` (not both) |\n| HTTP client | `fetch` OR `axios` (not both) |\n| UUID | `uuid` OR `nanoid` (not both) |\n| Schema validation (TS) | `zod` (preferred) — reject `joi`/`yup`/`ajv` additions |\n| HTTP client (PHP) | `guzzlehttp/guzzle` (Laravel default) — reject `symfony/http-client` unless project-wide |\n| Logging | `monolog` (Laravel default) for PHP; pick one of `pino`/`winston` for Node |\n| Form validation (Laravel) | Built-in `FormRequest` — reject standalone validation libs |\n\n## Detection\n\n```bash\n# Node: dependency-list audit\nnode -e \"\nconst pkg = require('./package.json');\nconst all = { ...pkg.dependencies, ...pkg.devDependencies };\nconst dupes = [\n ['dayjs', 'date-fns', 'moment', 'luxon'],\n ['axios', 'node-fetch', 'got', 'superagent'],\n ['uuid', 'nanoid', 'cuid'],\n ['lodash', 'lodash-es', 'ramda'],\n ['joi', 'yup', 'zod', 'ajv'],\n];\nfor (const group of dupes) {\n const found = group.filter(p => all[p]);\n if (found.length > 1) console.log('OVERLAP:', found.join(', '));\n}\n\"\n\n# PHP: check for HTTP-client overlap\ngrep -E '\\\"(guzzlehttp/guzzle|symfony/http-client|kriswallsmith/buzz)\\\"' composer.json\n```\n\nWhen a new dep lands in a PR, the PR author should justify why the existing options don't fit. \"AI suggested this\" is not a justification.\n\nReference: [`technical-debt`'s `deps-unused-deps`](../technical-debt/rules/deps-unused-deps.md) (the broader audit) · [BundlePhobia](https://bundlephobia.com/) (size impact for npm packages)\n\n---\n\n\n## Generic catch Blocks That Don't Distinguish Errors\n\n**Impact: CRITICAL (82% of AI PRs per OX Security study; masks bugs as 'handled', breaks observability, kills debuggability)**\n\nThe single most-cited AI anti-pattern in 2025 research. Try/catch wrapped around code, with a generic catch that logs and swallows. Looks responsible. Is the opposite — it hides bugs and converts loud failures into silent corruption.\n\nReal error handling distinguishes:\n- **What can throw** (specific exception types)\n- **What's recoverable** (catch, retry, fall back)\n- **What's a bug** (re-throw, let it propagate)\n\nA bare `catch (e) { console.error(e); }` does none of this. It says \"if anything goes wrong, I'll keep going\" — and \"anything\" includes programmer errors that should crash so they're noticed.\n\n## Incorrect\n\n```php\n// ❌ Generic catch that swallows everything\n\npublic function processPayment(Order $order): bool\n{\n try {\n $charge = $this->stripe->charges->create([\n 'amount' => $order->total->cents(),\n 'currency' => 'usd',\n 'source' => $order->paymentToken,\n ]);\n $order->update(['stripe_charge_id' => $charge->id, 'status' => 'paid']);\n return true;\n } catch (Exception $e) {\n Log::error('Payment failed: ' . $e->getMessage());\n return false;\n }\n}\n```\n\n```typescript\n// ❌ Same TS pattern\nasync function exportUsers(): Promise\u003cUser[]> {\n try {\n const users = await api.fetchAllUsers();\n return users;\n } catch (e) {\n console.error('Export failed:', e);\n return [];\n }\n}\n```\n\n**Why it's slop:**\n- `Exception $e` catches *everything*, including `TypeError` (programmer bug) and `OutOfMemoryError` (system crash) — those should not be \"handled\" by logging and continuing\n- Returning `false` / `[]` lets the caller think the operation succeeded with no data, masking a real failure\n- `Log::error` lacks context — what was the order? what user? what amount? Future debugger has no anchor\n- A real Stripe error (card declined) gets the same treatment as a typo in the code\n\n## Correct\n\n```php\n// ✅ Catch specifically; re-throw what you don't understand\n\npublic function processPayment(Order $order): void\n{\n try {\n $charge = $this->stripe->charges->create([\n 'amount' => $order->total->cents(),\n 'currency' => 'usd',\n 'source' => $order->paymentToken,\n ]);\n $order->update(['stripe_charge_id' => $charge->id, 'status' => 'paid']);\n } catch (CardException $e) {\n // Customer-facing failure — known, expected\n $order->update(['status' => 'declined', 'decline_reason' => $e->getStripeCode()]);\n throw new PaymentDeclined($e->getStripeCode(), previous: $e);\n } catch (RateLimitException $e) {\n // Transient — caller retries\n throw new TransientPaymentFailure(retryAfterSeconds: 30, previous: $e);\n }\n // Any other exception propagates: TypeError, OutOfMemoryError, etc.\n // — those are bugs we WANT to know about.\n}\n```\n\n```typescript\n// ✅ Specific errors, propagate the unknown\nasync function exportUsers(): Promise\u003cUser[]> {\n try {\n return await api.fetchAllUsers();\n } catch (e) {\n if (e instanceof AbortError) {\n // Caller aborted; that's a feature, not a failure\n throw e;\n }\n if (e instanceof RateLimitError) {\n await sleep(e.retryAfterMs);\n return api.fetchAllUsers(); // one retry\n }\n // Network / parse / bug — propagate; don't return [] (the caller would think it succeeded with no users)\n throw e;\n }\n}\n```\n\n**Why it reads human:**\n- Each `catch` branch handles a specific, named condition with a specific remediation\n- Unknown errors propagate — the system fails loud, observability catches them, on-call wakes up before customers do\n- No silent \"return false\" / \"return empty array\" — caller can't accidentally treat failure as empty success\n\n## The \"what would I want at 2am?\" test\n\nWhen debugging a production incident, you want:\n- **Errors at the right loudness** — bugs crash with stack traces; expected failures have domain-specific exceptions you can grep\n- **Context, not just the message** — order ID, user ID, request ID, all in the log line\n- **No silent corruption** — a \"successful\" call that returned no data is the worst kind of failure\n\nThe generic-catch pattern fails all three.\n\n## Detection\n\n```bash\n# Bare catches in PHP\ngrep -rEn 'catch\\s*\\(\\s*\\\\?Exception\\b' --include='*.php' app/\n\n# Bare catches in TS / JS\ngrep -rEn '\\}\\s*catch\\s*\\(\\s*[a-zA-Z_]+\\s*\\)\\s*\\{' --include='*.ts' --include='*.tsx' --include='*.js' src/\n\n# Catch blocks that only log and continue\ngrep -rEnB1 'console\\.error\\(.*\\)\\s*;\\s*

Code Slop Detection Taste-level review of code for AI-generated patterns. Contains 24 rules across 6 categories covering comments, naming, over-engineering, defensive overdose, test slop, and style fingerprints. Where measures quantitative code debt (complexity, duplication, CVEs), this skill measures the qualitative failure mode: code that passes every metric but reads like a tutorial blog post, not like a human wrote it. Metadata - Version: 1.0.0 - Scope: PHP / Laravel + TypeScript / React (Node) - Rule Count: 24 rules across 6 categories - License: MIT Why this skill exists Industry data o…

--include='*.ts' --include='*.tsx' src/\n\n# PHP equivalent\ngrep -rEnB1 'Log::(error|warning)\\(.*\\)' --include='*.php' app/Http/ app/Services/\n\n# Linter rules\n# ESLint: no-empty (already standard); add @typescript-eslint/no-explicit-any\n# PHPStan level >= 8 flags catch(\\Throwable) in some configurations\n```\n\nA small handful of generic catches is fine (e.g., at HTTP boundaries with structured logging). **Density** is the signal — > 5 generic catches in a single service is the AI fingerprint.\n\nReference: Internal: [`defensive-impossible-null`](defensive-impossible-null.md), [`defensive-missing-real`](defensive-missing-real.md)\n\n---\n\n\n## Null Checks for Impossible Nulls\n\n**Impact: HIGH (Defensive code in places that can't fail; clutters the path and signals model didn't trust the type system)**\n\nWhen the type system or surrounding code already guarantees a value is non-null, a defensive null check adds noise and tells the reader \"the author wasn't sure\". Common cases:\n\n- A non-nullable parameter (typed `User $user`, not `?User $user`) — can't be null\n- After `Model::findOrFail()` — never returns null (throws on miss)\n- After a non-null assertion in the previous line\n- Inside a `forEach` / `map` loop where the iterable can't contain null\n\nThe pattern signals the model defaulted to \"check everything for safety\" without reading the types around it.\n\n## Incorrect\n\n```php\n// ❌ Null check after findOrFail — findOrFail throws if not found\n\npublic function show(int $id): View\n{\n $user = User::findOrFail($id);\n\n if ($user === null) {\n abort(404);\n }\n\n return view('users.show', compact('user'));\n}\n\n// ❌ Null check on a non-nullable parameter\n\npublic function notify(User $user, string $message): void // $user is required\n{\n if ($user === null) {\n return;\n }\n Mail::to($user)->send(new GenericNotification($message));\n}\n\n// ❌ Null check repeated in same function\npublic function process(Order $order): void\n{\n if ($order === null) return;\n $items = $order->items;\n if ($order === null) return; // already checked above; flow can't make it null\n foreach ($items as $item) {\n if ($order === null) return; // still impossible\n // ...\n }\n}\n```\n\n```typescript\n// ❌ TS: defensive checks the compiler already enforced\n\nfunction greet(user: User): string { // `user: User` — non-null in type system\n if (!user) return ''; // dead branch; TS would error\n return `Hi, ${user.name}`;\n}\n\n// ❌ Defensive after a guard\nfunction process(user: User | null): void {\n if (!user) return; // narrows to User\n if (user) { // always true after the guard\n doStuff(user);\n }\n}\n\n// ❌ Optional chaining on a definitely-defined value\nconst userId = currentUser.id; // currentUser is non-nullable here\nconsole.log(currentUser?.email); // ?. is unnecessary\n```\n\n**Why it's slop:**\n- The dead branch is dead — never executes; reviewers must mentally rule it out\n- Signals the author didn't trust the type system\n- `findOrFail` is well-known Laravel API; checking its result for null says \"I read the docs but don't believe them\"\n- Stacking redundant guards in one function (the third example) is unmistakable\n\n## Correct\n\n```php\n// ✅ Trust findOrFail — it throws, doesn't return null\n\npublic function show(int $id): View\n{\n return view('users.show', [\n 'user' => User::findOrFail($id),\n ]);\n}\n\n// ✅ Non-nullable parameter — no check needed\npublic function notify(User $user, string $message): void\n{\n Mail::to($user)->send(new GenericNotification($message));\n}\n```\n\n```typescript\n// ✅ Trust the types\nfunction greet(user: User): string {\n return `Hi, ${user.name}`;\n}\n\n// ✅ Single guard; TS narrows the type below\nfunction process(user: User | null): void {\n if (!user) return;\n doStuff(user); // TS knows user is User here\n}\n\n// ✅ No optional chaining when not optional\nconsole.log(currentUser.email);\n```\n\n**Why it reads human:**\n- Each line carries weight; no dead branches\n- Type system does the work, not runtime checks\n- Reader can scan past the function without ruling out impossible paths\n\n## When null checks ARE warranted\n\n- **At trust boundaries** — input from HTTP/queue/file, before the type narrows\n- **On nullable returns** from third-party libraries (e.g., `Eloquent::find()` returns `?Model`)\n- **In `try {} catch` flows** where the value might be partially constructed\n- **When the type really IS nullable** — `?User $user` parameters that the caller can leave empty\n\nThe test: **does the type say it could be null?** If yes, check. If no, trust.\n\n## Detection\n\n```bash\n# Null checks immediately after findOrFail in PHP — a recognisable AI signature\n# (Two-pass: find files with findOrFail, then look for null checks inside them.)\ngrep -rl 'findOrFail' --include='*.php' app/ | xargs grep -EnB1 'if\\s*\\(\\s*\\$[a-zA-Z_]+\\s*===?\\s*null\\s*\\)' 2>/dev/null\n\n# TS: optional chaining on values typed as non-null\n# (best caught via TS strict mode: noUncheckedIndexedAccess + strictNullChecks)\nnpx tsc --noEmit --strict\n\n# PHPStan level 8+ catches many \"always-false condition\" cases\nvendor/bin/phpstan analyse --level=9\n```\n\n**PHPStan level 9+ (level 10 is max in PHPStan 2.0) and TypeScript strict mode are your best mechanical defenses** — both will flag \"condition is always false\" or \"value is never null\". Adopt them and most of these impossible-null checks disappear from new code automatically.\n\nReference: [PHPStan rule levels](https://phpstan.org/user-guide/rule-levels) · [TS strict mode](https://www.typescriptlang.org/tsconfig#strict) · Internal: [`defensive-generic-catch`](defensive-generic-catch.md), [`defensive-missing-real`](defensive-missing-real.md)\n\n---\n\n\n## Defensive in the Wrong Places — Missing the Real Defenses\n\n**Impact: CRITICAL (Code looks safe but the actual failure modes — network, queues, races — are unprotected)**\n\nOX Security 2025: **76% of AI-assisted PRs miss timeouts on external calls**. The same PRs are full of impossible null checks and generic try/catch. The pattern is: defensive in places that don't need it; not defensive in places that do.\n\nWhat AI often misses (the failure modes that actually take down production):\n\n- **No timeouts on outbound HTTP** — a slow third-party API hangs your request indefinitely\n- **No retry policy + jitter** — a transient blip cascades into a customer-visible failure\n- **No idempotency keys on payment/order creation** — a network retry creates duplicate charges\n- **No rate-limit checks before hitting an external API** — banned by Stripe / Shopify / etc.\n- **No circuit breaker** — keeps hammering a known-down dependency, queue backs up\n- **No locking / unique constraint** — race condition double-spends inventory\n- **No backpressure on workers** — queue grows unbounded; OOM\n\nThese are the defenses that matter. A robust system has these; the dramatic-looking try/catch and null checks don't replace them.\n\n## Incorrect\n\n```php\n// ❌ Defensive theatre: try/catch wraps the wrong thing; real risks unprotected\n\npublic function syncOrder(string $orderId): void\n{\n try {\n // No timeout — could hang for minutes\n $response = Http::get(\"https://api.shopify.com/orders/{$orderId}\");\n $data = $response->json();\n\n if ($data === null) { // impossible — Http returns array or throws\n Log::error('Sync failed');\n return;\n }\n\n // No idempotency — if this retries, we create duplicates\n Order::create($data);\n } catch (Exception $e) {\n Log::error('Sync failed: ' . $e->getMessage());\n }\n}\n```\n\n```typescript\n// ❌ Same pattern in TS\nasync function syncOrder(orderId: string): Promise\u003cvoid> {\n try {\n // No timeout; no retry; no backoff\n const res = await fetch(`https://api.shopify.com/orders/${orderId}`);\n const data = await res.json();\n\n if (!data) { // impossible if res.json() resolved\n console.error('Sync failed');\n return;\n }\n\n await db.orders.create({ data });\n } catch (e) {\n console.error(e); // swallow\n }\n}\n```\n\n**Why it's slop:**\n- The `if ($data === null)` is dead defence; the actual failure mode is \"Shopify takes 90s to respond and our request timeout hits us first\"\n- The try/catch swallows real failures but doesn't add the things that prevent them\n- Retried calls create duplicate orders (idempotency missing)\n- Looks responsible; isn't\n\n## Correct\n\n```php\n// ✅ The real defences — timeouts, idempotency, retry with backoff, circuit-break\n\npublic function syncOrder(string $orderId): void\n{\n $response = Http::timeout(10) // hard timeout\n ->retry(times: 3, sleepMilliseconds: 200, when: fn ($e) =>\n $e instanceof ConnectionException || $e->getCode() === 429)\n ->get(\"https://api.shopify.com/orders/{$orderId}\");\n\n $response->throw(); // throw on 4xx/5xx — let it propagate\n\n Order::updateOrCreate(\n ['shopify_id' => $orderId], // idempotency via unique key\n $response->json()\n );\n}\n```\n\n```typescript\n// ✅ TS with AbortController for timeout + retry policy\nasync function syncOrder(orderId: string): Promise\u003cvoid> {\n const data = await fetchWithRetry(\n `https://api.shopify.com/orders/${orderId}`,\n { timeoutMs: 10_000, retries: 3, retryOnStatus: [429, 502, 503, 504] },\n );\n\n // Idempotent upsert by external id (unique index on shopify_id)\n await db.orders.upsert({\n where: { shopify_id: orderId },\n create: data,\n update: data,\n });\n}\n```\n\n**Why it reads human:**\n- The actual failure modes (slow third party, transient errors, duplicate retries) are each addressed\n- Errors NOT covered by retry propagate — observability/alerts catch them\n- The idempotency key (`shopify_id`) prevents duplicate orders even under retry storms\n- The \"try/catch\" is gone — it added no value here\n\n## The real-defences checklist\n\nFor any code that talks to the outside world, ask:\n\n- [ ] **Timeout** on every outbound call (HTTP, DB, queue, cache)?\n- [ ] **Retry** policy: how many, with what backoff, on which error types?\n- [ ] **Idempotency**: if the call is retried, will it create duplicate side effects?\n- [ ] **Rate limit** awareness: am I tracking my own request rate or relying on the upstream's error?\n- [ ] **Circuit breaker** for known-down dependencies (or at least a backoff cap)?\n- [ ] **Locking / unique constraints** on writes that could race?\n- [ ] **Backpressure** on workers reading from a queue?\n- [ ] **Real errors propagated** to observability instead of swallowed?\n\nA try/catch that doesn't add any of these isn't a defense. It's theatre.\n\n## Detection\n\n```bash\n# HTTP calls without explicit timeout (Laravel Http facade)\ngrep -rEn 'Http::get\\(|Http::post\\(|Http::put\\(|Http::delete\\(' --include='*.php' app/ \\\n | grep -v 'timeout('\n\n# fetch() calls without AbortController / signal\ngrep -rEn 'await fetch\\(' --include='*.ts' --include='*.tsx' --include='*.js' src/ \\\n | grep -vE 'signal:|AbortController'\n\n# create() / insert() without idempotency check (rough — manual review)\ngrep -rEn '(Order|Charge|Payment)::create\\(' --include='*.php' app/\n```\n\nA repo can pass every other rule in this skill and still ship the wrong defenses. **This rule is the one to take seriously on payment, checkout, and integration code paths.**\n\nReference: [Stripe — Idempotent Requests](https://docs.stripe.com/api/idempotent_requests) · [Hystrix / Resilience4j circuit breaker patterns](https://github.com/resilience4j/resilience4j) · OX Security study · Internal: [`defensive-generic-catch`](defensive-generic-catch.md)\n\n---\n\n\n## Mock-Everything Tests That Assert Nothing\n\n**Impact: CRITICAL (Tests that pass forever; they re-encode the implementation rather than verify behaviour)**\n\nWhen AI generates tests, the most common failure mode is \"mock every dependency, then assert that the mocks were called\". The test passes the moment it's written and passes forever — including when the underlying behaviour is silently broken. arXiv 2602.00409 (2026): coding agents produce significantly more over-mocked tests than human authors.\n\nA real test verifies **outcome**, not **interaction**. Mocking a database to assert \"create() was called with X\" verifies that you wrote the implementation that calls `create()`. It doesn't verify that the customer record actually lands in the table.\n\n## Incorrect\n\n```typescript\n// ❌ Mock-everything test that asserts mock interactions\n\nimport { describe, it, expect, vi } from 'vitest';\nimport { UserService } from './UserService';\n\ndescribe('UserService', () => {\n it('createUser saves the user', async () => {\n const mockRepo = {\n save: vi.fn().mockResolvedValue({ id: 1, email: '[email protected]' }),\n findByEmail: vi.fn().mockResolvedValue(null),\n };\n const mockMailer = { send: vi.fn() };\n const mockHasher = { hash: vi.fn().mockReturnValue('hashed') };\n\n const service = new UserService(mockRepo, mockMailer, mockHasher);\n await service.createUser({ email: '[email protected]', password: 'x' });\n\n expect(mockRepo.save).toHaveBeenCalled(); // weak — passes if .save() called with anything\n expect(mockMailer.send).toHaveBeenCalled();\n expect(mockHasher.hash).toHaveBeenCalledWith('x'); // verifies you wrote .hash() — not that it's secure\n });\n});\n```\n\n```php\n// ❌ Same pattern in PHPUnit / Pest\ntest('processOrder calls payment gateway', function () {\n $payments = $this->mock(PaymentGateway::class);\n $payments->shouldReceive('charge')->once(); // verifies an interaction, not an outcome\n\n $service = new OrderService($payments);\n $service->process(new Order(/* ... */));\n});\n```\n\n**Why it's slop:**\n- The test passes even if `save()` stores the wrong fields, or `hash()` returns the input unchanged, or `charge()` skips actual payment\n- It locks in the implementation's structure (every refactor breaks the test even when behaviour is fine)\n- \"Was the mock called\" is rarely a useful assertion — what matters is \"did the outcome happen\"\n- The test gives false confidence in coverage reports\n\n## Correct — verify the outcome, with real dependencies where possible\n\n```typescript\n// ✅ Integration test against a real (in-memory) DB; assert what changed\nimport { describe, it, expect, beforeEach } from 'vitest';\nimport { createTestDb } from './testUtils';\nimport { UserService } from './UserService';\n\ndescribe('UserService', () => {\n let db;\n beforeEach(async () => { db = await createTestDb(); });\n\n it('createUser creates a user row with hashed password and sends welcome email', async () => {\n const mailer = makeFakeMailer(); // collect-and-inspect, not a mock-with-asserts\n const service = new UserService(db.users, mailer);\n\n await service.createUser({ email: '[email protected]', password: 'plaintext' });\n\n const stored = await db.users.findByEmail('[email protected]');\n expect(stored).toBeDefined();\n expect(stored!.email).toBe('[email protected]');\n expect(stored!.passwordHash).not.toBe('plaintext'); // hash actually happened\n expect(bcrypt.compareSync('plaintext', stored!.passwordHash)).toBe(true);\n expect(mailer.sentTo('[email protected]')).toHaveLength(1); // outcome, not interaction\n });\n});\n```\n\n```php\n// ✅ Laravel: use the real database (RefreshDatabase) + fake the boundary services\ntest('processOrder charges customer and marks order paid', function () {\n Mail::fake();\n Http::fake([\n 'api.stripe.com/*' => Http::response(['id' => 'ch_test_123', 'status' => 'succeeded'], 200),\n ]);\n\n $order = Order::factory()->create(['status' => 'pending', 'total' => 100_00]);\n\n (new OrderService(new StripeGateway()))->process($order);\n\n expect($order->fresh()->status)->toBe('paid');\n expect($order->fresh()->stripe_charge_id)->toBe('ch_test_123');\n Mail::assertSent(OrderConfirmation::class, fn ($m) => $m->order->is($order));\n});\n```\n\n**Why it reads human:**\n- Each assertion checks **what should be true after the operation** — not \"did you call X\"\n- Refactoring the internal calls is safe — the test still passes if the outcomes hold\n- Real bugs (forgotten hash, wrong field assignment, missing email) trigger the test failures\n- The test reads like a customer story: \"after createUser, the user exists, password is hashed, welcome email is sent\"\n\n## When mocks ARE appropriate\n\nA few legitimate uses:\n\n- **Cost / side-effect boundaries** — real Stripe calls (use `Http::fake()` in Laravel; mock the HTTP boundary)\n- **Slow externals** — third-party APIs with rate limits (mock the HTTP boundary)\n- **Time-sensitive code** — freeze the clock (Carbon test helpers, vi.setSystemTime), don't mock all of time\n- **Hard-to-reproduce error paths** — force a `ConnectionException` to test the retry handler\n\nThe pattern: **mock at the network/IO boundary, not at every internal class.**\n\n## Detection\n\n```bash\n# Tests that import the mock library more than the assertion library (rough)\ngrep -rEn '(vi\\.fn|jest\\.fn|->mock\\(|->shouldReceive)' \\\n --include='*.test.ts' --include='*.spec.ts' --include='*Test.php' \\\n | wc -l\n\n# Tests asserting only on mock interactions, no DB / outcome checks\n# (heuristic: file has 'toHaveBeenCalled' / 'shouldReceive' but no 'expect(\u003crepo>.find' or 'assertDatabaseHas')\nfor f in $(find . -name '*Test.php' -o -name '*.test.ts' 2>/dev/null); do\n has_mock=$(grep -cE 'toHaveBeenCalled|shouldReceive' \"$f\")\n has_outcome=$(grep -cE 'assertDatabaseHas|->fresh\\(|findByEmail|toBe\\(|toEqual\\(' \"$f\")\n if [ \"$has_mock\" -gt 3 ] && [ \"$has_outcome\" -lt 1 ]; then\n echo \"MOCK-ONLY: $f\"\n fi\ndone\n```\n\nReference: [arXiv 2602.00409 — Are Coding Agents Generating Over-Mocked Tests?](https://arxiv.org/abs/2602.00409) · Internal: [`test-mirror-implementation`](test-mirror-implementation.md), [`test-doesnt-throw`](test-doesnt-throw.md)\n\n---\n\n\n## \"Doesn't Throw\" Tests\n\n**Impact: HIGH (Tests that assert nothing meaningful; pass even when the function does the wrong thing)**\n\nA test that calls the function and then asserts the function didn't throw is barely a test. It verifies one of the weakest possible properties — \"the program didn't crash\" — and gives false confidence in coverage. AI often defaults to this pattern because it's the easiest way to make a test pass.\n\nA real test verifies a specific outcome. \"Didn't throw\" is an outcome only in the narrowest cases (you specifically want to verify a thrown error from a previous bug is now absent).\n\n## Incorrect\n\n```typescript\n// ❌ Tests that just verify \"the call completes\"\n\ndescribe('OrderService', () => {\n it('places an order', async () => {\n const service = new OrderService();\n await expect(service.place(makeValidOrder())).resolves.not.toThrow();\n });\n\n it('exports users', async () => {\n const service = new UserExportService();\n const result = await service.export();\n expect(result).toBeDefined(); // weak: undefined is the only failure mode caught\n });\n\n it('handles empty input', () => {\n const result = sum([]);\n expect(result).not.toBeNull(); // passes for 0, NaN, '', false, anything except null/undefined\n });\n});\n```\n\n```php\n// ❌ Same pattern in PHPUnit / Pest\ntest('it places an order', function () {\n $service = new OrderService();\n\n expect(fn () => $service->place(makeValidOrder()))->not->toThrow();\n});\n\ntest('export returns something', function () {\n $result = (new UserExportService())->export();\n expect($result)->not->toBeNull();\n});\n```\n\n**Why it's slop:**\n- \"Doesn't throw\" tells you nothing about whether the right thing happened\n- `place(order)` could return without throwing AND not actually place the order\n- `export()` could return `undefined` (the test fails) OR `null` (test fails) OR an empty array, a misleading \"true\", etc. — the test catches only the narrowest failure\n- `not.toBeNull()` passes for `0`, `''`, `NaN`, `false` — all of which are usually bugs\n\n## Correct — assert what should happen\n\n```typescript\n// ✅ Specific outcomes\n\ndescribe('OrderService', () => {\n it('place(order) persists the order and returns the saved row with status=pending', async () => {\n const service = new OrderService(db);\n const order = makeValidOrder({ total: 100_00 });\n\n const saved = await service.place(order);\n\n expect(saved.id).toBeDefined();\n expect(saved.status).toBe('pending');\n expect(saved.total).toBe(100_00);\n expect(await db.orders.findById(saved.id)).toMatchObject({ status: 'pending' });\n });\n\n it('exports all active users to CSV', async () => {\n await db.users.insertMany([\n { email: '[email protected]', active: true },\n { email: '[email protected]', active: false },\n { email: '[email protected]', active: true },\n ]);\n\n const csv = await new UserExportService(db).export();\n\n expect(csv).toContain('[email protected]');\n expect(csv).toContain('[email protected]');\n expect(csv).not.toContain('[email protected]'); // inactive should be excluded\n });\n\n it('sum([]) returns 0', () => {\n expect(sum([])).toBe(0);\n });\n});\n```\n\n**Why it reads human:**\n- Each test verifies a specific, named outcome\n- Bugs are detected: changing `status: 'pending'` to `'paid'` would fail the test; returning all users (not just active) would fail; `sum([])` returning `NaN` would fail\n- Test names describe the *behaviour*, not the *function called*\n\n## When \"doesn't throw\" IS legitimate\n\nRarely:\n- **Smoke tests** for a never-throws contract on a public API (one or two tests; not the bulk of the suite)\n- **Regression tests** for a specific previously-thrown error: \"after fix #123, calling X with Y no longer throws\"\n\nIn these cases, name the test clearly: `it('does not throw on empty input — regression for #123')`.\n\n## Detection\n\n```bash\n# Tests whose only assertion is \"not.toThrow\" or \"not.toBeNull\"\ngrep -rEn '\\.not\\.toThrow\\(|\\.not\\.toBeNull\\(|\\.toBeDefined\\(' \\\n --include='*.test.ts' --include='*.spec.ts' --include='*.test.tsx' src/ tests/ \\\n | head -30\n\n# PHPUnit / Pest equivalents\ngrep -rEn '\\bexpect\\(.*\\)->not->toThrow\\(\\)|->assertNotNull\\(\\$result\\)' \\\n --include='*Test.php' --include='*.test.php' tests/\n\n# Heuristic: test files where the ratio of weak assertions to strong assertions is high\n# (manual review of files with > 5 weak assertions)\n```\n\nA few weak assertions are fine. A test suite dominated by them is a slop fingerprint.\n\nReference: [Martin Fowler — Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html) · Internal: [`test-mock-everything`](test-mock-everything.md), [`test-mirror-implementation`](test-mirror-implementation.md)\n\n---\n\n\n## Tests That Mirror the Implementation\n\n**Impact: HIGH (Tests re-encode the production code; pass because they re-implement the same logic, not because they verify behaviour)**\n\nAI generates tests by reading the function and translating its logic into the test. If `calculateTax` does `subtotal * 0.06`, the test sets `subtotal = 100`, then says \"expect result to be `100 * 0.06`\". The test passes because both sides compute the same thing. It would still pass if the function silently switched to `subtotal * 0.07` — IF the AI also \"updated\" the test to match.\n\nReal tests state the **expected concrete answer**, not a formula computed from the input. They verify what *should* be true, not \"the function does what the function does\".\n\n## Incorrect\n\n```typescript\n// ❌ The test re-implements the function\n\n// Production:\nexport function calculateTax(subtotal: number): number {\n return subtotal * 0.06;\n}\n\n// Test:\nimport { calculateTax } from './calculateTax';\n\ndescribe('calculateTax', () => {\n it('calculates tax', () => {\n const subtotal = 100;\n expect(calculateTax(subtotal)).toBe(subtotal * 0.06); // re-encodes the formula\n });\n\n it('handles zero', () => {\n expect(calculateTax(0)).toBe(0 * 0.06); // tautology\n });\n});\n```\n\n```php\n// ❌ Same pattern in PHP\npublic function test_it_calculates_tax(): void\n{\n $subtotal = 100;\n $expected = $subtotal * 0.06; // re-encoded\n $this->assertEquals($expected, calculateTax($subtotal));\n}\n```\n\n**Why it's slop:**\n- The test passes because RHS and LHS use the same formula — you've verified `x === x`\n- If the formula in production silently changes to `0.07`, but the test \"uses the formula\" (or AI updates the test to match), the test still passes — and the bug ships\n- Both sides of the equation come from the same place, so the test catches nothing the type system doesn't\n\n## Correct — concrete, named expected values\n\n```typescript\n// ✅ Concrete expectations\ndescribe('calculateTax', () => {\n it('charges 6% on the subtotal', () => {\n expect(calculateTax(100)).toBe(6);\n expect(calculateTax(50)).toBe(3);\n expect(calculateTax(1.50)).toBe(0.09);\n });\n\n it('returns 0 for a 0 subtotal', () => {\n expect(calculateTax(0)).toBe(0);\n });\n\n it('rejects negative subtotals', () => {\n expect(() => calculateTax(-10)).toThrow(InvalidSubtotal);\n });\n});\n```\n\n```php\n// ✅ Concrete expectations\npublic function test_charges_six_percent_on_subtotal(): void\n{\n $this->assertEquals(6.0, calculateTax(100));\n $this->assertEquals(3.0, calculateTax(50));\n $this->assertEquals(0.09, calculateTax(1.50));\n}\n```\n\n**Why it reads human:**\n- The expected value is *named* — `6` is what 6% of 100 is, written as the answer\n- If the production formula changes to `0.07`, the test fails at `expect(calculateTax(100)).toBe(6)` because `7 !== 6`\n- The bug is caught at the boundary; the test holds the spec\n\n## A particularly dangerous variant: bug-then-regenerate\n\n```\nUser: \"There's a bug — calculateTax is returning the wrong value for negatives.\"\nAI: \"I'll regenerate the test for you.\"\n```\n\nThe regenerated test happens to pass for the new buggy behaviour. The test now ratifies the bug. This pattern is documented in Larridin (2025) and is one of the most insidious failure modes.\n\n**Rule:** when fixing a bug, the test is written FIRST (red), with concrete expected values that reflect the correct behaviour. Then the production code is changed. Then the test goes green. Never accept \"regenerate the test to match the new implementation\".\n\n## Detection\n\nThis is hard to detect mechanically — the pattern is \"RHS includes the same constants/operations as the production code\". Useful heuristics:\n\n```bash\n# Tests where expected values use the same magic number as the source\n# (extract numeric constants from src/, then grep tests for them)\ngrep -rEoh '[0-9]+\\.[0-9]+|[0-9]+_[0-9]+' --include='*.ts' src/ | sort -u > /tmp/src-constants.txt\ngrep -rEoh '[0-9]+\\.[0-9]+|[0-9]+_[0-9]+' --include='*.test.ts' tests/ | sort -u > /tmp/test-constants.txt\ncomm -12 /tmp/src-constants.txt /tmp/test-constants.txt | head\n# If many magic numbers from src appear in tests, they may be re-encoding the implementation\n\n# Mutation testing is the gold-standard detection:\n# - Stryker (JS/TS): https://stryker-mutator.io/\n# - Infection (PHP): https://infection.github.io/\n# If a mutation in the source doesn't fail a test, the test is mirror-ware.\n```\n\n**Mutation testing is the right answer here.** Add Stryker or Infection to your CI and require a minimum mutation score on PRs touching the unit-test directory.\n\nReference: [Stryker Mutator](https://stryker-mutator.io/) · [Infection](https://infection.github.io/) · Internal: [`test-mock-everything`](test-mock-everything.md), [`test-doesnt-throw`](test-doesnt-throw.md)\n\n---\n\n\n## Snapshot Tests Replacing Behavioural Assertions\n\n**Impact: HIGH (Snapshots ratify whatever the function currently returns; engineers approve diffs without reading them)**\n\nA snapshot test calls the function, captures the output, and asserts \"matches the saved snapshot\". For genuinely stable, large, structural outputs (rendered React component DOM, generated SQL, large JSON shapes), snapshots are useful. For everything else — and especially when AI generates them — they're a way to make a test pass without writing a real assertion.\n\nThe failure mode is well-documented: a test fails on a real bug, the engineer runs `vitest -u` to \"update snapshots\", ships. The snapshot test now ratifies the bug. Repeat across a team and snapshots become noise the team has trained itself to ignore.\n\n## Incorrect\n\n```typescript\n// ❌ Snapshot tests instead of behavioural assertions\n\ndescribe('calculateRefund', () => {\n it('matches snapshot', () => {\n expect(calculateRefund(order)).toMatchSnapshot(); // what should the answer be?\n });\n});\n\ndescribe('formatPrice', () => {\n it('matches snapshot', () => {\n expect(formatPrice(1234.5)).toMatchSnapshot();\n });\n});\n```\n\n```typescript\n// ❌ Component snapshots that lock in entire DOM\ndescribe('OrderCard', () => {\n it('renders', () => {\n const { container } = render(\u003cOrderCard order={makeOrder()} />);\n expect(container).toMatchSnapshot(); // 800-line snapshot file\n });\n});\n```\n\n**Why it's slop:**\n- \"Matches snapshot\" doesn't say what the answer is — readers can't tell what the test verifies\n- When the snapshot fails (because the team made an intentional change), the engineer reflexively runs `-u` to update\n- Snapshot files grow huge; nobody reviews them in PRs\n- AI generates snapshot tests by default because it's the easiest way to \"test\" without writing an actual assertion\n\n## Correct — explicit behavioural assertions\n\n```typescript\n// ✅ Concrete expectations\n\ndescribe('calculateRefund', () => {\n it('refunds the full order minus the restocking fee', () => {\n const order = makeOrder({ total: 100_00, restockingFee: 5_00 });\n expect(calculateRefund(order)).toEqual({\n amount: 95_00,\n stripeChargeId: order.stripeChargeId,\n });\n });\n\n it('refunds zero if the order is past the return window', () => {\n const order = makeOrder({ returnsCloseAt: subDays(new Date(), 1) });\n expect(() => calculateRefund(order)).toThrow(RefundWindowClosed);\n });\n});\n\ndescribe('formatPrice', () => {\n it('formats USD with two decimal places and a leading

Code Slop Detection Taste-level review of code for AI-generated patterns. Contains 24 rules across 6 categories covering comments, naming, over-engineering, defensive overdose, test slop, and style fingerprints. Where measures quantitative code debt (complexity, duplication, CVEs), this skill measures the qualitative failure mode: code that passes every metric but reads like a tutorial blog post, not like a human wrote it. Metadata - Version: 1.0.0 - Scope: PHP / Laravel + TypeScript / React (Node) - Rule Count: 24 rules across 6 categories - License: MIT Why this skill exists Industry data o…

, () => {\n expect(formatPrice(1234.5)).toBe('$1,234.50');\n });\n});\n\ndescribe('OrderCard', () => {\n it('shows the order number, total, and status', () => {\n const order = makeOrder({ id: 'ord_123', total: 50_00, status: 'paid' });\n render(\u003cOrderCard order={order} />);\n expect(screen.getByText('Order #ord_123')).toBeInTheDocument();\n expect(screen.getByText('$50.00')).toBeInTheDocument();\n expect(screen.getByText('Paid')).toBeInTheDocument();\n });\n});\n```\n\n**Why it reads human:**\n- The test reads as the spec — \"refunds the full order minus the restocking fee\"\n- Concrete expected values; bug-then-regenerate doesn't silently work\n- Component tests assert *behaviour* (specific text appears) not *DOM shape* (every class name and attribute)\n\n## When snapshots ARE worth using\n\nGenuine cases for snapshot testing:\n\n- **Large generated artifacts** — SQL output from an ORM, generated migration files, OpenAPI specs\n- **Structural data** — public API JSON responses where any structural change should be reviewed\n- **Visual regression** — actual screenshot comparison (e.g., Playwright `toHaveScreenshot`)\n\nFor these:\n- Keep the snapshot small (don't snapshot the whole HTML tree if you can snapshot the relevant attribute)\n- Review the snapshot diff IN the PR (treat snapshot files as code)\n- Don't `-u` reflexively; understand the change\n\n## Detection\n\n```bash\n# Snapshot usage in the repo\ngrep -rEn 'toMatchSnapshot\\(|toMatchInlineSnapshot\\(' --include='*.test.ts' --include='*.spec.ts' --include='*.test.tsx' src/ tests/ | wc -l\n\n# Snapshot files\nfind . -name '__snapshots__' -type d 2>/dev/null\n\n# Heuristic: snapshot files larger than 200 lines = probably too big to review\nfind . -path '*/__snapshots__/*.snap' -type f -exec wc -l {} \\; 2>/dev/null \\\n | awk '$1 > 200 { print \"TOO LARGE: \" $2 \" (\" $1 \" lines)\" }'\n\n# Tests that use ONLY snapshot assertions, no toBe / toEqual\nfor f in $(find . -name '*.test.tsx' -o -name '*.test.ts' 2>/dev/null); do\n snap=$(grep -c 'toMatchSnapshot' \"$f\")\n real=$(grep -cE 'toBe\\(|toEqual\\(|toContain\\(|toBeInTheDocument' \"$f\")\n if [ \"$snap\" -gt 0 ] && [ \"$real\" = \"0\" ]; then\n echo \"SNAPSHOT-ONLY: $f\"\n fi\ndone\n```\n\nReference: [Kent C. Dodds — Effective Snapshot Testing](https://kentcdodds.com/blog/effective-snapshot-testing) · [Vitest snapshot docs](https://vitest.dev/guide/snapshot) · Internal: [`test-mock-everything`](test-mock-everything.md), [`test-mirror-implementation`](test-mirror-implementation.md)\n\n---\n\n\n## Hyper-Consistent Formatting\n\n**Impact: MEDIUM (A codebase where every file looks linter-perfect is suspiciously not-human)**\n\nReal codebases have geology — older files use older conventions, busy modules have rushed sections, 2am hotfix code has different spacing than carefully-reviewed weeks. AI-generated code is **uniformly polished**: every function the same length, every blank line in the same place, every import alphabetized, no styling drift anywhere. This isn't a hallmark of quality — it's a hallmark of generation.\n\nThe fingerprint is **uniformity at scale**. One file looking pristine is fine. A 2000-line PR where every file is identically pristine, across files that should have different velocity histories, is a strong AI signal.\n\n## What it looks like\n\nThe PR diff shows:\n\n- Every PHP file has exactly 4-space indent, exactly one blank line between methods, exactly two blank lines between class members, every parameter aligned identically\n- Every TS file has the same import-grouping pattern, named imports always alphabetised, every arrow function body wrapped identically\n- No `// HACK:` / `// XXX:` / late-night comments anywhere\n- No commented-out code (clean — but also no signs of someone exploring)\n- Identical commit-message structure across 50 commits\n- All files use the same paradigm (e.g., every function is an arrow function; every class uses constructor property promotion) even in places where the existing codebase mixed styles\n\n## Why this matters\n\nCode geology is a tool for the next developer:\n- **Different ages tell different stories** — \"this section is from 2022, before we switched to...\"\n- **`// HACK:` markers signal known fragility** that hasn't been worth refactoring\n- **Commented-out code reveals exploration** — \"we tried X, didn't work, kept the option in case\"\n- **Velocity differences** show which paths were rushed and might harbour bugs\n\nA repo with no geology forces every reader to start fresh — no inherited context, no \"the team didn't bother fixing this, it works fine\" signal. AI-generated codebases are flat: every line treated as equally important.\n\n## The \"all green field\" tell\n\nHyper-consistency is most suspicious in **brownfield additions** — i.e., when AI adds code to a codebase that has its own conventions. Real engineers either:\n\n- **Match the existing style** (consistent with the surrounding 50 files)\n- **Refactor the surrounding files** alongside their change (drift announces itself in the diff)\n- **Introduce a deliberate change** with an explanation (\"new files use the new pattern; old files updated as touched\")\n\nAI typically does the first poorly — it picks \"the most recent convention\" or \"the most popular convention from training data\" rather than matching the actual codebase. Result: the new files are linter-perfect by some standard, but inconsistent with the surrounding codebase.\n\n## What \"human formatting\" looks like\n\nYou won't catch this with a linter. It's a feel:\n\n- Some files have slightly off blank-line spacing where someone hit enter twice\n- Imports sometimes group \"stuff I added recently\" at the top\n- One file has 6-space indent inside a heredoc because the dev didn't fight prettier on it\n- A function has an extra blank line before a tricky if-branch where the author paused to think\n- A 2-line comment is in mid-sentence-case because someone typed it angry\n\nThese tiny artifacts are how humans write code. Their absence — especially across many files at once — is the slop.\n\n## Detection\n\nThere's no automatic detector. The signal is human-judgment:\n\n1. **Run the diff through `git diff --stat`** — many files changed at once is normal; many files all with the same line-count profile is suspicious\n2. **`git log --author` distribution** — if the diff is from one author but covers 30 files in one commit, raise an eyebrow\n3. **Compare new files to surrounding files** — does the indentation, import order, brace style match the existing convention?\n4. **Search for `// HACK:` / `// XXX:` markers** in the new files — their *absence* is the signal:\n ```bash\n git diff origin/main...HEAD -- '*.ts' '*.php' | grep -E '// HACK:|// XXX:|// FIXME' || echo \"NO HACK/XXX MARKERS\"\n ```\n5. **Mixed-paradigm check** — in a TS PR, count `function` declarations vs arrow functions; in a PHP PR, count `final class` vs `class`. A sudden swing is a tell.\n\n## What to do\n\nIf a PR looks hyper-consistent:\n\n- Ask the author: \"Was this AI-assisted? Walk me through the section in `OrderService` line 80.\" Their ability to explain the *intent*, not just the code, separates \"AI-written, human-reviewed\" from \"AI-written, accepted-as-is\".\n- Sample a few functions and ask: \"What's the trade-off you chose here?\" If they shrug, the code wasn't really written by them.\n\nThe goal isn't to ban AI assistance — it's to make sure the author owns the code they ship.\n\nReference: Internal: [`style-no-hack-scars`](style-no-hack-scars.md)\n\n---\n\n\n## as any / @ts-ignore Escape Hatches\n\n**Impact: HIGH (AI sprinkles these wherever inference is hard — defeats the type system in exactly the places it earns its value)**\n\nAI's common move when TypeScript types don't line up cleanly: cast to `any`, add `@ts-ignore`, or annotate with `: any`. The escape hatch silences the compiler — and silences the protection it was about to provide. Worst case: a real bug (wrong shape, missing field, wrong arg order) survives review because the compiler stopped warning.\n\nA reviewer should treat every `as any` / `@ts-ignore` / `@ts-expect-error` as **a request for justification**. Sometimes legitimate (third-party library with bad types, complex generics, runtime-validated unknowns). Often not.\n\n## Incorrect\n\n```typescript\n// ❌ as any sprinkled to silence the compiler\n\nasync function processWebhook(payload: unknown): Promise\u003cvoid> {\n const event = (payload as any).event; // unknown → any without validation\n const orderId = (payload as any).data.order.id; // chain of unsafe access\n await db.orders.update({ where: { id: orderId }, data: { status: 'paid' } });\n}\n\n// ❌ @ts-ignore over a real type mismatch\nfunction calculateTotal(items: Item[]): number {\n // @ts-ignore — items.map sometimes returns undefined? not sure\n return items.map(i => i.price * i.quantity).reduce((a, b) => a + b, 0);\n}\n\n// ❌ : any in function signature\nfunction processData(data: any) { // gives up on the input shape\n return data.value;\n}\n\n// ❌ Mixing ts-ignore with no comment\nfunction send(): void {\n // @ts-ignore\n emailService.send(undefined); // why is undefined OK here?\n}\n```\n\n**Why it's slop:**\n- `as any` propagates — every subsequent access is also unchecked\n- A real bug (e.g., the payload's actual shape is `payload.event.data.order_id`, not `payload.data.order.id`) doesn't fail until production\n- `@ts-ignore` with no explanation is one of the most-cited AI tells in 2025 reviews\n- `: any` parameters defeat every downstream type check\n\n## Correct\n\n```typescript\n// ✅ Validate at the boundary; types flow through downstream\n\nimport { z } from 'zod';\n\nconst WebhookPayload = z.object({\n event: z.string(),\n data: z.object({\n order: z.object({ id: z.string() }),\n }),\n});\n\nasync function processWebhook(payload: unknown): Promise\u003cvoid> {\n const { event, data } = WebhookPayload.parse(payload); // typed from here on\n await db.orders.update({ where: { id: data.order.id }, data: { status: 'paid' } });\n}\n\n// ✅ Fix the real type issue rather than ignore it\nfunction calculateTotal(items: Item[]): number {\n return items.reduce((sum, i) => sum + i.price * i.quantity, 0);\n}\n\n// ✅ Type the input\nfunction processData\u003cT extends { value: string }>(data: T): string {\n return data.value;\n}\n```\n\n## When the escape hatch IS warranted\n\nLegitimate use is narrow:\n\n- **Bad third-party types** — the lib's `.d.ts` is wrong; you've filed an upstream issue. Use `@ts-expect-error` (not `@ts-ignore`) with a comment naming the issue:\n ```typescript\n // @ts-expect-error: upstream types incorrectly require `id` — see https://github.com/lib/issues/420\n ```\n- **Migration phase** — moving a JS file to TS gradually; mark debt and track removal\n- **Complex generic constraints** where the compiler can't prove correctness but humans can — annotate with a comment explaining why it's safe\n\nEvery escape hatch in production code should have an explanatory comment. No bare `// @ts-ignore`.\n\n## Detection\n\n```bash\n# Count escape hatches in the diff / repo\ngrep -rEn '\\bas any\\b' --include='*.ts' --include='*.tsx' src/ | wc -l\ngrep -rEn '@ts-ignore|@ts-nocheck' --include='*.ts' --include='*.tsx' src/ | wc -l\ngrep -rEn ': any\\b' --include='*.ts' --include='*.tsx' src/ | wc -l\n\n# Bare @ts-ignore / @ts-expect-error without an explanatory comment\ngrep -rEnA1 '@ts-(ignore|expect-error)\\s*

Code Slop Detection Taste-level review of code for AI-generated patterns. Contains 24 rules across 6 categories covering comments, naming, over-engineering, defensive overdose, test slop, and style fingerprints. Where measures quantitative code debt (complexity, duplication, CVEs), this skill measures the qualitative failure mode: code that passes every metric but reads like a tutorial blog post, not like a human wrote it. Metadata - Version: 1.0.0 - Scope: PHP / Laravel + TypeScript / React (Node) - Rule Count: 24 rules across 6 categories - License: MIT Why this skill exists Industry data o…

--include='*.ts' --include='*.tsx' src/\n\n# ESLint rules to enforce in tsconfig / .eslintrc:\n# @typescript-eslint/no-explicit-any: error\n# @typescript-eslint/ban-ts-comment: { ts-ignore: \"allow-with-description\" }\n# @typescript-eslint/no-unsafe-*: error\n```\n\nAdd to ESLint config:\n\n```json\n{\n \"rules\": {\n \"@typescript-eslint/no-explicit-any\": \"error\",\n \"@typescript-eslint/ban-ts-comment\": [\n \"error\",\n { \"ts-ignore\": \"allow-with-description\", \"ts-expect-error\": \"allow-with-description\" }\n ]\n }\n}\n```\n\n**Bar:** zero `as any` / bare `@ts-ignore` in new code. Existing ones grandfathered but tracked.\n\nReference: [TypeScript handbook — strict mode](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) · [typescript-eslint rules](https://typescript-eslint.io/rules/) · [Zod](https://zod.dev/) · Internal: [`defensive-impossible-null`](defensive-impossible-null.md)\n\n---\n\n\n## No HACK Scars — Suspiciously Pristine Code\n\n**Impact: MEDIUM (Real codebases have geology; absence of any '// HACK:' / '// XXX:' markers is itself a tell)**\n\nA real codebase has *scars*. `// HACK:` comments where someone went around the type system to ship. `// XXX:` markers where the engineer noticed something fishy but had to ship. Files with a \"we tried it the right way; this is the workaround until Stripe v2\" note. These markers signal:\n\n- A human engineered the code, hit a real constraint, and acknowledged the trade-off\n- The team agreed to ship with the trade-off rather than over-engineer\n- A future engineer is being warned\n\nAI-generated codebases are flat: no `// HACK:`, no `// XXX:`, no \"we know this is wrong but\" markers. Every line presented as equally polished. The *absence* of scars is the slop signal — real systems don't look like this.\n\n## The pattern\n\nYou can audit a repo's \"humanity\" by looking for these markers:\n\n```bash\ngit grep -E '// (HACK|XXX|NOTE|GOTCHA|HMMMM|WTF)' | wc -l\n```\n\nA repo with 50 of these is alive. A repo with zero across a 50K-line codebase is either:\n- A library that was rigorously reviewed across many years (rare)\n- An AI-generated codebase passing as human (more common)\n\nYou're not looking for high counts — you're looking for **some**. Two or three `// HACK:` markers in 5K LoC is healthy.\n\n## What healthy scars look like\n\n```php\n// ✅ Real human marker — explains the constraint\npublic function chargeCustomer(int $cents, string $token): string\n{\n // HACK: Stripe v1 SDK doesn't expose retry headers; manually parse from Response\n // until we migrate to v2 (tracked in #1842).\n try {\n return $this->stripe->charges->create([...])->id;\n } catch (RateLimitException $e) {\n $retryAfter = (int) ($e->getResponse()->headers['Retry-After'] ?? 30);\n sleep($retryAfter);\n return $this->stripe->charges->create([...])->id;\n }\n}\n```\n\n```typescript\n// ✅ XXX marker that warns the next reader\nfunction parseEnvNumber(key: string): number {\n const raw = process.env[key];\n // XXX: parseInt('1.5') === 1, parseFloat('1.5e10') === 15000000000 — fine for us\n // because we only use this for ports and integer caps. Audit before reusing.\n return parseInt(raw ?? '0', 10);\n}\n```\n\n```php\n// ✅ NOTE marker capturing tribal knowledge\nclass OrderObserver\n{\n public function creating(Order $order): void\n {\n // NOTE: tax_amount must be set BEFORE Stripe charge; the webhook handler\n // re-reads this row and expects it to be non-zero.\n $order->tax_amount = TaxCalculator::for($order);\n }\n}\n```\n\nThese tell the next engineer: *\"someone thought about this; the workaround is intentional; if you change it, here's the context.\"*\n\n## What absence looks like\n\nFive files, 600 lines, all in a PR:\n\n- Every method has the same length and shape\n- Every error path is the same `try { ... } catch (e) { console.error(e); }`\n- No comments about edge cases, browser quirks, library bugs, race conditions\n- No `// HACK:` / `// XXX:` / `// NOTE:` anywhere\n- Every variable has a \"perfect\" name\n\nThat's not human. Real production code dealing with real third parties has scars.\n\n## What to do when you find pristine-looking code\n\nThis rule is harder to action than to detect. Recommended response:\n\n1. **Pair-review with the author** — ask them to walk you through one of the \"perfect\" sections. If they can articulate the *why* (and the trade-offs they considered), it's fine. If they can't, treat it as AI-assisted code that needs deeper review.\n2. **Run mutation testing on the new code** — Stryker (TS) / Infection (PHP). Mirror-tests die fast.\n3. **Sample edge cases** — try inputs the AI wouldn't have thought of (empty string, very long string, Unicode, negative numbers, future dates, leap years). If the code falls over on basics, the polish was cosmetic.\n\n## Detection\n\n```bash\n# Count scars in the existing repo (the baseline)\ngit grep -cE '// (HACK|XXX|NOTE|GOTCHA|WTF|FIXME)' -- '*.ts' '*.tsx' '*.php' '*.js' '*.jsx' 2>/dev/null | head\n\n# Same, but for the PR diff only — if a 1000-line PR adds zero scars, suspicious\ngit diff origin/main...HEAD -- '*.ts' '*.tsx' '*.php' | \\\n grep -cE '^\\+.*// (HACK|XXX|NOTE|GOTCHA|WTF)'\n```\n\nA PR adding > 500 LoC with zero `HACK:` / `XXX:` / `NOTE:` is unusual. Real engineering on real systems leaves these.\n\n**Note:** this rule cuts both ways. Don't *add* a `// HACK:` just to look human. Add them when you actually have a hack to mark. The signal is genuine, not performative.\n\nReference: Internal: [`style-hyper-consistent`](style-hyper-consistent.md)\n\n---\n\n\n## Debug Artifacts Left in Production Code\n\n**Impact: HIGH (console.log, dd(), dump(), var_dump — AI's exploratory leftovers ship to production)**\n\n`console.log(\"here\")`, `console.log(\"got user\", user)`, `dd($order)`, `dump($result)`, `var_dump($payload)`, `print_r($data)`, `echo $error` — these are the breadcrumbs left from when the developer (or AI) was debugging. They ship to production and:\n\n- Leak sensitive data into stdout / log aggregators (PII, tokens)\n- Bloat production logs to the point you can't grep for real signal\n- `dd()` literally halts execution — if it reaches prod, your endpoint returns \"1\" + var_dump output instead of JSON\n\nAI is particularly bad about this because the model tends to add `console.log(\"got result\", x)` \"to help with debugging\" and rarely removes it before \"finalising\" the function.\n\n## Incorrect\n\n```typescript\n// ❌ Debug artifacts left in\n\nasync function processPayment(order: Order, token: string): Promise\u003cCharge> {\n console.log('processPayment start', order.id); // shipped\n console.log('token', token); // SHIPS THE TOKEN\n const charge = await stripe.charges.create({ /* ... */ });\n console.log('got charge', charge); // shipped\n return charge;\n}\n```\n\n```php\n// ❌ Same in PHP\npublic function processWebhook(Request $request): JsonResponse\n{\n $payload = $request->json()->all();\n dd($payload); // halts execution; returns a debug page\n // …rest never runs\n}\n\npublic function calculateTax(Order $order): Money\n{\n dump($order); // prints to stdout in production\n print_r($order->items);\n $taxRate = 0.06;\n var_dump($taxRate);\n return $order->subtotal->multiplied($taxRate);\n}\n```\n\n**Why it's slop:**\n- `dd()` in a controller is an outage — the request never completes\n- `console.log('token', token)` is a credentials leak; on serverless logs, every Stripe call ships the token to CloudWatch\n- `dump()` / `var_dump()` show up in HTTP responses if not in a CLI context (especially during `artisan tinker` or test failures)\n- A repo with 50+ stray `console.log` in production paths signals nobody is reading their own code before merging\n\n## Correct\n\n```typescript\n// ✅ No debug; if logging matters, use the proper logger with context\nimport { logger } from '@/lib/logger';\n\nasync function processPayment(order: Order, token: string): Promise\u003cCharge> {\n // Real logger — structured, redacts secrets, levels enforced\n const log = logger.child({ orderId: order.id });\n log.info('payment.start');\n\n const charge = await stripe.charges.create({ /* ... */ });\n\n log.info('payment.success', { chargeId: charge.id, amountCents: charge.amount });\n return charge;\n}\n```\n\n```php\n// ✅ Structured logging at the boundary; no debug() calls\n\npublic function processWebhook(Request $request): JsonResponse\n{\n Log::withContext([\n 'webhook_id' => $request->header('Stripe-Webhook-Id'),\n 'event_type' => $request->json('type'),\n ])->info('webhook.received');\n\n // … actual handling …\n\n return response()->json(['ok' => true]);\n}\n```\n\n**Why it reads human:**\n- A logger with structured fields and levels (info/warn/error) — not stdout spam\n- Tokens / secrets get redacted by the logger (or not logged at all)\n- The log lines are intentional, useful for production debugging, and won't break the response\n\n## When debug calls in production code ARE warranted\n\nRare. Usually zero. Specific cases:\n\n- **Logs in well-defined CLI scripts** that are *meant* to be verbose: a one-off data migration script can use `echo` / `console.log` freely\n- **Test-only files** (`*.test.ts`, `*Test.php`) — fine to keep debug there during development\n- **Explicit `if (DEBUG_MODE) console.log(...)`** wrapped behind a feature flag — fine, but rare in practice\n\nProduction controllers, services, jobs, listeners, middleware: **zero raw debug calls**.\n\n## Detection\n\n```bash\n# JavaScript / TypeScript — console.log in production code\ngrep -rEn '\\bconsole\\.(log|debug|info|warn)\\(' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' \\\n src/ resources/js/ 2>/dev/null \\\n | grep -v -E '\\.test\\.|\\.spec\\.|/__tests__/|/scripts/'\n\n# PHP — debug helpers\ngrep -rEn '\\b(dd|dump|var_dump|print_r|var_export)\\s*\\(' --include='*.php' \\\n app/ 2>/dev/null\n\n# CI gate — block PRs that introduce debug artifacts\nNEW_DEBUG=$(git diff --diff-filter=ACM origin/main...HEAD -- 'app/**/*.php' 'src/**/*.ts' \\\n | grep -E '^\\+.*\\b(console\\.log|dd\\(|dump\\(|var_dump\\(|print_r\\()')\ntest -z \"$NEW_DEBUG\" || { echo \"Debug artifacts in PR:\"; echo \"$NEW_DEBUG\"; exit 1; }\n```\n\nESLint rules:\n\n```json\n{\n \"rules\": {\n \"no-console\": [\"error\", { \"allow\": [\"warn\", \"error\"] }]\n }\n}\n```\n\nPHPStan + a custom rule can flag `dd`/`dump` similarly. PHP-CS-Fixer has a `no_debug_print` rule.\n\nReference: [Laravel Logging docs](https://laravel.com/docs/logging) · [ESLint no-console](https://eslint.org/docs/latest/rules/no-console) · [Pino structured logging](https://github.com/pinojs/pino)\n\n---\n\n\n## Trivial Boilerplate\n\n**Impact: MEDIUM (Pattern-matched boilerplate that hides intent and inflates line count)**\n\nThe class of \"code that says less than the underlying expression\". Common AI variants:\n\n- `if (x) return true; else return false;` — when `return x` does it\n- `return x === true ? true : false;` — ditto\n- `const isPaid: boolean = order.status === 'paid';` — TS infers `boolean` already; the annotation is noise\n- `const name: string = 'asyraf';` — TS infers `string`\n- `await Promise.resolve(value)` — when `value` is already non-Promise\n- `try { await fn(); } catch (e) { throw e; }` — pass-through catch\n- `[...array]` to \"make a copy\" when the next operation doesn't mutate\n\nEach is a small AI fingerprint. A few are fine. A repo with many of them across files reads as model-generated.\n\n## Incorrect\n\n```typescript\n// ❌ if-true-false\nfunction isPaid(order: Order): boolean {\n if (order.status === 'paid') {\n return true;\n } else {\n return false;\n }\n}\n\n// ❌ Ternary returning the booleans it was given\nfunction isCompleted(order: Order): boolean {\n return order.status === 'completed' ? true : false;\n}\n\n// ❌ Redundant type annotations on obvious literals\nconst name: string = 'asyraf';\nconst age: number = 30;\nconst isActive: boolean = true;\nconst orders: Order[] = await db.orders.find(); // db.orders.find() return type IS Order[]\n\n// ❌ Pass-through catch\nasync function processPayment(order: Order): Promise\u003cCharge> {\n try {\n return await stripe.charges.create({ /* ... */ });\n } catch (e) {\n throw e; // catch literally does nothing\n }\n}\n\n// ❌ Unnecessary await + Promise.resolve\nasync function formatName(user: User): Promise\u003cstring> {\n return await Promise.resolve(`${user.first} ${user.last}`);\n}\n\n// ❌ Spread to \"copy\" — but nothing mutates\nfunction totalsFor(items: Item[]): number {\n const copied = [...items]; // unused: the next op doesn't mutate\n return copied.reduce((s, i) => s + i.price, 0);\n}\n```\n\n```php\n// ❌ PHP equivalents\npublic function isPaid(Order $order): bool\n{\n if ($order->status === 'paid') {\n return true;\n } else {\n return false;\n }\n}\n\npublic function processWebhook(Request $request): JsonResponse\n{\n try {\n return $this->handle($request);\n } catch (\\Exception $e) {\n throw $e; // pass-through\n }\n}\n```\n\n**Why it's slop:**\n- Each line carries no information beyond the simpler form\n- The `try { ... } catch (e) { throw e; }` is the most embarrassing — six tokens to do nothing\n- Type annotations on inferred literals fight the type system instead of using it\n- A reader's eyes have to traverse \"what does this expand to?\" rather than read the expression directly\n\n## Correct\n\n```typescript\n// ✅ Direct expressions\nfunction isPaid(order: Order): boolean {\n return order.status === 'paid';\n}\n\nfunction isCompleted(order: Order): boolean {\n return order.status === 'completed';\n}\n\n// ✅ Let TS infer\nconst name = 'asyraf';\nconst age = 30;\nconst isActive = true;\nconst orders = await db.orders.find();\n\n// ✅ No catch when not handling\nasync function processPayment(order: Order): Promise\u003cCharge> {\n return stripe.charges.create({ /* ... */ });\n}\n\n// ✅ No need to await a non-Promise\nfunction formatName(user: User): string {\n return `${user.first} ${user.last}`;\n}\n\n// ✅ No spread when nothing mutates\nfunction totalsFor(items: Item[]): number {\n return items.reduce((s, i) => s + i.price, 0);\n}\n```\n\n```php\n// ✅ PHP equivalents\npublic function isPaid(Order $order): bool\n{\n return $order->status === 'paid';\n}\n\npublic function processWebhook(Request $request): JsonResponse\n{\n return $this->handle($request);\n}\n```\n\n**Why it reads human:**\n- Each expression does exactly the work; nothing extra\n- Type inference does its job; the type annotations are added where they earn their place (function signatures, public APIs)\n- No pass-through catch; if an error needs handling, it's handled\n\n## When annotations / spreads ARE worth it\n\nA few cases:\n\n- **Public-API parameters / returns** — explicit types document the contract: `function add(a: number, b: number): number`\n- **Complex inferred types** — annotate when the inferred type is hard to read\n- **Boundary code** — `as const`, `satisfies`, or explicit annotations on configuration objects help downstream type narrowing\n- **Spread when you actually want a copy** — before sorting, before mutating, before passing to a function that mutates\n\nThe trivial-boilerplate test: **does removing the construct change the program's behaviour or the reader's understanding?** If no, remove.\n\n## Detection\n\n```bash\n# if-true-false\ngrep -rEnB1 -A2 'if\\s*\\([^)]+\\)\\s*\\{?\\s*

Code Slop Detection Taste-level review of code for AI-generated patterns. Contains 24 rules across 6 categories covering comments, naming, over-engineering, defensive overdose, test slop, and style fingerprints. Where measures quantitative code debt (complexity, duplication, CVEs), this skill measures the qualitative failure mode: code that passes every metric but reads like a tutorial blog post, not like a human wrote it. Metadata - Version: 1.0.0 - Scope: PHP / Laravel + TypeScript / React (Node) - Rule Count: 24 rules across 6 categories - License: MIT Why this skill exists Industry data o…

--include='*.ts' --include='*.tsx' --include='*.js' --include='*.php' \\\n src/ app/ 2>/dev/null | grep -B2 -A1 'return true\\b' | grep -A2 'else' | head\n\n# Pass-through catch\ngrep -rEnB0 -A1 'catch\\s*\\([^)]*\\)\\s*\\{' --include='*.ts' --include='*.tsx' --include='*.php' \\\n src/ app/ 2>/dev/null | grep -B1 '^\\s*throw\\b'\n\n# Ternary returning booleans\ngrep -rEn '\\?\\s*true\\s*:\\s*false\\b' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.php' \\\n src/ app/\n\n# Redundant type annotations on string/number/boolean literals (TS)\ngrep -rEn ':\\s*(string|number|boolean)\\s*=\\s*(\"[^\"]*\"|[0-9]+|true|false)' --include='*.ts' --include='*.tsx' src/\n```\n\nESLint rules:\n\n```json\n{\n \"rules\": {\n \"no-useless-return\": \"error\",\n \"no-useless-catch\": \"error\",\n \"no-unneeded-ternary\": \"error\"\n }\n}\n```\n\nReference: [ESLint — no-useless-catch / ternary / return](https://eslint.org/docs/latest/rules/) · [PHP-CS-Fixer rules](https://cs.symfony.com/) · Internal: [`defensive-generic-catch`](defensive-generic-catch.md)\n\n---\n\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":117090,"content_sha256":"11daba64aceadd52aaa5474a7bd243f24ff6615eede7d7ed08a45a6471ba3855"},{"filename":"metadata.json","content":"{\n \"name\": \"Code Slop Detection\",\n \"version\": \"1.0.0\",\n \"description\": \"Taste-level review of code for AI-generated patterns in PHP/Laravel and TypeScript/React projects. Catches code that passes every metric but reads like a tutorial \\u2014 not human-written. Complements technical-debt's quantitative metrics with qualitative review.\",\n \"framework\": \"PHP / Laravel + Node / TypeScript / React\",\n \"license\": \"MIT\",\n \"author\": {\n \"name\": \"Agent Skills Contributors\",\n \"url\": \"https://github.com/AsyrafHussin/agent-skills\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/AsyrafHussin/agent-skills\"\n },\n \"keywords\": [\n \"ai-slop\",\n \"code-quality\",\n \"code-review\",\n \"ai-generated-code\",\n \"code-smells\",\n \"llm-code-patterns\",\n \"clean-code\",\n \"anti-patterns\",\n \"comments\",\n \"naming-conventions\",\n \"over-engineering\",\n \"defensive-programming\",\n \"test-quality\",\n \"code-taste\",\n \"human-readable\"\n ],\n \"categories\": [\n {\n \"id\": \"comments\",\n \"name\": \"Comments\",\n \"priority\": \"CRITICAL\",\n \"description\": \"Narration, empty docblocks, placeholder TODOs, closing-brace labels\",\n \"ruleCount\": 4\n },\n {\n \"id\": \"naming\",\n \"name\": \"Naming\",\n \"priority\": \"CRITICAL\",\n \"description\": \"Generic placeholders, over-descriptive run-ons, suffix abuse, type-in-name\",\n \"ruleCount\": 4\n },\n {\n \"id\": \"over-engineering\",\n \"name\": \"Over-engineering\",\n \"priority\": \"HIGH\",\n \"description\": \"Premature interfaces, single-method classes, useless wrappers, dependency creep\",\n \"ruleCount\": 4\n },\n {\n \"id\": \"defensive-overdose\",\n \"name\": \"Defensive overdose\",\n \"priority\": \"HIGH\",\n \"description\": \"Generic catch blocks, impossible null checks, defensive in wrong places\",\n \"ruleCount\": 3\n },\n {\n \"id\": \"test-slop\",\n \"name\": \"Test slop\",\n \"priority\": \"HIGH\",\n \"description\": \"Mock-everything tests, doesn't-throw asserts, mirror-implementation, snapshot abuse\",\n \"ruleCount\": 4\n },\n {\n \"id\": \"style-fingerprints\",\n \"name\": \"Style fingerprints\",\n \"priority\": \"MEDIUM\",\n \"description\": \"Hyper-consistent formatting, as-any escapes, no HACK scars, debug artifacts, trivial boilerplate\",\n \"ruleCount\": 5\n }\n ],\n \"references\": [\n {\n \"title\": \"arXiv 2510.03029 \\u2014 Investigating the Smells of LLM-Generated Code\",\n \"url\": \"https://arxiv.org/abs/2510.03029\",\n \"type\": \"research\"\n },\n {\n \"title\": \"hardikpandya/stop-slop \\u2014 prose-slop checklist\",\n \"url\": \"https://github.com/hardikpandya/stop-slop\",\n \"type\": \"reference\"\n },\n {\n \"title\": \"flamehaven01/AI-SLOP-Detector \\u2014 Python AST scanner\",\n \"url\": \"https://github.com/flamehaven01/AI-SLOP-Detector\",\n \"type\": \"tool\"\n }\n ],\n \"tags\": [\n \"ai-slop\",\n \"code-quality\",\n \"code-review\",\n \"clean-code\",\n \"anti-patterns\",\n \"ai-generated-code\",\n \"code-taste\",\n \"human-readable\"\n ],\n \"lastUpdated\": \"2026-05-17\",\n \"rulesTotal\": 24\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":3105,"content_sha256":"846413a338afdcc8d083a714560287180d343e9afa1c4c21c8ecc62721959157"},{"filename":"README.md","content":"# Code Slop Detection\n\nTaste-level review of code for AI-generated patterns (\"slop\") in **PHP/Laravel and TypeScript/React** projects. Catches code that passes every metric but reads like a tutorial blog post — not what a human teammate would write.\n\n**Version:** 1.0.0\n\n## Overview\n\n- Audits AI-assisted PRs and codebases for AI-fingerprint patterns\n- Verdict bands: CLEAN / SUSPICIOUS / INFLATED / CRITICAL\n- Stack: PHP / Laravel + Node / TypeScript / React\n- 24 rules across 6 categories\n- Complements `technical-debt` (quantitative metrics) with qualitative taste\n\n## What it catches that linters don't\n\n| Slop pattern | Why linters miss it |\n|---|---|\n| Narration comments | Looks like a \"valid comment\" to any linter |\n| Generic names (`data`, `result`) | Passes naming conventions |\n| Premature interfaces / single-method classes | Valid code structure |\n| Generic `catch (e) { console.error(...) }` | Try/catch is a valid pattern |\n| Mock-everything tests | Tests pass and run; coverage looks fine |\n| Missing `// HACK:` scars | No tool checks for *absence* of human imperfection |\n\n## Categories\n\n### 1. Comments (CRITICAL)\nNarration, empty docblocks, placeholder TODOs, closing-brace labels.\n\n### 2. Naming (CRITICAL)\nGeneric placeholders, over-descriptive run-ons, suffix abuse, type-in-name.\n\n### 3. Over-engineering (HIGH)\nPremature interfaces, single-method classes, useless wrappers, dependency creep.\n\n### 4. Defensive overdose (HIGH)\nGeneric catch blocks, impossible null checks, missing real defenses.\n\n### 5. Test slop (HIGH)\nMock-everything, \"doesn't throw\" assertions, mirror-implementation, snapshot abuse.\n\n### 6. Style fingerprints (MEDIUM)\nHyper-consistent formatting, `as any` escapes, no `// HACK:` scars, debug artifacts, trivial boilerplate.\n\n## Usage\n\n```\nReview this PR for AI slop\nAudit src/ for AI-generated code patterns\nDoes this code look human-written?\nFind the slop in app/Services/\nCheck this file against the slop checklist\n```\n\n## Differentiator vs `technical-debt`\n\n| Skill | Lens |\n|---|---|\n| `technical-debt` | Quantitative — complexity, duplication, CVEs, missing indexes |\n| `code-slop` | Qualitative — does this code feel AI-written? |\n\nSome overlap exists on dead code and generic catch blocks, but framing and remediation differ.\n\n## References\n\n- GitClear 2025 Trends Report\n- [arXiv 2510.03029 — LLM-Generated Code Smells](https://arxiv.org/abs/2510.03029)\n- [hardikpandya/stop-slop](https://github.com/hardikpandya/stop-slop) — prose-slop sister project\n- [flamehaven01/AI-SLOP-Detector](https://github.com/flamehaven01/AI-SLOP-Detector) — AST scanner\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2617,"content_sha256":"e87602d24190e8b462586fd56014e253c625b569c8d82fc75cebfa9b3f82c86f"},{"filename":"rules/_sections.md","content":"# Sections\n\nThis file defines all sections, their ordering, impact levels, and descriptions.\nThe section ID (in parentheses) is the filename prefix used to group rules.\n\n---\n\n## 1. Comments (comments)\n\n**Impact:** CRITICAL\n**Description:** The single loudest AI tell. Models love to narrate the next line in the comment above it, write empty docblocks that restate the function signature, and leave placeholder comments like `// TODO: implement` long after the code shipped. Cleaning these out is the fastest path to \"this looks human-written\".\n\n## 2. Naming (naming)\n\n**Impact:** CRITICAL\n**Description:** AI-generated code swings between two opposite failure modes — generic placeholders (`data`, `result`, `info`) and over-descriptive run-ons (`theUserWhoIsCurrentlyLoggedIn`) — often in the same file. A human teammate converges on a register. Suffix abuse (`*Helper`, `*Manager`, `*Util`) and type-in-name patterns (`userObject`, `resultArray`) round out the family.\n\n## 3. Over-engineering (over-eng)\n\n**Impact:** HIGH\n**Description:** Adding layers nobody asked for. Premature interfaces with one implementation, single-method classes that should be functions, wrappers called from one place, and pulling in a new dependency when an existing one does the job. AI defaults to \"enterprise-grade\" because its training distribution is enterprise-grade.\n\n## 4. Defensive overdose (defensive)\n\n**Impact:** HIGH\n**Description:** Try/catch wrapped around code that can't throw. Null checks after a non-null assertion. `if (array && array.length > 0)` three times in the same function. Meanwhile real defenses (timeouts on external calls, rate limits, circuit breakers) are missing. AI is defensive in the wrong places.\n\n## 5. Test slop (test)\n\n**Impact:** HIGH\n**Description:** Tests that mock everything and assert nothing. Tests that mirror the implementation's logic — they pass because they re-encode the same code, not because they verify behaviour. \"Doesn't throw\" assertions that don't check anything meaningful. Snapshot abuse instead of behavioural assertions.\n\n## 6. Style fingerprints (style)\n\n**Impact:** MEDIUM\n**Description:** The small tells: hyper-consistent formatting (no human drift), `as any` / `@ts-ignore` sprinkled where types are hard, *absence* of `// HACK:` / `// XXX:` scars (real codebases have geology), stray `console.log` / `dd()` debug artifacts, and trivial boilerplate like `if (x) return true; else return false`.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2454,"content_sha256":"9792a313e092b1e4285fe4e6b511c878b04996513ea486a0e219770dd0b55632"},{"filename":"rules/_template.md","content":"---\ntitle: Rule Title Here\nimpact: CRITICAL|HIGH|MEDIUM\nimpactDescription: \"Specific consequence — e.g., 'Makes code feel like a tutorial; reduces comprehension speed'\"\ntags: tag1, tag2, tag3\n---\n\n## Rule Title Here\n\n**Impact: LEVEL (impactDescription)**\n\n1-2 sentences explaining why this pattern is an AI fingerprint and why it hurts.\n\n## Incorrect\n\n```language\n// ❌ AI-slop example\n```\n\n**Why it's slop:**\n- Reason 1\n- Reason 2\n\n## Correct\n\n```language\n// ✅ Human-equivalent\n```\n\n**Why it reads human:**\n- Reason 1\n- Reason 2\n\n## Detection\n\n```bash\n# grep / eslint / phpstan invocation\n```\n\nReference: [Link](url) · Internal: [`related-rule`](related-rule.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":670,"content_sha256":"a4e02244afbaf73c55cd62324c9fc87437b4813aac11b1b0fc2796062d14f5e2"},{"filename":"rules/comments-closing-brace-labels.md","content":"---\ntitle: Closing-Brace Labels\nimpact: MEDIUM\nimpactDescription: \"Relic of older training corpora; clear AI fingerprint that modern editors make pointless\"\ntags: comments, closing-brace, ai-fingerprint, style\n---\n\n## Closing-Brace Labels\n\n**Impact: MEDIUM (Relic of older training corpora; clear AI fingerprint that modern editors make pointless)**\n\n`} // end function` / `} // end if block` / `} // close foreach` are a strong AI tell. The pattern comes from older C/COBOL/Verilog training data where blocks could span hundreds of lines and editors didn't fold scope. Modern PHP, TypeScript, and any editor with bracket-matching makes the label pure noise.\n\nIf your function is so long you need a label on its closing brace, the real fix is to split the function — not annotate the brace.\n\n## Incorrect\n\n```php\n// ❌ Labels on closing braces\n\nclass OrderService\n{\n public function process(Order $order): void\n {\n if ($order->isPaid()) {\n foreach ($order->items as $item) {\n if ($item->requiresShipping()) {\n $this->ship($item);\n } // end if requiresShipping\n } // end foreach items\n } // end if isPaid\n } // end function process\n} // end class OrderService\n```\n\n```typescript\n// ❌ TS variant\nfunction processOrder(order: Order): void {\n if (order.isPaid()) {\n order.items.forEach(item => {\n if (item.requiresShipping()) {\n ship(item);\n } // end if requiresShipping\n }); // end forEach\n } // end if isPaid\n} // end function processOrder\n```\n\n**Why it's slop:**\n- Every modern IDE highlights the matching opener when the cursor is on a brace\n- The labels add bytes for zero information\n- A function that needed labels for clarity is a function that needs to be split\n- Pattern is recognisably training-data noise (PHP-from-2008, Verilog, COBOL)\n\n## Correct\n\n```php\n// ✅ Either small enough not to need labels, OR split for clarity\n\nclass OrderService\n{\n public function process(Order $order): void\n {\n if (!$order->isPaid()) {\n return;\n }\n $this->shipEligibleItems($order);\n }\n\n private function shipEligibleItems(Order $order): void\n {\n foreach ($order->items->filter->requiresShipping() as $item) {\n $this->ship($item);\n }\n }\n}\n```\n\n```typescript\n// ✅ Smaller scopes, no labels needed\nfunction processOrder(order: Order): void {\n if (!order.isPaid()) return;\n shipEligibleItems(order);\n}\n\nfunction shipEligibleItems(order: Order): void {\n order.items\n .filter(i => i.requiresShipping())\n .forEach(ship);\n}\n```\n\n**Why it reads human:**\n- Short functions; the closing brace is visually close to the opener\n- No annotation noise; editor handles brace matching\n- If a developer felt the need to label, they'd split the function instead\n\n## Detection\n\n```bash\n# Closing brace followed by an \"end\" / function/class/if/foreach annotation comment\ngrep -rEn '^\\s*\\}\\s*(\\)\\s*)?\\s*(//|#).*\\b(end|close|finish)\\b' \\\n --include='*.php' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' \\\n app/ src/\n\n# Specific PHP closing-tag labels\ngrep -rEn '^\\s*\\}\\s*//.*(end (function|class|if|foreach|for|while|switch))' --include='*.php' app/\n```\n\nA single hit is borderline; **three or more in one file is an unmistakable AI fingerprint.**\n\nReference: [Code Complete — Chapter 32 on Self-Documenting Code](https://www.microsoftpressstore.com/store/code-complete-9780735619678) · Internal: [`comments-narration`](comments-narration.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3552,"content_sha256":"38cf419b554d12b29c25b840db75a0f5a33a28226ac52ae954ce9df9cff7c648"},{"filename":"rules/comments-empty-docblocks.md","content":"---\ntitle: Empty Docblocks That Restate the Signature\nimpact: HIGH\nimpactDescription: \"Adds bytes, zero information; suggests the author auto-generated without reading\"\ntags: comments, docblocks, jsdoc, phpdoc, ai-fingerprint\n---\n\n## Empty Docblocks That Restate the Signature\n\n**Impact: HIGH (Adds bytes, zero information; suggests the author auto-generated without reading)**\n\nA docblock that adds nothing the signature already says is a tell. `/** Get the user */` above `function getUser(): User` tells the reader nothing the function name and return type don't. Modern PHP and TypeScript both encode types in the signature; redundant docblocks are pure noise.\n\nUseful docblocks document **edge cases, thrown exceptions, side effects, or non-obvious semantics** — not \"this method does what its name says\".\n\n## Incorrect\n\n```php\n// ❌ Restates the signature; adds nothing\n/**\n * Get the user by ID.\n *\n * @param int $id The user ID\n * @return User The user\n */\npublic function getUser(int $id): User\n{\n return User::findOrFail($id);\n}\n\n/**\n * Adds two numbers together.\n *\n * @param int $a First number\n * @param int $b Second number\n * @return int The sum\n */\npublic function add(int $a, int $b): int\n{\n return $a + $b;\n}\n```\n\n```typescript\n// ❌ Same pattern in TS\n/**\n * Get the user.\n * @param id - The user ID\n * @returns The user\n */\nfunction getUser(id: string): User { /* ... */ }\n\n/**\n * Adds two numbers.\n * @param a - First number\n * @param b - Second number\n * @returns The sum\n */\nfunction add(a: number, b: number): number { return a + b; }\n```\n\n**Why it's slop:**\n- The signature already says all of this — return type, parameter types, what's returned\n- Bytes per useful info: ~zero\n- A human writes a docblock when there's something the signature *can't* express\n\n## Correct — option 1: no docblock at all\n\n```php\n// ✅ Signature speaks for itself\npublic function getUser(int $id): User\n{\n return User::findOrFail($id);\n}\n\npublic function add(int $a, int $b): int\n{\n return $a + $b;\n}\n```\n\n## Correct — option 2: docblock that earns its place\n\n```php\n// ✅ Docblock documents what the signature CAN'T say\n/**\n * Get a user, or throw if not found.\n *\n * @throws ModelNotFoundException if no user matches $id\n * @throws AuthorizationException if the current user can't view $id\n */\npublic function getUser(int $id): User\n{\n Gate::authorize('view', User::class);\n return User::findOrFail($id);\n}\n```\n\n```typescript\n// ✅ TSDoc that documents real edge cases\n/**\n * @throws OutOfRangeError if `amount` exceeds the Stripe 999,999.99 cap.\n * @remarks Rounds to 2 decimal places using banker's rounding.\n */\nfunction formatAmountForStripe(amount: number): string { /* ... */ }\n```\n\n**Why it reads human:**\n- The remaining docblocks all carry information the signature can't\n- A reader scanning the file sees docblocks and trusts they're load-bearing\n\n## Detection\n\n```bash\n# Files with @param/@return docblocks\ngrep -rln '@param\\|@return\\|@returns' --include='*.php' app/\ngrep -rln '@param\\|@returns' --include='*.ts' --include='*.tsx' src/\n\n# PHPStan can flag redundant @param when the typed signature is more informative:\nvendor/bin/phpstan analyse --level=9\n# (set `reportAlwaysTrueInLastCondition: true` and use phpstan-deprecation-rules)\n\n# ESLint rule for TS — flags JSDoc with no additional info:\n# https://github.com/gajus/eslint-plugin-jsdoc → jsdoc/require-jsdoc { contexts: 'never' }\n# Or simpler: turn OFF require-jsdoc and let humans write docblocks only when warranted.\n```\n\nThe most reliable enforcement is a **PR review rule**: \"every `@param` / `@return` must add information the signature doesn't already provide. Delete the rest.\"\n\nReference: [PHPDoc Tags Reference](https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/) · [TSDoc](https://tsdoc.org/) · Internal: [`comments-narration`](comments-narration.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3884,"content_sha256":"ad7860aa8637273b05f0cd11c11de4ed38fc0720be409e51c9223552542e5813"},{"filename":"rules/comments-narration.md","content":"---\ntitle: Narration Comments\nimpact: CRITICAL\nimpactDescription: \"The single loudest AI tell; cuts reading speed and signals the author didn't read what they shipped\"\ntags: comments, narration, ai-fingerprint\n---\n\n## Narration Comments\n\n**Impact: CRITICAL (The single loudest AI tell; cuts reading speed and signals the author didn't read what they shipped)**\n\nModels love narrating the next line in a comment above it. The comment adds no information — it just restates the code in English. A reader has to read both, decide they say the same thing, and feel a tiny tax of distrust. Across a 500-line file, the tax compounds into \"I have no idea what this codebase is doing.\"\n\nA useful comment answers **why**, not **what**. If the code itself says *what*, the comment is noise.\n\n## Incorrect\n\n```php\n// ❌ Comments narrate the next line — pure AI fingerprint\n\npublic function createUser(array $data): User\n{\n // Validate the input data\n $validated = Validator::make($data, [...])->validate();\n\n // Create a new user with the validated data\n $user = User::create($validated);\n\n // Send a welcome email to the user\n Mail::to($user)->send(new WelcomeEmail($user));\n\n // Return the newly created user\n return $user;\n}\n```\n\n```typescript\n// ❌ Same pattern in TS — every line narrated above\nfunction calculateTotal(items: Item[]): number {\n // Initialize the total to zero\n let total = 0;\n\n // Loop through each item in the items array\n for (const item of items) {\n // Add the item's price multiplied by quantity to the total\n total += item.price * item.quantity;\n }\n\n // Return the calculated total\n return total;\n}\n```\n\n**Why it's slop:**\n- Every comment is a literal English translation of the code on the next line\n- A reader's eyes have to traverse twice (comment → code) to learn nothing extra\n- Suggests the author was thinking out loud rather than thinking ahead\n- A human author who knew this code wouldn't bother writing these comments\n\n## Correct\n\n```php\n// ✅ Comments removed (the code says what it does); explain WHY only if non-obvious\n\npublic function createUser(array $data): User\n{\n $validated = Validator::make($data, [...])->validate();\n\n $user = User::create($validated);\n Mail::to($user)->send(new WelcomeEmail($user));\n\n return $user;\n}\n```\n\n```typescript\n// ✅ Total comments removed; if any line needed comment, it'd be a WHY\nfunction calculateTotal(items: Item[]): number {\n return items.reduce((sum, i) => sum + i.price * i.quantity, 0);\n}\n```\n\nIf a comment IS needed, it explains *why*:\n\n```typescript\n// ✅ Good comment — explains a hidden constraint\n// Note: `total` is in cents (Stripe API uses integer minor units).\nfunction calculateTotal(items: Item[]): number {\n // Stripe rejects single charges > 999,999_99 cents (= $999,999.99). Cap to avoid charge failure.\n const total = items.reduce((sum, i) => sum + i.price * i.quantity, 0);\n return Math.min(total, 99_999_999); // 999,999.99 expressed in cents\n}\n```\n\n**Why it reads human:**\n- Code speaks for itself; reader processes once\n- The one remaining comment carries a *load-bearing* piece of context (Stripe constraint) you couldn't infer from the code\n- Signals the author understood the code well enough to know what didn't need explaining\n\n## Detection\n\n```bash\n# Heuristic: comments immediately followed by code that uses the comment's keyword\n# (rough but effective — flags lines where the comment is one word-of-code below)\ngrep -rEn '^[[:space:]]*//.*' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' src/ | head -50\ngrep -rEn '^[[:space:]]*//.*' --include='*.php' app/ | head -50\n\n# Stronger heuristic: comment + next non-blank line share a keyword\n# (run in code review tools or PHPStorm \"Comment Density\" inspections)\n\n# Easiest enforcement: ESLint capital-comments / no-inline-comments rules combined\n# with a team-level \"comments answer WHY only\" code-review checklist.\n```\n\nThere is no fully automatic linter for this — it requires a reviewer's judgement. The detection pattern is \"does the comment translate, or add context?\"\n\nReference: [Code Complete — Self-Documenting Code](https://www.microsoftpressstore.com/store/code-complete-9780735619678) · Internal: [`comments-empty-docblocks`](comments-empty-docblocks.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4317,"content_sha256":"f06dffa9a3985243b68493fa66d1a97b029500ebefebec3bebde083c36a377a0"},{"filename":"rules/comments-placeholder.md","content":"---\ntitle: Placeholder Comments Left In\nimpact: CRITICAL\nimpactDescription: \"Reveals AI-generated code that was never completed; ships unfinished work as 'done'\"\ntags: comments, placeholder, todo, ai-fingerprint\n---\n\n## Placeholder Comments Left In\n\n**Impact: CRITICAL (Reveals AI-generated code that was never completed; ships unfinished work as 'done')**\n\nCommon AI-generated placeholders that should never reach `main`:\n\n```\n// implementation\n// implementation here\n// your code here\n// helper function\n// TODO: implement\n// TODO: implement this\n// TODO: add error handling\n// TODO: implement validation\n// FIXME: figure out edge case\n// HACK: temporary\n// placeholder\n// add your logic here\n```\n\nThese mean the AI produced scaffolding the developer was supposed to fill in. If they reach the PR, they signal the author shipped without reading.\n\nDistinct from properly-tagged debt markers — a `// TODO(#1234, asyraf): rewrite after Stripe v2 migration` with an owner, ticket, and concrete unblocking condition is *not* slop. That's tracked debt.\n\n## Incorrect\n\n```php\n// ❌ Placeholder comments shipped as 'done'\n\npublic function calculateRefund(Order $order): Money\n{\n // TODO: implement\n return Money::zero();\n}\n\npublic function process(Order $order): void\n{\n // Validate the order\n // TODO: add validation\n\n $this->charge($order);\n\n // helper function\n // TODO: implement error handling\n}\n```\n\n```typescript\n// ❌ Same in TS\nfunction exportUsers(): User[] {\n // your code here\n return [];\n}\n\nfunction handlePayment(token: string) {\n // implementation\n // TODO: implement\n}\n```\n\n**Why it's slop:**\n- The function name promises a behaviour the body doesn't deliver\n- Reviewers usually skip past `// TODO` without realising the function is empty\n- These slip through tests because the placeholder body type-checks\n- If genuinely intended as future work, it belongs in the issue tracker, not the source file\n\n## Correct\n\n```php\n// ✅ Either implement, or fail loud, or remove the method entirely\n\npublic function calculateRefund(Order $order): Money\n{\n throw new BadMethodCallException(\n 'Refund calculation not yet implemented — see #1842 for the policy spec.'\n );\n}\n\n// ✅ Or genuine, tracked work-in-progress with owner + ticket:\n// TODO(#1842, asyraf): cap refunds at original-charge-minus-shipping\npublic function calculateRefund(Order $order): Money\n{\n return $order->total->subtract($order->shippingCost);\n}\n```\n\n```typescript\n// ✅ Fail loud so the gap is impossible to ship past\nfunction exportUsers(): User[] {\n throw new Error('exportUsers: not implemented (see #420)');\n}\n```\n\n**Why it reads human:**\n- An empty function body that throws is impossible to merge by accident; tests will catch it\n- A tracked `TODO` with `(#issue, @owner)` is real debt, not abandoned scaffolding\n- A human author either implements the function or explicitly fails — they don't ship \"Returns nothing for now\"\n\n## Detection\n\n```bash\n# Common AI placeholders — match only when prefixed by a comment marker\n# (without the required prefix, the regex would flood on class names like \"ServiceImplementation\").\ngrep -rEn --include='*.php' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' \\\n '(//|/\\*|#)\\s*(implementation|implementation here|your code here|helper function|placeholder|add your logic|add error handling|add validation)\\b' \\\n app/ src/ 2>/dev/null\n\n# Bare TODO/FIXME (without ticket reference)\ngrep -rEn --include='*.php' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' \\\n '(TODO|FIXME)([^(#]|$)' app/ src/ 2>/dev/null | grep -v '#[0-9]'\n\n# Combined: TODOs lacking issue references — CI gate\nNEW=$(git diff --name-only --diff-filter=ACM origin/main...HEAD)\nBARE=$(echo \"$NEW\" | xargs grep -EnH '\\b(TODO|FIXME)\\b' 2>/dev/null | grep -v '#[0-9]')\ntest -z \"$BARE\" || { echo \"Bare TODO/FIXME found — add (#issue, @owner):\"; echo \"$BARE\"; exit 1; }\n```\n\nSee also `technical-debt`'s `process-todo-fixme-aging` rule — the broader policy for tracked debt markers.\n\nReference: Internal: [`comments-narration`](comments-narration.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4132,"content_sha256":"c6c73d3aae0f3ebf043c8a616796d41a90e74b2b6ce16b6b89aabc66e31268c5"},{"filename":"rules/defensive-generic-catch.md","content":"---\ntitle: Generic catch Blocks That Don't Distinguish Errors\nimpact: CRITICAL\nimpactDescription: \"82% of AI PRs (OX Security) — masks bugs as 'handled', breaks observability, kills debuggability\"\ntags: defensive-programming, error-handling, ai-fingerprint\n---\n\n## Generic catch Blocks That Don't Distinguish Errors\n\n**Impact: CRITICAL (82% of AI PRs per OX Security study; masks bugs as 'handled', breaks observability, kills debuggability)**\n\nThe single most-cited AI anti-pattern in 2025 research. Try/catch wrapped around code, with a generic catch that logs and swallows. Looks responsible. Is the opposite — it hides bugs and converts loud failures into silent corruption.\n\nReal error handling distinguishes:\n- **What can throw** (specific exception types)\n- **What's recoverable** (catch, retry, fall back)\n- **What's a bug** (re-throw, let it propagate)\n\nA bare `catch (e) { console.error(e); }` does none of this. It says \"if anything goes wrong, I'll keep going\" — and \"anything\" includes programmer errors that should crash so they're noticed.\n\n## Incorrect\n\n```php\n// ❌ Generic catch that swallows everything\n\npublic function processPayment(Order $order): bool\n{\n try {\n $charge = $this->stripe->charges->create([\n 'amount' => $order->total->cents(),\n 'currency' => 'usd',\n 'source' => $order->paymentToken,\n ]);\n $order->update(['stripe_charge_id' => $charge->id, 'status' => 'paid']);\n return true;\n } catch (Exception $e) {\n Log::error('Payment failed: ' . $e->getMessage());\n return false;\n }\n}\n```\n\n```typescript\n// ❌ Same TS pattern\nasync function exportUsers(): Promise\u003cUser[]> {\n try {\n const users = await api.fetchAllUsers();\n return users;\n } catch (e) {\n console.error('Export failed:', e);\n return [];\n }\n}\n```\n\n**Why it's slop:**\n- `Exception $e` catches *everything*, including `TypeError` (programmer bug) and `OutOfMemoryError` (system crash) — those should not be \"handled\" by logging and continuing\n- Returning `false` / `[]` lets the caller think the operation succeeded with no data, masking a real failure\n- `Log::error` lacks context — what was the order? what user? what amount? Future debugger has no anchor\n- A real Stripe error (card declined) gets the same treatment as a typo in the code\n\n## Correct\n\n```php\n// ✅ Catch specifically; re-throw what you don't understand\n\npublic function processPayment(Order $order): void\n{\n try {\n $charge = $this->stripe->charges->create([\n 'amount' => $order->total->cents(),\n 'currency' => 'usd',\n 'source' => $order->paymentToken,\n ]);\n $order->update(['stripe_charge_id' => $charge->id, 'status' => 'paid']);\n } catch (CardException $e) {\n // Customer-facing failure — known, expected\n $order->update(['status' => 'declined', 'decline_reason' => $e->getStripeCode()]);\n throw new PaymentDeclined($e->getStripeCode(), previous: $e);\n } catch (RateLimitException $e) {\n // Transient — caller retries\n throw new TransientPaymentFailure(retryAfterSeconds: 30, previous: $e);\n }\n // Any other exception propagates: TypeError, OutOfMemoryError, etc.\n // — those are bugs we WANT to know about.\n}\n```\n\n```typescript\n// ✅ Specific errors, propagate the unknown\nasync function exportUsers(): Promise\u003cUser[]> {\n try {\n return await api.fetchAllUsers();\n } catch (e) {\n if (e instanceof AbortError) {\n // Caller aborted; that's a feature, not a failure\n throw e;\n }\n if (e instanceof RateLimitError) {\n await sleep(e.retryAfterMs);\n return api.fetchAllUsers(); // one retry\n }\n // Network / parse / bug — propagate; don't return [] (the caller would think it succeeded with no users)\n throw e;\n }\n}\n```\n\n**Why it reads human:**\n- Each `catch` branch handles a specific, named condition with a specific remediation\n- Unknown errors propagate — the system fails loud, observability catches them, on-call wakes up before customers do\n- No silent \"return false\" / \"return empty array\" — caller can't accidentally treat failure as empty success\n\n## The \"what would I want at 2am?\" test\n\nWhen debugging a production incident, you want:\n- **Errors at the right loudness** — bugs crash with stack traces; expected failures have domain-specific exceptions you can grep\n- **Context, not just the message** — order ID, user ID, request ID, all in the log line\n- **No silent corruption** — a \"successful\" call that returned no data is the worst kind of failure\n\nThe generic-catch pattern fails all three.\n\n## Detection\n\n```bash\n# Bare catches in PHP\ngrep -rEn 'catch\\s*\\(\\s*\\\\?Exception\\b' --include='*.php' app/\n\n# Bare catches in TS / JS\ngrep -rEn '\\}\\s*catch\\s*\\(\\s*[a-zA-Z_]+\\s*\\)\\s*\\{' --include='*.ts' --include='*.tsx' --include='*.js' src/\n\n# Catch blocks that only log and continue\ngrep -rEnB1 'console\\.error\\(.*\\)\\s*;\\s*

Code Slop Detection Taste-level review of code for AI-generated patterns. Contains 24 rules across 6 categories covering comments, naming, over-engineering, defensive overdose, test slop, and style fingerprints. Where measures quantitative code debt (complexity, duplication, CVEs), this skill measures the qualitative failure mode: code that passes every metric but reads like a tutorial blog post, not like a human wrote it. Metadata - Version: 1.0.0 - Scope: PHP / Laravel + TypeScript / React (Node) - Rule Count: 24 rules across 6 categories - License: MIT Why this skill exists Industry data o…

--include='*.ts' --include='*.tsx' src/\n\n# PHP equivalent\ngrep -rEnB1 'Log::(error|warning)\\(.*\\)' --include='*.php' app/Http/ app/Services/\n\n# Linter rules\n# ESLint: no-empty (already standard); add @typescript-eslint/no-explicit-any\n# PHPStan level >= 8 flags catch(\\Throwable) in some configurations\n```\n\nA small handful of generic catches is fine (e.g., at HTTP boundaries with structured logging). **Density** is the signal — > 5 generic catches in a single service is the AI fingerprint.\n\nReference: Internal: [`defensive-impossible-null`](defensive-impossible-null.md), [`defensive-missing-real`](defensive-missing-real.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5617,"content_sha256":"6bafe9ebf33ae349a42c43f6c5b4ebfe2a399ebb4348b2a3aa6ff2677987d96a"},{"filename":"rules/defensive-impossible-null.md","content":"---\ntitle: Null Checks for Impossible Nulls\nimpact: HIGH\nimpactDescription: \"Defensive code in places that can't fail; clutters the path and signals model didn't trust the type system\"\ntags: defensive-programming, null-checks, type-safety, ai-fingerprint\n---\n\n## Null Checks for Impossible Nulls\n\n**Impact: HIGH (Defensive code in places that can't fail; clutters the path and signals model didn't trust the type system)**\n\nWhen the type system or surrounding code already guarantees a value is non-null, a defensive null check adds noise and tells the reader \"the author wasn't sure\". Common cases:\n\n- A non-nullable parameter (typed `User $user`, not `?User $user`) — can't be null\n- After `Model::findOrFail()` — never returns null (throws on miss)\n- After a non-null assertion in the previous line\n- Inside a `forEach` / `map` loop where the iterable can't contain null\n\nThe pattern signals the model defaulted to \"check everything for safety\" without reading the types around it.\n\n## Incorrect\n\n```php\n// ❌ Null check after findOrFail — findOrFail throws if not found\n\npublic function show(int $id): View\n{\n $user = User::findOrFail($id);\n\n if ($user === null) {\n abort(404);\n }\n\n return view('users.show', compact('user'));\n}\n\n// ❌ Null check on a non-nullable parameter\n\npublic function notify(User $user, string $message): void // $user is required\n{\n if ($user === null) {\n return;\n }\n Mail::to($user)->send(new GenericNotification($message));\n}\n\n// ❌ Null check repeated in same function\npublic function process(Order $order): void\n{\n if ($order === null) return;\n $items = $order->items;\n if ($order === null) return; // already checked above; flow can't make it null\n foreach ($items as $item) {\n if ($order === null) return; // still impossible\n // ...\n }\n}\n```\n\n```typescript\n// ❌ TS: defensive checks the compiler already enforced\n\nfunction greet(user: User): string { // `user: User` — non-null in type system\n if (!user) return ''; // dead branch; TS would error\n return `Hi, ${user.name}`;\n}\n\n// ❌ Defensive after a guard\nfunction process(user: User | null): void {\n if (!user) return; // narrows to User\n if (user) { // always true after the guard\n doStuff(user);\n }\n}\n\n// ❌ Optional chaining on a definitely-defined value\nconst userId = currentUser.id; // currentUser is non-nullable here\nconsole.log(currentUser?.email); // ?. is unnecessary\n```\n\n**Why it's slop:**\n- The dead branch is dead — never executes; reviewers must mentally rule it out\n- Signals the author didn't trust the type system\n- `findOrFail` is well-known Laravel API; checking its result for null says \"I read the docs but don't believe them\"\n- Stacking redundant guards in one function (the third example) is unmistakable\n\n## Correct\n\n```php\n// ✅ Trust findOrFail — it throws, doesn't return null\n\npublic function show(int $id): View\n{\n return view('users.show', [\n 'user' => User::findOrFail($id),\n ]);\n}\n\n// ✅ Non-nullable parameter — no check needed\npublic function notify(User $user, string $message): void\n{\n Mail::to($user)->send(new GenericNotification($message));\n}\n```\n\n```typescript\n// ✅ Trust the types\nfunction greet(user: User): string {\n return `Hi, ${user.name}`;\n}\n\n// ✅ Single guard; TS narrows the type below\nfunction process(user: User | null): void {\n if (!user) return;\n doStuff(user); // TS knows user is User here\n}\n\n// ✅ No optional chaining when not optional\nconsole.log(currentUser.email);\n```\n\n**Why it reads human:**\n- Each line carries weight; no dead branches\n- Type system does the work, not runtime checks\n- Reader can scan past the function without ruling out impossible paths\n\n## When null checks ARE warranted\n\n- **At trust boundaries** — input from HTTP/queue/file, before the type narrows\n- **On nullable returns** from third-party libraries (e.g., `Eloquent::find()` returns `?Model`)\n- **In `try {} catch` flows** where the value might be partially constructed\n- **When the type really IS nullable** — `?User $user` parameters that the caller can leave empty\n\nThe test: **does the type say it could be null?** If yes, check. If no, trust.\n\n## Detection\n\n```bash\n# Null checks immediately after findOrFail in PHP — a recognisable AI signature\n# (Two-pass: find files with findOrFail, then look for null checks inside them.)\ngrep -rl 'findOrFail' --include='*.php' app/ | xargs grep -EnB1 'if\\s*\\(\\s*\\$[a-zA-Z_]+\\s*===?\\s*null\\s*\\)' 2>/dev/null\n\n# TS: optional chaining on values typed as non-null\n# (best caught via TS strict mode: noUncheckedIndexedAccess + strictNullChecks)\nnpx tsc --noEmit --strict\n\n# PHPStan level 8+ catches many \"always-false condition\" cases\nvendor/bin/phpstan analyse --level=9\n```\n\n**PHPStan level 9+ (level 10 is max in PHPStan 2.0) and TypeScript strict mode are your best mechanical defenses** — both will flag \"condition is always false\" or \"value is never null\". Adopt them and most of these impossible-null checks disappear from new code automatically.\n\nReference: [PHPStan rule levels](https://phpstan.org/user-guide/rule-levels) · [TS strict mode](https://www.typescriptlang.org/tsconfig#strict) · Internal: [`defensive-generic-catch`](defensive-generic-catch.md), [`defensive-missing-real`](defensive-missing-real.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5426,"content_sha256":"1d05c97463f3cfa6a300b718eecdbfde1704630d3e3b57228a9e68f856db9554"},{"filename":"rules/defensive-missing-real.md","content":"---\ntitle: Defensive in the Wrong Places — Missing the Real Defenses\nimpact: CRITICAL\nimpactDescription: \"Code looks safe but the *actual* failure modes (network, queues, races) are unprotected\"\ntags: defensive-programming, timeouts, resilience, ai-fingerprint\n---\n\n## Defensive in the Wrong Places — Missing the Real Defenses\n\n**Impact: CRITICAL (Code looks safe but the actual failure modes — network, queues, races — are unprotected)**\n\nOX Security 2025: **76% of AI-assisted PRs miss timeouts on external calls**. The same PRs are full of impossible null checks and generic try/catch. The pattern is: defensive in places that don't need it; not defensive in places that do.\n\nWhat AI often misses (the failure modes that actually take down production):\n\n- **No timeouts on outbound HTTP** — a slow third-party API hangs your request indefinitely\n- **No retry policy + jitter** — a transient blip cascades into a customer-visible failure\n- **No idempotency keys on payment/order creation** — a network retry creates duplicate charges\n- **No rate-limit checks before hitting an external API** — banned by Stripe / Shopify / etc.\n- **No circuit breaker** — keeps hammering a known-down dependency, queue backs up\n- **No locking / unique constraint** — race condition double-spends inventory\n- **No backpressure on workers** — queue grows unbounded; OOM\n\nThese are the defenses that matter. A robust system has these; the dramatic-looking try/catch and null checks don't replace them.\n\n## Incorrect\n\n```php\n// ❌ Defensive theatre: try/catch wraps the wrong thing; real risks unprotected\n\npublic function syncOrder(string $orderId): void\n{\n try {\n // No timeout — could hang for minutes\n $response = Http::get(\"https://api.shopify.com/orders/{$orderId}\");\n $data = $response->json();\n\n if ($data === null) { // impossible — Http returns array or throws\n Log::error('Sync failed');\n return;\n }\n\n // No idempotency — if this retries, we create duplicates\n Order::create($data);\n } catch (Exception $e) {\n Log::error('Sync failed: ' . $e->getMessage());\n }\n}\n```\n\n```typescript\n// ❌ Same pattern in TS\nasync function syncOrder(orderId: string): Promise\u003cvoid> {\n try {\n // No timeout; no retry; no backoff\n const res = await fetch(`https://api.shopify.com/orders/${orderId}`);\n const data = await res.json();\n\n if (!data) { // impossible if res.json() resolved\n console.error('Sync failed');\n return;\n }\n\n await db.orders.create({ data });\n } catch (e) {\n console.error(e); // swallow\n }\n}\n```\n\n**Why it's slop:**\n- The `if ($data === null)` is dead defence; the actual failure mode is \"Shopify takes 90s to respond and our request timeout hits us first\"\n- The try/catch swallows real failures but doesn't add the things that prevent them\n- Retried calls create duplicate orders (idempotency missing)\n- Looks responsible; isn't\n\n## Correct\n\n```php\n// ✅ The real defences — timeouts, idempotency, retry with backoff, circuit-break\n\npublic function syncOrder(string $orderId): void\n{\n $response = Http::timeout(10) // hard timeout\n ->retry(times: 3, sleepMilliseconds: 200, when: fn ($e) =>\n $e instanceof ConnectionException || $e->getCode() === 429)\n ->get(\"https://api.shopify.com/orders/{$orderId}\");\n\n $response->throw(); // throw on 4xx/5xx — let it propagate\n\n Order::updateOrCreate(\n ['shopify_id' => $orderId], // idempotency via unique key\n $response->json()\n );\n}\n```\n\n```typescript\n// ✅ TS with AbortController for timeout + retry policy\nasync function syncOrder(orderId: string): Promise\u003cvoid> {\n const data = await fetchWithRetry(\n `https://api.shopify.com/orders/${orderId}`,\n { timeoutMs: 10_000, retries: 3, retryOnStatus: [429, 502, 503, 504] },\n );\n\n // Idempotent upsert by external id (unique index on shopify_id)\n await db.orders.upsert({\n where: { shopify_id: orderId },\n create: data,\n update: data,\n });\n}\n```\n\n**Why it reads human:**\n- The actual failure modes (slow third party, transient errors, duplicate retries) are each addressed\n- Errors NOT covered by retry propagate — observability/alerts catch them\n- The idempotency key (`shopify_id`) prevents duplicate orders even under retry storms\n- The \"try/catch\" is gone — it added no value here\n\n## The real-defences checklist\n\nFor any code that talks to the outside world, ask:\n\n- [ ] **Timeout** on every outbound call (HTTP, DB, queue, cache)?\n- [ ] **Retry** policy: how many, with what backoff, on which error types?\n- [ ] **Idempotency**: if the call is retried, will it create duplicate side effects?\n- [ ] **Rate limit** awareness: am I tracking my own request rate or relying on the upstream's error?\n- [ ] **Circuit breaker** for known-down dependencies (or at least a backoff cap)?\n- [ ] **Locking / unique constraints** on writes that could race?\n- [ ] **Backpressure** on workers reading from a queue?\n- [ ] **Real errors propagated** to observability instead of swallowed?\n\nA try/catch that doesn't add any of these isn't a defense. It's theatre.\n\n## Detection\n\n```bash\n# HTTP calls without explicit timeout (Laravel Http facade)\ngrep -rEn 'Http::get\\(|Http::post\\(|Http::put\\(|Http::delete\\(' --include='*.php' app/ \\\n | grep -v 'timeout('\n\n# fetch() calls without AbortController / signal\ngrep -rEn 'await fetch\\(' --include='*.ts' --include='*.tsx' --include='*.js' src/ \\\n | grep -vE 'signal:|AbortController'\n\n# create() / insert() without idempotency check (rough — manual review)\ngrep -rEn '(Order|Charge|Payment)::create\\(' --include='*.php' app/\n```\n\nA repo can pass every other rule in this skill and still ship the wrong defenses. **This rule is the one to take seriously on payment, checkout, and integration code paths.**\n\nReference: [Stripe — Idempotent Requests](https://docs.stripe.com/api/idempotent_requests) · [Hystrix / Resilience4j circuit breaker patterns](https://github.com/resilience4j/resilience4j) · OX Security study · Internal: [`defensive-generic-catch`](defensive-generic-catch.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6229,"content_sha256":"d31750f6380be9c6b8b5a9acddae220cc1ea0406a16d6ba5ef06bc18217a70ce"},{"filename":"rules/naming-generic-placeholders.md","content":"---\ntitle: Generic Placeholder Names\nimpact: CRITICAL\nimpactDescription: \"Reduces every variable to 'a thing' — kills the most powerful form of self-documentation\"\ntags: naming, generic-names, ai-fingerprint\n---\n\n## Generic Placeholder Names\n\n**Impact: CRITICAL (Reduces every variable to 'a thing' — kills the most powerful form of self-documentation)**\n\n`data`, `result`, `info`, `temp`, `value`, `item`, `helper`, `manager`, `utils` — these are the names a model picks when it doesn't know what the value represents in the domain. They pass linters, type checks, every metric. They cost the reader real attention to track (\"which `data` is this?\").\n\nNames are the cheapest, highest-leverage form of self-documentation in any codebase. A model that names every intermediate variable `data` or `result` is signalling it didn't think about the domain.\n\n## Incorrect\n\n```php\n// ❌ Every variable is generic\n\npublic function exportUsers(): Collection\n{\n $data = User::all();\n\n $result = [];\n foreach ($data as $item) {\n $info = [\n 'name' => $item->name,\n 'email' => $item->email,\n ];\n $result[] = $info;\n }\n\n return collect($result);\n}\n```\n\n```typescript\n// ❌ Same pattern\nasync function fetchOrders(userId: string) {\n const data = await api.getOrders(userId);\n const result = data.map(item => {\n const info = {\n id: item.id,\n total: item.total,\n };\n return info;\n });\n return result;\n}\n```\n\n**Why it's slop:**\n- Every variable is \"a thing\"; reader has no anchor to the domain\n- `$item` inside a loop over `$data` is doubly opaque — what kind of item? what kind of data?\n- A reader hitting line 50 of this file can't tell what `$result` holds without tracing the whole function\n- A human author who lived with this code would use domain words\n\n## Correct\n\n```php\n// ✅ Names tell you what the values are in the domain\n\npublic function exportUsers(): Collection\n{\n return User::all()\n ->map(fn (User $user) => [\n 'name' => $user->name,\n 'email' => $user->email,\n ]);\n}\n```\n\n```typescript\n// ✅ Domain words everywhere\nasync function fetchOrders(userId: string): Promise\u003cOrderSummary[]> {\n const orders = await api.getOrders(userId);\n return orders.map(order => ({\n id: order.id,\n total: order.total,\n }));\n}\n```\n\n**Why it reads human:**\n- A reader on any line knows the domain object in scope\n- Domain names compose — `orders.map(order => …)` reads as a sentence\n- The original `$data → $item → $info → $result` chain collapses into one expression because the names made the intermediate variables unnecessary\n\n## When generic names ARE OK\n\nA handful of names are conventional, short-scope, and fine:\n\n- **`i`, `j`, `k`** — loop indices in a 3-line `for`\n- **`x`, `y`, `z`** — math/geometry (coordinates)\n- **`_`** — explicit \"ignored value\"\n- **`acc`** — accumulator in a `reduce` callback\n- **`prev`, `next`** — in middleware / chained handlers where they're language conventions\n- **`req`, `res`** — Express handlers (don't fight a framework convention)\n\nIf the value flows through 5+ lines or escapes the immediate function, use a domain name.\n\n## Detection\n\n```bash\n# Bare local-variable declarations using generic names (PHP)\ngrep -rEn '\\$(data|result|info|temp|item|helper|value)\\b' --include='*.php' app/ | wc -l\n\n# TS/JS — generic const/let\ngrep -rEn '\\b(const|let)\\s+(data|result|info|temp|helper|value|item)\\b' --include='*.ts' --include='*.tsx' --include='*.js' src/ | wc -l\n\n# Density signal: files with > 10 generic-name hits\n# (in a 200-line file, 10+ generic names is suspicious)\n```\n\nThere's no fully automatic detector — judgment is required. The signal is **density**: a few are fine, a thicket is a slop fingerprint.\n\nReference: [Clean Code (Robert C. Martin) — Chapter 2: Meaningful Names](https://www.oreilly.com/library/view/clean-code/9780136083238/) · Internal: [`naming-over-descriptive`](naming-over-descriptive.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4015,"content_sha256":"bf2f2a9b0a261860c8484ff7cc8d5bb04ced5e6b4fc9d5276b41c68040466188"},{"filename":"rules/naming-over-descriptive.md","content":"---\ntitle: Over-Descriptive Run-On Names\nimpact: HIGH\nimpactDescription: \"Inflated identifiers that read like English instead of code\"\ntags: naming, verbose, ai-fingerprint\n---\n\n## Over-Descriptive Run-On Names\n\n**Impact: HIGH (Inflated identifiers that read like English instead of code)**\n\nThe opposite failure mode of generic names: instead of `user`, the AI produces `theUserWhoIsCurrentlyLoggedIn`. Instead of `total`, `calculateTotalAmountFromItemsList`. The identifier becomes a sentence. Real engineers know that *context* (function scope, parameter types, surrounding code) carries half the meaning — you don't need to encode all of it in every variable name.\n\nA name should be **as long as needed and no longer**. Once context establishes \"we're inside `processOrder`\", calling the order variable `order` is fine. Calling it `theOrderThatIsCurrentlyBeingProcessed` is slop.\n\n## Incorrect\n\n```php\n// ❌ Identifiers read as English\n\npublic function processOrderForCheckout(\n OrderThatIsCurrentlyBeingProcessed $theOrderToProcess\n): ProcessingResultObjectContainingStatus {\n $theCurrentlyLoggedInUserWhoIsCheckingOut = $theOrderToProcess->getUserWhoPlacedTheOrder();\n $theTotalAmountThatNeedsToBeCharged = $this->calculateTotalAmountFromItemsListInTheOrder($theOrderToProcess);\n\n return new ProcessingResultObjectContainingStatus(\n status: 'success',\n amountThatWasCharged: $theTotalAmountThatNeedsToBeCharged,\n );\n}\n```\n\n```typescript\n// ❌ Same in TS\nasync function fetchTheUserDataObjectForTheUserWithTheGivenId(\n theIdOfTheUserToFetch: string,\n): Promise\u003cTheUserDataResponseObject> {\n const theResponseFromTheAPI = await api.getUser(theIdOfTheUserToFetch);\n return theResponseFromTheAPI;\n}\n```\n\n**Why it's slop:**\n- Function signatures span multiple lines because identifiers eat the line budget\n- Reader has to scan past redundant words (\"theOrderToProcess\" — yes, it's the parameter, of course we're processing it)\n- \"The\" prefix is meaningless — what else would it be?\n- Inflates token count without conveying anything new\n\n## Correct\n\n```php\n// ✅ Tight; context carries the obvious\n\npublic function checkout(Order $order): ProcessingResult\n{\n $user = $order->user;\n $total = $this->totalFor($order);\n\n return new ProcessingResult(status: 'success', charged: $total);\n}\n```\n\n```typescript\n// ✅ Tight TS\nasync function fetchUser(id: string): Promise\u003cUser> {\n return api.getUser(id);\n}\n```\n\n**Why it reads human:**\n- The function is named `checkout`; obviously the parameter is the order being checked out — no need to spell that out\n- Function fits on one line; reader takes it in at a glance\n- Domain words (`Order`, `User`, `ProcessingResult`) replace English sentences\n\n## Guidelines\n\n| Scope | Reasonable length |\n|---|---|\n| Loop index | 1 char (`i`) |\n| Block-local (\u003c 10 lines) | 1 word (`user`, `total`) |\n| Function-local | 1–2 words (`pricedItems`, `taxedTotal`) |\n| Public API parameter | 2–3 words when domain requires (`stripeChargeId`, `webhookPayload`) |\n| Constants / config keys | Domain-precise (`MAX_STRIPE_AMOUNT_CENTS`) — those CAN be long |\n| Class names | 1–3 words (`OrderService`, `RefundCalculator`) |\n\nThe rule of thumb: **if you can drop a word and meaning survives, drop it.**\n\n## Detection\n\n```bash\n# Identifiers with 4+ camelCase words (heuristic; tune to taste).\n# Note: grep -E (ERE) does NOT support (?:...) non-capturing groups; use a capturing group.\ngrep -rEn '[a-z]+([A-Z][a-z]+){3,}' --include='*.ts' --include='*.tsx' --include='*.js' src/ \\\n | head -30\n\n# PHP equivalent (variables / method names)\ngrep -rEn '\\$[a-z][a-zA-Z]{20,}' --include='*.php' app/ | head -30\ngrep -rEn 'function [a-z][a-zA-Z]{30,}\\(' --include='*.php' app/ | head -30\n\n# Names starting with \"the\" — a strong AI tell\ngrep -rEnH '\\b(theUser|theOrder|theData|theResult|theItem|theCurrent)' \\\n --include='*.php' --include='*.ts' --include='*.tsx' app/ src/\n```\n\nReference: [Clean Code — Chapter 2](https://www.oreilly.com/library/view/clean-code/9780136083238/) · Internal: [`naming-generic-placeholders`](naming-generic-placeholders.md) (the opposite failure mode)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4160,"content_sha256":"dfa059e3798c7dd136e89ac7642efb058c1b6374e1e661175663207e58492747"},{"filename":"rules/naming-suffix-abuse.md","content":"---\ntitle: Suffix Abuse — *Helper, *Manager, *Util, *Wrapper, *Processor\nimpact: HIGH\nimpactDescription: \"Catch-all suffixes signal undecided design and pad code with empty abstractions\"\ntags: naming, suffixes, ai-fingerprint, abstractions\n---\n\n## Suffix Abuse — *Helper, *Manager, *Util, *Wrapper, *Processor\n\n**Impact: HIGH (Catch-all suffixes signal undecided design and pad code with empty abstractions)**\n\n`UserHelper`, `OrderManager`, `DataUtil`, `RequestWrapper`, `PaymentProcessor` — these suffixes are the AI's go-to when it doesn't know what to call something. The suffix is non-information: every class is in some sense a \"manager\" or \"helper\" or \"util\". The name describes the noun-form of \"code I had to put somewhere\".\n\nWhen a model generates these, it's because:\n- The \"Helper\" class is doing things that belong on the model\n- The \"Manager\" is a procedural blob masquerading as a class\n- The \"Util\" is a kitchen sink that should be split or absorbed into domain types\n\nReal domain code uses domain names: `Pricing`, `RefundPolicy`, `WebhookVerifier` — not `PricingHelper`, `RefundManager`, `WebhookProcessor`.\n\n## Incorrect\n\n```php\n// ❌ Suffix abuse — every concern wrapped in *Helper or *Manager\n\nclass UserHelper\n{\n public function formatName(User $user): string {\n return $user->firstName . ' ' . $user->lastName;\n }\n}\n\nclass OrderManager\n{\n public function processOrder(Order $order): void {\n // ...\n }\n}\n\nclass PaymentProcessor\n{\n public function processPayment(Order $order, string $token): Charge {\n // ...\n }\n}\n\nclass DataUtil\n{\n public static function arrayToCsv(array $rows): string { /* ... */ }\n public static function csvToArray(string $csv): array { /* ... */ }\n public static function snakeToCamel(string $s): string { /* ... */ }\n public static function camelToSnake(string $s): string { /* ... */ }\n // …grows forever\n}\n```\n\n```typescript\n// ❌ Same TS pattern\nclass StringHelper {\n static capitalize(s: string): string { /* ... */ }\n static slugify(s: string): string { /* ... */ }\n}\n\nclass ResponseWrapper {\n constructor(private res: ApiResponse) {}\n getData() { return this.res.data; }\n}\n```\n\n**Why it's slop:**\n- `UserHelper.formatName(user)` could just be `user.fullName` on the User model\n- `OrderManager.processOrder(order)` is two ways of saying the same thing\n- `DataUtil` is a kitchen sink — every utility ends up here, none have a clear home\n- The suffix doesn't add information; it admits the author didn't decide what the class is\n\n## Correct\n\n```php\n// ✅ Behaviour lives on the domain type; helpers split by concern\n\nclass User extends Model\n{\n public function getFullNameAttribute(): string\n {\n return \"{$this->firstName} {$this->lastName}\";\n }\n}\n\n// Action class — replaces \"OrderManager.processOrder\"\nfinal class PlaceOrder\n{\n public function __invoke(Order $order, PaymentIntent $payment): void { /* ... */ }\n}\n\n// Domain class with a clear single responsibility — replaces \"PaymentProcessor\"\nfinal class StripePaymentGateway implements PaymentGateway\n{\n public function charge(Money $amount, string $token): Charge { /* ... */ }\n}\n\n// CSV becomes its own type — replaces the kitchen-sink DataUtil\nfinal class CsvExporter\n{\n public function export(iterable $rows, array $headers): string { /* ... */ }\n}\n```\n\n```typescript\n// ✅ Methods on the domain type, or named domain functions\nclass User {\n get fullName(): string { return `${this.firstName} ${this.lastName}`; }\n}\n\n// Free function — capitalize doesn't need a class wrapper\nexport function capitalize(s: string): string { /* ... */ }\nexport function slugify(s: string): string { /* ... */ }\n```\n\n**Why it reads human:**\n- Each class has a specific domain responsibility (CsvExporter exports CSVs; PlaceOrder places orders)\n- Behaviour-on-data is on the type that holds the data (`user.fullName`)\n- Free functions stand on their own when there's no state to wrap\n\n## When suffixes ARE okay\n\nA handful of suffixes carry real meaning in their conventions:\n\n- **`*Repository`** — DDD-style data access (specific contract)\n- **`*Service`** — used sparingly for orchestration that doesn't fit on a domain type\n- **`*Controller`** — HTTP entry point (Laravel/Express convention)\n- **`*Middleware`** — request pipeline (Express/Laravel convention)\n- **`*Gateway`** — external integration boundary (Stripe, AWS)\n- **`*Listener`** / `*Observer` — event-handler conventions (Laravel)\n\n`*Helper`, `*Manager`, `*Util`, `*Wrapper`, `*Processor` are the suspect tier.\n\n## Detection\n\n```bash\n# Class declarations with suspect suffixes\ngrep -rEn 'class\\s+[A-Z][a-zA-Z]*(Helper|Manager|Util|Utils|Wrapper|Processor|Handler)\\b' \\\n --include='*.php' --include='*.ts' --include='*.tsx' app/ src/\n\n# Count by suffix to size the problem\ngrep -rE 'class\\s+[A-Z][a-zA-Z]*' --include='*.php' --include='*.ts' app/ src/ \\\n | grep -oE '(Helper|Manager|Util|Utils|Wrapper|Processor)\\b' \\\n | sort | uniq -c | sort -rn\n```\n\nA repo with **5+ `*Helper`** or **3+ `*Manager`** classes is almost certainly leaking domain logic into catch-all classes. Refactor by asking \"what would I call this if I couldn't use the suffix?\"\n\nReference: [Clean Code — Chapter 2: Meaningful Names](https://www.oreilly.com/library/view/clean-code/9780136083238/) · Internal: [`over-eng-single-method-class`](over-eng-single-method-class.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5424,"content_sha256":"1fb2e0245b472e816ec3b81c9fa6063409f6b4684d7c7551f6a295353af649c7"},{"filename":"rules/naming-type-in-name.md","content":"---\ntitle: Type Encoded in Variable Names\nimpact: MEDIUM\nimpactDescription: \"Embeds type in the identifier when the type system already does it; ages badly with refactors\"\ntags: naming, hungarian, type-in-name, ai-fingerprint\n---\n\n## Type Encoded in Variable Names\n\n**Impact: MEDIUM (Embeds type in the identifier when the type system already does it; ages badly with refactors)**\n\n`userObject`, `resultArray`, `stringData`, `listOfItems`, `userIdString`, `dataMap` — these names encode the *type* of the value, redundantly with the actual type declaration. Both PHP 8+ and TypeScript have rich type systems that show the type in the signature. The name should encode what the value **means**, not what its shape is.\n\nThe pattern is a tell because it suggests the author optimised for \"obvious from any one line\" rather than \"obvious in context\". A human author trusts the reader to look at the signature.\n\n## Incorrect\n\n```php\n// ❌ Type-in-name everywhere\n\npublic function processItemsList(array $itemsArray, int $userIdInteger): array\n{\n $resultArray = [];\n foreach ($itemsArray as $itemObject) {\n $priceFloat = $itemObject->priceFloat;\n $resultArray[] = $priceFloat;\n }\n return $resultArray;\n}\n```\n\n```typescript\n// ❌ Same in TS\nfunction getUserDataObject(userIdString: string): UserDataResponseObject {\n const responseObject = await api.fetch(userIdString);\n const dataArray = responseObject.dataArray;\n const userObjectList = dataArray.map((itemObject: UserObject) => itemObject);\n return userObjectList;\n}\n```\n\n**Why it's slop:**\n- The type is declared on the parameter and visible in the signature\n- Renaming `$itemsArray` to a `Collection` requires updating every variable reference\n- The name `$itemObject` is redundant — the static type says it's a `User`\n- Pattern is a clear \"tutorial-blog\" signature; teammates don't write like this\n\n## Correct\n\n```php\n// ✅ Names encode meaning, not shape\n\npublic function totalsFor(array $items, int $userId): array\n{\n return array_map(fn (Item $item) => $item->price, $items);\n}\n```\n\n```typescript\n// ✅ Domain words, types in the signature\nasync function fetchUser(userId: string): Promise\u003cUser> {\n const { data } = await api.fetch(userId);\n return data;\n}\n```\n\n**Why it reads human:**\n- Each name describes the role (`items`, `userId`, `total`) not the shape\n- Type info is in the type system, where refactors update it automatically\n- If `items` becomes a `Collection\u003cItem>`, the variable name still reads correctly\n\n## When type-in-name IS okay\n\nConventions where the type is part of the meaning:\n\n- **`*Id`** suffix — `userId` (distinguishes from `user` which is the whole object) ✓\n- **`*Count`** suffix — `userCount` ✓\n- **`is*`** / **`has*`** / **`can*`** booleans — `isPaid`, `hasShipping`, `canEdit` ✓\n- **Maps with two-type names** — `usersByEmail` (carries both types in one word) ✓\n- **DTO classes** — `UserResponse`, `OrderPayload` (clarifies \"this is the wire form, not the domain model\") ✓\n\nThe suspect patterns are `*Object`, `*Array`, `*List`, `*Map`, `*String`, `*Integer`, `*Float` on local variables.\n\n## Detection\n\n```bash\n# Suspect type-suffix names on variables (PHP)\ngrep -rEn '\\$[a-z][a-zA-Z]*?(Object|Array|List|Map|String|Integer|Float|Boolean|Bool)\\b' \\\n --include='*.php' app/\n\n# TS / JS\ngrep -rEn '\\b(const|let)\\s+[a-z][a-zA-Z]*?(Object|Array|List|Map|String|Boolean)\\b' \\\n --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' src/\n```\n\nA few hits are fine (especially in test data setups). **Density across multiple files** is the signal — a project with 30+ `*Array` / `*Object` variables almost certainly came from a model.\n\nReference: [Clean Code — Chapter 2](https://www.oreilly.com/library/view/clean-code/9780136083238/) · [Joel Spolsky — Making Wrong Code Look Wrong](https://www.joelonsoftware.com/2005/05/11/making-wrong-code-look-wrong/) (the \"useful Hungarian\" original — different from this rule's anti-pattern) · Internal: [`naming-over-descriptive`](naming-over-descriptive.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4068,"content_sha256":"2aac303101755b8507edffb9c3ce813f894759e438ff795c98b775d10f248d91"},{"filename":"rules/over-eng-dependency-creep.md","content":"---\ntitle: Dependency Creep — New Library When Existing One Suffices\nimpact: HIGH\nimpactDescription: \"Inflates bundle/install size, adds CVE surface, signals model picked training-data favourites\"\ntags: over-engineering, dependencies, dependency-creep, ai-fingerprint\n---\n\n## Dependency Creep — New Library When Existing One Suffices\n\n**Impact: HIGH (Inflates bundle/install size, adds CVE surface, signals model picked training-data favourites)**\n\nAI tends to introduce a new dependency whenever a problem matches a library it has seen in training, even when the project already includes a dependency that solves the same problem. Example signals:\n\n- Adding `date-fns` to a project that already uses `dayjs`\n- Adding `axios` to a project that already imports `fetch` everywhere\n- Adding `lodash` to a project that already has `lodash-es` (or uses native ES methods)\n- Adding `uuid` when the project already uses `nanoid`\n- Adding `winston` when the app uses `pino`\n- Adding `joi` / `yup` to a TS project that already uses `zod`\n- Adding `bcrypt` (the npm package) in a Laravel project where `Hash::make` handles it\n- Adding `moment` in a project that just removed `moment` last quarter\n\nEach new dependency:\n- Adds to install / bundle size\n- Adds a new CVE surface (every `npm audit` lights up)\n- Forces the team to maintain TWO libraries for the same concern, indefinitely\n- Drifts as the LLM picks whichever was popular in the training-data slice it sampled from\n\n## Incorrect\n\n```json\n// ❌ package.json — two libs doing the same job\n{\n \"dependencies\": {\n \"dayjs\": \"^1.11.10\", // already in use\n \"date-fns\": \"^3.0.0\", // ADDED — same purpose\n \"axios\": \"^1.6.0\", // ADDED — but app uses fetch everywhere\n \"lodash\": \"^4.17.21\", // ADDED — lodash-es already present\n \"lodash-es\": \"^4.17.21\"\n }\n}\n```\n\n```php\n// ❌ composer.json — two HTTP clients\n{\n \"require\": {\n \"guzzlehttp/guzzle\": \"^7.8\",\n \"symfony/http-client\": \"^7.0\" // ADDED — same purpose\n }\n}\n```\n\n**Why it's slop:**\n- Both libraries get pulled into every install\n- New `dayjs` code still gets written alongside the new `date-fns` code — drift forever\n- Two CVE feeds to track\n- Reviewer didn't notice because each PR seems reasonable in isolation\n\n## Correct\n\n```bash\n# ✅ Use the existing dep; reject the PR adding the new one with a note\n\n# Before merging a PR that adds a new dependency, ask:\n# 1. Does an existing dependency in package.json/composer.json already do this?\n# 2. Does a native browser/Node/PHP API already do this?\n# 3. Is the new dep > 50KB gzipped (or > 1MB unpacked) for a single function call?\n# 4. Does it have CVEs / abandoned upstream?\n#\n# If any answer is \"yes\" or \"maybe\", reject and use the existing tool.\n```\n\nCommon collisions and the canonical choice:\n\n| Concern | Pick one |\n|---|---|\n| Date / time | `dayjs` OR `date-fns` (not both) |\n| HTTP client | `fetch` OR `axios` (not both) |\n| UUID | `uuid` OR `nanoid` (not both) |\n| Schema validation (TS) | `zod` (preferred) — reject `joi`/`yup`/`ajv` additions |\n| HTTP client (PHP) | `guzzlehttp/guzzle` (Laravel default) — reject `symfony/http-client` unless project-wide |\n| Logging | `monolog` (Laravel default) for PHP; pick one of `pino`/`winston` for Node |\n| Form validation (Laravel) | Built-in `FormRequest` — reject standalone validation libs |\n\n## Detection\n\n```bash\n# Node: dependency-list audit\nnode -e \"\nconst pkg = require('./package.json');\nconst all = { ...pkg.dependencies, ...pkg.devDependencies };\nconst dupes = [\n ['dayjs', 'date-fns', 'moment', 'luxon'],\n ['axios', 'node-fetch', 'got', 'superagent'],\n ['uuid', 'nanoid', 'cuid'],\n ['lodash', 'lodash-es', 'ramda'],\n ['joi', 'yup', 'zod', 'ajv'],\n];\nfor (const group of dupes) {\n const found = group.filter(p => all[p]);\n if (found.length > 1) console.log('OVERLAP:', found.join(', '));\n}\n\"\n\n# PHP: check for HTTP-client overlap\ngrep -E '\\\"(guzzlehttp/guzzle|symfony/http-client|kriswallsmith/buzz)\\\"' composer.json\n```\n\nWhen a new dep lands in a PR, the PR author should justify why the existing options don't fit. \"AI suggested this\" is not a justification.\n\nReference: [`technical-debt`'s `deps-unused-deps`](../technical-debt/rules/deps-unused-deps.md) (the broader audit) · [BundlePhobia](https://bundlephobia.com/) (size impact for npm packages)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4382,"content_sha256":"7de469ce52491a9755a639cb2a760862a7133e9719bf1a0d3a7405053bdd8771"},{"filename":"rules/over-eng-premature-interface.md","content":"---\ntitle: Premature Interface (One Implementation, No Plan for a Second)\nimpact: HIGH\nimpactDescription: \"Adds indirection nobody asked for; doubles the surface for every change\"\ntags: over-engineering, interfaces, abstractions, ai-fingerprint\n---\n\n## Premature Interface (One Implementation, No Plan for a Second)\n\n**Impact: HIGH (Adds indirection nobody asked for; doubles the surface for every change)**\n\nAI defaults to \"enterprise-grade\" because its training distribution is enterprise-grade. The result: every service gets an interface and one implementation, with no second implementation in the foreseeable future. The interface is pure ceremony — every change updates two files, every test mocks the interface, every reader chases two clicks to find the actual code.\n\n**Interfaces earn their place when:**\n- There IS a second implementation (e.g., `StripeGateway` and `AdyenGateway`)\n- There IS a planned second implementation (in this quarter's roadmap)\n- The implementation is being swapped in tests with a fake (and even then, dependency injection of the concrete type is often fine)\n\n**Otherwise: write the concrete type. Add the interface when a second implementation actually shows up.** YAGNI is real.\n\n## Incorrect\n\n```php\n// ❌ Interface with exactly one implementation, no plan for a second\n\ninterface UserRepositoryInterface\n{\n public function find(int $id): ?User;\n public function save(User $user): void;\n public function delete(int $id): void;\n}\n\nfinal class UserRepository implements UserRepositoryInterface\n{\n public function find(int $id): ?User { /* ... */ }\n public function save(User $user): void { /* ... */ }\n public function delete(int $id): void { /* ... */ }\n}\n\n// Binding in a service provider, mostly to satisfy the interface\n$this->app->bind(UserRepositoryInterface::class, UserRepository::class);\n```\n\n```typescript\n// ❌ Same TS pattern\ninterface IOrderService {\n placeOrder(payload: OrderPayload): Promise\u003cOrder>;\n cancelOrder(orderId: string): Promise\u003cvoid>;\n}\n\nclass OrderService implements IOrderService {\n async placeOrder(payload: OrderPayload): Promise\u003cOrder> { /* ... */ }\n async cancelOrder(orderId: string): Promise\u003cvoid> { /* ... */ }\n}\n```\n\n**Why it's slop:**\n- Every method declared in two files\n- `find` / `save` / `delete` change — both interface AND implementation must be updated; nothing prevents drift\n- Reader hits `UserRepositoryInterface` in a controller, has to navigate through to find the actual code\n- The `I*` prefix or `*Interface` suffix is itself a tell — clean naming uses domain words without the marker\n\n## Correct\n\n```php\n// ✅ Concrete class until you need a second implementation\n\nfinal class UserRepository\n{\n public function find(int $id): ?User { /* ... */ }\n public function save(User $user): void { /* ... */ }\n public function delete(int $id): void { /* ... */ }\n}\n\n// Controller depends on the concrete class — Laravel auto-resolves it\npublic function show(int $id, UserRepository $users): UserResource\n{\n $user = $users->find($id) ?? throw new NotFoundException;\n return new UserResource($user);\n}\n```\n\n```typescript\n// ✅ Concrete class\nclass OrderService {\n async placeOrder(payload: OrderPayload): Promise\u003cOrder> { /* ... */ }\n async cancelOrder(orderId: string): Promise\u003cvoid> { /* ... */ }\n}\n```\n\n**When a second implementation arrives** — e.g., you need a `CachedUserRepository` — *then* extract the interface. By that point you'll know what shape it should have because you have two concrete examples.\n\n**Why it reads human:**\n- Concrete class is reachable in one click\n- No double-maintenance\n- Adding a second implementation is a deliberate refactor, not a default scaffold\n\n## Tests: don't create an interface just to mock\n\nIf you need to swap the dependency in tests, prefer:\n\n1. **Inject the concrete class; instantiate with fakes** (e.g., `UserRepository` with an in-memory `Database` connection)\n2. **Use Laravel's container-bound fake** (`$this->mock(UserRepository::class, ...)`) — no interface needed\n3. **Method-level mocks** via PHPUnit / Vitest\n\nAn interface created *only* to mock the dependency in tests is also slop. Real interfaces serve production code paths.\n\n## Detection\n\n```bash\n# Interfaces in the codebase\ngrep -rn '^interface\\s\\|^export interface' --include='*.php' --include='*.ts' app/ src/ \\\n | wc -l\n\n# For each interface, count implementations — flag interfaces with exactly 1\n# (manual review with `phpstorm` or VSCode Go to Implementation)\n\n# PHP heuristic: interfaces with 'Interface' suffix or 'I*' prefix\ngrep -rEn '^interface\\s+(I[A-Z]|[A-Z][a-zA-Z]+Interface)\\b' --include='*.php' app/\n\n# TS heuristic\ngrep -rEn '^export interface I[A-Z]|^interface I[A-Z]' --include='*.ts' --include='*.tsx' src/\n```\n\nReference: [Martin Fowler — YAGNI](https://martinfowler.com/bliki/Yagni.html) · [Sandi Metz — The Wrong Abstraction](https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction) · Internal: [`over-eng-useless-wrapper`](over-eng-useless-wrapper.md), [`naming-suffix-abuse`](naming-suffix-abuse.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5091,"content_sha256":"f968f6b4014f6f779abda98a0cf5757ea05b00d5931504384cad70dffc43d4ac"},{"filename":"rules/over-eng-single-method-class.md","content":"---\ntitle: Single-Method Class That Should Be a Function\nimpact: HIGH\nimpactDescription: \"Wraps a free function in class ceremony for no reason\"\ntags: over-engineering, classes, ai-fingerprint\n---\n\n## Single-Method Class That Should Be a Function\n\n**Impact: HIGH (Wraps a free function in class ceremony for no reason)**\n\nA class with exactly one public method, no state, and no dependencies should usually be a function. AI generates these because its training data is enterprise Java/C# where classes are mandatory for everything. PHP and TypeScript both support free functions / static methods / single-purpose action classes — the wrapping ceremony is pure slop.\n\nThe exception: **invokable action classes** (Laravel convention) using `__invoke()` are legitimate when they have constructor-injected dependencies and represent a named domain action. The slop variant is the class with no constructor, no state, one static method, and no clear domain identity.\n\n## Incorrect\n\n```php\n// ❌ Class wrapping a single pure function\n\nclass StringFormatter\n{\n public static function slugify(string $input): string\n {\n return Str::slug($input);\n }\n}\n\nclass EmailValidator\n{\n public function isValid(string $email): bool\n {\n return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;\n }\n}\n\nclass TimestampHelper\n{\n public static function toIso(DateTime $dt): string\n {\n return $dt->format(DateTimeInterface::ATOM);\n }\n}\n```\n\n```typescript\n// ❌ Same in TS\nclass StringUtil {\n static capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1);\n }\n}\n\nclass ResponseFormatter {\n static format(data: unknown): ApiResponse {\n return { data, timestamp: Date.now() };\n }\n}\n```\n\n**Why it's slop:**\n- Callers write `StringFormatter::slugify(...)` instead of `slugify(...)`\n- `new EmailValidator()->isValid($email)` is six tokens for what should be one\n- The class adds no encapsulation (no state to encapsulate)\n- Importing/autoloading the class wastes bytes for zero benefit\n- Pattern is recognisably \"Java port\" — PHP and TS have first-class functions\n\n## Correct\n\n```php\n// ✅ Free function or method on the existing domain type\n\n// helpers.php (or composer autoload \"files\")\nfunction slugify(string $input): string\n{\n return Str::slug($input);\n}\n\nfunction isValidEmail(string $email): bool\n{\n return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;\n}\n\n// Or extension methods on Carbon\nclass CustomCarbon extends Carbon\n{\n public function toIso(): string { return $this->format(DateTimeInterface::ATOM); }\n}\n```\n\n```typescript\n// ✅ Free exported functions\nexport function capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\n\nexport function formatResponse\u003cT>(data: T): ApiResponse\u003cT> {\n return { data, timestamp: Date.now() };\n}\n```\n\n**Why it reads human:**\n- Caller writes `slugify(input)` — one token, no ceremony\n- No autoloader hit, no class instantiation, no dependency to mock in tests\n- Functions are testable directly with no setup\n\n## Single-method class is OK when…\n\nThese are legitimate, NOT slop:\n\n```php\n// ✅ Invokable action with injected dependencies — Laravel idiom\nfinal class PlaceOrder\n{\n public function __construct(\n private PaymentGateway $payments,\n private InventoryService $inventory,\n ) {}\n\n public function __invoke(OrderRequest $request): Order { /* ... */ }\n}\n\n// ✅ Job / command — meant to be queued\nfinal class SendWeeklyDigest implements ShouldQueue\n{\n public function handle(MailerService $mailer): void { /* ... */ }\n}\n\n// ✅ Form Request — Laravel pattern\nfinal class StoreUserRequest extends FormRequest { /* ... */ }\n```\n\nThe test: **does it have state, dependencies, or a domain identity beyond \"I wrap a function\"?** If yes, it's a class. If no, it should be a function.\n\n## Detection\n\n```bash\n# PHP — classes with exactly one public method and no constructor injection\n# (rough heuristic: file has 'class X' + exactly one 'public function')\nfor f in $(find app/ -name '*.php'); do\n PUBLIC=$(grep -cE '^\\s+public function ' \"$f\")\n CTOR=$(grep -cE '^\\s+public function __construct' \"$f\")\n PROPS=$(grep -cE '^\\s+(private|protected) (readonly )?[a-zA-Z]+ \\

Code Slop Detection Taste-level review of code for AI-generated patterns. Contains 24 rules across 6 categories covering comments, naming, over-engineering, defensive overdose, test slop, and style fingerprints. Where measures quantitative code debt (complexity, duplication, CVEs), this skill measures the qualitative failure mode: code that passes every metric but reads like a tutorial blog post, not like a human wrote it. Metadata - Version: 1.0.0 - Scope: PHP / Laravel + TypeScript / React (Node) - Rule Count: 24 rules across 6 categories - License: MIT Why this skill exists Industry data o…

\"$f\")\n if [ \"$PUBLIC\" = \"1\" ] && [ \"$CTOR\" = \"0\" ] && [ \"$PROPS\" = \"0\" ]; then\n echo \"SUSPECT (single-method, no state): $f\"\n fi\ndone\n\n# TS — classes with one method and no constructor or fields\ngrep -rln 'class\\s\\+[A-Z]' --include='*.ts' src/ | while read f; do\n METHODS=$(grep -cE '^\\s+(public\\s+|private\\s+|protected\\s+)?[a-zA-Z]+\\s*\\(' \"$f\")\n CTOR=$(grep -c 'constructor' \"$f\")\n if [ \"$METHODS\" = \"1\" ] && [ \"$CTOR\" = \"0\" ]; then\n echo \"SUSPECT: $f\"\n fi\ndone\n```\n\nReference: Internal: [`naming-suffix-abuse`](naming-suffix-abuse.md), [`over-eng-useless-wrapper`](over-eng-useless-wrapper.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4872,"content_sha256":"3bfab4c7dcca4d58f5daf84b46567c4a40ebae5685a7a22ec4e81560523cebb4"},{"filename":"rules/over-eng-useless-wrapper.md","content":"---\ntitle: Useless Wrapper Functions\nimpact: HIGH\nimpactDescription: \"Adds indirection layer for no behavioural gain; callers chase one extra hop to find real code\"\ntags: over-engineering, wrappers, indirection, ai-fingerprint\n---\n\n## Useless Wrapper Functions\n\n**Impact: HIGH (Adds indirection layer for no behavioural gain; callers chase one extra hop to find real code)**\n\nA function that exists only to delegate to another function, with no added validation, transformation, or context. AI generates these because it pattern-matches on \"wrap external dependencies for testability\" but applies it indiscriminately.\n\nA wrapper earns its place when it adds **something** the underlying call doesn't: validation, default arguments, error normalisation, logging, retry logic, type narrowing. A wrapper that just forwards arguments is pure indirection.\n\n## Incorrect\n\n```php\n// ❌ Wrappers that just delegate\n\nclass UserService\n{\n public function __construct(private UserRepository $repo) {}\n\n public function findUser(int $id): ?User\n {\n return $this->repo->find($id);\n }\n\n public function deleteUser(int $id): void\n {\n $this->repo->delete($id);\n }\n\n public function createUser(array $data): User\n {\n return $this->repo->create($data);\n }\n}\n```\n\n```typescript\n// ❌ Same TS pattern\nclass StripeWrapper {\n private stripe: Stripe;\n constructor(stripe: Stripe) { this.stripe = stripe; }\n\n charge(amount: number, token: string) {\n return this.stripe.charges.create({ amount, source: token });\n }\n\n refund(chargeId: string) {\n return this.stripe.refunds.create({ charge: chargeId });\n }\n}\n```\n\n**Why it's slop:**\n- `UserService::findUser($id)` does exactly what `$repo->find($id)` does\n- Every caller now goes through TWO layers (`Service → Repo`) to find the actual logic\n- Adding a parameter requires touching both files\n- Tests of `UserService` mostly verify \"did we call the repo correctly?\" — they don't verify business behaviour\n\n## Correct\n\n```php\n// ✅ Drop the wrapper; use the underlying type directly\n\n// Controller depends on the Repository or Model directly\npublic function show(int $id, UserRepository $users): UserResource\n{\n return new UserResource($users->findOrFail($id));\n}\n```\n\n## When a wrapper IS worth keeping\n\nA wrapper earns its place when it adds something real:\n\n```php\n// ✅ Wrapper that normalises errors + adds retry — real value\nfinal class ResilientStripeGateway\n{\n public function __construct(private Stripe $stripe) {}\n\n public function charge(Money $amount, string $token): Charge\n {\n return retry(times: 3, sleepMs: 200, callback: function () use ($amount, $token) {\n try {\n return $this->stripe->charges->create([\n 'amount' => $amount->cents(),\n 'currency' => $amount->currency()->code(),\n 'source' => $token,\n ]);\n } catch (CardException $e) {\n throw new PaymentDeclined($e->getMessage(), $e); // domain exception\n }\n });\n }\n}\n```\n\nThis wrapper adds:\n- Retry on transient failures\n- Domain-specific exception (`PaymentDeclined`, not `CardException`)\n- Type-safe `Money` input rather than raw cents + currency strings\n\nThat's worth the file.\n\n**Why it reads human:**\n- Wrappers exist when they add value; non-wrappers don't pad the layer count\n- Caller can see \"this charge call is resilient\" because the type is `ResilientStripeGateway`\n- No mystery hops to chase\n\n## Detection\n\n```bash\n# Heuristic: methods that are one-liners and just call $this->dependency->method($args)\n# PHP — find public methods whose body is exactly one delegate call\ngrep -rEn -A1 '^\\s+public function [a-zA-Z]+\\([^)]*\\)' --include='*.php' app/ | \\\n awk '/public function/{name=$0; next} /^\\s+return \\$this->[a-zA-Z]+->[a-zA-Z]+\\(/{print name; print $0; print \"---\"}'\n\n# TS — same heuristic\ngrep -rEn -A1 '^\\s+(public |async )?[a-zA-Z]+\\([^)]*\\):' --include='*.ts' src/ | \\\n awk '/[a-zA-Z]+\\(/{name=$0; next} /return this\\.[a-zA-Z]+\\./{print name; print $0; print \"---\"}'\n```\n\nThe cleanest signal: **call-sites**. If a wrapper is called from exactly one place, it's almost certainly slop. Inline it.\n\n```bash\n# For each public method in Service.php, count call sites\n# (rough — use phpstorm 'Find Usages' for accurate results)\n```\n\nReference: [Sandi Metz — The Wrong Abstraction](https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction) · [Martin Fowler — Inline Function](https://refactoring.com/catalog/inlineFunction.html) · Internal: [`over-eng-single-method-class`](over-eng-single-method-class.md), [`over-eng-premature-interface`](over-eng-premature-interface.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4735,"content_sha256":"afa2359c9a599ef2625efdb1a4c57a72ae951417226679334e8e1439bef04f5a"},{"filename":"rules/style-as-any-escape.md","content":"---\ntitle: as any / @ts-ignore Escape Hatches\nimpact: HIGH\nimpactDescription: \"AI sprinkles these wherever inference is hard — defeats the type system in exactly the places it earns its value\"\ntags: style, typescript, type-safety, ai-fingerprint\n---\n\n## as any / @ts-ignore Escape Hatches\n\n**Impact: HIGH (AI sprinkles these wherever inference is hard — defeats the type system in exactly the places it earns its value)**\n\nAI's common move when TypeScript types don't line up cleanly: cast to `any`, add `@ts-ignore`, or annotate with `: any`. The escape hatch silences the compiler — and silences the protection it was about to provide. Worst case: a real bug (wrong shape, missing field, wrong arg order) survives review because the compiler stopped warning.\n\nA reviewer should treat every `as any` / `@ts-ignore` / `@ts-expect-error` as **a request for justification**. Sometimes legitimate (third-party library with bad types, complex generics, runtime-validated unknowns). Often not.\n\n## Incorrect\n\n```typescript\n// ❌ as any sprinkled to silence the compiler\n\nasync function processWebhook(payload: unknown): Promise\u003cvoid> {\n const event = (payload as any).event; // unknown → any without validation\n const orderId = (payload as any).data.order.id; // chain of unsafe access\n await db.orders.update({ where: { id: orderId }, data: { status: 'paid' } });\n}\n\n// ❌ @ts-ignore over a real type mismatch\nfunction calculateTotal(items: Item[]): number {\n // @ts-ignore — items.map sometimes returns undefined? not sure\n return items.map(i => i.price * i.quantity).reduce((a, b) => a + b, 0);\n}\n\n// ❌ : any in function signature\nfunction processData(data: any) { // gives up on the input shape\n return data.value;\n}\n\n// ❌ Mixing ts-ignore with no comment\nfunction send(): void {\n // @ts-ignore\n emailService.send(undefined); // why is undefined OK here?\n}\n```\n\n**Why it's slop:**\n- `as any` propagates — every subsequent access is also unchecked\n- A real bug (e.g., the payload's actual shape is `payload.event.data.order_id`, not `payload.data.order.id`) doesn't fail until production\n- `@ts-ignore` with no explanation is one of the most-cited AI tells in 2025 reviews\n- `: any` parameters defeat every downstream type check\n\n## Correct\n\n```typescript\n// ✅ Validate at the boundary; types flow through downstream\n\nimport { z } from 'zod';\n\nconst WebhookPayload = z.object({\n event: z.string(),\n data: z.object({\n order: z.object({ id: z.string() }),\n }),\n});\n\nasync function processWebhook(payload: unknown): Promise\u003cvoid> {\n const { event, data } = WebhookPayload.parse(payload); // typed from here on\n await db.orders.update({ where: { id: data.order.id }, data: { status: 'paid' } });\n}\n\n// ✅ Fix the real type issue rather than ignore it\nfunction calculateTotal(items: Item[]): number {\n return items.reduce((sum, i) => sum + i.price * i.quantity, 0);\n}\n\n// ✅ Type the input\nfunction processData\u003cT extends { value: string }>(data: T): string {\n return data.value;\n}\n```\n\n## When the escape hatch IS warranted\n\nLegitimate use is narrow:\n\n- **Bad third-party types** — the lib's `.d.ts` is wrong; you've filed an upstream issue. Use `@ts-expect-error` (not `@ts-ignore`) with a comment naming the issue:\n ```typescript\n // @ts-expect-error: upstream types incorrectly require `id` — see https://github.com/lib/issues/420\n ```\n- **Migration phase** — moving a JS file to TS gradually; mark debt and track removal\n- **Complex generic constraints** where the compiler can't prove correctness but humans can — annotate with a comment explaining why it's safe\n\nEvery escape hatch in production code should have an explanatory comment. No bare `// @ts-ignore`.\n\n## Detection\n\n```bash\n# Count escape hatches in the diff / repo\ngrep -rEn '\\bas any\\b' --include='*.ts' --include='*.tsx' src/ | wc -l\ngrep -rEn '@ts-ignore|@ts-nocheck' --include='*.ts' --include='*.tsx' src/ | wc -l\ngrep -rEn ': any\\b' --include='*.ts' --include='*.tsx' src/ | wc -l\n\n# Bare @ts-ignore / @ts-expect-error without an explanatory comment\ngrep -rEnA1 '@ts-(ignore|expect-error)\\s*

Code Slop Detection Taste-level review of code for AI-generated patterns. Contains 24 rules across 6 categories covering comments, naming, over-engineering, defensive overdose, test slop, and style fingerprints. Where measures quantitative code debt (complexity, duplication, CVEs), this skill measures the qualitative failure mode: code that passes every metric but reads like a tutorial blog post, not like a human wrote it. Metadata - Version: 1.0.0 - Scope: PHP / Laravel + TypeScript / React (Node) - Rule Count: 24 rules across 6 categories - License: MIT Why this skill exists Industry data o…

--include='*.ts' --include='*.tsx' src/\n\n# ESLint rules to enforce in tsconfig / .eslintrc:\n# @typescript-eslint/no-explicit-any: error\n# @typescript-eslint/ban-ts-comment: { ts-ignore: \"allow-with-description\" }\n# @typescript-eslint/no-unsafe-*: error\n```\n\nAdd to ESLint config:\n\n```json\n{\n \"rules\": {\n \"@typescript-eslint/no-explicit-any\": \"error\",\n \"@typescript-eslint/ban-ts-comment\": [\n \"error\",\n { \"ts-ignore\": \"allow-with-description\", \"ts-expect-error\": \"allow-with-description\" }\n ]\n }\n}\n```\n\n**Bar:** zero `as any` / bare `@ts-ignore` in new code. Existing ones grandfathered but tracked.\n\nReference: [TypeScript handbook — strict mode](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) · [typescript-eslint rules](https://typescript-eslint.io/rules/) · [Zod](https://zod.dev/) · Internal: [`defensive-impossible-null`](defensive-impossible-null.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5098,"content_sha256":"f4ab782b95537632d1e954acaaa401c22a2738d5e03222331cb90094a8d720b1"},{"filename":"rules/style-debug-artifacts.md","content":"---\ntitle: Debug Artifacts Left in Production Code\nimpact: HIGH\nimpactDescription: \"console.log, dd(), dump(), var_dump — AI's exploratory leftovers ship to production\"\ntags: style, debug, ai-fingerprint, cleanup\n---\n\n## Debug Artifacts Left in Production Code\n\n**Impact: HIGH (console.log, dd(), dump(), var_dump — AI's exploratory leftovers ship to production)**\n\n`console.log(\"here\")`, `console.log(\"got user\", user)`, `dd($order)`, `dump($result)`, `var_dump($payload)`, `print_r($data)`, `echo $error` — these are the breadcrumbs left from when the developer (or AI) was debugging. They ship to production and:\n\n- Leak sensitive data into stdout / log aggregators (PII, tokens)\n- Bloat production logs to the point you can't grep for real signal\n- `dd()` literally halts execution — if it reaches prod, your endpoint returns \"1\" + var_dump output instead of JSON\n\nAI is particularly bad about this because the model tends to add `console.log(\"got result\", x)` \"to help with debugging\" and rarely removes it before \"finalising\" the function.\n\n## Incorrect\n\n```typescript\n// ❌ Debug artifacts left in\n\nasync function processPayment(order: Order, token: string): Promise\u003cCharge> {\n console.log('processPayment start', order.id); // shipped\n console.log('token', token); // SHIPS THE TOKEN\n const charge = await stripe.charges.create({ /* ... */ });\n console.log('got charge', charge); // shipped\n return charge;\n}\n```\n\n```php\n// ❌ Same in PHP\npublic function processWebhook(Request $request): JsonResponse\n{\n $payload = $request->json()->all();\n dd($payload); // halts execution; returns a debug page\n // …rest never runs\n}\n\npublic function calculateTax(Order $order): Money\n{\n dump($order); // prints to stdout in production\n print_r($order->items);\n $taxRate = 0.06;\n var_dump($taxRate);\n return $order->subtotal->multiplied($taxRate);\n}\n```\n\n**Why it's slop:**\n- `dd()` in a controller is an outage — the request never completes\n- `console.log('token', token)` is a credentials leak; on serverless logs, every Stripe call ships the token to CloudWatch\n- `dump()` / `var_dump()` show up in HTTP responses if not in a CLI context (especially during `artisan tinker` or test failures)\n- A repo with 50+ stray `console.log` in production paths signals nobody is reading their own code before merging\n\n## Correct\n\n```typescript\n// ✅ No debug; if logging matters, use the proper logger with context\nimport { logger } from '@/lib/logger';\n\nasync function processPayment(order: Order, token: string): Promise\u003cCharge> {\n // Real logger — structured, redacts secrets, levels enforced\n const log = logger.child({ orderId: order.id });\n log.info('payment.start');\n\n const charge = await stripe.charges.create({ /* ... */ });\n\n log.info('payment.success', { chargeId: charge.id, amountCents: charge.amount });\n return charge;\n}\n```\n\n```php\n// ✅ Structured logging at the boundary; no debug() calls\n\npublic function processWebhook(Request $request): JsonResponse\n{\n Log::withContext([\n 'webhook_id' => $request->header('Stripe-Webhook-Id'),\n 'event_type' => $request->json('type'),\n ])->info('webhook.received');\n\n // … actual handling …\n\n return response()->json(['ok' => true]);\n}\n```\n\n**Why it reads human:**\n- A logger with structured fields and levels (info/warn/error) — not stdout spam\n- Tokens / secrets get redacted by the logger (or not logged at all)\n- The log lines are intentional, useful for production debugging, and won't break the response\n\n## When debug calls in production code ARE warranted\n\nRare. Usually zero. Specific cases:\n\n- **Logs in well-defined CLI scripts** that are *meant* to be verbose: a one-off data migration script can use `echo` / `console.log` freely\n- **Test-only files** (`*.test.ts`, `*Test.php`) — fine to keep debug there during development\n- **Explicit `if (DEBUG_MODE) console.log(...)`** wrapped behind a feature flag — fine, but rare in practice\n\nProduction controllers, services, jobs, listeners, middleware: **zero raw debug calls**.\n\n## Detection\n\n```bash\n# JavaScript / TypeScript — console.log in production code\ngrep -rEn '\\bconsole\\.(log|debug|info|warn)\\(' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' \\\n src/ resources/js/ 2>/dev/null \\\n | grep -v -E '\\.test\\.|\\.spec\\.|/__tests__/|/scripts/'\n\n# PHP — debug helpers\ngrep -rEn '\\b(dd|dump|var_dump|print_r|var_export)\\s*\\(' --include='*.php' \\\n app/ 2>/dev/null\n\n# CI gate — block PRs that introduce debug artifacts\nNEW_DEBUG=$(git diff --diff-filter=ACM origin/main...HEAD -- 'app/**/*.php' 'src/**/*.ts' \\\n | grep -E '^\\+.*\\b(console\\.log|dd\\(|dump\\(|var_dump\\(|print_r\\()')\ntest -z \"$NEW_DEBUG\" || { echo \"Debug artifacts in PR:\"; echo \"$NEW_DEBUG\"; exit 1; }\n```\n\nESLint rules:\n\n```json\n{\n \"rules\": {\n \"no-console\": [\"error\", { \"allow\": [\"warn\", \"error\"] }]\n }\n}\n```\n\nPHPStan + a custom rule can flag `dd`/`dump` similarly. PHP-CS-Fixer has a `no_debug_print` rule.\n\nReference: [Laravel Logging docs](https://laravel.com/docs/logging) · [ESLint no-console](https://eslint.org/docs/latest/rules/no-console) · [Pino structured logging](https://github.com/pinojs/pino)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5379,"content_sha256":"eef91306904f292351955f926a8440ac81c3e08d2467c13f459bbe4d8e570875"},{"filename":"rules/style-hyper-consistent.md","content":"---\ntitle: Hyper-Consistent Formatting\nimpact: MEDIUM\nimpactDescription: \"A codebase where every file looks linter-perfect is suspiciously not-human\"\ntags: style, formatting, ai-fingerprint\n---\n\n## Hyper-Consistent Formatting\n\n**Impact: MEDIUM (A codebase where every file looks linter-perfect is suspiciously not-human)**\n\nReal codebases have geology — older files use older conventions, busy modules have rushed sections, 2am hotfix code has different spacing than carefully-reviewed weeks. AI-generated code is **uniformly polished**: every function the same length, every blank line in the same place, every import alphabetized, no styling drift anywhere. This isn't a hallmark of quality — it's a hallmark of generation.\n\nThe fingerprint is **uniformity at scale**. One file looking pristine is fine. A 2000-line PR where every file is identically pristine, across files that should have different velocity histories, is a strong AI signal.\n\n## What it looks like\n\nThe PR diff shows:\n\n- Every PHP file has exactly 4-space indent, exactly one blank line between methods, exactly two blank lines between class members, every parameter aligned identically\n- Every TS file has the same import-grouping pattern, named imports always alphabetised, every arrow function body wrapped identically\n- No `// HACK:` / `// XXX:` / late-night comments anywhere\n- No commented-out code (clean — but also no signs of someone exploring)\n- Identical commit-message structure across 50 commits\n- All files use the same paradigm (e.g., every function is an arrow function; every class uses constructor property promotion) even in places where the existing codebase mixed styles\n\n## Why this matters\n\nCode geology is a tool for the next developer:\n- **Different ages tell different stories** — \"this section is from 2022, before we switched to...\"\n- **`// HACK:` markers signal known fragility** that hasn't been worth refactoring\n- **Commented-out code reveals exploration** — \"we tried X, didn't work, kept the option in case\"\n- **Velocity differences** show which paths were rushed and might harbour bugs\n\nA repo with no geology forces every reader to start fresh — no inherited context, no \"the team didn't bother fixing this, it works fine\" signal. AI-generated codebases are flat: every line treated as equally important.\n\n## The \"all green field\" tell\n\nHyper-consistency is most suspicious in **brownfield additions** — i.e., when AI adds code to a codebase that has its own conventions. Real engineers either:\n\n- **Match the existing style** (consistent with the surrounding 50 files)\n- **Refactor the surrounding files** alongside their change (drift announces itself in the diff)\n- **Introduce a deliberate change** with an explanation (\"new files use the new pattern; old files updated as touched\")\n\nAI typically does the first poorly — it picks \"the most recent convention\" or \"the most popular convention from training data\" rather than matching the actual codebase. Result: the new files are linter-perfect by some standard, but inconsistent with the surrounding codebase.\n\n## What \"human formatting\" looks like\n\nYou won't catch this with a linter. It's a feel:\n\n- Some files have slightly off blank-line spacing where someone hit enter twice\n- Imports sometimes group \"stuff I added recently\" at the top\n- One file has 6-space indent inside a heredoc because the dev didn't fight prettier on it\n- A function has an extra blank line before a tricky if-branch where the author paused to think\n- A 2-line comment is in mid-sentence-case because someone typed it angry\n\nThese tiny artifacts are how humans write code. Their absence — especially across many files at once — is the slop.\n\n## Detection\n\nThere's no automatic detector. The signal is human-judgment:\n\n1. **Run the diff through `git diff --stat`** — many files changed at once is normal; many files all with the same line-count profile is suspicious\n2. **`git log --author` distribution** — if the diff is from one author but covers 30 files in one commit, raise an eyebrow\n3. **Compare new files to surrounding files** — does the indentation, import order, brace style match the existing convention?\n4. **Search for `// HACK:` / `// XXX:` markers** in the new files — their *absence* is the signal:\n ```bash\n git diff origin/main...HEAD -- '*.ts' '*.php' | grep -E '// HACK:|// XXX:|// FIXME' || echo \"NO HACK/XXX MARKERS\"\n ```\n5. **Mixed-paradigm check** — in a TS PR, count `function` declarations vs arrow functions; in a PHP PR, count `final class` vs `class`. A sudden swing is a tell.\n\n## What to do\n\nIf a PR looks hyper-consistent:\n\n- Ask the author: \"Was this AI-assisted? Walk me through the section in `OrderService` line 80.\" Their ability to explain the *intent*, not just the code, separates \"AI-written, human-reviewed\" from \"AI-written, accepted-as-is\".\n- Sample a few functions and ask: \"What's the trade-off you chose here?\" If they shrug, the code wasn't really written by them.\n\nThe goal isn't to ban AI assistance — it's to make sure the author owns the code they ship.\n\nReference: Internal: [`style-no-hack-scars`](style-no-hack-scars.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5155,"content_sha256":"f42418b2b91adb63cb362b02078e7f220e404c26dae4e7127380853f86fcafba"},{"filename":"rules/style-no-hack-scars.md","content":"---\ntitle: No HACK Scars — Suspiciously Pristine Code\nimpact: MEDIUM\nimpactDescription: \"Real codebases have geology; absence of any '// HACK:' / '// XXX:' markers is itself a tell\"\ntags: style, geology, ai-fingerprint\n---\n\n## No HACK Scars — Suspiciously Pristine Code\n\n**Impact: MEDIUM (Real codebases have geology; absence of any '// HACK:' / '// XXX:' markers is itself a tell)**\n\nA real codebase has *scars*. `// HACK:` comments where someone went around the type system to ship. `// XXX:` markers where the engineer noticed something fishy but had to ship. Files with a \"we tried it the right way; this is the workaround until Stripe v2\" note. These markers signal:\n\n- A human engineered the code, hit a real constraint, and acknowledged the trade-off\n- The team agreed to ship with the trade-off rather than over-engineer\n- A future engineer is being warned\n\nAI-generated codebases are flat: no `// HACK:`, no `// XXX:`, no \"we know this is wrong but\" markers. Every line presented as equally polished. The *absence* of scars is the slop signal — real systems don't look like this.\n\n## The pattern\n\nYou can audit a repo's \"humanity\" by looking for these markers:\n\n```bash\ngit grep -E '// (HACK|XXX|NOTE|GOTCHA|HMMMM|WTF)' | wc -l\n```\n\nA repo with 50 of these is alive. A repo with zero across a 50K-line codebase is either:\n- A library that was rigorously reviewed across many years (rare)\n- An AI-generated codebase passing as human (more common)\n\nYou're not looking for high counts — you're looking for **some**. Two or three `// HACK:` markers in 5K LoC is healthy.\n\n## What healthy scars look like\n\n```php\n// ✅ Real human marker — explains the constraint\npublic function chargeCustomer(int $cents, string $token): string\n{\n // HACK: Stripe v1 SDK doesn't expose retry headers; manually parse from Response\n // until we migrate to v2 (tracked in #1842).\n try {\n return $this->stripe->charges->create([...])->id;\n } catch (RateLimitException $e) {\n $retryAfter = (int) ($e->getResponse()->headers['Retry-After'] ?? 30);\n sleep($retryAfter);\n return $this->stripe->charges->create([...])->id;\n }\n}\n```\n\n```typescript\n// ✅ XXX marker that warns the next reader\nfunction parseEnvNumber(key: string): number {\n const raw = process.env[key];\n // XXX: parseInt('1.5') === 1, parseFloat('1.5e10') === 15000000000 — fine for us\n // because we only use this for ports and integer caps. Audit before reusing.\n return parseInt(raw ?? '0', 10);\n}\n```\n\n```php\n// ✅ NOTE marker capturing tribal knowledge\nclass OrderObserver\n{\n public function creating(Order $order): void\n {\n // NOTE: tax_amount must be set BEFORE Stripe charge; the webhook handler\n // re-reads this row and expects it to be non-zero.\n $order->tax_amount = TaxCalculator::for($order);\n }\n}\n```\n\nThese tell the next engineer: *\"someone thought about this; the workaround is intentional; if you change it, here's the context.\"*\n\n## What absence looks like\n\nFive files, 600 lines, all in a PR:\n\n- Every method has the same length and shape\n- Every error path is the same `try { ... } catch (e) { console.error(e); }`\n- No comments about edge cases, browser quirks, library bugs, race conditions\n- No `// HACK:` / `// XXX:` / `// NOTE:` anywhere\n- Every variable has a \"perfect\" name\n\nThat's not human. Real production code dealing with real third parties has scars.\n\n## What to do when you find pristine-looking code\n\nThis rule is harder to action than to detect. Recommended response:\n\n1. **Pair-review with the author** — ask them to walk you through one of the \"perfect\" sections. If they can articulate the *why* (and the trade-offs they considered), it's fine. If they can't, treat it as AI-assisted code that needs deeper review.\n2. **Run mutation testing on the new code** — Stryker (TS) / Infection (PHP). Mirror-tests die fast.\n3. **Sample edge cases** — try inputs the AI wouldn't have thought of (empty string, very long string, Unicode, negative numbers, future dates, leap years). If the code falls over on basics, the polish was cosmetic.\n\n## Detection\n\n```bash\n# Count scars in the existing repo (the baseline)\ngit grep -cE '// (HACK|XXX|NOTE|GOTCHA|WTF|FIXME)' -- '*.ts' '*.tsx' '*.php' '*.js' '*.jsx' 2>/dev/null | head\n\n# Same, but for the PR diff only — if a 1000-line PR adds zero scars, suspicious\ngit diff origin/main...HEAD -- '*.ts' '*.tsx' '*.php' | \\\n grep -cE '^\\+.*// (HACK|XXX|NOTE|GOTCHA|WTF)'\n```\n\nA PR adding > 500 LoC with zero `HACK:` / `XXX:` / `NOTE:` is unusual. Real engineering on real systems leaves these.\n\n**Note:** this rule cuts both ways. Don't *add* a `// HACK:` just to look human. Add them when you actually have a hack to mark. The signal is genuine, not performative.\n\nReference: Internal: [`style-hyper-consistent`](style-hyper-consistent.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4859,"content_sha256":"394e40a2d49e085bc264e09d0432d43cc5db13672f496005064f8daa2f0489d4"},{"filename":"rules/style-trivial-boilerplate.md","content":"---\ntitle: Trivial Boilerplate\nimpact: MEDIUM\nimpactDescription: \"Pattern-matched boilerplate that hides intent and inflates line count\"\ntags: style, boilerplate, ai-fingerprint, readability\n---\n\n## Trivial Boilerplate\n\n**Impact: MEDIUM (Pattern-matched boilerplate that hides intent and inflates line count)**\n\nThe class of \"code that says less than the underlying expression\". Common AI variants:\n\n- `if (x) return true; else return false;` — when `return x` does it\n- `return x === true ? true : false;` — ditto\n- `const isPaid: boolean = order.status === 'paid';` — TS infers `boolean` already; the annotation is noise\n- `const name: string = 'asyraf';` — TS infers `string`\n- `await Promise.resolve(value)` — when `value` is already non-Promise\n- `try { await fn(); } catch (e) { throw e; }` — pass-through catch\n- `[...array]` to \"make a copy\" when the next operation doesn't mutate\n\nEach is a small AI fingerprint. A few are fine. A repo with many of them across files reads as model-generated.\n\n## Incorrect\n\n```typescript\n// ❌ if-true-false\nfunction isPaid(order: Order): boolean {\n if (order.status === 'paid') {\n return true;\n } else {\n return false;\n }\n}\n\n// ❌ Ternary returning the booleans it was given\nfunction isCompleted(order: Order): boolean {\n return order.status === 'completed' ? true : false;\n}\n\n// ❌ Redundant type annotations on obvious literals\nconst name: string = 'asyraf';\nconst age: number = 30;\nconst isActive: boolean = true;\nconst orders: Order[] = await db.orders.find(); // db.orders.find() return type IS Order[]\n\n// ❌ Pass-through catch\nasync function processPayment(order: Order): Promise\u003cCharge> {\n try {\n return await stripe.charges.create({ /* ... */ });\n } catch (e) {\n throw e; // catch literally does nothing\n }\n}\n\n// ❌ Unnecessary await + Promise.resolve\nasync function formatName(user: User): Promise\u003cstring> {\n return await Promise.resolve(`${user.first} ${user.last}`);\n}\n\n// ❌ Spread to \"copy\" — but nothing mutates\nfunction totalsFor(items: Item[]): number {\n const copied = [...items]; // unused: the next op doesn't mutate\n return copied.reduce((s, i) => s + i.price, 0);\n}\n```\n\n```php\n// ❌ PHP equivalents\npublic function isPaid(Order $order): bool\n{\n if ($order->status === 'paid') {\n return true;\n } else {\n return false;\n }\n}\n\npublic function processWebhook(Request $request): JsonResponse\n{\n try {\n return $this->handle($request);\n } catch (\\Exception $e) {\n throw $e; // pass-through\n }\n}\n```\n\n**Why it's slop:**\n- Each line carries no information beyond the simpler form\n- The `try { ... } catch (e) { throw e; }` is the most embarrassing — six tokens to do nothing\n- Type annotations on inferred literals fight the type system instead of using it\n- A reader's eyes have to traverse \"what does this expand to?\" rather than read the expression directly\n\n## Correct\n\n```typescript\n// ✅ Direct expressions\nfunction isPaid(order: Order): boolean {\n return order.status === 'paid';\n}\n\nfunction isCompleted(order: Order): boolean {\n return order.status === 'completed';\n}\n\n// ✅ Let TS infer\nconst name = 'asyraf';\nconst age = 30;\nconst isActive = true;\nconst orders = await db.orders.find();\n\n// ✅ No catch when not handling\nasync function processPayment(order: Order): Promise\u003cCharge> {\n return stripe.charges.create({ /* ... */ });\n}\n\n// ✅ No need to await a non-Promise\nfunction formatName(user: User): string {\n return `${user.first} ${user.last}`;\n}\n\n// ✅ No spread when nothing mutates\nfunction totalsFor(items: Item[]): number {\n return items.reduce((s, i) => s + i.price, 0);\n}\n```\n\n```php\n// ✅ PHP equivalents\npublic function isPaid(Order $order): bool\n{\n return $order->status === 'paid';\n}\n\npublic function processWebhook(Request $request): JsonResponse\n{\n return $this->handle($request);\n}\n```\n\n**Why it reads human:**\n- Each expression does exactly the work; nothing extra\n- Type inference does its job; the type annotations are added where they earn their place (function signatures, public APIs)\n- No pass-through catch; if an error needs handling, it's handled\n\n## When annotations / spreads ARE worth it\n\nA few cases:\n\n- **Public-API parameters / returns** — explicit types document the contract: `function add(a: number, b: number): number`\n- **Complex inferred types** — annotate when the inferred type is hard to read\n- **Boundary code** — `as const`, `satisfies`, or explicit annotations on configuration objects help downstream type narrowing\n- **Spread when you actually want a copy** — before sorting, before mutating, before passing to a function that mutates\n\nThe trivial-boilerplate test: **does removing the construct change the program's behaviour or the reader's understanding?** If no, remove.\n\n## Detection\n\n```bash\n# if-true-false\ngrep -rEnB1 -A2 'if\\s*\\([^)]+\\)\\s*\\{?\\s*

Code Slop Detection Taste-level review of code for AI-generated patterns. Contains 24 rules across 6 categories covering comments, naming, over-engineering, defensive overdose, test slop, and style fingerprints. Where measures quantitative code debt (complexity, duplication, CVEs), this skill measures the qualitative failure mode: code that passes every metric but reads like a tutorial blog post, not like a human wrote it. Metadata - Version: 1.0.0 - Scope: PHP / Laravel + TypeScript / React (Node) - Rule Count: 24 rules across 6 categories - License: MIT Why this skill exists Industry data o…

--include='*.ts' --include='*.tsx' --include='*.js' --include='*.php' \\\n src/ app/ 2>/dev/null | grep -B2 -A1 'return true\\b' | grep -A2 'else' | head\n\n# Pass-through catch\ngrep -rEnB0 -A1 'catch\\s*\\([^)]*\\)\\s*\\{' --include='*.ts' --include='*.tsx' --include='*.php' \\\n src/ app/ 2>/dev/null | grep -B1 '^\\s*throw\\b'\n\n# Ternary returning booleans\ngrep -rEn '\\?\\s*true\\s*:\\s*false\\b' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.php' \\\n src/ app/\n\n# Redundant type annotations on string/number/boolean literals (TS)\ngrep -rEn ':\\s*(string|number|boolean)\\s*=\\s*(\"[^\"]*\"|[0-9]+|true|false)' --include='*.ts' --include='*.tsx' src/\n```\n\nESLint rules:\n\n```json\n{\n \"rules\": {\n \"no-useless-return\": \"error\",\n \"no-useless-catch\": \"error\",\n \"no-unneeded-ternary\": \"error\"\n }\n}\n```\n\nReference: [ESLint — no-useless-catch / ternary / return](https://eslint.org/docs/latest/rules/) · [PHP-CS-Fixer rules](https://cs.symfony.com/) · Internal: [`defensive-generic-catch`](defensive-generic-catch.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6009,"content_sha256":"096ba0a04fabf6791718b2de6c08235255d917d1fd97ee3ef53fdc2f61f4ba66"},{"filename":"rules/test-doesnt-throw.md","content":"---\ntitle: \"Doesn't Throw\" Tests\nimpact: HIGH\nimpactDescription: \"Tests that assert nothing meaningful; pass even when the function does the wrong thing\"\ntags: testing, weak-assertions, ai-fingerprint\n---\n\n## \"Doesn't Throw\" Tests\n\n**Impact: HIGH (Tests that assert nothing meaningful; pass even when the function does the wrong thing)**\n\nA test that calls the function and then asserts the function didn't throw is barely a test. It verifies one of the weakest possible properties — \"the program didn't crash\" — and gives false confidence in coverage. AI often defaults to this pattern because it's the easiest way to make a test pass.\n\nA real test verifies a specific outcome. \"Didn't throw\" is an outcome only in the narrowest cases (you specifically want to verify a thrown error from a previous bug is now absent).\n\n## Incorrect\n\n```typescript\n// ❌ Tests that just verify \"the call completes\"\n\ndescribe('OrderService', () => {\n it('places an order', async () => {\n const service = new OrderService();\n await expect(service.place(makeValidOrder())).resolves.not.toThrow();\n });\n\n it('exports users', async () => {\n const service = new UserExportService();\n const result = await service.export();\n expect(result).toBeDefined(); // weak: undefined is the only failure mode caught\n });\n\n it('handles empty input', () => {\n const result = sum([]);\n expect(result).not.toBeNull(); // passes for 0, NaN, '', false, anything except null/undefined\n });\n});\n```\n\n```php\n// ❌ Same pattern in PHPUnit / Pest\ntest('it places an order', function () {\n $service = new OrderService();\n\n expect(fn () => $service->place(makeValidOrder()))->not->toThrow();\n});\n\ntest('export returns something', function () {\n $result = (new UserExportService())->export();\n expect($result)->not->toBeNull();\n});\n```\n\n**Why it's slop:**\n- \"Doesn't throw\" tells you nothing about whether the right thing happened\n- `place(order)` could return without throwing AND not actually place the order\n- `export()` could return `undefined` (the test fails) OR `null` (test fails) OR an empty array, a misleading \"true\", etc. — the test catches only the narrowest failure\n- `not.toBeNull()` passes for `0`, `''`, `NaN`, `false` — all of which are usually bugs\n\n## Correct — assert what should happen\n\n```typescript\n// ✅ Specific outcomes\n\ndescribe('OrderService', () => {\n it('place(order) persists the order and returns the saved row with status=pending', async () => {\n const service = new OrderService(db);\n const order = makeValidOrder({ total: 100_00 });\n\n const saved = await service.place(order);\n\n expect(saved.id).toBeDefined();\n expect(saved.status).toBe('pending');\n expect(saved.total).toBe(100_00);\n expect(await db.orders.findById(saved.id)).toMatchObject({ status: 'pending' });\n });\n\n it('exports all active users to CSV', async () => {\n await db.users.insertMany([\n { email: '[email protected]', active: true },\n { email: '[email protected]', active: false },\n { email: '[email protected]', active: true },\n ]);\n\n const csv = await new UserExportService(db).export();\n\n expect(csv).toContain('[email protected]');\n expect(csv).toContain('[email protected]');\n expect(csv).not.toContain('[email protected]'); // inactive should be excluded\n });\n\n it('sum([]) returns 0', () => {\n expect(sum([])).toBe(0);\n });\n});\n```\n\n**Why it reads human:**\n- Each test verifies a specific, named outcome\n- Bugs are detected: changing `status: 'pending'` to `'paid'` would fail the test; returning all users (not just active) would fail; `sum([])` returning `NaN` would fail\n- Test names describe the *behaviour*, not the *function called*\n\n## When \"doesn't throw\" IS legitimate\n\nRarely:\n- **Smoke tests** for a never-throws contract on a public API (one or two tests; not the bulk of the suite)\n- **Regression tests** for a specific previously-thrown error: \"after fix #123, calling X with Y no longer throws\"\n\nIn these cases, name the test clearly: `it('does not throw on empty input — regression for #123')`.\n\n## Detection\n\n```bash\n# Tests whose only assertion is \"not.toThrow\" or \"not.toBeNull\"\ngrep -rEn '\\.not\\.toThrow\\(|\\.not\\.toBeNull\\(|\\.toBeDefined\\(' \\\n --include='*.test.ts' --include='*.spec.ts' --include='*.test.tsx' src/ tests/ \\\n | head -30\n\n# PHPUnit / Pest equivalents\ngrep -rEn '\\bexpect\\(.*\\)->not->toThrow\\(\\)|->assertNotNull\\(\\$result\\)' \\\n --include='*Test.php' --include='*.test.php' tests/\n\n# Heuristic: test files where the ratio of weak assertions to strong assertions is high\n# (manual review of files with > 5 weak assertions)\n```\n\nA few weak assertions are fine. A test suite dominated by them is a slop fingerprint.\n\nReference: [Martin Fowler — Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html) · Internal: [`test-mock-everything`](test-mock-everything.md), [`test-mirror-implementation`](test-mirror-implementation.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4934,"content_sha256":"025a3bd5bd612fcc9969446e03478a221df724f367655f7d6f627b7ab89131a0"},{"filename":"rules/test-mirror-implementation.md","content":"---\ntitle: Tests That Mirror the Implementation\nimpact: HIGH\nimpactDescription: \"Tests re-encode the production code; pass because they re-implement the same logic, not because they verify behaviour\"\ntags: testing, mirror-tests, ai-fingerprint\n---\n\n## Tests That Mirror the Implementation\n\n**Impact: HIGH (Tests re-encode the production code; pass because they re-implement the same logic, not because they verify behaviour)**\n\nAI generates tests by reading the function and translating its logic into the test. If `calculateTax` does `subtotal * 0.06`, the test sets `subtotal = 100`, then says \"expect result to be `100 * 0.06`\". The test passes because both sides compute the same thing. It would still pass if the function silently switched to `subtotal * 0.07` — IF the AI also \"updated\" the test to match.\n\nReal tests state the **expected concrete answer**, not a formula computed from the input. They verify what *should* be true, not \"the function does what the function does\".\n\n## Incorrect\n\n```typescript\n// ❌ The test re-implements the function\n\n// Production:\nexport function calculateTax(subtotal: number): number {\n return subtotal * 0.06;\n}\n\n// Test:\nimport { calculateTax } from './calculateTax';\n\ndescribe('calculateTax', () => {\n it('calculates tax', () => {\n const subtotal = 100;\n expect(calculateTax(subtotal)).toBe(subtotal * 0.06); // re-encodes the formula\n });\n\n it('handles zero', () => {\n expect(calculateTax(0)).toBe(0 * 0.06); // tautology\n });\n});\n```\n\n```php\n// ❌ Same pattern in PHP\npublic function test_it_calculates_tax(): void\n{\n $subtotal = 100;\n $expected = $subtotal * 0.06; // re-encoded\n $this->assertEquals($expected, calculateTax($subtotal));\n}\n```\n\n**Why it's slop:**\n- The test passes because RHS and LHS use the same formula — you've verified `x === x`\n- If the formula in production silently changes to `0.07`, but the test \"uses the formula\" (or AI updates the test to match), the test still passes — and the bug ships\n- Both sides of the equation come from the same place, so the test catches nothing the type system doesn't\n\n## Correct — concrete, named expected values\n\n```typescript\n// ✅ Concrete expectations\ndescribe('calculateTax', () => {\n it('charges 6% on the subtotal', () => {\n expect(calculateTax(100)).toBe(6);\n expect(calculateTax(50)).toBe(3);\n expect(calculateTax(1.50)).toBe(0.09);\n });\n\n it('returns 0 for a 0 subtotal', () => {\n expect(calculateTax(0)).toBe(0);\n });\n\n it('rejects negative subtotals', () => {\n expect(() => calculateTax(-10)).toThrow(InvalidSubtotal);\n });\n});\n```\n\n```php\n// ✅ Concrete expectations\npublic function test_charges_six_percent_on_subtotal(): void\n{\n $this->assertEquals(6.0, calculateTax(100));\n $this->assertEquals(3.0, calculateTax(50));\n $this->assertEquals(0.09, calculateTax(1.50));\n}\n```\n\n**Why it reads human:**\n- The expected value is *named* — `6` is what 6% of 100 is, written as the answer\n- If the production formula changes to `0.07`, the test fails at `expect(calculateTax(100)).toBe(6)` because `7 !== 6`\n- The bug is caught at the boundary; the test holds the spec\n\n## A particularly dangerous variant: bug-then-regenerate\n\n```\nUser: \"There's a bug — calculateTax is returning the wrong value for negatives.\"\nAI: \"I'll regenerate the test for you.\"\n```\n\nThe regenerated test happens to pass for the new buggy behaviour. The test now ratifies the bug. This pattern is documented in Larridin (2025) and is one of the most insidious failure modes.\n\n**Rule:** when fixing a bug, the test is written FIRST (red), with concrete expected values that reflect the correct behaviour. Then the production code is changed. Then the test goes green. Never accept \"regenerate the test to match the new implementation\".\n\n## Detection\n\nThis is hard to detect mechanically — the pattern is \"RHS includes the same constants/operations as the production code\". Useful heuristics:\n\n```bash\n# Tests where expected values use the same magic number as the source\n# (extract numeric constants from src/, then grep tests for them)\ngrep -rEoh '[0-9]+\\.[0-9]+|[0-9]+_[0-9]+' --include='*.ts' src/ | sort -u > /tmp/src-constants.txt\ngrep -rEoh '[0-9]+\\.[0-9]+|[0-9]+_[0-9]+' --include='*.test.ts' tests/ | sort -u > /tmp/test-constants.txt\ncomm -12 /tmp/src-constants.txt /tmp/test-constants.txt | head\n# If many magic numbers from src appear in tests, they may be re-encoding the implementation\n\n# Mutation testing is the gold-standard detection:\n# - Stryker (JS/TS): https://stryker-mutator.io/\n# - Infection (PHP): https://infection.github.io/\n# If a mutation in the source doesn't fail a test, the test is mirror-ware.\n```\n\n**Mutation testing is the right answer here.** Add Stryker or Infection to your CI and require a minimum mutation score on PRs touching the unit-test directory.\n\nReference: [Stryker Mutator](https://stryker-mutator.io/) · [Infection](https://infection.github.io/) · Internal: [`test-mock-everything`](test-mock-everything.md), [`test-doesnt-throw`](test-doesnt-throw.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5109,"content_sha256":"76ccd3f720ce296ab758763b27ff018bf2c8256dc562fc08324afabe97c7323d"},{"filename":"rules/test-mock-everything.md","content":"---\ntitle: Mock-Everything Tests That Assert Nothing\nimpact: CRITICAL\nimpactDescription: \"Tests that pass forever; they re-encode the implementation rather than verify behaviour\"\ntags: testing, mocks, ai-fingerprint\n---\n\n## Mock-Everything Tests That Assert Nothing\n\n**Impact: CRITICAL (Tests that pass forever; they re-encode the implementation rather than verify behaviour)**\n\nWhen AI generates tests, the most common failure mode is \"mock every dependency, then assert that the mocks were called\". The test passes the moment it's written and passes forever — including when the underlying behaviour is silently broken. arXiv 2602.00409 (2026): coding agents produce significantly more over-mocked tests than human authors.\n\nA real test verifies **outcome**, not **interaction**. Mocking a database to assert \"create() was called with X\" verifies that you wrote the implementation that calls `create()`. It doesn't verify that the customer record actually lands in the table.\n\n## Incorrect\n\n```typescript\n// ❌ Mock-everything test that asserts mock interactions\n\nimport { describe, it, expect, vi } from 'vitest';\nimport { UserService } from './UserService';\n\ndescribe('UserService', () => {\n it('createUser saves the user', async () => {\n const mockRepo = {\n save: vi.fn().mockResolvedValue({ id: 1, email: '[email protected]' }),\n findByEmail: vi.fn().mockResolvedValue(null),\n };\n const mockMailer = { send: vi.fn() };\n const mockHasher = { hash: vi.fn().mockReturnValue('hashed') };\n\n const service = new UserService(mockRepo, mockMailer, mockHasher);\n await service.createUser({ email: '[email protected]', password: 'x' });\n\n expect(mockRepo.save).toHaveBeenCalled(); // weak — passes if .save() called with anything\n expect(mockMailer.send).toHaveBeenCalled();\n expect(mockHasher.hash).toHaveBeenCalledWith('x'); // verifies you wrote .hash() — not that it's secure\n });\n});\n```\n\n```php\n// ❌ Same pattern in PHPUnit / Pest\ntest('processOrder calls payment gateway', function () {\n $payments = $this->mock(PaymentGateway::class);\n $payments->shouldReceive('charge')->once(); // verifies an interaction, not an outcome\n\n $service = new OrderService($payments);\n $service->process(new Order(/* ... */));\n});\n```\n\n**Why it's slop:**\n- The test passes even if `save()` stores the wrong fields, or `hash()` returns the input unchanged, or `charge()` skips actual payment\n- It locks in the implementation's structure (every refactor breaks the test even when behaviour is fine)\n- \"Was the mock called\" is rarely a useful assertion — what matters is \"did the outcome happen\"\n- The test gives false confidence in coverage reports\n\n## Correct — verify the outcome, with real dependencies where possible\n\n```typescript\n// ✅ Integration test against a real (in-memory) DB; assert what changed\nimport { describe, it, expect, beforeEach } from 'vitest';\nimport { createTestDb } from './testUtils';\nimport { UserService } from './UserService';\n\ndescribe('UserService', () => {\n let db;\n beforeEach(async () => { db = await createTestDb(); });\n\n it('createUser creates a user row with hashed password and sends welcome email', async () => {\n const mailer = makeFakeMailer(); // collect-and-inspect, not a mock-with-asserts\n const service = new UserService(db.users, mailer);\n\n await service.createUser({ email: '[email protected]', password: 'plaintext' });\n\n const stored = await db.users.findByEmail('[email protected]');\n expect(stored).toBeDefined();\n expect(stored!.email).toBe('[email protected]');\n expect(stored!.passwordHash).not.toBe('plaintext'); // hash actually happened\n expect(bcrypt.compareSync('plaintext', stored!.passwordHash)).toBe(true);\n expect(mailer.sentTo('[email protected]')).toHaveLength(1); // outcome, not interaction\n });\n});\n```\n\n```php\n// ✅ Laravel: use the real database (RefreshDatabase) + fake the boundary services\ntest('processOrder charges customer and marks order paid', function () {\n Mail::fake();\n Http::fake([\n 'api.stripe.com/*' => Http::response(['id' => 'ch_test_123', 'status' => 'succeeded'], 200),\n ]);\n\n $order = Order::factory()->create(['status' => 'pending', 'total' => 100_00]);\n\n (new OrderService(new StripeGateway()))->process($order);\n\n expect($order->fresh()->status)->toBe('paid');\n expect($order->fresh()->stripe_charge_id)->toBe('ch_test_123');\n Mail::assertSent(OrderConfirmation::class, fn ($m) => $m->order->is($order));\n});\n```\n\n**Why it reads human:**\n- Each assertion checks **what should be true after the operation** — not \"did you call X\"\n- Refactoring the internal calls is safe — the test still passes if the outcomes hold\n- Real bugs (forgotten hash, wrong field assignment, missing email) trigger the test failures\n- The test reads like a customer story: \"after createUser, the user exists, password is hashed, welcome email is sent\"\n\n## When mocks ARE appropriate\n\nA few legitimate uses:\n\n- **Cost / side-effect boundaries** — real Stripe calls (use `Http::fake()` in Laravel; mock the HTTP boundary)\n- **Slow externals** — third-party APIs with rate limits (mock the HTTP boundary)\n- **Time-sensitive code** — freeze the clock (Carbon test helpers, vi.setSystemTime), don't mock all of time\n- **Hard-to-reproduce error paths** — force a `ConnectionException` to test the retry handler\n\nThe pattern: **mock at the network/IO boundary, not at every internal class.**\n\n## Detection\n\n```bash\n# Tests that import the mock library more than the assertion library (rough)\ngrep -rEn '(vi\\.fn|jest\\.fn|->mock\\(|->shouldReceive)' \\\n --include='*.test.ts' --include='*.spec.ts' --include='*Test.php' \\\n | wc -l\n\n# Tests asserting only on mock interactions, no DB / outcome checks\n# (heuristic: file has 'toHaveBeenCalled' / 'shouldReceive' but no 'expect(\u003crepo>.find' or 'assertDatabaseHas')\nfor f in $(find . -name '*Test.php' -o -name '*.test.ts' 2>/dev/null); do\n has_mock=$(grep -cE 'toHaveBeenCalled|shouldReceive' \"$f\")\n has_outcome=$(grep -cE 'assertDatabaseHas|->fresh\\(|findByEmail|toBe\\(|toEqual\\(' \"$f\")\n if [ \"$has_mock\" -gt 3 ] && [ \"$has_outcome\" -lt 1 ]; then\n echo \"MOCK-ONLY: $f\"\n fi\ndone\n```\n\nReference: [arXiv 2602.00409 — Are Coding Agents Generating Over-Mocked Tests?](https://arxiv.org/abs/2602.00409) · Internal: [`test-mirror-implementation`](test-mirror-implementation.md), [`test-doesnt-throw`](test-doesnt-throw.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6424,"content_sha256":"678efc23ef204337134ff5dedd50d47dea7a85de9d5263e74eeb3974d6e1a30a"},{"filename":"rules/test-snapshot-abuse.md","content":"---\ntitle: Snapshot Tests Replacing Behavioural Assertions\nimpact: HIGH\nimpactDescription: \"Snapshots ratify whatever the function currently returns; engineers approve diffs without reading them\"\ntags: testing, snapshots, ai-fingerprint\n---\n\n## Snapshot Tests Replacing Behavioural Assertions\n\n**Impact: HIGH (Snapshots ratify whatever the function currently returns; engineers approve diffs without reading them)**\n\nA snapshot test calls the function, captures the output, and asserts \"matches the saved snapshot\". For genuinely stable, large, structural outputs (rendered React component DOM, generated SQL, large JSON shapes), snapshots are useful. For everything else — and especially when AI generates them — they're a way to make a test pass without writing a real assertion.\n\nThe failure mode is well-documented: a test fails on a real bug, the engineer runs `vitest -u` to \"update snapshots\", ships. The snapshot test now ratifies the bug. Repeat across a team and snapshots become noise the team has trained itself to ignore.\n\n## Incorrect\n\n```typescript\n// ❌ Snapshot tests instead of behavioural assertions\n\ndescribe('calculateRefund', () => {\n it('matches snapshot', () => {\n expect(calculateRefund(order)).toMatchSnapshot(); // what should the answer be?\n });\n});\n\ndescribe('formatPrice', () => {\n it('matches snapshot', () => {\n expect(formatPrice(1234.5)).toMatchSnapshot();\n });\n});\n```\n\n```typescript\n// ❌ Component snapshots that lock in entire DOM\ndescribe('OrderCard', () => {\n it('renders', () => {\n const { container } = render(\u003cOrderCard order={makeOrder()} />);\n expect(container).toMatchSnapshot(); // 800-line snapshot file\n });\n});\n```\n\n**Why it's slop:**\n- \"Matches snapshot\" doesn't say what the answer is — readers can't tell what the test verifies\n- When the snapshot fails (because the team made an intentional change), the engineer reflexively runs `-u` to update\n- Snapshot files grow huge; nobody reviews them in PRs\n- AI generates snapshot tests by default because it's the easiest way to \"test\" without writing an actual assertion\n\n## Correct — explicit behavioural assertions\n\n```typescript\n// ✅ Concrete expectations\n\ndescribe('calculateRefund', () => {\n it('refunds the full order minus the restocking fee', () => {\n const order = makeOrder({ total: 100_00, restockingFee: 5_00 });\n expect(calculateRefund(order)).toEqual({\n amount: 95_00,\n stripeChargeId: order.stripeChargeId,\n });\n });\n\n it('refunds zero if the order is past the return window', () => {\n const order = makeOrder({ returnsCloseAt: subDays(new Date(), 1) });\n expect(() => calculateRefund(order)).toThrow(RefundWindowClosed);\n });\n});\n\ndescribe('formatPrice', () => {\n it('formats USD with two decimal places and a leading

Code Slop Detection Taste-level review of code for AI-generated patterns. Contains 24 rules across 6 categories covering comments, naming, over-engineering, defensive overdose, test slop, and style fingerprints. Where measures quantitative code debt (complexity, duplication, CVEs), this skill measures the qualitative failure mode: code that passes every metric but reads like a tutorial blog post, not like a human wrote it. Metadata - Version: 1.0.0 - Scope: PHP / Laravel + TypeScript / React (Node) - Rule Count: 24 rules across 6 categories - License: MIT Why this skill exists Industry data o…

, () => {\n expect(formatPrice(1234.5)).toBe('$1,234.50');\n });\n});\n\ndescribe('OrderCard', () => {\n it('shows the order number, total, and status', () => {\n const order = makeOrder({ id: 'ord_123', total: 50_00, status: 'paid' });\n render(\u003cOrderCard order={order} />);\n expect(screen.getByText('Order #ord_123')).toBeInTheDocument();\n expect(screen.getByText('$50.00')).toBeInTheDocument();\n expect(screen.getByText('Paid')).toBeInTheDocument();\n });\n});\n```\n\n**Why it reads human:**\n- The test reads as the spec — \"refunds the full order minus the restocking fee\"\n- Concrete expected values; bug-then-regenerate doesn't silently work\n- Component tests assert *behaviour* (specific text appears) not *DOM shape* (every class name and attribute)\n\n## When snapshots ARE worth using\n\nGenuine cases for snapshot testing:\n\n- **Large generated artifacts** — SQL output from an ORM, generated migration files, OpenAPI specs\n- **Structural data** — public API JSON responses where any structural change should be reviewed\n- **Visual regression** — actual screenshot comparison (e.g., Playwright `toHaveScreenshot`)\n\nFor these:\n- Keep the snapshot small (don't snapshot the whole HTML tree if you can snapshot the relevant attribute)\n- Review the snapshot diff IN the PR (treat snapshot files as code)\n- Don't `-u` reflexively; understand the change\n\n## Detection\n\n```bash\n# Snapshot usage in the repo\ngrep -rEn 'toMatchSnapshot\\(|toMatchInlineSnapshot\\(' --include='*.test.ts' --include='*.spec.ts' --include='*.test.tsx' src/ tests/ | wc -l\n\n# Snapshot files\nfind . -name '__snapshots__' -type d 2>/dev/null\n\n# Heuristic: snapshot files larger than 200 lines = probably too big to review\nfind . -path '*/__snapshots__/*.snap' -type f -exec wc -l {} \\; 2>/dev/null \\\n | awk '$1 > 200 { print \"TOO LARGE: \" $2 \" (\" $1 \" lines)\" }'\n\n# Tests that use ONLY snapshot assertions, no toBe / toEqual\nfor f in $(find . -name '*.test.tsx' -o -name '*.test.ts' 2>/dev/null); do\n snap=$(grep -c 'toMatchSnapshot' \"$f\")\n real=$(grep -cE 'toBe\\(|toEqual\\(|toContain\\(|toBeInTheDocument' \"$f\")\n if [ \"$snap\" -gt 0 ] && [ \"$real\" = \"0\" ]; then\n echo \"SNAPSHOT-ONLY: $f\"\n fi\ndone\n```\n\nReference: [Kent C. Dodds — Effective Snapshot Testing](https://kentcdodds.com/blog/effective-snapshot-testing) · [Vitest snapshot docs](https://vitest.dev/guide/snapshot) · Internal: [`test-mock-everything`](test-mock-everything.md), [`test-mirror-implementation`](test-mirror-implementation.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5322,"content_sha256":"c01a53ade90a6b94566a33ec687f848461a17e89069a6b64e64bfae8af821204"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Code Slop Detection","type":"text"}]},{"type":"paragraph","content":[{"text":"Taste-level review of code for AI-generated patterns. Contains 24 rules across 6 categories covering comments, naming, over-engineering, defensive overdose, test slop, and style fingerprints. Where ","type":"text"},{"text":"technical-debt","type":"text","marks":[{"type":"link","attrs":{"href":"../technical-debt","title":null}},{"type":"code_inline"}]},{"text":" measures ","type":"text"},{"text":"quantitative","type":"text","marks":[{"type":"em"}]},{"text":" code debt (complexity, duplication, CVEs), this skill measures the ","type":"text"},{"text":"qualitative","type":"text","marks":[{"type":"em"}]},{"text":" failure mode: code that passes every metric but reads like a tutorial blog post, not like a human wrote it.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Metadata","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Version:","type":"text","marks":[{"type":"strong"}]},{"text":" 1.0.0","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Scope:","type":"text","marks":[{"type":"strong"}]},{"text":" PHP / Laravel + TypeScript / React (Node)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rule Count:","type":"text","marks":[{"type":"strong"}]},{"text":" 24 rules across 6 categories","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"License:","type":"text","marks":[{"type":"strong"}]},{"text":" MIT","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Why this skill exists","type":"text"}]},{"type":"paragraph","content":[{"text":"Industry data on AI-assisted code (GitClear 2025, cURL bug-bounty shutdown 2025, arXiv 2510.03029):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Refactoring collapsed","type":"text","marks":[{"type":"strong"}]},{"text":" from 25% to \u003c10% of changes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Copy-paste surged","type":"text","marks":[{"type":"strong"}]},{"text":" 8.3% → 12.3%; code duplication rose ~8x","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Code-smell rates +42–85%","type":"text","marks":[{"type":"strong"}]},{"text":" over human baselines","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"82% of AI PRs","type":"text","marks":[{"type":"strong"}]},{"text":" use generic catch blocks that don't distinguish error types","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"76% miss timeouts","type":"text","marks":[{"type":"strong"}]},{"text":" on external calls","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"None of this fails a typical CI lint. It just makes the codebase slowly unmaintainable. This skill is the lens for catching it before it ships.","type":"text"}]},{"type":"paragraph","content":[{"text":"The core insight: ","type":"text"},{"text":"reading cost > writing cost now","type":"text","marks":[{"type":"strong"}]},{"text":". The cost of writing code collapsed; the cost of reading it didn't. Code you can't quickly understand is slop, even if it works.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"How to Audit","type":"text"}]},{"type":"paragraph","content":[{"text":"When the user asks \"review for AI slop\", \"audit code-quality taste\", or \"find AI patterns\" — run through this skill's rules as a checklist against the changed files (PR diff) or full repo.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Audit Step 1: Determine Scope","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If a PR diff is provided: audit only files changed in the diff","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If files are named: audit those","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If no scope: audit the whole repo, prioritized by recently-touched files (most likely AI output)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Audit Step 2: Detect Stack","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Signal","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stack","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"composer.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"artisan","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PHP / Laravel","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"package.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" (with TypeScript/React deps)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Node / TypeScript / React","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Both present","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Laravel + Inertia + React","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Audit Step 3: Run the Slop Checklist","type":"text"}]},{"type":"paragraph","content":[{"text":"For each item below, output:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CLEAN","type":"text","marks":[{"type":"strong"}]},{"text":" — pattern not present (brief confirmation)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SUSPICIOUS","type":"text","marks":[{"type":"strong"}]},{"text":" — present in small amounts; flag and discuss","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"INFLATED","type":"text","marks":[{"type":"strong"}]},{"text":" — present extensively; verbose-but-functional; remediation recommended","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CRITICAL","type":"text","marks":[{"type":"strong"}]},{"text":" — extensive AI-fingerprint presence; full review needed before merge","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Comments","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No comments that just narrate the code (","type":"text"},{"text":"// create user","type":"text","marks":[{"type":"code_inline"}]},{"text":" above ","type":"text"},{"text":"User::create(...)","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No empty docblocks (","type":"text"},{"text":"/** Get user */","type":"text","marks":[{"type":"code_inline"}]},{"text":" above ","type":"text"},{"text":"getUser()","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No placeholder comments left in (","type":"text"},{"text":"// TODO: implement","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"// your code here","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"// implementation","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No closing-brace labels (","type":"text"},{"text":"} // end function","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"} // end if block","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Naming","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No generic placeholder names (","type":"text"},{"text":"data","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"result","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"info","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"temp","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"helper","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No over-descriptive run-on names (","type":"text"},{"text":"theUserWhoIsCurrentlyLoggedIn","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No suffix abuse (","type":"text"},{"text":"*Helper","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"*Manager","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"*Util","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"*Wrapper","type":"text","marks":[{"type":"code_inline"}]},{"text":" overused without justification)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No type-in-name patterns (","type":"text"},{"text":"userObject","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"resultArray","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"stringData","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Over-engineering","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No interfaces with exactly one implementation (and no plan for a second)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No single-method classes that should be top-level functions","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No wrapper functions called once that just delegate","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No new dependency added when an existing one does the same job","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Defensive overdose","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No generic ","type":"text"},{"text":"catch (e) { console.error(...) }","type":"text","marks":[{"type":"code_inline"}]},{"text":" blocks around code that can't throw","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No null checks for impossible nulls (after non-null assertions / type-guaranteed values)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Real defensive concerns (timeouts on external calls, rate limits) ARE present","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Test slop","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No tests that mock every dependency with no real behavioural assertion","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No \"doesn't throw\" tests that just call and check for exceptions","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No tests that mirror the implementation's logic (re-encoding rather than verifying)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No snapshot tests standing in for behavioural assertions","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Style fingerprints","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Some formatting drift exists (no codebase looks like every file ran through the most aggressive linter)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No ","type":"text"},{"text":"as any","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"@ts-ignore","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"@ts-expect-error","type":"text","marks":[{"type":"code_inline"}]},{"text":" sprinkled where inference is hard","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Repo has some ","type":"text"},{"text":"// HACK:","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"// XXX:","type":"text","marks":[{"type":"code_inline"}]},{"text":" / 2am comments somewhere — codebases without scars are suspect","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No debug artifacts (","type":"text"},{"text":"console.log","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"var_dump","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"dd()","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"dump()","type":"text","marks":[{"type":"code_inline"}]},{"text":") left in production code","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No ","type":"text"},{"text":"if (x) return true; else return false","type":"text","marks":[{"type":"code_inline"}]},{"text":" / redundant type annotations on obvious literals","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Audit Step 4: Build the Slop Ledger","type":"text"}]},{"type":"paragraph","content":[{"text":"End the audit with a verdict table:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"## Code Slop Ledger\n\n| File | Verdict | Top findings | Suggested action |\n|------|---------|--------------|------------------|\n| app/Services/UserExportService.php | INFLATED | 12 narration comments; 3 closing-brace labels; `*Helper` overuse | Strip comments; rename Helper → split into functions |\n| resources/js/Pages/Orders/Show.tsx | CRITICAL | 4 `as any`; mock-everything tests; useless wrapper; impossible null checks | Rewrite section; remove tests; revisit type model |\n| app/Models/Order.php | CLEAN | — | — |\n\n## Summary\n- CLEAN: X files\n- SUSPICIOUS: Y files\n- INFLATED: Z files (top priority: …)\n- CRITICAL: N files (rewrite before merge)","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Apply","type":"text"}]},{"type":"paragraph","content":[{"text":"Reference this skill when:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Reviewing an AI-assisted PR before merge","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Auditing a repo that has accepted heavy AI-assisted contributions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Onboarding a codebase and assessing whether it reads as human-maintained","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hardening a team's code-review checklist against AI slop","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After a \"vibe coding\" sprint, before declaring features done","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Setting up CI gates for AI-output quality","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 1: Detect Project Stack","type":"text"}]},{"type":"paragraph","content":[{"text":"Most rules are stack-agnostic in concept, but examples and detection commands differ between PHP and TypeScript.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Signal","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stack","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tooling","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"composer.json","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PHP / Laravel","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"phpstan","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"phpcs","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"phpmd","type":"text","marks":[{"type":"code_inline"}]},{"text":", manual grep","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"package.json","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Node / TS / React","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"eslint","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"tsc --noEmit","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"knip","type":"text","marks":[{"type":"code_inline"}]},{"text":", manual grep","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Rule Categories by Priority","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Priority","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Category","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Impact","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Prefix","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Comments","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CRITICAL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"comments-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Naming","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CRITICAL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"naming-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Over-engineering","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"over-eng-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Defensive overdose","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"defensive-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Test slop","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HIGH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"test-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Style fingerprints","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MEDIUM","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"style-","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Reference","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Comments (CRITICAL)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"comments-narration","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Comments that just restate the code on the next line","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"comments-empty-docblocks","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Generic ","type":"text"},{"text":"/** Get the user */","type":"text","marks":[{"type":"code_inline"}]},{"text":" over a typed ","type":"text"},{"text":"getUser()","type":"text","marks":[{"type":"code_inline"}]},{"text":" signature","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"comments-placeholder","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"// TODO: implement","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"// your code here","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"// implementation","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"// helper function","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"comments-closing-brace-labels","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"} // end function","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"} // end if block","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Naming (CRITICAL)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"naming-generic-placeholders","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"data","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"result","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"info","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"temp","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"helper","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"value","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"naming-over-descriptive","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"theUserWhoIsCurrentlyLoggedIn","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"calculateTotalAmountFromItemsList","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"naming-suffix-abuse","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"*Helper","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"*Manager","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"*Util","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"*Wrapper","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"*Processor","type":"text","marks":[{"type":"code_inline"}]},{"text":" overused","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"naming-type-in-name","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"userObject","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"resultArray","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"stringData","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"listOfItems","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Over-engineering (HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"over-eng-premature-interface","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Interface with exactly one implementation and no second on the roadmap","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"over-eng-single-method-class","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Classes that exist solely to wrap one function","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"over-eng-useless-wrapper","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Wrapper called from exactly one place, just delegating","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"over-eng-dependency-creep","type":"text","marks":[{"type":"code_inline"}]},{"text":" — New library when an existing dep already does the job","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Defensive overdose (HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"defensive-generic-catch","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"try { ... } catch (e) { console.error(\"error\") }","type":"text","marks":[{"type":"code_inline"}]},{"text":" everywhere","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"defensive-impossible-null","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Null checks after non-null assertions / type-guaranteed values","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"defensive-missing-real","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Defensive in the wrong places; missing timeouts/rate-limits where it matters","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. Test slop (HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"test-mock-everything","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Mock for every dep; the test re-encodes the implementation, not the behaviour","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"test-doesnt-throw","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Tests that just call the function and assert no exception","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"test-mirror-implementation","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Tests whose logic mirrors the production code being tested","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"test-snapshot-abuse","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Snapshot tests replacing behavioural assertions","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"6. Style fingerprints (MEDIUM)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"style-hyper-consistent","type":"text","marks":[{"type":"code_inline"}]},{"text":" — No formatting drift anywhere; every file looks linter-perfect","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"style-as-any-escape","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"as any","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"@ts-ignore","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"@ts-expect-error","type":"text","marks":[{"type":"code_inline"}]},{"text":" sprinkled where types are hard","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"style-no-hack-scars","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Codebase has zero ","type":"text"},{"text":"// HACK:","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"// XXX:","type":"text","marks":[{"type":"code_inline"}]},{"text":" markers; no \"geology\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"style-debug-artifacts","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"console.log","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"var_dump","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"dd()","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"dump()","type":"text","marks":[{"type":"code_inline"}]},{"text":" left in production paths","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"style-trivial-boilerplate","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"if (x) return true; else return false;","type":"text","marks":[{"type":"code_inline"}]},{"text":", redundant TS type annotations on obvious literals","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Essential Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"The \"would this pass code review?\" filter","type":"text"}]},{"type":"paragraph","content":[{"text":"For each code chunk, ask:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Could I cut a third of these comments and the code would be clearer?","type":"text","marks":[{"type":"strong"}]},{"text":" → likely comment slop","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do the variable names tell me what they hold, or just what type they are?","type":"text","marks":[{"type":"strong"}]},{"text":" → likely naming slop","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Could this class be a function?","type":"text","marks":[{"type":"strong"}]},{"text":" → likely over-engineering","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Does this catch block actually handle anything, or just log?","type":"text","marks":[{"type":"strong"}]},{"text":" → likely defensive overdose","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Does this test fail if I break the function?","type":"text","marks":[{"type":"strong"}]},{"text":" → if no, test slop","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Are there any ","type":"text","marks":[{"type":"strong"}]},{"text":"// HACK:","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" / ","type":"text","marks":[{"type":"strong"}]},{"text":"// XXX:","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" markers in the diff?","type":"text","marks":[{"type":"strong"}]},{"text":" → if no, suspicious for AI","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Verdict bands (matched to AI-SLOP-Detector's scoring)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Verdict","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Meaning","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CLEAN","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\u003c 5% of lines flagged","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ship","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SUSPICIOUS","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5–15% flagged","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Review changes one more time","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"INFLATED","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"15–30% flagged","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Strip slop, split commits","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CRITICAL","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"> 30% flagged","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rewrite section before merge","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"How to Use","type":"text"}]},{"type":"paragraph","content":[{"text":"Read individual rule files for detailed conventions and examples:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"rules/comments-narration.md\nrules/naming-generic-placeholders.md\nrules/over-eng-premature-interface.md\nrules/defensive-generic-catch.md\nrules/test-mock-everything.md\nrules/style-hyper-consistent.md","type":"text"}]},{"type":"paragraph","content":[{"text":"Each rule file contains:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"YAML frontmatter (title, impact, tags)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Brief explanation of why the pattern is AI-fingerprint","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Incorrect\" example showing the slop","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Correct\" example showing the human-equivalent","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Detection guidance (grep / eslint / phpstan / heuristic)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Reference link","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"References","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"GitClear — 2025 Code-Quality Trends Report (refactoring collapse, copy-paste surge)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"arXiv 2510.03029 — Investigating the Smells of LLM-Generated Code","type":"text","marks":[{"type":"link","attrs":{"href":"https://arxiv.org/abs/2510.03029","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Addy Osmani — Comprehension Debt (O'Reilly Radar)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Stack Overflow Blog — Eno Reyes Q&A on AI code quality","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"hardikpandya/stop-slop","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/hardikpandya/stop-slop","title":null}}]},{"text":" — sister project for prose slop","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"flamehaven01/AI-SLOP-Detector","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/flamehaven01/AI-SLOP-Detector","title":null}}]},{"text":" — Python AST scanner with 27 patterns","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Full Compiled Document","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"For the complete guide with all rules expanded: ","type":"text"},{"text":"AGENTS.md","type":"text","marks":[{"type":"code_inline"}]}]}]},"metadata":{"date":"2026-06-05","name":"code-slop","author":"@skillopedia","source":{"stars":39,"repo_name":"agent-skills","origin_url":"https://github.com/asyrafhussin/agent-skills/blob/HEAD/skills/code-slop/SKILL.md","repo_owner":"asyrafhussin","body_sha256":"8448cbdb98ff9ab9f56428d313efb19c6c711a67267901261597d1785907b14f","cluster_key":"501f0bc00e04889d9dbc9e478ad9899b224b791c449b1bfee4d0a8fafc6837a1","clean_bundle":{"format":"clean-skill-bundle-v1","source":"asyrafhussin/agent-skills/skills/code-slop/SKILL.md","attachments":[{"id":"18061277-d49e-5d19-a2d4-918d70128f25","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/18061277-d49e-5d19-a2d4-918d70128f25/attachment.md","path":"AGENTS.md","size":117090,"sha256":"11daba64aceadd52aaa5474a7bd243f24ff6615eede7d7ed08a45a6471ba3855","contentType":"text/markdown; charset=utf-8"},{"id":"36d0b27c-f5cb-5091-b735-ff6b30cbbfdf","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/36d0b27c-f5cb-5091-b735-ff6b30cbbfdf/attachment.md","path":"README.md","size":2617,"sha256":"e87602d24190e8b462586fd56014e253c625b569c8d82fc75cebfa9b3f82c86f","contentType":"text/markdown; charset=utf-8"},{"id":"38ae0d9e-88b0-52c4-a06d-4e4fa88e1560","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/38ae0d9e-88b0-52c4-a06d-4e4fa88e1560/attachment.json","path":"metadata.json","size":3105,"sha256":"846413a338afdcc8d083a714560287180d343e9afa1c4c21c8ecc62721959157","contentType":"application/json; charset=utf-8"},{"id":"025c653d-d3b8-5f3f-a93f-76f1e2eec4f3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/025c653d-d3b8-5f3f-a93f-76f1e2eec4f3/attachment.md","path":"rules/_sections.md","size":2454,"sha256":"9792a313e092b1e4285fe4e6b511c878b04996513ea486a0e219770dd0b55632","contentType":"text/markdown; charset=utf-8"},{"id":"16472e71-272e-5374-a381-8f30ce6ab237","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/16472e71-272e-5374-a381-8f30ce6ab237/attachment.md","path":"rules/_template.md","size":670,"sha256":"a4e02244afbaf73c55cd62324c9fc87437b4813aac11b1b0fc2796062d14f5e2","contentType":"text/markdown; charset=utf-8"},{"id":"8044615c-c8e7-509f-8826-117537b5c258","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8044615c-c8e7-509f-8826-117537b5c258/attachment.md","path":"rules/comments-closing-brace-labels.md","size":3552,"sha256":"38cf419b554d12b29c25b840db75a0f5a33a28226ac52ae954ce9df9cff7c648","contentType":"text/markdown; charset=utf-8"},{"id":"a7848d70-4eeb-50e3-a9d0-826c77414468","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a7848d70-4eeb-50e3-a9d0-826c77414468/attachment.md","path":"rules/comments-empty-docblocks.md","size":3884,"sha256":"ad7860aa8637273b05f0cd11c11de4ed38fc0720be409e51c9223552542e5813","contentType":"text/markdown; charset=utf-8"},{"id":"56b33d0a-6e4d-5fd6-86e5-360850663341","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/56b33d0a-6e4d-5fd6-86e5-360850663341/attachment.md","path":"rules/comments-narration.md","size":4317,"sha256":"f06dffa9a3985243b68493fa66d1a97b029500ebefebec3bebde083c36a377a0","contentType":"text/markdown; charset=utf-8"},{"id":"28e28089-a99e-5ee8-a256-2103a46754b9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/28e28089-a99e-5ee8-a256-2103a46754b9/attachment.md","path":"rules/comments-placeholder.md","size":4132,"sha256":"c6c73d3aae0f3ebf043c8a616796d41a90e74b2b6ce16b6b89aabc66e31268c5","contentType":"text/markdown; charset=utf-8"},{"id":"25e26852-7aa1-5cb3-a472-db74fe950e7a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/25e26852-7aa1-5cb3-a472-db74fe950e7a/attachment.md","path":"rules/defensive-generic-catch.md","size":5617,"sha256":"6bafe9ebf33ae349a42c43f6c5b4ebfe2a399ebb4348b2a3aa6ff2677987d96a","contentType":"text/markdown; charset=utf-8"},{"id":"1dfb4a41-3cc6-5d11-8d1d-7248810cb5dd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1dfb4a41-3cc6-5d11-8d1d-7248810cb5dd/attachment.md","path":"rules/defensive-impossible-null.md","size":5426,"sha256":"1d05c97463f3cfa6a300b718eecdbfde1704630d3e3b57228a9e68f856db9554","contentType":"text/markdown; charset=utf-8"},{"id":"b2f7f05b-4b83-5a24-b56f-cf27a72c74cb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b2f7f05b-4b83-5a24-b56f-cf27a72c74cb/attachment.md","path":"rules/defensive-missing-real.md","size":6229,"sha256":"d31750f6380be9c6b8b5a9acddae220cc1ea0406a16d6ba5ef06bc18217a70ce","contentType":"text/markdown; charset=utf-8"},{"id":"7d3e9da9-b8a9-5eb1-959c-3e606781ec49","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7d3e9da9-b8a9-5eb1-959c-3e606781ec49/attachment.md","path":"rules/naming-generic-placeholders.md","size":4015,"sha256":"bf2f2a9b0a261860c8484ff7cc8d5bb04ced5e6b4fc9d5276b41c68040466188","contentType":"text/markdown; charset=utf-8"},{"id":"80f1c25e-16d5-5f00-8be4-d6a3d1d2fede","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/80f1c25e-16d5-5f00-8be4-d6a3d1d2fede/attachment.md","path":"rules/naming-over-descriptive.md","size":4160,"sha256":"dfa059e3798c7dd136e89ac7642efb058c1b6374e1e661175663207e58492747","contentType":"text/markdown; charset=utf-8"},{"id":"6b66f385-ee13-56d3-9c17-ac1b390941a4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6b66f385-ee13-56d3-9c17-ac1b390941a4/attachment.md","path":"rules/naming-suffix-abuse.md","size":5424,"sha256":"1fb2e0245b472e816ec3b81c9fa6063409f6b4684d7c7551f6a295353af649c7","contentType":"text/markdown; charset=utf-8"},{"id":"8aa45611-7f9b-5889-8cbd-f0dbbcf74d62","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8aa45611-7f9b-5889-8cbd-f0dbbcf74d62/attachment.md","path":"rules/naming-type-in-name.md","size":4068,"sha256":"2aac303101755b8507edffb9c3ce813f894759e438ff795c98b775d10f248d91","contentType":"text/markdown; charset=utf-8"},{"id":"8e5ef9dc-b72a-55cf-8ef6-0f190ab08ff4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8e5ef9dc-b72a-55cf-8ef6-0f190ab08ff4/attachment.md","path":"rules/over-eng-dependency-creep.md","size":4382,"sha256":"7de469ce52491a9755a639cb2a760862a7133e9719bf1a0d3a7405053bdd8771","contentType":"text/markdown; charset=utf-8"},{"id":"0e0d5260-b79a-5a4f-9459-da26a8b303cd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0e0d5260-b79a-5a4f-9459-da26a8b303cd/attachment.md","path":"rules/over-eng-premature-interface.md","size":5091,"sha256":"f968f6b4014f6f779abda98a0cf5757ea05b00d5931504384cad70dffc43d4ac","contentType":"text/markdown; charset=utf-8"},{"id":"2102d8c5-96cf-5eed-b37c-15480e90b46f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2102d8c5-96cf-5eed-b37c-15480e90b46f/attachment.md","path":"rules/over-eng-single-method-class.md","size":4872,"sha256":"3bfab4c7dcca4d58f5daf84b46567c4a40ebae5685a7a22ec4e81560523cebb4","contentType":"text/markdown; charset=utf-8"},{"id":"fbc20b7d-2cdf-562b-a56c-19089518df8e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fbc20b7d-2cdf-562b-a56c-19089518df8e/attachment.md","path":"rules/over-eng-useless-wrapper.md","size":4735,"sha256":"afa2359c9a599ef2625efdb1a4c57a72ae951417226679334e8e1439bef04f5a","contentType":"text/markdown; charset=utf-8"},{"id":"18c9f732-52a4-5d85-917a-0fb7015ba17b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/18c9f732-52a4-5d85-917a-0fb7015ba17b/attachment.md","path":"rules/style-as-any-escape.md","size":5098,"sha256":"f4ab782b95537632d1e954acaaa401c22a2738d5e03222331cb90094a8d720b1","contentType":"text/markdown; charset=utf-8"},{"id":"70f6f574-2a39-5d2c-a9cf-25d16f5ccf7c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/70f6f574-2a39-5d2c-a9cf-25d16f5ccf7c/attachment.md","path":"rules/style-debug-artifacts.md","size":5379,"sha256":"eef91306904f292351955f926a8440ac81c3e08d2467c13f459bbe4d8e570875","contentType":"text/markdown; charset=utf-8"},{"id":"878800c4-6bef-5241-bfab-d74996bf7b4b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/878800c4-6bef-5241-bfab-d74996bf7b4b/attachment.md","path":"rules/style-hyper-consistent.md","size":5155,"sha256":"f42418b2b91adb63cb362b02078e7f220e404c26dae4e7127380853f86fcafba","contentType":"text/markdown; charset=utf-8"},{"id":"ada41490-a2fc-5383-8c24-2a8948ce1c48","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ada41490-a2fc-5383-8c24-2a8948ce1c48/attachment.md","path":"rules/style-no-hack-scars.md","size":4859,"sha256":"394e40a2d49e085bc264e09d0432d43cc5db13672f496005064f8daa2f0489d4","contentType":"text/markdown; charset=utf-8"},{"id":"a50336ea-aa56-5057-adbf-7471bdc8aee7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a50336ea-aa56-5057-adbf-7471bdc8aee7/attachment.md","path":"rules/style-trivial-boilerplate.md","size":6009,"sha256":"096ba0a04fabf6791718b2de6c08235255d917d1fd97ee3ef53fdc2f61f4ba66","contentType":"text/markdown; charset=utf-8"},{"id":"9b5e9ef2-c3fb-5edf-9f15-3ac1b5e14153","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9b5e9ef2-c3fb-5edf-9f15-3ac1b5e14153/attachment.md","path":"rules/test-doesnt-throw.md","size":4934,"sha256":"025a3bd5bd612fcc9969446e03478a221df724f367655f7d6f627b7ab89131a0","contentType":"text/markdown; charset=utf-8"},{"id":"e9fed115-42cc-57c7-b7ad-0fcbec2bd837","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e9fed115-42cc-57c7-b7ad-0fcbec2bd837/attachment.md","path":"rules/test-mirror-implementation.md","size":5109,"sha256":"76ccd3f720ce296ab758763b27ff018bf2c8256dc562fc08324afabe97c7323d","contentType":"text/markdown; charset=utf-8"},{"id":"e662ad8c-f022-5b88-90e1-346e5cca68bf","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e662ad8c-f022-5b88-90e1-346e5cca68bf/attachment.md","path":"rules/test-mock-everything.md","size":6424,"sha256":"678efc23ef204337134ff5dedd50d47dea7a85de9d5263e74eeb3974d6e1a30a","contentType":"text/markdown; charset=utf-8"},{"id":"ce867fbc-ef33-5235-9017-aee00542fdd5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ce867fbc-ef33-5235-9017-aee00542fdd5/attachment.md","path":"rules/test-snapshot-abuse.md","size":5322,"sha256":"c01a53ade90a6b94566a33ec687f848461a17e89069a6b64e64bfae8af821204","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"1ad69f06278fc7be13d98eefef9f7ab4315427746b963bdade2bb6552e363cbc","attachment_count":29,"text_attachments":29,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/code-slop/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":0},"license":"MIT","version":"v1","category":"security","metadata":{"author":"agent-skills","version":"1.0.0"},"import_tag":"clean-skills-v1","description":"Detect AI-generated code patterns (\"slop\") in PHP/Laravel and TypeScript/React source — comment narration, generic naming, premature interfaces, defensive overdose, mock-everything tests, and the absence of human \"scars\". Use when reviewing AI-assisted PRs, auditing code for taste/quality (not metrics — that's technical-debt), or hardening a code-review checklist. Triggers on \"review for AI slop\", \"find AI patterns\", \"check code feels human\", \"audit code-quality taste\"."}},"renderedAt":1782979294991}

Code Slop Detection Taste-level review of code for AI-generated patterns. Contains 24 rules across 6 categories covering comments, naming, over-engineering, defensive overdose, test slop, and style fingerprints. Where measures quantitative code debt (complexity, duplication, CVEs), this skill measures the qualitative failure mode: code that passes every metric but reads like a tutorial blog post, not like a human wrote it. Metadata - Version: 1.0.0 - Scope: PHP / Laravel + TypeScript / React (Node) - Rule Count: 24 rules across 6 categories - License: MIT Why this skill exists Industry data o…