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 <noreply@anthropic.com>
This commit is contained in:
Pedro Rodrigues
2026-01-22 08:28:49 +00:00
committed by GitHub
parent 0ffac720f0
commit f323d3b601
13 changed files with 980 additions and 609 deletions

View File

@@ -1,22 +1,20 @@
{ {
"name": "supabase-agent-skills", "name": "supabase-agent-skills",
"owner": { "owner": {
"name": "Supabase", "name": "Supabase",
"email": "support@supabase.com" "email": "support@supabase.com"
}, },
"metadata": { "metadata": {
"description": "Official Supabase agent skills for Claude Code", "description": "Official Supabase agent skills for Claude Code",
"version": "1.0.0" "version": "1.0.0"
}, },
"plugins": [ "plugins": [
{ {
"name": "postgres-best-practices", "name": "postgres-best-practices",
"description": "Postgres performance optimization and best practices. Use when writing, reviewing, or optimizing Postgres queries, schema designs, or database configurations.", "description": "Postgres performance optimization and best practices. Use when writing, reviewing, or optimizing Postgres queries, schema designs, or database configurations.",
"source": "./", "source": "./",
"strict": false, "strict": false,
"skills": [ "skills": ["./skills/postgres-best-practices"]
"./skills/postgres-best-practices" }
] ]
}
]
} }

54
.github/workflows/ci.yml vendored Normal file
View File

@@ -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

34
biome.json Normal file
View File

@@ -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"
}
}
}
}

179
package-lock.json generated Normal file
View File

@@ -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"
}
}
}
}

View File

@@ -1,11 +1,20 @@
{ {
"name": "supabase-agent-skills", "name": "supabase-agent-skills",
"version": "1.0.0", "version": "1.0.0",
"author": "Supabase", "author": "Supabase",
"license": "MIT", "license": "MIT",
"description": "Official Supabase agent skills", "description": "Official Supabase agent skills",
"scripts": { "scripts": {
"build": "npm --prefix packages/postgres-best-practices-build run build", "build": "npm --prefix packages/postgres-best-practices-build run build",
"validate": "npm --prefix packages/postgres-best-practices-build run validate" "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"
}
} }

View File

@@ -1,18 +1,18 @@
{ {
"name": "postgres-best-practices-build", "name": "postgres-best-practices-build",
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"author": "Supabase", "author": "Supabase",
"license": "MIT", "license": "MIT",
"description": "Build system for Supabase agent skills", "description": "Build system for Supabase agent skills",
"scripts": { "scripts": {
"build": "tsx src/build.ts", "build": "tsx src/build.ts",
"validate": "tsx src/validate.ts", "validate": "tsx src/validate.ts",
"dev": "npm run validate && npm run build" "dev": "npm run validate && npm run build"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.10.0", "@types/node": "^20.10.0",
"tsx": "^4.7.0", "tsx": "^4.7.0",
"typescript": "^5.3.0" "typescript": "^5.3.0"
} }
} }

View File

@@ -1,243 +1,306 @@
import { readdirSync, readFileSync, writeFileSync, existsSync } from "fs"; import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join, basename } from "path"; import { basename, join } from "node:path";
import { AGENTS_OUTPUT, METADATA_FILE, RULES_DIR } from "./config.js";
import { parseRuleFile } from "./parser.js"; import { parseRuleFile } from "./parser.js";
import type { Metadata, Rule, Section } from "./types.js";
import { validateRuleFile } from "./validate.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 * Parse section definitions from _sections.md
*/ */
function parseSections(): Section[] { function parseSections(): Section[] {
const sectionsFile = join(RULES_DIR, "_sections.md"); const sectionsFile = join(RULES_DIR, "_sections.md");
if (!existsSync(sectionsFile)) { if (!existsSync(sectionsFile)) {
console.warn("Warning: _sections.md not found, using default sections"); console.warn("Warning: _sections.md not found, using default sections");
return getDefaultSections(); return getDefaultSections();
} }
const content = readFileSync(sectionsFile, "utf-8"); const content = readFileSync(sectionsFile, "utf-8");
const sections: Section[] = []; const sections: Section[] = [];
const sectionMatches = content.matchAll( const sectionMatches = content.matchAll(
/##\s+(\d+)\.\s+([^\n(]+)\s*\((\w+)\)\s*\n\*\*Impact:\*\*\s*(\w+(?:-\w+)?)\s*\n\*\*Description:\*\*\s*([^\n]+)/g /##\s+(\d+)\.\s+([^\n(]+)\s*\((\w+)\)\s*\n\*\*Impact:\*\*\s*(\w+(?:-\w+)?)\s*\n\*\*Description:\*\*\s*([^\n]+)/g,
); );
for (const match of sectionMatches) { for (const match of sectionMatches) {
sections.push({ sections.push({
number: parseInt(match[1], 10), number: parseInt(match[1], 10),
title: match[2].trim(), title: match[2].trim(),
prefix: match[3].trim(), prefix: match[3].trim(),
impact: match[4].trim() as Section["impact"], impact: match[4].trim() as Section["impact"],
description: match[5].trim(), 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 * Default sections if _sections.md is missing or unparseable
*/ */
function getDefaultSections(): Section[] { function getDefaultSections(): Section[] {
return [ 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: 1,
{ number: 3, title: "Schema Design", prefix: "schema", impact: "HIGH", description: "Table design, indexes, partitioning, data types" }, title: "Query Performance",
{ number: 4, title: "Concurrency & Locking", prefix: "lock", impact: "MEDIUM-HIGH", description: "Transactions, isolation, deadlocks" }, prefix: "query",
{ number: 5, title: "Security & RLS", prefix: "security", impact: "MEDIUM-HIGH", description: "Row-Level Security, privileges, auth patterns" }, impact: "CRITICAL",
{ number: 6, title: "Data Access Patterns", prefix: "data", impact: "MEDIUM", description: "N+1 queries, batch operations, pagination" }, description: "Slow queries, missing indexes, inefficient plans",
{ 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" }, {
]; 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 * Load metadata from metadata.json
*/ */
function loadMetadata(): Metadata { function loadMetadata(): Metadata {
if (!existsSync(METADATA_FILE)) { if (!existsSync(METADATA_FILE)) {
return { return {
version: "1.0.0", version: "1.0.0",
organization: "Supabase", organization: "Supabase",
date: new Date().toLocaleDateString("en-US", { month: "long", year: "numeric" }), date: new Date().toLocaleDateString("en-US", {
abstract: "Postgres performance optimization guide for developers.", month: "long",
references: [], 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 * Generate anchor from title
*/ */
function toAnchor(text: string): string { function toAnchor(text: string): string {
return text return text
.toLowerCase() .toLowerCase()
.replace(/[^a-z0-9\s-]/g, "") .replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-"); .replace(/\s+/g, "-");
} }
/** /**
* Build AGENTS.md from all rule files * Build AGENTS.md from all rule files
*/ */
function buildAgents(): void { function buildAgents(): void {
console.log("Building AGENTS.md...\n"); console.log("Building AGENTS.md...\n");
// Load metadata and sections // Load metadata and sections
const metadata = loadMetadata(); const metadata = loadMetadata();
const sections = parseSections(); const sections = parseSections();
// Get all rule files // Get all rule files
const ruleFiles = readdirSync(RULES_DIR) const ruleFiles = readdirSync(RULES_DIR)
.filter((f) => f.endsWith(".md") && !f.startsWith("_")) .filter((f) => f.endsWith(".md") && !f.startsWith("_"))
.map((f) => join(RULES_DIR, f)); .map((f) => join(RULES_DIR, f));
if (ruleFiles.length === 0) { if (ruleFiles.length === 0) {
console.log("No rule files found. Generating empty AGENTS.md template."); console.log("No rule files found. Generating empty AGENTS.md template.");
} }
// Parse and validate all rules // Parse and validate all rules
const rules: Rule[] = []; const rules: Rule[] = [];
for (const file of ruleFiles) { for (const file of ruleFiles) {
const validation = validateRuleFile(file); const validation = validateRuleFile(file);
if (!validation.valid) { if (!validation.valid) {
console.error(`Skipping invalid file ${basename(file)}:`); console.error(`Skipping invalid file ${basename(file)}:`);
validation.errors.forEach((e) => console.error(` - ${e}`)); for (const e of validation.errors) {
continue; console.error(` - ${e}`);
} }
continue;
}
const result = parseRuleFile(file); const result = parseRuleFile(file);
if (result.success && result.rule) { if (result.success && result.rule) {
rules.push(result.rule); rules.push(result.rule);
} }
} }
// Group rules by section and assign IDs // Group rules by section and assign IDs
const rulesBySection = new Map<number, Rule[]>(); const rulesBySection = new Map<number, Rule[]>();
for (const rule of rules) { for (const rule of rules) {
const sectionRules = rulesBySection.get(rule.section) || []; const sectionRules = rulesBySection.get(rule.section) || [];
sectionRules.push(rule); sectionRules.push(rule);
rulesBySection.set(rule.section, sectionRules); rulesBySection.set(rule.section, sectionRules);
} }
// Sort rules within each section and assign IDs // Sort rules within each section and assign IDs
for (const [sectionNum, sectionRules] of rulesBySection) { for (const [sectionNum, sectionRules] of rulesBySection) {
sectionRules.sort((a, b) => a.title.localeCompare(b.title)); sectionRules.sort((a, b) => a.title.localeCompare(b.title));
sectionRules.forEach((rule, index) => { sectionRules.forEach((rule, index) => {
rule.id = `${sectionNum}.${index + 1}`; rule.id = `${sectionNum}.${index + 1}`;
}); });
} }
// Generate markdown output // Generate markdown output
const output: string[] = []; const output: string[] = [];
// Header // Header
output.push("# Postgres Best Practices\n"); output.push("# Postgres Best Practices\n");
output.push(`**Version ${metadata.version}**`); output.push(`**Version ${metadata.version}**`);
output.push(`${metadata.organization}`); output.push(`${metadata.organization}`);
output.push(`${metadata.date}\n`); 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(
output.push("---\n"); "> This document is optimized for AI agents and LLMs. Rules are prioritized by performance impact.\n",
);
output.push("---\n");
// Abstract // Abstract
output.push("## Abstract\n"); output.push("## Abstract\n");
output.push(`${metadata.abstract}\n`); output.push(`${metadata.abstract}\n`);
output.push("---\n"); output.push("---\n");
// Table of Contents // Table of Contents
output.push("## Table of Contents\n"); output.push("## Table of Contents\n");
for (const section of sections) { for (const section of sections) {
const sectionRules = rulesBySection.get(section.number) || []; const sectionRules = rulesBySection.get(section.number) || [];
output.push(`${section.number}. [${section.title}](#${toAnchor(section.title)}) - **${section.impact}**`); output.push(
`${section.number}. [${section.title}](#${toAnchor(section.title)}) - **${section.impact}**`,
);
for (const rule of sectionRules) { for (const rule of sectionRules) {
output.push(` - ${rule.id} [${rule.title}](#${toAnchor(rule.id + "-" + rule.title)})`); output.push(
} ` - ${rule.id} [${rule.title}](#${toAnchor(`${rule.id}-${rule.title}`)})`,
);
}
output.push(""); output.push("");
} }
output.push("---\n"); output.push("---\n");
// Sections and Rules // Sections and Rules
for (const section of sections) { for (const section of sections) {
const sectionRules = rulesBySection.get(section.number) || []; const sectionRules = rulesBySection.get(section.number) || [];
output.push(`## ${section.number}. ${section.title}\n`); output.push(`## ${section.number}. ${section.title}\n`);
output.push(`**Impact: ${section.impact}**\n`); output.push(`**Impact: ${section.impact}**\n`);
output.push(`${section.description}\n`); output.push(`${section.description}\n`);
if (sectionRules.length === 0) { if (sectionRules.length === 0) {
output.push("*No rules defined yet. See rules/_template.md for creating new rules.*\n"); output.push(
} "*No rules defined yet. See rules/_template.md for creating new rules.*\n",
);
}
for (const rule of sectionRules) { for (const rule of sectionRules) {
output.push(`### ${rule.id} ${rule.title}\n`); output.push(`### ${rule.id} ${rule.title}\n`);
if (rule.impactDescription) { if (rule.impactDescription) {
output.push(`**Impact: ${rule.impact} (${rule.impactDescription})**\n`); output.push(`**Impact: ${rule.impact} (${rule.impactDescription})**\n`);
} else { } else {
output.push(`**Impact: ${rule.impact}**\n`); output.push(`**Impact: ${rule.impact}**\n`);
} }
output.push(`${rule.explanation}\n`); output.push(`${rule.explanation}\n`);
for (const example of rule.examples) { for (const example of rule.examples) {
if (example.description) { if (example.description) {
output.push(`**${example.label} (${example.description}):**\n`); output.push(`**${example.label} (${example.description}):**\n`);
} else { } else {
output.push(`**${example.label}:**\n`); output.push(`**${example.label}:**\n`);
} }
output.push("```" + (example.language || "sql")); output.push(`\`\`\`${example.language || "sql"}`);
output.push(example.code); output.push(example.code);
output.push("```\n"); output.push("```\n");
if (example.additionalText) { if (example.additionalText) {
output.push(`${example.additionalText}\n`); output.push(`${example.additionalText}\n`);
} }
} }
if (rule.references && rule.references.length > 0) { if (rule.references && rule.references.length > 0) {
if (rule.references.length === 1) { if (rule.references.length === 1) {
output.push(`Reference: ${rule.references[0]}\n`); output.push(`Reference: ${rule.references[0]}\n`);
} else { } else {
output.push("References:"); output.push("References:");
for (const ref of rule.references) { for (const ref of rule.references) {
output.push(`- ${ref}`); output.push(`- ${ref}`);
} }
output.push(""); output.push("");
} }
} }
output.push("---\n"); output.push("---\n");
} }
} }
// References section // References section
if (metadata.references && metadata.references.length > 0) { if (metadata.references && metadata.references.length > 0) {
output.push("## References\n"); output.push("## References\n");
for (const ref of metadata.references) { for (const ref of metadata.references) {
output.push(`- ${ref}`); output.push(`- ${ref}`);
} }
output.push(""); output.push("");
} }
// Write output // Write output
writeFileSync(AGENTS_OUTPUT, output.join("\n")); writeFileSync(AGENTS_OUTPUT, output.join("\n"));
console.log(`Generated: ${AGENTS_OUTPUT}`); console.log(`Generated: ${AGENTS_OUTPUT}`);
console.log(`Total rules: ${rules.length}`); console.log(`Total rules: ${rules.length}`);
} }
// Run build when executed directly // 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) { if (isMainModule) {
buildAgents(); buildAgents();
} }
export { buildAgents }; export { buildAgents };

View File

@@ -1,5 +1,5 @@
import { fileURLToPath } from "url"; import { dirname, join } from "node:path";
import { dirname, join } from "path"; import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
@@ -8,7 +8,10 @@ const __dirname = dirname(__filename);
export const BUILD_DIR = join(__dirname, ".."); export const BUILD_DIR = join(__dirname, "..");
// Skill directory (relative to build package) // 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 // Rules directory
export const RULES_DIR = join(SKILL_DIR, "rules"); 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 // Section prefix to number mapping
export const SECTION_MAP: Record<string, number> = { export const SECTION_MAP: Record<string, number> = {
query: 1, query: 1,
conn: 2, conn: 2,
connection: 2, connection: 2,
schema: 3, schema: 3,
lock: 4, lock: 4,
security: 5, security: 5,
data: 6, data: 6,
monitor: 7, monitor: 7,
advanced: 8, advanced: 8,
}; };
// Valid impact levels in priority order // Valid impact levels in priority order
export const IMPACT_LEVELS = [ export const IMPACT_LEVELS = [
"CRITICAL", "CRITICAL",
"HIGH", "HIGH",
"MEDIUM-HIGH", "MEDIUM-HIGH",
"MEDIUM", "MEDIUM",
"LOW-MEDIUM", "LOW-MEDIUM",
"LOW", "LOW",
] as const; ] as const;

View File

@@ -1,261 +1,275 @@
import { readFileSync } from "fs"; import { readFileSync } from "node:fs";
import { basename } from "path"; import { basename } from "node:path";
import type { Rule, CodeExample, ImpactLevel, ParseResult } from "./types.js"; import { IMPACT_LEVELS, SECTION_MAP } from "./config.js";
import { SECTION_MAP, IMPACT_LEVELS } from "./config.js"; import type { CodeExample, ImpactLevel, ParseResult, Rule } from "./types.js";
/** /**
* Parse YAML-style frontmatter from markdown content * Parse YAML-style frontmatter from markdown content
*/ */
function parseFrontmatter(content: string): { function parseFrontmatter(content: string): {
frontmatter: Record<string, string>; frontmatter: Record<string, string>;
body: string; body: string;
} { } {
const frontmatter: Record<string, string> = {}; const frontmatter: Record<string, string> = {};
if (!content.startsWith("---")) { if (!content.startsWith("---")) {
return { frontmatter, body: content }; return { frontmatter, body: content };
} }
const endIndex = content.indexOf("---", 3); const endIndex = content.indexOf("---", 3);
if (endIndex === -1) { if (endIndex === -1) {
return { frontmatter, body: content }; return { frontmatter, body: content };
} }
const frontmatterContent = content.slice(3, endIndex).trim(); const frontmatterContent = content.slice(3, endIndex).trim();
const body = content.slice(endIndex + 3).trim(); const body = content.slice(endIndex + 3).trim();
for (const line of frontmatterContent.split("\n")) { for (const line of frontmatterContent.split("\n")) {
const colonIndex = line.indexOf(":"); const colonIndex = line.indexOf(":");
if (colonIndex === -1) continue; if (colonIndex === -1) continue;
const key = line.slice(0, colonIndex).trim(); const key = line.slice(0, colonIndex).trim();
let value = line.slice(colonIndex + 1).trim(); let value = line.slice(colonIndex + 1).trim();
// Strip quotes // Strip quotes
if ((value.startsWith('"') && value.endsWith('"')) || if (
(value.startsWith("'") && value.endsWith("'"))) { (value.startsWith('"') && value.endsWith('"')) ||
value = value.slice(1, -1); (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 * Extract section number from filename prefix
*/ */
function getSectionFromFilename(filename: string): number | null { function getSectionFromFilename(filename: string): number | null {
const base = basename(filename, ".md"); const base = basename(filename, ".md");
const prefix = base.split("-")[0]; const prefix = base.split("-")[0];
return SECTION_MAP[prefix] ?? null; return SECTION_MAP[prefix] ?? null;
} }
/** /**
* Extract code examples from markdown body * Extract code examples from markdown body
*/ */
function extractExamples(body: string): CodeExample[] { function extractExamples(body: string): CodeExample[] {
const examples: CodeExample[] = []; const examples: CodeExample[] = [];
const lines = body.split("\n"); const lines = body.split("\n");
let currentLabel = ""; let currentLabel = "";
let currentDescription = ""; let currentDescription = "";
let inCodeBlock = false; let inCodeBlock = false;
let codeBlockLang = ""; let codeBlockLang = "";
let codeBlockContent: string[] = []; let codeBlockContent: string[] = [];
let additionalText: string[] = []; let additionalText: string[] = [];
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i]; const line = lines[i];
// Check for example label: **Label:** or **Label (description):** // Check for example label: **Label:** or **Label (description):**
const labelMatch = line.match(/^\*\*([^*]+?)(?:\s*\(([^)]+)\))?\s*:\*\*\s*$/); const labelMatch = line.match(
if (labelMatch && !inCodeBlock) { /^\*\*([^*]+?)(?:\s*\(([^)]+)\))?\s*:\*\*\s*$/,
// Save previous example if exists );
if (currentLabel && codeBlockContent.length > 0) { if (labelMatch && !inCodeBlock) {
examples.push({ // Save previous example if exists
label: currentLabel, if (currentLabel && codeBlockContent.length > 0) {
description: currentDescription || undefined, examples.push({
code: codeBlockContent.join("\n"), label: currentLabel,
language: codeBlockLang || undefined, description: currentDescription || undefined,
additionalText: additionalText.length > 0 ? additionalText.join("\n").trim() : undefined, code: codeBlockContent.join("\n"),
}); language: codeBlockLang || undefined,
} additionalText:
additionalText.length > 0
? additionalText.join("\n").trim()
: undefined,
});
}
currentLabel = labelMatch[1].trim(); currentLabel = labelMatch[1].trim();
currentDescription = labelMatch[2]?.trim() || ""; currentDescription = labelMatch[2]?.trim() || "";
codeBlockContent = []; codeBlockContent = [];
codeBlockLang = ""; codeBlockLang = "";
additionalText = []; additionalText = [];
continue; continue;
} }
// Check for code block start // Check for code block start
if (line.startsWith("```") && !inCodeBlock) { if (line.startsWith("```") && !inCodeBlock) {
inCodeBlock = true; inCodeBlock = true;
codeBlockLang = line.slice(3).trim(); codeBlockLang = line.slice(3).trim();
continue; continue;
} }
// Check for code block end // Check for code block end
if (line.startsWith("```") && inCodeBlock) { if (line.startsWith("```") && inCodeBlock) {
inCodeBlock = false; inCodeBlock = false;
continue; continue;
} }
// Collect code block content // Collect code block content
if (inCodeBlock) { if (inCodeBlock) {
codeBlockContent.push(line); codeBlockContent.push(line);
continue; continue;
} }
// Collect additional text after code block (before next label) // Collect additional text after code block (before next label)
if (currentLabel && codeBlockContent.length > 0 && line.trim()) { if (currentLabel && codeBlockContent.length > 0 && line.trim()) {
// Stop collecting if we hit a heading or reference // Stop collecting if we hit a heading or reference
if (line.startsWith("#") || line.startsWith("Reference")) { if (line.startsWith("#") || line.startsWith("Reference")) {
continue; continue;
} }
additionalText.push(line); additionalText.push(line);
} }
} }
// Save last example // Save last example
if (currentLabel && codeBlockContent.length > 0) { if (currentLabel && codeBlockContent.length > 0) {
examples.push({ examples.push({
label: currentLabel, label: currentLabel,
description: currentDescription || undefined, description: currentDescription || undefined,
code: codeBlockContent.join("\n"), code: codeBlockContent.join("\n"),
language: codeBlockLang || undefined, language: codeBlockLang || undefined,
additionalText: additionalText.length > 0 ? additionalText.join("\n").trim() : undefined, additionalText:
}); additionalText.length > 0
} ? additionalText.join("\n").trim()
: undefined,
});
}
return examples; return examples;
} }
/** /**
* Extract title from first ## heading * Extract title from first ## heading
*/ */
function extractTitle(body: string): string | null { function extractTitle(body: string): string | null {
const match = body.match(/^##\s+(.+)$/m); const match = body.match(/^##\s+(.+)$/m);
return match ? match[1].trim() : null; return match ? match[1].trim() : null;
} }
/** /**
* Extract explanation (content between title and first example) * Extract explanation (content between title and first example)
*/ */
function extractExplanation(body: string): string { function extractExplanation(body: string): string {
const lines = body.split("\n"); const lines = body.split("\n");
const explanationLines: string[] = []; const explanationLines: string[] = [];
let foundTitle = false; let foundTitle = false;
for (const line of lines) { for (const line of lines) {
if (line.startsWith("## ")) { if (line.startsWith("## ")) {
foundTitle = true; foundTitle = true;
continue; continue;
} }
if (!foundTitle) continue; if (!foundTitle) continue;
// Stop at first example label or code block // Stop at first example label or code block
if (line.match(/^\*\*[^*]+:\*\*/) || line.startsWith("```")) { if (line.match(/^\*\*[^*]+:\*\*/) || line.startsWith("```")) {
break; break;
} }
explanationLines.push(line); explanationLines.push(line);
} }
return explanationLines.join("\n").trim(); return explanationLines.join("\n").trim();
} }
/** /**
* Extract references from body * Extract references from body
*/ */
function extractReferences(body: string): string[] { function extractReferences(body: string): string[] {
const references: string[] = []; const references: string[] = [];
const lines = body.split("\n"); const lines = body.split("\n");
for (const line of lines) { for (const line of lines) {
// Match "Reference: [text](url)" or "- [text](url)" after "References:" // Match "Reference: [text](url)" or "- [text](url)" after "References:"
const refMatch = line.match(/Reference:\s*\[([^\]]+)\]\(([^)]+)\)/); const refMatch = line.match(/Reference:\s*\[([^\]]+)\]\(([^)]+)\)/);
if (refMatch) { if (refMatch) {
references.push(refMatch[2]); references.push(refMatch[2]);
continue; continue;
} }
// Match list items under References section // Match list items under References section
const listMatch = line.match(/^-\s*\[([^\]]+)\]\(([^)]+)\)/); const listMatch = line.match(/^-\s*\[([^\]]+)\]\(([^)]+)\)/);
if (listMatch) { if (listMatch) {
references.push(listMatch[2]); references.push(listMatch[2]);
} }
} }
return references; return references;
} }
/** /**
* Parse a rule file and return structured data * Parse a rule file and return structured data
*/ */
export function parseRuleFile(filePath: string): ParseResult { export function parseRuleFile(filePath: string): ParseResult {
const errors: string[] = []; const errors: string[] = [];
const warnings: string[] = []; const warnings: string[] = [];
try { try {
const content = readFileSync(filePath, "utf-8"); const content = readFileSync(filePath, "utf-8");
const { frontmatter, body } = parseFrontmatter(content); const { frontmatter, body } = parseFrontmatter(content);
// Extract section from filename // Extract section from filename
const section = getSectionFromFilename(filePath); const section = getSectionFromFilename(filePath);
if (section === null) { if (section === null) {
errors.push(`Could not determine section from filename: ${basename(filePath)}`); errors.push(
return { success: false, errors, warnings }; `Could not determine section from filename: ${basename(filePath)}`,
} );
return { success: false, errors, warnings };
}
// Get title from frontmatter or body // Get title from frontmatter or body
const title = frontmatter.title || extractTitle(body); const title = frontmatter.title || extractTitle(body);
if (!title) { if (!title) {
errors.push("Missing title in frontmatter or body"); errors.push("Missing title in frontmatter or body");
return { success: false, errors, warnings }; return { success: false, errors, warnings };
} }
// Get impact level // Get impact level
const impact = frontmatter.impact as ImpactLevel; const impact = frontmatter.impact as ImpactLevel;
if (!impact || !IMPACT_LEVELS.includes(impact)) { if (!impact || !IMPACT_LEVELS.includes(impact)) {
errors.push(`Invalid or missing impact level: ${impact}. Must be one of: ${IMPACT_LEVELS.join(", ")}`); errors.push(
return { success: false, errors, warnings }; `Invalid or missing impact level: ${impact}. Must be one of: ${IMPACT_LEVELS.join(", ")}`,
} );
return { success: false, errors, warnings };
}
// Extract other fields // Extract other fields
const explanation = extractExplanation(body); const explanation = extractExplanation(body);
const examples = extractExamples(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 // Validation warnings
if (!explanation || explanation.length < 20) { if (!explanation || explanation.length < 20) {
warnings.push("Explanation is very short or missing"); warnings.push("Explanation is very short or missing");
} }
if (examples.length === 0) { if (examples.length === 0) {
warnings.push("No code examples found"); warnings.push("No code examples found");
} }
const rule: Rule = { const rule: Rule = {
id: "", // Will be assigned during build id: "", // Will be assigned during build
title, title,
section, section,
impact, impact,
impactDescription: frontmatter.impactDescription, impactDescription: frontmatter.impactDescription,
explanation, explanation,
examples, examples,
references: extractReferences(body), references: extractReferences(body),
tags: tags.length > 0 ? tags : undefined, tags: tags.length > 0 ? tags : undefined,
}; };
return { success: true, rule, errors, warnings }; return { success: true, rule, errors, warnings };
} catch (error) { } catch (error) {
errors.push(`Failed to parse file: ${error}`); errors.push(`Failed to parse file: ${error}`);
return { success: false, errors, warnings }; return { success: false, errors, warnings };
} }
} }

View File

@@ -1,60 +1,59 @@
export type ImpactLevel = export type ImpactLevel =
| "CRITICAL" | "CRITICAL"
| "HIGH" | "HIGH"
| "MEDIUM-HIGH" | "MEDIUM-HIGH"
| "MEDIUM" | "MEDIUM"
| "LOW-MEDIUM" | "LOW-MEDIUM"
| "LOW"; | "LOW";
export interface CodeExample { export interface CodeExample {
label: string; label: string;
description?: string; description?: string;
code: string; code: string;
language?: string; language?: string;
additionalText?: string; additionalText?: string;
} }
export interface Rule { export interface Rule {
id: string; id: string;
title: string; title: string;
section: number; section: number;
subsection?: number; subsection?: number;
impact: ImpactLevel; impact: ImpactLevel;
impactDescription?: string; impactDescription?: string;
explanation: string; explanation: string;
examples: CodeExample[]; examples: CodeExample[];
references?: string[]; references?: string[];
tags?: string[]; tags?: string[];
supabaseNotes?: string; supabaseNotes?: string;
} }
export interface Section { export interface Section {
number: number; number: number;
title: string; title: string;
prefix: string; prefix: string;
impact: ImpactLevel; impact: ImpactLevel;
description: string; description: string;
} }
export interface Metadata { export interface Metadata {
version: string; version: string;
organization: string; organization: string;
date: string; date: string;
abstract: string; abstract: string;
references: string[]; references: string[];
maintainers?: string[]; maintainers?: string[];
} }
export interface ParseResult { export interface ParseResult {
success: boolean; success: boolean;
rule?: Rule; rule?: Rule;
errors: string[]; errors: string[];
warnings: string[]; warnings: string[];
} }
export interface ValidationResult { export interface ValidationResult {
valid: boolean; valid: boolean;
errors: string[]; errors: string[];
warnings: string[]; warnings: string[];
} }

View File

@@ -1,186 +1,204 @@
import { readdirSync } from "fs"; import { readdirSync } from "node:fs";
import { join, basename } from "path"; import { basename, join } from "node:path";
import { IMPACT_LEVELS, RULES_DIR } from "./config.js";
import { parseRuleFile } from "./parser.js"; import { parseRuleFile } from "./parser.js";
import { RULES_DIR, IMPACT_LEVELS } from "./config.js";
import type { ValidationResult } from "./types.js"; import type { ValidationResult } from "./types.js";
/** /**
* Check if an example label indicates a "bad" pattern * Check if an example label indicates a "bad" pattern
*/ */
function isBadExample(label: string): boolean { function isBadExample(label: string): boolean {
const lower = label.toLowerCase(); const lower = label.toLowerCase();
return lower.includes("incorrect") || lower.includes("wrong") || lower.includes("bad"); return (
lower.includes("incorrect") ||
lower.includes("wrong") ||
lower.includes("bad")
);
} }
/** /**
* Check if an example label indicates a "good" pattern * Check if an example label indicates a "good" pattern
*/ */
function isGoodExample(label: string): boolean { function isGoodExample(label: string): boolean {
const lower = label.toLowerCase(); const lower = label.toLowerCase();
return ( return (
lower.includes("correct") || lower.includes("correct") ||
lower.includes("good") || lower.includes("good") ||
lower.includes("usage") || lower.includes("usage") ||
lower.includes("implementation") || lower.includes("implementation") ||
lower.includes("example") || lower.includes("example") ||
lower.includes("recommended") lower.includes("recommended")
); );
} }
/** /**
* Validate a single rule file * Validate a single rule file
*/ */
export function validateRuleFile(filePath: string): ValidationResult { export function validateRuleFile(filePath: string): ValidationResult {
const errors: string[] = []; const errors: string[] = [];
const warnings: string[] = []; const warnings: string[] = [];
const result = parseRuleFile(filePath); const result = parseRuleFile(filePath);
// Add parser errors and warnings // Add parser errors and warnings
errors.push(...result.errors); errors.push(...result.errors);
warnings.push(...result.warnings); warnings.push(...result.warnings);
if (!result.success || !result.rule) { if (!result.success || !result.rule) {
return { valid: false, errors, warnings }; return { valid: false, errors, warnings };
} }
const rule = result.rule; const rule = result.rule;
// Validate title // Validate title
if (!rule.title || rule.title.trim().length === 0) { if (!rule.title || rule.title.trim().length === 0) {
errors.push("Missing or empty title"); errors.push("Missing or empty title");
} }
// Validate explanation // Validate explanation
if (!rule.explanation || rule.explanation.trim().length === 0) { if (!rule.explanation || rule.explanation.trim().length === 0) {
errors.push("Missing or empty explanation"); errors.push("Missing or empty explanation");
} else if (rule.explanation.length < 50) { } else if (rule.explanation.length < 50) {
warnings.push("Explanation is shorter than 50 characters"); warnings.push("Explanation is shorter than 50 characters");
} }
// Validate examples // Validate examples
if (rule.examples.length === 0) { if (rule.examples.length === 0) {
errors.push("Missing examples (need at least one bad and one good example)"); errors.push(
} else { "Missing examples (need at least one bad and one good example)",
const hasBad = rule.examples.some((e) => isBadExample(e.label)); );
const hasGood = rule.examples.some((e) => isGoodExample(e.label)); } else {
const hasBad = rule.examples.some((e) => isBadExample(e.label));
const hasGood = rule.examples.some((e) => isGoodExample(e.label));
if (!hasBad && !hasGood) { if (!hasBad && !hasGood) {
errors.push("Missing bad/incorrect and good/correct examples"); errors.push("Missing bad/incorrect and good/correct examples");
} else if (!hasBad) { } else if (!hasBad) {
warnings.push("Missing bad/incorrect example (recommended for clarity)"); warnings.push("Missing bad/incorrect example (recommended for clarity)");
} else if (!hasGood) { } else if (!hasGood) {
errors.push("Missing good/correct example"); errors.push("Missing good/correct example");
} }
// Check for code in examples // Check for code in examples
const hasCode = rule.examples.some((e) => e.code && e.code.trim().length > 0); const hasCode = rule.examples.some(
if (!hasCode) { (e) => e.code && e.code.trim().length > 0,
errors.push("Examples have no code"); );
} if (!hasCode) {
errors.push("Examples have no code");
}
// Check for language specification // Check for language specification
for (const example of rule.examples) { for (const example of rule.examples) {
if (example.code && !example.language) { if (example.code && !example.language) {
warnings.push(`Example "${example.label}" missing language specification`); warnings.push(
} `Example "${example.label}" missing language specification`,
} );
} }
}
}
// Validate impact level // Validate impact level
if (!IMPACT_LEVELS.includes(rule.impact)) { if (!IMPACT_LEVELS.includes(rule.impact)) {
errors.push(`Invalid impact level: ${rule.impact}. Must be one of: ${IMPACT_LEVELS.join(", ")}`); errors.push(
} `Invalid impact level: ${rule.impact}. Must be one of: ${IMPACT_LEVELS.join(", ")}`,
);
}
// Warning for missing impact description // Warning for missing impact description
if (!rule.impactDescription) { if (!rule.impactDescription) {
warnings.push("Missing impactDescription (recommended for quantifying benefit)"); warnings.push(
} "Missing impactDescription (recommended for quantifying benefit)",
);
}
return { return {
valid: errors.length === 0, valid: errors.length === 0,
errors, errors,
warnings, warnings,
}; };
} }
/** /**
* Validate all rule files in the rules directory * Validate all rule files in the rules directory
*/ */
export function validateAllRules(): { export function validateAllRules(): {
totalFiles: number; totalFiles: number;
validFiles: number; validFiles: number;
invalidFiles: number; invalidFiles: number;
results: Map<string, ValidationResult>; results: Map<string, ValidationResult>;
} { } {
const results = new Map<string, ValidationResult>(); const results = new Map<string, ValidationResult>();
let validFiles = 0; let validFiles = 0;
let invalidFiles = 0; let invalidFiles = 0;
// Get all markdown files (excluding _ prefixed files) // Get all markdown files (excluding _ prefixed files)
const files = readdirSync(RULES_DIR) const files = readdirSync(RULES_DIR)
.filter((f) => f.endsWith(".md") && !f.startsWith("_")) .filter((f) => f.endsWith(".md") && !f.startsWith("_"))
.map((f) => join(RULES_DIR, f)); .map((f) => join(RULES_DIR, f));
for (const file of files) { for (const file of files) {
const result = validateRuleFile(file); const result = validateRuleFile(file);
results.set(basename(file), result); results.set(basename(file), result);
if (result.valid) { if (result.valid) {
validFiles++; validFiles++;
} else { } else {
invalidFiles++; invalidFiles++;
} }
} }
return { return {
totalFiles: files.length, totalFiles: files.length,
validFiles, validFiles,
invalidFiles, invalidFiles,
results, results,
}; };
} }
// Run validation when executed directly // 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) { 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) { if (totalFiles === 0) {
console.log("No rule files found (this is expected for initial setup)."); 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("Create rule files in: skills/postgres-best-practices/rules/");
console.log("Use the _template.md as a starting point.\n"); console.log("Use the _template.md as a starting point.\n");
process.exit(0); process.exit(0);
} }
let hasErrors = false; let hasErrors = false;
for (const [filename, result] of results) { for (const [filename, result] of results) {
if (!result.valid || result.warnings.length > 0) { if (!result.valid || result.warnings.length > 0) {
console.log(`\n${filename}:`); console.log(`\n${filename}:`);
for (const error of result.errors) { for (const error of result.errors) {
console.log(` ERROR: ${error}`); console.log(` ERROR: ${error}`);
hasErrors = true; hasErrors = true;
} }
for (const warning of result.warnings) { for (const warning of result.warnings) {
console.log(` WARNING: ${warning}`); console.log(` WARNING: ${warning}`);
} }
} }
} }
console.log(`\n${"=".repeat(50)}`); console.log(`\n${"=".repeat(50)}`);
console.log(`Total: ${totalFiles} files | Valid: ${validFiles} | Invalid: ${invalidFiles}`); console.log(
`Total: ${totalFiles} files | Valid: ${validFiles} | Invalid: ${invalidFiles}`,
);
if (hasErrors) { if (hasErrors) {
console.log("\nValidation failed. Please fix the errors above."); console.log("\nValidation failed. Please fix the errors above.");
process.exit(1); process.exit(1);
} else { } else {
console.log("\nValidation passed!"); console.log("\nValidation passed!");
process.exit(0); process.exit(0);
} }
} }

View File

@@ -1,16 +1,16 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "ES2022",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"outDir": "dist", "outDir": "dist",
"rootDir": "src", "rootDir": "src",
"declaration": true, "declaration": true,
"resolveJsonModule": true "resolveJsonModule": true
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]
} }

View File

@@ -1,13 +1,13 @@
{ {
"version": "1.0.0", "version": "1.0.0",
"organization": "Supabase", "organization": "Supabase",
"date": "January 2026", "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.", "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": [ "references": [
"https://www.postgresql.org/docs/current/", "https://www.postgresql.org/docs/current/",
"https://supabase.com/docs", "https://supabase.com/docs",
"https://wiki.postgresql.org/wiki/Performance_Optimization", "https://wiki.postgresql.org/wiki/Performance_Optimization",
"https://supabase.com/docs/guides/database/overview", "https://supabase.com/docs/guides/database/overview",
"https://supabase.com/docs/guides/auth/row-level-security" "https://supabase.com/docs/guides/auth/row-level-security"
] ]
} }