Technical Debt Technical debt audit and prioritization framework for PHP/Laravel (MySQL) and Node/TypeScript/React projects. Contains 42 rules across 10 categories covering code, security, design, dependency, test, performance, data, documentation, infrastructure, and process debt. Produces a ranked ledger (effort × impact) so teams know what to fix first , not just what's broken. Supports both coding reference and audit mode with PASS/FAIL/N/A output. Metadata - Version: 1.0.0 - Scope: PHP / Laravel (MySQL) + Node / TypeScript / React - Rule Count: 42 rules across 10 categories - License: MI…

--include='*.php' --include='*.ts' --include='*.tsx' --include='*.js' .\n```\n\n## Incorrect\n\n```typescript\n// ❌ Dead imports, dead helper, dead branch, commented-out block\nimport { legacyFormatter } from './legacy'; // never used after v2 rewrite\nimport { format } from './format';\n\nfunction formatPrice(p: number, currency: string) {\n // const oldImpl = (p) => `${p.toFixed(2)}`; // kept \"just in case\"\n // if (currency === 'BTC') return formatBtc(p); // BTC support removed 2023\n\n if (currency === 'USD') return format(p, 'USD');\n if (currency === 'EUR') return format(p, 'EUR');\n return format(p, 'USD');\n return formatLegacy(p); // unreachable\n}\n\nexport function formatLegacy() { /* called nowhere */ }\n```\n\n**Problems:**\n- Reader has to puzzle out whether the commented BTC branch is coming back\n- `formatLegacy` blocks deleting the `./legacy` module\n- The unreachable `return` raises false suspicion during reviews\n\n## Correct\n\n```typescript\n// ✅ Delete it. Git remembers.\nimport { format } from './format';\n\nfunction formatPrice(p: number, currency: string): string {\n if (currency === 'EUR') return format(p, 'EUR');\n return format(p, 'USD');\n}\n```\n\n**Benefits:**\n- No phantom dependency on the legacy module\n- Reader sees only what runs\n- Diff in `git log` documents *when* and *why* BTC was removed — better than a stale comment\n\n## Remediation Strategy\n\n- **Effort:** S (deletion is mechanical; trust git history)\n- **When to pay down:** Immediately on detection — there is no reason to keep dead code in main.\n\n**Note:** Resist the urge to keep \"might-be-useful-later\" code commented out. If you genuinely need it later, restore it from git history. The cost of a `git revert` is far less than the cost of confusing every future reader.\n\nReference: [Refactoring — Remove Dead Code](https://refactoring.guru/smells/dead-code)\n\n---\n\n\n## Magic Numbers and Hardcoded Literals\n\n**Impact: MEDIUM (Obscure intent; require coordinated edits across files when changed)**\n\nA `0.06` in tax code is invisible business knowledge. The next time the tax rate changes — or the next reader who needs to understand the rule — pays the cost. Magic numbers also make the same value drift across copies (one file uses `0.06`, another `0.065`).\n\n## How to Detect\n\n```bash\n# TypeScript / JavaScript\nnpx eslint . --rule 'no-magic-numbers: [\"error\", { \"ignore\": [0, 1, -1] }]'\n\n# PHP — PHPMD has no built-in magic-number rule; use a Psalm/PHPStan extension\n# or a custom PHPCS sniff. Closest built-ins:\nvendor/bin/phpstan analyse --level=8 # catches some via type-aware analysis\n# Custom: a project-local PHPCS sniff for hardcoded literals in *Service* / *Calculator* classes\n\n# Cross-language grep for suspicious literals in business logic\ngrep -rEn '\\b[0-9]+\\.[0-9]+\\b' app/Services/ src/services/ | grep -v test\n```\n\nThreshold: any non-trivial literal (anything except 0, 1, -1, and indexes used for slicing) appearing in business logic — especially if it appears more than once.\n\n## Incorrect\n\n```typescript\n// ❌ Bare numbers and strings scattered through business logic\nexport function calculateOrder(items: Item[], user: User): Order {\n const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0);\n const tax = subtotal * 0.06; // what's 0.06?\n const shipping = subtotal > 100 ? 0 : 15; // why 100? why 15?\n const cacheKey = `order:${user.id}:v3`; // why v3?\n redis.set(cacheKey, JSON.stringify({ subtotal, tax, shipping }), 'EX', 3600); // 3600 what?\n return { subtotal, tax, shipping };\n}\n```\n\n**Problems:**\n- \"0.06\" appears in 4 other files — tax rate change requires hunting them all\n- A reader can't tell whether `3600` is seconds, milliseconds, or a row count\n- `'v3'` is silent invariant — changing cache format requires knowing about every caller\n\n## Correct\n\n```typescript\n// ✅ Named constants with units and intent\nconst TAX_RATE = 0.06;\nconst FREE_SHIPPING_THRESHOLD = 100;\nconst FLAT_SHIPPING_FEE = 15;\nconst CACHE_VERSION = 'v3';\nconst CACHE_TTL_SECONDS = 60 * 60;\n\nexport function calculateOrder(items: Item[], user: User): Order {\n const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0);\n const tax = subtotal * TAX_RATE;\n const shipping = subtotal > FREE_SHIPPING_THRESHOLD ? 0 : FLAT_SHIPPING_FEE;\n const cacheKey = `order:${user.id}:${CACHE_VERSION}`;\n redis.set(cacheKey, JSON.stringify({ subtotal, tax, shipping }), 'EX', CACHE_TTL_SECONDS);\n return { subtotal, tax, shipping };\n}\n```\n\n**Benefits:**\n- Tax-rate change is one line\n- Units are explicit (`60 * 60` reads as \"seconds in an hour\")\n- Constants are searchable; renames are mechanical\n\n## Remediation Strategy\n\n- **Effort:** S per file (extract constant, replace references)\n- **When to pay down:** When you next change a value (the change is now one line), or when you spot the same literal in 2+ places.\n- **Where to put constants:** as module-level `const` for local values; in a shared `pricing/Config.ts` or `config/billing.php` for cross-module business values.\n\n**Tip:** for genuinely tunable values (rates, thresholds, feature flags), put them in environment-driven config so changes don't require a deploy.\n\nReference: [Refactoring — Replace Magic Number with Symbolic Constant](https://refactoring.guru/replace-magic-number-with-symbolic-constant)\n\n---\n\n\n## Long Parameter Lists\n\n**Impact: MEDIUM (Hard to call correctly; signals mixed responsibilities)**\n\nA function with 7+ parameters is almost impossible to call correctly without re-reading the signature each time. Positional arguments get swapped (`createUser(name, email, ...)` vs `createUser(email, name, ...)`), and the function is usually doing too many things.\n\n## How to Detect\n\n```bash\n# JavaScript / TypeScript\nnpx eslint . --rule 'max-params: [\"error\", 4]'\n\n# PHP\nvendor/bin/phpmd app text codesize # ExcessiveParameterList (default 10; lower via ruleset)\n```\n\nThreshold: **> 4 parameters** (3 or fewer is fine, 4 is borderline, 5+ is debt).\n\n## Incorrect\n\n```typescript\n// ❌ Eight positional parameters — easy to swap, hard to call\nexport function createBooking(\n userId: string,\n hotelId: string,\n roomTypeId: string,\n startDate: Date,\n endDate: Date,\n guestCount: number,\n promoCode: string | null,\n notes: string,\n): Booking {\n // ...\n}\n\n// At the call site:\ncreateBooking(u, h, r, s, e, 2, null, 'Late check-in'); // is 2 the count or roomTypeId?\n```\n\n```php\n// ❌ Same anti-pattern in PHP\npublic function process(\n int $orderId,\n int $userId,\n int $shippingMethodId,\n string $address,\n string $city,\n string $postalCode,\n string $country,\n bool $expedited,\n bool $giftWrap,\n): Order { /* ... */ }\n```\n\n**Problems:**\n- Reordering parameters in a refactor silently breaks every caller (same type → no compile error)\n- Optional parameters force you to pass `null` for arguments you don't care about\n- The signature is doing the work of a value object\n\n## Correct\n\n```typescript\n// ✅ Introduce Parameter Object — named, optional fields make intent explicit\ninterface BookingRequest {\n userId: string;\n hotelId: string;\n roomTypeId: string;\n stay: { from: Date; to: Date };\n guestCount: number;\n promoCode?: string;\n notes?: string;\n}\n\nexport function createBooking(req: BookingRequest): Booking { /* ... */ }\n\n// Call site:\ncreateBooking({\n userId: u,\n hotelId: h,\n roomTypeId: r,\n stay: { from: s, to: e },\n guestCount: 2,\n notes: 'Late check-in',\n});\n```\n\n```php\n// ✅ PHP equivalent: a DTO / value object\nfinal readonly class BookingRequest\n{\n public function __construct(\n public int $userId,\n public int $hotelId,\n public int $roomTypeId,\n public DateRange $stay,\n public int $guestCount,\n public ?string $promoCode = null,\n public ?string $notes = null,\n ) {}\n}\n\npublic function process(BookingRequest $req): Order { /* ... */ }\n```\n\n**Benefits:**\n- Named arguments are self-documenting\n- Adding a field doesn't break callers\n- The object becomes a natural place to add validation or behaviour later\n\n## Remediation Strategy\n\n- **Effort:** S–M (Introduce Parameter Object is a well-known refactor; most IDEs automate it)\n- **When to pay down:** When you need to add yet another parameter to an already-long signature, or when a bug is traced to swapped arguments at a call site.\n\n**Anti-pattern:** \"Boolean flag parameters\" — `process(order, true, false, true)` is unreadable. Replace with enum values or split into separate functions.\n\nReference: [Refactoring — Introduce Parameter Object](https://refactoring.guru/introduce-parameter-object) · [Refactoring — Replace Parameter with Method Call](https://refactoring.guru/replace-parameter-with-method-call)\n\n---\n\n\n## Secrets in Source Code\n\n**Impact: CRITICAL (Once committed, a secret must be rotated AND scrubbed — git history is forever)**\n\nA secret committed to git is compromised the moment the commit lands, even if you delete it in the next commit. Public repos are crawled by bots within minutes; private repos leak through forks, backups, and CI logs. Rotation is mandatory — deletion alone is theater.\n\n## How to Detect\n\n```bash\n# Scan current tree and full history for secrets\ngitleaks git # scan repo history\ngitleaks git --pre-commit --staged # pre-commit hook form (v8.19+)\ntrufflehog filesystem . # alternative scanner\ntrufflehog git file://. --only-verified # verified live secrets\n\n# Targeted grep\ngrep -rEn '(aws_secret|api_key|password|token)\\s*=\\s*[\"\\047][A-Za-z0-9/+=_-]{16,}' .\n\n# Pre-commit hook (gitleaks) — pin to the latest stable release tag\n# .pre-commit-config.yaml\n# - repo: https://github.com/gitleaks/gitleaks\n# rev: v8.30.1\n# hooks: [ { id: gitleaks } ]\n```\n\n## Incorrect\n\n```php\n// ❌ Hardcoded API keys, database passwords, signing keys\n// config/services.php\nreturn [\n 'stripe' => [\n 'secret' => 'sk_live_51Hxxxxxxxxxxxxxxxxxxx', // committed to repo\n ],\n 'aws' => [\n 'key' => 'AKIAIOSFODNN7EXAMPLE',\n 'secret' => 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',\n ],\n];\n\n// .env.example with REAL values that got copied to .env and committed\nDB_PASSWORD=ProductionDb2024!\nJWT_SECRET=hardcoded-jwt-signing-key-do-not-use\n```\n\n**Problems:**\n- The Stripe key is now public — Stripe will eventually rotate it, but not before charges go through\n- The AWS key allows full account access until rotated; bots will find it\n- \"Look, I deleted it in the next commit\" — irrelevant. It's in git history.\n\n## Correct\n\n```php\n// ✅ Read from environment / secret manager\nreturn [\n 'stripe' => ['secret' => env('STRIPE_SECRET')],\n 'aws' => [\n 'key' => env('AWS_ACCESS_KEY_ID'),\n 'secret' => env('AWS_SECRET_ACCESS_KEY'),\n ],\n];\n```\n\n```bash\n# .env.example contains only placeholders\nSTRIPE_SECRET=\nAWS_ACCESS_KEY_ID=\nAWS_SECRET_ACCESS_KEY=\n\n# Real values live in:\n# - Doppler / Vault / AWS Secrets Manager / GCP Secret Manager (production)\n# - Local .env (gitignored)\n# - CI: GitHub Actions secrets, with OIDC for AWS where possible\n```\n\nCI gate:\n\n```yaml\n- uses: gitleaks/gitleaks-action@v2 # fails PR if any secret pattern detected\n```\n\n**Benefits:**\n- Secrets can be rotated without code changes\n- Audit logs show every access (with a real secret manager)\n- New engineers cannot accidentally leak production credentials\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — move forward: env vars + pre-commit hook + CI gate\n - **M** — clean active code paths to use env / secret manager\n - **L** — scrub git history if secrets are old (use `git filter-repo` or BFG; coordinate with team)\n- **When to pay down:**\n - **NOW:** any secret committed to a public repo — rotate first, then clean\n - **This sprint:** any committed secret in private repos\n - **Then:** install gitleaks pre-commit + CI gate to prevent regression\n\n**Rotation checklist for any discovered secret:**\n1. Revoke the secret at the issuer (Stripe, AWS, etc.)\n2. Generate a replacement\n3. Update production via secret manager\n4. Remove from code + commit replacement source\n5. Optionally scrub history (cost-benefit; sometimes rotation is enough)\n6. Add a regex rule to gitleaks to prevent the same shape from re-entering\n\nReference: [GitGuardian — State of Secrets Sprawl](https://www.gitguardian.com/state-of-secrets-sprawl-report-2024) · [gitleaks](https://github.com/gitleaks/gitleaks) · [trufflehog](https://github.com/trufflesecurity/trufflehog)\n\n---\n\n\n## Missing Input Validation\n\n**Impact: CRITICAL (Foundational defense; injection vectors compound silently across endpoints)**\n\nUntrusted input flowing into queries, templates, file paths, or shell commands is the source of most OWASP top-10 vulnerabilities. Every endpoint that accepts external input without explicit validation is debt — and it compounds because each new endpoint adds another opportunity.\n\n## How to Detect\n\n```bash\n# Laravel: controllers reaching directly into request without FormRequest\ngrep -rEn '\\$request->(input|get|all)\\b' app/Http/Controllers/ | \\\n grep -v 'FormRequest\\|validated('\n\n# Express / Node: handlers using req.body / req.query without zod/joi/yup\ngrep -rEn 'req\\.(body|query|params)' src/ | \\\n grep -vE 'parse\\(|validate\\(|safeParse'\n\n# SQL string concatenation (always bad) — grep's --include uses fnmatch, not brace expansion\ngrep -rEn '(SELECT|INSERT|UPDATE|DELETE).*\\+.*\\$|\\\\?.*concat' \\\n --include='*.ts' --include='*.tsx' --include='*.php' --include='*.js' .\n\n# Shell-out from app code (path for command injection)\ngrep -rEn 'exec\\(|shell_exec\\(|proc_open\\(|spawn\\(' \\\n --include='*.ts' --include='*.php' --include='*.js' .\n```\n\nAlso look at audit log coverage: every controller endpoint should map to an explicit validation rule set.\n\n## Incorrect\n\n```typescript\n// ❌ Direct use of request input — type cast is not validation\napp.get('/users', async (req, res) => {\n const limit = parseInt(req.query.limit as string);\n const search = req.query.q as string;\n const [rows] = await pool.query(\n `SELECT * FROM users WHERE name LIKE '%${search}%' LIMIT ${limit}`, // SQL injection\n );\n res.json(rows);\n});\n```\n\n```php\n// ❌ Laravel: same problem, different stack\npublic function index(Request $request) {\n $sort = $request->input('sort'); // attacker controls\n $users = DB::select(\"SELECT * FROM users ORDER BY $sort\");\n return response()->json($users);\n}\n```\n\n**Problems:**\n- SQL injection in both examples (no parameterization, no allowlist)\n- `parseInt` of attacker input returns NaN for non-numbers — `LIMIT NaN` errors leak SQL structure\n- The \"cast as string\" in TypeScript provides zero runtime validation\n\n## Correct\n\n```typescript\n// ✅ Zod schema + parameterized query\nimport { z } from 'zod';\n\nconst ListUsersQuery = z.object({\n limit: z.coerce.number().int().min(1).max(100).default(20),\n q: z.string().max(80).optional(),\n});\n\n// Using `mysql2/promise` — `?` placeholders, parameterized by the driver.\n// We use `pool.query()` (client-side escaping) rather than `pool.execute()`\n// (server-side prepared statements) because mysql2's prepared statements\n// can fail to bind JS numbers to `LIMIT ?` (ER_WRONG_ARGUMENTS in some\n// MySQL versions). With `query()` mysql2 escapes the number safely.\n// Also: MySQL's default collation (`utf8mb4_0900_ai_ci`) is case-insensitive,\n// so plain LIKE matches both 'Asyraf' and 'asyraf' without `LOWER(...)`.\nimport mysql from 'mysql2/promise';\nconst pool = mysql.createPool({ /* ... */ });\n\napp.get('/users', async (req, res) => {\n const parsed = ListUsersQuery.safeParse(req.query);\n if (!parsed.success) return res.status(400).json(parsed.error.flatten());\n\n const { limit, q } = parsed.data;\n const [rows] = await pool.query(\n 'SELECT * FROM users WHERE (? IS NULL OR name LIKE ?) LIMIT ?',\n [q ?? null, q ? `%${q}%` : null, limit],\n );\n res.json(rows);\n});\n```\n\n```php\n// ✅ Laravel FormRequest with allowlisted sort\nfinal class ListUsersRequest extends FormRequest\n{\n public function rules(): array {\n return [\n 'sort' => ['nullable', Rule::in(['name', 'created_at'])],\n 'limit' => ['integer', 'min:1', 'max:100'],\n ];\n }\n}\n\npublic function index(ListUsersRequest $request) {\n $sort = $request->validated('sort', 'created_at');\n return DB::table('users')->orderBy($sort)->paginate($request->validated('limit', 20));\n}\n```\n\n**Benefits:**\n- Bad input → 400 with a clear message, never reaches the database\n- SQL injection eliminated by parameterization + allowlist\n- Validation is a single auditable location per endpoint\n\n## Remediation Strategy\n\n- **Effort:** S per endpoint\n- **When to pay down:**\n - **NOW:** any endpoint that takes input into a raw SQL string, shell command, or file path\n - **This sprint:** all unvalidated endpoints in critical paths (auth, payment, profile)\n - **Then:** lint rules that fail PRs lacking validation schemas\n- **Tip:** put validation at the boundary (controller / route handler), then trust the validated shape downstream. Don't re-validate the same fields in 5 places.\n\nReference: [OWASP — Input Validation Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html) · [Laravel Validation](https://laravel.com/docs/validation) · [Zod](https://zod.dev/)\n\n---\n\n\n## Auth and Hardening Gaps\n\n**Impact: HIGH (Outdated auth defaults are tomorrow's breach disclosures)**\n\nAuth choices made years ago — password hashing algorithm, session lifetime, missing MFA, missing rate limits, missing security headers — accrue as silent debt. They only become visible during pen tests or incidents, where they're suddenly the biggest finding.\n\n## How to Detect\n\nAudit each of:\n1. **Password hashing** — bcrypt is OK, argon2id is preferred; MD5/SHA-1 are unacceptable\n2. **Session lifetime** — infinite or multi-month sessions are debt\n3. **MFA support** — is it offered? Is it enforced for admins?\n4. **Rate limiting** — login, password-reset, API endpoints\n5. **Security headers** — `Content-Security-Policy`, `Strict-Transport-Security`, `X-Content-Type-Options`, `Referrer-Policy`\n6. **Authorization checks** — every endpoint enforces who can call it\n7. **CSRF protection** — present on every state-changing endpoint that uses cookies\n\n```bash\n# Check security headers\ncurl -sI https://your.app | grep -iE 'content-security|strict-transport|x-content-type|x-frame|referrer'\n\n# Mozilla Observatory CLI\n# https://observatory.mozilla.org/\n# securityheaders.com\n\n# Laravel: scan controllers for missing authorization\ngrep -rEn 'function (index|show|store|update|destroy)\\(' app/Http/Controllers/ | \\\n while IFS=: read -r FILE LINE REST; do \\\n grep -q 'authorize\\|Gate::\\|middleware' \"$FILE\" || echo \"MISSING AUTHZ: $FILE:$LINE\"; \\\n done\n```\n\n## Incorrect\n\n```php\n// ❌ Multiple hardening gaps\n// User registration with MD5 (catastrophic)\npublic function register(Request $request) {\n User::create([\n 'email' => $request->email,\n 'password' => md5($request->password), // hash algorithm from 1992\n ]);\n}\n\n// Session config (config/session.php)\n'lifetime' => 525600, // 1 year sessions — every stolen device is forever\n\n// No rate limit on login → credential stuffing trivial\nRoute::post('/login', [AuthController::class, 'login']);\n\n// No CSP — XSS gets full DOM access\n// (no header set in middleware)\n\n// Authorization missing — anyone with a session can hit admin endpoints\nRoute::get('/admin/users', [AdminController::class, 'users']);\n```\n\n## Correct\n\n```php\n// ✅ Password hashing via Hash::make (bcrypt by default in Laravel 11+;\n// argon2id is opt-in via config/hashing.php — preferred for new projects)\nUser::create([\n 'email' => $request->validated('email'),\n 'password' => Hash::make($request->validated('password')),\n]);\n\n// Reasonable session lifetime, secure flags\n'lifetime' => 60 * 8, // 8h\n'secure' => true, // HTTPS only\n'http_only' => true,\n'same_site' => 'lax',\n\n// Login rate limited\nRoute::post('/login', [AuthController::class, 'login'])\n ->middleware('throttle:5,1'); // 5 attempts per minute per IP\n\n// Authorization on every admin endpoint\nRoute::middleware(['auth', 'can:viewAdmin'])->group(function () {\n Route::get('/admin/users', [AdminController::class, 'users']);\n});\n\n// Security headers via middleware (or a package like spatie/laravel-csp)\nreturn $next($request)\n ->header('Content-Security-Policy', \"default-src 'self'; ...\")\n ->header('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload')\n ->header('X-Content-Type-Options', 'nosniff')\n ->header('Referrer-Policy', 'strict-origin-when-cross-origin');\n```\n\n**Benefits:**\n- Hashing upgrade path supported by `Hash::needsRehash()` — old MD5 hashes can be transparently re-hashed on next successful login (after one-time migration to bcrypt/argon2id)\n- Session theft window is bounded\n- Authorization is explicit and uniformly applied via middleware\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — security headers, rate limits, session lifetime\n - **M** — adding MFA, enforcing authz across all endpoints\n - **L** — password hash migration (must be done on next login per user; takes weeks of natural traffic)\n- **When to pay down:**\n - **NOW:** any MD5/SHA-1 password hashing, missing CSRF, public admin endpoints\n - **This quarter:** MFA, security headers, rate limits\n - **Ongoing:** authz coverage in CI (e.g., test that every authenticated route asserts a policy)\n\n**Tip:** run `https://securityheaders.com` against staging at least once per quarter — it's free, fast, and surfaces missing headers immediately.\n\nReference: [OWASP — Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html) · [OWASP — Session Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html) · [Mozilla Observatory](https://observatory.mozilla.org/)\n\n---\n\n\n## Tight Coupling\n\n**Impact: HIGH (Changes ripple unpredictably; modules cannot be replaced)**\n\nTight coupling means modules depend on each other's internals. A change in one ripples into many, and you can't swap an implementation without rewriting consumers. The symptom: small features take days, simple bug fixes break unrelated tests.\n\n## How to Detect\n\n- Count cross-module imports — modules importing > 10 files from other modules are suspect\n- Look for direct instantiation (`new X()`) of cross-module dependencies in business logic\n- Search for reaches into private/internal namespaces\n\n```bash\n# Find imports from too many other modules\ngrep -rE \"^(import|use|require)\" src/orders/ | \\\n awk -F'[\\\"\\\\\\\\\\\\047]' '{print $2}' | sort -u | wc -l\n```\n\n## Incorrect\n\n```typescript\n// ❌ OrderService reaches deep into Payment, Inventory, and Email internals\nimport { StripeClient } from '../payments/stripe/client';\nimport { StripeWebhookSecret } from '../payments/stripe/config';\nimport { InventoryDB } from '../inventory/db/connection';\nimport { SesTransport } from '../email/transports/ses';\n\nexport class OrderService {\n async place(order: Order) {\n const stripe = new StripeClient(StripeWebhookSecret.value);\n const charge = await stripe.charges.create({ amount: order.total });\n\n const conn = await InventoryDB.connect();\n await conn.query('UPDATE inventory SET stock = stock - ? WHERE sku = ?', [order.qty, order.sku]);\n\n const ses = new SesTransport(process.env.AWS_KEY!);\n await ses.send({ to: order.email, body: '...' });\n }\n}\n```\n\n**Problems:**\n- Cannot test without a real Stripe key, DB, and SES credentials\n- Switching from Stripe → Adyen means rewriting `place()`\n- A schema change in Inventory breaks orders\n\n## Correct\n\n```typescript\n// ✅ Depend on interfaces, inject implementations\nexport class OrderService {\n constructor(\n private payments: PaymentGateway,\n private inventory: InventoryRepository,\n private notifications: NotificationSender,\n ) {}\n\n async place(order: Order) {\n await this.payments.charge(order.total, order.token);\n await this.inventory.reserve(order.sku, order.qty);\n await this.notifications.orderConfirmed(order);\n }\n}\n```\n\n**Benefits:**\n- Each dependency is a stable interface — internals can change freely\n- Tests use in-memory fakes; no credentials needed\n- Stripe → Adyen swap is a one-line wiring change\n\n## Remediation Strategy\n\n- **Effort:** M per module boundary\n- **When to pay down:** When two modules' release schedules need to decouple, or when a swap is on the roadmap.\n\nReference: [Robert Martin — Stable Dependencies Principle (Package Principles)](https://en.wikipedia.org/wiki/Package_principles)\n\n---\n\n\n## Circular Dependencies\n\n**Impact: HIGH (Indicates broken module boundaries; breaks tree-shaking and isolation)**\n\nModule A imports B, B imports A. Either the boundary is wrong, or one module is misplaced. Cycles break dead-code elimination, make modules impossible to test in isolation, and cause runtime initialization order bugs in many languages.\n\n## How to Detect\n\n```bash\n# JavaScript / TypeScript\nnpx madge --circular --extensions ts,tsx src/\n\n# PHP (architectural rules, including cycle detection between layers/modules)\nvendor/bin/deptrac analyse # qossmic/deptrac\n# or: vendor/bin/phparkitect check\n```\n\nThreshold: **zero cycles** is the only acceptable target. Even one cycle indicates a layering problem.\n\n## Incorrect\n\n```typescript\n// ❌ user/index.ts imports order/, order/index.ts imports user/\n// src/user/index.ts\nimport { Order } from '../order';\nexport class User {\n orders: Order[] = [];\n totalSpent() { return this.orders.reduce((s, o) => s + o.total, 0); }\n}\n\n// src/order/index.ts\nimport { User } from '../user';\nexport class Order {\n customer: User;\n total: number;\n}\n```\n\n**Problems:**\n- Either module fails to initialize cleanly under some bundlers (one side is `undefined` at import time)\n- Cannot extract `user` or `order` into a separate package\n- A test of `user` necessarily pulls in `order`\n\n## Correct\n\n```typescript\n// ✅ Option A: extract shared types to a third module\n// src/shared/types.ts\nexport interface UserRef { id: string; }\nexport interface OrderRef { id: string; total: number; }\n\n// src/user/index.ts → imports types only\n// src/order/index.ts → imports types only\n\n// ✅ Option B: invert the dependency — let one own the relationship\n// src/order/index.ts owns the customer reference;\n// user no longer knows about order. totalSpent() lives in an OrderService.\n```\n\n**Benefits:**\n- No initialization-order bugs\n- Each module can be packaged independently\n- Tests load the minimum surface\n\n## Remediation Strategy\n\n- **Effort:** M (mechanical once you decide the direction)\n- **When to pay down:** As soon as `madge`/equivalent reports a new cycle. Adding tests across the boundary first protects the refactor.\n\nReference: [Madge — Circular Dependencies](https://github.com/pahen/madge)\n\n---\n\n\n## Leaky Abstractions\n\n**Impact: HIGH (Implementation details leak past layer boundaries, blocking change)**\n\nAn abstraction leaks when its consumers depend on details it was supposed to hide — ORM models in controllers, framework types in domain logic, HTTP concerns in repositories. Once leaked, the abstraction can no longer be changed independently.\n\n## How to Detect\n\n- Grep for ORM/framework types in inner layers (`Eloquent\\Model`, `Request`, `HttpClient`) where they should not appear\n- Look for return types like `Builder`, `Collection\u003cModel>`, or `Response` crossing service boundaries\n\n```bash\n# Laravel: Eloquent leaking out of repositories\ngrep -rEn 'extends Model|Eloquent\\\\Builder' app/Services/ app/Domain/\n\n# Express: Request/Response leaking into services\ngrep -rEn '\\\\bRequest\\\\b|\\\\bResponse\\\\b' src/services/\n```\n\n## Incorrect\n\n```php\n// ❌ Repository returns an Eloquent Builder — controller chains ORM calls\nfinal class OrderRepository\n{\n public function forCustomer(int $customerId): Builder\n {\n return Order::query()->where('customer_id', $customerId);\n }\n}\n\n// Controller does ORM chaining directly:\n$orders = $this->repo->forCustomer($id)\n ->where('status', 'paid')\n ->with(['items', 'shipments'])\n ->orderByDesc('created_at')\n ->paginate(20);\n```\n\n**Problems:**\n- Repository's promised abstraction (\"get orders for customer\") is gone — controllers issue arbitrary queries\n- Cannot swap Eloquent for another data source without rewriting every controller\n- N+1 risk now lives in controllers, not in one auditable place\n\n## Correct\n\n```php\n// ✅ Repository returns plain DTOs or a paginated value object — no Builder leak\nfinal class OrderRepository\n{\n /** @return Paginated\u003cOrderSummary> */\n public function paidForCustomer(int $customerId, int $page = 1): Paginated\n {\n $query = Order::query()\n ->where('customer_id', $customerId)\n ->where('status', 'paid')\n ->with(['items', 'shipments'])\n ->orderByDesc('created_at');\n\n return Paginated::from($query->paginate(20, page: $page), OrderSummary::class);\n }\n}\n```\n\n**Benefits:**\n- Controllers receive a stable type; database choice is hidden\n- Eager-loading and ordering are owned by the repository (one auditable place)\n- Repository can be replaced with an HTTP gateway, gRPC client, or in-memory fake\n\n## Remediation Strategy\n\n- **Effort:** M–L (each leak is local but there are usually many)\n- **When to pay down:** When a swap or split is on the roadmap, OR when you find yourself fixing N+1 bugs in multiple controllers — the leak is now causing concrete pain.\n\nReference: [Joel Spolsky — The Law of Leaky Abstractions](https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/)\n\n---\n\n\n## Shotgun Surgery\n\n**Impact: HIGH (One conceptual change forces edits across many files)**\n\nA change is \"shotgun surgery\" when adding one concept (a new payment method, a new locale, a new currency) requires edits in 5+ files. The information is fragmented; whoever makes the change is guaranteed to miss a spot.\n\n## How to Detect\n\n```bash\n# Find files commonly changed together (co-change hotspots)\ngit log --since='6 months ago' --name-only --pretty=format:'COMMIT' | \\\n awk '/COMMIT/{print \"---\"; next} {print}' | \\\n # process: count file pairs appearing in the same commit\n # any pair > 10 co-changes is a shotgun-surgery suspect\n\n# Search for switch/if chains on the same enum across multiple files\ngrep -rEn \"case PaymentMethod::|=== ['\\\"]stripe['\\\"]\" --include='*.{php,ts}'\n```\n\n## Incorrect\n\n```typescript\n// ❌ Adding a new payment method \"klarna\" requires editing 6 files\n\n// src/payments/types.ts\ntype PaymentMethod = 'stripe' | 'paypal' | 'wire';\n\n// src/payments/validator.ts\nif (m === 'stripe') { /* */ } else if (m === 'paypal') { /* */ } else if (m === 'wire') { /* */ }\n\n// src/payments/feeCalculator.ts\nconst FEES = { stripe: 0.029, paypal: 0.034, wire: 0 };\n\n// src/payments/iconUrl.ts\nconst ICONS = { stripe: '...', paypal: '...', wire: '...' };\n\n// src/payments/displayName.ts\nconst NAMES = { stripe: 'Card', paypal: 'PayPal', wire: 'Bank wire' };\n\n// src/payments/router.ts\nswitch (m) { case 'stripe': return new StripeClient(); /* ... */ }\n```\n\n**Problems:**\n- Adding Klarna means hunting through 6+ files — easy to miss one\n- New engineers cannot find \"what makes a payment method valid\"\n- Tests don't catch a forgotten file until production\n\n## Correct\n\n```typescript\n// ✅ One Strategy per method — adding Klarna is one new file\n// src/payments/methods/StripeMethod.ts\nexport const StripeMethod: PaymentMethod = {\n id: 'stripe',\n displayName: 'Card',\n iconUrl: '/icons/stripe.svg',\n feeRate: 0.029,\n validate: (payload) => /* ... */,\n charge: (amount, token) => new StripeClient().charge(amount, token),\n};\n\n// src/payments/methods/index.ts\nexport const METHODS = [StripeMethod, PayPalMethod, WireMethod /*, KlarnaMethod */];\n```\n\n**Benefits:**\n- Adding Klarna = one new file + one export — nothing else changes\n- All knowledge about a method lives in one place\n- TypeScript types prevent \"forgot a branch\" bugs\n\n## Remediation Strategy\n\n- **Effort:** M (collect scattered knowledge into one type per concept)\n- **When to pay down:** Before the *next* new variant is added — the pain is highest right when adding one.\n\nReference: [Refactoring Guru — Shotgun Surgery](https://refactoring.guru/smells/shotgun-surgery)\n\n---\n\n\n## Outdated Dependency Versions\n\n**Impact: HIGH (Each major version skipped exponentially raises upgrade cost)**\n\nSkipping major versions doesn't save effort — it just defers and compounds it. Two majors behind is roughly 4× the upgrade work of one major behind, because deprecations from intermediate versions stack.\n\n## How to Detect\n\n```bash\n# Node / TypeScript\nnpm outdated # shows current vs wanted vs latest\nnpx npm-check-updates # interactive upgrade tool\n\n# PHP / Laravel\ncomposer outdated --direct # direct deps only\ncomposer outdated --direct --major-only\n```\n\nThreshold: any dependency **more than 2 major versions behind** OR **more than 18 months behind on minor/patch**.\n\n## Incorrect\n\n```json\n// ❌ package.json — multiple dependencies 3+ majors behind\n{\n \"dependencies\": {\n \"react\": \"^16.8.0\", // current major: 19\n \"express\": \"^4.17.0\", // current major: 5\n \"webpack\": \"^4.46.0\", // current major: 5\n \"jest\": \"^26.6.3\", // current major: 29\n \"@types/node\": \"^14.0.0\" // current: 22\n }\n}\n```\n\n**Problems:**\n- React 16 → 19 means a full upgrade path through legacy mode, automatic batching, new JSX transform, etc.\n- Webpack 4 → 5 requires polyfill changes, ESM handling, persistent caching adoption\n- Jest 26 → 29 changes test environment and ESM behaviour\n- Each upgrade *separately* is now too big to fit in one sprint\n\n## Correct\n\n```json\n// ✅ Upgraded incrementally; pin policies documented\n{\n \"dependencies\": {\n \"react\": \"^19.0.0\",\n \"express\": \"^5.0.0\",\n \"webpack\": \"^5.95.0\",\n \"jest\": \"^29.7.0\",\n \"@types/node\": \"^22.0.0\"\n }\n}\n```\n\nEstablish a **monthly upgrade rhythm** rather than letting deps drift for a year.\n\n**Benefits:**\n- Each upgrade fits in a small PR\n- Security patches and deprecation warnings land while context is fresh\n- Avoids the \"we can't upgrade React because of 5 transitive blockers\" situation\n\n## Remediation Strategy\n\n- **Effort:** S per minor upgrade, M–L per major upgrade\n- **When to pay down:**\n - **Patch/minor:** weekly or biweekly automated PRs (Renovate, Dependabot)\n - **Major:** scheduled, one dep at a time, with a release plan\n- **Order of operations:** upgrade dev tools (TypeScript, ESLint, Jest) before frameworks; framework before app code\n\nReference: [Renovate Bot](https://docs.renovatebot.com/) · [Dependabot](https://docs.github.com/en/code-security/dependabot)\n\n---\n\n\n## Abandoned and Unmaintained Packages\n\n**Impact: HIGH (No upstream fixes for bugs, CVEs, or runtime upgrades)**\n\nA package without a release in 24+ months, with open critical issues, or with the maintainer publicly stepping away is **abandonware**. The next CVE, the next Node/PHP version, or the next breaking dep change becomes *your* problem to fix.\n\n## How to Detect\n\nIndicators to check on each direct dependency:\n\n- **Last release date** (`npm view \u003cpkg> time.modified`, `composer info \u003cpkg>`)\n- **Open issues vs closed** ratio (high open count, no recent triage)\n- **Maintainer activity** (last commit > 2 years ago)\n- **Explicit deprecation** (`npm view \u003cpkg> deprecated`)\n- **Known alternatives community has migrated to**\n\n```bash\n# Node\nnpm view \u003cpkg> time.modified deprecated\nnpx npm-check # flags deprecated packages\nnpx snyk test # warns on unmaintained packages\n\n# PHP\ncomposer info \u003cpkg> # shows abandoned status from packagist\ncomposer audit # also reports abandonment\n```\n\n## Incorrect\n\n```json\n// ❌ Depending on packages flagged as abandoned or deprecated\n{\n \"dependencies\": {\n \"request\": \"^2.88.0\", // deprecated 2020 by maintainer\n \"node-uuid\": \"^1.4.8\", // replaced by `uuid` years ago\n \"moment\": \"^2.29.0\", // maintenance-only since 2020, deprecated by author\n \"babel-eslint\": \"^10.1.0\" // replaced by @babel/eslint-parser\n }\n}\n```\n\n**Problems:**\n- `request` has an open CVE with no upstream fix coming\n- `moment` ships ~290KB of timezone data — `date-fns` or `dayjs` do it in 10KB\n- Future engineer cannot tell whether these are \"trusted core deps\" or graveyard residents\n\n## Correct\n\n```json\n// ✅ Migrated to maintained alternatives\n{\n \"dependencies\": {\n \"undici\": \"^6.0.0\", // replaces `request`\n \"uuid\": \"^9.0.0\", // replaces `node-uuid`\n \"date-fns\": \"^3.0.0\", // replaces `moment`\n \"@babel/eslint-parser\": \"^7.23.0\"\n }\n}\n```\n\n**Benefits:**\n- CVEs in maintained packages get upstream fixes — you only patch\n- Bundle size and runtime characteristics improve\n- New engineers don't waste time on packages they \"shouldn't have learned\"\n\n## Remediation Strategy\n\n- **Effort:** S–M per package (depends on API surface used)\n- **When to pay down:**\n 1. **Now:** any abandoned dep with a known CVE\n 2. **This quarter:** any abandoned dep blocking a runtime upgrade\n 3. **Opportunistically:** the rest, when you're already touching that code path\n\n**Tip:** When forced to keep an abandoned dep temporarily, lock the version exactly, document why in a comment in the manifest, and create a tracking issue.\n\nReference: [npm Deprecation Policy](https://docs.npmjs.com/policies/deprecation) · [Packagist Abandoned Packages](https://packagist.org/about#abandoning-a-package)\n\n---\n\n\n## Known Security Advisories\n\n**Impact: CRITICAL (Public CVEs are pre-published attack instructions)**\n\nOnce a CVE is public, exploit attempts start within hours. A HIGH/CRITICAL advisory in your dependency tree is not \"tech debt to schedule\" — it's an unmitigated security incident you haven't responded to yet.\n\n## How to Detect\n\n```bash\n# Node\nnpm audit # full report\nnpm audit --audit-level=high # CI-friendly threshold\nnpm audit fix # auto-fix non-breaking\n\n# PHP\ncomposer audit # built-in since Composer 2.4\ncomposer audit --format=json\n\n# Cross-stack\nsnyk test # https://snyk.io\n```\n\n## Incorrect\n\n```bash\n# ❌ Pre-commit and CI ignore audit results\n$ npm audit\n12 vulnerabilities (3 moderate, 7 high, 2 critical)\n$ git push # CI passes — audit isn't a gate\n```\n\n**Problems:**\n- CRITICAL CVEs sitting in main are public attack surface\n- No paper trail of when each was acknowledged\n- Each new dep adds more without anyone noticing\n\n## Correct\n\n```yaml\n# ✅ CI gate that fails on high+ vulnerabilities\n# .github/workflows/security.yml\n- name: Audit dependencies\n run: |\n npm audit --audit-level=high\n composer audit --abandoned=fail\n\n# ✅ Renovate / Dependabot configured for security PRs\n# renovate.json\n{\n \"vulnerabilityAlerts\": { \"enabled\": true, \"labels\": [\"security\"] },\n \"osvVulnerabilityAlerts\": true\n}\n```\n\n**Benefits:**\n- New CVEs auto-generate PRs within hours of disclosure\n- CI fails the moment a high-severity advisory lands\n- Audit log of every advisory acknowledgement and fix\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — patch/minor bump available, no breaking change\n - **M** — requires upgrade across multiple deps\n - **L** — vulnerable code is in an abandoned dep; replacement needed\n- **When to pay down:**\n - **CRITICAL / HIGH:** within 24–72 hours\n - **MEDIUM:** within the current sprint\n - **LOW:** opportunistically with other dep work\n- If a fix is genuinely blocked, document the **compensating control** (WAF rule, input validation, feature disable) and the **target unblocking date**.\n\nReference: [GitHub Advisory Database](https://github.com/advisories) · [OSV.dev](https://osv.dev/)\n\n---\n\n\n## Unused Dependencies\n\n**Impact: MEDIUM (Inflate install size, supply-chain surface, and audit noise)**\n\nA dependency you don't use is one you still ship, audit, and trust. Each unused dep is a potential supply-chain footgun (compromised maintainer, malicious post-install script) for zero benefit.\n\n## How to Detect\n\n```bash\n# Node / TypeScript\nnpx depcheck # unused + missing deps\nnpx knip # also finds unused files and exports\n\n# PHP / Composer\ncomposer-unused # https://github.com/composer-unused/composer-unused\nvendor/bin/composer-unused\n```\n\n## Incorrect\n\n```json\n// ❌ package.json declares deps no longer imported\n{\n \"dependencies\": {\n \"lodash\": \"^4.17.21\", // grep shows zero `from 'lodash'` imports\n \"axios\": \"^1.6.0\", // migrated to fetch 6 months ago\n \"moment\": \"^2.29.4\", // migrated to date-fns; one stale import left\n \"node-fetch\": \"^3.3.0\" // only used in a deleted script\n }\n}\n```\n\n**Problems:**\n- Each `npm install` downloads code that does nothing\n- Each `npm audit` reports advisories you can't act on (you don't even use the affected code paths)\n- New engineers see them and assume they're load-bearing\n\n## Correct\n\n```bash\n# ✅ Remove unused deps\n$ npx depcheck\nUnused dependencies: lodash, axios, moment, node-fetch\n$ npm uninstall lodash axios moment node-fetch\n$ npm audit # quieter report\n```\n\n```yaml\n# Add to CI to keep it clean\n- run: npx depcheck --ignores=\"@types/*,eslint-*\"\n```\n\n**Benefits:**\n- Smaller `node_modules`, faster installs, faster CI\n- Audit reports are signal, not noise\n- Reduced supply-chain attack surface\n\n## Remediation Strategy\n\n- **Effort:** S (almost always)\n- **When to pay down:** Immediately on detection. Add a depcheck/composer-unused step to CI to prevent regression.\n\n**Watch out for:**\n- **Transitive usage only:** some deps are loaded by tooling (e.g., babel plugins listed in `babel.config.js`). Verify before removing.\n- **Type-only packages:** `@types/*` packages are used by the compiler but invisible to import scanners — configure your tool to ignore them.\n\nReference: [depcheck](https://github.com/depcheck/depcheck) · [composer-unused](https://github.com/composer-unused/composer-unused)\n\n---\n\n\n## Coverage Gaps on Critical Paths\n\n**Impact: HIGH (Uncovered critical paths fail in production, not in CI)**\n\nAggregate coverage percentage is a vanity metric. What matters is whether the **critical paths** (checkout, auth, payment, signup) have integration tests. A repo at 85% line coverage with zero tests on payment has more risk than one at 60% with a full checkout suite.\n\n## How to Detect\n\nIdentify critical paths from the product (signup, login, checkout, refund, etc.), then check:\n\n```bash\n# Node\nnpx jest --coverage --coverageReporters=text\nnpx vitest run --coverage\n\n# PHP\nvendor/bin/phpunit --coverage-html=coverage/\n\n# Targeted: are there any integration tests for the checkout flow?\ngrep -rln 'test.*checkout\\|describe.*checkout' tests/\n```\n\nFor each critical path, look for:\n- An end-to-end / integration test that exercises the happy path\n- Tests for failure modes (declined card, out-of-stock, expired session)\n- At least one test that runs against the real DB / real network adapter\n\n## Incorrect\n\n```\n// ❌ 92% line coverage — but all of it is on getters, mappers, and trivial helpers.\n// The actual checkout pipeline has NO integration test.\n\nsrc/\n├── utils/ — 100% covered (10 tests)\n├── formatters/ — 100% covered (15 tests)\n├── checkout/\n│ ├── pricing.ts — 20% covered\n│ ├── inventory.ts — 0% covered\n│ ├── payment.ts — 0% covered\n│ └── orchestrate.ts — 0% covered ← THE checkout flow\n```\n\n**Problems:**\n- Coverage metric is \"green\" → team feels safe → bugs land in checkout\n- No safety net for refactoring the orchestrator\n- On-call has no automated regression check before deploys\n\n## Correct\n\n```typescript\n// ✅ One integration test per critical-path scenario, hitting real adapters where feasible\n// tests/checkout.integration.test.ts\ndescribe('checkout', () => {\n it('places an order with valid card', async () => {\n const order = await checkoutClient.place({ /* ... */ });\n expect(order.status).toBe('confirmed');\n expect(inventory.stockFor('SKU-1')).toBe(initial - 1);\n });\n\n it('rejects when card is declined', async () => {\n await expect(checkoutClient.place({ token: 'tok_chargeDeclined' }))\n .rejects.toThrow(PaymentDeclined);\n expect(inventory.stockFor('SKU-1')).toBe(initial); // no leak\n });\n\n it('rejects when item is out of stock', async () => { /* ... */ });\n it('issues idempotent retries safely', async () => { /* ... */ });\n});\n```\n\n**Benefits:**\n- Regression on checkout fails CI, not customers\n- Refactoring the orchestrator is safe\n- New scenarios (e.g., new payment method) extend a known suite\n\n## Remediation Strategy\n\n- **Effort:** M per critical path (the first test costs the most; subsequent are cheap)\n- **When to pay down:** **Before** the next behaviour change on that path. The change itself is your justification.\n- **Target:** one integration test per critical path, covering happy + 2–3 failure modes. Don't chase a coverage number.\n\nReference: [Martin Fowler — Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html)\n\n---\n\n\n## Flaky Tests\n\n**Impact: HIGH (Erode trust in CI; teach the team to ignore failures)**\n\nA flaky test — one that passes and fails on the same code — is worse than no test. The first few times, you re-run. After that, the team learns to retry-and-merge, and the suite stops catching real regressions.\n\n## How to Detect\n\n```bash\n# Run the suite N times against the same commit\nfor i in {1..20}; do npm test -- --silent || echo \"Failed run $i\"; done\n\n# Better: track flakiness across CI runs\n# - GitHub Actions: re-run-on-failure stats\n# - CircleCI / Buildkite: built-in flaky-test reports\n# - https://github.com/jonny-improbable/jest-circus-flaky-retry\n\n# Identify suspect tests by name patterns\ngrep -rEn 'sleep|setTimeout|Date\\.now|Math\\.random' tests/\n```\n\nCommon flaky-test smells:\n- `sleep(N)` / `setTimeout` instead of waiting for a condition\n- Order-dependent tests (depend on previous test's state)\n- Time-dependent assertions (`expect(date).toBe(today)`)\n- Tests against shared mutable resources (real Redis without cleanup, real DB without transactions)\n\n## Incorrect\n\n```typescript\n// ❌ Three different forms of flakiness\ntest('debounced search', async () => {\n searchInput.value = 'foo';\n await sleep(300); // race: timing-dependent\n expect(results).toHaveLength(5);\n});\n\ntest('order created today', () => {\n const order = createOrder();\n expect(order.createdAt.toDateString())\n .toBe(new Date().toDateString()); // fails when run at midnight\n});\n\ntest('user can log in', async () => {\n await db.query('INSERT INTO users ...'); // depends on previous test's cleanup\n // ...\n});\n```\n\n**Problems:**\n- Sleep races: works on fast machines, fails on busy CI runners\n- Date-dependent: fails on DST changes, midnight, leap-day\n- Shared-state: passes alone, fails in suite\n\n## Correct\n\n```typescript\n// ✅ Deterministic alternatives\ntest('debounced search', async () => {\n searchInput.value = 'foo';\n await waitFor(() => expect(results).toHaveLength(5)); // wait on condition\n});\n\ntest('order created at the expected time', () => {\n vi.setSystemTime(new Date('2026-01-15T10:00:00Z')); // freeze time\n const order = createOrder();\n expect(order.createdAt).toEqual(new Date('2026-01-15T10:00:00Z'));\n});\n\nbeforeEach(async () => {\n await db.transaction(async (t) => { /* setup, rolled back after each test */ });\n});\n```\n\n**Benefits:**\n- Test passes deterministically regardless of machine speed, clock, or order\n- Suite can run in parallel without contention\n- CI failures become signal again\n\n## Remediation Strategy\n\n- **Effort:** S per test (the fix is usually local — replace sleep with waitFor, freeze the clock, use transactions)\n- **When to pay down:**\n 1. **Quarantine** the flaky test immediately (mark as `.skip` with a tracking issue) so it stops eroding trust\n 2. **Fix** within the sprint — quarantine is a deferral, not a destination\n 3. **Delete** if it can't be made deterministic in reasonable effort\n\n**Policy:** Re-running a failed CI without a root-cause is anti-pattern. Always file an issue.\n\nReference: [Google Testing Blog — Flaky Tests](https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html)\n\n---\n\n\n## Skipped and Disabled Tests\n\n**Impact: HIGH (Dark coverage — code looks tested, isn't)**\n\nSkipped tests are worse than missing tests because they create a false sense of safety. A `test.skip(...)` or `markTestSkipped()` left in main without an owner, issue, or deadline is debt that grows in silence.\n\n## How to Detect\n\n```bash\n# JavaScript / TypeScript (Jest / Vitest)\ngrep -rEn '\\\\.skip|xdescribe|xit|test\\\\.todo|describe\\\\.skip' tests/ src/\n\n# Pest / PHPUnit\ngrep -rEn 'markTestSkipped|markTestIncomplete|@group\\\\s+skip|->skip\\\\(' tests/\n```\n\nCross-reference each hit with:\n- Is there a linked issue?\n- Is there a comment explaining why?\n- Is there a date or condition for re-enabling?\n\n## Incorrect\n\n```typescript\n// ❌ Bare skips with no context\ndescribe.skip('checkout', () => { /* ... */ });\n\ntest.skip('refunds work', () => { /* ... */ });\n\ntest('payment webhook', () => {\n if (process.env.CI) return; // silent skip on CI\n // ...\n});\n```\n\n**Problems:**\n- Why are checkouts skipped? Nobody remembers\n- The webhook test runs only locally — production behaviour is untested\n- Coverage report shows them as \"executed\" but with zero assertions\n\n## Correct\n\n```typescript\n// ✅ Every skip has an owner, reason, and re-enable trigger\ntest.skip(\n 'refunds work — DISABLED 2026-02-10 (#1247) re-enable after Stripe webhook v2 migration',\n () => { /* ... */ }\n);\n\n// ✅ Or: delete and replace if the test cannot be repaired\n// git log will remember it ever existed.\n```\n\nAdd CI checks:\n\n```yaml\n# .github/workflows/test-hygiene.yml\n- name: Disallow new bare skips\n run: |\n NEW_SKIPS=$(git diff origin/main...HEAD -- 'tests/**' \\\n | grep -E '^\\+.*\\.skip\\(' | grep -v '#[0-9]')\n test -z \"$NEW_SKIPS\" || { echo \"Bare skip without ticket\"; exit 1; }\n```\n\n**Benefits:**\n- Every skip is auditable and assigned\n- New skips require a ticket — prevents quiet accumulation\n- The team has a count of \"real\" coverage\n\n## Remediation Strategy\n\n- **Effort:** S per skip (decide: fix, delete, or document)\n- **When to pay down:**\n - **Now:** audit existing skips → add owner + ticket OR delete\n - **Ongoing:** CI gate prevents new bare skips\n- **Default action:** if a skip is older than 90 days with no movement, delete the test. If it's worth keeping, it's worth re-enabling.\n\nReference: [PHPUnit docs](https://docs.phpunit.de/) (see the \"Incomplete and Skipped Tests\" chapter in the current major version)\n\n---\n\n\n## Slow Test Suites\n\n**Impact: HIGH (Slow feedback compounds across every engineer every day)**\n\nA test suite that takes 20 minutes costs every engineer 20 minutes per push. Multiplied across team size and PR count, it becomes the single biggest tax on velocity — and pushes the team to stop running tests locally.\n\n## How to Detect\n\n```bash\n# Show slowest tests\nnpx jest --verbose # per-test timing in output\nnpx jest --reporters=jest-slow-test-reporter # 3rd-party reporter (recommended)\nnpx vitest --reporter=verbose | sort -k4 -rn | head -20\n\n# PHPUnit\nvendor/bin/phpunit --log-junit=junit.xml\n# Then sort junit.xml by time\n\n# Track total wall-clock time per CI run\n# - Watch for trends: \"is it growing 10% per quarter?\"\n```\n\nTargets to aim for:\n- **Unit suite:** \u003c 30 seconds\n- **Integration suite:** \u003c 5 minutes\n- **Total CI per PR:** \u003c 10 minutes\n\n## Incorrect\n\n```typescript\n// ❌ Common slow-test smells\n\n// 1. Real network calls in unit tests\ntest('user can fetch profile', async () => {\n const data = await fetch('https://api.production.example.com/users/1');\n});\n\n// 2. Real sleeps for \"wait for something\"\ntest('debounced search', async () => {\n search('foo');\n await sleep(2000); // 2s × 50 tests = 100s wasted\n});\n\n// 3. Full DB rebuild per test instead of transaction rollback\nbeforeEach(async () => {\n await execSync('npm run db:migrate:fresh'); // 10s × 200 tests\n});\n```\n\n## Correct\n\n```typescript\n// ✅ Fast alternatives\n// 1. Mock the network at the boundary for unit tests (MSW v2)\nimport { http, HttpResponse } from 'msw';\nimport { setupServer } from 'msw/node';\nconst server = setupServer(\n http.get('/users/1', () => HttpResponse.json(USER)),\n);\n\n// 2. Wait on conditions, not on time\nawait waitFor(() => expect(results).toHaveLength(5)); // returns in ms\n\n// 3. Use transactions or schema snapshots\nbeforeEach(async () => {\n await db.beginTransaction();\n});\nafterEach(async () => {\n await db.rollback();\n});\n```\n\nParallelize where safe:\n\n```bash\n# Jest: --maxWorkers=50%\n# Vitest: vitest --pool=threads (Vitest v1+; `--threads` is deprecated)\n# PHPUnit: vendor/bin/paratest -p 8\n```\n\n**Benefits:**\n- Engineers run tests locally → faster feedback before push\n- CI cost drops linearly with wall time\n- Slow-by-design tests (E2E) can be quarantined to a nightly job\n\n## Remediation Strategy\n\n- **Effort:** S–M per hotspot (mostly mechanical: mock, fake, paratest, transactionalize)\n- **When to pay down:**\n 1. **First win:** identify and fix the 5 slowest tests — usually 50%+ of total time\n 2. **Then:** enable parallel execution\n 3. **Then:** budget. Document a per-suite wall-clock budget and fail CI if exceeded.\n\n**Budget enforcement:**\n\n```yaml\n# Hard cap test duration in CI\n- run: timeout 600 npm test # fails CI if > 10 minutes\n```\n\nReference: [Martin Fowler — Test Suite Speed](https://martinfowler.com/articles/practical-test-pyramid.html#TheImportanceOf(Test)Speed)\n\n---\n\n\n## N+1 Query Patterns\n\n**Impact: HIGH (Linear request → quadratic database load; latency scales with data, not traffic)**\n\nAn N+1 happens when fetching a list of N records issues 1 query for the list + N follow-up queries for each row's relations. Latency looks fine in dev (small N) and explodes in prod (large N). N+1 is the single most common database debt in ORM-driven codebases.\n\n## How to Detect\n\n```bash\n# Laravel: Telescope panel \"Queries\" — sort by request, count per page\nphp artisan telescope:install\n\n# Laravel: detect N+1s in dev by failing on excessive queries\ncomposer require beyondcode/laravel-query-detector --dev\n\n# Node / TypeScript: enable query logging in your ORM (Prisma `log: ['query']`,\n# TypeORM `logging: 'all'`, Drizzle `logger: true`) and count queries per request.\n\n# Heuristic: > ~10 queries for a typical list endpoint is suspect.\n```\n\nPattern signal: query count grows with result count instead of staying constant.\n\n## Incorrect\n\n```php\n// ❌ Laravel: relation accessed inside a loop → one query per order\n$orders = Order::where('status', 'paid')->get(); // 1 query\n\nforeach ($orders as $order) {\n $customer = $order->customer; // 1 query each (N more)\n $shipping = $order->shipments; // 1 query each (another N)\n echo \"{$customer->name}: \" . $shipping->count();\n}\n// Total: 1 + 2N queries for 100 orders → 201 queries\n```\n\n```typescript\n// ❌ TypeORM equivalent\nconst users = await userRepo.find(); // 1 query\nfor (const user of users) {\n const orders = await user.orders; // N queries (lazy relation)\n console.log(user.email, orders.length);\n}\n```\n\n**Problems:**\n- A list endpoint that returns 200 rows takes 401 round-trips to the database\n- Latency is invisible at low traffic; suddenly catastrophic with real data\n- Database connection pool saturates; queue depth spikes for unrelated requests\n\n## Correct\n\n```php\n// ✅ Eager-load with `with()` — fixed query count regardless of N\n$orders = Order::where('status', 'paid')\n ->with(['customer', 'shipments'])\n ->get(); // 3 queries total: orders, customers, shipments\n\nforeach ($orders as $order) {\n echo \"{$order->customer->name}: \" . $order->shipments->count();\n}\n```\n\n```typescript\n// ✅ Specify relations in the find call\nconst users = await userRepo.find({ relations: { orders: true } });\n```\n\nFor large result sets, also paginate:\n\n```php\n$orders = Order::with(['customer', 'shipments'])\n ->where('status', 'paid')\n ->cursorPaginate(50); // memory + DB bounded\n```\n\n**Benefits:**\n- Query count becomes constant per request, regardless of result size\n- Latency is predictable in production\n- Connection pool stays healthy under load\n\n## Remediation Strategy\n\n- **Effort:** S per endpoint (add `with(...)` or equivalent eager-load)\n- **When to pay down:**\n - **NOW:** any endpoint that emits > 10× more queries than its result count\n - **Then:** add a CI test that asserts query counts on critical endpoints\n- **Detection workflow:**\n 1. Install Telescope / Bullet / Silk\n 2. Walk the top 10 most-hit endpoints in a representative env\n 3. Sort by query count per request — N+1s are at the top\n 4. Add `with(...)` and re-measure\n\n**Anti-pattern:** \"let's add a cache\" before fixing N+1. Caching hides the problem but doesn't fix it — the first request after expiry still does 201 queries.\n\nReference: [Laravel Eager Loading](https://laravel.com/docs/eloquent-relationships#eager-loading) · [beyondcode/laravel-query-detector](https://github.com/beyondcode/laravel-query-detector)\n\n---\n\n\n## Unbounded Result Sets\n\n**Impact: HIGH (One large customer kills latency for everyone)**\n\nAn endpoint that returns \"all\" records works fine until one customer has 100,000 of them. At that point, the request times out, exhausts memory, and (in shared-tenancy systems) takes down the database for everyone else. Unbounded result sets are time bombs proportional to your most successful customer.\n\n## How to Detect\n\n```bash\n# Find list endpoints / repository methods without LIMIT or pagination\ngrep -rEn '\\\\->all\\\\(|\\\\->get\\\\(\\\\)|findAll|fetchAll' --include='*.php' --include='*.ts'\n\n# Frontend: full-result-set components without \"load more\" / virtualization\ngrep -rEn 'map\\\\(|forEach\\\\(' --include='*.tsx' src/ | head -50 # review for unbounded lists\n\n# Check actual prod metrics: max rows returned by each endpoint over the last 30 days\n# (most APMs / DB monitors expose this)\n```\n\nHeuristic: any list endpoint **without an explicit limit** is debt. Any endpoint that returns objects with nested collections (orders → items, users → posts) is doubly so.\n\n## Incorrect\n\n```php\n// ❌ Returns every order ever placed by the customer\npublic function index(Customer $customer) {\n return OrderResource::collection($customer->orders); // 50,000 rows → 8MB response\n}\n\n// ❌ \"Export all users\" endpoint loads entire table into memory\npublic function export() {\n $users = User::all(); // OOM at scale\n return Excel::download(new UsersExport($users), 'users.xlsx');\n}\n```\n\n```typescript\n// ❌ Frontend asks for everything and filters client-side\nconst allTransactions = await fetch('/api/transactions').then(r => r.json());\nconst recent = allTransactions.filter(t => isThisMonth(t.date)); // 100,000 rows → 200,000 ms parse\n```\n\n## Correct\n\n```php\n// ✅ Cursor-based pagination (preferred for \"infinite scroll\" / large datasets)\npublic function index(Customer $customer, Request $request) {\n return OrderResource::collection(\n $customer->orders()->latest()->cursorPaginate(50)\n );\n}\n\n// ✅ Streaming export — never loads the full set into memory\npublic function export() {\n return response()->streamDownload(function () {\n User::query()->orderBy('id')->lazy()->each(function ($user) {\n // write one row at a time\n });\n }, 'users.csv');\n}\n```\n\n```typescript\n// ✅ Server filters; client requests only what it needs\nconst { data, nextCursor } = await fetch('/api/transactions?since=2026-05-01&limit=50')\n .then(r => r.json());\n```\n\n**Benefits:**\n- Memory and latency stay bounded regardless of customer size\n- Database returns fewer rows over the wire\n- Frontend can render results progressively\n\n## Remediation Strategy\n\n- **Effort:** S–M per endpoint (cursor pagination is more invasive than offset; both are mechanical)\n- **When to pay down:**\n - **NOW:** any endpoint where the result count is user-controlled and unbounded\n - **NOW:** any export endpoint loading the entire result set into memory\n - **Then:** add a max-result-count assertion in CI for list endpoints\n- **Pagination choice:**\n - **Cursor** — best for \"next page\" UX, large datasets, real-time-ish data (no skip cost)\n - **Offset** — easier to implement, OK for small/medium datasets; expensive at high page numbers\n - **Keyset** — similar to cursor but using a real column (id, created_at)\n\n**Tip:** when retrofitting pagination on a public API, support both old (full response) and new (paginated) shapes during a deprecation window, then remove the unbounded form.\n\nReference: [Laravel Pagination](https://laravel.com/docs/pagination) · [Slack — Cursor Pagination](https://docs.slack.dev/apis/web-api/pagination) · [Use the Index, Luke — Paging Through Results](https://use-the-index-luke.com/sql/partial-results/fetch-next-page)\n\n---\n\n\n## Frontend Bundle Bloat\n\n**Impact: HIGH (Bundle size directly drives bounce rate, INP, and conversion)**\n\nEvery kilobyte the browser must parse and execute slows down first paint, interaction-readiness, and on mobile networks, page abandonment. Bundle bloat creeps in invisibly — a moment-import here, a `lodash` import there, an unused route bundled with the entry point — and the team only notices when Lighthouse drops a grade.\n\n## How to Detect\n\n```bash\n# Vite\nnpx vite-bundle-visualizer\n\n# Webpack\nnpx webpack-bundle-analyzer dist/stats.json\n\n# Any bundler — source map visualization\nnpx source-map-explorer 'dist/**/*.js'\n\n# CI bundle-size budget (size-limit)\nnpm install --save-dev size-limit @size-limit/preset-app\n# package.json:\n# \"size-limit\": [{ \"path\": \"dist/index.js\", \"limit\": \"200 KB\" }]\nnpx size-limit\n```\n\nBudget targets (gzipped, mobile-first):\n- **Initial bundle:** \u003c 200 KB\n- **Route bundles:** \u003c 100 KB\n- **Single dep:** anything > 50 KB deserves justification\n\n## Incorrect\n\n```typescript\n// ❌ Default import → ships the entire library\nimport _ from 'lodash'; // ~70 KB min / ~24 KB gzip\nimport moment from 'moment'; // ~290 KB min / ~70 KB gzip with locales\n\nconst debounced = _.debounce(handler, 200);\nconst formatted = moment().format('YYYY-MM-DD');\n\n// ❌ No code splitting — admin pages bundled with the public site\nimport AdminDashboard from './admin/Dashboard';\nimport AdminUsers from './admin/Users';\n// ... all eagerly imported in the entry file\n```\n\n**Problems:**\n- Importing all of lodash to use `debounce` is like buying a truck to carry one tomato\n- Moment with all locales ships ~70 KB gzip of timezone data nobody uses\n- Admin code bundled with the public site triples the entry-bundle for 99% of visitors who never visit `/admin`\n\n## Correct\n\n```typescript\n// ✅ Named imports / smaller libraries\nimport { debounce } from 'lodash-es'; // tree-shaken via ESM\nimport { format } from 'date-fns'; // ~10 KB gzip for the single `format` import\n\nconst debounced = debounce(handler, 200);\nconst formatted = format(new Date(), 'yyyy-MM-dd');\n\n// ✅ Route-level code splitting\nconst AdminDashboard = lazy(() => import('./admin/Dashboard'));\nconst AdminUsers = lazy(() => import('./admin/Users'));\n\n// In your router:\n\u003cRoute path=\"/admin\" element={\u003cSuspense fallback={\u003cSpinner />}>\u003cAdminDashboard />\u003c/Suspense>} />\n```\n\nAdd a CI guard:\n\n```yaml\n- name: Bundle-size budget\n run: npx size-limit\n# Fails the build if any tracked bundle exceeds its budget\n```\n\n**Benefits:**\n- Initial bundle shrinks dramatically; LCP and INP both improve\n- Admin code only loads for admin users\n- A regression (someone adds `import * as everything`) fails CI\n\n## Remediation Strategy\n\n- **Effort:** S–M per dep (swap imports, lazy-load routes)\n- **When to pay down:**\n - **First:** run the bundle visualizer and target the top 5 contributors\n - **Then:** install a bundle-size budget in CI to prevent regression\n - **Ongoing:** every PR that adds a dep > 20KB should be reviewed for alternatives\n- **Common wins:**\n - `moment` → `date-fns` or `dayjs` (–80–90% size)\n - `lodash` → `lodash-es` + named imports, or native equivalents\n - Route-level code splitting (10× reduction on admin-heavy apps)\n - Drop polyfills for unsupported browsers\n - Replace heavy SVG icon sets with on-demand icon components\n\nReference: [web.dev — Apply Instant Loading](https://web.dev/articles/apply-instant-loading-with-prpl) · [BundlePhobia](https://bundlephobia.com/) · [size-limit](https://github.com/ai/size-limit)\n\n---\n\n\n## Missing Caching Opportunities\n\n**Impact: HIGH (Repeated work on every request — predictable latency you're paying for indefinitely)**\n\nCaching is the highest-ROI performance work for read-heavy systems: a single Redis lookup replaces a 200ms aggregation query, and an HTTP cache header lets the browser skip the round-trip entirely. Missing caching is invisible — the system \"works\" — but every page load pays the full computation cost.\n\n## How to Detect\n\nLook for these signals in any read-heavy path:\n\n1. **Expensive aggregations recomputed per request** (dashboard counters, reports, leaderboards)\n2. **External API calls without caching** (currency rates, geolocation, third-party catalogs)\n3. **Static-ish responses with no `Cache-Control` headers** (asset metadata, config, public lists)\n4. **Database queries that join 5+ tables to return the same shape repeatedly**\n5. **Same query fired by N concurrent requests, no request coalescing**\n\n```bash\n# Look at HTTP response headers — missing Cache-Control on static-ish endpoints\ncurl -sI https://your.app/api/categories | grep -iE 'cache-control|etag|last-modified'\n\n# Find APIs that re-compute the same shape across requests\n# (look in your APM for endpoints with consistently high mean latency + low variance)\n\n# Laravel — endpoints that hit expensive accessors / relations without remember()\ngrep -rEn '\\\\->withCount\\\\(|\\\\->withSum\\\\(' app/Http/Controllers/ | head\n```\n\n## Incorrect\n\n```php\n// ❌ Recomputed on every request — even though categories change ~once a week\npublic function categories() {\n $categories = Category::query()\n ->withCount('products')\n ->with(['parent', 'translations'])\n ->orderBy('sort_order')\n ->get();\n\n return CategoryResource::collection($categories);\n}\n\n// ❌ Third-party rate fetched per request — 200ms latency on every checkout\npublic function checkout(Order $order) {\n $rate = Http::get('https://api.fx.example.com/rates/USD-MYR')->json('rate');\n return ['total' => $order->total * $rate];\n}\n\n// ❌ No HTTP caching → browser revalidates on every navigation\nreturn response()->json($publicConfig);\n```\n\n**Problems:**\n- Database scans for `categories` happen N times per second across the whole fleet\n- Every checkout pays 200ms for an FX rate that updates hourly\n- Browser fetches `publicConfig` on every page load even though it changes daily\n\n## Correct\n\n```php\n// ✅ Tag-keyed Redis cache with explicit invalidation\n// Note: Cache::tags() only works with the `redis` or `memcached` driver.\n// On `file` / `database` / `array` stores it throws BadMethodCallException —\n// fall back to plain keys + manual invalidation on those drivers.\npublic function categories() {\n $categories = Cache::tags(['categories'])->remember(\n 'categories:tree:v2',\n now()->addHour(),\n fn () => Category::query()\n ->withCount('products')\n ->with(['parent', 'translations'])\n ->orderBy('sort_order')\n ->get(),\n );\n return CategoryResource::collection($categories);\n}\n\n// In Category::saved / Category::deleted listeners:\n// Cache::tags(['categories'])->flush();\n```\n\n```php\n// ✅ External API result cached for an hour\n$rate = Cache::remember('fx:USD-MYR', now()->addHour(), function () {\n return Http::get('https://api.fx.example.com/rates/USD-MYR')->json('rate');\n});\n```\n\n```php\n// ✅ HTTP cache headers for safely-cacheable public responses\nreturn response()->json($publicConfig)\n ->header('Cache-Control', 'public, max-age=3600, stale-while-revalidate=86400')\n ->header('ETag', md5(json_encode($publicConfig)));\n```\n\n**Benefits:**\n- Mean latency drops orders of magnitude for cacheable endpoints\n- Origin server, database, and third-party APIs all see reduced load\n- Browser short-circuits revalidation for unchanged resources\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — add `remember()` / HTTP `Cache-Control` to a single endpoint\n - **M** — design a cache-key scheme + invalidation strategy for a domain\n - **L** — distributed caching with proper invalidation across services\n- **When to pay down:**\n - **NOW:** any endpoint hitting an expensive query AND showing high traffic AND data is read-mostly\n - **Then:** look at top-10 requests by total time in APM; cache the easy wins first\n- **Layer order (cheapest first):**\n 1. HTTP cache headers (browser does the work)\n 2. CDN / edge cache (`Cache-Control: public, s-maxage=N`)\n 3. Application memory cache (per-process, fastest, no network)\n 4. Redis / Memcached (shared across processes, sub-ms)\n 5. Database query cache / materialized views (last resort)\n\n**Anti-patterns:**\n- **Caching everything by default** — cache invalidation is hard; cache only what hurts\n- **TTL-only invalidation when freshness matters** — combine with event-based busts (`Cache::flush` on writes)\n- **Caching personalized data with a public key** — leaks one user's data to another\n- **Caching error responses indefinitely** — always exclude 4xx/5xx from caches\n\nReference: [Laravel — Cache](https://laravel.com/docs/cache) · [MDN — HTTP Caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching) · [RFC 9111 — HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111)\n\n---\n\n\n## Database Schema Drift\n\n**Impact: HIGH (Production schema diverges from code; migrations break unpredictably)**\n\nSchema drift is the gap between what the migrations say the database looks like and what it actually looks like. Once it exists, every new migration is a roll of the dice — it might apply cleanly, fail halfway through, or succeed but leave the schema in a state neither environment expects.\n\n## How to Detect\n\n```bash\n# Laravel: compare current schema to migration plan\nphp artisan migrate:status\nphp artisan schema:dump --prune # snapshot current schema\n# Apply on a fresh DB and diff against production schema\n\n# MySQL: schema-only dump for diffing across environments\nmysqldump --no-data --routines --triggers -u root -p prod_db > prod.sql\nmysqldump --no-data --routines --triggers -u root -p staging_db > staging.sql\ndiff -u staging.sql prod.sql\n\n# Drift signals:\n# - Tables that exist in prod but not in any migration\n# - Columns/indexes added manually via ALTER TABLE outside migration\n# - Migrations marked \"ran\" in different orders across environments\n# - Migrations that have been edited in place (hashes differ)\n```\n\nTooling: `atlas migrate diff`, `bytebase`, `liquibase diff`, `flyway info` for managed migration platforms.\n\n## Incorrect\n\n```\n❌ Common drift scenarios:\n\n1. \"Quick fix\" applied directly to prod\n DBA runs: ALTER TABLE orders ADD COLUMN priority INT DEFAULT 0;\n …without a corresponding migration. New migrations now run against a schema\n the codebase has never seen.\n\n2. Migration edited after being applied somewhere\n Original: CREATE TABLE refunds (id, order_id, amount);\n Edited: CREATE TABLE refunds (id, order_id, amount, reason TEXT NOT NULL);\n Some environments have the column; others don't. Same migration hash, two realities.\n\n3. Models with attributes not in any migration\n class Order extends Model {\n protected $fillable = ['status', 'priority']; // 'priority' has no migration\n }\n```\n\n**Problems:**\n- New environments (CI, staging, new dev laptops) can't reach the same schema\n- The next migration may fail at 50% completion, leaving the schema half-done\n- Reports and queries silently rely on columns that \"are there in prod\" but not in code\n\n## Correct\n\n```php\n// ✅ Every schema change goes through a migration\n// One migration per change, never edit a merged migration\n\n// database/migrations/2026_05_16_000000_add_priority_to_orders.php\nreturn new class extends Migration {\n public function up(): void {\n Schema::table('orders', fn (Blueprint $t) =>\n $t->unsignedTinyInteger('priority')->default(0)->after('status')\n );\n }\n public function down(): void {\n Schema::table('orders', fn (Blueprint $t) => $t->dropColumn('priority'));\n }\n};\n```\n\nCI gate:\n\n```yaml\n- name: Schema is migration-derivable\n run: |\n php artisan migrate --pretend --database=ci_clone # ensure all migrations apply\n php artisan schema:dump --prune\n git diff --exit-code database/schema/ # fail if dump differs\n```\n\n**Benefits:**\n- Any environment can be reconstructed from migrations alone\n- Code and schema move together, atomically reviewable in PRs\n- A failed migration in CI catches drift before it hits prod\n\n## Remediation Strategy\n\n- **Effort:** M–L (depends on how far drift has progressed)\n- **When to pay down:**\n - **NOW:** any drift discovered during incident response — fix during the postmortem\n - **As a cleanup project:** snapshot prod schema → generate a \"consolidation migration\" that brings empty databases to current state → mark all prior migrations as \"ran\" in environments that already match\n- **Anti-patterns:**\n - Editing applied migrations (always create a new one)\n - \"DBA runs prod ALTERs directly\" without a corresponding migration\n - Squashing migrations in a way that breaks existing environments\n\n**Tip:** in long-lived projects, periodically generate a \"consolidated migration\" from the current schema (`schema:dump`) so new environments don't have to replay 5 years of migrations. Keep the consolidated dump and historical migrations both checked in.\n\nReference: [Laravel — Schema Dumping](https://laravel.com/docs/migrations#squashing-migrations) · [Atlas — Migration Diff](https://atlasgo.io/) · [Liquibase Diff](https://docs.liquibase.com/commands/inspection/diff.html)\n\n---\n\n\n## Missing Database Indexes\n\n**Impact: HIGH (Query time grows linearly with data; locks and connection pool compound)**\n\nA missing index turns a 5ms query into a 5-second query as the table grows. Worse, because slow queries hold connections longer, missing indexes cascade into connection-pool exhaustion and 503s for unrelated traffic. Indexing decisions made early are usually right; indexes never added at all are silent debt.\n\n## How to Detect\n\n```sql\n-- MySQL (requires sys schema, enabled by default in 5.7+): tables doing frequent full scans\nSELECT * FROM sys.schema_tables_with_full_table_scans\nWHERE rows_full_scanned > 1000\nORDER BY rows_full_scanned DESC;\n\n-- MySQL: query plan for a hot query (type=ALL means full table scan)\nEXPLAIN SELECT * FROM orders WHERE customer_id = 123;\nEXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123; -- MySQL 8.0+\n\n-- MySQL: indexes that exist but are never read\nSELECT * FROM sys.schema_unused_indexes;\n\n-- MySQL: top queries by total latency (performance_schema must be ON)\nSELECT * FROM sys.statement_analysis\nORDER BY total_latency DESC LIMIT 20;\n```\n\nApplication-side:\n- **Laravel:** Telescope's \"Queries\" panel — sort by duration\n- **Node ORMs:** enable query logging (Prisma `log: ['query', 'warn']`, TypeORM `logging: 'all'`) and review slow entries\n\nLook for: queries on foreign keys, status columns, date filters, and `ORDER BY` columns without supporting indexes.\n\n## Incorrect\n\n```php\n// ❌ Foreign key columns without indexes\nSchema::create('orders', function (Blueprint $t) {\n $t->id();\n $t->unsignedBigInteger('customer_id'); // FK column — no index!\n $t->string('status'); // queried often — no index!\n $t->timestamp('created_at');\n});\n\n// Query: SELECT * FROM orders WHERE customer_id = ? AND status = 'paid' ORDER BY created_at DESC\n// → Seq Scan of the entire orders table for every customer profile load\n```\n\n**Problems:**\n- Customer-profile page becomes O(N) of total orders, not the customer's orders\n- Status-based dashboards lock the table during long scans\n- Connection pool saturates under modest load\n\n## Correct\n\n```php\n// ✅ Index foreign keys, status columns, and ORDER BY columns\nSchema::create('orders', function (Blueprint $t) {\n $t->id();\n $t->foreignId('customer_id')->constrained()->index(); // index on FK\n $t->string('status')->index();\n $t->timestamp('created_at');\n\n // Composite index for the common (customer_id, status, created_at) query path\n $t->index(['customer_id', 'status', 'created_at']);\n});\n```\n\nVerify the plan after indexing:\n\n```sql\nEXPLAIN\nSELECT * FROM orders\nWHERE customer_id = 123 AND status = 'paid'\nORDER BY created_at DESC LIMIT 50;\n\n-- Want to see in `key`: orders_customer_id_status_created_at_index\n-- NOT: NULL (or `type` = ALL → full table scan)\n```\n\n**Benefits:**\n- Query time becomes O(log N) instead of O(N)\n- Connection pool stays healthy under load\n- The index pays for itself many times over per request\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — add a single index (online index creation in MySQL/InnoDB minimizes downtime)\n - **M** — add several indexes; analyze query patterns first\n - **L** — large tables (100M+ rows) require careful online build + monitoring\n- **When to pay down:**\n - **NOW:** any query on a hot path showing `type=ALL` in EXPLAIN over a > 10k-row table\n - **NOW:** any FK column without an index (defaults vary by ORM — Eloquent does not auto-index FKs)\n - **Then:** monitor `sys.statement_analysis` weekly and add indexes for the top long-runners\n\n**Anti-patterns:**\n- **Indexing everything** — wastes write speed and disk; index the columns you actually filter/sort by\n- **Adding indexes blindly** without `EXPLAIN ANALYZE` — verify the planner actually uses them\n- **Forgetting to remove old indexes** — duplicate or unused indexes cost disk + write speed\n\n**Online index creation (MySQL/InnoDB):**\n```sql\nALTER TABLE orders\n ADD INDEX orders_customer_status_created_at_index (customer_id, status, created_at),\n ALGORITHM=INPLACE, LOCK=NONE;\n```\n\nFor huge tables, prefer `pt-online-schema-change` (Percona Toolkit) or `gh-ost` (GitHub) — both run truly non-blocking schema changes.\n\nReference: [Use the Index, Luke](https://use-the-index-luke.com/) · [MySQL — EXPLAIN Output Format](https://dev.mysql.com/doc/refman/8.4/en/explain-output.html) · [MySQL — sys Schema](https://dev.mysql.com/doc/refman/8.4/en/sys-schema.html) · [pt-online-schema-change](https://docs.percona.com/percona-toolkit/pt-online-schema-change.html)\n\n---\n\n\n## Orphaned Records and Referential Drift\n\n**Impact: MEDIUM (Bugs, broken reports, and inconsistent state compound over time)**\n\nOrphaned records — child rows whose parent no longer exists — are usually invisible until a report breaks or a query joins fail mysteriously. The root cause is almost always missing foreign-key constraints, ad-hoc deletes that bypass the ORM, or \"soft-delete the parent, leave children active\" semantics.\n\n## How to Detect\n\n```sql\n-- Orphans on a single relation\nSELECT child.id, child.parent_id\nFROM order_items child\nLEFT JOIN orders parent ON parent.id = child.parent_id\nWHERE parent.id IS NULL;\n\n-- MySQL: columns ending in _id that are NOT part of any FK constraint.\n-- Note: `_` is a single-char wildcard in LIKE — escape it (use `#` as ESCAPE char to\n-- avoid backslash-escape confusion in MySQL string literals). Exclude the PK `id` column.\nSELECT c.TABLE_NAME, c.COLUMN_NAME\nFROM information_schema.COLUMNS c\nWHERE c.TABLE_SCHEMA = DATABASE()\n AND c.COLUMN_NAME LIKE '%#_id' ESCAPE '#'\n AND c.COLUMN_NAME \u003c> 'id'\n AND NOT EXISTS (\n SELECT 1\n FROM information_schema.KEY_COLUMN_USAGE kcu\n JOIN information_schema.TABLE_CONSTRAINTS tc\n ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME\n AND tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA\n AND tc.TABLE_NAME = kcu.TABLE_NAME\n WHERE tc.CONSTRAINT_TYPE = 'FOREIGN KEY'\n AND kcu.TABLE_SCHEMA = c.TABLE_SCHEMA\n AND kcu.TABLE_NAME = c.TABLE_NAME\n AND kcu.COLUMN_NAME = c.COLUMN_NAME\n );\n\n-- Soft-deleted parents with active children\nSELECT COUNT(*) FROM orders\nWHERE deleted_at IS NOT NULL\n AND EXISTS (\n SELECT 1 FROM order_items\n WHERE order_id = orders.id AND deleted_at IS NULL\n );\n```\n\n## Incorrect\n\n```php\n// ❌ No FK constraints; manual delete bypasses cascades\nSchema::create('order_items', function (Blueprint $t) {\n $t->id();\n $t->unsignedBigInteger('order_id'); // no constraint, no index\n $t->unsignedBigInteger('product_id');\n});\n\n// Deletion via raw SQL — children become orphans\nDB::table('orders')->where('status', 'cancelled')->delete();\n// 200 order rows gone; their 1,800 order_items now point to nothing.\n\n// Soft-delete parent, hard-active children — reports show ghost orders\nclass Order extends Model { use SoftDeletes; }\nclass OrderItem extends Model {} // does NOT respect parent's deleted_at\n```\n\n**Problems:**\n- Joins return rows but with `NULL` parents, silently breaking sums and counts\n- Cleanup jobs run forever (\"delete order_items with no order\" finds millions)\n- Restore-from-backup workflows can't trust the data they restore\n\n## Correct\n\n```php\n// ✅ FK constraints with explicit cascade behaviour\nSchema::create('order_items', function (Blueprint $t) {\n $t->id();\n $t->foreignId('order_id')\n ->constrained() // creates FK to orders(id)\n ->cascadeOnDelete(); // delete items when order is deleted\n $t->foreignId('product_id')\n ->constrained()\n ->restrictOnDelete(); // can't delete a product still referenced\n});\n\n// ✅ Soft-deletes coordinated across parent and children\n// Both models must use SoftDeletes for soft-cascade to work (otherwise\n// `$order->items()->delete()` will hard-delete the children).\nclass OrderItem extends Model {\n use SoftDeletes;\n}\n\nclass Order extends Model {\n use SoftDeletes;\n protected static function booted() {\n static::deleted(fn($order) => $order->items()->delete()); // soft-deletes children\n static::restored(fn($order) => $order->items()->withTrashed()->restore());\n }\n}\n```\n\n```sql\n-- ✅ Add missing FKs to legacy tables (MySQL/InnoDB; clean orphans first)\n-- Note: adding a FK with `ALGORITHM=INPLACE` is only supported when\n-- `foreign_key_checks=OFF`, and `LOCK=NONE` is NOT supported for FK adds.\n-- With default settings MySQL forces `ALGORITHM=COPY`, which rewrites\n-- the table. For large tables, use `pt-online-schema-change` or `gh-ost`\n-- to add the FK without blocking writes.\nALTER TABLE order_items\n ADD CONSTRAINT fk_order_items_order\n FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE;\n```\n\n**Benefits:**\n- Database enforces integrity even when application code is buggy\n- Reports and joins are trustworthy\n- Cleanup jobs become unnecessary (or trivial)\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — adding constraints to greenfield tables\n - **M** — backfilling constraints + cleanup on small/medium tables\n - **L** — adding constraints to a 100M+ row table (must clean orphans first, then add constraint online)\n- **When to pay down:**\n - **NOW:** any data integrity issue traced back to orphans\n - **As cleanup project:** audit tables for `_id` columns without FK constraints\n - **Then:** make FKs required for all new tables (lint a migration template)\n\n**Cleanup workflow:**\n1. Run the orphan-detection query above for each suspected relation\n2. Decide policy: hard-delete orphans, mark them archived, or attach to a placeholder parent\n3. Apply the cleanup in batches (avoid one giant transaction)\n4. Add the FK constraint\n5. Add a CI test that runs the orphan-detection query against the test DB after each test run\n\nReference: [MySQL — FOREIGN KEY Constraints](https://dev.mysql.com/doc/refman/8.4/en/create-table-foreign-keys.html) · [Laravel — Foreign Key Constraints](https://laravel.com/docs/migrations#foreign-key-constraints)\n\n---\n\n\n## Stale Comments\n\n**Impact: MEDIUM (Wrong information is worse than no information)**\n\nA comment that contradicts the code it describes actively misleads readers. They either trust the comment and write a bug, or they distrust all comments and miss the load-bearing ones. Stale comments are negative-value documentation.\n\n## How to Detect\n\nThere is no perfect tool — stale comments are read-and-judge. Useful starting points:\n\n```bash\n# Find comments referencing functions, classes, files that no longer exist\ngrep -rEn '@see |@deprecated |TODO\\\\(|see also' src/ | \\\n while IFS= read -r line; do\n REF=$(echo \"$line\" | grep -oE '[A-Z][a-zA-Z]+::[a-zA-Z]+|[a-z_]+\\\\.[a-z]+')\n [ -n \"$REF\" ] && ! grep -rq \"$REF\" src/ && echo \"STALE: $line\"\n done\n\n# Find comments referencing removed parameters\n# (compare comment params with current function signature)\n```\n\nHotspot: comments next to code that has been git-touched more recently than the comment.\n\n## Incorrect\n\n```typescript\n// ❌ Comment lies about behaviour\n/**\n * Returns the user's full name in \"Last, First\" format.\n */\nfunction displayName(user: User): string {\n return `${user.firstName} ${user.lastName}`; // actually \"First Last\"\n}\n\n// ❌ Comment references removed parameter\n/**\n * @param userId - the user to load\n * @param includeArchived - whether to include archived records\n */\nfunction loadUser(userId: string) { // includeArchived removed last year\n return db.users.findOne({ id: userId, archived: false });\n}\n\n// ❌ \"Temporary\" workaround that became permanent\n// HACK: workaround for Stripe API bug — remove after their 2022 fix\nconst tax = Math.round(subtotal * 0.06 * 100) / 100;\n```\n\n**Problems:**\n- A reader follows the comment, writes integration code expecting \"Last, First\", ships a bug\n- IDE autocomplete picks up the stale `@param`, suggesting a parameter that no longer exists\n- \"Temporary\" workaround now load-bearing; nobody dares remove it\n\n## Correct\n\n```typescript\n// ✅ Comment matches reality, or is deleted\n/**\n * Returns \"First Last\" — used in user-facing greetings.\n */\nfunction displayName(user: User): string {\n return `${user.firstName} ${user.lastName}`;\n}\n\n// ✅ Parameter doc removed when parameter removed\nfunction loadUser(userId: string) {\n return db.users.findOne({ id: userId, archived: false });\n}\n\n// ✅ Either remove the workaround, or update the comment with current rationale\n// Stripe's 2022 fix shipped; this rounding is kept because our DB stores 4 decimal places\n// and accounting reports require 2-decimal-place reconciliation. (#TAX-431)\nconst tax = Math.round(subtotal * 0.06 * 100) / 100;\n```\n\n**Benefits:**\n- Comments become trusted again\n- IDE hints align with reality\n- \"Why does this exist\" is captured at the right level of fidelity\n\n## Remediation Strategy\n\n- **Effort:** S per comment\n- **When to pay down:** On every PR — if you touch code, read the surrounding comments and verify or delete. Reviewers should call out stale comments next to changed lines.\n- **Heuristic:** when in doubt, delete. Code documents *what*; the commit message documents *why*. A stale comment is rarely the right tool to keep.\n\nReference: [John Ousterhout — A Philosophy of Software Design, Ch. 13](https://web.stanford.edu/~ouster/cgi-bin/aposd.php)\n\n---\n\n\n## Outdated Architecture Documentation\n\n**Impact: MEDIUM (Onboarding and incident response collapse without accurate maps)**\n\nArchitecture docs that don't match the running system cause the worst kind of mistake: confident wrong decisions. The cost shows up at the worst moments — onboarding a new engineer or debugging a 2am incident.\n\n## How to Detect\n\nFor each architecture doc in the repo:\n- **README \"Getting Started\":** can a brand-new engineer follow it end-to-end *today* and end with a working dev environment?\n- **Architecture diagrams:** do all named services still exist with the same names?\n- **Database schema diagrams:** do all tables and columns exist?\n- **Sequence diagrams:** do the request paths still match the code?\n\n```bash\n# Find architecture docs and check last-modified vs the code they describe\nfind . \\( -name 'ARCHITECTURE.md' -o -name 'README.md' -o -path '*/docs/*' \\) | \\\n while read doc; do\n DOC_DATE=$(git log -1 --format=%ct -- \"$doc\")\n SRC_DATE=$(git log -1 --format=%ct -- src/ app/)\n if [ $((SRC_DATE - DOC_DATE)) -gt 7776000 ]; then # 90 days\n echo \"STALE: $doc (last touched $(date -r $DOC_DATE +%Y-%m-%d))\"\n fi\n done\n```\n\n## Incorrect\n\n```markdown\n\u003c!-- ❌ README from 18 months ago -->\n# CheckoutService\n\n## Architecture\n- Node.js 14 (LTS)\n- MongoDB for orders\n- Redis for sessions\n- Stripe for payments\n\n## Getting Started\n1. Clone the repo\n2. Copy `.env.example` to `.env` and fill in the DB credentials\n3. `composer install && npm install`\n4. `php artisan migrate && php artisan serve`\n\n## Services\nThe service is split into:\n- /api — REST endpoints\n- /jobs — background workers\n- /web — admin dashboard\n```\n\nReality:\n- Migrated to Node 22 last year\n- Moved from MongoDB to MySQL 8 months ago\n- Stripe was replaced with Adyen\n- `.env.example` no longer exists; uses Doppler\n- `/web` was extracted to its own repo 6 months ago\n\n**Problems:**\n- New engineer spends 2 days debugging \"why won't MongoDB connect\"\n- Incident responder pages the wrong on-call (the old `/web` team)\n- Architectural decisions get made against an imaginary system\n\n## Correct\n\n```markdown\n\u003c!-- ✅ README that matches reality, with a freshness signal -->\n# CheckoutService\n\n_Last verified against running system: 2026-05-10_\n\n## Architecture\n- Node.js 22 (LTS)\n- MySQL 8.4 for orders\n- Redis for sessions\n- Adyen for payments (migrated from Stripe — see `docs/adr/0007-adyen.md`)\n\n## Getting Started\n1. `gh repo clone org/checkout && cd checkout`\n2. `cp .env.example .env`; fill in DB and Adyen credentials (or pull via `doppler setup`)\n3. `composer install && npm install`\n4. `php artisan migrate --seed && php artisan serve` (in another shell: `npm run dev`)\n\n## Services\n- `/api` — REST endpoints\n- `/jobs` — background workers\n- (Admin dashboard lives in `org/checkout-admin`)\n```\n\n**Benefits:**\n- Onboarding works end-to-end without hidden tribal knowledge\n- Incident responders trust the doc to find the right team\n- Each ADR captures the *why* for architectural changes\n\n## Remediation Strategy\n\n- **Effort:** S–M per doc\n- **When to pay down:**\n - Whenever a PR changes architecture (new service, new datastore, new dependency): **update the doc in the same PR**\n - Quarterly: \"doc walk\" — pair an engineer with a brand-new hire, follow the README, fix what breaks\n- **Tip:** add a `Last verified` date to architecture docs; treat doc-aging like dep-aging\n\nReference: [Architecture Decision Records (ADR)](https://adr.github.io/) · [The Diátaxis Framework](https://diataxis.fr/)\n\n---\n\n\n## Undocumented Public APIs\n\n**Impact: MEDIUM (Consumers reverse-engineer behaviour; breaking changes blindside everyone)**\n\nA public API without documentation forces consumers to read source or guess. Once they guess wrong and ship, that \"wrong guess\" becomes the de facto contract — you can no longer change the implementation without breaking them.\n\n## How to Detect\n\n```bash\n# TypeScript: public exports without TSDoc\n# Requires eslint-plugin-jsdoc installed and registered in your eslint config; the\n# rule below is a config-file rule (not a CLI one-liner override):\n# { \"plugins\": [\"jsdoc\"], \"rules\": { \"jsdoc/require-jsdoc\": [\"error\", {\"publicOnly\": true}] } }\n\n# PHP: public methods on public classes without docblocks\n# PHPStan does NOT natively flag missing docblocks — use PHP_CodeSniffer with a\n# docblock-aware standard (e.g. Squiz.Commenting.FunctionComment), or phpDocumentor's\n# validator. PHPStan can still catch missing parameter/return *types* via higher levels.\nvendor/bin/phpcs --standard=Squiz --sniffs=Squiz.Commenting.FunctionComment app/\n\n# OpenAPI / REST APIs\n# Compare routes/controllers to swagger.json / openapi.yaml — drift is debt\n```\n\n## Incorrect\n\n```typescript\n// ❌ Exported function with non-obvious behaviour and no docs\nexport function calculateRefund(order: Order, items: Item[]): RefundResult {\n // ... 80 lines including special cases for partial refunds,\n // restocking fees, expired return windows, original-payment-method routing,\n // and tax recalculation under different jurisdictions\n}\n```\n\nQuestions a consumer can't answer without reading the body:\n- Does `items` default to all items if empty? Or refund nothing?\n- Are restocking fees deducted? How are they configured?\n- What happens if the original payment method is invalidated?\n- Are taxes recalculated, or refunded proportionally?\n\n## Correct\n\n```typescript\n/**\n * Calculate a refund for some or all items in an order.\n *\n * - If `items` is empty, refunds the entire order.\n * - Restocking fees (configured per category) are deducted from the refund amount.\n * - Taxes are recalculated based on the remaining order subtotal (not refunded proportionally).\n * - Refunds route back to the original payment method; if invalidated, returns\n * `RefundResult.requiresAlternativeMethod = true` and the caller must collect new instrument.\n * - Refunds are not allowed past the return window (`order.returnsCloseAt`).\n *\n * @throws RefundWindowClosed — if `order.returnsCloseAt` has passed\n * @throws PartialRefundNotAllowed — if the order's policy disallows partial returns\n *\n * @see docs/refunds.md for business rules and worked examples.\n */\nexport function calculateRefund(order: Order, items: Item[]): RefundResult {\n // ...\n}\n```\n\n**Benefits:**\n- Consumers depend on documented contract, not observed behaviour\n- You can change implementation freely as long as docs hold\n- Edge cases are surfaced — usually catching a bug or missing test in the process\n\n## Remediation Strategy\n\n- **Effort:** S per public function (write the doc; if it's hard to write, the function is doing too much)\n- **When to pay down:**\n - **Now:** add CI rule (`jsdoc/require-jsdoc`, PHPStan public-API check) so new APIs ship with docs\n - **Gradually:** document existing APIs as you touch them. Don't try to backfill all at once.\n- **Side benefit:** writing the doc is the cheapest way to find an API with too many concerns. If you need 3 paragraphs to describe a single function, split it.\n\nReference: [TSDoc](https://tsdoc.org/) · [PHPDoc](https://docs.phpdoc.org/) · [OpenAPI Specification](https://swagger.io/specification/)\n\n---\n\n\n## End-of-Life Runtime Versions\n\n**Impact: CRITICAL (EOL runtimes stop receiving security patches)**\n\nOnce a language runtime hits EOL, no more CVE fixes ship — the next disclosed vulnerability is unpatched and forever yours. EOL deadlines are public, hard, and immovable; treating them as \"we'll deal with it later\" is treating a deadline as optional.\n\n## How to Detect\n\n```bash\n# Check installed versions\nnode --version\nphp --version\n\n# Check declared versions\ncat .nvmrc .node-version 2>/dev/null\ngrep -E '\"engines\"|\"node\":' package.json\ngrep -E '\"php\"' composer.json\n\n# Server-side check (Forge / Vapor / shared hosting)\nssh user@server 'php -v && node -v'\n\n# Check against EOL dates\n# - https://endoflife.date/nodejs\n# - https://endoflife.date/php\n# - https://www.php.net/supported-versions.php\n```\n\n| Runtime | EOL Pattern |\n|---|---|\n| Node.js | Every 6 months; LTS for 30 months |\n| PHP | 2 years active + 2 years security (since 2024) |\n\nThreshold: **flag any version \u003c 6 months from EOL** as P1, **EOL today** as P0.\n\n## Incorrect\n\n```json\n// ❌ package.json declares EOL Node\n{\n \"engines\": { \"node\": \"14.x\" }\n}\n```\n\n```json\n// ❌ composer.json requires EOL PHP\n{\n \"require\": { \"php\": \"^7.4\" } // PHP 7.4 EOL: 2022-11-28\n}\n```\n\n```\n// ❌ .nvmrc still pins to Node 14\n14.21.3\n```\n\n**Problems:**\n- Any disclosed CVE in Node 14 / PHP 7.4 stays exploitable indefinitely\n- Modern dependencies start dropping support — package upgrades become impossible\n- Forge / Vapor / shared-hosting providers eventually remove EOL versions entirely → forced emergency migration\n\n## Correct\n\n```json\n// ✅ package.json — Node 22 LTS\n{\n \"engines\": { \"node\": \">=22 \u003c23\" }\n}\n```\n\n```json\n// ✅ composer.json — PHP 8.3 (LTS, supported through 2027-12)\n{\n \"require\": { \"php\": \"^8.3\" }\n}\n```\n\n```\n// ✅ .nvmrc\n22.6.0\n```\n\nAutomate the watch:\n\n```yaml\n# CI step\n- name: Check runtime EOL\n run: |\n NODE_MAJOR=$(node --version | sed 's/v\\([0-9]*\\).*/\\1/')\n EOL_NODE=20 # update annually\n test $NODE_MAJOR -ge $EOL_NODE || { echo \"Node $NODE_MAJOR is EOL\"; exit 1; }\n```\n\n**Benefits:**\n- Security patches keep arriving for free\n- Modern dep ecosystem stays compatible\n- No emergency \"runtime EOL was last week\" migration\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — minor runtime bump within supported major\n - **M** — major bump (Node 18 → 22, PHP 8.1 → 8.3)\n - **L** — multiple majors at once (Node 14 → 22) — break into steps\n- **When to pay down:**\n - **6 months before EOL:** start the upgrade\n - **3 months before EOL:** be done\n - Schedule a calendar reminder when you upgrade — the next deadline is already on the clock\n\nReference: [endoflife.date](https://endoflife.date/) · [Node Release Schedule](https://nodejs.org/en/about/previous-releases) · [PHP Supported Versions](https://www.php.net/supported-versions.php)\n\n---\n\n\n## Deprecated Framework APIs\n\n**Impact: HIGH (Deprecations become breakages on the next major upgrade)**\n\nEvery deprecation warning is the framework telling you exactly what will break in the next major release. Ignoring them doesn't postpone the cost — it concentrates it on whoever does the upgrade, who is now blocked by hundreds of issues at once.\n\n## How to Detect\n\n```bash\n# Laravel\nphp artisan about # framework version\ngrep -rn '@deprecated' vendor/laravel/ # known deprecated APIs\n# Run tests with deprecation reporting on:\nAPP_ENV=testing E_DEPRECATED=on php artisan test\n\n# React\n# React DevTools logs deprecations in console\n# eslint-plugin-react flags many deprecated APIs:\nnpx eslint . --rule 'react/no-deprecated: error'\n\n# Node\nnode --pending-deprecation app.js # surface upcoming deprecations\nnode --throw-deprecation app.js # treat them as errors in CI\n\n# Generic: parse build/test logs\nnpm test 2>&1 | grep -iE 'deprecat|warning' | sort -u\n```\n\n## Incorrect\n\n```php\n// ❌ Laravel deprecated API usage left in code\nuse Illuminate\\Support\\Facades\\Input; // deprecated in Laravel 5.x; removed in 6.0\n$name = Input::get('name');\n\n// ❌ Method signature that's been overhauled\npublic function failed(Exception $e) // Laravel 10+ expects ?Throwable\n{\n // ...\n}\n\n// ❌ Deprecated React lifecycle method\nclass UserList extends React.Component {\n componentWillMount() { // deprecated since React 16.3\n this.fetch();\n }\n}\n```\n\n**Problems:**\n- Next Laravel major: `Input` gone; `failed(Throwable)` enforced — both break at once\n- React 18 strict mode logs warnings; React 19 may remove them entirely\n- Deprecation log noise hides genuine warnings\n\n## Correct\n\n```php\n// ✅ Use current APIs\n$name = request()->input('name');\n\npublic function failed(?Throwable $e): void\n{\n // ...\n}\n```\n\n```typescript\n// ✅ Hooks or current lifecycle methods\nfunction UserList() {\n useEffect(() => { fetch(); }, []);\n}\n```\n\nAdd a CI gate:\n\n```yaml\n- name: No deprecation warnings\n run: |\n OUTPUT=$(npm test 2>&1 || true)\n echo \"$OUTPUT\" | grep -qiE 'deprecat' && { echo \"Deprecations found\"; exit 1; }\n exit 0\n```\n\n**Benefits:**\n- Next major upgrade is hours, not weeks\n- Test output is signal again\n- Framework authors' migration notes apply directly without rediscovery\n\n## Remediation Strategy\n\n- **Effort:** S–M per deprecation (the migration path is usually documented by the framework)\n- **When to pay down:**\n - **Read the framework's upgrade guide** when they ship a deprecation — pay down what you can immediately\n - **CI gate:** zero new deprecation warnings allowed; old ones whittled down per sprint\n- **Order of operations:** fix deprecations *before* attempting the major upgrade — never together\n\nReference: [Laravel Upgrade Guide](https://laravel.com/docs/upgrade) · [React Strict Mode](https://react.dev/reference/react/StrictMode) · [Node Deprecations](https://nodejs.org/api/deprecations.html)\n\n---\n\n\n## Ignored Build and Lint Warnings\n\n**Impact: MEDIUM (Warning noise hides real failures and trains the team to ignore output)**\n\nA build that emits dozens of warnings teaches every engineer that warnings are normal. The day a critical warning appears (a deprecation, a type-narrowing issue, a circular import), nobody sees it. Clean output is a precondition for noticing problems.\n\n## How to Detect\n\n```bash\n# Capture and count warnings from build/test/lint\nnpm run build 2>&1 | grep -ciE 'warning|deprecat'\nnpx tsc --noEmit 2>&1 | wc -l\nnpx eslint . 2>&1 | grep -c 'warning'\n\n# PHP\nvendor/bin/phpstan analyse --no-progress\nvendor/bin/phpcs --report=summary\n\n# Webpack / Vite\n# Look at the bundler output for \"compiled with N warnings\"\n```\n\nThreshold: **zero warnings tolerated**. Either fix or explicitly suppress with a comment explaining why.\n\n## Incorrect\n\n```bash\n# ❌ Build \"passes\" but emits a wall of warnings\n$ npm run build\n[tsc] src/orders/index.ts(42,5): warning TS6133: 'unused' is declared but never used.\n[tsc] src/orders/index.ts(55,3): warning TS2532: Object is possibly undefined.\n[eslint] src/payment/stripe.ts:18:1 warning no-explicit-any\n[eslint] src/payment/stripe.ts:34:5 warning react-hooks/exhaustive-deps\n[webpack] WARNING in ./node_modules/some-pkg/dist/index.js\n Critical dependency: the request of a dependency is an expression\n... 87 more warnings\nCompiled with 92 warnings.\n```\n\n**Problems:**\n- A new genuine warning (\"X will be removed in vNext\") buries in the noise\n- \"Compiled successfully\" with 92 warnings is a lie that erodes trust\n- New engineers conclude \"warnings don't matter here\"\n\n## Correct\n\n```bash\n$ npm run build\nCompiled successfully (0 warnings).\n```\n\nCI gates:\n\n```yaml\n- run: npx tsc --noEmit # fails on any type error\n- run: npx eslint . --max-warnings 0 # zero warnings\n- run: npm run build -- --no-warnings # bundler warnings → errors\n```\n\nWhen a warning genuinely must be suppressed:\n\n```typescript\n// ✅ Targeted suppression with reason\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Stripe SDK types are too narrow; tracked in #2034\nfunction configureStripe(opts: any) { /* ... */ }\n```\n\n**Benefits:**\n- Build output is signal — every line means something\n- Reviews can ask \"does this PR add any new warning?\" → easy answer\n- Genuine deprecations and CVE-related warnings are noticed immediately\n\n## Remediation Strategy\n\n- **Effort:** S–M (most warnings are mechanical fixes; a few require small refactors)\n- **When to pay down:**\n 1. **Snapshot the current count** in CI: `--max-warnings $CURRENT_COUNT`\n 2. **Ratchet down** — every PR can only equal or decrease the count\n 3. Reach zero, then flip to `--max-warnings 0`\n- **Anti-pattern:** disabling the lint rule entirely instead of fixing the underlying issues. The rule exists for a reason; suppress with context, don't disable globally.\n\nReference: [ESLint — Disabling Rules](https://eslint.org/docs/latest/use/configure/rules#disabling-rules) · [TypeScript Strict Mode](https://www.typescriptlang.org/tsconfig#strict)\n\n---\n\n\n## Insecure Secrets Management\n\n**Impact: HIGH (.env files leak, plain env vars surface in logs, no rotation = compounding risk)**\n\nEven when secrets stay out of source control, ad-hoc handling — `.env` files copied between machines, plain env vars echoed into CI logs, indefinite credential lifetimes — leaves a long tail of leakage paths. This rule covers everything *outside* the repo: secret-manager adoption, rotation, scope, and audit.\n\n## How to Detect\n\nFor each environment (dev, staging, prod), check:\n\n1. Where are secrets stored? (encrypted secret manager vs. plain files vs. shell history)\n2. Are they versioned and audit-logged?\n3. Is rotation automated or manual?\n4. Are CI/CD secrets scoped per repo/job, or org-wide?\n5. Are workload identities (OIDC / IAM roles) used instead of long-lived keys?\n\n```bash\n# Audit any .env files committed historically\ngitleaks git # full history scan\ngit log --all --diff-filter=A --name-only | grep -E '\\\\.env

Technical Debt Technical debt audit and prioritization framework for PHP/Laravel (MySQL) and Node/TypeScript/React projects. Contains 42 rules across 10 categories covering code, security, design, dependency, test, performance, data, documentation, infrastructure, and process debt. Produces a ranked ledger (effort × impact) so teams know what to fix first , not just what's broken. Supports both coding reference and audit mode with PASS/FAIL/N/A output. Metadata - Version: 1.0.0 - Scope: PHP / Laravel (MySQL) + Node / TypeScript / React - Rule Count: 42 rules across 10 categories - License: MI…

\n\n# CI: scan for echoes of secrets in logs (common when devs add `set -x`)\ngrep -E 'API_KEY=|TOKEN=|PASSWORD=' .github/workflows/*.yml\n\n# Cloud: list long-lived access keys (AWS example)\naws iam list-access-keys --user-name \u003ciam-user> # any keys > 90 days old?\n```\n\n## Incorrect\n\n```bash\n# ❌ Anti-patterns\n\n# 1. Plain .env file committed in history (even if removed later)\n$ git log -p --all -- '.env'\n... (compromised)\n\n# 2. CI workflow echoing secrets via debugging\n- run: echo \"AWS_KEY=$AWS_ACCESS_KEY_ID\" # ends up in build logs\n\n# 3. Single shared IAM access key used by all CI jobs, 4 years old, never rotated\n$ aws iam list-access-keys --user-name ci-deploy\nCreateDate: 2022-03-15 # 4 years; full admin permissions\n\n# 4. Slack/Notion link sharing for \"DB credentials\" — anyone with the link sees the password\n```\n\n**Problems:**\n- A leaked CI log exposes the entire production environment\n- Long-lived credentials with broad scope = compromise = full account access\n- No audit trail of who accessed what credential when\n\n## Correct\n\n```yaml\n# ✅ GitHub Actions: OIDC to AWS — no long-lived keys needed\npermissions:\n id-token: write\n contents: read\njobs:\n deploy:\n runs-on: ubuntu-latest\n steps:\n - uses: aws-actions/configure-aws-credentials@v6\n with:\n role-to-assume: arn:aws:iam::123456789012:role/deploy-role\n aws-region: ap-southeast-1\n # AWS calls now use short-lived session tokens scoped to this workflow\n```\n\n```bash\n# ✅ Production secrets in a dedicated manager (one of these):\n# - HashiCorp Vault (self-hosted, comprehensive)\n# - AWS Secrets Manager (with rotation Lambdas)\n# - GCP Secret Manager\n# - Doppler / 1Password Secrets Automation (SaaS)\n#\n# Apps read at startup via SDK; rotation is automated.\n\n# ✅ Local dev: .env files exist but are gitignored and contain dev-only credentials\necho '.env' >> .gitignore\n```\n\n**Benefits:**\n- No long-lived credentials to leak; access is scoped to job + duration\n- Audit logs record every secret access\n- Rotation is automatic and predictable\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — gitignore .env, install pre-commit gitleaks hook\n - **M** — migrate one application from env-file → secret manager\n - **L** — full org migration to OIDC + secret manager + rotation\n- **When to pay down:**\n - **NOW:** any committed .env, any 90-day-old long-lived key, any secrets in plain CI logs\n - **This quarter:** migrate CI/CD to OIDC where the cloud provider supports it (AWS, GCP, Azure all do)\n - **Then:** automated rotation for all production secrets, with audit alerts on access spikes\n\n**Hierarchy of secret handling, from worst to best:**\n1. Hardcoded in repo (CRITICAL — must rotate immediately)\n2. Untracked `.env` file emailed/Slacked around (high leak risk)\n3. Long-lived credentials in CI/CD secrets store\n4. Short-lived credentials issued via OIDC workload identity (best)\n\nReference: [GitHub Actions OIDC](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments) · [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/) · [HashiCorp Vault](https://developer.hashicorp.com/vault) · [Doppler](https://docs.doppler.com/)\n\n---\n\n\n## Observability and Monitoring Gaps\n\n**Impact: HIGH (Debt you can't measure can't be paid down; incidents take hours longer to diagnose)**\n\nA system without structured logs, metrics, traces, and alerts is a system that breaks silently. The \"we'll add monitoring later\" decision is a tax paid during every incident, every customer-reported bug, and every capacity-planning conversation. Observability debt has the unique property of being most expensive to fix *during* an incident.\n\n## How to Detect\n\nFor each service in scope, audit:\n\n1. **Structured logs** — JSON, with correlation IDs, request IDs, user IDs (when appropriate)\n2. **Application metrics** — request rates, latency percentiles (p50/p95/p99), error rates, queue depths\n3. **Tracing** — distributed traces with span IDs spanning service boundaries\n4. **Alerts** — paging only on user-facing symptoms; non-paging for early signals\n5. **SLOs** — explicit service-level objectives with error budgets\n\n```bash\n# Find log statements still using non-structured output\ngrep -rEn 'echo |print(|print_r(|var_dump(|console\\.log\\(' app/ src/ | wc -l\n\n# Laravel: is logging configured to JSON channel?\ngrep -A5 \"'channels' =>\" config/logging.php\n\n# Are there any Prometheus / OpenTelemetry imports/integrations?\ngrep -rEn 'prometheus|opentelemetry|datadog|sentry|new-relic' composer.json package.json\n\n# Sentry / Bugsnag / equivalent error tracking installed?\ngrep -rE 'sentry|bugsnag|rollbar|honeybadger' composer.lock package-lock.json\n```\n\n## Incorrect\n\n```php\n// ❌ Observability black hole\n\n// 1. Print-style logging — unstructured, no correlation\npublic function pay(Order $order) {\n echo \"Processing payment for order \" . $order->id . \"\\n\";\n try {\n $result = $this->stripe->charge($order);\n } catch (Exception $e) {\n echo \"FAILED: \" . $e->getMessage() . \"\\n\"; // lost on next request\n }\n}\n\n// 2. No request IDs\n// 3. No latency metrics — \"the app feels slow\" is the only signal\n// 4. Pager rule: \"send a Slack message if a single 500 occurs\"\n// → wakes people up for every transient hiccup; signal-to-noise = 0\n// 5. No SLOs → no shared definition of \"the service is broken\"\n```\n\n**Problems:**\n- During an incident, you can't tell which user, which request, or which span failed\n- Capacity planning is guesswork (\"we think we can handle 2× traffic\")\n- Slow regressions (p95 creeping from 200ms → 800ms over a quarter) go unnoticed\n- On-call burnout from low-quality alerts\n\n## Correct\n\n```php\n// ✅ Structured logging with correlation ID + context\npublic function pay(Order $order): PaymentResult {\n $log = Log::withContext([\n 'order_id' => $order->id,\n 'user_id' => $order->user_id,\n 'request_id' => request()->header('X-Request-ID') ?? Str::uuid(),\n ]);\n\n $log->info('payment.start', ['amount' => $order->total]);\n $start = microtime(true);\n\n try {\n $result = $this->stripe->charge($order);\n $log->info('payment.success', [\n 'charge_id' => $result->id,\n 'duration_ms' => (int) ((microtime(true) - $start) * 1000),\n ]);\n return $result;\n } catch (\\Throwable $e) {\n $log->error('payment.failure', [\n 'exception' => $e::class,\n 'message' => $e->getMessage(),\n 'duration_ms' => (int) ((microtime(true) - $start) * 1000),\n ]);\n report($e); // → Sentry / Bugsnag\n throw $e;\n }\n}\n```\n\nMinimum viable observability stack to install:\n- **Logs:** JSON channel + central log store (CloudWatch, Loki, Datadog Logs)\n- **Errors:** Sentry / Bugsnag / Rollbar\n- **Metrics + tracing:** OpenTelemetry SDK → vendor or self-hosted (Grafana stack, Datadog, Honeycomb)\n- **Uptime:** external prober (UptimeRobot, Pingdom, Datadog Synthetics)\n- **Alerts:** routed to a real pager system (PagerDuty, Opsgenie); thresholds based on SLO burn rate, not single events\n\nDefine SLOs explicitly (illustrative — real Sloth uses `version: prometheus/v1` plus an\n`sli.events` block with `error_query`/`total_query`; Pyrra ships as a Kubernetes CRD —\nconsult each tool's schema before adopting):\n\n```yaml\n# slo.yaml — illustrative shape only\nservice: checkout\nslos:\n - name: availability\n objective: 99.9%\n sli: error_rate \u003c 1% over 28d\n - name: latency\n objective: 99% of requests \u003c 500ms over 28d\n```\n\n**Benefits:**\n- Incidents resolve faster (mean MTTR drops 50%+ with traces)\n- Slow regressions are caught when they're small\n- Alerts wake people up only for user-facing problems\n- Engineering and product share a quantitative definition of \"the service is healthy\"\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — add Sentry + JSON logging to one service\n - **M** — add OpenTelemetry + APM dashboards\n - **L** — full SLO program (objectives, alerts on burn rate, error budgets, blameless postmortems)\n- **When to pay down:**\n - **NOW:** any production service without an error-tracker (Sentry-class)\n - **NOW:** any service whose only outage signal is \"a user complained\"\n - **This quarter:** structured logging + request IDs + p95 latency dashboards\n - **Then:** SLO program; alerts based on burn rates, not raw thresholds\n\n**Anti-patterns:**\n- **Alert on everything** — alarms drown signal; only page on customer-impacting symptoms\n- **Logs-only observability** — logs are expensive to query at scale; metrics + traces complement them\n- **No retention policy** — logs at 1TB/day with infinite retention becomes its own debt\n- **Tool sprawl** — one logs vendor, one APM, one error tracker is plenty\n\nReference: [Google SRE — Monitoring Distributed Systems](https://sre.google/sre-book/monitoring-distributed-systems/) · [OpenTelemetry](https://opentelemetry.io/) · [Sloth — SLO Generator](https://sloth.dev/)\n\n---\n\n\n## Aging TODO, FIXME, and HACK Comments\n\n**Impact: MEDIUM (Untracked promises that compound silently)**\n\nA `// TODO` is a promise to do something later, written by someone who has now forgotten. After 6 months, nobody remembers what the TODO meant, whether it still applies, or what the consequences are. These comments accumulate as untracked technical debt invisible to product and management.\n\n## How to Detect\n\n```bash\n# All TODO/FIXME/HACK with the introducing commit's date (via git blame)\n# Note: grep's --include uses fnmatch, not brace expansion — pass one --include per ext.\ngrep -rEn '(TODO|FIXME|HACK|XXX|BUG)' \\\n --include='*.php' --include='*.ts' --include='*.tsx' \\\n --include='*.js' --include='*.jsx' . | \\\n while IFS=: read -r file line content; do\n DATE=$(git blame -L \"$line,$line\" --date=short -- \"$file\" 2>/dev/null | awk '{print $3}')\n echo \"$DATE | $file:$line | $content\"\n done | sort\n\n# Quick count\ngrep -rEn '(TODO|FIXME|HACK)' src/ | wc -l\n```\n\nThreshold rules:\n- **TODO older than 6 months** without ticket reference → debt\n- **FIXME of any age** without ticket → debt (FIXME implies known broken)\n- **HACK of any age** without ticket → debt (HACK implies known wrong)\n\n## Incorrect\n\n```typescript\n// ❌ Naked TODOs / FIXMEs accumulated over years\nfunction calculateShipping(order: Order): number {\n // TODO: support international shipping\n // TODO: discount for premium members\n // FIXME: this is wrong for orders > $500\n // HACK: hardcoded $15 for now\n\n return 15;\n}\n```\n\n**Problems:**\n- \"Wrong for orders > $500\" is a bug nobody is tracking\n- International shipping was demanded a year ago; product team doesn't know it's blocked here\n- Each TODO is an island — no estimate, no owner, no priority\n\n## Correct\n\n```typescript\n// ✅ TODO with ticket, date, and owner (or no TODO at all)\nfunction calculateShipping(order: Order): number {\n // Hardcoded $15 — international + premium discounts tracked in #1842 (asyraf, 2026-03)\n return 15;\n}\n```\n\nOr — preferred when the work is real:\n\n1. **Open a ticket** in the issue tracker\n2. **Reference it** in code: `// see #1842`\n3. **Delete bare TODOs** — if they're worth keeping, they're worth tracking\n\nCI enforcement:\n\n```yaml\n- name: No new bare TODOs\n run: |\n BARE=$(git diff origin/main...HEAD \\\n | grep -E '^\\+.*\\b(TODO|FIXME|HACK)\\b' \\\n | grep -v -E '#[0-9]+')\n test -z \"$BARE\" || { echo \"Add a ticket reference to TODOs\"; exit 1; }\n```\n\n**Benefits:**\n- Every promise has an owner and a tracker entry\n- Product can see and prioritise the backlog\n- Code is honest about what's known-broken\n\n## Remediation Strategy\n\n- **Effort:** S per comment (decision: ticket, fix, or delete)\n- **When to pay down:**\n - **Now:** triage existing TODOs → ticket, fix immediately, or delete\n - **Ongoing:** CI gate prevents new bare TODOs\n- **Triage flow:**\n 1. Still relevant? If no, delete.\n 2. Worth doing? If no, delete.\n 3. Worth doing this quarter? File a ticket, reference it.\n 4. Worth doing now? Just do it — don't write a TODO.\n\nReference: [Steve McConnell — Code Complete, Ch. 32 on Self-Documenting Code](https://www.microsoftpressstore.com/store/code-complete-9780735619678)\n\n---\n\n\n## @deprecated Markers Without Removal Plan\n\n**Impact: MEDIUM (Indefinite deprecations become permanent — they never actually go away)**\n\nA `@deprecated` tag without a replacement, removal date, or migration plan is just a polite \"I wish you wouldn't use this.\" Consumers keep using it, the deprecation message persists for years, and the codebase carries dead-but-not-dead APIs forever.\n\n## How to Detect\n\n```bash\n# Find all @deprecated markers (one --include per extension; grep doesn't expand braces)\ngrep -rEn '@deprecated' \\\n --include='*.php' --include='*.ts' --include='*.tsx' \\\n --include='*.js' --include='*.jsx' src/ app/\n\n# For each, check whether it includes:\n# - Replacement / `@see` pointer\n# - Removal version or date\n# - Reason\n\n# Find @deprecated callers — these are migration targets\ngrep -rEn '@deprecated' -A2 src/ | grep -oE 'function [a-zA-Z]+|method [a-zA-Z]+'\n```\n\nFlag any `@deprecated` without:\n1. **What to use instead** (`@see` or \"use X instead\")\n2. **When it will be removed** (version, date, or trigger)\n3. **At least one PR removing internal usage** since the deprecation was added\n\n## Incorrect\n\n```typescript\n// ❌ Vague, unactionable deprecations\n/** @deprecated */\nexport function getUserById(id: string) { /* ... */ }\n\n/** @deprecated do not use */\nexport function legacyFormat(date: Date) { /* ... */ }\n\n/** @deprecated use the new API */ // which \"new API\"?\nexport function fetchOrders() { /* ... */ }\n```\n\n**Problems:**\n- Consumers see \"deprecated\" but cannot act\n- No deadline → no urgency → no migration\n- 5 years later still in code, still warning, still used everywhere\n\n## Correct\n\n```typescript\n/**\n * @deprecated Since v4.2 (2025-09). Use {@link findUserById} which supports\n * batching and returns `Result\u003cUser, NotFound>`. Will be removed in v5.0\n * (target: 2026-Q3). Internal callers: 0 (migration complete).\n *\n * @see findUserById\n */\nexport function getUserById(id: string): User | null { /* ... */ }\n```\n\nAdd CI to enforce:\n\n```yaml\n- name: No undocumented @deprecated\n run: |\n BAD=$(grep -rEn '@deprecated\\b' src/ | grep -v -E '@deprecated.*[0-9]{4}')\n test -z \"$BAD\" || { echo \"@deprecated needs a date/version\"; exit 1; }\n```\n\n**Benefits:**\n- Consumers see exactly what to do and when\n- Tracking the deprecation → removal cycle is mechanical (grep + count)\n- Old code actually leaves the codebase\n\n## Remediation Strategy\n\n- **Effort:** S per marker (audit), M per deprecation cycle (migrate internal callers, then remove)\n- **When to pay down:**\n - **Audit existing markers:** add date + replacement, OR upgrade to \"will be removed in next major\" + start migration\n - **Removal:** when a deprecation hits its target version, *actually remove the code*. A deprecation that doesn't end is a lie.\n- **Cycle:**\n 1. Mark `@deprecated since vX (date), use Y, removed in vZ`\n 2. Migrate internal callers (creates the proof the replacement works)\n 3. Wait one major or N months for external callers\n 4. Remove\n\nReference: [Semantic Versioning — Major changes](https://semver.org/) · [PHPDoc @deprecated](https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/deprecated.html)\n\n---\n\n\n## Untracked Technical Debt\n\n**Impact: MEDIUM (Debt that isn't tracked can't be prioritized or budgeted)**\n\nDebt that exists only in engineers' heads (or in scattered TODOs and Slack messages) competes with product features by stealth — engineers slow down, but leadership can't see why. A debt register makes the cost visible, so it can be funded properly instead of being paid in invisible overtime.\n\n## How to Detect\n\nCheck the project for an explicit debt-tracking mechanism:\n\n- **Issue tracker label** (e.g., GitHub `tech-debt`, Linear `Debt` project)\n- **Dedicated debt board** (Trello, Notion, or a `DEBT.md` in the repo)\n- **ADRs** for major debt accumulation decisions\n- **Quarterly debt-paydown allocation** (e.g., 20% capacity)\n\n```bash\n# Quick repo check\nls DEBT.md TECH_DEBT.md docs/debt/ 2>/dev/null\ngh issue list --label \"tech-debt\" --state=all\ngrep -rE 'tech.?debt' .github/ docs/\n```\n\nIf none of these exist, the project has **process debt about technical debt** — meta-debt.\n\n## Incorrect\n\n```\n❌ Debt situation in a typical repo:\n- 47 TODO comments, no tickets\n- 12 known \"we should rewrite that\" conversations on Slack\n- 3 engineers each have a mental list of \"things that scare me\"\n- Last debt-paydown sprint: never\n- When asked \"what's our biggest debt?\": four engineers give four different answers\n```\n\n**Problems:**\n- Debt accrues invisibly — leadership only sees velocity drop\n- Same problem gets discovered repeatedly by new hires\n- No paydown budget because no list to justify the budget\n\n## Correct\n\nPick **one** lightweight mechanism and use it consistently:\n\n```markdown\n\u003c!-- ✅ Option A: DEBT.md in the repo (low ceremony) -->\n# Technical Debt Register\n\n| # | Category | Item | Effort | Impact | Owner | Linked |\n|---|----------|------|--------|--------|-------|--------|\n| 1 | deps | guzzle 6.x (5y behind, blocks PHP 8.4) | M | HIGH | @asyraf | #1842 |\n| 2 | code | OrderService god class (820 LoC) | L | HIGH | @team-orders | #1844 |\n| 3 | test | Checkout flow has no integration test | M | CRITICAL | @asyraf | #1845 |\n\nLast reviewed: 2026-05-01. Next review: 2026-08-01.\n```\n\n```yaml\n# ✅ Option B: GitHub label + saved search\n# Label every debt-related issue with 'tech-debt'\n# Saved search: https://github.com/org/repo/issues?q=is%3Aopen+label%3Atech-debt+sort%3Areactions-%2B1-desc\n```\n\n**Allocate budget:** dedicate a consistent fraction of every sprint to debt paydown (commonly 15–25%). Without an allocation, debt always loses to features.\n\n**Benefits:**\n- Debt is visible to product and leadership\n- Prioritization is principled (effort × impact), not loudest-engineer\n- Paydown velocity is measurable\n\n## Remediation Strategy\n\n- **Effort:** S to start (one register + a label), ongoing M to maintain\n- **When to pay down:**\n - **Now:** start the register with the top 10 items from your last audit\n - **Quarterly:** review and reprioritize; close completed entries\n - **Per PR:** if a PR introduces accepted debt (shortcut, missing test), add a register entry as part of merge\n\n**Anti-patterns:**\n- Register that nobody owns → goes stale, becomes worse than nothing\n- Register with 200 entries → useless; cap at top 20–30 active items\n- Debt sprints disconnected from a register → effort goes to whatever's annoying that week, not what matters\n\nReference: [Martin Fowler — Technical Debt Quadrant](https://martinfowler.com/bliki/TechnicalDebtQuadrant.html) · [ThoughtWorks Tech Radar — Debt Register](https://www.thoughtworks.com/radar)\n\n---\n\n\n## Code Without Owners\n\n**Impact: MEDIUM (Orphaned code = nobody reviews, nobody maintains, nobody knows)**\n\nWhen code has no clear owner, two things happen: PRs touching it stall (nobody knows who should approve), and when it breaks at 2am, the on-call rotation finds out the hard way. Orphaned code accumulates as the \"bus-factor of one\" engineer who wrote it changes teams or leaves.\n\n## How to Detect\n\n```bash\n# Files not covered by CODEOWNERS\ngh api repos/:owner/:repo/contents | jq -r '.[].path' > all-files.txt\n# Compare against CODEOWNERS patterns (manual or via `git check-attr` for path-attribute alternative)\n\n# \"Author concentration\" — files where one person wrote >70% and they're gone\ngit ls-files | while read f; do\n TOP=$(git log --format='%ae' -- \"$f\" | sort | uniq -c | sort -rn | head -1)\n echo \"$TOP $f\"\ndone | sort -rn | head -30\n\n# High-churn, low-author-count files (bus-factor risk)\ngit log --since='12 months ago' --name-only --format='COMMIT %ae' | \\\n awk '/^COMMIT/{a=$2; next} {print a, $0}' | \\\n sort -k2 | uniq -c | sort -rn | head\n\n# Repos lacking a CODEOWNERS file at all\nls .github/CODEOWNERS docs/CODEOWNERS CODEOWNERS 2>/dev/null\n```\n\n## Incorrect\n\n```\n❌ A typical legacy repo:\n\n- No .github/CODEOWNERS file\n- 40% of files last touched by engineers who left 2+ years ago\n- Critical billing module written by one engineer, no shared knowledge\n- Incident response for `/api/legacy/*` routes pages a randomly-selected on-call\n who has never seen the code\n- PRs touching low-traffic areas wait 2 weeks for a review because nobody owns them\n```\n\n**Problems:**\n- Knowledge debt is invisible until incident — then it's catastrophic\n- Code reviews degrade to rubber-stamps because nobody has context\n- Refactoring is risky — \"is anyone using this?\" has no quick answer\n\n## Correct\n\n```\n# ✅ .github/CODEOWNERS — every path has an owner team\n\n# Default owners for everything not matched below\n* @org/platform\n\n# Domain owners\n/app/Billing/ @org/billing-team\n/app/Auth/ @org/identity-team\n/app/Notifications/ @org/messaging-team\n/resources/js/Pages/Admin/ @org/admin-frontend\n\n# Infrastructure / DevEx\n/.github/workflows/ @org/devex\n/deploy/ @org/devex\n\n# Database\n/database/migrations/ @org/data-engineering @org/platform\n```\n\nAdd a CI step to validate the file and check coverage:\n\n```yaml\n- name: CODEOWNERS lint + coverage\n uses: mszostok/[email protected]\n with:\n checks: \"files,owners,duppatterns,syntax\"\n experimental_checks: \"notowned\"\n github_access_token: ${{ secrets.GITHUB_TOKEN }}\n```\n\nThe `notowned` experimental check flags files in the repo not matched by any CODEOWNERS pattern — the right tool for \"is everything covered?\".\n\n**Benefits:**\n- GitHub auto-requests reviews from owners — no more \"who should review this?\"\n- Incident response routes to the right team\n- New engineers can find a domain expert by path\n- Refactor decisions get the input they need\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — bootstrap a CODEOWNERS with broad team-level patterns\n - **M** — refine ownership as teams form; add path-specific owners\n - **L** — rehome orphaned code (find a new owner team, document context, transfer ownership)\n- **When to pay down:**\n - **NOW:** if you've ever asked \"who owns this code?\" and the answer was unclear\n - **As a project:** quarterly ownership review — re-attest that owners listed are still active\n- **Anti-patterns:**\n - **Individual owners** for production code (bus-factor of 1) — prefer team owners\n - **Stale CODEOWNERS** referencing teams or people that no longer exist (CI lint catches this)\n - **Owning everything** by one team — fragment by domain so PRs route fast\n\n**Tip:** when no team wants to own a piece of code, that's a strong signal to delete it (if unused), extract it (if shared), or fold it into a new team's charter (if business-critical).\n\nReference: [GitHub — CODEOWNERS](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) · [GitLab — Code Owners](https://docs.gitlab.com/ee/user/project/codeowners/)\n\n---\n\n\n## Lingering Feature Flags\n\n**Impact: MEDIUM (Old flags become permanent special cases — dead branches that never die)**\n\nFeature flags are a release-management tool, not a config primitive. Once a feature has been at 100% rollout for weeks, the flag is no longer protecting anyone — it's just dead branches polluting the codebase. Lingering flags accumulate as conditional logic that nobody understands, can't safely delete, and silently doubles the test surface.\n\n## How to Detect\n\n```bash\n# Search for feature-flag SDK references\ngrep -rEn '(feature|flag|toggle)::(isOn|enabled|isEnabled|active)' \\\n --include='*.php' --include='*.ts' --include='*.tsx' --include='*.js' .\n\n# Specific SDKs\ngrep -rEn '(LaunchDarkly|posthog|growthbook|unleash|split\\.io|flipper|optimizely)' .\n\n# Laravel pennant\ngrep -rEn 'Feature::active\\(|Feature::for\\(' app/ resources/\n\n# Count distinct flag identifiers and look up their ages in the flag platform\n# (most platforms expose a \"stale flags\" view — LaunchDarkly, Statsig, GrowthBook all do)\n```\n\nFor each flag found, check:\n- **Rollout state:** 0%? 100%? Or in-flight rollout? (Anything > 6 weeks at 0 or 100% is debt.)\n- **Last evaluation:** flag platform usually tracks last-checked time per flag\n- **Owner:** is there a name attached? Is that person still on the team?\n\n## Incorrect\n\n```typescript\n// ❌ Flag from 2 years ago, shipped to 100% for 18 months, still in code\nfunction CheckoutPage() {\n const showNewCheckout = useFlag('new-checkout-redesign-v3'); // long since 100%\n return showNewCheckout ? \u003cNewCheckout /> : \u003cLegacyCheckout />;\n}\n\n// ❌ Nested flag conditions become an N×M maze\nif (flags.useNewPricing && flags.useNewTaxEngine && !flags.legacyShippingFallback) {\n // ... one path\n} else if (flags.useNewPricing && !flags.useNewTaxEngine) {\n // ... another path\n} else {\n // ... legacy path nobody has touched in a year\n}\n```\n\n**Problems:**\n- `LegacyCheckout` is dead code masquerading as a fallback\n- The flag service is queried for every page load even though the result is constant\n- New engineers see 3 versions of checkout and don't know which is current\n- Removing the flag is \"scary\" because nobody has touched the legacy branch in months\n\n## Correct\n\n```typescript\n// ✅ Flag retired: branch chosen, dead branch removed, flag deleted from platform\nfunction CheckoutPage() {\n return \u003cCheckout />; // formerly NewCheckout\n}\n// LegacyCheckout: deleted. New-checkout-redesign-v3 flag: archived in LaunchDarkly.\n```\n\nLifecycle policy as a CI gate:\n\n```yaml\n# .github/workflows/stale-flags.yml — runs weekly\n# LaunchDarkly: use the official action (the tool is a Go binary called\n# `ld-find-code-refs`, not an npm package — `npx` will not work).\n- name: Stale flag check (LaunchDarkly)\n uses: launchdarkly/find-code-references@v2\n with:\n accessToken: ${{ secrets.LD_ACCESS_TOKEN }}\n projKey: ${{ vars.LD_PROJECT_KEY }}\n repoName: ${{ github.event.repository.name }}\n```\n\nMaintain a register:\n\n```markdown\n| Flag | State | Created | Owner | Action |\n|-------------------------------|----------|------------|----------|--------|\n| new-checkout-redesign-v3 | 100% 18mo| 2024-09-01 | @asyraf | DELETE |\n| experimental-promo-engine | 50% A/B | 2026-04-01 | @growth | KEEP |\n| disable-legacy-search | 0% 9mo | 2025-09-15 | @search | DELETE |\n```\n\n**Benefits:**\n- Dead branches removed → smaller test matrix, simpler code, smaller bundle\n- Flag platform stops being queried for permanent constants (latency win)\n- Onboarding engineers see one path, not three\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — retire a single flag (delete code branch + remove flag)\n - **M** — clean up nested flag combinations\n - **L** — instate a flag-lifecycle program (creation requires expiry date, weekly stale-flag review)\n- **When to pay down:**\n - **NOW:** any flag at 100% for > 6 weeks with no rollback plan\n - **Per sprint:** drop one stale flag — small effort, compounding cleanup\n - **Then:** lifecycle policy — every new flag has an expiry date in the platform\n\n**Lifecycle policy (recommended):**\n1. **Create** — flag has a name, owner, intended rollout duration, and target removal date\n2. **Roll out** — gradually 1% → 5% → 25% → 100%\n3. **Stabilize** — at 100% for one release cycle; verify metrics\n4. **Retire** — delete the losing branch, remove flag from code, archive in platform\n5. **Audit** — weekly stale-flag report; any flag past expiry pings the owner\n\n**Anti-patterns:**\n- **Permanent flags used as \"config\"** — that's not a flag, that's an env var; treat differently\n- **Flags as authz** — use proper authorization layers, not flag SDKs\n- **No expiry on creation** — every flag should be born with a death date\n\nReference: [Martin Fowler — Feature Toggles](https://martinfowler.com/articles/feature-toggles.html) · [ld-find-code-refs (GitHub)](https://github.com/launchdarkly/ld-find-code-refs) · [Laravel Pennant](https://laravel.com/docs/pennant)\n\n---\n\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":152566,"content_sha256":"63fd21d4547a2918fa09d6bb6447e84955756232d71889ccf35e622405df34d4"},{"filename":"metadata.json","content":"{\n \"name\": \"Technical Debt\",\n \"version\": \"1.0.0\",\n \"description\": \"Technical debt inventory, prioritization, and audit framework for PHP/Laravel (MySQL) and Node/TypeScript/React projects. Produces a ranked debt ledger (effort × impact) covering code, security, design, dependency, test, performance, data, documentation, infrastructure, and process debt. Supports audit mode with PASS/FAIL/N/A output and priority ranking (P0–P3).\",\n \"framework\": \"PHP / Laravel (MySQL) + 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 \"technical-debt\",\n \"tech-debt\",\n \"code-health\",\n \"refactoring\",\n \"code-quality\",\n \"debt-inventory\",\n \"debt-prioritization\",\n \"audit\",\n \"code-smells\",\n \"legacy-code\",\n \"maintainability\",\n \"complexity\",\n \"duplication\",\n \"dependency-management\",\n \"test-debt\",\n \"documentation-debt\",\n \"security-debt\",\n \"performance-debt\",\n \"data-debt\",\n \"n-plus-one\",\n \"bundle-size\",\n \"schema-drift\",\n \"secrets-management\",\n \"caching\",\n \"observability\",\n \"feature-flags\",\n \"mysql\"\n ],\n \"categories\": [\n {\n \"id\": \"code-debt\",\n \"name\": \"Code Debt\",\n \"priority\": \"CRITICAL\",\n \"description\": \"Duplication, complexity, long functions, god classes, dead code, magic numbers, long parameter lists\",\n \"ruleCount\": 7\n },\n {\n \"id\": \"security-debt\",\n \"name\": \"Security Debt\",\n \"priority\": \"CRITICAL\",\n \"description\": \"Secrets in source, missing input validation, outdated auth and missing hardening\",\n \"ruleCount\": 3\n },\n {\n \"id\": \"design-debt\",\n \"name\": \"Design Debt\",\n \"priority\": \"HIGH\",\n \"description\": \"Tight coupling, circular dependencies, leaky abstractions, shotgun surgery\",\n \"ruleCount\": 4\n },\n {\n \"id\": \"dependency-debt\",\n \"name\": \"Dependency Debt\",\n \"priority\": \"HIGH\",\n \"description\": \"Outdated versions, abandoned packages, CVEs, unused dependencies\",\n \"ruleCount\": 4\n },\n {\n \"id\": \"test-debt\",\n \"name\": \"Test Debt\",\n \"priority\": \"HIGH\",\n \"description\": \"Coverage gaps, flaky tests, disabled tests, slow suites\",\n \"ruleCount\": 4\n },\n {\n \"id\": \"performance-debt\",\n \"name\": \"Performance Debt\",\n \"priority\": \"HIGH\",\n \"description\": \"N+1 queries, unbounded result sets, frontend bundle bloat, missing caching\",\n \"ruleCount\": 4\n },\n {\n \"id\": \"data-debt\",\n \"name\": \"Data Debt\",\n \"priority\": \"HIGH\",\n \"description\": \"Schema drift, missing indexes, orphaned records and referential gaps\",\n \"ruleCount\": 3\n },\n {\n \"id\": \"documentation-debt\",\n \"name\": \"Documentation Debt\",\n \"priority\": \"MEDIUM\",\n \"description\": \"Stale comments, outdated architecture docs, undocumented APIs\",\n \"ruleCount\": 3\n },\n {\n \"id\": \"infrastructure-debt\",\n \"name\": \"Infrastructure Debt\",\n \"priority\": \"MEDIUM\",\n \"description\": \"EOL runtimes, deprecated framework APIs, build warnings, secrets management, observability gaps\",\n \"ruleCount\": 5\n },\n {\n \"id\": \"process-debt\",\n \"name\": \"Process Debt\",\n \"priority\": \"MEDIUM\",\n \"description\": \"Aging TODOs, undated @deprecated markers, untracked debt, code without owners, lingering feature flags\",\n \"ruleCount\": 5\n }\n ],\n \"references\": [\n {\n \"title\": \"Martin Fowler — Technical Debt Quadrant\",\n \"url\": \"https://martinfowler.com/bliki/TechnicalDebtQuadrant.html\",\n \"type\": \"guide\"\n },\n {\n \"title\": \"Ward Cunningham — The Debt Metaphor\",\n \"url\": \"https://www.youtube.com/watch?v=pqeJFYwnkjE\",\n \"type\": \"guide\"\n },\n {\n \"title\": \"SonarQube Technical Debt Model\",\n \"url\": \"https://docs.sonarsource.com/sonarqube/latest/user-guide/metric-definitions/\",\n \"type\": \"reference\"\n },\n {\n \"title\": \"OWASP Top 10\",\n \"url\": \"https://owasp.org/www-project-top-ten/\",\n \"type\": \"reference\"\n },\n {\n \"title\": \"OWASP Dependency-Check\",\n \"url\": \"https://owasp.org/www-project-dependency-check/\",\n \"type\": \"tool\"\n },\n {\n \"title\": \"Snyk Vulnerability Database\",\n \"url\": \"https://security.snyk.io/\",\n \"type\": \"tool\"\n },\n {\n \"title\": \"npm audit\",\n \"url\": \"https://docs.npmjs.com/cli/v10/commands/npm-audit\",\n \"type\": \"tool\"\n },\n {\n \"title\": \"Composer audit\",\n \"url\": \"https://getcomposer.org/doc/03-cli.md#audit\",\n \"type\": \"tool\"\n },\n {\n \"title\": \"gitleaks\",\n \"url\": \"https://github.com/gitleaks/gitleaks\",\n \"type\": \"tool\"\n },\n {\n \"title\": \"Use the Index, Luke\",\n \"url\": \"https://use-the-index-luke.com/\",\n \"type\": \"guide\"\n },\n {\n \"title\": \"PHPStan\",\n \"url\": \"https://phpstan.org/\",\n \"type\": \"tool\"\n }\n ],\n \"tags\": [\n \"technical-debt\",\n \"code-quality\",\n \"maintainability\",\n \"refactoring\",\n \"audit\",\n \"dependency-management\",\n \"testing\",\n \"documentation\",\n \"security\",\n \"performance\",\n \"database\",\n \"infrastructure\",\n \"code-smells\",\n \"legacy-code\"\n ],\n \"lastUpdated\": \"2026-05-16\",\n \"rulesTotal\": 42\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":5408,"content_sha256":"78614ba9cf05d0d044f1f764e6eca02c9ef278000e97ea2a036251b40435abab"},{"filename":"README.md","content":"# Technical Debt\n\nTechnical debt inventory, prioritization, and audit framework for **PHP/Laravel (MySQL) and Node/TypeScript/React** projects. Produces a ranked debt ledger (effort × impact) so teams know what to pay down first — not just what's broken. Supports both coding reference and audit mode with PASS/FAIL/N/A output.\n\n**Version:** 1.0.0\n\n## Overview\n\n- Technical debt audit with PASS/FAIL/N/A checklist output\n- Ranked debt ledger sorted by effort × impact (P0–P3)\n- Code debt detection (duplication, complexity, god classes, dead code, magic numbers)\n- Security debt (secrets, validation, auth hardening)\n- Design debt (coupling, circular deps, leaky abstractions)\n- Dependency debt (outdated, abandoned, CVEs, unused)\n- Test debt (coverage gaps, flaky tests, disabled tests, slow suites)\n- Performance debt (N+1 queries, unbounded results, bundle bloat, missing caching)\n- Data debt (schema drift, missing indexes, orphaned records)\n- Infrastructure debt (EOL runtimes, secrets management, observability)\n- Process debt (aging TODOs, deprecated markers, ownership, feature flags)\n- 42 rules across 10 categories\n\n## Categories\n\n### 1. Code Debt (CRITICAL)\nDuplication, complexity, long functions, god classes, dead code, magic numbers, long parameter lists.\n\n### 2. Security Debt (CRITICAL)\nSecrets in source code, missing input validation, outdated auth and missing hardening (CSP, HSTS, rate limits, MFA).\n\n### 3. Design Debt (HIGH)\nTight coupling, circular dependencies, leaky abstractions, shotgun surgery.\n\n### 4. Dependency Debt (HIGH)\nOutdated versions, abandoned packages, security advisories, unused deps.\n\n### 5. Test Debt (HIGH)\nCoverage gaps, flaky tests, disabled tests, slow tests.\n\n### 6. Performance Debt (HIGH)\nN+1 queries, unbounded result sets, frontend bundle bloat, missing caching opportunities.\n\n### 7. Data Debt (HIGH)\nDatabase schema drift, missing indexes on hot queries, orphaned records / referential gaps.\n\n### 8. Documentation Debt (MEDIUM)\nStale comments, outdated architecture docs, undocumented public APIs.\n\n### 9. Infrastructure Debt (MEDIUM)\nEOL runtime versions, deprecated framework APIs, build warnings, secrets-management practices, observability and monitoring gaps.\n\n### 10. Process Debt (MEDIUM)\nAging TODO/FIXME comments, `@deprecated` markers without removal plans, untracked debt, code without owners (CODEOWNERS), lingering feature flags.\n\n## Usage\n\n```\nAudit technical debt in this project\nWhat should we refactor first?\nBuild a debt ledger for the checkout module\nFind the top 5 highest-impact tech debt items\nReview this PR for new technical debt\nAudit security debt: secrets, input validation, auth hardening\nFind N+1 queries and missing indexes\n```\n\n## References\n\n- [Martin Fowler — Technical Debt Quadrant](https://martinfowler.com/bliki/TechnicalDebtQuadrant.html)\n- [Ward Cunningham — The Debt Metaphor](https://www.youtube.com/watch?v=pqeJFYwnkjE)\n- [SonarQube — Technical Debt Model](https://docs.sonarsource.com/sonarqube/latest/user-guide/metric-definitions/)\n- [OWASP Top 10](https://owasp.org/www-project-top-ten/)\n- [Snyk Vulnerability Database](https://security.snyk.io/)\n- [Use the Index, Luke](https://use-the-index-luke.com/)\n- [gitleaks](https://github.com/gitleaks/gitleaks)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3269,"content_sha256":"04238ed998fd6a6925df5fcef09f57bbe786710806ef757b59c505f5b462c28f"},{"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. Code Debt (code)\n\n**Impact:** CRITICAL\n**Description:** Debt within the code itself — duplication, complex methods, oversized classes, dead code, magic literals, and long parameter lists. These are the most direct drivers of bug density and slow feature work, and they compound fastest as the codebase grows.\n\n## 2. Security Debt (security)\n\n**Impact:** CRITICAL\n**Description:** Accumulated security gaps — secrets in code, missing input validation, outdated auth defaults, and missing hardening. Security debt is the category most likely to become a *public* problem; what's tolerable as a backlog item today is tomorrow's breach disclosure.\n\n## 3. Design Debt (design)\n\n**Impact:** HIGH\n**Description:** Structural debt across modules — tight coupling, circular dependencies, leaky abstractions, and changes that ripple through many files. Design debt makes refactoring expensive and concentrates risk in small changes.\n\n## 4. Dependency Debt (deps)\n\n**Impact:** HIGH\n**Description:** Debt in third-party packages — outdated versions, abandoned libraries, known CVEs, and unused dependencies. Dependency debt is the cheapest debt to detect (tooling does it for you) and the most dangerous to ignore (it grows by itself even when you don't touch the code).\n\n## 5. Test Debt (test)\n\n**Impact:** HIGH\n**Description:** Gaps in test coverage, flaky or disabled tests, and slow suites that erode confidence in CI. Test debt directly slows the team — a flaky or slow suite costs every engineer every day.\n\n## 6. Performance Debt (perf)\n\n**Impact:** HIGH\n**Description:** N+1 queries, unbounded result sets, and bundle bloat. Performance debt looks fine in development and explodes proportionally to your most successful customer. It is the category most likely to convert directly into lost revenue.\n\n## 7. Data Debt (data)\n\n**Impact:** HIGH\n**Description:** Schema drift, missing indexes, and referential-integrity gaps. Data debt is the slowest to detect and the hardest to fix — every migration after the drift is a roll of the dice, and orphaned records propagate into reports nobody trusts.\n\n## 8. Documentation Debt (docs)\n\n**Impact:** MEDIUM\n**Description:** Stale comments, outdated READMEs, and undocumented public APIs. Documentation debt is invisible until onboarding or incident response, where it suddenly becomes the bottleneck.\n\n## 9. Infrastructure Debt (infra)\n\n**Impact:** MEDIUM\n**Description:** EOL runtimes, deprecated framework APIs, accumulated build warnings, insecure secrets handling, and observability gaps. Infrastructure debt has hard deadlines (CVEs, vendor EOL dates) and is non-negotiable once they hit.\n\n## 10. Process Debt (process)\n\n**Impact:** MEDIUM\n**Description:** Aging TODO/FIXME comments, `@deprecated` markers without removal plans, untracked debt, and code without owners. Process debt is about visibility and accountability — debt you can't see, or that nobody owns, cannot be prioritized.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3126,"content_sha256":"6732ba7dffa9d7b5fb3ad5e15e098367c22b8e27cc112e9b56017aaf4a016a7c"},{"filename":"rules/_template.md","content":"---\ntitle: Rule Title Here\nimpact: CRITICAL|HIGH|MEDIUM\nimpactDescription: \"Specific consequence — e.g., 'Drives bug density, slows feature work'\"\ntags: tag1, tag2, tag3\n---\n\n## Rule Title Here\n\n**Impact: LEVEL (impactDescription)**\n\n1-2 sentences explaining why this rule matters for managing technical debt.\n\n## How to Detect\n\n```bash\n# Tooling command or grep pattern\n```\n\n## Incorrect\n\n```language\n// ❌ Bad pattern — what debt looks like\n```\n\n**Problems:**\n- Problem 1\n- Problem 2\n\n## Correct\n\n```language\n// ✅ Remediated pattern\n```\n\n**Benefits:**\n- Benefit 1\n- Benefit 2\n\n## Remediation Strategy\n\n- **Effort:** S | M | L\n- **When to pay down:** (e.g., before adding next feature, during dedicated sprint, on first touch)\n\nReference: [Link](url)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":759,"content_sha256":"6dc8ebf76ba6ffd6ae6dfbf85d42a3113c7169ce7fceb290905087769b12b633"},{"filename":"rules/code-complexity.md","content":"---\ntitle: Cyclomatic and Cognitive Complexity\nimpact: CRITICAL\nimpactDescription: \"High-complexity methods harbor most production bugs\"\ntags: complexity, cyclomatic, cognitive\n---\n\n## Cyclomatic and Cognitive Complexity\n\n**Impact: CRITICAL (High-complexity methods harbor most production bugs)**\n\nMethods with cyclomatic complexity > 10 (or cognitive complexity > 15) are statistically the strongest predictor of bug density in a codebase. They are also the slowest to test, review, and modify.\n\n## How to Detect\n\n```bash\n# PHP\nvendor/bin/phpmd app text codesize --reportfile complexity.txt\n\n# JavaScript / TypeScript\nnpx eslint . --rule 'complexity: [\"error\", 10]'\n```\n\nThreshold: **cyclomatic complexity > 10** OR **cognitive complexity > 15** OR **nesting depth > 4**.\n\n## Incorrect\n\n```typescript\n// ❌ Cyclomatic complexity 14 — every branch combination is a separate path\nfunction calculateDiscount(user: User, order: Order): number {\n if (user.tier === 'gold') {\n if (order.total > 1000) {\n if (order.itemCount > 10) {\n if (user.yearsActive > 5) return 0.30;\n else return 0.25;\n } else if (order.itemCount > 5) {\n return 0.20;\n }\n return 0.15;\n } else if (order.total > 500) {\n return user.yearsActive > 2 ? 0.12 : 0.10;\n }\n return 0.05;\n } else if (user.tier === 'silver') {\n // ... another 20 lines of nested ifs\n }\n return 0;\n}\n```\n\n**Problems:**\n- 14+ independent code paths — minimum 14 tests to cover them all\n- Adding a new tier requires understanding every nested branch\n- Reviewers cannot hold the state in their head\n\n## Correct\n\n```typescript\n// ✅ Replace nested conditions with a lookup table + small predicates\ntype DiscountRule = { match: (u: User, o: Order) => boolean; rate: number };\n\nconst DISCOUNT_RULES: DiscountRule[] = [\n { match: (u, o) => u.tier === 'gold' && o.total > 1000 && o.itemCount > 10 && u.yearsActive > 5, rate: 0.30 },\n { match: (u, o) => u.tier === 'gold' && o.total > 1000 && o.itemCount > 10, rate: 0.25 },\n { match: (u, o) => u.tier === 'gold' && o.total > 1000 && o.itemCount > 5, rate: 0.20 },\n // ...\n];\n\nfunction calculateDiscount(user: User, order: Order): number {\n return DISCOUNT_RULES.find(r => r.match(user, order))?.rate ?? 0;\n}\n```\n\n**Benefits:**\n- Each rule is independently testable\n- New tiers/rules added by appending — no nested-branch surgery\n- Cyclomatic complexity of `calculateDiscount` drops to 1\n\n## Remediation Strategy\n\n- **Effort:** S–M per method\n- **When to pay down:** The next time you need to add a branch to a complexity-flagged method. Extract first, modify after.\n\nReference: [SonarSource — Cognitive Complexity](https://www.sonarsource.com/resources/cognitive-complexity/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2784,"content_sha256":"accce180e05973b49228f542a8f4e404300a2083735d4d30d1eea1d07b38d208"},{"filename":"rules/code-dead-code.md","content":"---\ntitle: Dead Code\nimpact: HIGH\nimpactDescription: \"Unused code misleads readers and inflates maintenance surface\"\ntags: dead-code, unused, cleanup\n---\n\n## Dead Code\n\n**Impact: HIGH (Unused code misleads readers and inflates maintenance surface)**\n\nDead code — unused exports, unreachable branches, commented-out blocks — costs nothing to delete and costs a lot to keep. Readers assume code that exists is code that runs; dead code wastes attention and creates phantom dependencies that block upgrades.\n\n## How to Detect\n\n```bash\n# TypeScript / JavaScript\nnpx knip # unused files, exports, deps (preferred; ts-prune is archived)\nnpx eslint . --rule 'no-unreachable: error'\n\n# PHP\nvendor/bin/phpstan analyse --level=9 # detects unreachable code and unused private elements\n# For broader dead-code detection, add: tomasvotruba/unused-public, or use Rector's DeadCodeSetList\n\n# Commented-out code (one --include per extension; grep doesn't expand braces)\ngrep -rEn '^\\s*//.*[;{}]

Technical Debt Technical debt audit and prioritization framework for PHP/Laravel (MySQL) and Node/TypeScript/React projects. Contains 42 rules across 10 categories covering code, security, design, dependency, test, performance, data, documentation, infrastructure, and process debt. Produces a ranked ledger (effort × impact) so teams know what to fix first , not just what's broken. Supports both coding reference and audit mode with PASS/FAIL/N/A output. Metadata - Version: 1.0.0 - Scope: PHP / Laravel (MySQL) + Node / TypeScript / React - Rule Count: 42 rules across 10 categories - License: MI…

--include='*.php' --include='*.ts' --include='*.tsx' --include='*.js' .\n```\n\n## Incorrect\n\n```typescript\n// ❌ Dead imports, dead helper, dead branch, commented-out block\nimport { legacyFormatter } from './legacy'; // never used after v2 rewrite\nimport { format } from './format';\n\nfunction formatPrice(p: number, currency: string) {\n // const oldImpl = (p) => `${p.toFixed(2)}`; // kept \"just in case\"\n // if (currency === 'BTC') return formatBtc(p); // BTC support removed 2023\n\n if (currency === 'USD') return format(p, 'USD');\n if (currency === 'EUR') return format(p, 'EUR');\n return format(p, 'USD');\n return formatLegacy(p); // unreachable\n}\n\nexport function formatLegacy() { /* called nowhere */ }\n```\n\n**Problems:**\n- Reader has to puzzle out whether the commented BTC branch is coming back\n- `formatLegacy` blocks deleting the `./legacy` module\n- The unreachable `return` raises false suspicion during reviews\n\n## Correct\n\n```typescript\n// ✅ Delete it. Git remembers.\nimport { format } from './format';\n\nfunction formatPrice(p: number, currency: string): string {\n if (currency === 'EUR') return format(p, 'EUR');\n return format(p, 'USD');\n}\n```\n\n**Benefits:**\n- No phantom dependency on the legacy module\n- Reader sees only what runs\n- Diff in `git log` documents *when* and *why* BTC was removed — better than a stale comment\n\n## Remediation Strategy\n\n- **Effort:** S (deletion is mechanical; trust git history)\n- **When to pay down:** Immediately on detection — there is no reason to keep dead code in main.\n\n**Note:** Resist the urge to keep \"might-be-useful-later\" code commented out. If you genuinely need it later, restore it from git history. The cost of a `git revert` is far less than the cost of confusing every future reader.\n\nReference: [Refactoring — Remove Dead Code](https://refactoring.guru/smells/dead-code)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2871,"content_sha256":"76a188ae813e5c4e53331a77f215baf8ed19828a834ee23ba1ed1c4ed32da20d"},{"filename":"rules/code-duplication.md","content":"---\ntitle: Code Duplication\nimpact: CRITICAL\nimpactDescription: \"Bug fixes multiply across copies; behaviour drifts silently\"\ntags: duplication, dry, refactoring\n---\n\n## Code Duplication\n\n**Impact: CRITICAL (Bug fixes multiply across copies; behaviour drifts silently)**\n\nDuplicated logic means every bug fix and behaviour change must be made in N places. Copies drift over time, producing inconsistent behaviour that is hard to detect and harder to test.\n\n## How to Detect\n\n```bash\n# Find duplicated blocks across the repo (multi-language)\nnpx jscpd --min-lines 30 --min-tokens 100 src/\nnpx jscpd --languages php --min-lines 30 app/ # PHP support via jscpd\n\n# PHP-specific (note: sebastian/phpcpd was archived in 2023 — prefer jscpd above)\n# vendor/bin/phpcpd app/ # legacy projects only\n\n# Language-agnostic\ngit ls-files | xargs sha1sum | sort | uniq -d -w 40 # exact file dupes\n```\n\nThreshold: any duplicated block longer than **30 lines** or appearing in **3+ locations** counts as debt.\n\n## Incorrect\n\n```php\n// ❌ Bad: same calculation duplicated in two controllers\n// app/Http/Controllers/OrderController.php\npublic function summary(Order $order) {\n $subtotal = $order->items->sum(fn($i) => $i->price * $i->quantity);\n $tax = $subtotal * 0.06;\n $shipping = $subtotal > 100 ? 0 : 15;\n return ['total' => $subtotal + $tax + $shipping];\n}\n\n// app/Http/Controllers/CartController.php\npublic function checkout(Cart $cart) {\n $subtotal = $cart->items->sum(fn($i) => $i->price * $i->quantity);\n $tax = $subtotal * 0.06;\n $shipping = $subtotal > 100 ? 0 : 15;\n return ['total' => $subtotal + $tax + $shipping];\n}\n```\n\n**Problems:**\n- Tax rate change requires editing both files (and any others that copy this)\n- One copy can drift (e.g., free-shipping threshold raised in one but not the other)\n- No single place to add tests for pricing rules\n\n## Correct\n\n```php\n// ✅ Single source of truth\nfinal class PricingCalculator\n{\n public function __construct(private TaxRate $tax, private ShippingPolicy $shipping) {}\n\n public function total(iterable $items): Money\n {\n $subtotal = collect($items)->sum(fn($i) => $i->price * $i->quantity);\n return $subtotal + $this->tax->for($subtotal) + $this->shipping->for($subtotal);\n }\n}\n\n// Both controllers inject and call PricingCalculator::total()\n```\n\n**Benefits:**\n- One place to change tax/shipping rules\n- One target for unit tests\n- Behaviour cannot drift between call sites\n\n## Remediation Strategy\n\n- **Effort:** S–M (depends on number of duplicates and parameter variation)\n- **When to pay down:** Before the next behaviour change touches *any* of the copies — the next edit pays for the refactor.\n\nReference: [Martin Fowler — Duplicated Code](https://refactoring.com/catalog/extractFunction.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2842,"content_sha256":"0e8813342d862405fcb04c6a7f95a0d6a428e1a505f47e236ec80d7c8d29036a"},{"filename":"rules/code-god-classes.md","content":"---\ntitle: God Classes\nimpact: CRITICAL\nimpactDescription: \"One-class kingdoms become merge-conflict and bug magnets\"\ntags: srp, god-class, refactoring\n---\n\n## God Classes\n\n**Impact: CRITICAL (One-class kingdoms become merge-conflict and bug magnets)**\n\nA class with too many responsibilities (often called a \"god class\") attracts every change in the system. It becomes the file with the most commits, the most authors, and the most bugs — and it blocks parallel work.\n\n## How to Detect\n\n```bash\n# Files larger than 300 lines (rough heuristic)\nfind . -type f \\( -name '*.php' -o -name '*.ts' \\) -exec wc -l {} \\; | \\\n awk '$1 > 300 { print }' | sort -rn\n\n# PHP\nvendor/bin/phpmd app text codesize # reports ExcessiveClassLength, TooManyMethods\n# PHPMD defaults are looser (1000 LoC, 25 methods); for stricter 300/15 limits, use a custom\n# ruleset overriding `\u003cproperty name=\"minimum\" .../>` on those rules.\n\n# Hotspot detection — files changed most often\ngit log --since='12 months ago' --name-only --pretty=format: | \\\n sort | uniq -c | sort -rn | head -20\n```\n\nThresholds: **> 300 lines**, **> 15 public methods**, OR **> 7 dependencies in the constructor**.\n\n## Incorrect\n\n```php\n// ❌ OrderService has 820 lines, 27 public methods, depends on 12 services\nfinal class OrderService\n{\n public function __construct(\n private OrderRepo $orders,\n private CartRepo $carts,\n private PaymentGateway $payments,\n private TaxCalculator $tax,\n private ShippingService $shipping,\n private InventoryService $inventory,\n private NotificationService $notifications,\n private AnalyticsService $analytics,\n private FraudCheckService $fraud,\n private LoyaltyService $loyalty,\n private InvoiceService $invoices,\n private RefundService $refunds,\n ) {}\n\n public function create(...) { /* 80 lines */ }\n public function update(...) { /* 60 lines */ }\n public function cancel(...) { /* 70 lines */ }\n public function refund(...) { /* 90 lines */ }\n public function ship(...) { /* ... */ }\n public function calculateTax(...) { /* ... */ }\n // ... 21 more methods\n}\n```\n\n**Problems:**\n- Every team that touches orders edits this one file → constant merge conflicts\n- Unit tests must mock 12 collaborators just to instantiate it\n- A bug in refund logic puts the entire order flow at risk to deploy\n\n## Correct\n\n```php\n// ✅ Split by lifecycle stage / responsibility\nfinal class OrderPlacement { /* create() */ }\nfinal class OrderFulfillment { /* ship(), markDelivered() */ }\nfinal class OrderCancellation { /* cancel() */ }\nfinal class OrderRefund { /* refund() */ }\nfinal class OrderTaxCalculation { /* calculateTax() */ }\n\n// Each has 2–4 dependencies and 30–100 LoC\n```\n\n**Benefits:**\n- Teams can work on different stages in parallel without conflicts\n- Each class has a focused test suite with minimal mocking\n- Bug in refund logic blocks only the refund deploy, not new orders\n\n## Remediation Strategy\n\n- **Effort:** L (almost always — break into multiple PRs)\n- **When to pay down:** Identify the highest-churn god class first (`git log` hotspots). Carve off one responsibility per PR — do not attempt a single big-bang refactor.\n\nReference: [Martin Fowler — Large Class](https://refactoring.com/catalog/extractClass.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3380,"content_sha256":"fae2d7bf58c4d2a143119b12a2bd6cb63a3170e40f622d8989dd85bfab3f5f86"},{"filename":"rules/code-long-functions.md","content":"---\ntitle: Long Functions and Methods\nimpact: CRITICAL\nimpactDescription: \"Long methods hide multiple responsibilities and resist testing\"\ntags: srp, function-length, refactoring\n---\n\n## Long Functions and Methods\n\n**Impact: CRITICAL (Long methods hide multiple responsibilities and resist testing)**\n\nA function longer than ~50 lines almost always does more than one thing. It cannot be unit-tested in isolation, cannot be named accurately, and is the most common location for \"scary code nobody touches\".\n\n## How to Detect\n\n```bash\n# Find functions over 50 lines (rough heuristic)\ngrep -rEn 'function |def |func ' --include='*.{php,ts,js,py,go}' | \\\n awk -F: '{ print $1 \":\" $2 }' | \\\n while read line; do : ; done # combine with editor stats or:\n\n# PHP\nvendor/bin/phpmd app text codesize # reports ExcessiveMethodLength (default 100; lower to 50 via custom ruleset)\n# \u003cproperty name=\"minimum\" value=\"50\"/> in the ExcessiveMethodLength rule reference\n\n# TS / JS\nnpx eslint . --rule 'max-lines-per-function: [\"error\", 50]'\n```\n\nThreshold: **method > 50 lines** OR **function > 50 lines** (excluding comments and blank lines).\n\n## Incorrect\n\n```php\n// ❌ 180-line controller action doing fetch, validate, transform, persist, notify\npublic function checkout(Request $request)\n{\n // 20 lines of input validation\n $data = $request->validate([ /* ... */ ]);\n\n // 30 lines of cart calculation\n $cart = Cart::findOrFail($data['cart_id']);\n $subtotal = 0;\n foreach ($cart->items as $item) { /* ... */ }\n $tax = /* ... */;\n\n // 25 lines of payment processing\n $charge = Stripe::charges()->create(/* ... */);\n if ($charge->status !== 'succeeded') { /* ... */ }\n\n // 40 lines of order creation + side effects\n $order = Order::create(/* ... */);\n foreach ($cart->items as $item) { /* ... */ }\n\n // 20 lines of notifications\n Mail::to($cart->user)->send(new OrderConfirmation($order));\n Slack::notify('#sales', /* ... */);\n\n return response()->json(/* ... */);\n}\n```\n\n**Problems:**\n- Cannot test calculation logic without mocking Stripe and Mail\n- Reviewer must understand the entire flow to verify a one-line change\n- Side effects (mail, Slack) hidden inside a request handler\n\n## Correct\n\n```php\n// ✅ Each responsibility extracted; controller orchestrates only\npublic function checkout(CheckoutRequest $request, CheckoutService $service)\n{\n $order = $service->process(\n cart: Cart::findOrFail($request->validated('cart_id')),\n paymentToken: $request->validated('payment_token'),\n );\n return new OrderResource($order);\n}\n\n// CheckoutService::process() is itself a short orchestrator that calls:\n// PricingCalculator, PaymentGateway, OrderRepository, NotificationDispatcher\n```\n\n**Benefits:**\n- Controller is 5 lines, trivially testable as an HTTP wrapper\n- Pricing, payment, persistence, notifications each have a focused unit test\n- Adding a new notification channel changes one class\n\n## Remediation Strategy\n\n- **Effort:** S–M (Extract Method is mechanical; the hard part is naming)\n- **When to pay down:** Whenever you need to add a feature inside a long method, extract first.\n\nReference: [Refactoring — Extract Function](https://refactoring.com/catalog/extractFunction.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3262,"content_sha256":"3fc310c53bc5a2ad0bd7346f90c5484721227596c6c6bc9e89a2d46d6d24a5c3"},{"filename":"rules/code-long-parameter-lists.md","content":"---\ntitle: Long Parameter Lists\nimpact: MEDIUM\nimpactDescription: \"Hard to call correctly; signals mixed responsibilities\"\ntags: parameters, srp, refactoring\n---\n\n## Long Parameter Lists\n\n**Impact: MEDIUM (Hard to call correctly; signals mixed responsibilities)**\n\nA function with 7+ parameters is almost impossible to call correctly without re-reading the signature each time. Positional arguments get swapped (`createUser(name, email, ...)` vs `createUser(email, name, ...)`), and the function is usually doing too many things.\n\n## How to Detect\n\n```bash\n# JavaScript / TypeScript\nnpx eslint . --rule 'max-params: [\"error\", 4]'\n\n# PHP\nvendor/bin/phpmd app text codesize # ExcessiveParameterList (default 10; lower via ruleset)\n```\n\nThreshold: **> 4 parameters** (3 or fewer is fine, 4 is borderline, 5+ is debt).\n\n## Incorrect\n\n```typescript\n// ❌ Eight positional parameters — easy to swap, hard to call\nexport function createBooking(\n userId: string,\n hotelId: string,\n roomTypeId: string,\n startDate: Date,\n endDate: Date,\n guestCount: number,\n promoCode: string | null,\n notes: string,\n): Booking {\n // ...\n}\n\n// At the call site:\ncreateBooking(u, h, r, s, e, 2, null, 'Late check-in'); // is 2 the count or roomTypeId?\n```\n\n```php\n// ❌ Same anti-pattern in PHP\npublic function process(\n int $orderId,\n int $userId,\n int $shippingMethodId,\n string $address,\n string $city,\n string $postalCode,\n string $country,\n bool $expedited,\n bool $giftWrap,\n): Order { /* ... */ }\n```\n\n**Problems:**\n- Reordering parameters in a refactor silently breaks every caller (same type → no compile error)\n- Optional parameters force you to pass `null` for arguments you don't care about\n- The signature is doing the work of a value object\n\n## Correct\n\n```typescript\n// ✅ Introduce Parameter Object — named, optional fields make intent explicit\ninterface BookingRequest {\n userId: string;\n hotelId: string;\n roomTypeId: string;\n stay: { from: Date; to: Date };\n guestCount: number;\n promoCode?: string;\n notes?: string;\n}\n\nexport function createBooking(req: BookingRequest): Booking { /* ... */ }\n\n// Call site:\ncreateBooking({\n userId: u,\n hotelId: h,\n roomTypeId: r,\n stay: { from: s, to: e },\n guestCount: 2,\n notes: 'Late check-in',\n});\n```\n\n```php\n// ✅ PHP equivalent: a DTO / value object\nfinal readonly class BookingRequest\n{\n public function __construct(\n public int $userId,\n public int $hotelId,\n public int $roomTypeId,\n public DateRange $stay,\n public int $guestCount,\n public ?string $promoCode = null,\n public ?string $notes = null,\n ) {}\n}\n\npublic function process(BookingRequest $req): Order { /* ... */ }\n```\n\n**Benefits:**\n- Named arguments are self-documenting\n- Adding a field doesn't break callers\n- The object becomes a natural place to add validation or behaviour later\n\n## Remediation Strategy\n\n- **Effort:** S–M (Introduce Parameter Object is a well-known refactor; most IDEs automate it)\n- **When to pay down:** When you need to add yet another parameter to an already-long signature, or when a bug is traced to swapped arguments at a call site.\n\n**Anti-pattern:** \"Boolean flag parameters\" — `process(order, true, false, true)` is unreadable. Replace with enum values or split into separate functions.\n\nReference: [Refactoring — Introduce Parameter Object](https://refactoring.guru/introduce-parameter-object) · [Refactoring — Replace Parameter with Method Call](https://refactoring.guru/replace-parameter-with-method-call)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3567,"content_sha256":"83757cd0023e8516de60a251967c36edf20bbda7f245e63b4ec0e9e971d73ad2"},{"filename":"rules/code-magic-numbers.md","content":"---\ntitle: Magic Numbers and Hardcoded Literals\nimpact: MEDIUM\nimpactDescription: \"Obscure intent; require coordinated edits across files when changed\"\ntags: magic-numbers, constants, readability\n---\n\n## Magic Numbers and Hardcoded Literals\n\n**Impact: MEDIUM (Obscure intent; require coordinated edits across files when changed)**\n\nA `0.06` in tax code is invisible business knowledge. The next time the tax rate changes — or the next reader who needs to understand the rule — pays the cost. Magic numbers also make the same value drift across copies (one file uses `0.06`, another `0.065`).\n\n## How to Detect\n\n```bash\n# TypeScript / JavaScript\nnpx eslint . --rule 'no-magic-numbers: [\"error\", { \"ignore\": [0, 1, -1] }]'\n\n# PHP — PHPMD has no built-in magic-number rule; use a Psalm/PHPStan extension\n# or a custom PHPCS sniff. Closest built-ins:\nvendor/bin/phpstan analyse --level=8 # catches some via type-aware analysis\n# Custom: a project-local PHPCS sniff for hardcoded literals in *Service* / *Calculator* classes\n\n# Cross-language grep for suspicious literals in business logic\ngrep -rEn '\\b[0-9]+\\.[0-9]+\\b' app/Services/ src/services/ | grep -v test\n```\n\nThreshold: any non-trivial literal (anything except 0, 1, -1, and indexes used for slicing) appearing in business logic — especially if it appears more than once.\n\n## Incorrect\n\n```typescript\n// ❌ Bare numbers and strings scattered through business logic\nexport function calculateOrder(items: Item[], user: User): Order {\n const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0);\n const tax = subtotal * 0.06; // what's 0.06?\n const shipping = subtotal > 100 ? 0 : 15; // why 100? why 15?\n const cacheKey = `order:${user.id}:v3`; // why v3?\n redis.set(cacheKey, JSON.stringify({ subtotal, tax, shipping }), 'EX', 3600); // 3600 what?\n return { subtotal, tax, shipping };\n}\n```\n\n**Problems:**\n- \"0.06\" appears in 4 other files — tax rate change requires hunting them all\n- A reader can't tell whether `3600` is seconds, milliseconds, or a row count\n- `'v3'` is silent invariant — changing cache format requires knowing about every caller\n\n## Correct\n\n```typescript\n// ✅ Named constants with units and intent\nconst TAX_RATE = 0.06;\nconst FREE_SHIPPING_THRESHOLD = 100;\nconst FLAT_SHIPPING_FEE = 15;\nconst CACHE_VERSION = 'v3';\nconst CACHE_TTL_SECONDS = 60 * 60;\n\nexport function calculateOrder(items: Item[], user: User): Order {\n const subtotal = items.reduce((s, i) => s + i.price * i.qty, 0);\n const tax = subtotal * TAX_RATE;\n const shipping = subtotal > FREE_SHIPPING_THRESHOLD ? 0 : FLAT_SHIPPING_FEE;\n const cacheKey = `order:${user.id}:${CACHE_VERSION}`;\n redis.set(cacheKey, JSON.stringify({ subtotal, tax, shipping }), 'EX', CACHE_TTL_SECONDS);\n return { subtotal, tax, shipping };\n}\n```\n\n**Benefits:**\n- Tax-rate change is one line\n- Units are explicit (`60 * 60` reads as \"seconds in an hour\")\n- Constants are searchable; renames are mechanical\n\n## Remediation Strategy\n\n- **Effort:** S per file (extract constant, replace references)\n- **When to pay down:** When you next change a value (the change is now one line), or when you spot the same literal in 2+ places.\n- **Where to put constants:** as module-level `const` for local values; in a shared `pricing/Config.ts` or `config/billing.php` for cross-module business values.\n\n**Tip:** for genuinely tunable values (rates, thresholds, feature flags), put them in environment-driven config so changes don't require a deploy.\n\nReference: [Refactoring — Replace Magic Number with Symbolic Constant](https://refactoring.guru/replace-magic-number-with-symbolic-constant)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3704,"content_sha256":"054b69bff399be6d4c5072a608617b907fd279dba3de491d38c043fec09e87dd"},{"filename":"rules/data-missing-indexes.md","content":"---\ntitle: Missing Database Indexes\nimpact: HIGH\nimpactDescription: \"Query time grows linearly with data; locks and connection pool compound\"\ntags: database, indexes, performance\n---\n\n## Missing Database Indexes\n\n**Impact: HIGH (Query time grows linearly with data; locks and connection pool compound)**\n\nA missing index turns a 5ms query into a 5-second query as the table grows. Worse, because slow queries hold connections longer, missing indexes cascade into connection-pool exhaustion and 503s for unrelated traffic. Indexing decisions made early are usually right; indexes never added at all are silent debt.\n\n## How to Detect\n\n```sql\n-- MySQL (requires sys schema, enabled by default in 5.7+): tables doing frequent full scans\nSELECT * FROM sys.schema_tables_with_full_table_scans\nWHERE rows_full_scanned > 1000\nORDER BY rows_full_scanned DESC;\n\n-- MySQL: query plan for a hot query (type=ALL means full table scan)\nEXPLAIN SELECT * FROM orders WHERE customer_id = 123;\nEXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123; -- MySQL 8.0+\n\n-- MySQL: indexes that exist but are never read\nSELECT * FROM sys.schema_unused_indexes;\n\n-- MySQL: top queries by total latency (performance_schema must be ON)\nSELECT * FROM sys.statement_analysis\nORDER BY total_latency DESC LIMIT 20;\n```\n\nApplication-side:\n- **Laravel:** Telescope's \"Queries\" panel — sort by duration\n- **Node ORMs:** enable query logging (Prisma `log: ['query', 'warn']`, TypeORM `logging: 'all'`) and review slow entries\n\nLook for: queries on foreign keys, status columns, date filters, and `ORDER BY` columns without supporting indexes.\n\n## Incorrect\n\n```php\n// ❌ Foreign key columns without indexes\nSchema::create('orders', function (Blueprint $t) {\n $t->id();\n $t->unsignedBigInteger('customer_id'); // FK column — no index!\n $t->string('status'); // queried often — no index!\n $t->timestamp('created_at');\n});\n\n// Query: SELECT * FROM orders WHERE customer_id = ? AND status = 'paid' ORDER BY created_at DESC\n// → Seq Scan of the entire orders table for every customer profile load\n```\n\n**Problems:**\n- Customer-profile page becomes O(N) of total orders, not the customer's orders\n- Status-based dashboards lock the table during long scans\n- Connection pool saturates under modest load\n\n## Correct\n\n```php\n// ✅ Index foreign keys, status columns, and ORDER BY columns\nSchema::create('orders', function (Blueprint $t) {\n $t->id();\n $t->foreignId('customer_id')->constrained()->index(); // index on FK\n $t->string('status')->index();\n $t->timestamp('created_at');\n\n // Composite index for the common (customer_id, status, created_at) query path\n $t->index(['customer_id', 'status', 'created_at']);\n});\n```\n\nVerify the plan after indexing:\n\n```sql\nEXPLAIN\nSELECT * FROM orders\nWHERE customer_id = 123 AND status = 'paid'\nORDER BY created_at DESC LIMIT 50;\n\n-- Want to see in `key`: orders_customer_id_status_created_at_index\n-- NOT: NULL (or `type` = ALL → full table scan)\n```\n\n**Benefits:**\n- Query time becomes O(log N) instead of O(N)\n- Connection pool stays healthy under load\n- The index pays for itself many times over per request\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — add a single index (online index creation in MySQL/InnoDB minimizes downtime)\n - **M** — add several indexes; analyze query patterns first\n - **L** — large tables (100M+ rows) require careful online build + monitoring\n- **When to pay down:**\n - **NOW:** any query on a hot path showing `type=ALL` in EXPLAIN over a > 10k-row table\n - **NOW:** any FK column without an index (defaults vary by ORM — Eloquent does not auto-index FKs)\n - **Then:** monitor `sys.statement_analysis` weekly and add indexes for the top long-runners\n\n**Anti-patterns:**\n- **Indexing everything** — wastes write speed and disk; index the columns you actually filter/sort by\n- **Adding indexes blindly** without `EXPLAIN ANALYZE` — verify the planner actually uses them\n- **Forgetting to remove old indexes** — duplicate or unused indexes cost disk + write speed\n\n**Online index creation (MySQL/InnoDB):**\n```sql\nALTER TABLE orders\n ADD INDEX orders_customer_status_created_at_index (customer_id, status, created_at),\n ALGORITHM=INPLACE, LOCK=NONE;\n```\n\nFor huge tables, prefer `pt-online-schema-change` (Percona Toolkit) or `gh-ost` (GitHub) — both run truly non-blocking schema changes.\n\nReference: [Use the Index, Luke](https://use-the-index-luke.com/) · [MySQL — EXPLAIN Output Format](https://dev.mysql.com/doc/refman/8.4/en/explain-output.html) · [MySQL — sys Schema](https://dev.mysql.com/doc/refman/8.4/en/sys-schema.html) · [pt-online-schema-change](https://docs.percona.com/percona-toolkit/pt-online-schema-change.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4801,"content_sha256":"9e28632dd73e3672cc8e3f01197aee45652ae8dadcfeb1273da181a91904217b"},{"filename":"rules/data-orphaned-records.md","content":"---\ntitle: Orphaned Records and Referential Drift\nimpact: MEDIUM\nimpactDescription: \"Bugs, broken reports, and inconsistent state compound over time\"\ntags: database, referential-integrity, data-quality\n---\n\n## Orphaned Records and Referential Drift\n\n**Impact: MEDIUM (Bugs, broken reports, and inconsistent state compound over time)**\n\nOrphaned records — child rows whose parent no longer exists — are usually invisible until a report breaks or a query joins fail mysteriously. The root cause is almost always missing foreign-key constraints, ad-hoc deletes that bypass the ORM, or \"soft-delete the parent, leave children active\" semantics.\n\n## How to Detect\n\n```sql\n-- Orphans on a single relation\nSELECT child.id, child.parent_id\nFROM order_items child\nLEFT JOIN orders parent ON parent.id = child.parent_id\nWHERE parent.id IS NULL;\n\n-- MySQL: columns ending in _id that are NOT part of any FK constraint.\n-- Note: `_` is a single-char wildcard in LIKE — escape it (use `#` as ESCAPE char to\n-- avoid backslash-escape confusion in MySQL string literals). Exclude the PK `id` column.\nSELECT c.TABLE_NAME, c.COLUMN_NAME\nFROM information_schema.COLUMNS c\nWHERE c.TABLE_SCHEMA = DATABASE()\n AND c.COLUMN_NAME LIKE '%#_id' ESCAPE '#'\n AND c.COLUMN_NAME \u003c> 'id'\n AND NOT EXISTS (\n SELECT 1\n FROM information_schema.KEY_COLUMN_USAGE kcu\n JOIN information_schema.TABLE_CONSTRAINTS tc\n ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME\n AND tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA\n AND tc.TABLE_NAME = kcu.TABLE_NAME\n WHERE tc.CONSTRAINT_TYPE = 'FOREIGN KEY'\n AND kcu.TABLE_SCHEMA = c.TABLE_SCHEMA\n AND kcu.TABLE_NAME = c.TABLE_NAME\n AND kcu.COLUMN_NAME = c.COLUMN_NAME\n );\n\n-- Soft-deleted parents with active children\nSELECT COUNT(*) FROM orders\nWHERE deleted_at IS NOT NULL\n AND EXISTS (\n SELECT 1 FROM order_items\n WHERE order_id = orders.id AND deleted_at IS NULL\n );\n```\n\n## Incorrect\n\n```php\n// ❌ No FK constraints; manual delete bypasses cascades\nSchema::create('order_items', function (Blueprint $t) {\n $t->id();\n $t->unsignedBigInteger('order_id'); // no constraint, no index\n $t->unsignedBigInteger('product_id');\n});\n\n// Deletion via raw SQL — children become orphans\nDB::table('orders')->where('status', 'cancelled')->delete();\n// 200 order rows gone; their 1,800 order_items now point to nothing.\n\n// Soft-delete parent, hard-active children — reports show ghost orders\nclass Order extends Model { use SoftDeletes; }\nclass OrderItem extends Model {} // does NOT respect parent's deleted_at\n```\n\n**Problems:**\n- Joins return rows but with `NULL` parents, silently breaking sums and counts\n- Cleanup jobs run forever (\"delete order_items with no order\" finds millions)\n- Restore-from-backup workflows can't trust the data they restore\n\n## Correct\n\n```php\n// ✅ FK constraints with explicit cascade behaviour\nSchema::create('order_items', function (Blueprint $t) {\n $t->id();\n $t->foreignId('order_id')\n ->constrained() // creates FK to orders(id)\n ->cascadeOnDelete(); // delete items when order is deleted\n $t->foreignId('product_id')\n ->constrained()\n ->restrictOnDelete(); // can't delete a product still referenced\n});\n\n// ✅ Soft-deletes coordinated across parent and children\n// Both models must use SoftDeletes for soft-cascade to work (otherwise\n// `$order->items()->delete()` will hard-delete the children).\nclass OrderItem extends Model {\n use SoftDeletes;\n}\n\nclass Order extends Model {\n use SoftDeletes;\n protected static function booted() {\n static::deleted(fn($order) => $order->items()->delete()); // soft-deletes children\n static::restored(fn($order) => $order->items()->withTrashed()->restore());\n }\n}\n```\n\n```sql\n-- ✅ Add missing FKs to legacy tables (MySQL/InnoDB; clean orphans first)\n-- Note: adding a FK with `ALGORITHM=INPLACE` is only supported when\n-- `foreign_key_checks=OFF`, and `LOCK=NONE` is NOT supported for FK adds.\n-- With default settings MySQL forces `ALGORITHM=COPY`, which rewrites\n-- the table. For large tables, use `pt-online-schema-change` or `gh-ost`\n-- to add the FK without blocking writes.\nALTER TABLE order_items\n ADD CONSTRAINT fk_order_items_order\n FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE;\n```\n\n**Benefits:**\n- Database enforces integrity even when application code is buggy\n- Reports and joins are trustworthy\n- Cleanup jobs become unnecessary (or trivial)\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — adding constraints to greenfield tables\n - **M** — backfilling constraints + cleanup on small/medium tables\n - **L** — adding constraints to a 100M+ row table (must clean orphans first, then add constraint online)\n- **When to pay down:**\n - **NOW:** any data integrity issue traced back to orphans\n - **As cleanup project:** audit tables for `_id` columns without FK constraints\n - **Then:** make FKs required for all new tables (lint a migration template)\n\n**Cleanup workflow:**\n1. Run the orphan-detection query above for each suspected relation\n2. Decide policy: hard-delete orphans, mark them archived, or attach to a placeholder parent\n3. Apply the cleanup in batches (avoid one giant transaction)\n4. Add the FK constraint\n5. Add a CI test that runs the orphan-detection query against the test DB after each test run\n\nReference: [MySQL — FOREIGN KEY Constraints](https://dev.mysql.com/doc/refman/8.4/en/create-table-foreign-keys.html) · [Laravel — Foreign Key Constraints](https://laravel.com/docs/migrations#foreign-key-constraints)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5680,"content_sha256":"94301e930057f85b783e68439de41f7c39f1bd16a48ad72d7d12dc8c43b36c31"},{"filename":"rules/data-schema-drift.md","content":"---\ntitle: Database Schema Drift\nimpact: HIGH\nimpactDescription: \"Production schema diverges from code; migrations break unpredictably\"\ntags: database, migrations, schema\n---\n\n## Database Schema Drift\n\n**Impact: HIGH (Production schema diverges from code; migrations break unpredictably)**\n\nSchema drift is the gap between what the migrations say the database looks like and what it actually looks like. Once it exists, every new migration is a roll of the dice — it might apply cleanly, fail halfway through, or succeed but leave the schema in a state neither environment expects.\n\n## How to Detect\n\n```bash\n# Laravel: compare current schema to migration plan\nphp artisan migrate:status\nphp artisan schema:dump --prune # snapshot current schema\n# Apply on a fresh DB and diff against production schema\n\n# MySQL: schema-only dump for diffing across environments\nmysqldump --no-data --routines --triggers -u root -p prod_db > prod.sql\nmysqldump --no-data --routines --triggers -u root -p staging_db > staging.sql\ndiff -u staging.sql prod.sql\n\n# Drift signals:\n# - Tables that exist in prod but not in any migration\n# - Columns/indexes added manually via ALTER TABLE outside migration\n# - Migrations marked \"ran\" in different orders across environments\n# - Migrations that have been edited in place (hashes differ)\n```\n\nTooling: `atlas migrate diff`, `bytebase`, `liquibase diff`, `flyway info` for managed migration platforms.\n\n## Incorrect\n\n```\n❌ Common drift scenarios:\n\n1. \"Quick fix\" applied directly to prod\n DBA runs: ALTER TABLE orders ADD COLUMN priority INT DEFAULT 0;\n …without a corresponding migration. New migrations now run against a schema\n the codebase has never seen.\n\n2. Migration edited after being applied somewhere\n Original: CREATE TABLE refunds (id, order_id, amount);\n Edited: CREATE TABLE refunds (id, order_id, amount, reason TEXT NOT NULL);\n Some environments have the column; others don't. Same migration hash, two realities.\n\n3. Models with attributes not in any migration\n class Order extends Model {\n protected $fillable = ['status', 'priority']; // 'priority' has no migration\n }\n```\n\n**Problems:**\n- New environments (CI, staging, new dev laptops) can't reach the same schema\n- The next migration may fail at 50% completion, leaving the schema half-done\n- Reports and queries silently rely on columns that \"are there in prod\" but not in code\n\n## Correct\n\n```php\n// ✅ Every schema change goes through a migration\n// One migration per change, never edit a merged migration\n\n// database/migrations/2026_05_16_000000_add_priority_to_orders.php\nreturn new class extends Migration {\n public function up(): void {\n Schema::table('orders', fn (Blueprint $t) =>\n $t->unsignedTinyInteger('priority')->default(0)->after('status')\n );\n }\n public function down(): void {\n Schema::table('orders', fn (Blueprint $t) => $t->dropColumn('priority'));\n }\n};\n```\n\nCI gate:\n\n```yaml\n- name: Schema is migration-derivable\n run: |\n php artisan migrate --pretend --database=ci_clone # ensure all migrations apply\n php artisan schema:dump --prune\n git diff --exit-code database/schema/ # fail if dump differs\n```\n\n**Benefits:**\n- Any environment can be reconstructed from migrations alone\n- Code and schema move together, atomically reviewable in PRs\n- A failed migration in CI catches drift before it hits prod\n\n## Remediation Strategy\n\n- **Effort:** M–L (depends on how far drift has progressed)\n- **When to pay down:**\n - **NOW:** any drift discovered during incident response — fix during the postmortem\n - **As a cleanup project:** snapshot prod schema → generate a \"consolidation migration\" that brings empty databases to current state → mark all prior migrations as \"ran\" in environments that already match\n- **Anti-patterns:**\n - Editing applied migrations (always create a new one)\n - \"DBA runs prod ALTERs directly\" without a corresponding migration\n - Squashing migrations in a way that breaks existing environments\n\n**Tip:** in long-lived projects, periodically generate a \"consolidated migration\" from the current schema (`schema:dump`) so new environments don't have to replay 5 years of migrations. Keep the consolidated dump and historical migrations both checked in.\n\nReference: [Laravel — Schema Dumping](https://laravel.com/docs/migrations#squashing-migrations) · [Atlas — Migration Diff](https://atlasgo.io/) · [Liquibase Diff](https://docs.liquibase.com/commands/inspection/diff.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4546,"content_sha256":"5a997454f715743dae6bd18a0038b9b3d72d5fddbc791ed7d00b06a16c308ff0"},{"filename":"rules/deps-abandoned-packages.md","content":"---\ntitle: Abandoned and Unmaintained Packages\nimpact: HIGH\nimpactDescription: \"No upstream fixes for bugs, CVEs, or runtime upgrades\"\ntags: dependencies, abandoned, maintenance\n---\n\n## Abandoned and Unmaintained Packages\n\n**Impact: HIGH (No upstream fixes for bugs, CVEs, or runtime upgrades)**\n\nA package without a release in 24+ months, with open critical issues, or with the maintainer publicly stepping away is **abandonware**. The next CVE, the next Node/PHP version, or the next breaking dep change becomes *your* problem to fix.\n\n## How to Detect\n\nIndicators to check on each direct dependency:\n\n- **Last release date** (`npm view \u003cpkg> time.modified`, `composer info \u003cpkg>`)\n- **Open issues vs closed** ratio (high open count, no recent triage)\n- **Maintainer activity** (last commit > 2 years ago)\n- **Explicit deprecation** (`npm view \u003cpkg> deprecated`)\n- **Known alternatives community has migrated to**\n\n```bash\n# Node\nnpm view \u003cpkg> time.modified deprecated\nnpx npm-check # flags deprecated packages\nnpx snyk test # warns on unmaintained packages\n\n# PHP\ncomposer info \u003cpkg> # shows abandoned status from packagist\ncomposer audit # also reports abandonment\n```\n\n## Incorrect\n\n```json\n// ❌ Depending on packages flagged as abandoned or deprecated\n{\n \"dependencies\": {\n \"request\": \"^2.88.0\", // deprecated 2020 by maintainer\n \"node-uuid\": \"^1.4.8\", // replaced by `uuid` years ago\n \"moment\": \"^2.29.0\", // maintenance-only since 2020, deprecated by author\n \"babel-eslint\": \"^10.1.0\" // replaced by @babel/eslint-parser\n }\n}\n```\n\n**Problems:**\n- `request` has an open CVE with no upstream fix coming\n- `moment` ships ~290KB of timezone data — `date-fns` or `dayjs` do it in 10KB\n- Future engineer cannot tell whether these are \"trusted core deps\" or graveyard residents\n\n## Correct\n\n```json\n// ✅ Migrated to maintained alternatives\n{\n \"dependencies\": {\n \"undici\": \"^6.0.0\", // replaces `request`\n \"uuid\": \"^9.0.0\", // replaces `node-uuid`\n \"date-fns\": \"^3.0.0\", // replaces `moment`\n \"@babel/eslint-parser\": \"^7.23.0\"\n }\n}\n```\n\n**Benefits:**\n- CVEs in maintained packages get upstream fixes — you only patch\n- Bundle size and runtime characteristics improve\n- New engineers don't waste time on packages they \"shouldn't have learned\"\n\n## Remediation Strategy\n\n- **Effort:** S–M per package (depends on API surface used)\n- **When to pay down:**\n 1. **Now:** any abandoned dep with a known CVE\n 2. **This quarter:** any abandoned dep blocking a runtime upgrade\n 3. **Opportunistically:** the rest, when you're already touching that code path\n\n**Tip:** When forced to keep an abandoned dep temporarily, lock the version exactly, document why in a comment in the manifest, and create a tracking issue.\n\nReference: [npm Deprecation Policy](https://docs.npmjs.com/policies/deprecation) · [Packagist Abandoned Packages](https://packagist.org/about#abandoning-a-package)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3074,"content_sha256":"d005cfd67e1bef7a43f808ed1138deed35ed9187b9d55be9eb65addf2b6a4229"},{"filename":"rules/deps-outdated-versions.md","content":"---\ntitle: Outdated Dependency Versions\nimpact: HIGH\nimpactDescription: \"Each major version skipped exponentially raises upgrade cost\"\ntags: dependencies, upgrades, versioning\n---\n\n## Outdated Dependency Versions\n\n**Impact: HIGH (Each major version skipped exponentially raises upgrade cost)**\n\nSkipping major versions doesn't save effort — it just defers and compounds it. Two majors behind is roughly 4× the upgrade work of one major behind, because deprecations from intermediate versions stack.\n\n## How to Detect\n\n```bash\n# Node / TypeScript\nnpm outdated # shows current vs wanted vs latest\nnpx npm-check-updates # interactive upgrade tool\n\n# PHP / Laravel\ncomposer outdated --direct # direct deps only\ncomposer outdated --direct --major-only\n```\n\nThreshold: any dependency **more than 2 major versions behind** OR **more than 18 months behind on minor/patch**.\n\n## Incorrect\n\n```json\n// ❌ package.json — multiple dependencies 3+ majors behind\n{\n \"dependencies\": {\n \"react\": \"^16.8.0\", // current major: 19\n \"express\": \"^4.17.0\", // current major: 5\n \"webpack\": \"^4.46.0\", // current major: 5\n \"jest\": \"^26.6.3\", // current major: 29\n \"@types/node\": \"^14.0.0\" // current: 22\n }\n}\n```\n\n**Problems:**\n- React 16 → 19 means a full upgrade path through legacy mode, automatic batching, new JSX transform, etc.\n- Webpack 4 → 5 requires polyfill changes, ESM handling, persistent caching adoption\n- Jest 26 → 29 changes test environment and ESM behaviour\n- Each upgrade *separately* is now too big to fit in one sprint\n\n## Correct\n\n```json\n// ✅ Upgraded incrementally; pin policies documented\n{\n \"dependencies\": {\n \"react\": \"^19.0.0\",\n \"express\": \"^5.0.0\",\n \"webpack\": \"^5.95.0\",\n \"jest\": \"^29.7.0\",\n \"@types/node\": \"^22.0.0\"\n }\n}\n```\n\nEstablish a **monthly upgrade rhythm** rather than letting deps drift for a year.\n\n**Benefits:**\n- Each upgrade fits in a small PR\n- Security patches and deprecation warnings land while context is fresh\n- Avoids the \"we can't upgrade React because of 5 transitive blockers\" situation\n\n## Remediation Strategy\n\n- **Effort:** S per minor upgrade, M–L per major upgrade\n- **When to pay down:**\n - **Patch/minor:** weekly or biweekly automated PRs (Renovate, Dependabot)\n - **Major:** scheduled, one dep at a time, with a release plan\n- **Order of operations:** upgrade dev tools (TypeScript, ESLint, Jest) before frameworks; framework before app code\n\nReference: [Renovate Bot](https://docs.renovatebot.com/) · [Dependabot](https://docs.github.com/en/code-security/dependabot)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2647,"content_sha256":"7f4a9233654eeab71584a8ae80b62b8746339c61a5ce56a4359b371fa7565eff"},{"filename":"rules/deps-security-advisories.md","content":"---\ntitle: Known Security Advisories\nimpact: CRITICAL\nimpactDescription: \"Public CVEs are pre-published attack instructions\"\ntags: dependencies, security, cve\n---\n\n## Known Security Advisories\n\n**Impact: CRITICAL (Public CVEs are pre-published attack instructions)**\n\nOnce a CVE is public, exploit attempts start within hours. A HIGH/CRITICAL advisory in your dependency tree is not \"tech debt to schedule\" — it's an unmitigated security incident you haven't responded to yet.\n\n## How to Detect\n\n```bash\n# Node\nnpm audit # full report\nnpm audit --audit-level=high # CI-friendly threshold\nnpm audit fix # auto-fix non-breaking\n\n# PHP\ncomposer audit # built-in since Composer 2.4\ncomposer audit --format=json\n\n# Cross-stack\nsnyk test # https://snyk.io\n```\n\n## Incorrect\n\n```bash\n# ❌ Pre-commit and CI ignore audit results\n$ npm audit\n12 vulnerabilities (3 moderate, 7 high, 2 critical)\n$ git push # CI passes — audit isn't a gate\n```\n\n**Problems:**\n- CRITICAL CVEs sitting in main are public attack surface\n- No paper trail of when each was acknowledged\n- Each new dep adds more without anyone noticing\n\n## Correct\n\n```yaml\n# ✅ CI gate that fails on high+ vulnerabilities\n# .github/workflows/security.yml\n- name: Audit dependencies\n run: |\n npm audit --audit-level=high\n composer audit --abandoned=fail\n\n# ✅ Renovate / Dependabot configured for security PRs\n# renovate.json\n{\n \"vulnerabilityAlerts\": { \"enabled\": true, \"labels\": [\"security\"] },\n \"osvVulnerabilityAlerts\": true\n}\n```\n\n**Benefits:**\n- New CVEs auto-generate PRs within hours of disclosure\n- CI fails the moment a high-severity advisory lands\n- Audit log of every advisory acknowledgement and fix\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — patch/minor bump available, no breaking change\n - **M** — requires upgrade across multiple deps\n - **L** — vulnerable code is in an abandoned dep; replacement needed\n- **When to pay down:**\n - **CRITICAL / HIGH:** within 24–72 hours\n - **MEDIUM:** within the current sprint\n - **LOW:** opportunistically with other dep work\n- If a fix is genuinely blocked, document the **compensating control** (WAF rule, input validation, feature disable) and the **target unblocking date**.\n\nReference: [GitHub Advisory Database](https://github.com/advisories) · [OSV.dev](https://osv.dev/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2430,"content_sha256":"8ef11595d9484cc4646f0957036189e2bdb3b2f10448b39e89bbed443ed8ba9d"},{"filename":"rules/deps-unused-deps.md","content":"---\ntitle: Unused Dependencies\nimpact: MEDIUM\nimpactDescription: \"Inflate install size, supply-chain surface, and audit noise\"\ntags: dependencies, unused, cleanup\n---\n\n## Unused Dependencies\n\n**Impact: MEDIUM (Inflate install size, supply-chain surface, and audit noise)**\n\nA dependency you don't use is one you still ship, audit, and trust. Each unused dep is a potential supply-chain footgun (compromised maintainer, malicious post-install script) for zero benefit.\n\n## How to Detect\n\n```bash\n# Node / TypeScript\nnpx depcheck # unused + missing deps\nnpx knip # also finds unused files and exports\n\n# PHP / Composer\ncomposer-unused # https://github.com/composer-unused/composer-unused\nvendor/bin/composer-unused\n```\n\n## Incorrect\n\n```json\n// ❌ package.json declares deps no longer imported\n{\n \"dependencies\": {\n \"lodash\": \"^4.17.21\", // grep shows zero `from 'lodash'` imports\n \"axios\": \"^1.6.0\", // migrated to fetch 6 months ago\n \"moment\": \"^2.29.4\", // migrated to date-fns; one stale import left\n \"node-fetch\": \"^3.3.0\" // only used in a deleted script\n }\n}\n```\n\n**Problems:**\n- Each `npm install` downloads code that does nothing\n- Each `npm audit` reports advisories you can't act on (you don't even use the affected code paths)\n- New engineers see them and assume they're load-bearing\n\n## Correct\n\n```bash\n# ✅ Remove unused deps\n$ npx depcheck\nUnused dependencies: lodash, axios, moment, node-fetch\n$ npm uninstall lodash axios moment node-fetch\n$ npm audit # quieter report\n```\n\n```yaml\n# Add to CI to keep it clean\n- run: npx depcheck --ignores=\"@types/*,eslint-*\"\n```\n\n**Benefits:**\n- Smaller `node_modules`, faster installs, faster CI\n- Audit reports are signal, not noise\n- Reduced supply-chain attack surface\n\n## Remediation Strategy\n\n- **Effort:** S (almost always)\n- **When to pay down:** Immediately on detection. Add a depcheck/composer-unused step to CI to prevent regression.\n\n**Watch out for:**\n- **Transitive usage only:** some deps are loaded by tooling (e.g., babel plugins listed in `babel.config.js`). Verify before removing.\n- **Type-only packages:** `@types/*` packages are used by the compiler but invisible to import scanners — configure your tool to ignore them.\n\nReference: [depcheck](https://github.com/depcheck/depcheck) · [composer-unused](https://github.com/composer-unused/composer-unused)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2463,"content_sha256":"83aab6760ad3b09784f0522cc4ab6820d4340d75105288ac0b182a16fabfa795"},{"filename":"rules/design-circular-deps.md","content":"---\ntitle: Circular Dependencies\nimpact: HIGH\nimpactDescription: \"Indicates broken module boundaries; breaks tree-shaking and isolation\"\ntags: circular-deps, modules, design\n---\n\n## Circular Dependencies\n\n**Impact: HIGH (Indicates broken module boundaries; breaks tree-shaking and isolation)**\n\nModule A imports B, B imports A. Either the boundary is wrong, or one module is misplaced. Cycles break dead-code elimination, make modules impossible to test in isolation, and cause runtime initialization order bugs in many languages.\n\n## How to Detect\n\n```bash\n# JavaScript / TypeScript\nnpx madge --circular --extensions ts,tsx src/\n\n# PHP (architectural rules, including cycle detection between layers/modules)\nvendor/bin/deptrac analyse # qossmic/deptrac\n# or: vendor/bin/phparkitect check\n```\n\nThreshold: **zero cycles** is the only acceptable target. Even one cycle indicates a layering problem.\n\n## Incorrect\n\n```typescript\n// ❌ user/index.ts imports order/, order/index.ts imports user/\n// src/user/index.ts\nimport { Order } from '../order';\nexport class User {\n orders: Order[] = [];\n totalSpent() { return this.orders.reduce((s, o) => s + o.total, 0); }\n}\n\n// src/order/index.ts\nimport { User } from '../user';\nexport class Order {\n customer: User;\n total: number;\n}\n```\n\n**Problems:**\n- Either module fails to initialize cleanly under some bundlers (one side is `undefined` at import time)\n- Cannot extract `user` or `order` into a separate package\n- A test of `user` necessarily pulls in `order`\n\n## Correct\n\n```typescript\n// ✅ Option A: extract shared types to a third module\n// src/shared/types.ts\nexport interface UserRef { id: string; }\nexport interface OrderRef { id: string; total: number; }\n\n// src/user/index.ts → imports types only\n// src/order/index.ts → imports types only\n\n// ✅ Option B: invert the dependency — let one own the relationship\n// src/order/index.ts owns the customer reference;\n// user no longer knows about order. totalSpent() lives in an OrderService.\n```\n\n**Benefits:**\n- No initialization-order bugs\n- Each module can be packaged independently\n- Tests load the minimum surface\n\n## Remediation Strategy\n\n- **Effort:** M (mechanical once you decide the direction)\n- **When to pay down:** As soon as `madge`/equivalent reports a new cycle. Adding tests across the boundary first protects the refactor.\n\nReference: [Madge — Circular Dependencies](https://github.com/pahen/madge)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2448,"content_sha256":"b1b8dcdd4ddc4e5e8b3016f3b02269c377506cdf2ccfa2b97df801000dd351bc"},{"filename":"rules/design-leaky-abstractions.md","content":"---\ntitle: Leaky Abstractions\nimpact: HIGH\nimpactDescription: \"Implementation details leak past layer boundaries, blocking change\"\ntags: abstractions, layers, design\n---\n\n## Leaky Abstractions\n\n**Impact: HIGH (Implementation details leak past layer boundaries, blocking change)**\n\nAn abstraction leaks when its consumers depend on details it was supposed to hide — ORM models in controllers, framework types in domain logic, HTTP concerns in repositories. Once leaked, the abstraction can no longer be changed independently.\n\n## How to Detect\n\n- Grep for ORM/framework types in inner layers (`Eloquent\\Model`, `Request`, `HttpClient`) where they should not appear\n- Look for return types like `Builder`, `Collection\u003cModel>`, or `Response` crossing service boundaries\n\n```bash\n# Laravel: Eloquent leaking out of repositories\ngrep -rEn 'extends Model|Eloquent\\\\Builder' app/Services/ app/Domain/\n\n# Express: Request/Response leaking into services\ngrep -rEn '\\\\bRequest\\\\b|\\\\bResponse\\\\b' src/services/\n```\n\n## Incorrect\n\n```php\n// ❌ Repository returns an Eloquent Builder — controller chains ORM calls\nfinal class OrderRepository\n{\n public function forCustomer(int $customerId): Builder\n {\n return Order::query()->where('customer_id', $customerId);\n }\n}\n\n// Controller does ORM chaining directly:\n$orders = $this->repo->forCustomer($id)\n ->where('status', 'paid')\n ->with(['items', 'shipments'])\n ->orderByDesc('created_at')\n ->paginate(20);\n```\n\n**Problems:**\n- Repository's promised abstraction (\"get orders for customer\") is gone — controllers issue arbitrary queries\n- Cannot swap Eloquent for another data source without rewriting every controller\n- N+1 risk now lives in controllers, not in one auditable place\n\n## Correct\n\n```php\n// ✅ Repository returns plain DTOs or a paginated value object — no Builder leak\nfinal class OrderRepository\n{\n /** @return Paginated\u003cOrderSummary> */\n public function paidForCustomer(int $customerId, int $page = 1): Paginated\n {\n $query = Order::query()\n ->where('customer_id', $customerId)\n ->where('status', 'paid')\n ->with(['items', 'shipments'])\n ->orderByDesc('created_at');\n\n return Paginated::from($query->paginate(20, page: $page), OrderSummary::class);\n }\n}\n```\n\n**Benefits:**\n- Controllers receive a stable type; database choice is hidden\n- Eager-loading and ordering are owned by the repository (one auditable place)\n- Repository can be replaced with an HTTP gateway, gRPC client, or in-memory fake\n\n## Remediation Strategy\n\n- **Effort:** M–L (each leak is local but there are usually many)\n- **When to pay down:** When a swap or split is on the roadmap, OR when you find yourself fixing N+1 bugs in multiple controllers — the leak is now causing concrete pain.\n\nReference: [Joel Spolsky — The Law of Leaky Abstractions](https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2958,"content_sha256":"85dd8c3f6d41912e14c319e9b3bd0af1bdde6e538fde589a8cb6320063037959"},{"filename":"rules/design-shotgun-surgery.md","content":"---\ntitle: Shotgun Surgery\nimpact: HIGH\nimpactDescription: \"One conceptual change forces edits across many files\"\ntags: shotgun-surgery, design, refactoring\n---\n\n## Shotgun Surgery\n\n**Impact: HIGH (One conceptual change forces edits across many files)**\n\nA change is \"shotgun surgery\" when adding one concept (a new payment method, a new locale, a new currency) requires edits in 5+ files. The information is fragmented; whoever makes the change is guaranteed to miss a spot.\n\n## How to Detect\n\n```bash\n# Find files commonly changed together (co-change hotspots)\ngit log --since='6 months ago' --name-only --pretty=format:'COMMIT' | \\\n awk '/COMMIT/{print \"---\"; next} {print}' | \\\n # process: count file pairs appearing in the same commit\n # any pair > 10 co-changes is a shotgun-surgery suspect\n\n# Search for switch/if chains on the same enum across multiple files\ngrep -rEn \"case PaymentMethod::|=== ['\\\"]stripe['\\\"]\" --include='*.{php,ts}'\n```\n\n## Incorrect\n\n```typescript\n// ❌ Adding a new payment method \"klarna\" requires editing 6 files\n\n// src/payments/types.ts\ntype PaymentMethod = 'stripe' | 'paypal' | 'wire';\n\n// src/payments/validator.ts\nif (m === 'stripe') { /* */ } else if (m === 'paypal') { /* */ } else if (m === 'wire') { /* */ }\n\n// src/payments/feeCalculator.ts\nconst FEES = { stripe: 0.029, paypal: 0.034, wire: 0 };\n\n// src/payments/iconUrl.ts\nconst ICONS = { stripe: '...', paypal: '...', wire: '...' };\n\n// src/payments/displayName.ts\nconst NAMES = { stripe: 'Card', paypal: 'PayPal', wire: 'Bank wire' };\n\n// src/payments/router.ts\nswitch (m) { case 'stripe': return new StripeClient(); /* ... */ }\n```\n\n**Problems:**\n- Adding Klarna means hunting through 6+ files — easy to miss one\n- New engineers cannot find \"what makes a payment method valid\"\n- Tests don't catch a forgotten file until production\n\n## Correct\n\n```typescript\n// ✅ One Strategy per method — adding Klarna is one new file\n// src/payments/methods/StripeMethod.ts\nexport const StripeMethod: PaymentMethod = {\n id: 'stripe',\n displayName: 'Card',\n iconUrl: '/icons/stripe.svg',\n feeRate: 0.029,\n validate: (payload) => /* ... */,\n charge: (amount, token) => new StripeClient().charge(amount, token),\n};\n\n// src/payments/methods/index.ts\nexport const METHODS = [StripeMethod, PayPalMethod, WireMethod /*, KlarnaMethod */];\n```\n\n**Benefits:**\n- Adding Klarna = one new file + one export — nothing else changes\n- All knowledge about a method lives in one place\n- TypeScript types prevent \"forgot a branch\" bugs\n\n## Remediation Strategy\n\n- **Effort:** M (collect scattered knowledge into one type per concept)\n- **When to pay down:** Before the *next* new variant is added — the pain is highest right when adding one.\n\nReference: [Refactoring Guru — Shotgun Surgery](https://refactoring.guru/smells/shotgun-surgery)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2826,"content_sha256":"39904f75256f62aa4821ab3337fa19717939e72f9c1e75b1a9a3e338f859de15"},{"filename":"rules/design-tight-coupling.md","content":"---\ntitle: Tight Coupling\nimpact: HIGH\nimpactDescription: \"Changes ripple unpredictably; modules cannot be replaced\"\ntags: coupling, design, dependencies\n---\n\n## Tight Coupling\n\n**Impact: HIGH (Changes ripple unpredictably; modules cannot be replaced)**\n\nTight coupling means modules depend on each other's internals. A change in one ripples into many, and you can't swap an implementation without rewriting consumers. The symptom: small features take days, simple bug fixes break unrelated tests.\n\n## How to Detect\n\n- Count cross-module imports — modules importing > 10 files from other modules are suspect\n- Look for direct instantiation (`new X()`) of cross-module dependencies in business logic\n- Search for reaches into private/internal namespaces\n\n```bash\n# Find imports from too many other modules\ngrep -rE \"^(import|use|require)\" src/orders/ | \\\n awk -F'[\\\"\\\\\\\\\\\\047]' '{print $2}' | sort -u | wc -l\n```\n\n## Incorrect\n\n```typescript\n// ❌ OrderService reaches deep into Payment, Inventory, and Email internals\nimport { StripeClient } from '../payments/stripe/client';\nimport { StripeWebhookSecret } from '../payments/stripe/config';\nimport { InventoryDB } from '../inventory/db/connection';\nimport { SesTransport } from '../email/transports/ses';\n\nexport class OrderService {\n async place(order: Order) {\n const stripe = new StripeClient(StripeWebhookSecret.value);\n const charge = await stripe.charges.create({ amount: order.total });\n\n const conn = await InventoryDB.connect();\n await conn.query('UPDATE inventory SET stock = stock - ? WHERE sku = ?', [order.qty, order.sku]);\n\n const ses = new SesTransport(process.env.AWS_KEY!);\n await ses.send({ to: order.email, body: '...' });\n }\n}\n```\n\n**Problems:**\n- Cannot test without a real Stripe key, DB, and SES credentials\n- Switching from Stripe → Adyen means rewriting `place()`\n- A schema change in Inventory breaks orders\n\n## Correct\n\n```typescript\n// ✅ Depend on interfaces, inject implementations\nexport class OrderService {\n constructor(\n private payments: PaymentGateway,\n private inventory: InventoryRepository,\n private notifications: NotificationSender,\n ) {}\n\n async place(order: Order) {\n await this.payments.charge(order.total, order.token);\n await this.inventory.reserve(order.sku, order.qty);\n await this.notifications.orderConfirmed(order);\n }\n}\n```\n\n**Benefits:**\n- Each dependency is a stable interface — internals can change freely\n- Tests use in-memory fakes; no credentials needed\n- Stripe → Adyen swap is a one-line wiring change\n\n## Remediation Strategy\n\n- **Effort:** M per module boundary\n- **When to pay down:** When two modules' release schedules need to decouple, or when a swap is on the roadmap.\n\nReference: [Robert Martin — Stable Dependencies Principle (Package Principles)](https://en.wikipedia.org/wiki/Package_principles)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2872,"content_sha256":"6439589a54e318ba2d2f38b72024284ee033ef57678b343e8f720d9cab81dffe"},{"filename":"rules/docs-outdated-architecture.md","content":"---\ntitle: Outdated Architecture Documentation\nimpact: MEDIUM\nimpactDescription: \"Onboarding and incident response collapse without accurate maps\"\ntags: documentation, architecture, readme\n---\n\n## Outdated Architecture Documentation\n\n**Impact: MEDIUM (Onboarding and incident response collapse without accurate maps)**\n\nArchitecture docs that don't match the running system cause the worst kind of mistake: confident wrong decisions. The cost shows up at the worst moments — onboarding a new engineer or debugging a 2am incident.\n\n## How to Detect\n\nFor each architecture doc in the repo:\n- **README \"Getting Started\":** can a brand-new engineer follow it end-to-end *today* and end with a working dev environment?\n- **Architecture diagrams:** do all named services still exist with the same names?\n- **Database schema diagrams:** do all tables and columns exist?\n- **Sequence diagrams:** do the request paths still match the code?\n\n```bash\n# Find architecture docs and check last-modified vs the code they describe\nfind . \\( -name 'ARCHITECTURE.md' -o -name 'README.md' -o -path '*/docs/*' \\) | \\\n while read doc; do\n DOC_DATE=$(git log -1 --format=%ct -- \"$doc\")\n SRC_DATE=$(git log -1 --format=%ct -- src/ app/)\n if [ $((SRC_DATE - DOC_DATE)) -gt 7776000 ]; then # 90 days\n echo \"STALE: $doc (last touched $(date -r $DOC_DATE +%Y-%m-%d))\"\n fi\n done\n```\n\n## Incorrect\n\n```markdown\n\u003c!-- ❌ README from 18 months ago -->\n# CheckoutService\n\n## Architecture\n- Node.js 14 (LTS)\n- MongoDB for orders\n- Redis for sessions\n- Stripe for payments\n\n## Getting Started\n1. Clone the repo\n2. Copy `.env.example` to `.env` and fill in the DB credentials\n3. `composer install && npm install`\n4. `php artisan migrate && php artisan serve`\n\n## Services\nThe service is split into:\n- /api — REST endpoints\n- /jobs — background workers\n- /web — admin dashboard\n```\n\nReality:\n- Migrated to Node 22 last year\n- Moved from MongoDB to MySQL 8 months ago\n- Stripe was replaced with Adyen\n- `.env.example` no longer exists; uses Doppler\n- `/web` was extracted to its own repo 6 months ago\n\n**Problems:**\n- New engineer spends 2 days debugging \"why won't MongoDB connect\"\n- Incident responder pages the wrong on-call (the old `/web` team)\n- Architectural decisions get made against an imaginary system\n\n## Correct\n\n```markdown\n\u003c!-- ✅ README that matches reality, with a freshness signal -->\n# CheckoutService\n\n_Last verified against running system: 2026-05-10_\n\n## Architecture\n- Node.js 22 (LTS)\n- MySQL 8.4 for orders\n- Redis for sessions\n- Adyen for payments (migrated from Stripe — see `docs/adr/0007-adyen.md`)\n\n## Getting Started\n1. `gh repo clone org/checkout && cd checkout`\n2. `cp .env.example .env`; fill in DB and Adyen credentials (or pull via `doppler setup`)\n3. `composer install && npm install`\n4. `php artisan migrate --seed && php artisan serve` (in another shell: `npm run dev`)\n\n## Services\n- `/api` — REST endpoints\n- `/jobs` — background workers\n- (Admin dashboard lives in `org/checkout-admin`)\n```\n\n**Benefits:**\n- Onboarding works end-to-end without hidden tribal knowledge\n- Incident responders trust the doc to find the right team\n- Each ADR captures the *why* for architectural changes\n\n## Remediation Strategy\n\n- **Effort:** S–M per doc\n- **When to pay down:**\n - Whenever a PR changes architecture (new service, new datastore, new dependency): **update the doc in the same PR**\n - Quarterly: \"doc walk\" — pair an engineer with a brand-new hire, follow the README, fix what breaks\n- **Tip:** add a `Last verified` date to architecture docs; treat doc-aging like dep-aging\n\nReference: [Architecture Decision Records (ADR)](https://adr.github.io/) · [The Diátaxis Framework](https://diataxis.fr/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3755,"content_sha256":"60c3bc615dfe90508e0261925f0c2e81cdc559a38464ddcfb8269b1468871f99"},{"filename":"rules/docs-stale-comments.md","content":"---\ntitle: Stale Comments\nimpact: MEDIUM\nimpactDescription: \"Wrong information is worse than no information\"\ntags: documentation, comments, maintenance\n---\n\n## Stale Comments\n\n**Impact: MEDIUM (Wrong information is worse than no information)**\n\nA comment that contradicts the code it describes actively misleads readers. They either trust the comment and write a bug, or they distrust all comments and miss the load-bearing ones. Stale comments are negative-value documentation.\n\n## How to Detect\n\nThere is no perfect tool — stale comments are read-and-judge. Useful starting points:\n\n```bash\n# Find comments referencing functions, classes, files that no longer exist\ngrep -rEn '@see |@deprecated |TODO\\\\(|see also' src/ | \\\n while IFS= read -r line; do\n REF=$(echo \"$line\" | grep -oE '[A-Z][a-zA-Z]+::[a-zA-Z]+|[a-z_]+\\\\.[a-z]+')\n [ -n \"$REF\" ] && ! grep -rq \"$REF\" src/ && echo \"STALE: $line\"\n done\n\n# Find comments referencing removed parameters\n# (compare comment params with current function signature)\n```\n\nHotspot: comments next to code that has been git-touched more recently than the comment.\n\n## Incorrect\n\n```typescript\n// ❌ Comment lies about behaviour\n/**\n * Returns the user's full name in \"Last, First\" format.\n */\nfunction displayName(user: User): string {\n return `${user.firstName} ${user.lastName}`; // actually \"First Last\"\n}\n\n// ❌ Comment references removed parameter\n/**\n * @param userId - the user to load\n * @param includeArchived - whether to include archived records\n */\nfunction loadUser(userId: string) { // includeArchived removed last year\n return db.users.findOne({ id: userId, archived: false });\n}\n\n// ❌ \"Temporary\" workaround that became permanent\n// HACK: workaround for Stripe API bug — remove after their 2022 fix\nconst tax = Math.round(subtotal * 0.06 * 100) / 100;\n```\n\n**Problems:**\n- A reader follows the comment, writes integration code expecting \"Last, First\", ships a bug\n- IDE autocomplete picks up the stale `@param`, suggesting a parameter that no longer exists\n- \"Temporary\" workaround now load-bearing; nobody dares remove it\n\n## Correct\n\n```typescript\n// ✅ Comment matches reality, or is deleted\n/**\n * Returns \"First Last\" — used in user-facing greetings.\n */\nfunction displayName(user: User): string {\n return `${user.firstName} ${user.lastName}`;\n}\n\n// ✅ Parameter doc removed when parameter removed\nfunction loadUser(userId: string) {\n return db.users.findOne({ id: userId, archived: false });\n}\n\n// ✅ Either remove the workaround, or update the comment with current rationale\n// Stripe's 2022 fix shipped; this rounding is kept because our DB stores 4 decimal places\n// and accounting reports require 2-decimal-place reconciliation. (#TAX-431)\nconst tax = Math.round(subtotal * 0.06 * 100) / 100;\n```\n\n**Benefits:**\n- Comments become trusted again\n- IDE hints align with reality\n- \"Why does this exist\" is captured at the right level of fidelity\n\n## Remediation Strategy\n\n- **Effort:** S per comment\n- **When to pay down:** On every PR — if you touch code, read the surrounding comments and verify or delete. Reviewers should call out stale comments next to changed lines.\n- **Heuristic:** when in doubt, delete. Code documents *what*; the commit message documents *why*. A stale comment is rarely the right tool to keep.\n\nReference: [John Ousterhout — A Philosophy of Software Design, Ch. 13](https://web.stanford.edu/~ouster/cgi-bin/aposd.php)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3450,"content_sha256":"0bd82b95ae89150bcf81b32a88f3bc33971075039a83078422778548e32ea58f"},{"filename":"rules/docs-undocumented-api.md","content":"---\ntitle: Undocumented Public APIs\nimpact: MEDIUM\nimpactDescription: \"Consumers reverse-engineer behaviour; breaking changes blindside everyone\"\ntags: documentation, api, public-interface\n---\n\n## Undocumented Public APIs\n\n**Impact: MEDIUM (Consumers reverse-engineer behaviour; breaking changes blindside everyone)**\n\nA public API without documentation forces consumers to read source or guess. Once they guess wrong and ship, that \"wrong guess\" becomes the de facto contract — you can no longer change the implementation without breaking them.\n\n## How to Detect\n\n```bash\n# TypeScript: public exports without TSDoc\n# Requires eslint-plugin-jsdoc installed and registered in your eslint config; the\n# rule below is a config-file rule (not a CLI one-liner override):\n# { \"plugins\": [\"jsdoc\"], \"rules\": { \"jsdoc/require-jsdoc\": [\"error\", {\"publicOnly\": true}] } }\n\n# PHP: public methods on public classes without docblocks\n# PHPStan does NOT natively flag missing docblocks — use PHP_CodeSniffer with a\n# docblock-aware standard (e.g. Squiz.Commenting.FunctionComment), or phpDocumentor's\n# validator. PHPStan can still catch missing parameter/return *types* via higher levels.\nvendor/bin/phpcs --standard=Squiz --sniffs=Squiz.Commenting.FunctionComment app/\n\n# OpenAPI / REST APIs\n# Compare routes/controllers to swagger.json / openapi.yaml — drift is debt\n```\n\n## Incorrect\n\n```typescript\n// ❌ Exported function with non-obvious behaviour and no docs\nexport function calculateRefund(order: Order, items: Item[]): RefundResult {\n // ... 80 lines including special cases for partial refunds,\n // restocking fees, expired return windows, original-payment-method routing,\n // and tax recalculation under different jurisdictions\n}\n```\n\nQuestions a consumer can't answer without reading the body:\n- Does `items` default to all items if empty? Or refund nothing?\n- Are restocking fees deducted? How are they configured?\n- What happens if the original payment method is invalidated?\n- Are taxes recalculated, or refunded proportionally?\n\n## Correct\n\n```typescript\n/**\n * Calculate a refund for some or all items in an order.\n *\n * - If `items` is empty, refunds the entire order.\n * - Restocking fees (configured per category) are deducted from the refund amount.\n * - Taxes are recalculated based on the remaining order subtotal (not refunded proportionally).\n * - Refunds route back to the original payment method; if invalidated, returns\n * `RefundResult.requiresAlternativeMethod = true` and the caller must collect new instrument.\n * - Refunds are not allowed past the return window (`order.returnsCloseAt`).\n *\n * @throws RefundWindowClosed — if `order.returnsCloseAt` has passed\n * @throws PartialRefundNotAllowed — if the order's policy disallows partial returns\n *\n * @see docs/refunds.md for business rules and worked examples.\n */\nexport function calculateRefund(order: Order, items: Item[]): RefundResult {\n // ...\n}\n```\n\n**Benefits:**\n- Consumers depend on documented contract, not observed behaviour\n- You can change implementation freely as long as docs hold\n- Edge cases are surfaced — usually catching a bug or missing test in the process\n\n## Remediation Strategy\n\n- **Effort:** S per public function (write the doc; if it's hard to write, the function is doing too much)\n- **When to pay down:**\n - **Now:** add CI rule (`jsdoc/require-jsdoc`, PHPStan public-API check) so new APIs ship with docs\n - **Gradually:** document existing APIs as you touch them. Don't try to backfill all at once.\n- **Side benefit:** writing the doc is the cheapest way to find an API with too many concerns. If you need 3 paragraphs to describe a single function, split it.\n\nReference: [TSDoc](https://tsdoc.org/) · [PHPDoc](https://docs.phpdoc.org/) · [OpenAPI Specification](https://swagger.io/specification/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3828,"content_sha256":"0671e07063a0545023a5f8df7472c11500f711b8a61762dd97900c1814664304"},{"filename":"rules/infra-build-warnings.md","content":"---\ntitle: Ignored Build and Lint Warnings\nimpact: MEDIUM\nimpactDescription: \"Warning noise hides real failures and trains the team to ignore output\"\ntags: build, lint, warnings, ci\n---\n\n## Ignored Build and Lint Warnings\n\n**Impact: MEDIUM (Warning noise hides real failures and trains the team to ignore output)**\n\nA build that emits dozens of warnings teaches every engineer that warnings are normal. The day a critical warning appears (a deprecation, a type-narrowing issue, a circular import), nobody sees it. Clean output is a precondition for noticing problems.\n\n## How to Detect\n\n```bash\n# Capture and count warnings from build/test/lint\nnpm run build 2>&1 | grep -ciE 'warning|deprecat'\nnpx tsc --noEmit 2>&1 | wc -l\nnpx eslint . 2>&1 | grep -c 'warning'\n\n# PHP\nvendor/bin/phpstan analyse --no-progress\nvendor/bin/phpcs --report=summary\n\n# Webpack / Vite\n# Look at the bundler output for \"compiled with N warnings\"\n```\n\nThreshold: **zero warnings tolerated**. Either fix or explicitly suppress with a comment explaining why.\n\n## Incorrect\n\n```bash\n# ❌ Build \"passes\" but emits a wall of warnings\n$ npm run build\n[tsc] src/orders/index.ts(42,5): warning TS6133: 'unused' is declared but never used.\n[tsc] src/orders/index.ts(55,3): warning TS2532: Object is possibly undefined.\n[eslint] src/payment/stripe.ts:18:1 warning no-explicit-any\n[eslint] src/payment/stripe.ts:34:5 warning react-hooks/exhaustive-deps\n[webpack] WARNING in ./node_modules/some-pkg/dist/index.js\n Critical dependency: the request of a dependency is an expression\n... 87 more warnings\nCompiled with 92 warnings.\n```\n\n**Problems:**\n- A new genuine warning (\"X will be removed in vNext\") buries in the noise\n- \"Compiled successfully\" with 92 warnings is a lie that erodes trust\n- New engineers conclude \"warnings don't matter here\"\n\n## Correct\n\n```bash\n$ npm run build\nCompiled successfully (0 warnings).\n```\n\nCI gates:\n\n```yaml\n- run: npx tsc --noEmit # fails on any type error\n- run: npx eslint . --max-warnings 0 # zero warnings\n- run: npm run build -- --no-warnings # bundler warnings → errors\n```\n\nWhen a warning genuinely must be suppressed:\n\n```typescript\n// ✅ Targeted suppression with reason\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Stripe SDK types are too narrow; tracked in #2034\nfunction configureStripe(opts: any) { /* ... */ }\n```\n\n**Benefits:**\n- Build output is signal — every line means something\n- Reviews can ask \"does this PR add any new warning?\" → easy answer\n- Genuine deprecations and CVE-related warnings are noticed immediately\n\n## Remediation Strategy\n\n- **Effort:** S–M (most warnings are mechanical fixes; a few require small refactors)\n- **When to pay down:**\n 1. **Snapshot the current count** in CI: `--max-warnings $CURRENT_COUNT`\n 2. **Ratchet down** — every PR can only equal or decrease the count\n 3. Reach zero, then flip to `--max-warnings 0`\n- **Anti-pattern:** disabling the lint rule entirely instead of fixing the underlying issues. The rule exists for a reason; suppress with context, don't disable globally.\n\nReference: [ESLint — Disabling Rules](https://eslint.org/docs/latest/use/configure/rules#disabling-rules) · [TypeScript Strict Mode](https://www.typescriptlang.org/tsconfig#strict)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3313,"content_sha256":"51073c2a7cd6de7d2e3ddeaed41a22b4171807ac032316d3f66836923e360d35"},{"filename":"rules/infra-deprecated-apis.md","content":"---\ntitle: Deprecated Framework APIs\nimpact: HIGH\nimpactDescription: \"Deprecations become breakages on the next major upgrade\"\ntags: framework, deprecations, upgrade\n---\n\n## Deprecated Framework APIs\n\n**Impact: HIGH (Deprecations become breakages on the next major upgrade)**\n\nEvery deprecation warning is the framework telling you exactly what will break in the next major release. Ignoring them doesn't postpone the cost — it concentrates it on whoever does the upgrade, who is now blocked by hundreds of issues at once.\n\n## How to Detect\n\n```bash\n# Laravel\nphp artisan about # framework version\ngrep -rn '@deprecated' vendor/laravel/ # known deprecated APIs\n# Run tests with deprecation reporting on:\nAPP_ENV=testing E_DEPRECATED=on php artisan test\n\n# React\n# React DevTools logs deprecations in console\n# eslint-plugin-react flags many deprecated APIs:\nnpx eslint . --rule 'react/no-deprecated: error'\n\n# Node\nnode --pending-deprecation app.js # surface upcoming deprecations\nnode --throw-deprecation app.js # treat them as errors in CI\n\n# Generic: parse build/test logs\nnpm test 2>&1 | grep -iE 'deprecat|warning' | sort -u\n```\n\n## Incorrect\n\n```php\n// ❌ Laravel deprecated API usage left in code\nuse Illuminate\\Support\\Facades\\Input; // deprecated in Laravel 5.x; removed in 6.0\n$name = Input::get('name');\n\n// ❌ Method signature that's been overhauled\npublic function failed(Exception $e) // Laravel 10+ expects ?Throwable\n{\n // ...\n}\n\n// ❌ Deprecated React lifecycle method\nclass UserList extends React.Component {\n componentWillMount() { // deprecated since React 16.3\n this.fetch();\n }\n}\n```\n\n**Problems:**\n- Next Laravel major: `Input` gone; `failed(Throwable)` enforced — both break at once\n- React 18 strict mode logs warnings; React 19 may remove them entirely\n- Deprecation log noise hides genuine warnings\n\n## Correct\n\n```php\n// ✅ Use current APIs\n$name = request()->input('name');\n\npublic function failed(?Throwable $e): void\n{\n // ...\n}\n```\n\n```typescript\n// ✅ Hooks or current lifecycle methods\nfunction UserList() {\n useEffect(() => { fetch(); }, []);\n}\n```\n\nAdd a CI gate:\n\n```yaml\n- name: No deprecation warnings\n run: |\n OUTPUT=$(npm test 2>&1 || true)\n echo \"$OUTPUT\" | grep -qiE 'deprecat' && { echo \"Deprecations found\"; exit 1; }\n exit 0\n```\n\n**Benefits:**\n- Next major upgrade is hours, not weeks\n- Test output is signal again\n- Framework authors' migration notes apply directly without rediscovery\n\n## Remediation Strategy\n\n- **Effort:** S–M per deprecation (the migration path is usually documented by the framework)\n- **When to pay down:**\n - **Read the framework's upgrade guide** when they ship a deprecation — pay down what you can immediately\n - **CI gate:** zero new deprecation warnings allowed; old ones whittled down per sprint\n- **Order of operations:** fix deprecations *before* attempting the major upgrade — never together\n\nReference: [Laravel Upgrade Guide](https://laravel.com/docs/upgrade) · [React Strict Mode](https://react.dev/reference/react/StrictMode) · [Node Deprecations](https://nodejs.org/api/deprecations.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3198,"content_sha256":"02a494647d8695100dddbe24f71fd1ac9db9ffcf78c45a476b16623959fb74da"},{"filename":"rules/infra-monitoring-gaps.md","content":"---\ntitle: Observability and Monitoring Gaps\nimpact: HIGH\nimpactDescription: \"Debt you can't measure can't be paid down; incidents take hours longer to diagnose\"\ntags: observability, monitoring, alerts, slo\n---\n\n## Observability and Monitoring Gaps\n\n**Impact: HIGH (Debt you can't measure can't be paid down; incidents take hours longer to diagnose)**\n\nA system without structured logs, metrics, traces, and alerts is a system that breaks silently. The \"we'll add monitoring later\" decision is a tax paid during every incident, every customer-reported bug, and every capacity-planning conversation. Observability debt has the unique property of being most expensive to fix *during* an incident.\n\n## How to Detect\n\nFor each service in scope, audit:\n\n1. **Structured logs** — JSON, with correlation IDs, request IDs, user IDs (when appropriate)\n2. **Application metrics** — request rates, latency percentiles (p50/p95/p99), error rates, queue depths\n3. **Tracing** — distributed traces with span IDs spanning service boundaries\n4. **Alerts** — paging only on user-facing symptoms; non-paging for early signals\n5. **SLOs** — explicit service-level objectives with error budgets\n\n```bash\n# Find log statements still using non-structured output\ngrep -rEn 'echo |print(|print_r(|var_dump(|console\\.log\\(' app/ src/ | wc -l\n\n# Laravel: is logging configured to JSON channel?\ngrep -A5 \"'channels' =>\" config/logging.php\n\n# Are there any Prometheus / OpenTelemetry imports/integrations?\ngrep -rEn 'prometheus|opentelemetry|datadog|sentry|new-relic' composer.json package.json\n\n# Sentry / Bugsnag / equivalent error tracking installed?\ngrep -rE 'sentry|bugsnag|rollbar|honeybadger' composer.lock package-lock.json\n```\n\n## Incorrect\n\n```php\n// ❌ Observability black hole\n\n// 1. Print-style logging — unstructured, no correlation\npublic function pay(Order $order) {\n echo \"Processing payment for order \" . $order->id . \"\\n\";\n try {\n $result = $this->stripe->charge($order);\n } catch (Exception $e) {\n echo \"FAILED: \" . $e->getMessage() . \"\\n\"; // lost on next request\n }\n}\n\n// 2. No request IDs\n// 3. No latency metrics — \"the app feels slow\" is the only signal\n// 4. Pager rule: \"send a Slack message if a single 500 occurs\"\n// → wakes people up for every transient hiccup; signal-to-noise = 0\n// 5. No SLOs → no shared definition of \"the service is broken\"\n```\n\n**Problems:**\n- During an incident, you can't tell which user, which request, or which span failed\n- Capacity planning is guesswork (\"we think we can handle 2× traffic\")\n- Slow regressions (p95 creeping from 200ms → 800ms over a quarter) go unnoticed\n- On-call burnout from low-quality alerts\n\n## Correct\n\n```php\n// ✅ Structured logging with correlation ID + context\npublic function pay(Order $order): PaymentResult {\n $log = Log::withContext([\n 'order_id' => $order->id,\n 'user_id' => $order->user_id,\n 'request_id' => request()->header('X-Request-ID') ?? Str::uuid(),\n ]);\n\n $log->info('payment.start', ['amount' => $order->total]);\n $start = microtime(true);\n\n try {\n $result = $this->stripe->charge($order);\n $log->info('payment.success', [\n 'charge_id' => $result->id,\n 'duration_ms' => (int) ((microtime(true) - $start) * 1000),\n ]);\n return $result;\n } catch (\\Throwable $e) {\n $log->error('payment.failure', [\n 'exception' => $e::class,\n 'message' => $e->getMessage(),\n 'duration_ms' => (int) ((microtime(true) - $start) * 1000),\n ]);\n report($e); // → Sentry / Bugsnag\n throw $e;\n }\n}\n```\n\nMinimum viable observability stack to install:\n- **Logs:** JSON channel + central log store (CloudWatch, Loki, Datadog Logs)\n- **Errors:** Sentry / Bugsnag / Rollbar\n- **Metrics + tracing:** OpenTelemetry SDK → vendor or self-hosted (Grafana stack, Datadog, Honeycomb)\n- **Uptime:** external prober (UptimeRobot, Pingdom, Datadog Synthetics)\n- **Alerts:** routed to a real pager system (PagerDuty, Opsgenie); thresholds based on SLO burn rate, not single events\n\nDefine SLOs explicitly (illustrative — real Sloth uses `version: prometheus/v1` plus an\n`sli.events` block with `error_query`/`total_query`; Pyrra ships as a Kubernetes CRD —\nconsult each tool's schema before adopting):\n\n```yaml\n# slo.yaml — illustrative shape only\nservice: checkout\nslos:\n - name: availability\n objective: 99.9%\n sli: error_rate \u003c 1% over 28d\n - name: latency\n objective: 99% of requests \u003c 500ms over 28d\n```\n\n**Benefits:**\n- Incidents resolve faster (mean MTTR drops 50%+ with traces)\n- Slow regressions are caught when they're small\n- Alerts wake people up only for user-facing problems\n- Engineering and product share a quantitative definition of \"the service is healthy\"\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — add Sentry + JSON logging to one service\n - **M** — add OpenTelemetry + APM dashboards\n - **L** — full SLO program (objectives, alerts on burn rate, error budgets, blameless postmortems)\n- **When to pay down:**\n - **NOW:** any production service without an error-tracker (Sentry-class)\n - **NOW:** any service whose only outage signal is \"a user complained\"\n - **This quarter:** structured logging + request IDs + p95 latency dashboards\n - **Then:** SLO program; alerts based on burn rates, not raw thresholds\n\n**Anti-patterns:**\n- **Alert on everything** — alarms drown signal; only page on customer-impacting symptoms\n- **Logs-only observability** — logs are expensive to query at scale; metrics + traces complement them\n- **No retention policy** — logs at 1TB/day with infinite retention becomes its own debt\n- **Tool sprawl** — one logs vendor, one APM, one error tracker is plenty\n\nReference: [Google SRE — Monitoring Distributed Systems](https://sre.google/sre-book/monitoring-distributed-systems/) · [OpenTelemetry](https://opentelemetry.io/) · [Sloth — SLO Generator](https://sloth.dev/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6069,"content_sha256":"d77d1bc04c7a26feb418cdee545de37ea526d521afde17ad2c278c7221ae38a0"},{"filename":"rules/infra-runtime-versions.md","content":"---\ntitle: End-of-Life Runtime Versions\nimpact: CRITICAL\nimpactDescription: \"EOL runtimes stop receiving security patches\"\ntags: runtime, eol, security\n---\n\n## End-of-Life Runtime Versions\n\n**Impact: CRITICAL (EOL runtimes stop receiving security patches)**\n\nOnce a language runtime hits EOL, no more CVE fixes ship — the next disclosed vulnerability is unpatched and forever yours. EOL deadlines are public, hard, and immovable; treating them as \"we'll deal with it later\" is treating a deadline as optional.\n\n## How to Detect\n\n```bash\n# Check installed versions\nnode --version\nphp --version\n\n# Check declared versions\ncat .nvmrc .node-version 2>/dev/null\ngrep -E '\"engines\"|\"node\":' package.json\ngrep -E '\"php\"' composer.json\n\n# Server-side check (Forge / Vapor / shared hosting)\nssh user@server 'php -v && node -v'\n\n# Check against EOL dates\n# - https://endoflife.date/nodejs\n# - https://endoflife.date/php\n# - https://www.php.net/supported-versions.php\n```\n\n| Runtime | EOL Pattern |\n|---|---|\n| Node.js | Every 6 months; LTS for 30 months |\n| PHP | 2 years active + 2 years security (since 2024) |\n\nThreshold: **flag any version \u003c 6 months from EOL** as P1, **EOL today** as P0.\n\n## Incorrect\n\n```json\n// ❌ package.json declares EOL Node\n{\n \"engines\": { \"node\": \"14.x\" }\n}\n```\n\n```json\n// ❌ composer.json requires EOL PHP\n{\n \"require\": { \"php\": \"^7.4\" } // PHP 7.4 EOL: 2022-11-28\n}\n```\n\n```\n// ❌ .nvmrc still pins to Node 14\n14.21.3\n```\n\n**Problems:**\n- Any disclosed CVE in Node 14 / PHP 7.4 stays exploitable indefinitely\n- Modern dependencies start dropping support — package upgrades become impossible\n- Forge / Vapor / shared-hosting providers eventually remove EOL versions entirely → forced emergency migration\n\n## Correct\n\n```json\n// ✅ package.json — Node 22 LTS\n{\n \"engines\": { \"node\": \">=22 \u003c23\" }\n}\n```\n\n```json\n// ✅ composer.json — PHP 8.3 (LTS, supported through 2027-12)\n{\n \"require\": { \"php\": \"^8.3\" }\n}\n```\n\n```\n// ✅ .nvmrc\n22.6.0\n```\n\nAutomate the watch:\n\n```yaml\n# CI step\n- name: Check runtime EOL\n run: |\n NODE_MAJOR=$(node --version | sed 's/v\\([0-9]*\\).*/\\1/')\n EOL_NODE=20 # update annually\n test $NODE_MAJOR -ge $EOL_NODE || { echo \"Node $NODE_MAJOR is EOL\"; exit 1; }\n```\n\n**Benefits:**\n- Security patches keep arriving for free\n- Modern dep ecosystem stays compatible\n- No emergency \"runtime EOL was last week\" migration\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — minor runtime bump within supported major\n - **M** — major bump (Node 18 → 22, PHP 8.1 → 8.3)\n - **L** — multiple majors at once (Node 14 → 22) — break into steps\n- **When to pay down:**\n - **6 months before EOL:** start the upgrade\n - **3 months before EOL:** be done\n - Schedule a calendar reminder when you upgrade — the next deadline is already on the clock\n\nReference: [endoflife.date](https://endoflife.date/) · [Node Release Schedule](https://nodejs.org/en/about/previous-releases) · [PHP Supported Versions](https://www.php.net/supported-versions.php)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3031,"content_sha256":"6617914cbc4953e4399c8486f26b0d77aaf432e858b26bf8cb940ef2493db085"},{"filename":"rules/infra-secrets-management.md","content":"---\ntitle: Insecure Secrets Management\nimpact: HIGH\nimpactDescription: \".env files leak, plain env vars surface in logs, no rotation = compounding risk\"\ntags: security, secrets, configuration\n---\n\n## Insecure Secrets Management\n\n**Impact: HIGH (.env files leak, plain env vars surface in logs, no rotation = compounding risk)**\n\nEven when secrets stay out of source control, ad-hoc handling — `.env` files copied between machines, plain env vars echoed into CI logs, indefinite credential lifetimes — leaves a long tail of leakage paths. This rule covers everything *outside* the repo: secret-manager adoption, rotation, scope, and audit.\n\n## How to Detect\n\nFor each environment (dev, staging, prod), check:\n\n1. Where are secrets stored? (encrypted secret manager vs. plain files vs. shell history)\n2. Are they versioned and audit-logged?\n3. Is rotation automated or manual?\n4. Are CI/CD secrets scoped per repo/job, or org-wide?\n5. Are workload identities (OIDC / IAM roles) used instead of long-lived keys?\n\n```bash\n# Audit any .env files committed historically\ngitleaks git # full history scan\ngit log --all --diff-filter=A --name-only | grep -E '\\\\.env

Technical Debt Technical debt audit and prioritization framework for PHP/Laravel (MySQL) and Node/TypeScript/React projects. Contains 42 rules across 10 categories covering code, security, design, dependency, test, performance, data, documentation, infrastructure, and process debt. Produces a ranked ledger (effort × impact) so teams know what to fix first , not just what's broken. Supports both coding reference and audit mode with PASS/FAIL/N/A output. Metadata - Version: 1.0.0 - Scope: PHP / Laravel (MySQL) + Node / TypeScript / React - Rule Count: 42 rules across 10 categories - License: MI…

\n\n# CI: scan for echoes of secrets in logs (common when devs add `set -x`)\ngrep -E 'API_KEY=|TOKEN=|PASSWORD=' .github/workflows/*.yml\n\n# Cloud: list long-lived access keys (AWS example)\naws iam list-access-keys --user-name \u003ciam-user> # any keys > 90 days old?\n```\n\n## Incorrect\n\n```bash\n# ❌ Anti-patterns\n\n# 1. Plain .env file committed in history (even if removed later)\n$ git log -p --all -- '.env'\n... (compromised)\n\n# 2. CI workflow echoing secrets via debugging\n- run: echo \"AWS_KEY=$AWS_ACCESS_KEY_ID\" # ends up in build logs\n\n# 3. Single shared IAM access key used by all CI jobs, 4 years old, never rotated\n$ aws iam list-access-keys --user-name ci-deploy\nCreateDate: 2022-03-15 # 4 years; full admin permissions\n\n# 4. Slack/Notion link sharing for \"DB credentials\" — anyone with the link sees the password\n```\n\n**Problems:**\n- A leaked CI log exposes the entire production environment\n- Long-lived credentials with broad scope = compromise = full account access\n- No audit trail of who accessed what credential when\n\n## Correct\n\n```yaml\n# ✅ GitHub Actions: OIDC to AWS — no long-lived keys needed\npermissions:\n id-token: write\n contents: read\njobs:\n deploy:\n runs-on: ubuntu-latest\n steps:\n - uses: aws-actions/configure-aws-credentials@v6\n with:\n role-to-assume: arn:aws:iam::123456789012:role/deploy-role\n aws-region: ap-southeast-1\n # AWS calls now use short-lived session tokens scoped to this workflow\n```\n\n```bash\n# ✅ Production secrets in a dedicated manager (one of these):\n# - HashiCorp Vault (self-hosted, comprehensive)\n# - AWS Secrets Manager (with rotation Lambdas)\n# - GCP Secret Manager\n# - Doppler / 1Password Secrets Automation (SaaS)\n#\n# Apps read at startup via SDK; rotation is automated.\n\n# ✅ Local dev: .env files exist but are gitignored and contain dev-only credentials\necho '.env' >> .gitignore\n```\n\n**Benefits:**\n- No long-lived credentials to leak; access is scoped to job + duration\n- Audit logs record every secret access\n- Rotation is automatic and predictable\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — gitignore .env, install pre-commit gitleaks hook\n - **M** — migrate one application from env-file → secret manager\n - **L** — full org migration to OIDC + secret manager + rotation\n- **When to pay down:**\n - **NOW:** any committed .env, any 90-day-old long-lived key, any secrets in plain CI logs\n - **This quarter:** migrate CI/CD to OIDC where the cloud provider supports it (AWS, GCP, Azure all do)\n - **Then:** automated rotation for all production secrets, with audit alerts on access spikes\n\n**Hierarchy of secret handling, from worst to best:**\n1. Hardcoded in repo (CRITICAL — must rotate immediately)\n2. Untracked `.env` file emailed/Slacked around (high leak risk)\n3. Long-lived credentials in CI/CD secrets store\n4. Short-lived credentials issued via OIDC workload identity (best)\n\nReference: [GitHub Actions OIDC](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments) · [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/) · [HashiCorp Vault](https://developer.hashicorp.com/vault) · [Doppler](https://docs.doppler.com/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4423,"content_sha256":"caab862a51854f0b3ce7a178614277c35069251a1133a2f4e22a14cf533eb5ca"},{"filename":"rules/perf-bundle-bloat.md","content":"---\ntitle: Frontend Bundle Bloat\nimpact: HIGH\nimpactDescription: \"Bundle size directly drives bounce rate, INP, and conversion\"\ntags: performance, frontend, bundle-size\n---\n\n## Frontend Bundle Bloat\n\n**Impact: HIGH (Bundle size directly drives bounce rate, INP, and conversion)**\n\nEvery kilobyte the browser must parse and execute slows down first paint, interaction-readiness, and on mobile networks, page abandonment. Bundle bloat creeps in invisibly — a moment-import here, a `lodash` import there, an unused route bundled with the entry point — and the team only notices when Lighthouse drops a grade.\n\n## How to Detect\n\n```bash\n# Vite\nnpx vite-bundle-visualizer\n\n# Webpack\nnpx webpack-bundle-analyzer dist/stats.json\n\n# Any bundler — source map visualization\nnpx source-map-explorer 'dist/**/*.js'\n\n# CI bundle-size budget (size-limit)\nnpm install --save-dev size-limit @size-limit/preset-app\n# package.json:\n# \"size-limit\": [{ \"path\": \"dist/index.js\", \"limit\": \"200 KB\" }]\nnpx size-limit\n```\n\nBudget targets (gzipped, mobile-first):\n- **Initial bundle:** \u003c 200 KB\n- **Route bundles:** \u003c 100 KB\n- **Single dep:** anything > 50 KB deserves justification\n\n## Incorrect\n\n```typescript\n// ❌ Default import → ships the entire library\nimport _ from 'lodash'; // ~70 KB min / ~24 KB gzip\nimport moment from 'moment'; // ~290 KB min / ~70 KB gzip with locales\n\nconst debounced = _.debounce(handler, 200);\nconst formatted = moment().format('YYYY-MM-DD');\n\n// ❌ No code splitting — admin pages bundled with the public site\nimport AdminDashboard from './admin/Dashboard';\nimport AdminUsers from './admin/Users';\n// ... all eagerly imported in the entry file\n```\n\n**Problems:**\n- Importing all of lodash to use `debounce` is like buying a truck to carry one tomato\n- Moment with all locales ships ~70 KB gzip of timezone data nobody uses\n- Admin code bundled with the public site triples the entry-bundle for 99% of visitors who never visit `/admin`\n\n## Correct\n\n```typescript\n// ✅ Named imports / smaller libraries\nimport { debounce } from 'lodash-es'; // tree-shaken via ESM\nimport { format } from 'date-fns'; // ~10 KB gzip for the single `format` import\n\nconst debounced = debounce(handler, 200);\nconst formatted = format(new Date(), 'yyyy-MM-dd');\n\n// ✅ Route-level code splitting\nconst AdminDashboard = lazy(() => import('./admin/Dashboard'));\nconst AdminUsers = lazy(() => import('./admin/Users'));\n\n// In your router:\n\u003cRoute path=\"/admin\" element={\u003cSuspense fallback={\u003cSpinner />}>\u003cAdminDashboard />\u003c/Suspense>} />\n```\n\nAdd a CI guard:\n\n```yaml\n- name: Bundle-size budget\n run: npx size-limit\n# Fails the build if any tracked bundle exceeds its budget\n```\n\n**Benefits:**\n- Initial bundle shrinks dramatically; LCP and INP both improve\n- Admin code only loads for admin users\n- A regression (someone adds `import * as everything`) fails CI\n\n## Remediation Strategy\n\n- **Effort:** S–M per dep (swap imports, lazy-load routes)\n- **When to pay down:**\n - **First:** run the bundle visualizer and target the top 5 contributors\n - **Then:** install a bundle-size budget in CI to prevent regression\n - **Ongoing:** every PR that adds a dep > 20KB should be reviewed for alternatives\n- **Common wins:**\n - `moment` → `date-fns` or `dayjs` (–80–90% size)\n - `lodash` → `lodash-es` + named imports, or native equivalents\n - Route-level code splitting (10× reduction on admin-heavy apps)\n - Drop polyfills for unsupported browsers\n - Replace heavy SVG icon sets with on-demand icon components\n\nReference: [web.dev — Apply Instant Loading](https://web.dev/articles/apply-instant-loading-with-prpl) · [BundlePhobia](https://bundlephobia.com/) · [size-limit](https://github.com/ai/size-limit)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3780,"content_sha256":"3a522929ef8a92f8e80170b5369474cbdf1048e7bc70afe4b918d0394633393f"},{"filename":"rules/perf-missing-pagination.md","content":"---\ntitle: Unbounded Result Sets\nimpact: HIGH\nimpactDescription: \"One large customer kills latency for everyone\"\ntags: performance, pagination, database\n---\n\n## Unbounded Result Sets\n\n**Impact: HIGH (One large customer kills latency for everyone)**\n\nAn endpoint that returns \"all\" records works fine until one customer has 100,000 of them. At that point, the request times out, exhausts memory, and (in shared-tenancy systems) takes down the database for everyone else. Unbounded result sets are time bombs proportional to your most successful customer.\n\n## How to Detect\n\n```bash\n# Find list endpoints / repository methods without LIMIT or pagination\ngrep -rEn '\\\\->all\\\\(|\\\\->get\\\\(\\\\)|findAll|fetchAll' --include='*.php' --include='*.ts'\n\n# Frontend: full-result-set components without \"load more\" / virtualization\ngrep -rEn 'map\\\\(|forEach\\\\(' --include='*.tsx' src/ | head -50 # review for unbounded lists\n\n# Check actual prod metrics: max rows returned by each endpoint over the last 30 days\n# (most APMs / DB monitors expose this)\n```\n\nHeuristic: any list endpoint **without an explicit limit** is debt. Any endpoint that returns objects with nested collections (orders → items, users → posts) is doubly so.\n\n## Incorrect\n\n```php\n// ❌ Returns every order ever placed by the customer\npublic function index(Customer $customer) {\n return OrderResource::collection($customer->orders); // 50,000 rows → 8MB response\n}\n\n// ❌ \"Export all users\" endpoint loads entire table into memory\npublic function export() {\n $users = User::all(); // OOM at scale\n return Excel::download(new UsersExport($users), 'users.xlsx');\n}\n```\n\n```typescript\n// ❌ Frontend asks for everything and filters client-side\nconst allTransactions = await fetch('/api/transactions').then(r => r.json());\nconst recent = allTransactions.filter(t => isThisMonth(t.date)); // 100,000 rows → 200,000 ms parse\n```\n\n## Correct\n\n```php\n// ✅ Cursor-based pagination (preferred for \"infinite scroll\" / large datasets)\npublic function index(Customer $customer, Request $request) {\n return OrderResource::collection(\n $customer->orders()->latest()->cursorPaginate(50)\n );\n}\n\n// ✅ Streaming export — never loads the full set into memory\npublic function export() {\n return response()->streamDownload(function () {\n User::query()->orderBy('id')->lazy()->each(function ($user) {\n // write one row at a time\n });\n }, 'users.csv');\n}\n```\n\n```typescript\n// ✅ Server filters; client requests only what it needs\nconst { data, nextCursor } = await fetch('/api/transactions?since=2026-05-01&limit=50')\n .then(r => r.json());\n```\n\n**Benefits:**\n- Memory and latency stay bounded regardless of customer size\n- Database returns fewer rows over the wire\n- Frontend can render results progressively\n\n## Remediation Strategy\n\n- **Effort:** S–M per endpoint (cursor pagination is more invasive than offset; both are mechanical)\n- **When to pay down:**\n - **NOW:** any endpoint where the result count is user-controlled and unbounded\n - **NOW:** any export endpoint loading the entire result set into memory\n - **Then:** add a max-result-count assertion in CI for list endpoints\n- **Pagination choice:**\n - **Cursor** — best for \"next page\" UX, large datasets, real-time-ish data (no skip cost)\n - **Offset** — easier to implement, OK for small/medium datasets; expensive at high page numbers\n - **Keyset** — similar to cursor but using a real column (id, created_at)\n\n**Tip:** when retrofitting pagination on a public API, support both old (full response) and new (paginated) shapes during a deprecation window, then remove the unbounded form.\n\nReference: [Laravel Pagination](https://laravel.com/docs/pagination) · [Slack — Cursor Pagination](https://docs.slack.dev/apis/web-api/pagination) · [Use the Index, Luke — Paging Through Results](https://use-the-index-luke.com/sql/partial-results/fetch-next-page)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3981,"content_sha256":"60d9ea94efba5416d96361940010d99841fb5f3e2ced08a3bed7439df36513f3"},{"filename":"rules/perf-n-plus-one.md","content":"---\ntitle: N+1 Query Patterns\nimpact: HIGH\nimpactDescription: \"Linear request → quadratic database load; latency scales with data, not traffic\"\ntags: performance, database, n-plus-one\n---\n\n## N+1 Query Patterns\n\n**Impact: HIGH (Linear request → quadratic database load; latency scales with data, not traffic)**\n\nAn N+1 happens when fetching a list of N records issues 1 query for the list + N follow-up queries for each row's relations. Latency looks fine in dev (small N) and explodes in prod (large N). N+1 is the single most common database debt in ORM-driven codebases.\n\n## How to Detect\n\n```bash\n# Laravel: Telescope panel \"Queries\" — sort by request, count per page\nphp artisan telescope:install\n\n# Laravel: detect N+1s in dev by failing on excessive queries\ncomposer require beyondcode/laravel-query-detector --dev\n\n# Node / TypeScript: enable query logging in your ORM (Prisma `log: ['query']`,\n# TypeORM `logging: 'all'`, Drizzle `logger: true`) and count queries per request.\n\n# Heuristic: > ~10 queries for a typical list endpoint is suspect.\n```\n\nPattern signal: query count grows with result count instead of staying constant.\n\n## Incorrect\n\n```php\n// ❌ Laravel: relation accessed inside a loop → one query per order\n$orders = Order::where('status', 'paid')->get(); // 1 query\n\nforeach ($orders as $order) {\n $customer = $order->customer; // 1 query each (N more)\n $shipping = $order->shipments; // 1 query each (another N)\n echo \"{$customer->name}: \" . $shipping->count();\n}\n// Total: 1 + 2N queries for 100 orders → 201 queries\n```\n\n```typescript\n// ❌ TypeORM equivalent\nconst users = await userRepo.find(); // 1 query\nfor (const user of users) {\n const orders = await user.orders; // N queries (lazy relation)\n console.log(user.email, orders.length);\n}\n```\n\n**Problems:**\n- A list endpoint that returns 200 rows takes 401 round-trips to the database\n- Latency is invisible at low traffic; suddenly catastrophic with real data\n- Database connection pool saturates; queue depth spikes for unrelated requests\n\n## Correct\n\n```php\n// ✅ Eager-load with `with()` — fixed query count regardless of N\n$orders = Order::where('status', 'paid')\n ->with(['customer', 'shipments'])\n ->get(); // 3 queries total: orders, customers, shipments\n\nforeach ($orders as $order) {\n echo \"{$order->customer->name}: \" . $order->shipments->count();\n}\n```\n\n```typescript\n// ✅ Specify relations in the find call\nconst users = await userRepo.find({ relations: { orders: true } });\n```\n\nFor large result sets, also paginate:\n\n```php\n$orders = Order::with(['customer', 'shipments'])\n ->where('status', 'paid')\n ->cursorPaginate(50); // memory + DB bounded\n```\n\n**Benefits:**\n- Query count becomes constant per request, regardless of result size\n- Latency is predictable in production\n- Connection pool stays healthy under load\n\n## Remediation Strategy\n\n- **Effort:** S per endpoint (add `with(...)` or equivalent eager-load)\n- **When to pay down:**\n - **NOW:** any endpoint that emits > 10× more queries than its result count\n - **Then:** add a CI test that asserts query counts on critical endpoints\n- **Detection workflow:**\n 1. Install Telescope / Bullet / Silk\n 2. Walk the top 10 most-hit endpoints in a representative env\n 3. Sort by query count per request — N+1s are at the top\n 4. Add `with(...)` and re-measure\n\n**Anti-pattern:** \"let's add a cache\" before fixing N+1. Caching hides the problem but doesn't fix it — the first request after expiry still does 201 queries.\n\nReference: [Laravel Eager Loading](https://laravel.com/docs/eloquent-relationships#eager-loading) · [beyondcode/laravel-query-detector](https://github.com/beyondcode/laravel-query-detector)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3808,"content_sha256":"893909a2e7d2f0b976d3e8967eda6ed6ea7721ddb884a94e3d3950f5fb2e1f6e"},{"filename":"rules/perf-no-caching.md","content":"---\ntitle: Missing Caching Opportunities\nimpact: HIGH\nimpactDescription: \"Repeated work on every request — predictable latency you're paying for indefinitely\"\ntags: performance, caching, redis, http-cache\n---\n\n## Missing Caching Opportunities\n\n**Impact: HIGH (Repeated work on every request — predictable latency you're paying for indefinitely)**\n\nCaching is the highest-ROI performance work for read-heavy systems: a single Redis lookup replaces a 200ms aggregation query, and an HTTP cache header lets the browser skip the round-trip entirely. Missing caching is invisible — the system \"works\" — but every page load pays the full computation cost.\n\n## How to Detect\n\nLook for these signals in any read-heavy path:\n\n1. **Expensive aggregations recomputed per request** (dashboard counters, reports, leaderboards)\n2. **External API calls without caching** (currency rates, geolocation, third-party catalogs)\n3. **Static-ish responses with no `Cache-Control` headers** (asset metadata, config, public lists)\n4. **Database queries that join 5+ tables to return the same shape repeatedly**\n5. **Same query fired by N concurrent requests, no request coalescing**\n\n```bash\n# Look at HTTP response headers — missing Cache-Control on static-ish endpoints\ncurl -sI https://your.app/api/categories | grep -iE 'cache-control|etag|last-modified'\n\n# Find APIs that re-compute the same shape across requests\n# (look in your APM for endpoints with consistently high mean latency + low variance)\n\n# Laravel — endpoints that hit expensive accessors / relations without remember()\ngrep -rEn '\\\\->withCount\\\\(|\\\\->withSum\\\\(' app/Http/Controllers/ | head\n```\n\n## Incorrect\n\n```php\n// ❌ Recomputed on every request — even though categories change ~once a week\npublic function categories() {\n $categories = Category::query()\n ->withCount('products')\n ->with(['parent', 'translations'])\n ->orderBy('sort_order')\n ->get();\n\n return CategoryResource::collection($categories);\n}\n\n// ❌ Third-party rate fetched per request — 200ms latency on every checkout\npublic function checkout(Order $order) {\n $rate = Http::get('https://api.fx.example.com/rates/USD-MYR')->json('rate');\n return ['total' => $order->total * $rate];\n}\n\n// ❌ No HTTP caching → browser revalidates on every navigation\nreturn response()->json($publicConfig);\n```\n\n**Problems:**\n- Database scans for `categories` happen N times per second across the whole fleet\n- Every checkout pays 200ms for an FX rate that updates hourly\n- Browser fetches `publicConfig` on every page load even though it changes daily\n\n## Correct\n\n```php\n// ✅ Tag-keyed Redis cache with explicit invalidation\n// Note: Cache::tags() only works with the `redis` or `memcached` driver.\n// On `file` / `database` / `array` stores it throws BadMethodCallException —\n// fall back to plain keys + manual invalidation on those drivers.\npublic function categories() {\n $categories = Cache::tags(['categories'])->remember(\n 'categories:tree:v2',\n now()->addHour(),\n fn () => Category::query()\n ->withCount('products')\n ->with(['parent', 'translations'])\n ->orderBy('sort_order')\n ->get(),\n );\n return CategoryResource::collection($categories);\n}\n\n// In Category::saved / Category::deleted listeners:\n// Cache::tags(['categories'])->flush();\n```\n\n```php\n// ✅ External API result cached for an hour\n$rate = Cache::remember('fx:USD-MYR', now()->addHour(), function () {\n return Http::get('https://api.fx.example.com/rates/USD-MYR')->json('rate');\n});\n```\n\n```php\n// ✅ HTTP cache headers for safely-cacheable public responses\nreturn response()->json($publicConfig)\n ->header('Cache-Control', 'public, max-age=3600, stale-while-revalidate=86400')\n ->header('ETag', md5(json_encode($publicConfig)));\n```\n\n**Benefits:**\n- Mean latency drops orders of magnitude for cacheable endpoints\n- Origin server, database, and third-party APIs all see reduced load\n- Browser short-circuits revalidation for unchanged resources\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — add `remember()` / HTTP `Cache-Control` to a single endpoint\n - **M** — design a cache-key scheme + invalidation strategy for a domain\n - **L** — distributed caching with proper invalidation across services\n- **When to pay down:**\n - **NOW:** any endpoint hitting an expensive query AND showing high traffic AND data is read-mostly\n - **Then:** look at top-10 requests by total time in APM; cache the easy wins first\n- **Layer order (cheapest first):**\n 1. HTTP cache headers (browser does the work)\n 2. CDN / edge cache (`Cache-Control: public, s-maxage=N`)\n 3. Application memory cache (per-process, fastest, no network)\n 4. Redis / Memcached (shared across processes, sub-ms)\n 5. Database query cache / materialized views (last resort)\n\n**Anti-patterns:**\n- **Caching everything by default** — cache invalidation is hard; cache only what hurts\n- **TTL-only invalidation when freshness matters** — combine with event-based busts (`Cache::flush` on writes)\n- **Caching personalized data with a public key** — leaks one user's data to another\n- **Caching error responses indefinitely** — always exclude 4xx/5xx from caches\n\nReference: [Laravel — Cache](https://laravel.com/docs/cache) · [MDN — HTTP Caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching) · [RFC 9111 — HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5486,"content_sha256":"c019e02098375e3eef3ccd0e1a50a6a2d9a0b87c5c747442d60779779f039d1f"},{"filename":"rules/process-debt-tracking.md","content":"---\ntitle: Untracked Technical Debt\nimpact: MEDIUM\nimpactDescription: \"Debt that isn't tracked can't be prioritized or budgeted\"\ntags: debt-register, process, prioritization\n---\n\n## Untracked Technical Debt\n\n**Impact: MEDIUM (Debt that isn't tracked can't be prioritized or budgeted)**\n\nDebt that exists only in engineers' heads (or in scattered TODOs and Slack messages) competes with product features by stealth — engineers slow down, but leadership can't see why. A debt register makes the cost visible, so it can be funded properly instead of being paid in invisible overtime.\n\n## How to Detect\n\nCheck the project for an explicit debt-tracking mechanism:\n\n- **Issue tracker label** (e.g., GitHub `tech-debt`, Linear `Debt` project)\n- **Dedicated debt board** (Trello, Notion, or a `DEBT.md` in the repo)\n- **ADRs** for major debt accumulation decisions\n- **Quarterly debt-paydown allocation** (e.g., 20% capacity)\n\n```bash\n# Quick repo check\nls DEBT.md TECH_DEBT.md docs/debt/ 2>/dev/null\ngh issue list --label \"tech-debt\" --state=all\ngrep -rE 'tech.?debt' .github/ docs/\n```\n\nIf none of these exist, the project has **process debt about technical debt** — meta-debt.\n\n## Incorrect\n\n```\n❌ Debt situation in a typical repo:\n- 47 TODO comments, no tickets\n- 12 known \"we should rewrite that\" conversations on Slack\n- 3 engineers each have a mental list of \"things that scare me\"\n- Last debt-paydown sprint: never\n- When asked \"what's our biggest debt?\": four engineers give four different answers\n```\n\n**Problems:**\n- Debt accrues invisibly — leadership only sees velocity drop\n- Same problem gets discovered repeatedly by new hires\n- No paydown budget because no list to justify the budget\n\n## Correct\n\nPick **one** lightweight mechanism and use it consistently:\n\n```markdown\n\u003c!-- ✅ Option A: DEBT.md in the repo (low ceremony) -->\n# Technical Debt Register\n\n| # | Category | Item | Effort | Impact | Owner | Linked |\n|---|----------|------|--------|--------|-------|--------|\n| 1 | deps | guzzle 6.x (5y behind, blocks PHP 8.4) | M | HIGH | @asyraf | #1842 |\n| 2 | code | OrderService god class (820 LoC) | L | HIGH | @team-orders | #1844 |\n| 3 | test | Checkout flow has no integration test | M | CRITICAL | @asyraf | #1845 |\n\nLast reviewed: 2026-05-01. Next review: 2026-08-01.\n```\n\n```yaml\n# ✅ Option B: GitHub label + saved search\n# Label every debt-related issue with 'tech-debt'\n# Saved search: https://github.com/org/repo/issues?q=is%3Aopen+label%3Atech-debt+sort%3Areactions-%2B1-desc\n```\n\n**Allocate budget:** dedicate a consistent fraction of every sprint to debt paydown (commonly 15–25%). Without an allocation, debt always loses to features.\n\n**Benefits:**\n- Debt is visible to product and leadership\n- Prioritization is principled (effort × impact), not loudest-engineer\n- Paydown velocity is measurable\n\n## Remediation Strategy\n\n- **Effort:** S to start (one register + a label), ongoing M to maintain\n- **When to pay down:**\n - **Now:** start the register with the top 10 items from your last audit\n - **Quarterly:** review and reprioritize; close completed entries\n - **Per PR:** if a PR introduces accepted debt (shortcut, missing test), add a register entry as part of merge\n\n**Anti-patterns:**\n- Register that nobody owns → goes stale, becomes worse than nothing\n- Register with 200 entries → useless; cap at top 20–30 active items\n- Debt sprints disconnected from a register → effort goes to whatever's annoying that week, not what matters\n\nReference: [Martin Fowler — Technical Debt Quadrant](https://martinfowler.com/bliki/TechnicalDebtQuadrant.html) · [ThoughtWorks Tech Radar — Debt Register](https://www.thoughtworks.com/radar)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3720,"content_sha256":"5ed244d0285eb8a4e3101dbf2501452b46b21ae5a57513dcefc3aba67bc05b7c"},{"filename":"rules/process-deprecated-markers.md","content":"---\ntitle: '@deprecated Markers Without Removal Plan'\nimpact: MEDIUM\nimpactDescription: \"Indefinite deprecations become permanent — they never actually go away\"\ntags: deprecated, lifecycle, process\n---\n\n## @deprecated Markers Without Removal Plan\n\n**Impact: MEDIUM (Indefinite deprecations become permanent — they never actually go away)**\n\nA `@deprecated` tag without a replacement, removal date, or migration plan is just a polite \"I wish you wouldn't use this.\" Consumers keep using it, the deprecation message persists for years, and the codebase carries dead-but-not-dead APIs forever.\n\n## How to Detect\n\n```bash\n# Find all @deprecated markers (one --include per extension; grep doesn't expand braces)\ngrep -rEn '@deprecated' \\\n --include='*.php' --include='*.ts' --include='*.tsx' \\\n --include='*.js' --include='*.jsx' src/ app/\n\n# For each, check whether it includes:\n# - Replacement / `@see` pointer\n# - Removal version or date\n# - Reason\n\n# Find @deprecated callers — these are migration targets\ngrep -rEn '@deprecated' -A2 src/ | grep -oE 'function [a-zA-Z]+|method [a-zA-Z]+'\n```\n\nFlag any `@deprecated` without:\n1. **What to use instead** (`@see` or \"use X instead\")\n2. **When it will be removed** (version, date, or trigger)\n3. **At least one PR removing internal usage** since the deprecation was added\n\n## Incorrect\n\n```typescript\n// ❌ Vague, unactionable deprecations\n/** @deprecated */\nexport function getUserById(id: string) { /* ... */ }\n\n/** @deprecated do not use */\nexport function legacyFormat(date: Date) { /* ... */ }\n\n/** @deprecated use the new API */ // which \"new API\"?\nexport function fetchOrders() { /* ... */ }\n```\n\n**Problems:**\n- Consumers see \"deprecated\" but cannot act\n- No deadline → no urgency → no migration\n- 5 years later still in code, still warning, still used everywhere\n\n## Correct\n\n```typescript\n/**\n * @deprecated Since v4.2 (2025-09). Use {@link findUserById} which supports\n * batching and returns `Result\u003cUser, NotFound>`. Will be removed in v5.0\n * (target: 2026-Q3). Internal callers: 0 (migration complete).\n *\n * @see findUserById\n */\nexport function getUserById(id: string): User | null { /* ... */ }\n```\n\nAdd CI to enforce:\n\n```yaml\n- name: No undocumented @deprecated\n run: |\n BAD=$(grep -rEn '@deprecated\\b' src/ | grep -v -E '@deprecated.*[0-9]{4}')\n test -z \"$BAD\" || { echo \"@deprecated needs a date/version\"; exit 1; }\n```\n\n**Benefits:**\n- Consumers see exactly what to do and when\n- Tracking the deprecation → removal cycle is mechanical (grep + count)\n- Old code actually leaves the codebase\n\n## Remediation Strategy\n\n- **Effort:** S per marker (audit), M per deprecation cycle (migrate internal callers, then remove)\n- **When to pay down:**\n - **Audit existing markers:** add date + replacement, OR upgrade to \"will be removed in next major\" + start migration\n - **Removal:** when a deprecation hits its target version, *actually remove the code*. A deprecation that doesn't end is a lie.\n- **Cycle:**\n 1. Mark `@deprecated since vX (date), use Y, removed in vZ`\n 2. Migrate internal callers (creates the proof the replacement works)\n 3. Wait one major or N months for external callers\n 4. Remove\n\nReference: [Semantic Versioning — Major changes](https://semver.org/) · [PHPDoc @deprecated](https://docs.phpdoc.org/3.0/guide/references/phpdoc/tags/deprecated.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3383,"content_sha256":"f399b7289253d922f8816ff5a93a51e519b37bcacfe64bc15ecf64306f327d2c"},{"filename":"rules/process-feature-flags-lingering.md","content":"---\ntitle: Lingering Feature Flags\nimpact: MEDIUM\nimpactDescription: \"Old flags become permanent special cases — dead branches that never die\"\ntags: feature-flags, process, cleanup\n---\n\n## Lingering Feature Flags\n\n**Impact: MEDIUM (Old flags become permanent special cases — dead branches that never die)**\n\nFeature flags are a release-management tool, not a config primitive. Once a feature has been at 100% rollout for weeks, the flag is no longer protecting anyone — it's just dead branches polluting the codebase. Lingering flags accumulate as conditional logic that nobody understands, can't safely delete, and silently doubles the test surface.\n\n## How to Detect\n\n```bash\n# Search for feature-flag SDK references\ngrep -rEn '(feature|flag|toggle)::(isOn|enabled|isEnabled|active)' \\\n --include='*.php' --include='*.ts' --include='*.tsx' --include='*.js' .\n\n# Specific SDKs\ngrep -rEn '(LaunchDarkly|posthog|growthbook|unleash|split\\.io|flipper|optimizely)' .\n\n# Laravel pennant\ngrep -rEn 'Feature::active\\(|Feature::for\\(' app/ resources/\n\n# Count distinct flag identifiers and look up their ages in the flag platform\n# (most platforms expose a \"stale flags\" view — LaunchDarkly, Statsig, GrowthBook all do)\n```\n\nFor each flag found, check:\n- **Rollout state:** 0%? 100%? Or in-flight rollout? (Anything > 6 weeks at 0 or 100% is debt.)\n- **Last evaluation:** flag platform usually tracks last-checked time per flag\n- **Owner:** is there a name attached? Is that person still on the team?\n\n## Incorrect\n\n```typescript\n// ❌ Flag from 2 years ago, shipped to 100% for 18 months, still in code\nfunction CheckoutPage() {\n const showNewCheckout = useFlag('new-checkout-redesign-v3'); // long since 100%\n return showNewCheckout ? \u003cNewCheckout /> : \u003cLegacyCheckout />;\n}\n\n// ❌ Nested flag conditions become an N×M maze\nif (flags.useNewPricing && flags.useNewTaxEngine && !flags.legacyShippingFallback) {\n // ... one path\n} else if (flags.useNewPricing && !flags.useNewTaxEngine) {\n // ... another path\n} else {\n // ... legacy path nobody has touched in a year\n}\n```\n\n**Problems:**\n- `LegacyCheckout` is dead code masquerading as a fallback\n- The flag service is queried for every page load even though the result is constant\n- New engineers see 3 versions of checkout and don't know which is current\n- Removing the flag is \"scary\" because nobody has touched the legacy branch in months\n\n## Correct\n\n```typescript\n// ✅ Flag retired: branch chosen, dead branch removed, flag deleted from platform\nfunction CheckoutPage() {\n return \u003cCheckout />; // formerly NewCheckout\n}\n// LegacyCheckout: deleted. New-checkout-redesign-v3 flag: archived in LaunchDarkly.\n```\n\nLifecycle policy as a CI gate:\n\n```yaml\n# .github/workflows/stale-flags.yml — runs weekly\n# LaunchDarkly: use the official action (the tool is a Go binary called\n# `ld-find-code-refs`, not an npm package — `npx` will not work).\n- name: Stale flag check (LaunchDarkly)\n uses: launchdarkly/find-code-references@v2\n with:\n accessToken: ${{ secrets.LD_ACCESS_TOKEN }}\n projKey: ${{ vars.LD_PROJECT_KEY }}\n repoName: ${{ github.event.repository.name }}\n```\n\nMaintain a register:\n\n```markdown\n| Flag | State | Created | Owner | Action |\n|-------------------------------|----------|------------|----------|--------|\n| new-checkout-redesign-v3 | 100% 18mo| 2024-09-01 | @asyraf | DELETE |\n| experimental-promo-engine | 50% A/B | 2026-04-01 | @growth | KEEP |\n| disable-legacy-search | 0% 9mo | 2025-09-15 | @search | DELETE |\n```\n\n**Benefits:**\n- Dead branches removed → smaller test matrix, simpler code, smaller bundle\n- Flag platform stops being queried for permanent constants (latency win)\n- Onboarding engineers see one path, not three\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — retire a single flag (delete code branch + remove flag)\n - **M** — clean up nested flag combinations\n - **L** — instate a flag-lifecycle program (creation requires expiry date, weekly stale-flag review)\n- **When to pay down:**\n - **NOW:** any flag at 100% for > 6 weeks with no rollback plan\n - **Per sprint:** drop one stale flag — small effort, compounding cleanup\n - **Then:** lifecycle policy — every new flag has an expiry date in the platform\n\n**Lifecycle policy (recommended):**\n1. **Create** — flag has a name, owner, intended rollout duration, and target removal date\n2. **Roll out** — gradually 1% → 5% → 25% → 100%\n3. **Stabilize** — at 100% for one release cycle; verify metrics\n4. **Retire** — delete the losing branch, remove flag from code, archive in platform\n5. **Audit** — weekly stale-flag report; any flag past expiry pings the owner\n\n**Anti-patterns:**\n- **Permanent flags used as \"config\"** — that's not a flag, that's an env var; treat differently\n- **Flags as authz** — use proper authorization layers, not flag SDKs\n- **No expiry on creation** — every flag should be born with a death date\n\nReference: [Martin Fowler — Feature Toggles](https://martinfowler.com/articles/feature-toggles.html) · [ld-find-code-refs (GitHub)](https://github.com/launchdarkly/ld-find-code-refs) · [Laravel Pennant](https://laravel.com/docs/pennant)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5247,"content_sha256":"5e54225e4a2ea4ecc55d0725c97e7693a9b32d8fb61a08f2b984f042e405c39e"},{"filename":"rules/process-ownership-gaps.md","content":"---\ntitle: Code Without Owners\nimpact: MEDIUM\nimpactDescription: \"Orphaned code = nobody reviews, nobody maintains, nobody knows\"\ntags: ownership, codeowners, process\n---\n\n## Code Without Owners\n\n**Impact: MEDIUM (Orphaned code = nobody reviews, nobody maintains, nobody knows)**\n\nWhen code has no clear owner, two things happen: PRs touching it stall (nobody knows who should approve), and when it breaks at 2am, the on-call rotation finds out the hard way. Orphaned code accumulates as the \"bus-factor of one\" engineer who wrote it changes teams or leaves.\n\n## How to Detect\n\n```bash\n# Files not covered by CODEOWNERS\ngh api repos/:owner/:repo/contents | jq -r '.[].path' > all-files.txt\n# Compare against CODEOWNERS patterns (manual or via `git check-attr` for path-attribute alternative)\n\n# \"Author concentration\" — files where one person wrote >70% and they're gone\ngit ls-files | while read f; do\n TOP=$(git log --format='%ae' -- \"$f\" | sort | uniq -c | sort -rn | head -1)\n echo \"$TOP $f\"\ndone | sort -rn | head -30\n\n# High-churn, low-author-count files (bus-factor risk)\ngit log --since='12 months ago' --name-only --format='COMMIT %ae' | \\\n awk '/^COMMIT/{a=$2; next} {print a, $0}' | \\\n sort -k2 | uniq -c | sort -rn | head\n\n# Repos lacking a CODEOWNERS file at all\nls .github/CODEOWNERS docs/CODEOWNERS CODEOWNERS 2>/dev/null\n```\n\n## Incorrect\n\n```\n❌ A typical legacy repo:\n\n- No .github/CODEOWNERS file\n- 40% of files last touched by engineers who left 2+ years ago\n- Critical billing module written by one engineer, no shared knowledge\n- Incident response for `/api/legacy/*` routes pages a randomly-selected on-call\n who has never seen the code\n- PRs touching low-traffic areas wait 2 weeks for a review because nobody owns them\n```\n\n**Problems:**\n- Knowledge debt is invisible until incident — then it's catastrophic\n- Code reviews degrade to rubber-stamps because nobody has context\n- Refactoring is risky — \"is anyone using this?\" has no quick answer\n\n## Correct\n\n```\n# ✅ .github/CODEOWNERS — every path has an owner team\n\n# Default owners for everything not matched below\n* @org/platform\n\n# Domain owners\n/app/Billing/ @org/billing-team\n/app/Auth/ @org/identity-team\n/app/Notifications/ @org/messaging-team\n/resources/js/Pages/Admin/ @org/admin-frontend\n\n# Infrastructure / DevEx\n/.github/workflows/ @org/devex\n/deploy/ @org/devex\n\n# Database\n/database/migrations/ @org/data-engineering @org/platform\n```\n\nAdd a CI step to validate the file and check coverage:\n\n```yaml\n- name: CODEOWNERS lint + coverage\n uses: mszostok/[email protected]\n with:\n checks: \"files,owners,duppatterns,syntax\"\n experimental_checks: \"notowned\"\n github_access_token: ${{ secrets.GITHUB_TOKEN }}\n```\n\nThe `notowned` experimental check flags files in the repo not matched by any CODEOWNERS pattern — the right tool for \"is everything covered?\".\n\n**Benefits:**\n- GitHub auto-requests reviews from owners — no more \"who should review this?\"\n- Incident response routes to the right team\n- New engineers can find a domain expert by path\n- Refactor decisions get the input they need\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — bootstrap a CODEOWNERS with broad team-level patterns\n - **M** — refine ownership as teams form; add path-specific owners\n - **L** — rehome orphaned code (find a new owner team, document context, transfer ownership)\n- **When to pay down:**\n - **NOW:** if you've ever asked \"who owns this code?\" and the answer was unclear\n - **As a project:** quarterly ownership review — re-attest that owners listed are still active\n- **Anti-patterns:**\n - **Individual owners** for production code (bus-factor of 1) — prefer team owners\n - **Stale CODEOWNERS** referencing teams or people that no longer exist (CI lint catches this)\n - **Owning everything** by one team — fragment by domain so PRs route fast\n\n**Tip:** when no team wants to own a piece of code, that's a strong signal to delete it (if unused), extract it (if shared), or fold it into a new team's charter (if business-critical).\n\nReference: [GitHub — CODEOWNERS](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) · [GitLab — Code Owners](https://docs.gitlab.com/ee/user/project/codeowners/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4449,"content_sha256":"6fd073ffcb54dce7fc06386301a3427c9422652c0b2d21e7244c251f3b9631d6"},{"filename":"rules/process-todo-fixme-aging.md","content":"---\ntitle: Aging TODO, FIXME, and HACK Comments\nimpact: MEDIUM\nimpactDescription: \"Untracked promises that compound silently\"\ntags: todo, fixme, comments, process\n---\n\n## Aging TODO, FIXME, and HACK Comments\n\n**Impact: MEDIUM (Untracked promises that compound silently)**\n\nA `// TODO` is a promise to do something later, written by someone who has now forgotten. After 6 months, nobody remembers what the TODO meant, whether it still applies, or what the consequences are. These comments accumulate as untracked technical debt invisible to product and management.\n\n## How to Detect\n\n```bash\n# All TODO/FIXME/HACK with the introducing commit's date (via git blame)\n# Note: grep's --include uses fnmatch, not brace expansion — pass one --include per ext.\ngrep -rEn '(TODO|FIXME|HACK|XXX|BUG)' \\\n --include='*.php' --include='*.ts' --include='*.tsx' \\\n --include='*.js' --include='*.jsx' . | \\\n while IFS=: read -r file line content; do\n DATE=$(git blame -L \"$line,$line\" --date=short -- \"$file\" 2>/dev/null | awk '{print $3}')\n echo \"$DATE | $file:$line | $content\"\n done | sort\n\n# Quick count\ngrep -rEn '(TODO|FIXME|HACK)' src/ | wc -l\n```\n\nThreshold rules:\n- **TODO older than 6 months** without ticket reference → debt\n- **FIXME of any age** without ticket → debt (FIXME implies known broken)\n- **HACK of any age** without ticket → debt (HACK implies known wrong)\n\n## Incorrect\n\n```typescript\n// ❌ Naked TODOs / FIXMEs accumulated over years\nfunction calculateShipping(order: Order): number {\n // TODO: support international shipping\n // TODO: discount for premium members\n // FIXME: this is wrong for orders > $500\n // HACK: hardcoded $15 for now\n\n return 15;\n}\n```\n\n**Problems:**\n- \"Wrong for orders > $500\" is a bug nobody is tracking\n- International shipping was demanded a year ago; product team doesn't know it's blocked here\n- Each TODO is an island — no estimate, no owner, no priority\n\n## Correct\n\n```typescript\n// ✅ TODO with ticket, date, and owner (or no TODO at all)\nfunction calculateShipping(order: Order): number {\n // Hardcoded $15 — international + premium discounts tracked in #1842 (asyraf, 2026-03)\n return 15;\n}\n```\n\nOr — preferred when the work is real:\n\n1. **Open a ticket** in the issue tracker\n2. **Reference it** in code: `// see #1842`\n3. **Delete bare TODOs** — if they're worth keeping, they're worth tracking\n\nCI enforcement:\n\n```yaml\n- name: No new bare TODOs\n run: |\n BARE=$(git diff origin/main...HEAD \\\n | grep -E '^\\+.*\\b(TODO|FIXME|HACK)\\b' \\\n | grep -v -E '#[0-9]+')\n test -z \"$BARE\" || { echo \"Add a ticket reference to TODOs\"; exit 1; }\n```\n\n**Benefits:**\n- Every promise has an owner and a tracker entry\n- Product can see and prioritise the backlog\n- Code is honest about what's known-broken\n\n## Remediation Strategy\n\n- **Effort:** S per comment (decision: ticket, fix, or delete)\n- **When to pay down:**\n - **Now:** triage existing TODOs → ticket, fix immediately, or delete\n - **Ongoing:** CI gate prevents new bare TODOs\n- **Triage flow:**\n 1. Still relevant? If no, delete.\n 2. Worth doing? If no, delete.\n 3. Worth doing this quarter? File a ticket, reference it.\n 4. Worth doing now? Just do it — don't write a TODO.\n\nReference: [Steve McConnell — Code Complete, Ch. 32 on Self-Documenting Code](https://www.microsoftpressstore.com/store/code-complete-9780735619678)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3381,"content_sha256":"d5c0b7acaf2e8957723e280bf09661f4f2502c9e8fc721ebeaaf5f004d9595c7"},{"filename":"rules/security-auth-hardening.md","content":"---\ntitle: Auth and Hardening Gaps\nimpact: HIGH\nimpactDescription: \"Outdated auth defaults are tomorrow's breach disclosures\"\ntags: security, authentication, authorization, hardening\n---\n\n## Auth and Hardening Gaps\n\n**Impact: HIGH (Outdated auth defaults are tomorrow's breach disclosures)**\n\nAuth choices made years ago — password hashing algorithm, session lifetime, missing MFA, missing rate limits, missing security headers — accrue as silent debt. They only become visible during pen tests or incidents, where they're suddenly the biggest finding.\n\n## How to Detect\n\nAudit each of:\n1. **Password hashing** — bcrypt is OK, argon2id is preferred; MD5/SHA-1 are unacceptable\n2. **Session lifetime** — infinite or multi-month sessions are debt\n3. **MFA support** — is it offered? Is it enforced for admins?\n4. **Rate limiting** — login, password-reset, API endpoints\n5. **Security headers** — `Content-Security-Policy`, `Strict-Transport-Security`, `X-Content-Type-Options`, `Referrer-Policy`\n6. **Authorization checks** — every endpoint enforces who can call it\n7. **CSRF protection** — present on every state-changing endpoint that uses cookies\n\n```bash\n# Check security headers\ncurl -sI https://your.app | grep -iE 'content-security|strict-transport|x-content-type|x-frame|referrer'\n\n# Mozilla Observatory CLI\n# https://observatory.mozilla.org/\n# securityheaders.com\n\n# Laravel: scan controllers for missing authorization\ngrep -rEn 'function (index|show|store|update|destroy)\\(' app/Http/Controllers/ | \\\n while IFS=: read -r FILE LINE REST; do \\\n grep -q 'authorize\\|Gate::\\|middleware' \"$FILE\" || echo \"MISSING AUTHZ: $FILE:$LINE\"; \\\n done\n```\n\n## Incorrect\n\n```php\n// ❌ Multiple hardening gaps\n// User registration with MD5 (catastrophic)\npublic function register(Request $request) {\n User::create([\n 'email' => $request->email,\n 'password' => md5($request->password), // hash algorithm from 1992\n ]);\n}\n\n// Session config (config/session.php)\n'lifetime' => 525600, // 1 year sessions — every stolen device is forever\n\n// No rate limit on login → credential stuffing trivial\nRoute::post('/login', [AuthController::class, 'login']);\n\n// No CSP — XSS gets full DOM access\n// (no header set in middleware)\n\n// Authorization missing — anyone with a session can hit admin endpoints\nRoute::get('/admin/users', [AdminController::class, 'users']);\n```\n\n## Correct\n\n```php\n// ✅ Password hashing via Hash::make (bcrypt by default in Laravel 11+;\n// argon2id is opt-in via config/hashing.php — preferred for new projects)\nUser::create([\n 'email' => $request->validated('email'),\n 'password' => Hash::make($request->validated('password')),\n]);\n\n// Reasonable session lifetime, secure flags\n'lifetime' => 60 * 8, // 8h\n'secure' => true, // HTTPS only\n'http_only' => true,\n'same_site' => 'lax',\n\n// Login rate limited\nRoute::post('/login', [AuthController::class, 'login'])\n ->middleware('throttle:5,1'); // 5 attempts per minute per IP\n\n// Authorization on every admin endpoint\nRoute::middleware(['auth', 'can:viewAdmin'])->group(function () {\n Route::get('/admin/users', [AdminController::class, 'users']);\n});\n\n// Security headers via middleware (or a package like spatie/laravel-csp)\nreturn $next($request)\n ->header('Content-Security-Policy', \"default-src 'self'; ...\")\n ->header('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload')\n ->header('X-Content-Type-Options', 'nosniff')\n ->header('Referrer-Policy', 'strict-origin-when-cross-origin');\n```\n\n**Benefits:**\n- Hashing upgrade path supported by `Hash::needsRehash()` — old MD5 hashes can be transparently re-hashed on next successful login (after one-time migration to bcrypt/argon2id)\n- Session theft window is bounded\n- Authorization is explicit and uniformly applied via middleware\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — security headers, rate limits, session lifetime\n - **M** — adding MFA, enforcing authz across all endpoints\n - **L** — password hash migration (must be done on next login per user; takes weeks of natural traffic)\n- **When to pay down:**\n - **NOW:** any MD5/SHA-1 password hashing, missing CSRF, public admin endpoints\n - **This quarter:** MFA, security headers, rate limits\n - **Ongoing:** authz coverage in CI (e.g., test that every authenticated route asserts a policy)\n\n**Tip:** run `https://securityheaders.com` against staging at least once per quarter — it's free, fast, and surfaces missing headers immediately.\n\nReference: [OWASP — Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html) · [OWASP — Session Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html) · [Mozilla Observatory](https://observatory.mozilla.org/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4886,"content_sha256":"e50d1b58c4b9fd2a45c40fa65bd6d7ab8c04ab6c0daf114e6cc9782e7f29bdcf"},{"filename":"rules/security-input-validation.md","content":"---\ntitle: Missing Input Validation\nimpact: CRITICAL\nimpactDescription: \"Foundational defense; injection vectors compound silently across endpoints\"\ntags: security, validation, injection\n---\n\n## Missing Input Validation\n\n**Impact: CRITICAL (Foundational defense; injection vectors compound silently across endpoints)**\n\nUntrusted input flowing into queries, templates, file paths, or shell commands is the source of most OWASP top-10 vulnerabilities. Every endpoint that accepts external input without explicit validation is debt — and it compounds because each new endpoint adds another opportunity.\n\n## How to Detect\n\n```bash\n# Laravel: controllers reaching directly into request without FormRequest\ngrep -rEn '\\$request->(input|get|all)\\b' app/Http/Controllers/ | \\\n grep -v 'FormRequest\\|validated('\n\n# Express / Node: handlers using req.body / req.query without zod/joi/yup\ngrep -rEn 'req\\.(body|query|params)' src/ | \\\n grep -vE 'parse\\(|validate\\(|safeParse'\n\n# SQL string concatenation (always bad) — grep's --include uses fnmatch, not brace expansion\ngrep -rEn '(SELECT|INSERT|UPDATE|DELETE).*\\+.*\\$|\\\\?.*concat' \\\n --include='*.ts' --include='*.tsx' --include='*.php' --include='*.js' .\n\n# Shell-out from app code (path for command injection)\ngrep -rEn 'exec\\(|shell_exec\\(|proc_open\\(|spawn\\(' \\\n --include='*.ts' --include='*.php' --include='*.js' .\n```\n\nAlso look at audit log coverage: every controller endpoint should map to an explicit validation rule set.\n\n## Incorrect\n\n```typescript\n// ❌ Direct use of request input — type cast is not validation\napp.get('/users', async (req, res) => {\n const limit = parseInt(req.query.limit as string);\n const search = req.query.q as string;\n const [rows] = await pool.query(\n `SELECT * FROM users WHERE name LIKE '%${search}%' LIMIT ${limit}`, // SQL injection\n );\n res.json(rows);\n});\n```\n\n```php\n// ❌ Laravel: same problem, different stack\npublic function index(Request $request) {\n $sort = $request->input('sort'); // attacker controls\n $users = DB::select(\"SELECT * FROM users ORDER BY $sort\");\n return response()->json($users);\n}\n```\n\n**Problems:**\n- SQL injection in both examples (no parameterization, no allowlist)\n- `parseInt` of attacker input returns NaN for non-numbers — `LIMIT NaN` errors leak SQL structure\n- The \"cast as string\" in TypeScript provides zero runtime validation\n\n## Correct\n\n```typescript\n// ✅ Zod schema + parameterized query\nimport { z } from 'zod';\n\nconst ListUsersQuery = z.object({\n limit: z.coerce.number().int().min(1).max(100).default(20),\n q: z.string().max(80).optional(),\n});\n\n// Using `mysql2/promise` — `?` placeholders, parameterized by the driver.\n// We use `pool.query()` (client-side escaping) rather than `pool.execute()`\n// (server-side prepared statements) because mysql2's prepared statements\n// can fail to bind JS numbers to `LIMIT ?` (ER_WRONG_ARGUMENTS in some\n// MySQL versions). With `query()` mysql2 escapes the number safely.\n// Also: MySQL's default collation (`utf8mb4_0900_ai_ci`) is case-insensitive,\n// so plain LIKE matches both 'Asyraf' and 'asyraf' without `LOWER(...)`.\nimport mysql from 'mysql2/promise';\nconst pool = mysql.createPool({ /* ... */ });\n\napp.get('/users', async (req, res) => {\n const parsed = ListUsersQuery.safeParse(req.query);\n if (!parsed.success) return res.status(400).json(parsed.error.flatten());\n\n const { limit, q } = parsed.data;\n const [rows] = await pool.query(\n 'SELECT * FROM users WHERE (? IS NULL OR name LIKE ?) LIMIT ?',\n [q ?? null, q ? `%${q}%` : null, limit],\n );\n res.json(rows);\n});\n```\n\n```php\n// ✅ Laravel FormRequest with allowlisted sort\nfinal class ListUsersRequest extends FormRequest\n{\n public function rules(): array {\n return [\n 'sort' => ['nullable', Rule::in(['name', 'created_at'])],\n 'limit' => ['integer', 'min:1', 'max:100'],\n ];\n }\n}\n\npublic function index(ListUsersRequest $request) {\n $sort = $request->validated('sort', 'created_at');\n return DB::table('users')->orderBy($sort)->paginate($request->validated('limit', 20));\n}\n```\n\n**Benefits:**\n- Bad input → 400 with a clear message, never reaches the database\n- SQL injection eliminated by parameterization + allowlist\n- Validation is a single auditable location per endpoint\n\n## Remediation Strategy\n\n- **Effort:** S per endpoint\n- **When to pay down:**\n - **NOW:** any endpoint that takes input into a raw SQL string, shell command, or file path\n - **This sprint:** all unvalidated endpoints in critical paths (auth, payment, profile)\n - **Then:** lint rules that fail PRs lacking validation schemas\n- **Tip:** put validation at the boundary (controller / route handler), then trust the validated shape downstream. Don't re-validate the same fields in 5 places.\n\nReference: [OWASP — Input Validation Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html) · [Laravel Validation](https://laravel.com/docs/validation) · [Zod](https://zod.dev/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5045,"content_sha256":"37cfa478dccd75fa099ca248265285933b1971a5690b486fedfeadd792f30871"},{"filename":"rules/security-secrets-in-code.md","content":"---\ntitle: Secrets in Source Code\nimpact: CRITICAL\nimpactDescription: \"Once committed, a secret must be rotated AND scrubbed — git history is forever\"\ntags: security, secrets, credentials\n---\n\n## Secrets in Source Code\n\n**Impact: CRITICAL (Once committed, a secret must be rotated AND scrubbed — git history is forever)**\n\nA secret committed to git is compromised the moment the commit lands, even if you delete it in the next commit. Public repos are crawled by bots within minutes; private repos leak through forks, backups, and CI logs. Rotation is mandatory — deletion alone is theater.\n\n## How to Detect\n\n```bash\n# Scan current tree and full history for secrets\ngitleaks git # scan repo history\ngitleaks git --pre-commit --staged # pre-commit hook form (v8.19+)\ntrufflehog filesystem . # alternative scanner\ntrufflehog git file://. --only-verified # verified live secrets\n\n# Targeted grep\ngrep -rEn '(aws_secret|api_key|password|token)\\s*=\\s*[\"\\047][A-Za-z0-9/+=_-]{16,}' .\n\n# Pre-commit hook (gitleaks) — pin to the latest stable release tag\n# .pre-commit-config.yaml\n# - repo: https://github.com/gitleaks/gitleaks\n# rev: v8.30.1\n# hooks: [ { id: gitleaks } ]\n```\n\n## Incorrect\n\n```php\n// ❌ Hardcoded API keys, database passwords, signing keys\n// config/services.php\nreturn [\n 'stripe' => [\n 'secret' => 'sk_live_51Hxxxxxxxxxxxxxxxxxxx', // committed to repo\n ],\n 'aws' => [\n 'key' => 'AKIAIOSFODNN7EXAMPLE',\n 'secret' => 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',\n ],\n];\n\n// .env.example with REAL values that got copied to .env and committed\nDB_PASSWORD=ProductionDb2024!\nJWT_SECRET=hardcoded-jwt-signing-key-do-not-use\n```\n\n**Problems:**\n- The Stripe key is now public — Stripe will eventually rotate it, but not before charges go through\n- The AWS key allows full account access until rotated; bots will find it\n- \"Look, I deleted it in the next commit\" — irrelevant. It's in git history.\n\n## Correct\n\n```php\n// ✅ Read from environment / secret manager\nreturn [\n 'stripe' => ['secret' => env('STRIPE_SECRET')],\n 'aws' => [\n 'key' => env('AWS_ACCESS_KEY_ID'),\n 'secret' => env('AWS_SECRET_ACCESS_KEY'),\n ],\n];\n```\n\n```bash\n# .env.example contains only placeholders\nSTRIPE_SECRET=\nAWS_ACCESS_KEY_ID=\nAWS_SECRET_ACCESS_KEY=\n\n# Real values live in:\n# - Doppler / Vault / AWS Secrets Manager / GCP Secret Manager (production)\n# - Local .env (gitignored)\n# - CI: GitHub Actions secrets, with OIDC for AWS where possible\n```\n\nCI gate:\n\n```yaml\n- uses: gitleaks/gitleaks-action@v2 # fails PR if any secret pattern detected\n```\n\n**Benefits:**\n- Secrets can be rotated without code changes\n- Audit logs show every access (with a real secret manager)\n- New engineers cannot accidentally leak production credentials\n\n## Remediation Strategy\n\n- **Effort:**\n - **S** — move forward: env vars + pre-commit hook + CI gate\n - **M** — clean active code paths to use env / secret manager\n - **L** — scrub git history if secrets are old (use `git filter-repo` or BFG; coordinate with team)\n- **When to pay down:**\n - **NOW:** any secret committed to a public repo — rotate first, then clean\n - **This sprint:** any committed secret in private repos\n - **Then:** install gitleaks pre-commit + CI gate to prevent regression\n\n**Rotation checklist for any discovered secret:**\n1. Revoke the secret at the issuer (Stripe, AWS, etc.)\n2. Generate a replacement\n3. Update production via secret manager\n4. Remove from code + commit replacement source\n5. Optionally scrub history (cost-benefit; sometimes rotation is enough)\n6. Add a regex rule to gitleaks to prevent the same shape from re-entering\n\nReference: [GitGuardian — State of Secrets Sprawl](https://www.gitguardian.com/state-of-secrets-sprawl-report-2024) · [gitleaks](https://github.com/gitleaks/gitleaks) · [trufflehog](https://github.com/trufflesecurity/trufflehog)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3998,"content_sha256":"15922420e67cfd015bf24a3521ac061b720d462ec9d6e8b22af2414bac448a73"},{"filename":"rules/test-coverage-gaps.md","content":"---\ntitle: Coverage Gaps on Critical Paths\nimpact: HIGH\nimpactDescription: \"Uncovered critical paths fail in production, not in CI\"\ntags: testing, coverage, regression\n---\n\n## Coverage Gaps on Critical Paths\n\n**Impact: HIGH (Uncovered critical paths fail in production, not in CI)**\n\nAggregate coverage percentage is a vanity metric. What matters is whether the **critical paths** (checkout, auth, payment, signup) have integration tests. A repo at 85% line coverage with zero tests on payment has more risk than one at 60% with a full checkout suite.\n\n## How to Detect\n\nIdentify critical paths from the product (signup, login, checkout, refund, etc.), then check:\n\n```bash\n# Node\nnpx jest --coverage --coverageReporters=text\nnpx vitest run --coverage\n\n# PHP\nvendor/bin/phpunit --coverage-html=coverage/\n\n# Targeted: are there any integration tests for the checkout flow?\ngrep -rln 'test.*checkout\\|describe.*checkout' tests/\n```\n\nFor each critical path, look for:\n- An end-to-end / integration test that exercises the happy path\n- Tests for failure modes (declined card, out-of-stock, expired session)\n- At least one test that runs against the real DB / real network adapter\n\n## Incorrect\n\n```\n// ❌ 92% line coverage — but all of it is on getters, mappers, and trivial helpers.\n// The actual checkout pipeline has NO integration test.\n\nsrc/\n├── utils/ — 100% covered (10 tests)\n├── formatters/ — 100% covered (15 tests)\n├── checkout/\n│ ├── pricing.ts — 20% covered\n│ ├── inventory.ts — 0% covered\n│ ├── payment.ts — 0% covered\n│ └── orchestrate.ts — 0% covered ← THE checkout flow\n```\n\n**Problems:**\n- Coverage metric is \"green\" → team feels safe → bugs land in checkout\n- No safety net for refactoring the orchestrator\n- On-call has no automated regression check before deploys\n\n## Correct\n\n```typescript\n// ✅ One integration test per critical-path scenario, hitting real adapters where feasible\n// tests/checkout.integration.test.ts\ndescribe('checkout', () => {\n it('places an order with valid card', async () => {\n const order = await checkoutClient.place({ /* ... */ });\n expect(order.status).toBe('confirmed');\n expect(inventory.stockFor('SKU-1')).toBe(initial - 1);\n });\n\n it('rejects when card is declined', async () => {\n await expect(checkoutClient.place({ token: 'tok_chargeDeclined' }))\n .rejects.toThrow(PaymentDeclined);\n expect(inventory.stockFor('SKU-1')).toBe(initial); // no leak\n });\n\n it('rejects when item is out of stock', async () => { /* ... */ });\n it('issues idempotent retries safely', async () => { /* ... */ });\n});\n```\n\n**Benefits:**\n- Regression on checkout fails CI, not customers\n- Refactoring the orchestrator is safe\n- New scenarios (e.g., new payment method) extend a known suite\n\n## Remediation Strategy\n\n- **Effort:** M per critical path (the first test costs the most; subsequent are cheap)\n- **When to pay down:** **Before** the next behaviour change on that path. The change itself is your justification.\n- **Target:** one integration test per critical path, covering happy + 2–3 failure modes. Don't chase a coverage number.\n\nReference: [Martin Fowler — Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3324,"content_sha256":"caf4753704ace83210c8b344f086cad2980f32dfe17121bc0f3b4f59e83c61fd"},{"filename":"rules/test-disabled-tests.md","content":"---\ntitle: Skipped and Disabled Tests\nimpact: HIGH\nimpactDescription: \"Dark coverage — code looks tested, isn't\"\ntags: testing, skip, disabled\n---\n\n## Skipped and Disabled Tests\n\n**Impact: HIGH (Dark coverage — code looks tested, isn't)**\n\nSkipped tests are worse than missing tests because they create a false sense of safety. A `test.skip(...)` or `markTestSkipped()` left in main without an owner, issue, or deadline is debt that grows in silence.\n\n## How to Detect\n\n```bash\n# JavaScript / TypeScript (Jest / Vitest)\ngrep -rEn '\\\\.skip|xdescribe|xit|test\\\\.todo|describe\\\\.skip' tests/ src/\n\n# Pest / PHPUnit\ngrep -rEn 'markTestSkipped|markTestIncomplete|@group\\\\s+skip|->skip\\\\(' tests/\n```\n\nCross-reference each hit with:\n- Is there a linked issue?\n- Is there a comment explaining why?\n- Is there a date or condition for re-enabling?\n\n## Incorrect\n\n```typescript\n// ❌ Bare skips with no context\ndescribe.skip('checkout', () => { /* ... */ });\n\ntest.skip('refunds work', () => { /* ... */ });\n\ntest('payment webhook', () => {\n if (process.env.CI) return; // silent skip on CI\n // ...\n});\n```\n\n**Problems:**\n- Why are checkouts skipped? Nobody remembers\n- The webhook test runs only locally — production behaviour is untested\n- Coverage report shows them as \"executed\" but with zero assertions\n\n## Correct\n\n```typescript\n// ✅ Every skip has an owner, reason, and re-enable trigger\ntest.skip(\n 'refunds work — DISABLED 2026-02-10 (#1247) re-enable after Stripe webhook v2 migration',\n () => { /* ... */ }\n);\n\n// ✅ Or: delete and replace if the test cannot be repaired\n// git log will remember it ever existed.\n```\n\nAdd CI checks:\n\n```yaml\n# .github/workflows/test-hygiene.yml\n- name: Disallow new bare skips\n run: |\n NEW_SKIPS=$(git diff origin/main...HEAD -- 'tests/**' \\\n | grep -E '^\\+.*\\.skip\\(' | grep -v '#[0-9]')\n test -z \"$NEW_SKIPS\" || { echo \"Bare skip without ticket\"; exit 1; }\n```\n\n**Benefits:**\n- Every skip is auditable and assigned\n- New skips require a ticket — prevents quiet accumulation\n- The team has a count of \"real\" coverage\n\n## Remediation Strategy\n\n- **Effort:** S per skip (decide: fix, delete, or document)\n- **When to pay down:**\n - **Now:** audit existing skips → add owner + ticket OR delete\n - **Ongoing:** CI gate prevents new bare skips\n- **Default action:** if a skip is older than 90 days with no movement, delete the test. If it's worth keeping, it's worth re-enabling.\n\nReference: [PHPUnit docs](https://docs.phpunit.de/) (see the \"Incomplete and Skipped Tests\" chapter in the current major version)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2587,"content_sha256":"82685b131f58279ae4611b8428f6043b36fd326f41f1850bf8a723123c3cb5f6"},{"filename":"rules/test-flaky-tests.md","content":"---\ntitle: Flaky Tests\nimpact: HIGH\nimpactDescription: \"Erode trust in CI; teach the team to ignore failures\"\ntags: testing, flaky, ci\n---\n\n## Flaky Tests\n\n**Impact: HIGH (Erode trust in CI; teach the team to ignore failures)**\n\nA flaky test — one that passes and fails on the same code — is worse than no test. The first few times, you re-run. After that, the team learns to retry-and-merge, and the suite stops catching real regressions.\n\n## How to Detect\n\n```bash\n# Run the suite N times against the same commit\nfor i in {1..20}; do npm test -- --silent || echo \"Failed run $i\"; done\n\n# Better: track flakiness across CI runs\n# - GitHub Actions: re-run-on-failure stats\n# - CircleCI / Buildkite: built-in flaky-test reports\n# - https://github.com/jonny-improbable/jest-circus-flaky-retry\n\n# Identify suspect tests by name patterns\ngrep -rEn 'sleep|setTimeout|Date\\.now|Math\\.random' tests/\n```\n\nCommon flaky-test smells:\n- `sleep(N)` / `setTimeout` instead of waiting for a condition\n- Order-dependent tests (depend on previous test's state)\n- Time-dependent assertions (`expect(date).toBe(today)`)\n- Tests against shared mutable resources (real Redis without cleanup, real DB without transactions)\n\n## Incorrect\n\n```typescript\n// ❌ Three different forms of flakiness\ntest('debounced search', async () => {\n searchInput.value = 'foo';\n await sleep(300); // race: timing-dependent\n expect(results).toHaveLength(5);\n});\n\ntest('order created today', () => {\n const order = createOrder();\n expect(order.createdAt.toDateString())\n .toBe(new Date().toDateString()); // fails when run at midnight\n});\n\ntest('user can log in', async () => {\n await db.query('INSERT INTO users ...'); // depends on previous test's cleanup\n // ...\n});\n```\n\n**Problems:**\n- Sleep races: works on fast machines, fails on busy CI runners\n- Date-dependent: fails on DST changes, midnight, leap-day\n- Shared-state: passes alone, fails in suite\n\n## Correct\n\n```typescript\n// ✅ Deterministic alternatives\ntest('debounced search', async () => {\n searchInput.value = 'foo';\n await waitFor(() => expect(results).toHaveLength(5)); // wait on condition\n});\n\ntest('order created at the expected time', () => {\n vi.setSystemTime(new Date('2026-01-15T10:00:00Z')); // freeze time\n const order = createOrder();\n expect(order.createdAt).toEqual(new Date('2026-01-15T10:00:00Z'));\n});\n\nbeforeEach(async () => {\n await db.transaction(async (t) => { /* setup, rolled back after each test */ });\n});\n```\n\n**Benefits:**\n- Test passes deterministically regardless of machine speed, clock, or order\n- Suite can run in parallel without contention\n- CI failures become signal again\n\n## Remediation Strategy\n\n- **Effort:** S per test (the fix is usually local — replace sleep with waitFor, freeze the clock, use transactions)\n- **When to pay down:**\n 1. **Quarantine** the flaky test immediately (mark as `.skip` with a tracking issue) so it stops eroding trust\n 2. **Fix** within the sprint — quarantine is a deferral, not a destination\n 3. **Delete** if it can't be made deterministic in reasonable effort\n\n**Policy:** Re-running a failed CI without a root-cause is anti-pattern. Always file an issue.\n\nReference: [Google Testing Blog — Flaky Tests](https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3349,"content_sha256":"1a26dc72d697c341b3dd307297c5edd24898c38e40f6027865a2c689dd92a8cf"},{"filename":"rules/test-slow-tests.md","content":"---\ntitle: Slow Test Suites\nimpact: HIGH\nimpactDescription: \"Slow feedback compounds across every engineer every day\"\ntags: testing, performance, ci\n---\n\n## Slow Test Suites\n\n**Impact: HIGH (Slow feedback compounds across every engineer every day)**\n\nA test suite that takes 20 minutes costs every engineer 20 minutes per push. Multiplied across team size and PR count, it becomes the single biggest tax on velocity — and pushes the team to stop running tests locally.\n\n## How to Detect\n\n```bash\n# Show slowest tests\nnpx jest --verbose # per-test timing in output\nnpx jest --reporters=jest-slow-test-reporter # 3rd-party reporter (recommended)\nnpx vitest --reporter=verbose | sort -k4 -rn | head -20\n\n# PHPUnit\nvendor/bin/phpunit --log-junit=junit.xml\n# Then sort junit.xml by time\n\n# Track total wall-clock time per CI run\n# - Watch for trends: \"is it growing 10% per quarter?\"\n```\n\nTargets to aim for:\n- **Unit suite:** \u003c 30 seconds\n- **Integration suite:** \u003c 5 minutes\n- **Total CI per PR:** \u003c 10 minutes\n\n## Incorrect\n\n```typescript\n// ❌ Common slow-test smells\n\n// 1. Real network calls in unit tests\ntest('user can fetch profile', async () => {\n const data = await fetch('https://api.production.example.com/users/1');\n});\n\n// 2. Real sleeps for \"wait for something\"\ntest('debounced search', async () => {\n search('foo');\n await sleep(2000); // 2s × 50 tests = 100s wasted\n});\n\n// 3. Full DB rebuild per test instead of transaction rollback\nbeforeEach(async () => {\n await execSync('npm run db:migrate:fresh'); // 10s × 200 tests\n});\n```\n\n## Correct\n\n```typescript\n// ✅ Fast alternatives\n// 1. Mock the network at the boundary for unit tests (MSW v2)\nimport { http, HttpResponse } from 'msw';\nimport { setupServer } from 'msw/node';\nconst server = setupServer(\n http.get('/users/1', () => HttpResponse.json(USER)),\n);\n\n// 2. Wait on conditions, not on time\nawait waitFor(() => expect(results).toHaveLength(5)); // returns in ms\n\n// 3. Use transactions or schema snapshots\nbeforeEach(async () => {\n await db.beginTransaction();\n});\nafterEach(async () => {\n await db.rollback();\n});\n```\n\nParallelize where safe:\n\n```bash\n# Jest: --maxWorkers=50%\n# Vitest: vitest --pool=threads (Vitest v1+; `--threads` is deprecated)\n# PHPUnit: vendor/bin/paratest -p 8\n```\n\n**Benefits:**\n- Engineers run tests locally → faster feedback before push\n- CI cost drops linearly with wall time\n- Slow-by-design tests (E2E) can be quarantined to a nightly job\n\n## Remediation Strategy\n\n- **Effort:** S–M per hotspot (mostly mechanical: mock, fake, paratest, transactionalize)\n- **When to pay down:**\n 1. **First win:** identify and fix the 5 slowest tests — usually 50%+ of total time\n 2. **Then:** enable parallel execution\n 3. **Then:** budget. Document a per-suite wall-clock budget and fail CI if exceeded.\n\n**Budget enforcement:**\n\n```yaml\n# Hard cap test duration in CI\n- run: timeout 600 npm test # fails CI if > 10 minutes\n```\n\nReference: [Martin Fowler — Test Suite Speed](https://martinfowler.com/articles/practical-test-pyramid.html#TheImportanceOf(Test)Speed)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3146,"content_sha256":"4bf848d3556951b4e28e8a462e5199dc8c3f6fbf8d6f4a6320769117894ebe26"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Technical Debt","type":"text"}]},{"type":"paragraph","content":[{"text":"Technical debt audit and prioritization framework for ","type":"text"},{"text":"PHP/Laravel (MySQL) and Node/TypeScript/React","type":"text","marks":[{"type":"strong"}]},{"text":" projects. Contains 42 rules across 10 categories covering code, security, design, dependency, test, performance, data, documentation, infrastructure, and process debt. Produces a ranked ledger (effort × impact) so teams know ","type":"text"},{"text":"what to fix first","type":"text","marks":[{"type":"strong"}]},{"text":", not just what's broken. Supports both ","type":"text"},{"text":"coding reference","type":"text","marks":[{"type":"strong"}]},{"text":" and ","type":"text"},{"text":"audit mode","type":"text","marks":[{"type":"strong"}]},{"text":" with PASS/FAIL/N/A output.","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 (MySQL) + Node / TypeScript / React","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rule Count:","type":"text","marks":[{"type":"strong"}]},{"text":" 42 rules across 10 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":"How to Audit","type":"text"}]},{"type":"paragraph","content":[{"text":"When the user asks to \"audit technical debt\", \"find tech debt\", or \"what should we refactor first\" — run the checklist below against their codebase and produce a ranked debt ledger.","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 arguments provided (","type":"text"},{"text":"$ARGUMENTS","type":"text","marks":[{"type":"code_inline"}]},{"text":"): audit only those paths or modules","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If no arguments: audit the entire repository starting from the root","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Audit Step 2: Detect Project Stack","type":"text"}]},{"type":"paragraph","content":[{"text":"Inspect ","type":"text"},{"text":"composer.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"package.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" to determine which of the supported stacks (PHP/Laravel, Node/TypeScript/React, or both) is in use. Tooling commands (","type":"text"},{"text":"composer outdated","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"npm outdated","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"phpstan","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"eslint","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"knip","type":"text","marks":[{"type":"code_inline"}]},{"text":", etc.) are chosen based on this detection.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Audit Step 3: Run Debt Checklist","type":"text"}]},{"type":"paragraph","content":[{"text":"Work through every item below. For each, output:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PASS","type":"text","marks":[{"type":"strong"}]},{"text":" — brief confirmation of what was verified","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"FAIL","type":"text","marks":[{"type":"strong"}]},{"text":" — exact ","type":"text"},{"text":"file:line","type":"text","marks":[{"type":"code_inline"}]},{"text":" (or command output), description of the debt, ","type":"text"},{"text":"effort estimate","type":"text","marks":[{"type":"strong"}]},{"text":" (S/M/L), and ","type":"text"},{"text":"impact","type":"text","marks":[{"type":"strong"}]},{"text":" (LOW/MED/HIGH/CRITICAL)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"N/A","type":"text","marks":[{"type":"strong"}]},{"text":" — if the check does not apply to this project","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Code Debt","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No duplicated blocks > 30 lines across files","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No function exceeds 50 lines or cyclomatic complexity > 10","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No class exceeds 300 lines or has > 15 public methods (god class)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No unreachable code, unused exports, or commented-out blocks left in source","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No magic numbers or unexplained string literals in business logic","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No function exceeds 4 parameters (use a parameter object)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Security Debt","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No secrets, API keys, or credentials committed to source (or git history)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Every public endpoint validates input via a schema / FormRequest / DTO","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Passwords hashed with bcrypt or argon2id; no MD5/SHA-1","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Sessions have a bounded lifetime; auth-sensitive endpoints rate-limited","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Security headers present (CSP, HSTS, X-Content-Type-Options, Referrer-Policy)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Authorization (not just authentication) enforced on every protected route","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Design Debt","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No circular dependencies between modules/packages","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Layers respect direction (UI → service → repository, never reversed)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No \"shotgun surgery\" patterns (one change forcing edits in 5+ files)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Abstractions hide implementation details (no leaking framework types across boundaries)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Dependency Debt","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No dependencies more than 2 major versions behind","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No abandoned/unmaintained packages (no release in > 24 months)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No known CVEs reported by ","type":"text"},{"text":"npm audit","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"composer audit","type":"text","marks":[{"type":"code_inline"}]},{"text":" at HIGH or CRITICAL","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No unused dependencies in ","type":"text"},{"text":"package.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"composer.json","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Test Debt","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Critical paths have integration test coverage","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No skipped/disabled tests without linked issue or removal date","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No known flaky tests left in main branch","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Test suite runs in under 10 minutes (or has explicit budget documented)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Performance Debt","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No N+1 query patterns on list endpoints (query count constant per request)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No list endpoint returns an unbounded result set (pagination present)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Frontend initial bundle within budget (~200 KB gzip); route-level code splitting in place","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Expensive read paths (aggregations, external APIs, static configs) are cached at a sensible layer","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Data Debt","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Migrations are the only source of schema changes; production matches migration history","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"All foreign-key and frequently-queried columns are indexed","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No orphaned child records; FK constraints enforced","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Documentation Debt","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"README reflects current setup and dev workflow","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No stale comments contradicting the code they describe","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Public APIs / exported modules have docblocks or type hints","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Infrastructure Debt","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Runtime versions (Node, PHP) are on supported (non-EOL) releases","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No deprecated framework APIs in use (e.g., Laravel ","type":"text"},{"text":"Route::get()","type":"text","marks":[{"type":"code_inline"}]},{"text":" deprecations)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Build runs cleanly with zero warnings","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Secrets stored in a manager (Vault / Doppler / cloud SM); no long-lived shared credentials","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Structured logs, error tracking, p95 latency dashboards, and SLO-based alerts in place","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Process Debt","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No ","type":"text"},{"text":"TODO","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"FIXME","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"HACK","type":"text","marks":[{"type":"code_inline"}]},{"text":" comments older than 6 months without owner or ticket","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No ","type":"text"},{"text":"@deprecated","type":"text","marks":[{"type":"code_inline"}]},{"text":" markers without a removal date or replacement","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Debt is tracked somewhere visible (issue tracker label, debt register, ADR)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"Every path has an owner (CODEOWNERS file present and current)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"No feature flags at 100% rollout for more than 6 weeks without a removal plan","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Audit Step 4: Build the Debt Ledger","type":"text"}]},{"type":"paragraph","content":[{"text":"End the audit with a prioritized table:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"## Technical Debt Ledger\n\n| # | Category | Item | File / Location | Effort | Impact | Priority |\n|---|----------|------|-----------------|--------|--------|----------|\n| 1 | deps | jQuery 1.12 (8y old, 3 CVEs) | package.json:14 | L | CRITICAL | P0 |\n| 2 | code | OrderService god class (820 lines) | app/Services/OrderService.php | M | HIGH | P1 |\n| 3 | test | 12 disabled tests in auth/ | tests/Feature/Auth/* | S | HIGH | P1 |\n...\n\n## Summary\n- **PASS:** X checks\n- **FAIL:** Y checks\n- **N/A:** Z checks\n- **Top 3 to pay down first:** (list highest-priority items with rationale)","type":"text"}]},{"type":"paragraph","content":[{"text":"Priority formula:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"P0 = CRITICAL impact","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"P1 = HIGH impact","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"P2 = MED","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"P3 = LOW","type":"text","marks":[{"type":"code_inline"}]},{"text":". Within a priority, sort by ascending effort (cheap wins first).","type":"text"}]},{"type":"paragraph","content":[{"text":"Effort scale:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"S","type":"text","marks":[{"type":"strong"}]},{"text":" = under a day","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"M","type":"text","marks":[{"type":"strong"}]},{"text":" = 1–5 days","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"L","type":"text","marks":[{"type":"strong"}]},{"text":" = more than a week (likely needs to be broken down)","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Apply","type":"text"}]},{"type":"paragraph","content":[{"text":"Reference these guidelines when:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Running a tech-debt audit on a codebase","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Planning a refactoring sprint or debt-paydown initiative","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Reviewing a PR that introduces shortcuts (and deciding whether to accept them)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Building a debt register or backlog category in your issue tracker","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Justifying engineering investment to non-engineering stakeholders","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Onboarding to a new codebase and assessing its health","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Writing an ADR (architecture decision record) for a debt-related decision","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 1: Detect Project Stack","type":"text"}]},{"type":"paragraph","content":[{"text":"Always detect the stack before running tooling.","type":"text","marks":[{"type":"strong"}]},{"text":" This skill targets PHP / Laravel (with MySQL) and Node / TypeScript / React projects; detection commands below assume one or both are present.","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"}]},{"text":" present","type":"text"}]}]},{"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":"composer outdated","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"composer audit","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"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":", ","type":"text"},{"text":"deptrac","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"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":" present","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Node / JS / TS / React","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"npm outdated","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"npm audit","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"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":", ","type":"text"},{"text":"madge","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MySQL connection available","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Database","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"EXPLAIN","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"sys.schema_tables_with_full_table_scans","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"sys.schema_unused_indexes","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"sys.statement_analysis","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"any repo","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Secrets scan","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"gitleaks git","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"trufflehog","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"paragraph","content":[{"text":"If both stacks are present (e.g., Laravel + Inertia + React), run audits for each.","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":"Code Debt","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":"code-","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":"Security Debt","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":"security-","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":"Design Debt","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":"design-","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":"Dependency Debt","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":"deps-","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 Debt","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":"Performance Debt","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":"perf-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Data Debt","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":"data-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"8","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Documentation Debt","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":"docs-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"9","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Infrastructure Debt","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":"infra-","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"10","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Process Debt","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":"process-","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. Code Debt (CRITICAL)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"code-duplication","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Detect and consolidate duplicated logic","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"code-complexity","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Cyclomatic and cognitive complexity thresholds","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"code-long-functions","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Function and method length limits","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"code-god-classes","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Class size and responsibility limits","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"code-dead-code","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Unused code, unreachable branches, commented blocks","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"code-magic-numbers","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Hardcoded literals in business logic","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"code-long-parameter-lists","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Functions with too many positional params","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Security Debt (CRITICAL)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"security-secrets-in-code","type":"text","marks":[{"type":"code_inline"}]},{"text":" — API keys, passwords, tokens in source / history","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"security-input-validation","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Endpoints accepting untrusted input without schemas","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"security-auth-hardening","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Outdated auth, missing MFA, missing security headers","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Design Debt (HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"design-tight-coupling","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Excessive direct dependencies between modules","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"design-circular-deps","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Cyclic imports / requires","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"design-leaky-abstractions","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Framework types crossing layer boundaries","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"design-shotgun-surgery","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Changes that touch many files at once","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Dependency Debt (HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"deps-outdated-versions","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Major versions behind on dependencies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"deps-abandoned-packages","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Unmaintained / abandoned libraries","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"deps-security-advisories","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Known CVEs in installed dependencies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"deps-unused-deps","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Declared but unused packages","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. Test Debt (HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"test-coverage-gaps","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Critical paths without tests","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"test-flaky-tests","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Tests with non-deterministic outcomes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"test-disabled-tests","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Skipped tests left in the suite","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"test-slow-tests","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Tests blocking fast feedback","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"6. Performance Debt (HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"perf-n-plus-one","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Linear request → quadratic database load","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"perf-missing-pagination","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Unbounded result sets","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"perf-bundle-bloat","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Heavy / unsplit frontend bundles","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"perf-no-caching","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Missing cache layers on read-heavy paths","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"7. Data Debt (HIGH)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"data-schema-drift","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Production schema diverges from migrations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"data-missing-indexes","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Hot queries doing sequential scans","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"data-orphaned-records","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Referential integrity gaps","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"8. Documentation Debt (MEDIUM)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"docs-stale-comments","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Comments contradicting current behavior","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"docs-outdated-architecture","type":"text","marks":[{"type":"code_inline"}]},{"text":" — README/architecture docs out of date","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"docs-undocumented-api","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Public APIs without docblocks or types","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"9. Infrastructure Debt (MEDIUM)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"infra-runtime-versions","type":"text","marks":[{"type":"code_inline"}]},{"text":" — EOL or near-EOL language/runtime versions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"infra-deprecated-apis","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Framework deprecations still in use","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"infra-build-warnings","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Build/compile warnings ignored","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"infra-secrets-management","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Long-lived credentials, plain env files, leaked logs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"infra-monitoring-gaps","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Missing logs, metrics, traces, alerts, or SLOs","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"10. Process Debt (MEDIUM)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"process-todo-fixme-aging","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Aging TODO/FIXME/HACK comments","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"process-deprecated-markers","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"@deprecated","type":"text","marks":[{"type":"code_inline"}]},{"text":" without removal plan","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"process-debt-tracking","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Debt visible in a register / backlog","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"process-ownership-gaps","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Code without an owning team (CODEOWNERS)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"process-feature-flags-lingering","type":"text","marks":[{"type":"code_inline"}]},{"text":" — Stale feature flags polluting code paths","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Essential Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Debt Ledger Output Format","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"| # | Category | Item | Location | Effort | Impact | Priority |\n|---|----------|-------------------------------|-----------------------|--------|----------|----------|\n| 1 | deps | guzzle 6.x (5y behind) | composer.json:18 | M | HIGH | P1 |\n| 2 | code | InvoiceService dup logic | app/Services/Invoice* | S | MEDIUM | P2 |\n| 3 | test | 8 skipped tests in checkout/ | tests/Feature/Checkout| S | HIGH | P1 |","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Effort × Impact Prioritization","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":" LOW MEDIUM HIGH CRITICAL\nS (\u003c1d) → P3 P2 P1 P0\nM (1-5d) → P3 P2 P1 P0\nL (>1w) → P3 P3 P2 P1 (break down)","type":"text"}]},{"type":"paragraph","content":[{"text":"Cheap + high-impact items go first. Expensive items always get broken down before scheduling.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Tooling Cheatsheet","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# PHP / Laravel\ncomposer outdated --direct # list outdated direct deps\ncomposer audit # known CVEs\nvendor/bin/phpstan analyse # static analysis\nvendor/bin/phpmd app text cleancode # mess detector\n\n# Node / TS\nnpm outdated # list outdated deps\nnpm audit # known CVEs\nnpx depcheck # unused deps\nnpx tsc --noEmit # type errors\nnpx eslint . --max-warnings 0 # lint warnings\n\n# Cross-language\ngit log --since=\"6 months ago\" --diff-filter=A -p | grep -E \"TODO|FIXME|HACK\"\ncloc . # lines of code per language","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"How to Use","type":"text"}]},{"type":"paragraph","content":[{"text":"Read individual rule files for detailed explanations and code examples:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"rules/code-duplication.md\nrules/design-circular-deps.md\nrules/deps-outdated-versions.md\nrules/test-flaky-tests.md\nrules/process-todo-fixme-aging.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 with metadata (title, impact, tags)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Brief explanation of why it matters","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"How to detect (commands / patterns)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Incorrect example with explanation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Correct example or remediation strategy","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"References","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Martin Fowler — Technical Debt Quadrant","type":"text","marks":[{"type":"link","attrs":{"href":"https://martinfowler.com/bliki/TechnicalDebtQuadrant.html","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ward Cunningham — The Debt Metaphor","type":"text","marks":[{"type":"link","attrs":{"href":"https://www.youtube.com/watch?v=pqeJFYwnkjE","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SonarQube — Technical Debt Model","type":"text","marks":[{"type":"link","attrs":{"href":"https://docs.sonarsource.com/sonarqube/latest/user-guide/metric-definitions/","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OWASP Dependency-Check","type":"text","marks":[{"type":"link","attrs":{"href":"https://owasp.org/www-project-dependency-check/","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Snyk Open Source Vulnerability Database","type":"text","marks":[{"type":"link","attrs":{"href":"https://security.snyk.io/","title":null}}]}]}]}]},{"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":"technical-debt","author":"@skillopedia","source":{"stars":39,"repo_name":"agent-skills","origin_url":"https://github.com/asyrafhussin/agent-skills/blob/HEAD/skills/technical-debt/SKILL.md","repo_owner":"asyrafhussin","body_sha256":"5e6422156c896a2c58cc4931a025c8c55c64369c658fd08db5d770aa7807d15f","cluster_key":"fe75b205cb770f9df1d43153554a71d282cb2c47906be5ba9cdedbe588debaad","clean_bundle":{"format":"clean-skill-bundle-v1","source":"asyrafhussin/agent-skills/skills/technical-debt/SKILL.md","attachments":[{"id":"1c294745-d8f4-51ce-9a50-0466dd588abf","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1c294745-d8f4-51ce-9a50-0466dd588abf/attachment.md","path":"AGENTS.md","size":152566,"sha256":"63fd21d4547a2918fa09d6bb6447e84955756232d71889ccf35e622405df34d4","contentType":"text/markdown; charset=utf-8"},{"id":"59e560a3-4477-5bbc-852e-63a9501c1f67","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/59e560a3-4477-5bbc-852e-63a9501c1f67/attachment.md","path":"README.md","size":3269,"sha256":"04238ed998fd6a6925df5fcef09f57bbe786710806ef757b59c505f5b462c28f","contentType":"text/markdown; charset=utf-8"},{"id":"48819c69-d199-5215-871d-377dd5c805a7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/48819c69-d199-5215-871d-377dd5c805a7/attachment.json","path":"metadata.json","size":5408,"sha256":"78614ba9cf05d0d044f1f764e6eca02c9ef278000e97ea2a036251b40435abab","contentType":"application/json; charset=utf-8"},{"id":"422563fa-00ee-5059-9e8e-fc14566cc261","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/422563fa-00ee-5059-9e8e-fc14566cc261/attachment.md","path":"rules/_sections.md","size":3126,"sha256":"6732ba7dffa9d7b5fb3ad5e15e098367c22b8e27cc112e9b56017aaf4a016a7c","contentType":"text/markdown; charset=utf-8"},{"id":"eefed77f-713b-5f4c-8ea1-0c9865e5f223","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/eefed77f-713b-5f4c-8ea1-0c9865e5f223/attachment.md","path":"rules/_template.md","size":759,"sha256":"6dc8ebf76ba6ffd6ae6dfbf85d42a3113c7169ce7fceb290905087769b12b633","contentType":"text/markdown; charset=utf-8"},{"id":"5b01caf1-d935-51f1-8075-dbbacb180d84","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5b01caf1-d935-51f1-8075-dbbacb180d84/attachment.md","path":"rules/code-complexity.md","size":2784,"sha256":"accce180e05973b49228f542a8f4e404300a2083735d4d30d1eea1d07b38d208","contentType":"text/markdown; charset=utf-8"},{"id":"b2e7d2be-503e-5209-b4ac-efa722052fc4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b2e7d2be-503e-5209-b4ac-efa722052fc4/attachment.md","path":"rules/code-dead-code.md","size":2871,"sha256":"76a188ae813e5c4e53331a77f215baf8ed19828a834ee23ba1ed1c4ed32da20d","contentType":"text/markdown; charset=utf-8"},{"id":"1e2a9a6d-53c5-5620-974a-bdf5f2ffd844","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1e2a9a6d-53c5-5620-974a-bdf5f2ffd844/attachment.md","path":"rules/code-duplication.md","size":2842,"sha256":"0e8813342d862405fcb04c6a7f95a0d6a428e1a505f47e236ec80d7c8d29036a","contentType":"text/markdown; charset=utf-8"},{"id":"9eed7044-d3b8-5a32-b342-0da5e0794281","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9eed7044-d3b8-5a32-b342-0da5e0794281/attachment.md","path":"rules/code-god-classes.md","size":3380,"sha256":"fae2d7bf58c4d2a143119b12a2bd6cb63a3170e40f622d8989dd85bfab3f5f86","contentType":"text/markdown; charset=utf-8"},{"id":"1caaf6f1-419a-5563-a6ae-efeb2e721871","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1caaf6f1-419a-5563-a6ae-efeb2e721871/attachment.md","path":"rules/code-long-functions.md","size":3262,"sha256":"3fc310c53bc5a2ad0bd7346f90c5484721227596c6c6bc9e89a2d46d6d24a5c3","contentType":"text/markdown; charset=utf-8"},{"id":"942289ac-a5f9-5f32-9c0f-ba843e4918d3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/942289ac-a5f9-5f32-9c0f-ba843e4918d3/attachment.md","path":"rules/code-long-parameter-lists.md","size":3567,"sha256":"83757cd0023e8516de60a251967c36edf20bbda7f245e63b4ec0e9e971d73ad2","contentType":"text/markdown; charset=utf-8"},{"id":"5d884ce8-25cc-5fc0-bd78-c2f7a369d80a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5d884ce8-25cc-5fc0-bd78-c2f7a369d80a/attachment.md","path":"rules/code-magic-numbers.md","size":3704,"sha256":"054b69bff399be6d4c5072a608617b907fd279dba3de491d38c043fec09e87dd","contentType":"text/markdown; charset=utf-8"},{"id":"dce10468-cd95-5739-89fb-da58a53760b6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dce10468-cd95-5739-89fb-da58a53760b6/attachment.md","path":"rules/data-missing-indexes.md","size":4801,"sha256":"9e28632dd73e3672cc8e3f01197aee45652ae8dadcfeb1273da181a91904217b","contentType":"text/markdown; charset=utf-8"},{"id":"e1bab407-2a56-5665-ac0d-055dda69386f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e1bab407-2a56-5665-ac0d-055dda69386f/attachment.md","path":"rules/data-orphaned-records.md","size":5680,"sha256":"94301e930057f85b783e68439de41f7c39f1bd16a48ad72d7d12dc8c43b36c31","contentType":"text/markdown; charset=utf-8"},{"id":"9d3bbb81-2962-5f67-b02e-4fb0f1f4eccb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9d3bbb81-2962-5f67-b02e-4fb0f1f4eccb/attachment.md","path":"rules/data-schema-drift.md","size":4546,"sha256":"5a997454f715743dae6bd18a0038b9b3d72d5fddbc791ed7d00b06a16c308ff0","contentType":"text/markdown; charset=utf-8"},{"id":"d5cb2c2b-4fc0-5c47-a5bd-b233ee91c6c7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d5cb2c2b-4fc0-5c47-a5bd-b233ee91c6c7/attachment.md","path":"rules/deps-abandoned-packages.md","size":3074,"sha256":"d005cfd67e1bef7a43f808ed1138deed35ed9187b9d55be9eb65addf2b6a4229","contentType":"text/markdown; charset=utf-8"},{"id":"ba6006f5-0c87-544d-a913-285d13fcd167","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ba6006f5-0c87-544d-a913-285d13fcd167/attachment.md","path":"rules/deps-outdated-versions.md","size":2647,"sha256":"7f4a9233654eeab71584a8ae80b62b8746339c61a5ce56a4359b371fa7565eff","contentType":"text/markdown; charset=utf-8"},{"id":"d5d5715b-793a-5e78-9bb5-77026c04df76","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d5d5715b-793a-5e78-9bb5-77026c04df76/attachment.md","path":"rules/deps-security-advisories.md","size":2430,"sha256":"8ef11595d9484cc4646f0957036189e2bdb3b2f10448b39e89bbed443ed8ba9d","contentType":"text/markdown; charset=utf-8"},{"id":"df7d0d7a-d797-5971-8fe7-216b7f8e62dc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/df7d0d7a-d797-5971-8fe7-216b7f8e62dc/attachment.md","path":"rules/deps-unused-deps.md","size":2463,"sha256":"83aab6760ad3b09784f0522cc4ab6820d4340d75105288ac0b182a16fabfa795","contentType":"text/markdown; charset=utf-8"},{"id":"814109f0-094e-5801-bc41-4ecc6f2a974b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/814109f0-094e-5801-bc41-4ecc6f2a974b/attachment.md","path":"rules/design-circular-deps.md","size":2448,"sha256":"b1b8dcdd4ddc4e5e8b3016f3b02269c377506cdf2ccfa2b97df801000dd351bc","contentType":"text/markdown; charset=utf-8"},{"id":"bc6f2e70-dd78-5f62-9b2e-a8fa108c6a7b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bc6f2e70-dd78-5f62-9b2e-a8fa108c6a7b/attachment.md","path":"rules/design-leaky-abstractions.md","size":2958,"sha256":"85dd8c3f6d41912e14c319e9b3bd0af1bdde6e538fde589a8cb6320063037959","contentType":"text/markdown; charset=utf-8"},{"id":"ee48c040-a86b-550f-8b48-d9263ec76529","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ee48c040-a86b-550f-8b48-d9263ec76529/attachment.md","path":"rules/design-shotgun-surgery.md","size":2826,"sha256":"39904f75256f62aa4821ab3337fa19717939e72f9c1e75b1a9a3e338f859de15","contentType":"text/markdown; charset=utf-8"},{"id":"027befc1-4bc8-55f4-9372-853af61e6274","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/027befc1-4bc8-55f4-9372-853af61e6274/attachment.md","path":"rules/design-tight-coupling.md","size":2872,"sha256":"6439589a54e318ba2d2f38b72024284ee033ef57678b343e8f720d9cab81dffe","contentType":"text/markdown; charset=utf-8"},{"id":"53401db3-3947-595e-8eb3-0744d41fabfd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/53401db3-3947-595e-8eb3-0744d41fabfd/attachment.md","path":"rules/docs-outdated-architecture.md","size":3755,"sha256":"60c3bc615dfe90508e0261925f0c2e81cdc559a38464ddcfb8269b1468871f99","contentType":"text/markdown; charset=utf-8"},{"id":"07ac2b12-6345-5668-b22e-15c8e7cf05df","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/07ac2b12-6345-5668-b22e-15c8e7cf05df/attachment.md","path":"rules/docs-stale-comments.md","size":3450,"sha256":"0bd82b95ae89150bcf81b32a88f3bc33971075039a83078422778548e32ea58f","contentType":"text/markdown; charset=utf-8"},{"id":"20042d16-3fbc-5046-bcf5-96d559648092","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/20042d16-3fbc-5046-bcf5-96d559648092/attachment.md","path":"rules/docs-undocumented-api.md","size":3828,"sha256":"0671e07063a0545023a5f8df7472c11500f711b8a61762dd97900c1814664304","contentType":"text/markdown; charset=utf-8"},{"id":"b665bf9b-436b-5f2f-955d-71b523c25d5a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b665bf9b-436b-5f2f-955d-71b523c25d5a/attachment.md","path":"rules/infra-build-warnings.md","size":3313,"sha256":"51073c2a7cd6de7d2e3ddeaed41a22b4171807ac032316d3f66836923e360d35","contentType":"text/markdown; charset=utf-8"},{"id":"e5857b2f-dc0d-5d16-8a49-b16175b4116b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e5857b2f-dc0d-5d16-8a49-b16175b4116b/attachment.md","path":"rules/infra-deprecated-apis.md","size":3198,"sha256":"02a494647d8695100dddbe24f71fd1ac9db9ffcf78c45a476b16623959fb74da","contentType":"text/markdown; charset=utf-8"},{"id":"4104aaeb-912c-5f4a-8b66-babb7ae1bb7f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4104aaeb-912c-5f4a-8b66-babb7ae1bb7f/attachment.md","path":"rules/infra-monitoring-gaps.md","size":6069,"sha256":"d77d1bc04c7a26feb418cdee545de37ea526d521afde17ad2c278c7221ae38a0","contentType":"text/markdown; charset=utf-8"},{"id":"83092929-4e9a-5c80-9daf-9c8a4eb66e8a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/83092929-4e9a-5c80-9daf-9c8a4eb66e8a/attachment.md","path":"rules/infra-runtime-versions.md","size":3031,"sha256":"6617914cbc4953e4399c8486f26b0d77aaf432e858b26bf8cb940ef2493db085","contentType":"text/markdown; charset=utf-8"},{"id":"0c191165-d70c-56c9-98c7-430a7fc963a0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0c191165-d70c-56c9-98c7-430a7fc963a0/attachment.md","path":"rules/infra-secrets-management.md","size":4423,"sha256":"caab862a51854f0b3ce7a178614277c35069251a1133a2f4e22a14cf533eb5ca","contentType":"text/markdown; charset=utf-8"},{"id":"00f99e9d-70f0-54c6-8e89-11b0988a5259","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/00f99e9d-70f0-54c6-8e89-11b0988a5259/attachment.md","path":"rules/perf-bundle-bloat.md","size":3780,"sha256":"3a522929ef8a92f8e80170b5369474cbdf1048e7bc70afe4b918d0394633393f","contentType":"text/markdown; charset=utf-8"},{"id":"e5c27a7f-1fa1-55a0-b1ab-37f26dd3983b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e5c27a7f-1fa1-55a0-b1ab-37f26dd3983b/attachment.md","path":"rules/perf-missing-pagination.md","size":3981,"sha256":"60d9ea94efba5416d96361940010d99841fb5f3e2ced08a3bed7439df36513f3","contentType":"text/markdown; charset=utf-8"},{"id":"35b4a95b-3a05-5ddb-91fd-1c8634be6558","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/35b4a95b-3a05-5ddb-91fd-1c8634be6558/attachment.md","path":"rules/perf-n-plus-one.md","size":3808,"sha256":"893909a2e7d2f0b976d3e8967eda6ed6ea7721ddb884a94e3d3950f5fb2e1f6e","contentType":"text/markdown; charset=utf-8"},{"id":"0d55fea6-9d8b-5d9e-b222-69677586e64f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0d55fea6-9d8b-5d9e-b222-69677586e64f/attachment.md","path":"rules/perf-no-caching.md","size":5486,"sha256":"c019e02098375e3eef3ccd0e1a50a6a2d9a0b87c5c747442d60779779f039d1f","contentType":"text/markdown; charset=utf-8"},{"id":"eb5da4f7-3576-558d-a256-73acbb9262e6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/eb5da4f7-3576-558d-a256-73acbb9262e6/attachment.md","path":"rules/process-debt-tracking.md","size":3720,"sha256":"5ed244d0285eb8a4e3101dbf2501452b46b21ae5a57513dcefc3aba67bc05b7c","contentType":"text/markdown; charset=utf-8"},{"id":"72f4117c-5454-5785-8913-f41ee060b159","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/72f4117c-5454-5785-8913-f41ee060b159/attachment.md","path":"rules/process-deprecated-markers.md","size":3383,"sha256":"f399b7289253d922f8816ff5a93a51e519b37bcacfe64bc15ecf64306f327d2c","contentType":"text/markdown; charset=utf-8"},{"id":"d3056a06-2260-51ca-9dfc-0ac77b9f4527","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d3056a06-2260-51ca-9dfc-0ac77b9f4527/attachment.md","path":"rules/process-feature-flags-lingering.md","size":5247,"sha256":"5e54225e4a2ea4ecc55d0725c97e7693a9b32d8fb61a08f2b984f042e405c39e","contentType":"text/markdown; charset=utf-8"},{"id":"f6c1ab21-a640-570f-b8b2-037400209033","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f6c1ab21-a640-570f-b8b2-037400209033/attachment.md","path":"rules/process-ownership-gaps.md","size":4449,"sha256":"6fd073ffcb54dce7fc06386301a3427c9422652c0b2d21e7244c251f3b9631d6","contentType":"text/markdown; charset=utf-8"},{"id":"81eddb51-2004-5f77-9ae6-69039407ad00","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/81eddb51-2004-5f77-9ae6-69039407ad00/attachment.md","path":"rules/process-todo-fixme-aging.md","size":3381,"sha256":"d5c0b7acaf2e8957723e280bf09661f4f2502c9e8fc721ebeaaf5f004d9595c7","contentType":"text/markdown; charset=utf-8"},{"id":"aabde29e-a0fc-5171-b60a-140426570a0c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/aabde29e-a0fc-5171-b60a-140426570a0c/attachment.md","path":"rules/security-auth-hardening.md","size":4886,"sha256":"e50d1b58c4b9fd2a45c40fa65bd6d7ab8c04ab6c0daf114e6cc9782e7f29bdcf","contentType":"text/markdown; charset=utf-8"},{"id":"290424ee-518d-53b6-bcb7-cbc2f05b2d8b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/290424ee-518d-53b6-bcb7-cbc2f05b2d8b/attachment.md","path":"rules/security-input-validation.md","size":5045,"sha256":"37cfa478dccd75fa099ca248265285933b1971a5690b486fedfeadd792f30871","contentType":"text/markdown; charset=utf-8"},{"id":"6012a8aa-f34f-5e98-95fe-15e089d60562","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6012a8aa-f34f-5e98-95fe-15e089d60562/attachment.md","path":"rules/security-secrets-in-code.md","size":3998,"sha256":"15922420e67cfd015bf24a3521ac061b720d462ec9d6e8b22af2414bac448a73","contentType":"text/markdown; charset=utf-8"},{"id":"f0c973b7-c171-5d96-bc32-a264ed449729","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f0c973b7-c171-5d96-bc32-a264ed449729/attachment.md","path":"rules/test-coverage-gaps.md","size":3324,"sha256":"caf4753704ace83210c8b344f086cad2980f32dfe17121bc0f3b4f59e83c61fd","contentType":"text/markdown; charset=utf-8"},{"id":"91594060-c537-5cfe-99b8-c6de1fe424b5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/91594060-c537-5cfe-99b8-c6de1fe424b5/attachment.md","path":"rules/test-disabled-tests.md","size":2587,"sha256":"82685b131f58279ae4611b8428f6043b36fd326f41f1850bf8a723123c3cb5f6","contentType":"text/markdown; charset=utf-8"},{"id":"e6d3f530-45dc-51c2-acd3-12f1d765711b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e6d3f530-45dc-51c2-acd3-12f1d765711b/attachment.md","path":"rules/test-flaky-tests.md","size":3349,"sha256":"1a26dc72d697c341b3dd307297c5edd24898c38e40f6027865a2c689dd92a8cf","contentType":"text/markdown; charset=utf-8"},{"id":"f0372120-8380-5574-a720-8df69a6d6f88","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f0372120-8380-5574-a720-8df69a6d6f88/attachment.md","path":"rules/test-slow-tests.md","size":3146,"sha256":"4bf848d3556951b4e28e8a462e5199dc8c3f6fbf8d6f4a6320769117894ebe26","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"7254c2f1b26b7c2ddb107fb24598702e873d4f0c51ab0547a1d62aba655b9905","attachment_count":47,"text_attachments":47,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/technical-debt/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":"Technical debt inventory, prioritization, and audit for PHP/Laravel (MySQL) and Node/TypeScript/React projects. Use when assessing code health, identifying refactoring candidates, planning debt paydown, or auditing a codebase for accumulated debt. Triggers on \"audit technical debt\", \"find tech debt\", \"debt inventory\", \"what should we refactor first\", or tasks involving code health, security debt, performance debt, data debt, observability debt, debt prioritization, or remediation planning."}},"renderedAt":1782986986324}

Technical Debt Technical debt audit and prioritization framework for PHP/Laravel (MySQL) and Node/TypeScript/React projects. Contains 42 rules across 10 categories covering code, security, design, dependency, test, performance, data, documentation, infrastructure, and process debt. Produces a ranked ledger (effort × impact) so teams know what to fix first , not just what's broken. Supports both coding reference and audit mode with PASS/FAIL/N/A output. Metadata - Version: 1.0.0 - Scope: PHP / Laravel (MySQL) + Node / TypeScript / React - Rule Count: 42 rules across 10 categories - License: MI…