From f323d3b601d04f1221f0dfd52aded9a4ae899fff Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues <44656907+Rodriguespn@users.noreply.github.com> Date: Thu, 22 Jan 2026 08:28:49 +0000 Subject: [PATCH] Add Biome formatter/linter and restore CI workflow (#6) - Install Biome as the project formatter and linter - Configure Biome with recommended settings - Add format, lint, and check scripts to package.json - Restore CI workflow from git history (commit 0a543e1) - Extend CI with new Biome job for format and lint checks - Apply Biome formatting to all TypeScript files - Fix linting issues (use node: protocol, template literals, forEach pattern) CI now runs on: - All pushes to main branch - All pull requests Co-authored-by: Claude Sonnet 4.5 --- .claude-plugin/marketplace.json | 38 +- .github/workflows/ci.yml | 54 +++ biome.json | 34 ++ package-lock.json | 179 ++++++++ package.json | 27 +- .../package.json | 32 +- .../src/build.ts | 401 ++++++++++-------- .../src/config.ts | 39 +- .../src/parser.ts | 378 +++++++++-------- .../src/types.ts | 81 ++-- .../src/validate.ts | 276 ++++++------ .../tsconfig.json | 28 +- skills/postgres-best-practices/metadata.json | 22 +- 13 files changed, 980 insertions(+), 609 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 biome.json create mode 100644 package-lock.json diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index fd31e0a..739389c 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,22 +1,20 @@ { - "name": "supabase-agent-skills", - "owner": { - "name": "Supabase", - "email": "support@supabase.com" - }, - "metadata": { - "description": "Official Supabase agent skills for Claude Code", - "version": "1.0.0" - }, - "plugins": [ - { - "name": "postgres-best-practices", - "description": "Postgres performance optimization and best practices. Use when writing, reviewing, or optimizing Postgres queries, schema designs, or database configurations.", - "source": "./", - "strict": false, - "skills": [ - "./skills/postgres-best-practices" - ] - } - ] + "name": "supabase-agent-skills", + "owner": { + "name": "Supabase", + "email": "support@supabase.com" + }, + "metadata": { + "description": "Official Supabase agent skills for Claude Code", + "version": "1.0.0" + }, + "plugins": [ + { + "name": "postgres-best-practices", + "description": "Postgres performance optimization and best practices. Use when writing, reviewing, or optimizing Postgres queries, schema designs, or database configurations.", + "source": "./", + "strict": false, + "skills": ["./skills/postgres-best-practices"] + } + ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..efdd399 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: Postgres Best Practices CI + +on: + push: + branches: [main] + pull_request: + +jobs: + biome: + name: Format and Lint (Biome) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm install + + - name: Run Biome CI + run: npm run ci:check + + validate-and-build: + name: Validate and Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + working-directory: packages/postgres-best-practices-build + run: npm install + + - name: Validate rule files + working-directory: packages/postgres-best-practices-build + run: npm run validate + + - name: Build AGENTS.md + working-directory: packages/postgres-best-practices-build + run: npm run build + + - name: Check for uncommitted changes + run: | + if [[ -n $(git status --porcelain skills/postgres-best-practices/AGENTS.md) ]]; then + echo "Error: AGENTS.md is not up to date" + echo "Run 'npm run build' and commit the changes" + git diff skills/postgres-best-practices/AGENTS.md + exit 1 + fi diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..750946b --- /dev/null +++ b/biome.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..478a0c9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,179 @@ +{ + "name": "supabase-agent-skills", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "supabase-agent-skills", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@biomejs/biome": "2.3.11" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.11.tgz", + "integrity": "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.11", + "@biomejs/cli-darwin-x64": "2.3.11", + "@biomejs/cli-linux-arm64": "2.3.11", + "@biomejs/cli-linux-arm64-musl": "2.3.11", + "@biomejs/cli-linux-x64": "2.3.11", + "@biomejs/cli-linux-x64-musl": "2.3.11", + "@biomejs/cli-win32-arm64": "2.3.11", + "@biomejs/cli-win32-x64": "2.3.11" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.11.tgz", + "integrity": "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.11.tgz", + "integrity": "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.11.tgz", + "integrity": "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.11.tgz", + "integrity": "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.11.tgz", + "integrity": "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.11.tgz", + "integrity": "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.11.tgz", + "integrity": "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.11.tgz", + "integrity": "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + } + } +} diff --git a/package.json b/package.json index 03c99e6..b84bb6c 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,20 @@ { - "name": "supabase-agent-skills", - "version": "1.0.0", - "author": "Supabase", - "license": "MIT", - "description": "Official Supabase agent skills", - "scripts": { - "build": "npm --prefix packages/postgres-best-practices-build run build", - "validate": "npm --prefix packages/postgres-best-practices-build run validate" - } + "name": "supabase-agent-skills", + "version": "1.0.0", + "author": "Supabase", + "license": "MIT", + "description": "Official Supabase agent skills", + "scripts": { + "build": "npm --prefix packages/postgres-best-practices-build run build", + "validate": "npm --prefix packages/postgres-best-practices-build run validate", + "format": "biome format --write .", + "format:check": "biome format .", + "lint": "biome lint --write .", + "lint:check": "biome lint .", + "check": "biome check --write .", + "ci:check": "biome ci ." + }, + "devDependencies": { + "@biomejs/biome": "2.3.11" + } } diff --git a/packages/postgres-best-practices-build/package.json b/packages/postgres-best-practices-build/package.json index 8fb62e2..b738a26 100644 --- a/packages/postgres-best-practices-build/package.json +++ b/packages/postgres-best-practices-build/package.json @@ -1,18 +1,18 @@ { - "name": "postgres-best-practices-build", - "version": "1.0.0", - "type": "module", - "author": "Supabase", - "license": "MIT", - "description": "Build system for Supabase agent skills", - "scripts": { - "build": "tsx src/build.ts", - "validate": "tsx src/validate.ts", - "dev": "npm run validate && npm run build" - }, - "devDependencies": { - "@types/node": "^20.10.0", - "tsx": "^4.7.0", - "typescript": "^5.3.0" - } + "name": "postgres-best-practices-build", + "version": "1.0.0", + "type": "module", + "author": "Supabase", + "license": "MIT", + "description": "Build system for Supabase agent skills", + "scripts": { + "build": "tsx src/build.ts", + "validate": "tsx src/validate.ts", + "dev": "npm run validate && npm run build" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + } } diff --git a/packages/postgres-best-practices-build/src/build.ts b/packages/postgres-best-practices-build/src/build.ts index 152c152..27f7203 100644 --- a/packages/postgres-best-practices-build/src/build.ts +++ b/packages/postgres-best-practices-build/src/build.ts @@ -1,243 +1,306 @@ -import { readdirSync, readFileSync, writeFileSync, existsSync } from "fs"; -import { join, basename } from "path"; +import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { basename, join } from "node:path"; +import { AGENTS_OUTPUT, METADATA_FILE, RULES_DIR } from "./config.js"; import { parseRuleFile } from "./parser.js"; +import type { Metadata, Rule, Section } from "./types.js"; import { validateRuleFile } from "./validate.js"; -import { RULES_DIR, AGENTS_OUTPUT, METADATA_FILE } from "./config.js"; -import type { Rule, Metadata, Section } from "./types.js"; /** * Parse section definitions from _sections.md */ function parseSections(): Section[] { - const sectionsFile = join(RULES_DIR, "_sections.md"); - if (!existsSync(sectionsFile)) { - console.warn("Warning: _sections.md not found, using default sections"); - return getDefaultSections(); - } + const sectionsFile = join(RULES_DIR, "_sections.md"); + if (!existsSync(sectionsFile)) { + console.warn("Warning: _sections.md not found, using default sections"); + return getDefaultSections(); + } - const content = readFileSync(sectionsFile, "utf-8"); - const sections: Section[] = []; + const content = readFileSync(sectionsFile, "utf-8"); + const sections: Section[] = []; - const sectionMatches = content.matchAll( - /##\s+(\d+)\.\s+([^\n(]+)\s*\((\w+)\)\s*\n\*\*Impact:\*\*\s*(\w+(?:-\w+)?)\s*\n\*\*Description:\*\*\s*([^\n]+)/g - ); + const sectionMatches = content.matchAll( + /##\s+(\d+)\.\s+([^\n(]+)\s*\((\w+)\)\s*\n\*\*Impact:\*\*\s*(\w+(?:-\w+)?)\s*\n\*\*Description:\*\*\s*([^\n]+)/g, + ); - for (const match of sectionMatches) { - sections.push({ - number: parseInt(match[1], 10), - title: match[2].trim(), - prefix: match[3].trim(), - impact: match[4].trim() as Section["impact"], - description: match[5].trim(), - }); - } + for (const match of sectionMatches) { + sections.push({ + number: parseInt(match[1], 10), + title: match[2].trim(), + prefix: match[3].trim(), + impact: match[4].trim() as Section["impact"], + description: match[5].trim(), + }); + } - return sections.length > 0 ? sections : getDefaultSections(); + return sections.length > 0 ? sections : getDefaultSections(); } /** * Default sections if _sections.md is missing or unparseable */ function getDefaultSections(): Section[] { - return [ - { number: 1, title: "Query Performance", prefix: "query", impact: "CRITICAL", description: "Slow queries, missing indexes, inefficient plans" }, - { number: 2, title: "Connection Management", prefix: "conn", impact: "CRITICAL", description: "Pooling, limits, serverless strategies" }, - { number: 3, title: "Schema Design", prefix: "schema", impact: "HIGH", description: "Table design, indexes, partitioning, data types" }, - { number: 4, title: "Concurrency & Locking", prefix: "lock", impact: "MEDIUM-HIGH", description: "Transactions, isolation, deadlocks" }, - { number: 5, title: "Security & RLS", prefix: "security", impact: "MEDIUM-HIGH", description: "Row-Level Security, privileges, auth patterns" }, - { number: 6, title: "Data Access Patterns", prefix: "data", impact: "MEDIUM", description: "N+1 queries, batch operations, pagination" }, - { number: 7, title: "Monitoring & Diagnostics", prefix: "monitor", impact: "LOW-MEDIUM", description: "pg_stat_statements, EXPLAIN, metrics" }, - { number: 8, title: "Advanced Features", prefix: "advanced", impact: "LOW", description: "Full-text search, JSONB, extensions" }, - ]; + return [ + { + number: 1, + title: "Query Performance", + prefix: "query", + impact: "CRITICAL", + description: "Slow queries, missing indexes, inefficient plans", + }, + { + number: 2, + title: "Connection Management", + prefix: "conn", + impact: "CRITICAL", + description: "Pooling, limits, serverless strategies", + }, + { + number: 3, + title: "Schema Design", + prefix: "schema", + impact: "HIGH", + description: "Table design, indexes, partitioning, data types", + }, + { + number: 4, + title: "Concurrency & Locking", + prefix: "lock", + impact: "MEDIUM-HIGH", + description: "Transactions, isolation, deadlocks", + }, + { + number: 5, + title: "Security & RLS", + prefix: "security", + impact: "MEDIUM-HIGH", + description: "Row-Level Security, privileges, auth patterns", + }, + { + number: 6, + title: "Data Access Patterns", + prefix: "data", + impact: "MEDIUM", + description: "N+1 queries, batch operations, pagination", + }, + { + number: 7, + title: "Monitoring & Diagnostics", + prefix: "monitor", + impact: "LOW-MEDIUM", + description: "pg_stat_statements, EXPLAIN, metrics", + }, + { + number: 8, + title: "Advanced Features", + prefix: "advanced", + impact: "LOW", + description: "Full-text search, JSONB, extensions", + }, + ]; } /** * Load metadata from metadata.json */ function loadMetadata(): Metadata { - if (!existsSync(METADATA_FILE)) { - return { - version: "1.0.0", - organization: "Supabase", - date: new Date().toLocaleDateString("en-US", { month: "long", year: "numeric" }), - abstract: "Postgres performance optimization guide for developers.", - references: [], - }; - } + if (!existsSync(METADATA_FILE)) { + return { + version: "1.0.0", + organization: "Supabase", + date: new Date().toLocaleDateString("en-US", { + month: "long", + year: "numeric", + }), + abstract: "Postgres performance optimization guide for developers.", + references: [], + }; + } - return JSON.parse(readFileSync(METADATA_FILE, "utf-8")); + return JSON.parse(readFileSync(METADATA_FILE, "utf-8")); } /** * Generate anchor from title */ function toAnchor(text: string): string { - return text - .toLowerCase() - .replace(/[^a-z0-9\s-]/g, "") - .replace(/\s+/g, "-"); + return text + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, "") + .replace(/\s+/g, "-"); } /** * Build AGENTS.md from all rule files */ function buildAgents(): void { - console.log("Building AGENTS.md...\n"); + console.log("Building AGENTS.md...\n"); - // Load metadata and sections - const metadata = loadMetadata(); - const sections = parseSections(); + // Load metadata and sections + const metadata = loadMetadata(); + const sections = parseSections(); - // Get all rule files - const ruleFiles = readdirSync(RULES_DIR) - .filter((f) => f.endsWith(".md") && !f.startsWith("_")) - .map((f) => join(RULES_DIR, f)); + // Get all rule files + const ruleFiles = readdirSync(RULES_DIR) + .filter((f) => f.endsWith(".md") && !f.startsWith("_")) + .map((f) => join(RULES_DIR, f)); - if (ruleFiles.length === 0) { - console.log("No rule files found. Generating empty AGENTS.md template."); - } + if (ruleFiles.length === 0) { + console.log("No rule files found. Generating empty AGENTS.md template."); + } - // Parse and validate all rules - const rules: Rule[] = []; + // Parse and validate all rules + const rules: Rule[] = []; - for (const file of ruleFiles) { - const validation = validateRuleFile(file); - if (!validation.valid) { - console.error(`Skipping invalid file ${basename(file)}:`); - validation.errors.forEach((e) => console.error(` - ${e}`)); - continue; - } + for (const file of ruleFiles) { + const validation = validateRuleFile(file); + if (!validation.valid) { + console.error(`Skipping invalid file ${basename(file)}:`); + for (const e of validation.errors) { + console.error(` - ${e}`); + } + continue; + } - const result = parseRuleFile(file); - if (result.success && result.rule) { - rules.push(result.rule); - } - } + const result = parseRuleFile(file); + if (result.success && result.rule) { + rules.push(result.rule); + } + } - // Group rules by section and assign IDs - const rulesBySection = new Map(); + // Group rules by section and assign IDs + const rulesBySection = new Map(); - for (const rule of rules) { - const sectionRules = rulesBySection.get(rule.section) || []; - sectionRules.push(rule); - rulesBySection.set(rule.section, sectionRules); - } + for (const rule of rules) { + const sectionRules = rulesBySection.get(rule.section) || []; + sectionRules.push(rule); + rulesBySection.set(rule.section, sectionRules); + } - // Sort rules within each section and assign IDs - for (const [sectionNum, sectionRules] of rulesBySection) { - sectionRules.sort((a, b) => a.title.localeCompare(b.title)); - sectionRules.forEach((rule, index) => { - rule.id = `${sectionNum}.${index + 1}`; - }); - } + // Sort rules within each section and assign IDs + for (const [sectionNum, sectionRules] of rulesBySection) { + sectionRules.sort((a, b) => a.title.localeCompare(b.title)); + sectionRules.forEach((rule, index) => { + rule.id = `${sectionNum}.${index + 1}`; + }); + } - // Generate markdown output - const output: string[] = []; + // Generate markdown output + const output: string[] = []; - // Header - output.push("# Postgres Best Practices\n"); - output.push(`**Version ${metadata.version}**`); - output.push(`${metadata.organization}`); - output.push(`${metadata.date}\n`); - output.push("> This document is optimized for AI agents and LLMs. Rules are prioritized by performance impact.\n"); - output.push("---\n"); + // Header + output.push("# Postgres Best Practices\n"); + output.push(`**Version ${metadata.version}**`); + output.push(`${metadata.organization}`); + output.push(`${metadata.date}\n`); + output.push( + "> This document is optimized for AI agents and LLMs. Rules are prioritized by performance impact.\n", + ); + output.push("---\n"); - // Abstract - output.push("## Abstract\n"); - output.push(`${metadata.abstract}\n`); - output.push("---\n"); + // Abstract + output.push("## Abstract\n"); + output.push(`${metadata.abstract}\n`); + output.push("---\n"); - // Table of Contents - output.push("## Table of Contents\n"); + // Table of Contents + output.push("## Table of Contents\n"); - for (const section of sections) { - const sectionRules = rulesBySection.get(section.number) || []; - output.push(`${section.number}. [${section.title}](#${toAnchor(section.title)}) - **${section.impact}**`); + for (const section of sections) { + const sectionRules = rulesBySection.get(section.number) || []; + output.push( + `${section.number}. [${section.title}](#${toAnchor(section.title)}) - **${section.impact}**`, + ); - for (const rule of sectionRules) { - output.push(` - ${rule.id} [${rule.title}](#${toAnchor(rule.id + "-" + rule.title)})`); - } + for (const rule of sectionRules) { + output.push( + ` - ${rule.id} [${rule.title}](#${toAnchor(`${rule.id}-${rule.title}`)})`, + ); + } - output.push(""); - } + output.push(""); + } - output.push("---\n"); + output.push("---\n"); - // Sections and Rules - for (const section of sections) { - const sectionRules = rulesBySection.get(section.number) || []; + // Sections and Rules + for (const section of sections) { + const sectionRules = rulesBySection.get(section.number) || []; - output.push(`## ${section.number}. ${section.title}\n`); - output.push(`**Impact: ${section.impact}**\n`); - output.push(`${section.description}\n`); + output.push(`## ${section.number}. ${section.title}\n`); + output.push(`**Impact: ${section.impact}**\n`); + output.push(`${section.description}\n`); - if (sectionRules.length === 0) { - output.push("*No rules defined yet. See rules/_template.md for creating new rules.*\n"); - } + if (sectionRules.length === 0) { + output.push( + "*No rules defined yet. See rules/_template.md for creating new rules.*\n", + ); + } - for (const rule of sectionRules) { - output.push(`### ${rule.id} ${rule.title}\n`); + for (const rule of sectionRules) { + output.push(`### ${rule.id} ${rule.title}\n`); - if (rule.impactDescription) { - output.push(`**Impact: ${rule.impact} (${rule.impactDescription})**\n`); - } else { - output.push(`**Impact: ${rule.impact}**\n`); - } + if (rule.impactDescription) { + output.push(`**Impact: ${rule.impact} (${rule.impactDescription})**\n`); + } else { + output.push(`**Impact: ${rule.impact}**\n`); + } - output.push(`${rule.explanation}\n`); + output.push(`${rule.explanation}\n`); - for (const example of rule.examples) { - if (example.description) { - output.push(`**${example.label} (${example.description}):**\n`); - } else { - output.push(`**${example.label}:**\n`); - } + for (const example of rule.examples) { + if (example.description) { + output.push(`**${example.label} (${example.description}):**\n`); + } else { + output.push(`**${example.label}:**\n`); + } - output.push("```" + (example.language || "sql")); - output.push(example.code); - output.push("```\n"); + output.push(`\`\`\`${example.language || "sql"}`); + output.push(example.code); + output.push("```\n"); - if (example.additionalText) { - output.push(`${example.additionalText}\n`); - } - } + if (example.additionalText) { + output.push(`${example.additionalText}\n`); + } + } - if (rule.references && rule.references.length > 0) { - if (rule.references.length === 1) { - output.push(`Reference: ${rule.references[0]}\n`); - } else { - output.push("References:"); - for (const ref of rule.references) { - output.push(`- ${ref}`); - } - output.push(""); - } - } + if (rule.references && rule.references.length > 0) { + if (rule.references.length === 1) { + output.push(`Reference: ${rule.references[0]}\n`); + } else { + output.push("References:"); + for (const ref of rule.references) { + output.push(`- ${ref}`); + } + output.push(""); + } + } - output.push("---\n"); - } - } + output.push("---\n"); + } + } - // References section - if (metadata.references && metadata.references.length > 0) { - output.push("## References\n"); - for (const ref of metadata.references) { - output.push(`- ${ref}`); - } - output.push(""); - } + // References section + if (metadata.references && metadata.references.length > 0) { + output.push("## References\n"); + for (const ref of metadata.references) { + output.push(`- ${ref}`); + } + output.push(""); + } - // Write output - writeFileSync(AGENTS_OUTPUT, output.join("\n")); - console.log(`Generated: ${AGENTS_OUTPUT}`); - console.log(`Total rules: ${rules.length}`); + // Write output + writeFileSync(AGENTS_OUTPUT, output.join("\n")); + console.log(`Generated: ${AGENTS_OUTPUT}`); + console.log(`Total rules: ${rules.length}`); } // Run build when executed directly -const isMainModule = process.argv[1]?.endsWith("build.ts") || process.argv[1]?.endsWith("build.js"); +const isMainModule = + process.argv[1]?.endsWith("build.ts") || + process.argv[1]?.endsWith("build.js"); if (isMainModule) { - buildAgents(); + buildAgents(); } export { buildAgents }; diff --git a/packages/postgres-best-practices-build/src/config.ts b/packages/postgres-best-practices-build/src/config.ts index 0fcb720..a8e640f 100644 --- a/packages/postgres-best-practices-build/src/config.ts +++ b/packages/postgres-best-practices-build/src/config.ts @@ -1,5 +1,5 @@ -import { fileURLToPath } from "url"; -import { dirname, join } from "path"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -8,7 +8,10 @@ const __dirname = dirname(__filename); export const BUILD_DIR = join(__dirname, ".."); // Skill directory (relative to build package) -export const SKILL_DIR = join(BUILD_DIR, "../../skills/postgres-best-practices"); +export const SKILL_DIR = join( + BUILD_DIR, + "../../skills/postgres-best-practices", +); // Rules directory export const RULES_DIR = join(SKILL_DIR, "rules"); @@ -19,23 +22,23 @@ export const METADATA_FILE = join(SKILL_DIR, "metadata.json"); // Section prefix to number mapping export const SECTION_MAP: Record = { - query: 1, - conn: 2, - connection: 2, - schema: 3, - lock: 4, - security: 5, - data: 6, - monitor: 7, - advanced: 8, + query: 1, + conn: 2, + connection: 2, + schema: 3, + lock: 4, + security: 5, + data: 6, + monitor: 7, + advanced: 8, }; // Valid impact levels in priority order export const IMPACT_LEVELS = [ - "CRITICAL", - "HIGH", - "MEDIUM-HIGH", - "MEDIUM", - "LOW-MEDIUM", - "LOW", + "CRITICAL", + "HIGH", + "MEDIUM-HIGH", + "MEDIUM", + "LOW-MEDIUM", + "LOW", ] as const; diff --git a/packages/postgres-best-practices-build/src/parser.ts b/packages/postgres-best-practices-build/src/parser.ts index 5fbc9fb..c63fa75 100644 --- a/packages/postgres-best-practices-build/src/parser.ts +++ b/packages/postgres-best-practices-build/src/parser.ts @@ -1,261 +1,275 @@ -import { readFileSync } from "fs"; -import { basename } from "path"; -import type { Rule, CodeExample, ImpactLevel, ParseResult } from "./types.js"; -import { SECTION_MAP, IMPACT_LEVELS } from "./config.js"; +import { readFileSync } from "node:fs"; +import { basename } from "node:path"; +import { IMPACT_LEVELS, SECTION_MAP } from "./config.js"; +import type { CodeExample, ImpactLevel, ParseResult, Rule } from "./types.js"; /** * Parse YAML-style frontmatter from markdown content */ function parseFrontmatter(content: string): { - frontmatter: Record; - body: string; + frontmatter: Record; + body: string; } { - const frontmatter: Record = {}; + const frontmatter: Record = {}; - if (!content.startsWith("---")) { - return { frontmatter, body: content }; - } + if (!content.startsWith("---")) { + return { frontmatter, body: content }; + } - const endIndex = content.indexOf("---", 3); - if (endIndex === -1) { - return { frontmatter, body: content }; - } + const endIndex = content.indexOf("---", 3); + if (endIndex === -1) { + return { frontmatter, body: content }; + } - const frontmatterContent = content.slice(3, endIndex).trim(); - const body = content.slice(endIndex + 3).trim(); + const frontmatterContent = content.slice(3, endIndex).trim(); + const body = content.slice(endIndex + 3).trim(); - for (const line of frontmatterContent.split("\n")) { - const colonIndex = line.indexOf(":"); - if (colonIndex === -1) continue; + for (const line of frontmatterContent.split("\n")) { + const colonIndex = line.indexOf(":"); + if (colonIndex === -1) continue; - const key = line.slice(0, colonIndex).trim(); - let value = line.slice(colonIndex + 1).trim(); + const key = line.slice(0, colonIndex).trim(); + let value = line.slice(colonIndex + 1).trim(); - // Strip quotes - if ((value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } + // Strip quotes + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } - frontmatter[key] = value; - } + frontmatter[key] = value; + } - return { frontmatter, body }; + return { frontmatter, body }; } /** * Extract section number from filename prefix */ function getSectionFromFilename(filename: string): number | null { - const base = basename(filename, ".md"); - const prefix = base.split("-")[0]; - return SECTION_MAP[prefix] ?? null; + const base = basename(filename, ".md"); + const prefix = base.split("-")[0]; + return SECTION_MAP[prefix] ?? null; } /** * Extract code examples from markdown body */ function extractExamples(body: string): CodeExample[] { - const examples: CodeExample[] = []; - const lines = body.split("\n"); + const examples: CodeExample[] = []; + const lines = body.split("\n"); - let currentLabel = ""; - let currentDescription = ""; - let inCodeBlock = false; - let codeBlockLang = ""; - let codeBlockContent: string[] = []; - let additionalText: string[] = []; + let currentLabel = ""; + let currentDescription = ""; + let inCodeBlock = false; + let codeBlockLang = ""; + let codeBlockContent: string[] = []; + let additionalText: string[] = []; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; - // Check for example label: **Label:** or **Label (description):** - const labelMatch = line.match(/^\*\*([^*]+?)(?:\s*\(([^)]+)\))?\s*:\*\*\s*$/); - if (labelMatch && !inCodeBlock) { - // Save previous example if exists - if (currentLabel && codeBlockContent.length > 0) { - examples.push({ - label: currentLabel, - description: currentDescription || undefined, - code: codeBlockContent.join("\n"), - language: codeBlockLang || undefined, - additionalText: additionalText.length > 0 ? additionalText.join("\n").trim() : undefined, - }); - } + // Check for example label: **Label:** or **Label (description):** + const labelMatch = line.match( + /^\*\*([^*]+?)(?:\s*\(([^)]+)\))?\s*:\*\*\s*$/, + ); + if (labelMatch && !inCodeBlock) { + // Save previous example if exists + if (currentLabel && codeBlockContent.length > 0) { + examples.push({ + label: currentLabel, + description: currentDescription || undefined, + code: codeBlockContent.join("\n"), + language: codeBlockLang || undefined, + additionalText: + additionalText.length > 0 + ? additionalText.join("\n").trim() + : undefined, + }); + } - currentLabel = labelMatch[1].trim(); - currentDescription = labelMatch[2]?.trim() || ""; - codeBlockContent = []; - codeBlockLang = ""; - additionalText = []; - continue; - } + currentLabel = labelMatch[1].trim(); + currentDescription = labelMatch[2]?.trim() || ""; + codeBlockContent = []; + codeBlockLang = ""; + additionalText = []; + continue; + } - // Check for code block start - if (line.startsWith("```") && !inCodeBlock) { - inCodeBlock = true; - codeBlockLang = line.slice(3).trim(); - continue; - } + // Check for code block start + if (line.startsWith("```") && !inCodeBlock) { + inCodeBlock = true; + codeBlockLang = line.slice(3).trim(); + continue; + } - // Check for code block end - if (line.startsWith("```") && inCodeBlock) { - inCodeBlock = false; - continue; - } + // Check for code block end + if (line.startsWith("```") && inCodeBlock) { + inCodeBlock = false; + continue; + } - // Collect code block content - if (inCodeBlock) { - codeBlockContent.push(line); - continue; - } + // Collect code block content + if (inCodeBlock) { + codeBlockContent.push(line); + continue; + } - // Collect additional text after code block (before next label) - if (currentLabel && codeBlockContent.length > 0 && line.trim()) { - // Stop collecting if we hit a heading or reference - if (line.startsWith("#") || line.startsWith("Reference")) { - continue; - } - additionalText.push(line); - } - } + // Collect additional text after code block (before next label) + if (currentLabel && codeBlockContent.length > 0 && line.trim()) { + // Stop collecting if we hit a heading or reference + if (line.startsWith("#") || line.startsWith("Reference")) { + continue; + } + additionalText.push(line); + } + } - // Save last example - if (currentLabel && codeBlockContent.length > 0) { - examples.push({ - label: currentLabel, - description: currentDescription || undefined, - code: codeBlockContent.join("\n"), - language: codeBlockLang || undefined, - additionalText: additionalText.length > 0 ? additionalText.join("\n").trim() : undefined, - }); - } + // Save last example + if (currentLabel && codeBlockContent.length > 0) { + examples.push({ + label: currentLabel, + description: currentDescription || undefined, + code: codeBlockContent.join("\n"), + language: codeBlockLang || undefined, + additionalText: + additionalText.length > 0 + ? additionalText.join("\n").trim() + : undefined, + }); + } - return examples; + return examples; } /** * Extract title from first ## heading */ function extractTitle(body: string): string | null { - const match = body.match(/^##\s+(.+)$/m); - return match ? match[1].trim() : null; + const match = body.match(/^##\s+(.+)$/m); + return match ? match[1].trim() : null; } /** * Extract explanation (content between title and first example) */ function extractExplanation(body: string): string { - const lines = body.split("\n"); - const explanationLines: string[] = []; - let foundTitle = false; + const lines = body.split("\n"); + const explanationLines: string[] = []; + let foundTitle = false; - for (const line of lines) { - if (line.startsWith("## ")) { - foundTitle = true; - continue; - } + for (const line of lines) { + if (line.startsWith("## ")) { + foundTitle = true; + continue; + } - if (!foundTitle) continue; + if (!foundTitle) continue; - // Stop at first example label or code block - if (line.match(/^\*\*[^*]+:\*\*/) || line.startsWith("```")) { - break; - } + // Stop at first example label or code block + if (line.match(/^\*\*[^*]+:\*\*/) || line.startsWith("```")) { + break; + } - explanationLines.push(line); - } + explanationLines.push(line); + } - return explanationLines.join("\n").trim(); + return explanationLines.join("\n").trim(); } /** * Extract references from body */ function extractReferences(body: string): string[] { - const references: string[] = []; - const lines = body.split("\n"); + const references: string[] = []; + const lines = body.split("\n"); - for (const line of lines) { - // Match "Reference: [text](url)" or "- [text](url)" after "References:" - const refMatch = line.match(/Reference:\s*\[([^\]]+)\]\(([^)]+)\)/); - if (refMatch) { - references.push(refMatch[2]); - continue; - } + for (const line of lines) { + // Match "Reference: [text](url)" or "- [text](url)" after "References:" + const refMatch = line.match(/Reference:\s*\[([^\]]+)\]\(([^)]+)\)/); + if (refMatch) { + references.push(refMatch[2]); + continue; + } - // Match list items under References section - const listMatch = line.match(/^-\s*\[([^\]]+)\]\(([^)]+)\)/); - if (listMatch) { - references.push(listMatch[2]); - } - } + // Match list items under References section + const listMatch = line.match(/^-\s*\[([^\]]+)\]\(([^)]+)\)/); + if (listMatch) { + references.push(listMatch[2]); + } + } - return references; + return references; } /** * Parse a rule file and return structured data */ export function parseRuleFile(filePath: string): ParseResult { - const errors: string[] = []; - const warnings: string[] = []; + const errors: string[] = []; + const warnings: string[] = []; - try { - const content = readFileSync(filePath, "utf-8"); - const { frontmatter, body } = parseFrontmatter(content); + try { + const content = readFileSync(filePath, "utf-8"); + const { frontmatter, body } = parseFrontmatter(content); - // Extract section from filename - const section = getSectionFromFilename(filePath); - if (section === null) { - errors.push(`Could not determine section from filename: ${basename(filePath)}`); - return { success: false, errors, warnings }; - } + // Extract section from filename + const section = getSectionFromFilename(filePath); + if (section === null) { + errors.push( + `Could not determine section from filename: ${basename(filePath)}`, + ); + return { success: false, errors, warnings }; + } - // Get title from frontmatter or body - const title = frontmatter.title || extractTitle(body); - if (!title) { - errors.push("Missing title in frontmatter or body"); - return { success: false, errors, warnings }; - } + // Get title from frontmatter or body + const title = frontmatter.title || extractTitle(body); + if (!title) { + errors.push("Missing title in frontmatter or body"); + return { success: false, errors, warnings }; + } - // Get impact level - const impact = frontmatter.impact as ImpactLevel; - if (!impact || !IMPACT_LEVELS.includes(impact)) { - errors.push(`Invalid or missing impact level: ${impact}. Must be one of: ${IMPACT_LEVELS.join(", ")}`); - return { success: false, errors, warnings }; - } + // Get impact level + const impact = frontmatter.impact as ImpactLevel; + if (!impact || !IMPACT_LEVELS.includes(impact)) { + errors.push( + `Invalid or missing impact level: ${impact}. Must be one of: ${IMPACT_LEVELS.join(", ")}`, + ); + return { success: false, errors, warnings }; + } - // Extract other fields - const explanation = extractExplanation(body); - const examples = extractExamples(body); + // Extract other fields + const explanation = extractExplanation(body); + const examples = extractExamples(body); - const tags = frontmatter.tags?.split(",").map((t) => t.trim()) || []; + const tags = frontmatter.tags?.split(",").map((t) => t.trim()) || []; - // Validation warnings - if (!explanation || explanation.length < 20) { - warnings.push("Explanation is very short or missing"); - } + // Validation warnings + if (!explanation || explanation.length < 20) { + warnings.push("Explanation is very short or missing"); + } - if (examples.length === 0) { - warnings.push("No code examples found"); - } + if (examples.length === 0) { + warnings.push("No code examples found"); + } - const rule: Rule = { - id: "", // Will be assigned during build - title, - section, - impact, - impactDescription: frontmatter.impactDescription, - explanation, - examples, - references: extractReferences(body), - tags: tags.length > 0 ? tags : undefined, - }; + const rule: Rule = { + id: "", // Will be assigned during build + title, + section, + impact, + impactDescription: frontmatter.impactDescription, + explanation, + examples, + references: extractReferences(body), + tags: tags.length > 0 ? tags : undefined, + }; - return { success: true, rule, errors, warnings }; - } catch (error) { - errors.push(`Failed to parse file: ${error}`); - return { success: false, errors, warnings }; - } + return { success: true, rule, errors, warnings }; + } catch (error) { + errors.push(`Failed to parse file: ${error}`); + return { success: false, errors, warnings }; + } } diff --git a/packages/postgres-best-practices-build/src/types.ts b/packages/postgres-best-practices-build/src/types.ts index 03a9eac..440359b 100644 --- a/packages/postgres-best-practices-build/src/types.ts +++ b/packages/postgres-best-practices-build/src/types.ts @@ -1,60 +1,59 @@ export type ImpactLevel = - | "CRITICAL" - | "HIGH" - | "MEDIUM-HIGH" - | "MEDIUM" - | "LOW-MEDIUM" - | "LOW"; + | "CRITICAL" + | "HIGH" + | "MEDIUM-HIGH" + | "MEDIUM" + | "LOW-MEDIUM" + | "LOW"; export interface CodeExample { - label: string; - description?: string; - code: string; - language?: string; - additionalText?: string; + label: string; + description?: string; + code: string; + language?: string; + additionalText?: string; } export interface Rule { - id: string; - title: string; - section: number; - subsection?: number; - impact: ImpactLevel; - impactDescription?: string; - explanation: string; - examples: CodeExample[]; - references?: string[]; - tags?: string[]; - supabaseNotes?: string; + id: string; + title: string; + section: number; + subsection?: number; + impact: ImpactLevel; + impactDescription?: string; + explanation: string; + examples: CodeExample[]; + references?: string[]; + tags?: string[]; + supabaseNotes?: string; } export interface Section { - number: number; - title: string; - prefix: string; - impact: ImpactLevel; - description: string; + number: number; + title: string; + prefix: string; + impact: ImpactLevel; + description: string; } export interface Metadata { - version: string; - organization: string; - date: string; - abstract: string; - references: string[]; - maintainers?: string[]; + version: string; + organization: string; + date: string; + abstract: string; + references: string[]; + maintainers?: string[]; } export interface ParseResult { - success: boolean; - rule?: Rule; - errors: string[]; - warnings: string[]; + success: boolean; + rule?: Rule; + errors: string[]; + warnings: string[]; } export interface ValidationResult { - valid: boolean; - errors: string[]; - warnings: string[]; + valid: boolean; + errors: string[]; + warnings: string[]; } - diff --git a/packages/postgres-best-practices-build/src/validate.ts b/packages/postgres-best-practices-build/src/validate.ts index 3ab91fa..fb4e69c 100644 --- a/packages/postgres-best-practices-build/src/validate.ts +++ b/packages/postgres-best-practices-build/src/validate.ts @@ -1,186 +1,204 @@ -import { readdirSync } from "fs"; -import { join, basename } from "path"; +import { readdirSync } from "node:fs"; +import { basename, join } from "node:path"; +import { IMPACT_LEVELS, RULES_DIR } from "./config.js"; import { parseRuleFile } from "./parser.js"; -import { RULES_DIR, IMPACT_LEVELS } from "./config.js"; import type { ValidationResult } from "./types.js"; /** * Check if an example label indicates a "bad" pattern */ function isBadExample(label: string): boolean { - const lower = label.toLowerCase(); - return lower.includes("incorrect") || lower.includes("wrong") || lower.includes("bad"); + const lower = label.toLowerCase(); + return ( + lower.includes("incorrect") || + lower.includes("wrong") || + lower.includes("bad") + ); } /** * Check if an example label indicates a "good" pattern */ function isGoodExample(label: string): boolean { - const lower = label.toLowerCase(); - return ( - lower.includes("correct") || - lower.includes("good") || - lower.includes("usage") || - lower.includes("implementation") || - lower.includes("example") || - lower.includes("recommended") - ); + const lower = label.toLowerCase(); + return ( + lower.includes("correct") || + lower.includes("good") || + lower.includes("usage") || + lower.includes("implementation") || + lower.includes("example") || + lower.includes("recommended") + ); } /** * Validate a single rule file */ export function validateRuleFile(filePath: string): ValidationResult { - const errors: string[] = []; - const warnings: string[] = []; + const errors: string[] = []; + const warnings: string[] = []; - const result = parseRuleFile(filePath); + const result = parseRuleFile(filePath); - // Add parser errors and warnings - errors.push(...result.errors); - warnings.push(...result.warnings); + // Add parser errors and warnings + errors.push(...result.errors); + warnings.push(...result.warnings); - if (!result.success || !result.rule) { - return { valid: false, errors, warnings }; - } + if (!result.success || !result.rule) { + return { valid: false, errors, warnings }; + } - const rule = result.rule; + const rule = result.rule; - // Validate title - if (!rule.title || rule.title.trim().length === 0) { - errors.push("Missing or empty title"); - } + // Validate title + if (!rule.title || rule.title.trim().length === 0) { + errors.push("Missing or empty title"); + } - // Validate explanation - if (!rule.explanation || rule.explanation.trim().length === 0) { - errors.push("Missing or empty explanation"); - } else if (rule.explanation.length < 50) { - warnings.push("Explanation is shorter than 50 characters"); - } + // Validate explanation + if (!rule.explanation || rule.explanation.trim().length === 0) { + errors.push("Missing or empty explanation"); + } else if (rule.explanation.length < 50) { + warnings.push("Explanation is shorter than 50 characters"); + } - // Validate examples - if (rule.examples.length === 0) { - errors.push("Missing examples (need at least one bad and one good example)"); - } else { - const hasBad = rule.examples.some((e) => isBadExample(e.label)); - const hasGood = rule.examples.some((e) => isGoodExample(e.label)); + // Validate examples + if (rule.examples.length === 0) { + errors.push( + "Missing examples (need at least one bad and one good example)", + ); + } else { + const hasBad = rule.examples.some((e) => isBadExample(e.label)); + const hasGood = rule.examples.some((e) => isGoodExample(e.label)); - if (!hasBad && !hasGood) { - errors.push("Missing bad/incorrect and good/correct examples"); - } else if (!hasBad) { - warnings.push("Missing bad/incorrect example (recommended for clarity)"); - } else if (!hasGood) { - errors.push("Missing good/correct example"); - } + if (!hasBad && !hasGood) { + errors.push("Missing bad/incorrect and good/correct examples"); + } else if (!hasBad) { + warnings.push("Missing bad/incorrect example (recommended for clarity)"); + } else if (!hasGood) { + errors.push("Missing good/correct example"); + } - // Check for code in examples - const hasCode = rule.examples.some((e) => e.code && e.code.trim().length > 0); - if (!hasCode) { - errors.push("Examples have no code"); - } + // Check for code in examples + const hasCode = rule.examples.some( + (e) => e.code && e.code.trim().length > 0, + ); + if (!hasCode) { + errors.push("Examples have no code"); + } - // Check for language specification - for (const example of rule.examples) { - if (example.code && !example.language) { - warnings.push(`Example "${example.label}" missing language specification`); - } - } - } + // Check for language specification + for (const example of rule.examples) { + if (example.code && !example.language) { + warnings.push( + `Example "${example.label}" missing language specification`, + ); + } + } + } - // Validate impact level - if (!IMPACT_LEVELS.includes(rule.impact)) { - errors.push(`Invalid impact level: ${rule.impact}. Must be one of: ${IMPACT_LEVELS.join(", ")}`); - } + // Validate impact level + if (!IMPACT_LEVELS.includes(rule.impact)) { + errors.push( + `Invalid impact level: ${rule.impact}. Must be one of: ${IMPACT_LEVELS.join(", ")}`, + ); + } - // Warning for missing impact description - if (!rule.impactDescription) { - warnings.push("Missing impactDescription (recommended for quantifying benefit)"); - } + // Warning for missing impact description + if (!rule.impactDescription) { + warnings.push( + "Missing impactDescription (recommended for quantifying benefit)", + ); + } - return { - valid: errors.length === 0, - errors, - warnings, - }; + return { + valid: errors.length === 0, + errors, + warnings, + }; } /** * Validate all rule files in the rules directory */ export function validateAllRules(): { - totalFiles: number; - validFiles: number; - invalidFiles: number; - results: Map; + totalFiles: number; + validFiles: number; + invalidFiles: number; + results: Map; } { - const results = new Map(); - let validFiles = 0; - let invalidFiles = 0; + const results = new Map(); + let validFiles = 0; + let invalidFiles = 0; - // Get all markdown files (excluding _ prefixed files) - const files = readdirSync(RULES_DIR) - .filter((f) => f.endsWith(".md") && !f.startsWith("_")) - .map((f) => join(RULES_DIR, f)); + // Get all markdown files (excluding _ prefixed files) + const files = readdirSync(RULES_DIR) + .filter((f) => f.endsWith(".md") && !f.startsWith("_")) + .map((f) => join(RULES_DIR, f)); - for (const file of files) { - const result = validateRuleFile(file); - results.set(basename(file), result); + for (const file of files) { + const result = validateRuleFile(file); + results.set(basename(file), result); - if (result.valid) { - validFiles++; - } else { - invalidFiles++; - } - } + if (result.valid) { + validFiles++; + } else { + invalidFiles++; + } + } - return { - totalFiles: files.length, - validFiles, - invalidFiles, - results, - }; + return { + totalFiles: files.length, + validFiles, + invalidFiles, + results, + }; } // Run validation when executed directly -const isMainModule = process.argv[1]?.endsWith("validate.ts") || process.argv[1]?.endsWith("validate.js"); +const isMainModule = + process.argv[1]?.endsWith("validate.ts") || + process.argv[1]?.endsWith("validate.js"); if (isMainModule) { - console.log("Validating Postgres best practices rules...\n"); + console.log("Validating Postgres best practices rules...\n"); - const { totalFiles, validFiles, invalidFiles, results } = validateAllRules(); + const { totalFiles, validFiles, invalidFiles, results } = validateAllRules(); - if (totalFiles === 0) { - console.log("No rule files found (this is expected for initial setup)."); - console.log("Create rule files in: skills/postgres-best-practices/rules/"); - console.log("Use the _template.md as a starting point.\n"); - process.exit(0); - } + if (totalFiles === 0) { + console.log("No rule files found (this is expected for initial setup)."); + console.log("Create rule files in: skills/postgres-best-practices/rules/"); + console.log("Use the _template.md as a starting point.\n"); + process.exit(0); + } - let hasErrors = false; + let hasErrors = false; - for (const [filename, result] of results) { - if (!result.valid || result.warnings.length > 0) { - console.log(`\n${filename}:`); + for (const [filename, result] of results) { + if (!result.valid || result.warnings.length > 0) { + console.log(`\n${filename}:`); - for (const error of result.errors) { - console.log(` ERROR: ${error}`); - hasErrors = true; - } + for (const error of result.errors) { + console.log(` ERROR: ${error}`); + hasErrors = true; + } - for (const warning of result.warnings) { - console.log(` WARNING: ${warning}`); - } - } - } + for (const warning of result.warnings) { + console.log(` WARNING: ${warning}`); + } + } + } - console.log(`\n${"=".repeat(50)}`); - console.log(`Total: ${totalFiles} files | Valid: ${validFiles} | Invalid: ${invalidFiles}`); + console.log(`\n${"=".repeat(50)}`); + console.log( + `Total: ${totalFiles} files | Valid: ${validFiles} | Invalid: ${invalidFiles}`, + ); - if (hasErrors) { - console.log("\nValidation failed. Please fix the errors above."); - process.exit(1); - } else { - console.log("\nValidation passed!"); - process.exit(0); - } + if (hasErrors) { + console.log("\nValidation failed. Please fix the errors above."); + process.exit(1); + } else { + console.log("\nValidation passed!"); + process.exit(0); + } } diff --git a/packages/postgres-best-practices-build/tsconfig.json b/packages/postgres-best-practices-build/tsconfig.json index f5733ee..c0524f9 100644 --- a/packages/postgres-best-practices-build/tsconfig.json +++ b/packages/postgres-best-practices-build/tsconfig.json @@ -1,16 +1,16 @@ { - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "esModuleInterop": true, - "strict": true, - "skipLibCheck": true, - "outDir": "dist", - "rootDir": "src", - "declaration": true, - "resolveJsonModule": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] } diff --git a/skills/postgres-best-practices/metadata.json b/skills/postgres-best-practices/metadata.json index c82546a..1cd5f0a 100644 --- a/skills/postgres-best-practices/metadata.json +++ b/skills/postgres-best-practices/metadata.json @@ -1,13 +1,13 @@ { - "version": "1.0.0", - "organization": "Supabase", - "date": "January 2026", - "abstract": "Comprehensive Postgres performance optimization guide for developers using Supabase and Postgres. Contains performance rules across 8 categories, prioritized by impact from critical (query performance, connection management) to incremental (advanced features). Each rule includes detailed explanations, incorrect vs. correct SQL examples, query plan analysis, and specific performance metrics to guide automated optimization and code generation.", - "references": [ - "https://www.postgresql.org/docs/current/", - "https://supabase.com/docs", - "https://wiki.postgresql.org/wiki/Performance_Optimization", - "https://supabase.com/docs/guides/database/overview", - "https://supabase.com/docs/guides/auth/row-level-security" - ] + "version": "1.0.0", + "organization": "Supabase", + "date": "January 2026", + "abstract": "Comprehensive Postgres performance optimization guide for developers using Supabase and Postgres. Contains performance rules across 8 categories, prioritized by impact from critical (query performance, connection management) to incremental (advanced features). Each rule includes detailed explanations, incorrect vs. correct SQL examples, query plan analysis, and specific performance metrics to guide automated optimization and code generation.", + "references": [ + "https://www.postgresql.org/docs/current/", + "https://supabase.com/docs", + "https://wiki.postgresql.org/wiki/Performance_Optimization", + "https://supabase.com/docs/guides/database/overview", + "https://supabase.com/docs/guides/auth/row-level-security" + ] }