feat(builtin-skills): batch 56 (4 files)

This commit is contained in:
YeonGyu-Kim
2026-05-30 19:12:09 +09:00
parent bdb2b7b8ba
commit 110a4e17d4
4 changed files with 477 additions and 0 deletions
@@ -0,0 +1,282 @@
#!/usr/bin/env bun
/**
* Check TypeScript files for no-excuse violations.
*
* Rules:
* no-any-assertion - `as any`
* no-unknown-assertion - `as unknown`
* no-ts-ignore - `@ts-ignore` comments
* no-ts-expect-error - `@ts-expect-error` comments
* no-enum - `enum` declarations
* no-non-null-assertion - `x!` postfix operator
* no-throw-literal - `throw "string"` / `throw 123`
* no-mutable-export - `export let` / `export var`
* no-any-annotation - `: any` in annotations (opt out: `// no-excuse-ok: any`)
* no-explicit-any-return - `(): any` return types (opt out: `// no-excuse-ok: any`)
* empty-catch - `catch { }` or `catch (e) { }` with empty body
* catch-without-narrowing - catch block that uses error without instanceof narrowing
*
* Usage:
* bun run scripts/check-no-excuse-rules.ts <file-or-dir>...
*
* Exit codes:
* 0 - no violations
* 1 - violations found
* 2 - input error
*/
import fs from "node:fs"
import path from "node:path"
import process from "node:process"
import ts from "typescript"
type RuleId =
| "no-any-assertion"
| "no-unknown-assertion"
| "no-ts-ignore"
| "no-ts-expect-error"
| "no-enum"
| "no-non-null-assertion"
| "no-throw-literal"
| "no-mutable-export"
| "no-any-annotation"
| "no-explicit-any-return"
| "empty-catch"
| "catch-without-narrowing"
type Violation = {
readonly ruleId: RuleId
readonly filePath: string
readonly line: number
readonly column: number
readonly message: string
}
const INCLUDED_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"])
const IGNORED_DIRECTORIES = new Set([
".git", ".next", ".nuxt", ".turbo", ".yarn",
"coverage", "dist", "build", "node_modules",
])
const OPT_OUT_RE = /\/\/\s*no-excuse-ok:\s*any/
const CATCH_OK_RE = /\/\/\s*no-excuse-ok:\s*catch/
function isIncludedFile(filePath: string): boolean {
return INCLUDED_EXTENSIONS.has(path.extname(filePath).toLowerCase())
}
function isDeclarationFile(filePath: string): boolean {
return filePath.endsWith(".d.ts") || filePath.endsWith(".d.mts") || filePath.endsWith(".d.cts")
}
function discoverFiles(inputs: string[]): string[] {
const files: string[] = []
for (const input of inputs) {
const resolved = path.resolve(input)
if (!fs.existsSync(resolved)) {
console.error(`Path does not exist: ${resolved}`)
process.exit(2)
}
if (fs.statSync(resolved).isFile()) {
if (isIncludedFile(resolved) && !isDeclarationFile(resolved)) files.push(resolved)
continue
}
const walk = (dir: string): void => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!IGNORED_DIRECTORIES.has(entry.name)) walk(path.join(dir, entry.name))
} else if (isIncludedFile(entry.name) && !isDeclarationFile(entry.name)) {
files.push(path.join(dir, entry.name))
}
}
}
walk(resolved)
}
return files
}
function getLineText(sourceFile: ts.SourceFile, line: number): string {
const lineStarts = sourceFile.getLineStarts()
const start = lineStarts[line]
const end = line + 1 < lineStarts.length ? lineStarts[line + 1] : sourceFile.getEnd()
return sourceFile.text.slice(start, end)
}
function analyzeFile(filePath: string): Violation[] {
const source = fs.readFileSync(filePath, "utf-8")
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true)
const violations: Violation[] = []
function pos(node: ts.Node): { line: number; column: number } {
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
return { line: line + 1, column: character + 1 }
}
function lineHasOptOut(node: ts.Node): boolean {
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
return OPT_OUT_RE.test(getLineText(sourceFile, line))
}
function visit(node: ts.Node): void {
// ── as any / as unknown ──
if (ts.isAsExpression(node)) {
const typeText = node.type.getText(sourceFile)
if (typeText === "any") {
const p = pos(node)
violations.push({ ruleId: "no-any-assertion", filePath, ...p, message: "`as any` — narrow with type guards or redesign the types" })
}
if (typeText === "unknown") {
const p = pos(node)
violations.push({ ruleId: "no-unknown-assertion", filePath, ...p, message: "`as unknown` — redesign the types" })
}
}
// ── enum ──
if (ts.isEnumDeclaration(node)) {
const p = pos(node)
violations.push({ ruleId: "no-enum", filePath, ...p, message: "`enum` — use `as const` object + literal union type" })
}
// ── x! non-null assertion ──
if (ts.isNonNullExpression(node)) {
const p = pos(node)
violations.push({ ruleId: "no-non-null-assertion", filePath, ...p, message: "`x!` — use narrowing or optional chaining" })
}
// ── throw "literal" ──
if (ts.isThrowStatement(node) && node.expression) {
const expr = node.expression
if (ts.isStringLiteral(expr) || ts.isNumericLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
const p = pos(node)
violations.push({ ruleId: "no-throw-literal", filePath, ...p, message: "`throw literal` — throw an Error subclass" })
}
if (ts.isTemplateExpression(expr)) {
const p = pos(node)
violations.push({ ruleId: "no-throw-literal", filePath, ...p, message: "`throw template` — throw an Error subclass" })
}
}
// ── export let / export var ──
if (ts.isVariableStatement(node)) {
const hasExport = node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
if (hasExport) {
const flags = node.declarationList.flags
if (!(flags & ts.NodeFlags.Const)) {
const p = pos(node)
violations.push({ ruleId: "no-mutable-export", filePath, ...p, message: "`export let/var` — use `export const`" })
}
}
}
// ── : any in annotations ──
if (ts.isTypeReferenceNode(node) || node.kind === ts.SyntaxKind.AnyKeyword) {
if (node.kind === ts.SyntaxKind.AnyKeyword && !lineHasOptOut(node)) {
const parent = node.parent
// Skip `as any` — already caught by no-any-assertion
if (parent && ts.isAsExpression(parent)) {
// already handled
} else if (parent && (
ts.isParameter(parent) ||
ts.isVariableDeclaration(parent) ||
ts.isPropertyDeclaration(parent) ||
ts.isPropertySignature(parent)
)) {
const p = pos(node)
violations.push({ ruleId: "no-any-annotation", filePath, ...p, message: "`: any` annotation — use `unknown` and narrow" })
} else if (parent && (
ts.isFunctionDeclaration(parent) ||
ts.isMethodDeclaration(parent) ||
ts.isArrowFunction(parent) ||
ts.isFunctionExpression(parent)
)) {
const p = pos(node)
violations.push({ ruleId: "no-explicit-any-return", filePath, ...p, message: "`(): any` return — use a specific type" })
}
}
}
// ── empty catch / catch without narrowing ──
if (ts.isCatchClause(node)) {
const catchLine = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line
const catchLineText = getLineText(sourceFile, catchLine)
if (!CATCH_OK_RE.test(catchLineText)) {
const body = node.block
const stmts = body.statements
if (stmts.length === 0) {
// Empty catch — swallows everything silently
const p = pos(node)
violations.push({ ruleId: "empty-catch", filePath, ...p, message: "empty `catch` block — handle, re-throw, or remove the try/catch" })
} else if (node.variableDeclaration) {
// Has a bound variable — check if it's narrowed with instanceof
const varName = node.variableDeclaration.name.getText(sourceFile)
const blockText = body.getText(sourceFile)
const hasInstanceof = blockText.includes(`instanceof`)
const hasRethrow = blockText.includes(`throw ${varName}`) || blockText.includes(`throw new`)
if (!hasInstanceof && !hasRethrow) {
const p = pos(node)
violations.push({ ruleId: "catch-without-narrowing", filePath, ...p, message: "`catch` without `instanceof` narrowing or re-throw — narrow the error type or re-throw" })
}
}
}
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
// ── @ts-ignore / @ts-expect-error in comments ──
const commentRanges = [
...(ts.getLeadingCommentRanges(source, 0) ?? []),
]
// Scan all comments via regex for reliability
const commentRegex = /\/\/\s*@ts-(ignore|expect-error)/g
let match: RegExpExecArray | null
while ((match = commentRegex.exec(source)) !== null) {
const { line, character } = sourceFile.getLineAndCharacterOfPosition(match.index)
const kind = match[1]
violations.push({
ruleId: kind === "ignore" ? "no-ts-ignore" : "no-ts-expect-error",
filePath,
line: line + 1,
column: character + 1,
message: `\`@ts-${kind}\` — fix the underlying type`,
})
}
return violations
}
function formatViolation(v: Violation): string {
return `${v.filePath}:${v.line}:${v.column}: [${v.ruleId}] ${v.message}`
}
function main(): void {
const args = process.argv.slice(2)
if (args.length === 0) {
console.error("usage: check-no-excuse-rules.ts <file-or-dir>...")
process.exit(2)
}
const files = discoverFiles(args)
if (files.length === 0) {
console.error("No TypeScript files found.")
process.exit(2)
}
const violations = files.flatMap((f) => analyzeFile(f))
if (violations.length === 0) {
console.log(`No violations in ${files.length} file(s).`)
return
}
for (const v of violations) {
console.error(formatViolation(v))
}
console.error(`\n${violations.length} violation(s) in ${files.length} file(s).`)
process.exit(1)
}
main()
@@ -0,0 +1,177 @@
#!/usr/bin/env bun
/**
* Scaffold a new TypeScript project with ultra-strict defaults.
*
* ─── How to run ───
* 1. Install Bun: curl -fsSL https://bun.sh/install | bash
* 2. Run:
* bun run scripts/new-project.ts my-api
* bun run scripts/new-project.ts my-api --path ./projects
* ──────────────────
*
* Creates:
* <name>/
* package.json (Bun + Hono + Zod + Drizzle + Biome)
* tsconfig.json (ultra-strict from tsconfig-strict.md)
* biome.json (strict from tsconfig-strict.md)
* src/index.ts (minimal Hono entrypoint)
* .gitignore
*/
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
import { join, resolve } from "node:path";
import { parseArgs } from "node:util";
const { values, positionals } = parseArgs({
args: Bun.argv.slice(2),
options: {
path: { type: "string", default: "." },
help: { type: "boolean", short: "h", default: false },
},
allowPositionals: true,
strict: true,
});
if (values.help || positionals.length === 0) {
console.log(`Usage: bun run new-project.ts <name> [--path <dir>]
Arguments:
name Project directory name (kebab-case)
Options:
--path Parent directory (default: current dir)
-h, --help Show this help`);
process.exit(positionals.length === 0 ? 2 : 0);
}
const name = positionals[0]!;
const root = resolve(values.path!, name);
if (existsSync(root)) {
console.error(`Error: ${root} already exists`);
process.exit(1);
}
// ── Directory structure ──
mkdirSync(join(root, "src"), { recursive: true });
// ── package.json ──
const pkg = {
name,
version: "0.0.1",
private: true,
type: "module",
scripts: {
dev: "bun --hot src/index.ts",
start: "bun src/index.ts",
check: "bunx biome check . && bunx tsc --noEmit && bun test",
"check:fix": "bunx biome check --write .",
test: "bun test",
},
dependencies: {
hono: "^4.12.5",
zod: "^3.24.0",
},
devDependencies: {
"@biomejs/biome": "^1.9.0",
"@types/bun": "latest",
typescript: "^5.8.0",
},
};
writeFileSync(join(root, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
// ── tsconfig.json (ultra-strict) ──
const tsconfig = {
compilerOptions: {
strict: true,
noUncheckedIndexedAccess: true,
exactOptionalPropertyTypes: true,
noFallthroughCasesInSwitch: true,
forceConsistentCasingInFileNames: true,
verbatimModuleSyntax: true,
isolatedModules: true,
esModuleInterop: true,
resolveJsonModule: true,
target: "ESNext",
lib: ["ESNext"],
declaration: true,
declarationMap: true,
sourceMap: true,
outDir: "dist",
rootDir: "src",
module: "ESNext",
moduleResolution: "bundler",
types: ["bun-types"],
skipLibCheck: true,
noEmit: true,
},
include: ["src/**/*.ts"],
exclude: ["node_modules", "dist"],
};
writeFileSync(
join(root, "tsconfig.json"),
JSON.stringify(tsconfig, null, 2) + "\n",
);
// ── biome.json (strict) ──
const biome = {
$schema: "https://biomejs.dev/schemas/1.9.0/schema.json",
organizeImports: { enabled: true },
formatter: {
enabled: true,
indentStyle: "space",
indentWidth: 2,
lineWidth: 100,
},
linter: {
enabled: true,
rules: {
recommended: true,
complexity: {
noBannedTypes: "error",
noExtraBooleanCast: "error",
noUselessConstructor: "error",
noUselessRename: "error",
noVoid: "error",
},
correctness: {
noUnusedVariables: "error",
noUnusedImports: "error",
useExhaustiveDependencies: "warn",
},
style: {
noNonNullAssertion: "error",
useConst: "error",
noParameterAssign: "error",
},
suspicious: {
noExplicitAny: "error",
noAssertion: "warn",
},
},
},
};
writeFileSync(join(root, "biome.json"), JSON.stringify(biome, null, 2) + "\n");
// ── src/index.ts ──
const indexTs = `import { Hono } from "hono";
const app = new Hono();
app.get("/", (c) => c.json({ status: "ok" }));
export default app;
`;
writeFileSync(join(root, "src/index.ts"), indexTs);
// ── .gitignore ──
const gitignore = `node_modules/
dist/
*.tsbuildinfo
.env
.env.*
`;
writeFileSync(join(root, ".gitignore"), gitignore);
console.log(`✓ Created: ${root}`);
console.log(` cd ${name} && bun install && bun run check`);
@@ -0,0 +1,9 @@
import { loadSharedSkillTemplate } from "../skill-file-loader"
import type { BuiltinSkill } from "../types"
export const initDeepSkill: BuiltinSkill = {
name: "init-deep",
description: "(builtin) Initialize hierarchical AGENTS.md knowledge base",
template: loadSharedSkillTemplate("init-deep"),
argumentHint: "[--create-new] [--max-depth=N]",
}
@@ -0,0 +1,9 @@
import { loadSharedSkillTemplate } from "../skill-file-loader"
import type { BuiltinSkill } from "../types"
export const removeAiSlopsSkill: BuiltinSkill = {
name: "remove-ai-slops",
description:
'Remove AI-generated code smells (slop) from branch changes or an explicit file list. Locks behavior with regression tests FIRST, then runs categorized cleanup via parallel `deep` agents in batches of 5, then verifies with quality gates. Covers 10 slop categories including performance equivalences, excessive complexity (object annotations, if/elif variant chains), and oversized modules (250+ pure LOC with mandatory modular refactoring). MUST USE when the user asks to "remove slop", "clean AI code", "deslop", "clean up AI-generated code", "remove AI slop", or wants to clean up AI-generated patterns from recent changes. Triggers - "remove ai slops", "clean ai code", "deslop", "cleanup AI generated", "remove AI slop", "clean up AI-generated code", "strip slop", "ai-slop cleanup".',
template: loadSharedSkillTemplate("remove-ai-slops"),
}