mirror of
https://github.com/supabase/agent-skills.git
synced 2026-01-26 19:09:51 +08:00
Initial setup: PostgreSQL best practices repository
Skeleton structure for Supabase PostgreSQL experts to add performance optimization rules. Modeled after Vercel's react-best-practices-build. Includes: - Build system (parser, validator, builder) - Skill manifest and metadata - Rule templates and writing guidelines - CI workflow for validation - Getting started guide for Postgres team Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
43
.github/workflows/ci.yml
vendored
Normal file
43
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: PostgreSQL Best Practices CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'skills/postgresql-best-practices/**'
|
||||
- 'packages/postgresql-best-practices-build/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'skills/postgresql-best-practices/**'
|
||||
- 'packages/postgresql-best-practices-build/**'
|
||||
|
||||
jobs:
|
||||
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/postgresql-best-practices-build
|
||||
run: npm install
|
||||
|
||||
- name: Validate rule files
|
||||
working-directory: packages/postgresql-best-practices-build
|
||||
run: npm run validate
|
||||
|
||||
- name: Build AGENTS.md
|
||||
working-directory: packages/postgresql-best-practices-build
|
||||
run: npm run build
|
||||
|
||||
- name: Check for uncommitted changes
|
||||
run: |
|
||||
if [[ -n $(git status --porcelain skills/postgresql-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/postgresql-best-practices/AGENTS.md
|
||||
exit 1
|
||||
fi
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
85
GETTING_STARTED.md
Normal file
85
GETTING_STARTED.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Getting Started - Postgres Team
|
||||
|
||||
Quick guide to start adding PostgreSQL best practice rules.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd packages/postgresql-best-practices-build
|
||||
npm install
|
||||
```
|
||||
|
||||
## Add a Rule
|
||||
|
||||
1. Copy template:
|
||||
```bash
|
||||
cp skills/postgresql-best-practices/rules/_template.md \
|
||||
skills/postgresql-best-practices/rules/query-your-rule.md
|
||||
```
|
||||
|
||||
2. Edit the file with your rule content
|
||||
|
||||
3. Validate & build:
|
||||
```bash
|
||||
cd packages/postgresql-best-practices-build
|
||||
npm run validate
|
||||
npm run build
|
||||
```
|
||||
|
||||
4. Check `skills/postgresql-best-practices/AGENTS.md` for output
|
||||
|
||||
## File Prefixes → Sections
|
||||
|
||||
| Prefix | Section |
|
||||
|--------|---------|
|
||||
| `query-` | 1. Query Performance (CRITICAL) |
|
||||
| `conn-` | 2. Connection Management (CRITICAL) |
|
||||
| `schema-` | 3. Schema Design (HIGH) |
|
||||
| `lock-` | 4. Concurrency & Locking (MEDIUM-HIGH) |
|
||||
| `security-` | 5. Security & RLS (MEDIUM-HIGH) |
|
||||
| `data-` | 6. Data Access Patterns (MEDIUM) |
|
||||
| `monitor-` | 7. Monitoring & Diagnostics (LOW-MEDIUM) |
|
||||
| `advanced-` | 8. Advanced Features (LOW) |
|
||||
|
||||
## Rule Structure
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Action-Oriented Title
|
||||
impact: CRITICAL|HIGH|MEDIUM-HIGH|MEDIUM|LOW-MEDIUM|LOW
|
||||
impactDescription: 10x faster queries
|
||||
tags: indexes, performance
|
||||
---
|
||||
|
||||
## Title
|
||||
|
||||
Brief explanation.
|
||||
|
||||
**Incorrect (why it's bad):**
|
||||
```sql
|
||||
-- Bad pattern
|
||||
```
|
||||
|
||||
**Correct (why it's better):**
|
||||
```sql
|
||||
-- Good pattern
|
||||
```
|
||||
|
||||
**Supabase Note:** Optional platform guidance.
|
||||
|
||||
Reference: [Link](url)
|
||||
```
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `rules/_template.md` | Copy this to create new rules |
|
||||
| `rules/_contributing.md` | Writing guidelines |
|
||||
| `rules/_sections.md` | Section definitions (editable) |
|
||||
| `AGENTS.md` | Generated output (don't edit directly) |
|
||||
|
||||
## Questions?
|
||||
|
||||
- Writing guidelines: `rules/_contributing.md`
|
||||
- Full contributor guide: `skills/postgresql-best-practices/README.md`
|
||||
590
packages/postgresql-best-practices-build/package-lock.json
generated
Normal file
590
packages/postgresql-best-practices-build/package-lock.json
generated
Normal file
@@ -0,0 +1,590 @@
|
||||
{
|
||||
"name": "postgresql-best-practices-build",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "postgresql-best-practices-build",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
|
||||
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
|
||||
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
|
||||
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
|
||||
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
|
||||
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
|
||||
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
|
||||
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
|
||||
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
|
||||
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
|
||||
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
|
||||
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
||||
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.2",
|
||||
"@esbuild/android-arm": "0.27.2",
|
||||
"@esbuild/android-arm64": "0.27.2",
|
||||
"@esbuild/android-x64": "0.27.2",
|
||||
"@esbuild/darwin-arm64": "0.27.2",
|
||||
"@esbuild/darwin-x64": "0.27.2",
|
||||
"@esbuild/freebsd-arm64": "0.27.2",
|
||||
"@esbuild/freebsd-x64": "0.27.2",
|
||||
"@esbuild/linux-arm": "0.27.2",
|
||||
"@esbuild/linux-arm64": "0.27.2",
|
||||
"@esbuild/linux-ia32": "0.27.2",
|
||||
"@esbuild/linux-loong64": "0.27.2",
|
||||
"@esbuild/linux-mips64el": "0.27.2",
|
||||
"@esbuild/linux-ppc64": "0.27.2",
|
||||
"@esbuild/linux-riscv64": "0.27.2",
|
||||
"@esbuild/linux-s390x": "0.27.2",
|
||||
"@esbuild/linux-x64": "0.27.2",
|
||||
"@esbuild/netbsd-arm64": "0.27.2",
|
||||
"@esbuild/netbsd-x64": "0.27.2",
|
||||
"@esbuild/openbsd-arm64": "0.27.2",
|
||||
"@esbuild/openbsd-x64": "0.27.2",
|
||||
"@esbuild/openharmony-arm64": "0.27.2",
|
||||
"@esbuild/sunos-x64": "0.27.2",
|
||||
"@esbuild/win32-arm64": "0.27.2",
|
||||
"@esbuild/win32-ia32": "0.27.2",
|
||||
"@esbuild/win32-x64": "0.27.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
|
||||
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
18
packages/postgresql-best-practices-build/package.json
Normal file
18
packages/postgresql-best-practices-build/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "postgresql-best-practices-build",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsx src/build.ts",
|
||||
"build-agents": "tsx src/build.ts --agents-only",
|
||||
"validate": "tsx src/validate.ts",
|
||||
"extract-tests": "tsx src/extract-tests.ts",
|
||||
"dev": "npm run validate && npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
247
packages/postgresql-best-practices-build/src/build.ts
Normal file
247
packages/postgresql-best-practices-build/src/build.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { readdirSync, readFileSync, writeFileSync, existsSync } from "fs";
|
||||
import { join, basename } from "path";
|
||||
import { parseRuleFile } from "./parser.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 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
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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" },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load metadata from metadata.json
|
||||
*/
|
||||
function loadMetadata(): Metadata {
|
||||
if (!existsSync(METADATA_FILE)) {
|
||||
return {
|
||||
version: "0.1.0",
|
||||
organization: "Supabase",
|
||||
date: new Date().toLocaleDateString("en-US", { month: "long", year: "numeric" }),
|
||||
abstract: "PostgreSQL performance optimization guide for developers.",
|
||||
references: [],
|
||||
};
|
||||
}
|
||||
|
||||
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, "-");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build AGENTS.md from all rule files
|
||||
*/
|
||||
function buildAgents(): void {
|
||||
console.log("Building AGENTS.md...\n");
|
||||
|
||||
// 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));
|
||||
|
||||
if (ruleFiles.length === 0) {
|
||||
console.log("No rule files found. Generating empty AGENTS.md template.");
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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[]>();
|
||||
|
||||
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}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Generate markdown output
|
||||
const output: string[] = [];
|
||||
|
||||
// Header
|
||||
output.push("# PostgreSQL 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");
|
||||
|
||||
// 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 rule of sectionRules) {
|
||||
output.push(` - ${rule.id} [${rule.title}](#${toAnchor(rule.id + "-" + rule.title)})`);
|
||||
}
|
||||
|
||||
output.push("");
|
||||
}
|
||||
|
||||
output.push("---\n");
|
||||
|
||||
// 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`);
|
||||
|
||||
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`);
|
||||
|
||||
if (rule.impactDescription) {
|
||||
output.push(`**Impact: ${rule.impact} (${rule.impactDescription})**\n`);
|
||||
} else {
|
||||
output.push(`**Impact: ${rule.impact}**\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`);
|
||||
}
|
||||
|
||||
output.push("```" + (example.language || "sql"));
|
||||
output.push(example.code);
|
||||
output.push("```\n");
|
||||
|
||||
if (example.additionalText) {
|
||||
output.push(`${example.additionalText}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
if (rule.supabaseNotes) {
|
||||
output.push(`**Supabase Note:** ${rule.supabaseNotes}\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("");
|
||||
}
|
||||
}
|
||||
|
||||
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("");
|
||||
}
|
||||
|
||||
// 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");
|
||||
|
||||
if (isMainModule) {
|
||||
buildAgents();
|
||||
}
|
||||
|
||||
export { buildAgents };
|
||||
42
packages/postgresql-best-practices-build/src/config.ts
Normal file
42
packages/postgresql-best-practices-build/src/config.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Build package directory
|
||||
export const BUILD_DIR = join(__dirname, "..");
|
||||
|
||||
// Skill directory (relative to build package)
|
||||
export const SKILL_DIR = join(BUILD_DIR, "../../skills/postgresql-best-practices");
|
||||
|
||||
// Rules directory
|
||||
export const RULES_DIR = join(SKILL_DIR, "rules");
|
||||
|
||||
// Output files
|
||||
export const AGENTS_OUTPUT = join(SKILL_DIR, "AGENTS.md");
|
||||
export const METADATA_FILE = join(SKILL_DIR, "metadata.json");
|
||||
export const TEST_CASES_OUTPUT = join(BUILD_DIR, "test-cases.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,
|
||||
};
|
||||
|
||||
// Valid impact levels in priority order
|
||||
export const IMPACT_LEVELS = [
|
||||
"CRITICAL",
|
||||
"HIGH",
|
||||
"MEDIUM-HIGH",
|
||||
"MEDIUM",
|
||||
"LOW-MEDIUM",
|
||||
"LOW",
|
||||
] as const;
|
||||
120
packages/postgresql-best-practices-build/src/extract-tests.ts
Normal file
120
packages/postgresql-best-practices-build/src/extract-tests.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { readdirSync, writeFileSync } from "fs";
|
||||
import { join, basename } from "path";
|
||||
import { parseRuleFile } from "./parser.js";
|
||||
import { validateRuleFile } from "./validate.js";
|
||||
import { RULES_DIR, TEST_CASES_OUTPUT } from "./config.js";
|
||||
import type { TestCase } 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");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract test cases from all rule files
|
||||
*/
|
||||
function extractTestCases(): TestCase[] {
|
||||
const testCases: TestCase[] = [];
|
||||
|
||||
// Get all rule files
|
||||
const ruleFiles = readdirSync(RULES_DIR)
|
||||
.filter((f) => f.endsWith(".md") && !f.startsWith("_"))
|
||||
.map((f) => join(RULES_DIR, f));
|
||||
|
||||
// Track rule IDs by section for assignment
|
||||
const ruleCountBySection = new Map<number, number>();
|
||||
|
||||
for (const file of ruleFiles) {
|
||||
const validation = validateRuleFile(file);
|
||||
if (!validation.valid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = parseRuleFile(file);
|
||||
if (!result.success || !result.rule) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rule = result.rule;
|
||||
|
||||
// Assign rule ID
|
||||
const sectionCount = (ruleCountBySection.get(rule.section) || 0) + 1;
|
||||
ruleCountBySection.set(rule.section, sectionCount);
|
||||
const ruleId = `${rule.section}.${sectionCount}`;
|
||||
|
||||
// Extract test cases from examples
|
||||
for (const example of rule.examples) {
|
||||
if (!example.code || example.code.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let type: "bad" | "good" | null = null;
|
||||
|
||||
if (isBadExample(example.label)) {
|
||||
type = "bad";
|
||||
} else if (isGoodExample(example.label)) {
|
||||
type = "good";
|
||||
}
|
||||
|
||||
if (type) {
|
||||
testCases.push({
|
||||
ruleId,
|
||||
ruleTitle: rule.title,
|
||||
type,
|
||||
code: example.code,
|
||||
language: example.language || "sql",
|
||||
description: example.description || `${example.label} example for ${rule.title}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return testCases;
|
||||
}
|
||||
|
||||
// Run extraction when executed directly
|
||||
const isMainModule = process.argv[1]?.endsWith("extract-tests.ts") || process.argv[1]?.endsWith("extract-tests.js");
|
||||
|
||||
if (isMainModule) {
|
||||
console.log("Extracting test cases from rules...\n");
|
||||
|
||||
const testCases = extractTestCases();
|
||||
|
||||
if (testCases.length === 0) {
|
||||
console.log("No test cases extracted (no valid rules found).");
|
||||
console.log("This is expected for initial setup.\n");
|
||||
|
||||
// Write empty array
|
||||
writeFileSync(TEST_CASES_OUTPUT, JSON.stringify([], null, 2));
|
||||
console.log(`Generated: ${TEST_CASES_OUTPUT} (empty)`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Write test cases
|
||||
writeFileSync(TEST_CASES_OUTPUT, JSON.stringify(testCases, null, 2));
|
||||
|
||||
console.log(`Generated: ${TEST_CASES_OUTPUT}`);
|
||||
console.log(`Total test cases: ${testCases.length}`);
|
||||
console.log(` Bad examples: ${testCases.filter((t) => t.type === "bad").length}`);
|
||||
console.log(` Good examples: ${testCases.filter((t) => t.type === "good").length}`);
|
||||
}
|
||||
|
||||
export { extractTestCases };
|
||||
271
packages/postgresql-best-practices-build/src/parser.ts
Normal file
271
packages/postgresql-best-practices-build/src/parser.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
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";
|
||||
|
||||
/**
|
||||
* Parse YAML-style frontmatter from markdown content
|
||||
*/
|
||||
function parseFrontmatter(content: string): {
|
||||
frontmatter: Record<string, string>;
|
||||
body: string;
|
||||
} {
|
||||
const frontmatter: Record<string, string> = {};
|
||||
|
||||
if (!content.startsWith("---")) {
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
// Strip quotes
|
||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
frontmatter[key] = value;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract code examples from markdown body
|
||||
*/
|
||||
function extractExamples(body: string): CodeExample[] {
|
||||
const examples: CodeExample[] = [];
|
||||
const lines = body.split("\n");
|
||||
|
||||
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];
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract title from first ## heading
|
||||
*/
|
||||
function extractTitle(body: string): string | 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;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("## ")) {
|
||||
foundTitle = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!foundTitle) continue;
|
||||
|
||||
// Stop at first example label or code block
|
||||
if (line.match(/^\*\*[^*]+:\*\*/) || line.startsWith("```")) {
|
||||
break;
|
||||
}
|
||||
|
||||
explanationLines.push(line);
|
||||
}
|
||||
|
||||
return explanationLines.join("\n").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract references from body
|
||||
*/
|
||||
function extractReferences(body: string): string[] {
|
||||
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;
|
||||
}
|
||||
|
||||
// Match list items under References section
|
||||
const listMatch = line.match(/^-\s*\[([^\]]+)\]\(([^)]+)\)/);
|
||||
if (listMatch) {
|
||||
references.push(listMatch[2]);
|
||||
}
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Supabase notes
|
||||
*/
|
||||
function extractSupabaseNotes(body: string): string | undefined {
|
||||
const match = body.match(/\*\*Supabase Note:\*\*\s*(.+?)(?=\n\n|\n\*\*|$)/s);
|
||||
return match ? match[1].trim() : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a rule file and return structured data
|
||||
*/
|
||||
export function parseRuleFile(filePath: string): ParseResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
// 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 };
|
||||
}
|
||||
|
||||
// Extract other fields
|
||||
const explanation = extractExplanation(body);
|
||||
const examples = extractExamples(body);
|
||||
const references = extractReferences(body);
|
||||
const supabaseNotes = extractSupabaseNotes(body);
|
||||
const tags = frontmatter.tags?.split(",").map((t) => t.trim()) || [];
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
const rule: Rule = {
|
||||
id: "", // Will be assigned during build
|
||||
title,
|
||||
section,
|
||||
impact,
|
||||
impactDescription: frontmatter.impactDescription,
|
||||
explanation,
|
||||
examples,
|
||||
references: references.length > 0 ? references : undefined,
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
supabaseNotes,
|
||||
};
|
||||
|
||||
return { success: true, rule, errors, warnings };
|
||||
} catch (error) {
|
||||
errors.push(`Failed to parse file: ${error}`);
|
||||
return { success: false, errors, warnings };
|
||||
}
|
||||
}
|
||||
68
packages/postgresql-best-practices-build/src/types.ts
Normal file
68
packages/postgresql-best-practices-build/src/types.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export type ImpactLevel =
|
||||
| "CRITICAL"
|
||||
| "HIGH"
|
||||
| "MEDIUM-HIGH"
|
||||
| "MEDIUM"
|
||||
| "LOW-MEDIUM"
|
||||
| "LOW";
|
||||
|
||||
export interface CodeExample {
|
||||
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;
|
||||
}
|
||||
|
||||
export interface Section {
|
||||
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[];
|
||||
}
|
||||
|
||||
export interface ParseResult {
|
||||
success: boolean;
|
||||
rule?: Rule;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface TestCase {
|
||||
ruleId: string;
|
||||
ruleTitle: string;
|
||||
type: "bad" | "good";
|
||||
code: string;
|
||||
language: string;
|
||||
description?: string;
|
||||
}
|
||||
186
packages/postgresql-best-practices-build/src/validate.ts
Normal file
186
packages/postgresql-best-practices-build/src/validate.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { readdirSync } from "fs";
|
||||
import { join, basename } from "path";
|
||||
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");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a single rule file
|
||||
*/
|
||||
export function validateRuleFile(filePath: string): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
const result = parseRuleFile(filePath);
|
||||
|
||||
// Add parser errors and warnings
|
||||
errors.push(...result.errors);
|
||||
warnings.push(...result.warnings);
|
||||
|
||||
if (!result.success || !result.rule) {
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
|
||||
const rule = result.rule;
|
||||
|
||||
// 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 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");
|
||||
}
|
||||
|
||||
// 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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)");
|
||||
}
|
||||
|
||||
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>;
|
||||
} {
|
||||
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));
|
||||
|
||||
for (const file of files) {
|
||||
const result = validateRuleFile(file);
|
||||
results.set(basename(file), result);
|
||||
|
||||
if (result.valid) {
|
||||
validFiles++;
|
||||
} else {
|
||||
invalidFiles++;
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
if (isMainModule) {
|
||||
console.log("Validating PostgreSQL best practices rules...\n");
|
||||
|
||||
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/postgresql-best-practices/rules/");
|
||||
console.log("Use the _template.md as a starting point.\n");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let hasErrors = false;
|
||||
|
||||
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 warning of result.warnings) {
|
||||
console.log(` WARNING: ${warning}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
16
packages/postgresql-best-practices-build/tsconfig.json
Normal file
16
packages/postgresql-best-practices-build/tsconfig.json
Normal file
@@ -0,0 +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"]
|
||||
}
|
||||
128
skills/postgresql-best-practices/README.md
Normal file
128
skills/postgresql-best-practices/README.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# PostgreSQL Best Practices - Contributor Guide
|
||||
|
||||
This repository contains PostgreSQL performance optimization rules optimized for AI agents and LLMs.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
cd packages/postgresql-best-practices-build
|
||||
npm install
|
||||
|
||||
# Validate existing rules
|
||||
npm run validate
|
||||
|
||||
# Build AGENTS.md
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Creating a New Rule
|
||||
|
||||
1. **Choose a section prefix** based on the category:
|
||||
- `query-` Query Performance (CRITICAL)
|
||||
- `conn-` Connection Management (CRITICAL)
|
||||
- `schema-` Schema Design (HIGH)
|
||||
- `lock-` Concurrency & Locking (MEDIUM-HIGH)
|
||||
- `security-` Security & RLS (MEDIUM-HIGH)
|
||||
- `data-` Data Access Patterns (MEDIUM)
|
||||
- `monitor-` Monitoring & Diagnostics (LOW-MEDIUM)
|
||||
- `advanced-` Advanced Features (LOW)
|
||||
|
||||
2. **Copy the template**:
|
||||
```bash
|
||||
cp rules/_template.md rules/query-your-rule-name.md
|
||||
```
|
||||
|
||||
3. **Fill in the content** following the template structure
|
||||
|
||||
4. **Validate and build**:
|
||||
```bash
|
||||
npm run validate
|
||||
npm run build
|
||||
```
|
||||
|
||||
5. **Review** the generated `AGENTS.md`
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
skills/postgresql-best-practices/
|
||||
├── SKILL.md # Agent-facing skill manifest
|
||||
├── AGENTS.md # [GENERATED] Compiled rules document
|
||||
├── README.md # This file
|
||||
├── metadata.json # Version and metadata
|
||||
└── rules/
|
||||
├── _template.md # Rule template
|
||||
├── _sections.md # Section definitions
|
||||
├── _contributing.md # Writing guidelines
|
||||
└── *.md # Individual rules
|
||||
|
||||
packages/postgresql-best-practices-build/
|
||||
├── src/ # Build system source
|
||||
├── package.json # NPM scripts
|
||||
└── test-cases.json # [GENERATED] Test artifacts
|
||||
```
|
||||
|
||||
## Rule File Structure
|
||||
|
||||
See `rules/_template.md` for the complete template. Key elements:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Clear, Action-Oriented Title
|
||||
impact: CRITICAL|HIGH|MEDIUM-HIGH|MEDIUM|LOW-MEDIUM|LOW
|
||||
impactDescription: Quantified benefit (e.g., "10-100x faster")
|
||||
tags: relevant, keywords
|
||||
---
|
||||
|
||||
## [Title]
|
||||
|
||||
[1-2 sentence explanation]
|
||||
|
||||
**Incorrect (description):**
|
||||
|
||||
```sql
|
||||
-- Comment explaining what's wrong
|
||||
[Bad SQL example]
|
||||
```
|
||||
|
||||
**Correct (description):**
|
||||
|
||||
```sql
|
||||
-- Comment explaining why this is better
|
||||
[Good SQL example]
|
||||
```
|
||||
|
||||
**Supabase Note:** [Optional platform-specific guidance]
|
||||
|
||||
Reference: [Link](url)
|
||||
```
|
||||
|
||||
## Writing Guidelines
|
||||
|
||||
See `rules/_contributing.md` for detailed guidelines. Key principles:
|
||||
|
||||
1. **Show concrete transformations** - "Change X to Y", not abstract advice
|
||||
2. **Error-first structure** - Show the problem before the solution
|
||||
3. **Quantify impact** - Include specific metrics (10x faster, 50% smaller)
|
||||
4. **Self-contained examples** - Complete, runnable SQL
|
||||
5. **Semantic naming** - Use meaningful names (users, email), not (table1, col1)
|
||||
|
||||
## Impact Levels
|
||||
|
||||
| Level | Improvement | Examples |
|
||||
|-------|-------------|----------|
|
||||
| CRITICAL | 10-100x | Missing indexes, connection exhaustion |
|
||||
| HIGH | 5-20x | Wrong index types, poor partitioning |
|
||||
| MEDIUM-HIGH | 2-5x | N+1 queries, RLS optimization |
|
||||
| MEDIUM | 1.5-3x | Redundant indexes, stale statistics |
|
||||
| LOW-MEDIUM | 1.2-2x | VACUUM tuning, config tweaks |
|
||||
| LOW | Incremental | Advanced patterns, edge cases |
|
||||
|
||||
## Supabase-Specific Content
|
||||
|
||||
Keep ~90% of content as universal PostgreSQL patterns. Add Supabase notes for:
|
||||
- Supavisor connection pooling
|
||||
- Dashboard features (index monitoring, query stats)
|
||||
- RLS best practices with Supabase auth
|
||||
- PostgREST considerations
|
||||
57
skills/postgresql-best-practices/SKILL.md
Normal file
57
skills/postgresql-best-practices/SKILL.md
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: supabase-postgresql-best-practices
|
||||
description: PostgreSQL performance optimization and best practices from Supabase. Use this skill when writing, reviewing, or optimizing PostgreSQL queries, schema designs, or database configurations.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: supabase
|
||||
version: "0.1.0"
|
||||
---
|
||||
|
||||
# Supabase PostgreSQL Best Practices
|
||||
|
||||
Comprehensive performance optimization guide for PostgreSQL, maintained by Supabase. Contains rules across 8 categories, prioritized by impact to guide automated query optimization and schema design.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Reference these guidelines when:
|
||||
- Writing SQL queries or designing schemas
|
||||
- Implementing indexes or query optimization
|
||||
- Reviewing database performance issues
|
||||
- Configuring connection pooling or scaling
|
||||
- Optimizing for PostgreSQL-specific features
|
||||
- Working with Row-Level Security (RLS)
|
||||
|
||||
## Rule Categories by Priority
|
||||
|
||||
| Priority | Category | Impact | Prefix |
|
||||
|----------|----------|--------|--------|
|
||||
| 1 | Query Performance | CRITICAL | `query-` |
|
||||
| 2 | Connection Management | CRITICAL | `conn-` |
|
||||
| 3 | Schema Design | HIGH | `schema-` |
|
||||
| 4 | Concurrency & Locking | MEDIUM-HIGH | `lock-` |
|
||||
| 5 | Security & RLS | MEDIUM-HIGH | `security-` |
|
||||
| 6 | Data Access Patterns | MEDIUM | `data-` |
|
||||
| 7 | Monitoring & Diagnostics | LOW-MEDIUM | `monitor-` |
|
||||
| 8 | Advanced Features | LOW | `advanced-` |
|
||||
|
||||
## How to Use
|
||||
|
||||
Read individual rule files for detailed explanations and SQL examples:
|
||||
|
||||
```
|
||||
rules/query-missing-indexes.md
|
||||
rules/schema-partial-indexes.md
|
||||
rules/_sections.md
|
||||
```
|
||||
|
||||
Each rule file contains:
|
||||
- Brief explanation of why it matters
|
||||
- Incorrect SQL example with explanation
|
||||
- Correct SQL example with explanation
|
||||
- Optional EXPLAIN output or metrics
|
||||
- Additional context and references
|
||||
- Supabase-specific notes (when applicable)
|
||||
|
||||
## Full Compiled Document
|
||||
|
||||
For the complete guide with all rules expanded: `AGENTS.md`
|
||||
16
skills/postgresql-best-practices/metadata.json
Normal file
16
skills/postgresql-best-practices/metadata.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"organization": "Supabase",
|
||||
"date": "January 2026",
|
||||
"abstract": "Comprehensive PostgreSQL performance optimization guide for developers using Supabase and PostgreSQL. Contains performance rules across 8 categories, prioritized by impact from critical (query performance, connection management) to incremental (advanced features). Each rule includes detailed explanations, incorrect vs. correct SQL examples, query plan analysis, and specific performance metrics to guide automated optimization and code generation.",
|
||||
"references": [
|
||||
"https://www.postgresql.org/docs/current/",
|
||||
"https://supabase.com/docs",
|
||||
"https://wiki.postgresql.org/wiki/Performance_Optimization",
|
||||
"https://supabase.com/docs/guides/database/overview",
|
||||
"https://supabase.com/docs/guides/auth/row-level-security"
|
||||
],
|
||||
"maintainers": [
|
||||
"Supabase PostgreSQL Team"
|
||||
]
|
||||
}
|
||||
179
skills/postgresql-best-practices/rules/_contributing.md
Normal file
179
skills/postgresql-best-practices/rules/_contributing.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Writing Guidelines for PostgreSQL Rules
|
||||
|
||||
This document provides guidelines for creating effective PostgreSQL best practice rules that work well with AI agents and LLMs.
|
||||
|
||||
## Key Principles
|
||||
|
||||
### 1. Concrete Transformation Patterns
|
||||
|
||||
Show exact SQL rewrites. Avoid philosophical advice.
|
||||
|
||||
**Good:** "Use `WHERE id = ANY(ARRAY[...])` instead of `WHERE id IN (SELECT ...)`"
|
||||
**Bad:** "Design good schemas"
|
||||
|
||||
### 2. Error-First Structure
|
||||
|
||||
Always show the problematic pattern first, then the solution. This trains agents to recognize anti-patterns.
|
||||
|
||||
```markdown
|
||||
**Incorrect (sequential queries):**
|
||||
[bad example]
|
||||
|
||||
**Correct (batched query):**
|
||||
[good example]
|
||||
```
|
||||
|
||||
### 3. Quantified Impact
|
||||
|
||||
Include specific metrics. Helps agents prioritize fixes.
|
||||
|
||||
**Good:** "10x faster queries", "50% smaller index", "Eliminates N+1"
|
||||
**Bad:** "Faster", "Better", "More efficient"
|
||||
|
||||
### 4. Self-Contained Examples
|
||||
|
||||
Examples should be complete and runnable (or close to it). Include CREATE TABLE if context is needed.
|
||||
|
||||
```sql
|
||||
-- Include table definition when needed for clarity
|
||||
CREATE TABLE users (
|
||||
id bigint PRIMARY KEY,
|
||||
email text NOT NULL,
|
||||
deleted_at timestamptz
|
||||
);
|
||||
|
||||
-- Now show the index
|
||||
CREATE INDEX users_active_email_idx ON users(email) WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
### 5. Semantic Naming
|
||||
|
||||
Use meaningful table/column names. Names carry intent for LLMs.
|
||||
|
||||
**Good:** `users`, `email`, `created_at`, `is_active`
|
||||
**Bad:** `table1`, `col1`, `field`, `flag`
|
||||
|
||||
---
|
||||
|
||||
## Code Example Standards
|
||||
|
||||
### SQL Formatting
|
||||
|
||||
```sql
|
||||
-- Use lowercase keywords, clear formatting
|
||||
CREATE INDEX CONCURRENTLY users_email_idx
|
||||
ON users(email)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- Not cramped or ALL CAPS
|
||||
CREATE INDEX CONCURRENTLY USERS_EMAIL_IDX ON USERS(EMAIL) WHERE DELETED_AT IS NULL;
|
||||
```
|
||||
|
||||
### Comments
|
||||
|
||||
- Explain *why*, not *what*
|
||||
- Highlight performance implications
|
||||
- Point out common pitfalls
|
||||
|
||||
### Language Tags
|
||||
|
||||
- `sql` - Standard SQL queries
|
||||
- `plpgsql` - Stored procedures/functions
|
||||
- `typescript` - Application code (when needed)
|
||||
- `python` - Application code (when needed)
|
||||
|
||||
---
|
||||
|
||||
## When to Include Application Code
|
||||
|
||||
**Default: SQL Only**
|
||||
|
||||
Most rules should focus on pure SQL patterns. This keeps examples portable.
|
||||
|
||||
**Include Application Code When:**
|
||||
- Connection pooling configuration
|
||||
- Transaction management in application context
|
||||
- ORM anti-patterns (N+1 in Prisma/TypeORM)
|
||||
- Prepared statement usage
|
||||
|
||||
**Format for Mixed Examples:**
|
||||
|
||||
```markdown
|
||||
**Incorrect (N+1 in application):**
|
||||
|
||||
```typescript
|
||||
for (const user of users) {
|
||||
const posts = await db.query('SELECT * FROM posts WHERE user_id = $1', [user.id])
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (batch query):**
|
||||
|
||||
```typescript
|
||||
const posts = await db.query('SELECT * FROM posts WHERE user_id = ANY($1)', [userIds])
|
||||
```
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact Level Guidelines
|
||||
|
||||
| Level | Improvement | Use When |
|
||||
|-------|-------------|----------|
|
||||
| **CRITICAL** | 10-100x | Missing indexes, connection exhaustion, sequential scans on large tables |
|
||||
| **HIGH** | 5-20x | Wrong index types, poor partitioning, missing covering indexes |
|
||||
| **MEDIUM-HIGH** | 2-5x | N+1 queries, inefficient pagination, RLS optimization |
|
||||
| **MEDIUM** | 1.5-3x | Redundant indexes, query plan instability |
|
||||
| **LOW-MEDIUM** | 1.2-2x | VACUUM tuning, configuration tweaks |
|
||||
| **LOW** | Incremental | Advanced patterns, edge cases |
|
||||
|
||||
---
|
||||
|
||||
## Supabase-Specific Notes
|
||||
|
||||
**When to Add:**
|
||||
- Supavisor pooling configuration
|
||||
- Dashboard features (index monitoring, query stats)
|
||||
- RLS patterns specific to Supabase auth
|
||||
- PostgREST implications
|
||||
|
||||
**Format:**
|
||||
```markdown
|
||||
**Supabase Note:** The Dashboard > Database > Indexes page shows index usage statistics.
|
||||
```
|
||||
|
||||
**Balance:** ~10% of content should be Supabase-specific. Core rules should work on any PostgreSQL.
|
||||
|
||||
---
|
||||
|
||||
## Reference Standards
|
||||
|
||||
**Primary Sources:**
|
||||
- Official PostgreSQL documentation
|
||||
- Supabase documentation
|
||||
- PostgreSQL wiki
|
||||
- Established blogs (2ndQuadrant, Crunchy Data)
|
||||
|
||||
**Format:**
|
||||
```markdown
|
||||
Reference: [PostgreSQL Indexes](https://www.postgresql.org/docs/current/indexes.html)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
Before submitting a rule:
|
||||
|
||||
- [ ] Title is clear and action-oriented
|
||||
- [ ] Impact level matches the performance gain
|
||||
- [ ] impactDescription includes quantification
|
||||
- [ ] Explanation is concise (1-2 sentences)
|
||||
- [ ] Has at least 1 **Incorrect** SQL example
|
||||
- [ ] Has at least 1 **Correct** SQL example
|
||||
- [ ] SQL uses semantic naming
|
||||
- [ ] Comments explain *why*, not *what*
|
||||
- [ ] Trade-offs mentioned if applicable
|
||||
- [ ] Reference links included
|
||||
- [ ] `npm run validate` passes
|
||||
- [ ] `npm run build` generates correct output
|
||||
37
skills/postgresql-best-practices/rules/_sections.md
Normal file
37
skills/postgresql-best-practices/rules/_sections.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Section Definitions
|
||||
|
||||
This file defines the 8 rule categories for PostgreSQL best practices. Rules are automatically assigned to sections based on their filename prefix.
|
||||
|
||||
---
|
||||
|
||||
## 1. Query Performance (query)
|
||||
**Impact:** CRITICAL
|
||||
**Description:** Slow queries, missing indexes, inefficient query plans. The most common source of PostgreSQL performance issues.
|
||||
|
||||
## 2. Connection Management (conn)
|
||||
**Impact:** CRITICAL
|
||||
**Description:** Connection pooling, limits, and serverless strategies. Critical for applications with high concurrency or serverless deployments.
|
||||
|
||||
## 3. Schema Design (schema)
|
||||
**Impact:** HIGH
|
||||
**Description:** Table design, index strategies, partitioning, and data type selection. Foundation for long-term performance.
|
||||
|
||||
## 4. Concurrency & Locking (lock)
|
||||
**Impact:** MEDIUM-HIGH
|
||||
**Description:** Transaction management, isolation levels, deadlock prevention, and lock contention patterns.
|
||||
|
||||
## 5. Security & RLS (security)
|
||||
**Impact:** MEDIUM-HIGH
|
||||
**Description:** Row-Level Security policies, privilege management, and authentication patterns.
|
||||
|
||||
## 6. Data Access Patterns (data)
|
||||
**Impact:** MEDIUM
|
||||
**Description:** N+1 query elimination, batch operations, cursor-based pagination, and efficient data fetching.
|
||||
|
||||
## 7. Monitoring & Diagnostics (monitor)
|
||||
**Impact:** LOW-MEDIUM
|
||||
**Description:** Using pg_stat_statements, EXPLAIN ANALYZE, metrics collection, and performance diagnostics.
|
||||
|
||||
## 8. Advanced Features (advanced)
|
||||
**Impact:** LOW
|
||||
**Description:** Full-text search, JSONB optimization, PostGIS, extensions, and advanced PostgreSQL features.
|
||||
36
skills/postgresql-best-practices/rules/_template.md
Normal file
36
skills/postgresql-best-practices/rules/_template.md
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Clear, Action-Oriented Title (e.g., "Use Partial Indexes for Filtered Queries")
|
||||
impact: MEDIUM
|
||||
impactDescription: 5-20x query speedup for filtered queries
|
||||
tags: indexes, query-optimization, performance
|
||||
---
|
||||
|
||||
## [Rule Title]
|
||||
|
||||
[1-2 sentence explanation of the problem and why it matters. Focus on performance impact.]
|
||||
|
||||
**Incorrect (describe the problem):**
|
||||
|
||||
```sql
|
||||
-- Comment explaining what makes this slow/problematic
|
||||
CREATE INDEX users_email_idx ON users(email);
|
||||
|
||||
SELECT * FROM users WHERE email = 'user@example.com' AND deleted_at IS NULL;
|
||||
-- This scans deleted records unnecessarily
|
||||
```
|
||||
|
||||
**Correct (describe the solution):**
|
||||
|
||||
```sql
|
||||
-- Comment explaining why this is better
|
||||
CREATE INDEX users_active_email_idx ON users(email) WHERE deleted_at IS NULL;
|
||||
|
||||
SELECT * FROM users WHERE email = 'user@example.com' AND deleted_at IS NULL;
|
||||
-- Only indexes active users, 10x smaller index, faster queries
|
||||
```
|
||||
|
||||
[Optional: Additional context, edge cases, or trade-offs]
|
||||
|
||||
**Supabase Note:** [Optional platform-specific guidance, e.g., "Use Dashboard > Database > Indexes to monitor index usage"]
|
||||
|
||||
Reference: [PostgreSQL Docs](https://www.postgresql.org/docs/current/)
|
||||
Reference in New Issue
Block a user