TS Reuse Review Scans a TypeScript/JavaScript diff for code that reinvents existing utilities. Reports a prioritized list of reuse opportunities — external libs (es-toolkit, date-fns, zod), ES2020+ native APIs, installed project libs (effect, remeda, ts-pattern, etc.), and already-existing internal helpers. Never applies edits. Prerequisites - ripgrep — used for internal helper search. Install: or . - ast-grep — structural pattern matching. Install: or . If is missing, fall back to -only detection and flag the degraded mode in the report header. Do not abort. Workflow Do not read script sourc…

+ $N.toFixed(2)\"\n - pattern: '\"$\" + $N.toFixed(2)'\nmetadata:\n replace_native: \"new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format($N)\"\n priority: P3\n fallback_regex: \"\\\\$\\\\{\\\\w+\\\\.toFixed\\\\(2\\\\)\\\\}|['\\\"]\\\\$['\\\"]\\\\s*\\\\+\\\\s*\\\\w+\\\\.toFixed\"\n notes: \"Hardcoded '

TS Reuse Review Scans a TypeScript/JavaScript diff for code that reinvents existing utilities. Reports a prioritized list of reuse opportunities — external libs (es-toolkit, date-fns, zod), ES2020+ native APIs, installed project libs (effect, remeda, ts-pattern, etc.), and already-existing internal helpers. Never applies edits. Prerequisites - ripgrep — used for internal helper search. Install: or . - ast-grep — structural pattern matching. Install: or . If is missing, fall back to -only detection and flag the degraded mode in the report header. Do not abort. Workflow Do not read script sourc…

fails for locales/currencies. Intl handles euro/yen/rounding/spacing.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":721,"content_sha256":"6236e34006d635b21098ebb158534aed75ebb756f628c26bda3f2c7f48b1bb25"},{"filename":"scripts/patterns/date-add-days.yml","content":"id: date-add-days\nlanguage: TypeScript\nmessage: \"Manual date arithmetic via milliseconds. Use addDays from date-fns for clarity + DST safety.\"\nseverity: info\nrule:\n any:\n - pattern: new Date($D.getTime() + $N * 86400000)\n - pattern: new Date($D.getTime() + $N * 24 * 60 * 60 * 1000)\n - pattern: new Date($D.getTime() - $N * 86400000)\nmetadata:\n replace_catalog: \"import { addDays } from 'date-fns'; addDays($D, $N)\"\n priority: P2\n fallback_regex: \"getTime\\\\(\\\\)\\\\s*[+-]\\\\s*[^;]*86400000\"\n notes: \"Manual ms math ignores DST transitions. date-fns handles them.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":575,"content_sha256":"07aaf9db74b4edb70aee0dcc38e387bf5964f72d7072f2f5f4c2fc136ffa2095"},{"filename":"scripts/patterns/date-getday-weekday.yml","content":"id: date-getday-weekday\nlanguage: TypeScript\nmessage: \"Manual weekday-name lookup from getDay(). Use date-fns format(d, 'EEEE') or Intl.DateTimeFormat({ weekday: 'long' }).\"\nseverity: info\nrule:\n any:\n - pattern: \"['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][$D.getDay()]\"\n - pattern: \"['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][$D.getDay()]\"\n - pattern: \"[\\\"Sun\\\", \\\"Mon\\\", \\\"Tue\\\", \\\"Wed\\\", \\\"Thu\\\", \\\"Fri\\\", \\\"Sat\\\"][$D.getDay()]\"\n - pattern: \"[\\\"Sunday\\\", \\\"Monday\\\", \\\"Tuesday\\\", \\\"Wednesday\\\", \\\"Thursday\\\", \\\"Friday\\\", \\\"Saturday\\\"][$D.getDay()]\"\nmetadata:\n replace_native: \"new Intl.DateTimeFormat(locale, { weekday: 'long' }).format($D)\"\n replace_catalog: \"import { format } from 'date-fns'; format($D, 'EEEE', { locale: $LOC })\"\n priority: P2\n fallback_regex: \"\\\\[\\\\s*['\\\"]Sun['\\\"]\\\\s*,[\\\\s\\\\S]{0,150}['\\\"]Sat['\\\"]\\\\s*\\\\]\\\\s*\\\\[\\\\s*\\\\w+\\\\.getDay\\\\(\\\\)\\\\s*\\\\]\"\n notes: \"Hardcoded English weekdays break for i18n. Intl handles locale.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":1003,"content_sha256":"8ee452d8c28a5571d7b76581b1000d6e2e089993cba3e0b0eaeac5af7c558ae5"},{"filename":"scripts/patterns/debounce-settimeout.yml","content":"id: debounce-settimeout\nlanguage: TypeScript\nmessage: \"Manual debounce via clearTimeout/setTimeout. Use debounce from es-toolkit.\"\nseverity: info\nrule:\n all:\n - any:\n - kind: arrow_function\n - kind: function_expression\n - kind: function_declaration\n - has:\n pattern: \"clearTimeout($$)\"\n stopBy: end\n - has:\n pattern: \"setTimeout($$)\"\n stopBy: end\nmetadata:\n replace_catalog: \"import { debounce } from 'es-toolkit'; debounce(fn, ms)\"\n priority: P1\n fallback_regex: \"clearTimeout\\\\([^)]*\\\\)[\\\\s\\\\S]{0,200}setTimeout\\\\(\"\n notes: \"Heuristic — any fn containing both clearTimeout and setTimeout is suspicious. Manual review to distinguish debounce vs legitimate timer management.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":743,"content_sha256":"beebe53acd5718fb81192d8e6a5d4d5b52902b231819279603cfa6637a5cfb3f"},{"filename":"scripts/patterns/deep-clone-json.yml","content":"id: deep-clone-json\nlanguage: TypeScript\nmessage: \"JSON.parse(JSON.stringify(x)) silently drops Dates/Maps/functions. Use structuredClone(x) (native) or cloneDeep from es-toolkit/lodash.\"\nseverity: info\nrule:\n pattern: JSON.parse(JSON.stringify($X))\nmetadata:\n replace_native: \"structuredClone($X)\"\n replace_catalog: \"cloneDeep($X) // from es-toolkit\"\n priority: P1\n fallback_regex: \"JSON\\\\.parse\\\\(\\\\s*JSON\\\\.stringify\\\\(\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":430,"content_sha256":"5a6e714cf615c940b2508a3f43ee771af63108d3c2cad40c28c4cc1544a86246"},{"filename":"scripts/patterns/difference-in-days.yml","content":"id: difference-in-days\nlanguage: TypeScript\nmessage: \"Manual day difference via ms arithmetic. Use differenceInDays from date-fns — DST-safe.\"\nseverity: info\nrule:\n any:\n - pattern: \"Math.floor(($A.getTime() - $B.getTime()) / 86400000)\"\n - pattern: \"Math.floor(($A - $B) / 86400000)\"\n - pattern: \"Math.round(($A.getTime() - $B.getTime()) / 86400000)\"\nmetadata:\n replace_catalog: \"import { differenceInDays } from 'date-fns'; differenceInDays($A, $B)\"\n priority: P2\n fallback_regex: \"Math\\\\.(floor|round)\\\\([^)]*getTime\\\\(\\\\)[^)]*/ ?86400000\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":557,"content_sha256":"229475c91b7e53be996083f8e6ee4a094cb988077247158d74d4992908e23623"},{"filename":"scripts/patterns/duration-ms-arith.yml","content":"id: duration-ms-arith\nlanguage: TypeScript\nmessage: \"Inline milliseconds arithmetic. If effect is installed, prefer Duration helpers (Duration.hours/minutes/seconds) — self-documenting + DST-aware for date diffs.\"\nseverity: info\nrule:\n any:\n - pattern: \"$N * 60 * 60 * 1000\"\n - pattern: \"$N * 60 * 1000\"\n - pattern: \"$N * 24 * 60 * 60 * 1000\"\n - pattern: \"$N * 86400000\"\n - pattern: \"$N * 3600000\"\n - pattern: \"$N * 60000\"\nmetadata:\n replace_catalog: \"import { Duration } from 'effect'; Duration.hours($N) // or: Duration.minutes / Duration.seconds / Duration.days\"\n priority: P3\n fallback_regex: \"\\\\b\\\\w+\\\\s*\\\\*\\\\s*(60\\\\s*\\\\*\\\\s*60\\\\s*\\\\*\\\\s*1000|60\\\\s*\\\\*\\\\s*1000|86400000|3600000)\\\\b\"\n notes: \"Only P3 when effect not installed — inline ms is not a reuse violation by itself. P2 when effect detected.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":833,"content_sha256":"b84eb8d193b8a34db99d7d0d3f6976a3f8976db702c376a69167258d80479424"},{"filename":"scripts/patterns/effect-all-promise-all.yml","content":"id: effect-all-promise-all\nlanguage: TypeScript\nmessage: \"Promise.all inside Effect.gen context. Use Effect.all — keeps error channel typed and composable.\"\nseverity: info\nrule:\n kind: call_expression\n all:\n - has:\n pattern: \"Promise.all($_)\"\n stopBy: end\n - inside:\n any:\n - pattern: \"Effect.gen(function* () { $$ })\"\n - pattern: \"Effect.gen(function* ($_) { $$ })\"\n - pattern: \"Effect.fn($$)\"\n stopBy: end\nmetadata:\n replace_catalog: \"yield* Effect.all(effects, { concurrency: 'unbounded' })\"\n priority: P2\n fallback_regex: \"Effect\\\\.gen[\\\\s\\\\S]{0,500}Promise\\\\.all\"\n notes: \"Only suggest if 'effect' is in installed libs. Promise.all in Effect.gen breaks error-channel typing and prevents structured concurrency.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":789,"content_sha256":"2c4cf4a87f87ced93f4ff5cd377556f5481527fc1fc69b2f6b9311e8d33fda9d"},{"filename":"scripts/patterns/effect-tagged-error.yml","content":"id: effect-tagged-error\nlanguage: TypeScript\nmessage: \"Custom Error class with manual _tag. Use Data.TaggedError from effect for ergonomics + correct equality/printing.\"\nseverity: info\nrule:\n any:\n - pattern: |\n class $NAME extends Error {\n readonly _tag = $TAG\n $$\n }\n - pattern: |\n class $NAME extends Error {\n readonly _tag: $_ = $TAG\n $$\n }\nmetadata:\n replace_catalog: \"import { Data } from 'effect'; class $NAME extends Data.TaggedError('$NAME')\u003c{ ... }> {}\"\n priority: P2\n fallback_regex: \"class\\\\s+\\\\w+\\\\s+extends\\\\s+Error[\\\\s\\\\S]{0,200}readonly\\\\s+_tag\"\n notes: \"Only suggest if 'effect' is in installed libs. Data.TaggedError provides structural equality + Match.tag compatibility.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":769,"content_sha256":"42381dffe5f763b62f790fbf3ed44d37ffa2366da05d32a19861b0ff788110dc"},{"filename":"scripts/patterns/email-regex.yml","content":"id: email-regex\nlanguage: TypeScript\nmessage: \"Hand-rolled email regex. Use zod z.string().email() — tested, spec-compliant, typed.\"\nseverity: info\nrule:\n any:\n - pattern: \"/^[^\\\\s@]+@[^\\\\s@]+\\\\.[^\\\\s@]+$/.test($STR)\"\n - pattern: \"/^[\\\\w.+-]+@[\\\\w-]+\\\\.[\\\\w.-]+$/.test($STR)\"\n - pattern: \"/\\\\S+@\\\\S+\\\\.\\\\S+/.test($STR)\"\nmetadata:\n replace_catalog: \"import { z } from 'zod'; z.string().email().safeParse($STR).success\"\n priority: P2\n fallback_regex: \"/[^/]*@[^/]*\\\\.[^/]*/\\\\s*\\\\.test\"\n notes: \"Email regexes are famously wrong (RFC 5322 is hard). zod uses a battle-tested pattern and produces a typed Result.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":625,"content_sha256":"7117ea8e34134f406bdeb420e8434941dc1b54251c24a91e9d08880a00904c0e"},{"filename":"scripts/patterns/exhaust-never-helper.yml","content":"id: exhaust-never-helper\nlanguage: TypeScript\nmessage: \"Hand-rolled exhaustiveness helper. Use Match.exhaustive (effect) or import assertNever from ts-pattern / type-fest.\"\nseverity: info\nrule:\n any:\n - pattern: \"const exhaust = ($V: never): never => $V\"\n - pattern: \"const exhaust = ($V: never) => $V\"\n - pattern: \"function exhaust($V: never): never { return $V }\"\n - pattern: \"const assertNever = ($V: never): never => { throw new Error($$) }\"\nmetadata:\n replace_catalog: \"import { Match } from 'effect'; Match.exhaustive // or: import { assertNever } from 'ts-pattern'\"\n priority: P2\n fallback_regex: \"\\\\(\\\\s*\\\\w+\\\\s*:\\\\s*never\\\\s*\\\\)\\\\s*(:\\\\s*never\\\\s*)?=>\"\n notes: \"Only flag when paired with a switch-on-tag or similar union-exhaustion site — otherwise the helper has no replacement.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":812,"content_sha256":"b026aa7652c77e48020973163d02b9bc2d9e6bc9f70b8112e6a37d1c71dfd60e"},{"filename":"scripts/patterns/findindex-splice-remove.yml","content":"id: findindex-splice-remove\nlanguage: TypeScript\nmessage: \"findIndex + splice to remove an item. Prefer arr.filter() — non-mutating, one pass, no index arithmetic.\"\nseverity: info\nrule:\n kind: statement_block\n all:\n - has:\n any:\n - pattern: \"const $I = $ARR.findIndex($PRED)\"\n - pattern: \"let $I = $ARR.findIndex($PRED)\"\n stopBy: end\n - has:\n any:\n - pattern: \"$ARR.splice($I, 1)\"\n - pattern: \"if ($I !== -1) $ARR.splice($I, 1)\"\n - pattern: \"if ($I >= 0) $ARR.splice($I, 1)\"\n stopBy: end\nmetadata:\n replace_native: \"$ARR = $ARR.filter(x => !($PRED)(x)) // or keep splice if in-place mutation is required\"\n priority: P2\n fallback_regex: \"findIndex\\\\([\\\\s\\\\S]{0,200}splice\\\\(\\\\s*\\\\w+\\\\s*,\\\\s*1\\\\s*\\\\)\"\n notes: \"Only suggest filter if the array is reassigned or local. If callers hold a reference and rely on mutation, keep splice.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":918,"content_sha256":"045ea0420222288101275ee7237dac4da3ec33f27b28023a08a30c1a50abcf91"},{"filename":"scripts/patterns/for-push-spread.yml","content":"id: for-push-spread\nlanguage: TypeScript\nmessage: \"for-loop pushing each element of another array into existing array. Use arr.push(...other) native.\"\nseverity: info\nrule:\n any:\n - pattern: |\n for (const $X of $SRC) {\n $DEST.push($X);\n }\n - pattern: |\n for (const $X of $SRC) $DEST.push($X);\n - pattern: |\n $SRC.forEach(($X) => $DEST.push($X));\nmetadata:\n replace_native: \"$DEST.push(...$SRC)\"\n priority: P2\n fallback_regex: \"for\\\\s*\\\\([^)]*of\\\\s+\\\\w+\\\\)[\\\\s\\\\S]{0,80}\\\\.push\\\\(\\\\s*\\\\w+\\\\s*\\\\)\"\n notes: \"Only for 'push each' pattern. If loop has other logic (filter/transform), keep the loop.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":645,"content_sha256":"b63711969a283c5e05388836a6c547e65db45014a435746606cf867e221d0327"},{"filename":"scripts/patterns/fs-readfile-callback.yml","content":"id: fs-readfile-callback\nlanguage: TypeScript\nmessage: \"Node fs callback API. Use fs/promises — async/await, typed, composable.\"\nseverity: info\nrule:\n any:\n - pattern: \"fs.readFile($PATH, $ENC, ($ERR, $DATA) => { $$ })\"\n - pattern: \"fs.readFile($PATH, ($ERR, $DATA) => { $$ })\"\n - pattern: \"fs.writeFile($PATH, $DATA, ($ERR) => { $$ })\"\n - pattern: \"fs.stat($PATH, ($ERR, $STATS) => { $$ })\"\nmetadata:\n replace_native: \"import { readFile } from 'node:fs/promises'; const data = await readFile($PATH, 'utf8')\"\n priority: P2\n fallback_regex: \"fs\\\\.(readFile|writeFile|stat|readdir|mkdir|unlink|rename)\\\\([^)]+,\\\\s*\\\\([^)]*err[^)]*\\\\)\\\\s*=>\"\n notes: \"Node ≥10 has fs/promises stable. Callback form composes poorly with async/await.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":755,"content_sha256":"e1495d82bf48a22a06a748969c1240cb3019a306b5d7635c2f2627f56f4dd777"},{"filename":"scripts/patterns/groupby-reduce.yml","content":"id: groupby-reduce\nlanguage: TypeScript\nmessage: \"Manual groupBy via reduce + push into record. Use groupBy from es-toolkit or native Object.groupBy (Node 21+).\"\nseverity: info\nrule:\n all:\n - pattern: \"$ARR.reduce(($ACC, $X) => { $$BODY }, $$INIT)\"\n - has:\n pattern: \"$$.push($$)\"\n stopBy: end\n - has:\n pattern: \"return $ACC\"\n stopBy: end\n - not:\n has:\n pattern: \"{ ...$ACC, $$REST }\"\n stopBy: end\nmetadata:\n replace_native: \"Object.groupBy($ARR, ($X) => $K) // ES2024 / Node 21+\"\n replace_catalog: \"import { groupBy } from 'es-toolkit'; groupBy($ARR, ($X) => $K)\"\n priority: P1\n fallback_regex: \"\\\\.reduce\\\\([\\\\s\\\\S]{0,200}\\\\.push\\\\([\\\\s\\\\S]{0,100}return\\\\s+\\\\w+\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":741,"content_sha256":"af1942e60c46afa29920988c873e9bbc9e19dff2ca3f4f1314c10adbdda1f954"},{"filename":"scripts/patterns/html-escape-chain.yml","content":"id: html-escape-chain\nlanguage: TypeScript\nmessage: \"Hand-rolled HTML escape via chained .replace(). Use textContent assignment or a well-tested lib (e.g., DOMPurify for HTML output, he for entities). Chains miss cases like quotes, single-quote attrs, and non-ASCII.\"\nseverity: info\nrule:\n all:\n - pattern: \"$STR.replace(/&/g, '&').replace(/\u003c/g, '<').replace(/>/g, '>')\"\nmetadata:\n replace_native: \"el.textContent = $STR // if writing to DOM, not string concat\"\n replace_catalog: \"import DOMPurify from 'dompurify'; DOMPurify.sanitize(html) // for full HTML\"\n priority: P2\n fallback_regex: \"\\\\.replace\\\\(/&/g[\\\\s\\\\S]{0,50}\\\\.replace\\\\(/\u003c/g\"\n notes: \"Chained-escape usually indicates an XSS-prone pattern. Review security context carefully.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":762,"content_sha256":"e0cef398f9d324a509ac92ea555572cc50d1e417ed02dd29c8816ce06ba4d43d"},{"filename":"scripts/patterns/identity-map.yml","content":"id: identity-map\nlanguage: TypeScript\nmessage: \"Identity map callback returning the parameter unchanged. The whole .map(x => x) is a no-op — delete it.\"\nseverity: info\nrule:\n any:\n - pattern: \"$ARR.map(($X) => $X)\"\n - pattern: \"$ARR.map($X => $X)\"\n - pattern: \"Effect.map(($X) => $X)\"\n - pattern: \"Effect.map($X => $X)\"\nmetadata:\n replace_native: \"\u003cremove the .map() call entirely>\"\n priority: P2\n fallback_regex: \"\\\\.map\\\\(\\\\s*\\\\(?(\\\\w+)\\\\)?\\\\s*=>\\\\s*\\\\1\\\\s*\\\\)\"\n notes: \"Identity map usually left over from refactors. Also covers Effect.map — drop the call.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":582,"content_sha256":"023dada89aebcd5cfb5755d1f26212480941e21a2f339b5e62eb7c34dc9da42b"},{"filename":"scripts/patterns/is-same-day.yml","content":"id: is-same-day\nlanguage: TypeScript\nmessage: \"Manual isSameDay via y/m/d compare. Use isSameDay from date-fns.\"\nseverity: info\nrule:\n pattern: \"$A.getFullYear() === $B.getFullYear() && $A.getMonth() === $B.getMonth() && $A.getDate() === $B.getDate()\"\nmetadata:\n replace_catalog: \"import { isSameDay } from 'date-fns'; isSameDay($A, $B)\"\n priority: P2\n fallback_regex: \"getFullYear\\\\(\\\\)[^&]*===[^&]*getFullYear\\\\(\\\\)[\\\\s\\\\S]{0,150}getDate\\\\(\\\\)\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":451,"content_sha256":"8b903243c9bdc8d18c226042636d6472f53960595b44a492f5a2f52f6c4b22d8"},{"filename":"scripts/patterns/isempty-keys-length.yml","content":"id: isempty-keys-length\nlanguage: TypeScript\nmessage: \"Object.keys(obj).length === 0 is a fine native check — only flag to suggest the clearer isEmpty helper when the project already uses one.\"\nseverity: info\nrule:\n any:\n - pattern: \"Object.keys($OBJ).length === 0\"\n - pattern: \"Object.keys($OBJ).length \u003c 1\"\nmetadata:\n replace_native: \"Object.keys($OBJ).length === 0 // already native — fine as-is\"\n replace_catalog: \"import { isEmpty } from 'es-toolkit'; isEmpty($OBJ) // also handles arrays/Maps/Sets\"\n priority: P3\n fallback_regex: \"Object\\\\.keys\\\\([^)]+\\\\)\\\\.length\\\\s*===\\\\s*0\"\n notes: \"Low-priority — suggest the helper only if isEmpty is already imported elsewhere in the project.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":710,"content_sha256":"433eada1a2d0011db7a3572796233f1a4725b0eda4de1b51c47ac745aa99118f"},{"filename":"scripts/patterns/isnumber-typeof.yml","content":"id: isnumber-typeof\nlanguage: TypeScript\nmessage: \"typeof x === 'number' && !isNaN(x) is equivalent to Number.isFinite(x) for typed input (and excludes Infinity, which is usually what you want).\"\nseverity: info\nrule:\n any:\n - pattern: \"typeof $X === 'number' && !isNaN($X)\"\n - pattern: \"typeof $X === 'number' && !Number.isNaN($X)\"\nmetadata:\n replace_native: \"Number.isFinite($X) // also rejects Infinity; Number.isNaN for NaN-only check\"\n priority: P3\n fallback_regex: \"typeof\\\\s+\\\\w+\\\\s*===\\\\s*['\\\"]number['\\\"]\\\\s*&&\\\\s*!\"\n notes: \"Number.isFinite is stricter than typeof+isNaN (rejects Infinity). If Infinity is valid in domain, keep manual check.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":664,"content_sha256":"bcd59edfc0ba79b8d064299f1973f129f4c893169a30802deb6c322fb6299280"},{"filename":"scripts/patterns/kebab-case-manual.yml","content":"id: kebab-case-manual\nlanguage: TypeScript\nmessage: \"Manual kebabCase via regex split + toLowerCase. Use kebabCase from es-toolkit.\"\nseverity: info\nrule:\n pattern: \"$S.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()\"\nmetadata:\n replace_catalog: \"import { kebabCase } from 'es-toolkit'; kebabCase($S)\"\n priority: P3\n fallback_regex: \"\\\\.replace\\\\(/\\\\(\\\\[a-z\\\\]\\\\)\\\\(\\\\[A-Z\\\\]\\\\)/\"\n notes: \"Naive regex misses acronyms ('URLParser' → 'u-r-l-parser'). es-toolkit handles common cases.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":492,"content_sha256":"27e1fcdc14ec5d3b302b2b0eb2a2f0090263e1cbb50e41e93d33c99f20edd69e"},{"filename":"scripts/patterns/last-index.yml","content":"id: last-index\nlanguage: TypeScript\nmessage: \"arr[arr.length - 1] is the last element. Native arr.at(-1) is clearer.\"\nseverity: info\nrule:\n pattern: $ARR[$ARR.length - 1]\nmetadata:\n replace_native: \"$ARR.at(-1)\"\n priority: P3\n fallback_regex: \"\\\\[\\\\s*\\\\w+\\\\.length\\\\s*-\\\\s*1\\\\s*\\\\]\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":287,"content_sha256":"ac825589797f5d49016f46cccd3cd12f98d8e8a17041f5c9d9c0c534a3955414"},{"filename":"scripts/patterns/localecompare-sort.yml","content":"id: localecompare-sort\nlanguage: TypeScript\nmessage: \"Repeated localeCompare in sort. For hot paths prefer Intl.Collator — one-time setup, ~10x faster than localeCompare per call.\"\nseverity: info\nrule:\n any:\n - pattern: \"$ARR.sort(($A, $B) => $A.localeCompare($B))\"\n - pattern: \"$ARR.sort(($A, $B) => $A.$K.localeCompare($B.$K))\"\n - pattern: \"$ARR.toSorted(($A, $B) => $A.localeCompare($B))\"\nmetadata:\n replace_native: \"const coll = new Intl.Collator(locale); $ARR.sort((a, b) => coll.compare(a, b));\"\n priority: P3\n fallback_regex: \"\\\\.sort\\\\(\\\\s*\\\\([^)]+\\\\)\\\\s*=>\\\\s*\\\\w+(\\\\.\\\\w+)?\\\\.localeCompare\"\n notes: \"Only meaningful benefit on large arrays (>1000). P3 stylistic for small arrays.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":707,"content_sha256":"83d9b76e9e8dd44636fb411317ae33099bc650fb210650899698488b56959e22"},{"filename":"scripts/patterns/manual-chunk.yml","content":"id: manual-chunk\nlanguage: TypeScript\nmessage: \"Manual chunk via for-loop + slice. Use chunk from es-toolkit.\"\nseverity: info\nrule:\n all:\n - kind: for_statement\n - has:\n pattern: \"$I += $N\"\n stopBy: end\n - has:\n pattern: \"$ARR.slice($I, $I + $N)\"\n stopBy: end\nmetadata:\n replace_catalog: \"import { chunk } from 'es-toolkit'; chunk($ARR, $N)\"\n priority: P1\n fallback_regex: \"for\\\\s*\\\\([^)]*\\\\+=[^)]*\\\\)[\\\\s\\\\S]{0,150}\\\\.slice\\\\(\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":470,"content_sha256":"f5836be4cfd07f8fbcaa79beafbc1eaec61df26c976749907cd63236e15cea8b"},{"filename":"scripts/patterns/manual-event-emitter.yml","content":"id: manual-event-emitter\nlanguage: TypeScript\nmessage: \"Hand-rolled event emitter. Use Node EventEmitter, mitt (browser), or EventTarget (web-standard).\"\nseverity: info\nrule:\n kind: class_declaration\n all:\n - has:\n kind: public_field_definition\n has:\n field: name\n regex: \"^(listeners|handlers|subscribers|observers)$\"\n stopBy: end\n - has:\n kind: method_definition\n has:\n field: name\n regex: \"^(on|subscribe|addListener|addEventListener)$\"\n stopBy: end\n - has:\n kind: method_definition\n has:\n field: name\n regex: \"^(emit|publish|fire|dispatchEvent)$\"\n stopBy: end\nmetadata:\n replace_catalog: \"import { EventEmitter } from 'node:events'; class X extends EventEmitter {} // or mitt (~200B) for browsers\"\n priority: P2\n fallback_regex: \"class\\\\s+\\\\w+[\\\\s\\\\S]{0,500}(on|subscribe|addListener)\\\\s*\\\\([^)]+\\\\)\\\\s*\\\\{[\\\\s\\\\S]{0,500}(emit|publish|fire)\\\\s*\\\\(\"\n notes: \"For browsers use mitt (~200B) or native EventTarget. For Node, EventEmitter. Do not flag if class has additional domain logic — just exporting a pub/sub is what triggers this.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":1174,"content_sha256":"d4a3050997efbe2541c1b4ff80925fd50ed2a8b62bc66065cc41a90f18b433b4"},{"filename":"scripts/patterns/manual-pick.yml","content":"id: manual-pick\nlanguage: TypeScript\nmessage: \"Rebuilding an object from a subset of keys. Use pick from es-toolkit.\"\nseverity: info\nrule:\n any:\n - pattern: \"const $SUB = { $K1: $OBJ.$K1, $K2: $OBJ.$K2, $$REST }\"\n - pattern: \"const $SUB: $T = { $K1: $OBJ.$K1, $K2: $OBJ.$K2, $$REST }\"\n - pattern: \"let $SUB = { $K1: $OBJ.$K1, $K2: $OBJ.$K2, $$REST }\"\n - pattern: \"return { $K1: $OBJ.$K1, $K2: $OBJ.$K2, $$REST }\"\nmetadata:\n replace_catalog: \"import { pick } from 'es-toolkit'; pick($OBJ, ['$K1', '$K2', ...])\"\n priority: P3\n fallback_regex: \"\\\\{\\\\s*\\\\w+\\\\s*:\\\\s*(\\\\w+)\\\\.\\\\w+\\\\s*,\\\\s*\\\\w+\\\\s*:\\\\s*\\\\1\\\\.\\\\w+\"\n notes: \"Pattern requires ≥2 same-source field rebuilds to reduce false positives.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":716,"content_sha256":"21d8a02fa07aa59e3e9ec99c3cd7a970be0cb0c57d43265b4ec658f443bff6c8"},{"filename":"scripts/patterns/manual-schema-guard.yml","content":"id: manual-schema-guard\nlanguage: TypeScript\nmessage: \"Hand-rolled object-shape type guard. Consider a zod (or valibot/yup if installed) schema.\"\nseverity: info\nrule:\n all:\n - kind: function_declaration\n - has:\n kind: type_predicate\n stopBy: end\n - any:\n - has:\n pattern: \"typeof $X === 'object'\"\n stopBy: end\n - has:\n pattern: \"typeof $X !== 'object'\"\n stopBy: end\nmetadata:\n replace_catalog: \"import { z } from 'zod'; const Schema = z.object({...}); const isX = (x: unknown): x is X => Schema.safeParse(x).success\"\n priority: P3\n fallback_regex: \"function\\\\s+\\\\w+\\\\([^)]*:\\\\s*unknown\\\\)[^{]*:\\\\s*\\\\w+\\\\s+is\\\\s+\"\n notes: \"Confidence medium — prefer when guard body is ≥5 lines or checks ≥3 fields.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":792,"content_sha256":"b1a3e7eae2d16e8630d4e9ac76c57e45edb2a4b9e4031f8036dbdf484257cdde"},{"filename":"scripts/patterns/manual-semaphore.yml","content":"id: manual-semaphore\nlanguage: TypeScript\nmessage: \"Hand-rolled semaphore / queue with active counter. Use p-limit, p-queue, or Effect.forEach({ concurrency }).\"\nseverity: info\nrule:\n kind: statement_block\n all:\n - has:\n any:\n - pattern: \"let $ACTIVE = 0\"\n - pattern: \"let $ACTIVE: number = 0\"\n stopBy: end\n - has:\n any:\n - pattern: \"const $QUEUE: $_ = []\"\n - pattern: \"const $QUEUE = []\"\n stopBy: end\n - has:\n pattern: \"if ($ACTIVE \u003c $_) { $$ }\"\n stopBy: end\n - has:\n pattern: \"$ACTIVE++\"\n stopBy: end\nmetadata:\n replace_catalog: \"import pLimit from 'p-limit'; const limit = pLimit(N); await Promise.all(tasks.map(t => limit(t)))\"\n priority: P2\n fallback_regex: \"let\\\\s+active\\\\s*=\\\\s*0[\\\\s\\\\S]{0,500}active\\\\+\\\\+\"\n notes: \"Classic bugs in hand-rolled semaphores: miscounting, lost rejections, stuck queue on throw. Use a lib.\"\n fallback_keywords: [\"semaphore\", \"concurrency\", \"active\", \"queue\"]\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":1009,"content_sha256":"eb19867456facf4993b26de9c65dad16d4511e721ace14e1df072817d294ac66"},{"filename":"scripts/patterns/manual-uuid.yml","content":"id: manual-uuid\nlanguage: TypeScript\nmessage: \"Manual UUID generation. Use crypto.randomUUID() (native, V4).\"\nseverity: info\nrule:\n any:\n - pattern: |\n 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, $$)\n - pattern: |\n Math.random().toString(36).substring(2) + Date.now().toString(36)\nmetadata:\n replace_native: \"crypto.randomUUID()\"\n priority: P1\n fallback_regex: \"'xxxxxxxx-xxxx-[34]xxx-yxxx|Math\\\\.random\\\\(\\\\)\\\\.toString\\\\(36\\\\)\"\n notes: \"Math.random()-based IDs are not cryptographically secure.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":536,"content_sha256":"ab0c5cbc578efa7f890ee1965f8a5f6461ecaee3be5e1cf7719a1e58aaffc3f1"},{"filename":"scripts/patterns/map-values-dedup.yml","content":"id: map-values-dedup\nlanguage: TypeScript\nmessage: \"Dedup via new Map with key extraction + values(). Use uniqBy from es-toolkit.\"\nseverity: info\nrule:\n any:\n - pattern: \"[...new Map($ARR.map(($X) => [$X.$K, $X])).values()]\"\n - pattern: \"Array.from(new Map($ARR.map(($X) => [$X.$K, $X])).values())\"\n - pattern: \"[...new Map($ARR.map(($X) => [$KEY, $X])).values()]\"\nmetadata:\n replace_catalog: \"import { uniqBy } from 'es-toolkit'; uniqBy($ARR, (x) => x.$K)\"\n priority: P1\n fallback_regex: \"\\\\[\\\\s*\\\\.\\\\.\\\\.new Map\\\\([^)]*\\\\.map\\\\([^)]*=>\\\\s*\\\\[[^)]*\\\\][^)]*\\\\)\\\\)\\\\.values\\\\(\\\\)\\\\s*\\\\]\"\n notes: \"The new Map(...).values() trick is 'last-write-wins' dedup by key — identical to uniqBy semantics.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":712,"content_sha256":"7b47c0fddfbaff6f4d565462e75dcb4ac9c4d13e7441804828f66c259e2e9f45"},{"filename":"scripts/patterns/maxby-reduce.yml","content":"id: maxby-reduce\nlanguage: TypeScript\nmessage: \"Manual max-by via reduce comparing one field. Use maxBy from es-toolkit — also handles empty array (returns undefined).\"\nseverity: info\nrule:\n any:\n - pattern: \"$ARR.reduce(($ACC, $CUR) => $CUR.$K > $ACC.$K ? $CUR : $ACC)\"\n - pattern: \"$ARR.reduce(($ACC, $CUR) => ($CUR.$K > $ACC.$K ? $CUR : $ACC))\"\n - pattern: \"$ARR.reduce(($ACC, $CUR) => $CUR.$K >= $ACC.$K ? $CUR : $ACC)\"\nmetadata:\n replace_catalog: \"import { maxBy } from 'es-toolkit'; maxBy($ARR, (x) => x.$K)\"\n priority: P1\n fallback_regex: \"\\\\.reduce\\\\(\\\\s*\\\\([^)]+\\\\)\\\\s*=>\\\\s*\\\\w+\\\\.\\\\w+\\\\s*>=?\\\\s*\\\\w+\\\\.\\\\w+\\\\s*\\\\?\"\n notes: \"Manual version often paired with a preceding empty-array guard. maxBy handles empty internally.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":748,"content_sha256":"959a0eb245197ba14d7eeb943cdef4e010d7ff8a8bd5dfac259da2b03a536570"},{"filename":"scripts/patterns/memoize-map.yml","content":"id: memoize-map\nlanguage: TypeScript\nmessage: \"Manual memoization via local Map with get-or-set. Use memoize from es-toolkit.\"\nseverity: info\nrule:\n all:\n - any:\n - kind: function_declaration\n - kind: arrow_function\n - kind: function_expression\n - has:\n pattern: \"$CACHE.has($K)\"\n stopBy: end\n - has:\n pattern: \"$CACHE.set($K, $V)\"\n stopBy: end\n - has:\n pattern: \"$CACHE.get($K)\"\n stopBy: end\nmetadata:\n replace_catalog: \"import { memoize } from 'es-toolkit'; const memoFn = memoize(fn)\"\n priority: P2\n fallback_regex: \"\\\\w+\\\\.has\\\\([^)]*\\\\)[\\\\s\\\\S]{0,100}\\\\w+\\\\.set\\\\(\"\n notes: \"Some manual caches are intentional (per-instance, with eviction). Review before replacing.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":752,"content_sha256":"9dffdc1943a29bfcc01904dada6187cafe26fff2b68e339e9d9e3402e49af8ff"},{"filename":"scripts/patterns/minby-reduce.yml","content":"id: minby-reduce\nlanguage: TypeScript\nmessage: \"Manual min-by via reduce comparing one field. Use minBy from es-toolkit.\"\nseverity: info\nrule:\n any:\n - pattern: \"$ARR.reduce(($ACC, $CUR) => $CUR.$K \u003c $ACC.$K ? $CUR : $ACC)\"\n - pattern: \"$ARR.reduce(($ACC, $CUR) => ($CUR.$K \u003c $ACC.$K ? $CUR : $ACC))\"\n - pattern: \"$ARR.reduce(($ACC, $CUR) => $CUR.$K \u003c= $ACC.$K ? $CUR : $ACC)\"\nmetadata:\n replace_catalog: \"import { minBy } from 'es-toolkit'; minBy($ARR, (x) => x.$K)\"\n priority: P1\n fallback_regex: \"\\\\.reduce\\\\(\\\\s*\\\\([^)]+\\\\)\\\\s*=>\\\\s*\\\\w+\\\\.\\\\w+\\\\s*\u003c=?\\\\s*\\\\w+\\\\.\\\\w+\\\\s*\\\\?\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":592,"content_sha256":"955256711cd652dd4e7aaa89ab9363362e73dc07af6be0b6462aedb382356e64"},{"filename":"scripts/patterns/nested-spread-update.yml","content":"id: nested-spread-update\nlanguage: TypeScript\nmessage: \"3-level nested spread update. Use immer produce() — readable, structurally safe, same perf.\"\nseverity: info\nrule:\n any:\n - pattern: \"{ ...$A, $K1: { ...$A.$K1, $K2: { ...$A.$K1.$K2, $$FIELDS } } }\"\n - pattern: \"return { ...$A, $K1: { ...$A.$K1, $K2: { ...$A.$K1.$K2, $$FIELDS } } }\"\nmetadata:\n replace_catalog: \"import { produce } from 'immer'; produce($A, draft => { draft.$K1.$K2.\u003cfield> = \u003cvalue> })\"\n priority: P2\n fallback_regex: \"\\\\{\\\\s*\\\\.\\\\.\\\\.\\\\w+\\\\s*,[\\\\s\\\\S]{0,80}:\\\\s*\\\\{\\\\s*\\\\.\\\\.\\\\.\\\\w+\\\\.\\\\w+\\\\s*,[\\\\s\\\\S]{0,80}:\\\\s*\\\\{\\\\s*\\\\.\\\\.\\\\.\"\n notes: \"3 levels is the readability threshold. 2 levels of spread is fine — don't push immer on small updates.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":735,"content_sha256":"12b17a7127eeb61cafd4af1bb721fc57209b16d154a7d7961ed780b438304195"},{"filename":"scripts/patterns/new-map-chained-set.yml","content":"id: new-map-chained-set\nlanguage: TypeScript\nmessage: \"Chained Map.set() calls. Use iterable initializer new Map([[k,v],[k,v]]) or from Object.entries.\"\nseverity: info\nrule:\n any:\n - pattern: \"new Map().set($K1, $V1).set($K2, $V2)\"\n - pattern: \"new Map\u003c$_, $_>().set($K1, $V1).set($K2, $V2)\"\nmetadata:\n replace_native: \"new Map([[$K1, $V1], [$K2, $V2]])\"\n priority: P3\n fallback_regex: \"new\\\\s+Map(\u003c[^>]+>)?\\\\(\\\\)\\\\s*\\\\.set\\\\([^)]+\\\\)\\\\s*\\\\.set\\\\(\"\n notes: \"Iterable init is O(n) single pass; chained sets are the same but visually worse. Stylistic.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":562,"content_sha256":"8bec3a599b013780a697b554bd7e1b2edd820344c40b6a86282fb0ca0561b686"},{"filename":"scripts/patterns/nullish-chain-coerce.yml","content":"id: nullish-chain-coerce\nlanguage: TypeScript\nmessage: \"Chained null/undefined checks. Use ?? (nullish coalescing) or optional chaining ?. .\"\nseverity: info\nrule:\n any:\n - pattern: \"$X !== null && $X !== undefined\"\n - pattern: \"$X !== undefined && $X !== null\"\n - pattern: \"$X != null && $X != undefined\"\n - pattern: \"$X === null || $X === undefined\"\n - pattern: \"$X === undefined || $X === null\"\nmetadata:\n replace_native: \"$X != null // strict: $X !== null && $X !== undefined is equivalent to $X != null\"\n priority: P3\n fallback_regex: \"\\\\w+\\\\s*[!=]==\\\\s*(null|undefined)\\\\s*(&&|\\\\|\\\\|)\\\\s*\\\\w+\\\\s*[!=]==\\\\s*(null|undefined)\"\n notes: \"`$X != null` (loose eq) is the canonical short form. Tsc + eslint-strict allows it. For fallback, prefer `x ?? default`.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":781,"content_sha256":"aa8073fc4e6444380736e1e4d285dbc704324fde0ffd5ef27ca87002f3413753"},{"filename":"scripts/patterns/object-hasown.yml","content":"id: object-hasown\nlanguage: TypeScript\nmessage: \"hasOwnProperty.call is verbose and error-prone. Use Object.hasOwn(obj, key) (ES2022).\"\nseverity: info\nrule:\n any:\n - pattern: \"Object.prototype.hasOwnProperty.call($OBJ, $KEY)\"\n - pattern: \"{}.hasOwnProperty.call($OBJ, $KEY)\"\n - pattern: \"$OBJ.hasOwnProperty($KEY)\"\nmetadata:\n replace_native: \"Object.hasOwn($OBJ, $KEY)\"\n priority: P3\n fallback_regex: \"Object\\\\.prototype\\\\.hasOwnProperty\\\\.call|\\\\.hasOwnProperty\\\\(\"\n notes: \"Last variant (direct .hasOwnProperty) is unsafe if obj overrides the prop or has null prototype. Object.hasOwn is always safe.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":618,"content_sha256":"d9f278829efd4c05b034a3b41f499db588dbf4dc5f586c3ffb1b675f7f54a2c3"},{"filename":"scripts/patterns/once-flag.yml","content":"id: once-flag\nlanguage: TypeScript\nmessage: \"Manual 'run once' via boolean flag. Use once from es-toolkit.\"\nseverity: info\nrule:\n all:\n - any:\n - kind: function_declaration\n - kind: arrow_function\n - kind: function_expression\n - has:\n pattern: \"if ($FLAG) return;\"\n stopBy: end\n - has:\n pattern: \"$FLAG = true;\"\n stopBy: end\nmetadata:\n replace_catalog: \"import { once } from 'es-toolkit'; const init = once(doInit)\"\n priority: P2\n fallback_regex: \"let\\\\s+\\\\w+\\\\s*=\\\\s*false[\\\\s\\\\S]{0,200}=\\\\s*true\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":561,"content_sha256":"c48a1ec654b49b456835a722a14f49014da081eb1c70494bddaef18352f1babc"},{"filename":"scripts/patterns/option-match-undefined.yml","content":"id: option-match-undefined\nlanguage: TypeScript\nmessage: \"Option.match with onNone returning undefined and onSome returning the value. Use Option.getOrUndefined.\"\nseverity: info\nrule:\n any:\n - pattern: \"Option.match($V, { onNone: () => undefined, onSome: ($X) => $X })\"\n - pattern: \"Option.match($V, { onNone: () => undefined, onSome: $ID })\"\n - pattern: \"Option.match({ onNone: () => undefined, onSome: ($X) => $X })($V)\"\nmetadata:\n replace_catalog: \"import { Option } from 'effect'; Option.getOrUndefined($V)\"\n priority: P2\n fallback_regex: \"Option\\\\.match\\\\([\\\\s\\\\S]{0,200}onNone:\\\\s*\\\\(\\\\)\\\\s*=>\\\\s*undefined\"\n notes: \"Effect-specific. Similar pattern for Option.getOrNull (null instead of undefined).\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":721,"content_sha256":"f2b6b357c86dd37a51e4a8e62bd0bb05dcf0e1d05381b4d894ad940eabed390a"},{"filename":"scripts/patterns/partition-two-filters.yml","content":"id: partition-two-filters\nlanguage: TypeScript\nmessage: \"Two complementary .filter() calls for partition. Use partition from es-toolkit (one pass, both arrays).\"\nseverity: info\nrule:\n any:\n - pattern: \"[$ARR.filter($PRED), $ARR.filter(($X) => !$PRED($X))]\"\n - pattern: \"[$ARR.filter(($X) => $COND), $ARR.filter(($X) => !$COND)]\"\nmetadata:\n replace_catalog: \"import { partition } from 'es-toolkit'; const [pass, fail] = partition($ARR, $PRED)\"\n priority: P2\n fallback_regex: \"\\\\.filter\\\\([^)]*\\\\)[^,]*,\\\\s*\\\\w+\\\\.filter\\\\([^)]*!\"\n notes: \"Low-confidence pattern — ast-grep exact match preferred.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":609,"content_sha256":"34dcebd18a9337dff1a0a09de39ddee53709c96d467240237b3fb3b457ca555c"},{"filename":"scripts/patterns/path-concat-string.yml","content":"id: path-concat-string\nlanguage: TypeScript\nmessage: \"String concatenation for paths. Use path.join / path.resolve — handles separators, redundant slashes, cross-platform.\"\nseverity: info\nrule:\n any:\n - pattern: \"$DIR + '/' + $FILE\"\n - pattern: \"$DIR + \\\"/\\\" + $FILE\"\n - pattern: \"`${$DIR}/${$FILE}`\"\nmetadata:\n replace_native: \"import { join } from 'node:path'; join($DIR, $FILE) // or new URL($FILE, $BASE) for URLs\"\n priority: P3\n fallback_regex: \"\\\\w+\\\\s*\\\\+\\\\s*['\\\"]/['\\\"]?\\\\s*\\\\+\\\\s*\\\\w+\"\n notes: \"Windows path separators, double-slashes, and trailing-slash issues are real. Only flag if both operands read as path-ish (names ending in Dir/Path/Root/Base, or absolute-path strings).\"\n fallback_keywords: [\"path\", \"dir\", \"file\", \"root\", \"base\"]\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":768,"content_sha256":"be2102d08d73dce4e2847ba92ba8233c877982446728b5566ceb3651d1c17053"},{"filename":"scripts/patterns/pluralize-ternary.yml","content":"id: pluralize-ternary\nlanguage: TypeScript\nmessage: \"Ternary plural suffix. Use Intl.PluralRules for locale-correct plural forms.\"\nseverity: info\nrule:\n any:\n - pattern: \"$COUNT === 1 ? $SINGULAR : $PLURAL\"\n - pattern: \"$COUNT == 1 ? $SINGULAR : $PLURAL\"\n - pattern: \"$COUNT !== 1 ? $PLURAL : $SINGULAR\"\nmetadata:\n replace_native: \"const pr = new Intl.PluralRules(locale); pr.select($COUNT) === 'one' ? $SINGULAR : $PLURAL\"\n priority: P3\n fallback_regex: \"\\\\w+\\\\s*===?\\\\s*1\\\\s*\\\\?\\\\s*['\\\"`][^'\\\"`]+['\\\"`]\\\\s*:\\\\s*['\\\"`][^'\\\"`]+['\\\"`]\"\n notes: \"Only flag when both branches are string literals (clear pluralization intent). Locale matters for Polish/Arabic/etc. — for English-only UIs, Intl.PluralRules is overkill; confidence low.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":748,"content_sha256":"3098c485e4b82e806d56feb3e84dc69d4c4ca32e7e0ab9d18fdc020ce80699e7"},{"filename":"scripts/patterns/promise-race-timeout.yml","content":"id: promise-race-timeout\nlanguage: TypeScript\nmessage: \"Promise.race with setTimeout for timeout. Prefer AbortSignal.timeout(ms) passed to the underlying API.\"\nseverity: info\nrule:\n all:\n - pattern: \"Promise.race($$)\"\n - has:\n pattern: \"setTimeout($$)\"\n stopBy: end\nmetadata:\n replace_native: \"AbortSignal.timeout($MS) — pass signal to fetch/etc.\"\n replace_catalog: \"import { timeout } from 'es-toolkit'; timeout(fn, ms)\"\n priority: P2\n fallback_regex: \"Promise\\\\.race\\\\([\\\\s\\\\S]*setTimeout\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":521,"content_sha256":"254209120f3b030855375ac62bc129fcfcab0dac1777623f70f1ed8bb6ed602e"},{"filename":"scripts/patterns/promise-withresolvers.yml","content":"id: promise-withresolvers\nlanguage: TypeScript\nmessage: \"Leaking resolve/reject out of Promise executor. Use Promise.withResolvers() (ES2024).\"\nseverity: info\nrule:\n any:\n - pattern: \"const $P = new Promise(($R, $J) => { $RES = $R; $REJ = $J; })\"\n - pattern: \"const $P = new Promise(($R, $J) => { $RES = $R; $REJ = $J })\"\n - pattern: \"const $P = new Promise\u003c$_>(($R, $J) => { $RES = $R; $REJ = $J; })\"\n - pattern: \"const $P = new Promise\u003c$_>(($R, $J) => { $RES = $R; $REJ = $J })\"\nmetadata:\n replace_native: \"const { promise: $P, resolve: $RES, reject: $REJ } = Promise.withResolvers()\"\n priority: P2\n fallback_regex: \"new\\\\s+Promise\\\\s*(\u003c[^>]+>)?\\\\s*\\\\(\\\\s*\\\\(\\\\s*\\\\w+\\\\s*,\\\\s*\\\\w+\\\\s*\\\\)\\\\s*=>\\\\s*\\\\{\\\\s*\\\\w+\\\\s*=\\\\s*\\\\w+\\\\s*;?\\\\s*\\\\w+\\\\s*=\"\n notes: \"Node ≥22, Bun ≥1.1. High-confidence idiom match — executor body leaks both callbacks to outer scope.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":878,"content_sha256":"6583a790084e07d9ac0cc33bd03b19867c071115d68167aa6bac9ef7478489d7"},{"filename":"scripts/patterns/range-for-push.yml","content":"id: range-for-push\nlanguage: TypeScript\nmessage: \"for-loop pushing sequential numbers into an array. Use Array.from({length: n}, (_, i) => i) or range from es-toolkit.\"\nseverity: info\nrule:\n all:\n - kind: for_statement\n - has:\n pattern: \"$ARR.push($I)\"\n stopBy: end\nmetadata:\n replace_native: \"Array.from({ length: $N }, (_, i) => i)\"\n replace_catalog: \"import { range } from 'es-toolkit'; range($N)\"\n priority: P1\n fallback_regex: \"for\\\\s*\\\\([^)]*\\\\bi\\\\+\\\\+[^)]*\\\\)[\\\\s\\\\S]{0,80}\\\\.push\\\\(\\\\s*\\\\w+\\\\s*\\\\)\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":532,"content_sha256":"16d40dea2bc4fa3784e20633e8fbdfeda1b0b90537163b103efcfd64f958f91f"},{"filename":"scripts/patterns/react-fetch-useeffect.yml","content":"id: react-fetch-useeffect\nlanguage: Tsx\nmessage: \"useEffect + fetch + setState. Use TanStack Query (useQuery) or SWR — handles loading, error, caching, refetch.\"\nseverity: info\nrule:\n kind: call_expression\n all:\n - has:\n pattern: \"useEffect\"\n stopBy: end\n - has:\n pattern: \"fetch($_)\"\n stopBy: end\n - has:\n any:\n - pattern: \"set$STATE($_)\"\n - pattern: \"setData($_)\"\n - pattern: \"setResult($_)\"\n - pattern: \"setResponse($_)\"\n stopBy: end\nmetadata:\n replace_catalog: \"import { useQuery } from '@tanstack/react-query'; const { data, error, isLoading } = useQuery({ queryKey, queryFn })\"\n priority: P2\n fallback_regex: \"useEffect\\\\s*\\\\([\\\\s\\\\S]{0,300}fetch\\\\([\\\\s\\\\S]{0,500}set\\\\w+\\\\(\"\n notes: \"Only suggest react-query if it's installed (Step 2). Otherwise suggest SWR. If neither, suggest useQuery as install.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":904,"content_sha256":"11a1b935f4aa65be6e1b0107bb6eae90cecd999ddea375f35f317e2668c6374f"},{"filename":"scripts/patterns/react-object-literal-dep.yml","content":"id: react-object-literal-dep\nlanguage: Tsx\nmessage: \"Object/array literal in hook deps. Every render it's a fresh reference → hook runs every time. Wrap in useMemo.\"\nseverity: info\nrule:\n any:\n - pattern: \"useEffect($FN, [{ $$ }])\"\n - pattern: \"useMemo($FN, [{ $$ }])\"\n - pattern: \"useCallback($FN, [{ $$ }])\"\n - pattern: \"useEffect($FN, [[$$]])\"\n - pattern: \"useMemo($FN, [[$$]])\"\n - pattern: \"useCallback($FN, [[$$]])\"\nmetadata:\n replace_native: \"const stable = useMemo(() => ({ ... }), [primitiveDeps]); useEffect($FN, [stable])\"\n priority: P1\n fallback_regex: \"use(Effect|Memo|Callback)\\\\s*\\\\([^,]+,\\\\s*\\\\[\\\\s*\\\\{\"\n notes: \"Correctness bug — defeats memoization. P1.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":705,"content_sha256":"9bc1c45092c2361a4989394cea39ef8bc2bb327a7902f89bb2f402ce2b54038e"},{"filename":"scripts/patterns/README.md","content":"# ast-grep pattern rules\n\nEach `*.yml` file is an ast-grep rule. Run against a single file:\n\n```bash\nast-grep scan -r scripts/patterns/debounce-settimeout.yml path/to/file.ts\n```\n\nOr all rules over a dir:\n\n```bash\nast-grep scan -c scripts/patterns path/to/dir\n```\n\n## Rule fields (custom metadata)\n\nast-grep rule schema + our metadata extensions:\n\n- `id`, `language`, `message`, `severity`, `rule` — standard ast-grep fields.\n- `metadata.replace_native` — preferred native-API replacement (highest priority).\n- `metadata.replace_catalog` — es-toolkit / date-fns / zod replacement (fallback).\n- `metadata.priority` — `P1` | `P2` | `P3` — see SKILL.md Step 7 thresholds.\n- `metadata.fallback_regex` — ripgrep-compatible regex used when ast-grep is unavailable. Lower confidence.\n- `metadata.notes` — optional reviewer context (DST, O(n²), crypto, etc).\n\n## Adding a new rule\n\n1. Identify the reinvention shape. Test against 2-3 real examples.\n2. Write the `rule` section using ast-grep pattern syntax. Prefer `pattern:` for single-line shapes, `all:` + `has:` for multi-constraint.\n3. Add the `metadata` block with at least `replace_native` or `replace_catalog`, plus `priority` and `fallback_regex`.\n4. Smoke test: `ast-grep scan -r \u003crule>.yml \u003csample-file>` — ensure zero false positives on the sample.\n5. Set `priority` conservatively — P3 for stylistic, P2 for shape-preserving swaps, P1 for perf/correctness wins.\n\n## Language\n\nRules declare `language: TypeScript` or `language: Tsx`. ast-grep's TypeScript parser does not match `.tsx` / `.jsx` files (separate `Tsx` parser). The `scripts/run-patterns.sh` wrapper handles this transparently:\n\n- `.ts` / `.js` / `.mjs` / `.cjs` → rules as written.\n- `.tsx` / `.jsx` → rules with `language: TypeScript` swapped to `language: Tsx` via `sed` into a tmp dir.\n\nRules specifically for JSX/React components (e.g., `use-previous-manual`, `use-latest-ref`) declare `language: Tsx` directly — the wrapper only runs these for `.tsx` / `.jsx` files.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2015,"content_sha256":"dda2dddd6bd7eebae7e6f1ca7676e28f09f8133e21000bca200eaa330e7d1b89"},{"filename":"scripts/patterns/reduce-to-object.yml","content":"id: reduce-to-object\nlanguage: TypeScript\nmessage: \"reduce spreading into object literal is O(n^2). Use Object.fromEntries(arr.map(...)) or keyBy/groupBy from es-toolkit.\"\nseverity: info\nrule:\n pattern: \"$ARR.reduce(($ACC, $X) => ({ ...$ACC, [$K]: $V }), {})\"\nmetadata:\n replace_native: \"Object.fromEntries($ARR.map(($X) => [$K, $V]))\"\n replace_catalog: \"import { keyBy } from 'es-toolkit'; keyBy($ARR, ($X) => $K)\"\n priority: P1\n fallback_regex: \"\\\\.reduce\\\\([^)]*=>\\\\s*\\\\(\\\\{\\\\s*\\\\.\\\\.\\\\.\"\n notes: \"O(n^2) — native Object.fromEntries is O(n).\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":554,"content_sha256":"d9f090972f39f503c5311a8fd8b8eeee1b242682a2f4c28a02723fdb13ff3332"},{"filename":"scripts/patterns/regex-ipv4.yml","content":"id: regex-ipv4\nlanguage: TypeScript\nmessage: \"Hand-rolled IPv4 regex. Use zod z.string().ip({ version: 'v4' }) — handles octet range correctly.\"\nseverity: info\nrule:\n any:\n - pattern: \"/^(\\\\d{1,3}\\\\.){3}\\\\d{1,3}$/.test($STR)\"\n - pattern: \"/^(?:\\\\d{1,3}\\\\.){3}\\\\d{1,3}$/.test($STR)\"\nmetadata:\n replace_catalog: \"import { z } from 'zod'; z.string().ip({ version: 'v4' }).safeParse($STR).success\"\n priority: P2\n fallback_regex: \"/\\\\^\\\\(\\\\?:\\\\\\\\d\\\\{1,3\\\\}\\\\\\\\\\\\.\\\\)\\\\{3\\\\}\\\\\\\\d\\\\{1,3\\\\}\\\\$/\"\n notes: \"Common IPv4 regex accepts 999.999.999.999. zod validates each octet ≤ 255.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":588,"content_sha256":"1428430c5827b223dbefd2d1a236c7a0441860bd850d8a456f68d531658ccf81"},{"filename":"scripts/patterns/regex-iso-datetime.yml","content":"id: regex-iso-datetime\nlanguage: TypeScript\nmessage: \"Hand-rolled ISO datetime regex. Use zod z.string().datetime() or parseISO + isValid.\"\nseverity: info\nrule:\n any:\n - pattern: \"/^\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}/.test($STR)\"\n - pattern: \"/^\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}\\\\.\\\\d+Z?$/.test($STR)\"\n - pattern: \"/^\\\\d{4}-\\\\d{2}-\\\\d{2}$/.test($STR)\"\nmetadata:\n replace_catalog: \"import { z } from 'zod'; z.string().datetime().safeParse($STR).success\"\n priority: P2\n fallback_regex: \"/\\\\^\\\\\\\\d\\\\{4\\\\}-\\\\\\\\d\\\\{2\\\\}-\\\\\\\\d\\\\{2\\\\}(T\\\\\\\\d\\\\{2\\\\}:)?\"\n notes: \"Date-only form → z.string().date() (zod v3.23+). Handles timezones and leap seconds.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":672,"content_sha256":"cf8ee66d4a5d3154d7a8312e9f563b792f5c57d82aa5fc7a87e8f0ccac9e1f2e"},{"filename":"scripts/patterns/regex-uuid.yml","content":"id: regex-uuid\nlanguage: TypeScript\nmessage: \"Hand-rolled UUID regex. Use zod z.string().uuid() — version-aware, typed, ergonomic.\"\nseverity: info\nrule:\n any:\n - pattern: \"/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/i.test($STR)\"\n - pattern: \"/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test($STR)\"\n - pattern: \"/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test($STR)\"\nmetadata:\n replace_catalog: \"import { z } from 'zod'; z.string().uuid().safeParse($STR).success\"\n priority: P2\n fallback_regex: \"/\\\\^\\\\[0-9a-f(A-F)?\\\\]\\\\{8\\\\}-\\\\[0-9a-f(A-F)?\\\\]\\\\{4\\\\}-\\\\[0-9a-f(A-F)?\\\\]\\\\{4\\\\}-\\\\[0-9a-f(A-F)?\\\\]\\\\{4\\\\}-\\\\[0-9a-f(A-F)?\\\\]\\\\{12\\\\}\"\n notes: \"If validation happens at API boundary, zod schema is preferred over one-off regex.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":835,"content_sha256":"8ae332504a7c85e46f7673e6dbc663c7596750165a2e8c7a4c14b772eb2bc430"},{"filename":"scripts/patterns/relative-time-chain.yml","content":"id: relative-time-chain\nlanguage: TypeScript\nmessage: \"Hand-rolled 'X ago' relative time via sec/min/hr/day if-chain. Use Intl.RelativeTimeFormat (native, locale-aware) or formatDistance from date-fns.\"\nseverity: info\nrule:\n all:\n - any:\n - kind: function_declaration\n - kind: arrow_function\n - kind: function_expression\n - has:\n pattern: \"Math.floor($$ / 1000)\"\n stopBy: end\n - has:\n pattern: \"Math.floor($$ / 60)\"\n stopBy: end\n - has:\n pattern: \"Math.floor($$ / 24)\"\n stopBy: end\nmetadata:\n replace_native: \"new Intl.RelativeTimeFormat('en', { numeric: 'auto' }).format(-minutes, 'minute')\"\n replace_catalog: \"import { formatDistanceToNow } from 'date-fns'; formatDistanceToNow(date, { addSuffix: true })\"\n priority: P2\n fallback_regex: \"Math\\\\.floor\\\\([^)]*/\\\\s*1000\\\\s*\\\\)[\\\\s\\\\S]{0,300}Math\\\\.floor\\\\([^)]*/\\\\s*60\\\\s*\\\\)[\\\\s\\\\S]{0,300}Math\\\\.floor\\\\([^)]*/\\\\s*24\"\n notes: \"Often duplicated across 3+ files in a codebase (this project had formatRelativeTime in 3 places). Also run internal-util scan for consolidation opportunity.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":1120,"content_sha256":"8e148192a706068334dbb2eb8f1a5a47fae15e09166e6d30eb58ea0b3c7ec949"},{"filename":"scripts/patterns/retry-loop.yml","content":"id: retry-loop\nlanguage: TypeScript\nmessage: \"Manual retry via for/while loop with try/catch + sleep. Use retry from es-toolkit (or p-retry / Effect.retry if installed).\"\nseverity: info\nrule:\n all:\n - any:\n - kind: for_statement\n - kind: while_statement\n - has:\n kind: try_statement\n stopBy: end\n - has:\n pattern: \"setTimeout($$)\"\n stopBy: end\nmetadata:\n replace_catalog: \"import { retry } from 'es-toolkit'; await retry(fn, { retries: 3, delay: 200 })\"\n priority: P1\n fallback_regex: \"(for|while)\\\\s*\\\\([\\\\s\\\\S]{0,200}try\\\\s*\\\\{[\\\\s\\\\S]{0,300}setTimeout\"\n notes: \"Also flags when `effect` or `p-retry` is installed — those are preferred over es-toolkit.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":715,"content_sha256":"4f6769c0c2fec5bb701a7335faa8213c2590250de03ba6fa3a8024b890c3df59"},{"filename":"scripts/patterns/set-tracked-dedup.yml","content":"id: set-tracked-dedup\nlanguage: TypeScript\nmessage: \"reduce tracking a Set of seen + a Set of duplicates. Use countBy from es-toolkit + Object.entries.filter(([, n]) => n > 1).\"\nseverity: info\nrule:\n all:\n - pattern: \"$ARR.reduce(($ACC, $X) => { $$BODY }, $$INIT)\"\n - has:\n pattern: \"$ACC.$SEEN.has($$)\"\n stopBy: end\n - has:\n pattern: \"$ACC.$SEEN.add($$)\"\n stopBy: end\nmetadata:\n replace_catalog: |\n import { countBy } from 'es-toolkit';\n const dupes = Object.entries(countBy($ARR, (x) => x.id))\n .filter(([, n]) => n > 1)\n .map(([id]) => id);\n priority: P2\n fallback_regex: \"\\\\.reduce\\\\([\\\\s\\\\S]{0,400}\\\\.has\\\\([^)]*\\\\)[\\\\s\\\\S]{0,200}\\\\.add\\\\(\"\n notes: \"Pattern specifically for 'collect duplicates while iterating' — single-purpose dedup already covered by uniq/uniqBy rules.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":843,"content_sha256":"745bafff5a40f0c6cb3d1b2f95f20437cc6221182d9d739bca28f73c9e089231"},{"filename":"scripts/patterns/set-uniq.yml","content":"id: set-uniq\nlanguage: TypeScript\nmessage: \"Manual de-dup loop over an array. [...new Set(arr)] is the native one-liner.\"\nseverity: info\nrule:\n any:\n - pattern: \"$ARR.filter(($X, $I, $SELF) => $SELF.indexOf($X) === $I)\"\n - pattern: \"$ARR.reduce(($ACC, $X) => $ACC.includes($X) ? $ACC : [...$ACC, $X], [])\"\nmetadata:\n replace_native: \"[...new Set($ARR)]\"\n replace_catalog: \"import { uniq } from 'es-toolkit'; uniq($ARR)\"\n priority: P2\n fallback_regex: \"\\\\.filter\\\\([^)]*=>\\\\s*\\\\w+\\\\.indexOf\\\\([^)]*\\\\)\\\\s*===\\\\s*\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":524,"content_sha256":"9cae911eb57eab86bc86c8651cfa86541bfa89c60c0e3d8e0681165e0fc4a5bd"},{"filename":"scripts/patterns/sleep-promise.yml","content":"id: sleep-promise\nlanguage: TypeScript\nmessage: \"setTimeout wrapped in a Promise is a sleep helper. Use delay/sleep from es-toolkit or an internal helper.\"\nseverity: info\nrule:\n any:\n - pattern: new Promise($R => setTimeout($R, $MS))\n - pattern: new Promise(($R) => setTimeout($R, $MS))\n - pattern: new Promise(function ($R) { setTimeout($R, $MS) })\nmetadata:\n replace_catalog: \"import { delay } from 'es-toolkit'; await delay(ms)\"\n priority: P2\n fallback_regex: \"new Promise\\\\([^)]*=>\\\\s*setTimeout\\\\(\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":517,"content_sha256":"8a58a31467ba446234ab2eaeb835e88c585634e1e1fc6066ad0919cb884c7c79"},{"filename":"scripts/patterns/start-of-day.yml","content":"id: start-of-day\nlanguage: TypeScript\nmessage: \"Manual startOfDay via new Date(y, m, d) constructor. Use startOfDay from date-fns — handles DST.\"\nseverity: info\nrule:\n pattern: \"new Date($D.getFullYear(), $D.getMonth(), $D.getDate())\"\nmetadata:\n replace_catalog: \"import { startOfDay } from 'date-fns'; startOfDay($D)\"\n priority: P2\n fallback_regex: \"new Date\\\\([^)]*\\\\.getFullYear\\\\(\\\\)[^)]*\\\\.getMonth\\\\(\\\\)[^)]*\\\\.getDate\\\\(\\\\)\\\\)\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":441,"content_sha256":"62ea433fd196c72184c0eaad374930db123837df8d0a9c198bd53e013c9af25a"},{"filename":"scripts/patterns/sum-reduce.yml","content":"id: sum-reduce\nlanguage: TypeScript\nmessage: \"Manual sum via reduce. Use sum from es-toolkit.\"\nseverity: info\nrule:\n any:\n - pattern: \"$ARR.reduce(($A, $B) => $A + $B, 0)\"\n - pattern: \"$ARR.reduce(($A, $B) => $A + $B, $$)\"\nmetadata:\n replace_catalog: \"import { sum } from 'es-toolkit'; sum($ARR)\"\n priority: P1\n fallback_regex: \"\\\\.reduce\\\\(\\\\s*\\\\([^)]+\\\\)\\\\s*=>\\\\s*\\\\w+\\\\s*\\\\+\\\\s*\\\\w+\\\\s*,\"\n notes: \"Also covers mean/average — users often combine sum + divide by length.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":487,"content_sha256":"5238d355ce61301e426cb993e57a92c28d5406f452ec35fa30ffc3eb78ade7f0"},{"filename":"scripts/patterns/switch-tag-match.yml","content":"id: switch-tag-match\nlanguage: TypeScript\nmessage: \"switch on ._tag of a tagged union. Prefer Match.tag (effect) or match().with() (ts-pattern) — both give exhaustiveness checking without the exhaust(never) helper.\"\nseverity: info\nrule:\n all:\n - kind: switch_statement\n - has:\n pattern: \"$E._tag\"\n stopBy: end\nmetadata:\n replace_catalog: \"import { Match } from 'effect'; Match.type\u003cT>().pipe(Match.tag('X', fn), ..., Match.exhaustive)\"\n priority: P1\n fallback_regex: \"switch\\\\s*\\\\(\\\\s*\\\\w+\\\\._tag\\\\s*\\\\)\"\n notes: \"If `effect` installed — Match.tag + Match.exhaustive. If `ts-pattern` installed — match(e).with({_tag: 'X'}, fn).exhaustive(). Otherwise switch is fine.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":699,"content_sha256":"1b961a5c90918858bef36f18da56777d070649f31b9689f72e1edd1342c6666d"},{"filename":"scripts/patterns/template-replace-placeholder.yml","content":"id: template-replace-placeholder\nlanguage: TypeScript\nmessage: \"String placeholder interpolation via .replace with brace regex/string. Use template literals or a templating lib.\"\nseverity: info\nrule:\n any:\n - pattern: \"$STR.replace($ARG, $VAL)\"\n - pattern: \"$STR.replaceAll($ARG, $VAL)\"\nconstraints:\n ARG:\n regex: \"\\\\\\\\?[{]\\\\\\\\?[{]?\\\\w+\\\\\\\\?[}]\\\\\\\\?[}]?\"\nmetadata:\n replace_native: \"Template literal: `${prefix}${value}${suffix}`\"\n priority: P3\n fallback_regex: \"\\\\.replace(All)?\\\\(\\\\s*/?\\\\{\\\\{?\\\\w+\\\\}?\\\\}/?g?\"\n notes: \"Single-placeholder interpolation rarely needs .replace. Template literals are ergonomic.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":626,"content_sha256":"a9289ddb58a74f3661f5d0401784b61d86c9a5a5ac45994c3670aa9dce64fef3"},{"filename":"scripts/patterns/tolocalestring-currency.yml","content":"id: tolocalestring-currency\nlanguage: TypeScript\nmessage: \"Number.toLocaleString with currency opts works — but Intl.NumberFormat allows cached instance and consistent opts. Flag only if called repeatedly in hot paths.\"\nseverity: info\nrule:\n any:\n - pattern: \"$N.toLocaleString($_, { style: 'currency', currency: $CUR })\"\n - pattern: \"$N.toLocaleString($_, { style: \\\"currency\\\", currency: $CUR })\"\nmetadata:\n replace_native: \"const fmt = new Intl.NumberFormat(locale, { style: 'currency', currency: $CUR }); fmt.format($N);\"\n priority: P3\n fallback_regex: \"\\\\.toLocaleString\\\\([^)]*style\\\\s*:\\\\s*['\\\"]currency['\\\"]\"\n notes: \"Stylistic only. Do not flag if only called once per component render.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":710,"content_sha256":"c6499799f02124796ffec7a6305115c0de8d30b01d985b48e71ae5b5cf64f2ff"},{"filename":"scripts/patterns/typeof-undefined-compare.yml","content":"id: typeof-undefined-compare\nlanguage: TypeScript\nmessage: \"typeof x === 'undefined' is vestigial. In TS, x === undefined is type-checked and shorter.\"\nseverity: info\nrule:\n any:\n - pattern: \"typeof $X === 'undefined'\"\n - pattern: \"typeof $X !== 'undefined'\"\n - pattern: \"typeof $X === \\\"undefined\\\"\"\n - pattern: \"typeof $X !== \\\"undefined\\\"\"\nmetadata:\n replace_native: \"$X === undefined (or $X !== undefined)\"\n priority: P3\n fallback_regex: \"typeof\\\\s+\\\\w+(\\\\.\\\\w+)*\\\\s*[!=]==\\\\s*['\\\"]undefined['\\\"]\"\n notes: \"In pre-ES5 global scripts, `undefined` could be shadowed — that's why typeof was safer. In modern TS modules, it can't. Stylistic P3.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":665,"content_sha256":"8dba53956e5549320aec6729ebab83a3f1612414cc18665b05093016b06075bc"},{"filename":"scripts/patterns/url-query-split.yml","content":"id: url-query-split\nlanguage: TypeScript\nmessage: \"Manual query string parse via split('?') and split('&'). Use URL + URLSearchParams (native).\"\nseverity: info\nrule:\n any:\n - pattern: \"$URL.split('?')[1].split('&')\"\n - pattern: '$URL.split(\"?\")[1].split(\"&\")'\nmetadata:\n replace_native: \"new URL($URL).searchParams // or: Object.fromEntries(new URL($URL).searchParams)\"\n priority: P2\n fallback_regex: \"\\\\.split\\\\(['\\\"]\\\\?['\\\"]\\\\)\\\\[1\\\\]\\\\.split\\\\(['\\\"]&['\\\"]\\\\)\"\n notes: \"URL + URLSearchParams handles encoding and edge cases (missing '=', '+' vs '%20').\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":568,"content_sha256":"f1df6a59240c720e354650fa37c6f0035dab46cdbeaf15f7c5dfc050c22f93dd"},{"filename":"scripts/patterns/url-validate-try.yml","content":"id: url-validate-try\nlanguage: TypeScript\nmessage: \"try/catch around new URL to validate a string. Prefer URL.canParse(str) (ES2023) or zod z.string().url().\"\nseverity: info\nrule:\n any:\n - pattern: |\n try {\n new URL($STR);\n $$\n } catch {\n $$\n }\n - pattern: |\n try {\n return new URL($STR);\n } catch {\n return $_;\n }\nmetadata:\n replace_native: \"URL.canParse($STR)\"\n replace_catalog: \"import { z } from 'zod'; const schema = z.string().url(); schema.safeParse($STR).success\"\n priority: P2\n fallback_regex: \"try\\\\s*\\\\{[^}]*new\\\\s+URL\\\\s*\\\\([^)]+\\\\)[^}]*\\\\}\\\\s*catch\"\n notes: \"URL.canParse requires Node ≥20. Downgrade to medium on older targets. Use zod when validating at API boundary.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":787,"content_sha256":"c47df1c131d7c91e20d8721b12e8709310fe41de2e51cb9d69e3cc14a249a31a"},{"filename":"scripts/patterns/use-latest-ref.yml","content":"id: use-latest-ref\nlanguage: Tsx\nmessage: \"Ref mirroring latest state without deps. Use useLatest / useLatestRef from usehooks-ts or ahooks.\"\nseverity: info\nrule:\n any:\n - pattern: \"useEffect(() => { $REF.current = $VAL; })\"\n - pattern: \"React.useEffect(() => { $REF.current = $VAL; })\"\n inside:\n kind: function_declaration\n has:\n any:\n - pattern: \"const $REF = useRef($_)\"\n - pattern: \"const $REF = useRef\u003c$_>($_)\"\n - pattern: \"const $REF = React.useRef($_)\"\n - pattern: \"const $REF = React.useRef\u003c$_>($_)\"\n stopBy: end\n stopBy: end\nmetadata:\n replace_catalog: \"import { useLatest } from 'ahooks'; const latestRef = useLatest($VAL);\"\n priority: P2\n fallback_regex: \"useRef[\u003c(][\\\\s\\\\S]{0,100}[>(][^)]+\\\\)[\\\\s\\\\S]{0,200}useEffect\\\\s*\\\\(\\\\s*\\\\(\\\\s*\\\\)\\\\s*=>\\\\s*\\\\{[^}]*\\\\.current\\\\s*=[^}]*\\\\}\\\\s*\\\\)\"\n notes: \"No-deps useEffect assigning to a ref declared within the same function. Latest-value mirror pattern.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":969,"content_sha256":"90ace99e16020d0acbf712ae0856d2935b833b6870adbff192973afab5b9b7c6"},{"filename":"scripts/patterns/use-previous-manual.yml","content":"id: use-previous-manual\nlanguage: Tsx\nmessage: \"Hand-rolled previous-value tracking. Use usePrevious from usehooks-ts or ahooks.\"\nseverity: info\nrule:\n any:\n - pattern: \"useEffect(() => { $REF.current = $VAL; }, [$VAL])\"\n - pattern: \"React.useEffect(() => { $REF.current = $VAL; }, [$VAL])\"\n inside:\n kind: function_declaration\n has:\n any:\n - pattern: \"const $REF = useRef($_)\"\n - pattern: \"const $REF = useRef\u003c$_>($_)\"\n - pattern: \"const $REF = React.useRef($_)\"\n - pattern: \"const $REF = React.useRef\u003c$_>($_)\"\n stopBy: end\n stopBy: end\nmetadata:\n replace_catalog: \"import { usePrevious } from 'usehooks-ts'; const prev = usePrevious($VAL);\"\n priority: P2\n fallback_regex: \"useRef[\u003c(]\\\\s*[\\\\w\u003c>, ]*\\\\s*[>(][^)]*\\\\)[\\\\s\\\\S]{0,300}useEffect\\\\s*\\\\(\\\\s*\\\\(\\\\s*\\\\)\\\\s*=>\\\\s*\\\\{[^}]*\\\\.current\\\\s*=\"\n notes: \"useEffect with [value] deps assigning to a ref declared within the same function. Prior-value mirror pattern.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":974,"content_sha256":"9e6044674049234d4a65b029180be25ab2e437554719b76052f807d833056f80"},{"filename":"scripts/patterns/zod-discriminated-union.yml","content":"id: zod-discriminated-union\nlanguage: TypeScript\nmessage: \"Manual branch-by-tag parsing. Use z.discriminatedUnion for typed, exhaustive parsing.\"\nseverity: info\nrule:\n kind: if_statement\n all:\n - has:\n pattern: \"$DATA.$TAG === $VAL\"\n stopBy: end\n - has:\n any:\n - pattern: \"$SCHEMA.parse($DATA)\"\n - pattern: \"$SCHEMA.safeParse($DATA)\"\n stopBy: end\nmetadata:\n replace_catalog: \"const U = z.discriminatedUnion('$TAG', [SchemaA, SchemaB]); U.parse($DATA)\"\n priority: P3\n fallback_regex: \"if\\\\s*\\\\(\\\\s*\\\\w+\\\\.(type|kind|_tag|variant)\\\\s*===\\\\s*['\\\"]\"\n notes: \"Only suggest if the branches call different schema parses on the same discriminant field. Otherwise keep the if.\"\n","content_type":"application/yaml; charset=utf-8","language":"yaml","size":727,"content_sha256":"48dc793fd2d4056071caf7e4c1a44814e5b9a8cfbeb972109808e0a827ac97f4"},{"filename":"scripts/run-patterns.sh","content":"#!/usr/bin/env bash\n# run-patterns.sh — run all ast-grep rules over one or more TS/JS/TSX/JSX files.\n# Handles language auto-switch: rules live as `language: TypeScript` but are\n# replayed with `language: Tsx` for .tsx/.jsx files (different parser).\n\nset -euo pipefail\n\nusage() {\n cat \u003c\u003c'USAGE'\nUsage: run-patterns.sh FILE [FILE...]\n\nRuns every rule in scripts/patterns/ against each FILE.\nOutput from ast-grep goes to stdout. Non-TS/JS files are skipped silently.\n\nRequires: ast-grep on PATH (npm i -g @ast-grep/cli).\nUSAGE\n}\n\ncase \"${1:-}\" in\n-h | --help | \"\")\n usage\n [[ -z \"${1:-}\" ]] && exit 2\n exit 0\n ;;\nesac\n\nif ! command -v ast-grep >/dev/null 2>&1; then\n echo \"ast-grep not found — install: npm i -g @ast-grep/cli\" >&2\n exit 3\nfi\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nRULE_DIR=\"$SCRIPT_DIR/patterns\"\n\nif [[ ! -d \"$RULE_DIR\" ]]; then\n echo \"patterns/ dir missing: $RULE_DIR\" >&2\n exit 4\nfi\n\nTMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'tsrr')\ntrap 'rm -rf \"$TMP_DIR\"' EXIT\n\n# Pre-build Tsx variants (once) — same content, language swapped\nfor rule in \"$RULE_DIR\"/*.yml; do\n sed 's/^language: TypeScript$/language: Tsx/' \"$rule\" >\"$TMP_DIR/$(basename \"$rule\")\"\ndone\n\nfor file in \"$@\"; do\n [[ ! -f \"$file\" ]] && continue\n ext=\"${file##*.}\"\n case \"$ext\" in\n tsx | jsx)\n ACTIVE_DIR=\"$TMP_DIR\"\n ;;\n ts | js | mjs | cjs)\n ACTIVE_DIR=\"$RULE_DIR\"\n ;;\n *)\n continue\n ;;\n esac\n for rule in \"$ACTIVE_DIR\"/*.yml; do\n ast-grep scan -r \"$rule\" \"$file\" 2>/dev/null || true\n done\ndone\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1616,"content_sha256":"c40c190b52df59d454a0993d0553a9577d2356e8d38d92d383101656f73c3873"},{"filename":"scripts/scan-internal-utils.sh","content":"#!/usr/bin/env bash\n# scan-internal-utils.sh — search workspace util dirs for existing helpers.\n# Input: candidate function names (positional args).\n# Output: NDJSON, one line per match.\n# Exit 0 even when nothing found.\n\nset -euo pipefail\n\nusage() {\n cat \u003c\u003c'USAGE'\nUsage: scan-internal-utils.sh [--root DIR] NAME [NAME...]\n\nSearch workspace utility directories for function exports matching candidate names.\n\nMatch forms:\n export function \u003cNAME>(...)\n export const \u003cNAME> = ...\n export { \u003cNAME> }\n export { foo as \u003cNAME> }\n\nSearched paths (relative to --root, default \".\"):\n src/**/utils/**, src/**/lib/**, src/**/helpers/**\n packages/*/src/**, apps/*/src/**\n shared/**, common/**\n root: utils.{ts,js}, helpers.{ts,js}, lib.{ts,js}\n\nOutput: NDJSON, e.g.\n {\"name\":\"chunk\",\"path\":\"src/utils/array.ts\",\"line\":12,\"kind\":\"function\"}\n\nExit 0 even if no matches.\nUSAGE\n}\n\nROOT=\".\"\nif [[ \"${1:-}\" == \"--root\" ]]; then\n ROOT=\"$2\"\n shift 2\nfi\n\ncase \"${1:-}\" in\n-h | --help | \"\")\n usage\n [[ -z \"${1:-}\" ]] && exit 2\n exit 0\n ;;\nesac\n\nif ! command -v rg >/dev/null 2>&1; then\n echo \"ripgrep (rg) not found — install: brew install ripgrep\" >&2\n exit 3\nfi\n\nNAMES=(\"$@\")\n\n# build rg alternation: name1|name2|name3\nPATTERN=\"\"\nfor N in \"${NAMES[@]}\"; do\n [[ -n \"$PATTERN\" ]] && PATTERN=\"$PATTERN|\"\n PATTERN=\"$PATTERN$N\"\ndone\n\n# ripgrep globs for util-ish directories\nGLOBS=(\n -g \"src/**/utils/**/*.{ts,tsx,js,jsx,mjs,cjs}\"\n -g \"src/**/lib/**/*.{ts,tsx,js,jsx,mjs,cjs}\"\n -g \"src/**/helpers/**/*.{ts,tsx,js,jsx,mjs,cjs}\"\n -g \"packages/*/src/**/*.{ts,tsx,js,jsx,mjs,cjs}\"\n -g \"apps/*/src/**/*.{ts,tsx,js,jsx,mjs,cjs}\"\n -g \"shared/**/*.{ts,tsx,js,jsx,mjs,cjs}\"\n -g \"common/**/*.{ts,tsx,js,jsx,mjs,cjs}\"\n -g \"utils.{ts,js,mjs,cjs}\"\n -g \"helpers.{ts,js,mjs,cjs}\"\n -g \"lib.{ts,js,mjs,cjs}\"\n -g \"!**/node_modules/**\"\n -g \"!**/dist/**\"\n -g \"!**/build/**\"\n -g \"!**/.next/**\"\n -g \"!**/*.d.ts\"\n)\n\n# regex matches: function, const-arrow, named export\nREGEX=\"export[[:space:]]+function[[:space:]]+($PATTERN)\\\\b|\\\nexport[[:space:]]+const[[:space:]]+($PATTERN)[[:space:]]*=|\\\nexport[[:space:]]*\\\\{[^}]*\\\\b($PATTERN)\\\\b[^}]*\\\\}|\\\nexport[[:space:]]*\\\\{[^}]*\\\\bas[[:space:]]+($PATTERN)\\\\b[^}]*\\\\}\"\n\ncd \"$ROOT\"\n\n{ rg --json --multiline \"${GLOBS[@]}\" -e \"$REGEX\" 2>/dev/null || true; } | while IFS= read -r LINE; do\n # only look at \"match\" events\n [[ \"$LINE\" != *'\"type\":\"match\"'* ]] && continue\n\n # Extract path, line_number, line text via python for robustness\n python3 -c '\nimport json, sys, re\ndata = json.loads(sys.argv[1])\nif data.get(\"type\") != \"match\":\n sys.exit(0)\nd = data[\"data\"]\npath = d[\"path\"][\"text\"]\nbase_line = d[\"line_number\"]\ntext = d[\"lines\"][\"text\"]\nnames_re = re.compile(r\"export\\s+function\\s+(\\w+)|export\\s+const\\s+(\\w+)\\s*=|export\\s*\\{([^}]*)\\}\")\nfor m in names_re.finditer(text):\n offset = text[:m.start()].count(\"\\n\")\n line_num = base_line + offset\n if m.group(1):\n print(json.dumps({\"name\": m.group(1), \"path\": path, \"line\": line_num, \"kind\": \"function\"}))\n elif m.group(2):\n print(json.dumps({\"name\": m.group(2), \"path\": path, \"line\": line_num, \"kind\": \"const-arrow\"}))\n elif m.group(3):\n inside = m.group(3)\n for piece in inside.split(\",\"):\n piece = piece.strip()\n if \" as \" in piece:\n _, name = piece.split(\" as \", 1)\n name = name.strip()\n else:\n name = piece\n if name:\n print(json.dumps({\"name\": name, \"path\": path, \"line\": line_num, \"kind\": \"named-export\"}))\n' \"$LINE\" 2>/dev/null || true\ndone\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3642,"content_sha256":"9e1ebe4eca5c22860a39c81dee6002888822ce9447399eb426e9b79f58a9573e"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"TS Reuse Review","type":"text"}]},{"type":"paragraph","content":[{"text":"Scans a TypeScript/JavaScript diff for code that reinvents existing utilities. Reports a prioritized list of reuse opportunities — external libs (es-toolkit, date-fns, zod), ES2020+ native APIs, installed project libs (effect, remeda, ts-pattern, etc.), and already-existing internal helpers. Never applies edits.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ripgrep","type":"text","marks":[{"type":"strong"}]},{"text":" — used for internal helper search. Install: ","type":"text"},{"text":"brew install ripgrep","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"cargo install ripgrep","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ast-grep","type":"text","marks":[{"type":"strong"}]},{"text":" — structural pattern matching. Install: ","type":"text"},{"text":"npm i -g @ast-grep/cli","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"brew install ast-grep","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"ast-grep","type":"text","marks":[{"type":"code_inline"}]},{"text":" is missing, fall back to ","type":"text"},{"text":"ripgrep","type":"text","marks":[{"type":"code_inline"}]},{"text":"-only detection and flag the degraded mode in the report header. Do not abort.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Workflow","type":"text"}]},{"type":"paragraph","content":[{"text":"Do not read script source code. Run scripts directly and use ","type":"text"},{"text":"--help","type":"text","marks":[{"type":"code_inline"}]},{"text":" for usage. Scripts live under ","type":"text"},{"text":"scripts/","type":"text","marks":[{"type":"code_inline"}]},{"text":" relative to this skill's directory.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 1: Determine review scope","type":"text"}]},{"type":"paragraph","content":[{"text":"If the scope is not already clear from the invocation, use AskUserQuestion:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Uncommitted changes","type":"text","marks":[{"type":"strong"}]},{"text":" (default) — staged, unstaged, and untracked TS/JS files","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Branch diff","type":"text","marks":[{"type":"strong"}]},{"text":" — current branch vs a base branch","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Specific commit","type":"text","marks":[{"type":"strong"}]},{"text":" — one changeset by SHA","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Extract the list of changed ","type":"text"},{"text":".ts","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".tsx","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".js","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".jsx","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".mjs","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".cjs","type":"text","marks":[{"type":"code_inline"}]},{"text":" files. Skip ","type":"text"},{"text":"*.d.ts","type":"text","marks":[{"type":"code_inline"}]},{"text":", generated files under ","type":"text"},{"text":"**/dist/**","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"**/build/**","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"**/.next/**","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"**/node_modules/**","type":"text","marks":[{"type":"code_inline"}]},{"text":", and test fixtures under ","type":"text"},{"text":"**/fixtures/**","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 2: Detect installed project libraries","type":"text"}]},{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"scripts/detect-libs.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" to scan ","type":"text"},{"text":"package.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" (plus workspace ","type":"text"},{"text":"packages/*/package.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" if monorepo) for relevant libs. It returns a JSON-ish list of installed libs grouped by domain:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"general utils","type":"text","marks":[{"type":"strong"}]},{"text":": es-toolkit, lodash-es, ramda, remeda, radash","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"date","type":"text","marks":[{"type":"strong"}]},{"text":": date-fns, dayjs, luxon","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"schema","type":"text","marks":[{"type":"strong"}]},{"text":": zod, valibot, yup, superstruct, arktype","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"async/effects","type":"text","marks":[{"type":"strong"}]},{"text":": effect, neverthrow, rxjs, ts-pattern","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"http/query","type":"text","marks":[{"type":"strong"}]},{"text":": ky, ofetch, axios, @tanstack/react-query, swr","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"collections","type":"text","marks":[{"type":"strong"}]},{"text":": immer, immutable","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"The output decides the \"prefer\" tier for Step 5. Fixed targets (es-toolkit, date-fns, zod) stay in the catalog even when not installed — the report suggests installing them.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 3: Extract changed code regions","type":"text"}]},{"type":"paragraph","content":[{"text":"For each changed file, get the new-side hunks via ","type":"text"},{"text":"git diff","type":"text","marks":[{"type":"code_inline"}]},{"text":" and keep only added or modified lines. Discard pure deletions and context. Store as ","type":"text"},{"text":"{ file, startLine, endLine, content }","type":"text","marks":[{"type":"code_inline"}]},{"text":" for pattern scanning.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 4: Run pattern scan","type":"text"}]},{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"scripts/run-patterns.sh \u003cfile1> [\u003cfile2> ...]","type":"text","marks":[{"type":"code_inline"}]},{"text":" on each changed file. The wrapper runs all rules in ","type":"text"},{"text":"scripts/patterns/","type":"text","marks":[{"type":"code_inline"}]},{"text":" with the correct language per extension (","type":"text"},{"text":"TypeScript","type":"text","marks":[{"type":"code_inline"}]},{"text":" for .ts/.js/.mjs/.cjs, ","type":"text"},{"text":"Tsx","type":"text","marks":[{"type":"code_inline"}]},{"text":" for .tsx/.jsx — ast-grep has separate parsers). Only keep matches whose line range overlaps the changed region.","type":"text"}]},{"type":"paragraph","content":[{"text":"The catalog is ~80 rules grouped into 14 categories (collection shape, timing/async, equality/clone, date, schema, effect-specific, web runtime, node APIs, native supersedes, correctness bugs, React hooks, event/pub-sub, i18n, stringly). Load ","type":"text"},{"text":"references/rule-categories.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" when you need to understand which rules fire for a diff or are adding new rules.","type":"text"}]},{"type":"paragraph","content":[{"text":"If ast-grep is unavailable, fall back to the ripgrep heuristic patterns embedded in each rule's ","type":"text"},{"text":"fallback_regex","type":"text","marks":[{"type":"code_inline"}]},{"text":" metadata field and mark matches as ","type":"text"},{"text":"confidence: low","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 5: Cross-reference against catalogs","type":"text"}]},{"type":"paragraph","content":[{"text":"Load the references relevant to the hits from Step 4 — do not load every file:","type":"text"}]},{"type":"paragraph","content":[{"text":"Always load (catalog core):","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/external-libs.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — fixed external catalog (es-toolkit + date-fns + zod).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/native-apis.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ES2020+ through ES2025 natives (immutable array variants, ","type":"text"},{"text":"Object.hasOwn","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Promise.withResolvers","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"URL.canParse","type":"text","marks":[{"type":"code_inline"}]},{"text":", Set operations, Iterator helpers).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/project-libs.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — dynamic handling of installed libs from Step 2.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Load on match (progressive disclosure):","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/react-patterns.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — diff touches ","type":"text"},{"text":".tsx","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":".jsx","type":"text","marks":[{"type":"code_inline"}]},{"text":" files.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/effect-patterns.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"effect","type":"text","marks":[{"type":"code_inline"}]},{"text":" in installed deps.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/node-vs-web.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"wrangler.toml","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"vercel.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" edge config, ","type":"text"},{"text":"deno.json","type":"text","marks":[{"type":"code_inline"}]},{"text":", or a file under ","type":"text"},{"text":"workers/","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"edge/","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"functions/","type":"text","marks":[{"type":"code_inline"}]},{"text":" is in the diff.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/testing-patterns.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — diff includes test files AND ","type":"text"},{"text":"vitest","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"jest","type":"text","marks":[{"type":"code_inline"}]},{"text":" is installed.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/zod-patterns.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — any zod-family rule fires (","type":"text"},{"text":"email-regex","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"regex-uuid","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"regex-ipv4","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"regex-iso-datetime","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"zod-discriminated-union","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"url-validate-try","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"manual-schema-guard","type":"text","marks":[{"type":"code_inline"}]},{"text":") and zod/valibot/yup/arktype is installed.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/query-patterns.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"react-fetch-useeffect","type":"text","marks":[{"type":"code_inline"}]},{"text":" rule fires OR any of ","type":"text"},{"text":"@tanstack/react-query","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"swr","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"@reduxjs/toolkit/query","type":"text","marks":[{"type":"code_inline"}]},{"text":" is installed.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/form-patterns.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"react-hook-form","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"formik","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"@tanstack/react-form","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"@conform-to/react","type":"text","marks":[{"type":"code_inline"}]},{"text":" installed AND diff contains a React form component.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/immer-immutability.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"nested-spread-update","type":"text","marks":[{"type":"code_inline"}]},{"text":" rule fires OR ","type":"text"},{"text":"immer","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"use-immer","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"mutative","type":"text","marks":[{"type":"code_inline"}]},{"text":" installed.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"This progressive-disclosure approach keeps the skill under the per-invocation context budget even as the catalog grows.","type":"text"}]},{"type":"paragraph","content":[{"text":"For each pattern match:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the primary replacement lives in a ","type":"text"},{"text":"native API","type":"text","marks":[{"type":"strong"}]},{"text":", recommend the native (highest priority — zero deps).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Else if an ","type":"text"},{"text":"installed project lib","type":"text","marks":[{"type":"strong"}]},{"text":" (from Step 2) has a canonical match, recommend that lib's import. Example: ","type":"text"},{"text":"effect","type":"text","marks":[{"type":"code_inline"}]},{"text":" installed → prefer ","type":"text"},{"text":"Effect.retry","type":"text","marks":[{"type":"code_inline"}]},{"text":" over es-toolkit ","type":"text"},{"text":"retry","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Else if a ","type":"text"},{"text":"fixed external lib","type":"text","marks":[{"type":"strong"}]},{"text":" matches (es-toolkit/date-fns/zod), recommend the import and — if the lib is not installed — suggest ","type":"text"},{"text":"bun add es-toolkit","type":"text","marks":[{"type":"code_inline"}]},{"text":" (or npm/pnpm equivalent detected from the lockfile).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Drop the match if none apply.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 6: Internal helper dedup check","type":"text"}]},{"type":"paragraph","content":[{"text":"Before emitting any external replacement, run ","type":"text"},{"text":"scripts/scan-internal-utils.sh \u003cfn-name-candidates>","type":"text","marks":[{"type":"code_inline"}]},{"text":" to grep workspace util directories (","type":"text"},{"text":"src/**/utils/**","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"src/**/lib/**","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"packages/*/src/**","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"shared/**","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"common/**","type":"text","marks":[{"type":"code_inline"}]},{"text":") for existing helpers with matching names or call signatures.","type":"text"}]},{"type":"paragraph","content":[{"text":"If a matching internal helper exists, replace the external recommendation with ","type":"text"},{"text":"use existing: \u003cpath>:\u003cline>","type":"text","marks":[{"type":"code_inline"}]},{"text":". This prevents suggesting a new import when the project already has the util.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 7: Emit report","type":"text"}]},{"type":"paragraph","content":[{"text":"Output format — load ","type":"text"},{"text":"references/output-format.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the full template. Summary:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"ts-reuse-review findings:\n\n\u003cP?> \u003cfile>:\u003cline>\n pattern: \u003cshort description>\n replace: \u003csuggested replacement>\n confidence: high|medium|low\n why: \u003ctrigger source — catalog entry, installed lib, native API, internal helper>\n\nsummary: \u003cN> findings (\u003cbreakdown by priority>)","type":"text"}]},{"type":"paragraph","content":[{"text":"Priority levels:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"P1","type":"text","marks":[{"type":"strong"}]},{"text":" — clear reinvention, replacement is a one-line import swap, reduces ≥5 lines.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"P2","type":"text","marks":[{"type":"strong"}]},{"text":" — reinvention but replacement changes the shape slightly (named args vs positional, different null handling).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"P3","type":"text","marks":[{"type":"strong"}]},{"text":" — stylistic preference (native vs lib, one lib vs another already installed).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Drop matches below P3 threshold to keep signal high.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"End the report. Do not apply edits. Do not open files for modification.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Rules","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Report only","type":"text","marks":[{"type":"strong"}]},{"text":" — Never invoke Edit, Write, or NotebookEdit. Output is text findings; the user or a downstream skill decides whether to apply changes. This makes the skill safe to run in parallel with other reviewers.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Scope to changed regions","type":"text","marks":[{"type":"strong"}]},{"text":" — Matches outside added/modified hunks are noise. Do not flag pre-existing code the user did not touch in this diff.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Prefer native > installed-lib > fixed-catalog","type":"text","marks":[{"type":"strong"}]},{"text":" — Order preserves dependency minimalism. A native API suggestion beats an es-toolkit suggestion even when es-toolkit is installed.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check internal helpers before suggesting externals","type":"text","marks":[{"type":"strong"}]},{"text":" — A workspace that already has ","type":"text"},{"text":"utils/chunk.ts","type":"text","marks":[{"type":"code_inline"}]},{"text":" should not be told to import ","type":"text"},{"text":"chunk","type":"text","marks":[{"type":"code_inline"}]},{"text":" from es-toolkit. Reuse trumps install.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Suggest install only when the fixed-catalog lib is missing and no native/internal alternative exists","type":"text","marks":[{"type":"strong"}]},{"text":" — Avoid noisy \"install es-toolkit\" spam when a one-liner native works.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do not flag trivial wrappers","type":"text","marks":[{"type":"strong"}]},{"text":" — A 3-line helper around a native call is not a reuse violation unless it duplicates an installed lib's behavior. Minimum threshold: reinvention ≥5 lines OR exact signature match to a catalog entry.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Merge duplicate findings per file","type":"text","marks":[{"type":"strong"}]},{"text":" — If the same reinvention appears on multiple lines of one file, collapse to one finding listing all line numbers.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Confidence calibration","type":"text","marks":[{"type":"strong"}]},{"text":" — ","type":"text"},{"text":"high","type":"text","marks":[{"type":"code_inline"}]},{"text":" when ast-grep structural match + catalog signature match; ","type":"text"},{"text":"medium","type":"text","marks":[{"type":"code_inline"}]},{"text":" when regex-only match or catalog signature match only; ","type":"text"},{"text":"low","type":"text","marks":[{"type":"code_inline"}]},{"text":" when fallback regex heuristic or ambiguous shape.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Language scope — TS/JS only","type":"text","marks":[{"type":"strong"}]},{"text":" — Skip ","type":"text"},{"text":".py","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".rs","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".go","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".json","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".yml","type":"text","marks":[{"type":"code_inline"}]},{"text":". Extension enforcement happens in Step 1.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never mutate workspace state","type":"text","marks":[{"type":"strong"}]},{"text":" — No writes to ","type":"text"},{"text":"package.json","type":"text","marks":[{"type":"code_inline"}]},{"text":", no ","type":"text"},{"text":"bun install","type":"text","marks":[{"type":"code_inline"}]},{"text":" execution. Install suggestions are text-only.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"ts-reuse-review","author":"@skillopedia","source":{"stars":6,"repo_name":"agentskills","origin_url":"https://github.com/trancong12102/agentskills/blob/HEAD/ts-reuse-review/SKILL.md","repo_owner":"trancong12102","body_sha256":"734e1a9901e5324c3b804a7387e35f12633d897574484c711358b09826adb51d","cluster_key":"e39ce601b9b237b1829291961ef56aec5b0da5c06c1685738611128dc388b82f","clean_bundle":{"format":"clean-skill-bundle-v1","source":"trancong12102/agentskills/ts-reuse-review/SKILL.md","attachments":[{"id":"d6f85a52-727b-5a72-b1dc-bd6c3a8ec5bf","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d6f85a52-727b-5a72-b1dc-bd6c3a8ec5bf/attachment.md","path":"references/effect-patterns.md","size":10109,"sha256":"9d0664b965b94c05c073a90ee8310b48864dff42989830f2fbb647f94df89930","contentType":"text/markdown; charset=utf-8"},{"id":"0d0d5c9f-fd83-539d-b01b-a55131392a05","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0d0d5c9f-fd83-539d-b01b-a55131392a05/attachment.md","path":"references/external-libs.md","size":17778,"sha256":"3c75108fd03435b715155d3100a4053004b0d0190ee437333b62426de605bb8c","contentType":"text/markdown; charset=utf-8"},{"id":"c0ce52c8-884f-50db-b144-608d97adda6a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c0ce52c8-884f-50db-b144-608d97adda6a/attachment.md","path":"references/form-patterns.md","size":6042,"sha256":"e5f17db85e38814821383452410b6ca7581adbb3f9d8d65271dba98ced426e3a","contentType":"text/markdown; charset=utf-8"},{"id":"abb53c90-3dc5-5eab-a7d8-d4f50ed8f2dc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/abb53c90-3dc5-5eab-a7d8-d4f50ed8f2dc/attachment.md","path":"references/immer-immutability.md","size":4655,"sha256":"c2b5bfde7a0615d427f23ee143edf0e6fc1499d1f8d574b675d7be10f70225ad","contentType":"text/markdown; charset=utf-8"},{"id":"6f92167c-6c3f-5773-941e-2f93ff2c92ff","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6f92167c-6c3f-5773-941e-2f93ff2c92ff/attachment.md","path":"references/native-apis.md","size":16170,"sha256":"d2d9541f78ad96699920c096d90a4d24a0c4af73a77790c5fe12afe3f85c27c2","contentType":"text/markdown; charset=utf-8"},{"id":"e33b5205-8f3b-5fb0-a40e-1de3f2571035","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e33b5205-8f3b-5fb0-a40e-1de3f2571035/attachment.md","path":"references/node-vs-web.md","size":9421,"sha256":"d798f6c35450688cf0d72a5d10c70f57cb0ecb1b80c93b0a8dc2ff1fcbee7553","contentType":"text/markdown; charset=utf-8"},{"id":"0da5a8c9-6e83-530e-a26b-d16e2e1c65a9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0da5a8c9-6e83-530e-a26b-d16e2e1c65a9/attachment.md","path":"references/output-format.md","size":4446,"sha256":"37452df32f83b2e5f0a04348879f31f1f113027178eb83ebd2c67ddd0c74182e","contentType":"text/markdown; charset=utf-8"},{"id":"46a3047a-e1bf-5588-9573-95f68f8fa527","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/46a3047a-e1bf-5588-9573-95f68f8fa527/attachment.md","path":"references/project-libs.md","size":17506,"sha256":"f0d6b5f812147048c1870cb4802959ef3b730a7f8b1f729c931faf17b10f29ed","contentType":"text/markdown; charset=utf-8"},{"id":"b8dedffa-7414-534f-9cd7-3ae1387befd1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b8dedffa-7414-534f-9cd7-3ae1387befd1/attachment.md","path":"references/query-patterns.md","size":5430,"sha256":"54683402e30820334d8489afed1d01f6af904fcea2daeb7bb84024fc33484a10","contentType":"text/markdown; charset=utf-8"},{"id":"43ba0a35-d497-564a-a8f0-d23427ce471c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/43ba0a35-d497-564a-a8f0-d23427ce471c/attachment.md","path":"references/react-patterns.md","size":7397,"sha256":"0dd50dfb72db6b3ddb7be8d65c7d8d99a2567f8f8ab55265151d79c37f4e61a7","contentType":"text/markdown; charset=utf-8"},{"id":"61dfc673-0b7b-510e-b8bc-b863339a56f2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/61dfc673-0b7b-510e-b8bc-b863339a56f2/attachment.md","path":"references/rule-categories.md","size":3273,"sha256":"fc0e50c0b30a12fc6f4338712223c945b7a0a219658bc93eb4a57b987f89ce4c","contentType":"text/markdown; charset=utf-8"},{"id":"ee136130-b5db-526f-929b-65b7b86967d9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ee136130-b5db-526f-929b-65b7b86967d9/attachment.md","path":"references/testing-patterns.md","size":8279,"sha256":"28e1aa82d24a9e18bb24875cc59ac2b85ebffaa79610353e705f4af3e89ec84e","contentType":"text/markdown; charset=utf-8"},{"id":"709dae12-2159-5e22-ba67-170c05a3fbea","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/709dae12-2159-5e22-ba67-170c05a3fbea/attachment.md","path":"references/zod-patterns.md","size":9392,"sha256":"deb1d40cd7e9c80c5a4dbcf0415d421ab308286bc56307fd811422a4ab56a4e5","contentType":"text/markdown; charset=utf-8"},{"id":"7de62749-a552-520c-8dfc-488db84c88cc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7de62749-a552-520c-8dfc-488db84c88cc/attachment.sh","path":"scripts/detect-libs.sh","size":3155,"sha256":"ad054a1ee35528573619a6c21d7b9bcc91d2cee09413df30babde9c3f65ce1ad","contentType":"application/x-sh; charset=utf-8"},{"id":"3b4cba6a-e666-5270-ae7c-5820e6554623","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3b4cba6a-e666-5270-ae7c-5820e6554623/attachment.md","path":"scripts/patterns/README.md","size":2015,"sha256":"dda2dddd6bd7eebae7e6f1ca7676e28f09f8133e21000bca200eaa330e7d1b89","contentType":"text/markdown; charset=utf-8"},{"id":"b1916553-f1c1-5209-a59e-5322ebcd2b28","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b1916553-f1c1-5209-a59e-5322ebcd2b28/attachment.yml","path":"scripts/patterns/abortcontroller-flag.yml","size":856,"sha256":"1d57db8adb31bb367ad6dae36ba7e5645fa246b43086a7dd12073d46810fdfb9","contentType":"application/yaml; charset=utf-8"},{"id":"35c8fc53-7241-5354-9f27-c0cb60a7c0dd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/35c8fc53-7241-5354-9f27-c0cb60a7c0dd/attachment.yml","path":"scripts/patterns/array-fill-same-init.yml","size":763,"sha256":"dc93464d0454960f9f94567a5c88a18373430e19d4d60e8187762b4e4f8c5c8d","contentType":"application/yaml; charset=utf-8"},{"id":"73ac3c06-a8c6-5da4-9264-9b8ea7b070d2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/73ac3c06-a8c6-5da4-9264-9b8ea7b070d2/attachment.yml","path":"scripts/patterns/array-toreversed.yml","size":502,"sha256":"29bb96ae20b5f7d94e28752ed696c82d78a2b16f31270730bf802dd3d369403d","contentType":"application/yaml; charset=utf-8"},{"id":"7b784d5f-2b19-57ea-a3fc-37d6156b1fd1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7b784d5f-2b19-57ea-a3fc-37d6156b1fd1/attachment.yml","path":"scripts/patterns/array-tosorted.yml","size":623,"sha256":"c6f5f6bdb74fcd5563ba9c5b32a0380240fec924267f28c469223fc09e6f31d9","contentType":"application/yaml; charset=utf-8"},{"id":"b59bb2de-f3d6-5f73-be01-c54959fd796d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b59bb2de-f3d6-5f73-be01-c54959fd796d/attachment.yml","path":"scripts/patterns/async-concurrency-chunk.yml","size":802,"sha256":"5f7d34f6be0362f02f2d3f43cf9090e72fdca6a55eea6aeb6229f838e09b623d","contentType":"application/yaml; charset=utf-8"},{"id":"afd8a695-43c1-519c-ae0b-3137f089597e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/afd8a695-43c1-519c-ae0b-3137f089597e/attachment.yml","path":"scripts/patterns/buffer-base64-string.yml","size":701,"sha256":"30dcdea3ec4a29ec5103c446a540e004d003d6c4fb1ffd2061f4a6738c34bdd9","contentType":"application/yaml; charset=utf-8"},{"id":"1d4482f8-577d-5494-b76d-164fd022dd79","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1d4482f8-577d-5494-b76d-164fd022dd79/attachment.yml","path":"scripts/patterns/buffer-utf8-decode.yml","size":643,"sha256":"ffdb5b60215162fed73a405dacead4f5e30fbc185418e1bb1ef47f8e6a9d8323","contentType":"application/yaml; charset=utf-8"},{"id":"76469be9-3cf4-5ef6-8671-5594b40504d8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/76469be9-3cf4-5ef6-8671-5594b40504d8/attachment.yml","path":"scripts/patterns/capitalize-manual.yml","size":529,"sha256":"a3dde8ab4737fbaab310a59f81abec82cbfd1958ea81648c13b67b5decdf3dda","contentType":"application/yaml; charset=utf-8"},{"id":"37ddb443-9b3a-5ab6-87d5-1fed5c144a8e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/37ddb443-9b3a-5ab6-87d5-1fed5c144a8e/attachment.yml","path":"scripts/patterns/clamp-math.yml","size":428,"sha256":"d046998702d1bfe3b41363c27eaacfb7b5933617518e27f503a973c6d3947e44","contentType":"application/yaml; charset=utf-8"},{"id":"1761258a-376d-5567-822b-cee173e73718","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1761258a-376d-5567-822b-cee173e73718/attachment.yml","path":"scripts/patterns/compact-filter-null.yml","size":841,"sha256":"0fecf67e26ed7a0fcb6b9db453b2a2bf9ad6d3697848706d0ebf2060813fe95b","contentType":"application/yaml; charset=utf-8"},{"id":"2359441b-b7df-58c3-a5e4-91cef1225f0f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2359441b-b7df-58c3-a5e4-91cef1225f0f/attachment.yml","path":"scripts/patterns/conditional-push-static.yml","size":910,"sha256":"6b651d5d40e6348e83171b26e28d778999dd1622bcfa49d9b2c642ef375f26c2","contentType":"application/yaml; charset=utf-8"},{"id":"6137c208-14c1-51b9-8020-777880c1cd73","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6137c208-14c1-51b9-8020-777880c1cd73/attachment.yml","path":"scripts/patterns/console-timer-manual.yml","size":1008,"sha256":"05fd7f85390134dd8f155ac6fca6349449fbde444f31ba7b06ab85bdb8803113","contentType":"application/yaml; charset=utf-8"},{"id":"20d00565-d33d-52b2-beb2-534fbc1a553e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/20d00565-d33d-52b2-beb2-534fbc1a553e/attachment.yml","path":"scripts/patterns/crypto-createhash-node.yml","size":899,"sha256":"cf3faf58cf5f2de464c6961ca61edc4c7f95dec5eedec0d90791c4d96f552b50","contentType":"application/yaml; charset=utf-8"},{"id":"e6ec8e16-5455-5d6f-810e-c0b7c8ab42fc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e6ec8e16-5455-5d6f-810e-c0b7c8ab42fc/attachment.yml","path":"scripts/patterns/currency-hardcode.yml","size":721,"sha256":"6236e34006d635b21098ebb158534aed75ebb756f628c26bda3f2c7f48b1bb25","contentType":"application/yaml; charset=utf-8"},{"id":"4b3c702a-9544-541d-9ea7-c6cd35755ecd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4b3c702a-9544-541d-9ea7-c6cd35755ecd/attachment.yml","path":"scripts/patterns/date-add-days.yml","size":575,"sha256":"07aaf9db74b4edb70aee0dcc38e387bf5964f72d7072f2f5f4c2fc136ffa2095","contentType":"application/yaml; charset=utf-8"},{"id":"9f34c259-2caa-5750-9a20-b9f96ef852f1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9f34c259-2caa-5750-9a20-b9f96ef852f1/attachment.yml","path":"scripts/patterns/date-getday-weekday.yml","size":1003,"sha256":"8ee452d8c28a5571d7b76581b1000d6e2e089993cba3e0b0eaeac5af7c558ae5","contentType":"application/yaml; charset=utf-8"},{"id":"0445c4fd-88f3-52f1-a6d0-a732fcba007f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0445c4fd-88f3-52f1-a6d0-a732fcba007f/attachment.yml","path":"scripts/patterns/debounce-settimeout.yml","size":743,"sha256":"beebe53acd5718fb81192d8e6a5d4d5b52902b231819279603cfa6637a5cfb3f","contentType":"application/yaml; charset=utf-8"},{"id":"791af248-8186-5e47-84c5-9cc08fe1bc2f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/791af248-8186-5e47-84c5-9cc08fe1bc2f/attachment.yml","path":"scripts/patterns/deep-clone-json.yml","size":430,"sha256":"5a6e714cf615c940b2508a3f43ee771af63108d3c2cad40c28c4cc1544a86246","contentType":"application/yaml; charset=utf-8"},{"id":"f906ed6a-5a4a-5634-ba06-655225b06d14","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f906ed6a-5a4a-5634-ba06-655225b06d14/attachment.yml","path":"scripts/patterns/difference-in-days.yml","size":557,"sha256":"229475c91b7e53be996083f8e6ee4a094cb988077247158d74d4992908e23623","contentType":"application/yaml; charset=utf-8"},{"id":"1099b07c-330b-58b4-9eba-6cfc06508b5a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1099b07c-330b-58b4-9eba-6cfc06508b5a/attachment.yml","path":"scripts/patterns/duration-ms-arith.yml","size":833,"sha256":"b84eb8d193b8a34db99d7d0d3f6976a3f8976db702c376a69167258d80479424","contentType":"application/yaml; charset=utf-8"},{"id":"71667b1d-3cfa-5d96-8892-3cb8920dc5b6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/71667b1d-3cfa-5d96-8892-3cb8920dc5b6/attachment.yml","path":"scripts/patterns/effect-all-promise-all.yml","size":789,"sha256":"2c4cf4a87f87ced93f4ff5cd377556f5481527fc1fc69b2f6b9311e8d33fda9d","contentType":"application/yaml; charset=utf-8"},{"id":"0ba38fed-71db-530d-99a5-702d39b630c1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0ba38fed-71db-530d-99a5-702d39b630c1/attachment.yml","path":"scripts/patterns/effect-tagged-error.yml","size":769,"sha256":"42381dffe5f763b62f790fbf3ed44d37ffa2366da05d32a19861b0ff788110dc","contentType":"application/yaml; charset=utf-8"},{"id":"5c6e0320-0066-5242-bff9-f5368c33c02e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5c6e0320-0066-5242-bff9-f5368c33c02e/attachment.yml","path":"scripts/patterns/email-regex.yml","size":625,"sha256":"7117ea8e34134f406bdeb420e8434941dc1b54251c24a91e9d08880a00904c0e","contentType":"application/yaml; charset=utf-8"},{"id":"21294d2d-a280-5821-8660-6db3a16f9d84","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/21294d2d-a280-5821-8660-6db3a16f9d84/attachment.yml","path":"scripts/patterns/exhaust-never-helper.yml","size":812,"sha256":"b026aa7652c77e48020973163d02b9bc2d9e6bc9f70b8112e6a37d1c71dfd60e","contentType":"application/yaml; charset=utf-8"},{"id":"60a3ce75-c83a-5746-bffa-fc516a943f4a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/60a3ce75-c83a-5746-bffa-fc516a943f4a/attachment.yml","path":"scripts/patterns/findindex-splice-remove.yml","size":918,"sha256":"045ea0420222288101275ee7237dac4da3ec33f27b28023a08a30c1a50abcf91","contentType":"application/yaml; charset=utf-8"},{"id":"c17571db-69a1-5de5-85bd-e42eee19780f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c17571db-69a1-5de5-85bd-e42eee19780f/attachment.yml","path":"scripts/patterns/for-push-spread.yml","size":645,"sha256":"b63711969a283c5e05388836a6c547e65db45014a435746606cf867e221d0327","contentType":"application/yaml; charset=utf-8"},{"id":"7daf5fb1-850c-5d13-96e7-075601f29908","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7daf5fb1-850c-5d13-96e7-075601f29908/attachment.yml","path":"scripts/patterns/fs-readfile-callback.yml","size":755,"sha256":"e1495d82bf48a22a06a748969c1240cb3019a306b5d7635c2f2627f56f4dd777","contentType":"application/yaml; charset=utf-8"},{"id":"05600722-2ddb-516a-9369-177b8174d35a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/05600722-2ddb-516a-9369-177b8174d35a/attachment.yml","path":"scripts/patterns/groupby-reduce.yml","size":741,"sha256":"af1942e60c46afa29920988c873e9bbc9e19dff2ca3f4f1314c10adbdda1f954","contentType":"application/yaml; charset=utf-8"},{"id":"5cf95f6d-1489-5a9c-9574-89df02cff26c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5cf95f6d-1489-5a9c-9574-89df02cff26c/attachment.yml","path":"scripts/patterns/html-escape-chain.yml","size":762,"sha256":"e0cef398f9d324a509ac92ea555572cc50d1e417ed02dd29c8816ce06ba4d43d","contentType":"application/yaml; charset=utf-8"},{"id":"491ce047-15e5-503a-98a8-4a62ef79c406","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/491ce047-15e5-503a-98a8-4a62ef79c406/attachment.yml","path":"scripts/patterns/identity-map.yml","size":582,"sha256":"023dada89aebcd5cfb5755d1f26212480941e21a2f339b5e62eb7c34dc9da42b","contentType":"application/yaml; charset=utf-8"},{"id":"53bf5598-735c-51a8-87ad-36975ac58f44","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/53bf5598-735c-51a8-87ad-36975ac58f44/attachment.yml","path":"scripts/patterns/is-same-day.yml","size":451,"sha256":"8b903243c9bdc8d18c226042636d6472f53960595b44a492f5a2f52f6c4b22d8","contentType":"application/yaml; charset=utf-8"},{"id":"d78e832e-49c7-5ce5-961c-e2bb84541cc3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d78e832e-49c7-5ce5-961c-e2bb84541cc3/attachment.yml","path":"scripts/patterns/isempty-keys-length.yml","size":710,"sha256":"433eada1a2d0011db7a3572796233f1a4725b0eda4de1b51c47ac745aa99118f","contentType":"application/yaml; charset=utf-8"},{"id":"8fa9036e-73fc-5788-9a80-c8f6d0a30d9e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8fa9036e-73fc-5788-9a80-c8f6d0a30d9e/attachment.yml","path":"scripts/patterns/isnumber-typeof.yml","size":664,"sha256":"bcd59edfc0ba79b8d064299f1973f129f4c893169a30802deb6c322fb6299280","contentType":"application/yaml; charset=utf-8"},{"id":"c84b2453-5708-50e7-bff0-012faad9bd7c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c84b2453-5708-50e7-bff0-012faad9bd7c/attachment.yml","path":"scripts/patterns/kebab-case-manual.yml","size":492,"sha256":"27e1fcdc14ec5d3b302b2b0eb2a2f0090263e1cbb50e41e93d33c99f20edd69e","contentType":"application/yaml; charset=utf-8"},{"id":"addd8827-ef4b-5418-802c-06af86cb147a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/addd8827-ef4b-5418-802c-06af86cb147a/attachment.yml","path":"scripts/patterns/last-index.yml","size":287,"sha256":"ac825589797f5d49016f46cccd3cd12f98d8e8a17041f5c9d9c0c534a3955414","contentType":"application/yaml; charset=utf-8"},{"id":"bec205a2-ad95-5000-bc31-8df844b15b8d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bec205a2-ad95-5000-bc31-8df844b15b8d/attachment.yml","path":"scripts/patterns/localecompare-sort.yml","size":707,"sha256":"83d9b76e9e8dd44636fb411317ae33099bc650fb210650899698488b56959e22","contentType":"application/yaml; charset=utf-8"},{"id":"2b953021-65aa-5f1f-9c64-adb311fc9ef1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2b953021-65aa-5f1f-9c64-adb311fc9ef1/attachment.yml","path":"scripts/patterns/manual-chunk.yml","size":470,"sha256":"f5836be4cfd07f8fbcaa79beafbc1eaec61df26c976749907cd63236e15cea8b","contentType":"application/yaml; charset=utf-8"},{"id":"fd2b4575-ff6b-59e7-a840-0d313410fea5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fd2b4575-ff6b-59e7-a840-0d313410fea5/attachment.yml","path":"scripts/patterns/manual-event-emitter.yml","size":1174,"sha256":"d4a3050997efbe2541c1b4ff80925fd50ed2a8b62bc66065cc41a90f18b433b4","contentType":"application/yaml; charset=utf-8"},{"id":"d05e70de-b2fc-5755-967e-2c59ccd01f76","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d05e70de-b2fc-5755-967e-2c59ccd01f76/attachment.yml","path":"scripts/patterns/manual-pick.yml","size":716,"sha256":"21d8a02fa07aa59e3e9ec99c3cd7a970be0cb0c57d43265b4ec658f443bff6c8","contentType":"application/yaml; charset=utf-8"},{"id":"ac3cab1f-cec3-5b16-8921-6221c7c4e317","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ac3cab1f-cec3-5b16-8921-6221c7c4e317/attachment.yml","path":"scripts/patterns/manual-schema-guard.yml","size":792,"sha256":"b1a3e7eae2d16e8630d4e9ac76c57e45edb2a4b9e4031f8036dbdf484257cdde","contentType":"application/yaml; charset=utf-8"},{"id":"2eb9d3dd-0a7e-593d-9613-f6d7fde6df53","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2eb9d3dd-0a7e-593d-9613-f6d7fde6df53/attachment.yml","path":"scripts/patterns/manual-semaphore.yml","size":1009,"sha256":"eb19867456facf4993b26de9c65dad16d4511e721ace14e1df072817d294ac66","contentType":"application/yaml; charset=utf-8"},{"id":"37c718a1-efd7-5fb2-b73b-36c00136e903","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/37c718a1-efd7-5fb2-b73b-36c00136e903/attachment.yml","path":"scripts/patterns/manual-uuid.yml","size":536,"sha256":"ab0c5cbc578efa7f890ee1965f8a5f6461ecaee3be5e1cf7719a1e58aaffc3f1","contentType":"application/yaml; charset=utf-8"},{"id":"a1743941-95af-5cbd-b6a1-27d53349bac9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a1743941-95af-5cbd-b6a1-27d53349bac9/attachment.yml","path":"scripts/patterns/map-values-dedup.yml","size":712,"sha256":"7b47c0fddfbaff6f4d565462e75dcb4ac9c4d13e7441804828f66c259e2e9f45","contentType":"application/yaml; charset=utf-8"},{"id":"49834e3b-2961-5e71-bcfa-20142cb93799","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/49834e3b-2961-5e71-bcfa-20142cb93799/attachment.yml","path":"scripts/patterns/maxby-reduce.yml","size":748,"sha256":"959a0eb245197ba14d7eeb943cdef4e010d7ff8a8bd5dfac259da2b03a536570","contentType":"application/yaml; charset=utf-8"},{"id":"b2b9eff0-9894-51e6-a4fa-4cb7bcaf946d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b2b9eff0-9894-51e6-a4fa-4cb7bcaf946d/attachment.yml","path":"scripts/patterns/memoize-map.yml","size":752,"sha256":"9dffdc1943a29bfcc01904dada6187cafe26fff2b68e339e9d9e3402e49af8ff","contentType":"application/yaml; charset=utf-8"},{"id":"fc7bf666-962b-5ffa-a909-ba4d6e6bd948","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fc7bf666-962b-5ffa-a909-ba4d6e6bd948/attachment.yml","path":"scripts/patterns/minby-reduce.yml","size":592,"sha256":"955256711cd652dd4e7aaa89ab9363362e73dc07af6be0b6462aedb382356e64","contentType":"application/yaml; charset=utf-8"},{"id":"1c2be738-f473-5291-9284-64f930103773","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1c2be738-f473-5291-9284-64f930103773/attachment.yml","path":"scripts/patterns/nested-spread-update.yml","size":735,"sha256":"12b17a7127eeb61cafd4af1bb721fc57209b16d154a7d7961ed780b438304195","contentType":"application/yaml; charset=utf-8"},{"id":"5c344271-8545-5ae8-88d3-2645be61d1ee","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5c344271-8545-5ae8-88d3-2645be61d1ee/attachment.yml","path":"scripts/patterns/new-map-chained-set.yml","size":562,"sha256":"8bec3a599b013780a697b554bd7e1b2edd820344c40b6a86282fb0ca0561b686","contentType":"application/yaml; charset=utf-8"},{"id":"efe10b98-79f9-5705-9c90-06984f3007fb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/efe10b98-79f9-5705-9c90-06984f3007fb/attachment.yml","path":"scripts/patterns/nullish-chain-coerce.yml","size":781,"sha256":"aa8073fc4e6444380736e1e4d285dbc704324fde0ffd5ef27ca87002f3413753","contentType":"application/yaml; charset=utf-8"},{"id":"015a3b42-f1ef-528b-9e79-141542a81f04","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/015a3b42-f1ef-528b-9e79-141542a81f04/attachment.yml","path":"scripts/patterns/object-hasown.yml","size":618,"sha256":"d9f278829efd4c05b034a3b41f499db588dbf4dc5f586c3ffb1b675f7f54a2c3","contentType":"application/yaml; charset=utf-8"},{"id":"231348b4-3f56-5fac-8422-236d68d9b0d7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/231348b4-3f56-5fac-8422-236d68d9b0d7/attachment.yml","path":"scripts/patterns/once-flag.yml","size":561,"sha256":"c48a1ec654b49b456835a722a14f49014da081eb1c70494bddaef18352f1babc","contentType":"application/yaml; charset=utf-8"},{"id":"e610645e-eec4-57a1-9995-4b73f029ad77","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e610645e-eec4-57a1-9995-4b73f029ad77/attachment.yml","path":"scripts/patterns/option-match-undefined.yml","size":721,"sha256":"f2b6b357c86dd37a51e4a8e62bd0bb05dcf0e1d05381b4d894ad940eabed390a","contentType":"application/yaml; charset=utf-8"},{"id":"5b312c13-e8e1-51e3-8bc3-cbab69f0a83c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5b312c13-e8e1-51e3-8bc3-cbab69f0a83c/attachment.yml","path":"scripts/patterns/partition-two-filters.yml","size":609,"sha256":"34dcebd18a9337dff1a0a09de39ddee53709c96d467240237b3fb3b457ca555c","contentType":"application/yaml; charset=utf-8"},{"id":"ec9348a0-69e7-55af-ba49-b933944334f0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ec9348a0-69e7-55af-ba49-b933944334f0/attachment.yml","path":"scripts/patterns/path-concat-string.yml","size":768,"sha256":"be2102d08d73dce4e2847ba92ba8233c877982446728b5566ceb3651d1c17053","contentType":"application/yaml; charset=utf-8"},{"id":"0b7439d1-282e-5b77-9f8a-b72c6985edf8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0b7439d1-282e-5b77-9f8a-b72c6985edf8/attachment.yml","path":"scripts/patterns/pluralize-ternary.yml","size":748,"sha256":"3098c485e4b82e806d56feb3e84dc69d4c4ca32e7e0ab9d18fdc020ce80699e7","contentType":"application/yaml; charset=utf-8"},{"id":"e34d2605-27b6-5faf-beab-9515dfccf6a1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e34d2605-27b6-5faf-beab-9515dfccf6a1/attachment.yml","path":"scripts/patterns/promise-race-timeout.yml","size":521,"sha256":"254209120f3b030855375ac62bc129fcfcab0dac1777623f70f1ed8bb6ed602e","contentType":"application/yaml; charset=utf-8"},{"id":"97735571-3191-5e8d-800a-ff407a4be523","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/97735571-3191-5e8d-800a-ff407a4be523/attachment.yml","path":"scripts/patterns/promise-withresolvers.yml","size":878,"sha256":"6583a790084e07d9ac0cc33bd03b19867c071115d68167aa6bac9ef7478489d7","contentType":"application/yaml; charset=utf-8"},{"id":"eccc985a-1d0d-5f7d-ba62-5ff72b66d14d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/eccc985a-1d0d-5f7d-ba62-5ff72b66d14d/attachment.yml","path":"scripts/patterns/range-for-push.yml","size":532,"sha256":"16d40dea2bc4fa3784e20633e8fbdfeda1b0b90537163b103efcfd64f958f91f","contentType":"application/yaml; charset=utf-8"},{"id":"39c80a71-a5cc-5d3f-8d16-d8f841dafcad","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/39c80a71-a5cc-5d3f-8d16-d8f841dafcad/attachment.yml","path":"scripts/patterns/react-fetch-useeffect.yml","size":904,"sha256":"11a1b935f4aa65be6e1b0107bb6eae90cecd999ddea375f35f317e2668c6374f","contentType":"application/yaml; charset=utf-8"},{"id":"924a18a4-39d4-5952-9303-565547b2ab9a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/924a18a4-39d4-5952-9303-565547b2ab9a/attachment.yml","path":"scripts/patterns/react-object-literal-dep.yml","size":705,"sha256":"9bc1c45092c2361a4989394cea39ef8bc2bb327a7902f89bb2f402ce2b54038e","contentType":"application/yaml; charset=utf-8"},{"id":"8e954134-0e82-5bf9-9a09-95d6db5a8f8a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8e954134-0e82-5bf9-9a09-95d6db5a8f8a/attachment.yml","path":"scripts/patterns/reduce-to-object.yml","size":554,"sha256":"d9f090972f39f503c5311a8fd8b8eeee1b242682a2f4c28a02723fdb13ff3332","contentType":"application/yaml; charset=utf-8"},{"id":"bd615037-9fe8-549b-8f83-c01a0396d516","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bd615037-9fe8-549b-8f83-c01a0396d516/attachment.yml","path":"scripts/patterns/regex-ipv4.yml","size":588,"sha256":"1428430c5827b223dbefd2d1a236c7a0441860bd850d8a456f68d531658ccf81","contentType":"application/yaml; charset=utf-8"},{"id":"c5acb230-dc1d-50af-8a83-2bf2f7a3ff7a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c5acb230-dc1d-50af-8a83-2bf2f7a3ff7a/attachment.yml","path":"scripts/patterns/regex-iso-datetime.yml","size":672,"sha256":"cf8ee66d4a5d3154d7a8312e9f563b792f5c57d82aa5fc7a87e8f0ccac9e1f2e","contentType":"application/yaml; charset=utf-8"},{"id":"c13976f0-c462-5665-9ec5-df95a2b1f6d7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c13976f0-c462-5665-9ec5-df95a2b1f6d7/attachment.yml","path":"scripts/patterns/regex-uuid.yml","size":835,"sha256":"8ae332504a7c85e46f7673e6dbc663c7596750165a2e8c7a4c14b772eb2bc430","contentType":"application/yaml; charset=utf-8"},{"id":"f8062489-8c70-5703-a11f-cc5affcee53f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f8062489-8c70-5703-a11f-cc5affcee53f/attachment.yml","path":"scripts/patterns/relative-time-chain.yml","size":1120,"sha256":"8e148192a706068334dbb2eb8f1a5a47fae15e09166e6d30eb58ea0b3c7ec949","contentType":"application/yaml; charset=utf-8"},{"id":"6596fad3-ff91-55fd-9113-c36b1a4c408e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6596fad3-ff91-55fd-9113-c36b1a4c408e/attachment.yml","path":"scripts/patterns/retry-loop.yml","size":715,"sha256":"4f6769c0c2fec5bb701a7335faa8213c2590250de03ba6fa3a8024b890c3df59","contentType":"application/yaml; charset=utf-8"},{"id":"398ff059-65a2-5cd7-ab19-02157d3e23e4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/398ff059-65a2-5cd7-ab19-02157d3e23e4/attachment.yml","path":"scripts/patterns/set-tracked-dedup.yml","size":843,"sha256":"745bafff5a40f0c6cb3d1b2f95f20437cc6221182d9d739bca28f73c9e089231","contentType":"application/yaml; charset=utf-8"},{"id":"38bd46b0-e117-59a9-b20d-edaec3f36567","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/38bd46b0-e117-59a9-b20d-edaec3f36567/attachment.yml","path":"scripts/patterns/set-uniq.yml","size":524,"sha256":"9cae911eb57eab86bc86c8651cfa86541bfa89c60c0e3d8e0681165e0fc4a5bd","contentType":"application/yaml; charset=utf-8"},{"id":"607bac16-ca72-5ea5-8000-bd283e85d3f6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/607bac16-ca72-5ea5-8000-bd283e85d3f6/attachment.yml","path":"scripts/patterns/sleep-promise.yml","size":517,"sha256":"8a58a31467ba446234ab2eaeb835e88c585634e1e1fc6066ad0919cb884c7c79","contentType":"application/yaml; charset=utf-8"},{"id":"d0435b7c-0ed8-52be-96b0-09079041ce0f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d0435b7c-0ed8-52be-96b0-09079041ce0f/attachment.yml","path":"scripts/patterns/start-of-day.yml","size":441,"sha256":"62ea433fd196c72184c0eaad374930db123837df8d0a9c198bd53e013c9af25a","contentType":"application/yaml; charset=utf-8"},{"id":"36921c54-9a4d-59d9-94ec-9e785adc8283","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/36921c54-9a4d-59d9-94ec-9e785adc8283/attachment.yml","path":"scripts/patterns/sum-reduce.yml","size":487,"sha256":"5238d355ce61301e426cb993e57a92c28d5406f452ec35fa30ffc3eb78ade7f0","contentType":"application/yaml; charset=utf-8"},{"id":"9c5ecf9e-6505-5254-af5b-4dd8f70932b5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9c5ecf9e-6505-5254-af5b-4dd8f70932b5/attachment.yml","path":"scripts/patterns/switch-tag-match.yml","size":699,"sha256":"1b961a5c90918858bef36f18da56777d070649f31b9689f72e1edd1342c6666d","contentType":"application/yaml; charset=utf-8"},{"id":"6a60a5c9-84f2-5ec0-ac45-ed14bb4374ab","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6a60a5c9-84f2-5ec0-ac45-ed14bb4374ab/attachment.yml","path":"scripts/patterns/template-replace-placeholder.yml","size":626,"sha256":"a9289ddb58a74f3661f5d0401784b61d86c9a5a5ac45994c3670aa9dce64fef3","contentType":"application/yaml; charset=utf-8"},{"id":"aaac3073-34b6-5c4d-8788-ee3d84df58ee","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/aaac3073-34b6-5c4d-8788-ee3d84df58ee/attachment.yml","path":"scripts/patterns/tolocalestring-currency.yml","size":710,"sha256":"c6499799f02124796ffec7a6305115c0de8d30b01d985b48e71ae5b5cf64f2ff","contentType":"application/yaml; charset=utf-8"},{"id":"e713b786-3982-57d0-8dcf-7af391734dd5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e713b786-3982-57d0-8dcf-7af391734dd5/attachment.yml","path":"scripts/patterns/typeof-undefined-compare.yml","size":665,"sha256":"8dba53956e5549320aec6729ebab83a3f1612414cc18665b05093016b06075bc","contentType":"application/yaml; charset=utf-8"},{"id":"d4f4abd8-594c-525c-8664-e643231dc4da","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d4f4abd8-594c-525c-8664-e643231dc4da/attachment.yml","path":"scripts/patterns/url-query-split.yml","size":568,"sha256":"f1df6a59240c720e354650fa37c6f0035dab46cdbeaf15f7c5dfc050c22f93dd","contentType":"application/yaml; charset=utf-8"},{"id":"dcb47d05-4489-58f1-8ee0-bd20138b1ab9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dcb47d05-4489-58f1-8ee0-bd20138b1ab9/attachment.yml","path":"scripts/patterns/url-validate-try.yml","size":787,"sha256":"c47df1c131d7c91e20d8721b12e8709310fe41de2e51cb9d69e3cc14a249a31a","contentType":"application/yaml; charset=utf-8"},{"id":"11f34e94-f0a9-5652-9f1f-0e54eda775eb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/11f34e94-f0a9-5652-9f1f-0e54eda775eb/attachment.yml","path":"scripts/patterns/use-latest-ref.yml","size":969,"sha256":"90ace99e16020d0acbf712ae0856d2935b833b6870adbff192973afab5b9b7c6","contentType":"application/yaml; charset=utf-8"},{"id":"7a8aa14e-9f9d-5eec-af2d-60a5a311081a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7a8aa14e-9f9d-5eec-af2d-60a5a311081a/attachment.yml","path":"scripts/patterns/use-previous-manual.yml","size":974,"sha256":"9e6044674049234d4a65b029180be25ab2e437554719b76052f807d833056f80","contentType":"application/yaml; charset=utf-8"},{"id":"c4ba263e-3bdb-5cf5-a695-4e75c3bab83b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c4ba263e-3bdb-5cf5-a695-4e75c3bab83b/attachment.yml","path":"scripts/patterns/zod-discriminated-union.yml","size":727,"sha256":"48dc793fd2d4056071caf7e4c1a44814e5b9a8cfbeb972109808e0a827ac97f4","contentType":"application/yaml; charset=utf-8"},{"id":"1ffc73c5-7db9-54fb-b087-71c5e2ef9969","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1ffc73c5-7db9-54fb-b087-71c5e2ef9969/attachment.sh","path":"scripts/run-patterns.sh","size":1616,"sha256":"c40c190b52df59d454a0993d0553a9577d2356e8d38d92d383101656f73c3873","contentType":"application/x-sh; charset=utf-8"},{"id":"1ab77f4c-b652-523f-834a-e9427dfa4049","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1ab77f4c-b652-523f-834a-e9427dfa4049/attachment.sh","path":"scripts/scan-internal-utils.sh","size":3642,"sha256":"9e1ebe4eca5c22860a39c81dee6002888822ce9447399eb426e9b79f58a9573e","contentType":"application/x-sh; charset=utf-8"}],"bundle_sha256":"dccd6e484cb322b194d2e82c84c5dfac1fc08e581b145befe72ee40e1362a177","attachment_count":97,"text_attachments":97,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"ts-reuse-review/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"security","import_tag":"clean-skills-v1","description":"Reviews TypeScript/JavaScript diffs for reinvented utilities and missed reuse of standard libraries, native APIs, or existing workspace helpers. Use when reviewing TS/JS code changes, auditing a diff, checking a PR before merge, writing new utility helpers, or refactoring existing helpers. Detects installed project libraries and prefers those when present. Reports findings as text only — never edits files. Do not use for non-JS/TS code, documentation, configuration, or trivial single-line changes."}},"renderedAt":1782981835017}

TS Reuse Review Scans a TypeScript/JavaScript diff for code that reinvents existing utilities. Reports a prioritized list of reuse opportunities — external libs (es-toolkit, date-fns, zod), ES2020+ native APIs, installed project libs (effect, remeda, ts-pattern, etc.), and already-existing internal helpers. Never applies edits. Prerequisites - ripgrep — used for internal helper search. Install: or . - ast-grep — structural pattern matching. Install: or . If is missing, fall back to -only detection and flag the degraded mode in the report header. Do not abort. Workflow Do not read script sourc…