mirror of
https://github.com/supabase/agent-skills.git
synced 2026-01-26 19:09:51 +08:00
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:
@@ -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
54
.github/workflows/ci.yml
vendored
Normal 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
34
biome.json
Normal 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
179
package-lock.json
generated
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
package.json
11
package.json
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,4 +57,3 @@ export interface ValidationResult {
|
|||||||
errors: string[];
|
errors: string[];
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
Reference in New Issue
Block a user