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

View File

@@ -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<number, Rule[]>();
// Group rules by section and assign IDs
const rulesBySection = new Map<number, Rule[]>();
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 };

View File

@@ -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<string, number> = {
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;

View File

@@ -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<string, string>;
body: string;
frontmatter: Record<string, string>;
body: string;
} {
const frontmatter: Record<string, string> = {};
const frontmatter: Record<string, string> = {};
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 };
}
}

View File

@@ -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[];
}

View File

@@ -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<string, ValidationResult>;
totalFiles: number;
validFiles: number;
invalidFiles: number;
results: Map<string, ValidationResult>;
} {
const results = new Map<string, ValidationResult>();
let validFiles = 0;
let invalidFiles = 0;
const results = new Map<string, ValidationResult>();
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);
}
}

View File

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