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

@@ -14,9 +14,7 @@
"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

@@ -6,6 +6,15 @@
"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,9 +1,9 @@
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
@@ -19,7 +19,7 @@ function parseSections(): Section[] {
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) {
@@ -40,14 +40,62 @@ function parseSections(): Section[] {
*/ */
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",
},
]; ];
} }
@@ -59,7 +107,10 @@ function loadMetadata(): Metadata {
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", {
month: "long",
year: "numeric",
}),
abstract: "Postgres performance optimization guide for developers.", abstract: "Postgres performance optimization guide for developers.",
references: [], references: [],
}; };
@@ -104,7 +155,9 @@ function buildAgents(): void {
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) {
console.error(` - ${e}`);
}
continue; continue;
} }
@@ -139,7 +192,9 @@ function buildAgents(): void {
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(
"> This document is optimized for AI agents and LLMs. Rules are prioritized by performance impact.\n",
);
output.push("---\n"); output.push("---\n");
// Abstract // Abstract
@@ -152,10 +207,14 @@ function buildAgents(): void {
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("");
@@ -172,7 +231,9 @@ function buildAgents(): void {
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) {
@@ -193,7 +254,7 @@ function buildAgents(): void {
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");
@@ -234,7 +295,9 @@ function buildAgents(): void {
} }
// 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();

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");

View File

@@ -1,7 +1,7 @@
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
@@ -32,8 +32,10 @@ function parseFrontmatter(content: string): {
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.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1); value = value.slice(1, -1);
} }
@@ -70,7 +72,9 @@ function extractExamples(body: string): CodeExample[] {
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(
/^\*\*([^*]+?)(?:\s*\(([^)]+)\))?\s*:\*\*\s*$/,
);
if (labelMatch && !inCodeBlock) { if (labelMatch && !inCodeBlock) {
// Save previous example if exists // Save previous example if exists
if (currentLabel && codeBlockContent.length > 0) { if (currentLabel && codeBlockContent.length > 0) {
@@ -79,7 +83,10 @@ function extractExamples(body: string): CodeExample[] {
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,
}); });
} }
@@ -127,7 +134,10 @@ function extractExamples(body: string): CodeExample[] {
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,
}); });
} }
@@ -208,7 +218,9 @@ export function parseRuleFile(filePath: string): ParseResult {
// 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(
`Could not determine section from filename: ${basename(filePath)}`,
);
return { success: false, errors, warnings }; return { success: false, errors, warnings };
} }
@@ -222,7 +234,9 @@ export function parseRuleFile(filePath: string): ParseResult {
// 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(
`Invalid or missing impact level: ${impact}. Must be one of: ${IMPACT_LEVELS.join(", ")}`,
);
return { success: false, errors, warnings }; return { success: false, errors, warnings };
} }

View File

@@ -57,4 +57,3 @@ export interface ValidationResult {
errors: string[]; errors: string[];
warnings: string[]; warnings: string[];
} }

View File

@@ -1,7 +1,7 @@
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";
/** /**
@@ -9,7 +9,11 @@ import type { ValidationResult } from "./types.js";
*/ */
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")
);
} }
/** /**
@@ -60,7 +64,9 @@ export function validateRuleFile(filePath: string): ValidationResult {
// 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(
"Missing examples (need at least one bad and one good example)",
);
} else { } else {
const hasBad = rule.examples.some((e) => isBadExample(e.label)); const hasBad = rule.examples.some((e) => isBadExample(e.label));
const hasGood = rule.examples.some((e) => isGoodExample(e.label)); const hasGood = rule.examples.some((e) => isGoodExample(e.label));
@@ -74,7 +80,9 @@ export function validateRuleFile(filePath: string): ValidationResult {
} }
// 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(
(e) => e.code && e.code.trim().length > 0,
);
if (!hasCode) { if (!hasCode) {
errors.push("Examples have no code"); errors.push("Examples have no code");
} }
@@ -82,19 +90,25 @@ export function validateRuleFile(filePath: string): ValidationResult {
// 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 {
@@ -142,7 +156,9 @@ export function validateAllRules(): {
} }
// 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");
@@ -174,7 +190,9 @@ if (isMainModule) {
} }
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.");